From 6fe1b68510397ccccaf4339d2974784864ad02bb Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 7 Apr 2021 22:30:40 +0900 Subject: [PATCH 001/205] Add a way to retrieve new WorkingBeatmap instances --- osu.Game/Beatmaps/BeatmapManager.cs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index b4ea898b7d..69b513d256 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -281,8 +281,9 @@ namespace osu.Game.Beatmaps /// /// The beatmap to lookup. /// The currently loaded . Allows for optimisation where elements are shared with the new beatmap. May be returned if beatmapInfo requested matches + /// Whether to bypass the cache and return a new instance. /// A instance correlating to the provided . - public WorkingBeatmap GetWorkingBeatmap(BeatmapInfo beatmapInfo, WorkingBeatmap previous = null) + public WorkingBeatmap GetWorkingBeatmap(BeatmapInfo beatmapInfo, WorkingBeatmap previous = null, bool bypassCache = false) { if (beatmapInfo?.ID > 0 && previous != null && previous.BeatmapInfo?.ID == beatmapInfo.ID) return previous; @@ -301,9 +302,14 @@ namespace osu.Game.Beatmaps lock (workingCache) { - var working = workingCache.FirstOrDefault(w => w.BeatmapInfo?.ID == beatmapInfo.ID); - if (working != null) - return working; + BeatmapManagerWorkingBeatmap working; + + if (!bypassCache) + { + working = workingCache.FirstOrDefault(w => w.BeatmapInfo?.ID == beatmapInfo.ID); + if (working != null) + return working; + } beatmapInfo.Metadata ??= beatmapInfo.BeatmapSet.Metadata; From d64b236f8619451b3827b3dd1ca6a263ee9c7ea9 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 8 Apr 2021 21:27:16 +0900 Subject: [PATCH 002/205] Add a container that provides an isolated gameplay context --- .../Spectate/GameplayIsolationContainer.cs | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/GameplayIsolationContainer.cs diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/GameplayIsolationContainer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/GameplayIsolationContainer.cs new file mode 100644 index 0000000000..3ccb67d342 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/GameplayIsolationContainer.cs @@ -0,0 +1,43 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics.Containers; +using osu.Game.Beatmaps; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Mods; + +namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate +{ + public class GameplayIsolationContainer : Container + { + [Cached] + private readonly Bindable ruleset = new Bindable(); + + [Cached] + private readonly Bindable beatmap = new Bindable(); + + [Cached] + private readonly Bindable> mods = new Bindable>(); + + public GameplayIsolationContainer(WorkingBeatmap beatmap, RulesetInfo ruleset, IReadOnlyList mods) + { + this.beatmap.Value = beatmap; + this.ruleset.Value = ruleset; + this.mods.Value = mods; + + beatmap.LoadTrack(); + } + + protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) + { + var dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); + dependencies.CacheAs(ruleset.BeginLease(false)); + dependencies.CacheAs(beatmap.BeginLease(false)); + dependencies.CacheAs(mods.BeginLease(false)); + return dependencies; + } + } +} From 709016f0d639f6899de78d5b2e153ec3775d829a Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 8 Apr 2021 22:07:00 +0900 Subject: [PATCH 003/205] Add initial multiplayer screen implementation --- .../Spectate/MultiplayerSpectator.cs | 146 ++++++++++++++++++ .../Spectate/MultiplayerSpectatorPlayer.cs | 19 +++ .../MultiplayerSpectatorPlayerLoader.cs | 26 ++++ .../Multiplayer/Spectate/PlayerGrid.cs | 13 ++ .../Multiplayer/Spectate/PlayerInstance.cs | 55 +++++++ .../Screens/Play/SpectatorPlayerLoader.cs | 7 +- 6 files changed, 265 insertions(+), 1 deletion(-) create mode 100644 osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectator.cs create mode 100644 osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectatorPlayer.cs create mode 100644 osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectatorPlayerLoader.cs create mode 100644 osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerInstance.cs diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectator.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectator.cs new file mode 100644 index 0000000000..6b8249eae0 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectator.cs @@ -0,0 +1,146 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Testing; +using osu.Framework.Utils; +using osu.Game.Online.Rooms; +using osu.Game.Online.Spectator; +using osu.Game.Screens.Play; +using osu.Game.Screens.Spectate; + +namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate +{ + public class MultiplayerSpectator : SpectatorScreen + { + private const double min_duration_to_allow_playback = 50; + private const double max_sync_offset = 2; + + // Isolates beatmap/ruleset to this screen. + public override bool DisallowExternalBeatmapRulesetChanges => true; + + public bool AllPlayersLoaded => instances.All(p => p?.PlayerLoaded == true); + + [Resolved] + private SpectatorStreamingClient spectatorClient { get; set; } + + private readonly int[] userIds; + private readonly PlayerInstance[] instances; + + private PlayerGrid grid; + + public MultiplayerSpectator(PlaylistItem playlistItem, int[] userIds) + : this(userIds.AsSpan().Slice(0, Math.Min(16, userIds.Length)).ToArray()) + { + } + + private MultiplayerSpectator(int[] userIds) + : base(userIds) + { + this.userIds = userIds; + instances = new PlayerInstance[userIds.Length]; + } + + [BackgroundDependencyLoader] + private void load() + { + InternalChild = grid = new PlayerGrid + { + RelativeSizeAxes = Axes.Both + }; + } + + protected override void Update() + { + base.Update(); + updatePlayTime(); + } + + private bool gameplayStarted; + + private void updatePlayTime() + { + if (gameplayStarted) + { + ensurePlaying(instances.Select(i => i.Beatmap.Track.CurrentTime).Max()); + return; + } + + // Make sure all players are loaded. + if (!AllPlayersLoaded) + { + ensureAllStopped(); + return; + } + + if (!instances.All(i => i.Score.Replay.Frames.Count > 0)) + { + ensureAllStopped(); + return; + } + + gameplayStarted = true; + } + + private void ensureAllStopped() + { + foreach (var inst in instances) + inst.ChildrenOfType().SingleOrDefault()?.Stop(); + } + + private readonly BindableDouble catchupFrequencyAdjustment = new BindableDouble(2.0); + + private void ensurePlaying(double targetTime) + { + foreach (var inst in instances) + { + double lastFrameTime = inst.Score.Replay.Frames.Select(f => f.Time).Last(); + double currentTime = inst.Beatmap.Track.CurrentTime; + + // If we have enough frames to play back, start playback. + if (Precision.DefinitelyBigger(lastFrameTime, currentTime, min_duration_to_allow_playback)) + { + inst.ChildrenOfType().Single().Start(); + + if (targetTime < lastFrameTime && targetTime > currentTime + 16) + inst.Beatmap.Track.AddAdjustment(AdjustableProperty.Frequency, catchupFrequencyAdjustment); + else + inst.Beatmap.Track.RemoveAdjustment(AdjustableProperty.Frequency, catchupFrequencyAdjustment); + } + else + inst.Beatmap.Track.RemoveAdjustment(AdjustableProperty.Frequency, catchupFrequencyAdjustment); + } + } + + protected override void OnUserStateChanged(int userId, SpectatorState spectatorState) + { + } + + protected override void StartGameplay(int userId, GameplayState gameplayState) + { + int userIndex = getIndexForUser(userId); + var existingInstance = instances[userIndex]; + + if (existingInstance != null) + grid.Remove(existingInstance); + + LoadComponentAsync(instances[userIndex] = new PlayerInstance(gameplayState.Score), d => + { + if (instances[userIndex] == d) + grid.Add(d); + }); + } + + protected override void EndGameplay(int userId) + { + spectatorClient.StopWatchingUser(userId); + } + + private int getIndexForUser(int userId) => Array.IndexOf(userIds, userId); + } +} diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectatorPlayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectatorPlayer.cs new file mode 100644 index 0000000000..bad093f666 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectatorPlayer.cs @@ -0,0 +1,19 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Rulesets.Scoring; +using osu.Game.Scoring; +using osu.Game.Screens.Play; + +namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate +{ + public class MultiplayerSpectatorPlayer : SpectatorPlayer + { + public new ScoreProcessor ScoreProcessor => base.ScoreProcessor; + + public MultiplayerSpectatorPlayer(Score score) + : base(score) + { + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectatorPlayerLoader.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectatorPlayerLoader.cs new file mode 100644 index 0000000000..c8f16b5e90 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectatorPlayerLoader.cs @@ -0,0 +1,26 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Game.Scoring; +using osu.Game.Screens.Menu; +using osu.Game.Screens.Play; + +namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate +{ + public class MultiplayerSpectatorPlayerLoader : SpectatorPlayerLoader + { + public MultiplayerSpectatorPlayerLoader(Score score, Func createPlayer) + : base(score, createPlayer) + { + } + + protected override void LogoArriving(OsuLogo logo, bool resuming) + { + } + + protected override void LogoExiting(OsuLogo logo) + { + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid.cs index 830378f129..b2d114d9cc 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid.cs @@ -75,6 +75,19 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate cellContainer.Add(cell); } + public void Remove(Drawable content) + { + var cell = cellContainer.FirstOrDefault(c => c.Content == content); + if (cell == null) + return; + + if (cell.IsMaximised) + toggleMaximisationState(cell); + + cellContainer.Remove(cell); + facadeContainer.Remove(facadeContainer[cell.FacadeIndex]); + } + /// /// The content added to this grid. /// diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerInstance.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerInstance.cs new file mode 100644 index 0000000000..ab7d3e2502 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerInstance.cs @@ -0,0 +1,55 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Beatmaps; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Scoring; +using osu.Game.Scoring; +using osu.Game.Screens.Play; +using osu.Game.Users; + +namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate +{ + public class PlayerInstance : CompositeDrawable + { + public bool PlayerLoaded => stack?.CurrentScreen is Player; + + public User User => Score.ScoreInfo.User; + public ScoreProcessor ScoreProcessor => player?.ScoreProcessor; + + public WorkingBeatmap Beatmap { get; private set; } + public Ruleset Ruleset { get; private set; } + + public readonly Score Score; + + private OsuScreenStack stack; + private MultiplayerSpectatorPlayer player; + + public PlayerInstance(Score score) + { + Score = score; + } + + [BackgroundDependencyLoader] + private void load(BeatmapManager beatmapManager) + { + Beatmap = beatmapManager.GetWorkingBeatmap(Score.ScoreInfo.Beatmap, bypassCache: true); + Ruleset = Score.ScoreInfo.Ruleset.CreateInstance(); + + InternalChild = new GameplayIsolationContainer(Beatmap, Score.ScoreInfo.Ruleset, Score.ScoreInfo.Mods) + { + RelativeSizeAxes = Axes.Both, + Child = new DrawSizePreservingFillContainer + { + RelativeSizeAxes = Axes.Both, + Child = stack = new OsuScreenStack() + } + }; + + stack.Push(new MultiplayerSpectatorPlayerLoader(Score, () => player = new MultiplayerSpectatorPlayer(Score))); + } + } +} diff --git a/osu.Game/Screens/Play/SpectatorPlayerLoader.cs b/osu.Game/Screens/Play/SpectatorPlayerLoader.cs index 580af81166..bdd23962dc 100644 --- a/osu.Game/Screens/Play/SpectatorPlayerLoader.cs +++ b/osu.Game/Screens/Play/SpectatorPlayerLoader.cs @@ -12,7 +12,12 @@ namespace osu.Game.Screens.Play public readonly ScoreInfo Score; public SpectatorPlayerLoader(Score score) - : base(() => new SpectatorPlayer(score)) + : this(score, () => new SpectatorPlayer(score)) + { + } + + public SpectatorPlayerLoader(Score score, Func createPlayer) + : base(createPlayer) { if (score.Replay == null) throw new ArgumentException($"{nameof(score)} must have a non-null {nameof(score.Replay)}.", nameof(score)); From 7d276144b871f219d07321480aea1852e9aa7d44 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 8 Apr 2021 22:13:54 +0900 Subject: [PATCH 004/205] Fix player sizing + masking --- .../Screens/OnlinePlay/Multiplayer/Spectate/PlayerInstance.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerInstance.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerInstance.cs index ab7d3e2502..5689a3222a 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerInstance.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerInstance.cs @@ -31,6 +31,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate public PlayerInstance(Score score) { Score = score; + + RelativeSizeAxes = Axes.Both; + Masking = true; } [BackgroundDependencyLoader] From 1b5679b0d75509eae6ba24761965ec5904020fbd Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 8 Apr 2021 22:14:26 +0900 Subject: [PATCH 005/205] Refactor ctor --- .../Multiplayer/Spectate/MultiplayerSpectator.cs | 15 +++------------ osu.Game/Screens/Spectate/SpectatorScreen.cs | 6 +++--- 2 files changed, 6 insertions(+), 15 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectator.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectator.cs index 6b8249eae0..8fa9dcf3af 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectator.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectator.cs @@ -9,7 +9,6 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Testing; using osu.Framework.Utils; -using osu.Game.Online.Rooms; using osu.Game.Online.Spectator; using osu.Game.Screens.Play; using osu.Game.Screens.Spectate; @@ -29,20 +28,12 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate [Resolved] private SpectatorStreamingClient spectatorClient { get; set; } - private readonly int[] userIds; private readonly PlayerInstance[] instances; - private PlayerGrid grid; - public MultiplayerSpectator(PlaylistItem playlistItem, int[] userIds) - : this(userIds.AsSpan().Slice(0, Math.Min(16, userIds.Length)).ToArray()) + public MultiplayerSpectator(int[] userIds) + : base(userIds.AsSpan().Slice(0, Math.Min(16, userIds.Length)).ToArray()) { - } - - private MultiplayerSpectator(int[] userIds) - : base(userIds) - { - this.userIds = userIds; instances = new PlayerInstance[userIds.Length]; } @@ -141,6 +132,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate spectatorClient.StopWatchingUser(userId); } - private int getIndexForUser(int userId) => Array.IndexOf(userIds, userId); + private int getIndexForUser(int userId) => Array.IndexOf(UserIds, userId); } } diff --git a/osu.Game/Screens/Spectate/SpectatorScreen.cs b/osu.Game/Screens/Spectate/SpectatorScreen.cs index 6dd3144fc8..b7e1b8496c 100644 --- a/osu.Game/Screens/Spectate/SpectatorScreen.cs +++ b/osu.Game/Screens/Spectate/SpectatorScreen.cs @@ -26,7 +26,7 @@ namespace osu.Game.Screens.Spectate /// public abstract class SpectatorScreen : OsuScreen { - private readonly int[] userIds; + protected readonly int[] UserIds; [Resolved] private BeatmapManager beatmaps { get; set; } @@ -55,7 +55,7 @@ namespace osu.Game.Screens.Spectate /// The users to spectate. protected SpectatorScreen(params int[] userIds) { - this.userIds = userIds; + this.UserIds = userIds; } protected override void LoadComplete() @@ -66,7 +66,7 @@ namespace osu.Game.Screens.Spectate spectatorClient.OnUserFinishedPlaying += userFinishedPlaying; spectatorClient.OnNewFrames += userSentFrames; - foreach (var id in userIds) + foreach (var id in UserIds) { userLookupCache.GetUserAsync(id).ContinueWith(u => Schedule(() => { From 7958a257e88d04a97e9b72e4a4493eb67dc28141 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 8 Apr 2021 22:14:30 +0900 Subject: [PATCH 006/205] Add tests --- .../Multiplayer/TestSceneMultiplayerScreen.cs | 327 ++++++++++++++++++ 1 file changed, 327 insertions(+) create mode 100644 osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerScreen.cs diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerScreen.cs new file mode 100644 index 0000000000..d085827986 --- /dev/null +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerScreen.cs @@ -0,0 +1,327 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Testing; +using osu.Framework.Utils; +using osu.Game.Beatmaps; +using osu.Game.Database; +using osu.Game.Online; +using osu.Game.Online.Spectator; +using osu.Game.Replays.Legacy; +using osu.Game.Scoring; +using osu.Game.Screens.OnlinePlay.Multiplayer.Spectate; +using osu.Game.Screens.Play; +using osu.Game.Tests.Beatmaps.IO; +using osu.Game.Users; + +namespace osu.Game.Tests.Visual.Multiplayer +{ + public class TestSceneMultiplayerSpectator : MultiplayerTestScene + { + [Cached(typeof(SpectatorStreamingClient))] + private TestSpectatorStreamingClient testSpectatorStreamingClient = new TestSpectatorStreamingClient(); + + [Cached(typeof(UserLookupCache))] + private UserLookupCache lookupCache = new TestUserLookupCache(); + + [Resolved] + private OsuGameBase game { get; set; } + + [Resolved] + private BeatmapManager beatmapManager { get; set; } + + private MultiplayerSpectator spectator; + + private readonly List playingUserIds = new List(); + private readonly Dictionary nextFrame = new Dictionary(); + + private BeatmapSetInfo importedSet; + private BeatmapInfo importedBeatmap; + private int importedBeatmapId; + + [BackgroundDependencyLoader] + private void load() + { + importedSet = ImportBeatmapTest.LoadOszIntoOsu(game, virtualTrack: true).Result; + importedBeatmap = importedSet.Beatmaps.First(b => b.RulesetID == 0); + importedBeatmapId = importedBeatmap.OnlineBeatmapID ?? -1; + } + + public override void SetUpSteps() + { + base.SetUpSteps(); + + AddStep("reset sent frames", () => nextFrame.Clear()); + + AddStep("add streaming client", () => + { + Remove(testSpectatorStreamingClient); + Add(testSpectatorStreamingClient); + }); + + AddStep("finish previous gameplay", () => + { + foreach (var id in playingUserIds) + testSpectatorStreamingClient.EndPlay(id, importedBeatmapId); + playingUserIds.Clear(); + }); + } + + [Test] + public void TestGeneral() + { + int[] userIds = Enumerable.Range(0, 4).Select(i => 55 + i).ToArray(); + + start(userIds); + loadSpectateScreen(); + + sendFrames(userIds, 1000); + AddWaitStep("wait a bit", 20); + } + + [Test] + public void TestPlayersMustStartSimultaneously() + { + start(new[] { 55, 56 }); + loadSpectateScreen(); + + // Send frames for one player only, both should remain paused. + sendFrames(55, 20); + checkPausedInstant(55, true); + checkPausedInstant(56, true); + + // Send frames for the other player, both should now start playing. + sendFrames(56, 20); + checkPausedInstant(55, false); + checkPausedInstant(56, false); + } + + [Test] + public void TestPlayersDoNotStartSimultaneouslyIfBufferingFor15Seconds() + { + start(new[] { 55, 56 }); + loadSpectateScreen(); + + // Send frames for one player only, both should remain paused. + sendFrames(55, 20); + checkPausedInstant(55, true); + checkPausedInstant(56, true); + + // Wait 15 seconds... + AddWaitStep("wait 15 seconds", (int)(15000 / TimePerAction)); + + // Player 1 should start playing by itself, player 2 should remain paused. + checkPausedInstant(55, false); + checkPausedInstant(56, true); + } + + [Test] + public void TestPlayersContinueWhileOthersBuffer() + { + start(new[] { 55, 56 }); + loadSpectateScreen(); + + // Send initial frames for both players. A few more for player 1. + sendFrames(55, 20); + sendFrames(56, 10); + checkPausedInstant(55, false); + checkPausedInstant(56, false); + + // Eventually player 2 will pause, player 1 must remain running. + checkPaused(56, true); + checkPausedInstant(55, false); + + // Eventually both players will run out of frames and should pause. + checkPaused(55, true); + checkPausedInstant(56, true); + + // Send more frames for the first player only. Player 1 should start playing with player 2 remaining paused. + sendFrames(55, 20); + checkPausedInstant(56, true); + checkPausedInstant(55, false); + + // Send more frames for the second player. Both should be playing + sendFrames(56, 20); + checkPausedInstant(56, false); + checkPausedInstant(55, false); + } + + [Test] + public void TestPlayersCatchUpAfterFallingBehind() + { + start(new[] { 55, 56 }); + loadSpectateScreen(); + + // Send initial frames for both players. A few more for player 1. + sendFrames(55, 100); + sendFrames(56, 10); + checkPausedInstant(55, false); + checkPausedInstant(56, false); + + // Eventually player 2 will run out of frames and should pause. + checkPaused(56, true); + AddWaitStep("wait a few more frames", 10); + + // Send more frames for player 2. It should unpause. + sendFrames(56, 100); + checkPausedInstant(56, false); + + // Player 2 should catch up to player 1 after unpausing. + AddUntilStep("player 1 time == player 2 time", () => Precision.AlmostEquals(getGameplayTime(55), getGameplayTime(56), 16)); + } + + private void loadSpectateScreen() + { + AddStep("load screen", () => + { + Beatmap.Value = beatmapManager.GetWorkingBeatmap(importedBeatmap); + Ruleset.Value = importedBeatmap.Ruleset; + + LoadScreen(spectator = new MultiplayerSpectator(playingUserIds.ToArray())); + }); + + AddUntilStep("wait for screen load", () => spectator.LoadState == LoadState.Loaded && spectator.AllPlayersLoaded); + } + + private void start(int userId, int? beatmapId = null) => start(new[] { userId }, beatmapId); + + private void start(int[] userIds, int? beatmapId = null) + { + AddStep("start play", () => + { + foreach (int id in userIds) + { + Client.CurrentMatchPlayingUserIds.Add(id); + testSpectatorStreamingClient.StartPlay(id, beatmapId ?? importedBeatmapId); + playingUserIds.Add(id); + nextFrame[id] = 0; + } + }); + } + + private void finish(int userId, int? beatmapId = null) + { + AddStep("end play", () => + { + testSpectatorStreamingClient.EndPlay(userId, beatmapId ?? importedBeatmapId); + playingUserIds.Remove(userId); + nextFrame.Remove(userId); + }); + } + + private void sendFrames(int userId, int count = 10) => sendFrames(new[] { userId }, count); + + private void sendFrames(int[] userIds, int count = 10) + { + AddStep("send frames", () => + { + foreach (int id in userIds) + { + testSpectatorStreamingClient.SendFrames(id, nextFrame[id], count); + nextFrame[id] += count; + } + }); + } + + private void checkPaused(int userId, bool state) => + AddUntilStep($"{userId} is {(state ? "paused" : "playing")}", () => getPlayer(userId).ChildrenOfType().First().IsPaused.Value == state); + + private void checkPausedInstant(int userId, bool state) => + AddAssert($"{userId} is {(state ? "paused" : "playing")}", () => getPlayer(userId).ChildrenOfType().First().IsPaused.Value == state); + + private double getGameplayTime(int userId) => getPlayer(userId).ChildrenOfType().Single().GameplayClock.CurrentTime; + + private Player getPlayer(int userId) => getInstance(userId).ChildrenOfType().Single(); + + private PlayerInstance getInstance(int userId) => spectator.ChildrenOfType().Single(p => p.User.Id == userId); + + public class TestSpectatorStreamingClient : SpectatorStreamingClient + { + private readonly Dictionary userBeatmapDictionary = new Dictionary(); + private readonly Dictionary userSentStateDictionary = new Dictionary(); + + public TestSpectatorStreamingClient() + : base(new DevelopmentEndpointConfiguration()) + { + } + + public void StartPlay(int userId, int beatmapId) + { + userBeatmapDictionary[userId] = beatmapId; + userSentStateDictionary[userId] = false; + + sendState(userId, beatmapId); + } + + public void EndPlay(int userId, int beatmapId) + { + ((ISpectatorClient)this).UserFinishedPlaying(userId, new SpectatorState + { + BeatmapID = beatmapId, + RulesetID = 0, + }); + + userSentStateDictionary[userId] = false; + } + + public void SendFrames(int userId, int index, int count) + { + var frames = new List(); + + for (int i = index; i < index + count; i++) + { + var buttonState = i == index + count - 1 ? ReplayButtonState.None : ReplayButtonState.Left1; + + frames.Add(new LegacyReplayFrame(i * 100, RNG.Next(0, 512), RNG.Next(0, 512), buttonState)); + } + + var bundle = new FrameDataBundle(new ScoreInfo { Combo = index + count }, frames); + ((ISpectatorClient)this).UserSentFrames(userId, bundle); + + if (!userSentStateDictionary[userId]) + sendState(userId, userBeatmapDictionary[userId]); + } + + public override void WatchUser(int userId) + { + if (userSentStateDictionary[userId]) + { + // usually the server would do this. + sendState(userId, userBeatmapDictionary[userId]); + } + + base.WatchUser(userId); + } + + private void sendState(int userId, int beatmapId) + { + ((ISpectatorClient)this).UserBeganPlaying(userId, new SpectatorState + { + BeatmapID = beatmapId, + RulesetID = 0, + }); + + userSentStateDictionary[userId] = true; + } + } + + internal class TestUserLookupCache : UserLookupCache + { + protected override Task ComputeValueAsync(int lookup, CancellationToken token = default) + { + return Task.FromResult(new User + { + Id = lookup, + Username = $"User {lookup}" + }); + } + } + } +} From ecd0b84d944ed82e9de9e244c32230918846055b Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 8 Apr 2021 22:15:07 +0900 Subject: [PATCH 007/205] Use max_sync_offset constant --- .../OnlinePlay/Multiplayer/Spectate/MultiplayerSpectator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectator.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectator.cs index 8fa9dcf3af..90cf031584 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectator.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectator.cs @@ -98,7 +98,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate { inst.ChildrenOfType().Single().Start(); - if (targetTime < lastFrameTime && targetTime > currentTime + 16) + if (targetTime < lastFrameTime && targetTime > currentTime + max_sync_offset) inst.Beatmap.Track.AddAdjustment(AdjustableProperty.Frequency, catchupFrequencyAdjustment); else inst.Beatmap.Track.RemoveAdjustment(AdjustableProperty.Frequency, catchupFrequencyAdjustment); From 8005f146a914b26bbe87d763d8ee7d073101711c Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 9 Apr 2021 18:37:11 +0900 Subject: [PATCH 008/205] Fix inspection --- osu.Game/Screens/Spectate/SpectatorScreen.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Spectate/SpectatorScreen.cs b/osu.Game/Screens/Spectate/SpectatorScreen.cs index b7e1b8496c..6fa045c962 100644 --- a/osu.Game/Screens/Spectate/SpectatorScreen.cs +++ b/osu.Game/Screens/Spectate/SpectatorScreen.cs @@ -55,7 +55,7 @@ namespace osu.Game.Screens.Spectate /// The users to spectate. protected SpectatorScreen(params int[] userIds) { - this.UserIds = userIds; + UserIds = userIds; } protected override void LoadComplete() From 4fa51d5ec87a9b5592968737c7e908cfaa0da10b Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 9 Apr 2021 18:41:48 +0900 Subject: [PATCH 009/205] Add leaderboard to multiplayer spectate screen --- .../Spectate/MultiplayerSpectator.cs | 38 ++++++++++++++++++- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectator.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectator.cs index 90cf031584..d415669cb1 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectator.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectator.cs @@ -7,6 +7,7 @@ using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Framework.Testing; using osu.Framework.Utils; using osu.Game.Online.Spectator; @@ -30,6 +31,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate private readonly PlayerInstance[] instances; private PlayerGrid grid; + private MultiplayerSpectatorLeaderboard leaderboard; public MultiplayerSpectator(int[] userIds) : base(userIds.AsSpan().Slice(0, Math.Min(16, userIds.Length)).ToArray()) @@ -40,10 +42,35 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate [BackgroundDependencyLoader] private void load() { - InternalChild = grid = new PlayerGrid + Container leaderboardContainer; + + InternalChild = new GridContainer { - RelativeSizeAxes = Axes.Both + RelativeSizeAxes = Axes.Both, + ColumnDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize) + }, + Content = new[] + { + new Drawable[] + { + leaderboardContainer = new Container + { + RelativeSizeAxes = Axes.Y, + AutoSizeAxes = Axes.X + }, + grid = new PlayerGrid { RelativeSizeAxes = Axes.Both } + } + } }; + + // Todo: This is not quite correct - it should be per-user to adjust for other mod combinations. + var playableBeatmap = Beatmap.Value.GetPlayableBeatmap(Ruleset.Value); + var scoreProcessor = Ruleset.Value.CreateInstance().CreateScoreProcessor(); + scoreProcessor.ApplyBeatmap(playableBeatmap); + + LoadComponentAsync(leaderboard = new MultiplayerSpectatorLeaderboard(scoreProcessor, UserIds) { Expanded = { Value = true } }, leaderboardContainer.Add); } protected override void Update() @@ -118,18 +145,25 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate var existingInstance = instances[userIndex]; if (existingInstance != null) + { grid.Remove(existingInstance); + leaderboard.RemoveClock(existingInstance.User.Id); + } LoadComponentAsync(instances[userIndex] = new PlayerInstance(gameplayState.Score), d => { if (instances[userIndex] == d) + { grid.Add(d); + leaderboard.AddClock(d.User.Id, d.Beatmap.Track); + } }); } protected override void EndGameplay(int userId) { spectatorClient.StopWatchingUser(userId); + leaderboard.RemoveClock(userId); } private int getIndexForUser(int userId) => Array.IndexOf(UserIds, userId); From c93ce73123c142499f554fa77b752ec668573876 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 9 Apr 2021 19:58:24 +0900 Subject: [PATCH 010/205] Move catchup logic inside PlayerInstance, fixup some edge cases --- .../Spectate/MultiplayerSpectator.cs | 37 +++------ .../Spectate/MultiplayerSpectatorPlayer.cs | 2 + .../Multiplayer/Spectate/PlayerInstance.cs | 79 ++++++++++++++++++- 3 files changed, 93 insertions(+), 25 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectator.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectator.cs index d415669cb1..19c0d4e742 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectator.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectator.cs @@ -2,16 +2,13 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Diagnostics; using System.Linq; using osu.Framework.Allocation; -using osu.Framework.Audio; -using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Testing; using osu.Framework.Utils; using osu.Game.Online.Spectator; -using osu.Game.Screens.Play; using osu.Game.Screens.Spectate; namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate @@ -19,7 +16,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate public class MultiplayerSpectator : SpectatorScreen { private const double min_duration_to_allow_playback = 50; - private const double max_sync_offset = 2; // Isolates beatmap/ruleset to this screen. public override bool DisallowExternalBeatmapRulesetChanges => true; @@ -73,9 +69,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate LoadComponentAsync(leaderboard = new MultiplayerSpectatorLeaderboard(scoreProcessor, UserIds) { Expanded = { Value = true } }, leaderboardContainer.Add); } - protected override void Update() + protected override void UpdateAfterChildren() { - base.Update(); + base.UpdateAfterChildren(); updatePlayTime(); } @@ -85,7 +81,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate { if (gameplayStarted) { - ensurePlaying(instances.Select(i => i.Beatmap.Track.CurrentTime).Max()); + ensurePlaying(instances.Select(i => i.GetCurrentTrackTime()).Max()); return; } @@ -108,30 +104,23 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate private void ensureAllStopped() { foreach (var inst in instances) - inst.ChildrenOfType().SingleOrDefault()?.Stop(); + inst?.PauseGameplay(); } - private readonly BindableDouble catchupFrequencyAdjustment = new BindableDouble(2.0); - - private void ensurePlaying(double targetTime) + private void ensurePlaying(double targetTrackTime) { foreach (var inst in instances) { + Debug.Assert(inst != null); + double lastFrameTime = inst.Score.Replay.Frames.Select(f => f.Time).Last(); - double currentTime = inst.Beatmap.Track.CurrentTime; + double currentTime = inst.GetCurrentGameplayTime(); - // If we have enough frames to play back, start playback. - if (Precision.DefinitelyBigger(lastFrameTime, currentTime, min_duration_to_allow_playback)) - { - inst.ChildrenOfType().Single().Start(); + bool canContinuePlayback = Precision.DefinitelyBigger(lastFrameTime, currentTime, min_duration_to_allow_playback); + if (!canContinuePlayback) + continue; - if (targetTime < lastFrameTime && targetTime > currentTime + max_sync_offset) - inst.Beatmap.Track.AddAdjustment(AdjustableProperty.Frequency, catchupFrequencyAdjustment); - else - inst.Beatmap.Track.RemoveAdjustment(AdjustableProperty.Frequency, catchupFrequencyAdjustment); - } - else - inst.Beatmap.Track.RemoveAdjustment(AdjustableProperty.Frequency, catchupFrequencyAdjustment); + inst.ContinueGameplay(targetTrackTime); } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectatorPlayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectatorPlayer.cs index bad093f666..1634438850 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectatorPlayer.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectatorPlayer.cs @@ -11,6 +11,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate { public new ScoreProcessor ScoreProcessor => base.ScoreProcessor; + public new GameplayClockContainer GameplayClockContainer => base.GameplayClockContainer; + public MultiplayerSpectatorPlayer(Score score) : base(score) { diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerInstance.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerInstance.cs index 5689a3222a..acac753fe2 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerInstance.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerInstance.cs @@ -2,6 +2,8 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Beatmaps; @@ -15,10 +17,12 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate { public class PlayerInstance : CompositeDrawable { + private const double catchup_rate = 2; + private const double max_sync_offset = catchup_rate * 2; // Double the catchup rate to prevent ringing. + public bool PlayerLoaded => stack?.CurrentScreen is Player; public User User => Score.ScoreInfo.User; - public ScoreProcessor ScoreProcessor => player?.ScoreProcessor; public WorkingBeatmap Beatmap { get; private set; } public Ruleset Ruleset { get; private set; } @@ -54,5 +58,78 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate stack.Push(new MultiplayerSpectatorPlayerLoader(Score, () => player = new MultiplayerSpectatorPlayer(Score))); } + + protected override void UpdateAfterChildren() + { + base.UpdateAfterChildren(); + updateCatchup(); + } + + private readonly BindableDouble catchupFrequencyAdjustment = new BindableDouble(catchup_rate); + private double targetTrackTime; + private bool isCatchingUp; + + private void updateCatchup() + { + if (player?.IsLoaded != true) + return; + + if (Score.Replay.Frames.Count == 0) + return; + + if (player.GameplayClockContainer.IsPaused.Value) + return; + + double currentTime = Beatmap.Track.CurrentTime; + bool catchupRequired = targetTrackTime > currentTime + max_sync_offset; + + // Skip catchup if nothing needs to be done. + if (catchupRequired == isCatchingUp) + return; + + if (catchupRequired) + { + Beatmap.Track.AddAdjustment(AdjustableProperty.Frequency, catchupFrequencyAdjustment); + isCatchingUp = true; + } + else + { + Beatmap.Track.RemoveAdjustment(AdjustableProperty.Frequency, catchupFrequencyAdjustment); + isCatchingUp = false; + } + } + + public double GetCurrentGameplayTime() + { + if (player?.IsLoaded != true) + return 0; + + return player.GameplayClockContainer.GameplayClock.CurrentTime; + } + + public double GetCurrentTrackTime() + { + if (player?.IsLoaded != true) + return 0; + + return Beatmap.Track.CurrentTime; + } + + public void ContinueGameplay(double targetTrackTime) + { + if (player?.IsLoaded != true) + return; + + player.GameplayClockContainer.Start(); + this.targetTrackTime = targetTrackTime; + } + + public void PauseGameplay() + { + if (player?.IsLoaded != true) + return; + + player.GameplayClockContainer.Stop(); + } } } From 49b7519c53a765d9bb4576dd1f0c529a0027fd24 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 9 Apr 2021 20:03:34 +0900 Subject: [PATCH 011/205] Refactor gameplay starting logic --- .../Spectate/MultiplayerSpectator.cs | 36 ++++--------------- .../Multiplayer/Spectate/PlayerInstance.cs | 1 - 2 files changed, 7 insertions(+), 30 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectator.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectator.cs index 19c0d4e742..b0747f5027 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectator.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectator.cs @@ -72,43 +72,21 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate protected override void UpdateAfterChildren() { base.UpdateAfterChildren(); - updatePlayTime(); + updateGameplayPlayingState(); } - private bool gameplayStarted; - - private void updatePlayTime() + private void updateGameplayPlayingState() { - if (gameplayStarted) + // Make sure all players are loaded and have frames before starting any. + if (!AllPlayersLoaded || !instances.All(i => i.Score.Replay.Frames.Count > 0)) { - ensurePlaying(instances.Select(i => i.GetCurrentTrackTime()).Max()); + foreach (var inst in instances) + inst?.PauseGameplay(); return; } - // Make sure all players are loaded. - if (!AllPlayersLoaded) - { - ensureAllStopped(); - return; - } + double targetTrackTime = instances.Select(i => i.GetCurrentGameplayTime()).Max(); - if (!instances.All(i => i.Score.Replay.Frames.Count > 0)) - { - ensureAllStopped(); - return; - } - - gameplayStarted = true; - } - - private void ensureAllStopped() - { - foreach (var inst in instances) - inst?.PauseGameplay(); - } - - private void ensurePlaying(double targetTrackTime) - { foreach (var inst in instances) { Debug.Assert(inst != null); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerInstance.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerInstance.cs index acac753fe2..ca58d888fa 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerInstance.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerInstance.cs @@ -8,7 +8,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Beatmaps; using osu.Game.Rulesets; -using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using osu.Game.Screens.Play; using osu.Game.Users; From eccd269ccee483973c260daa44a0dddc7740f9c0 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 9 Apr 2021 20:17:57 +0900 Subject: [PATCH 012/205] Implement maximum start delay --- .../Multiplayer/TestSceneMultiplayerScreen.cs | 2 +- .../Spectate/MultiplayerSpectator.cs | 26 ++++++++++++++++--- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerScreen.cs index d085827986..897219dbad 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerScreen.cs @@ -110,7 +110,7 @@ namespace osu.Game.Tests.Visual.Multiplayer loadSpectateScreen(); // Send frames for one player only, both should remain paused. - sendFrames(55, 20); + sendFrames(55, 1000); checkPausedInstant(55, true); checkPausedInstant(56, true); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectator.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectator.cs index b0747f5027..d8ec25fedb 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectator.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectator.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using System.Diagnostics; using System.Linq; using osu.Framework.Allocation; @@ -16,6 +17,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate public class MultiplayerSpectator : SpectatorScreen { private const double min_duration_to_allow_playback = 50; + private const double maximum_start_delay = 15000; // Isolates beatmap/ruleset to this screen. public override bool DisallowExternalBeatmapRulesetChanges => true; @@ -28,6 +30,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate private readonly PlayerInstance[] instances; private PlayerGrid grid; private MultiplayerSpectatorLeaderboard leaderboard; + private double? loadStartTime; public MultiplayerSpectator(int[] userIds) : base(userIds.AsSpan().Slice(0, Math.Min(16, userIds.Length)).ToArray()) @@ -72,22 +75,39 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate protected override void UpdateAfterChildren() { base.UpdateAfterChildren(); + + loadStartTime ??= Time.Current; + updateGameplayPlayingState(); } + private bool canStartGameplay => + // All players must be loaded. + AllPlayersLoaded + && ( + // All players have frames... + instances.All(i => i.Score.Replay.Frames.Count > 0) + // Or any player has frames and the maximum start delay has been exceeded. + || (Time.Current - loadStartTime > maximum_start_delay + && instances.Any(i => i.Score.Replay.Frames.Count > 0)) + ); + private void updateGameplayPlayingState() { // Make sure all players are loaded and have frames before starting any. - if (!AllPlayersLoaded || !instances.All(i => i.Score.Replay.Frames.Count > 0)) + if (!canStartGameplay) { foreach (var inst in instances) inst?.PauseGameplay(); return; } - double targetTrackTime = instances.Select(i => i.GetCurrentGameplayTime()).Max(); + // Not all instances may be in a valid gameplay state (see canStartGameplay). Only control the ones that are. + IEnumerable validInstances = instances.Where(i => i.Score.Replay.Frames.Count > 0); - foreach (var inst in instances) + double targetTrackTime = validInstances.Select(i => i.GetCurrentTrackTime()).Max(); + + foreach (var inst in validInstances) { Debug.Assert(inst != null); From 61c400b1a1187a17a17467a02a061fb87dca65f4 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 9 Apr 2021 20:18:26 +0900 Subject: [PATCH 013/205] Fix filename --- ...SceneMultiplayerScreen.cs => TestSceneMultiplayerSpectator.cs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename osu.Game.Tests/Visual/Multiplayer/{TestSceneMultiplayerScreen.cs => TestSceneMultiplayerSpectator.cs} (100%) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectator.cs similarity index 100% rename from osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerScreen.cs rename to osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectator.cs From 3e46d6401eeaef7a63ed6506f8b8d2840b53767c Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 9 Apr 2021 20:22:30 +0900 Subject: [PATCH 014/205] Remove some unnecessary code --- .../Multiplayer/Spectate/MultiplayerSpectator.cs | 2 +- .../OnlinePlay/Multiplayer/Spectate/PlayerInstance.cs | 11 ++--------- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectator.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectator.cs index d8ec25fedb..db5de45f72 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectator.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectator.cs @@ -82,7 +82,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate } private bool canStartGameplay => - // All players must be loaded. + // All players must be loaded, and... AllPlayersLoaded && ( // All players have frames... diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerInstance.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerInstance.cs index ca58d888fa..6b5f102543 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerInstance.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerInstance.cs @@ -7,7 +7,6 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Beatmaps; -using osu.Game.Rulesets; using osu.Game.Scoring; using osu.Game.Screens.Play; using osu.Game.Users; @@ -24,7 +23,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate public User User => Score.ScoreInfo.User; public WorkingBeatmap Beatmap { get; private set; } - public Ruleset Ruleset { get; private set; } public readonly Score Score; @@ -43,7 +41,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate private void load(BeatmapManager beatmapManager) { Beatmap = beatmapManager.GetWorkingBeatmap(Score.ScoreInfo.Beatmap, bypassCache: true); - Ruleset = Score.ScoreInfo.Ruleset.CreateInstance(); InternalChild = new GameplayIsolationContainer(Beatmap, Score.ScoreInfo.Ruleset, Score.ScoreInfo.Mods) { @@ -87,15 +84,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate return; if (catchupRequired) - { Beatmap.Track.AddAdjustment(AdjustableProperty.Frequency, catchupFrequencyAdjustment); - isCatchingUp = true; - } else - { Beatmap.Track.RemoveAdjustment(AdjustableProperty.Frequency, catchupFrequencyAdjustment); - isCatchingUp = false; - } + + isCatchingUp = catchupRequired; } public double GetCurrentGameplayTime() From 6eddc6c59e6c63c219798bc0fb92a59e0d3c3b96 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 9 Apr 2021 21:03:45 +0900 Subject: [PATCH 015/205] Enable spectating multiplayer matches --- .../Online/Multiplayer/MultiplayerClient.cs | 3 --- .../Screens/OnlinePlay/Match/RoomSubScreen.cs | 6 ++++-- .../Multiplayer/MultiplayerMatchSubScreen.cs | 20 +++++++++++++++---- 3 files changed, 20 insertions(+), 9 deletions(-) diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 37e11cc576..4529dfd0a7 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -96,9 +96,6 @@ namespace osu.Game.Online.Multiplayer if (!IsConnected.Value) return Task.CompletedTask; - if (newState == MultiplayerUserState.Spectating) - return Task.CompletedTask; // Not supported yet. - return connection.InvokeAsync(nameof(IMultiplayerServer.ChangeState), newState); } diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index 706da05d15..0d1ed5d88e 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -148,10 +148,12 @@ namespace osu.Game.Screens.OnlinePlay.Match return base.OnExiting(next); } - protected void StartPlay(Func player) + protected void StartPlay(Func player) => PushTopLevelScreen(() => new PlayerLoader(player)); + + protected void PushTopLevelScreen(Func screen) { sampleStart?.Play(); - ParentScreen?.Push(new PlayerLoader(player)); + ParentScreen?.Push(screen()); } private void selectedItemChanged() diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index 90cef0107c..035eba4f30 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -25,6 +25,7 @@ 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.Participants; +using osu.Game.Screens.OnlinePlay.Multiplayer.Spectate; using osu.Game.Screens.Play.HUD; using osu.Game.Users; using osuTK; @@ -405,11 +406,22 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer } } - private void onRoomUpdated() + private void onRoomUpdated() => Scheduler.Add(() => { - // user mods may have changed. - Scheduler.AddOnce(UpdateMods); - } + if (client.Room == null) + return; + + Debug.Assert(client.LocalUser != null); + + UpdateMods(); + + if (client.LocalUser.State == MultiplayerUserState.Spectating + && (client.Room.State == MultiplayerRoomState.Playing || client.Room.State == MultiplayerRoomState.WaitingForLoad) + && ParentScreen.IsCurrentScreen()) + { + PushTopLevelScreen(() => new MultiplayerSpectator(client.CurrentMatchPlayingUserIds.ToArray())); + } + }); private void onLoadRequested() { From 4409c1a36f18dbae1484dd6ef65413a275e0b57b Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 9 Apr 2021 22:01:21 +0900 Subject: [PATCH 016/205] Increase sync offset to prevent constant catchups --- .../Screens/OnlinePlay/Multiplayer/Spectate/PlayerInstance.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerInstance.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerInstance.cs index 6b5f102543..f0994ec375 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerInstance.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerInstance.cs @@ -16,7 +16,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate public class PlayerInstance : CompositeDrawable { private const double catchup_rate = 2; - private const double max_sync_offset = catchup_rate * 2; // Double the catchup rate to prevent ringing. + private const double max_sync_offset = 50; public bool PlayerLoaded => stack?.CurrentScreen is Player; From 627dd960b07198135b941c1a82a70338058fd21d Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 13 Apr 2021 20:52:20 +0900 Subject: [PATCH 017/205] Disable player input for now --- .../Screens/OnlinePlay/Multiplayer/Spectate/PlayerInstance.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerInstance.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerInstance.cs index f0994ec375..32cc3b48d6 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerInstance.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerInstance.cs @@ -123,5 +123,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate player.GameplayClockContainer.Stop(); } + + // Player interferes with global input, so disable input for now. + public override bool PropagatePositionalInputSubTree => false; + public override bool PropagateNonPositionalInputSubTree => false; } } From 20823abb309ac0fd908bdd03b532abfbdbd22afd Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 13 Apr 2021 22:10:35 +0900 Subject: [PATCH 018/205] Make resyncing a bit more resilient --- .../TestSceneMultiplayerSpectator.cs | 33 ++++++++++++++++- .../Multiplayer/Spectate/PlayerInstance.cs | 35 +++++++++++++++---- 2 files changed, 61 insertions(+), 7 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectator.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectator.cs index 897219dbad..1090f1d10e 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectator.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectator.cs @@ -1,12 +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 System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using NUnit.Framework; using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Testing; using osu.Framework.Utils; @@ -153,6 +156,27 @@ namespace osu.Game.Tests.Visual.Multiplayer checkPausedInstant(55, false); } + [Test] + public void TestPlayerStartsCatchingUpOnlyAfterExceedingMaxOffset() + { + start(new[] { 55, 56 }); + loadSpectateScreen(); + + sendFrames(55, 1000); + sendFrames(56, 1000); + + Bindable slowDownAdjustment; + + AddStep("slow down player 2", () => + { + slowDownAdjustment = new Bindable(0.99); + getInstance(56).Beatmap.Track.AddAdjustment(AdjustableProperty.Frequency, slowDownAdjustment); + }); + + AddUntilStep("exceeded min offset but not catching up", () => getGameplayOffset(55, 56) > PlayerInstance.MAX_OFFSET && !getInstance(56).IsCatchingUp); + AddUntilStep("catching up or caught up", () => getInstance(56).IsCatchingUp || Math.Abs(getGameplayOffset(55, 56)) < PlayerInstance.SYNC_TARGET * 2); + } + [Test] public void TestPlayersCatchUpAfterFallingBehind() { @@ -174,7 +198,9 @@ namespace osu.Game.Tests.Visual.Multiplayer checkPausedInstant(56, false); // Player 2 should catch up to player 1 after unpausing. - AddUntilStep("player 1 time == player 2 time", () => Precision.AlmostEquals(getGameplayTime(55), getGameplayTime(56), 16)); + AddUntilStep("player 2 not catching up", () => !getInstance(56).IsCatchingUp); + AddAssert("player 1 time == player 2 time", () => Math.Abs(getGameplayOffset(55, 56)) <= 2 * PlayerInstance.SYNC_TARGET); + AddWaitStep("wait a bit", 5); } private void loadSpectateScreen() @@ -236,6 +262,11 @@ namespace osu.Game.Tests.Visual.Multiplayer private void checkPausedInstant(int userId, bool state) => AddAssert($"{userId} is {(state ? "paused" : "playing")}", () => getPlayer(userId).ChildrenOfType().First().IsPaused.Value == state); + /// + /// Returns time(user1) - time(user2). + /// + private double getGameplayOffset(int user1, int user2) => getGameplayTime(user1) - getGameplayTime(user2); + private double getGameplayTime(int userId) => getPlayer(userId).ChildrenOfType().Single().GameplayClock.CurrentTime; private Player getPlayer(int userId) => getInstance(userId).ChildrenOfType().Single(); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerInstance.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerInstance.cs index 32cc3b48d6..a4a3a0c133 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerInstance.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerInstance.cs @@ -6,6 +6,7 @@ using osu.Framework.Audio; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Logging; using osu.Game.Beatmaps; using osu.Game.Scoring; using osu.Game.Screens.Play; @@ -15,8 +16,20 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate { public class PlayerInstance : CompositeDrawable { + /// + /// The rate at which a user catches up after becoming desynchronised. + /// private const double catchup_rate = 2; - private const double max_sync_offset = 50; + + /// + /// The offset from the expected time at which to START synchronisation. + /// + public const double MAX_OFFSET = 50; + + /// + /// The maximum offset from the expected time at which to STOP synchronisation. + /// + public const double SYNC_TARGET = 16; public bool PlayerLoaded => stack?.CurrentScreen is Player; @@ -26,6 +39,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate public readonly Score Score; + public bool IsCatchingUp { get; private set; } + private OsuScreenStack stack; private MultiplayerSpectatorPlayer player; @@ -63,7 +78,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate private readonly BindableDouble catchupFrequencyAdjustment = new BindableDouble(catchup_rate); private double targetTrackTime; - private bool isCatchingUp; private void updateCatchup() { @@ -77,18 +91,27 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate return; double currentTime = Beatmap.Track.CurrentTime; - bool catchupRequired = targetTrackTime > currentTime + max_sync_offset; + double timeBehind = targetTrackTime - currentTime; - // Skip catchup if nothing needs to be done. - if (catchupRequired == isCatchingUp) + double offsetForCatchup = IsCatchingUp ? SYNC_TARGET : MAX_OFFSET; + bool catchupRequired = timeBehind > offsetForCatchup; + + // Skip catchup if no work needs to be done. + if (catchupRequired == IsCatchingUp) return; if (catchupRequired) + { Beatmap.Track.AddAdjustment(AdjustableProperty.Frequency, catchupFrequencyAdjustment); + Logger.Log($"{User.Id} catchup started (behind: {timeBehind})"); + } else + { Beatmap.Track.RemoveAdjustment(AdjustableProperty.Frequency, catchupFrequencyAdjustment); + Logger.Log($"{User.Id} catchup finished (behind: {timeBehind})"); + } - isCatchingUp = catchupRequired; + IsCatchingUp = catchupRequired; } public double GetCurrentGameplayTime() From 3039b7b0f9731e881dc79f23a741d7b3b29df18d Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 13 Apr 2021 22:40:10 +0900 Subject: [PATCH 019/205] Make tests a bit more resilient --- .../Visual/Multiplayer/TestSceneMultiplayerSpectator.cs | 3 +-- .../Multiplayer/Spectate/MultiplayerSpectator.cs | 7 ++++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectator.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectator.cs index 1090f1d10e..1ac012c0b5 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectator.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectator.cs @@ -199,8 +199,7 @@ namespace osu.Game.Tests.Visual.Multiplayer // Player 2 should catch up to player 1 after unpausing. AddUntilStep("player 2 not catching up", () => !getInstance(56).IsCatchingUp); - AddAssert("player 1 time == player 2 time", () => Math.Abs(getGameplayOffset(55, 56)) <= 2 * PlayerInstance.SYNC_TARGET); - AddWaitStep("wait a bit", 5); + AddWaitStep("wait a bit", 10); } private void loadSpectateScreen() diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectator.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectator.cs index db5de45f72..636e7b9a49 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectator.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectator.cs @@ -30,7 +30,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate private readonly PlayerInstance[] instances; private PlayerGrid grid; private MultiplayerSpectatorLeaderboard leaderboard; - private double? loadStartTime; + private double? loadFinishTime; public MultiplayerSpectator(int[] userIds) : base(userIds.AsSpan().Slice(0, Math.Min(16, userIds.Length)).ToArray()) @@ -76,7 +76,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate { base.UpdateAfterChildren(); - loadStartTime ??= Time.Current; + if (AllPlayersLoaded) + loadFinishTime ??= Time.Current; updateGameplayPlayingState(); } @@ -88,7 +89,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate // All players have frames... instances.All(i => i.Score.Replay.Frames.Count > 0) // Or any player has frames and the maximum start delay has been exceeded. - || (Time.Current - loadStartTime > maximum_start_delay + || (Time.Current - loadFinishTime > maximum_start_delay && instances.Any(i => i.Score.Replay.Frames.Count > 0)) ); From d49b90877e906960cfc081894cc3ecd8687f35db Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 13 Apr 2021 23:21:35 +0900 Subject: [PATCH 020/205] Fix operation remaining in progress --- .../OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index 035eba4f30..9a68ff908d 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -420,6 +420,10 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer && ParentScreen.IsCurrentScreen()) { PushTopLevelScreen(() => new MultiplayerSpectator(client.CurrentMatchPlayingUserIds.ToArray())); + + // If the current user was host, they started the match and the in-progres operation needs to be stopped now. + readyClickOperation?.Dispose(); + readyClickOperation = null; } }); From 56e1bffdfd237ceb45db074982b912e27612640e Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 13 Apr 2021 23:40:10 +0900 Subject: [PATCH 021/205] Populate initial user states --- osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs b/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs index 2ddc10db0f..fe1201ef89 100644 --- a/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs @@ -144,6 +144,9 @@ namespace osu.Game.Online.Multiplayer Room = joinedRoom; apiRoom = room; defaultPlaylistItemId = apiRoom.Playlist.FirstOrDefault()?.ID ?? 0; + + foreach (var user in joinedRoom.Users) + updateUserPlayingState(user.UserID, user.State); }, cancellationSource.Token).ConfigureAwait(false); // Update room settings. From 77830527e7e06df39fbf8b8ae341b9847bef2795 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 13 Apr 2021 23:49:23 +0900 Subject: [PATCH 022/205] Fix spectate button being disabled during play --- .../OnlinePlay/Multiplayer/Match/MultiplayerSpectateButton.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerSpectateButton.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerSpectateButton.cs index 4b3fb5d00f..465af037e2 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerSpectateButton.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerSpectateButton.cs @@ -81,7 +81,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match break; } - button.Enabled.Value = Client.Room?.State == MultiplayerRoomState.Open && !operationInProgress.Value; + button.Enabled.Value = !operationInProgress.Value; } private class ButtonWithTrianglesExposed : TriangleButton From 69b01e727008bb8dd7d15bce834e72aee9ae3ca1 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 14 Apr 2021 00:58:03 +0900 Subject: [PATCH 023/205] Add some debugging --- .../OnlinePlay/Multiplayer/Spectate/MultiplayerSpectator.cs | 4 ++++ .../Screens/OnlinePlay/Multiplayer/Spectate/PlayerInstance.cs | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectator.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectator.cs index 636e7b9a49..a56104439d 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectator.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectator.cs @@ -8,6 +8,7 @@ using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Logging; using osu.Framework.Utils; using osu.Game.Online.Spectator; using osu.Game.Screens.Spectate; @@ -108,6 +109,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate double targetTrackTime = validInstances.Select(i => i.GetCurrentTrackTime()).Max(); + var instanceTimes = string.Join(',', validInstances.Select(i => $" {i.User.Id}: {(int)i.GetCurrentTrackTime()}")); + Logger.Log($"target: {(int)targetTrackTime},{instanceTimes}"); + foreach (var inst in validInstances) { Debug.Assert(inst != null); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerInstance.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerInstance.cs index a4a3a0c133..12a710e9f0 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerInstance.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerInstance.cs @@ -103,12 +103,12 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate if (catchupRequired) { Beatmap.Track.AddAdjustment(AdjustableProperty.Frequency, catchupFrequencyAdjustment); - Logger.Log($"{User.Id} catchup started (behind: {timeBehind})"); + Logger.Log($"{User.Id} catchup started (behind: {(int)timeBehind})"); } else { Beatmap.Track.RemoveAdjustment(AdjustableProperty.Frequency, catchupFrequencyAdjustment); - Logger.Log($"{User.Id} catchup finished (behind: {timeBehind})"); + Logger.Log($"{User.Id} catchup finished (behind: {(int)timeBehind})"); } IsCatchingUp = catchupRequired; From 774cca38c4c66627c3f501712f9e40cba1ef8474 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 14 Apr 2021 20:39:14 +0900 Subject: [PATCH 024/205] Make spectating instances use custom GCC --- .../Spectate/MultiplayerSpectator.cs | 48 ++++++++++++------- .../Spectate/MultiplayerSpectatorPlayer.cs | 33 ++++++++++++- .../Multiplayer/Spectate/PlayerInstance.cs | 29 ++++++----- 3 files changed, 75 insertions(+), 35 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectator.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectator.cs index a56104439d..931d13942b 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectator.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectator.cs @@ -11,6 +11,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Logging; using osu.Framework.Utils; using osu.Game.Online.Spectator; +using osu.Game.Screens.Play; using osu.Game.Screens.Spectate; namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate @@ -29,6 +30,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate private SpectatorStreamingClient spectatorClient { get; set; } private readonly PlayerInstance[] instances; + private GameplayClockContainer gameplayClockContainer; private PlayerGrid grid; private MultiplayerSpectatorLeaderboard leaderboard; private double? loadFinishTime; @@ -44,23 +46,26 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate { Container leaderboardContainer; - InternalChild = new GridContainer + InternalChild = gameplayClockContainer = new MasterGameplayClockContainer(Beatmap.Value, 0) { - RelativeSizeAxes = Axes.Both, - ColumnDimensions = new[] + Child = new GridContainer { - new Dimension(GridSizeMode.AutoSize) - }, - Content = new[] - { - new Drawable[] + RelativeSizeAxes = Axes.Both, + ColumnDimensions = new[] { - leaderboardContainer = new Container + new Dimension(GridSizeMode.AutoSize) + }, + Content = new[] + { + new Drawable[] { - RelativeSizeAxes = Axes.Y, - AutoSizeAxes = Axes.X - }, - grid = new PlayerGrid { RelativeSizeAxes = Axes.Both } + leaderboardContainer = new Container + { + RelativeSizeAxes = Axes.Y, + AutoSizeAxes = Axes.X + }, + grid = new PlayerGrid { RelativeSizeAxes = Axes.Both } + } } } }; @@ -94,6 +99,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate && instances.Any(i => i.Score.Replay.Frames.Count > 0)) ); + private bool firstStartFrame = true; + private void updateGameplayPlayingState() { // Make sure all players are loaded and have frames before starting any. @@ -104,13 +111,16 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate return; } + if (firstStartFrame) + gameplayClockContainer.Restart(); + // Not all instances may be in a valid gameplay state (see canStartGameplay). Only control the ones that are. IEnumerable validInstances = instances.Where(i => i.Score.Replay.Frames.Count > 0); - double targetTrackTime = validInstances.Select(i => i.GetCurrentTrackTime()).Max(); + double targetGameplayTime = gameplayClockContainer.GameplayClock.CurrentTime; - var instanceTimes = string.Join(',', validInstances.Select(i => $" {i.User.Id}: {(int)i.GetCurrentTrackTime()}")); - Logger.Log($"target: {(int)targetTrackTime},{instanceTimes}"); + var instanceTimes = string.Join(',', validInstances.Select(i => $" {i.User.Id}: {(int)i.GetCurrentGameplayTime()}")); + Logger.Log($"target: {(int)targetGameplayTime},{instanceTimes}"); foreach (var inst in validInstances) { @@ -123,8 +133,10 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate if (!canContinuePlayback) continue; - inst.ContinueGameplay(targetTrackTime); + inst.ContinueGameplay(targetGameplayTime); } + + firstStartFrame = false; } protected override void OnUserStateChanged(int userId, SpectatorState spectatorState) @@ -142,7 +154,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate leaderboard.RemoveClock(existingInstance.User.Id); } - LoadComponentAsync(instances[userIndex] = new PlayerInstance(gameplayState.Score), d => + LoadComponentAsync(instances[userIndex] = new PlayerInstance(gameplayState.Score, gameplayClockContainer.GameplayClock), d => { if (instances[userIndex] == d) { diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectatorPlayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectatorPlayer.cs index 1634438850..abb7ec83d8 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectatorPlayer.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectatorPlayer.cs @@ -1,6 +1,9 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.Bindables; +using osu.Framework.Timing; +using osu.Game.Beatmaps; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using osu.Game.Screens.Play; @@ -11,11 +14,37 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate { public new ScoreProcessor ScoreProcessor => base.ScoreProcessor; - public new GameplayClockContainer GameplayClockContainer => base.GameplayClockContainer; + public new SubGameplayClockContainer GameplayClockContainer => (SubGameplayClockContainer)base.GameplayClockContainer; - public MultiplayerSpectatorPlayer(Score score) + private readonly GameplayClock gameplayClock; + + public MultiplayerSpectatorPlayer(Score score, GameplayClock gameplayClock) : base(score) { + this.gameplayClock = gameplayClock; } + + protected override GameplayClockContainer CreateGameplayClockContainer(WorkingBeatmap beatmap, double gameplayStart) + => new SubGameplayClockContainer(gameplayClock); + } + + public class SubGameplayClockContainer : GameplayClockContainer + { + public new DecoupleableInterpolatingFramedClock AdjustableClock => base.AdjustableClock; + + public SubGameplayClockContainer(IClock sourceClock) + : base(sourceClock) + { + } + + protected override void OnIsPausedChanged(ValueChangedEvent isPaused) + { + if (isPaused.NewValue) + AdjustableClock.Stop(); + else + AdjustableClock.Start(); + } + + protected override GameplayClock CreateGameplayClock(IFrameBasedClock source) => new GameplayClock(source); } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerInstance.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerInstance.cs index 12a710e9f0..724a685cb7 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerInstance.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerInstance.cs @@ -2,8 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; -using osu.Framework.Audio; -using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Logging; @@ -38,15 +36,17 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate public WorkingBeatmap Beatmap { get; private set; } public readonly Score Score; + private readonly GameplayClock gameplayClock; public bool IsCatchingUp { get; private set; } private OsuScreenStack stack; private MultiplayerSpectatorPlayer player; - public PlayerInstance(Score score) + public PlayerInstance(Score score, GameplayClock gameplayClock) { Score = score; + this.gameplayClock = gameplayClock; RelativeSizeAxes = Axes.Both; Masking = true; @@ -67,7 +67,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate } }; - stack.Push(new MultiplayerSpectatorPlayerLoader(Score, () => player = new MultiplayerSpectatorPlayer(Score))); + stack.Push(new MultiplayerSpectatorPlayerLoader(Score, () => player = new MultiplayerSpectatorPlayer(Score, gameplayClock))); } protected override void UpdateAfterChildren() @@ -76,8 +76,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate updateCatchup(); } - private readonly BindableDouble catchupFrequencyAdjustment = new BindableDouble(catchup_rate); - private double targetTrackTime; + private double targetGameplayTime; private void updateCatchup() { @@ -91,7 +90,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate return; double currentTime = Beatmap.Track.CurrentTime; - double timeBehind = targetTrackTime - currentTime; + double timeBehind = targetGameplayTime - currentTime; double offsetForCatchup = IsCatchingUp ? SYNC_TARGET : MAX_OFFSET; bool catchupRequired = timeBehind > offsetForCatchup; @@ -102,12 +101,12 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate if (catchupRequired) { - Beatmap.Track.AddAdjustment(AdjustableProperty.Frequency, catchupFrequencyAdjustment); + // player.GameplayClockContainer.AdjustableClock.Rate = catchup_rate; Logger.Log($"{User.Id} catchup started (behind: {(int)timeBehind})"); } else { - Beatmap.Track.RemoveAdjustment(AdjustableProperty.Frequency, catchupFrequencyAdjustment); + // player.GameplayClockContainer.AdjustableClock.Rate = 1; Logger.Log($"{User.Id} catchup finished (behind: {(int)timeBehind})"); } @@ -122,21 +121,21 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate return player.GameplayClockContainer.GameplayClock.CurrentTime; } - public double GetCurrentTrackTime() + public bool IsPlaying() { - if (player?.IsLoaded != true) - return 0; + if (player.IsLoaded != true) + return false; - return Beatmap.Track.CurrentTime; + return player.GameplayClockContainer.GameplayClock.IsRunning; } - public void ContinueGameplay(double targetTrackTime) + public void ContinueGameplay(double targetGameplayTime) { if (player?.IsLoaded != true) return; player.GameplayClockContainer.Start(); - this.targetTrackTime = targetTrackTime; + this.targetGameplayTime = targetGameplayTime; } public void PauseGameplay() From 6fc7488a67ddf580358369327c78ddea5d7aab8d Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 15 Apr 2021 16:33:59 +0900 Subject: [PATCH 025/205] Reimplement syncing logic as a new component --- .../OnlinePlay/MultiplayerSyncManagerTest.cs | 213 ++++++++++++++++++ .../Spectate/IMultiplayerSlaveClock.cs | 17 ++ .../Spectate/IMultiplayerSyncManager.cs | 16 ++ .../Spectate/MultiplayerSyncManager.cs | 162 +++++++++++++ 4 files changed, 408 insertions(+) create mode 100644 osu.Game.Tests/OnlinePlay/MultiplayerSyncManagerTest.cs create mode 100644 osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/IMultiplayerSlaveClock.cs create mode 100644 osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/IMultiplayerSyncManager.cs create mode 100644 osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSyncManager.cs diff --git a/osu.Game.Tests/OnlinePlay/MultiplayerSyncManagerTest.cs b/osu.Game.Tests/OnlinePlay/MultiplayerSyncManagerTest.cs new file mode 100644 index 0000000000..2a6dfa9c8d --- /dev/null +++ b/osu.Game.Tests/OnlinePlay/MultiplayerSyncManagerTest.cs @@ -0,0 +1,213 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using NUnit.Framework; +using osu.Framework.Bindables; +using osu.Framework.Testing; +using osu.Framework.Timing; +using osu.Game.Screens.OnlinePlay.Multiplayer.Spectate; +using osu.Game.Tests.Visual; + +namespace osu.Game.Tests.OnlinePlay +{ + [HeadlessTest] + public class MultiplayerSyncManagerTest : OsuTestScene + { + private TestManualClock master; + private MultiplayerSyncManager syncManager; + + private TestSlaveClock slave1; + private TestSlaveClock slave2; + + [SetUp] + public void Setup() + { + syncManager = new MultiplayerSyncManager(master = new TestManualClock()); + syncManager.AddSlave(slave1 = new TestSlaveClock(1)); + syncManager.AddSlave(slave2 = new TestSlaveClock(2)); + + Schedule(() => Child = syncManager); + } + + [Test] + public void TestMasterClockStartsWhenAllSlavesHaveFrames() + { + setWaiting(() => slave1, false); + assertMasterState(false); + assertSlaveState(() => slave1, false); + assertSlaveState(() => slave2, false); + + setWaiting(() => slave2, false); + assertMasterState(true); + assertSlaveState(() => slave1, true); + assertSlaveState(() => slave2, true); + } + + [Test] + public void TestMasterClockDoesNotStartWhenNoneReadyForMaximumDelayTime() + { + AddWaitStep($"wait {MultiplayerSyncManager.MAXIMUM_START_DELAY} milliseconds", (int)Math.Ceiling(MultiplayerSyncManager.MAXIMUM_START_DELAY / TimePerAction)); + assertMasterState(false); + } + + [Test] + public void TestMasterClockStartsWhenAnyReadyForMaximumDelayTime() + { + setWaiting(() => slave1, false); + AddWaitStep($"wait {MultiplayerSyncManager.MAXIMUM_START_DELAY} milliseconds", (int)Math.Ceiling(MultiplayerSyncManager.MAXIMUM_START_DELAY / TimePerAction)); + assertMasterState(true); + } + + [Test] + public void TestSlaveDoesNotCatchUpWhenSlightlyOutOfSync() + { + setAllWaiting(false); + + setMasterTime(MultiplayerSyncManager.SYNC_TARGET + 1); + assertCatchingUp(() => slave1, false); + } + + [Test] + public void TestSlaveStartsCatchingUpWhenTooFarBehind() + { + setAllWaiting(false); + + setMasterTime(MultiplayerSyncManager.MAX_SYNC_OFFSET + 1); + assertCatchingUp(() => slave1, true); + assertCatchingUp(() => slave2, true); + } + + [Test] + public void TestSlaveKeepsCatchingUpWhenSlightlyOutOfSync() + { + setAllWaiting(false); + + setMasterTime(MultiplayerSyncManager.MAX_SYNC_OFFSET + 1); + setSlaveTime(() => slave1, MultiplayerSyncManager.SYNC_TARGET + 1); + assertCatchingUp(() => slave1, true); + } + + [Test] + public void TestSlaveStopsCatchingUpWhenInSync() + { + setAllWaiting(false); + + setMasterTime(MultiplayerSyncManager.MAX_SYNC_OFFSET + 2); + setSlaveTime(() => slave1, MultiplayerSyncManager.SYNC_TARGET); + assertCatchingUp(() => slave1, false); + assertCatchingUp(() => slave2, true); + } + + [Test] + public void TestSlaveDoesNotStopWhenSlightlyAhead() + { + setAllWaiting(false); + + setSlaveTime(() => slave1, -MultiplayerSyncManager.SYNC_TARGET); + assertCatchingUp(() => slave1, false); + assertSlaveState(() => slave1, true); + } + + [Test] + public void TestSlaveStopsWhenTooFarAheadAndStartsWhenBackInSync() + { + setAllWaiting(false); + + setSlaveTime(() => slave1, -MultiplayerSyncManager.SYNC_TARGET - 1); + + // This is a silent catchup, where IsCatchingUp = false but IsRunning = false also. + assertCatchingUp(() => slave1, false); + assertSlaveState(() => slave1, false); + + setMasterTime(1); + assertCatchingUp(() => slave1, false); + assertSlaveState(() => slave1, true); + } + + [Test] + public void TestInSyncSlaveDoesNotStartIfWaitingOnFrames() + { + setAllWaiting(false); + + assertSlaveState(() => slave1, true); + setWaiting(() => slave1, true); + assertSlaveState(() => slave1, false); + } + + private void setWaiting(Func slave, bool waiting) + => AddStep($"set slave {slave().Id} waiting = {waiting}", () => slave().WaitingOnFrames.Value = waiting); + + private void setAllWaiting(bool waiting) => AddStep($"set all slaves waiting = {waiting}", () => + { + slave1.WaitingOnFrames.Value = waiting; + slave2.WaitingOnFrames.Value = waiting; + }); + + private void setMasterTime(double time) + => AddStep($"set master = {time}", () => master.Seek(time)); + + /// + /// slave.Time = master.Time - offsetFromMaster + /// + private void setSlaveTime(Func slave, double offsetFromMaster) + => AddStep($"set slave {slave().Id} = master - {offsetFromMaster}", () => slave().Seek(master.CurrentTime - offsetFromMaster)); + + private void assertMasterState(bool running) + => AddAssert($"master clock {(running ? "is" : "is not")} running", () => master.IsRunning == running); + + private void assertCatchingUp(Func slave, bool catchingUp) => + AddAssert($"slave {slave().Id} {(catchingUp ? "is" : "is not")} catching up", () => slave().IsCatchingUp == catchingUp); + + private void assertSlaveState(Func slave, bool running) + => AddAssert($"slave {slave().Id} {(running ? "is" : "is not")} running", () => slave().IsRunning == running); + + private class TestSlaveClock : TestManualClock, IMultiplayerSlaveClock + { + public readonly Bindable WaitingOnFrames = new Bindable(true); + IBindable IMultiplayerSlaveClock.WaitingOnFrames => WaitingOnFrames; + + public double LastFrameTime => 0; + + double IMultiplayerSlaveClock.LastFrameTime => LastFrameTime; + + public bool IsCatchingUp { get; set; } + + public readonly int Id; + + public TestSlaveClock(int id) + { + Id = id; + + WaitingOnFrames.BindValueChanged(waiting => + { + if (waiting.NewValue) + Stop(); + else + Start(); + }); + } + } + + private class TestManualClock : ManualClock, IAdjustableClock + { + public void Start() => IsRunning = true; + + public void Stop() => IsRunning = false; + + public bool Seek(double position) + { + CurrentTime = position; + return true; + } + + public void Reset() + { + } + + public void ResetSpeedAdjustments() + { + } + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/IMultiplayerSlaveClock.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/IMultiplayerSlaveClock.cs new file mode 100644 index 0000000000..e0fca45bdd --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/IMultiplayerSlaveClock.cs @@ -0,0 +1,17 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Bindables; +using osu.Framework.Timing; + +namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate +{ + public interface IMultiplayerSlaveClock : IAdjustableClock + { + IBindable WaitingOnFrames { get; } + + double LastFrameTime { get; } + + bool IsCatchingUp { get; set; } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/IMultiplayerSyncManager.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/IMultiplayerSyncManager.cs new file mode 100644 index 0000000000..2c50596823 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/IMultiplayerSyncManager.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. + +using osu.Framework.Timing; + +namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate +{ + public interface IMultiplayerSyncManager + { + IAdjustableClock Master { get; } + + void AddSlave(IMultiplayerSlaveClock clock); + + void RemoveSlave(IMultiplayerSlaveClock clock); + } +} diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSyncManager.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSyncManager.cs new file mode 100644 index 0000000000..42f6536e90 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSyncManager.cs @@ -0,0 +1,162 @@ +// 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.Logging; +using osu.Framework.Timing; + +namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate +{ + public class MultiplayerSyncManager : Component, IMultiplayerSyncManager + { + /// + /// The offset from the master clock to which slaves should be synchronised to. + /// + public const double SYNC_TARGET = 16; + + /// + /// The offset from the master clock at which slaves begin resynchronising. + /// + public const double MAX_SYNC_OFFSET = 50; + + /// + /// The maximum delay to start gameplay, if any (but not all) slaves are ready. + /// + public const double MAXIMUM_START_DELAY = 15000; + + /// + /// The catchup rate. + /// + public const double CATCHUP_RATE = 2; + + /// + /// The master clock which is used to control the timing of all slave clocks. + /// + public IAdjustableClock Master { get; } + + /// + /// The slave clocks. + /// + private readonly List slaves = new List(); + + private bool hasStarted; + private double? firstStartAttemptTime; + + public MultiplayerSyncManager(IAdjustableClock master) + { + Master = master; + } + + public void AddSlave(IMultiplayerSlaveClock clock) => slaves.Add(clock); + + public void RemoveSlave(IMultiplayerSlaveClock clock) => slaves.Remove(clock); + + protected override void Update() + { + base.Update(); + + if (!attemptStart()) + { + // Ensure all slaves are stopped until the start succeeds. + foreach (var slave in slaves) + slave.Stop(); + return; + } + + updateCatchup(); + updateMasterClock(); + } + + /// + /// Attempts to start playback. Awaits for all slaves to have available frames for up to milliseconds. + /// + /// Whether playback was started and syncing should occur. + private bool attemptStart() + { + if (hasStarted) + return true; + + if (slaves.Count == 0) + return false; + + firstStartAttemptTime ??= Time.Current; + + int readyCount = slaves.Count(s => !s.WaitingOnFrames.Value); + + if (readyCount == slaves.Count) + { + Logger.Log("Gameplay started (all ready)."); + return hasStarted = true; + } + + if (readyCount > 0 && (Time.Current - firstStartAttemptTime) > MAXIMUM_START_DELAY) + { + Logger.Log($"Gameplay started (maximum delay exceeded, {readyCount}/{slaves.Count} ready)."); + return hasStarted = true; + } + + return false; + } + + /// + /// Updates the catchup states of all slave clocks. + /// + private void updateCatchup() + { + for (int i = 0; i < slaves.Count; i++) + { + var slave = slaves[i]; + double timeDelta = Master.CurrentTime - slave.CurrentTime; + + // Check that the slave isn't too far ahead. + // This is a quiet case in which the catchup is done by the master clock, so IsCatchingUp is not set on the slave. + if (timeDelta < -SYNC_TARGET) + { + slave.Stop(); + continue; + } + + // Make sure the slave is running if it can. + if (!slave.WaitingOnFrames.Value) + slave.Start(); + + if (slave.IsCatchingUp) + { + // Stop the slave from catching up if it's within the sync target. + if (timeDelta <= SYNC_TARGET) + { + slave.IsCatchingUp = false; + Logger.Log($"Slave {i} catchup finished (delta = {timeDelta})"); + } + } + else + { + // Make the slave start catching up if it's exceeded the maximum allowable sync offset. + if (timeDelta > MAX_SYNC_OFFSET) + { + slave.IsCatchingUp = true; + Logger.Log($"Slave {i} catchup started (too far behind, delta = {timeDelta})"); + } + } + } + } + + /// + /// Updates the master clock's running state. + /// + private void updateMasterClock() + { + bool anyInSync = slaves.Any(s => !s.IsCatchingUp); + + if (Master.IsRunning != anyInSync) + { + if (anyInSync) + Master.Start(); + else + Master.Stop(); + } + } + } +} From 33ad7850cb432a2f2d7cfbbe60ae302f353443a4 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 15 Apr 2021 16:45:59 +0900 Subject: [PATCH 026/205] Remove LastFrameTime --- osu.Game.Tests/OnlinePlay/MultiplayerSyncManagerTest.cs | 4 ---- .../OnlinePlay/Multiplayer/Spectate/IMultiplayerSlaveClock.cs | 2 -- 2 files changed, 6 deletions(-) diff --git a/osu.Game.Tests/OnlinePlay/MultiplayerSyncManagerTest.cs b/osu.Game.Tests/OnlinePlay/MultiplayerSyncManagerTest.cs index 2a6dfa9c8d..6ee867c6f8 100644 --- a/osu.Game.Tests/OnlinePlay/MultiplayerSyncManagerTest.cs +++ b/osu.Game.Tests/OnlinePlay/MultiplayerSyncManagerTest.cs @@ -167,10 +167,6 @@ namespace osu.Game.Tests.OnlinePlay public readonly Bindable WaitingOnFrames = new Bindable(true); IBindable IMultiplayerSlaveClock.WaitingOnFrames => WaitingOnFrames; - public double LastFrameTime => 0; - - double IMultiplayerSlaveClock.LastFrameTime => LastFrameTime; - public bool IsCatchingUp { get; set; } public readonly int Id; diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/IMultiplayerSlaveClock.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/IMultiplayerSlaveClock.cs index e0fca45bdd..e90eed68ab 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/IMultiplayerSlaveClock.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/IMultiplayerSlaveClock.cs @@ -10,8 +10,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate { IBindable WaitingOnFrames { get; } - double LastFrameTime { get; } - bool IsCatchingUp { get; set; } } } From fe3ba2b80ee1ad0034217ec1693646a6f5d31d95 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 15 Apr 2021 19:07:25 +0900 Subject: [PATCH 027/205] Implement IAdjustableClock on GameplayClockContainer --- .../Screens/Play/GameplayClockContainer.cs | 35 ++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/GameplayClockContainer.cs b/osu.Game/Screens/Play/GameplayClockContainer.cs index 6d863f0094..bbffdc2325 100644 --- a/osu.Game/Screens/Play/GameplayClockContainer.cs +++ b/osu.Game/Screens/Play/GameplayClockContainer.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.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -9,7 +10,7 @@ using osu.Framework.Timing; namespace osu.Game.Screens.Play { - public abstract class GameplayClockContainer : Container + public abstract class GameplayClockContainer : Container, IAdjustableClock { /// /// The final clock which is exposed to underlying components. @@ -90,5 +91,37 @@ namespace osu.Game.Screens.Play protected virtual IFrameBasedClock ClockToProcess => AdjustableClock; protected abstract GameplayClock CreateGameplayClock(IFrameBasedClock source); + + #region IAdjustableClock + + bool IAdjustableClock.Seek(double position) + { + Seek(position); + return true; + } + + void IAdjustableClock.Reset() + { + Restart(); + Stop(); + } + + public void ResetSpeedAdjustments() + { + } + + double IAdjustableClock.Rate + { + get => GameplayClock.Rate; + set => throw new NotSupportedException(); + } + + double IClock.Rate => GameplayClock.Rate; + + public double CurrentTime => GameplayClock.CurrentTime; + + public bool IsRunning => GameplayClock.IsRunning; + + #endregion } } From 1705d472b5ff9a6194e0b18ddae14f01c1b91286 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 15 Apr 2021 19:12:52 +0900 Subject: [PATCH 028/205] Reimplement multiplayer syncing using new master/slave clocks --- .../TestSceneMultiplayerSpectator.cs | 30 +---- .../Spectate/MultiplayerSlaveClock.cs | 78 +++++++++++++ .../Spectate/MultiplayerSpectator.cs | 86 +++------------ .../Spectate/MultiplayerSpectatorPlayer.cs | 9 +- .../Spectate/MultiplayerSyncManager.cs | 5 - .../Multiplayer/Spectate/PlayerInstance.cs | 103 +----------------- 6 files changed, 104 insertions(+), 207 deletions(-) create mode 100644 osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSlaveClock.cs diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectator.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectator.cs index 1ac012c0b5..ab6324df2d 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectator.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectator.cs @@ -1,15 +1,12 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using NUnit.Framework; using osu.Framework.Allocation; -using osu.Framework.Audio; -using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Testing; using osu.Framework.Utils; @@ -156,27 +153,6 @@ namespace osu.Game.Tests.Visual.Multiplayer checkPausedInstant(55, false); } - [Test] - public void TestPlayerStartsCatchingUpOnlyAfterExceedingMaxOffset() - { - start(new[] { 55, 56 }); - loadSpectateScreen(); - - sendFrames(55, 1000); - sendFrames(56, 1000); - - Bindable slowDownAdjustment; - - AddStep("slow down player 2", () => - { - slowDownAdjustment = new Bindable(0.99); - getInstance(56).Beatmap.Track.AddAdjustment(AdjustableProperty.Frequency, slowDownAdjustment); - }); - - AddUntilStep("exceeded min offset but not catching up", () => getGameplayOffset(55, 56) > PlayerInstance.MAX_OFFSET && !getInstance(56).IsCatchingUp); - AddUntilStep("catching up or caught up", () => getInstance(56).IsCatchingUp || Math.Abs(getGameplayOffset(55, 56)) < PlayerInstance.SYNC_TARGET * 2); - } - [Test] public void TestPlayersCatchUpAfterFallingBehind() { @@ -184,7 +160,7 @@ namespace osu.Game.Tests.Visual.Multiplayer loadSpectateScreen(); // Send initial frames for both players. A few more for player 1. - sendFrames(55, 100); + sendFrames(55, 1000); sendFrames(56, 10); checkPausedInstant(55, false); checkPausedInstant(56, false); @@ -194,11 +170,11 @@ namespace osu.Game.Tests.Visual.Multiplayer AddWaitStep("wait a few more frames", 10); // Send more frames for player 2. It should unpause. - sendFrames(56, 100); + sendFrames(56, 1000); checkPausedInstant(56, false); // Player 2 should catch up to player 1 after unpausing. - AddUntilStep("player 2 not catching up", () => !getInstance(56).IsCatchingUp); + AddUntilStep("player 2 not catching up", () => !getInstance(56).GameplayClock.IsCatchingUp); AddWaitStep("wait a bit", 10); } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSlaveClock.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSlaveClock.cs new file mode 100644 index 0000000000..84a87271a2 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSlaveClock.cs @@ -0,0 +1,78 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework.Bindables; +using osu.Framework.Timing; + +namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate +{ + public class MultiplayerSlaveClock : IFrameBasedClock, IMultiplayerSlaveClock + { + /// + /// The catchup rate. + /// + public const double CATCHUP_RATE = 2; + + private readonly IFrameBasedClock masterClock; + + public MultiplayerSlaveClock(IFrameBasedClock masterClock) + { + this.masterClock = masterClock; + } + + public double CurrentTime { get; private set; } + + public bool IsRunning { get; private set; } + + public void Reset() => CurrentTime = 0; + + public void Start() => IsRunning = true; + + public void Stop() => IsRunning = false; + + public bool Seek(double position) => true; + + public void ResetSpeedAdjustments() + { + } + + public double Rate => IsCatchingUp ? CATCHUP_RATE : 1; + + double IAdjustableClock.Rate + { + get => Rate; + set => throw new NotSupportedException(); + } + + double IClock.Rate => Rate; + + public void ProcessFrame() + { + masterClock.ProcessFrame(); + + ElapsedFrameTime = 0; + FramesPerSecond = 0; + + if (IsRunning) + { + double elapsedSource = masterClock.ElapsedFrameTime; + double elapsed = elapsedSource * Rate; + + CurrentTime += elapsed; + ElapsedFrameTime = elapsed; + FramesPerSecond = masterClock.FramesPerSecond; + } + } + + public double ElapsedFrameTime { get; private set; } + + public double FramesPerSecond { get; private set; } + + public FrameTimeInfo TimeInfo => new FrameTimeInfo { Elapsed = ElapsedFrameTime, Current = CurrentTime }; + + public IBindable WaitingOnFrames { get; } = new Bindable(); + + public bool IsCatchingUp { get; set; } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectator.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectator.cs index 931d13942b..be5a8cf8c0 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectator.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectator.cs @@ -2,14 +2,10 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; -using System.Diagnostics; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Logging; -using osu.Framework.Utils; using osu.Game.Online.Spectator; using osu.Game.Screens.Play; using osu.Game.Screens.Spectate; @@ -18,9 +14,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate { public class MultiplayerSpectator : SpectatorScreen { - private const double min_duration_to_allow_playback = 50; - private const double maximum_start_delay = 15000; - // Isolates beatmap/ruleset to this screen. public override bool DisallowExternalBeatmapRulesetChanges => true; @@ -30,10 +23,10 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate private SpectatorStreamingClient spectatorClient { get; set; } private readonly PlayerInstance[] instances; - private GameplayClockContainer gameplayClockContainer; + private MasterGameplayClockContainer masterClockContainer; + private IMultiplayerSyncManager syncManager; private PlayerGrid grid; private MultiplayerSpectatorLeaderboard leaderboard; - private double? loadFinishTime; public MultiplayerSpectator(int[] userIds) : base(userIds.AsSpan().Slice(0, Math.Min(16, userIds.Length)).ToArray()) @@ -46,7 +39,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate { Container leaderboardContainer; - InternalChild = gameplayClockContainer = new MasterGameplayClockContainer(Beatmap.Value, 0) + masterClockContainer = new MasterGameplayClockContainer(Beatmap.Value, 0) { Child = new GridContainer { @@ -70,6 +63,12 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate } }; + InternalChildren = new[] + { + (Drawable)(syncManager = new MultiplayerSyncManager(masterClockContainer)), + masterClockContainer + }; + // Todo: This is not quite correct - it should be per-user to adjust for other mod combinations. var playableBeatmap = Beatmap.Value.GetPlayableBeatmap(Ruleset.Value); var scoreProcessor = Ruleset.Value.CreateInstance().CreateScoreProcessor(); @@ -78,65 +77,12 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate LoadComponentAsync(leaderboard = new MultiplayerSpectatorLeaderboard(scoreProcessor, UserIds) { Expanded = { Value = true } }, leaderboardContainer.Add); } - protected override void UpdateAfterChildren() + protected override void LoadComplete() { - base.UpdateAfterChildren(); + base.LoadComplete(); - if (AllPlayersLoaded) - loadFinishTime ??= Time.Current; - - updateGameplayPlayingState(); - } - - private bool canStartGameplay => - // All players must be loaded, and... - AllPlayersLoaded - && ( - // All players have frames... - instances.All(i => i.Score.Replay.Frames.Count > 0) - // Or any player has frames and the maximum start delay has been exceeded. - || (Time.Current - loadFinishTime > maximum_start_delay - && instances.Any(i => i.Score.Replay.Frames.Count > 0)) - ); - - private bool firstStartFrame = true; - - private void updateGameplayPlayingState() - { - // Make sure all players are loaded and have frames before starting any. - if (!canStartGameplay) - { - foreach (var inst in instances) - inst?.PauseGameplay(); - return; - } - - if (firstStartFrame) - gameplayClockContainer.Restart(); - - // Not all instances may be in a valid gameplay state (see canStartGameplay). Only control the ones that are. - IEnumerable validInstances = instances.Where(i => i.Score.Replay.Frames.Count > 0); - - double targetGameplayTime = gameplayClockContainer.GameplayClock.CurrentTime; - - var instanceTimes = string.Join(',', validInstances.Select(i => $" {i.User.Id}: {(int)i.GetCurrentGameplayTime()}")); - Logger.Log($"target: {(int)targetGameplayTime},{instanceTimes}"); - - foreach (var inst in validInstances) - { - Debug.Assert(inst != null); - - double lastFrameTime = inst.Score.Replay.Frames.Select(f => f.Time).Last(); - double currentTime = inst.GetCurrentGameplayTime(); - - bool canContinuePlayback = Precision.DefinitelyBigger(lastFrameTime, currentTime, min_duration_to_allow_playback); - if (!canContinuePlayback) - continue; - - inst.ContinueGameplay(targetGameplayTime); - } - - firstStartFrame = false; + masterClockContainer.Stop(); + masterClockContainer.Restart(); } protected override void OnUserStateChanged(int userId, SpectatorState spectatorState) @@ -151,15 +97,17 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate if (existingInstance != null) { grid.Remove(existingInstance); + syncManager.RemoveSlave(existingInstance.GameplayClock); leaderboard.RemoveClock(existingInstance.User.Id); } - LoadComponentAsync(instances[userIndex] = new PlayerInstance(gameplayState.Score, gameplayClockContainer.GameplayClock), d => + LoadComponentAsync(instances[userIndex] = new PlayerInstance(gameplayState.Score, new MultiplayerSlaveClock(masterClockContainer.GameplayClock)), d => { if (instances[userIndex] == d) { grid.Add(d); - leaderboard.AddClock(d.User.Id, d.Beatmap.Track); + syncManager.AddSlave(d.GameplayClock); + leaderboard.AddClock(d.User.Id, d.GameplayClock); } }); } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectatorPlayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectatorPlayer.cs index abb7ec83d8..c82c407c2c 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectatorPlayer.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectatorPlayer.cs @@ -4,7 +4,6 @@ using osu.Framework.Bindables; using osu.Framework.Timing; using osu.Game.Beatmaps; -using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using osu.Game.Screens.Play; @@ -12,13 +11,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate { public class MultiplayerSpectatorPlayer : SpectatorPlayer { - public new ScoreProcessor ScoreProcessor => base.ScoreProcessor; + private readonly MultiplayerSlaveClock gameplayClock; - public new SubGameplayClockContainer GameplayClockContainer => (SubGameplayClockContainer)base.GameplayClockContainer; - - private readonly GameplayClock gameplayClock; - - public MultiplayerSpectatorPlayer(Score score, GameplayClock gameplayClock) + public MultiplayerSpectatorPlayer(Score score, MultiplayerSlaveClock gameplayClock) : base(score) { this.gameplayClock = gameplayClock; diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSyncManager.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSyncManager.cs index 42f6536e90..8cbfa823cf 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSyncManager.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSyncManager.cs @@ -26,11 +26,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate /// public const double MAXIMUM_START_DELAY = 15000; - /// - /// The catchup rate. - /// - public const double CATCHUP_RATE = 2; - /// /// The master clock which is used to control the timing of all slave clocks. /// diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerInstance.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerInstance.cs index 724a685cb7..631ac2d52e 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerInstance.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerInstance.cs @@ -4,7 +4,6 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Logging; using osu.Game.Beatmaps; using osu.Game.Scoring; using osu.Game.Screens.Play; @@ -14,21 +13,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate { public class PlayerInstance : CompositeDrawable { - /// - /// The rate at which a user catches up after becoming desynchronised. - /// - private const double catchup_rate = 2; - - /// - /// The offset from the expected time at which to START synchronisation. - /// - public const double MAX_OFFSET = 50; - - /// - /// The maximum offset from the expected time at which to STOP synchronisation. - /// - public const double SYNC_TARGET = 16; - public bool PlayerLoaded => stack?.CurrentScreen is Player; public User User => Score.ScoreInfo.User; @@ -36,17 +20,14 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate public WorkingBeatmap Beatmap { get; private set; } public readonly Score Score; - private readonly GameplayClock gameplayClock; - - public bool IsCatchingUp { get; private set; } + public readonly MultiplayerSlaveClock GameplayClock; private OsuScreenStack stack; - private MultiplayerSpectatorPlayer player; - public PlayerInstance(Score score, GameplayClock gameplayClock) + public PlayerInstance(Score score, MultiplayerSlaveClock gameplayClock) { Score = score; - this.gameplayClock = gameplayClock; + GameplayClock = gameplayClock; RelativeSizeAxes = Axes.Both; Masking = true; @@ -67,83 +48,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate } }; - stack.Push(new MultiplayerSpectatorPlayerLoader(Score, () => player = new MultiplayerSpectatorPlayer(Score, gameplayClock))); - } - - protected override void UpdateAfterChildren() - { - base.UpdateAfterChildren(); - updateCatchup(); - } - - private double targetGameplayTime; - - private void updateCatchup() - { - if (player?.IsLoaded != true) - return; - - if (Score.Replay.Frames.Count == 0) - return; - - if (player.GameplayClockContainer.IsPaused.Value) - return; - - double currentTime = Beatmap.Track.CurrentTime; - double timeBehind = targetGameplayTime - currentTime; - - double offsetForCatchup = IsCatchingUp ? SYNC_TARGET : MAX_OFFSET; - bool catchupRequired = timeBehind > offsetForCatchup; - - // Skip catchup if no work needs to be done. - if (catchupRequired == IsCatchingUp) - return; - - if (catchupRequired) - { - // player.GameplayClockContainer.AdjustableClock.Rate = catchup_rate; - Logger.Log($"{User.Id} catchup started (behind: {(int)timeBehind})"); - } - else - { - // player.GameplayClockContainer.AdjustableClock.Rate = 1; - Logger.Log($"{User.Id} catchup finished (behind: {(int)timeBehind})"); - } - - IsCatchingUp = catchupRequired; - } - - public double GetCurrentGameplayTime() - { - if (player?.IsLoaded != true) - return 0; - - return player.GameplayClockContainer.GameplayClock.CurrentTime; - } - - public bool IsPlaying() - { - if (player.IsLoaded != true) - return false; - - return player.GameplayClockContainer.GameplayClock.IsRunning; - } - - public void ContinueGameplay(double targetGameplayTime) - { - if (player?.IsLoaded != true) - return; - - player.GameplayClockContainer.Start(); - this.targetGameplayTime = targetGameplayTime; - } - - public void PauseGameplay() - { - if (player?.IsLoaded != true) - return; - - player.GameplayClockContainer.Stop(); + stack.Push(new MultiplayerSpectatorPlayerLoader(Score, () => new MultiplayerSpectatorPlayer(Score, GameplayClock))); } // Player interferes with global input, so disable input for now. From df4fce2c570b885176cb70f1f2232610ad2a7848 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 15 Apr 2021 19:16:00 +0900 Subject: [PATCH 029/205] Rename classes --- ...s => MultiplayerCatchupSyncManagerTest.cs} | 50 +++++++++---------- ....cs => IMultiplayerSpectatorSlaveClock.cs} | 2 +- ...cs => IMultiplayerSpectatorSyncManager.cs} | 6 +-- ...er.cs => MultiplayerCatchupSyncManager.cs} | 10 ++-- .../Spectate/MultiplayerSpectator.cs | 6 +-- .../Spectate/MultiplayerSpectatorPlayer.cs | 4 +- ...k.cs => MultiplayerSpectatorSlaveClock.cs} | 4 +- .../Multiplayer/Spectate/PlayerInstance.cs | 4 +- 8 files changed, 43 insertions(+), 43 deletions(-) rename osu.Game.Tests/OnlinePlay/{MultiplayerSyncManagerTest.cs => MultiplayerCatchupSyncManagerTest.cs} (70%) rename osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/{IMultiplayerSlaveClock.cs => IMultiplayerSpectatorSlaveClock.cs} (83%) rename osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/{IMultiplayerSyncManager.cs => IMultiplayerSpectatorSyncManager.cs} (62%) rename osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/{MultiplayerSyncManager.cs => MultiplayerCatchupSyncManager.cs} (91%) rename osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/{MultiplayerSlaveClock.cs => MultiplayerSpectatorSlaveClock.cs} (91%) diff --git a/osu.Game.Tests/OnlinePlay/MultiplayerSyncManagerTest.cs b/osu.Game.Tests/OnlinePlay/MultiplayerCatchupSyncManagerTest.cs similarity index 70% rename from osu.Game.Tests/OnlinePlay/MultiplayerSyncManagerTest.cs rename to osu.Game.Tests/OnlinePlay/MultiplayerCatchupSyncManagerTest.cs index 6ee867c6f8..f15fb5cfd1 100644 --- a/osu.Game.Tests/OnlinePlay/MultiplayerSyncManagerTest.cs +++ b/osu.Game.Tests/OnlinePlay/MultiplayerCatchupSyncManagerTest.cs @@ -12,22 +12,22 @@ using osu.Game.Tests.Visual; namespace osu.Game.Tests.OnlinePlay { [HeadlessTest] - public class MultiplayerSyncManagerTest : OsuTestScene + public class MultiplayerCatchupSyncManagerTest : OsuTestScene { private TestManualClock master; - private MultiplayerSyncManager syncManager; + private MultiplayerCatchupSyncManager catchupSyncManager; - private TestSlaveClock slave1; - private TestSlaveClock slave2; + private TestSpectatorSlaveClock slave1; + private TestSpectatorSlaveClock slave2; [SetUp] public void Setup() { - syncManager = new MultiplayerSyncManager(master = new TestManualClock()); - syncManager.AddSlave(slave1 = new TestSlaveClock(1)); - syncManager.AddSlave(slave2 = new TestSlaveClock(2)); + catchupSyncManager = new MultiplayerCatchupSyncManager(master = new TestManualClock()); + catchupSyncManager.AddSlave(slave1 = new TestSpectatorSlaveClock(1)); + catchupSyncManager.AddSlave(slave2 = new TestSpectatorSlaveClock(2)); - Schedule(() => Child = syncManager); + Schedule(() => Child = catchupSyncManager); } [Test] @@ -47,7 +47,7 @@ namespace osu.Game.Tests.OnlinePlay [Test] public void TestMasterClockDoesNotStartWhenNoneReadyForMaximumDelayTime() { - AddWaitStep($"wait {MultiplayerSyncManager.MAXIMUM_START_DELAY} milliseconds", (int)Math.Ceiling(MultiplayerSyncManager.MAXIMUM_START_DELAY / TimePerAction)); + AddWaitStep($"wait {MultiplayerCatchupSyncManager.MAXIMUM_START_DELAY} milliseconds", (int)Math.Ceiling(MultiplayerCatchupSyncManager.MAXIMUM_START_DELAY / TimePerAction)); assertMasterState(false); } @@ -55,7 +55,7 @@ namespace osu.Game.Tests.OnlinePlay public void TestMasterClockStartsWhenAnyReadyForMaximumDelayTime() { setWaiting(() => slave1, false); - AddWaitStep($"wait {MultiplayerSyncManager.MAXIMUM_START_DELAY} milliseconds", (int)Math.Ceiling(MultiplayerSyncManager.MAXIMUM_START_DELAY / TimePerAction)); + AddWaitStep($"wait {MultiplayerCatchupSyncManager.MAXIMUM_START_DELAY} milliseconds", (int)Math.Ceiling(MultiplayerCatchupSyncManager.MAXIMUM_START_DELAY / TimePerAction)); assertMasterState(true); } @@ -64,7 +64,7 @@ namespace osu.Game.Tests.OnlinePlay { setAllWaiting(false); - setMasterTime(MultiplayerSyncManager.SYNC_TARGET + 1); + setMasterTime(MultiplayerCatchupSyncManager.SYNC_TARGET + 1); assertCatchingUp(() => slave1, false); } @@ -73,7 +73,7 @@ namespace osu.Game.Tests.OnlinePlay { setAllWaiting(false); - setMasterTime(MultiplayerSyncManager.MAX_SYNC_OFFSET + 1); + setMasterTime(MultiplayerCatchupSyncManager.MAX_SYNC_OFFSET + 1); assertCatchingUp(() => slave1, true); assertCatchingUp(() => slave2, true); } @@ -83,8 +83,8 @@ namespace osu.Game.Tests.OnlinePlay { setAllWaiting(false); - setMasterTime(MultiplayerSyncManager.MAX_SYNC_OFFSET + 1); - setSlaveTime(() => slave1, MultiplayerSyncManager.SYNC_TARGET + 1); + setMasterTime(MultiplayerCatchupSyncManager.MAX_SYNC_OFFSET + 1); + setSlaveTime(() => slave1, MultiplayerCatchupSyncManager.SYNC_TARGET + 1); assertCatchingUp(() => slave1, true); } @@ -93,8 +93,8 @@ namespace osu.Game.Tests.OnlinePlay { setAllWaiting(false); - setMasterTime(MultiplayerSyncManager.MAX_SYNC_OFFSET + 2); - setSlaveTime(() => slave1, MultiplayerSyncManager.SYNC_TARGET); + setMasterTime(MultiplayerCatchupSyncManager.MAX_SYNC_OFFSET + 2); + setSlaveTime(() => slave1, MultiplayerCatchupSyncManager.SYNC_TARGET); assertCatchingUp(() => slave1, false); assertCatchingUp(() => slave2, true); } @@ -104,7 +104,7 @@ namespace osu.Game.Tests.OnlinePlay { setAllWaiting(false); - setSlaveTime(() => slave1, -MultiplayerSyncManager.SYNC_TARGET); + setSlaveTime(() => slave1, -MultiplayerCatchupSyncManager.SYNC_TARGET); assertCatchingUp(() => slave1, false); assertSlaveState(() => slave1, true); } @@ -114,7 +114,7 @@ namespace osu.Game.Tests.OnlinePlay { setAllWaiting(false); - setSlaveTime(() => slave1, -MultiplayerSyncManager.SYNC_TARGET - 1); + setSlaveTime(() => slave1, -MultiplayerCatchupSyncManager.SYNC_TARGET - 1); // This is a silent catchup, where IsCatchingUp = false but IsRunning = false also. assertCatchingUp(() => slave1, false); @@ -135,7 +135,7 @@ namespace osu.Game.Tests.OnlinePlay assertSlaveState(() => slave1, false); } - private void setWaiting(Func slave, bool waiting) + private void setWaiting(Func slave, bool waiting) => AddStep($"set slave {slave().Id} waiting = {waiting}", () => slave().WaitingOnFrames.Value = waiting); private void setAllWaiting(bool waiting) => AddStep($"set all slaves waiting = {waiting}", () => @@ -150,28 +150,28 @@ namespace osu.Game.Tests.OnlinePlay /// /// slave.Time = master.Time - offsetFromMaster /// - private void setSlaveTime(Func slave, double offsetFromMaster) + private void setSlaveTime(Func slave, double offsetFromMaster) => AddStep($"set slave {slave().Id} = master - {offsetFromMaster}", () => slave().Seek(master.CurrentTime - offsetFromMaster)); private void assertMasterState(bool running) => AddAssert($"master clock {(running ? "is" : "is not")} running", () => master.IsRunning == running); - private void assertCatchingUp(Func slave, bool catchingUp) => + private void assertCatchingUp(Func slave, bool catchingUp) => AddAssert($"slave {slave().Id} {(catchingUp ? "is" : "is not")} catching up", () => slave().IsCatchingUp == catchingUp); - private void assertSlaveState(Func slave, bool running) + private void assertSlaveState(Func slave, bool running) => AddAssert($"slave {slave().Id} {(running ? "is" : "is not")} running", () => slave().IsRunning == running); - private class TestSlaveClock : TestManualClock, IMultiplayerSlaveClock + private class TestSpectatorSlaveClock : TestManualClock, IMultiplayerSpectatorSlaveClock { public readonly Bindable WaitingOnFrames = new Bindable(true); - IBindable IMultiplayerSlaveClock.WaitingOnFrames => WaitingOnFrames; + IBindable IMultiplayerSpectatorSlaveClock.WaitingOnFrames => WaitingOnFrames; public bool IsCatchingUp { get; set; } public readonly int Id; - public TestSlaveClock(int id) + public TestSpectatorSlaveClock(int id) { Id = id; diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/IMultiplayerSlaveClock.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/IMultiplayerSpectatorSlaveClock.cs similarity index 83% rename from osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/IMultiplayerSlaveClock.cs rename to osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/IMultiplayerSpectatorSlaveClock.cs index e90eed68ab..fe2c2dfec9 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/IMultiplayerSlaveClock.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/IMultiplayerSpectatorSlaveClock.cs @@ -6,7 +6,7 @@ using osu.Framework.Timing; namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate { - public interface IMultiplayerSlaveClock : IAdjustableClock + public interface IMultiplayerSpectatorSlaveClock : IAdjustableClock { IBindable WaitingOnFrames { get; } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/IMultiplayerSyncManager.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/IMultiplayerSpectatorSyncManager.cs similarity index 62% rename from osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/IMultiplayerSyncManager.cs rename to osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/IMultiplayerSpectatorSyncManager.cs index 2c50596823..5643bde68f 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/IMultiplayerSyncManager.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/IMultiplayerSpectatorSyncManager.cs @@ -5,12 +5,12 @@ using osu.Framework.Timing; namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate { - public interface IMultiplayerSyncManager + public interface IMultiplayerSpectatorSyncManager { IAdjustableClock Master { get; } - void AddSlave(IMultiplayerSlaveClock clock); + void AddSlave(IMultiplayerSpectatorSlaveClock clock); - void RemoveSlave(IMultiplayerSlaveClock clock); + void RemoveSlave(IMultiplayerSpectatorSlaveClock clock); } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSyncManager.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerCatchupSyncManager.cs similarity index 91% rename from osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSyncManager.cs rename to osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerCatchupSyncManager.cs index 8cbfa823cf..873f34626d 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSyncManager.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerCatchupSyncManager.cs @@ -9,7 +9,7 @@ using osu.Framework.Timing; namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate { - public class MultiplayerSyncManager : Component, IMultiplayerSyncManager + public class MultiplayerCatchupSyncManager : Component, IMultiplayerSpectatorSyncManager { /// /// The offset from the master clock to which slaves should be synchronised to. @@ -34,19 +34,19 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate /// /// The slave clocks. /// - private readonly List slaves = new List(); + private readonly List slaves = new List(); private bool hasStarted; private double? firstStartAttemptTime; - public MultiplayerSyncManager(IAdjustableClock master) + public MultiplayerCatchupSyncManager(IAdjustableClock master) { Master = master; } - public void AddSlave(IMultiplayerSlaveClock clock) => slaves.Add(clock); + public void AddSlave(IMultiplayerSpectatorSlaveClock clock) => slaves.Add(clock); - public void RemoveSlave(IMultiplayerSlaveClock clock) => slaves.Remove(clock); + public void RemoveSlave(IMultiplayerSpectatorSlaveClock clock) => slaves.Remove(clock); protected override void Update() { diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectator.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectator.cs index be5a8cf8c0..3dade4d32c 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectator.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectator.cs @@ -24,7 +24,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate private readonly PlayerInstance[] instances; private MasterGameplayClockContainer masterClockContainer; - private IMultiplayerSyncManager syncManager; + private IMultiplayerSpectatorSyncManager syncManager; private PlayerGrid grid; private MultiplayerSpectatorLeaderboard leaderboard; @@ -65,7 +65,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate InternalChildren = new[] { - (Drawable)(syncManager = new MultiplayerSyncManager(masterClockContainer)), + (Drawable)(syncManager = new MultiplayerCatchupSyncManager(masterClockContainer)), masterClockContainer }; @@ -101,7 +101,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate leaderboard.RemoveClock(existingInstance.User.Id); } - LoadComponentAsync(instances[userIndex] = new PlayerInstance(gameplayState.Score, new MultiplayerSlaveClock(masterClockContainer.GameplayClock)), d => + LoadComponentAsync(instances[userIndex] = new PlayerInstance(gameplayState.Score, new MultiplayerSpectatorSlaveClock(masterClockContainer.GameplayClock)), d => { if (instances[userIndex] == d) { diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectatorPlayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectatorPlayer.cs index c82c407c2c..7fe1416224 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectatorPlayer.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectatorPlayer.cs @@ -11,9 +11,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate { public class MultiplayerSpectatorPlayer : SpectatorPlayer { - private readonly MultiplayerSlaveClock gameplayClock; + private readonly MultiplayerSpectatorSlaveClock gameplayClock; - public MultiplayerSpectatorPlayer(Score score, MultiplayerSlaveClock gameplayClock) + public MultiplayerSpectatorPlayer(Score score, MultiplayerSpectatorSlaveClock gameplayClock) : base(score) { this.gameplayClock = gameplayClock; diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSlaveClock.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectatorSlaveClock.cs similarity index 91% rename from osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSlaveClock.cs rename to osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectatorSlaveClock.cs index 84a87271a2..6960d24295 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSlaveClock.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectatorSlaveClock.cs @@ -7,7 +7,7 @@ using osu.Framework.Timing; namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate { - public class MultiplayerSlaveClock : IFrameBasedClock, IMultiplayerSlaveClock + public class MultiplayerSpectatorSlaveClock : IFrameBasedClock, IMultiplayerSpectatorSlaveClock { /// /// The catchup rate. @@ -16,7 +16,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate private readonly IFrameBasedClock masterClock; - public MultiplayerSlaveClock(IFrameBasedClock masterClock) + public MultiplayerSpectatorSlaveClock(IFrameBasedClock masterClock) { this.masterClock = masterClock; } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerInstance.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerInstance.cs index 631ac2d52e..384c1f1f2b 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerInstance.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerInstance.cs @@ -20,11 +20,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate public WorkingBeatmap Beatmap { get; private set; } public readonly Score Score; - public readonly MultiplayerSlaveClock GameplayClock; + public readonly MultiplayerSpectatorSlaveClock GameplayClock; private OsuScreenStack stack; - public PlayerInstance(Score score, MultiplayerSlaveClock gameplayClock) + public PlayerInstance(Score score, MultiplayerSpectatorSlaveClock gameplayClock) { Score = score; GameplayClock = gameplayClock; From 82fcabb8f0d94b083b25306d1647c1925c598f5c Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 15 Apr 2021 19:32:55 +0900 Subject: [PATCH 030/205] More refactorings/renamespacings/xmldocs --- ...s => MultiplayerCatchUpSyncManagerTest.cs} | 48 +++++++++++-------- .../IMultiplayerSpectatorSlaveClock.cs | 15 ------ .../IMultiplayerSpectatorSyncManager.cs | 16 ------- .../Spectate/MultiplayerSpectator.cs | 7 +-- .../Spectate/MultiplayerSpectatorPlayer.cs | 5 +- .../Multiplayer/Spectate/PlayerInstance.cs | 5 +- .../Spectate/Sync/ISpectatorSlaveClock.cs | 24 ++++++++++ .../Spectate/Sync/ISpectatorSyncManager.cs | 30 ++++++++++++ .../SpectatorCatchUpSlaveClock.cs} | 6 +-- .../SpectatorCatchUpSyncManager.cs} | 15 +++--- 10 files changed, 105 insertions(+), 66 deletions(-) rename osu.Game.Tests/OnlinePlay/{MultiplayerCatchupSyncManagerTest.cs => MultiplayerCatchUpSyncManagerTest.cs} (76%) delete mode 100644 osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/IMultiplayerSpectatorSlaveClock.cs delete mode 100644 osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/IMultiplayerSpectatorSyncManager.cs create mode 100644 osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/Sync/ISpectatorSlaveClock.cs create mode 100644 osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/Sync/ISpectatorSyncManager.cs rename osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/{MultiplayerSpectatorSlaveClock.cs => Sync/SpectatorCatchUpSlaveClock.cs} (89%) rename osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/{MultiplayerCatchupSyncManager.cs => Sync/SpectatorCatchUpSyncManager.cs} (88%) diff --git a/osu.Game.Tests/OnlinePlay/MultiplayerCatchupSyncManagerTest.cs b/osu.Game.Tests/OnlinePlay/MultiplayerCatchUpSyncManagerTest.cs similarity index 76% rename from osu.Game.Tests/OnlinePlay/MultiplayerCatchupSyncManagerTest.cs rename to osu.Game.Tests/OnlinePlay/MultiplayerCatchUpSyncManagerTest.cs index f15fb5cfd1..6aa1219d53 100644 --- a/osu.Game.Tests/OnlinePlay/MultiplayerCatchupSyncManagerTest.cs +++ b/osu.Game.Tests/OnlinePlay/MultiplayerCatchUpSyncManagerTest.cs @@ -6,16 +6,16 @@ using NUnit.Framework; using osu.Framework.Bindables; using osu.Framework.Testing; using osu.Framework.Timing; -using osu.Game.Screens.OnlinePlay.Multiplayer.Spectate; +using osu.Game.Screens.OnlinePlay.Multiplayer.Spectate.Sync; using osu.Game.Tests.Visual; namespace osu.Game.Tests.OnlinePlay { [HeadlessTest] - public class MultiplayerCatchupSyncManagerTest : OsuTestScene + public class MultiplayerCatchUpSyncManagerTest : OsuTestScene { private TestManualClock master; - private MultiplayerCatchupSyncManager catchupSyncManager; + private SpectatorCatchUpSyncManager syncManager; private TestSpectatorSlaveClock slave1; private TestSpectatorSlaveClock slave2; @@ -23,11 +23,11 @@ namespace osu.Game.Tests.OnlinePlay [SetUp] public void Setup() { - catchupSyncManager = new MultiplayerCatchupSyncManager(master = new TestManualClock()); - catchupSyncManager.AddSlave(slave1 = new TestSpectatorSlaveClock(1)); - catchupSyncManager.AddSlave(slave2 = new TestSpectatorSlaveClock(2)); + syncManager = new SpectatorCatchUpSyncManager(master = new TestManualClock()); + syncManager.AddSlave(slave1 = new TestSpectatorSlaveClock(1)); + syncManager.AddSlave(slave2 = new TestSpectatorSlaveClock(2)); - Schedule(() => Child = catchupSyncManager); + Schedule(() => Child = syncManager); } [Test] @@ -47,7 +47,7 @@ namespace osu.Game.Tests.OnlinePlay [Test] public void TestMasterClockDoesNotStartWhenNoneReadyForMaximumDelayTime() { - AddWaitStep($"wait {MultiplayerCatchupSyncManager.MAXIMUM_START_DELAY} milliseconds", (int)Math.Ceiling(MultiplayerCatchupSyncManager.MAXIMUM_START_DELAY / TimePerAction)); + AddWaitStep($"wait {SpectatorCatchUpSyncManager.MAXIMUM_START_DELAY} milliseconds", (int)Math.Ceiling(SpectatorCatchUpSyncManager.MAXIMUM_START_DELAY / TimePerAction)); assertMasterState(false); } @@ -55,7 +55,7 @@ namespace osu.Game.Tests.OnlinePlay public void TestMasterClockStartsWhenAnyReadyForMaximumDelayTime() { setWaiting(() => slave1, false); - AddWaitStep($"wait {MultiplayerCatchupSyncManager.MAXIMUM_START_DELAY} milliseconds", (int)Math.Ceiling(MultiplayerCatchupSyncManager.MAXIMUM_START_DELAY / TimePerAction)); + AddWaitStep($"wait {SpectatorCatchUpSyncManager.MAXIMUM_START_DELAY} milliseconds", (int)Math.Ceiling(SpectatorCatchUpSyncManager.MAXIMUM_START_DELAY / TimePerAction)); assertMasterState(true); } @@ -64,7 +64,7 @@ namespace osu.Game.Tests.OnlinePlay { setAllWaiting(false); - setMasterTime(MultiplayerCatchupSyncManager.SYNC_TARGET + 1); + setMasterTime(SpectatorCatchUpSyncManager.SYNC_TARGET + 1); assertCatchingUp(() => slave1, false); } @@ -73,7 +73,7 @@ namespace osu.Game.Tests.OnlinePlay { setAllWaiting(false); - setMasterTime(MultiplayerCatchupSyncManager.MAX_SYNC_OFFSET + 1); + setMasterTime(SpectatorCatchUpSyncManager.MAX_SYNC_OFFSET + 1); assertCatchingUp(() => slave1, true); assertCatchingUp(() => slave2, true); } @@ -83,8 +83,8 @@ namespace osu.Game.Tests.OnlinePlay { setAllWaiting(false); - setMasterTime(MultiplayerCatchupSyncManager.MAX_SYNC_OFFSET + 1); - setSlaveTime(() => slave1, MultiplayerCatchupSyncManager.SYNC_TARGET + 1); + setMasterTime(SpectatorCatchUpSyncManager.MAX_SYNC_OFFSET + 1); + setSlaveTime(() => slave1, SpectatorCatchUpSyncManager.SYNC_TARGET + 1); assertCatchingUp(() => slave1, true); } @@ -93,8 +93,8 @@ namespace osu.Game.Tests.OnlinePlay { setAllWaiting(false); - setMasterTime(MultiplayerCatchupSyncManager.MAX_SYNC_OFFSET + 2); - setSlaveTime(() => slave1, MultiplayerCatchupSyncManager.SYNC_TARGET); + setMasterTime(SpectatorCatchUpSyncManager.MAX_SYNC_OFFSET + 2); + setSlaveTime(() => slave1, SpectatorCatchUpSyncManager.SYNC_TARGET); assertCatchingUp(() => slave1, false); assertCatchingUp(() => slave2, true); } @@ -104,7 +104,7 @@ namespace osu.Game.Tests.OnlinePlay { setAllWaiting(false); - setSlaveTime(() => slave1, -MultiplayerCatchupSyncManager.SYNC_TARGET); + setSlaveTime(() => slave1, -SpectatorCatchUpSyncManager.SYNC_TARGET); assertCatchingUp(() => slave1, false); assertSlaveState(() => slave1, true); } @@ -114,7 +114,7 @@ namespace osu.Game.Tests.OnlinePlay { setAllWaiting(false); - setSlaveTime(() => slave1, -MultiplayerCatchupSyncManager.SYNC_TARGET - 1); + setSlaveTime(() => slave1, -SpectatorCatchUpSyncManager.SYNC_TARGET - 1); // This is a silent catchup, where IsCatchingUp = false but IsRunning = false also. assertCatchingUp(() => slave1, false); @@ -162,10 +162,10 @@ namespace osu.Game.Tests.OnlinePlay private void assertSlaveState(Func slave, bool running) => AddAssert($"slave {slave().Id} {(running ? "is" : "is not")} running", () => slave().IsRunning == running); - private class TestSpectatorSlaveClock : TestManualClock, IMultiplayerSpectatorSlaveClock + private class TestSpectatorSlaveClock : TestManualClock, ISpectatorSlaveClock { public readonly Bindable WaitingOnFrames = new Bindable(true); - IBindable IMultiplayerSpectatorSlaveClock.WaitingOnFrames => WaitingOnFrames; + IBindable ISpectatorSlaveClock.WaitingOnFrames => WaitingOnFrames; public bool IsCatchingUp { get; set; } @@ -183,6 +183,16 @@ namespace osu.Game.Tests.OnlinePlay Start(); }); } + + public void ProcessFrame() + { + } + + public double ElapsedFrameTime => 0; + + public double FramesPerSecond => 0; + + public FrameTimeInfo TimeInfo => default; } private class TestManualClock : ManualClock, IAdjustableClock diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/IMultiplayerSpectatorSlaveClock.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/IMultiplayerSpectatorSlaveClock.cs deleted file mode 100644 index fe2c2dfec9..0000000000 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/IMultiplayerSpectatorSlaveClock.cs +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Framework.Bindables; -using osu.Framework.Timing; - -namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate -{ - public interface IMultiplayerSpectatorSlaveClock : IAdjustableClock - { - IBindable WaitingOnFrames { get; } - - bool IsCatchingUp { get; set; } - } -} diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/IMultiplayerSpectatorSyncManager.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/IMultiplayerSpectatorSyncManager.cs deleted file mode 100644 index 5643bde68f..0000000000 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/IMultiplayerSpectatorSyncManager.cs +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Framework.Timing; - -namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate -{ - public interface IMultiplayerSpectatorSyncManager - { - IAdjustableClock Master { get; } - - void AddSlave(IMultiplayerSpectatorSlaveClock clock); - - void RemoveSlave(IMultiplayerSpectatorSlaveClock clock); - } -} diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectator.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectator.cs index 3dade4d32c..441839b401 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectator.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectator.cs @@ -7,6 +7,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Online.Spectator; +using osu.Game.Screens.OnlinePlay.Multiplayer.Spectate.Sync; using osu.Game.Screens.Play; using osu.Game.Screens.Spectate; @@ -24,7 +25,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate private readonly PlayerInstance[] instances; private MasterGameplayClockContainer masterClockContainer; - private IMultiplayerSpectatorSyncManager syncManager; + private ISpectatorSyncManager syncManager; private PlayerGrid grid; private MultiplayerSpectatorLeaderboard leaderboard; @@ -65,7 +66,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate InternalChildren = new[] { - (Drawable)(syncManager = new MultiplayerCatchupSyncManager(masterClockContainer)), + (Drawable)(syncManager = new SpectatorCatchUpSyncManager(masterClockContainer)), masterClockContainer }; @@ -101,7 +102,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate leaderboard.RemoveClock(existingInstance.User.Id); } - LoadComponentAsync(instances[userIndex] = new PlayerInstance(gameplayState.Score, new MultiplayerSpectatorSlaveClock(masterClockContainer.GameplayClock)), d => + LoadComponentAsync(instances[userIndex] = new PlayerInstance(gameplayState.Score, new SpectatorCatchUpSlaveClock(masterClockContainer.GameplayClock)), d => { if (instances[userIndex] == d) { diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectatorPlayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectatorPlayer.cs index 7fe1416224..1a7e0dd36a 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectatorPlayer.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectatorPlayer.cs @@ -5,15 +5,16 @@ using osu.Framework.Bindables; using osu.Framework.Timing; using osu.Game.Beatmaps; using osu.Game.Scoring; +using osu.Game.Screens.OnlinePlay.Multiplayer.Spectate.Sync; using osu.Game.Screens.Play; namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate { public class MultiplayerSpectatorPlayer : SpectatorPlayer { - private readonly MultiplayerSpectatorSlaveClock gameplayClock; + private readonly SpectatorCatchUpSlaveClock gameplayClock; - public MultiplayerSpectatorPlayer(Score score, MultiplayerSpectatorSlaveClock gameplayClock) + public MultiplayerSpectatorPlayer(Score score, SpectatorCatchUpSlaveClock gameplayClock) : base(score) { this.gameplayClock = gameplayClock; diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerInstance.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerInstance.cs index 384c1f1f2b..401732f7fb 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerInstance.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerInstance.cs @@ -6,6 +6,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Beatmaps; using osu.Game.Scoring; +using osu.Game.Screens.OnlinePlay.Multiplayer.Spectate.Sync; using osu.Game.Screens.Play; using osu.Game.Users; @@ -20,11 +21,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate public WorkingBeatmap Beatmap { get; private set; } public readonly Score Score; - public readonly MultiplayerSpectatorSlaveClock GameplayClock; + public readonly SpectatorCatchUpSlaveClock GameplayClock; private OsuScreenStack stack; - public PlayerInstance(Score score, MultiplayerSpectatorSlaveClock gameplayClock) + public PlayerInstance(Score score, SpectatorCatchUpSlaveClock gameplayClock) { Score = score; GameplayClock = gameplayClock; diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/Sync/ISpectatorSlaveClock.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/Sync/ISpectatorSlaveClock.cs new file mode 100644 index 0000000000..d8733d4322 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/Sync/ISpectatorSlaveClock.cs @@ -0,0 +1,24 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Bindables; +using osu.Framework.Timing; + +namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate.Sync +{ + /// + /// A clock which is used by s and managed by an . + /// + public interface ISpectatorSlaveClock : IFrameBasedClock, IAdjustableClock + { + /// + /// Whether this clock is waiting on frames to continue playback. + /// + IBindable WaitingOnFrames { get; } + + /// + /// Whether this clock is resynchronising to the master clock. + /// + bool IsCatchingUp { get; set; } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/Sync/ISpectatorSyncManager.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/Sync/ISpectatorSyncManager.cs new file mode 100644 index 0000000000..107f9637a3 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/Sync/ISpectatorSyncManager.cs @@ -0,0 +1,30 @@ +// 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.Timing; + +namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate.Sync +{ + /// + /// Manages the synchronisation between one or more slave clocks in relation to a master clock. + /// + public interface ISpectatorSyncManager + { + /// + /// The master clock which slaves should synchronise to. + /// + IAdjustableClock Master { get; } + + /// + /// Adds a slave clock. + /// + /// The clock to add. + void AddSlave(ISpectatorSlaveClock clock); + + /// + /// Removes a slave clock. + /// + /// The clock to remove. + void RemoveSlave(ISpectatorSlaveClock clock); + } +} diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectatorSlaveClock.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/Sync/SpectatorCatchUpSlaveClock.cs similarity index 89% rename from osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectatorSlaveClock.cs rename to osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/Sync/SpectatorCatchUpSlaveClock.cs index 6960d24295..e4d25894c8 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectatorSlaveClock.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/Sync/SpectatorCatchUpSlaveClock.cs @@ -5,9 +5,9 @@ using System; using osu.Framework.Bindables; using osu.Framework.Timing; -namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate +namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate.Sync { - public class MultiplayerSpectatorSlaveClock : IFrameBasedClock, IMultiplayerSpectatorSlaveClock + public class SpectatorCatchUpSlaveClock : ISpectatorSlaveClock { /// /// The catchup rate. @@ -16,7 +16,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate private readonly IFrameBasedClock masterClock; - public MultiplayerSpectatorSlaveClock(IFrameBasedClock masterClock) + public SpectatorCatchUpSlaveClock(IFrameBasedClock masterClock) { this.masterClock = masterClock; } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerCatchupSyncManager.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/Sync/SpectatorCatchUpSyncManager.cs similarity index 88% rename from osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerCatchupSyncManager.cs rename to osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/Sync/SpectatorCatchUpSyncManager.cs index 873f34626d..46e86177ca 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerCatchupSyncManager.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/Sync/SpectatorCatchUpSyncManager.cs @@ -7,9 +7,12 @@ using osu.Framework.Graphics; using osu.Framework.Logging; using osu.Framework.Timing; -namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate +namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate.Sync { - public class MultiplayerCatchupSyncManager : Component, IMultiplayerSpectatorSyncManager + /// + /// A which synchronises de-synced slave clocks through catchup. + /// + public class SpectatorCatchUpSyncManager : Component, ISpectatorSyncManager { /// /// The offset from the master clock to which slaves should be synchronised to. @@ -34,19 +37,19 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate /// /// The slave clocks. /// - private readonly List slaves = new List(); + private readonly List slaves = new List(); private bool hasStarted; private double? firstStartAttemptTime; - public MultiplayerCatchupSyncManager(IAdjustableClock master) + public SpectatorCatchUpSyncManager(IAdjustableClock master) { Master = master; } - public void AddSlave(IMultiplayerSpectatorSlaveClock clock) => slaves.Add(clock); + public void AddSlave(ISpectatorSlaveClock clock) => slaves.Add(clock); - public void RemoveSlave(IMultiplayerSpectatorSlaveClock clock) => slaves.Remove(clock); + public void RemoveSlave(ISpectatorSlaveClock clock) => slaves.Remove(clock); protected override void Update() { From 33cc5c5cb36456479bacf67ccdb4e585ce8e7ca5 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 15 Apr 2021 19:35:57 +0900 Subject: [PATCH 031/205] A few more xmldocs --- .../Multiplayer/Spectate/Sync/ISpectatorSyncManager.cs | 10 +++++----- .../Spectate/Sync/SpectatorCatchUpSlaveClock.cs | 5 ++++- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/Sync/ISpectatorSyncManager.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/Sync/ISpectatorSyncManager.cs index 107f9637a3..5edbb3bc2c 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/Sync/ISpectatorSyncManager.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/Sync/ISpectatorSyncManager.cs @@ -6,7 +6,7 @@ using osu.Framework.Timing; namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate.Sync { /// - /// Manages the synchronisation between one or more slave clocks in relation to a master clock. + /// Manages the synchronisation between one or more s in relation to a master clock. /// public interface ISpectatorSyncManager { @@ -16,15 +16,15 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate.Sync IAdjustableClock Master { get; } /// - /// Adds a slave clock. + /// Adds an to manage. /// - /// The clock to add. + /// The to add. void AddSlave(ISpectatorSlaveClock clock); /// - /// Removes a slave clock. + /// Removes an , stopping it from being managed by this . /// - /// The clock to remove. + /// The to remove. void RemoveSlave(ISpectatorSlaveClock clock); } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/Sync/SpectatorCatchUpSlaveClock.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/Sync/SpectatorCatchUpSlaveClock.cs index e4d25894c8..4b529021de 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/Sync/SpectatorCatchUpSlaveClock.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/Sync/SpectatorCatchUpSlaveClock.cs @@ -7,10 +7,13 @@ using osu.Framework.Timing; namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate.Sync { + /// + /// A which catches up using rate adjustment. + /// public class SpectatorCatchUpSlaveClock : ISpectatorSlaveClock { /// - /// The catchup rate. + /// The catch up rate. /// public const double CATCHUP_RATE = 2; From b391a8f94e23b66a82825fc180a8fd10db0ffba3 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 15 Apr 2021 19:37:45 +0900 Subject: [PATCH 032/205] Properly bind WaitingOnFrames --- .../Spectate/MultiplayerSpectatorPlayer.cs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectatorPlayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectatorPlayer.cs index 1a7e0dd36a..19cc9f18ad 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectatorPlayer.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectatorPlayer.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Timing; using osu.Game.Beatmaps; @@ -12,14 +13,20 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate { public class MultiplayerSpectatorPlayer : SpectatorPlayer { - private readonly SpectatorCatchUpSlaveClock gameplayClock; + private readonly ISpectatorSlaveClock gameplayClock; - public MultiplayerSpectatorPlayer(Score score, SpectatorCatchUpSlaveClock gameplayClock) + public MultiplayerSpectatorPlayer(Score score, ISpectatorSlaveClock gameplayClock) : base(score) { this.gameplayClock = gameplayClock; } + [BackgroundDependencyLoader] + private void load() + { + gameplayClock.WaitingOnFrames.BindTo(DrawableRuleset.FrameStableClock.WaitingOnFrames); + } + protected override GameplayClockContainer CreateGameplayClockContainer(WorkingBeatmap beatmap, double gameplayStart) => new SubGameplayClockContainer(gameplayClock); } From 5ac0eb02cd0bffd1c7876178c5b8ad50bad3b165 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 16 Apr 2021 12:25:29 +0900 Subject: [PATCH 033/205] Always add player instances at first, populate later --- .../TestSceneMultiplayerSpectator.cs | 46 ++++++++++++++----- .../Spectate/MultiplayerSpectator.cs | 27 ++++------- .../Multiplayer/Spectate/PlayerInstance.cs | 23 +++++----- 3 files changed, 56 insertions(+), 40 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectator.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectator.cs index ab6324df2d..4026258830 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectator.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectator.cs @@ -26,7 +26,7 @@ namespace osu.Game.Tests.Visual.Multiplayer public class TestSceneMultiplayerSpectator : MultiplayerTestScene { [Cached(typeof(SpectatorStreamingClient))] - private TestSpectatorStreamingClient testSpectatorStreamingClient = new TestSpectatorStreamingClient(); + private TestSpectatorStreamingClient streamingClient = new TestSpectatorStreamingClient(); [Cached(typeof(UserLookupCache))] private UserLookupCache lookupCache = new TestUserLookupCache(); @@ -62,18 +62,42 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("add streaming client", () => { - Remove(testSpectatorStreamingClient); - Add(testSpectatorStreamingClient); + Remove(streamingClient); + Add(streamingClient); }); AddStep("finish previous gameplay", () => { foreach (var id in playingUserIds) - testSpectatorStreamingClient.EndPlay(id, importedBeatmapId); + streamingClient.EndPlay(id, importedBeatmapId); playingUserIds.Clear(); }); } + [Test] + public void TestDelayedStart() + { + AddStep("start players silently", () => + { + Client.CurrentMatchPlayingUserIds.Add(55); + Client.CurrentMatchPlayingUserIds.Add(56); + playingUserIds.Add(55); + playingUserIds.Add(56); + nextFrame[55] = 0; + nextFrame[56] = 0; + }); + + loadSpectateScreen(false); + + AddWaitStep("wait a bit", 10); + AddStep("load player 55", () => streamingClient.StartPlay(55, importedBeatmapId)); + AddUntilStep("one player added", () => spectator.ChildrenOfType().Count() == 1); + + AddWaitStep("wait a bit", 10); + AddStep("load player 56", () => streamingClient.StartPlay(56, importedBeatmapId)); + AddUntilStep("two players added", () => spectator.ChildrenOfType().Count() == 2); + } + [Test] public void TestGeneral() { @@ -178,7 +202,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddWaitStep("wait a bit", 10); } - private void loadSpectateScreen() + private void loadSpectateScreen(bool waitForPlayerLoad = true) { AddStep("load screen", () => { @@ -188,7 +212,7 @@ namespace osu.Game.Tests.Visual.Multiplayer LoadScreen(spectator = new MultiplayerSpectator(playingUserIds.ToArray())); }); - AddUntilStep("wait for screen load", () => spectator.LoadState == LoadState.Loaded && spectator.AllPlayersLoaded); + AddUntilStep("wait for screen load", () => spectator.LoadState == LoadState.Loaded && (!waitForPlayerLoad || spectator.AllPlayersLoaded)); } private void start(int userId, int? beatmapId = null) => start(new[] { userId }, beatmapId); @@ -200,7 +224,7 @@ namespace osu.Game.Tests.Visual.Multiplayer foreach (int id in userIds) { Client.CurrentMatchPlayingUserIds.Add(id); - testSpectatorStreamingClient.StartPlay(id, beatmapId ?? importedBeatmapId); + streamingClient.StartPlay(id, beatmapId ?? importedBeatmapId); playingUserIds.Add(id); nextFrame[id] = 0; } @@ -211,7 +235,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("end play", () => { - testSpectatorStreamingClient.EndPlay(userId, beatmapId ?? importedBeatmapId); + streamingClient.EndPlay(userId, beatmapId ?? importedBeatmapId); playingUserIds.Remove(userId); nextFrame.Remove(userId); }); @@ -225,7 +249,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { foreach (int id in userIds) { - testSpectatorStreamingClient.SendFrames(id, nextFrame[id], count); + streamingClient.SendFrames(id, nextFrame[id], count); nextFrame[id] += count; } }); @@ -246,7 +270,7 @@ namespace osu.Game.Tests.Visual.Multiplayer private Player getPlayer(int userId) => getInstance(userId).ChildrenOfType().Single(); - private PlayerInstance getInstance(int userId) => spectator.ChildrenOfType().Single(p => p.User.Id == userId); + private PlayerInstance getInstance(int userId) => spectator.ChildrenOfType().Single(p => p.UserId == userId); public class TestSpectatorStreamingClient : SpectatorStreamingClient { @@ -297,7 +321,7 @@ namespace osu.Game.Tests.Visual.Multiplayer public override void WatchUser(int userId) { - if (userSentStateDictionary[userId]) + if (userSentStateDictionary.TryGetValue(userId, out var sent) && sent) { // usually the server would do this. sendState(userId, userBeatmapDictionary[userId]); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectator.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectator.cs index 441839b401..08b9938e79 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectator.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectator.cs @@ -32,7 +32,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate public MultiplayerSpectator(int[] userIds) : base(userIds.AsSpan().Slice(0, Math.Min(16, userIds.Length)).ToArray()) { - instances = new PlayerInstance[userIds.Length]; + instances = new PlayerInstance[UserIds.Length]; } [BackgroundDependencyLoader] @@ -70,6 +70,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate masterClockContainer }; + for (int i = 0; i < UserIds.Length; i++) + grid.Add(instances[i] = new PlayerInstance(UserIds[i], new SpectatorCatchUpSlaveClock(masterClockContainer.GameplayClock))); + // Todo: This is not quite correct - it should be per-user to adjust for other mod combinations. var playableBeatmap = Beatmap.Value.GetPlayableBeatmap(Ruleset.Value); var scoreProcessor = Ruleset.Value.CreateInstance().CreateScoreProcessor(); @@ -93,24 +96,14 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate protected override void StartGameplay(int userId, GameplayState gameplayState) { int userIndex = getIndexForUser(userId); - var existingInstance = instances[userIndex]; - if (existingInstance != null) - { - grid.Remove(existingInstance); - syncManager.RemoveSlave(existingInstance.GameplayClock); - leaderboard.RemoveClock(existingInstance.User.Id); - } + var instance = instances[userIndex]; + syncManager.RemoveSlave(instance.GameplayClock); + leaderboard.RemoveClock(instance.UserId); - LoadComponentAsync(instances[userIndex] = new PlayerInstance(gameplayState.Score, new SpectatorCatchUpSlaveClock(masterClockContainer.GameplayClock)), d => - { - if (instances[userIndex] == d) - { - grid.Add(d); - syncManager.AddSlave(d.GameplayClock); - leaderboard.AddClock(d.User.Id, d.GameplayClock); - } - }); + instance.LoadPlayer(gameplayState.Score); + syncManager.AddSlave(instance.GameplayClock); + leaderboard.AddClock(instance.UserId, instance.GameplayClock); } protected override void EndGameplay(int userId) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerInstance.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerInstance.cs index 401732f7fb..8c54fc9ec2 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerInstance.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerInstance.cs @@ -8,7 +8,6 @@ using osu.Game.Beatmaps; using osu.Game.Scoring; using osu.Game.Screens.OnlinePlay.Multiplayer.Spectate.Sync; using osu.Game.Screens.Play; -using osu.Game.Users; namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate { @@ -16,30 +15,30 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate { public bool PlayerLoaded => stack?.CurrentScreen is Player; - public User User => Score.ScoreInfo.User; - - public WorkingBeatmap Beatmap { get; private set; } - - public readonly Score Score; + public readonly int UserId; public readonly SpectatorCatchUpSlaveClock GameplayClock; + public Score Score { get; private set; } + + [Resolved] + private BeatmapManager beatmapManager { get; set; } + private OsuScreenStack stack; - public PlayerInstance(Score score, SpectatorCatchUpSlaveClock gameplayClock) + public PlayerInstance(int userId, SpectatorCatchUpSlaveClock gameplayClock) { - Score = score; + UserId = userId; GameplayClock = gameplayClock; RelativeSizeAxes = Axes.Both; Masking = true; } - [BackgroundDependencyLoader] - private void load(BeatmapManager beatmapManager) + public void LoadPlayer(Score score) { - Beatmap = beatmapManager.GetWorkingBeatmap(Score.ScoreInfo.Beatmap, bypassCache: true); + Score = score; - InternalChild = new GameplayIsolationContainer(Beatmap, Score.ScoreInfo.Ruleset, Score.ScoreInfo.Mods) + InternalChild = new GameplayIsolationContainer(beatmapManager.GetWorkingBeatmap(Score.ScoreInfo.Beatmap, bypassCache: true), Score.ScoreInfo.Ruleset, Score.ScoreInfo.Mods) { RelativeSizeAxes = Axes.Both, Child = new DrawSizePreservingFillContainer From 1c086d99deae8c03b1412b13ec6f5c00e0abfe94 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 16 Apr 2021 13:28:32 +0900 Subject: [PATCH 034/205] Add loading spinner --- .../Spectate/MultiplayerSpectator.cs | 8 ++----- .../Multiplayer/Spectate/PlayerInstance.cs | 24 +++++++++++++------ 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectator.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectator.cs index 08b9938e79..e1af95b870 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectator.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectator.cs @@ -95,13 +95,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate protected override void StartGameplay(int userId, GameplayState gameplayState) { - int userIndex = getIndexForUser(userId); + var instance = instances[getIndexForUser(userId)]; + instance.LoadScore(gameplayState.Score); - var instance = instances[userIndex]; - syncManager.RemoveSlave(instance.GameplayClock); - leaderboard.RemoveClock(instance.UserId); - - instance.LoadPlayer(gameplayState.Score); syncManager.AddSlave(instance.GameplayClock); leaderboard.AddClock(instance.UserId, instance.GameplayClock); } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerInstance.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerInstance.cs index 8c54fc9ec2..31d2d458df 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerInstance.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerInstance.cs @@ -1,10 +1,12 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Beatmaps; +using osu.Game.Graphics.UserInterface; using osu.Game.Scoring; using osu.Game.Screens.OnlinePlay.Multiplayer.Spectate.Sync; using osu.Game.Screens.Play; @@ -23,6 +25,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate [Resolved] private BeatmapManager beatmapManager { get; set; } + private readonly Container content; + private readonly LoadingLayer loadingLayer; private OsuScreenStack stack; public PlayerInstance(int userId, SpectatorCatchUpSlaveClock gameplayClock) @@ -32,23 +36,29 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate RelativeSizeAxes = Axes.Both; Masking = true; + + InternalChildren = new Drawable[] + { + content = new DrawSizePreservingFillContainer { RelativeSizeAxes = Axes.Both }, + loadingLayer = new LoadingLayer(true) { State = { Value = Visibility.Visible } } + }; } - public void LoadPlayer(Score score) + public void LoadScore(Score score) { + if (Score != null) + throw new InvalidOperationException($"Cannot load a new score on a {nameof(PlayerInstance)} with an existing score."); + Score = score; - InternalChild = new GameplayIsolationContainer(beatmapManager.GetWorkingBeatmap(Score.ScoreInfo.Beatmap, bypassCache: true), Score.ScoreInfo.Ruleset, Score.ScoreInfo.Mods) + content.Child = new GameplayIsolationContainer(beatmapManager.GetWorkingBeatmap(Score.ScoreInfo.Beatmap, bypassCache: true), Score.ScoreInfo.Ruleset, Score.ScoreInfo.Mods) { RelativeSizeAxes = Axes.Both, - Child = new DrawSizePreservingFillContainer - { - RelativeSizeAxes = Axes.Both, - Child = stack = new OsuScreenStack() - } + Child = stack = new OsuScreenStack() }; stack.Push(new MultiplayerSpectatorPlayerLoader(Score, () => new MultiplayerSpectatorPlayer(Score, GameplayClock))); + loadingLayer.Hide(); } // Player interferes with global input, so disable input for now. From f98ffbb1b365294b424ee28938072ba59b9cf1e2 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 16 Apr 2021 20:15:42 +0900 Subject: [PATCH 035/205] Remove ClockToProcess, always process underlying clock --- osu.Game/Screens/Play/GameplayClock.cs | 18 +++++++++--------- .../Screens/Play/GameplayClockContainer.cs | 4 +--- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/osu.Game/Screens/Play/GameplayClock.cs b/osu.Game/Screens/Play/GameplayClock.cs index db4b5d300b..54aa395f5f 100644 --- a/osu.Game/Screens/Play/GameplayClock.cs +++ b/osu.Game/Screens/Play/GameplayClock.cs @@ -19,7 +19,7 @@ namespace osu.Game.Screens.Play /// public class GameplayClock : IFrameBasedClock { - private readonly IFrameBasedClock underlyingClock; + internal readonly IFrameBasedClock UnderlyingClock; public readonly BindableBool IsPaused = new BindableBool(); @@ -30,12 +30,12 @@ namespace osu.Game.Screens.Play public GameplayClock(IFrameBasedClock underlyingClock) { - this.underlyingClock = underlyingClock; + UnderlyingClock = underlyingClock; } - public double CurrentTime => underlyingClock.CurrentTime; + public double CurrentTime => UnderlyingClock.CurrentTime; - public double Rate => underlyingClock.Rate; + public double Rate => UnderlyingClock.Rate; /// /// The rate of gameplay when playback is at 100%. @@ -59,19 +59,19 @@ namespace osu.Game.Screens.Play } } - public bool IsRunning => underlyingClock.IsRunning; + public bool IsRunning => UnderlyingClock.IsRunning; public void ProcessFrame() { // intentionally not updating the underlying clock (handled externally). } - public double ElapsedFrameTime => underlyingClock.ElapsedFrameTime; + public double ElapsedFrameTime => UnderlyingClock.ElapsedFrameTime; - public double FramesPerSecond => underlyingClock.FramesPerSecond; + public double FramesPerSecond => UnderlyingClock.FramesPerSecond; - public FrameTimeInfo TimeInfo => underlyingClock.TimeInfo; + public FrameTimeInfo TimeInfo => UnderlyingClock.TimeInfo; - public IClock Source => underlyingClock; + public IClock Source => UnderlyingClock; } } diff --git a/osu.Game/Screens/Play/GameplayClockContainer.cs b/osu.Game/Screens/Play/GameplayClockContainer.cs index 6d863f0094..b7dc55277f 100644 --- a/osu.Game/Screens/Play/GameplayClockContainer.cs +++ b/osu.Game/Screens/Play/GameplayClockContainer.cs @@ -80,15 +80,13 @@ namespace osu.Game.Screens.Play protected override void Update() { if (!IsPaused.Value) - ClockToProcess.ProcessFrame(); + GameplayClock.UnderlyingClock.ProcessFrame(); base.Update(); } protected abstract void OnIsPausedChanged(ValueChangedEvent isPaused); - protected virtual IFrameBasedClock ClockToProcess => AdjustableClock; - protected abstract GameplayClock CreateGameplayClock(IFrameBasedClock source); } } From 7d5d7088cd4dd1be6a0c1ec5f7a8113a2ab68aa4 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 16 Apr 2021 20:51:07 +0900 Subject: [PATCH 036/205] Remove now unnecessary override --- .../Spectate/MultiplayerSpectatorPlayer.cs | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectatorPlayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectatorPlayer.cs index 19cc9f18ad..d4ff65cd01 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectatorPlayer.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectatorPlayer.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Framework.Timing; using osu.Game.Beatmaps; using osu.Game.Scoring; @@ -33,21 +32,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate public class SubGameplayClockContainer : GameplayClockContainer { - public new DecoupleableInterpolatingFramedClock AdjustableClock => base.AdjustableClock; - public SubGameplayClockContainer(IClock sourceClock) : base(sourceClock) { } - protected override void OnIsPausedChanged(ValueChangedEvent isPaused) - { - if (isPaused.NewValue) - AdjustableClock.Stop(); - else - AdjustableClock.Start(); - } - protected override GameplayClock CreateGameplayClock(IFrameBasedClock source) => new GameplayClock(source); } } From 4c5d4752b13807081be44f17a736822f6f57e3dc Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 16 Apr 2021 22:47:52 +0900 Subject: [PATCH 037/205] Rename classes to reduce redundant naming --- ...rTest.cs => TestCaseCatchUpSyncManager.cs} | 48 +++++++++---------- .../Spectate/MultiplayerSpectator.cs | 6 +-- .../Spectate/MultiplayerSpectatorPlayer.cs | 4 +- .../Multiplayer/Spectate/PlayerInstance.cs | 4 +- ...chUpSlaveClock.cs => CatchUpSlaveClock.cs} | 6 +-- ...UpSyncManager.cs => CatchUpSyncManager.cs} | 12 ++--- ...ISpectatorSlaveClock.cs => ISlaveClock.cs} | 4 +- ...pectatorSyncManager.cs => ISyncManager.cs} | 16 +++---- 8 files changed, 50 insertions(+), 50 deletions(-) rename osu.Game.Tests/OnlinePlay/{MultiplayerCatchUpSyncManagerTest.cs => TestCaseCatchUpSyncManager.cs} (73%) rename osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/Sync/{SpectatorCatchUpSlaveClock.cs => CatchUpSlaveClock.cs} (90%) rename osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/Sync/{SpectatorCatchUpSyncManager.cs => CatchUpSyncManager.cs} (90%) rename osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/Sync/{ISpectatorSlaveClock.cs => ISlaveClock.cs} (83%) rename osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/Sync/{ISpectatorSyncManager.cs => ISyncManager.cs} (50%) diff --git a/osu.Game.Tests/OnlinePlay/MultiplayerCatchUpSyncManagerTest.cs b/osu.Game.Tests/OnlinePlay/TestCaseCatchUpSyncManager.cs similarity index 73% rename from osu.Game.Tests/OnlinePlay/MultiplayerCatchUpSyncManagerTest.cs rename to osu.Game.Tests/OnlinePlay/TestCaseCatchUpSyncManager.cs index 6aa1219d53..a8c0a763e9 100644 --- a/osu.Game.Tests/OnlinePlay/MultiplayerCatchUpSyncManagerTest.cs +++ b/osu.Game.Tests/OnlinePlay/TestCaseCatchUpSyncManager.cs @@ -12,20 +12,20 @@ using osu.Game.Tests.Visual; namespace osu.Game.Tests.OnlinePlay { [HeadlessTest] - public class MultiplayerCatchUpSyncManagerTest : OsuTestScene + public class TestCaseCatchUpSyncManager : OsuTestScene { private TestManualClock master; - private SpectatorCatchUpSyncManager syncManager; + private CatchUpSyncManager syncManager; - private TestSpectatorSlaveClock slave1; - private TestSpectatorSlaveClock slave2; + private TestSlaveClock slave1; + private TestSlaveClock slave2; [SetUp] public void Setup() { - syncManager = new SpectatorCatchUpSyncManager(master = new TestManualClock()); - syncManager.AddSlave(slave1 = new TestSpectatorSlaveClock(1)); - syncManager.AddSlave(slave2 = new TestSpectatorSlaveClock(2)); + syncManager = new CatchUpSyncManager(master = new TestManualClock()); + syncManager.AddSlave(slave1 = new TestSlaveClock(1)); + syncManager.AddSlave(slave2 = new TestSlaveClock(2)); Schedule(() => Child = syncManager); } @@ -47,7 +47,7 @@ namespace osu.Game.Tests.OnlinePlay [Test] public void TestMasterClockDoesNotStartWhenNoneReadyForMaximumDelayTime() { - AddWaitStep($"wait {SpectatorCatchUpSyncManager.MAXIMUM_START_DELAY} milliseconds", (int)Math.Ceiling(SpectatorCatchUpSyncManager.MAXIMUM_START_DELAY / TimePerAction)); + AddWaitStep($"wait {CatchUpSyncManager.MAXIMUM_START_DELAY} milliseconds", (int)Math.Ceiling(CatchUpSyncManager.MAXIMUM_START_DELAY / TimePerAction)); assertMasterState(false); } @@ -55,7 +55,7 @@ namespace osu.Game.Tests.OnlinePlay public void TestMasterClockStartsWhenAnyReadyForMaximumDelayTime() { setWaiting(() => slave1, false); - AddWaitStep($"wait {SpectatorCatchUpSyncManager.MAXIMUM_START_DELAY} milliseconds", (int)Math.Ceiling(SpectatorCatchUpSyncManager.MAXIMUM_START_DELAY / TimePerAction)); + AddWaitStep($"wait {CatchUpSyncManager.MAXIMUM_START_DELAY} milliseconds", (int)Math.Ceiling(CatchUpSyncManager.MAXIMUM_START_DELAY / TimePerAction)); assertMasterState(true); } @@ -64,7 +64,7 @@ namespace osu.Game.Tests.OnlinePlay { setAllWaiting(false); - setMasterTime(SpectatorCatchUpSyncManager.SYNC_TARGET + 1); + setMasterTime(CatchUpSyncManager.SYNC_TARGET + 1); assertCatchingUp(() => slave1, false); } @@ -73,7 +73,7 @@ namespace osu.Game.Tests.OnlinePlay { setAllWaiting(false); - setMasterTime(SpectatorCatchUpSyncManager.MAX_SYNC_OFFSET + 1); + setMasterTime(CatchUpSyncManager.MAX_SYNC_OFFSET + 1); assertCatchingUp(() => slave1, true); assertCatchingUp(() => slave2, true); } @@ -83,8 +83,8 @@ namespace osu.Game.Tests.OnlinePlay { setAllWaiting(false); - setMasterTime(SpectatorCatchUpSyncManager.MAX_SYNC_OFFSET + 1); - setSlaveTime(() => slave1, SpectatorCatchUpSyncManager.SYNC_TARGET + 1); + setMasterTime(CatchUpSyncManager.MAX_SYNC_OFFSET + 1); + setSlaveTime(() => slave1, CatchUpSyncManager.SYNC_TARGET + 1); assertCatchingUp(() => slave1, true); } @@ -93,8 +93,8 @@ namespace osu.Game.Tests.OnlinePlay { setAllWaiting(false); - setMasterTime(SpectatorCatchUpSyncManager.MAX_SYNC_OFFSET + 2); - setSlaveTime(() => slave1, SpectatorCatchUpSyncManager.SYNC_TARGET); + setMasterTime(CatchUpSyncManager.MAX_SYNC_OFFSET + 2); + setSlaveTime(() => slave1, CatchUpSyncManager.SYNC_TARGET); assertCatchingUp(() => slave1, false); assertCatchingUp(() => slave2, true); } @@ -104,7 +104,7 @@ namespace osu.Game.Tests.OnlinePlay { setAllWaiting(false); - setSlaveTime(() => slave1, -SpectatorCatchUpSyncManager.SYNC_TARGET); + setSlaveTime(() => slave1, -CatchUpSyncManager.SYNC_TARGET); assertCatchingUp(() => slave1, false); assertSlaveState(() => slave1, true); } @@ -114,7 +114,7 @@ namespace osu.Game.Tests.OnlinePlay { setAllWaiting(false); - setSlaveTime(() => slave1, -SpectatorCatchUpSyncManager.SYNC_TARGET - 1); + setSlaveTime(() => slave1, -CatchUpSyncManager.SYNC_TARGET - 1); // This is a silent catchup, where IsCatchingUp = false but IsRunning = false also. assertCatchingUp(() => slave1, false); @@ -135,7 +135,7 @@ namespace osu.Game.Tests.OnlinePlay assertSlaveState(() => slave1, false); } - private void setWaiting(Func slave, bool waiting) + private void setWaiting(Func slave, bool waiting) => AddStep($"set slave {slave().Id} waiting = {waiting}", () => slave().WaitingOnFrames.Value = waiting); private void setAllWaiting(bool waiting) => AddStep($"set all slaves waiting = {waiting}", () => @@ -150,28 +150,28 @@ namespace osu.Game.Tests.OnlinePlay /// /// slave.Time = master.Time - offsetFromMaster /// - private void setSlaveTime(Func slave, double offsetFromMaster) + private void setSlaveTime(Func slave, double offsetFromMaster) => AddStep($"set slave {slave().Id} = master - {offsetFromMaster}", () => slave().Seek(master.CurrentTime - offsetFromMaster)); private void assertMasterState(bool running) => AddAssert($"master clock {(running ? "is" : "is not")} running", () => master.IsRunning == running); - private void assertCatchingUp(Func slave, bool catchingUp) => + private void assertCatchingUp(Func slave, bool catchingUp) => AddAssert($"slave {slave().Id} {(catchingUp ? "is" : "is not")} catching up", () => slave().IsCatchingUp == catchingUp); - private void assertSlaveState(Func slave, bool running) + private void assertSlaveState(Func slave, bool running) => AddAssert($"slave {slave().Id} {(running ? "is" : "is not")} running", () => slave().IsRunning == running); - private class TestSpectatorSlaveClock : TestManualClock, ISpectatorSlaveClock + private class TestSlaveClock : TestManualClock, ISlaveClock { public readonly Bindable WaitingOnFrames = new Bindable(true); - IBindable ISpectatorSlaveClock.WaitingOnFrames => WaitingOnFrames; + IBindable ISlaveClock.WaitingOnFrames => WaitingOnFrames; public bool IsCatchingUp { get; set; } public readonly int Id; - public TestSpectatorSlaveClock(int id) + public TestSlaveClock(int id) { Id = id; diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectator.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectator.cs index e1af95b870..23cc787e20 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectator.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectator.cs @@ -25,7 +25,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate private readonly PlayerInstance[] instances; private MasterGameplayClockContainer masterClockContainer; - private ISpectatorSyncManager syncManager; + private ISyncManager syncManager; private PlayerGrid grid; private MultiplayerSpectatorLeaderboard leaderboard; @@ -66,12 +66,12 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate InternalChildren = new[] { - (Drawable)(syncManager = new SpectatorCatchUpSyncManager(masterClockContainer)), + (Drawable)(syncManager = new CatchUpSyncManager(masterClockContainer)), masterClockContainer }; for (int i = 0; i < UserIds.Length; i++) - grid.Add(instances[i] = new PlayerInstance(UserIds[i], new SpectatorCatchUpSlaveClock(masterClockContainer.GameplayClock))); + grid.Add(instances[i] = new PlayerInstance(UserIds[i], new CatchUpSlaveClock(masterClockContainer.GameplayClock))); // Todo: This is not quite correct - it should be per-user to adjust for other mod combinations. var playableBeatmap = Beatmap.Value.GetPlayableBeatmap(Ruleset.Value); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectatorPlayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectatorPlayer.cs index d4ff65cd01..55f483586c 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectatorPlayer.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectatorPlayer.cs @@ -12,9 +12,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate { public class MultiplayerSpectatorPlayer : SpectatorPlayer { - private readonly ISpectatorSlaveClock gameplayClock; + private readonly ISlaveClock gameplayClock; - public MultiplayerSpectatorPlayer(Score score, ISpectatorSlaveClock gameplayClock) + public MultiplayerSpectatorPlayer(Score score, ISlaveClock gameplayClock) : base(score) { this.gameplayClock = gameplayClock; diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerInstance.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerInstance.cs index 31d2d458df..407732ee2e 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerInstance.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerInstance.cs @@ -18,7 +18,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate public bool PlayerLoaded => stack?.CurrentScreen is Player; public readonly int UserId; - public readonly SpectatorCatchUpSlaveClock GameplayClock; + public readonly CatchUpSlaveClock GameplayClock; public Score Score { get; private set; } @@ -29,7 +29,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate private readonly LoadingLayer loadingLayer; private OsuScreenStack stack; - public PlayerInstance(int userId, SpectatorCatchUpSlaveClock gameplayClock) + public PlayerInstance(int userId, CatchUpSlaveClock gameplayClock) { UserId = userId; GameplayClock = gameplayClock; diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/Sync/SpectatorCatchUpSlaveClock.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/Sync/CatchUpSlaveClock.cs similarity index 90% rename from osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/Sync/SpectatorCatchUpSlaveClock.cs rename to osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/Sync/CatchUpSlaveClock.cs index 4b529021de..f128ea619b 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/Sync/SpectatorCatchUpSlaveClock.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/Sync/CatchUpSlaveClock.cs @@ -8,9 +8,9 @@ using osu.Framework.Timing; namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate.Sync { /// - /// A which catches up using rate adjustment. + /// A which catches up using rate adjustment. /// - public class SpectatorCatchUpSlaveClock : ISpectatorSlaveClock + public class CatchUpSlaveClock : ISlaveClock { /// /// The catch up rate. @@ -19,7 +19,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate.Sync private readonly IFrameBasedClock masterClock; - public SpectatorCatchUpSlaveClock(IFrameBasedClock masterClock) + public CatchUpSlaveClock(IFrameBasedClock masterClock) { this.masterClock = masterClock; } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/Sync/SpectatorCatchUpSyncManager.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/Sync/CatchUpSyncManager.cs similarity index 90% rename from osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/Sync/SpectatorCatchUpSyncManager.cs rename to osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/Sync/CatchUpSyncManager.cs index 46e86177ca..68bfef6500 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/Sync/SpectatorCatchUpSyncManager.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/Sync/CatchUpSyncManager.cs @@ -10,9 +10,9 @@ using osu.Framework.Timing; namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate.Sync { /// - /// A which synchronises de-synced slave clocks through catchup. + /// A which synchronises de-synced slave clocks through catchup. /// - public class SpectatorCatchUpSyncManager : Component, ISpectatorSyncManager + public class CatchUpSyncManager : Component, ISyncManager { /// /// The offset from the master clock to which slaves should be synchronised to. @@ -37,19 +37,19 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate.Sync /// /// The slave clocks. /// - private readonly List slaves = new List(); + private readonly List slaves = new List(); private bool hasStarted; private double? firstStartAttemptTime; - public SpectatorCatchUpSyncManager(IAdjustableClock master) + public CatchUpSyncManager(IAdjustableClock master) { Master = master; } - public void AddSlave(ISpectatorSlaveClock clock) => slaves.Add(clock); + public void AddSlave(ISlaveClock clock) => slaves.Add(clock); - public void RemoveSlave(ISpectatorSlaveClock clock) => slaves.Remove(clock); + public void RemoveSlave(ISlaveClock clock) => slaves.Remove(clock); protected override void Update() { diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/Sync/ISpectatorSlaveClock.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/Sync/ISlaveClock.cs similarity index 83% rename from osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/Sync/ISpectatorSlaveClock.cs rename to osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/Sync/ISlaveClock.cs index d8733d4322..f45cb1fde6 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/Sync/ISpectatorSlaveClock.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/Sync/ISlaveClock.cs @@ -7,9 +7,9 @@ using osu.Framework.Timing; namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate.Sync { /// - /// A clock which is used by s and managed by an . + /// A clock which is used by s and managed by an . /// - public interface ISpectatorSlaveClock : IFrameBasedClock, IAdjustableClock + public interface ISlaveClock : IFrameBasedClock, IAdjustableClock { /// /// Whether this clock is waiting on frames to continue playback. diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/Sync/ISpectatorSyncManager.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/Sync/ISyncManager.cs similarity index 50% rename from osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/Sync/ISpectatorSyncManager.cs rename to osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/Sync/ISyncManager.cs index 5edbb3bc2c..d63ac7e1ca 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/Sync/ISpectatorSyncManager.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/Sync/ISyncManager.cs @@ -6,9 +6,9 @@ using osu.Framework.Timing; namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate.Sync { /// - /// Manages the synchronisation between one or more s in relation to a master clock. + /// Manages the synchronisation between one or more s in relation to a master clock. /// - public interface ISpectatorSyncManager + public interface ISyncManager { /// /// The master clock which slaves should synchronise to. @@ -16,15 +16,15 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate.Sync IAdjustableClock Master { get; } /// - /// Adds an to manage. + /// Adds an to manage. /// - /// The to add. - void AddSlave(ISpectatorSlaveClock clock); + /// The to add. + void AddSlave(ISlaveClock clock); /// - /// Removes an , stopping it from being managed by this . + /// Removes an , stopping it from being managed by this . /// - /// The to remove. - void RemoveSlave(ISpectatorSlaveClock clock); + /// The to remove. + void RemoveSlave(ISlaveClock clock); } } From 72ebcb157f585869b5b41593d0d262561ffa7369 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 16 Apr 2021 22:57:27 +0900 Subject: [PATCH 038/205] Dispose track on dispose --- .../Spectate/GameplayIsolationContainer.cs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/GameplayIsolationContainer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/GameplayIsolationContainer.cs index 3ccb67d342..7b6a544084 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/GameplayIsolationContainer.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/GameplayIsolationContainer.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using osu.Framework.Allocation; +using osu.Framework.Audio.Track; using osu.Framework.Bindables; using osu.Framework.Graphics.Containers; using osu.Game.Beatmaps; @@ -22,13 +23,15 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate [Cached] private readonly Bindable> mods = new Bindable>(); + private readonly Track track; + public GameplayIsolationContainer(WorkingBeatmap beatmap, RulesetInfo ruleset, IReadOnlyList mods) { this.beatmap.Value = beatmap; this.ruleset.Value = ruleset; this.mods.Value = mods; - beatmap.LoadTrack(); + track = beatmap.LoadTrack(); } protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) @@ -39,5 +42,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate dependencies.CacheAs(mods.BeginLease(false)); return dependencies; } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + track?.Dispose(); + } } } From 724fe3d37847ee70044e90c21c219ff79da03cc4 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 16 Apr 2021 22:57:34 +0900 Subject: [PATCH 039/205] Remove unnecessary method --- .../OnlinePlay/Multiplayer/Spectate/PlayerGrid.cs | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid.cs index b2d114d9cc..830378f129 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid.cs @@ -75,19 +75,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate cellContainer.Add(cell); } - public void Remove(Drawable content) - { - var cell = cellContainer.FirstOrDefault(c => c.Content == content); - if (cell == null) - return; - - if (cell.IsMaximised) - toggleMaximisationState(cell); - - cellContainer.Remove(cell); - facadeContainer.Remove(facadeContainer[cell.FacadeIndex]); - } - /// /// The content added to this grid. /// From d5b26b0ab506a62ececcf94781df307023ca0362 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 16 Apr 2021 23:01:34 +0900 Subject: [PATCH 040/205] Fix incorrect test spectator client implementation --- .../Visual/Multiplayer/TestSceneMultiplayerSpectator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectator.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectator.cs index 4026258830..e395ebd29d 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectator.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectator.cs @@ -321,7 +321,7 @@ namespace osu.Game.Tests.Visual.Multiplayer public override void WatchUser(int userId) { - if (userSentStateDictionary.TryGetValue(userId, out var sent) && sent) + if (!PlayingUsers.Contains(userId) && userSentStateDictionary.TryGetValue(userId, out var sent) && sent) { // usually the server would do this. sendState(userId, userBeatmapDictionary[userId]); From f32d00c0d99ecccdbf1452c210f0a75769932298 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 21 Apr 2021 17:13:01 +0900 Subject: [PATCH 041/205] Fix post-merge errors --- .../OnlinePlay/Multiplayer/Spectate/MultiplayerSpectator.cs | 2 +- osu.Game/Screens/Play/GameplayClockContainer.cs | 6 +----- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectator.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectator.cs index 23cc787e20..a5bd449e7e 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectator.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectator.cs @@ -86,7 +86,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate base.LoadComplete(); masterClockContainer.Stop(); - masterClockContainer.Restart(); + masterClockContainer.Reset(); } protected override void OnUserStateChanged(int userId, SpectatorState spectatorState) diff --git a/osu.Game/Screens/Play/GameplayClockContainer.cs b/osu.Game/Screens/Play/GameplayClockContainer.cs index dac3dad5d2..4109fd32bc 100644 --- a/osu.Game/Screens/Play/GameplayClockContainer.cs +++ b/osu.Game/Screens/Play/GameplayClockContainer.cs @@ -154,11 +154,7 @@ namespace osu.Game.Screens.Play return true; } - void IAdjustableClock.Reset() - { - Restart(); - Stop(); - } + void IAdjustableClock.Reset() => Reset(); public void ResetSpeedAdjustments() { From 2bea6256137a82375ac2e0f05a39f95b4f286503 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 21 Apr 2021 23:21:03 +0900 Subject: [PATCH 042/205] Fix initial playback states not being correct --- .../TestSceneMultiplayerSpectator.cs | 11 ++----- .../Spectate/MultiplayerSpectator.cs | 14 +++------ .../Spectate/MultiplayerSpectatorPlayer.cs | 31 ++++++++++++++++--- .../Spectate/Sync/CatchUpSlaveClock.cs | 2 +- 4 files changed, 34 insertions(+), 24 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectator.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectator.cs index e395ebd29d..9ec7bfb60b 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectator.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectator.cs @@ -256,17 +256,10 @@ namespace osu.Game.Tests.Visual.Multiplayer } private void checkPaused(int userId, bool state) => - AddUntilStep($"{userId} is {(state ? "paused" : "playing")}", () => getPlayer(userId).ChildrenOfType().First().IsPaused.Value == state); + AddUntilStep($"{userId} is {(state ? "paused" : "playing")}", () => getPlayer(userId).ChildrenOfType().First().GameplayClock.IsRunning != state); private void checkPausedInstant(int userId, bool state) => - AddAssert($"{userId} is {(state ? "paused" : "playing")}", () => getPlayer(userId).ChildrenOfType().First().IsPaused.Value == state); - - /// - /// Returns time(user1) - time(user2). - /// - private double getGameplayOffset(int user1, int user2) => getGameplayTime(user1) - getGameplayTime(user2); - - private double getGameplayTime(int userId) => getPlayer(userId).ChildrenOfType().Single().GameplayClock.CurrentTime; + AddAssert($"{userId} is {(state ? "paused" : "playing")}", () => getPlayer(userId).ChildrenOfType().First().GameplayClock.IsRunning != state); private Player getPlayer(int userId) => getInstance(userId).ChildrenOfType().Single(); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectator.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectator.cs index a5bd449e7e..2db470da67 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectator.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectator.cs @@ -39,10 +39,12 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate private void load() { Container leaderboardContainer; + masterClockContainer = new MasterGameplayClockContainer(Beatmap.Value, 0); - masterClockContainer = new MasterGameplayClockContainer(Beatmap.Value, 0) + InternalChildren = new[] { - Child = new GridContainer + (Drawable)(syncManager = new CatchUpSyncManager(masterClockContainer)), + masterClockContainer.WithChild(new GridContainer { RelativeSizeAxes = Axes.Both, ColumnDimensions = new[] @@ -61,13 +63,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate grid = new PlayerGrid { RelativeSizeAxes = Axes.Both } } } - } - }; - - InternalChildren = new[] - { - (Drawable)(syncManager = new CatchUpSyncManager(masterClockContainer)), - masterClockContainer + }) }; for (int i = 0; i < UserIds.Length; i++) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectatorPlayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectatorPlayer.cs index 55f483586c..5af0d19a4d 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectatorPlayer.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectatorPlayer.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Timing; using osu.Game.Beatmaps; using osu.Game.Scoring; @@ -12,22 +13,31 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate { public class MultiplayerSpectatorPlayer : SpectatorPlayer { - private readonly ISlaveClock gameplayClock; + private readonly Bindable waitingOnFrames = new Bindable(true); + private readonly Score score; + private readonly ISlaveClock slaveClock; - public MultiplayerSpectatorPlayer(Score score, ISlaveClock gameplayClock) + public MultiplayerSpectatorPlayer(Score score, ISlaveClock slaveClock) : base(score) { - this.gameplayClock = gameplayClock; + this.score = score; + this.slaveClock = slaveClock; } [BackgroundDependencyLoader] private void load() { - gameplayClock.WaitingOnFrames.BindTo(DrawableRuleset.FrameStableClock.WaitingOnFrames); + slaveClock.WaitingOnFrames.BindTo(waitingOnFrames); + } + + protected override void UpdateAfterChildren() + { + base.UpdateAfterChildren(); + waitingOnFrames.Value = DrawableRuleset.FrameStableClock.WaitingOnFrames.Value || score.Replay.Frames.Count == 0; } protected override GameplayClockContainer CreateGameplayClockContainer(WorkingBeatmap beatmap, double gameplayStart) - => new SubGameplayClockContainer(gameplayClock); + => new SubGameplayClockContainer(slaveClock); } public class SubGameplayClockContainer : GameplayClockContainer @@ -37,6 +47,17 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate { } + protected override void Update() + { + // The slave clock's running state is controlled by the sync manager, but the local pausing state needs to be updated to stop gameplay. + if (SourceClock.IsRunning) + Start(); + else + Stop(); + + base.Update(); + } + protected override GameplayClock CreateGameplayClock(IFrameBasedClock source) => new GameplayClock(source); } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/Sync/CatchUpSlaveClock.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/Sync/CatchUpSlaveClock.cs index f128ea619b..cefe5ff04f 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/Sync/CatchUpSlaveClock.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/Sync/CatchUpSlaveClock.cs @@ -74,7 +74,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate.Sync public FrameTimeInfo TimeInfo => new FrameTimeInfo { Elapsed = ElapsedFrameTime, Current = CurrentTime }; - public IBindable WaitingOnFrames { get; } = new Bindable(); + public IBindable WaitingOnFrames { get; } = new Bindable(true); public bool IsCatchingUp { get; set; } } From 1ca2152e6191e0da98abdf034f1d50c20734fe7b Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 21 Apr 2021 23:22:36 +0900 Subject: [PATCH 043/205] Privatise + rename to SlaveGameplayClockContainer --- .../Spectate/MultiplayerSpectatorPlayer.cs | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectatorPlayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectatorPlayer.cs index 5af0d19a4d..69b38c61f7 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectatorPlayer.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectatorPlayer.cs @@ -37,27 +37,27 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate } protected override GameplayClockContainer CreateGameplayClockContainer(WorkingBeatmap beatmap, double gameplayStart) - => new SubGameplayClockContainer(slaveClock); - } + => new SlaveGameplayClockContainer(slaveClock); - public class SubGameplayClockContainer : GameplayClockContainer - { - public SubGameplayClockContainer(IClock sourceClock) - : base(sourceClock) + private class SlaveGameplayClockContainer : GameplayClockContainer { + public SlaveGameplayClockContainer(IClock sourceClock) + : base(sourceClock) + { + } + + protected override void Update() + { + // The slave clock's running state is controlled by the sync manager, but the local pausing state needs to be updated to stop gameplay. + if (SourceClock.IsRunning) + Start(); + else + Stop(); + + base.Update(); + } + + protected override GameplayClock CreateGameplayClock(IFrameBasedClock source) => new GameplayClock(source); } - - protected override void Update() - { - // The slave clock's running state is controlled by the sync manager, but the local pausing state needs to be updated to stop gameplay. - if (SourceClock.IsRunning) - Start(); - else - Stop(); - - base.Update(); - } - - protected override GameplayClock CreateGameplayClock(IFrameBasedClock source) => new GameplayClock(source); } } From 6588859c32296e9538f819a49328382f314b62bc Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 22 Apr 2021 22:29:18 +0900 Subject: [PATCH 044/205] Remove loggings --- .../Multiplayer/Spectate/Sync/CatchUpSyncManager.cs | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/Sync/CatchUpSyncManager.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/Sync/CatchUpSyncManager.cs index 68bfef6500..1d3fcd824c 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/Sync/CatchUpSyncManager.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/Sync/CatchUpSyncManager.cs @@ -4,7 +4,6 @@ using System.Collections.Generic; using System.Linq; using osu.Framework.Graphics; -using osu.Framework.Logging; using osu.Framework.Timing; namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate.Sync @@ -84,16 +83,10 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate.Sync int readyCount = slaves.Count(s => !s.WaitingOnFrames.Value); if (readyCount == slaves.Count) - { - Logger.Log("Gameplay started (all ready)."); return hasStarted = true; - } if (readyCount > 0 && (Time.Current - firstStartAttemptTime) > MAXIMUM_START_DELAY) - { - Logger.Log($"Gameplay started (maximum delay exceeded, {readyCount}/{slaves.Count} ready)."); return hasStarted = true; - } return false; } @@ -124,19 +117,13 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate.Sync { // Stop the slave from catching up if it's within the sync target. if (timeDelta <= SYNC_TARGET) - { slave.IsCatchingUp = false; - Logger.Log($"Slave {i} catchup finished (delta = {timeDelta})"); - } } else { // Make the slave start catching up if it's exceeded the maximum allowable sync offset. if (timeDelta > MAX_SYNC_OFFSET) - { slave.IsCatchingUp = true; - Logger.Log($"Slave {i} catchup started (too far behind, delta = {timeDelta})"); - } } } } From 64579d50ac160215d5a8f60dc5f5fa8526b27fbc Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 22 Apr 2021 22:59:47 +0900 Subject: [PATCH 045/205] Use only single PlayerInstance for hit sample playback --- .../TestSceneMultiplayerSpectator.cs | 46 ++++++++++++-- .../Spectate/MultiplayerSpectator.cs | 20 +++++++ .../Multiplayer/Spectate/PlayerInstance.cs | 60 +++++++++++++++++-- 3 files changed, 117 insertions(+), 9 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectator.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectator.cs index 9ec7bfb60b..cd89d3bcc1 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectator.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectator.cs @@ -198,10 +198,40 @@ namespace osu.Game.Tests.Visual.Multiplayer checkPausedInstant(56, false); // Player 2 should catch up to player 1 after unpausing. - AddUntilStep("player 2 not catching up", () => !getInstance(56).GameplayClock.IsCatchingUp); + waitForCatchup(56); AddWaitStep("wait a bit", 10); } + [Test] + public void TestMostInSyncUserIsAudioSource() + { + start(new[] { 55, 56 }); + loadSpectateScreen(); + + assertVolume(55, 0); + assertVolume(56, 0); + + sendFrames(55, 10); + sendFrames(56, 20); + assertVolume(55, 1); + assertVolume(56, 0); + + checkPaused(55, true); + assertVolume(55, 0); + assertVolume(56, 1); + + sendFrames(55, 100); + waitForCatchup(55); + checkPaused(56, true); + assertVolume(55, 1); + assertVolume(56, 0); + + sendFrames(56, 100); + waitForCatchup(56); + assertVolume(55, 1); + assertVolume(56, 0); + } + private void loadSpectateScreen(bool waitForPlayerLoad = true) { AddStep("load screen", () => @@ -255,11 +285,17 @@ namespace osu.Game.Tests.Visual.Multiplayer }); } - private void checkPaused(int userId, bool state) => - AddUntilStep($"{userId} is {(state ? "paused" : "playing")}", () => getPlayer(userId).ChildrenOfType().First().GameplayClock.IsRunning != state); + private void checkPaused(int userId, bool state) + => AddUntilStep($"{userId} is {(state ? "paused" : "playing")}", () => getPlayer(userId).ChildrenOfType().First().GameplayClock.IsRunning != state); - private void checkPausedInstant(int userId, bool state) => - AddAssert($"{userId} is {(state ? "paused" : "playing")}", () => getPlayer(userId).ChildrenOfType().First().GameplayClock.IsRunning != state); + private void checkPausedInstant(int userId, bool state) + => AddAssert($"{userId} is {(state ? "paused" : "playing")}", () => getPlayer(userId).ChildrenOfType().First().GameplayClock.IsRunning != state); + + private void assertVolume(int userId, double volume) + => AddAssert($"{userId} volume is {volume}", () => getInstance(userId).Volume.Value == volume); + + private void waitForCatchup(int userId) + => AddUntilStep($"{userId} not catching up", () => !getInstance(userId).GameplayClock.IsCatchingUp); private Player getPlayer(int userId) => getInstance(userId).ChildrenOfType().Single(); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectator.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectator.cs index 2db470da67..33dd57586c 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectator.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectator.cs @@ -3,6 +3,7 @@ using System; using System.Linq; +using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -28,6 +29,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate private ISyncManager syncManager; private PlayerGrid grid; private MultiplayerSpectatorLeaderboard leaderboard; + private PlayerInstance currentAudioSource; public MultiplayerSpectator(int[] userIds) : base(userIds.AsSpan().Slice(0, Math.Min(16, userIds.Length)).ToArray()) @@ -85,6 +87,24 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate masterClockContainer.Reset(); } + protected override void Update() + { + base.Update(); + + if (!isCandidateAudioSource(currentAudioSource?.GameplayClock)) + { + currentAudioSource = instances.Where(i => isCandidateAudioSource(i.GameplayClock)) + .OrderBy(i => Math.Abs(i.GameplayClock.CurrentTime - syncManager.Master.CurrentTime)) + .FirstOrDefault(); + + foreach (var instance in instances) + instance.Volume.Value = instance == currentAudioSource ? 1 : 0; + } + } + + private bool isCandidateAudioSource([CanBeNull] ISlaveClock clock) + => clock?.IsRunning == true && !clock.IsCatchingUp && !clock.WaitingOnFrames.Value; + protected override void OnUserStateChanged(int userId, SpectatorState spectatorState) { } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerInstance.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerInstance.cs index 407732ee2e..80d6727599 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerInstance.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerInstance.cs @@ -3,6 +3,8 @@ using System; using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Beatmaps; @@ -13,7 +15,7 @@ using osu.Game.Screens.Play; namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate { - public class PlayerInstance : CompositeDrawable + public class PlayerInstance : CompositeDrawable, IAdjustableAudioComponent { public bool PlayerLoaded => stack?.CurrentScreen is Player; @@ -25,8 +27,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate [Resolved] private BeatmapManager beatmapManager { get; set; } - private readonly Container content; + private readonly Container gameplayContent; private readonly LoadingLayer loadingLayer; + private readonly AudioContainer audioContainer; private OsuScreenStack stack; public PlayerInstance(int userId, CatchUpSlaveClock gameplayClock) @@ -39,7 +42,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate InternalChildren = new Drawable[] { - content = new DrawSizePreservingFillContainer { RelativeSizeAxes = Axes.Both }, + audioContainer = new AudioContainer + { + RelativeSizeAxes = Axes.Both, + Child = gameplayContent = new DrawSizePreservingFillContainer { RelativeSizeAxes = Axes.Both }, + }, loadingLayer = new LoadingLayer(true) { State = { Value = Visibility.Visible } } }; } @@ -51,7 +58,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate Score = score; - content.Child = new GameplayIsolationContainer(beatmapManager.GetWorkingBeatmap(Score.ScoreInfo.Beatmap, bypassCache: true), Score.ScoreInfo.Ruleset, Score.ScoreInfo.Mods) + gameplayContent.Child = new GameplayIsolationContainer(beatmapManager.GetWorkingBeatmap(Score.ScoreInfo.Beatmap), Score.ScoreInfo.Ruleset, Score.ScoreInfo.Mods) { RelativeSizeAxes = Axes.Both, Child = stack = new OsuScreenStack() @@ -64,5 +71,50 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate // Player interferes with global input, so disable input for now. public override bool PropagatePositionalInputSubTree => false; public override bool PropagateNonPositionalInputSubTree => false; + + #region IAdjustableAudioComponent + + public IBindable AggregateVolume => audioContainer.AggregateVolume; + + public IBindable AggregateBalance => audioContainer.AggregateBalance; + + public IBindable AggregateFrequency => audioContainer.AggregateFrequency; + + public IBindable AggregateTempo => audioContainer.AggregateTempo; + + public void BindAdjustments(IAggregateAudioAdjustment component) + { + audioContainer.BindAdjustments(component); + } + + public void UnbindAdjustments(IAggregateAudioAdjustment component) + { + audioContainer.UnbindAdjustments(component); + } + + public void AddAdjustment(AdjustableProperty type, IBindable adjustBindable) + { + audioContainer.AddAdjustment(type, adjustBindable); + } + + public void RemoveAdjustment(AdjustableProperty type, IBindable adjustBindable) + { + audioContainer.RemoveAdjustment(type, adjustBindable); + } + + public void RemoveAllAdjustments(AdjustableProperty type) + { + audioContainer.RemoveAllAdjustments(type); + } + + public BindableNumber Volume => audioContainer.Volume; + + public BindableNumber Balance => audioContainer.Balance; + + public BindableNumber Frequency => audioContainer.Frequency; + + public BindableNumber Tempo => audioContainer.Tempo; + + #endregion } } From 22a3f3ad3e9f022fb85e1258aa0a2184a58fe671 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 22 Apr 2021 23:04:54 +0900 Subject: [PATCH 046/205] Revert unnecessary BeatmapManager change --- osu.Game/Beatmaps/BeatmapManager.cs | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index abb1f5a93c..5e975de77c 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -281,9 +281,8 @@ namespace osu.Game.Beatmaps /// /// The beatmap to lookup. /// The currently loaded . Allows for optimisation where elements are shared with the new beatmap. May be returned if beatmapInfo requested matches - /// Whether to bypass the cache and return a new instance. /// A instance correlating to the provided . - public WorkingBeatmap GetWorkingBeatmap(BeatmapInfo beatmapInfo, WorkingBeatmap previous = null, bool bypassCache = false) + public WorkingBeatmap GetWorkingBeatmap(BeatmapInfo beatmapInfo, WorkingBeatmap previous = null) { if (beatmapInfo?.ID > 0 && previous != null && previous.BeatmapInfo?.ID == beatmapInfo.ID) return previous; @@ -302,14 +301,9 @@ namespace osu.Game.Beatmaps lock (workingCache) { - BeatmapManagerWorkingBeatmap working; - - if (!bypassCache) - { - working = workingCache.FirstOrDefault(w => w.BeatmapInfo?.ID == beatmapInfo.ID); - if (working != null) - return working; - } + var working = workingCache.FirstOrDefault(w => w.BeatmapInfo?.ID == beatmapInfo.ID); + if (working != null) + return working; beatmapInfo.Metadata ??= beatmapInfo.BeatmapSet.Metadata; From fd0b030cf431ecb0ab3ea45bbcac68ee0545a207 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 22 Apr 2021 23:37:33 +0900 Subject: [PATCH 047/205] Refactor gameplay screen creation --- .../Screens/OnlinePlay/Match/RoomSubScreen.cs | 13 +++++++----- .../Multiplayer/MultiplayerMatchSubScreen.cs | 21 ++++++++++++------- .../Playlists/PlaylistsRoomSubScreen.cs | 9 +++----- 3 files changed, 24 insertions(+), 19 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index 0d1ed5d88e..68bdd9160e 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -17,7 +17,6 @@ using osu.Game.Online.Rooms; using osu.Game.Overlays; using osu.Game.Overlays.Mods; using osu.Game.Rulesets.Mods; -using osu.Game.Screens.Play; namespace osu.Game.Screens.OnlinePlay.Match { @@ -148,14 +147,18 @@ namespace osu.Game.Screens.OnlinePlay.Match return base.OnExiting(next); } - protected void StartPlay(Func player) => PushTopLevelScreen(() => new PlayerLoader(player)); - - protected void PushTopLevelScreen(Func screen) + protected void StartPlay() { sampleStart?.Play(); - ParentScreen?.Push(screen()); + ParentScreen?.Push(CreateGameplayScreen()); } + /// + /// Creates the gameplay screen to be entered. + /// + /// The screen to enter. + protected abstract Screen CreateGameplayScreen(); + private void selectedItemChanged() { updateWorkingBeatmap(); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index 9a68ff908d..c5d7610b53 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -416,10 +416,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer UpdateMods(); if (client.LocalUser.State == MultiplayerUserState.Spectating - && (client.Room.State == MultiplayerRoomState.Playing || client.Room.State == MultiplayerRoomState.WaitingForLoad) - && ParentScreen.IsCurrentScreen()) + && (client.Room.State == MultiplayerRoomState.Playing || client.Room.State == MultiplayerRoomState.WaitingForLoad)) { - PushTopLevelScreen(() => new MultiplayerSpectator(client.CurrentMatchPlayingUserIds.ToArray())); + StartPlay(); // If the current user was host, they started the match and the in-progres operation needs to be stopped now. readyClickOperation?.Dispose(); @@ -429,16 +428,22 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer private void onLoadRequested() { - Debug.Assert(client.Room != null); - - int[] userIds = client.CurrentMatchPlayingUserIds.ToArray(); - - StartPlay(() => new MultiplayerPlayer(SelectedItem.Value, userIds)); + StartPlay(); readyClickOperation?.Dispose(); readyClickOperation = null; } + protected override Screen CreateGameplayScreen() + { + Debug.Assert(client.LocalUser != null); + + if (client.LocalUser.State == MultiplayerUserState.Spectating) + return new MultiSpectatorScreen(client.CurrentMatchPlayingUserIds.ToArray()); + + return new MultiplayerPlayer(SelectedItem.Value, client.CurrentMatchPlayingUserIds.ToArray()); + } + protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs index 6542d01e64..11bc55823f 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs @@ -218,10 +218,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists }, new Drawable[] { - new Footer - { - OnStart = onStart, - } + new Footer { OnStart = StartPlay } } }, RowDimensions = new[] @@ -274,9 +271,9 @@ namespace osu.Game.Screens.OnlinePlay.Playlists }, true); } - private void onStart() => StartPlay(() => new PlaylistsPlayer(SelectedItem.Value) + protected override Screen CreateGameplayScreen() => new PlaylistsPlayer(SelectedItem.Value) { Exited = () => leaderboard.RefreshScores() - }); + }; } } From 4aceb75eb2f8c5c40ff796ecc8d8fd22c3a6ddf1 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 22 Apr 2021 23:37:45 +0900 Subject: [PATCH 048/205] Disable spectate button on closed rooms Doesn't have an effect normally - only for safety purposes in case we allow entering the match subscreen after a match has finished in the future. --- .../OnlinePlay/Multiplayer/Match/MultiplayerSpectateButton.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerSpectateButton.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerSpectateButton.cs index 465af037e2..04150902bc 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerSpectateButton.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerSpectateButton.cs @@ -81,7 +81,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match break; } - button.Enabled.Value = !operationInProgress.Value; + button.Enabled.Value = Client.Room?.State != MultiplayerRoomState.Closed && !operationInProgress.Value; } private class ButtonWithTrianglesExposed : TriangleButton From 8a0ba3a05557e3d1f20521e577bb61d7604c9a97 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 22 Apr 2021 23:38:51 +0900 Subject: [PATCH 049/205] Merge GameplayIsolationContainer into PlayerInstance, remove track --- .../Spectate/GameplayIsolationContainer.cs | 52 ------------------- .../Multiplayer/Spectate/PlayerInstance.cs | 35 ++++++++++++- 2 files changed, 33 insertions(+), 54 deletions(-) delete mode 100644 osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/GameplayIsolationContainer.cs diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/GameplayIsolationContainer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/GameplayIsolationContainer.cs deleted file mode 100644 index 7b6a544084..0000000000 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/GameplayIsolationContainer.cs +++ /dev/null @@ -1,52 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System.Collections.Generic; -using osu.Framework.Allocation; -using osu.Framework.Audio.Track; -using osu.Framework.Bindables; -using osu.Framework.Graphics.Containers; -using osu.Game.Beatmaps; -using osu.Game.Rulesets; -using osu.Game.Rulesets.Mods; - -namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate -{ - public class GameplayIsolationContainer : Container - { - [Cached] - private readonly Bindable ruleset = new Bindable(); - - [Cached] - private readonly Bindable beatmap = new Bindable(); - - [Cached] - private readonly Bindable> mods = new Bindable>(); - - private readonly Track track; - - public GameplayIsolationContainer(WorkingBeatmap beatmap, RulesetInfo ruleset, IReadOnlyList mods) - { - this.beatmap.Value = beatmap; - this.ruleset.Value = ruleset; - this.mods.Value = mods; - - track = beatmap.LoadTrack(); - } - - protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) - { - var dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); - dependencies.CacheAs(ruleset.BeginLease(false)); - dependencies.CacheAs(beatmap.BeginLease(false)); - dependencies.CacheAs(mods.BeginLease(false)); - return dependencies; - } - - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - track?.Dispose(); - } - } -} diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerInstance.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerInstance.cs index 80d6727599..8007c9e068 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerInstance.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerInstance.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Bindables; @@ -9,6 +10,8 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Beatmaps; using osu.Game.Graphics.UserInterface; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Mods; using osu.Game.Scoring; using osu.Game.Screens.OnlinePlay.Multiplayer.Spectate.Sync; using osu.Game.Screens.Play; @@ -58,13 +61,13 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate Score = score; - gameplayContent.Child = new GameplayIsolationContainer(beatmapManager.GetWorkingBeatmap(Score.ScoreInfo.Beatmap), Score.ScoreInfo.Ruleset, Score.ScoreInfo.Mods) + gameplayContent.Child = new PlayerIsolationContainer(beatmapManager.GetWorkingBeatmap(Score.ScoreInfo.Beatmap), Score.ScoreInfo.Ruleset, Score.ScoreInfo.Mods) { RelativeSizeAxes = Axes.Both, Child = stack = new OsuScreenStack() }; - stack.Push(new MultiplayerSpectatorPlayerLoader(Score, () => new MultiplayerSpectatorPlayer(Score, GameplayClock))); + stack.Push(new MultiSpectatorPlayerLoader(Score, () => new MultiSpectatorPlayer(Score, GameplayClock))); loadingLayer.Hide(); } @@ -116,5 +119,33 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate public BindableNumber Tempo => audioContainer.Tempo; #endregion + + private class PlayerIsolationContainer : Container + { + [Cached] + private readonly Bindable ruleset = new Bindable(); + + [Cached] + private readonly Bindable beatmap = new Bindable(); + + [Cached] + private readonly Bindable> mods = new Bindable>(); + + public PlayerIsolationContainer(WorkingBeatmap beatmap, RulesetInfo ruleset, IReadOnlyList mods) + { + this.beatmap.Value = beatmap; + this.ruleset.Value = ruleset; + this.mods.Value = mods; + } + + protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) + { + var dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); + dependencies.CacheAs(ruleset.BeginLease(false)); + dependencies.CacheAs(beatmap.BeginLease(false)); + dependencies.CacheAs(mods.BeginLease(false)); + return dependencies; + } + } } } From ee259497519f71d360616b6f2dc92247bfdfe43b Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 22 Apr 2021 23:39:02 +0900 Subject: [PATCH 050/205] Rename classes --- .../Multiplayer/TestSceneMultiplayerSpectator.cs | 12 ++++++------ .../TestSceneMultiplayerSpectatorLeaderboard.cs | 4 ++-- ...orLeaderboard.cs => MultiSpectatorLeaderboard.cs} | 4 ++-- ...yerSpectatorPlayer.cs => MultiSpectatorPlayer.cs} | 4 ++-- ...PlayerLoader.cs => MultiSpectatorPlayerLoader.cs} | 4 ++-- ...ltiplayerSpectator.cs => MultiSpectatorScreen.cs} | 8 ++++---- .../Multiplayer/Spectate/Sync/ISlaveClock.cs | 2 +- 7 files changed, 19 insertions(+), 19 deletions(-) rename osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/{MultiplayerSpectatorLeaderboard.cs => MultiSpectatorLeaderboard.cs} (92%) rename osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/{MultiplayerSpectatorPlayer.cs => MultiSpectatorPlayer.cs} (93%) rename osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/{MultiplayerSpectatorPlayerLoader.cs => MultiSpectatorPlayerLoader.cs} (75%) rename osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/{MultiplayerSpectator.cs => MultiSpectatorScreen.cs} (93%) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectator.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectator.cs index cd89d3bcc1..0b77a1a9e2 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectator.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectator.cs @@ -37,7 +37,7 @@ namespace osu.Game.Tests.Visual.Multiplayer [Resolved] private BeatmapManager beatmapManager { get; set; } - private MultiplayerSpectator spectator; + private MultiSpectatorScreen spectatorScreen; private readonly List playingUserIds = new List(); private readonly Dictionary nextFrame = new Dictionary(); @@ -91,11 +91,11 @@ namespace osu.Game.Tests.Visual.Multiplayer AddWaitStep("wait a bit", 10); AddStep("load player 55", () => streamingClient.StartPlay(55, importedBeatmapId)); - AddUntilStep("one player added", () => spectator.ChildrenOfType().Count() == 1); + AddUntilStep("one player added", () => spectatorScreen.ChildrenOfType().Count() == 1); AddWaitStep("wait a bit", 10); AddStep("load player 56", () => streamingClient.StartPlay(56, importedBeatmapId)); - AddUntilStep("two players added", () => spectator.ChildrenOfType().Count() == 2); + AddUntilStep("two players added", () => spectatorScreen.ChildrenOfType().Count() == 2); } [Test] @@ -239,10 +239,10 @@ namespace osu.Game.Tests.Visual.Multiplayer Beatmap.Value = beatmapManager.GetWorkingBeatmap(importedBeatmap); Ruleset.Value = importedBeatmap.Ruleset; - LoadScreen(spectator = new MultiplayerSpectator(playingUserIds.ToArray())); + LoadScreen(spectatorScreen = new MultiSpectatorScreen(playingUserIds.ToArray())); }); - AddUntilStep("wait for screen load", () => spectator.LoadState == LoadState.Loaded && (!waitForPlayerLoad || spectator.AllPlayersLoaded)); + AddUntilStep("wait for screen load", () => spectatorScreen.LoadState == LoadState.Loaded && (!waitForPlayerLoad || spectatorScreen.AllPlayersLoaded)); } private void start(int userId, int? beatmapId = null) => start(new[] { userId }, beatmapId); @@ -299,7 +299,7 @@ namespace osu.Game.Tests.Visual.Multiplayer private Player getPlayer(int userId) => getInstance(userId).ChildrenOfType().Single(); - private PlayerInstance getInstance(int userId) => spectator.ChildrenOfType().Single(p => p.UserId == userId); + private PlayerInstance getInstance(int userId) => spectatorScreen.ChildrenOfType().Single(p => p.UserId == userId); public class TestSpectatorStreamingClient : SpectatorStreamingClient { diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectatorLeaderboard.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectatorLeaderboard.cs index 3b2cfb1c7b..8368186219 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectatorLeaderboard.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectatorLeaderboard.cs @@ -54,7 +54,7 @@ namespace osu.Game.Tests.Visual.Multiplayer [SetUpSteps] public new void SetUpSteps() { - MultiplayerSpectatorLeaderboard leaderboard = null; + MultiSpectatorLeaderboard leaderboard = null; AddStep("reset", () => { @@ -78,7 +78,7 @@ namespace osu.Game.Tests.Visual.Multiplayer var scoreProcessor = new OsuScoreProcessor(); scoreProcessor.ApplyBeatmap(playable); - LoadComponentAsync(leaderboard = new MultiplayerSpectatorLeaderboard(scoreProcessor, clocks.Keys.ToArray()) { Expanded = { Value = true } }, Add); + LoadComponentAsync(leaderboard = new MultiSpectatorLeaderboard(scoreProcessor, clocks.Keys.ToArray()) { Expanded = { Value = true } }, Add); }); AddUntilStep("wait for load", () => leaderboard.IsLoaded); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectatorLeaderboard.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorLeaderboard.cs similarity index 92% rename from osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectatorLeaderboard.cs rename to osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorLeaderboard.cs index 1b9e2bda2d..96c7702048 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectatorLeaderboard.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorLeaderboard.cs @@ -9,9 +9,9 @@ using osu.Game.Screens.Play.HUD; namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate { - public class MultiplayerSpectatorLeaderboard : MultiplayerGameplayLeaderboard + public class MultiSpectatorLeaderboard : MultiplayerGameplayLeaderboard { - public MultiplayerSpectatorLeaderboard(ScoreProcessor scoreProcessor, int[] userIds) + public MultiSpectatorLeaderboard(ScoreProcessor scoreProcessor, int[] userIds) : base(scoreProcessor, userIds) { } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectatorPlayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorPlayer.cs similarity index 93% rename from osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectatorPlayer.cs rename to osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorPlayer.cs index 69b38c61f7..0101c00041 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectatorPlayer.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorPlayer.cs @@ -11,13 +11,13 @@ using osu.Game.Screens.Play; namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate { - public class MultiplayerSpectatorPlayer : SpectatorPlayer + public class MultiSpectatorPlayer : SpectatorPlayer { private readonly Bindable waitingOnFrames = new Bindable(true); private readonly Score score; private readonly ISlaveClock slaveClock; - public MultiplayerSpectatorPlayer(Score score, ISlaveClock slaveClock) + public MultiSpectatorPlayer(Score score, ISlaveClock slaveClock) : base(score) { this.score = score; diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectatorPlayerLoader.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorPlayerLoader.cs similarity index 75% rename from osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectatorPlayerLoader.cs rename to osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorPlayerLoader.cs index c8f16b5e90..fce44296f3 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectatorPlayerLoader.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorPlayerLoader.cs @@ -8,9 +8,9 @@ using osu.Game.Screens.Play; namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate { - public class MultiplayerSpectatorPlayerLoader : SpectatorPlayerLoader + public class MultiSpectatorPlayerLoader : SpectatorPlayerLoader { - public MultiplayerSpectatorPlayerLoader(Score score, Func createPlayer) + public MultiSpectatorPlayerLoader(Score score, Func createPlayer) : base(score, createPlayer) { } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectator.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs similarity index 93% rename from osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectator.cs rename to osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs index 33dd57586c..52b7898028 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectator.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs @@ -14,7 +14,7 @@ using osu.Game.Screens.Spectate; namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate { - public class MultiplayerSpectator : SpectatorScreen + public class MultiSpectatorScreen : SpectatorScreen { // Isolates beatmap/ruleset to this screen. public override bool DisallowExternalBeatmapRulesetChanges => true; @@ -28,10 +28,10 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate private MasterGameplayClockContainer masterClockContainer; private ISyncManager syncManager; private PlayerGrid grid; - private MultiplayerSpectatorLeaderboard leaderboard; + private MultiSpectatorLeaderboard leaderboard; private PlayerInstance currentAudioSource; - public MultiplayerSpectator(int[] userIds) + public MultiSpectatorScreen(int[] userIds) : base(userIds.AsSpan().Slice(0, Math.Min(16, userIds.Length)).ToArray()) { instances = new PlayerInstance[UserIds.Length]; @@ -76,7 +76,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate var scoreProcessor = Ruleset.Value.CreateInstance().CreateScoreProcessor(); scoreProcessor.ApplyBeatmap(playableBeatmap); - LoadComponentAsync(leaderboard = new MultiplayerSpectatorLeaderboard(scoreProcessor, UserIds) { Expanded = { Value = true } }, leaderboardContainer.Add); + LoadComponentAsync(leaderboard = new MultiSpectatorLeaderboard(scoreProcessor, UserIds) { Expanded = { Value = true } }, leaderboardContainer.Add); } protected override void LoadComplete() diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/Sync/ISlaveClock.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/Sync/ISlaveClock.cs index f45cb1fde6..08d69f9560 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/Sync/ISlaveClock.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/Sync/ISlaveClock.cs @@ -7,7 +7,7 @@ using osu.Framework.Timing; namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate.Sync { /// - /// A clock which is used by s and managed by an . + /// A clock which is used by s and managed by an . /// public interface ISlaveClock : IFrameBasedClock, IAdjustableClock { From 4f0857f946e96d5c0c9da4440587e09488ca083e Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 22 Apr 2021 23:52:22 +0900 Subject: [PATCH 051/205] Xmldocs and general refactorings --- .../TestSceneMultiplayerSpectator.cs | 2 +- .../Spectate/MultiSpectatorLeaderboard.cs | 2 +- .../Spectate/MultiSpectatorPlayer.cs | 17 +++++++++-- .../Spectate/MultiSpectatorPlayerLoader.cs | 6 +++- .../Spectate/MultiSpectatorScreen.cs | 18 +++++++++--- .../{PlayerInstance.cs => PlayerArea.cs} | 29 +++++++++++++++---- 6 files changed, 59 insertions(+), 15 deletions(-) rename osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/{PlayerInstance.cs => PlayerArea.cs} (83%) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectator.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectator.cs index 0b77a1a9e2..a82dea5640 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectator.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectator.cs @@ -299,7 +299,7 @@ namespace osu.Game.Tests.Visual.Multiplayer private Player getPlayer(int userId) => getInstance(userId).ChildrenOfType().Single(); - private PlayerInstance getInstance(int userId) => spectatorScreen.ChildrenOfType().Single(p => p.UserId == userId); + private PlayerArea getInstance(int userId) => spectatorScreen.ChildrenOfType().Single(p => p.UserId == userId); public class TestSpectatorStreamingClient : SpectatorStreamingClient { diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorLeaderboard.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorLeaderboard.cs index 96c7702048..ab3ead68b5 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorLeaderboard.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorLeaderboard.cs @@ -11,7 +11,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate { public class MultiSpectatorLeaderboard : MultiplayerGameplayLeaderboard { - public MultiSpectatorLeaderboard(ScoreProcessor scoreProcessor, int[] userIds) + public MultiSpectatorLeaderboard([NotNull] ScoreProcessor scoreProcessor, int[] userIds) : base(scoreProcessor, userIds) { } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorPlayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorPlayer.cs index 0101c00041..4ed949893c 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorPlayer.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorPlayer.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 JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Timing; @@ -11,13 +12,21 @@ using osu.Game.Screens.Play; namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate { + /// + /// A single spectated player within a . + /// public class MultiSpectatorPlayer : SpectatorPlayer { private readonly Bindable waitingOnFrames = new Bindable(true); private readonly Score score; private readonly ISlaveClock slaveClock; - public MultiSpectatorPlayer(Score score, ISlaveClock slaveClock) + /// + /// Creates a new . + /// + /// The score containing the player's replay. + /// The clock controlling the gameplay running state. + public MultiSpectatorPlayer([NotNull] Score score, [NotNull] ISlaveClock slaveClock) : base(score) { this.score = score; @@ -33,6 +42,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate protected override void UpdateAfterChildren() { base.UpdateAfterChildren(); + + // This is required because the frame stable clock is set to WaitingOnFrames = false for one frame. waitingOnFrames.Value = DrawableRuleset.FrameStableClock.WaitingOnFrames.Value || score.Replay.Frames.Count == 0; } @@ -41,14 +52,14 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate private class SlaveGameplayClockContainer : GameplayClockContainer { - public SlaveGameplayClockContainer(IClock sourceClock) + public SlaveGameplayClockContainer([NotNull] IClock sourceClock) : base(sourceClock) { } protected override void Update() { - // The slave clock's running state is controlled by the sync manager, but the local pausing state needs to be updated to stop gameplay. + // The slave clock's running state is controlled externally, but the local pausing state needs to be updated to stop gameplay. if (SourceClock.IsRunning) Start(); else diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorPlayerLoader.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorPlayerLoader.cs index fce44296f3..5a1d28e9c4 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorPlayerLoader.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorPlayerLoader.cs @@ -2,15 +2,19 @@ // See the LICENCE file in the repository root for full licence text. using System; +using JetBrains.Annotations; using osu.Game.Scoring; using osu.Game.Screens.Menu; using osu.Game.Screens.Play; namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate { + /// + /// Used to load a single in a . + /// public class MultiSpectatorPlayerLoader : SpectatorPlayerLoader { - public MultiSpectatorPlayerLoader(Score score, Func createPlayer) + public MultiSpectatorPlayerLoader([NotNull] Score score, [NotNull] Func createPlayer) : base(score, createPlayer) { } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs index 52b7898028..609905a312 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs @@ -14,27 +14,37 @@ using osu.Game.Screens.Spectate; namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate { + /// + /// A that spectates multiple users in a match. + /// public class MultiSpectatorScreen : SpectatorScreen { // Isolates beatmap/ruleset to this screen. public override bool DisallowExternalBeatmapRulesetChanges => true; + /// + /// Whether all spectating players have finished loading. + /// public bool AllPlayersLoaded => instances.All(p => p?.PlayerLoaded == true); [Resolved] private SpectatorStreamingClient spectatorClient { get; set; } - private readonly PlayerInstance[] instances; + private readonly PlayerArea[] instances; private MasterGameplayClockContainer masterClockContainer; private ISyncManager syncManager; private PlayerGrid grid; private MultiSpectatorLeaderboard leaderboard; - private PlayerInstance currentAudioSource; + private PlayerArea currentAudioSource; + /// + /// Creates a new . + /// + /// The players to spectate. public MultiSpectatorScreen(int[] userIds) : base(userIds.AsSpan().Slice(0, Math.Min(16, userIds.Length)).ToArray()) { - instances = new PlayerInstance[UserIds.Length]; + instances = new PlayerArea[UserIds.Length]; } [BackgroundDependencyLoader] @@ -69,7 +79,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate }; for (int i = 0; i < UserIds.Length; i++) - grid.Add(instances[i] = new PlayerInstance(UserIds[i], new CatchUpSlaveClock(masterClockContainer.GameplayClock))); + grid.Add(instances[i] = new PlayerArea(UserIds[i], new CatchUpSlaveClock(masterClockContainer.GameplayClock))); // Todo: This is not quite correct - it should be per-user to adjust for other mod combinations. var playableBeatmap = Beatmap.Value.GetPlayableBeatmap(Ruleset.Value); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerInstance.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs similarity index 83% rename from osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerInstance.cs rename to osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs index 8007c9e068..494f182251 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerInstance.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Bindables; @@ -18,13 +19,31 @@ using osu.Game.Screens.Play; namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate { - public class PlayerInstance : CompositeDrawable, IAdjustableAudioComponent + /// + /// Provides an area for and manages the hierarchy of a spectated player within a . + /// + public class PlayerArea : CompositeDrawable, IAdjustableAudioComponent { + /// + /// Whether a is loaded in the area. + /// public bool PlayerLoaded => stack?.CurrentScreen is Player; + /// + /// The user id this corresponds to. + /// public readonly int UserId; - public readonly CatchUpSlaveClock GameplayClock; + /// + /// The used to control the gameplay running state of a loaded . + /// + [NotNull] + public readonly ISlaveClock GameplayClock; + + /// + /// The currently-loaded score. + /// + [CanBeNull] public Score Score { get; private set; } [Resolved] @@ -35,7 +54,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate private readonly AudioContainer audioContainer; private OsuScreenStack stack; - public PlayerInstance(int userId, CatchUpSlaveClock gameplayClock) + public PlayerArea(int userId, [NotNull] ISlaveClock gameplayClock) { UserId = userId; GameplayClock = gameplayClock; @@ -54,10 +73,10 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate }; } - public void LoadScore(Score score) + public void LoadScore([NotNull] Score score) { if (Score != null) - throw new InvalidOperationException($"Cannot load a new score on a {nameof(PlayerInstance)} with an existing score."); + throw new InvalidOperationException($"Cannot load a new score on a {nameof(PlayerArea)} that has an existing score."); Score = score; From 90ecda91af351c248882469195f4a964dd2636bf Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 23 Apr 2021 00:06:54 +0900 Subject: [PATCH 052/205] Fix exception --- .../OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index c5d7610b53..aac03622e3 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -416,7 +416,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer UpdateMods(); if (client.LocalUser.State == MultiplayerUserState.Spectating - && (client.Room.State == MultiplayerRoomState.Playing || client.Room.State == MultiplayerRoomState.WaitingForLoad)) + && (client.Room.State == MultiplayerRoomState.Playing || client.Room.State == MultiplayerRoomState.WaitingForLoad) + && ParentScreen.IsCurrentScreen()) { StartPlay(); From b25340653df7e657884cfd077c53eea43613f25b Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 23 Apr 2021 00:49:14 +0900 Subject: [PATCH 053/205] Fix failing tests --- .../TestSceneMultiplayerSpectateButton.cs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs index e65e4a68a7..afbc9c32b3 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs @@ -120,9 +120,12 @@ namespace osu.Game.Tests.Visual.Multiplayer }; }); - [Test] - public void TestEnabledWhenRoomOpen() + [TestCase(MultiplayerRoomState.Open)] + [TestCase(MultiplayerRoomState.WaitingForLoad)] + [TestCase(MultiplayerRoomState.Playing)] + public void TestEnabledWhenRoomOpenOrInGameplay(MultiplayerRoomState roomState) { + AddStep($"change room to {roomState}", () => Client.ChangeRoomState(roomState)); assertSpectateButtonEnablement(true); } @@ -137,12 +140,10 @@ namespace osu.Game.Tests.Visual.Multiplayer AddAssert("user is idle", () => Client.Room?.Users[0].State == MultiplayerUserState.Idle); } - [TestCase(MultiplayerRoomState.WaitingForLoad)] - [TestCase(MultiplayerRoomState.Playing)] [TestCase(MultiplayerRoomState.Closed)] - public void TestDisabledDuringGameplayOrClosed(MultiplayerRoomState roomState) + public void TestDisabledWhenClosed(MultiplayerRoomState roomState) { - AddStep($"change user to {roomState}", () => Client.ChangeRoomState(roomState)); + AddStep($"change room to {roomState}", () => Client.ChangeRoomState(roomState)); assertSpectateButtonEnablement(false); } From 575ec7c528af97d00e7aa3e3fce940ae5e64350f Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 23 Apr 2021 19:09:54 +0900 Subject: [PATCH 054/205] Document + refactor max player limitation --- .../Multiplayer/Spectate/MultiSpectatorScreen.cs | 2 +- .../OnlinePlay/Multiplayer/Spectate/PlayerGrid.cs | 12 +++++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs index 609905a312..eb59d090be 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs @@ -42,7 +42,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate /// /// The players to spectate. public MultiSpectatorScreen(int[] userIds) - : base(userIds.AsSpan().Slice(0, Math.Min(16, userIds.Length)).ToArray()) + : base(userIds.Take(PlayerGrid.MAX_PLAYERS).ToArray()) { instances = new PlayerArea[UserIds.Length]; } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid.cs index 830378f129..6638d47dca 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid.cs @@ -15,6 +15,12 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate /// public partial class PlayerGrid : CompositeDrawable { + /// + /// A temporary limitation on the number of players, because only layouts up to 16 players are supported for a single screen. + /// Todo: Can be removed in the future with scrolling support + performance improvements. + /// + public const int MAX_PLAYERS = 16; + private const float player_spacing = 5; /// @@ -58,11 +64,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate /// Adds a new cell with content to this grid. /// /// The content the cell should contain. - /// If more than 16 cells are added. + /// If more than cells are added. public void Add(Drawable content) { - if (cellContainer.Count == 16) - throw new InvalidOperationException("Only 16 cells are supported."); + if (cellContainer.Count == MAX_PLAYERS) + throw new InvalidOperationException($"Only {MAX_PLAYERS} cells are supported."); int index = cellContainer.Count; From 63a948425513a3bba7c660850a5d562bf3efc3ec Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 23 Apr 2021 19:11:47 +0900 Subject: [PATCH 055/205] Expose WaitingOnFrames as mutable bindable --- osu.Game.Tests/OnlinePlay/TestCaseCatchUpSyncManager.cs | 3 +-- .../OnlinePlay/Multiplayer/Spectate/Sync/CatchUpSlaveClock.cs | 2 +- .../OnlinePlay/Multiplayer/Spectate/Sync/ISlaveClock.cs | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/OnlinePlay/TestCaseCatchUpSyncManager.cs b/osu.Game.Tests/OnlinePlay/TestCaseCatchUpSyncManager.cs index a8c0a763e9..2c10be9c90 100644 --- a/osu.Game.Tests/OnlinePlay/TestCaseCatchUpSyncManager.cs +++ b/osu.Game.Tests/OnlinePlay/TestCaseCatchUpSyncManager.cs @@ -164,8 +164,7 @@ namespace osu.Game.Tests.OnlinePlay private class TestSlaveClock : TestManualClock, ISlaveClock { - public readonly Bindable WaitingOnFrames = new Bindable(true); - IBindable ISlaveClock.WaitingOnFrames => WaitingOnFrames; + public Bindable WaitingOnFrames { get; } = new Bindable(true); public bool IsCatchingUp { get; set; } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/Sync/CatchUpSlaveClock.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/Sync/CatchUpSlaveClock.cs index cefe5ff04f..21241f8158 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/Sync/CatchUpSlaveClock.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/Sync/CatchUpSlaveClock.cs @@ -74,7 +74,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate.Sync public FrameTimeInfo TimeInfo => new FrameTimeInfo { Elapsed = ElapsedFrameTime, Current = CurrentTime }; - public IBindable WaitingOnFrames { get; } = new Bindable(true); + public Bindable WaitingOnFrames { get; } = new Bindable(true); public bool IsCatchingUp { get; set; } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/Sync/ISlaveClock.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/Sync/ISlaveClock.cs index 08d69f9560..6b7a4f5221 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/Sync/ISlaveClock.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/Sync/ISlaveClock.cs @@ -14,7 +14,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate.Sync /// /// Whether this clock is waiting on frames to continue playback. /// - IBindable WaitingOnFrames { get; } + Bindable WaitingOnFrames { get; } /// /// Whether this clock is resynchronising to the master clock. From b18635341e7692dfbfcbc7e3b35b0efd23a409e3 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 23 Apr 2021 19:12:30 +0900 Subject: [PATCH 056/205] Rename file --- ...CaseCatchUpSyncManager.cs => TestSceneCatchUpSyncManager.cs} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename osu.Game.Tests/OnlinePlay/{TestCaseCatchUpSyncManager.cs => TestSceneCatchUpSyncManager.cs} (99%) diff --git a/osu.Game.Tests/OnlinePlay/TestCaseCatchUpSyncManager.cs b/osu.Game.Tests/OnlinePlay/TestSceneCatchUpSyncManager.cs similarity index 99% rename from osu.Game.Tests/OnlinePlay/TestCaseCatchUpSyncManager.cs rename to osu.Game.Tests/OnlinePlay/TestSceneCatchUpSyncManager.cs index 2c10be9c90..93801bd5d7 100644 --- a/osu.Game.Tests/OnlinePlay/TestCaseCatchUpSyncManager.cs +++ b/osu.Game.Tests/OnlinePlay/TestSceneCatchUpSyncManager.cs @@ -12,7 +12,7 @@ using osu.Game.Tests.Visual; namespace osu.Game.Tests.OnlinePlay { [HeadlessTest] - public class TestCaseCatchUpSyncManager : OsuTestScene + public class TestSceneCatchUpSyncManager : OsuTestScene { private TestManualClock master; private CatchUpSyncManager syncManager; From b41897fd9b34e09ba252638c75d0173e61f7c94a Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 23 Apr 2021 19:23:52 +0900 Subject: [PATCH 057/205] Rename testscene to match class --- ...MultiplayerSpectator.cs => TestSceneMultiSpectatorScreen.cs} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename osu.Game.Tests/Visual/Multiplayer/{TestSceneMultiplayerSpectator.cs => TestSceneMultiSpectatorScreen.cs} (99%) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectator.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs similarity index 99% rename from osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectator.cs rename to osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs index a82dea5640..9cc8eaf876 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectator.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs @@ -23,7 +23,7 @@ using osu.Game.Users; namespace osu.Game.Tests.Visual.Multiplayer { - public class TestSceneMultiplayerSpectator : MultiplayerTestScene + public class TestSceneMultiSpectatorScreen : MultiplayerTestScene { [Cached(typeof(SpectatorStreamingClient))] private TestSpectatorStreamingClient streamingClient = new TestSpectatorStreamingClient(); From aa99c192d025240f6571eb87f856d3235e07e91c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 26 Apr 2021 16:21:12 +0900 Subject: [PATCH 058/205] Fix type in inline comment --- .../Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index aac03622e3..0cd3b448d2 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -421,7 +421,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer { StartPlay(); - // If the current user was host, they started the match and the in-progres operation needs to be stopped now. + // If the current user was host, they started the match and the in-progress operation needs to be stopped now. readyClickOperation?.Dispose(); readyClickOperation = null; } From 6d30a1a80f5f7e19893a6340c23aca44b0f20a47 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 26 Apr 2021 16:45:20 +0900 Subject: [PATCH 059/205] Reference constant for test startup delay --- .../Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs index 9cc8eaf876..82bbc7b6d3 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs @@ -17,6 +17,7 @@ using osu.Game.Online.Spectator; using osu.Game.Replays.Legacy; using osu.Game.Scoring; using osu.Game.Screens.OnlinePlay.Multiplayer.Spectate; +using osu.Game.Screens.OnlinePlay.Multiplayer.Spectate.Sync; using osu.Game.Screens.Play; using osu.Game.Tests.Beatmaps.IO; using osu.Game.Users; @@ -128,7 +129,7 @@ namespace osu.Game.Tests.Visual.Multiplayer } [Test] - public void TestPlayersDoNotStartSimultaneouslyIfBufferingFor15Seconds() + public void TestPlayersDoNotStartSimultaneouslyIfBufferingForMaximumStartDelay() { start(new[] { 55, 56 }); loadSpectateScreen(); @@ -138,8 +139,8 @@ namespace osu.Game.Tests.Visual.Multiplayer checkPausedInstant(55, true); checkPausedInstant(56, true); - // Wait 15 seconds... - AddWaitStep("wait 15 seconds", (int)(15000 / TimePerAction)); + // Wait for the start delay seconds... + AddWaitStep("wait maximum start delay seconds", (int)(CatchUpSyncManager.MAXIMUM_START_DELAY / TimePerAction)); // Player 1 should start playing by itself, player 2 should remain paused. checkPausedInstant(55, false); From 55f383c71ea4577017c85b229bfc1127434b229a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 26 Apr 2021 16:48:32 +0900 Subject: [PATCH 060/205] Rename test to match new `MultiSpectatorLeaderboard` class name --- ...orLeaderboard.cs => TestSceneMultiSpectatorLeaderboard.cs} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename osu.Game.Tests/Visual/Multiplayer/{TestSceneMultiplayerSpectatorLeaderboard.cs => TestSceneMultiSpectatorLeaderboard.cs} (98%) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectatorLeaderboard.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorLeaderboard.cs similarity index 98% rename from osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectatorLeaderboard.cs rename to osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorLeaderboard.cs index 8368186219..da76adf83f 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectatorLeaderboard.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorLeaderboard.cs @@ -24,7 +24,7 @@ using osu.Game.Users; namespace osu.Game.Tests.Visual.Multiplayer { - public class TestSceneMultiplayerSpectatorLeaderboard : MultiplayerTestScene + public class TestSceneMultiSpectatorLeaderboard : MultiplayerTestScene { [Cached(typeof(SpectatorStreamingClient))] private TestSpectatorStreamingClient streamingClient = new TestSpectatorStreamingClient(); @@ -41,7 +41,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { 56, new ManualClock() } }; - public TestSceneMultiplayerSpectatorLeaderboard() + public TestSceneMultiSpectatorLeaderboard() { base.Content.AddRange(new Drawable[] { From 737a15c2d4c549423d763f29b93680a1391606a5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 26 Apr 2021 17:04:39 +0900 Subject: [PATCH 061/205] Extract out test player IDs to constants --- .../TestSceneMultiSpectatorScreen.cs | 133 +++++++++--------- 1 file changed, 68 insertions(+), 65 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs index 82bbc7b6d3..512311ff03 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs @@ -47,6 +47,9 @@ namespace osu.Game.Tests.Visual.Multiplayer private BeatmapInfo importedBeatmap; private int importedBeatmapId; + private const int player_1_id = 55; + private const int player_2_id = 56; + [BackgroundDependencyLoader] private void load() { @@ -80,29 +83,29 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("start players silently", () => { - Client.CurrentMatchPlayingUserIds.Add(55); - Client.CurrentMatchPlayingUserIds.Add(56); - playingUserIds.Add(55); - playingUserIds.Add(56); - nextFrame[55] = 0; - nextFrame[56] = 0; + Client.CurrentMatchPlayingUserIds.Add(player_1_id); + Client.CurrentMatchPlayingUserIds.Add(player_2_id); + playingUserIds.Add(player_1_id); + playingUserIds.Add(player_2_id); + nextFrame[player_1_id] = 0; + nextFrame[player_2_id] = 0; }); loadSpectateScreen(false); AddWaitStep("wait a bit", 10); - AddStep("load player 55", () => streamingClient.StartPlay(55, importedBeatmapId)); + AddStep("load player first_player_id", () => streamingClient.StartPlay(player_1_id, importedBeatmapId)); AddUntilStep("one player added", () => spectatorScreen.ChildrenOfType().Count() == 1); AddWaitStep("wait a bit", 10); - AddStep("load player 56", () => streamingClient.StartPlay(56, importedBeatmapId)); + AddStep("load player second_player_id", () => streamingClient.StartPlay(player_2_id, importedBeatmapId)); AddUntilStep("two players added", () => spectatorScreen.ChildrenOfType().Count() == 2); } [Test] public void TestGeneral() { - int[] userIds = Enumerable.Range(0, 4).Select(i => 55 + i).ToArray(); + int[] userIds = Enumerable.Range(0, 4).Select(i => player_1_id + i).ToArray(); start(userIds); loadSpectateScreen(); @@ -114,123 +117,123 @@ namespace osu.Game.Tests.Visual.Multiplayer [Test] public void TestPlayersMustStartSimultaneously() { - start(new[] { 55, 56 }); + start(new[] { player_1_id, player_2_id }); loadSpectateScreen(); // Send frames for one player only, both should remain paused. - sendFrames(55, 20); - checkPausedInstant(55, true); - checkPausedInstant(56, true); + sendFrames(player_1_id, 20); + checkPausedInstant(player_1_id, true); + checkPausedInstant(player_2_id, true); // Send frames for the other player, both should now start playing. - sendFrames(56, 20); - checkPausedInstant(55, false); - checkPausedInstant(56, false); + sendFrames(player_2_id, 20); + checkPausedInstant(player_1_id, false); + checkPausedInstant(player_2_id, false); } [Test] public void TestPlayersDoNotStartSimultaneouslyIfBufferingForMaximumStartDelay() { - start(new[] { 55, 56 }); + start(new[] { player_1_id, player_2_id }); loadSpectateScreen(); // Send frames for one player only, both should remain paused. - sendFrames(55, 1000); - checkPausedInstant(55, true); - checkPausedInstant(56, true); + sendFrames(player_1_id, 1000); + checkPausedInstant(player_1_id, true); + checkPausedInstant(player_2_id, true); // Wait for the start delay seconds... AddWaitStep("wait maximum start delay seconds", (int)(CatchUpSyncManager.MAXIMUM_START_DELAY / TimePerAction)); // Player 1 should start playing by itself, player 2 should remain paused. - checkPausedInstant(55, false); - checkPausedInstant(56, true); + checkPausedInstant(player_1_id, false); + checkPausedInstant(player_2_id, true); } [Test] public void TestPlayersContinueWhileOthersBuffer() { - start(new[] { 55, 56 }); + start(new[] { player_1_id, player_2_id }); loadSpectateScreen(); // Send initial frames for both players. A few more for player 1. - sendFrames(55, 20); - sendFrames(56, 10); - checkPausedInstant(55, false); - checkPausedInstant(56, false); + sendFrames(player_1_id, 20); + sendFrames(player_2_id, 10); + checkPausedInstant(player_1_id, false); + checkPausedInstant(player_2_id, false); // Eventually player 2 will pause, player 1 must remain running. - checkPaused(56, true); - checkPausedInstant(55, false); + checkPaused(player_2_id, true); + checkPausedInstant(player_1_id, false); // Eventually both players will run out of frames and should pause. - checkPaused(55, true); - checkPausedInstant(56, true); + checkPaused(player_1_id, true); + checkPausedInstant(player_2_id, true); // Send more frames for the first player only. Player 1 should start playing with player 2 remaining paused. - sendFrames(55, 20); - checkPausedInstant(56, true); - checkPausedInstant(55, false); + sendFrames(player_1_id, 20); + checkPausedInstant(player_2_id, true); + checkPausedInstant(player_1_id, false); // Send more frames for the second player. Both should be playing - sendFrames(56, 20); - checkPausedInstant(56, false); - checkPausedInstant(55, false); + sendFrames(player_2_id, 20); + checkPausedInstant(player_2_id, false); + checkPausedInstant(player_1_id, false); } [Test] public void TestPlayersCatchUpAfterFallingBehind() { - start(new[] { 55, 56 }); + start(new[] { player_1_id, player_2_id }); loadSpectateScreen(); // Send initial frames for both players. A few more for player 1. - sendFrames(55, 1000); - sendFrames(56, 10); - checkPausedInstant(55, false); - checkPausedInstant(56, false); + sendFrames(player_1_id, 1000); + sendFrames(player_2_id, 10); + checkPausedInstant(player_1_id, false); + checkPausedInstant(player_2_id, false); // Eventually player 2 will run out of frames and should pause. - checkPaused(56, true); + checkPaused(player_2_id, true); AddWaitStep("wait a few more frames", 10); // Send more frames for player 2. It should unpause. - sendFrames(56, 1000); - checkPausedInstant(56, false); + sendFrames(player_2_id, 1000); + checkPausedInstant(player_2_id, false); // Player 2 should catch up to player 1 after unpausing. - waitForCatchup(56); + waitForCatchup(player_2_id); AddWaitStep("wait a bit", 10); } [Test] public void TestMostInSyncUserIsAudioSource() { - start(new[] { 55, 56 }); + start(new[] { player_1_id, player_2_id }); loadSpectateScreen(); - assertVolume(55, 0); - assertVolume(56, 0); + assertVolume(player_1_id, 0); + assertVolume(player_2_id, 0); - sendFrames(55, 10); - sendFrames(56, 20); - assertVolume(55, 1); - assertVolume(56, 0); + sendFrames(player_1_id, 10); + sendFrames(player_2_id, 20); + assertVolume(player_1_id, 1); + assertVolume(player_2_id, 0); - checkPaused(55, true); - assertVolume(55, 0); - assertVolume(56, 1); + checkPaused(player_1_id, true); + assertVolume(player_1_id, 0); + assertVolume(player_2_id, 1); - sendFrames(55, 100); - waitForCatchup(55); - checkPaused(56, true); - assertVolume(55, 1); - assertVolume(56, 0); + sendFrames(player_1_id, 100); + waitForCatchup(player_1_id); + checkPaused(player_2_id, true); + assertVolume(player_1_id, 1); + assertVolume(player_2_id, 0); - sendFrames(56, 100); - waitForCatchup(56); - assertVolume(55, 1); - assertVolume(56, 0); + sendFrames(player_2_id, 100); + waitForCatchup(player_2_id); + assertVolume(player_1_id, 1); + assertVolume(player_2_id, 0); } private void loadSpectateScreen(bool waitForPlayerLoad = true) From 5b4cb71cc782e4f8ddc2d9e1d6872ef0787f4caf Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 26 Apr 2021 17:19:44 +0900 Subject: [PATCH 062/205] Change terminology from "slave" to "player clock" --- .../OnlinePlay/TestSceneCatchUpSyncManager.cs | 104 +++++++++--------- .../Spectate/MultiSpectatorPlayer.cs | 18 +-- .../Spectate/MultiSpectatorScreen.cs | 8 +- .../Multiplayer/Spectate/PlayerArea.cs | 6 +- ...lock.cs => CatchUpSpectatorPlayerClock.cs} | 6 +- .../Spectate/Sync/CatchUpSyncManager.cs | 74 ++++++------- ...SlaveClock.cs => ISpectatorPlayerClock.cs} | 2 +- .../Multiplayer/Spectate/Sync/ISyncManager.cs | 18 +-- 8 files changed, 118 insertions(+), 118 deletions(-) rename osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/Sync/{CatchUpSlaveClock.cs => CatchUpSpectatorPlayerClock.cs} (90%) rename osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/Sync/{ISlaveClock.cs => ISpectatorPlayerClock.cs} (90%) diff --git a/osu.Game.Tests/OnlinePlay/TestSceneCatchUpSyncManager.cs b/osu.Game.Tests/OnlinePlay/TestSceneCatchUpSyncManager.cs index 93801bd5d7..73ec5fb934 100644 --- a/osu.Game.Tests/OnlinePlay/TestSceneCatchUpSyncManager.cs +++ b/osu.Game.Tests/OnlinePlay/TestSceneCatchUpSyncManager.cs @@ -17,31 +17,31 @@ namespace osu.Game.Tests.OnlinePlay private TestManualClock master; private CatchUpSyncManager syncManager; - private TestSlaveClock slave1; - private TestSlaveClock slave2; + private TestSpectatorPlayerClock player1; + private TestSpectatorPlayerClock player2; [SetUp] public void Setup() { syncManager = new CatchUpSyncManager(master = new TestManualClock()); - syncManager.AddSlave(slave1 = new TestSlaveClock(1)); - syncManager.AddSlave(slave2 = new TestSlaveClock(2)); + syncManager.AddPlayerClock(player1 = new TestSpectatorPlayerClock(1)); + syncManager.AddPlayerClock(player2 = new TestSpectatorPlayerClock(2)); Schedule(() => Child = syncManager); } [Test] - public void TestMasterClockStartsWhenAllSlavesHaveFrames() + public void TestMasterClockStartsWhenAllPlayerClocksHaveFrames() { - setWaiting(() => slave1, false); + setWaiting(() => player1, false); assertMasterState(false); - assertSlaveState(() => slave1, false); - assertSlaveState(() => slave2, false); + assertPlayerClockState(() => player1, false); + assertPlayerClockState(() => player2, false); - setWaiting(() => slave2, false); + setWaiting(() => player2, false); assertMasterState(true); - assertSlaveState(() => slave1, true); - assertSlaveState(() => slave2, true); + assertPlayerClockState(() => player1, true); + assertPlayerClockState(() => player2, true); } [Test] @@ -54,115 +54,115 @@ namespace osu.Game.Tests.OnlinePlay [Test] public void TestMasterClockStartsWhenAnyReadyForMaximumDelayTime() { - setWaiting(() => slave1, false); + setWaiting(() => player1, false); AddWaitStep($"wait {CatchUpSyncManager.MAXIMUM_START_DELAY} milliseconds", (int)Math.Ceiling(CatchUpSyncManager.MAXIMUM_START_DELAY / TimePerAction)); assertMasterState(true); } [Test] - public void TestSlaveDoesNotCatchUpWhenSlightlyOutOfSync() + public void TestPlayerClockDoesNotCatchUpWhenSlightlyOutOfSync() { setAllWaiting(false); setMasterTime(CatchUpSyncManager.SYNC_TARGET + 1); - assertCatchingUp(() => slave1, false); + assertCatchingUp(() => player1, false); } [Test] - public void TestSlaveStartsCatchingUpWhenTooFarBehind() + public void TestPlayerClockStartsCatchingUpWhenTooFarBehind() { setAllWaiting(false); setMasterTime(CatchUpSyncManager.MAX_SYNC_OFFSET + 1); - assertCatchingUp(() => slave1, true); - assertCatchingUp(() => slave2, true); + assertCatchingUp(() => player1, true); + assertCatchingUp(() => player2, true); } [Test] - public void TestSlaveKeepsCatchingUpWhenSlightlyOutOfSync() + public void TestPlayerClockKeepsCatchingUpWhenSlightlyOutOfSync() { setAllWaiting(false); setMasterTime(CatchUpSyncManager.MAX_SYNC_OFFSET + 1); - setSlaveTime(() => slave1, CatchUpSyncManager.SYNC_TARGET + 1); - assertCatchingUp(() => slave1, true); + setPlayerClockTime(() => player1, CatchUpSyncManager.SYNC_TARGET + 1); + assertCatchingUp(() => player1, true); } [Test] - public void TestSlaveStopsCatchingUpWhenInSync() + public void TestPlayerClockStopsCatchingUpWhenInSync() { setAllWaiting(false); setMasterTime(CatchUpSyncManager.MAX_SYNC_OFFSET + 2); - setSlaveTime(() => slave1, CatchUpSyncManager.SYNC_TARGET); - assertCatchingUp(() => slave1, false); - assertCatchingUp(() => slave2, true); + setPlayerClockTime(() => player1, CatchUpSyncManager.SYNC_TARGET); + assertCatchingUp(() => player1, false); + assertCatchingUp(() => player2, true); } [Test] - public void TestSlaveDoesNotStopWhenSlightlyAhead() + public void TestPlayerClockDoesNotStopWhenSlightlyAhead() { setAllWaiting(false); - setSlaveTime(() => slave1, -CatchUpSyncManager.SYNC_TARGET); - assertCatchingUp(() => slave1, false); - assertSlaveState(() => slave1, true); + setPlayerClockTime(() => player1, -CatchUpSyncManager.SYNC_TARGET); + assertCatchingUp(() => player1, false); + assertPlayerClockState(() => player1, true); } [Test] - public void TestSlaveStopsWhenTooFarAheadAndStartsWhenBackInSync() + public void TestPlayerClockStopsWhenTooFarAheadAndStartsWhenBackInSync() { setAllWaiting(false); - setSlaveTime(() => slave1, -CatchUpSyncManager.SYNC_TARGET - 1); + setPlayerClockTime(() => player1, -CatchUpSyncManager.SYNC_TARGET - 1); // This is a silent catchup, where IsCatchingUp = false but IsRunning = false also. - assertCatchingUp(() => slave1, false); - assertSlaveState(() => slave1, false); + assertCatchingUp(() => player1, false); + assertPlayerClockState(() => player1, false); setMasterTime(1); - assertCatchingUp(() => slave1, false); - assertSlaveState(() => slave1, true); + assertCatchingUp(() => player1, false); + assertPlayerClockState(() => player1, true); } [Test] - public void TestInSyncSlaveDoesNotStartIfWaitingOnFrames() + public void TestInSyncPlayerClockDoesNotStartIfWaitingOnFrames() { setAllWaiting(false); - assertSlaveState(() => slave1, true); - setWaiting(() => slave1, true); - assertSlaveState(() => slave1, false); + assertPlayerClockState(() => player1, true); + setWaiting(() => player1, true); + assertPlayerClockState(() => player1, false); } - private void setWaiting(Func slave, bool waiting) - => AddStep($"set slave {slave().Id} waiting = {waiting}", () => slave().WaitingOnFrames.Value = waiting); + private void setWaiting(Func playerClock, bool waiting) + => AddStep($"set player clock {playerClock().Id} waiting = {waiting}", () => playerClock().WaitingOnFrames.Value = waiting); - private void setAllWaiting(bool waiting) => AddStep($"set all slaves waiting = {waiting}", () => + private void setAllWaiting(bool waiting) => AddStep($"set all player clocks waiting = {waiting}", () => { - slave1.WaitingOnFrames.Value = waiting; - slave2.WaitingOnFrames.Value = waiting; + player1.WaitingOnFrames.Value = waiting; + player2.WaitingOnFrames.Value = waiting; }); private void setMasterTime(double time) => AddStep($"set master = {time}", () => master.Seek(time)); /// - /// slave.Time = master.Time - offsetFromMaster + /// clock.Time = master.Time - offsetFromMaster /// - private void setSlaveTime(Func slave, double offsetFromMaster) - => AddStep($"set slave {slave().Id} = master - {offsetFromMaster}", () => slave().Seek(master.CurrentTime - offsetFromMaster)); + private void setPlayerClockTime(Func playerClock, double offsetFromMaster) + => AddStep($"set player clock {playerClock().Id} = master - {offsetFromMaster}", () => playerClock().Seek(master.CurrentTime - offsetFromMaster)); private void assertMasterState(bool running) => AddAssert($"master clock {(running ? "is" : "is not")} running", () => master.IsRunning == running); - private void assertCatchingUp(Func slave, bool catchingUp) => - AddAssert($"slave {slave().Id} {(catchingUp ? "is" : "is not")} catching up", () => slave().IsCatchingUp == catchingUp); + private void assertCatchingUp(Func playerClock, bool catchingUp) => + AddAssert($"player clock {playerClock().Id} {(catchingUp ? "is" : "is not")} catching up", () => playerClock().IsCatchingUp == catchingUp); - private void assertSlaveState(Func slave, bool running) - => AddAssert($"slave {slave().Id} {(running ? "is" : "is not")} running", () => slave().IsRunning == running); + private void assertPlayerClockState(Func playerClock, bool running) + => AddAssert($"player clock {playerClock().Id} {(running ? "is" : "is not")} running", () => playerClock().IsRunning == running); - private class TestSlaveClock : TestManualClock, ISlaveClock + private class TestSpectatorPlayerClock : TestManualClock, ISpectatorPlayerClock { public Bindable WaitingOnFrames { get; } = new Bindable(true); @@ -170,7 +170,7 @@ namespace osu.Game.Tests.OnlinePlay public readonly int Id; - public TestSlaveClock(int id) + public TestSpectatorPlayerClock(int id) { Id = id; diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorPlayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorPlayer.cs index 4ed949893c..a17519c3aa 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorPlayer.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorPlayer.cs @@ -19,24 +19,24 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate { private readonly Bindable waitingOnFrames = new Bindable(true); private readonly Score score; - private readonly ISlaveClock slaveClock; + private readonly ISpectatorPlayerClock spectatorPlayerClock; /// /// Creates a new . /// /// The score containing the player's replay. - /// The clock controlling the gameplay running state. - public MultiSpectatorPlayer([NotNull] Score score, [NotNull] ISlaveClock slaveClock) + /// The clock controlling the gameplay running state. + public MultiSpectatorPlayer([NotNull] Score score, [NotNull] ISpectatorPlayerClock spectatorPlayerClock) : base(score) { this.score = score; - this.slaveClock = slaveClock; + this.spectatorPlayerClock = spectatorPlayerClock; } [BackgroundDependencyLoader] private void load() { - slaveClock.WaitingOnFrames.BindTo(waitingOnFrames); + spectatorPlayerClock.WaitingOnFrames.BindTo(waitingOnFrames); } protected override void UpdateAfterChildren() @@ -48,18 +48,18 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate } protected override GameplayClockContainer CreateGameplayClockContainer(WorkingBeatmap beatmap, double gameplayStart) - => new SlaveGameplayClockContainer(slaveClock); + => new SpectatorGameplayClockContainer(spectatorPlayerClock); - private class SlaveGameplayClockContainer : GameplayClockContainer + private class SpectatorGameplayClockContainer : GameplayClockContainer { - public SlaveGameplayClockContainer([NotNull] IClock sourceClock) + public SpectatorGameplayClockContainer([NotNull] IClock sourceClock) : base(sourceClock) { } protected override void Update() { - // The slave clock's running state is controlled externally, but the local pausing state needs to be updated to stop gameplay. + // The player clock's running state is controlled externally, but the local pausing state needs to be updated to stop gameplay. if (SourceClock.IsRunning) Start(); else diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs index eb59d090be..6a8c10e336 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs @@ -79,7 +79,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate }; for (int i = 0; i < UserIds.Length; i++) - grid.Add(instances[i] = new PlayerArea(UserIds[i], new CatchUpSlaveClock(masterClockContainer.GameplayClock))); + grid.Add(instances[i] = new PlayerArea(UserIds[i], new CatchUpSpectatorPlayerClock(masterClockContainer.GameplayClock))); // Todo: This is not quite correct - it should be per-user to adjust for other mod combinations. var playableBeatmap = Beatmap.Value.GetPlayableBeatmap(Ruleset.Value); @@ -104,7 +104,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate if (!isCandidateAudioSource(currentAudioSource?.GameplayClock)) { currentAudioSource = instances.Where(i => isCandidateAudioSource(i.GameplayClock)) - .OrderBy(i => Math.Abs(i.GameplayClock.CurrentTime - syncManager.Master.CurrentTime)) + .OrderBy(i => Math.Abs(i.GameplayClock.CurrentTime - syncManager.MasterClock.CurrentTime)) .FirstOrDefault(); foreach (var instance in instances) @@ -112,7 +112,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate } } - private bool isCandidateAudioSource([CanBeNull] ISlaveClock clock) + private bool isCandidateAudioSource([CanBeNull] ISpectatorPlayerClock clock) => clock?.IsRunning == true && !clock.IsCatchingUp && !clock.WaitingOnFrames.Value; protected override void OnUserStateChanged(int userId, SpectatorState spectatorState) @@ -124,7 +124,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate var instance = instances[getIndexForUser(userId)]; instance.LoadScore(gameplayState.Score); - syncManager.AddSlave(instance.GameplayClock); + syncManager.AddPlayerClock(instance.GameplayClock); leaderboard.AddClock(instance.UserId, instance.GameplayClock); } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs index 494f182251..2accfaec0c 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs @@ -35,10 +35,10 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate public readonly int UserId; /// - /// The used to control the gameplay running state of a loaded . + /// The used to control the gameplay running state of a loaded . /// [NotNull] - public readonly ISlaveClock GameplayClock; + public readonly ISpectatorPlayerClock GameplayClock; /// /// The currently-loaded score. @@ -54,7 +54,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate private readonly AudioContainer audioContainer; private OsuScreenStack stack; - public PlayerArea(int userId, [NotNull] ISlaveClock gameplayClock) + public PlayerArea(int userId, [NotNull] ISpectatorPlayerClock gameplayClock) { UserId = userId; GameplayClock = gameplayClock; diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/Sync/CatchUpSlaveClock.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/Sync/CatchUpSpectatorPlayerClock.cs similarity index 90% rename from osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/Sync/CatchUpSlaveClock.cs rename to osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/Sync/CatchUpSpectatorPlayerClock.cs index 21241f8158..a8ed7b415f 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/Sync/CatchUpSlaveClock.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/Sync/CatchUpSpectatorPlayerClock.cs @@ -8,9 +8,9 @@ using osu.Framework.Timing; namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate.Sync { /// - /// A which catches up using rate adjustment. + /// A which catches up using rate adjustment. /// - public class CatchUpSlaveClock : ISlaveClock + public class CatchUpSpectatorPlayerClock : ISpectatorPlayerClock { /// /// The catch up rate. @@ -19,7 +19,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate.Sync private readonly IFrameBasedClock masterClock; - public CatchUpSlaveClock(IFrameBasedClock masterClock) + public CatchUpSpectatorPlayerClock(IFrameBasedClock masterClock) { this.masterClock = masterClock; } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/Sync/CatchUpSyncManager.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/Sync/CatchUpSyncManager.cs index 1d3fcd824c..f5c780e8b1 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/Sync/CatchUpSyncManager.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/Sync/CatchUpSyncManager.cs @@ -9,46 +9,46 @@ using osu.Framework.Timing; namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate.Sync { /// - /// A which synchronises de-synced slave clocks through catchup. + /// A which synchronises de-synced player clocks through catchup. /// public class CatchUpSyncManager : Component, ISyncManager { /// - /// The offset from the master clock to which slaves should be synchronised to. + /// The offset from the master clock to which player clocks should be synchronised to. /// public const double SYNC_TARGET = 16; /// - /// The offset from the master clock at which slaves begin resynchronising. + /// The offset from the master clock at which player clocks begin resynchronising. /// public const double MAX_SYNC_OFFSET = 50; /// - /// The maximum delay to start gameplay, if any (but not all) slaves are ready. + /// The maximum delay to start gameplay, if any (but not all) player clocks are ready. /// public const double MAXIMUM_START_DELAY = 15000; /// - /// The master clock which is used to control the timing of all slave clocks. + /// The master clock which is used to control the timing of all player clocks clocks. /// - public IAdjustableClock Master { get; } + public IAdjustableClock MasterClock { get; } /// - /// The slave clocks. + /// The player clocks. /// - private readonly List slaves = new List(); + private readonly List playerClocks = new List(); private bool hasStarted; private double? firstStartAttemptTime; public CatchUpSyncManager(IAdjustableClock master) { - Master = master; + MasterClock = master; } - public void AddSlave(ISlaveClock clock) => slaves.Add(clock); + public void AddPlayerClock(ISpectatorPlayerClock clock) => playerClocks.Add(clock); - public void RemoveSlave(ISlaveClock clock) => slaves.Remove(clock); + public void RemovePlayerClock(ISpectatorPlayerClock clock) => playerClocks.Remove(clock); protected override void Update() { @@ -56,9 +56,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate.Sync if (!attemptStart()) { - // Ensure all slaves are stopped until the start succeeds. - foreach (var slave in slaves) - slave.Stop(); + // Ensure all player clocks are stopped until the start succeeds. + foreach (var clock in playerClocks) + clock.Stop(); return; } @@ -67,7 +67,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate.Sync } /// - /// Attempts to start playback. Awaits for all slaves to have available frames for up to milliseconds. + /// Attempts to start playback. Waits for all player clocks to have available frames for up to milliseconds. /// /// Whether playback was started and syncing should occur. private bool attemptStart() @@ -75,14 +75,14 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate.Sync if (hasStarted) return true; - if (slaves.Count == 0) + if (playerClocks.Count == 0) return false; firstStartAttemptTime ??= Time.Current; - int readyCount = slaves.Count(s => !s.WaitingOnFrames.Value); + int readyCount = playerClocks.Count(s => !s.WaitingOnFrames.Value); - if (readyCount == slaves.Count) + if (readyCount == playerClocks.Count) return hasStarted = true; if (readyCount > 0 && (Time.Current - firstStartAttemptTime) > MAXIMUM_START_DELAY) @@ -92,38 +92,38 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate.Sync } /// - /// Updates the catchup states of all slave clocks. + /// Updates the catchup states of all player clocks clocks. /// private void updateCatchup() { - for (int i = 0; i < slaves.Count; i++) + for (int i = 0; i < playerClocks.Count; i++) { - var slave = slaves[i]; - double timeDelta = Master.CurrentTime - slave.CurrentTime; + var clock = playerClocks[i]; + double timeDelta = MasterClock.CurrentTime - clock.CurrentTime; - // Check that the slave isn't too far ahead. - // This is a quiet case in which the catchup is done by the master clock, so IsCatchingUp is not set on the slave. + // Check that the player clock isn't too far ahead. + // This is a quiet case in which the catchup is done by the master clock, so IsCatchingUp is not set on the player clock. if (timeDelta < -SYNC_TARGET) { - slave.Stop(); + clock.Stop(); continue; } - // Make sure the slave is running if it can. - if (!slave.WaitingOnFrames.Value) - slave.Start(); + // Make sure the player clock is running if it can. + if (!clock.WaitingOnFrames.Value) + clock.Start(); - if (slave.IsCatchingUp) + if (clock.IsCatchingUp) { - // Stop the slave from catching up if it's within the sync target. + // Stop the player clock from catching up if it's within the sync target. if (timeDelta <= SYNC_TARGET) - slave.IsCatchingUp = false; + clock.IsCatchingUp = false; } else { - // Make the slave start catching up if it's exceeded the maximum allowable sync offset. + // Make the player clock start catching up if it's exceeded the maximum allowable sync offset. if (timeDelta > MAX_SYNC_OFFSET) - slave.IsCatchingUp = true; + clock.IsCatchingUp = true; } } } @@ -133,14 +133,14 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate.Sync /// private void updateMasterClock() { - bool anyInSync = slaves.Any(s => !s.IsCatchingUp); + bool anyInSync = playerClocks.Any(s => !s.IsCatchingUp); - if (Master.IsRunning != anyInSync) + if (MasterClock.IsRunning != anyInSync) { if (anyInSync) - Master.Start(); + MasterClock.Start(); else - Master.Stop(); + MasterClock.Stop(); } } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/Sync/ISlaveClock.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/Sync/ISpectatorPlayerClock.cs similarity index 90% rename from osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/Sync/ISlaveClock.cs rename to osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/Sync/ISpectatorPlayerClock.cs index 6b7a4f5221..46f80288c6 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/Sync/ISlaveClock.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/Sync/ISpectatorPlayerClock.cs @@ -9,7 +9,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate.Sync /// /// A clock which is used by s and managed by an . /// - public interface ISlaveClock : IFrameBasedClock, IAdjustableClock + public interface ISpectatorPlayerClock : IFrameBasedClock, IAdjustableClock { /// /// Whether this clock is waiting on frames to continue playback. diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/Sync/ISyncManager.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/Sync/ISyncManager.cs index d63ac7e1ca..1b83ea8cf3 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/Sync/ISyncManager.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/Sync/ISyncManager.cs @@ -6,25 +6,25 @@ using osu.Framework.Timing; namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate.Sync { /// - /// Manages the synchronisation between one or more s in relation to a master clock. + /// Manages the synchronisation between one or more s in relation to a master clock. /// public interface ISyncManager { /// - /// The master clock which slaves should synchronise to. + /// The master clock which player clocks should synchronise to. /// - IAdjustableClock Master { get; } + IAdjustableClock MasterClock { get; } /// - /// Adds an to manage. + /// Adds an to manage. /// - /// The to add. - void AddSlave(ISlaveClock clock); + /// The to add. + void AddPlayerClock(ISpectatorPlayerClock clock); /// - /// Removes an , stopping it from being managed by this . + /// Removes an , stopping it from being managed by this . /// - /// The to remove. - void RemoveSlave(ISlaveClock clock); + /// The to remove. + void RemovePlayerClock(ISpectatorPlayerClock clock); } } From 120fb8974df0c4a4fcf390e7aaf8ab94d021aa2a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 26 Apr 2021 17:22:16 +0900 Subject: [PATCH 063/205] Combine more instances of test player IDs --- .../StatefulMultiplayerClientTest.cs | 4 +- .../Visual/Gameplay/TestSceneSpectator.cs | 3 +- .../TestSceneMultiSpectatorLeaderboard.cs | 42 +++--- .../TestSceneMultiSpectatorScreen.cs | 133 +++++++++--------- .../TestSceneMultiplayerMatchSubScreen.cs | 4 +- .../TestSceneMultiplayerSpectateButton.cs | 10 +- .../Multiplayer/MultiplayerTestScene.cs | 3 + 7 files changed, 100 insertions(+), 99 deletions(-) diff --git a/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs b/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs index 377a33b527..14589f8e6c 100644 --- a/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs +++ b/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs @@ -53,9 +53,9 @@ namespace osu.Game.Tests.NonVisual.Multiplayer Client.RoomSetupAction = room => { room.State = MultiplayerRoomState.Playing; - room.Users.Add(new MultiplayerRoomUser(55) + room.Users.Add(new MultiplayerRoomUser(PLAYER_1_ID) { - User = new User { Id = 55 }, + User = new User { Id = PLAYER_1_ID }, State = MultiplayerUserState.Playing }); }; diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs index 74ce66096e..0777571d02 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs @@ -23,6 +23,7 @@ using osu.Game.Rulesets.UI; using osu.Game.Scoring; using osu.Game.Screens.Play; using osu.Game.Tests.Beatmaps.IO; +using osu.Game.Tests.Visual.Multiplayer; using osu.Game.Users; namespace osu.Game.Tests.Visual.Gameplay @@ -238,7 +239,7 @@ namespace osu.Game.Tests.Visual.Gameplay public class TestSpectatorStreamingClient : SpectatorStreamingClient { - public readonly User StreamingUser = new User { Id = 55, Username = "Test user" }; + public readonly User StreamingUser = new User { Id = MultiplayerTestScene.PLAYER_1_ID, Username = "Test user" }; public new BindableList PlayingUsers => (BindableList)base.PlayingUsers; diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorLeaderboard.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorLeaderboard.cs index da76adf83f..d3a5daeac6 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorLeaderboard.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorLeaderboard.cs @@ -37,8 +37,8 @@ namespace osu.Game.Tests.Visual.Multiplayer private readonly Dictionary clocks = new Dictionary { - { 55, new ManualClock() }, - { 56, new ManualClock() } + { PLAYER_1_ID, new ManualClock() }, + { PLAYER_2_ID, new ManualClock() } }; public TestSceneMultiSpectatorLeaderboard() @@ -95,46 +95,46 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("send frames", () => { - // For user 55, send frames in sets of 1. - // For user 56, send frames in sets of 10. + // For player 1, send frames in sets of 1. + // For player 2, send frames in sets of 10. for (int i = 0; i < 100; i++) { - streamingClient.SendFrames(55, i, 1); + streamingClient.SendFrames(PLAYER_1_ID, i, 1); if (i % 10 == 0) - streamingClient.SendFrames(56, i, 10); + streamingClient.SendFrames(PLAYER_2_ID, i, 10); } }); - assertCombo(55, 1); - assertCombo(56, 10); + assertCombo(PLAYER_1_ID, 1); + assertCombo(PLAYER_2_ID, 10); - // Advance to a point where only user 55's frame changes. + // Advance to a point where only user player 1's frame changes. setTime(500); - assertCombo(55, 5); - assertCombo(56, 10); + assertCombo(PLAYER_1_ID, 5); + assertCombo(PLAYER_2_ID, 10); // Advance to a point where both user's frame changes. setTime(1100); - assertCombo(55, 11); - assertCombo(56, 20); + assertCombo(PLAYER_1_ID, 11); + assertCombo(PLAYER_2_ID, 20); - // Advance user 56 only to a point where its frame changes. - setTime(56, 2100); - assertCombo(55, 11); - assertCombo(56, 30); + // Advance user player 2 only to a point where its frame changes. + setTime(PLAYER_2_ID, 2100); + assertCombo(PLAYER_1_ID, 11); + assertCombo(PLAYER_2_ID, 30); // Advance both users beyond their last frame setTime(101 * 100); - assertCombo(55, 100); - assertCombo(56, 100); + assertCombo(PLAYER_1_ID, 100); + assertCombo(PLAYER_2_ID, 100); } [Test] public void TestNoFrames() { - assertCombo(55, 0); - assertCombo(56, 0); + assertCombo(PLAYER_1_ID, 0); + assertCombo(PLAYER_2_ID, 0); } private void setTime(double time) => AddStep($"set time {time}", () => diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs index 512311ff03..7abeed2cbf 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs @@ -47,9 +47,6 @@ namespace osu.Game.Tests.Visual.Multiplayer private BeatmapInfo importedBeatmap; private int importedBeatmapId; - private const int player_1_id = 55; - private const int player_2_id = 56; - [BackgroundDependencyLoader] private void load() { @@ -83,29 +80,29 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("start players silently", () => { - Client.CurrentMatchPlayingUserIds.Add(player_1_id); - Client.CurrentMatchPlayingUserIds.Add(player_2_id); - playingUserIds.Add(player_1_id); - playingUserIds.Add(player_2_id); - nextFrame[player_1_id] = 0; - nextFrame[player_2_id] = 0; + Client.CurrentMatchPlayingUserIds.Add(PLAYER_1_ID); + Client.CurrentMatchPlayingUserIds.Add(PLAYER_2_ID); + playingUserIds.Add(PLAYER_1_ID); + playingUserIds.Add(PLAYER_2_ID); + nextFrame[PLAYER_1_ID] = 0; + nextFrame[PLAYER_2_ID] = 0; }); loadSpectateScreen(false); AddWaitStep("wait a bit", 10); - AddStep("load player first_player_id", () => streamingClient.StartPlay(player_1_id, importedBeatmapId)); + AddStep("load player first_player_id", () => streamingClient.StartPlay(PLAYER_1_ID, importedBeatmapId)); AddUntilStep("one player added", () => spectatorScreen.ChildrenOfType().Count() == 1); AddWaitStep("wait a bit", 10); - AddStep("load player second_player_id", () => streamingClient.StartPlay(player_2_id, importedBeatmapId)); + AddStep("load player second_player_id", () => streamingClient.StartPlay(PLAYER_2_ID, importedBeatmapId)); AddUntilStep("two players added", () => spectatorScreen.ChildrenOfType().Count() == 2); } [Test] public void TestGeneral() { - int[] userIds = Enumerable.Range(0, 4).Select(i => player_1_id + i).ToArray(); + int[] userIds = Enumerable.Range(0, 4).Select(i => PLAYER_1_ID + i).ToArray(); start(userIds); loadSpectateScreen(); @@ -117,123 +114,123 @@ namespace osu.Game.Tests.Visual.Multiplayer [Test] public void TestPlayersMustStartSimultaneously() { - start(new[] { player_1_id, player_2_id }); + start(new[] { PLAYER_1_ID, PLAYER_2_ID }); loadSpectateScreen(); // Send frames for one player only, both should remain paused. - sendFrames(player_1_id, 20); - checkPausedInstant(player_1_id, true); - checkPausedInstant(player_2_id, true); + sendFrames(PLAYER_1_ID, 20); + checkPausedInstant(PLAYER_1_ID, true); + checkPausedInstant(PLAYER_2_ID, true); // Send frames for the other player, both should now start playing. - sendFrames(player_2_id, 20); - checkPausedInstant(player_1_id, false); - checkPausedInstant(player_2_id, false); + sendFrames(PLAYER_2_ID, 20); + checkPausedInstant(PLAYER_1_ID, false); + checkPausedInstant(PLAYER_2_ID, false); } [Test] public void TestPlayersDoNotStartSimultaneouslyIfBufferingForMaximumStartDelay() { - start(new[] { player_1_id, player_2_id }); + start(new[] { PLAYER_1_ID, PLAYER_2_ID }); loadSpectateScreen(); // Send frames for one player only, both should remain paused. - sendFrames(player_1_id, 1000); - checkPausedInstant(player_1_id, true); - checkPausedInstant(player_2_id, true); + sendFrames(PLAYER_1_ID, 1000); + checkPausedInstant(PLAYER_1_ID, true); + checkPausedInstant(PLAYER_2_ID, true); // Wait for the start delay seconds... AddWaitStep("wait maximum start delay seconds", (int)(CatchUpSyncManager.MAXIMUM_START_DELAY / TimePerAction)); // Player 1 should start playing by itself, player 2 should remain paused. - checkPausedInstant(player_1_id, false); - checkPausedInstant(player_2_id, true); + checkPausedInstant(PLAYER_1_ID, false); + checkPausedInstant(PLAYER_2_ID, true); } [Test] public void TestPlayersContinueWhileOthersBuffer() { - start(new[] { player_1_id, player_2_id }); + start(new[] { PLAYER_1_ID, PLAYER_2_ID }); loadSpectateScreen(); // Send initial frames for both players. A few more for player 1. - sendFrames(player_1_id, 20); - sendFrames(player_2_id, 10); - checkPausedInstant(player_1_id, false); - checkPausedInstant(player_2_id, false); + sendFrames(PLAYER_1_ID, 20); + sendFrames(PLAYER_2_ID, 10); + checkPausedInstant(PLAYER_1_ID, false); + checkPausedInstant(PLAYER_2_ID, false); // Eventually player 2 will pause, player 1 must remain running. - checkPaused(player_2_id, true); - checkPausedInstant(player_1_id, false); + checkPaused(PLAYER_2_ID, true); + checkPausedInstant(PLAYER_1_ID, false); // Eventually both players will run out of frames and should pause. - checkPaused(player_1_id, true); - checkPausedInstant(player_2_id, true); + checkPaused(PLAYER_1_ID, true); + checkPausedInstant(PLAYER_2_ID, true); // Send more frames for the first player only. Player 1 should start playing with player 2 remaining paused. - sendFrames(player_1_id, 20); - checkPausedInstant(player_2_id, true); - checkPausedInstant(player_1_id, false); + sendFrames(PLAYER_1_ID, 20); + checkPausedInstant(PLAYER_2_ID, true); + checkPausedInstant(PLAYER_1_ID, false); // Send more frames for the second player. Both should be playing - sendFrames(player_2_id, 20); - checkPausedInstant(player_2_id, false); - checkPausedInstant(player_1_id, false); + sendFrames(PLAYER_2_ID, 20); + checkPausedInstant(PLAYER_2_ID, false); + checkPausedInstant(PLAYER_1_ID, false); } [Test] public void TestPlayersCatchUpAfterFallingBehind() { - start(new[] { player_1_id, player_2_id }); + start(new[] { PLAYER_1_ID, PLAYER_2_ID }); loadSpectateScreen(); // Send initial frames for both players. A few more for player 1. - sendFrames(player_1_id, 1000); - sendFrames(player_2_id, 10); - checkPausedInstant(player_1_id, false); - checkPausedInstant(player_2_id, false); + sendFrames(PLAYER_1_ID, 1000); + sendFrames(PLAYER_2_ID, 10); + checkPausedInstant(PLAYER_1_ID, false); + checkPausedInstant(PLAYER_2_ID, false); // Eventually player 2 will run out of frames and should pause. - checkPaused(player_2_id, true); + checkPaused(PLAYER_2_ID, true); AddWaitStep("wait a few more frames", 10); // Send more frames for player 2. It should unpause. - sendFrames(player_2_id, 1000); - checkPausedInstant(player_2_id, false); + sendFrames(PLAYER_2_ID, 1000); + checkPausedInstant(PLAYER_2_ID, false); // Player 2 should catch up to player 1 after unpausing. - waitForCatchup(player_2_id); + waitForCatchup(PLAYER_2_ID); AddWaitStep("wait a bit", 10); } [Test] public void TestMostInSyncUserIsAudioSource() { - start(new[] { player_1_id, player_2_id }); + start(new[] { PLAYER_1_ID, PLAYER_2_ID }); loadSpectateScreen(); - assertVolume(player_1_id, 0); - assertVolume(player_2_id, 0); + assertVolume(PLAYER_1_ID, 0); + assertVolume(PLAYER_2_ID, 0); - sendFrames(player_1_id, 10); - sendFrames(player_2_id, 20); - assertVolume(player_1_id, 1); - assertVolume(player_2_id, 0); + sendFrames(PLAYER_1_ID, 10); + sendFrames(PLAYER_2_ID, 20); + assertVolume(PLAYER_1_ID, 1); + assertVolume(PLAYER_2_ID, 0); - checkPaused(player_1_id, true); - assertVolume(player_1_id, 0); - assertVolume(player_2_id, 1); + checkPaused(PLAYER_1_ID, true); + assertVolume(PLAYER_1_ID, 0); + assertVolume(PLAYER_2_ID, 1); - sendFrames(player_1_id, 100); - waitForCatchup(player_1_id); - checkPaused(player_2_id, true); - assertVolume(player_1_id, 1); - assertVolume(player_2_id, 0); + sendFrames(PLAYER_1_ID, 100); + waitForCatchup(PLAYER_1_ID); + checkPaused(PLAYER_2_ID, true); + assertVolume(PLAYER_1_ID, 1); + assertVolume(PLAYER_2_ID, 0); - sendFrames(player_2_id, 100); - waitForCatchup(player_2_id); - assertVolume(player_1_id, 1); - assertVolume(player_2_id, 0); + sendFrames(PLAYER_2_ID, 100); + waitForCatchup(PLAYER_2_ID); + assertVolume(PLAYER_1_ID, 1); + assertVolume(PLAYER_2_ID, 0); } private void loadSpectateScreen(bool waitForPlayerLoad = true) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs index 7c6c158b5a..f611d5fecf 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs @@ -119,8 +119,8 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("join other user (ready)", () => { - Client.AddUser(new User { Id = 55 }); - Client.ChangeUserState(55, MultiplayerUserState.Ready); + Client.AddUser(new User { Id = PLAYER_1_ID }); + Client.ChangeUserState(PLAYER_1_ID, MultiplayerUserState.Ready); }); AddStep("click spectate button", () => diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs index afbc9c32b3..e59b342176 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs @@ -157,8 +157,8 @@ namespace osu.Game.Tests.Visual.Multiplayer [Test] public void TestReadyButtonEnabledWhenHostAndUsersReady() { - AddStep("add user", () => Client.AddUser(new User { Id = 55 })); - AddStep("set user ready", () => Client.ChangeUserState(55, MultiplayerUserState.Ready)); + AddStep("add user", () => Client.AddUser(new User { Id = PLAYER_1_ID })); + AddStep("set user ready", () => Client.ChangeUserState(PLAYER_1_ID, MultiplayerUserState.Ready)); addClickSpectateButtonStep(); assertReadyButtonEnablement(true); @@ -169,11 +169,11 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("add user and transfer host", () => { - Client.AddUser(new User { Id = 55 }); - Client.TransferHost(55); + Client.AddUser(new User { Id = PLAYER_1_ID }); + Client.TransferHost(PLAYER_1_ID); }); - AddStep("set user ready", () => Client.ChangeUserState(55, MultiplayerUserState.Ready)); + AddStep("set user ready", () => Client.ChangeUserState(PLAYER_1_ID, MultiplayerUserState.Ready)); addClickSpectateButtonStep(); assertReadyButtonEnablement(false); diff --git a/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs b/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs index 7775c2bd24..db344b28dd 100644 --- a/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs +++ b/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs @@ -16,6 +16,9 @@ namespace osu.Game.Tests.Visual.Multiplayer { public abstract class MultiplayerTestScene : RoomTestScene { + public const int PLAYER_1_ID = 55; + public const int PLAYER_2_ID = 56; + [Cached(typeof(StatefulMultiplayerClient))] public TestMultiplayerClient Client { get; } From 6626e70c95557a17c07216b5514ae4d9440d92a3 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 26 Apr 2021 17:30:27 +0900 Subject: [PATCH 064/205] Pass in master clock instead of slave clock --- .../OnlinePlay/TestSceneCatchUpSyncManager.cs | 5 ++++ .../Spectate/MultiSpectatorScreen.cs | 5 +++- .../Multiplayer/Spectate/PlayerArea.cs | 8 ++++--- .../Sync/CatchUpSpectatorPlayerClock.cs | 23 +++++++++++-------- .../Spectate/Sync/ISpectatorPlayerClock.cs | 5 ++++ 5 files changed, 32 insertions(+), 14 deletions(-) diff --git a/osu.Game.Tests/OnlinePlay/TestSceneCatchUpSyncManager.cs b/osu.Game.Tests/OnlinePlay/TestSceneCatchUpSyncManager.cs index 73ec5fb934..75ba362146 100644 --- a/osu.Game.Tests/OnlinePlay/TestSceneCatchUpSyncManager.cs +++ b/osu.Game.Tests/OnlinePlay/TestSceneCatchUpSyncManager.cs @@ -168,6 +168,11 @@ namespace osu.Game.Tests.OnlinePlay public bool IsCatchingUp { get; set; } + public IFrameBasedClock Source + { + set => throw new NotImplementedException(); + } + public readonly int Id; public TestSpectatorPlayerClock(int id) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs index 6a8c10e336..86c975d12f 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs @@ -79,7 +79,10 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate }; for (int i = 0; i < UserIds.Length; i++) - grid.Add(instances[i] = new PlayerArea(UserIds[i], new CatchUpSpectatorPlayerClock(masterClockContainer.GameplayClock))); + { + grid.Add(instances[i] = new PlayerArea(UserIds[i], masterClockContainer.GameplayClock)); + syncManager.AddPlayerClock(instances[i].GameplayClock); + } // Todo: This is not quite correct - it should be per-user to adjust for other mod combinations. var playableBeatmap = Beatmap.Value.GetPlayableBeatmap(Ruleset.Value); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs index 2accfaec0c..3ef1693ca6 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs @@ -9,6 +9,7 @@ using osu.Framework.Audio; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Timing; using osu.Game.Beatmaps; using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets; @@ -38,7 +39,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate /// The used to control the gameplay running state of a loaded . /// [NotNull] - public readonly ISpectatorPlayerClock GameplayClock; + public readonly ISpectatorPlayerClock GameplayClock = new CatchUpSpectatorPlayerClock(); /// /// The currently-loaded score. @@ -54,10 +55,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate private readonly AudioContainer audioContainer; private OsuScreenStack stack; - public PlayerArea(int userId, [NotNull] ISpectatorPlayerClock gameplayClock) + public PlayerArea(int userId, IFrameBasedClock masterClock) { UserId = userId; - GameplayClock = gameplayClock; RelativeSizeAxes = Axes.Both; Masking = true; @@ -71,6 +71,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate }, loadingLayer = new LoadingLayer(true) { State = { Value = Visibility.Visible } } }; + + GameplayClock.Source = masterClock; } public void LoadScore([NotNull] Score score) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/Sync/CatchUpSpectatorPlayerClock.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/Sync/CatchUpSpectatorPlayerClock.cs index a8ed7b415f..e2a6b7c9ae 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/Sync/CatchUpSpectatorPlayerClock.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/Sync/CatchUpSpectatorPlayerClock.cs @@ -1,6 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +#nullable enable + using System; using osu.Framework.Bindables; using osu.Framework.Timing; @@ -17,12 +19,10 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate.Sync /// public const double CATCHUP_RATE = 2; - private readonly IFrameBasedClock masterClock; - - public CatchUpSpectatorPlayerClock(IFrameBasedClock masterClock) - { - this.masterClock = masterClock; - } + /// + /// The source clock. + /// + public IFrameBasedClock? Source { get; set; } public double CurrentTime { get; private set; } @@ -52,19 +52,22 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate.Sync public void ProcessFrame() { - masterClock.ProcessFrame(); - ElapsedFrameTime = 0; FramesPerSecond = 0; + if (Source == null) + return; + + Source.ProcessFrame(); + if (IsRunning) { - double elapsedSource = masterClock.ElapsedFrameTime; + double elapsedSource = Source.ElapsedFrameTime; double elapsed = elapsedSource * Rate; CurrentTime += elapsed; ElapsedFrameTime = elapsed; - FramesPerSecond = masterClock.FramesPerSecond; + FramesPerSecond = Source.FramesPerSecond; } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/Sync/ISpectatorPlayerClock.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/Sync/ISpectatorPlayerClock.cs index 46f80288c6..311969c92c 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/Sync/ISpectatorPlayerClock.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/Sync/ISpectatorPlayerClock.cs @@ -20,5 +20,10 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate.Sync /// Whether this clock is resynchronising to the master clock. /// bool IsCatchingUp { get; set; } + + /// + /// The source clock + /// + IFrameBasedClock Source { set; } } } From d7618b63fa38512a7449c65eb6d92bbca140cb54 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 26 Apr 2021 17:35:13 +0900 Subject: [PATCH 065/205] Fix test failure --- .../Multiplayer/Spectate/Sync/CatchUpSyncManager.cs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/Sync/CatchUpSyncManager.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/Sync/CatchUpSyncManager.cs index f5c780e8b1..5912c0803b 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/Sync/CatchUpSyncManager.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/Sync/CatchUpSyncManager.cs @@ -78,15 +78,18 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate.Sync if (playerClocks.Count == 0) return false; - firstStartAttemptTime ??= Time.Current; - int readyCount = playerClocks.Count(s => !s.WaitingOnFrames.Value); if (readyCount == playerClocks.Count) return hasStarted = true; - if (readyCount > 0 && (Time.Current - firstStartAttemptTime) > MAXIMUM_START_DELAY) - return hasStarted = true; + if (readyCount > 0) + { + firstStartAttemptTime ??= Time.Current; + + if (Time.Current - firstStartAttemptTime > MAXIMUM_START_DELAY) + return hasStarted = true; + } return false; } From 94d0b064931736c7e6947a13b35ec0f39a67889e Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 26 Apr 2021 19:01:30 +0900 Subject: [PATCH 066/205] Expose mute adjustment instead --- .../TestSceneMultiSpectatorScreen.cs | 24 +++---- .../Spectate/MultiSpectatorScreen.cs | 2 +- .../Multiplayer/Spectate/PlayerArea.cs | 64 +++++-------------- 3 files changed, 30 insertions(+), 60 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs index 7abeed2cbf..a4856d1ee8 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs @@ -209,28 +209,28 @@ namespace osu.Game.Tests.Visual.Multiplayer start(new[] { PLAYER_1_ID, PLAYER_2_ID }); loadSpectateScreen(); - assertVolume(PLAYER_1_ID, 0); - assertVolume(PLAYER_2_ID, 0); + assertMuted(PLAYER_1_ID, true); + assertMuted(PLAYER_2_ID, true); sendFrames(PLAYER_1_ID, 10); sendFrames(PLAYER_2_ID, 20); - assertVolume(PLAYER_1_ID, 1); - assertVolume(PLAYER_2_ID, 0); + assertMuted(PLAYER_1_ID, false); + assertMuted(PLAYER_2_ID, true); checkPaused(PLAYER_1_ID, true); - assertVolume(PLAYER_1_ID, 0); - assertVolume(PLAYER_2_ID, 1); + assertMuted(PLAYER_1_ID, true); + assertMuted(PLAYER_2_ID, false); sendFrames(PLAYER_1_ID, 100); waitForCatchup(PLAYER_1_ID); checkPaused(PLAYER_2_ID, true); - assertVolume(PLAYER_1_ID, 1); - assertVolume(PLAYER_2_ID, 0); + assertMuted(PLAYER_1_ID, false); + assertMuted(PLAYER_2_ID, true); sendFrames(PLAYER_2_ID, 100); waitForCatchup(PLAYER_2_ID); - assertVolume(PLAYER_1_ID, 1); - assertVolume(PLAYER_2_ID, 0); + assertMuted(PLAYER_1_ID, false); + assertMuted(PLAYER_2_ID, true); } private void loadSpectateScreen(bool waitForPlayerLoad = true) @@ -292,8 +292,8 @@ namespace osu.Game.Tests.Visual.Multiplayer private void checkPausedInstant(int userId, bool state) => AddAssert($"{userId} is {(state ? "paused" : "playing")}", () => getPlayer(userId).ChildrenOfType().First().GameplayClock.IsRunning != state); - private void assertVolume(int userId, double volume) - => AddAssert($"{userId} volume is {volume}", () => getInstance(userId).Volume.Value == volume); + private void assertMuted(int userId, bool muted) + => AddAssert($"{userId} {(muted ? "is" : "is not")} muted", () => getInstance(userId).Mute == muted); private void waitForCatchup(int userId) => AddUntilStep($"{userId} not catching up", () => !getInstance(userId).GameplayClock.IsCatchingUp); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs index 86c975d12f..2bd0ad8748 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs @@ -111,7 +111,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate .FirstOrDefault(); foreach (var instance in instances) - instance.Volume.Value = instance == currentAudioSource ? 1 : 0; + instance.Mute = instance != currentAudioSource; } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs index 3ef1693ca6..89c69cc666 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs @@ -23,7 +23,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate /// /// Provides an area for and manages the hierarchy of a spectated player within a . /// - public class PlayerArea : CompositeDrawable, IAdjustableAudioComponent + public class PlayerArea : CompositeDrawable { /// /// Whether a is loaded in the area. @@ -50,9 +50,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate [Resolved] private BeatmapManager beatmapManager { get; set; } + private readonly BindableDouble volumeAdjustment = new BindableDouble(); private readonly Container gameplayContent; private readonly LoadingLayer loadingLayer; - private readonly AudioContainer audioContainer; private OsuScreenStack stack; public PlayerArea(int userId, IFrameBasedClock masterClock) @@ -62,6 +62,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate RelativeSizeAxes = Axes.Both; Masking = true; + AudioContainer audioContainer; InternalChildren = new Drawable[] { audioContainer = new AudioContainer @@ -72,6 +73,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate loadingLayer = new LoadingLayer(true) { State = { Value = Visibility.Visible } } }; + audioContainer.AddAdjustment(AdjustableProperty.Volume, volumeAdjustment); + GameplayClock.Source = masterClock; } @@ -92,55 +95,22 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate loadingLayer.Hide(); } + private bool mute = true; + + public bool Mute + { + get => mute; + set + { + mute = value; + volumeAdjustment.Value = value ? 0 : 1; + } + } + // Player interferes with global input, so disable input for now. public override bool PropagatePositionalInputSubTree => false; public override bool PropagateNonPositionalInputSubTree => false; - #region IAdjustableAudioComponent - - public IBindable AggregateVolume => audioContainer.AggregateVolume; - - public IBindable AggregateBalance => audioContainer.AggregateBalance; - - public IBindable AggregateFrequency => audioContainer.AggregateFrequency; - - public IBindable AggregateTempo => audioContainer.AggregateTempo; - - public void BindAdjustments(IAggregateAudioAdjustment component) - { - audioContainer.BindAdjustments(component); - } - - public void UnbindAdjustments(IAggregateAudioAdjustment component) - { - audioContainer.UnbindAdjustments(component); - } - - public void AddAdjustment(AdjustableProperty type, IBindable adjustBindable) - { - audioContainer.AddAdjustment(type, adjustBindable); - } - - public void RemoveAdjustment(AdjustableProperty type, IBindable adjustBindable) - { - audioContainer.RemoveAdjustment(type, adjustBindable); - } - - public void RemoveAllAdjustments(AdjustableProperty type) - { - audioContainer.RemoveAllAdjustments(type); - } - - public BindableNumber Volume => audioContainer.Volume; - - public BindableNumber Balance => audioContainer.Balance; - - public BindableNumber Frequency => audioContainer.Frequency; - - public BindableNumber Tempo => audioContainer.Tempo; - - #endregion - private class PlayerIsolationContainer : Container { [Cached] From 7e11d520d587a905f64bf25d023c5515c046b812 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 26 Apr 2021 21:25:34 +0900 Subject: [PATCH 067/205] Remove finished players from multi spectator screen --- .../Spectate/MultiSpectatorScreen.cs | 15 ++++---- osu.Game/Screens/Spectate/SpectatorScreen.cs | 35 ++++++++++++++----- 2 files changed, 35 insertions(+), 15 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs index 2bd0ad8748..c3917e321d 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs @@ -27,6 +27,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate /// public bool AllPlayersLoaded => instances.All(p => p?.PlayerLoaded == true); + private readonly int[] userIds; + [Resolved] private SpectatorStreamingClient spectatorClient { get; set; } @@ -44,7 +46,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate public MultiSpectatorScreen(int[] userIds) : base(userIds.Take(PlayerGrid.MAX_PLAYERS).ToArray()) { - instances = new PlayerArea[UserIds.Length]; + this.userIds = GetUserIds().ToArray(); + instances = new PlayerArea[this.userIds.Length]; } [BackgroundDependencyLoader] @@ -78,9 +81,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate }) }; - for (int i = 0; i < UserIds.Length; i++) + for (int i = 0; i < userIds.Length; i++) { - grid.Add(instances[i] = new PlayerArea(UserIds[i], masterClockContainer.GameplayClock)); + grid.Add(instances[i] = new PlayerArea(userIds[i], masterClockContainer.GameplayClock)); syncManager.AddPlayerClock(instances[i].GameplayClock); } @@ -89,7 +92,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate var scoreProcessor = Ruleset.Value.CreateInstance().CreateScoreProcessor(); scoreProcessor.ApplyBeatmap(playableBeatmap); - LoadComponentAsync(leaderboard = new MultiSpectatorLeaderboard(scoreProcessor, UserIds) { Expanded = { Value = true } }, leaderboardContainer.Add); + LoadComponentAsync(leaderboard = new MultiSpectatorLeaderboard(scoreProcessor, userIds) { Expanded = { Value = true } }, leaderboardContainer.Add); } protected override void LoadComplete() @@ -133,10 +136,10 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate protected override void EndGameplay(int userId) { - spectatorClient.StopWatchingUser(userId); + RemoveUser(userId); leaderboard.RemoveClock(userId); } - private int getIndexForUser(int userId) => Array.IndexOf(UserIds, userId); + private int getIndexForUser(int userId) => Array.IndexOf(userIds, userId); } } diff --git a/osu.Game/Screens/Spectate/SpectatorScreen.cs b/osu.Game/Screens/Spectate/SpectatorScreen.cs index 36768a512d..4aca379ebe 100644 --- a/osu.Game/Screens/Spectate/SpectatorScreen.cs +++ b/osu.Game/Screens/Spectate/SpectatorScreen.cs @@ -7,6 +7,7 @@ using System.Diagnostics; using System.Linq; using System.Threading.Tasks; using JetBrains.Annotations; +using NuGet.Packaging; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Game.Beatmaps; @@ -26,7 +27,8 @@ namespace osu.Game.Screens.Spectate /// public abstract class SpectatorScreen : OsuScreen { - protected readonly int[] UserIds; + protected IEnumerable GetUserIds() => userIds; + private readonly HashSet userIds = new HashSet(); [Resolved] private BeatmapManager beatmaps { get; set; } @@ -54,7 +56,7 @@ namespace osu.Game.Screens.Spectate /// The users to spectate. protected SpectatorScreen(params int[] userIds) { - UserIds = userIds; + this.userIds.AddRange(userIds); } protected override void LoadComplete() @@ -80,20 +82,18 @@ namespace osu.Game.Screens.Spectate private Task populateAllUsers() { - var userLookupTasks = new Task[UserIds.Length]; + var userLookupTasks = new List(); - for (int i = 0; i < UserIds.Length; i++) + foreach (var u in userIds) { - var userId = UserIds[i]; - - userLookupTasks[i] = userLookupCache.GetUserAsync(userId).ContinueWith(task => + userLookupTasks.Add(userLookupCache.GetUserAsync(u).ContinueWith(task => { if (!task.IsCompletedSuccessfully) return; lock (stateLock) - userMap[userId] = task.Result; - }); + userMap[u] = task.Result; + })); } return Task.WhenAll(userLookupTasks); @@ -239,6 +239,23 @@ namespace osu.Game.Screens.Spectate /// The user to end gameplay for. protected abstract void EndGameplay(int userId); + /// + /// Stops spectating a user. + /// + /// The user to stop spectating. + protected void RemoveUser(int userId) + { + lock (stateLock) + { + userFinishedPlaying(userId, null); + + userIds.Remove(userId); + userMap.Remove(userId); + + spectatorClient.StopWatchingUser(userId); + } + } + protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); From ed93e26e52b70127ff3e34db5671b25fb6172299 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 26 Apr 2021 21:55:38 +0900 Subject: [PATCH 068/205] Use single method for starting/restarting spectator screen --- .../OnlinePlay/Multiplayer/Multiplayer.cs | 2 +- .../Multiplayer/MultiplayerMatchSubScreen.cs | 31 +++++++------------ .../Spectate/MultiSpectatorScreen.cs | 7 +++++ 3 files changed, 20 insertions(+), 20 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs index ae22e1fcec..085c824bdc 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs @@ -21,7 +21,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer { base.OnResuming(last); - if (client.Room != null) + if (client.Room != null && client.LocalUser?.State != MultiplayerUserState.Spectating) client.ChangeState(MultiplayerUserState.Idle); } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index 0cd3b448d2..b8347d0537 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -406,29 +406,22 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer } } - private void onRoomUpdated() => Scheduler.Add(() => + private void onRoomUpdated() { - if (client.Room == null) - return; - - Debug.Assert(client.LocalUser != null); - - UpdateMods(); - - if (client.LocalUser.State == MultiplayerUserState.Spectating - && (client.Room.State == MultiplayerRoomState.Playing || client.Room.State == MultiplayerRoomState.WaitingForLoad) - && ParentScreen.IsCurrentScreen()) - { - StartPlay(); - - // If the current user was host, they started the match and the in-progress operation needs to be stopped now. - readyClickOperation?.Dispose(); - readyClickOperation = null; - } - }); + Scheduler.AddOnce(UpdateMods); + } private void onLoadRequested() { + // If the user is spectating, the multi-spectator screen may still be the current screen. + if (!ParentScreen.IsCurrentScreen()) + { + ParentScreen.MakeCurrent(); + + Schedule(onLoadRequested); + return; + } + StartPlay(); readyClickOperation?.Dispose(); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs index c3917e321d..85ef375fae 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs @@ -140,6 +140,13 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate leaderboard.RemoveClock(userId); } + public override bool OnBackButton() + { + // On a manual exit, set the player state back to idle. + multiplayerClient.ChangeState(MultiplayerUserState.Idle); + return base.OnBackButton(); + } + private int getIndexForUser(int userId) => Array.IndexOf(userIds, userId); } } From 630a6dc46ac262742f9a8a6c1c68e98100c789dc Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 26 Apr 2021 22:23:44 +0900 Subject: [PATCH 069/205] Fix missing dependency --- .../OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs index 85ef375fae..ef31a5a7f0 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs @@ -7,6 +7,7 @@ using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Game.Online.Multiplayer; using osu.Game.Online.Spectator; using osu.Game.Screens.OnlinePlay.Multiplayer.Spectate.Sync; using osu.Game.Screens.Play; @@ -32,6 +33,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate [Resolved] private SpectatorStreamingClient spectatorClient { get; set; } + [Resolved] + private StatefulMultiplayerClient multiplayerClient { get; set; } + private readonly PlayerArea[] instances; private MasterGameplayClockContainer masterClockContainer; private ISyncManager syncManager; From 6da4105da6c4b2bcf53f5b2dbc61cfe5b22533c5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 3 May 2021 13:38:53 +0900 Subject: [PATCH 070/205] Remove Sync namespace (feels unnecessary) --- osu.Game.Tests/OnlinePlay/TestSceneCatchUpSyncManager.cs | 2 +- .../Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs | 1 - .../Spectate/{Sync => }/CatchUpSpectatorPlayerClock.cs | 2 +- .../Multiplayer/Spectate/{Sync => }/CatchUpSyncManager.cs | 2 +- .../Multiplayer/Spectate/{Sync => }/ISpectatorPlayerClock.cs | 2 +- .../OnlinePlay/Multiplayer/Spectate/{Sync => }/ISyncManager.cs | 2 +- .../OnlinePlay/Multiplayer/Spectate/MultiSpectatorPlayer.cs | 1 - .../OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs | 1 - osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs | 1 - 9 files changed, 5 insertions(+), 9 deletions(-) rename osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/{Sync => }/CatchUpSpectatorPlayerClock.cs (97%) rename osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/{Sync => }/CatchUpSyncManager.cs (98%) rename osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/{Sync => }/ISpectatorPlayerClock.cs (93%) rename osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/{Sync => }/ISyncManager.cs (94%) diff --git a/osu.Game.Tests/OnlinePlay/TestSceneCatchUpSyncManager.cs b/osu.Game.Tests/OnlinePlay/TestSceneCatchUpSyncManager.cs index 75ba362146..d4e591cf09 100644 --- a/osu.Game.Tests/OnlinePlay/TestSceneCatchUpSyncManager.cs +++ b/osu.Game.Tests/OnlinePlay/TestSceneCatchUpSyncManager.cs @@ -6,7 +6,7 @@ using NUnit.Framework; using osu.Framework.Bindables; using osu.Framework.Testing; using osu.Framework.Timing; -using osu.Game.Screens.OnlinePlay.Multiplayer.Spectate.Sync; +using osu.Game.Screens.OnlinePlay.Multiplayer.Spectate; using osu.Game.Tests.Visual; namespace osu.Game.Tests.OnlinePlay diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs index a4856d1ee8..db431c0a82 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs @@ -17,7 +17,6 @@ using osu.Game.Online.Spectator; using osu.Game.Replays.Legacy; using osu.Game.Scoring; using osu.Game.Screens.OnlinePlay.Multiplayer.Spectate; -using osu.Game.Screens.OnlinePlay.Multiplayer.Spectate.Sync; using osu.Game.Screens.Play; using osu.Game.Tests.Beatmaps.IO; using osu.Game.Users; diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/Sync/CatchUpSpectatorPlayerClock.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/CatchUpSpectatorPlayerClock.cs similarity index 97% rename from osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/Sync/CatchUpSpectatorPlayerClock.cs rename to osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/CatchUpSpectatorPlayerClock.cs index e2a6b7c9ae..9e1a020eca 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/Sync/CatchUpSpectatorPlayerClock.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/CatchUpSpectatorPlayerClock.cs @@ -7,7 +7,7 @@ using System; using osu.Framework.Bindables; using osu.Framework.Timing; -namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate.Sync +namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate { /// /// A which catches up using rate adjustment. diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/Sync/CatchUpSyncManager.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/CatchUpSyncManager.cs similarity index 98% rename from osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/Sync/CatchUpSyncManager.cs rename to osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/CatchUpSyncManager.cs index 5912c0803b..6028bc84f7 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/Sync/CatchUpSyncManager.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/CatchUpSyncManager.cs @@ -6,7 +6,7 @@ using System.Linq; using osu.Framework.Graphics; using osu.Framework.Timing; -namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate.Sync +namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate { /// /// A which synchronises de-synced player clocks through catchup. diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/Sync/ISpectatorPlayerClock.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/ISpectatorPlayerClock.cs similarity index 93% rename from osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/Sync/ISpectatorPlayerClock.cs rename to osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/ISpectatorPlayerClock.cs index 311969c92c..1a5231e602 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/Sync/ISpectatorPlayerClock.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/ISpectatorPlayerClock.cs @@ -4,7 +4,7 @@ using osu.Framework.Bindables; using osu.Framework.Timing; -namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate.Sync +namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate { /// /// A clock which is used by s and managed by an . diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/Sync/ISyncManager.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/ISyncManager.cs similarity index 94% rename from osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/Sync/ISyncManager.cs rename to osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/ISyncManager.cs index 1b83ea8cf3..bd698108f6 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/Sync/ISyncManager.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/ISyncManager.cs @@ -3,7 +3,7 @@ using osu.Framework.Timing; -namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate.Sync +namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate { /// /// Manages the synchronisation between one or more s in relation to a master clock. diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorPlayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorPlayer.cs index a17519c3aa..0fe9e01d9d 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorPlayer.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorPlayer.cs @@ -7,7 +7,6 @@ using osu.Framework.Bindables; using osu.Framework.Timing; using osu.Game.Beatmaps; using osu.Game.Scoring; -using osu.Game.Screens.OnlinePlay.Multiplayer.Spectate.Sync; using osu.Game.Screens.Play; namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs index ef31a5a7f0..fd2c79f8e4 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs @@ -9,7 +9,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Online.Multiplayer; using osu.Game.Online.Spectator; -using osu.Game.Screens.OnlinePlay.Multiplayer.Spectate.Sync; using osu.Game.Screens.Play; using osu.Game.Screens.Spectate; diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs index 89c69cc666..50b5fc2110 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs @@ -15,7 +15,6 @@ using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Scoring; -using osu.Game.Screens.OnlinePlay.Multiplayer.Spectate.Sync; using osu.Game.Screens.Play; namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate From 66ae6e58d18b8c1f5be9d73095c2ea9aac73791c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 3 May 2021 14:01:10 +0900 Subject: [PATCH 071/205] Reword comment regarding LoadRequested special case to be easier to understand context --- .../OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index b8347d0537..e6ce135660 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -413,7 +413,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer private void onLoadRequested() { - // If the user is spectating, the multi-spectator screen may still be the current screen. + // In the case of spectating, IMultiplayerClient.LoadRequested can be fired while the game is still spectating a previous session. + // For now, we want to game to switch to the new game so need to request exiting from the play screen. if (!ParentScreen.IsCurrentScreen()) { ParentScreen.MakeCurrent(); From dc5ee31d946918c2cbb2d0727e6039544b3a38d3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 3 May 2021 14:04:20 +0900 Subject: [PATCH 072/205] Use switch for screen construction --- .../Multiplayer/MultiplayerMatchSubScreen.cs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index e6ce135660..ab22d40181 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -433,10 +433,16 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer { Debug.Assert(client.LocalUser != null); - if (client.LocalUser.State == MultiplayerUserState.Spectating) - return new MultiSpectatorScreen(client.CurrentMatchPlayingUserIds.ToArray()); + int[] userIds = client.CurrentMatchPlayingUserIds.ToArray(); - return new MultiplayerPlayer(SelectedItem.Value, client.CurrentMatchPlayingUserIds.ToArray()); + switch (client.LocalUser.State) + { + case MultiplayerUserState.Spectating: + return new MultiSpectatorScreen(userIds); + + default: + return new MultiplayerPlayer(SelectedItem.Value, userIds); + } } protected override void Dispose(bool isDisposing) From c065092e72f67e41d02e6d3a75637f7722c26b4d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 3 May 2021 14:25:52 +0900 Subject: [PATCH 073/205] Fix weird access to userIds in `MultiplayerSpectatorScreen` --- .../Multiplayer/Spectate/MultiSpectatorScreen.cs | 16 ++++++---------- osu.Game/Screens/Spectate/SpectatorScreen.cs | 6 +++--- 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs index fd2c79f8e4..be4450787a 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs @@ -27,8 +27,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate /// public bool AllPlayersLoaded => instances.All(p => p?.PlayerLoaded == true); - private readonly int[] userIds; - [Resolved] private SpectatorStreamingClient spectatorClient { get; set; } @@ -49,8 +47,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate public MultiSpectatorScreen(int[] userIds) : base(userIds.Take(PlayerGrid.MAX_PLAYERS).ToArray()) { - this.userIds = GetUserIds().ToArray(); - instances = new PlayerArea[this.userIds.Length]; + instances = new PlayerArea[UserIds.Count]; } [BackgroundDependencyLoader] @@ -84,9 +81,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate }) }; - for (int i = 0; i < userIds.Length; i++) + for (int i = 0; i < UserIds.Count; i++) { - grid.Add(instances[i] = new PlayerArea(userIds[i], masterClockContainer.GameplayClock)); + grid.Add(instances[i] = new PlayerArea(UserIds[i], masterClockContainer.GameplayClock)); syncManager.AddPlayerClock(instances[i].GameplayClock); } @@ -95,7 +92,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate var scoreProcessor = Ruleset.Value.CreateInstance().CreateScoreProcessor(); scoreProcessor.ApplyBeatmap(playableBeatmap); - LoadComponentAsync(leaderboard = new MultiSpectatorLeaderboard(scoreProcessor, userIds) { Expanded = { Value = true } }, leaderboardContainer.Add); + LoadComponentAsync(leaderboard = new MultiSpectatorLeaderboard(scoreProcessor, UserIds.ToArray()) { Expanded = { Value = true } }, leaderboardContainer.Add); } protected override void LoadComplete() @@ -130,7 +127,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate protected override void StartGameplay(int userId, GameplayState gameplayState) { - var instance = instances[getIndexForUser(userId)]; + var instance = instances.Single(i => i.UserId == userId); + instance.LoadScore(gameplayState.Score); syncManager.AddPlayerClock(instance.GameplayClock); @@ -149,7 +147,5 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate multiplayerClient.ChangeState(MultiplayerUserState.Idle); return base.OnBackButton(); } - - private int getIndexForUser(int userId) => Array.IndexOf(userIds, userId); } } diff --git a/osu.Game/Screens/Spectate/SpectatorScreen.cs b/osu.Game/Screens/Spectate/SpectatorScreen.cs index 4aca379ebe..bcebd51954 100644 --- a/osu.Game/Screens/Spectate/SpectatorScreen.cs +++ b/osu.Game/Screens/Spectate/SpectatorScreen.cs @@ -7,7 +7,6 @@ using System.Diagnostics; using System.Linq; using System.Threading.Tasks; using JetBrains.Annotations; -using NuGet.Packaging; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Game.Beatmaps; @@ -27,8 +26,9 @@ namespace osu.Game.Screens.Spectate /// public abstract class SpectatorScreen : OsuScreen { - protected IEnumerable GetUserIds() => userIds; - private readonly HashSet userIds = new HashSet(); + protected IReadOnlyList UserIds => userIds; + + private readonly List userIds = new List(); [Resolved] private BeatmapManager beatmaps { get; set; } From 2aa21e2affb3f9993ec42b87228236e768aa7e05 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 3 May 2021 14:37:11 +0900 Subject: [PATCH 074/205] Adjust documentation in `CatchUpSyncManager` --- .../OnlinePlay/Multiplayer/Spectate/CatchUpSyncManager.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/CatchUpSyncManager.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/CatchUpSyncManager.cs index 6028bc84f7..efc12eaaa5 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/CatchUpSyncManager.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/CatchUpSyncManager.cs @@ -14,7 +14,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate public class CatchUpSyncManager : Component, ISyncManager { /// - /// The offset from the master clock to which player clocks should be synchronised to. + /// The offset from the master clock to which player clocks should remain within to be considered in-sync. /// public const double SYNC_TARGET = 16; @@ -102,6 +102,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate for (int i = 0; i < playerClocks.Count; i++) { var clock = playerClocks[i]; + + // How far this player's clock is out of sync, compared to the master clock. + // A negative value means the player is running fast (ahead); a positive value means the player is running behind (catching up). double timeDelta = MasterClock.CurrentTime - clock.CurrentTime; // Check that the player clock isn't too far ahead. From b1a19b6dd6f6ea6782828fdd1cc23883bf9c947a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 3 May 2021 14:41:55 +0900 Subject: [PATCH 075/205] Add xmldoc for `PlayerIsolationContainer` --- osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs index 50b5fc2110..fe79e5db72 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs @@ -110,6 +110,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate public override bool PropagatePositionalInputSubTree => false; public override bool PropagateNonPositionalInputSubTree => false; + /// + /// Isolates each player instance from the game-wide ruleset/beatmap/mods (to allow for different players having different settings). + /// private class PlayerIsolationContainer : Container { [Cached] From 54abf8f6f685a31487579d5f5bde1db7d4d67479 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 3 May 2021 14:48:04 +0900 Subject: [PATCH 076/205] Vertically centre leaderboard for now --- .../Multiplayer/Spectate/MultiSpectatorScreen.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs index be4450787a..8c7b7bab01 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs @@ -92,7 +92,12 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate var scoreProcessor = Ruleset.Value.CreateInstance().CreateScoreProcessor(); scoreProcessor.ApplyBeatmap(playableBeatmap); - LoadComponentAsync(leaderboard = new MultiSpectatorLeaderboard(scoreProcessor, UserIds.ToArray()) { Expanded = { Value = true } }, leaderboardContainer.Add); + LoadComponentAsync(leaderboard = new MultiSpectatorLeaderboard(scoreProcessor, UserIds.ToArray()) + { + Expanded = { Value = true }, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + }, leaderboardContainer.Add); } protected override void LoadComplete() From 713c169332dacbd7b63b0948ade3290c614004dd Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 11 May 2021 16:08:25 +0900 Subject: [PATCH 077/205] Fix mania crashing on playing samples after skin change --- .../Drawables/DrawableManiaHitObject.cs | 6 +++++ osu.Game.Rulesets.Mania/UI/Column.cs | 27 ++++++++++++++++++- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs index 1550faee50..003646d654 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs @@ -6,6 +6,7 @@ using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Game.Audio; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Rulesets.Mania.UI; @@ -24,6 +25,11 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables [Resolved(canBeNull: true)] private ManiaPlayfield playfield { get; set; } + /// + /// Gets the samples that are played by this object during gameplay. + /// + public ISampleInfo[] GetGameplaySamples() => Samples.Samples; + protected override float SamplePlaybackPosition { get diff --git a/osu.Game.Rulesets.Mania/UI/Column.cs b/osu.Game.Rulesets.Mania/UI/Column.cs index d2a9b69b60..fcbbd8a01c 100644 --- a/osu.Game.Rulesets.Mania/UI/Column.cs +++ b/osu.Game.Rulesets.Mania/UI/Column.cs @@ -27,6 +27,15 @@ namespace osu.Game.Rulesets.Mania.UI public const float COLUMN_WIDTH = 80; public const float SPECIAL_COLUMN_WIDTH = 70; + /// + /// For hitsounds played by this (i.e. not as a result of hitting a hitobject), + /// a certain number of samples are allowed to be played concurrently so that it feels better when spam-pressing the key. + /// + /// + /// + /// + private const int max_concurrent_hitsounds = OsuGameBase.SAMPLE_CONCURRENCY; + /// /// The index of this column as part of the whole playfield. /// @@ -38,6 +47,7 @@ namespace osu.Game.Rulesets.Mania.UI internal readonly Container TopLevelContainer; private readonly DrawablePool hitExplosionPool; private readonly OrderedHitPolicy hitPolicy; + private readonly SkinnableSound[] hitSounds; public Container UnderlayElements => HitObjectArea.UnderlayElements; @@ -64,6 +74,11 @@ namespace osu.Game.Rulesets.Mania.UI RelativeSizeAxes = Axes.Both }, background, + new Container + { + RelativeSizeAxes = Axes.Both, + ChildrenEnumerable = hitSounds = Enumerable.Range(0, max_concurrent_hitsounds).Select(_ => new SkinnableSound()).ToArray() + }, TopLevelContainer = new Container { RelativeSizeAxes = Axes.Both } }; @@ -120,6 +135,8 @@ namespace osu.Game.Rulesets.Mania.UI HitObjectArea.Explosions.Add(hitExplosionPool.Get(e => e.Apply(result))); } + private int nextHitSound; + public bool OnPressed(ManiaAction action) { if (action != Action.Value) @@ -131,7 +148,15 @@ namespace osu.Game.Rulesets.Mania.UI HitObjectContainer.Objects.FirstOrDefault(h => h.HitObject.StartTime > Time.Current) ?? HitObjectContainer.Objects.LastOrDefault(); - nextObject?.PlaySamples(); + if (nextObject is DrawableManiaHitObject maniaObject) + { + var hitSound = hitSounds[nextHitSound]; + + hitSound.Samples = maniaObject.GetGameplaySamples(); + hitSound.Play(); + + nextHitSound = (nextHitSound + 1) % max_concurrent_hitsounds; + } return true; } From b9ab9342faeee26c54903beb326109698c8525b1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 6 May 2021 15:16:16 +0900 Subject: [PATCH 078/205] Setup basics to allow extracting serializable content from skinnable `Drawable`s --- osu.Game/Extensions/DrawableExtensions.cs | 3 ++ osu.Game/Screens/Play/HUD/ISkinnableInfo.cs | 32 ++++++++++++++ .../HUD/SkinnableElementTargetContainer.cs | 11 +++++ .../Screens/Play/HUD/StoredSkinnableInfo.cs | 43 +++++++++++++++++++ osu.Game/Screens/Play/HUDOverlay.cs | 38 +++++++--------- .../Skinning/Editor/SkinBlueprintContainer.cs | 13 +++++- osu.Game/Skinning/ISkin.cs | 2 + 7 files changed, 119 insertions(+), 23 deletions(-) create mode 100644 osu.Game/Screens/Play/HUD/ISkinnableInfo.cs create mode 100644 osu.Game/Screens/Play/HUD/SkinnableElementTargetContainer.cs create mode 100644 osu.Game/Screens/Play/HUD/StoredSkinnableInfo.cs diff --git a/osu.Game/Extensions/DrawableExtensions.cs b/osu.Game/Extensions/DrawableExtensions.cs index a8de3f6407..3d2ffe0ea1 100644 --- a/osu.Game/Extensions/DrawableExtensions.cs +++ b/osu.Game/Extensions/DrawableExtensions.cs @@ -5,6 +5,7 @@ using System; using osu.Framework.Graphics; using osu.Framework.Input.Bindings; using osu.Framework.Threading; +using osu.Game.Screens.Play.HUD; using osuTK; namespace osu.Game.Extensions @@ -43,5 +44,7 @@ namespace osu.Game.Extensions /// The delta vector in Parent's coordinates. public static Vector2 ScreenSpaceDeltaToParentSpace(this Drawable drawable, Vector2 delta) => drawable.Parent.ToLocalSpace(drawable.Parent.ToScreenSpace(Vector2.Zero) + delta); + + public static StoredSkinnableInfo CreateSerialisedInformation(this Drawable component) => new StoredSkinnableInfo(component); } } diff --git a/osu.Game/Screens/Play/HUD/ISkinnableInfo.cs b/osu.Game/Screens/Play/HUD/ISkinnableInfo.cs new file mode 100644 index 0000000000..0e571b5cb4 --- /dev/null +++ b/osu.Game/Screens/Play/HUD/ISkinnableInfo.cs @@ -0,0 +1,32 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework.Graphics; +using osu.Game.IO.Serialization; +using osuTK; + +namespace osu.Game.Screens.Play.HUD +{ + public interface ISkinnableInfo : IJsonSerializable + { + public Type Type { get; set; } + + public Type Target { get; set; } + + public Vector2 Position { get; set; } + + public float Rotation { get; set; } + + public Vector2 Scale { get; set; } + + public Anchor Anchor { get; set; } + } + + /// + /// A container which supports skinnable components being added to it. + /// + public interface ISkinnableTarget + { + } +} diff --git a/osu.Game/Screens/Play/HUD/SkinnableElementTargetContainer.cs b/osu.Game/Screens/Play/HUD/SkinnableElementTargetContainer.cs new file mode 100644 index 0000000000..7ba32e2d46 --- /dev/null +++ b/osu.Game/Screens/Play/HUD/SkinnableElementTargetContainer.cs @@ -0,0 +1,11 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics.Containers; + +namespace osu.Game.Screens.Play.HUD +{ + public class SkinnableElementTargetContainer : Container, ISkinnableTarget + { + } +} diff --git a/osu.Game/Screens/Play/HUD/StoredSkinnableInfo.cs b/osu.Game/Screens/Play/HUD/StoredSkinnableInfo.cs new file mode 100644 index 0000000000..e4ec035e68 --- /dev/null +++ b/osu.Game/Screens/Play/HUD/StoredSkinnableInfo.cs @@ -0,0 +1,43 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework.Graphics; +using osuTK; + +namespace osu.Game.Screens.Play.HUD +{ + /// + /// Serialised information governing custom changes to an . + /// + public class StoredSkinnableInfo : ISkinnableInfo + { + public StoredSkinnableInfo(Drawable component) + { + Type = component.GetType(); + + var target = component.Parent as ISkinnableTarget + // todo: this is temporary until we serialise the default layouts out of SkinnableDrawables. + ?? component.Parent?.Parent as ISkinnableTarget; + + Target = target?.GetType(); + + Position = component.Position; + Rotation = component.Rotation; + Scale = component.Scale; + Anchor = component.Anchor; + } + + public Type Type { get; set; } + + public Type Target { get; set; } + + public Vector2 Position { get; set; } + + public float Rotation { get; set; } + + public Vector2 Scale { get; set; } + + public Anchor Anchor { get; set; } + } +} diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index 6ddaa338cc..13449e3a2b 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -91,16 +91,16 @@ namespace osu.Game.Screens.Play { new Drawable[] { - new Container + new MainHUDElements { RelativeSizeAxes = Axes.Both, Children = new Drawable[] { - HealthDisplay = CreateHealthDisplay(), - AccuracyCounter = CreateAccuracyCounter(), - ScoreCounter = CreateScoreCounter(), - CreateComboCounter(), - CreateHitErrorDisplayOverlay(), + HealthDisplay = new SkinnableHealthDisplay(), + AccuracyCounter = new SkinnableAccuracyCounter(), + ScoreCounter = new SkinnableScoreCounter(), + new SkinnableComboCounter(), + new HitErrorDisplay(this.drawableRuleset?.FirstAvailableHitWindows), } }, }, @@ -263,48 +263,38 @@ namespace osu.Game.Screens.Play Progress.BindDrawableRuleset(drawableRuleset); } - protected SkinnableAccuracyCounter CreateAccuracyCounter() => new SkinnableAccuracyCounter(); - - protected SkinnableScoreCounter CreateScoreCounter() => new SkinnableScoreCounter(); - - protected SkinnableComboCounter CreateComboCounter() => new SkinnableComboCounter(); - - protected SkinnableHealthDisplay CreateHealthDisplay() => new SkinnableHealthDisplay(); - - protected virtual FailingLayer CreateFailingLayer() => new FailingLayer + protected FailingLayer CreateFailingLayer() => new FailingLayer { ShowHealth = { BindTarget = ShowHealthbar } }; - protected virtual KeyCounterDisplay CreateKeyCounter() => new KeyCounterDisplay + protected KeyCounterDisplay CreateKeyCounter() => new KeyCounterDisplay { Anchor = Anchor.BottomRight, Origin = Anchor.BottomRight, }; - protected virtual SongProgress CreateProgress() => new SongProgress + protected SongProgress CreateProgress() => new SongProgress { Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, RelativeSizeAxes = Axes.X, }; - protected virtual HoldForMenuButton CreateHoldForMenuButton() => new HoldForMenuButton + protected HoldForMenuButton CreateHoldForMenuButton() => new HoldForMenuButton { Anchor = Anchor.BottomRight, Origin = Anchor.BottomRight, }; - protected virtual ModDisplay CreateModsContainer() => new ModDisplay + protected ModDisplay CreateModsContainer() => new ModDisplay { Anchor = Anchor.TopRight, Origin = Anchor.TopRight, AutoSizeAxes = Axes.Both, }; - protected virtual HitErrorDisplay CreateHitErrorDisplayOverlay() => new HitErrorDisplay(drawableRuleset?.FirstAvailableHitWindows); - - protected virtual PlayerSettingsOverlay CreatePlayerSettingsOverlay() => new PlayerSettingsOverlay(); + protected PlayerSettingsOverlay CreatePlayerSettingsOverlay() => new PlayerSettingsOverlay(); public bool OnPressed(GlobalAction action) { @@ -347,5 +337,9 @@ namespace osu.Game.Screens.Play break; } } + + public class MainHUDElements : SkinnableElementTargetContainer + { + } } } diff --git a/osu.Game/Skinning/Editor/SkinBlueprintContainer.cs b/osu.Game/Skinning/Editor/SkinBlueprintContainer.cs index 35e93d9aff..a6f60afd90 100644 --- a/osu.Game/Skinning/Editor/SkinBlueprintContainer.cs +++ b/osu.Game/Skinning/Editor/SkinBlueprintContainer.cs @@ -1,9 +1,12 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.IO; using System.Linq; +using Newtonsoft.Json; using osu.Framework.Graphics; using osu.Framework.Testing; +using osu.Game.Extensions; using osu.Game.Rulesets.Edit; using osu.Game.Screens.Edit.Compose.Components; @@ -27,7 +30,15 @@ namespace osu.Game.Skinning.Editor private void checkForComponents() { - foreach (var c in target.ChildrenOfType().ToArray()) AddBlueprintFor(c); + ISkinnableComponent[] skinnableComponents = target.ChildrenOfType().ToArray(); + + // todo: the OfType() call can be removed with better IDrawable support. + string json = JsonConvert.SerializeObject(skinnableComponents.OfType().Select(d => d.CreateSerialisedInformation()), new JsonSerializerSettings { Formatting = Formatting.Indented }); + + File.WriteAllText("/Users/Dean/json-out.json", json); + + foreach (var c in skinnableComponents) + AddBlueprintFor(c); // We'd hope to eventually be running this in a more sensible way, but this handles situations where new drawables become present (ie. during ongoing gameplay) // or when drawables in the target are loaded asynchronously and may not be immediately available when this BlueprintContainer is loaded. diff --git a/osu.Game/Skinning/ISkin.cs b/osu.Game/Skinning/ISkin.cs index 73f7cf6d39..9fa781fa5d 100644 --- a/osu.Game/Skinning/ISkin.cs +++ b/osu.Game/Skinning/ISkin.cs @@ -57,5 +57,7 @@ namespace osu.Game.Skinning /// A matching value boxed in an , or null if unavailable. [CanBeNull] IBindable GetConfig(TLookup lookup); + + // IEnumerable ComponentInfo { get; } } } From 67ea4a7e97bce85d0f5f861d021b7113809bc16b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 May 2021 18:18:29 +0900 Subject: [PATCH 079/205] Read from skin config --- .../Play/HUD/SkinnableElementTargetContainer.cs | 17 +++++++++++++++-- osu.Game/Skinning/ISkin.cs | 4 ++-- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/SkinnableElementTargetContainer.cs b/osu.Game/Screens/Play/HUD/SkinnableElementTargetContainer.cs index 7ba32e2d46..8da7950c57 100644 --- a/osu.Game/Screens/Play/HUD/SkinnableElementTargetContainer.cs +++ b/osu.Game/Screens/Play/HUD/SkinnableElementTargetContainer.cs @@ -1,11 +1,24 @@ // 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.Containers; +using System; +using System.Collections.Generic; +using osu.Framework.Graphics; +using osu.Game.Skinning; namespace osu.Game.Screens.Play.HUD { - public class SkinnableElementTargetContainer : Container, ISkinnableTarget + public class SkinnableElementTargetContainer : SkinReloadableDrawable, ISkinnableTarget { + protected override void SkinChanged(ISkinSource skin, bool allowFallback) + { + base.SkinChanged(skin, allowFallback); + + var loadable = skin.GetConfig>(this.GetType()); + + ClearInternal(); + if (loadable != null) + LoadComponentsAsync(loadable.Value, AddRangeInternal); + } } } diff --git a/osu.Game/Skinning/ISkin.cs b/osu.Game/Skinning/ISkin.cs index 9fa781fa5d..5371893882 100644 --- a/osu.Game/Skinning/ISkin.cs +++ b/osu.Game/Skinning/ISkin.cs @@ -1,6 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; +using System.Collections.Generic; using JetBrains.Annotations; using osu.Framework.Audio.Sample; using osu.Framework.Bindables; @@ -57,7 +59,5 @@ namespace osu.Game.Skinning /// A matching value boxed in an , or null if unavailable. [CanBeNull] IBindable GetConfig(TLookup lookup); - - // IEnumerable ComponentInfo { get; } } } From 95a8f21ab26065f1bccc4d8cfa2ad83051da27a6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 May 2021 19:13:38 +0900 Subject: [PATCH 080/205] Add the concept of skinnable target containers and mark a basic one for HUD elements --- osu.Game/Extensions/DrawableExtensions.cs | 11 +++++- .../HUD/HitErrorMeters/BarHitErrorMeter.cs | 3 +- osu.Game/Screens/Play/HUD/ISkinnableInfo.cs | 10 +---- osu.Game/Screens/Play/HUD/ISkinnableTarget.cs | 15 +++++++ .../HUD/SkinnableElementTargetContainer.cs | 14 ++++--- ...toredSkinnableInfo.cs => SkinnableInfo.cs} | 32 +++++++++++---- osu.Game/Screens/Play/HUDOverlay.cs | 37 +++++++++++++----- osu.Game/Skinning/DefaultSkin.cs | 39 ++++++++++++++++++- osu.Game/Skinning/ISkin.cs | 2 - osu.Game/Skinning/SkinnableTarget.cs | 10 +++++ osu.Game/Skinning/SkinnableTargetComponent.cs | 17 ++++++++ 11 files changed, 154 insertions(+), 36 deletions(-) create mode 100644 osu.Game/Screens/Play/HUD/ISkinnableTarget.cs rename osu.Game/Screens/Play/HUD/{StoredSkinnableInfo.cs => SkinnableInfo.cs} (51%) create mode 100644 osu.Game/Skinning/SkinnableTarget.cs create mode 100644 osu.Game/Skinning/SkinnableTargetComponent.cs diff --git a/osu.Game/Extensions/DrawableExtensions.cs b/osu.Game/Extensions/DrawableExtensions.cs index 3d2ffe0ea1..289b305c27 100644 --- a/osu.Game/Extensions/DrawableExtensions.cs +++ b/osu.Game/Extensions/DrawableExtensions.cs @@ -45,6 +45,15 @@ namespace osu.Game.Extensions public static Vector2 ScreenSpaceDeltaToParentSpace(this Drawable drawable, Vector2 delta) => drawable.Parent.ToLocalSpace(drawable.Parent.ToScreenSpace(Vector2.Zero) + delta); - public static StoredSkinnableInfo CreateSerialisedInformation(this Drawable component) => new StoredSkinnableInfo(component); + public static SkinnableInfo CreateSerialisedInformation(this Drawable component) => new SkinnableInfo(component); + + public static void ApplySerialisedInformation(this Drawable component, ISkinnableInfo info) + { + // todo: can probably make this better via deserialisation directly using a common interface. + component.Position = info.Position; + component.Rotation = info.Rotation; + component.Scale = info.Scale; + component.Anchor = info.Anchor; + } } } diff --git a/osu.Game/Screens/Play/HUD/HitErrorMeters/BarHitErrorMeter.cs b/osu.Game/Screens/Play/HUD/HitErrorMeters/BarHitErrorMeter.cs index 2e84c9c97d..0e147f9238 100644 --- a/osu.Game/Screens/Play/HUD/HitErrorMeters/BarHitErrorMeter.cs +++ b/osu.Game/Screens/Play/HUD/HitErrorMeters/BarHitErrorMeter.cs @@ -13,13 +13,12 @@ using osu.Framework.Graphics.Sprites; using osu.Game.Graphics; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Scoring; -using osu.Game.Skinning; using osuTK; using osuTK.Graphics; namespace osu.Game.Screens.Play.HUD.HitErrorMeters { - public class BarHitErrorMeter : HitErrorMeter, ISkinnableComponent + public class BarHitErrorMeter : HitErrorMeter { private readonly Anchor alignment; diff --git a/osu.Game/Screens/Play/HUD/ISkinnableInfo.cs b/osu.Game/Screens/Play/HUD/ISkinnableInfo.cs index 0e571b5cb4..fc4e9680a8 100644 --- a/osu.Game/Screens/Play/HUD/ISkinnableInfo.cs +++ b/osu.Game/Screens/Play/HUD/ISkinnableInfo.cs @@ -4,6 +4,7 @@ using System; using osu.Framework.Graphics; using osu.Game.IO.Serialization; +using osu.Game.Skinning; using osuTK; namespace osu.Game.Screens.Play.HUD @@ -12,7 +13,7 @@ namespace osu.Game.Screens.Play.HUD { public Type Type { get; set; } - public Type Target { get; set; } + public SkinnableTarget? Target { get; set; } public Vector2 Position { get; set; } @@ -22,11 +23,4 @@ namespace osu.Game.Screens.Play.HUD public Anchor Anchor { get; set; } } - - /// - /// A container which supports skinnable components being added to it. - /// - public interface ISkinnableTarget - { - } } diff --git a/osu.Game/Screens/Play/HUD/ISkinnableTarget.cs b/osu.Game/Screens/Play/HUD/ISkinnableTarget.cs new file mode 100644 index 0000000000..6cd269333a --- /dev/null +++ b/osu.Game/Screens/Play/HUD/ISkinnableTarget.cs @@ -0,0 +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.Game.Skinning; + +namespace osu.Game.Screens.Play.HUD +{ + /// + /// A container which supports skinnable components being added to it. + /// + public interface ISkinnableTarget + { + public SkinnableTarget Target { get; } + } +} diff --git a/osu.Game/Screens/Play/HUD/SkinnableElementTargetContainer.cs b/osu.Game/Screens/Play/HUD/SkinnableElementTargetContainer.cs index 8da7950c57..bd82533823 100644 --- a/osu.Game/Screens/Play/HUD/SkinnableElementTargetContainer.cs +++ b/osu.Game/Screens/Play/HUD/SkinnableElementTargetContainer.cs @@ -1,24 +1,28 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; -using System.Collections.Generic; -using osu.Framework.Graphics; using osu.Game.Skinning; namespace osu.Game.Screens.Play.HUD { public class SkinnableElementTargetContainer : SkinReloadableDrawable, ISkinnableTarget { + public SkinnableTarget Target { get; } + + public SkinnableElementTargetContainer(SkinnableTarget target) + { + Target = target; + } + protected override void SkinChanged(ISkinSource skin, bool allowFallback) { base.SkinChanged(skin, allowFallback); - var loadable = skin.GetConfig>(this.GetType()); + var loadable = skin.GetDrawableComponent(new SkinnableTargetComponent(Target)); ClearInternal(); if (loadable != null) - LoadComponentsAsync(loadable.Value, AddRangeInternal); + LoadComponentAsync(loadable, AddInternal); } } } diff --git a/osu.Game/Screens/Play/HUD/StoredSkinnableInfo.cs b/osu.Game/Screens/Play/HUD/SkinnableInfo.cs similarity index 51% rename from osu.Game/Screens/Play/HUD/StoredSkinnableInfo.cs rename to osu.Game/Screens/Play/HUD/SkinnableInfo.cs index e4ec035e68..5b1f840efd 100644 --- a/osu.Game/Screens/Play/HUD/StoredSkinnableInfo.cs +++ b/osu.Game/Screens/Play/HUD/SkinnableInfo.cs @@ -2,7 +2,11 @@ // See the LICENCE file in the repository root for full licence text. using System; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; using osu.Framework.Graphics; +using osu.Game.Extensions; +using osu.Game.Skinning; using osuTK; namespace osu.Game.Screens.Play.HUD @@ -10,17 +14,20 @@ namespace osu.Game.Screens.Play.HUD /// /// Serialised information governing custom changes to an . /// - public class StoredSkinnableInfo : ISkinnableInfo + [Serializable] + public class SkinnableInfo : ISkinnableInfo { - public StoredSkinnableInfo(Drawable component) + public SkinnableInfo() + { + } + + public SkinnableInfo(Drawable component) { Type = component.GetType(); - var target = component.Parent as ISkinnableTarget - // todo: this is temporary until we serialise the default layouts out of SkinnableDrawables. - ?? component.Parent?.Parent as ISkinnableTarget; + ISkinnableTarget target = component.Parent as ISkinnableTarget; - Target = target?.GetType(); + Target = target?.Target; Position = component.Position; Rotation = component.Rotation; @@ -30,7 +37,8 @@ namespace osu.Game.Screens.Play.HUD public Type Type { get; set; } - public Type Target { get; set; } + [JsonConverter(typeof(StringEnumConverter))] + public SkinnableTarget? Target { get; set; } public Vector2 Position { get; set; } @@ -40,4 +48,14 @@ namespace osu.Game.Screens.Play.HUD public Anchor Anchor { get; set; } } + + public static class SkinnableInfoExtensions + { + public static Drawable CreateInstance(this ISkinnableInfo info) + { + Drawable d = (Drawable)Activator.CreateInstance(info.Type); + d.ApplySerialisedInformation(info); + return d; + } + } } diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index 13449e3a2b..1a12ca4cbd 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -91,16 +91,37 @@ namespace osu.Game.Screens.Play { new Drawable[] { - new MainHUDElements + new Container { RelativeSizeAxes = Axes.Both, Children = new Drawable[] { - HealthDisplay = new SkinnableHealthDisplay(), - AccuracyCounter = new SkinnableAccuracyCounter(), - ScoreCounter = new SkinnableScoreCounter(), - new SkinnableComboCounter(), - new HitErrorDisplay(this.drawableRuleset?.FirstAvailableHitWindows), + new SkinnableElementTargetContainer(SkinnableTarget.MainHUDComponents) + { + RelativeSizeAxes = Axes.Both, + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Alpha = 0, + Children = new Drawable[] + { + // remaining cross-dependencies need tidying. + // kept to ensure non-null, but hidden for testing. + HealthDisplay = new SkinnableHealthDisplay(), + AccuracyCounter = new SkinnableAccuracyCounter(), + ScoreCounter = new SkinnableScoreCounter(), + } + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + // still need to be migrated; a bit more involved. + new HitErrorDisplay(this.drawableRuleset?.FirstAvailableHitWindows), + } + }, } }, }, @@ -337,9 +358,5 @@ namespace osu.Game.Screens.Play break; } } - - public class MainHUDElements : SkinnableElementTargetContainer - { - } } } diff --git a/osu.Game/Skinning/DefaultSkin.cs b/osu.Game/Skinning/DefaultSkin.cs index 0b3f5f3cde..48da841c45 100644 --- a/osu.Game/Skinning/DefaultSkin.cs +++ b/osu.Game/Skinning/DefaultSkin.cs @@ -2,12 +2,17 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; +using System.IO; +using System.Linq; +using Newtonsoft.Json; using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.OpenGL.Textures; using osu.Framework.Graphics.Textures; using osu.Game.Audio; +using osu.Game.Screens.Play.HUD; using osuTK.Graphics; namespace osu.Game.Skinning @@ -20,7 +25,29 @@ namespace osu.Game.Skinning Configuration = new DefaultSkinConfiguration(); } - public override Drawable GetDrawableComponent(ISkinComponent component) => null; + public override Drawable GetDrawableComponent(ISkinComponent component) + { + switch (component) + { + case SkinnableTargetComponent target: + switch (target.Target) + { + case SkinnableTarget.MainHUDComponents: + var infos = JsonConvert.DeserializeObject>(File.ReadAllText("/Users/Dean/json-out.json")).Where(i => i.Target == SkinnableTarget.MainHUDComponents); + + var container = new SkinnableTargetWrapper(target.Target) + { + ChildrenEnumerable = infos.Select(i => i.CreateInstance()) + }; + + return container; + } + + break; + } + + return null; + } public override Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => null; @@ -45,4 +72,14 @@ namespace osu.Game.Skinning return null; } } + + public class SkinnableTargetWrapper : Container, ISkinnableTarget + { + public SkinnableTarget Target { get; } + + public SkinnableTargetWrapper(SkinnableTarget target) + { + Target = target; + } + } } diff --git a/osu.Game/Skinning/ISkin.cs b/osu.Game/Skinning/ISkin.cs index 5371893882..73f7cf6d39 100644 --- a/osu.Game/Skinning/ISkin.cs +++ b/osu.Game/Skinning/ISkin.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; -using System.Collections.Generic; using JetBrains.Annotations; using osu.Framework.Audio.Sample; using osu.Framework.Bindables; diff --git a/osu.Game/Skinning/SkinnableTarget.cs b/osu.Game/Skinning/SkinnableTarget.cs new file mode 100644 index 0000000000..7b1eae126c --- /dev/null +++ b/osu.Game/Skinning/SkinnableTarget.cs @@ -0,0 +1,10 @@ +// 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.Skinning +{ + public enum SkinnableTarget + { + MainHUDComponents + } +} diff --git a/osu.Game/Skinning/SkinnableTargetComponent.cs b/osu.Game/Skinning/SkinnableTargetComponent.cs new file mode 100644 index 0000000000..a17aafe6e7 --- /dev/null +++ b/osu.Game/Skinning/SkinnableTargetComponent.cs @@ -0,0 +1,17 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +namespace osu.Game.Skinning +{ + public class SkinnableTargetComponent : ISkinComponent + { + public readonly SkinnableTarget Target; + + public string LookupName => Target.ToString(); + + public SkinnableTargetComponent(SkinnableTarget target) + { + Target = target; + } + } +} From 95a497e9df7313ed1e6ae0513c1a623fba63c7cf Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 10 May 2021 15:31:14 +0900 Subject: [PATCH 081/205] Remove unused interface class for simplicity --- osu.Game/Extensions/DrawableExtensions.cs | 2 +- osu.Game/Screens/Play/HUD/ISkinnableInfo.cs | 26 --------------------- osu.Game/Screens/Play/HUD/SkinnableInfo.cs | 12 ++++------ 3 files changed, 6 insertions(+), 34 deletions(-) delete mode 100644 osu.Game/Screens/Play/HUD/ISkinnableInfo.cs diff --git a/osu.Game/Extensions/DrawableExtensions.cs b/osu.Game/Extensions/DrawableExtensions.cs index 289b305c27..e84c9e5a98 100644 --- a/osu.Game/Extensions/DrawableExtensions.cs +++ b/osu.Game/Extensions/DrawableExtensions.cs @@ -47,7 +47,7 @@ namespace osu.Game.Extensions public static SkinnableInfo CreateSerialisedInformation(this Drawable component) => new SkinnableInfo(component); - public static void ApplySerialisedInformation(this Drawable component, ISkinnableInfo info) + public static void ApplySerialisedInformation(this Drawable component, SkinnableInfo info) { // todo: can probably make this better via deserialisation directly using a common interface. component.Position = info.Position; diff --git a/osu.Game/Screens/Play/HUD/ISkinnableInfo.cs b/osu.Game/Screens/Play/HUD/ISkinnableInfo.cs deleted file mode 100644 index fc4e9680a8..0000000000 --- a/osu.Game/Screens/Play/HUD/ISkinnableInfo.cs +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System; -using osu.Framework.Graphics; -using osu.Game.IO.Serialization; -using osu.Game.Skinning; -using osuTK; - -namespace osu.Game.Screens.Play.HUD -{ - public interface ISkinnableInfo : IJsonSerializable - { - public Type Type { get; set; } - - public SkinnableTarget? Target { get; set; } - - public Vector2 Position { get; set; } - - public float Rotation { get; set; } - - public Vector2 Scale { get; set; } - - public Anchor Anchor { get; set; } - } -} diff --git a/osu.Game/Screens/Play/HUD/SkinnableInfo.cs b/osu.Game/Screens/Play/HUD/SkinnableInfo.cs index 5b1f840efd..2edc99781f 100644 --- a/osu.Game/Screens/Play/HUD/SkinnableInfo.cs +++ b/osu.Game/Screens/Play/HUD/SkinnableInfo.cs @@ -6,6 +6,7 @@ using Newtonsoft.Json; using Newtonsoft.Json.Converters; using osu.Framework.Graphics; using osu.Game.Extensions; +using osu.Game.IO.Serialization; using osu.Game.Skinning; using osuTK; @@ -15,7 +16,7 @@ namespace osu.Game.Screens.Play.HUD /// Serialised information governing custom changes to an . /// [Serializable] - public class SkinnableInfo : ISkinnableInfo + public class SkinnableInfo : IJsonSerializable { public SkinnableInfo() { @@ -47,14 +48,11 @@ namespace osu.Game.Screens.Play.HUD public Vector2 Scale { get; set; } public Anchor Anchor { get; set; } - } - public static class SkinnableInfoExtensions - { - public static Drawable CreateInstance(this ISkinnableInfo info) + public Drawable CreateInstance() { - Drawable d = (Drawable)Activator.CreateInstance(info.Type); - d.ApplySerialisedInformation(info); + Drawable d = (Drawable)Activator.CreateInstance(Type); + d.ApplySerialisedInformation(this); return d; } } From df72656aa1238a337550ed1b114a45300c2455a3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 10 May 2021 15:40:33 +0900 Subject: [PATCH 082/205] Remove downwards dependency from `HUDOverlay` to `HealthDisplay` --- osu.Game/Screens/Play/HUD/HealthDisplay.cs | 13 +++++++++++-- osu.Game/Screens/Play/HUDOverlay.cs | 3 --- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/HealthDisplay.cs b/osu.Game/Screens/Play/HUD/HealthDisplay.cs index 6c2571cc28..e0ddde026b 100644 --- a/osu.Game/Screens/Play/HUD/HealthDisplay.cs +++ b/osu.Game/Screens/Play/HUD/HealthDisplay.cs @@ -3,6 +3,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Scoring; @@ -16,6 +17,8 @@ namespace osu.Game.Screens.Play.HUD /// public abstract class HealthDisplay : Container { + private Bindable showHealthbar; + [Resolved] protected HealthProcessor HealthProcessor { get; private set; } @@ -29,12 +32,18 @@ namespace osu.Game.Screens.Play.HUD { } - [BackgroundDependencyLoader] - private void load() + [BackgroundDependencyLoader(true)] + private void load(HUDOverlay hud) { Current.BindTo(HealthProcessor.Health); HealthProcessor.NewJudgement += onNewJudgement; + + if (hud != null) + { + showHealthbar = hud.ShowHealthbar.GetBoundCopy(); + showHealthbar.BindValueChanged(healthBar => this.FadeTo(healthBar.NewValue ? 1 : 0, HUDOverlay.FADE_DURATION, HUDOverlay.FADE_EASING), true); + } } private void onNewJudgement(JudgementResult judgement) diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index 1a12ca4cbd..7220365673 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -36,7 +36,6 @@ namespace osu.Game.Screens.Play public readonly KeyCounterDisplay KeyCounter; public readonly SkinnableScoreCounter ScoreCounter; public readonly SkinnableAccuracyCounter AccuracyCounter; - public readonly SkinnableHealthDisplay HealthDisplay; public readonly SongProgress Progress; public readonly ModDisplay ModDisplay; public readonly HoldForMenuButton HoldToQuit; @@ -108,7 +107,6 @@ namespace osu.Game.Screens.Play { // remaining cross-dependencies need tidying. // kept to ensure non-null, but hidden for testing. - HealthDisplay = new SkinnableHealthDisplay(), AccuracyCounter = new SkinnableAccuracyCounter(), ScoreCounter = new SkinnableScoreCounter(), } @@ -206,7 +204,6 @@ namespace osu.Game.Screens.Play { base.LoadComplete(); - ShowHealthbar.BindValueChanged(healthBar => HealthDisplay.FadeTo(healthBar.NewValue ? 1 : 0, FADE_DURATION, FADE_EASING), true); ShowHud.BindValueChanged(visible => hideTargets.ForEach(d => d.FadeTo(visible.NewValue ? 1 : 0, FADE_DURATION, FADE_EASING))); IsBreakTime.BindValueChanged(_ => updateVisibility()); From 1742ee89e0680662cd5d12348ddceef4c5df2be9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 10 May 2021 22:32:56 +0900 Subject: [PATCH 083/205] Fix incorrect xmldoc for `DeleteFile` --- osu.Game/Database/ArchiveModelManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs index dbeaebb1cd..e0f80d2743 100644 --- a/osu.Game/Database/ArchiveModelManager.cs +++ b/osu.Game/Database/ArchiveModelManager.cs @@ -472,7 +472,7 @@ namespace osu.Game.Database } /// - /// Delete new file. + /// Delete an existing file. /// /// The item to operate on. /// The existing file to be deleted. From 6a88b8888b1be641d203ed598998b7f0e985c410 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 10 May 2021 22:33:45 +0900 Subject: [PATCH 084/205] Add basic support for child serialisation --- osu.Game/Extensions/DrawableExtensions.cs | 7 ++++ osu.Game/Screens/Play/HUD/SkinnableInfo.cs | 37 ++++++++++++++-------- 2 files changed, 31 insertions(+), 13 deletions(-) diff --git a/osu.Game/Extensions/DrawableExtensions.cs b/osu.Game/Extensions/DrawableExtensions.cs index e84c9e5a98..6837a802f2 100644 --- a/osu.Game/Extensions/DrawableExtensions.cs +++ b/osu.Game/Extensions/DrawableExtensions.cs @@ -3,6 +3,7 @@ using System; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Framework.Input.Bindings; using osu.Framework.Threading; using osu.Game.Screens.Play.HUD; @@ -54,6 +55,12 @@ namespace osu.Game.Extensions component.Rotation = info.Rotation; component.Scale = info.Scale; component.Anchor = info.Anchor; + + if (component is Container container) + { + foreach (var child in info.Children) + container.Add(child.CreateInstance()); + } } } } diff --git a/osu.Game/Screens/Play/HUD/SkinnableInfo.cs b/osu.Game/Screens/Play/HUD/SkinnableInfo.cs index 2edc99781f..ec4eaa0e59 100644 --- a/osu.Game/Screens/Play/HUD/SkinnableInfo.cs +++ b/osu.Game/Screens/Play/HUD/SkinnableInfo.cs @@ -2,9 +2,12 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; +using System.Linq; using Newtonsoft.Json; using Newtonsoft.Json.Converters; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Game.Extensions; using osu.Game.IO.Serialization; using osu.Game.Skinning; @@ -18,6 +21,21 @@ namespace osu.Game.Screens.Play.HUD [Serializable] public class SkinnableInfo : IJsonSerializable { + public Type Type { get; set; } + + [JsonConverter(typeof(StringEnumConverter))] + public SkinnableTarget? Target { get; set; } + + public Vector2 Position { get; set; } + + public float Rotation { get; set; } + + public Vector2 Scale { get; set; } + + public Anchor Anchor { get; set; } + + public List Children { get; } = new List(); + public SkinnableInfo() { } @@ -34,21 +52,14 @@ namespace osu.Game.Screens.Play.HUD Rotation = component.Rotation; Scale = component.Scale; Anchor = component.Anchor; + + if (component is Container container) + { + foreach (var child in container.Children.OfType().OfType()) + Children.Add(child.CreateSerialisedInformation()); + } } - public Type Type { get; set; } - - [JsonConverter(typeof(StringEnumConverter))] - public SkinnableTarget? Target { get; set; } - - public Vector2 Position { get; set; } - - public float Rotation { get; set; } - - public Vector2 Scale { get; set; } - - public Anchor Anchor { get; set; } - public Drawable CreateInstance() { Drawable d = (Drawable)Activator.CreateInstance(Type); From b54eb56169e08757e85abe78b1113e7425108717 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 10 May 2021 22:34:24 +0900 Subject: [PATCH 085/205] Move new judgement binding to `LoadComplete` to ensure containers are loaded --- osu.Game/Screens/Play/HUD/HealthDisplay.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/HealthDisplay.cs b/osu.Game/Screens/Play/HUD/HealthDisplay.cs index e0ddde026b..e18a28eded 100644 --- a/osu.Game/Screens/Play/HUD/HealthDisplay.cs +++ b/osu.Game/Screens/Play/HUD/HealthDisplay.cs @@ -37,8 +37,6 @@ namespace osu.Game.Screens.Play.HUD { Current.BindTo(HealthProcessor.Health); - HealthProcessor.NewJudgement += onNewJudgement; - if (hud != null) { showHealthbar = hud.ShowHealthbar.GetBoundCopy(); @@ -46,6 +44,13 @@ namespace osu.Game.Screens.Play.HUD } } + protected override void LoadComplete() + { + base.LoadComplete(); + + HealthProcessor.NewJudgement += onNewJudgement; + } + private void onNewJudgement(JudgementResult judgement) { if (judgement.IsHit && judgement.Type != HitResult.IgnoreHit) From 004798d61d44d88114acfd7c2861378261eab107 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 10 May 2021 22:36:20 +0900 Subject: [PATCH 086/205] Update Legacy components to not require skin in ctor --- .../Legacy/LegacyCatchComboCounter.cs | 4 ++-- .../Skinning/Legacy/LegacySpinner.cs | 4 ++-- .../Legacy/OsuLegacySkinTransformer.cs | 2 +- .../Visual/Gameplay/TestSceneSkinEditor.cs | 14 ++++++++++++- .../Screens/Play/HUD/LegacyComboCounter.cs | 4 ++-- .../HUD/SkinnableElementTargetContainer.cs | 15 ++++++++++--- osu.Game/Skinning/LegacyAccuracyCounter.cs | 11 +++++----- osu.Game/Skinning/LegacyHealthDisplay.cs | 21 ++++++++----------- osu.Game/Skinning/LegacyRollingCounter.cs | 7 ++----- osu.Game/Skinning/LegacyScoreCounter.cs | 12 +++++------ osu.Game/Skinning/LegacySkin.cs | 6 +++--- osu.Game/Skinning/LegacySpriteText.cs | 14 ++++++++++--- 12 files changed, 68 insertions(+), 46 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyCatchComboCounter.cs b/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyCatchComboCounter.cs index 28ee7bd813..33c3867f5a 100644 --- a/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyCatchComboCounter.cs +++ b/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyCatchComboCounter.cs @@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy InternalChildren = new Drawable[] { - explosion = new LegacyRollingCounter(skin, LegacyFont.Combo) + explosion = new LegacyRollingCounter(LegacyFont.Combo) { Alpha = 0.65f, Blending = BlendingParameters.Additive, @@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy Origin = Anchor.Centre, Scale = new Vector2(1.5f), }, - counter = new LegacyRollingCounter(skin, LegacyFont.Combo) + counter = new LegacyRollingCounter(LegacyFont.Combo) { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs index 7eb6898abc..959589620b 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs @@ -76,7 +76,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy Scale = new Vector2(SPRITE_SCALE), Y = SPINNER_TOP_OFFSET + 115, }, - bonusCounter = new LegacySpriteText(source, LegacyFont.Score) + bonusCounter = new LegacySpriteText(LegacyFont.Score) { Alpha = 0f, Anchor = Anchor.TopCentre, @@ -92,7 +92,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy Scale = new Vector2(SPRITE_SCALE), Position = new Vector2(-87, 445 + spm_hide_offset), }, - spmCounter = new LegacySpriteText(source, LegacyFont.Score) + spmCounter = new LegacySpriteText(LegacyFont.Score) { Anchor = Anchor.TopCentre, Origin = Anchor.TopRight, diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs index ffe238c507..88302ebc57 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs @@ -100,7 +100,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy if (!this.HasFont(LegacyFont.HitCircle)) return null; - return new LegacySpriteText(Source, LegacyFont.HitCircle) + return new LegacySpriteText(LegacyFont.HitCircle) { // stable applies a blanket 0.8x scale to hitcircle fonts Scale = new Vector2(0.8f), diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs index c53ac42d12..b0edc0dd68 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs @@ -2,10 +2,12 @@ // 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 @@ -14,12 +16,22 @@ namespace osu.Game.Tests.Visual.Gameplay { private SkinEditor skinEditor; + [Resolved] + private SkinManager skinManager { get; set; } + [SetUpSteps] public override void SetUpSteps() { + AddStep("set empty legacy skin", () => + { + var imported = skinManager.Import(new SkinInfo { Name = "test skin" }).Result; + + skinManager.CurrentSkinInfo.Value = imported; + }); + base.SetUpSteps(); - AddStep("add editor overlay", () => + AddStep("reload skin editor", () => { skinEditor?.Expire(); Player.ScaleTo(SkinEditorOverlay.VISIBLE_TARGET_SCALE); diff --git a/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs b/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs index 73305ac93e..2565faf423 100644 --- a/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs +++ b/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs @@ -84,13 +84,13 @@ namespace osu.Game.Screens.Play.HUD { InternalChildren = new[] { - popOutCount = new LegacySpriteText(skin, LegacyFont.Combo) + popOutCount = new LegacySpriteText(LegacyFont.Combo) { Alpha = 0, Margin = new MarginPadding(0.05f), Blending = BlendingParameters.Additive, }, - displayedCountSpriteText = new LegacySpriteText(skin, LegacyFont.Combo) + displayedCountSpriteText = new LegacySpriteText(LegacyFont.Combo) { Alpha = 0, }, diff --git a/osu.Game/Screens/Play/HUD/SkinnableElementTargetContainer.cs b/osu.Game/Screens/Play/HUD/SkinnableElementTargetContainer.cs index bd82533823..f1b282119b 100644 --- a/osu.Game/Screens/Play/HUD/SkinnableElementTargetContainer.cs +++ b/osu.Game/Screens/Play/HUD/SkinnableElementTargetContainer.cs @@ -1,12 +1,17 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Collections.Generic; +using System.Linq; +using osu.Game.Extensions; using osu.Game.Skinning; namespace osu.Game.Screens.Play.HUD { public class SkinnableElementTargetContainer : SkinReloadableDrawable, ISkinnableTarget { + private SkinnableTargetWrapper content; + public SkinnableTarget Target { get; } public SkinnableElementTargetContainer(SkinnableTarget target) @@ -14,15 +19,19 @@ namespace osu.Game.Screens.Play.HUD Target = target; } + public IEnumerable CreateSerialisedChildren() => + content.Select(d => d.CreateSerialisedInformation()); + protected override void SkinChanged(ISkinSource skin, bool allowFallback) { base.SkinChanged(skin, allowFallback); - var loadable = skin.GetDrawableComponent(new SkinnableTargetComponent(Target)); + content = skin.GetDrawableComponent(new SkinnableTargetComponent(Target)) as SkinnableTargetWrapper; ClearInternal(); - if (loadable != null) - LoadComponentAsync(loadable, AddInternal); + + if (content != null) + LoadComponentAsync(content, AddInternal); } } } diff --git a/osu.Game/Skinning/LegacyAccuracyCounter.cs b/osu.Game/Skinning/LegacyAccuracyCounter.cs index 3efcd5555e..e2fe01efe4 100644 --- a/osu.Game/Skinning/LegacyAccuracyCounter.cs +++ b/osu.Game/Skinning/LegacyAccuracyCounter.cs @@ -12,23 +12,22 @@ namespace osu.Game.Skinning { public class LegacyAccuracyCounter : GameplayAccuracyCounter, ISkinnableComponent { - private readonly ISkin skin; + [Resolved] + private ISkinSource skin { get; set; } - public LegacyAccuracyCounter(ISkin skin) + public LegacyAccuracyCounter() { Anchor = Anchor.TopRight; Origin = Anchor.TopRight; Scale = new Vector2(0.6f); Margin = new MarginPadding(10); - - this.skin = skin; } [Resolved(canBeNull: true)] private HUDOverlay hud { get; set; } - protected sealed override OsuSpriteText CreateSpriteText() => new LegacySpriteText(skin, LegacyFont.Score) + protected sealed override OsuSpriteText CreateSpriteText() => new LegacySpriteText(LegacyFont.Score) { Anchor = Anchor.TopRight, Origin = Anchor.TopRight, @@ -38,7 +37,7 @@ namespace osu.Game.Skinning { base.Update(); - if (hud?.ScoreCounter.Drawable is LegacyScoreCounter score) + if (hud?.ScoreCounter?.Drawable is LegacyScoreCounter score) { // for now align with the score counter. eventually this will be user customisable. Y = Parent.ToLocalSpace(score.ScreenSpaceDrawQuad.BottomRight).Y; diff --git a/osu.Game/Skinning/LegacyHealthDisplay.cs b/osu.Game/Skinning/LegacyHealthDisplay.cs index c1979efbc2..717ca62da7 100644 --- a/osu.Game/Skinning/LegacyHealthDisplay.cs +++ b/osu.Game/Skinning/LegacyHealthDisplay.cs @@ -20,7 +20,9 @@ namespace osu.Game.Skinning { private const double epic_cutoff = 0.5; - private readonly Skin skin; + [Resolved] + private ISkinSource skin { get; set; } + private LegacyHealthPiece fill; private LegacyHealthPiece marker; @@ -28,11 +30,6 @@ namespace osu.Game.Skinning private bool isNewStyle; - public LegacyHealthDisplay(Skin skin) - { - this.skin = skin; - } - [BackgroundDependencyLoader] private void load() { @@ -79,7 +76,7 @@ namespace osu.Game.Skinning protected override void Flash(JudgementResult result) => marker.Flash(result); - private static Texture getTexture(Skin skin, string name) => skin.GetTexture($"scorebar-{name}"); + private static Texture getTexture(ISkinSource skin, string name) => skin.GetTexture($"scorebar-{name}"); private static Color4 getFillColour(double hp) { @@ -98,7 +95,7 @@ namespace osu.Game.Skinning private readonly Texture dangerTexture; private readonly Texture superDangerTexture; - public LegacyOldStyleMarker(Skin skin) + public LegacyOldStyleMarker(ISkinSource skin) { normalTexture = getTexture(skin, "ki"); dangerTexture = getTexture(skin, "kidanger"); @@ -129,9 +126,9 @@ namespace osu.Game.Skinning public class LegacyNewStyleMarker : LegacyMarker { - private readonly Skin skin; + private readonly ISkinSource skin; - public LegacyNewStyleMarker(Skin skin) + public LegacyNewStyleMarker(ISkinSource skin) { this.skin = skin; } @@ -153,7 +150,7 @@ namespace osu.Game.Skinning internal class LegacyOldStyleFill : LegacyHealthPiece { - public LegacyOldStyleFill(Skin skin) + public LegacyOldStyleFill(ISkinSource skin) { // required for sizing correctly.. var firstFrame = getTexture(skin, "colour-0"); @@ -176,7 +173,7 @@ namespace osu.Game.Skinning internal class LegacyNewStyleFill : LegacyHealthPiece { - public LegacyNewStyleFill(Skin skin) + public LegacyNewStyleFill(ISkinSource skin) { InternalChild = new Sprite { diff --git a/osu.Game/Skinning/LegacyRollingCounter.cs b/osu.Game/Skinning/LegacyRollingCounter.cs index 0261db0e64..b531ae1e6f 100644 --- a/osu.Game/Skinning/LegacyRollingCounter.cs +++ b/osu.Game/Skinning/LegacyRollingCounter.cs @@ -12,7 +12,6 @@ namespace osu.Game.Skinning /// public class LegacyRollingCounter : RollingCounter { - private readonly ISkin skin; private readonly LegacyFont font; protected override bool IsRollingProportional => true; @@ -20,11 +19,9 @@ namespace osu.Game.Skinning /// /// Creates a new . /// - /// The from which to get counter number sprites. /// The legacy font to use for the counter. - public LegacyRollingCounter(ISkin skin, LegacyFont font) + public LegacyRollingCounter(LegacyFont font) { - this.skin = skin; this.font = font; } @@ -33,6 +30,6 @@ namespace osu.Game.Skinning return Math.Abs(newValue - currentValue) * 75.0; } - protected sealed override OsuSpriteText CreateSpriteText() => new LegacySpriteText(skin, font); + protected sealed override OsuSpriteText CreateSpriteText() => new LegacySpriteText(font); } } diff --git a/osu.Game/Skinning/LegacyScoreCounter.cs b/osu.Game/Skinning/LegacyScoreCounter.cs index ecb907e601..0696d2bedd 100644 --- a/osu.Game/Skinning/LegacyScoreCounter.cs +++ b/osu.Game/Skinning/LegacyScoreCounter.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Screens.Play.HUD; @@ -10,24 +11,23 @@ namespace osu.Game.Skinning { public class LegacyScoreCounter : GameplayScoreCounter, ISkinnableComponent { - private readonly ISkin skin; - protected override double RollingDuration => 1000; protected override Easing RollingEasing => Easing.Out; - public LegacyScoreCounter(ISkin skin) + [Resolved] + private ISkinSource skin { get; set; } + + public LegacyScoreCounter() : base(6) { Anchor = Anchor.TopRight; Origin = Anchor.TopRight; - this.skin = skin; - Scale = new Vector2(0.96f); Margin = new MarginPadding(10); } - protected sealed override OsuSpriteText CreateSpriteText() => new LegacySpriteText(skin, LegacyFont.Score) + protected sealed override OsuSpriteText CreateSpriteText() => new LegacySpriteText(LegacyFont.Score) { Anchor = Anchor.TopRight, Origin = Anchor.TopRight, diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index eae3b69233..70f8d1d882 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -334,13 +334,13 @@ namespace osu.Game.Skinning return new LegacyComboCounter(); case HUDSkinComponents.ScoreCounter: - return new LegacyScoreCounter(this); + return new LegacyScoreCounter(); case HUDSkinComponents.AccuracyCounter: - return new LegacyAccuracyCounter(this); + return new LegacyAccuracyCounter(); case HUDSkinComponents.HealthDisplay: - return new LegacyHealthDisplay(this); + return new LegacyHealthDisplay(); } return null; diff --git a/osu.Game/Skinning/LegacySpriteText.cs b/osu.Game/Skinning/LegacySpriteText.cs index c55400e219..7895fcccca 100644 --- a/osu.Game/Skinning/LegacySpriteText.cs +++ b/osu.Game/Skinning/LegacySpriteText.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System.Threading.Tasks; +using osu.Framework.Allocation; using osu.Framework.Graphics.Sprites; using osu.Framework.Text; using osu.Game.Graphics.Sprites; @@ -9,19 +10,26 @@ using osuTK; namespace osu.Game.Skinning { - public class LegacySpriteText : OsuSpriteText + public sealed class LegacySpriteText : OsuSpriteText { - private readonly LegacyGlyphStore glyphStore; + private readonly LegacyFont font; + + private LegacyGlyphStore glyphStore; protected override char FixedWidthReferenceCharacter => '5'; protected override char[] FixedWidthExcludeCharacters => new[] { ',', '.', '%', 'x' }; - public LegacySpriteText(ISkin skin, LegacyFont font) + public LegacySpriteText(LegacyFont font) { + this.font = font; Shadow = false; UseFullGlyphHeight = false; + } + [BackgroundDependencyLoader] + private void load(ISkinSource skin) + { Font = new FontUsage(skin.GetFontPrefix(font), 1, fixedWidth: true); Spacing = new Vector2(-skin.GetFontOverlap(font), 0); From b248b2e5e3bf8b62ea94b9e1d5eb85e3809d448e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 10 May 2021 22:43:48 +0900 Subject: [PATCH 087/205] Hook up full save/load flow --- osu.Game/Screens/Play/HUD/ISkinnableTarget.cs | 15 ---- osu.Game/Screens/Play/HUDOverlay.cs | 2 +- osu.Game/Skinning/DefaultSkin.cs | 42 +--------- .../Skinning/Editor/SkinBlueprintContainer.cs | 8 -- osu.Game/Skinning/Editor/SkinEditor.cs | 43 +++++++++- osu.Game/Skinning/ISkinnableTarget.cs | 4 +- osu.Game/Skinning/LegacySkin.cs | 26 +++++- osu.Game/Skinning/Skin.cs | 81 ++++++++++++++++++- osu.Game/Skinning/SkinManager.cs | 32 ++++++++ 9 files changed, 181 insertions(+), 72 deletions(-) delete mode 100644 osu.Game/Screens/Play/HUD/ISkinnableTarget.cs diff --git a/osu.Game/Screens/Play/HUD/ISkinnableTarget.cs b/osu.Game/Screens/Play/HUD/ISkinnableTarget.cs deleted file mode 100644 index 6cd269333a..0000000000 --- a/osu.Game/Screens/Play/HUD/ISkinnableTarget.cs +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Game.Skinning; - -namespace osu.Game.Screens.Play.HUD -{ - /// - /// A container which supports skinnable components being added to it. - /// - public interface ISkinnableTarget - { - public SkinnableTarget Target { get; } - } -} diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index 7220365673..30b735d28a 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -22,7 +22,7 @@ using osuTK; namespace osu.Game.Screens.Play { [Cached] - public class HUDOverlay : Container, IKeyBindingHandler, IDefaultSkinnableTarget + public class HUDOverlay : Container, IKeyBindingHandler { public const float FADE_DURATION = 300; diff --git a/osu.Game/Skinning/DefaultSkin.cs b/osu.Game/Skinning/DefaultSkin.cs index 48da841c45..bfb5b8ef56 100644 --- a/osu.Game/Skinning/DefaultSkin.cs +++ b/osu.Game/Skinning/DefaultSkin.cs @@ -2,17 +2,11 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; -using System.IO; -using System.Linq; -using Newtonsoft.Json; using osu.Framework.Audio.Sample; using osu.Framework.Bindables; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.OpenGL.Textures; using osu.Framework.Graphics.Textures; using osu.Game.Audio; -using osu.Game.Screens.Play.HUD; using osuTK.Graphics; namespace osu.Game.Skinning @@ -20,35 +14,11 @@ namespace osu.Game.Skinning public class DefaultSkin : Skin { public DefaultSkin() - : base(SkinInfo.Default) + : base(SkinInfo.Default, null) { Configuration = new DefaultSkinConfiguration(); } - public override Drawable GetDrawableComponent(ISkinComponent component) - { - switch (component) - { - case SkinnableTargetComponent target: - switch (target.Target) - { - case SkinnableTarget.MainHUDComponents: - var infos = JsonConvert.DeserializeObject>(File.ReadAllText("/Users/Dean/json-out.json")).Where(i => i.Target == SkinnableTarget.MainHUDComponents); - - var container = new SkinnableTargetWrapper(target.Target) - { - ChildrenEnumerable = infos.Select(i => i.CreateInstance()) - }; - - return container; - } - - break; - } - - return null; - } - public override Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => null; public override ISample GetSample(ISampleInfo sampleInfo) => null; @@ -72,14 +42,4 @@ namespace osu.Game.Skinning return null; } } - - public class SkinnableTargetWrapper : Container, ISkinnableTarget - { - public SkinnableTarget Target { get; } - - public SkinnableTargetWrapper(SkinnableTarget target) - { - Target = target; - } - } } diff --git a/osu.Game/Skinning/Editor/SkinBlueprintContainer.cs b/osu.Game/Skinning/Editor/SkinBlueprintContainer.cs index a6f60afd90..45e541abf0 100644 --- a/osu.Game/Skinning/Editor/SkinBlueprintContainer.cs +++ b/osu.Game/Skinning/Editor/SkinBlueprintContainer.cs @@ -1,12 +1,9 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.IO; using System.Linq; -using Newtonsoft.Json; using osu.Framework.Graphics; using osu.Framework.Testing; -using osu.Game.Extensions; using osu.Game.Rulesets.Edit; using osu.Game.Screens.Edit.Compose.Components; @@ -32,11 +29,6 @@ namespace osu.Game.Skinning.Editor { ISkinnableComponent[] skinnableComponents = target.ChildrenOfType().ToArray(); - // todo: the OfType() call can be removed with better IDrawable support. - string json = JsonConvert.SerializeObject(skinnableComponents.OfType().Select(d => d.CreateSerialisedInformation()), new JsonSerializerSettings { Formatting = Formatting.Indented }); - - File.WriteAllText("/Users/Dean/json-out.json", json); - foreach (var c in skinnableComponents) AddBlueprintFor(c); diff --git a/osu.Game/Skinning/Editor/SkinEditor.cs b/osu.Game/Skinning/Editor/SkinEditor.cs index 18a8b220df..30ee3097d2 100644 --- a/osu.Game/Skinning/Editor/SkinEditor.cs +++ b/osu.Game/Skinning/Editor/SkinEditor.cs @@ -11,6 +11,8 @@ using osu.Framework.Testing; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Cursor; +using osu.Game.Graphics.UserInterface; +using osu.Game.Screens.Play.HUD; namespace osu.Game.Skinning.Editor { @@ -24,6 +26,9 @@ namespace osu.Game.Skinning.Editor protected override bool StartHidden => true; + [Resolved] + private SkinManager skins { get; set; } + public SkinEditor(Drawable target) { this.target = target; @@ -53,7 +58,24 @@ namespace osu.Game.Skinning.Editor Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, RequestPlacement = placeComponent - } + }, + new TriangleButton + { + Text = "Save Changes", + Width = 140, + Height = 50, + Padding = new MarginPadding + { + Top = 10, + Left = 10, + }, + Margin = new MarginPadding + { + Right = 10, + Bottom = 10, + }, + Action = save, + }, } }; @@ -71,7 +93,7 @@ namespace osu.Game.Skinning.Editor var targetContainer = target.ChildrenOfType().FirstOrDefault(); - targetContainer?.Add(instance); + (targetContainer as Container)?.Add(instance); } protected override void LoadComplete() @@ -80,6 +102,23 @@ namespace osu.Game.Skinning.Editor Show(); } + private void save() + { + var currentSkin = skins.CurrentSkin.Value; + + var legacySkin = currentSkin as LegacySkin; + + if (legacySkin == null) + return; + + SkinnableElementTargetContainer[] targetContainers = target.ChildrenOfType().ToArray(); + + foreach (var t in targetContainers) + legacySkin.UpdateDrawableTarget(t); + + skins.Save(skins.CurrentSkin.Value); + } + protected override bool OnHover(HoverEvent e) => true; protected override bool OnMouseDown(MouseDownEvent e) => true; diff --git a/osu.Game/Skinning/ISkinnableTarget.cs b/osu.Game/Skinning/ISkinnableTarget.cs index 607e89fdec..90e48c8bba 100644 --- a/osu.Game/Skinning/ISkinnableTarget.cs +++ b/osu.Game/Skinning/ISkinnableTarget.cs @@ -2,14 +2,14 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; namespace osu.Game.Skinning { /// /// Denotes a container which can house s. /// - public interface ISkinnableTarget : IContainerCollection + public interface ISkinnableTarget : IDrawable { + public SkinnableTarget Target { get; } } } diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index 70f8d1d882..fc03ce909c 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -58,7 +58,7 @@ namespace osu.Game.Skinning } protected LegacySkin(SkinInfo skin, [CanBeNull] IResourceStore storage, [CanBeNull] IStorageResourceProvider resources, string filename) - : base(skin) + : base(skin, resources) { using (var stream = storage?.GetStream(filename)) { @@ -321,8 +321,32 @@ namespace osu.Game.Skinning public override Drawable GetDrawableComponent(ISkinComponent component) { + if (base.GetDrawableComponent(component) is Drawable c) + return c; + switch (component) { + case SkinnableTargetComponent target: + + switch (target.Target) + { + case SkinnableTarget.MainHUDComponents: + return new SkinnableTargetWrapper(target.Target) + { + RelativeSizeAxes = Axes.Both, + Children = new[] + { + // TODO: these should fallback to the osu!classic skin. + GetDrawableComponent(new HUDSkinComponent(HUDSkinComponents.ComboCounter)) ?? new DefaultComboCounter(), + GetDrawableComponent(new HUDSkinComponent(HUDSkinComponents.ScoreCounter)) ?? new DefaultScoreCounter(), + GetDrawableComponent(new HUDSkinComponent(HUDSkinComponents.AccuracyCounter)) ?? new DefaultAccuracyCounter(), + GetDrawableComponent(new HUDSkinComponent(HUDSkinComponents.HealthDisplay)) ?? new DefaultHealthDisplay(), + } + }; + } + + return null; + case HUDSkinComponent hudComponent: { if (!this.HasFont(LegacyFont.Score)) diff --git a/osu.Game/Skinning/Skin.cs b/osu.Game/Skinning/Skin.cs index 6d1bce2cb1..03ecabc886 100644 --- a/osu.Game/Skinning/Skin.cs +++ b/osu.Game/Skinning/Skin.cs @@ -2,12 +2,19 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Newtonsoft.Json; using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.OpenGL.Textures; using osu.Framework.Graphics.Textures; using osu.Game.Audio; +using osu.Game.IO; +using osu.Game.Screens.Play.HUD; namespace osu.Game.Skinning { @@ -15,9 +22,13 @@ namespace osu.Game.Skinning { public readonly SkinInfo SkinInfo; + private readonly IStorageResourceProvider resources; + public SkinConfiguration Configuration { get; protected set; } - public abstract Drawable GetDrawableComponent(ISkinComponent componentName); + public IDictionary DrawableComponentInfo => drawableComponentInfo; + + private readonly Dictionary drawableComponentInfo = new Dictionary(); public abstract ISample GetSample(ISampleInfo sampleInfo); @@ -27,9 +38,62 @@ namespace osu.Game.Skinning public abstract IBindable GetConfig(TLookup lookup); - protected Skin(SkinInfo skin) + protected Skin(SkinInfo skin, IStorageResourceProvider resources) { SkinInfo = skin; + + // may be null for default skin. + this.resources = resources; + } + + public void UpdateDrawableTarget(SkinnableElementTargetContainer targetContainer) + { + DrawableComponentInfo[targetContainer.Target] = targetContainer.CreateSerialisedChildren().ToArray(); + } + + public virtual Drawable GetDrawableComponent(ISkinComponent component) + { + switch (component) + { + case SkinnableTargetComponent target: + + var skinnableTarget = target.Target; + + if (!DrawableComponentInfo.TryGetValue(skinnableTarget, out var skinnableInfo)) + { + switch (skinnableTarget) + { + case SkinnableTarget.MainHUDComponents: + + // skininfo files may be null for default skin. + var fileInfo = SkinInfo.Files?.FirstOrDefault(f => f.Filename == $"{skinnableTarget}.json"); + + if (fileInfo == null) + return null; + + var bytes = resources?.Files.Get(fileInfo.FileInfo.StoragePath); + + if (bytes == null) + return null; + + string jsonContent = Encoding.UTF8.GetString(bytes); + + DrawableComponentInfo[skinnableTarget] = skinnableInfo = + JsonConvert.DeserializeObject>(jsonContent).Where(i => i.Target == SkinnableTarget.MainHUDComponents).ToArray(); + break; + } + } + + if (skinnableInfo == null) + return null; + + return new SkinnableTargetWrapper(skinnableTarget) + { + ChildrenEnumerable = skinnableInfo.Select(i => i.CreateInstance()) + }; + } + + return null; } #region Disposal @@ -58,4 +122,17 @@ namespace osu.Game.Skinning #endregion } + + public class SkinnableTargetWrapper : Container, IDefaultSkinnableTarget + { + // this is just here to implement the interface and allow a silly parent lookup to work (where the target is not the direct parent for reasons). + // TODO: need to fix this. + public SkinnableTarget Target { get; } + + public SkinnableTargetWrapper(SkinnableTarget target) + { + Target = target; + RelativeSizeAxes = Axes.Both; + } + } } diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs index ac4d63159a..f9e22c9c89 100644 --- a/osu.Game/Skinning/SkinManager.cs +++ b/osu.Game/Skinning/SkinManager.cs @@ -6,9 +6,11 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Linq.Expressions; +using System.Text; using System.Threading; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; +using Newtonsoft.Json; using osu.Framework.Audio; using osu.Framework.Audio.Sample; using osu.Framework.Bindables; @@ -17,6 +19,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.OpenGL.Textures; using osu.Framework.Graphics.Textures; using osu.Framework.IO.Stores; +using osu.Framework.Logging; using osu.Framework.Platform; using osu.Framework.Testing; using osu.Framework.Utils; @@ -158,6 +161,35 @@ namespace osu.Game.Skinning return new LegacySkin(skinInfo, this); } + public void Save(Skin skin) + { + // some skins don't support saving just yet. + // eventually we will want to create a copy of the skin to allow for customisation. + if (skin.SkinInfo.Files == null) + return; + + foreach (var drawableInfo in skin.DrawableComponentInfo) + { + // todo: the OfType() call can be removed with better IDrawable support. + string json = JsonConvert.SerializeObject(drawableInfo.Value, new JsonSerializerSettings { Formatting = Formatting.Indented }); + + using (var streamContent = new MemoryStream(Encoding.UTF8.GetBytes(json))) + { + string filename = $"{drawableInfo.Key}.json"; + + var oldFile = skin.SkinInfo.Files.FirstOrDefault(f => f.Filename == filename); + + if (oldFile != null) + ReplaceFile(skin.SkinInfo, oldFile, streamContent, oldFile.Filename); + else + AddFile(skin.SkinInfo, streamContent, filename); + + Logger.Log($"Saving out {filename} with {json.Length} bytes"); + Logger.Log(json); + } + } + } + /// /// Perform a lookup query on available s. /// From 1bb3c609aeec1c773aac2a7a28f4c21d52674052 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 11 May 2021 11:00:57 +0900 Subject: [PATCH 088/205] Centralise implementation of cancel button logic --- .../UserInterface/DangerousTriangleButton.cs | 18 ++++++++++++++++++ osu.Game/Overlays/KeyBinding/KeyBindingRow.cs | 11 +---------- .../KeyBinding/KeyBindingsSubsection.cs | 9 ++------- 3 files changed, 21 insertions(+), 17 deletions(-) create mode 100644 osu.Game/Graphics/UserInterface/DangerousTriangleButton.cs diff --git a/osu.Game/Graphics/UserInterface/DangerousTriangleButton.cs b/osu.Game/Graphics/UserInterface/DangerousTriangleButton.cs new file mode 100644 index 0000000000..89a4c28c8c --- /dev/null +++ b/osu.Game/Graphics/UserInterface/DangerousTriangleButton.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. + +using osu.Framework.Allocation; + +namespace osu.Game.Graphics.UserInterface +{ + public class DangerousTriangleButton : TriangleButton + { + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + BackgroundColour = colours.PinkDark; + Triangles.ColourDark = colours.PinkDarker; + Triangles.ColourLight = colours.Pink; + } + } +} diff --git a/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs b/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs index 300fce962a..43942d2d52 100644 --- a/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs +++ b/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs @@ -339,22 +339,13 @@ namespace osu.Game.Overlays.KeyBinding } } - public class ClearButton : TriangleButton + public class ClearButton : DangerousTriangleButton { public ClearButton() { Text = "Clear"; Size = new Vector2(80, 20); } - - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - BackgroundColour = colours.Pink; - - Triangles.ColourDark = colours.PinkDark; - Triangles.ColourLight = colours.PinkLight; - } } public class KeyButton : Container diff --git a/osu.Game/Overlays/KeyBinding/KeyBindingsSubsection.cs b/osu.Game/Overlays/KeyBinding/KeyBindingsSubsection.cs index d784b7aec9..707176e63e 100644 --- a/osu.Game/Overlays/KeyBinding/KeyBindingsSubsection.cs +++ b/osu.Game/Overlays/KeyBinding/KeyBindingsSubsection.cs @@ -11,7 +11,6 @@ using osu.Game.Input; using osu.Game.Overlays.Settings; using osu.Game.Rulesets; using osuTK; -using osu.Game.Graphics; namespace osu.Game.Overlays.KeyBinding { @@ -55,10 +54,10 @@ namespace osu.Game.Overlays.KeyBinding } } - public class ResetButton : TriangleButton + public class ResetButton : DangerousTriangleButton { [BackgroundDependencyLoader] - private void load(OsuColour colours) + private void load() { Text = "Reset all bindings in section"; RelativeSizeAxes = Axes.X; @@ -66,10 +65,6 @@ namespace osu.Game.Overlays.KeyBinding Height = 20; Content.CornerRadius = 5; - - BackgroundColour = colours.PinkDark; - Triangles.ColourDark = colours.PinkDarker; - Triangles.ColourLight = colours.Pink; } } } From c957293ec3a580875f7961bcde4e47bfd20c2a27 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 11 May 2021 11:54:45 +0900 Subject: [PATCH 089/205] Load json from disk store at skin construction for now This allows for easier mutation without worrying about changes being re-read from disk unexpectedly. --- osu.Game/Skinning/Skin.cs | 62 ++++++++++++++++++--------------------- 1 file changed, 29 insertions(+), 33 deletions(-) diff --git a/osu.Game/Skinning/Skin.cs b/osu.Game/Skinning/Skin.cs index 03ecabc886..34571ec688 100644 --- a/osu.Game/Skinning/Skin.cs +++ b/osu.Game/Skinning/Skin.cs @@ -44,6 +44,32 @@ namespace osu.Game.Skinning // may be null for default skin. this.resources = resources; + + // we may want to move this to some kind of async operation in the future. + foreach (SkinnableTarget skinnableTarget in Enum.GetValues(typeof(SkinnableTarget))) + { + string filename = $"{skinnableTarget}.json"; + + // skininfo files may be null for default skin. + var fileInfo = SkinInfo.Files?.FirstOrDefault(f => f.Filename == filename); + + if (fileInfo == null) + continue; + + var bytes = resources?.Files.Get(fileInfo.FileInfo.StoragePath); + + if (bytes == null) + continue; + + string jsonContent = Encoding.UTF8.GetString(bytes); + + DrawableComponentInfo[skinnableTarget] = JsonConvert.DeserializeObject>(jsonContent).ToArray(); + } + } + + public void ResetDrawableTarget(SkinnableElementTargetContainer targetContainer) + { + DrawableComponentInfo.Remove(targetContainer.Target); } public void UpdateDrawableTarget(SkinnableElementTargetContainer targetContainer) @@ -60,34 +86,9 @@ namespace osu.Game.Skinning var skinnableTarget = target.Target; if (!DrawableComponentInfo.TryGetValue(skinnableTarget, out var skinnableInfo)) - { - switch (skinnableTarget) - { - case SkinnableTarget.MainHUDComponents: - - // skininfo files may be null for default skin. - var fileInfo = SkinInfo.Files?.FirstOrDefault(f => f.Filename == $"{skinnableTarget}.json"); - - if (fileInfo == null) - return null; - - var bytes = resources?.Files.Get(fileInfo.FileInfo.StoragePath); - - if (bytes == null) - return null; - - string jsonContent = Encoding.UTF8.GetString(bytes); - - DrawableComponentInfo[skinnableTarget] = skinnableInfo = - JsonConvert.DeserializeObject>(jsonContent).Where(i => i.Target == SkinnableTarget.MainHUDComponents).ToArray(); - break; - } - } - - if (skinnableInfo == null) return null; - return new SkinnableTargetWrapper(skinnableTarget) + return new SkinnableTargetWrapper { ChildrenEnumerable = skinnableInfo.Select(i => i.CreateInstance()) }; @@ -123,15 +124,10 @@ namespace osu.Game.Skinning #endregion } - public class SkinnableTargetWrapper : Container, IDefaultSkinnableTarget + public class SkinnableTargetWrapper : Container, ISkinnableComponent { - // this is just here to implement the interface and allow a silly parent lookup to work (where the target is not the direct parent for reasons). - // TODO: need to fix this. - public SkinnableTarget Target { get; } - - public SkinnableTargetWrapper(SkinnableTarget target) + public SkinnableTargetWrapper() { - Target = target; RelativeSizeAxes = Axes.Both; } } From 4769a95b4994b032f64e446272ffed0051230f43 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 11 May 2021 11:56:14 +0900 Subject: [PATCH 090/205] Fix encapsulation and remove target lookup overhead --- .../HUD/SkinnableElementTargetContainer.cs | 23 ++++++++++++++----- osu.Game/Screens/Play/HUD/SkinnableInfo.cs | 9 -------- osu.Game/Skinning/IDefaultSkinnableTarget.cs | 12 ---------- osu.Game/Skinning/ISkinnableTarget.cs | 12 +++++++++- osu.Game/Skinning/LegacySkin.cs | 3 +-- 5 files changed, 29 insertions(+), 30 deletions(-) delete mode 100644 osu.Game/Skinning/IDefaultSkinnableTarget.cs diff --git a/osu.Game/Screens/Play/HUD/SkinnableElementTargetContainer.cs b/osu.Game/Screens/Play/HUD/SkinnableElementTargetContainer.cs index f1b282119b..79a0db1751 100644 --- a/osu.Game/Screens/Play/HUD/SkinnableElementTargetContainer.cs +++ b/osu.Game/Screens/Play/HUD/SkinnableElementTargetContainer.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; +using osu.Framework.Graphics; using osu.Game.Extensions; using osu.Game.Skinning; @@ -19,6 +20,21 @@ namespace osu.Game.Screens.Play.HUD Target = target; } + public void Reload() + { + content = CurrentSkin.GetDrawableComponent(new SkinnableTargetComponent(Target)) as SkinnableTargetWrapper; + + ClearInternal(); + + if (content != null) + LoadComponentAsync(content, AddInternal); + } + + public void Add(Drawable drawable) + { + content.Add(drawable); + } + public IEnumerable CreateSerialisedChildren() => content.Select(d => d.CreateSerialisedInformation()); @@ -26,12 +42,7 @@ namespace osu.Game.Screens.Play.HUD { base.SkinChanged(skin, allowFallback); - content = skin.GetDrawableComponent(new SkinnableTargetComponent(Target)) as SkinnableTargetWrapper; - - ClearInternal(); - - if (content != null) - LoadComponentAsync(content, AddInternal); + Reload(); } } } diff --git a/osu.Game/Screens/Play/HUD/SkinnableInfo.cs b/osu.Game/Screens/Play/HUD/SkinnableInfo.cs index ec4eaa0e59..abeb96f647 100644 --- a/osu.Game/Screens/Play/HUD/SkinnableInfo.cs +++ b/osu.Game/Screens/Play/HUD/SkinnableInfo.cs @@ -4,8 +4,6 @@ using System; using System.Collections.Generic; using System.Linq; -using Newtonsoft.Json; -using Newtonsoft.Json.Converters; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Extensions; @@ -23,9 +21,6 @@ namespace osu.Game.Screens.Play.HUD { public Type Type { get; set; } - [JsonConverter(typeof(StringEnumConverter))] - public SkinnableTarget? Target { get; set; } - public Vector2 Position { get; set; } public float Rotation { get; set; } @@ -44,10 +39,6 @@ namespace osu.Game.Screens.Play.HUD { Type = component.GetType(); - ISkinnableTarget target = component.Parent as ISkinnableTarget; - - Target = target?.Target; - Position = component.Position; Rotation = component.Rotation; Scale = component.Scale; diff --git a/osu.Game/Skinning/IDefaultSkinnableTarget.cs b/osu.Game/Skinning/IDefaultSkinnableTarget.cs deleted file mode 100644 index 24fb454af8..0000000000 --- a/osu.Game/Skinning/IDefaultSkinnableTarget.cs +++ /dev/null @@ -1,12 +0,0 @@ -// 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.Skinning -{ - /// - /// The default placement location for new s. - /// - public interface IDefaultSkinnableTarget : ISkinnableTarget - { - } -} diff --git a/osu.Game/Skinning/ISkinnableTarget.cs b/osu.Game/Skinning/ISkinnableTarget.cs index 90e48c8bba..2379fd4247 100644 --- a/osu.Game/Skinning/ISkinnableTarget.cs +++ b/osu.Game/Skinning/ISkinnableTarget.cs @@ -8,8 +8,18 @@ namespace osu.Game.Skinning /// /// Denotes a container which can house s. /// - public interface ISkinnableTarget : IDrawable + public interface ISkinnableTarget { public SkinnableTarget Target { get; } + + /// + /// Reload this target from the current skin. + /// + public void Reload(); + + /// + /// Add the provided item to this target. + /// + public void Add(Drawable drawable); } } diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index fc03ce909c..3ffd2245c2 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -331,9 +331,8 @@ namespace osu.Game.Skinning switch (target.Target) { case SkinnableTarget.MainHUDComponents: - return new SkinnableTargetWrapper(target.Target) + return new SkinnableTargetWrapper { - RelativeSizeAxes = Axes.Both, Children = new[] { // TODO: these should fallback to the osu!classic skin. From 81902ad6a688f2e143d06234006027889d8808be Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 11 May 2021 11:57:12 +0900 Subject: [PATCH 091/205] Add the ability to revert all skin changes --- osu.Game/Skinning/Editor/SkinEditor.cs | 66 +++++++++++++++++++++----- 1 file changed, 53 insertions(+), 13 deletions(-) diff --git a/osu.Game/Skinning/Editor/SkinEditor.cs b/osu.Game/Skinning/Editor/SkinEditor.cs index 30ee3097d2..6b7d289284 100644 --- a/osu.Game/Skinning/Editor/SkinEditor.cs +++ b/osu.Game/Skinning/Editor/SkinEditor.cs @@ -13,6 +13,7 @@ using osu.Game.Graphics.Containers; using osu.Game.Graphics.Cursor; using osu.Game.Graphics.UserInterface; using osu.Game.Screens.Play.HUD; +using osuTK; namespace osu.Game.Skinning.Editor { @@ -20,7 +21,7 @@ namespace osu.Game.Skinning.Editor { public const double TRANSITION_DURATION = 500; - private readonly Drawable target; + private readonly Drawable targetScreen; private OsuTextFlowContainer headerText; @@ -29,9 +30,9 @@ namespace osu.Game.Skinning.Editor [Resolved] private SkinManager skins { get; set; } - public SkinEditor(Drawable target) + public SkinEditor(Drawable targetScreen) { - this.target = target; + this.targetScreen = targetScreen; RelativeSizeAxes = Axes.Both; } @@ -52,18 +53,20 @@ namespace osu.Game.Skinning.Editor Origin = Anchor.TopCentre, RelativeSizeAxes = Axes.X }, - new SkinBlueprintContainer(target), + new SkinBlueprintContainer(targetScreen), new SkinComponentToolbox(600) { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, RequestPlacement = placeComponent }, - new TriangleButton + new FillFlowContainer { - Text = "Save Changes", - Width = 140, - Height = 50, + Direction = FillDirection.Horizontal, + AutoSizeAxes = Axes.Both, + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + Spacing = new Vector2(5), Padding = new MarginPadding { Top = 10, @@ -74,7 +77,21 @@ namespace osu.Game.Skinning.Editor Right = 10, Bottom = 10, }, - Action = save, + Children = new Drawable[] + { + new TriangleButton + { + Text = "Save Changes", + Width = 140, + Action = save, + }, + new DangerousTriangleButton + { + Text = "Revert to default", + Width = 140, + Action = revert, + }, + } }, } }; @@ -89,11 +106,14 @@ namespace osu.Game.Skinning.Editor private void placeComponent(Type type) { - var instance = (Drawable)Activator.CreateInstance(type); + Drawable instance = (Drawable)Activator.CreateInstance(type); - var targetContainer = target.ChildrenOfType().FirstOrDefault(); + getTarget(SkinnableTarget.MainHUDComponents)?.Add(instance); + } - (targetContainer as Container)?.Add(instance); + private ISkinnableTarget getTarget(SkinnableTarget target) + { + return targetScreen.ChildrenOfType().FirstOrDefault(c => c.Target == target); } protected override void LoadComplete() @@ -102,6 +122,26 @@ namespace osu.Game.Skinning.Editor Show(); } + private void revert() + { + var currentSkin = skins.CurrentSkin.Value; + + var legacySkin = currentSkin as LegacySkin; + + if (legacySkin == null) + return; + + SkinnableElementTargetContainer[] targetContainers = targetScreen.ChildrenOfType().ToArray(); + + foreach (var t in targetContainers) + { + legacySkin.ResetDrawableTarget(t); + + // add back default components + getTarget(t.Target).Reload(); + } + } + private void save() { var currentSkin = skins.CurrentSkin.Value; @@ -111,7 +151,7 @@ namespace osu.Game.Skinning.Editor if (legacySkin == null) return; - SkinnableElementTargetContainer[] targetContainers = target.ChildrenOfType().ToArray(); + SkinnableElementTargetContainer[] targetContainers = targetScreen.ChildrenOfType().ToArray(); foreach (var t in targetContainers) legacySkin.UpdateDrawableTarget(t); From bf65547eec7419273bd106f30d4842dd19ea86fb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 11 May 2021 12:04:25 +0900 Subject: [PATCH 092/205] Allow some serialised components to not be mutable by the user --- osu.Game/Screens/Play/HUD/SkinnableInfo.cs | 4 ++-- osu.Game/Skinning/IImmutableSkinnableComponent.cs | 15 +++++++++++++++ osu.Game/Skinning/ISkinnableComponent.cs | 4 +--- osu.Game/Skinning/Skin.cs | 5 +---- 4 files changed, 19 insertions(+), 9 deletions(-) create mode 100644 osu.Game/Skinning/IImmutableSkinnableComponent.cs diff --git a/osu.Game/Screens/Play/HUD/SkinnableInfo.cs b/osu.Game/Screens/Play/HUD/SkinnableInfo.cs index abeb96f647..988d73fed6 100644 --- a/osu.Game/Screens/Play/HUD/SkinnableInfo.cs +++ b/osu.Game/Screens/Play/HUD/SkinnableInfo.cs @@ -14,7 +14,7 @@ using osuTK; namespace osu.Game.Screens.Play.HUD { /// - /// Serialised information governing custom changes to an . + /// Serialised information governing custom changes to an . /// [Serializable] public class SkinnableInfo : IJsonSerializable @@ -46,7 +46,7 @@ namespace osu.Game.Screens.Play.HUD if (component is Container container) { - foreach (var child in container.Children.OfType().OfType()) + foreach (var child in container.Children.OfType().OfType()) Children.Add(child.CreateSerialisedInformation()); } } diff --git a/osu.Game/Skinning/IImmutableSkinnableComponent.cs b/osu.Game/Skinning/IImmutableSkinnableComponent.cs new file mode 100644 index 0000000000..d1777512af --- /dev/null +++ b/osu.Game/Skinning/IImmutableSkinnableComponent.cs @@ -0,0 +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.Graphics; + +namespace osu.Game.Skinning +{ + /// + /// Denotes a drawable component which should be serialised as part of a skin. + /// Use for components which should be mutable by the user / editor. + /// + public interface ISkinSerialisable : IDrawable + { + } +} diff --git a/osu.Game/Skinning/ISkinnableComponent.cs b/osu.Game/Skinning/ISkinnableComponent.cs index f6b0a182b4..e44c7be13d 100644 --- a/osu.Game/Skinning/ISkinnableComponent.cs +++ b/osu.Game/Skinning/ISkinnableComponent.cs @@ -1,14 +1,12 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using osu.Framework.Graphics; - namespace osu.Game.Skinning { /// /// Denotes a drawable which, as a drawable, can be adjusted via skinning specifications. /// - public interface ISkinnableComponent : IDrawable + public interface ISkinnableComponent : ISkinSerialisable { } } diff --git a/osu.Game/Skinning/Skin.cs b/osu.Game/Skinning/Skin.cs index 34571ec688..cbbad4a607 100644 --- a/osu.Game/Skinning/Skin.cs +++ b/osu.Game/Skinning/Skin.cs @@ -22,8 +22,6 @@ namespace osu.Game.Skinning { public readonly SkinInfo SkinInfo; - private readonly IStorageResourceProvider resources; - public SkinConfiguration Configuration { get; protected set; } public IDictionary DrawableComponentInfo => drawableComponentInfo; @@ -43,7 +41,6 @@ namespace osu.Game.Skinning SkinInfo = skin; // may be null for default skin. - this.resources = resources; // we may want to move this to some kind of async operation in the future. foreach (SkinnableTarget skinnableTarget in Enum.GetValues(typeof(SkinnableTarget))) @@ -124,7 +121,7 @@ namespace osu.Game.Skinning #endregion } - public class SkinnableTargetWrapper : Container, ISkinnableComponent + public class SkinnableTargetWrapper : Container, ISkinSerialisable { public SkinnableTargetWrapper() { From 2396ba42a6ec6c6e9f3e4b8d89513fec123706e2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 11 May 2021 12:21:31 +0900 Subject: [PATCH 093/205] Change `HealthDisplay` to be a `CompositeDrawable` --- osu.Game.Tests/Visual/Gameplay/TestSceneFailingLayer.cs | 6 ++++-- osu.Game/Screens/Play/HUD/DefaultHealthDisplay.cs | 2 +- osu.Game/Screens/Play/HUD/FailingLayer.cs | 2 +- osu.Game/Screens/Play/HUD/HealthDisplay.cs | 2 +- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneFailingLayer.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneFailingLayer.cs index fb4c9d713a..c335f7c99e 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneFailingLayer.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneFailingLayer.cs @@ -1,11 +1,13 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Testing; using osu.Game.Configuration; using osu.Game.Rulesets.Scoring; using osu.Game.Screens.Play.HUD; @@ -50,9 +52,9 @@ namespace osu.Game.Tests.Visual.Gameplay }); AddStep("set health to 0.10", () => layer.Current.Value = 0.1); - AddUntilStep("layer fade is visible", () => layer.Child.Alpha > 0.1f); + AddUntilStep("layer fade is visible", () => layer.ChildrenOfType().First().Alpha > 0.1f); AddStep("set health to 1", () => layer.Current.Value = 1f); - AddUntilStep("layer fade is invisible", () => !layer.Child.IsPresent); + AddUntilStep("layer fade is invisible", () => !layer.ChildrenOfType().First().IsPresent); } [Test] diff --git a/osu.Game/Screens/Play/HUD/DefaultHealthDisplay.cs b/osu.Game/Screens/Play/HUD/DefaultHealthDisplay.cs index 37bd289aee..241777244b 100644 --- a/osu.Game/Screens/Play/HUD/DefaultHealthDisplay.cs +++ b/osu.Game/Screens/Play/HUD/DefaultHealthDisplay.cs @@ -78,7 +78,7 @@ namespace osu.Game.Screens.Play.HUD RelativeSizeAxes = Axes.X; Margin = new MarginPadding { Top = 20 }; - Children = new Drawable[] + InternalChildren = new Drawable[] { new Box { diff --git a/osu.Game/Screens/Play/HUD/FailingLayer.cs b/osu.Game/Screens/Play/HUD/FailingLayer.cs index e071337f34..424ee55766 100644 --- a/osu.Game/Screens/Play/HUD/FailingLayer.cs +++ b/osu.Game/Screens/Play/HUD/FailingLayer.cs @@ -43,7 +43,7 @@ namespace osu.Game.Screens.Play.HUD public FailingLayer() { RelativeSizeAxes = Axes.Both; - Children = new Drawable[] + InternalChildren = new Drawable[] { boxes = new Container { diff --git a/osu.Game/Screens/Play/HUD/HealthDisplay.cs b/osu.Game/Screens/Play/HUD/HealthDisplay.cs index e18a28eded..5a2c764a1a 100644 --- a/osu.Game/Screens/Play/HUD/HealthDisplay.cs +++ b/osu.Game/Screens/Play/HUD/HealthDisplay.cs @@ -15,7 +15,7 @@ namespace osu.Game.Screens.Play.HUD /// A container for components displaying the current player health. /// Gets bound automatically to the when inserted to hierarchy. /// - public abstract class HealthDisplay : Container + public abstract class HealthDisplay : CompositeDrawable { private Bindable showHealthbar; From 4c4d75e6f9c341548b310a767cc15d6839559905 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 11 May 2021 12:42:32 +0900 Subject: [PATCH 094/205] Remove `AccuracyCounter` sizing dependency in `HUDOverlay` --- .../HUD/SkinnableElementTargetContainer.cs | 2 ++ osu.Game/Screens/Play/HUDOverlay.cs | 34 ++++++++++++++----- 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/SkinnableElementTargetContainer.cs b/osu.Game/Screens/Play/HUD/SkinnableElementTargetContainer.cs index 79a0db1751..051ab9ad3c 100644 --- a/osu.Game/Screens/Play/HUD/SkinnableElementTargetContainer.cs +++ b/osu.Game/Screens/Play/HUD/SkinnableElementTargetContainer.cs @@ -20,6 +20,8 @@ namespace osu.Game.Screens.Play.HUD Target = target; } + public IReadOnlyList Children => content?.Children; + public void Reload() { content = CurrentSkin.GetDrawableComponent(new SkinnableTargetComponent(Target)) as SkinnableTargetWrapper; diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index 30b735d28a..033a280da2 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -35,7 +36,6 @@ namespace osu.Game.Screens.Play public readonly KeyCounterDisplay KeyCounter; public readonly SkinnableScoreCounter ScoreCounter; - public readonly SkinnableAccuracyCounter AccuracyCounter; public readonly SongProgress Progress; public readonly ModDisplay ModDisplay; public readonly HoldForMenuButton HoldToQuit; @@ -68,6 +68,8 @@ namespace osu.Game.Screens.Play private bool holdingForHUD; + private readonly SkinnableElementTargetContainer mainComponents; + private IEnumerable hideTargets => new Drawable[] { visibilityContainer, KeyCounter, topRightElements }; public HUDOverlay(DrawableRuleset drawableRuleset, IReadOnlyList mods) @@ -95,7 +97,7 @@ namespace osu.Game.Screens.Play RelativeSizeAxes = Axes.Both, Children = new Drawable[] { - new SkinnableElementTargetContainer(SkinnableTarget.MainHUDComponents) + mainComponents = new SkinnableElementTargetContainer(SkinnableTarget.MainHUDComponents) { RelativeSizeAxes = Axes.Both, }, @@ -107,7 +109,6 @@ namespace osu.Game.Screens.Play { // remaining cross-dependencies need tidying. // kept to ensure non-null, but hidden for testing. - AccuracyCounter = new SkinnableAccuracyCounter(), ScoreCounter = new SkinnableScoreCounter(), } }, @@ -216,12 +217,29 @@ namespace osu.Game.Screens.Play { base.Update(); - // HACK: for now align with the accuracy counter. - // this is done for the sake of hacky legacy skins which extend the health bar to take up the full screen area. - // it only works with the default skin due to padding offsetting it *just enough* to coexist. - topRightElements.Y = TopScoringElementsHeight = ToLocalSpace(AccuracyCounter.Drawable.ScreenSpaceDrawQuad.BottomRight).Y; + Vector2 lowestScreenSpace = Vector2.Zero; - bottomRightElements.Y = -Progress.Height; + // TODO: may be null during skin switching. not sure if there's a better way of exposing these children. + if (mainComponents.Children != null) + { + foreach (var element in mainComponents.Children) + { + // for now align top-right components with the bottom-edge of the lowest top-anchored hud element. + if (!element.Anchor.HasFlagFast(Anchor.TopRight)) + continue; + + // health bars are excluded for the sake of hacky legacy skins which extend the health bar to take up the full screen area. + if (element is HealthDisplay) + continue; + + var bottomRight = element.ScreenSpaceDrawQuad.BottomRight; + if (bottomRight.Y > lowestScreenSpace.Y) + lowestScreenSpace = bottomRight; + } + + topRightElements.Y = TopScoringElementsHeight = ToLocalSpace(lowestScreenSpace).Y; + bottomRightElements.Y = -Progress.Height; + } } private void updateVisibility() From 117d6d731d1a0f5de4648888cfd91f180b1cac2e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 11 May 2021 13:12:24 +0900 Subject: [PATCH 095/205] Move cross-component layout dependencies for legacy skin to `LegacySkin` --- osu.Game/Skinning/LegacyAccuracyCounter.cs | 11 ------ osu.Game/Skinning/LegacySkin.cs | 15 +++++++- osu.Game/Skinning/Skin.cs | 9 ----- osu.Game/Skinning/SkinnableTargetWrapper.cs | 41 +++++++++++++++++++++ 4 files changed, 54 insertions(+), 22 deletions(-) create mode 100644 osu.Game/Skinning/SkinnableTargetWrapper.cs diff --git a/osu.Game/Skinning/LegacyAccuracyCounter.cs b/osu.Game/Skinning/LegacyAccuracyCounter.cs index e2fe01efe4..3eea5d22d5 100644 --- a/osu.Game/Skinning/LegacyAccuracyCounter.cs +++ b/osu.Game/Skinning/LegacyAccuracyCounter.cs @@ -32,16 +32,5 @@ namespace osu.Game.Skinning Anchor = Anchor.TopRight, Origin = Anchor.TopRight, }; - - protected override void Update() - { - base.Update(); - - if (hud?.ScoreCounter?.Drawable is LegacyScoreCounter score) - { - // for now align with the score counter. eventually this will be user customisable. - Y = Parent.ToLocalSpace(score.ScreenSpaceDrawQuad.BottomRight).Y; - } - } } } diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index 3ffd2245c2..85cfab8297 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -327,11 +327,20 @@ namespace osu.Game.Skinning switch (component) { case SkinnableTargetComponent target: - switch (target.Target) { case SkinnableTarget.MainHUDComponents: - return new SkinnableTargetWrapper + + var skinnableTargetWrapper = new SkinnableTargetWrapper(container => + { + var score = container.OfType().FirstOrDefault(); + var accuracy = container.OfType().FirstOrDefault(); + + if (score != null && accuracy != null) + { + accuracy.Y = container.ToLocalSpace(score.ScreenSpaceDrawQuad.BottomRight).Y; + } + }) { Children = new[] { @@ -342,6 +351,8 @@ namespace osu.Game.Skinning GetDrawableComponent(new HUDSkinComponent(HUDSkinComponents.HealthDisplay)) ?? new DefaultHealthDisplay(), } }; + + return skinnableTargetWrapper; } return null; diff --git a/osu.Game/Skinning/Skin.cs b/osu.Game/Skinning/Skin.cs index cbbad4a607..10481e4efd 100644 --- a/osu.Game/Skinning/Skin.cs +++ b/osu.Game/Skinning/Skin.cs @@ -9,7 +9,6 @@ using Newtonsoft.Json; using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.OpenGL.Textures; using osu.Framework.Graphics.Textures; using osu.Game.Audio; @@ -120,12 +119,4 @@ namespace osu.Game.Skinning #endregion } - - public class SkinnableTargetWrapper : Container, ISkinSerialisable - { - public SkinnableTargetWrapper() - { - RelativeSizeAxes = Axes.Both; - } - } } diff --git a/osu.Game/Skinning/SkinnableTargetWrapper.cs b/osu.Game/Skinning/SkinnableTargetWrapper.cs new file mode 100644 index 0000000000..a0df610488 --- /dev/null +++ b/osu.Game/Skinning/SkinnableTargetWrapper.cs @@ -0,0 +1,41 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; + +namespace osu.Game.Skinning +{ + /// + /// A container which is serialised and can encapsulate multiple skinnable elements into a single return type. + /// Will also optionally apply default cross-element layout dependencies when initialised from a non-deserialised source. + /// + public class SkinnableTargetWrapper : Container, ISkinSerialisable + { + private readonly Action applyDefaults; + + /// + /// Construct a wrapper with defaults that should be applied once. + /// + /// A function with default to apply after the initial layout (ie. consuming autosize) + public SkinnableTargetWrapper(Action applyDefaults) + : this() + { + this.applyDefaults = applyDefaults; + } + + public SkinnableTargetWrapper() + { + RelativeSizeAxes = Axes.Both; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + // schedule is required to allow children to run their LoadComplete and take on their correct sizes. + Schedule(() => applyDefaults?.Invoke(this)); + } + } +} From f53ce951dc1ee6594d5a6cc4e975fa9e9a15dee6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 11 May 2021 13:13:40 +0900 Subject: [PATCH 096/205] Remove `DefaultScoreCounter` animation for the time being May add this back in the future, but for now it's causing issues as it operates on `this`. The default skin may be changing quite a bit in the near future, so we can decide what to do about animation at that point in time. --- osu.Game/Screens/Play/HUD/DefaultScoreCounter.cs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/DefaultScoreCounter.cs b/osu.Game/Screens/Play/HUD/DefaultScoreCounter.cs index 8e37797446..bd18050dfb 100644 --- a/osu.Game/Screens/Play/HUD/DefaultScoreCounter.cs +++ b/osu.Game/Screens/Play/HUD/DefaultScoreCounter.cs @@ -24,12 +24,6 @@ namespace osu.Game.Screens.Play.HUD private void load(OsuColour colours) { Colour = colours.BlueLighter; - - // todo: check if default once health display is skinnable - hud?.ShowHealthbar.BindValueChanged(healthBar => - { - this.MoveToY(healthBar.NewValue ? 30 : 0, HUDOverlay.FADE_DURATION, HUDOverlay.FADE_EASING); - }, true); } } } From c94df672e5f30c4d87738ca37a019685773c4df2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 11 May 2021 13:39:32 +0900 Subject: [PATCH 097/205] Also serialise `Origin` out --- osu.Game/Extensions/DrawableExtensions.cs | 1 + osu.Game/Screens/Play/HUD/SkinnableInfo.cs | 3 +++ 2 files changed, 4 insertions(+) diff --git a/osu.Game/Extensions/DrawableExtensions.cs b/osu.Game/Extensions/DrawableExtensions.cs index 6837a802f2..0ec96a876e 100644 --- a/osu.Game/Extensions/DrawableExtensions.cs +++ b/osu.Game/Extensions/DrawableExtensions.cs @@ -55,6 +55,7 @@ namespace osu.Game.Extensions component.Rotation = info.Rotation; component.Scale = info.Scale; component.Anchor = info.Anchor; + component.Origin = info.Origin; if (component is Container container) { diff --git a/osu.Game/Screens/Play/HUD/SkinnableInfo.cs b/osu.Game/Screens/Play/HUD/SkinnableInfo.cs index 988d73fed6..2de3f1b729 100644 --- a/osu.Game/Screens/Play/HUD/SkinnableInfo.cs +++ b/osu.Game/Screens/Play/HUD/SkinnableInfo.cs @@ -29,6 +29,8 @@ namespace osu.Game.Screens.Play.HUD public Anchor Anchor { get; set; } + public Anchor Origin { get; set; } + public List Children { get; } = new List(); public SkinnableInfo() @@ -43,6 +45,7 @@ namespace osu.Game.Screens.Play.HUD Rotation = component.Rotation; Scale = component.Scale; Anchor = component.Anchor; + Origin = component.Origin; if (component is Container container) { From 12684de66eb76365a3f10d4bee629eb9f0ada1d5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 11 May 2021 14:09:56 +0900 Subject: [PATCH 098/205] Add ability to adjust origin in skin editor --- .../Skinning/Editor/SkinSelectionHandler.cs | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/osu.Game/Skinning/Editor/SkinSelectionHandler.cs b/osu.Game/Skinning/Editor/SkinSelectionHandler.cs index ad783a9c0e..cf5ece03e9 100644 --- a/osu.Game/Skinning/Editor/SkinSelectionHandler.cs +++ b/osu.Game/Skinning/Editor/SkinSelectionHandler.cs @@ -70,13 +70,18 @@ namespace osu.Game.Skinning.Editor { yield return new OsuMenuItem("Anchor") { - Items = createAnchorItems().ToArray() + Items = createAnchorItems(d => d.Anchor, applyAnchor).ToArray() + }; + + yield return new OsuMenuItem("Origin") + { + Items = createAnchorItems(d => d.Origin, applyOrigin).ToArray() }; foreach (var item in base.GetContextMenuItemsForSelection(selection)) yield return item; - IEnumerable createAnchorItems() + IEnumerable createAnchorItems(Func checkFunction, Action applyFunction) { var displayableAnchors = new[] { @@ -93,14 +98,20 @@ namespace osu.Game.Skinning.Editor return displayableAnchors.Select(a => { - return new AnchorMenuItem(a, selection, _ => applyAnchor(a)) + return new AnchorMenuItem(a, selection, _ => applyFunction(a)) { - State = { Value = GetStateFromSelection(selection, c => ((Drawable)c.Item).Anchor == a) } + State = { Value = GetStateFromSelection(selection, c => checkFunction((Drawable)c.Item) == a) } }; }); } } + private void applyOrigin(Anchor anchor) + { + foreach (var item in SelectedItems) + ((Drawable)item).Origin = anchor; + } + private void applyAnchor(Anchor anchor) { foreach (var item in SelectedItems) From 944f09ec98e54f296ee6fe04a0b183ab31bd4550 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 11 May 2021 14:12:28 +0900 Subject: [PATCH 099/205] Move default skin cross-component dependencies out to default specifications --- .../Play/HUD/DefaultAccuracyCounter.cs | 22 ------- .../Screens/Play/HUD/DefaultComboCounter.cs | 14 ---- osu.Game/Screens/Play/HUDOverlay.cs | 12 ---- osu.Game/Skinning/DefaultSkin.cs | 64 +++++++++++++++++++ 4 files changed, 64 insertions(+), 48 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/DefaultAccuracyCounter.cs b/osu.Game/Screens/Play/HUD/DefaultAccuracyCounter.cs index e4c865803d..d8dff89b29 100644 --- a/osu.Game/Screens/Play/HUD/DefaultAccuracyCounter.cs +++ b/osu.Game/Screens/Play/HUD/DefaultAccuracyCounter.cs @@ -2,23 +2,13 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; -using osu.Framework.Graphics; using osu.Game.Graphics; using osu.Game.Skinning; -using osuTK; namespace osu.Game.Screens.Play.HUD { public class DefaultAccuracyCounter : GameplayAccuracyCounter, ISkinnableComponent { - private readonly Vector2 offset = new Vector2(-20, 5); - - public DefaultAccuracyCounter() - { - Origin = Anchor.TopRight; - Anchor = Anchor.TopRight; - } - [Resolved(canBeNull: true)] private HUDOverlay hud { get; set; } @@ -27,17 +17,5 @@ namespace osu.Game.Screens.Play.HUD { Colour = colours.BlueLighter; } - - protected override void Update() - { - base.Update(); - - if (hud?.ScoreCounter.Drawable is DefaultScoreCounter score) - { - // for now align with the score counter. eventually this will be user customisable. - Anchor = Anchor.TopLeft; - Position = Parent.ToLocalSpace(score.ScreenSpaceDrawQuad.TopLeft) + offset; - } - } } } diff --git a/osu.Game/Screens/Play/HUD/DefaultComboCounter.cs b/osu.Game/Screens/Play/HUD/DefaultComboCounter.cs index 375ff293aa..13bd045fc0 100644 --- a/osu.Game/Screens/Play/HUD/DefaultComboCounter.cs +++ b/osu.Game/Screens/Play/HUD/DefaultComboCounter.cs @@ -9,14 +9,11 @@ using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets.Scoring; using osu.Game.Skinning; -using osuTK; namespace osu.Game.Screens.Play.HUD { public class DefaultComboCounter : RollingCounter, ISkinnableComponent { - private readonly Vector2 offset = new Vector2(20, 5); - [Resolved(canBeNull: true)] private HUDOverlay hud { get; set; } @@ -32,17 +29,6 @@ namespace osu.Game.Screens.Play.HUD Current.BindTo(scoreProcessor.Combo); } - protected override void Update() - { - base.Update(); - - if (hud?.ScoreCounter.Drawable is DefaultScoreCounter score) - { - // for now align with the score counter. eventually this will be user customisable. - Position = Parent.ToLocalSpace(score.ScreenSpaceDrawQuad.TopRight) + offset; - } - } - protected override string FormatCount(int count) { return $@"{count}x"; diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index 033a280da2..88176e7ea2 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -35,7 +35,6 @@ namespace osu.Game.Screens.Play public float TopScoringElementsHeight { get; private set; } public readonly KeyCounterDisplay KeyCounter; - public readonly SkinnableScoreCounter ScoreCounter; public readonly SongProgress Progress; public readonly ModDisplay ModDisplay; public readonly HoldForMenuButton HoldToQuit; @@ -102,17 +101,6 @@ namespace osu.Game.Screens.Play RelativeSizeAxes = Axes.Both, }, new Container - { - RelativeSizeAxes = Axes.Both, - Alpha = 0, - Children = new Drawable[] - { - // remaining cross-dependencies need tidying. - // kept to ensure non-null, but hidden for testing. - ScoreCounter = new SkinnableScoreCounter(), - } - }, - new Container { RelativeSizeAxes = Axes.Both, Children = new Drawable[] diff --git a/osu.Game/Skinning/DefaultSkin.cs b/osu.Game/Skinning/DefaultSkin.cs index bfb5b8ef56..ef3dffe59c 100644 --- a/osu.Game/Skinning/DefaultSkin.cs +++ b/osu.Game/Skinning/DefaultSkin.cs @@ -2,11 +2,16 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; +using System.Linq; using osu.Framework.Audio.Sample; using osu.Framework.Bindables; +using osu.Framework.Graphics; using osu.Framework.Graphics.OpenGL.Textures; using osu.Framework.Graphics.Textures; using osu.Game.Audio; +using osu.Game.Extensions; +using osu.Game.Screens.Play.HUD; +using osuTK; using osuTK.Graphics; namespace osu.Game.Skinning @@ -23,6 +28,65 @@ namespace osu.Game.Skinning public override ISample GetSample(ISampleInfo sampleInfo) => null; + public override Drawable GetDrawableComponent(ISkinComponent component) + { + switch (component) + { + case SkinnableTargetComponent target: + switch (target.Target) + { + case SkinnableTarget.MainHUDComponents: + var skinnableTargetWrapper = new SkinnableTargetWrapper(container => + { + var score = container.OfType().FirstOrDefault(); + var accuracy = container.OfType().FirstOrDefault(); + var combo = container.OfType().FirstOrDefault(); + + if (score != null) + { + score.Anchor = Anchor.TopCentre; + score.Origin = Anchor.TopCentre; + + // elements default to beneath the health bar + const float vertical_offset = 30; + + const float horizontal_padding = 20; + + score.Position = new Vector2(0, vertical_offset); + + if (accuracy != null) + { + accuracy.Position = new Vector2(-accuracy.ScreenSpaceDeltaToParentSpace(score.ScreenSpaceDrawQuad.Size).X / 2 - horizontal_padding, vertical_offset + 5); + accuracy.Origin = Anchor.TopRight; + accuracy.Anchor = Anchor.TopCentre; + } + + if (combo != null) + { + combo.Position = new Vector2(accuracy.ScreenSpaceDeltaToParentSpace(score.ScreenSpaceDrawQuad.Size).X / 2 + horizontal_padding, vertical_offset + 5); + combo.Anchor = Anchor.TopCentre; + } + } + }) + { + Children = new Drawable[] + { + new DefaultComboCounter(), + new DefaultScoreCounter(), + new DefaultAccuracyCounter(), + new DefaultHealthDisplay(), + } + }; + + return skinnableTargetWrapper; + } + + return null; + } + + return base.GetDrawableComponent(component); + } + public override IBindable GetConfig(TLookup lookup) { switch (lookup) From 03d5f10744b91aa189d5bf1fe5a153a2117830b0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 11 May 2021 14:22:19 +0900 Subject: [PATCH 100/205] Fix default health bar not being considered for top-right flow layout --- osu.Game/Screens/Play/HUDOverlay.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index 88176e7ea2..c4a8860bb6 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -213,11 +213,11 @@ namespace osu.Game.Screens.Play foreach (var element in mainComponents.Children) { // for now align top-right components with the bottom-edge of the lowest top-anchored hud element. - if (!element.Anchor.HasFlagFast(Anchor.TopRight)) + if (!element.Anchor.HasFlagFast(Anchor.TopRight) && !element.RelativeSizeAxes.HasFlagFast(Axes.X)) continue; // health bars are excluded for the sake of hacky legacy skins which extend the health bar to take up the full screen area. - if (element is HealthDisplay) + if (element is LegacyHealthDisplay) continue; var bottomRight = element.ScreenSpaceDrawQuad.BottomRight; From d5fe4f0f72e404f9527e2ccfe444dcd6645de0fc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 11 May 2021 14:34:49 +0900 Subject: [PATCH 101/205] Remove unused skin resolution in `LegacyScoreCounter` --- osu.Game/Skinning/LegacyScoreCounter.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/osu.Game/Skinning/LegacyScoreCounter.cs b/osu.Game/Skinning/LegacyScoreCounter.cs index 0696d2bedd..8aa48da453 100644 --- a/osu.Game/Skinning/LegacyScoreCounter.cs +++ b/osu.Game/Skinning/LegacyScoreCounter.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Screens.Play.HUD; @@ -14,9 +13,6 @@ namespace osu.Game.Skinning protected override double RollingDuration => 1000; protected override Easing RollingEasing => Easing.Out; - [Resolved] - private ISkinSource skin { get; set; } - public LegacyScoreCounter() : base(6) { From 0cf3efa16b9c20dccd26722e599ffa75bc5b9b72 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 11 May 2021 16:56:23 +0900 Subject: [PATCH 102/205] Remove customisation support for `SongProgressDisplay` --- osu.Game/Screens/Play/SongProgress.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Play/SongProgress.cs b/osu.Game/Screens/Play/SongProgress.cs index d85f3538e4..4a0787bfae 100644 --- a/osu.Game/Screens/Play/SongProgress.cs +++ b/osu.Game/Screens/Play/SongProgress.cs @@ -14,7 +14,6 @@ using osu.Framework.Timing; using osu.Game.Configuration; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.UI; -using osu.Game.Skinning; namespace osu.Game.Screens.Play { @@ -181,12 +180,12 @@ namespace osu.Game.Screens.Play info.TransformTo(nameof(info.Margin), new MarginPadding { Bottom = finalMargin }, transition_duration, Easing.In); } - public class SongProgressDisplay : Container, ISkinnableComponent + public class SongProgressDisplay : Container { public SongProgressDisplay() { // TODO: move actual implementation into this. - // exists for skin customisation purposes. + // exists for skin customisation purposes (interface should be added to this container). Masking = true; RelativeSizeAxes = Axes.Both; From f6f4b90d2bd71680a470d92c0aef5a98494e8580 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 11 May 2021 16:56:55 +0900 Subject: [PATCH 103/205] Add customisation support for `LegacyHealthDisplay` --- osu.Game/Skinning/LegacyHealthDisplay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Skinning/LegacyHealthDisplay.cs b/osu.Game/Skinning/LegacyHealthDisplay.cs index 717ca62da7..dfb6ca1a64 100644 --- a/osu.Game/Skinning/LegacyHealthDisplay.cs +++ b/osu.Game/Skinning/LegacyHealthDisplay.cs @@ -16,7 +16,7 @@ using osuTK.Graphics; namespace osu.Game.Skinning { - public class LegacyHealthDisplay : HealthDisplay + public class LegacyHealthDisplay : HealthDisplay, ISkinnableComponent { private const double epic_cutoff = 0.5; From a67cead0b3adfce14608060d51131145ff653c81 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 11 May 2021 17:00:24 +0900 Subject: [PATCH 104/205] Add `SkinInfo.InstantiationInfo` to allow creating different skin types --- .../TestSceneHitCircleArea.cs | 2 +- osu.Game/Beatmaps/WorkingBeatmap.cs | 2 +- ...60743_AddSkinInstantiationInfo.Designer.cs | 508 ++++++++++++++++++ ...20210511060743_AddSkinInstantiationInfo.cs | 22 + .../Migrations/OsuDbContextModelSnapshot.cs | 6 +- osu.Game/Skinning/DefaultLegacySkin.cs | 10 +- osu.Game/Skinning/DefaultSkin.cs | 10 +- osu.Game/Skinning/SkinInfo.cs | 36 +- osu.Game/Skinning/SkinManager.cs | 22 +- 9 files changed, 597 insertions(+), 21 deletions(-) create mode 100644 osu.Game/Migrations/20210511060743_AddSkinInstantiationInfo.Designer.cs create mode 100644 osu.Game/Migrations/20210511060743_AddSkinInstantiationInfo.cs diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleArea.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleArea.cs index 0649989dc0..1fdcd73dde 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleArea.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleArea.cs @@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.Osu.Tests hitCircle.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); - Child = new SkinProvidingContainer(new DefaultSkin()) + Child = new SkinProvidingContainer(new DefaultSkin(null)) { RelativeSizeAxes = Axes.Both, Child = drawableHitCircle = new DrawableHitCircle(hitCircle) diff --git a/osu.Game/Beatmaps/WorkingBeatmap.cs b/osu.Game/Beatmaps/WorkingBeatmap.cs index e0eeaf6db0..3576b149bf 100644 --- a/osu.Game/Beatmaps/WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/WorkingBeatmap.cs @@ -324,7 +324,7 @@ namespace osu.Game.Beatmaps public bool SkinLoaded => skin.IsResultAvailable; public ISkin Skin => skin.Value; - protected virtual ISkin GetSkin() => new DefaultSkin(); + protected virtual ISkin GetSkin() => new DefaultSkin(null); private readonly RecyclableLazy skin; public abstract Stream GetStream(string storagePath); diff --git a/osu.Game/Migrations/20210511060743_AddSkinInstantiationInfo.Designer.cs b/osu.Game/Migrations/20210511060743_AddSkinInstantiationInfo.Designer.cs new file mode 100644 index 0000000000..b808c648da --- /dev/null +++ b/osu.Game/Migrations/20210511060743_AddSkinInstantiationInfo.Designer.cs @@ -0,0 +1,508 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using osu.Game.Database; + +namespace osu.Game.Migrations +{ + [DbContext(typeof(OsuDbContext))] + [Migration("20210511060743_AddSkinInstantiationInfo")] + partial class AddSkinInstantiationInfo + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "2.2.6-servicing-10079"); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapDifficulty", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("ApproachRate"); + + b.Property("CircleSize"); + + b.Property("DrainRate"); + + b.Property("OverallDifficulty"); + + b.Property("SliderMultiplier"); + + b.Property("SliderTickRate"); + + b.HasKey("ID"); + + b.ToTable("BeatmapDifficulty"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("AudioLeadIn"); + + b.Property("BPM"); + + b.Property("BaseDifficultyID"); + + b.Property("BeatDivisor"); + + b.Property("BeatmapSetInfoID"); + + b.Property("Countdown"); + + b.Property("DistanceSpacing"); + + b.Property("EpilepsyWarning"); + + b.Property("GridSize"); + + b.Property("Hash"); + + b.Property("Hidden"); + + b.Property("Length"); + + b.Property("LetterboxInBreaks"); + + b.Property("MD5Hash"); + + b.Property("MetadataID"); + + b.Property("OnlineBeatmapID"); + + b.Property("Path"); + + b.Property("RulesetID"); + + b.Property("SpecialStyle"); + + b.Property("StackLeniency"); + + b.Property("StarDifficulty"); + + b.Property("Status"); + + b.Property("StoredBookmarks"); + + b.Property("TimelineZoom"); + + b.Property("Version"); + + b.Property("WidescreenStoryboard"); + + b.HasKey("ID"); + + b.HasIndex("BaseDifficultyID"); + + b.HasIndex("BeatmapSetInfoID"); + + b.HasIndex("Hash"); + + b.HasIndex("MD5Hash"); + + b.HasIndex("MetadataID"); + + b.HasIndex("OnlineBeatmapID") + .IsUnique(); + + b.HasIndex("RulesetID"); + + b.ToTable("BeatmapInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapMetadata", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Artist"); + + b.Property("ArtistUnicode"); + + b.Property("AudioFile"); + + b.Property("AuthorString") + .HasColumnName("Author"); + + b.Property("BackgroundFile"); + + b.Property("PreviewTime"); + + b.Property("Source"); + + b.Property("Tags"); + + b.Property("Title"); + + b.Property("TitleUnicode"); + + b.HasKey("ID"); + + b.ToTable("BeatmapMetadata"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("BeatmapSetInfoID"); + + b.Property("FileInfoID"); + + b.Property("Filename") + .IsRequired(); + + b.HasKey("ID"); + + b.HasIndex("BeatmapSetInfoID"); + + b.HasIndex("FileInfoID"); + + b.ToTable("BeatmapSetFileInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("DeletePending"); + + b.Property("Hash"); + + b.Property("MetadataID"); + + b.Property("OnlineBeatmapSetID"); + + b.Property("Protected"); + + b.Property("Status"); + + b.HasKey("ID"); + + b.HasIndex("DeletePending"); + + b.HasIndex("Hash") + .IsUnique(); + + b.HasIndex("MetadataID"); + + b.HasIndex("OnlineBeatmapSetID") + .IsUnique(); + + b.ToTable("BeatmapSetInfo"); + }); + + modelBuilder.Entity("osu.Game.Configuration.DatabasedSetting", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Key") + .HasColumnName("Key"); + + b.Property("RulesetID"); + + b.Property("SkinInfoID"); + + b.Property("StringValue") + .HasColumnName("Value"); + + b.Property("Variant"); + + b.HasKey("ID"); + + b.HasIndex("SkinInfoID"); + + b.HasIndex("RulesetID", "Variant"); + + b.ToTable("Settings"); + }); + + modelBuilder.Entity("osu.Game.IO.FileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Hash"); + + b.Property("ReferenceCount"); + + b.HasKey("ID"); + + b.HasIndex("Hash") + .IsUnique(); + + b.HasIndex("ReferenceCount"); + + b.ToTable("FileInfo"); + }); + + modelBuilder.Entity("osu.Game.Input.Bindings.DatabasedKeyBinding", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("IntAction") + .HasColumnName("Action"); + + b.Property("KeysString") + .HasColumnName("Keys"); + + b.Property("RulesetID"); + + b.Property("Variant"); + + b.HasKey("ID"); + + b.HasIndex("IntAction"); + + b.HasIndex("RulesetID", "Variant"); + + b.ToTable("KeyBinding"); + }); + + modelBuilder.Entity("osu.Game.Rulesets.RulesetInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Available"); + + b.Property("InstantiationInfo"); + + b.Property("Name"); + + b.Property("ShortName"); + + b.HasKey("ID"); + + b.HasIndex("Available"); + + b.HasIndex("ShortName") + .IsUnique(); + + b.ToTable("RulesetInfo"); + }); + + modelBuilder.Entity("osu.Game.Scoring.ScoreFileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("FileInfoID"); + + b.Property("Filename") + .IsRequired(); + + b.Property("ScoreInfoID"); + + b.HasKey("ID"); + + b.HasIndex("FileInfoID"); + + b.HasIndex("ScoreInfoID"); + + b.ToTable("ScoreFileInfo"); + }); + + modelBuilder.Entity("osu.Game.Scoring.ScoreInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Accuracy") + .HasColumnType("DECIMAL(1,4)"); + + b.Property("BeatmapInfoID"); + + b.Property("Combo"); + + b.Property("Date"); + + b.Property("DeletePending"); + + b.Property("Hash"); + + b.Property("MaxCombo"); + + b.Property("ModsJson") + .HasColumnName("Mods"); + + b.Property("OnlineScoreID"); + + b.Property("PP"); + + b.Property("Rank"); + + b.Property("RulesetID"); + + b.Property("StatisticsJson") + .HasColumnName("Statistics"); + + b.Property("TotalScore"); + + b.Property("UserID") + .HasColumnName("UserID"); + + b.Property("UserString") + .HasColumnName("User"); + + b.HasKey("ID"); + + b.HasIndex("BeatmapInfoID"); + + b.HasIndex("OnlineScoreID") + .IsUnique(); + + b.HasIndex("RulesetID"); + + b.ToTable("ScoreInfo"); + }); + + modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("FileInfoID"); + + b.Property("Filename") + .IsRequired(); + + b.Property("SkinInfoID"); + + b.HasKey("ID"); + + b.HasIndex("FileInfoID"); + + b.HasIndex("SkinInfoID"); + + b.ToTable("SkinFileInfo"); + }); + + modelBuilder.Entity("osu.Game.Skinning.SkinInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Creator"); + + b.Property("DeletePending"); + + b.Property("Hash"); + + b.Property("InstantiationInfo"); + + b.Property("Name"); + + b.HasKey("ID"); + + b.HasIndex("DeletePending"); + + b.HasIndex("Hash") + .IsUnique(); + + b.ToTable("SkinInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapDifficulty", "BaseDifficulty") + .WithMany() + .HasForeignKey("BaseDifficultyID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo", "BeatmapSet") + .WithMany("Beatmaps") + .HasForeignKey("BeatmapSetInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata") + .WithMany("Beatmaps") + .HasForeignKey("MetadataID"); + + b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset") + .WithMany() + .HasForeignKey("RulesetID") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo") + .WithMany("Files") + .HasForeignKey("BeatmapSetInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.IO.FileInfo", "FileInfo") + .WithMany() + .HasForeignKey("FileInfoID") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata") + .WithMany("BeatmapSets") + .HasForeignKey("MetadataID"); + }); + + modelBuilder.Entity("osu.Game.Configuration.DatabasedSetting", b => + { + b.HasOne("osu.Game.Skinning.SkinInfo") + .WithMany("Settings") + .HasForeignKey("SkinInfoID"); + }); + + modelBuilder.Entity("osu.Game.Scoring.ScoreFileInfo", b => + { + b.HasOne("osu.Game.IO.FileInfo", "FileInfo") + .WithMany() + .HasForeignKey("FileInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Scoring.ScoreInfo") + .WithMany("Files") + .HasForeignKey("ScoreInfoID"); + }); + + modelBuilder.Entity("osu.Game.Scoring.ScoreInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapInfo", "Beatmap") + .WithMany("Scores") + .HasForeignKey("BeatmapInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset") + .WithMany() + .HasForeignKey("RulesetID") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b => + { + b.HasOne("osu.Game.IO.FileInfo", "FileInfo") + .WithMany() + .HasForeignKey("FileInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Skinning.SkinInfo") + .WithMany("Files") + .HasForeignKey("SkinInfoID") + .OnDelete(DeleteBehavior.Cascade); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/osu.Game/Migrations/20210511060743_AddSkinInstantiationInfo.cs b/osu.Game/Migrations/20210511060743_AddSkinInstantiationInfo.cs new file mode 100644 index 0000000000..1d5b0769a4 --- /dev/null +++ b/osu.Game/Migrations/20210511060743_AddSkinInstantiationInfo.cs @@ -0,0 +1,22 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace osu.Game.Migrations +{ + public partial class AddSkinInstantiationInfo : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "InstantiationInfo", + table: "SkinInfo", + nullable: true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "InstantiationInfo", + table: "SkinInfo"); + } + } +} diff --git a/osu.Game/Migrations/OsuDbContextModelSnapshot.cs b/osu.Game/Migrations/OsuDbContextModelSnapshot.cs index ec4461ca56..d4bde50b60 100644 --- a/osu.Game/Migrations/OsuDbContextModelSnapshot.cs +++ b/osu.Game/Migrations/OsuDbContextModelSnapshot.cs @@ -141,8 +141,6 @@ namespace osu.Game.Migrations b.Property("TitleUnicode"); - b.Property("VideoFile"); - b.HasKey("ID"); b.ToTable("BeatmapMetadata"); @@ -352,7 +350,7 @@ namespace osu.Game.Migrations b.Property("TotalScore"); - b.Property("UserID") + b.Property("UserID") .HasColumnName("UserID"); b.Property("UserString") @@ -402,6 +400,8 @@ namespace osu.Game.Migrations b.Property("Hash"); + b.Property("InstantiationInfo"); + b.Property("Name"); b.HasKey("ID"); diff --git a/osu.Game/Skinning/DefaultLegacySkin.cs b/osu.Game/Skinning/DefaultLegacySkin.cs index 4027cc650d..564be8630e 100644 --- a/osu.Game/Skinning/DefaultLegacySkin.cs +++ b/osu.Game/Skinning/DefaultLegacySkin.cs @@ -10,7 +10,12 @@ namespace osu.Game.Skinning public class DefaultLegacySkin : LegacySkin { public DefaultLegacySkin(IResourceStore storage, IStorageResourceProvider resources) - : base(Info, storage, resources, string.Empty) + : this(Info, storage, resources) + { + } + + public DefaultLegacySkin(SkinInfo skin, IResourceStore storage, IStorageResourceProvider resources) + : base(skin, storage, resources, string.Empty) { Configuration.CustomColours["SliderBall"] = new Color4(2, 170, 255, 255); Configuration.AddComboColours( @@ -27,7 +32,8 @@ namespace osu.Game.Skinning { ID = SkinInfo.CLASSIC_SKIN, // this is temporary until database storage is decided upon. Name = "osu!classic", - Creator = "team osu!" + Creator = "team osu!", + InstantiationInfo = typeof(DefaultLegacySkin).AssemblyQualifiedName, }; } } diff --git a/osu.Game/Skinning/DefaultSkin.cs b/osu.Game/Skinning/DefaultSkin.cs index ef3dffe59c..1c063b6ef2 100644 --- a/osu.Game/Skinning/DefaultSkin.cs +++ b/osu.Game/Skinning/DefaultSkin.cs @@ -10,6 +10,7 @@ using osu.Framework.Graphics.OpenGL.Textures; using osu.Framework.Graphics.Textures; using osu.Game.Audio; using osu.Game.Extensions; +using osu.Game.IO; using osu.Game.Screens.Play.HUD; using osuTK; using osuTK.Graphics; @@ -18,8 +19,13 @@ namespace osu.Game.Skinning { public class DefaultSkin : Skin { - public DefaultSkin() - : base(SkinInfo.Default, null) + public DefaultSkin(IStorageResourceProvider resources) + : this(SkinInfo.Default, resources) + { + } + + public DefaultSkin(SkinInfo skin, IStorageResourceProvider resources) + : base(skin, resources) { Configuration = new DefaultSkinConfiguration(); } diff --git a/osu.Game/Skinning/SkinInfo.cs b/osu.Game/Skinning/SkinInfo.cs index aaccbefb3d..2e29808cb4 100644 --- a/osu.Game/Skinning/SkinInfo.cs +++ b/osu.Game/Skinning/SkinInfo.cs @@ -3,8 +3,11 @@ using System; using System.Collections.Generic; +using System.Linq; +using osu.Framework.IO.Stores; using osu.Game.Configuration; using osu.Game.Database; +using osu.Game.IO; namespace osu.Game.Skinning { @@ -22,7 +25,35 @@ namespace osu.Game.Skinning public string Creator { get; set; } - public List Files { get; set; } + private string instantiationInfo; + + public string InstantiationInfo + { + get => instantiationInfo; + set => instantiationInfo = abbreviateInstantiationInfo(value); + } + + private string abbreviateInstantiationInfo(string value) + { + // exclude version onwards, matching only on namespace and type. + // this is mainly to allow for new versions of already loaded rulesets to "upgrade" from old. + return string.Join(',', value.Split(',').Take(2)); + } + + public virtual Skin CreateInstance(IResourceStore legacyDefaultResources, IStorageResourceProvider resources) + { + var type = string.IsNullOrEmpty(InstantiationInfo) + // handle the case of skins imported before InstantiationInfo was added. + ? typeof(LegacySkin) + : Type.GetType(InstantiationInfo); + + if (type == typeof(DefaultLegacySkin)) + return (Skin)Activator.CreateInstance(type, this, legacyDefaultResources, resources); + + return (Skin)Activator.CreateInstance(type, this, resources); + } + + public List Files { get; set; } = new List(); public List Settings { get; set; } @@ -32,7 +63,8 @@ namespace osu.Game.Skinning { ID = DEFAULT_SKIN, Name = "osu!lazer", - Creator = "team osu!" + Creator = "team osu!", + InstantiationInfo = typeof(DefaultSkin).AssemblyQualifiedName, }; public bool Equals(SkinInfo other) => other != null && ID == other.ID; diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs index f9e22c9c89..2ea236e44f 100644 --- a/osu.Game/Skinning/SkinManager.cs +++ b/osu.Game/Skinning/SkinManager.cs @@ -39,7 +39,7 @@ namespace osu.Game.Skinning private readonly IResourceStore legacyDefaultResources; - public readonly Bindable CurrentSkin = new Bindable(new DefaultSkin()); + public readonly Bindable CurrentSkin = new Bindable(new DefaultSkin(null)); public readonly Bindable CurrentSkinInfo = new Bindable(SkinInfo.Default) { Default = SkinInfo.Default }; public override IEnumerable HandledExtensions => new[] { ".osk" }; @@ -108,7 +108,7 @@ namespace osu.Game.Skinning { // we need to populate early to create a hash based off skin.ini contents if (item.Name?.Contains(".osk", StringComparison.OrdinalIgnoreCase) == true) - populateMetadata(item); + populateMetadata(item, GetSkin(item)); if (item.Creator != null && item.Creator != unknown_creator_string) { @@ -125,18 +125,20 @@ namespace osu.Game.Skinning { await base.Populate(model, archive, cancellationToken).ConfigureAwait(false); + var instance = GetSkin(model); + + model.InstantiationInfo ??= instance.GetType().AssemblyQualifiedName; + if (model.Name?.Contains(".osk", StringComparison.OrdinalIgnoreCase) == true) - populateMetadata(model); + populateMetadata(model, instance); } - private void populateMetadata(SkinInfo item) + private void populateMetadata(SkinInfo item, Skin instance) { - Skin reference = GetSkin(item); - - if (!string.IsNullOrEmpty(reference.Configuration.SkinInfo.Name)) + if (!string.IsNullOrEmpty(instance.Configuration.SkinInfo.Name)) { - item.Name = reference.Configuration.SkinInfo.Name; - item.Creator = reference.Configuration.SkinInfo.Creator; + item.Name = instance.Configuration.SkinInfo.Name; + item.Creator = instance.Configuration.SkinInfo.Creator; } else { @@ -150,7 +152,7 @@ namespace osu.Game.Skinning /// /// The skin to lookup. /// A instance correlating to the provided . - public Skin GetSkin(SkinInfo skinInfo) + public Skin GetSkin(SkinInfo skinInfo) => skinInfo.CreateInstance(legacyDefaultResources, this); { if (skinInfo == SkinInfo.Default) return new DefaultSkin(); From a7e83aacfb2b01201f7f8d87f584fbc871c30b3e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 11 May 2021 17:00:56 +0900 Subject: [PATCH 105/205] Ensure default skins are copied before modifying --- osu.Game/Skinning/Editor/SkinEditor.cs | 30 ++++++++++++-------------- osu.Game/Skinning/SkinManager.cs | 26 ++++++++++++++-------- 2 files changed, 31 insertions(+), 25 deletions(-) diff --git a/osu.Game/Skinning/Editor/SkinEditor.cs b/osu.Game/Skinning/Editor/SkinEditor.cs index 6b7d289284..90b003153b 100644 --- a/osu.Game/Skinning/Editor/SkinEditor.cs +++ b/osu.Game/Skinning/Editor/SkinEditor.cs @@ -4,6 +4,7 @@ using System; using System.Linq; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Input.Events; @@ -30,6 +31,8 @@ namespace osu.Game.Skinning.Editor [Resolved] private SkinManager skins { get; set; } + private Bindable currentSkin; + public SkinEditor(Drawable targetScreen) { this.targetScreen = targetScreen; @@ -119,23 +122,25 @@ namespace osu.Game.Skinning.Editor protected override void LoadComplete() { base.LoadComplete(); + Show(); + + // as long as the skin editor is loaded, let's make sure we can modify the current skin. + currentSkin = skins.CurrentSkin.GetBoundCopy(); + + // schedule ensures this only happens when the skin editor is visible. + // also avoid some weird endless recursion / bindable feedback loop (something to do with tracking skins across three different bindable types). + // probably something which will be factored out in a future database refactor so not too concerning for now. + currentSkin.BindValueChanged(skin => Scheduler.AddOnce(skins.EnsureMutableSkin), true); } private void revert() { - var currentSkin = skins.CurrentSkin.Value; - - var legacySkin = currentSkin as LegacySkin; - - if (legacySkin == null) - return; - SkinnableElementTargetContainer[] targetContainers = targetScreen.ChildrenOfType().ToArray(); foreach (var t in targetContainers) { - legacySkin.ResetDrawableTarget(t); + currentSkin.Value.ResetDrawableTarget(t); // add back default components getTarget(t.Target).Reload(); @@ -144,17 +149,10 @@ namespace osu.Game.Skinning.Editor private void save() { - var currentSkin = skins.CurrentSkin.Value; - - var legacySkin = currentSkin as LegacySkin; - - if (legacySkin == null) - return; - SkinnableElementTargetContainer[] targetContainers = targetScreen.ChildrenOfType().ToArray(); foreach (var t in targetContainers) - legacySkin.UpdateDrawableTarget(t); + currentSkin.Value.UpdateDrawableTarget(t); skins.Save(skins.CurrentSkin.Value); } diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs index 2ea236e44f..63d7e4f86f 100644 --- a/osu.Game/Skinning/SkinManager.cs +++ b/osu.Game/Skinning/SkinManager.cs @@ -153,22 +153,30 @@ namespace osu.Game.Skinning /// The skin to lookup. /// A instance correlating to the provided . public Skin GetSkin(SkinInfo skinInfo) => skinInfo.CreateInstance(legacyDefaultResources, this); + + /// + /// Ensure that the current skin is in a state it can accept user modifications. + /// This will create a copy of any internal skin and being tracking in the database if not already. + /// + public void EnsureMutableSkin() { - if (skinInfo == SkinInfo.Default) - return new DefaultSkin(); + if (CurrentSkinInfo.Value.ID >= 1) return; - if (skinInfo == DefaultLegacySkin.Info) - return new DefaultLegacySkin(legacyDefaultResources, this); + var skin = CurrentSkin.Value; - return new LegacySkin(skinInfo, this); + // if the user is attempting to save one of the default skin implementations, create a copy first. + CurrentSkinInfo.Value = Import(new SkinInfo + { + Name = skin.SkinInfo.Name + " (modified)", + Creator = skin.SkinInfo.Creator, + InstantiationInfo = skin.SkinInfo.InstantiationInfo, + }).Result; } public void Save(Skin skin) { - // some skins don't support saving just yet. - // eventually we will want to create a copy of the skin to allow for customisation. - if (skin.SkinInfo.Files == null) - return; + if (skin.SkinInfo.ID <= 0) + throw new InvalidOperationException($"Attempting to save a skin which is not yet tracked. Call {nameof(EnsureMutableSkin)} first."); foreach (var drawableInfo in skin.DrawableComponentInfo) { From 61ea3f2e6410980341803eb2c4548e917229ee3e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 11 May 2021 17:08:32 +0900 Subject: [PATCH 106/205] Remove unnecessary test step creating needless skins --- osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs | 7 ------- 1 file changed, 7 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs index b0edc0dd68..73da76df78 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs @@ -22,13 +22,6 @@ namespace osu.Game.Tests.Visual.Gameplay [SetUpSteps] public override void SetUpSteps() { - AddStep("set empty legacy skin", () => - { - var imported = skinManager.Import(new SkinInfo { Name = "test skin" }).Result; - - skinManager.CurrentSkinInfo.Value = imported; - }); - base.SetUpSteps(); AddStep("reload skin editor", () => From a88a8b7d8d504cf981bbcd65c1d81ba9a69a6320 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 11 May 2021 17:48:08 +0900 Subject: [PATCH 107/205] Use `ISkinnableComponent` wherever possible (and expose as `BindableList`) --- .../HUD/SkinnableElementTargetContainer.cs | 32 +++++++++++++++---- osu.Game/Screens/Play/HUDOverlay.cs | 32 +++++++++---------- osu.Game/Skinning/ISkinnableTarget.cs | 4 +-- osu.Game/Skinning/SkinnableTargetWrapper.cs | 2 +- 4 files changed, 42 insertions(+), 28 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/SkinnableElementTargetContainer.cs b/osu.Game/Screens/Play/HUD/SkinnableElementTargetContainer.cs index 051ab9ad3c..b2f6d32927 100644 --- a/osu.Game/Screens/Play/HUD/SkinnableElementTargetContainer.cs +++ b/osu.Game/Screens/Play/HUD/SkinnableElementTargetContainer.cs @@ -1,8 +1,10 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Collections.Generic; using System.Linq; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Extensions; using osu.Game.Skinning; @@ -15,30 +17,46 @@ namespace osu.Game.Screens.Play.HUD public SkinnableTarget Target { get; } + public IBindableList Components => components; + + private readonly BindableList components = new BindableList(); + public SkinnableElementTargetContainer(SkinnableTarget target) { Target = target; } - public IReadOnlyList Children => content?.Children; - public void Reload() { + ClearInternal(); + components.Clear(); + content = CurrentSkin.GetDrawableComponent(new SkinnableTargetComponent(Target)) as SkinnableTargetWrapper; - ClearInternal(); - if (content != null) - LoadComponentAsync(content, AddInternal); + { + LoadComponentAsync(content, wrapper => + { + AddInternal(wrapper); + components.AddRange(wrapper.Children.OfType()); + }); + } } - public void Add(Drawable drawable) + public void Add(ISkinnableComponent component) { + if (content == null) + throw new NotSupportedException("Attempting to add a new component to a target container which is not supported by the current skin."); + + if (!(component is Drawable drawable)) + throw new ArgumentException("Provided argument must be of type {nameof(ISkinnableComponent)}.", nameof(drawable)); + content.Add(drawable); + components.Add(component); } public IEnumerable CreateSerialisedChildren() => - content.Select(d => d.CreateSerialisedInformation()); + components.Select(d => ((Drawable)d).CreateSerialisedInformation()); protected override void SkinChanged(ISkinSource skin, bool allowFallback) { diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index c4a8860bb6..887346b5df 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.EnumExtensions; @@ -207,27 +208,24 @@ namespace osu.Game.Screens.Play Vector2 lowestScreenSpace = Vector2.Zero; - // TODO: may be null during skin switching. not sure if there's a better way of exposing these children. - if (mainComponents.Children != null) + // LINQ cast can be removed when IDrawable interface includes Anchor / RelativeSizeAxes. + foreach (var element in mainComponents.Components.Cast()) { - foreach (var element in mainComponents.Children) - { - // for now align top-right components with the bottom-edge of the lowest top-anchored hud element. - if (!element.Anchor.HasFlagFast(Anchor.TopRight) && !element.RelativeSizeAxes.HasFlagFast(Axes.X)) - continue; + // for now align top-right components with the bottom-edge of the lowest top-anchored hud element. + if (!element.Anchor.HasFlagFast(Anchor.TopRight) && !element.RelativeSizeAxes.HasFlagFast(Axes.X)) + continue; - // health bars are excluded for the sake of hacky legacy skins which extend the health bar to take up the full screen area. - if (element is LegacyHealthDisplay) - continue; + // health bars are excluded for the sake of hacky legacy skins which extend the health bar to take up the full screen area. + if (element is LegacyHealthDisplay) + continue; - var bottomRight = element.ScreenSpaceDrawQuad.BottomRight; - if (bottomRight.Y > lowestScreenSpace.Y) - lowestScreenSpace = bottomRight; - } - - topRightElements.Y = TopScoringElementsHeight = ToLocalSpace(lowestScreenSpace).Y; - bottomRightElements.Y = -Progress.Height; + var bottomRight = element.ScreenSpaceDrawQuad.BottomRight; + if (bottomRight.Y > lowestScreenSpace.Y) + lowestScreenSpace = bottomRight; } + + topRightElements.Y = TopScoringElementsHeight = ToLocalSpace(lowestScreenSpace).Y; + bottomRightElements.Y = -Progress.Height; } private void updateVisibility() diff --git a/osu.Game/Skinning/ISkinnableTarget.cs b/osu.Game/Skinning/ISkinnableTarget.cs index 2379fd4247..4d97b42e8e 100644 --- a/osu.Game/Skinning/ISkinnableTarget.cs +++ b/osu.Game/Skinning/ISkinnableTarget.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using osu.Framework.Graphics; - namespace osu.Game.Skinning { /// @@ -20,6 +18,6 @@ namespace osu.Game.Skinning /// /// Add the provided item to this target. /// - public void Add(Drawable drawable); + public void Add(ISkinnableComponent drawable); } } diff --git a/osu.Game/Skinning/SkinnableTargetWrapper.cs b/osu.Game/Skinning/SkinnableTargetWrapper.cs index a0df610488..0d16775f51 100644 --- a/osu.Game/Skinning/SkinnableTargetWrapper.cs +++ b/osu.Game/Skinning/SkinnableTargetWrapper.cs @@ -8,7 +8,7 @@ using osu.Framework.Graphics.Containers; namespace osu.Game.Skinning { /// - /// A container which is serialised and can encapsulate multiple skinnable elements into a single return type. + /// A container which is serialised and can encapsulate multiple skinnable elements into a single return type (for consumption via . /// Will also optionally apply default cross-element layout dependencies when initialised from a non-deserialised source. /// public class SkinnableTargetWrapper : Container, ISkinSerialisable From a4e052961770c3eb98408393ad198287ebcb0766 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 11 May 2021 17:49:00 +0900 Subject: [PATCH 108/205] Replace polling logic with direct bindable reactions --- .../Compose/Components/BlueprintContainer.cs | 23 +++++++++ .../Components/EditorBlueprintContainer.cs | 22 +-------- .../Skinning/Editor/SkinBlueprintContainer.cs | 49 ++++++++++++++++--- osu.Game/Skinning/Editor/SkinEditor.cs | 15 +++++- 4 files changed, 78 insertions(+), 31 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs index 361e98e0dd..edd1acbd6c 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs @@ -3,9 +3,11 @@ using System; using System.Collections.Generic; +using System.Collections.Specialized; using System.Diagnostics; using System.Linq; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Primitives; @@ -24,6 +26,7 @@ namespace osu.Game.Screens.Edit.Compose.Components /// Includes selection and manipulation support via a . /// public abstract class BlueprintContainer : CompositeDrawable, IKeyBindingHandler + where T : class { protected DragBox DragBox { get; private set; } @@ -39,6 +42,8 @@ namespace osu.Game.Screens.Edit.Compose.Components [Resolved(CanBeNull = true)] private IEditorChangeHandler changeHandler { get; set; } + protected readonly BindableList SelectedItems = new BindableList(); + protected BlueprintContainer() { RelativeSizeAxes = Axes.Both; @@ -47,6 +52,24 @@ namespace osu.Game.Screens.Edit.Compose.Components [BackgroundDependencyLoader] private void load() { + SelectedItems.CollectionChanged += (selectedObjects, args) => + { + switch (args.Action) + { + case NotifyCollectionChangedAction.Add: + foreach (var o in args.NewItems) + SelectionBlueprints.FirstOrDefault(b => b.Item == o)?.Select(); + + break; + + case NotifyCollectionChangedAction.Remove: + foreach (var o in args.OldItems) + SelectionBlueprints.FirstOrDefault(b => b.Item == o)?.Deselect(); + + break; + } + }; + SelectionHandler = CreateSelectionHandler(); SelectionHandler.DeselectAll = deselectAll; diff --git a/osu.Game/Screens/Edit/Compose/Components/EditorBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/EditorBlueprintContainer.cs index db322faf65..31a191c80c 100644 --- a/osu.Game/Screens/Edit/Compose/Components/EditorBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/EditorBlueprintContainer.cs @@ -2,10 +2,8 @@ // 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.Allocation; -using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Input.Events; @@ -24,8 +22,6 @@ namespace osu.Game.Screens.Edit.Compose.Components protected readonly HitObjectComposer Composer; - private readonly BindableList selectedHitObjects = new BindableList(); - protected EditorBlueprintContainer(HitObjectComposer composer) { Composer = composer; @@ -34,23 +30,7 @@ namespace osu.Game.Screens.Edit.Compose.Components [BackgroundDependencyLoader] private void load() { - selectedHitObjects.BindTo(Beatmap.SelectedHitObjects); - selectedHitObjects.CollectionChanged += (selectedObjects, args) => - { - switch (args.Action) - { - case NotifyCollectionChangedAction.Add: - foreach (var o in args.NewItems) - SelectionBlueprints.FirstOrDefault(b => b.Item == o)?.Select(); - break; - - case NotifyCollectionChangedAction.Remove: - foreach (var o in args.OldItems) - SelectionBlueprints.FirstOrDefault(b => b.Item == o)?.Deselect(); - - break; - } - }; + SelectedItems.BindTo(Beatmap.SelectedHitObjects); } protected override void LoadComplete() diff --git a/osu.Game/Skinning/Editor/SkinBlueprintContainer.cs b/osu.Game/Skinning/Editor/SkinBlueprintContainer.cs index 45e541abf0..bc1ea02090 100644 --- a/osu.Game/Skinning/Editor/SkinBlueprintContainer.cs +++ b/osu.Game/Skinning/Editor/SkinBlueprintContainer.cs @@ -1,11 +1,16 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Collections.Generic; +using System.Collections.Specialized; using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Testing; using osu.Game.Rulesets.Edit; using osu.Game.Screens.Edit.Compose.Components; +using osu.Game.Screens.Play.HUD; namespace osu.Game.Skinning.Editor { @@ -18,23 +23,51 @@ namespace osu.Game.Skinning.Editor this.target = target; } + [BackgroundDependencyLoader(true)] + private void load(SkinEditor editor) + { + SelectedItems.BindTo(editor.SelectedComponents); + } + + private readonly List> targetComponents = new List>(); + protected override void LoadComplete() { base.LoadComplete(); - checkForComponents(); + // track each target container on the current screen. + foreach (var targetContainer in target.ChildrenOfType()) + { + var bindableList = new BindableList { BindTarget = targetContainer.Components }; + bindableList.BindCollectionChanged(componentsChanged, true); + + targetComponents.Add(bindableList); + } } - private void checkForComponents() + private void componentsChanged(object sender, NotifyCollectionChangedEventArgs e) { - ISkinnableComponent[] skinnableComponents = target.ChildrenOfType().ToArray(); + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: + foreach (var item in e.NewItems.Cast()) + AddBlueprintFor(item); + break; - foreach (var c in skinnableComponents) - AddBlueprintFor(c); + case NotifyCollectionChangedAction.Remove: + case NotifyCollectionChangedAction.Reset: + foreach (var item in e.OldItems.Cast()) + RemoveBlueprintFor(item); + break; - // We'd hope to eventually be running this in a more sensible way, but this handles situations where new drawables become present (ie. during ongoing gameplay) - // or when drawables in the target are loaded asynchronously and may not be immediately available when this BlueprintContainer is loaded. - Scheduler.AddDelayed(checkForComponents, 1000); + case NotifyCollectionChangedAction.Replace: + foreach (var item in e.OldItems.Cast()) + RemoveBlueprintFor(item); + + foreach (var item in e.NewItems.Cast()) + AddBlueprintFor(item); + break; + } } protected override SelectionHandler CreateSelectionHandler() => new SkinSelectionHandler(); diff --git a/osu.Game/Skinning/Editor/SkinEditor.cs b/osu.Game/Skinning/Editor/SkinEditor.cs index 90b003153b..b098136eda 100644 --- a/osu.Game/Skinning/Editor/SkinEditor.cs +++ b/osu.Game/Skinning/Editor/SkinEditor.cs @@ -18,6 +18,7 @@ using osuTK; namespace osu.Game.Skinning.Editor { + [Cached(typeof(SkinEditor))] public class SkinEditor : FocusedOverlayContainer { public const double TRANSITION_DURATION = 500; @@ -28,11 +29,15 @@ namespace osu.Game.Skinning.Editor protected override bool StartHidden => true; + public readonly BindableList SelectedComponents = new BindableList(); + [Resolved] private SkinManager skins { get; set; } private Bindable currentSkin; + private SkinBlueprintContainer blueprintContainer; + public SkinEditor(Drawable targetScreen) { this.targetScreen = targetScreen; @@ -56,7 +61,7 @@ namespace osu.Game.Skinning.Editor Origin = Anchor.TopCentre, RelativeSizeAxes = Axes.X }, - new SkinBlueprintContainer(targetScreen), + blueprintContainer = new SkinBlueprintContainer(targetScreen), new SkinComponentToolbox(600) { Anchor = Anchor.CentreLeft, @@ -109,9 +114,15 @@ namespace osu.Game.Skinning.Editor private void placeComponent(Type type) { - Drawable instance = (Drawable)Activator.CreateInstance(type); + var instance = (Drawable)Activator.CreateInstance(type) as ISkinnableComponent; + + if (instance == null) + throw new InvalidOperationException("Attempted to instantiate a component for placement which was not an {typeof(ISkinnableComponent)}."); getTarget(SkinnableTarget.MainHUDComponents)?.Add(instance); + + SelectedComponents.Clear(); + SelectedComponents.Add(instance); } private ISkinnableTarget getTarget(SkinnableTarget target) From 1831f581aa15f14f4cd01bf73122dedc5a5d804b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 11 May 2021 18:07:58 +0900 Subject: [PATCH 109/205] Add basic metadata display and remove outdated message about not saving --- osu.Game/Skinning/Editor/SkinEditor.cs | 55 ++++++++++++++++---------- 1 file changed, 35 insertions(+), 20 deletions(-) diff --git a/osu.Game/Skinning/Editor/SkinEditor.cs b/osu.Game/Skinning/Editor/SkinEditor.cs index b098136eda..0e243d1a02 100644 --- a/osu.Game/Skinning/Editor/SkinEditor.cs +++ b/osu.Game/Skinning/Editor/SkinEditor.cs @@ -36,7 +36,8 @@ namespace osu.Game.Skinning.Editor private Bindable currentSkin; - private SkinBlueprintContainer blueprintContainer; + [Resolved] + private OsuColour colours { get; set; } public SkinEditor(Drawable targetScreen) { @@ -46,7 +47,7 @@ namespace osu.Game.Skinning.Editor } [BackgroundDependencyLoader] - private void load(OsuColour colours) + private void load() { InternalChild = new OsuContextMenuContainer { @@ -61,7 +62,7 @@ namespace osu.Game.Skinning.Editor Origin = Anchor.TopCentre, RelativeSizeAxes = Axes.X }, - blueprintContainer = new SkinBlueprintContainer(targetScreen), + new SkinBlueprintContainer(targetScreen), new SkinComponentToolbox(600) { Anchor = Anchor.CentreLeft, @@ -103,13 +104,42 @@ namespace osu.Game.Skinning.Editor }, } }; + } - headerText.AddParagraph("Skin editor (preview)", cp => cp.Font = OsuFont.Default.With(size: 24)); - headerText.AddParagraph("This is a preview of what is to come. Changes are lost on changing screens.", cp => + protected override void LoadComplete() + { + base.LoadComplete(); + + Show(); + + // as long as the skin editor is loaded, let's make sure we can modify the current skin. + currentSkin = skins.CurrentSkin.GetBoundCopy(); + + // schedule ensures this only happens when the skin editor is visible. + // also avoid some weird endless recursion / bindable feedback loop (something to do with tracking skins across three different bindable types). + // probably something which will be factored out in a future database refactor so not too concerning for now. + currentSkin.BindValueChanged(skin => Scheduler.AddOnce(skinChanged), true); + } + + private void skinChanged() + { + headerText.Clear(); + + headerText.AddParagraph("Skin editor", cp => cp.Font = OsuFont.Default.With(size: 24)); + headerText.NewParagraph(); + headerText.AddText("Currently editing ", cp => { cp.Font = OsuFont.Default.With(size: 12); cp.Colour = colours.Yellow; }); + + headerText.AddText($"{currentSkin.Value.SkinInfo}", cp => + { + cp.Font = OsuFont.Default.With(size: 12, weight: FontWeight.Bold); + cp.Colour = colours.Yellow; + }); + + skins.EnsureMutableSkin(); } private void placeComponent(Type type) @@ -130,21 +160,6 @@ namespace osu.Game.Skinning.Editor return targetScreen.ChildrenOfType().FirstOrDefault(c => c.Target == target); } - protected override void LoadComplete() - { - base.LoadComplete(); - - Show(); - - // as long as the skin editor is loaded, let's make sure we can modify the current skin. - currentSkin = skins.CurrentSkin.GetBoundCopy(); - - // schedule ensures this only happens when the skin editor is visible. - // also avoid some weird endless recursion / bindable feedback loop (something to do with tracking skins across three different bindable types). - // probably something which will be factored out in a future database refactor so not too concerning for now. - currentSkin.BindValueChanged(skin => Scheduler.AddOnce(skins.EnsureMutableSkin), true); - } - private void revert() { SkinnableElementTargetContainer[] targetContainers = targetScreen.ChildrenOfType().ToArray(); From 6d587dc39229c8150e6f563ec5122b81115f5edc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 11 May 2021 18:17:05 +0900 Subject: [PATCH 110/205] Adjust target size slightly to better align with the screen --- osu.Game/Skinning/Editor/SkinEditorOverlay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Skinning/Editor/SkinEditorOverlay.cs b/osu.Game/Skinning/Editor/SkinEditorOverlay.cs index cc989bb459..cbed498a38 100644 --- a/osu.Game/Skinning/Editor/SkinEditorOverlay.cs +++ b/osu.Game/Skinning/Editor/SkinEditorOverlay.cs @@ -71,7 +71,7 @@ namespace osu.Game.Skinning.Editor target.RelativePositionAxes = Axes.Both; target.ScaleTo(VISIBLE_TARGET_SCALE, SkinEditor.TRANSITION_DURATION, Easing.OutQuint); - target.MoveToX(0.1f, SkinEditor.TRANSITION_DURATION, Easing.OutQuint); + target.MoveToX(0.095f, SkinEditor.TRANSITION_DURATION, Easing.OutQuint); } else { From f55407f871a94a5846270f947b1326ecdb9de8bf Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 11 May 2021 18:37:41 +0900 Subject: [PATCH 111/205] Show a message when attempting to customisse a screen which doesn't support it --- osu.Game/Screens/ScreenWhiteBox.cs | 24 ++--- .../Skinning/Editor/SkinBlueprintContainer.cs | 14 ++- osu.Game/Skinning/Editor/SkinEditor.cs | 90 ++++++++++++------- 3 files changed, 81 insertions(+), 47 deletions(-) diff --git a/osu.Game/Screens/ScreenWhiteBox.cs b/osu.Game/Screens/ScreenWhiteBox.cs index 3d8fd5dad7..cf0c183766 100644 --- a/osu.Game/Screens/ScreenWhiteBox.cs +++ b/osu.Game/Screens/ScreenWhiteBox.cs @@ -87,9 +87,9 @@ namespace osu.Game.Screens private static Color4 getColourFor(object type) { int hash = type.GetHashCode(); - byte r = (byte)Math.Clamp(((hash & 0xFF0000) >> 16) * 0.8f, 20, 255); - byte g = (byte)Math.Clamp(((hash & 0x00FF00) >> 8) * 0.8f, 20, 255); - byte b = (byte)Math.Clamp((hash & 0x0000FF) * 0.8f, 20, 255); + byte r = (byte)Math.Clamp(((hash & 0xFF0000) >> 16) * 2, 128, 255); + byte g = (byte)Math.Clamp(((hash & 0x00FF00) >> 8) * 2, 128, 255); + byte b = (byte)Math.Clamp((hash & 0x0000FF) * 2, 128, 255); return new Color4(r, g, b, 255); } @@ -109,10 +109,10 @@ namespace osu.Game.Screens private readonly Container boxContainer; - public UnderConstructionMessage(string name) + public UnderConstructionMessage(string name, string description = "is not yet ready for use!") { - RelativeSizeAxes = Axes.Both; - Size = new Vector2(0.3f); + AutoSizeAxes = Axes.Both; + Anchor = Anchor.Centre; Origin = Anchor.Centre; @@ -124,7 +124,7 @@ namespace osu.Game.Screens { CornerRadius = 20, Masking = true, - RelativeSizeAxes = Axes.Both, + AutoSizeAxes = Axes.Both, Anchor = Anchor.Centre, Origin = Anchor.Centre, Children = new Drawable[] @@ -133,15 +133,15 @@ namespace osu.Game.Screens { RelativeSizeAxes = Axes.Both, - Colour = colour, - Alpha = 0.2f, - Blending = BlendingParameters.Additive, + Colour = colour.Darken(0.8f), + Alpha = 0.8f, }, TextContainer = new FillFlowContainer { AutoSizeAxes = Axes.Both, Anchor = Anchor.Centre, Origin = Anchor.Centre, + Padding = new MarginPadding(20), Direction = FillDirection.Vertical, Children = new Drawable[] { @@ -157,14 +157,14 @@ namespace osu.Game.Screens Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, Text = name, - Colour = colour.Lighten(0.8f), + Colour = colour, Font = OsuFont.GetFont(size: 36), }, new OsuSpriteText { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, - Text = "is not yet ready for use!", + Text = description, Font = OsuFont.GetFont(size: 20), }, new OsuSpriteText diff --git a/osu.Game/Skinning/Editor/SkinBlueprintContainer.cs b/osu.Game/Skinning/Editor/SkinBlueprintContainer.cs index bc1ea02090..ef189ed165 100644 --- a/osu.Game/Skinning/Editor/SkinBlueprintContainer.cs +++ b/osu.Game/Skinning/Editor/SkinBlueprintContainer.cs @@ -7,8 +7,10 @@ using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Framework.Screens; using osu.Framework.Testing; using osu.Game.Rulesets.Edit; +using osu.Game.Screens; using osu.Game.Screens.Edit.Compose.Components; using osu.Game.Screens.Play.HUD; @@ -36,7 +38,17 @@ namespace osu.Game.Skinning.Editor base.LoadComplete(); // track each target container on the current screen. - foreach (var targetContainer in target.ChildrenOfType()) + var targetContainers = target.ChildrenOfType().ToArray(); + + if (targetContainers.Length == 0) + { + var targetScreen = target.ChildrenOfType().LastOrDefault()?.GetType().Name ?? "this screen"; + + AddInternal(new ScreenWhiteBox.UnderConstructionMessage(targetScreen, "doesn't support skin customisation just yet.")); + return; + } + + foreach (var targetContainer in targetContainers) { var bindableList = new BindableList { BindTarget = targetContainer.Components }; bindableList.BindCollectionChanged(componentsChanged, true); diff --git a/osu.Game/Skinning/Editor/SkinEditor.cs b/osu.Game/Skinning/Editor/SkinEditor.cs index 0e243d1a02..08fc458b94 100644 --- a/osu.Game/Skinning/Editor/SkinEditor.cs +++ b/osu.Game/Skinning/Editor/SkinEditor.cs @@ -62,46 +62,68 @@ namespace osu.Game.Skinning.Editor Origin = Anchor.TopCentre, RelativeSizeAxes = Axes.X }, - new SkinBlueprintContainer(targetScreen), - new SkinComponentToolbox(600) + new GridContainer { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - RequestPlacement = placeComponent - }, - new FillFlowContainer - { - Direction = FillDirection.Horizontal, - AutoSizeAxes = Axes.Both, - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - Spacing = new Vector2(5), - Padding = new MarginPadding + RelativeSizeAxes = Axes.Both, + ColumnDimensions = new[] { - Top = 10, - Left = 10, + new Dimension(GridSizeMode.AutoSize), + new Dimension() }, - Margin = new MarginPadding + Content = new[] { - Right = 10, - Bottom = 10, - }, - Children = new Drawable[] - { - new TriangleButton + new Drawable[] { - Text = "Save Changes", - Width = 140, - Action = save, - }, - new DangerousTriangleButton - { - Text = "Revert to default", - Width = 140, - Action = revert, - }, + new SkinComponentToolbox(600) + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + RequestPlacement = placeComponent + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + new SkinBlueprintContainer(targetScreen), + new FillFlowContainer + { + Direction = FillDirection.Horizontal, + AutoSizeAxes = Axes.Both, + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + Spacing = new Vector2(5), + Padding = new MarginPadding + { + Top = 10, + Left = 10, + }, + Margin = new MarginPadding + { + Right = 10, + Bottom = 10, + }, + Children = new Drawable[] + { + new TriangleButton + { + Text = "Save Changes", + Width = 140, + Action = save, + }, + new DangerousTriangleButton + { + Text = "Revert to default", + Width = 140, + Action = revert, + }, + } + }, + } + }, + } } - }, + } } }; } From 4bb933e4b111439ac022ed82c713b0197845c469 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 11 May 2021 18:55:45 +0900 Subject: [PATCH 112/205] Add missing base lookup call to `DefaultSkin` --- osu.Game/Skinning/DefaultSkin.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Skinning/DefaultSkin.cs b/osu.Game/Skinning/DefaultSkin.cs index 1c063b6ef2..3de3dc7702 100644 --- a/osu.Game/Skinning/DefaultSkin.cs +++ b/osu.Game/Skinning/DefaultSkin.cs @@ -36,6 +36,9 @@ namespace osu.Game.Skinning public override Drawable GetDrawableComponent(ISkinComponent component) { + if (base.GetDrawableComponent(component) is Drawable c) + return c; + switch (component) { case SkinnableTargetComponent target: From 1231c08a0791ec9b739303f29ce3bc2aa9a36906 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 11 May 2021 18:58:26 +0900 Subject: [PATCH 113/205] Rename mismatching file --- .../{IImmutableSkinnableComponent.cs => ISkinSerialisable.cs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename osu.Game/Skinning/{IImmutableSkinnableComponent.cs => ISkinSerialisable.cs} (100%) diff --git a/osu.Game/Skinning/IImmutableSkinnableComponent.cs b/osu.Game/Skinning/ISkinSerialisable.cs similarity index 100% rename from osu.Game/Skinning/IImmutableSkinnableComponent.cs rename to osu.Game/Skinning/ISkinSerialisable.cs From 811282a975ca48c91239599c0521dbe60a3811f8 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 11 May 2021 19:01:41 +0900 Subject: [PATCH 114/205] Add failing test --- .../Multiplayer/TestSceneMultiplayer.cs | 93 +++++++++++++++++++ .../Multiplayer/TestMultiplayerClient.cs | 10 +- 2 files changed, 102 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs index 78bc51e47b..519d76fc93 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs @@ -1,9 +1,24 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Linq; +using NUnit.Framework; using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Platform; +using osu.Framework.Screens; +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.Rulesets.Osu; using osu.Game.Screens.OnlinePlay.Components; +using osu.Game.Screens.OnlinePlay.Multiplayer.Match; +using osu.Game.Tests.Beatmaps; +using osu.Game.Tests.Resources; +using osu.Game.Users; +using osuTK.Input; namespace osu.Game.Tests.Visual.Multiplayer { @@ -11,7 +26,85 @@ namespace osu.Game.Tests.Visual.Multiplayer { private TestMultiplayer multiplayerScreen; + private BeatmapManager beatmaps; + private RulesetStore rulesets; + private BeatmapSetInfo importedSet; + + private TestMultiplayerClient client => multiplayerScreen.Client; + private Room room => client.APIRoom; + public TestSceneMultiplayer() + { + loadMultiplayer(); + } + + [BackgroundDependencyLoader] + private void load(GameHost host, AudioManager audio) + { + Dependencies.Cache(rulesets = new RulesetStore(ContextFactory)); + Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, audio, host, Beatmap.Default)); + beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).Wait(); + + importedSet = beatmaps.GetAllUsableBeatmapSetsEnumerable(IncludedDetails.All).First(); + } + + [Test] + public void TestMatchStartWhileSpectatingWithNoBeatmap() + { + loadMultiplayer(); + + AddStep("open room", () => + { + multiplayerScreen.OpenNewRoom(new Room + { + Name = { Value = "Test Room" }, + Playlist = + { + new PlaylistItem + { + Beatmap = { Value = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo }, + Ruleset = { Value = new OsuRuleset().RulesetInfo }, + } + } + }); + }); + + AddStep("create room", () => + { + InputManager.MoveMouseTo(this.ChildrenOfType().Single()); + InputManager.Click(MouseButton.Left); + }); + + AddUntilStep("wait for join", () => client.Room != null); + + AddStep("join other user (ready, host)", () => + { + client.AddUser(new User { Id = MultiplayerTestScene.PLAYER_1_ID, Username = "Other" }); + client.TransferHost(MultiplayerTestScene.PLAYER_1_ID); + client.ChangeUserState(MultiplayerTestScene.PLAYER_1_ID, MultiplayerUserState.Ready); + }); + + AddStep("change playlist", () => + { + room.Playlist.Add(new PlaylistItem + { + Beatmap = { Value = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo }, + Ruleset = { Value = new OsuRuleset().RulesetInfo }, + }); + }); + + AddStep("click spectate button", () => + { + InputManager.MoveMouseTo(this.ChildrenOfType().Single()); + InputManager.Click(MouseButton.Left); + }); + + AddStep("start match externally", () => client.StartMatch()); + + AddAssert("screen not changed", () => multiplayerScreen.IsCurrentScreen()); + } + + private void loadMultiplayer() { AddStep("show", () => { diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index de77a15da0..167cf705a7 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -25,6 +25,8 @@ namespace osu.Game.Tests.Visual.Multiplayer public override IBindable IsConnected => isConnected; private readonly Bindable isConnected = new Bindable(true); + public Room? APIRoom { get; private set; } + public Action? RoomSetupAction; [Resolved] @@ -138,10 +140,16 @@ namespace osu.Game.Tests.Visual.Multiplayer RoomSetupAction?.Invoke(room); RoomSetupAction = null; + APIRoom = apiRoom; + return Task.FromResult(room); } - protected override Task LeaveRoomInternal() => Task.CompletedTask; + protected override Task LeaveRoomInternal() + { + APIRoom = null; + return Task.CompletedTask; + } public override Task TransferHost(int userId) => ((IMultiplayerClient)this).HostChanged(userId); From 7fe8737d9437073a6c6137483d6c9be48cf3c9c4 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 11 May 2021 19:21:44 +0900 Subject: [PATCH 115/205] Add failing tests --- .../Multiplayer/TestSceneMultiplayer.cs | 92 ++++++++++++++----- 1 file changed, 67 insertions(+), 25 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs index 519d76fc93..a81927bec2 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs @@ -49,34 +49,23 @@ namespace osu.Game.Tests.Visual.Multiplayer } [Test] - public void TestMatchStartWhileSpectatingWithNoBeatmap() + public void TestLocalPlayDoesNotStartWhileSpectatingWithNoBeatmap() { loadMultiplayer(); - AddStep("open room", () => + createRoom(new Room { - multiplayerScreen.OpenNewRoom(new Room + Name = { Value = "Test Room" }, + Playlist = { - Name = { Value = "Test Room" }, - Playlist = + new PlaylistItem { - new PlaylistItem - { - Beatmap = { Value = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo }, - Ruleset = { Value = new OsuRuleset().RulesetInfo }, - } + Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.RulesetID == 0)).BeatmapInfo }, + Ruleset = { Value = new OsuRuleset().RulesetInfo }, } - }); + } }); - AddStep("create room", () => - { - InputManager.MoveMouseTo(this.ChildrenOfType().Single()); - InputManager.Click(MouseButton.Left); - }); - - AddUntilStep("wait for join", () => client.Room != null); - AddStep("join other user (ready, host)", () => { client.AddUser(new User { Id = MultiplayerTestScene.PLAYER_1_ID, Username = "Other" }); @@ -84,13 +73,44 @@ namespace osu.Game.Tests.Visual.Multiplayer client.ChangeUserState(MultiplayerTestScene.PLAYER_1_ID, MultiplayerUserState.Ready); }); - AddStep("change playlist", () => + AddStep("delete beatmap", () => beatmaps.Delete(importedSet)); + + AddStep("click spectate button", () => { - room.Playlist.Add(new PlaylistItem + InputManager.MoveMouseTo(this.ChildrenOfType().Single()); + InputManager.Click(MouseButton.Left); + }); + + AddStep("start match externally", () => client.StartMatch()); + + AddAssert("play not started", () => multiplayerScreen.IsCurrentScreen()); + } + + [Test] + public void TestLocalPlayStartsWhileSpectatingWhenBeatmapBecomesAvailable() + { + loadMultiplayer(); + + createRoom(new Room + { + Name = { Value = "Test Room" }, + Playlist = { - Beatmap = { Value = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo }, - Ruleset = { Value = new OsuRuleset().RulesetInfo }, - }); + new PlaylistItem + { + Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.RulesetID == 0)).BeatmapInfo }, + Ruleset = { Value = new OsuRuleset().RulesetInfo }, + } + } + }); + + AddStep("delete beatmap", () => beatmaps.Delete(importedSet)); + + AddStep("join other user (ready, host)", () => + { + client.AddUser(new User { Id = MultiplayerTestScene.PLAYER_1_ID, Username = "Other" }); + client.TransferHost(MultiplayerTestScene.PLAYER_1_ID); + client.ChangeUserState(MultiplayerTestScene.PLAYER_1_ID, MultiplayerUserState.Ready); }); AddStep("click spectate button", () => @@ -101,7 +121,29 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("start match externally", () => client.StartMatch()); - AddAssert("screen not changed", () => multiplayerScreen.IsCurrentScreen()); + AddStep("restore beatmap", () => + { + beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).Wait(); + importedSet = beatmaps.GetAllUsableBeatmapSetsEnumerable(IncludedDetails.All).First(); + }); + + AddUntilStep("play started", () => !multiplayerScreen.IsCurrentScreen()); + } + + private void createRoom(Room room) + { + AddStep("open room", () => + { + multiplayerScreen.OpenNewRoom(room); + }); + + AddStep("create room", () => + { + InputManager.MoveMouseTo(this.ChildrenOfType().Single()); + InputManager.Click(MouseButton.Left); + }); + + AddUntilStep("wait for join", () => client.Room != null); } private void loadMultiplayer() From 9ad1e5067e0494730446a34bc01e0222010e438a Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 11 May 2021 19:22:09 +0900 Subject: [PATCH 116/205] Fix spectate being entered while not having the beatmap --- .../Multiplayer/MultiplayerMatchSubScreen.cs | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index ab22d40181..fa18b792c3 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -14,6 +14,7 @@ using osu.Framework.Screens; using osu.Framework.Threading; using osu.Game.Beatmaps; using osu.Game.Configuration; +using osu.Game.Online; using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; using osu.Game.Overlays; @@ -354,10 +355,17 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer client.ChangeBeatmapAvailability(availability.NewValue); - // while this flow is handled server-side, this covers the edge case of the local user being in a ready state and then deleting the current beatmap. - if (availability.NewValue != Online.Rooms.BeatmapAvailability.LocallyAvailable() - && client.LocalUser?.State == MultiplayerUserState.Ready) - client.ChangeState(MultiplayerUserState.Idle); + if (availability.NewValue.State != DownloadState.LocallyAvailable) + { + // while this flow is handled server-side, this covers the edge case of the local user being in a ready state and then deleting the current beatmap. + if (client.LocalUser?.State == MultiplayerUserState.Ready) + client.ChangeState(MultiplayerUserState.Idle); + } + else + { + if (client.LocalUser?.State == MultiplayerUserState.Spectating && (client.Room?.State == MultiplayerRoomState.WaitingForLoad || client.Room?.State == MultiplayerRoomState.Playing)) + onLoadRequested(); + } } private void onReadyClick() @@ -413,6 +421,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer private void onLoadRequested() { + if (BeatmapAvailability.Value.State != DownloadState.LocallyAvailable) + return; + // In the case of spectating, IMultiplayerClient.LoadRequested can be fired while the game is still spectating a previous session. // For now, we want to game to switch to the new game so need to request exiting from the play screen. if (!ParentScreen.IsCurrentScreen()) From bc4213eea1fd86a2184d1c0ba8d874025e9c3ce6 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 11 May 2021 19:26:58 +0900 Subject: [PATCH 117/205] Add test for changing back to idle on deletion --- .../Multiplayer/TestSceneMultiplayer.cs | 31 +++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs index a81927bec2..f2331542c6 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs @@ -15,7 +15,6 @@ using osu.Game.Rulesets; using osu.Game.Rulesets.Osu; using osu.Game.Screens.OnlinePlay.Components; using osu.Game.Screens.OnlinePlay.Multiplayer.Match; -using osu.Game.Tests.Beatmaps; using osu.Game.Tests.Resources; using osu.Game.Users; using osuTK.Input; @@ -43,9 +42,37 @@ namespace osu.Game.Tests.Visual.Multiplayer { Dependencies.Cache(rulesets = new RulesetStore(ContextFactory)); Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, audio, host, Beatmap.Default)); - beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).Wait(); + } + [SetUp] + public void Setup() => Schedule(() => + { + beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).Wait(); importedSet = beatmaps.GetAllUsableBeatmapSetsEnumerable(IncludedDetails.All).First(); + }); + + [Test] + public void TestUserSetToIdleWhenBeatmapDeleted() + { + loadMultiplayer(); + + 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("set user ready", () => client.ChangeState(MultiplayerUserState.Ready)); + AddStep("delete beatmap", () => beatmaps.Delete(importedSet)); + + AddAssert("user state is idle", () => client.LocalUser?.State == MultiplayerUserState.Idle); } [Test] From f5bc389998283eca1a7e4d0363945f1800840aa7 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 11 May 2021 19:31:32 +0900 Subject: [PATCH 118/205] Fix flaky tests --- .../Visual/Multiplayer/TestSceneMultiplayer.cs | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs index f2331542c6..69fbd56490 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; @@ -14,6 +15,7 @@ using osu.Game.Online.Rooms; using osu.Game.Rulesets; using osu.Game.Rulesets.Osu; using osu.Game.Screens.OnlinePlay.Components; +using osu.Game.Screens.OnlinePlay.Multiplayer; using osu.Game.Screens.OnlinePlay.Multiplayer.Match; using osu.Game.Tests.Resources; using osu.Game.Users; @@ -56,7 +58,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { loadMultiplayer(); - createRoom(new Room + createRoom(() => new Room { Name = { Value = "Test Room" }, Playlist = @@ -80,7 +82,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { loadMultiplayer(); - createRoom(new Room + createRoom(() => new Room { Name = { Value = "Test Room" }, Playlist = @@ -118,7 +120,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { loadMultiplayer(); - createRoom(new Room + createRoom(() => new Room { Name = { Value = "Test Room" }, Playlist = @@ -157,13 +159,16 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("play started", () => !multiplayerScreen.IsCurrentScreen()); } - private void createRoom(Room room) + private void createRoom(Func room) { AddStep("open room", () => { - multiplayerScreen.OpenNewRoom(room); + multiplayerScreen.OpenNewRoom(room()); }); + AddUntilStep("wait for room open", () => this.ChildrenOfType().FirstOrDefault()?.IsLoaded == true); + AddWaitStep("wait for transition", 2); + AddStep("create room", () => { InputManager.MoveMouseTo(this.ChildrenOfType().Single()); From 8e226319e2603b0fc8aae7c645c6709fcef19f85 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 10 May 2021 15:40:33 +0900 Subject: [PATCH 119/205] Remove downwards dependency from `HUDOverlay` to `HealthDisplay` --- osu.Game/Screens/Play/HUD/HealthDisplay.cs | 13 +++++++++++-- osu.Game/Screens/Play/HUDOverlay.cs | 4 +--- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/HealthDisplay.cs b/osu.Game/Screens/Play/HUD/HealthDisplay.cs index 6c2571cc28..e0ddde026b 100644 --- a/osu.Game/Screens/Play/HUD/HealthDisplay.cs +++ b/osu.Game/Screens/Play/HUD/HealthDisplay.cs @@ -3,6 +3,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Scoring; @@ -16,6 +17,8 @@ namespace osu.Game.Screens.Play.HUD /// public abstract class HealthDisplay : Container { + private Bindable showHealthbar; + [Resolved] protected HealthProcessor HealthProcessor { get; private set; } @@ -29,12 +32,18 @@ namespace osu.Game.Screens.Play.HUD { } - [BackgroundDependencyLoader] - private void load() + [BackgroundDependencyLoader(true)] + private void load(HUDOverlay hud) { Current.BindTo(HealthProcessor.Health); HealthProcessor.NewJudgement += onNewJudgement; + + if (hud != null) + { + showHealthbar = hud.ShowHealthbar.GetBoundCopy(); + showHealthbar.BindValueChanged(healthBar => this.FadeTo(healthBar.NewValue ? 1 : 0, HUDOverlay.FADE_DURATION, HUDOverlay.FADE_EASING), true); + } } private void onNewJudgement(JudgementResult judgement) diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index 6ddaa338cc..c9b80be3dc 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -36,7 +36,6 @@ namespace osu.Game.Screens.Play public readonly KeyCounterDisplay KeyCounter; public readonly SkinnableScoreCounter ScoreCounter; public readonly SkinnableAccuracyCounter AccuracyCounter; - public readonly SkinnableHealthDisplay HealthDisplay; public readonly SongProgress Progress; public readonly ModDisplay ModDisplay; public readonly HoldForMenuButton HoldToQuit; @@ -96,7 +95,7 @@ namespace osu.Game.Screens.Play RelativeSizeAxes = Axes.Both, Children = new Drawable[] { - HealthDisplay = CreateHealthDisplay(), + CreateHealthDisplay(), AccuracyCounter = CreateAccuracyCounter(), ScoreCounter = CreateScoreCounter(), CreateComboCounter(), @@ -185,7 +184,6 @@ namespace osu.Game.Screens.Play { base.LoadComplete(); - ShowHealthbar.BindValueChanged(healthBar => HealthDisplay.FadeTo(healthBar.NewValue ? 1 : 0, FADE_DURATION, FADE_EASING), true); ShowHud.BindValueChanged(visible => hideTargets.ForEach(d => d.FadeTo(visible.NewValue ? 1 : 0, FADE_DURATION, FADE_EASING))); IsBreakTime.BindValueChanged(_ => updateVisibility()); From 77e422409cdc9e6b216724f778c2b32617878ebf Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 11 May 2021 17:00:24 +0900 Subject: [PATCH 120/205] Add `SkinInfo.InstantiationInfo` to allow creating different skin types --- .../TestSceneHitCircleArea.cs | 2 +- osu.Game/Beatmaps/WorkingBeatmap.cs | 2 +- ...60743_AddSkinInstantiationInfo.Designer.cs | 508 ++++++++++++++++++ ...20210511060743_AddSkinInstantiationInfo.cs | 22 + .../Migrations/OsuDbContextModelSnapshot.cs | 6 +- osu.Game/Skinning/DefaultLegacySkin.cs | 10 +- osu.Game/Skinning/DefaultSkin.cs | 10 +- osu.Game/Skinning/SkinInfo.cs | 36 +- osu.Game/Skinning/SkinManager.cs | 31 +- 9 files changed, 597 insertions(+), 30 deletions(-) create mode 100644 osu.Game/Migrations/20210511060743_AddSkinInstantiationInfo.Designer.cs create mode 100644 osu.Game/Migrations/20210511060743_AddSkinInstantiationInfo.cs diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleArea.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleArea.cs index 0649989dc0..1fdcd73dde 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleArea.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleArea.cs @@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.Osu.Tests hitCircle.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); - Child = new SkinProvidingContainer(new DefaultSkin()) + Child = new SkinProvidingContainer(new DefaultSkin(null)) { RelativeSizeAxes = Axes.Both, Child = drawableHitCircle = new DrawableHitCircle(hitCircle) diff --git a/osu.Game/Beatmaps/WorkingBeatmap.cs b/osu.Game/Beatmaps/WorkingBeatmap.cs index e0eeaf6db0..3576b149bf 100644 --- a/osu.Game/Beatmaps/WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/WorkingBeatmap.cs @@ -324,7 +324,7 @@ namespace osu.Game.Beatmaps public bool SkinLoaded => skin.IsResultAvailable; public ISkin Skin => skin.Value; - protected virtual ISkin GetSkin() => new DefaultSkin(); + protected virtual ISkin GetSkin() => new DefaultSkin(null); private readonly RecyclableLazy skin; public abstract Stream GetStream(string storagePath); diff --git a/osu.Game/Migrations/20210511060743_AddSkinInstantiationInfo.Designer.cs b/osu.Game/Migrations/20210511060743_AddSkinInstantiationInfo.Designer.cs new file mode 100644 index 0000000000..b808c648da --- /dev/null +++ b/osu.Game/Migrations/20210511060743_AddSkinInstantiationInfo.Designer.cs @@ -0,0 +1,508 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using osu.Game.Database; + +namespace osu.Game.Migrations +{ + [DbContext(typeof(OsuDbContext))] + [Migration("20210511060743_AddSkinInstantiationInfo")] + partial class AddSkinInstantiationInfo + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "2.2.6-servicing-10079"); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapDifficulty", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("ApproachRate"); + + b.Property("CircleSize"); + + b.Property("DrainRate"); + + b.Property("OverallDifficulty"); + + b.Property("SliderMultiplier"); + + b.Property("SliderTickRate"); + + b.HasKey("ID"); + + b.ToTable("BeatmapDifficulty"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("AudioLeadIn"); + + b.Property("BPM"); + + b.Property("BaseDifficultyID"); + + b.Property("BeatDivisor"); + + b.Property("BeatmapSetInfoID"); + + b.Property("Countdown"); + + b.Property("DistanceSpacing"); + + b.Property("EpilepsyWarning"); + + b.Property("GridSize"); + + b.Property("Hash"); + + b.Property("Hidden"); + + b.Property("Length"); + + b.Property("LetterboxInBreaks"); + + b.Property("MD5Hash"); + + b.Property("MetadataID"); + + b.Property("OnlineBeatmapID"); + + b.Property("Path"); + + b.Property("RulesetID"); + + b.Property("SpecialStyle"); + + b.Property("StackLeniency"); + + b.Property("StarDifficulty"); + + b.Property("Status"); + + b.Property("StoredBookmarks"); + + b.Property("TimelineZoom"); + + b.Property("Version"); + + b.Property("WidescreenStoryboard"); + + b.HasKey("ID"); + + b.HasIndex("BaseDifficultyID"); + + b.HasIndex("BeatmapSetInfoID"); + + b.HasIndex("Hash"); + + b.HasIndex("MD5Hash"); + + b.HasIndex("MetadataID"); + + b.HasIndex("OnlineBeatmapID") + .IsUnique(); + + b.HasIndex("RulesetID"); + + b.ToTable("BeatmapInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapMetadata", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Artist"); + + b.Property("ArtistUnicode"); + + b.Property("AudioFile"); + + b.Property("AuthorString") + .HasColumnName("Author"); + + b.Property("BackgroundFile"); + + b.Property("PreviewTime"); + + b.Property("Source"); + + b.Property("Tags"); + + b.Property("Title"); + + b.Property("TitleUnicode"); + + b.HasKey("ID"); + + b.ToTable("BeatmapMetadata"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("BeatmapSetInfoID"); + + b.Property("FileInfoID"); + + b.Property("Filename") + .IsRequired(); + + b.HasKey("ID"); + + b.HasIndex("BeatmapSetInfoID"); + + b.HasIndex("FileInfoID"); + + b.ToTable("BeatmapSetFileInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("DeletePending"); + + b.Property("Hash"); + + b.Property("MetadataID"); + + b.Property("OnlineBeatmapSetID"); + + b.Property("Protected"); + + b.Property("Status"); + + b.HasKey("ID"); + + b.HasIndex("DeletePending"); + + b.HasIndex("Hash") + .IsUnique(); + + b.HasIndex("MetadataID"); + + b.HasIndex("OnlineBeatmapSetID") + .IsUnique(); + + b.ToTable("BeatmapSetInfo"); + }); + + modelBuilder.Entity("osu.Game.Configuration.DatabasedSetting", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Key") + .HasColumnName("Key"); + + b.Property("RulesetID"); + + b.Property("SkinInfoID"); + + b.Property("StringValue") + .HasColumnName("Value"); + + b.Property("Variant"); + + b.HasKey("ID"); + + b.HasIndex("SkinInfoID"); + + b.HasIndex("RulesetID", "Variant"); + + b.ToTable("Settings"); + }); + + modelBuilder.Entity("osu.Game.IO.FileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Hash"); + + b.Property("ReferenceCount"); + + b.HasKey("ID"); + + b.HasIndex("Hash") + .IsUnique(); + + b.HasIndex("ReferenceCount"); + + b.ToTable("FileInfo"); + }); + + modelBuilder.Entity("osu.Game.Input.Bindings.DatabasedKeyBinding", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("IntAction") + .HasColumnName("Action"); + + b.Property("KeysString") + .HasColumnName("Keys"); + + b.Property("RulesetID"); + + b.Property("Variant"); + + b.HasKey("ID"); + + b.HasIndex("IntAction"); + + b.HasIndex("RulesetID", "Variant"); + + b.ToTable("KeyBinding"); + }); + + modelBuilder.Entity("osu.Game.Rulesets.RulesetInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Available"); + + b.Property("InstantiationInfo"); + + b.Property("Name"); + + b.Property("ShortName"); + + b.HasKey("ID"); + + b.HasIndex("Available"); + + b.HasIndex("ShortName") + .IsUnique(); + + b.ToTable("RulesetInfo"); + }); + + modelBuilder.Entity("osu.Game.Scoring.ScoreFileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("FileInfoID"); + + b.Property("Filename") + .IsRequired(); + + b.Property("ScoreInfoID"); + + b.HasKey("ID"); + + b.HasIndex("FileInfoID"); + + b.HasIndex("ScoreInfoID"); + + b.ToTable("ScoreFileInfo"); + }); + + modelBuilder.Entity("osu.Game.Scoring.ScoreInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Accuracy") + .HasColumnType("DECIMAL(1,4)"); + + b.Property("BeatmapInfoID"); + + b.Property("Combo"); + + b.Property("Date"); + + b.Property("DeletePending"); + + b.Property("Hash"); + + b.Property("MaxCombo"); + + b.Property("ModsJson") + .HasColumnName("Mods"); + + b.Property("OnlineScoreID"); + + b.Property("PP"); + + b.Property("Rank"); + + b.Property("RulesetID"); + + b.Property("StatisticsJson") + .HasColumnName("Statistics"); + + b.Property("TotalScore"); + + b.Property("UserID") + .HasColumnName("UserID"); + + b.Property("UserString") + .HasColumnName("User"); + + b.HasKey("ID"); + + b.HasIndex("BeatmapInfoID"); + + b.HasIndex("OnlineScoreID") + .IsUnique(); + + b.HasIndex("RulesetID"); + + b.ToTable("ScoreInfo"); + }); + + modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("FileInfoID"); + + b.Property("Filename") + .IsRequired(); + + b.Property("SkinInfoID"); + + b.HasKey("ID"); + + b.HasIndex("FileInfoID"); + + b.HasIndex("SkinInfoID"); + + b.ToTable("SkinFileInfo"); + }); + + modelBuilder.Entity("osu.Game.Skinning.SkinInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Creator"); + + b.Property("DeletePending"); + + b.Property("Hash"); + + b.Property("InstantiationInfo"); + + b.Property("Name"); + + b.HasKey("ID"); + + b.HasIndex("DeletePending"); + + b.HasIndex("Hash") + .IsUnique(); + + b.ToTable("SkinInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapDifficulty", "BaseDifficulty") + .WithMany() + .HasForeignKey("BaseDifficultyID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo", "BeatmapSet") + .WithMany("Beatmaps") + .HasForeignKey("BeatmapSetInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata") + .WithMany("Beatmaps") + .HasForeignKey("MetadataID"); + + b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset") + .WithMany() + .HasForeignKey("RulesetID") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo") + .WithMany("Files") + .HasForeignKey("BeatmapSetInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.IO.FileInfo", "FileInfo") + .WithMany() + .HasForeignKey("FileInfoID") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata") + .WithMany("BeatmapSets") + .HasForeignKey("MetadataID"); + }); + + modelBuilder.Entity("osu.Game.Configuration.DatabasedSetting", b => + { + b.HasOne("osu.Game.Skinning.SkinInfo") + .WithMany("Settings") + .HasForeignKey("SkinInfoID"); + }); + + modelBuilder.Entity("osu.Game.Scoring.ScoreFileInfo", b => + { + b.HasOne("osu.Game.IO.FileInfo", "FileInfo") + .WithMany() + .HasForeignKey("FileInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Scoring.ScoreInfo") + .WithMany("Files") + .HasForeignKey("ScoreInfoID"); + }); + + modelBuilder.Entity("osu.Game.Scoring.ScoreInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapInfo", "Beatmap") + .WithMany("Scores") + .HasForeignKey("BeatmapInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset") + .WithMany() + .HasForeignKey("RulesetID") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b => + { + b.HasOne("osu.Game.IO.FileInfo", "FileInfo") + .WithMany() + .HasForeignKey("FileInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Skinning.SkinInfo") + .WithMany("Files") + .HasForeignKey("SkinInfoID") + .OnDelete(DeleteBehavior.Cascade); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/osu.Game/Migrations/20210511060743_AddSkinInstantiationInfo.cs b/osu.Game/Migrations/20210511060743_AddSkinInstantiationInfo.cs new file mode 100644 index 0000000000..1d5b0769a4 --- /dev/null +++ b/osu.Game/Migrations/20210511060743_AddSkinInstantiationInfo.cs @@ -0,0 +1,22 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace osu.Game.Migrations +{ + public partial class AddSkinInstantiationInfo : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "InstantiationInfo", + table: "SkinInfo", + nullable: true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "InstantiationInfo", + table: "SkinInfo"); + } + } +} diff --git a/osu.Game/Migrations/OsuDbContextModelSnapshot.cs b/osu.Game/Migrations/OsuDbContextModelSnapshot.cs index ec4461ca56..d4bde50b60 100644 --- a/osu.Game/Migrations/OsuDbContextModelSnapshot.cs +++ b/osu.Game/Migrations/OsuDbContextModelSnapshot.cs @@ -141,8 +141,6 @@ namespace osu.Game.Migrations b.Property("TitleUnicode"); - b.Property("VideoFile"); - b.HasKey("ID"); b.ToTable("BeatmapMetadata"); @@ -352,7 +350,7 @@ namespace osu.Game.Migrations b.Property("TotalScore"); - b.Property("UserID") + b.Property("UserID") .HasColumnName("UserID"); b.Property("UserString") @@ -402,6 +400,8 @@ namespace osu.Game.Migrations b.Property("Hash"); + b.Property("InstantiationInfo"); + b.Property("Name"); b.HasKey("ID"); diff --git a/osu.Game/Skinning/DefaultLegacySkin.cs b/osu.Game/Skinning/DefaultLegacySkin.cs index 4027cc650d..564be8630e 100644 --- a/osu.Game/Skinning/DefaultLegacySkin.cs +++ b/osu.Game/Skinning/DefaultLegacySkin.cs @@ -10,7 +10,12 @@ namespace osu.Game.Skinning public class DefaultLegacySkin : LegacySkin { public DefaultLegacySkin(IResourceStore storage, IStorageResourceProvider resources) - : base(Info, storage, resources, string.Empty) + : this(Info, storage, resources) + { + } + + public DefaultLegacySkin(SkinInfo skin, IResourceStore storage, IStorageResourceProvider resources) + : base(skin, storage, resources, string.Empty) { Configuration.CustomColours["SliderBall"] = new Color4(2, 170, 255, 255); Configuration.AddComboColours( @@ -27,7 +32,8 @@ namespace osu.Game.Skinning { ID = SkinInfo.CLASSIC_SKIN, // this is temporary until database storage is decided upon. Name = "osu!classic", - Creator = "team osu!" + Creator = "team osu!", + InstantiationInfo = typeof(DefaultLegacySkin).AssemblyQualifiedName, }; } } diff --git a/osu.Game/Skinning/DefaultSkin.cs b/osu.Game/Skinning/DefaultSkin.cs index 0b3f5f3cde..fcd874d6ca 100644 --- a/osu.Game/Skinning/DefaultSkin.cs +++ b/osu.Game/Skinning/DefaultSkin.cs @@ -8,14 +8,20 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.OpenGL.Textures; using osu.Framework.Graphics.Textures; using osu.Game.Audio; +using osu.Game.IO; using osuTK.Graphics; namespace osu.Game.Skinning { public class DefaultSkin : Skin { - public DefaultSkin() - : base(SkinInfo.Default) + public DefaultSkin(IStorageResourceProvider resources) + : this(SkinInfo.Default, resources) + { + } + + public DefaultSkin(SkinInfo skin, IStorageResourceProvider resources) + : base(skin) { Configuration = new DefaultSkinConfiguration(); } diff --git a/osu.Game/Skinning/SkinInfo.cs b/osu.Game/Skinning/SkinInfo.cs index aaccbefb3d..2e29808cb4 100644 --- a/osu.Game/Skinning/SkinInfo.cs +++ b/osu.Game/Skinning/SkinInfo.cs @@ -3,8 +3,11 @@ using System; using System.Collections.Generic; +using System.Linq; +using osu.Framework.IO.Stores; using osu.Game.Configuration; using osu.Game.Database; +using osu.Game.IO; namespace osu.Game.Skinning { @@ -22,7 +25,35 @@ namespace osu.Game.Skinning public string Creator { get; set; } - public List Files { get; set; } + private string instantiationInfo; + + public string InstantiationInfo + { + get => instantiationInfo; + set => instantiationInfo = abbreviateInstantiationInfo(value); + } + + private string abbreviateInstantiationInfo(string value) + { + // exclude version onwards, matching only on namespace and type. + // this is mainly to allow for new versions of already loaded rulesets to "upgrade" from old. + return string.Join(',', value.Split(',').Take(2)); + } + + public virtual Skin CreateInstance(IResourceStore legacyDefaultResources, IStorageResourceProvider resources) + { + var type = string.IsNullOrEmpty(InstantiationInfo) + // handle the case of skins imported before InstantiationInfo was added. + ? typeof(LegacySkin) + : Type.GetType(InstantiationInfo); + + if (type == typeof(DefaultLegacySkin)) + return (Skin)Activator.CreateInstance(type, this, legacyDefaultResources, resources); + + return (Skin)Activator.CreateInstance(type, this, resources); + } + + public List Files { get; set; } = new List(); public List Settings { get; set; } @@ -32,7 +63,8 @@ namespace osu.Game.Skinning { ID = DEFAULT_SKIN, Name = "osu!lazer", - Creator = "team osu!" + Creator = "team osu!", + InstantiationInfo = typeof(DefaultSkin).AssemblyQualifiedName, }; public bool Equals(SkinInfo other) => other != null && ID == other.ID; diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs index ac4d63159a..dbb9dcb7fc 100644 --- a/osu.Game/Skinning/SkinManager.cs +++ b/osu.Game/Skinning/SkinManager.cs @@ -36,7 +36,7 @@ namespace osu.Game.Skinning private readonly IResourceStore legacyDefaultResources; - public readonly Bindable CurrentSkin = new Bindable(new DefaultSkin()); + public readonly Bindable CurrentSkin = new Bindable(new DefaultSkin(null)); public readonly Bindable CurrentSkinInfo = new Bindable(SkinInfo.Default) { Default = SkinInfo.Default }; public override IEnumerable HandledExtensions => new[] { ".osk" }; @@ -105,7 +105,7 @@ namespace osu.Game.Skinning { // we need to populate early to create a hash based off skin.ini contents if (item.Name?.Contains(".osk", StringComparison.OrdinalIgnoreCase) == true) - populateMetadata(item); + populateMetadata(item, GetSkin(item)); if (item.Creator != null && item.Creator != unknown_creator_string) { @@ -122,18 +122,20 @@ namespace osu.Game.Skinning { await base.Populate(model, archive, cancellationToken).ConfigureAwait(false); + var instance = GetSkin(model); + + model.InstantiationInfo ??= instance.GetType().AssemblyQualifiedName; + if (model.Name?.Contains(".osk", StringComparison.OrdinalIgnoreCase) == true) - populateMetadata(model); + populateMetadata(model, instance); } - private void populateMetadata(SkinInfo item) + private void populateMetadata(SkinInfo item, Skin instance) { - Skin reference = GetSkin(item); - - if (!string.IsNullOrEmpty(reference.Configuration.SkinInfo.Name)) + if (!string.IsNullOrEmpty(instance.Configuration.SkinInfo.Name)) { - item.Name = reference.Configuration.SkinInfo.Name; - item.Creator = reference.Configuration.SkinInfo.Creator; + item.Name = instance.Configuration.SkinInfo.Name; + item.Creator = instance.Configuration.SkinInfo.Creator; } else { @@ -147,16 +149,7 @@ namespace osu.Game.Skinning /// /// The skin to lookup. /// A instance correlating to the provided . - public Skin GetSkin(SkinInfo skinInfo) - { - if (skinInfo == SkinInfo.Default) - return new DefaultSkin(); - - if (skinInfo == DefaultLegacySkin.Info) - return new DefaultLegacySkin(legacyDefaultResources, this); - - return new LegacySkin(skinInfo, this); - } + public Skin GetSkin(SkinInfo skinInfo) => skinInfo.CreateInstance(legacyDefaultResources, this); /// /// Perform a lookup query on available s. From d706073e014c1849cabf2b252a292188b26f8173 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 11 May 2021 23:08:50 +0200 Subject: [PATCH 121/205] Trim empty remarks xmldoc tag --- osu.Game.Rulesets.Mania/UI/Column.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/osu.Game.Rulesets.Mania/UI/Column.cs b/osu.Game.Rulesets.Mania/UI/Column.cs index fcbbd8a01c..88c3bd5dbf 100644 --- a/osu.Game.Rulesets.Mania/UI/Column.cs +++ b/osu.Game.Rulesets.Mania/UI/Column.cs @@ -31,9 +31,6 @@ namespace osu.Game.Rulesets.Mania.UI /// For hitsounds played by this (i.e. not as a result of hitting a hitobject), /// a certain number of samples are allowed to be played concurrently so that it feels better when spam-pressing the key. /// - /// - /// - /// private const int max_concurrent_hitsounds = OsuGameBase.SAMPLE_CONCURRENCY; /// From 97bd482d4dae632548438ec05315ac37aec8c8fa Mon Sep 17 00:00:00 2001 From: Naxess <30292137+Naxesss@users.noreply.github.com> Date: Wed, 12 May 2021 01:21:38 +0200 Subject: [PATCH 122/205] Factor out `load` from settings into new `Settings` class --- osu.Game/Screens/Edit/Settings.cs | 44 +++++++++++++++++++ .../Edit/Timing/ControlPointSettings.cs | 35 +-------------- osu.Game/Screens/Edit/Verify/IssueSettings.cs | 32 +------------- 3 files changed, 48 insertions(+), 63 deletions(-) create mode 100644 osu.Game/Screens/Edit/Settings.cs diff --git a/osu.Game/Screens/Edit/Settings.cs b/osu.Game/Screens/Edit/Settings.cs new file mode 100644 index 0000000000..758414333d --- /dev/null +++ b/osu.Game/Screens/Edit/Settings.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 osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics.Containers; +using osu.Game.Overlays; + +namespace osu.Game.Screens.Edit +{ + public abstract class Settings : CompositeDrawable + { + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colours) + { + RelativeSizeAxes = Axes.Both; + + InternalChildren = new Drawable[] + { + new Box + { + Colour = colours.Background4, + RelativeSizeAxes = Axes.Both, + }, + new OsuScrollContainer + { + RelativeSizeAxes = Axes.Both, + Child = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Children = CreateSections() + }, + } + }; + } + + protected abstract IReadOnlyList CreateSections(); + } +} diff --git a/osu.Game/Screens/Edit/Timing/ControlPointSettings.cs b/osu.Game/Screens/Edit/Timing/ControlPointSettings.cs index 921fa675b3..36f31d4be4 100644 --- a/osu.Game/Screens/Edit/Timing/ControlPointSettings.cs +++ b/osu.Game/Screens/Edit/Timing/ControlPointSettings.cs @@ -2,44 +2,13 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; -using osu.Framework.Allocation; using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osu.Game.Graphics.Containers; -using osu.Game.Overlays; namespace osu.Game.Screens.Edit.Timing { - public class ControlPointSettings : CompositeDrawable + public class ControlPointSettings : Settings { - [BackgroundDependencyLoader] - private void load(OverlayColourProvider colours) - { - RelativeSizeAxes = Axes.Both; - - InternalChildren = new Drawable[] - { - new Box - { - Colour = colours.Background4, - RelativeSizeAxes = Axes.Both, - }, - new OsuScrollContainer - { - RelativeSizeAxes = Axes.Both, - Child = new FillFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Vertical, - Children = createSections() - }, - } - }; - } - - private IReadOnlyList createSections() => new Drawable[] + protected override IReadOnlyList CreateSections() => new Drawable[] { new GroupSection(), new TimingSection(), diff --git a/osu.Game/Screens/Edit/Verify/IssueSettings.cs b/osu.Game/Screens/Edit/Verify/IssueSettings.cs index 4519231cd2..15fc54d64f 100644 --- a/osu.Game/Screens/Edit/Verify/IssueSettings.cs +++ b/osu.Game/Screens/Edit/Verify/IssueSettings.cs @@ -2,44 +2,16 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; -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.Graphics.Containers; namespace osu.Game.Screens.Edit.Verify { - public class IssueSettings : CompositeDrawable + public class IssueSettings : Settings { - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - RelativeSizeAxes = Axes.Both; - InternalChildren = new Drawable[] - { - new Box - { - Colour = colours.Gray3, - RelativeSizeAxes = Axes.Both, - }, - new OsuScrollContainer - { - RelativeSizeAxes = Axes.Both, - Child = new FillFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Vertical, - Children = createSections() - }, - } - }; } - private IReadOnlyList createSections() => new Drawable[] + protected override IReadOnlyList CreateSections() => new Drawable[] { }; } From d3c1ec55eec7399ea8405d1f39ee92a6f9b2fca6 Mon Sep 17 00:00:00 2001 From: Naxess <30292137+Naxesss@users.noreply.github.com> Date: Wed, 12 May 2021 01:22:32 +0200 Subject: [PATCH 123/205] Take `IssueList` in `IssueSettings` constructor We'll be using this for bindables later. --- osu.Game/Screens/Edit/Verify/IssueSettings.cs | 4 ++++ osu.Game/Screens/Edit/Verify/VerifyScreen.cs | 6 ++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Edit/Verify/IssueSettings.cs b/osu.Game/Screens/Edit/Verify/IssueSettings.cs index 15fc54d64f..be06700b28 100644 --- a/osu.Game/Screens/Edit/Verify/IssueSettings.cs +++ b/osu.Game/Screens/Edit/Verify/IssueSettings.cs @@ -8,7 +8,11 @@ namespace osu.Game.Screens.Edit.Verify { public class IssueSettings : Settings { + private readonly IssueList issueList; + public IssueSettings(IssueList issueList) + { + this.issueList = issueList; } protected override IReadOnlyList CreateSections() => new Drawable[] diff --git a/osu.Game/Screens/Edit/Verify/VerifyScreen.cs b/osu.Game/Screens/Edit/Verify/VerifyScreen.cs index 9de1f04271..9fb81a6681 100644 --- a/osu.Game/Screens/Edit/Verify/VerifyScreen.cs +++ b/osu.Game/Screens/Edit/Verify/VerifyScreen.cs @@ -30,6 +30,8 @@ namespace osu.Game.Screens.Edit.Verify [BackgroundDependencyLoader] private void load() { + IssueList issueList; + Child = new Container { RelativeSizeAxes = Axes.Both, @@ -45,8 +47,8 @@ namespace osu.Game.Screens.Edit.Verify { new Drawable[] { - new IssueList(), - new IssueSettings(), + issueList = new IssueList(), + new IssueSettings(issueList), }, } } From 1de35f880b8b571e7aa89c597c69a8dcd1dd8dde Mon Sep 17 00:00:00 2001 From: Naxess <30292137+Naxesss@users.noreply.github.com> Date: Wed, 12 May 2021 01:23:31 +0200 Subject: [PATCH 124/205] Separate `IssueList` into own class --- osu.Game/Screens/Edit/Verify/IssueList.cs | 100 +++++++++++++++++++ osu.Game/Screens/Edit/Verify/VerifyScreen.cs | 88 ---------------- 2 files changed, 100 insertions(+), 88 deletions(-) create mode 100644 osu.Game/Screens/Edit/Verify/IssueList.cs diff --git a/osu.Game/Screens/Edit/Verify/IssueList.cs b/osu.Game/Screens/Edit/Verify/IssueList.cs new file mode 100644 index 0000000000..6d319eb09e --- /dev/null +++ b/osu.Game/Screens/Edit/Verify/IssueList.cs @@ -0,0 +1,100 @@ +// 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.Framework.Graphics.Shapes; +using osu.Game.Beatmaps; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.UserInterface; +using osu.Game.Overlays; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Edit.Checks.Components; +using osuTK; + +namespace osu.Game.Screens.Edit.Verify +{ + public class IssueList : CompositeDrawable + { + private IssueTable table; + + [Resolved] + private EditorClock clock { get; set; } + + [Resolved] + private IBindable workingBeatmap { get; set; } + + [Resolved] + private EditorBeatmap beatmap { get; set; } + + [Resolved] + private Bindable selectedIssue { get; set; } + + private IBeatmapVerifier rulesetVerifier; + private BeatmapVerifier generalVerifier; + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colours) + { + generalVerifier = new BeatmapVerifier(); + rulesetVerifier = beatmap.BeatmapInfo.Ruleset?.CreateInstance()?.CreateBeatmapVerifier(); + + RelativeSizeAxes = Axes.Both; + + InternalChildren = new Drawable[] + { + new Box + { + Colour = colours.Background2, + RelativeSizeAxes = Axes.Both, + }, + new OsuScrollContainer + { + RelativeSizeAxes = Axes.Both, + Child = table = new IssueTable(), + }, + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight, + Margin = new MarginPadding(20), + Children = new Drawable[] + { + new TriangleButton + { + Text = "Refresh", + Action = refresh, + Size = new Vector2(120, 40), + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight, + }, + } + }, + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + refresh(); + } + + private void refresh() + { + var issues = generalVerifier.Run(beatmap, workingBeatmap.Value); + + if (rulesetVerifier != null) + issues = issues.Concat(rulesetVerifier.Run(beatmap, workingBeatmap.Value)); + + table.Issues = issues + .OrderBy(issue => issue.Template.Type) + .ThenBy(issue => issue.Check.Metadata.Category); + } + } +} diff --git a/osu.Game/Screens/Edit/Verify/VerifyScreen.cs b/osu.Game/Screens/Edit/Verify/VerifyScreen.cs index 9fb81a6681..ed4658da8e 100644 --- a/osu.Game/Screens/Edit/Verify/VerifyScreen.cs +++ b/osu.Game/Screens/Edit/Verify/VerifyScreen.cs @@ -1,19 +1,11 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osu.Game.Beatmaps; -using osu.Game.Graphics.Containers; -using osu.Game.Graphics.UserInterface; -using osu.Game.Overlays; -using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit.Checks.Components; -using osuTK; namespace osu.Game.Screens.Edit.Verify { @@ -54,85 +46,5 @@ namespace osu.Game.Screens.Edit.Verify } }; } - - public class IssueList : CompositeDrawable - { - private IssueTable table; - - [Resolved] - private EditorClock clock { get; set; } - - [Resolved] - private IBindable workingBeatmap { get; set; } - - [Resolved] - private EditorBeatmap beatmap { get; set; } - - [Resolved] - private Bindable selectedIssue { get; set; } - - private IBeatmapVerifier rulesetVerifier; - private BeatmapVerifier generalVerifier; - - [BackgroundDependencyLoader] - private void load(OverlayColourProvider colours) - { - generalVerifier = new BeatmapVerifier(); - rulesetVerifier = beatmap.BeatmapInfo.Ruleset?.CreateInstance()?.CreateBeatmapVerifier(); - - RelativeSizeAxes = Axes.Both; - - InternalChildren = new Drawable[] - { - new Box - { - Colour = colours.Background2, - RelativeSizeAxes = Axes.Both, - }, - new OsuScrollContainer - { - RelativeSizeAxes = Axes.Both, - Child = table = new IssueTable(), - }, - new FillFlowContainer - { - AutoSizeAxes = Axes.Both, - Anchor = Anchor.BottomRight, - Origin = Anchor.BottomRight, - Margin = new MarginPadding(20), - Children = new Drawable[] - { - new TriangleButton - { - Text = "Refresh", - Action = refresh, - Size = new Vector2(120, 40), - Anchor = Anchor.BottomRight, - Origin = Anchor.BottomRight, - }, - } - }, - }; - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - refresh(); - } - - private void refresh() - { - var issues = generalVerifier.Run(beatmap, workingBeatmap.Value); - - if (rulesetVerifier != null) - issues = issues.Concat(rulesetVerifier.Run(beatmap, workingBeatmap.Value)); - - table.Issues = issues - .OrderBy(issue => issue.Template.Type) - .ThenBy(issue => issue.Check.Metadata.Category); - } - } } } From 01b87947579c417395458c2832ba730d331d6e6b Mon Sep 17 00:00:00 2001 From: Naxess <30292137+Naxesss@users.noreply.github.com> Date: Wed, 12 May 2021 01:26:12 +0200 Subject: [PATCH 125/205] Add abstract `Section` class Similar to `Section` in the timing screen, but does not make use of checkboxes, nor specific to control points. So there's a lot of things that differ, hence new class instead of factoring that out. --- osu.Game/Screens/Edit/Verify/Section.cs | 67 +++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 osu.Game/Screens/Edit/Verify/Section.cs diff --git a/osu.Game/Screens/Edit/Verify/Section.cs b/osu.Game/Screens/Edit/Verify/Section.cs new file mode 100644 index 0000000000..f6815a05a1 --- /dev/null +++ b/osu.Game/Screens/Edit/Verify/Section.cs @@ -0,0 +1,67 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Game.Graphics.Sprites; +using osu.Game.Overlays; +using osuTK; + +namespace osu.Game.Screens.Edit.Verify +{ + public abstract class Section : CompositeDrawable + { + private const int header_height = 50; + + protected readonly IssueList IssueList; + + protected FillFlowContainer Flow; + protected abstract string Header { get; } + + protected Section(IssueList issueList) + { + IssueList = issueList; + } + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colours) + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + Masking = true; + + InternalChildren = new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.X, + Height = header_height, + Padding = new MarginPadding { Horizontal = 20 }, + Child = new OsuSpriteText + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Text = Header, + Font = new FontUsage(size: 25, weight: "bold") + } + }, + new Container + { + Y = header_height, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Child = Flow = new FillFlowContainer + { + Padding = new MarginPadding { Horizontal = 20 }, + Spacing = new Vector2(10), + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + } + } + }; + } + } +} From 2e4399f0c1f4bcb5cc66bf755f0e4954ab5c6b66 Mon Sep 17 00:00:00 2001 From: Naxess <30292137+Naxesss@users.noreply.github.com> Date: Wed, 12 May 2021 01:27:21 +0200 Subject: [PATCH 126/205] Add `VisibilitySection` and its bindables in `IssueList` --- osu.Game/Screens/Edit/Verify/IssueList.cs | 10 +++++ osu.Game/Screens/Edit/Verify/IssueSettings.cs | 1 + .../Screens/Edit/Verify/VisibilitySection.cs | 38 +++++++++++++++++++ 3 files changed, 49 insertions(+) create mode 100644 osu.Game/Screens/Edit/Verify/VisibilitySection.cs diff --git a/osu.Game/Screens/Edit/Verify/IssueList.cs b/osu.Game/Screens/Edit/Verify/IssueList.cs index 6d319eb09e..dd1dffcb42 100644 --- a/osu.Game/Screens/Edit/Verify/IssueList.cs +++ b/osu.Game/Screens/Edit/Verify/IssueList.cs @@ -34,12 +34,22 @@ namespace osu.Game.Screens.Edit.Verify [Resolved] private Bindable selectedIssue { get; set; } + public Dictionary> ShowType { get; set; } + private IBeatmapVerifier rulesetVerifier; private BeatmapVerifier generalVerifier; [BackgroundDependencyLoader] private void load(OverlayColourProvider colours) { + // Reflects the user interface. Only types in this dictionary have configurable visibility. + ShowType = new Dictionary> + { + { IssueType.Warning, new Bindable(true) }, + { IssueType.Error, new Bindable(true) }, + { IssueType.Negligible, new Bindable(false) } + }; + generalVerifier = new BeatmapVerifier(); rulesetVerifier = beatmap.BeatmapInfo.Ruleset?.CreateInstance()?.CreateBeatmapVerifier(); diff --git a/osu.Game/Screens/Edit/Verify/IssueSettings.cs b/osu.Game/Screens/Edit/Verify/IssueSettings.cs index be06700b28..0df3ab0dcb 100644 --- a/osu.Game/Screens/Edit/Verify/IssueSettings.cs +++ b/osu.Game/Screens/Edit/Verify/IssueSettings.cs @@ -17,6 +17,7 @@ namespace osu.Game.Screens.Edit.Verify protected override IReadOnlyList CreateSections() => new Drawable[] { + new VisibilitySection(issueList) }; } } diff --git a/osu.Game/Screens/Edit/Verify/VisibilitySection.cs b/osu.Game/Screens/Edit/Verify/VisibilitySection.cs new file mode 100644 index 0000000000..e9fa9b56ed --- /dev/null +++ b/osu.Game/Screens/Edit/Verify/VisibilitySection.cs @@ -0,0 +1,38 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Game.Overlays; +using osu.Game.Overlays.Settings; +using osu.Game.Rulesets.Edit.Checks.Components; + +namespace osu.Game.Screens.Edit.Verify +{ + internal class VisibilitySection : Section + { + public VisibilitySection(IssueList issueList) + : base(issueList) + { + } + + protected override string Header => "Visibility"; + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colours) + { + foreach (IssueType issueType in IssueList.ShowType.Keys) + { + var checkbox = new SettingsCheckbox + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + LabelText = issueType.ToString() + }; + + checkbox.Current.BindTo(IssueList.ShowType[issueType]); + Flow.Add(checkbox); + } + } + } +} From 1bb7d412daf2ad534dcb3a09161f459fb698e535 Mon Sep 17 00:00:00 2001 From: Naxess <30292137+Naxesss@users.noreply.github.com> Date: Wed, 12 May 2021 01:29:46 +0200 Subject: [PATCH 127/205] Add `IssueList` filtering based on those bindables --- osu.Game/Screens/Edit/Verify/IssueList.cs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/osu.Game/Screens/Edit/Verify/IssueList.cs b/osu.Game/Screens/Edit/Verify/IssueList.cs index dd1dffcb42..7a2202c198 100644 --- a/osu.Game/Screens/Edit/Verify/IssueList.cs +++ b/osu.Game/Screens/Edit/Verify/IssueList.cs @@ -102,9 +102,22 @@ namespace osu.Game.Screens.Edit.Verify if (rulesetVerifier != null) issues = issues.Concat(rulesetVerifier.Run(beatmap, workingBeatmap.Value)); + issues = filter(issues); + table.Issues = issues .OrderBy(issue => issue.Template.Type) .ThenBy(issue => issue.Check.Metadata.Category); } + + private IEnumerable filter(IEnumerable issues) + { + foreach (IssueType issueType in ShowType.Keys) + { + if (!ShowType[issueType].Value) + issues = issues.Where(issue => issue.Template.Type != issueType); + } + + return issues; + } } } From ad78aec1ef8380db80d9e57eb495fa3be2aa2c38 Mon Sep 17 00:00:00 2001 From: Naxess <30292137+Naxesss@users.noreply.github.com> Date: Wed, 12 May 2021 01:30:45 +0200 Subject: [PATCH 128/205] Refresh `IssueList` on changes in `VisibilitySection` --- osu.Game/Screens/Edit/Verify/IssueList.cs | 6 +++--- osu.Game/Screens/Edit/Verify/VisibilitySection.cs | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Edit/Verify/IssueList.cs b/osu.Game/Screens/Edit/Verify/IssueList.cs index 7a2202c198..b7a6d769e3 100644 --- a/osu.Game/Screens/Edit/Verify/IssueList.cs +++ b/osu.Game/Screens/Edit/Verify/IssueList.cs @@ -78,7 +78,7 @@ namespace osu.Game.Screens.Edit.Verify new TriangleButton { Text = "Refresh", - Action = refresh, + Action = Refresh, Size = new Vector2(120, 40), Anchor = Anchor.BottomRight, Origin = Anchor.BottomRight, @@ -92,10 +92,10 @@ namespace osu.Game.Screens.Edit.Verify { base.LoadComplete(); - refresh(); + Refresh(); } - private void refresh() + public void Refresh() { var issues = generalVerifier.Run(beatmap, workingBeatmap.Value); diff --git a/osu.Game/Screens/Edit/Verify/VisibilitySection.cs b/osu.Game/Screens/Edit/Verify/VisibilitySection.cs index e9fa9b56ed..f849426800 100644 --- a/osu.Game/Screens/Edit/Verify/VisibilitySection.cs +++ b/osu.Game/Screens/Edit/Verify/VisibilitySection.cs @@ -31,6 +31,7 @@ namespace osu.Game.Screens.Edit.Verify }; checkbox.Current.BindTo(IssueList.ShowType[issueType]); + checkbox.Current.BindValueChanged(_ => IssueList.Refresh()); Flow.Add(checkbox); } } From 75adec57ebfa8aaefcb124d3c802e2e8f69ee0a4 Mon Sep 17 00:00:00 2001 From: Naxess <30292137+Naxesss@users.noreply.github.com> Date: Wed, 12 May 2021 01:31:16 +0200 Subject: [PATCH 129/205] Remove negligible default hidden TODO --- osu.Game/Rulesets/Edit/Checks/Components/IssueType.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Rulesets/Edit/Checks/Components/IssueType.cs b/osu.Game/Rulesets/Edit/Checks/Components/IssueType.cs index 1241e058ad..1f708209fe 100644 --- a/osu.Game/Rulesets/Edit/Checks/Components/IssueType.cs +++ b/osu.Game/Rulesets/Edit/Checks/Components/IssueType.cs @@ -18,7 +18,6 @@ namespace osu.Game.Rulesets.Edit.Checks.Components /// An error occurred and a complete check could not be made. Error, - // TODO: Negligible issues should be hidden by default. /// A possible mistake so minor/unlikely that it can often be safely ignored. Negligible, } From 4aeaec6ecc610e4c043881451ff6495edea6bcbe Mon Sep 17 00:00:00 2001 From: Naxess <30292137+Naxesss@users.noreply.github.com> Date: Wed, 12 May 2021 01:32:18 +0200 Subject: [PATCH 130/205] Add `InterpretationSection` and its bindable in `IssueList` We'll eventually connect that bindable so that checks can access it. --- .../Edit/Verify/InterpretationSection.cs | 34 +++++++++++++++++++ osu.Game/Screens/Edit/Verify/IssueList.cs | 4 +++ osu.Game/Screens/Edit/Verify/IssueSettings.cs | 1 + 3 files changed, 39 insertions(+) create mode 100644 osu.Game/Screens/Edit/Verify/InterpretationSection.cs diff --git a/osu.Game/Screens/Edit/Verify/InterpretationSection.cs b/osu.Game/Screens/Edit/Verify/InterpretationSection.cs new file mode 100644 index 0000000000..fa0bde9ddc --- /dev/null +++ b/osu.Game/Screens/Edit/Verify/InterpretationSection.cs @@ -0,0 +1,34 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Game.Beatmaps; +using osu.Game.Overlays.Settings; + +namespace osu.Game.Screens.Edit.Verify +{ + internal class InterpretationSection : Section + { + public InterpretationSection(IssueList issueList) + : base(issueList) + { + } + + protected override string Header => "Interpretation"; + + [BackgroundDependencyLoader] + private void load() + { + var dropdown = new SettingsEnumDropdown + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + TooltipText = "Affects checks that depend on difficulty level" + }; + dropdown.Current.BindTo(IssueList.InterpretedDifficulty); + + Flow.Add(dropdown); + } + } +} diff --git a/osu.Game/Screens/Edit/Verify/IssueList.cs b/osu.Game/Screens/Edit/Verify/IssueList.cs index b7a6d769e3..3e836c4010 100644 --- a/osu.Game/Screens/Edit/Verify/IssueList.cs +++ b/osu.Game/Screens/Edit/Verify/IssueList.cs @@ -36,6 +36,8 @@ namespace osu.Game.Screens.Edit.Verify public Dictionary> ShowType { get; set; } + public Bindable InterpretedDifficulty { get; set; } + private IBeatmapVerifier rulesetVerifier; private BeatmapVerifier generalVerifier; @@ -53,6 +55,8 @@ namespace osu.Game.Screens.Edit.Verify generalVerifier = new BeatmapVerifier(); rulesetVerifier = beatmap.BeatmapInfo.Ruleset?.CreateInstance()?.CreateBeatmapVerifier(); + InterpretedDifficulty = new Bindable(beatmap.BeatmapInfo.DifficultyRating); + RelativeSizeAxes = Axes.Both; InternalChildren = new Drawable[] diff --git a/osu.Game/Screens/Edit/Verify/IssueSettings.cs b/osu.Game/Screens/Edit/Verify/IssueSettings.cs index 0df3ab0dcb..68ab51c3c8 100644 --- a/osu.Game/Screens/Edit/Verify/IssueSettings.cs +++ b/osu.Game/Screens/Edit/Verify/IssueSettings.cs @@ -17,6 +17,7 @@ namespace osu.Game.Screens.Edit.Verify protected override IReadOnlyList CreateSections() => new Drawable[] { + new InterpretationSection(issueList), new VisibilitySection(issueList) }; } From 9b09361cc9a5b3ef1fb9cecf34e28f6401ae9def Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 12 May 2021 12:16:41 +0900 Subject: [PATCH 131/205] Add testable spectator streaming client --- .../Spectator/TestSpectatorStreamingClient.cs | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 osu.Game/Tests/Visual/Spectator/TestSpectatorStreamingClient.cs diff --git a/osu.Game/Tests/Visual/Spectator/TestSpectatorStreamingClient.cs b/osu.Game/Tests/Visual/Spectator/TestSpectatorStreamingClient.cs new file mode 100644 index 0000000000..c177d3f399 --- /dev/null +++ b/osu.Game/Tests/Visual/Spectator/TestSpectatorStreamingClient.cs @@ -0,0 +1,84 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using osu.Framework.Bindables; +using osu.Framework.Utils; +using osu.Game.Online; +using osu.Game.Online.Spectator; +using osu.Game.Replays.Legacy; +using osu.Game.Scoring; + +namespace osu.Game.Tests.Visual.Spectator +{ + public class TestSpectatorStreamingClient : SpectatorStreamingClient + { + public new BindableList PlayingUsers => (BindableList)base.PlayingUsers; + private readonly HashSet watchingUsers = new HashSet(); + + private readonly Dictionary userBeatmapDictionary = new Dictionary(); + private readonly Dictionary userSentStateDictionary = new Dictionary(); + + public TestSpectatorStreamingClient() + : base(new DevelopmentEndpointConfiguration()) + { + } + + public void StartPlay(int userId, int beatmapId) + { + userBeatmapDictionary[userId] = beatmapId; + sendState(userId, beatmapId); + } + + public void EndPlay(int userId, int beatmapId) + { + ((ISpectatorClient)this).UserFinishedPlaying(userId, new SpectatorState + { + BeatmapID = beatmapId, + RulesetID = 0, + }); + + userSentStateDictionary[userId] = false; + } + + public void SendFrames(int userId, int index, int count) + { + var frames = new List(); + + for (int i = index; i < index + count; i++) + { + var buttonState = i == index + count - 1 ? ReplayButtonState.None : ReplayButtonState.Left1; + + frames.Add(new LegacyReplayFrame(i * 100, RNG.Next(0, 512), RNG.Next(0, 512), buttonState)); + } + + var bundle = new FrameDataBundle(new ScoreInfo(), frames); + ((ISpectatorClient)this).UserSentFrames(userId, bundle); + + if (!userSentStateDictionary[userId]) + sendState(userId, userBeatmapDictionary[userId]); + } + + public override void WatchUser(int userId) + { + // When newly watching a user, the server sends the playing state immediately. + if (!watchingUsers.Contains(userId) && PlayingUsers.Contains(userId)) + sendState(userId, userBeatmapDictionary[userId]); + + base.WatchUser(userId); + + watchingUsers.Add(userId); + } + + private void sendState(int userId, int beatmapId) + { + ((ISpectatorClient)this).UserBeganPlaying(userId, new SpectatorState + { + BeatmapID = beatmapId, + RulesetID = 0, + }); + + userSentStateDictionary[userId] = true; + } + } +} From 184dbaf2029a4b123213b068c0c19484770e57d7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 12 May 2021 12:53:30 +0900 Subject: [PATCH 132/205] Improve safety of bindings in `HealthDisplay` --- osu.Game/Screens/Play/HUD/HealthDisplay.cs | 23 ++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/HealthDisplay.cs b/osu.Game/Screens/Play/HUD/HealthDisplay.cs index e0ddde026b..d6b4934731 100644 --- a/osu.Game/Screens/Play/HUD/HealthDisplay.cs +++ b/osu.Game/Screens/Play/HUD/HealthDisplay.cs @@ -17,7 +17,7 @@ namespace osu.Game.Screens.Play.HUD /// public abstract class HealthDisplay : Container { - private Bindable showHealthbar; + private readonly Bindable showHealthbar = new Bindable(true); [Resolved] protected HealthProcessor HealthProcessor { get; private set; } @@ -32,18 +32,21 @@ namespace osu.Game.Screens.Play.HUD { } - [BackgroundDependencyLoader(true)] - private void load(HUDOverlay hud) - { - Current.BindTo(HealthProcessor.Health); + [Resolved(canBeNull: true)] + private HUDOverlay hudOverlay { get; set; } + protected override void LoadComplete() + { + base.LoadComplete(); + + Current.BindTo(HealthProcessor.Health); HealthProcessor.NewJudgement += onNewJudgement; - if (hud != null) - { - showHealthbar = hud.ShowHealthbar.GetBoundCopy(); - showHealthbar.BindValueChanged(healthBar => this.FadeTo(healthBar.NewValue ? 1 : 0, HUDOverlay.FADE_DURATION, HUDOverlay.FADE_EASING), true); - } + if (hudOverlay != null) + showHealthbar.BindTo(hudOverlay.ShowHealthbar); + + // this probably shouldn't be operating on `this.` + showHealthbar.BindValueChanged(healthBar => this.FadeTo(healthBar.NewValue ? 1 : 0, HUDOverlay.FADE_DURATION, HUDOverlay.FADE_EASING), true); } private void onNewJudgement(JudgementResult judgement) From bf44c09a9142d567878a27e2a30fc1b311201023 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 12 May 2021 12:59:46 +0900 Subject: [PATCH 133/205] Add name identifying container and rename index variable --- osu.Game.Rulesets.Mania/UI/Column.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Mania/UI/Column.cs b/osu.Game.Rulesets.Mania/UI/Column.cs index 88c3bd5dbf..827d8e2a0a 100644 --- a/osu.Game.Rulesets.Mania/UI/Column.cs +++ b/osu.Game.Rulesets.Mania/UI/Column.cs @@ -73,6 +73,7 @@ namespace osu.Game.Rulesets.Mania.UI background, new Container { + Name = "Column samples pool", RelativeSizeAxes = Axes.Both, ChildrenEnumerable = hitSounds = Enumerable.Range(0, max_concurrent_hitsounds).Select(_ => new SkinnableSound()).ToArray() }, @@ -132,7 +133,7 @@ namespace osu.Game.Rulesets.Mania.UI HitObjectArea.Explosions.Add(hitExplosionPool.Get(e => e.Apply(result))); } - private int nextHitSound; + private int nextHitSoundIndex; public bool OnPressed(ManiaAction action) { @@ -147,12 +148,12 @@ namespace osu.Game.Rulesets.Mania.UI if (nextObject is DrawableManiaHitObject maniaObject) { - var hitSound = hitSounds[nextHitSound]; + var hitSound = hitSounds[nextHitSoundIndex]; hitSound.Samples = maniaObject.GetGameplaySamples(); hitSound.Play(); - nextHitSound = (nextHitSound + 1) % max_concurrent_hitsounds; + nextHitSoundIndex = (nextHitSoundIndex + 1) % max_concurrent_hitsounds; } return true; From 3428056113d4c8a2cdfd391af9c9e81d32d42241 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 12 May 2021 13:00:02 +0900 Subject: [PATCH 134/205] Remove unnecessary usage of `ChildrenEnumerable` for array assignment --- osu.Game.Rulesets.Mania/UI/Column.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Mania/UI/Column.cs b/osu.Game.Rulesets.Mania/UI/Column.cs index 827d8e2a0a..79a41ae19e 100644 --- a/osu.Game.Rulesets.Mania/UI/Column.cs +++ b/osu.Game.Rulesets.Mania/UI/Column.cs @@ -75,7 +75,7 @@ namespace osu.Game.Rulesets.Mania.UI { Name = "Column samples pool", RelativeSizeAxes = Axes.Both, - ChildrenEnumerable = hitSounds = Enumerable.Range(0, max_concurrent_hitsounds).Select(_ => new SkinnableSound()).ToArray() + Children = hitSounds = Enumerable.Range(0, max_concurrent_hitsounds).Select(_ => new SkinnableSound()).ToArray() }, TopLevelContainer = new Container { RelativeSizeAxes = Axes.Both } }; From 21fc0ba43baea6e0cb115952ab21df06f1d1c003 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 12 May 2021 12:17:42 +0900 Subject: [PATCH 135/205] Combine test spectator streaming client implementations --- .../Visual/Gameplay/TestSceneSpectator.cs | 89 ++----------------- .../TestSceneMultiSpectatorLeaderboard.cs | 70 +-------------- .../TestSceneMultiSpectatorScreen.cs | 75 +--------------- .../TestSceneCurrentlyPlayingDisplay.cs | 4 +- 4 files changed, 11 insertions(+), 227 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs index 0777571d02..a7ed217b4d 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs @@ -1,35 +1,32 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using NUnit.Framework; using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Screens; using osu.Framework.Testing; -using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Database; -using osu.Game.Online; using osu.Game.Online.Spectator; -using osu.Game.Replays.Legacy; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Replays; using osu.Game.Rulesets.UI; -using osu.Game.Scoring; using osu.Game.Screens.Play; using osu.Game.Tests.Beatmaps.IO; using osu.Game.Tests.Visual.Multiplayer; +using osu.Game.Tests.Visual.Spectator; using osu.Game.Users; namespace osu.Game.Tests.Visual.Gameplay { public class TestSceneSpectator : ScreenTestScene { + private readonly User streamingUser = new User { Id = MultiplayerTestScene.PLAYER_1_ID, Username = "Test user" }; + [Cached(typeof(SpectatorStreamingClient))] private TestSpectatorStreamingClient testSpectatorStreamingClient = new TestSpectatorStreamingClient(); @@ -215,9 +212,9 @@ namespace osu.Game.Tests.Visual.Gameplay private void waitForPlayer() => AddUntilStep("wait for player", () => Stack.CurrentScreen is Player); - private void start(int? beatmapId = null) => AddStep("start play", () => testSpectatorStreamingClient.StartPlay(beatmapId ?? importedBeatmapId)); + private void start(int? beatmapId = null) => AddStep("start play", () => testSpectatorStreamingClient.StartPlay(streamingUser.Id, beatmapId ?? importedBeatmapId)); - private void finish(int? beatmapId = null) => AddStep("end play", () => testSpectatorStreamingClient.EndPlay(beatmapId ?? importedBeatmapId)); + private void finish(int? beatmapId = null) => AddStep("end play", () => testSpectatorStreamingClient.EndPlay(streamingUser.Id, beatmapId ?? importedBeatmapId)); private void checkPaused(bool state) => AddUntilStep($"game is {(state ? "paused" : "playing")}", () => player.ChildrenOfType().First().IsPaused.Value == state); @@ -226,89 +223,17 @@ namespace osu.Game.Tests.Visual.Gameplay { AddStep("send frames", () => { - testSpectatorStreamingClient.SendFrames(nextFrame, count); + testSpectatorStreamingClient.SendFrames(streamingUser.Id, nextFrame, count); nextFrame += count; }); } private void loadSpectatingScreen() { - AddStep("load screen", () => LoadScreen(spectatorScreen = new SoloSpectator(testSpectatorStreamingClient.StreamingUser))); + AddStep("load screen", () => LoadScreen(spectatorScreen = new SoloSpectator(streamingUser))); AddUntilStep("wait for screen load", () => spectatorScreen.LoadState == LoadState.Loaded); } - public class TestSpectatorStreamingClient : SpectatorStreamingClient - { - public readonly User StreamingUser = new User { Id = MultiplayerTestScene.PLAYER_1_ID, Username = "Test user" }; - - public new BindableList PlayingUsers => (BindableList)base.PlayingUsers; - - private int beatmapId; - - public TestSpectatorStreamingClient() - : base(new DevelopmentEndpointConfiguration()) - { - } - - public void StartPlay(int beatmapId) - { - this.beatmapId = beatmapId; - sendState(beatmapId); - } - - public void EndPlay(int beatmapId) - { - ((ISpectatorClient)this).UserFinishedPlaying(StreamingUser.Id, new SpectatorState - { - BeatmapID = beatmapId, - RulesetID = 0, - }); - - sentState = false; - } - - private bool sentState; - - public void SendFrames(int index, int count) - { - var frames = new List(); - - for (int i = index; i < index + count; i++) - { - var buttonState = i == index + count - 1 ? ReplayButtonState.None : ReplayButtonState.Left1; - - frames.Add(new LegacyReplayFrame(i * 100, RNG.Next(0, 512), RNG.Next(0, 512), buttonState)); - } - - var bundle = new FrameDataBundle(new ScoreInfo(), frames); - ((ISpectatorClient)this).UserSentFrames(StreamingUser.Id, bundle); - - if (!sentState) - sendState(beatmapId); - } - - public override void WatchUser(int userId) - { - if (!PlayingUsers.Contains(userId) && sentState) - { - // usually the server would do this. - sendState(beatmapId); - } - - base.WatchUser(userId); - } - - private void sendState(int beatmapId) - { - sentState = true; - ((ISpectatorClient)this).UserBeganPlaying(StreamingUser.Id, new SpectatorState - { - BeatmapID = beatmapId, - RulesetID = 0, - }); - } - } - internal class TestUserLookupCache : UserLookupCache { protected override Task ComputeValueAsync(int lookup, CancellationToken token = default) => Task.FromResult(new User diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorLeaderboard.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorLeaderboard.cs index d3a5daeac6..263adc07e1 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorLeaderboard.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorLeaderboard.cs @@ -11,15 +11,12 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Testing; using osu.Framework.Timing; -using osu.Framework.Utils; using osu.Game.Database; -using osu.Game.Online; using osu.Game.Online.Spectator; -using osu.Game.Replays.Legacy; using osu.Game.Rulesets.Osu.Scoring; -using osu.Game.Scoring; using osu.Game.Screens.OnlinePlay.Multiplayer.Spectate; using osu.Game.Screens.Play.HUD; +using osu.Game.Tests.Visual.Spectator; using osu.Game.Users; namespace osu.Game.Tests.Visual.Multiplayer @@ -149,71 +146,6 @@ namespace osu.Game.Tests.Visual.Multiplayer private void assertCombo(int userId, int expectedCombo) => AddUntilStep($"player {userId} has {expectedCombo} combo", () => this.ChildrenOfType().Single(s => s.User?.Id == userId).Combo.Value == expectedCombo); - private class TestSpectatorStreamingClient : SpectatorStreamingClient - { - private readonly Dictionary userBeatmapDictionary = new Dictionary(); - private readonly Dictionary userSentStateDictionary = new Dictionary(); - - public TestSpectatorStreamingClient() - : base(new DevelopmentEndpointConfiguration()) - { - } - - public void StartPlay(int userId, int beatmapId) - { - userBeatmapDictionary[userId] = beatmapId; - userSentStateDictionary[userId] = false; - sendState(userId, beatmapId); - } - - public void EndPlay(int userId, int beatmapId) - { - ((ISpectatorClient)this).UserFinishedPlaying(userId, new SpectatorState - { - BeatmapID = beatmapId, - RulesetID = 0, - }); - userSentStateDictionary[userId] = false; - } - - public void SendFrames(int userId, int index, int count) - { - var frames = new List(); - - for (int i = index; i < index + count; i++) - { - var buttonState = i == index + count - 1 ? ReplayButtonState.None : ReplayButtonState.Left1; - frames.Add(new LegacyReplayFrame(i * 100, RNG.Next(0, 512), RNG.Next(0, 512), buttonState)); - } - - var bundle = new FrameDataBundle(new ScoreInfo { Combo = index + count }, frames); - ((ISpectatorClient)this).UserSentFrames(userId, bundle); - if (!userSentStateDictionary[userId]) - sendState(userId, userBeatmapDictionary[userId]); - } - - public override void WatchUser(int userId) - { - if (userSentStateDictionary[userId]) - { - // usually the server would do this. - sendState(userId, userBeatmapDictionary[userId]); - } - - base.WatchUser(userId); - } - - private void sendState(int userId, int beatmapId) - { - ((ISpectatorClient)this).UserBeganPlaying(userId, new SpectatorState - { - BeatmapID = beatmapId, - RulesetID = 0, - }); - userSentStateDictionary[userId] = true; - } - } - private class TestUserLookupCache : UserLookupCache { protected override Task ComputeValueAsync(int lookup, CancellationToken token = default) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs index db431c0a82..689c249d05 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs @@ -9,16 +9,13 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Testing; -using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Database; -using osu.Game.Online; using osu.Game.Online.Spectator; -using osu.Game.Replays.Legacy; -using osu.Game.Scoring; using osu.Game.Screens.OnlinePlay.Multiplayer.Spectate; using osu.Game.Screens.Play; using osu.Game.Tests.Beatmaps.IO; +using osu.Game.Tests.Visual.Spectator; using osu.Game.Users; namespace osu.Game.Tests.Visual.Multiplayer @@ -301,76 +298,6 @@ namespace osu.Game.Tests.Visual.Multiplayer private PlayerArea getInstance(int userId) => spectatorScreen.ChildrenOfType().Single(p => p.UserId == userId); - public class TestSpectatorStreamingClient : SpectatorStreamingClient - { - private readonly Dictionary userBeatmapDictionary = new Dictionary(); - private readonly Dictionary userSentStateDictionary = new Dictionary(); - - public TestSpectatorStreamingClient() - : base(new DevelopmentEndpointConfiguration()) - { - } - - public void StartPlay(int userId, int beatmapId) - { - userBeatmapDictionary[userId] = beatmapId; - userSentStateDictionary[userId] = false; - - sendState(userId, beatmapId); - } - - public void EndPlay(int userId, int beatmapId) - { - ((ISpectatorClient)this).UserFinishedPlaying(userId, new SpectatorState - { - BeatmapID = beatmapId, - RulesetID = 0, - }); - - userSentStateDictionary[userId] = false; - } - - public void SendFrames(int userId, int index, int count) - { - var frames = new List(); - - for (int i = index; i < index + count; i++) - { - var buttonState = i == index + count - 1 ? ReplayButtonState.None : ReplayButtonState.Left1; - - frames.Add(new LegacyReplayFrame(i * 100, RNG.Next(0, 512), RNG.Next(0, 512), buttonState)); - } - - var bundle = new FrameDataBundle(new ScoreInfo { Combo = index + count }, frames); - ((ISpectatorClient)this).UserSentFrames(userId, bundle); - - if (!userSentStateDictionary[userId]) - sendState(userId, userBeatmapDictionary[userId]); - } - - public override void WatchUser(int userId) - { - if (!PlayingUsers.Contains(userId) && userSentStateDictionary.TryGetValue(userId, out var sent) && sent) - { - // usually the server would do this. - sendState(userId, userBeatmapDictionary[userId]); - } - - base.WatchUser(userId); - } - - private void sendState(int userId, int beatmapId) - { - ((ISpectatorClient)this).UserBeganPlaying(userId, new SpectatorState - { - BeatmapID = beatmapId, - RulesetID = 0, - }); - - userSentStateDictionary[userId] = true; - } - } - internal class TestUserLookupCache : UserLookupCache { protected override Task ComputeValueAsync(int lookup, CancellationToken token = default) diff --git a/osu.Game.Tests/Visual/Online/TestSceneCurrentlyPlayingDisplay.cs b/osu.Game.Tests/Visual/Online/TestSceneCurrentlyPlayingDisplay.cs index 1baa07f208..8ae6398003 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneCurrentlyPlayingDisplay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneCurrentlyPlayingDisplay.cs @@ -12,7 +12,7 @@ using osu.Framework.Testing; using osu.Game.Database; using osu.Game.Online.Spectator; using osu.Game.Overlays.Dashboard; -using osu.Game.Tests.Visual.Gameplay; +using osu.Game.Tests.Visual.Spectator; using osu.Game.Users; namespace osu.Game.Tests.Visual.Online @@ -20,7 +20,7 @@ namespace osu.Game.Tests.Visual.Online public class TestSceneCurrentlyPlayingDisplay : OsuTestScene { [Cached(typeof(SpectatorStreamingClient))] - private TestSceneSpectator.TestSpectatorStreamingClient testSpectatorStreamingClient = new TestSceneSpectator.TestSpectatorStreamingClient(); + private TestSpectatorStreamingClient testSpectatorStreamingClient = new TestSpectatorStreamingClient(); private CurrentlyPlayingDisplay currentlyPlaying; From ad11818868d50e704d98ec28ec12f7ee2a2df9b8 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 12 May 2021 12:19:36 +0900 Subject: [PATCH 136/205] Remove watched users on stop watching --- osu.Game/Online/Spectator/SpectatorStreamingClient.cs | 2 +- .../Visual/Spectator/TestSpectatorStreamingClient.cs | 10 ++++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs index 378096c7fb..691ad5624a 100644 --- a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs +++ b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs @@ -230,7 +230,7 @@ namespace osu.Game.Online.Spectator connection.SendAsync(nameof(ISpectatorServer.StartWatchingUser), userId); } - public void StopWatchingUser(int userId) + public virtual void StopWatchingUser(int userId) { lock (userLock) { diff --git a/osu.Game/Tests/Visual/Spectator/TestSpectatorStreamingClient.cs b/osu.Game/Tests/Visual/Spectator/TestSpectatorStreamingClient.cs index c177d3f399..806d9b45ab 100644 --- a/osu.Game/Tests/Visual/Spectator/TestSpectatorStreamingClient.cs +++ b/osu.Game/Tests/Visual/Spectator/TestSpectatorStreamingClient.cs @@ -38,7 +38,8 @@ namespace osu.Game.Tests.Visual.Spectator RulesetID = 0, }); - userSentStateDictionary[userId] = false; + userBeatmapDictionary.Remove(userId); + userSentStateDictionary.Remove(userId); } public void SendFrames(int userId, int index, int count) @@ -66,10 +67,15 @@ namespace osu.Game.Tests.Visual.Spectator sendState(userId, userBeatmapDictionary[userId]); base.WatchUser(userId); - watchingUsers.Add(userId); } + public override void StopWatchingUser(int userId) + { + base.StopWatchingUser(userId); + watchingUsers.Remove(userId); + } + private void sendState(int userId, int beatmapId) { ((ISpectatorClient)this).UserBeganPlaying(userId, new SpectatorState From e0e8f5ab80700db4113b5df45a7d91046cbe57ad Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 12 May 2021 13:06:28 +0900 Subject: [PATCH 137/205] Fix ordering + threading issues --- .../Spectator/TestSpectatorStreamingClient.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/osu.Game/Tests/Visual/Spectator/TestSpectatorStreamingClient.cs b/osu.Game/Tests/Visual/Spectator/TestSpectatorStreamingClient.cs index 806d9b45ab..c2428ce415 100644 --- a/osu.Game/Tests/Visual/Spectator/TestSpectatorStreamingClient.cs +++ b/osu.Game/Tests/Visual/Spectator/TestSpectatorStreamingClient.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Collections.Concurrent; using System.Collections.Generic; using osu.Framework.Bindables; using osu.Framework.Utils; @@ -14,7 +15,7 @@ namespace osu.Game.Tests.Visual.Spectator public class TestSpectatorStreamingClient : SpectatorStreamingClient { public new BindableList PlayingUsers => (BindableList)base.PlayingUsers; - private readonly HashSet watchingUsers = new HashSet(); + private readonly ConcurrentDictionary watchingUsers = new ConcurrentDictionary(); private readonly Dictionary userBeatmapDictionary = new Dictionary(); private readonly Dictionary userSentStateDictionary = new Dictionary(); @@ -62,18 +63,17 @@ namespace osu.Game.Tests.Visual.Spectator public override void WatchUser(int userId) { - // When newly watching a user, the server sends the playing state immediately. - if (!watchingUsers.Contains(userId) && PlayingUsers.Contains(userId)) - sendState(userId, userBeatmapDictionary[userId]); - base.WatchUser(userId); - watchingUsers.Add(userId); + + // When newly watching a user, the server sends the playing state immediately. + if (watchingUsers.TryAdd(userId, 0) && PlayingUsers.Contains(userId)) + sendState(userId, userBeatmapDictionary[userId]); } public override void StopWatchingUser(int userId) { base.StopWatchingUser(userId); - watchingUsers.Remove(userId); + watchingUsers.TryRemove(userId, out _); } private void sendState(int userId, int beatmapId) From f4c96b26750a9c1556a7c62b5f60e95b1a95e476 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 12 May 2021 13:10:59 +0900 Subject: [PATCH 138/205] Only update playing user states when users are watched --- osu.Game/Online/Spectator/SpectatorStreamingClient.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs index 691ad5624a..ec6d1bf9d8 100644 --- a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs +++ b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs @@ -142,7 +142,11 @@ namespace osu.Game.Online.Spectator if (!playingUsers.Contains(userId)) playingUsers.Add(userId); - playingUserStates[userId] = state; + // UserBeganPlaying() is called by the server regardless of whether the local user is watching the remote user, and is called a further time when the remote user is watched. + // This may be a temporary thing (see: https://github.com/ppy/osu-server-spectator/blob/2273778e02cfdb4a9c6a934f2a46a8459cb5d29c/osu.Server.Spectator/Hubs/SpectatorHub.cs#L28-L29). + // We don't want the user states to update unless the player is being watched, otherwise calling BindUserBeganPlaying() can lead to double invocations. + if (watchingUsers.Contains(userId)) + playingUserStates[userId] = state; } OnUserBeganPlaying?.Invoke(userId, state); From 672108edcf841b30d3f40a8bdb58695b206272c9 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 12 May 2021 13:27:12 +0900 Subject: [PATCH 139/205] Use container instead of array for field --- osu.Game.Rulesets.Mania/UI/Column.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Mania/UI/Column.cs b/osu.Game.Rulesets.Mania/UI/Column.cs index 79a41ae19e..0f02e2cd4b 100644 --- a/osu.Game.Rulesets.Mania/UI/Column.cs +++ b/osu.Game.Rulesets.Mania/UI/Column.cs @@ -44,7 +44,7 @@ namespace osu.Game.Rulesets.Mania.UI internal readonly Container TopLevelContainer; private readonly DrawablePool hitExplosionPool; private readonly OrderedHitPolicy hitPolicy; - private readonly SkinnableSound[] hitSounds; + private readonly Container hitSounds; public Container UnderlayElements => HitObjectArea.UnderlayElements; @@ -71,11 +71,11 @@ namespace osu.Game.Rulesets.Mania.UI RelativeSizeAxes = Axes.Both }, background, - new Container + hitSounds = new Container { Name = "Column samples pool", RelativeSizeAxes = Axes.Both, - Children = hitSounds = Enumerable.Range(0, max_concurrent_hitsounds).Select(_ => new SkinnableSound()).ToArray() + Children = Enumerable.Range(0, max_concurrent_hitsounds).Select(_ => new SkinnableSound()).ToArray() }, TopLevelContainer = new Container { RelativeSizeAxes = Axes.Both } }; From 05c21fb5b39ba1f0b52db2df3f06aacee25f4e99 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 12 May 2021 13:27:30 +0900 Subject: [PATCH 140/205] Increase mania HP lenience --- osu.Game.Rulesets.Mania/ManiaRuleset.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index b3889bc7d3..fbb9b3c466 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -47,7 +47,7 @@ namespace osu.Game.Rulesets.Mania public override ScoreProcessor CreateScoreProcessor() => new ManiaScoreProcessor(); - public override HealthProcessor CreateHealthProcessor(double drainStartTime) => new DrainingHealthProcessor(drainStartTime, 0.2); + public override HealthProcessor CreateHealthProcessor(double drainStartTime) => new DrainingHealthProcessor(drainStartTime, 0.5); public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new ManiaBeatmapConverter(beatmap, this); From 96d4011de2927520121581413837f34e78657abe Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 12 May 2021 14:02:20 +0900 Subject: [PATCH 141/205] Use pattern matching to tidy up instance construction --- osu.Game/Skinning/Editor/SkinEditor.cs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/osu.Game/Skinning/Editor/SkinEditor.cs b/osu.Game/Skinning/Editor/SkinEditor.cs index 08fc458b94..b32d117a92 100644 --- a/osu.Game/Skinning/Editor/SkinEditor.cs +++ b/osu.Game/Skinning/Editor/SkinEditor.cs @@ -166,15 +166,13 @@ namespace osu.Game.Skinning.Editor private void placeComponent(Type type) { - var instance = (Drawable)Activator.CreateInstance(type) as ISkinnableComponent; - - if (instance == null) + if (!(Activator.CreateInstance(type) is ISkinnableComponent component)) throw new InvalidOperationException("Attempted to instantiate a component for placement which was not an {typeof(ISkinnableComponent)}."); - getTarget(SkinnableTarget.MainHUDComponents)?.Add(instance); + getTarget(SkinnableTarget.MainHUDComponents)?.Add(component); SelectedComponents.Clear(); - SelectedComponents.Add(instance); + SelectedComponents.Add(component); } private ISkinnableTarget getTarget(SkinnableTarget target) From 42e67952517e43eef66f688099bd5490efe6ea36 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 12 May 2021 14:11:40 +0900 Subject: [PATCH 142/205] Place new skin components at the centre of the screen by default --- osu.Game/Skinning/Editor/SkinEditor.cs | 14 +++++++++++++- osu.Game/Skinning/ISkinnableTarget.cs | 4 +++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/osu.Game/Skinning/Editor/SkinEditor.cs b/osu.Game/Skinning/Editor/SkinEditor.cs index b32d117a92..4e38caa806 100644 --- a/osu.Game/Skinning/Editor/SkinEditor.cs +++ b/osu.Game/Skinning/Editor/SkinEditor.cs @@ -166,10 +166,22 @@ namespace osu.Game.Skinning.Editor private void placeComponent(Type type) { + var targetContainer = getTarget(SkinnableTarget.MainHUDComponents); + + if (targetContainer == null) + return; + if (!(Activator.CreateInstance(type) is ISkinnableComponent component)) throw new InvalidOperationException("Attempted to instantiate a component for placement which was not an {typeof(ISkinnableComponent)}."); - getTarget(SkinnableTarget.MainHUDComponents)?.Add(component); + var drawableComponent = (Drawable)component; + + // give newly added components a sane starting location. + drawableComponent.Origin = Anchor.TopCentre; + drawableComponent.Anchor = Anchor.TopCentre; + drawableComponent.Y = targetContainer.DrawSize.Y / 2; + + targetContainer.Add(component); SelectedComponents.Clear(); SelectedComponents.Add(component); diff --git a/osu.Game/Skinning/ISkinnableTarget.cs b/osu.Game/Skinning/ISkinnableTarget.cs index 4d97b42e8e..4f6e3e66c3 100644 --- a/osu.Game/Skinning/ISkinnableTarget.cs +++ b/osu.Game/Skinning/ISkinnableTarget.cs @@ -1,12 +1,14 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.Graphics; + namespace osu.Game.Skinning { /// /// Denotes a container which can house s. /// - public interface ISkinnableTarget + public interface ISkinnableTarget : IDrawable { public SkinnableTarget Target { get; } From 273cd18b8aac5d2b8593dac1e48bdd609957e7e1 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 12 May 2021 14:19:36 +0900 Subject: [PATCH 143/205] Use test streaming client in gameplay leaderboard test --- ...TestSceneMultiplayerGameplayLeaderboard.cs | 32 +++---------------- 1 file changed, 5 insertions(+), 27 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs index b6c06bb149..6813a6e7dd 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs @@ -6,14 +6,12 @@ using System.Collections.Generic; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Testing; using osu.Framework.Utils; using osu.Game.Configuration; using osu.Game.Database; -using osu.Game.Online; using osu.Game.Online.API; using osu.Game.Online.Spectator; using osu.Game.Replays.Legacy; @@ -22,6 +20,7 @@ using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using osu.Game.Screens.Play.HUD; using osu.Game.Tests.Visual.Online; +using osu.Game.Tests.Visual.Spectator; namespace osu.Game.Tests.Visual.Multiplayer { @@ -30,7 +29,7 @@ namespace osu.Game.Tests.Visual.Multiplayer private const int users = 16; [Cached(typeof(SpectatorStreamingClient))] - private TestMultiplayerStreaming streamingClient = new TestMultiplayerStreaming(users); + private TestMultiplayerStreaming streamingClient = new TestMultiplayerStreaming(); [Cached(typeof(UserLookupCache))] private UserLookupCache lookupCache = new TestSceneCurrentlyPlayingDisplay.TestUserLookupCache(); @@ -71,7 +70,8 @@ namespace osu.Game.Tests.Visual.Multiplayer var playable = Beatmap.Value.GetPlayableBeatmap(Ruleset.Value); - streamingClient.Start(Beatmap.Value.BeatmapInfo.OnlineBeatmapID ?? 0); + for (int i = 0; i < users; i++) + streamingClient.StartPlay(i, Beatmap.Value.BeatmapInfo.OnlineBeatmapID ?? 0); Client.CurrentMatchPlayingUserIds.Clear(); Client.CurrentMatchPlayingUserIds.AddRange(streamingClient.PlayingUsers); @@ -114,30 +114,8 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("change to standardised", () => config.SetValue(OsuSetting.ScoreDisplayMode, ScoringMode.Standardised)); } - public class TestMultiplayerStreaming : SpectatorStreamingClient + public class TestMultiplayerStreaming : TestSpectatorStreamingClient { - public new BindableList PlayingUsers => (BindableList)base.PlayingUsers; - - private readonly int totalUsers; - - public TestMultiplayerStreaming(int totalUsers) - : base(new DevelopmentEndpointConfiguration()) - { - this.totalUsers = totalUsers; - } - - public void Start(int beatmapId) - { - for (int i = 0; i < totalUsers; i++) - { - ((ISpectatorClient)this).UserBeganPlaying(i, new SpectatorState - { - BeatmapID = beatmapId, - RulesetID = 0, - }); - } - } - private readonly Dictionary lastHeaders = new Dictionary(); public void RandomlyUpdateState() From e1dacde31456e2366226c64398b0dab1a116d3ab Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 12 May 2021 14:22:15 +0900 Subject: [PATCH 144/205] Add combo to test streaming client --- osu.Game/Tests/Visual/Spectator/TestSpectatorStreamingClient.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Tests/Visual/Spectator/TestSpectatorStreamingClient.cs b/osu.Game/Tests/Visual/Spectator/TestSpectatorStreamingClient.cs index c2428ce415..cc8437479d 100644 --- a/osu.Game/Tests/Visual/Spectator/TestSpectatorStreamingClient.cs +++ b/osu.Game/Tests/Visual/Spectator/TestSpectatorStreamingClient.cs @@ -54,7 +54,7 @@ namespace osu.Game.Tests.Visual.Spectator frames.Add(new LegacyReplayFrame(i * 100, RNG.Next(0, 512), RNG.Next(0, 512), buttonState)); } - var bundle = new FrameDataBundle(new ScoreInfo(), frames); + var bundle = new FrameDataBundle(new ScoreInfo { Combo = index + count }, frames); ((ISpectatorClient)this).UserSentFrames(userId, bundle); if (!userSentStateDictionary[userId]) From 05e0c57a6af3fd579abc9ea73fd4de33702f9284 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 12 May 2021 15:30:52 +0900 Subject: [PATCH 145/205] Keep component positions stable when changing anchor/origin --- osu.Game/Skinning/Editor/SkinSelectionHandler.cs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/osu.Game/Skinning/Editor/SkinSelectionHandler.cs b/osu.Game/Skinning/Editor/SkinSelectionHandler.cs index cf5ece03e9..410ec7a272 100644 --- a/osu.Game/Skinning/Editor/SkinSelectionHandler.cs +++ b/osu.Game/Skinning/Editor/SkinSelectionHandler.cs @@ -109,13 +109,25 @@ namespace osu.Game.Skinning.Editor private void applyOrigin(Anchor anchor) { foreach (var item in SelectedItems) - ((Drawable)item).Origin = anchor; + { + var drawable = (Drawable)item; + + var previousOrigin = drawable.OriginPosition; + drawable.Origin = anchor; + drawable.Position += drawable.OriginPosition - previousOrigin; + } } private void applyAnchor(Anchor anchor) { foreach (var item in SelectedItems) - ((Drawable)item).Anchor = anchor; + { + var drawable = (Drawable)item; + + var previousAnchor = (drawable.AnchorPosition); + drawable.Anchor = anchor; + drawable.Position -= drawable.AnchorPosition - previousAnchor; + } } private static void adjustScaleFromAnchor(ref Vector2 scale, Anchor reference) From 29e6f6b6b6cc1cc8931c13525da01f224ae8de89 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 12 May 2021 15:58:21 +0900 Subject: [PATCH 146/205] Remove `public` prefixes from interface type and add `Components` list for future use --- .../Skinning/Editor/SkinBlueprintContainer.cs | 3 +-- osu.Game/Skinning/ISkinnableTarget.cs | 16 +++++++++++++--- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/osu.Game/Skinning/Editor/SkinBlueprintContainer.cs b/osu.Game/Skinning/Editor/SkinBlueprintContainer.cs index ef189ed165..9890b8e8b6 100644 --- a/osu.Game/Skinning/Editor/SkinBlueprintContainer.cs +++ b/osu.Game/Skinning/Editor/SkinBlueprintContainer.cs @@ -12,7 +12,6 @@ using osu.Framework.Testing; using osu.Game.Rulesets.Edit; using osu.Game.Screens; using osu.Game.Screens.Edit.Compose.Components; -using osu.Game.Screens.Play.HUD; namespace osu.Game.Skinning.Editor { @@ -38,7 +37,7 @@ namespace osu.Game.Skinning.Editor base.LoadComplete(); // track each target container on the current screen. - var targetContainers = target.ChildrenOfType().ToArray(); + var targetContainers = target.ChildrenOfType().ToArray(); if (targetContainers.Length == 0) { diff --git a/osu.Game/Skinning/ISkinnableTarget.cs b/osu.Game/Skinning/ISkinnableTarget.cs index 4d97b42e8e..ab3c24a1e2 100644 --- a/osu.Game/Skinning/ISkinnableTarget.cs +++ b/osu.Game/Skinning/ISkinnableTarget.cs @@ -1,6 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.Bindables; + namespace osu.Game.Skinning { /// @@ -8,16 +10,24 @@ namespace osu.Game.Skinning /// public interface ISkinnableTarget { - public SkinnableTarget Target { get; } + /// + /// The definition of this target. + /// + SkinnableTarget Target { get; } + + /// + /// A bindable list of components which are being tracked by this skinnable target. + /// + IBindableList Components { get; } /// /// Reload this target from the current skin. /// - public void Reload(); + void Reload(); /// /// Add the provided item to this target. /// - public void Add(ISkinnableComponent drawable); + void Add(ISkinnableComponent drawable); } } From 494a1b01a536d6470b2700d4879cd0a05dc13e7e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 12 May 2021 15:59:33 +0900 Subject: [PATCH 147/205] Move `SkinnableElementTargetContainer` out of HUD namespace --- osu.Game/Skinning/Editor/SkinEditor.cs | 1 - .../Play/HUD => Skinning}/SkinnableElementTargetContainer.cs | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) rename osu.Game/{Screens/Play/HUD => Skinning}/SkinnableElementTargetContainer.cs (97%) diff --git a/osu.Game/Skinning/Editor/SkinEditor.cs b/osu.Game/Skinning/Editor/SkinEditor.cs index b32d117a92..a00557ee4e 100644 --- a/osu.Game/Skinning/Editor/SkinEditor.cs +++ b/osu.Game/Skinning/Editor/SkinEditor.cs @@ -13,7 +13,6 @@ using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Cursor; using osu.Game.Graphics.UserInterface; -using osu.Game.Screens.Play.HUD; using osuTK; namespace osu.Game.Skinning.Editor diff --git a/osu.Game/Screens/Play/HUD/SkinnableElementTargetContainer.cs b/osu.Game/Skinning/SkinnableElementTargetContainer.cs similarity index 97% rename from osu.Game/Screens/Play/HUD/SkinnableElementTargetContainer.cs rename to osu.Game/Skinning/SkinnableElementTargetContainer.cs index b2f6d32927..b900fdf3e0 100644 --- a/osu.Game/Screens/Play/HUD/SkinnableElementTargetContainer.cs +++ b/osu.Game/Skinning/SkinnableElementTargetContainer.cs @@ -7,9 +7,9 @@ using System.Linq; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Extensions; -using osu.Game.Skinning; +using osu.Game.Screens.Play.HUD; -namespace osu.Game.Screens.Play.HUD +namespace osu.Game.Skinning { public class SkinnableElementTargetContainer : SkinReloadableDrawable, ISkinnableTarget { From 9df08560b69519f94779e22e2ce6c49433b1808b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 12 May 2021 16:07:00 +0900 Subject: [PATCH 148/205] Save skin editor changes on forced exit --- osu.Game/Skinning/Editor/SkinEditor.cs | 4 ++-- osu.Game/Skinning/Editor/SkinEditorOverlay.cs | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game/Skinning/Editor/SkinEditor.cs b/osu.Game/Skinning/Editor/SkinEditor.cs index a00557ee4e..5deebe9800 100644 --- a/osu.Game/Skinning/Editor/SkinEditor.cs +++ b/osu.Game/Skinning/Editor/SkinEditor.cs @@ -108,7 +108,7 @@ namespace osu.Game.Skinning.Editor { Text = "Save Changes", Width = 140, - Action = save, + Action = Save, }, new DangerousTriangleButton { @@ -192,7 +192,7 @@ namespace osu.Game.Skinning.Editor } } - private void save() + public void Save() { SkinnableElementTargetContainer[] targetContainers = targetScreen.ChildrenOfType().ToArray(); diff --git a/osu.Game/Skinning/Editor/SkinEditorOverlay.cs b/osu.Game/Skinning/Editor/SkinEditorOverlay.cs index cbed498a38..2d7cae71ff 100644 --- a/osu.Game/Skinning/Editor/SkinEditorOverlay.cs +++ b/osu.Game/Skinning/Editor/SkinEditorOverlay.cs @@ -92,8 +92,10 @@ namespace osu.Game.Skinning.Editor /// public void Reset() { + skinEditor?.Save(); skinEditor?.Hide(); skinEditor?.Expire(); + skinEditor = null; } } From 17e376457610652e9cfac1470eb8748114441022 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 12 May 2021 16:38:04 +0900 Subject: [PATCH 149/205] Rename `Settings` to have a more localised name --- .../Edit/{Settings.cs => RoundedContentEditorScreenSettings.cs} | 2 +- osu.Game/Screens/Edit/Timing/ControlPointSettings.cs | 2 +- osu.Game/Screens/Edit/Verify/IssueSettings.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) rename osu.Game/Screens/Edit/{Settings.cs => RoundedContentEditorScreenSettings.cs} (94%) diff --git a/osu.Game/Screens/Edit/Settings.cs b/osu.Game/Screens/Edit/RoundedContentEditorScreenSettings.cs similarity index 94% rename from osu.Game/Screens/Edit/Settings.cs rename to osu.Game/Screens/Edit/RoundedContentEditorScreenSettings.cs index 758414333d..06293fbaac 100644 --- a/osu.Game/Screens/Edit/Settings.cs +++ b/osu.Game/Screens/Edit/RoundedContentEditorScreenSettings.cs @@ -11,7 +11,7 @@ using osu.Game.Overlays; namespace osu.Game.Screens.Edit { - public abstract class Settings : CompositeDrawable + public abstract class RoundedContentEditorScreenSettings : CompositeDrawable { [BackgroundDependencyLoader] private void load(OverlayColourProvider colours) diff --git a/osu.Game/Screens/Edit/Timing/ControlPointSettings.cs b/osu.Game/Screens/Edit/Timing/ControlPointSettings.cs index 36f31d4be4..13f81c75da 100644 --- a/osu.Game/Screens/Edit/Timing/ControlPointSettings.cs +++ b/osu.Game/Screens/Edit/Timing/ControlPointSettings.cs @@ -6,7 +6,7 @@ using osu.Framework.Graphics; namespace osu.Game.Screens.Edit.Timing { - public class ControlPointSettings : Settings + public class ControlPointSettings : RoundedContentEditorScreenSettings { protected override IReadOnlyList CreateSections() => new Drawable[] { diff --git a/osu.Game/Screens/Edit/Verify/IssueSettings.cs b/osu.Game/Screens/Edit/Verify/IssueSettings.cs index 68ab51c3c8..506694aa46 100644 --- a/osu.Game/Screens/Edit/Verify/IssueSettings.cs +++ b/osu.Game/Screens/Edit/Verify/IssueSettings.cs @@ -6,7 +6,7 @@ using osu.Framework.Graphics; namespace osu.Game.Screens.Edit.Verify { - public class IssueSettings : Settings + public class IssueSettings : RoundedContentEditorScreenSettings { private readonly IssueList issueList; From d2e0e8ad948aac6ef137abf81d50963ba8e55297 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 12 May 2021 16:53:49 +0900 Subject: [PATCH 150/205] Reverse direction of binding to allow for better abstract class definitions --- ...tEditorScreen.cs => EditorRoundedScreen.cs} | 4 ++-- ...tings.cs => EditorRoundedScreenSettings.cs} | 2 +- ...s => EditorRoundedScreenSettingsSection.cs} | 11 ++--------- osu.Game/Screens/Edit/Setup/SetupScreen.cs | 2 +- .../Screens/Edit/Setup/SetupScreenHeader.cs | 2 +- osu.Game/Screens/Edit/Setup/SetupSection.cs | 2 +- .../Edit/Timing/ControlPointSettings.cs | 2 +- osu.Game/Screens/Edit/Timing/TimingScreen.cs | 2 +- .../Edit/Verify/InterpretationSection.cs | 11 +++++------ osu.Game/Screens/Edit/Verify/IssueList.cs | 5 +++-- osu.Game/Screens/Edit/Verify/IssueSettings.cs | 13 +++---------- osu.Game/Screens/Edit/Verify/IssueTable.cs | 5 ++++- osu.Game/Screens/Edit/Verify/VerifyScreen.cs | 18 +++++++++++------- .../Screens/Edit/Verify/VisibilitySection.cs | 14 ++++++-------- 14 files changed, 42 insertions(+), 51 deletions(-) rename osu.Game/Screens/Edit/{RoundedContentEditorScreen.cs => EditorRoundedScreen.cs} (93%) rename osu.Game/Screens/Edit/{RoundedContentEditorScreenSettings.cs => EditorRoundedScreenSettings.cs} (94%) rename osu.Game/Screens/Edit/{Verify/Section.cs => EditorRoundedScreenSettingsSection.cs} (88%) diff --git a/osu.Game/Screens/Edit/RoundedContentEditorScreen.cs b/osu.Game/Screens/Edit/EditorRoundedScreen.cs similarity index 93% rename from osu.Game/Screens/Edit/RoundedContentEditorScreen.cs rename to osu.Game/Screens/Edit/EditorRoundedScreen.cs index a55ae3f635..c6ced02021 100644 --- a/osu.Game/Screens/Edit/RoundedContentEditorScreen.cs +++ b/osu.Game/Screens/Edit/EditorRoundedScreen.cs @@ -10,7 +10,7 @@ using osu.Game.Overlays; namespace osu.Game.Screens.Edit { - public class RoundedContentEditorScreen : EditorScreen + public class EditorRoundedScreen : EditorScreen { public const int HORIZONTAL_PADDING = 100; @@ -24,7 +24,7 @@ namespace osu.Game.Screens.Edit protected override Container Content => roundedContent; - public RoundedContentEditorScreen(EditorScreenMode mode) + public EditorRoundedScreen(EditorScreenMode mode) : base(mode) { ColourProvider = new OverlayColourProvider(OverlayColourScheme.Blue); diff --git a/osu.Game/Screens/Edit/RoundedContentEditorScreenSettings.cs b/osu.Game/Screens/Edit/EditorRoundedScreenSettings.cs similarity index 94% rename from osu.Game/Screens/Edit/RoundedContentEditorScreenSettings.cs rename to osu.Game/Screens/Edit/EditorRoundedScreenSettings.cs index 06293fbaac..cb17484d27 100644 --- a/osu.Game/Screens/Edit/RoundedContentEditorScreenSettings.cs +++ b/osu.Game/Screens/Edit/EditorRoundedScreenSettings.cs @@ -11,7 +11,7 @@ using osu.Game.Overlays; namespace osu.Game.Screens.Edit { - public abstract class RoundedContentEditorScreenSettings : CompositeDrawable + public abstract class EditorRoundedScreenSettings : CompositeDrawable { [BackgroundDependencyLoader] private void load(OverlayColourProvider colours) diff --git a/osu.Game/Screens/Edit/Verify/Section.cs b/osu.Game/Screens/Edit/EditorRoundedScreenSettingsSection.cs similarity index 88% rename from osu.Game/Screens/Edit/Verify/Section.cs rename to osu.Game/Screens/Edit/EditorRoundedScreenSettingsSection.cs index f6815a05a1..87ed98a439 100644 --- a/osu.Game/Screens/Edit/Verify/Section.cs +++ b/osu.Game/Screens/Edit/EditorRoundedScreenSettingsSection.cs @@ -9,22 +9,15 @@ using osu.Game.Graphics.Sprites; using osu.Game.Overlays; using osuTK; -namespace osu.Game.Screens.Edit.Verify +namespace osu.Game.Screens.Edit { - public abstract class Section : CompositeDrawable + public abstract class EditorRoundedScreenSettingsSection : CompositeDrawable { private const int header_height = 50; - protected readonly IssueList IssueList; - protected FillFlowContainer Flow; protected abstract string Header { get; } - protected Section(IssueList issueList) - { - IssueList = issueList; - } - [BackgroundDependencyLoader] private void load(OverlayColourProvider colours) { diff --git a/osu.Game/Screens/Edit/Setup/SetupScreen.cs b/osu.Game/Screens/Edit/Setup/SetupScreen.cs index 0af7cf095b..5bbec2574f 100644 --- a/osu.Game/Screens/Edit/Setup/SetupScreen.cs +++ b/osu.Game/Screens/Edit/Setup/SetupScreen.cs @@ -7,7 +7,7 @@ using osu.Game.Graphics.Containers; namespace osu.Game.Screens.Edit.Setup { - public class SetupScreen : RoundedContentEditorScreen + public class SetupScreen : EditorRoundedScreen { [Cached] private SectionsContainer sections = new SectionsContainer(); diff --git a/osu.Game/Screens/Edit/Setup/SetupScreenHeader.cs b/osu.Game/Screens/Edit/Setup/SetupScreenHeader.cs index 10daacc359..2d0afda001 100644 --- a/osu.Game/Screens/Edit/Setup/SetupScreenHeader.cs +++ b/osu.Game/Screens/Edit/Setup/SetupScreenHeader.cs @@ -93,7 +93,7 @@ namespace osu.Game.Screens.Edit.Setup public SetupScreenTabControl() { - TabContainer.Margin = new MarginPadding { Horizontal = RoundedContentEditorScreen.HORIZONTAL_PADDING }; + TabContainer.Margin = new MarginPadding { Horizontal = EditorRoundedScreen.HORIZONTAL_PADDING }; AddInternal(background = new Box { diff --git a/osu.Game/Screens/Edit/Setup/SetupSection.cs b/osu.Game/Screens/Edit/Setup/SetupSection.cs index b3ae15900f..8964e651df 100644 --- a/osu.Game/Screens/Edit/Setup/SetupSection.cs +++ b/osu.Game/Screens/Edit/Setup/SetupSection.cs @@ -33,7 +33,7 @@ namespace osu.Game.Screens.Edit.Setup Padding = new MarginPadding { Vertical = 10, - Horizontal = RoundedContentEditorScreen.HORIZONTAL_PADDING + Horizontal = EditorRoundedScreen.HORIZONTAL_PADDING }; InternalChild = new FillFlowContainer diff --git a/osu.Game/Screens/Edit/Timing/ControlPointSettings.cs b/osu.Game/Screens/Edit/Timing/ControlPointSettings.cs index 13f81c75da..48639789af 100644 --- a/osu.Game/Screens/Edit/Timing/ControlPointSettings.cs +++ b/osu.Game/Screens/Edit/Timing/ControlPointSettings.cs @@ -6,7 +6,7 @@ using osu.Framework.Graphics; namespace osu.Game.Screens.Edit.Timing { - public class ControlPointSettings : RoundedContentEditorScreenSettings + public class ControlPointSettings : EditorRoundedScreenSettings { protected override IReadOnlyList CreateSections() => new Drawable[] { diff --git a/osu.Game/Screens/Edit/Timing/TimingScreen.cs b/osu.Game/Screens/Edit/Timing/TimingScreen.cs index 9f26dece08..a4193d5084 100644 --- a/osu.Game/Screens/Edit/Timing/TimingScreen.cs +++ b/osu.Game/Screens/Edit/Timing/TimingScreen.cs @@ -15,7 +15,7 @@ using osuTK; namespace osu.Game.Screens.Edit.Timing { - public class TimingScreen : RoundedContentEditorScreen + public class TimingScreen : EditorRoundedScreen { [Cached] private Bindable selectedGroup = new Bindable(); diff --git a/osu.Game/Screens/Edit/Verify/InterpretationSection.cs b/osu.Game/Screens/Edit/Verify/InterpretationSection.cs index fa0bde9ddc..7991786542 100644 --- a/osu.Game/Screens/Edit/Verify/InterpretationSection.cs +++ b/osu.Game/Screens/Edit/Verify/InterpretationSection.cs @@ -8,12 +8,10 @@ using osu.Game.Overlays.Settings; namespace osu.Game.Screens.Edit.Verify { - internal class InterpretationSection : Section + internal class InterpretationSection : EditorRoundedScreenSettingsSection { - public InterpretationSection(IssueList issueList) - : base(issueList) - { - } + [Resolved] + private VerifyScreen verify { get; set; } protected override string Header => "Interpretation"; @@ -26,7 +24,8 @@ namespace osu.Game.Screens.Edit.Verify Origin = Anchor.CentreLeft, TooltipText = "Affects checks that depend on difficulty level" }; - dropdown.Current.BindTo(IssueList.InterpretedDifficulty); + + dropdown.Current.BindTo(verify.InterpretedDifficulty); Flow.Add(dropdown); } diff --git a/osu.Game/Screens/Edit/Verify/IssueList.cs b/osu.Game/Screens/Edit/Verify/IssueList.cs index 3e836c4010..befffdd9db 100644 --- a/osu.Game/Screens/Edit/Verify/IssueList.cs +++ b/osu.Game/Screens/Edit/Verify/IssueList.cs @@ -18,6 +18,7 @@ using osuTK; namespace osu.Game.Screens.Edit.Verify { + [Cached] public class IssueList : CompositeDrawable { private IssueTable table; @@ -32,7 +33,7 @@ namespace osu.Game.Screens.Edit.Verify private EditorBeatmap beatmap { get; set; } [Resolved] - private Bindable selectedIssue { get; set; } + private VerifyScreen verify { get; set; } public Dictionary> ShowType { get; set; } @@ -55,7 +56,7 @@ namespace osu.Game.Screens.Edit.Verify generalVerifier = new BeatmapVerifier(); rulesetVerifier = beatmap.BeatmapInfo.Ruleset?.CreateInstance()?.CreateBeatmapVerifier(); - InterpretedDifficulty = new Bindable(beatmap.BeatmapInfo.DifficultyRating); + InterpretedDifficulty = verify.InterpretedDifficulty.GetBoundCopy(); RelativeSizeAxes = Axes.Both; diff --git a/osu.Game/Screens/Edit/Verify/IssueSettings.cs b/osu.Game/Screens/Edit/Verify/IssueSettings.cs index 506694aa46..ae3ef7e0b0 100644 --- a/osu.Game/Screens/Edit/Verify/IssueSettings.cs +++ b/osu.Game/Screens/Edit/Verify/IssueSettings.cs @@ -6,19 +6,12 @@ using osu.Framework.Graphics; namespace osu.Game.Screens.Edit.Verify { - public class IssueSettings : RoundedContentEditorScreenSettings + public class IssueSettings : EditorRoundedScreenSettings { - private readonly IssueList issueList; - - public IssueSettings(IssueList issueList) - { - this.issueList = issueList; - } - protected override IReadOnlyList CreateSections() => new Drawable[] { - new InterpretationSection(issueList), - new VisibilitySection(issueList) + new InterpretationSection(), + new VisibilitySection() }; } } diff --git a/osu.Game/Screens/Edit/Verify/IssueTable.cs b/osu.Game/Screens/Edit/Verify/IssueTable.cs index 44244028c9..05a8fdd26d 100644 --- a/osu.Game/Screens/Edit/Verify/IssueTable.cs +++ b/osu.Game/Screens/Edit/Verify/IssueTable.cs @@ -18,7 +18,9 @@ namespace osu.Game.Screens.Edit.Verify public class IssueTable : EditorTable { [Resolved] - private Bindable selectedIssue { get; set; } + private VerifyScreen verify { get; set; } + + private Bindable selectedIssue; [Resolved] private EditorClock clock { get; set; } @@ -71,6 +73,7 @@ namespace osu.Game.Screens.Edit.Verify { base.LoadComplete(); + selectedIssue = verify.SelectedIssue.GetBoundCopy(); selectedIssue.BindValueChanged(issue => { foreach (var b in BackgroundFlow) b.Selected = b.Item == issue.NewValue; diff --git a/osu.Game/Screens/Edit/Verify/VerifyScreen.cs b/osu.Game/Screens/Edit/Verify/VerifyScreen.cs index ed4658da8e..afd0c8760d 100644 --- a/osu.Game/Screens/Edit/Verify/VerifyScreen.cs +++ b/osu.Game/Screens/Edit/Verify/VerifyScreen.cs @@ -5,14 +5,19 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Game.Beatmaps; using osu.Game.Rulesets.Edit.Checks.Components; namespace osu.Game.Screens.Edit.Verify { - public class VerifyScreen : RoundedContentEditorScreen + [Cached] + public class VerifyScreen : EditorRoundedScreen { - [Cached] - private Bindable selectedIssue = new Bindable(); + public readonly Bindable SelectedIssue = new Bindable(); + + public readonly Bindable InterpretedDifficulty = new Bindable(); + + public IssueList IssueList { get; private set; } public VerifyScreen() : base(EditorScreenMode.Verify) @@ -22,8 +27,7 @@ namespace osu.Game.Screens.Edit.Verify [BackgroundDependencyLoader] private void load() { - IssueList issueList; - + IssueList = new IssueList(); Child = new Container { RelativeSizeAxes = Axes.Both, @@ -39,8 +43,8 @@ namespace osu.Game.Screens.Edit.Verify { new Drawable[] { - issueList = new IssueList(), - new IssueSettings(issueList), + IssueList, + new IssueSettings(), }, } } diff --git a/osu.Game/Screens/Edit/Verify/VisibilitySection.cs b/osu.Game/Screens/Edit/Verify/VisibilitySection.cs index f849426800..ce755bdcb4 100644 --- a/osu.Game/Screens/Edit/Verify/VisibilitySection.cs +++ b/osu.Game/Screens/Edit/Verify/VisibilitySection.cs @@ -9,19 +9,17 @@ using osu.Game.Rulesets.Edit.Checks.Components; namespace osu.Game.Screens.Edit.Verify { - internal class VisibilitySection : Section + internal class VisibilitySection : EditorRoundedScreenSettingsSection { - public VisibilitySection(IssueList issueList) - : base(issueList) - { - } + [Resolved] + private VerifyScreen verify { get; set; } protected override string Header => "Visibility"; [BackgroundDependencyLoader] private void load(OverlayColourProvider colours) { - foreach (IssueType issueType in IssueList.ShowType.Keys) + foreach (IssueType issueType in verify.IssueList.ShowType.Keys) { var checkbox = new SettingsCheckbox { @@ -30,8 +28,8 @@ namespace osu.Game.Screens.Edit.Verify LabelText = issueType.ToString() }; - checkbox.Current.BindTo(IssueList.ShowType[issueType]); - checkbox.Current.BindValueChanged(_ => IssueList.Refresh()); + checkbox.Current.BindTo(verify.IssueList.ShowType[issueType]); + checkbox.Current.BindValueChanged(_ => verify.IssueList.Refresh()); Flow.Add(checkbox); } } From be187e8ebdd8d981b1c3c1620960e1aaca27d44b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 12 May 2021 17:42:04 +0900 Subject: [PATCH 151/205] Avoid hard crash if `Save()` is called before preparing for mutation --- osu.Game/Skinning/Editor/SkinEditor.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/osu.Game/Skinning/Editor/SkinEditor.cs b/osu.Game/Skinning/Editor/SkinEditor.cs index 5deebe9800..fcb1392ed5 100644 --- a/osu.Game/Skinning/Editor/SkinEditor.cs +++ b/osu.Game/Skinning/Editor/SkinEditor.cs @@ -38,6 +38,8 @@ namespace osu.Game.Skinning.Editor [Resolved] private OsuColour colours { get; set; } + private bool hasBegunMutating; + public SkinEditor(Drawable targetScreen) { this.targetScreen = targetScreen; @@ -139,7 +141,11 @@ namespace osu.Game.Skinning.Editor // schedule ensures this only happens when the skin editor is visible. // also avoid some weird endless recursion / bindable feedback loop (something to do with tracking skins across three different bindable types). // probably something which will be factored out in a future database refactor so not too concerning for now. - currentSkin.BindValueChanged(skin => Scheduler.AddOnce(skinChanged), true); + currentSkin.BindValueChanged(skin => + { + hasBegunMutating = false; + Scheduler.AddOnce(skinChanged); + }, true); } private void skinChanged() @@ -161,6 +167,7 @@ namespace osu.Game.Skinning.Editor }); skins.EnsureMutableSkin(); + hasBegunMutating = true; } private void placeComponent(Type type) @@ -194,6 +201,9 @@ namespace osu.Game.Skinning.Editor public void Save() { + if (!hasBegunMutating) + return; + SkinnableElementTargetContainer[] targetContainers = targetScreen.ChildrenOfType().ToArray(); foreach (var t in targetContainers) From 2f55d1e5ab9c734bb07519849d6ca4c31f32b375 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 12 May 2021 17:42:12 +0900 Subject: [PATCH 152/205] Also save on skin switch --- osu.Game/Skinning/Editor/SkinEditor.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Skinning/Editor/SkinEditor.cs b/osu.Game/Skinning/Editor/SkinEditor.cs index fcb1392ed5..9e50aab829 100644 --- a/osu.Game/Skinning/Editor/SkinEditor.cs +++ b/osu.Game/Skinning/Editor/SkinEditor.cs @@ -143,6 +143,8 @@ namespace osu.Game.Skinning.Editor // probably something which will be factored out in a future database refactor so not too concerning for now. currentSkin.BindValueChanged(skin => { + Save(); + hasBegunMutating = false; Scheduler.AddOnce(skinChanged); }, true); From 80e231d90ad9a25444b7a2e2c1d43413cbaf327e Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 12 May 2021 11:07:31 +0300 Subject: [PATCH 153/205] Add failing test case --- .../Visual/Editing/TestSceneComposeSelectBox.cs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs b/osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs index e383aa8008..d5cfeb1878 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs @@ -167,5 +167,21 @@ namespace osu.Game.Tests.Visual.Editing AddStep("move mouse away", () => InputManager.MoveMouseTo(selectionBox, new Vector2(20))); AddUntilStep("rotation handle hidden", () => rotationHandle.Alpha == 0); } + + /// + /// Tests that hovering over two handles instantaneously from one to another does not crash or cause issues to the visibility state. + /// + [Test] + public void TestHoverOverTwoHandlesInstantaneously() + { + AddStep("hover over top-left scale handle", () => + InputManager.MoveMouseTo(this.ChildrenOfType().Single(s => s.Anchor == Anchor.TopLeft))); + AddStep("hover over top-right scale handle", () => + InputManager.MoveMouseTo(this.ChildrenOfType().Single(s => s.Anchor == Anchor.TopRight))); + AddUntilStep("top-left rotation handle hidden", () => + this.ChildrenOfType().Single(r => r.Anchor == Anchor.TopLeft).Alpha == 0); + AddUntilStep("top-right rotation handle shown", () => + this.ChildrenOfType().Single(r => r.Anchor == Anchor.TopRight).Alpha == 1); + } } } From 96d3586294dbfef0f9af5b7a9e70e00a868d0600 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 12 May 2021 11:30:12 +0300 Subject: [PATCH 154/205] Fix rotation handle visibility logic not handling two handles hovered at once --- .../Compose/Components/SelectionBoxDragHandleContainer.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxDragHandleContainer.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxDragHandleContainer.cs index 456f72878d..397158b9f6 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxDragHandleContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxDragHandleContainer.cs @@ -84,8 +84,8 @@ namespace osu.Game.Screens.Edit.Compose.Components if (activeHandle?.IsHeld == true) return; - activeHandle = rotationHandles.SingleOrDefault(h => h.IsHeld || h.IsHovered); - activeHandle ??= allDragHandles.SingleOrDefault(h => h.IsHovered); + activeHandle = rotationHandles.FirstOrDefault(h => h.IsHeld || h.IsHovered); + activeHandle ??= allDragHandles.FirstOrDefault(h => h.IsHovered); if (activeHandle != null) { From 088335a0359f73ecb10625c5bcccbaefff9edbd4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 12 May 2021 17:45:51 +0900 Subject: [PATCH 155/205] Revert "Also save on skin switch" This reverts commit 2f55d1e5ab9c734bb07519849d6ca4c31f32b375. --- osu.Game/Skinning/Editor/SkinEditor.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game/Skinning/Editor/SkinEditor.cs b/osu.Game/Skinning/Editor/SkinEditor.cs index 9e50aab829..fcb1392ed5 100644 --- a/osu.Game/Skinning/Editor/SkinEditor.cs +++ b/osu.Game/Skinning/Editor/SkinEditor.cs @@ -143,8 +143,6 @@ namespace osu.Game.Skinning.Editor // probably something which will be factored out in a future database refactor so not too concerning for now. currentSkin.BindValueChanged(skin => { - Save(); - hasBegunMutating = false; Scheduler.AddOnce(skinChanged); }, true); From 0a895fff158ab57c8dc0e9a4ff9e6d9f5a7b218e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 12 May 2021 18:53:25 +0900 Subject: [PATCH 156/205] Remove remaining test usage of `SkinnableXXX` HUD components --- .../Visual/Gameplay/TestSceneComboCounter.cs | 8 ++------ .../TestSceneSkinnableAccuracyCounter.cs | 3 ++- .../TestSceneSkinnableHealthDisplay.cs | 11 ++-------- .../TestSceneSkinnableScoreCounter.cs | 11 +++------- .../Play/HUD/SkinnableAccuracyCounter.cs | 16 --------------- .../Screens/Play/HUD/SkinnableComboCounter.cs | 16 --------------- .../Play/HUD/SkinnableHealthDisplay.cs | 16 --------------- .../Screens/Play/HUD/SkinnableScoreCounter.cs | 16 --------------- osu.Game/Skinning/DefaultSkin.cs | 20 +++++++++++++++++++ osu.Game/Skinning/SkinnableDrawable.cs | 4 ++-- 10 files changed, 31 insertions(+), 90 deletions(-) delete mode 100644 osu.Game/Screens/Play/HUD/SkinnableAccuracyCounter.cs delete mode 100644 osu.Game/Screens/Play/HUD/SkinnableComboCounter.cs delete mode 100644 osu.Game/Screens/Play/HUD/SkinnableHealthDisplay.cs delete mode 100644 osu.Game/Screens/Play/HUD/SkinnableScoreCounter.cs diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneComboCounter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneComboCounter.cs index b0a0b5189f..b22af0f7ac 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneComboCounter.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneComboCounter.cs @@ -1,22 +1,18 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Collections.Generic; -using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Testing; using osu.Game.Rulesets; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Scoring; -using osu.Game.Screens.Play.HUD; +using osu.Game.Skinning; namespace osu.Game.Tests.Visual.Gameplay { public class TestSceneComboCounter : SkinnableTestScene { - private IEnumerable comboCounters => CreatedDrawables.OfType(); - protected override Ruleset CreateRulesetForSkinProvider() => new OsuRuleset(); [Cached] @@ -25,7 +21,7 @@ namespace osu.Game.Tests.Visual.Gameplay [SetUpSteps] public void SetUpSteps() { - AddStep("Create combo counters", () => SetContents(() => new SkinnableComboCounter())); + AddStep("Create combo counters", () => SetContents(() => new SkinnableDrawable(new HUDSkinComponent(HUDSkinComponents.ComboCounter)))); } [Test] diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableAccuracyCounter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableAccuracyCounter.cs index 6a8a2187f9..d9139299ea 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableAccuracyCounter.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableAccuracyCounter.cs @@ -8,6 +8,7 @@ using osu.Game.Rulesets; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Scoring; using osu.Game.Screens.Play.HUD; +using osu.Game.Skinning; namespace osu.Game.Tests.Visual.Gameplay { @@ -22,7 +23,7 @@ namespace osu.Game.Tests.Visual.Gameplay public void SetUpSteps() { AddStep("Set initial accuracy", () => scoreProcessor.Accuracy.Value = 1); - AddStep("Create accuracy counters", () => SetContents(() => new SkinnableAccuracyCounter())); + AddStep("Create accuracy counters", () => SetContents(() => new SkinnableDrawable(new HUDSkinComponent(HUDSkinComponents.AccuracyCounter)))); } [Test] diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHealthDisplay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHealthDisplay.cs index 4f50613416..ead27bf017 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHealthDisplay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHealthDisplay.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Collections.Generic; -using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Testing; @@ -12,14 +10,12 @@ using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Scoring; -using osu.Game.Screens.Play.HUD; +using osu.Game.Skinning; namespace osu.Game.Tests.Visual.Gameplay { public class TestSceneSkinnableHealthDisplay : SkinnableTestScene { - private IEnumerable healthDisplays => CreatedDrawables.OfType(); - protected override Ruleset CreateRulesetForSkinProvider() => new OsuRuleset(); [Cached(typeof(HealthProcessor))] @@ -28,10 +24,7 @@ namespace osu.Game.Tests.Visual.Gameplay [SetUpSteps] public void SetUpSteps() { - AddStep("Create health displays", () => - { - SetContents(() => new SkinnableHealthDisplay()); - }); + AddStep("Create health displays", () => SetContents(() => new SkinnableDrawable(new HUDSkinComponent(HUDSkinComponents.HealthDisplay)))); AddStep(@"Reset all", delegate { healthProcessor.Health.Value = 1; diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableScoreCounter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableScoreCounter.cs index 4f2183711e..8d633c3ca2 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableScoreCounter.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableScoreCounter.cs @@ -1,23 +1,18 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Collections.Generic; -using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; -using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Testing; using osu.Game.Rulesets; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Scoring; -using osu.Game.Screens.Play.HUD; +using osu.Game.Skinning; namespace osu.Game.Tests.Visual.Gameplay { public class TestSceneSkinnableScoreCounter : SkinnableTestScene { - private IEnumerable scoreCounters => CreatedDrawables.OfType(); - protected override Ruleset CreateRulesetForSkinProvider() => new OsuRuleset(); [Cached] @@ -26,7 +21,7 @@ namespace osu.Game.Tests.Visual.Gameplay [SetUpSteps] public void SetUpSteps() { - AddStep("Create score counters", () => SetContents(() => new SkinnableScoreCounter())); + AddStep("Create score counters", () => SetContents(() => new SkinnableDrawable(new HUDSkinComponent(HUDSkinComponents.ScoreCounter)))); } [Test] @@ -40,7 +35,7 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestVeryLargeScore() { - AddStep("set large score", () => scoreCounters.ForEach(counter => scoreProcessor.TotalScore.Value = 1_000_000_000)); + AddStep("set large score", () => scoreProcessor.TotalScore.Value = 1_000_000_000); } } } diff --git a/osu.Game/Screens/Play/HUD/SkinnableAccuracyCounter.cs b/osu.Game/Screens/Play/HUD/SkinnableAccuracyCounter.cs deleted file mode 100644 index fcb8fca35d..0000000000 --- a/osu.Game/Screens/Play/HUD/SkinnableAccuracyCounter.cs +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Game.Skinning; - -namespace osu.Game.Screens.Play.HUD -{ - public class SkinnableAccuracyCounter : SkinnableDrawable - { - public SkinnableAccuracyCounter() - : base(new HUDSkinComponent(HUDSkinComponents.AccuracyCounter), _ => new DefaultAccuracyCounter()) - { - CentreComponent = false; - } - } -} diff --git a/osu.Game/Screens/Play/HUD/SkinnableComboCounter.cs b/osu.Game/Screens/Play/HUD/SkinnableComboCounter.cs deleted file mode 100644 index c62f1460c9..0000000000 --- a/osu.Game/Screens/Play/HUD/SkinnableComboCounter.cs +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Game.Skinning; - -namespace osu.Game.Screens.Play.HUD -{ - public class SkinnableComboCounter : SkinnableDrawable - { - public SkinnableComboCounter() - : base(new HUDSkinComponent(HUDSkinComponents.ComboCounter), skinComponent => new DefaultComboCounter()) - { - CentreComponent = false; - } - } -} diff --git a/osu.Game/Screens/Play/HUD/SkinnableHealthDisplay.cs b/osu.Game/Screens/Play/HUD/SkinnableHealthDisplay.cs deleted file mode 100644 index 3ba6a33276..0000000000 --- a/osu.Game/Screens/Play/HUD/SkinnableHealthDisplay.cs +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Game.Skinning; - -namespace osu.Game.Screens.Play.HUD -{ - public class SkinnableHealthDisplay : SkinnableDrawable - { - public SkinnableHealthDisplay() - : base(new HUDSkinComponent(HUDSkinComponents.HealthDisplay), _ => new DefaultHealthDisplay()) - { - CentreComponent = false; - } - } -} diff --git a/osu.Game/Screens/Play/HUD/SkinnableScoreCounter.cs b/osu.Game/Screens/Play/HUD/SkinnableScoreCounter.cs deleted file mode 100644 index cc9a712e97..0000000000 --- a/osu.Game/Screens/Play/HUD/SkinnableScoreCounter.cs +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Game.Skinning; - -namespace osu.Game.Screens.Play.HUD -{ - public class SkinnableScoreCounter : SkinnableDrawable - { - public SkinnableScoreCounter() - : base(new HUDSkinComponent(HUDSkinComponents.ScoreCounter), _ => new DefaultScoreCounter()) - { - CentreComponent = false; - } - } -} diff --git a/osu.Game/Skinning/DefaultSkin.cs b/osu.Game/Skinning/DefaultSkin.cs index 3de3dc7702..a9ab4e53c8 100644 --- a/osu.Game/Skinning/DefaultSkin.cs +++ b/osu.Game/Skinning/DefaultSkin.cs @@ -91,6 +91,26 @@ namespace osu.Game.Skinning } return null; + + case HUDSkinComponent hudComponent: + { + switch (hudComponent.Component) + { + case HUDSkinComponents.ComboCounter: + return new DefaultComboCounter(); + + case HUDSkinComponents.ScoreCounter: + return new DefaultScoreCounter(); + + case HUDSkinComponents.AccuracyCounter: + return new DefaultAccuracyCounter(); + + case HUDSkinComponents.HealthDisplay: + return new DefaultHealthDisplay(); + } + + return null; + } } return base.GetDrawableComponent(component); diff --git a/osu.Game/Skinning/SkinnableDrawable.cs b/osu.Game/Skinning/SkinnableDrawable.cs index 5a48bc4baf..77f7cf5c3f 100644 --- a/osu.Game/Skinning/SkinnableDrawable.cs +++ b/osu.Game/Skinning/SkinnableDrawable.cs @@ -42,7 +42,7 @@ namespace osu.Game.Skinning /// A function to create the default skin implementation of this element. /// A conditional to decide whether to allow fallback to the default implementation if a skinned element is not present. /// How (if at all) the should be resize to fit within our own bounds. - public SkinnableDrawable(ISkinComponent component, Func defaultImplementation, Func allowFallback = null, ConfineMode confineMode = ConfineMode.NoScaling) + public SkinnableDrawable(ISkinComponent component, Func defaultImplementation = null, Func allowFallback = null, ConfineMode confineMode = ConfineMode.NoScaling) : this(component, allowFallback, confineMode) { createDefault = defaultImplementation; @@ -68,7 +68,7 @@ namespace osu.Game.Skinning private bool isDefault; - protected virtual Drawable CreateDefault(ISkinComponent component) => createDefault(component); + protected virtual Drawable CreateDefault(ISkinComponent component) => createDefault?.Invoke(component) ?? Empty(); /// /// Whether to apply size restrictions (specified via ) to the default implementation. From 75227e5a70f21cb941e249c6e5d80b15b62e956a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 12 May 2021 18:55:48 +0900 Subject: [PATCH 157/205] Change default skin to use component lookup for conformity --- osu.Game/Skinning/DefaultSkin.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Skinning/DefaultSkin.cs b/osu.Game/Skinning/DefaultSkin.cs index a9ab4e53c8..2715c9fce2 100644 --- a/osu.Game/Skinning/DefaultSkin.cs +++ b/osu.Game/Skinning/DefaultSkin.cs @@ -80,10 +80,10 @@ namespace osu.Game.Skinning { Children = new Drawable[] { - new DefaultComboCounter(), - new DefaultScoreCounter(), - new DefaultAccuracyCounter(), - new DefaultHealthDisplay(), + GetDrawableComponent(new HUDSkinComponent(HUDSkinComponents.ComboCounter)), + GetDrawableComponent(new HUDSkinComponent(HUDSkinComponents.ScoreCounter)), + GetDrawableComponent(new HUDSkinComponent(HUDSkinComponents.AccuracyCounter)), + GetDrawableComponent(new HUDSkinComponent(HUDSkinComponents.HealthDisplay)), } }; From 55e1f97f5947dc8e1d65c14935b4a51eaae7c4ae Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 12 May 2021 19:06:28 +0900 Subject: [PATCH 158/205] Remove unused using statement --- .../Visual/Gameplay/TestSceneSkinnableAccuracyCounter.cs | 1 - osu.Game/Skinning/DefaultSkin.cs | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableAccuracyCounter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableAccuracyCounter.cs index d9139299ea..6f4e6a2420 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableAccuracyCounter.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableAccuracyCounter.cs @@ -7,7 +7,6 @@ using osu.Framework.Testing; using osu.Game.Rulesets; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Scoring; -using osu.Game.Screens.Play.HUD; using osu.Game.Skinning; namespace osu.Game.Tests.Visual.Gameplay diff --git a/osu.Game/Skinning/DefaultSkin.cs b/osu.Game/Skinning/DefaultSkin.cs index 2715c9fce2..3dcfbcda13 100644 --- a/osu.Game/Skinning/DefaultSkin.cs +++ b/osu.Game/Skinning/DefaultSkin.cs @@ -78,7 +78,7 @@ namespace osu.Game.Skinning } }) { - Children = new Drawable[] + Children = new[] { GetDrawableComponent(new HUDSkinComponent(HUDSkinComponents.ComboCounter)), GetDrawableComponent(new HUDSkinComponent(HUDSkinComponents.ScoreCounter)), From 7bac81f39487c446d515e5454f49b402680e38ca Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 12 May 2021 19:37:00 +0900 Subject: [PATCH 159/205] Fix incorrect inline comments Co-authored-by: Salman Ahmed --- osu.Game/Skinning/Editor/SkinBlueprint.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Skinning/Editor/SkinBlueprint.cs b/osu.Game/Skinning/Editor/SkinBlueprint.cs index 23411f2b3d..58fa255508 100644 --- a/osu.Game/Skinning/Editor/SkinBlueprint.cs +++ b/osu.Game/Skinning/Editor/SkinBlueprint.cs @@ -85,13 +85,13 @@ namespace osu.Game.Skinning.Editor protected override void OnSelected() { - // base logic hides selected blueprints when not selected, but timeline doesn't do that. + // base logic hides selected blueprints when not selected, but skin blueprints don't do that. updateSelectedState(); } protected override void OnDeselected() { - // base logic hides selected blueprints when not selected, but timeline doesn't do that. + // base logic hides selected blueprints when not selected, but skin blueprints don't do that. updateSelectedState(); } From 4464204e336688bcfce0266c2014ec20a7978e99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 12 May 2021 22:18:15 +0200 Subject: [PATCH 160/205] Mark all skin ctors used via reflection in `SkinInfo.CreateInstance()` --- osu.Game/Skinning/DefaultLegacySkin.cs | 2 ++ osu.Game/Skinning/DefaultSkin.cs | 2 ++ osu.Game/Skinning/LegacySkin.cs | 1 + 3 files changed, 5 insertions(+) diff --git a/osu.Game/Skinning/DefaultLegacySkin.cs b/osu.Game/Skinning/DefaultLegacySkin.cs index 564be8630e..7adf53e5e7 100644 --- a/osu.Game/Skinning/DefaultLegacySkin.cs +++ b/osu.Game/Skinning/DefaultLegacySkin.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 JetBrains.Annotations; using osu.Framework.IO.Stores; using osu.Game.IO; using osuTK.Graphics; @@ -9,6 +10,7 @@ namespace osu.Game.Skinning { public class DefaultLegacySkin : LegacySkin { + [UsedImplicitly(ImplicitUseKindFlags.InstantiatedWithFixedConstructorSignature)] public DefaultLegacySkin(IResourceStore storage, IStorageResourceProvider resources) : this(Info, storage, resources) { diff --git a/osu.Game/Skinning/DefaultSkin.cs b/osu.Game/Skinning/DefaultSkin.cs index fcd874d6ca..745bdf0bb2 100644 --- a/osu.Game/Skinning/DefaultSkin.cs +++ b/osu.Game/Skinning/DefaultSkin.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; +using JetBrains.Annotations; using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -20,6 +21,7 @@ namespace osu.Game.Skinning { } + [UsedImplicitly(ImplicitUseKindFlags.InstantiatedWithFixedConstructorSignature)] public DefaultSkin(SkinInfo skin, IStorageResourceProvider resources) : base(skin) { diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index eae3b69233..74250f9684 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -52,6 +52,7 @@ namespace osu.Game.Skinning private readonly Dictionary maniaConfigurations = new Dictionary(); + [UsedImplicitly(ImplicitUseKindFlags.InstantiatedWithFixedConstructorSignature)] public LegacySkin(SkinInfo skin, IStorageResourceProvider resources) : this(skin, new LegacySkinResourceStore(skin, resources.Files), resources, "skin.ini") { From 1b579dd83801849c33ce06a685eab48230cdd648 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 12 May 2021 22:42:26 +0200 Subject: [PATCH 161/205] Extract invariant instantiation info extension method --- osu.Game/Extensions/TypeExtensions.cs | 31 +++++++++++++++++++++++++++ osu.Game/Rulesets/Ruleset.cs | 3 ++- osu.Game/Rulesets/RulesetInfo.cs | 16 +------------- osu.Game/Skinning/SkinInfo.cs | 16 +------------- osu.Game/Skinning/SkinManager.cs | 3 ++- 5 files changed, 37 insertions(+), 32 deletions(-) create mode 100644 osu.Game/Extensions/TypeExtensions.cs diff --git a/osu.Game/Extensions/TypeExtensions.cs b/osu.Game/Extensions/TypeExtensions.cs new file mode 100644 index 0000000000..2e93c81758 --- /dev/null +++ b/osu.Game/Extensions/TypeExtensions.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; +using System.Linq; + +namespace osu.Game.Extensions +{ + internal static class TypeExtensions + { + /// + /// Returns 's + /// with the assembly version, culture and public key token values removed. + /// + /// + /// This method is usually used in extensibility scenarios (i.e. for custom rulesets or skins) + /// when a version-agnostic identifier associated with a C# class - potentially originating from + /// an external assembly - is needed. + /// Leaving only the type and assembly names in such a scenario allows to preserve compatibility + /// across assembly versions. + /// + internal static string GetInvariantInstantiationInfo(this Type type) + { + string assemblyQualifiedName = type.AssemblyQualifiedName; + if (assemblyQualifiedName == null) + throw new ArgumentException($"{type}'s assembly-qualified name is null. Ensure that it is a concrete type and not a generic type parameter.", nameof(type)); + + return string.Join(',', assemblyQualifiedName.Split(',').Take(2)); + } + } +} diff --git a/osu.Game/Rulesets/Ruleset.cs b/osu.Game/Rulesets/Ruleset.cs index 7f0c27adfc..7bdf84ace4 100644 --- a/osu.Game/Rulesets/Ruleset.cs +++ b/osu.Game/Rulesets/Ruleset.cs @@ -26,6 +26,7 @@ using JetBrains.Annotations; using osu.Framework.Extensions; using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Testing; +using osu.Game.Extensions; using osu.Game.Rulesets.Filter; using osu.Game.Screens.Ranking.Statistics; @@ -135,7 +136,7 @@ namespace osu.Game.Rulesets Name = Description, ShortName = ShortName, ID = (this as ILegacyRuleset)?.LegacyID, - InstantiationInfo = GetType().AssemblyQualifiedName, + InstantiationInfo = GetType().GetInvariantInstantiationInfo(), Available = true, }; } diff --git a/osu.Game/Rulesets/RulesetInfo.cs b/osu.Game/Rulesets/RulesetInfo.cs index d5aca8c650..702bf35fa8 100644 --- a/osu.Game/Rulesets/RulesetInfo.cs +++ b/osu.Game/Rulesets/RulesetInfo.cs @@ -3,7 +3,6 @@ using System; using System.Diagnostics.CodeAnalysis; -using System.Linq; using Newtonsoft.Json; using osu.Framework.Testing; @@ -18,20 +17,7 @@ namespace osu.Game.Rulesets public string ShortName { get; set; } - private string instantiationInfo; - - public string InstantiationInfo - { - get => instantiationInfo; - set => instantiationInfo = abbreviateInstantiationInfo(value); - } - - private string abbreviateInstantiationInfo(string value) - { - // exclude version onwards, matching only on namespace and type. - // this is mainly to allow for new versions of already loaded rulesets to "upgrade" from old. - return string.Join(',', value.Split(',').Take(2)); - } + public string InstantiationInfo { get; set; } [JsonIgnore] public bool Available { get; set; } diff --git a/osu.Game/Skinning/SkinInfo.cs b/osu.Game/Skinning/SkinInfo.cs index 2e29808cb4..a61a6dd1ce 100644 --- a/osu.Game/Skinning/SkinInfo.cs +++ b/osu.Game/Skinning/SkinInfo.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.Linq; using osu.Framework.IO.Stores; using osu.Game.Configuration; using osu.Game.Database; @@ -25,20 +24,7 @@ namespace osu.Game.Skinning public string Creator { get; set; } - private string instantiationInfo; - - public string InstantiationInfo - { - get => instantiationInfo; - set => instantiationInfo = abbreviateInstantiationInfo(value); - } - - private string abbreviateInstantiationInfo(string value) - { - // exclude version onwards, matching only on namespace and type. - // this is mainly to allow for new versions of already loaded rulesets to "upgrade" from old. - return string.Join(',', value.Split(',').Take(2)); - } + public string InstantiationInfo { get; set; } public virtual Skin CreateInstance(IResourceStore legacyDefaultResources, IStorageResourceProvider resources) { diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs index dbb9dcb7fc..b4051286aa 100644 --- a/osu.Game/Skinning/SkinManager.cs +++ b/osu.Game/Skinning/SkinManager.cs @@ -22,6 +22,7 @@ using osu.Framework.Testing; using osu.Framework.Utils; using osu.Game.Audio; using osu.Game.Database; +using osu.Game.Extensions; using osu.Game.IO; using osu.Game.IO.Archives; @@ -124,7 +125,7 @@ namespace osu.Game.Skinning var instance = GetSkin(model); - model.InstantiationInfo ??= instance.GetType().AssemblyQualifiedName; + model.InstantiationInfo ??= instance.GetType().GetInvariantInstantiationInfo(); if (model.Name?.Contains(".osk", StringComparison.OrdinalIgnoreCase) == true) populateMetadata(model, instance); From a6aec6e0074db91968c3526c4f9c556fba1c2329 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 12 May 2021 23:34:25 +0200 Subject: [PATCH 162/205] Fix missed `InstantiationInfo` setter usages --- osu.Game/Skinning/DefaultLegacySkin.cs | 3 ++- osu.Game/Skinning/SkinInfo.cs | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game/Skinning/DefaultLegacySkin.cs b/osu.Game/Skinning/DefaultLegacySkin.cs index 7adf53e5e7..b05c309e4e 100644 --- a/osu.Game/Skinning/DefaultLegacySkin.cs +++ b/osu.Game/Skinning/DefaultLegacySkin.cs @@ -3,6 +3,7 @@ using JetBrains.Annotations; using osu.Framework.IO.Stores; +using osu.Game.Extensions; using osu.Game.IO; using osuTK.Graphics; @@ -35,7 +36,7 @@ namespace osu.Game.Skinning ID = SkinInfo.CLASSIC_SKIN, // this is temporary until database storage is decided upon. Name = "osu!classic", Creator = "team osu!", - InstantiationInfo = typeof(DefaultLegacySkin).AssemblyQualifiedName, + InstantiationInfo = typeof(DefaultLegacySkin).GetInvariantInstantiationInfo() }; } } diff --git a/osu.Game/Skinning/SkinInfo.cs b/osu.Game/Skinning/SkinInfo.cs index a61a6dd1ce..bc57a8e71c 100644 --- a/osu.Game/Skinning/SkinInfo.cs +++ b/osu.Game/Skinning/SkinInfo.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using osu.Framework.IO.Stores; using osu.Game.Configuration; using osu.Game.Database; +using osu.Game.Extensions; using osu.Game.IO; namespace osu.Game.Skinning @@ -50,7 +51,7 @@ namespace osu.Game.Skinning ID = DEFAULT_SKIN, Name = "osu!lazer", Creator = "team osu!", - InstantiationInfo = typeof(DefaultSkin).AssemblyQualifiedName, + InstantiationInfo = typeof(DefaultSkin).GetInvariantInstantiationInfo() }; public bool Equals(SkinInfo other) => other != null && ID == other.ID; From 27ca7d0f4fa24f8837607dab5f3009459e92b940 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 12 May 2021 23:53:39 +0200 Subject: [PATCH 163/205] Actually annotate the correct ctor --- osu.Game/Skinning/DefaultLegacySkin.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Skinning/DefaultLegacySkin.cs b/osu.Game/Skinning/DefaultLegacySkin.cs index b05c309e4e..f30130b1fb 100644 --- a/osu.Game/Skinning/DefaultLegacySkin.cs +++ b/osu.Game/Skinning/DefaultLegacySkin.cs @@ -11,12 +11,12 @@ namespace osu.Game.Skinning { public class DefaultLegacySkin : LegacySkin { - [UsedImplicitly(ImplicitUseKindFlags.InstantiatedWithFixedConstructorSignature)] public DefaultLegacySkin(IResourceStore storage, IStorageResourceProvider resources) : this(Info, storage, resources) { } + [UsedImplicitly(ImplicitUseKindFlags.InstantiatedWithFixedConstructorSignature)] public DefaultLegacySkin(SkinInfo skin, IResourceStore storage, IStorageResourceProvider resources) : base(skin, storage, resources, string.Empty) { From 56bd8976666ee49318ab22d4dcfa86f5f7b627ae Mon Sep 17 00:00:00 2001 From: Naxess <30292137+Naxesss@users.noreply.github.com> Date: Thu, 13 May 2021 04:29:27 +0200 Subject: [PATCH 164/205] Move `ShowIssueTypes` to `VerifyScreen` --- osu.Game/Screens/Edit/Verify/IssueList.cs | 14 ++------------ osu.Game/Screens/Edit/Verify/VerifyScreen.cs | 8 ++++++++ osu.Game/Screens/Edit/Verify/VisibilitySection.cs | 4 ++-- 3 files changed, 12 insertions(+), 14 deletions(-) diff --git a/osu.Game/Screens/Edit/Verify/IssueList.cs b/osu.Game/Screens/Edit/Verify/IssueList.cs index befffdd9db..f2d3ef9dda 100644 --- a/osu.Game/Screens/Edit/Verify/IssueList.cs +++ b/osu.Game/Screens/Edit/Verify/IssueList.cs @@ -35,8 +35,6 @@ namespace osu.Game.Screens.Edit.Verify [Resolved] private VerifyScreen verify { get; set; } - public Dictionary> ShowType { get; set; } - public Bindable InterpretedDifficulty { get; set; } private IBeatmapVerifier rulesetVerifier; @@ -45,14 +43,6 @@ namespace osu.Game.Screens.Edit.Verify [BackgroundDependencyLoader] private void load(OverlayColourProvider colours) { - // Reflects the user interface. Only types in this dictionary have configurable visibility. - ShowType = new Dictionary> - { - { IssueType.Warning, new Bindable(true) }, - { IssueType.Error, new Bindable(true) }, - { IssueType.Negligible, new Bindable(false) } - }; - generalVerifier = new BeatmapVerifier(); rulesetVerifier = beatmap.BeatmapInfo.Ruleset?.CreateInstance()?.CreateBeatmapVerifier(); @@ -116,9 +106,9 @@ namespace osu.Game.Screens.Edit.Verify private IEnumerable filter(IEnumerable issues) { - foreach (IssueType issueType in ShowType.Keys) + foreach (var issueType in verify.ShowIssueType.Keys) { - if (!ShowType[issueType].Value) + if (!verify.ShowIssueType[issueType].Value) issues = issues.Where(issue => issue.Template.Type != issueType); } diff --git a/osu.Game/Screens/Edit/Verify/VerifyScreen.cs b/osu.Game/Screens/Edit/Verify/VerifyScreen.cs index afd0c8760d..cf1a471714 100644 --- a/osu.Game/Screens/Edit/Verify/VerifyScreen.cs +++ b/osu.Game/Screens/Edit/Verify/VerifyScreen.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -17,6 +18,13 @@ namespace osu.Game.Screens.Edit.Verify public readonly Bindable InterpretedDifficulty = new Bindable(); + public readonly Dictionary> ShowIssueType = new Dictionary> + { + { IssueType.Warning, new Bindable(true) }, + { IssueType.Error, new Bindable(true) }, + { IssueType.Negligible, new Bindable(false) } + }; + public IssueList IssueList { get; private set; } public VerifyScreen() diff --git a/osu.Game/Screens/Edit/Verify/VisibilitySection.cs b/osu.Game/Screens/Edit/Verify/VisibilitySection.cs index ce755bdcb4..71f3740d86 100644 --- a/osu.Game/Screens/Edit/Verify/VisibilitySection.cs +++ b/osu.Game/Screens/Edit/Verify/VisibilitySection.cs @@ -19,7 +19,7 @@ namespace osu.Game.Screens.Edit.Verify [BackgroundDependencyLoader] private void load(OverlayColourProvider colours) { - foreach (IssueType issueType in verify.IssueList.ShowType.Keys) + foreach (IssueType issueType in verify.ShowIssueType.Keys) { var checkbox = new SettingsCheckbox { @@ -28,7 +28,7 @@ namespace osu.Game.Screens.Edit.Verify LabelText = issueType.ToString() }; - checkbox.Current.BindTo(verify.IssueList.ShowType[issueType]); + checkbox.Current.BindTo(verify.ShowIssueType[issueType]); checkbox.Current.BindValueChanged(_ => verify.IssueList.Refresh()); Flow.Add(checkbox); } From 6806e40ad99b1bb6b561765d05858d11e6f5223a Mon Sep 17 00:00:00 2001 From: Naxess <30292137+Naxesss@users.noreply.github.com> Date: Thu, 13 May 2021 04:30:40 +0200 Subject: [PATCH 165/205] Remove unnecessary local variable This now exists in `VerifyScreen`, which we can access from here. --- osu.Game/Screens/Edit/Verify/IssueList.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/osu.Game/Screens/Edit/Verify/IssueList.cs b/osu.Game/Screens/Edit/Verify/IssueList.cs index f2d3ef9dda..19536401e9 100644 --- a/osu.Game/Screens/Edit/Verify/IssueList.cs +++ b/osu.Game/Screens/Edit/Verify/IssueList.cs @@ -35,8 +35,6 @@ namespace osu.Game.Screens.Edit.Verify [Resolved] private VerifyScreen verify { get; set; } - public Bindable InterpretedDifficulty { get; set; } - private IBeatmapVerifier rulesetVerifier; private BeatmapVerifier generalVerifier; @@ -46,8 +44,6 @@ namespace osu.Game.Screens.Edit.Verify generalVerifier = new BeatmapVerifier(); rulesetVerifier = beatmap.BeatmapInfo.Ruleset?.CreateInstance()?.CreateBeatmapVerifier(); - InterpretedDifficulty = verify.InterpretedDifficulty.GetBoundCopy(); - RelativeSizeAxes = Axes.Both; InternalChildren = new Drawable[] From dd8423c4c4350b5db811c8338d80235b2fc8a432 Mon Sep 17 00:00:00 2001 From: Naxess <30292137+Naxesss@users.noreply.github.com> Date: Thu, 13 May 2021 04:36:20 +0200 Subject: [PATCH 166/205] Set interpreted difficulty to correct default --- osu.Game/Screens/Edit/Verify/VerifyScreen.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Screens/Edit/Verify/VerifyScreen.cs b/osu.Game/Screens/Edit/Verify/VerifyScreen.cs index cf1a471714..963b77baa1 100644 --- a/osu.Game/Screens/Edit/Verify/VerifyScreen.cs +++ b/osu.Game/Screens/Edit/Verify/VerifyScreen.cs @@ -35,6 +35,9 @@ namespace osu.Game.Screens.Edit.Verify [BackgroundDependencyLoader] private void load() { + InterpretedDifficulty.Default = EditorBeatmap.BeatmapInfo.DifficultyRating; + InterpretedDifficulty.SetDefault(); + IssueList = new IssueList(); Child = new Container { From fbb76ba5989d10690411561dbec0d74b4c0151d6 Mon Sep 17 00:00:00 2001 From: Naxess <30292137+Naxesss@users.noreply.github.com> Date: Thu, 13 May 2021 04:50:32 +0200 Subject: [PATCH 167/205] Split `ShowIssueTypes` dict into hidden and configurable lists This way `VerifyScreen` is decoupled from which options `VisibilitySection` provides. Bindings are a bit less neat, though. --- osu.Game/Screens/Edit/Verify/IssueList.cs | 8 +------ osu.Game/Screens/Edit/Verify/VerifyScreen.cs | 8 +------ .../Screens/Edit/Verify/VisibilitySection.cs | 23 ++++++++++++++++--- 3 files changed, 22 insertions(+), 17 deletions(-) diff --git a/osu.Game/Screens/Edit/Verify/IssueList.cs b/osu.Game/Screens/Edit/Verify/IssueList.cs index 19536401e9..22d410592e 100644 --- a/osu.Game/Screens/Edit/Verify/IssueList.cs +++ b/osu.Game/Screens/Edit/Verify/IssueList.cs @@ -102,13 +102,7 @@ namespace osu.Game.Screens.Edit.Verify private IEnumerable filter(IEnumerable issues) { - foreach (var issueType in verify.ShowIssueType.Keys) - { - if (!verify.ShowIssueType[issueType].Value) - issues = issues.Where(issue => issue.Template.Type != issueType); - } - - return issues; + return issues.Where(issue => !verify.HiddenIssueTypes.Contains(issue.Template.Type)); } } } diff --git a/osu.Game/Screens/Edit/Verify/VerifyScreen.cs b/osu.Game/Screens/Edit/Verify/VerifyScreen.cs index 963b77baa1..6d7a4a72e2 100644 --- a/osu.Game/Screens/Edit/Verify/VerifyScreen.cs +++ b/osu.Game/Screens/Edit/Verify/VerifyScreen.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -18,12 +17,7 @@ namespace osu.Game.Screens.Edit.Verify public readonly Bindable InterpretedDifficulty = new Bindable(); - public readonly Dictionary> ShowIssueType = new Dictionary> - { - { IssueType.Warning, new Bindable(true) }, - { IssueType.Error, new Bindable(true) }, - { IssueType.Negligible, new Bindable(false) } - }; + public readonly BindableList HiddenIssueTypes = new BindableList { IssueType.Negligible }; public IssueList IssueList { get; private set; } diff --git a/osu.Game/Screens/Edit/Verify/VisibilitySection.cs b/osu.Game/Screens/Edit/Verify/VisibilitySection.cs index 71f3740d86..3698f51f66 100644 --- a/osu.Game/Screens/Edit/Verify/VisibilitySection.cs +++ b/osu.Game/Screens/Edit/Verify/VisibilitySection.cs @@ -14,12 +14,19 @@ namespace osu.Game.Screens.Edit.Verify [Resolved] private VerifyScreen verify { get; set; } + private readonly IssueType[] configurableIssueTypes = + { + IssueType.Warning, + IssueType.Error, + IssueType.Negligible + }; + protected override string Header => "Visibility"; [BackgroundDependencyLoader] private void load(OverlayColourProvider colours) { - foreach (IssueType issueType in verify.ShowIssueType.Keys) + foreach (IssueType issueType in configurableIssueTypes) { var checkbox = new SettingsCheckbox { @@ -28,8 +35,18 @@ namespace osu.Game.Screens.Edit.Verify LabelText = issueType.ToString() }; - checkbox.Current.BindTo(verify.ShowIssueType[issueType]); - checkbox.Current.BindValueChanged(_ => verify.IssueList.Refresh()); + checkbox.Current.Default = !verify.HiddenIssueTypes.Contains(issueType); + checkbox.Current.SetDefault(); + checkbox.Current.BindValueChanged(state => + { + if (!state.NewValue) + verify.HiddenIssueTypes.Add(issueType); + else + verify.HiddenIssueTypes.Remove(issueType); + + verify.IssueList.Refresh(); + }); + Flow.Add(checkbox); } } From 5b0309296812f93f50aef02444367f7698cddb8c Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 13 May 2021 11:53:50 +0900 Subject: [PATCH 168/205] Fix possible test failure --- osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs index 69fbd56490..bba7e2b391 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs @@ -74,7 +74,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("set user ready", () => client.ChangeState(MultiplayerUserState.Ready)); AddStep("delete beatmap", () => beatmaps.Delete(importedSet)); - AddAssert("user state is idle", () => client.LocalUser?.State == MultiplayerUserState.Idle); + AddUntilStep("user state is idle", () => client.LocalUser?.State == MultiplayerUserState.Idle); } [Test] From c8d21f2c3fe65f4c5add3e1429c907a370a61b6a Mon Sep 17 00:00:00 2001 From: Naxess <30292137+Naxesss@users.noreply.github.com> Date: Thu, 13 May 2021 05:25:02 +0200 Subject: [PATCH 169/205] Isolate refreshing to `IssueList` --- osu.Game/Screens/Edit/Verify/IssueList.cs | 6 +++--- osu.Game/Screens/Edit/Verify/VisibilitySection.cs | 2 -- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Edit/Verify/IssueList.cs b/osu.Game/Screens/Edit/Verify/IssueList.cs index 22d410592e..7eba50498c 100644 --- a/osu.Game/Screens/Edit/Verify/IssueList.cs +++ b/osu.Game/Screens/Edit/Verify/IssueList.cs @@ -69,7 +69,7 @@ namespace osu.Game.Screens.Edit.Verify new TriangleButton { Text = "Refresh", - Action = Refresh, + Action = refresh, Size = new Vector2(120, 40), Anchor = Anchor.BottomRight, Origin = Anchor.BottomRight, @@ -83,10 +83,10 @@ namespace osu.Game.Screens.Edit.Verify { base.LoadComplete(); - Refresh(); + verify.HiddenIssueTypes.BindCollectionChanged((o, s) => refresh(), runOnceImmediately: true); } - public void Refresh() + private void refresh() { var issues = generalVerifier.Run(beatmap, workingBeatmap.Value); diff --git a/osu.Game/Screens/Edit/Verify/VisibilitySection.cs b/osu.Game/Screens/Edit/Verify/VisibilitySection.cs index 3698f51f66..7ec3ecee07 100644 --- a/osu.Game/Screens/Edit/Verify/VisibilitySection.cs +++ b/osu.Game/Screens/Edit/Verify/VisibilitySection.cs @@ -43,8 +43,6 @@ namespace osu.Game.Screens.Edit.Verify verify.HiddenIssueTypes.Add(issueType); else verify.HiddenIssueTypes.Remove(issueType); - - verify.IssueList.Refresh(); }); Flow.Add(checkbox); From e86834b74060d5fc7e2e9314142316b74ccf821b Mon Sep 17 00:00:00 2001 From: Naxess <30292137+Naxesss@users.noreply.github.com> Date: Thu, 13 May 2021 05:25:20 +0200 Subject: [PATCH 170/205] Use local bound copy for `HiddenIssueTypes` --- osu.Game/Screens/Edit/Verify/VisibilitySection.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Edit/Verify/VisibilitySection.cs b/osu.Game/Screens/Edit/Verify/VisibilitySection.cs index 7ec3ecee07..48bf0523b1 100644 --- a/osu.Game/Screens/Edit/Verify/VisibilitySection.cs +++ b/osu.Game/Screens/Edit/Verify/VisibilitySection.cs @@ -26,6 +26,8 @@ namespace osu.Game.Screens.Edit.Verify [BackgroundDependencyLoader] private void load(OverlayColourProvider colours) { + var hiddenIssueTypes = verify.HiddenIssueTypes.GetBoundCopy(); + foreach (IssueType issueType in configurableIssueTypes) { var checkbox = new SettingsCheckbox @@ -35,14 +37,14 @@ namespace osu.Game.Screens.Edit.Verify LabelText = issueType.ToString() }; - checkbox.Current.Default = !verify.HiddenIssueTypes.Contains(issueType); + checkbox.Current.Default = !hiddenIssueTypes.Contains(issueType); checkbox.Current.SetDefault(); checkbox.Current.BindValueChanged(state => { if (!state.NewValue) - verify.HiddenIssueTypes.Add(issueType); + hiddenIssueTypes.Add(issueType); else - verify.HiddenIssueTypes.Remove(issueType); + hiddenIssueTypes.Remove(issueType); }); Flow.Add(checkbox); From 04c1585eb24ef0fa07333bc354c7122dc728e8df Mon Sep 17 00:00:00 2001 From: Naxess <30292137+Naxesss@users.noreply.github.com> Date: Thu, 13 May 2021 05:38:45 +0200 Subject: [PATCH 171/205] Use more consistent lambda discards --- osu.Game/Screens/Edit/Verify/IssueList.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Verify/IssueList.cs b/osu.Game/Screens/Edit/Verify/IssueList.cs index 7eba50498c..03cd22ad54 100644 --- a/osu.Game/Screens/Edit/Verify/IssueList.cs +++ b/osu.Game/Screens/Edit/Verify/IssueList.cs @@ -83,7 +83,7 @@ namespace osu.Game.Screens.Edit.Verify { base.LoadComplete(); - verify.HiddenIssueTypes.BindCollectionChanged((o, s) => refresh(), runOnceImmediately: true); + verify.HiddenIssueTypes.BindCollectionChanged((_, __) => refresh(), runOnceImmediately: true); } private void refresh() From e80d8f69220509dc78a9fbdedd9eef797c27c7f1 Mon Sep 17 00:00:00 2001 From: Naxess <30292137+Naxesss@users.noreply.github.com> Date: Thu, 13 May 2021 05:46:47 +0200 Subject: [PATCH 172/205] Keep track of local bound copy --- osu.Game/Screens/Edit/Verify/VisibilitySection.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Verify/VisibilitySection.cs b/osu.Game/Screens/Edit/Verify/VisibilitySection.cs index 48bf0523b1..cb7deb8566 100644 --- a/osu.Game/Screens/Edit/Verify/VisibilitySection.cs +++ b/osu.Game/Screens/Edit/Verify/VisibilitySection.cs @@ -2,6 +2,7 @@ // 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.Game.Overlays; using osu.Game.Overlays.Settings; @@ -21,12 +22,14 @@ namespace osu.Game.Screens.Edit.Verify IssueType.Negligible }; + private BindableList hiddenIssueTypes; + protected override string Header => "Visibility"; [BackgroundDependencyLoader] private void load(OverlayColourProvider colours) { - var hiddenIssueTypes = verify.HiddenIssueTypes.GetBoundCopy(); + hiddenIssueTypes = verify.HiddenIssueTypes.GetBoundCopy(); foreach (IssueType issueType in configurableIssueTypes) { From 6caf4e38790298f24837a0764fe5bfa778118674 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 May 2021 12:57:28 +0900 Subject: [PATCH 173/205] Add xmldoc to `SkinnableInfo` --- osu.Game/Screens/Play/HUD/SkinnableInfo.cs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/SkinnableInfo.cs b/osu.Game/Screens/Play/HUD/SkinnableInfo.cs index 2de3f1b729..2e9235df97 100644 --- a/osu.Game/Screens/Play/HUD/SkinnableInfo.cs +++ b/osu.Game/Screens/Play/HUD/SkinnableInfo.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using Newtonsoft.Json; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Extensions; @@ -33,10 +34,15 @@ namespace osu.Game.Screens.Play.HUD public List Children { get; } = new List(); + [JsonConstructor] public SkinnableInfo() { } + /// + /// Construct a new instance populating all attributes from the provided drawable. + /// + /// The drawable which attributes should be sourced from. public SkinnableInfo(Drawable component) { Type = component.GetType(); @@ -47,13 +53,17 @@ namespace osu.Game.Screens.Play.HUD Anchor = component.Anchor; Origin = component.Origin; - if (component is Container container) + if (component is Container container) { - foreach (var child in container.Children.OfType().OfType()) + foreach (var child in container.OfType().OfType()) Children.Add(child.CreateSerialisedInformation()); } } + /// + /// Construct an instance of the drawable with all attributes applied. + /// + /// The new instance. public Drawable CreateInstance() { Drawable d = (Drawable)Activator.CreateInstance(Type); From ee0a6ba93e25df5425480f0427826b182055f6c4 Mon Sep 17 00:00:00 2001 From: Naxess <30292137+Naxesss@users.noreply.github.com> Date: Thu, 13 May 2021 05:59:49 +0200 Subject: [PATCH 174/205] Use local bound copy in `InterpretationSection` as well Else we're relying on the `VerifyScreen`'s bindable instance, and by extension the `VerifyScreen` instance itself. --- osu.Game/Screens/Edit/Verify/InterpretationSection.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Verify/InterpretationSection.cs b/osu.Game/Screens/Edit/Verify/InterpretationSection.cs index 7991786542..1bdb528473 100644 --- a/osu.Game/Screens/Edit/Verify/InterpretationSection.cs +++ b/osu.Game/Screens/Edit/Verify/InterpretationSection.cs @@ -2,6 +2,7 @@ // 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.Game.Beatmaps; using osu.Game.Overlays.Settings; @@ -15,9 +16,13 @@ namespace osu.Game.Screens.Edit.Verify protected override string Header => "Interpretation"; + private Bindable interpretedDifficulty; + [BackgroundDependencyLoader] private void load() { + interpretedDifficulty = verify.InterpretedDifficulty.GetBoundCopy(); + var dropdown = new SettingsEnumDropdown { Anchor = Anchor.CentreLeft, @@ -25,7 +30,7 @@ namespace osu.Game.Screens.Edit.Verify TooltipText = "Affects checks that depend on difficulty level" }; - dropdown.Current.BindTo(verify.InterpretedDifficulty); + dropdown.Current.BindTo(interpretedDifficulty); Flow.Add(dropdown); } From fb305130deb9ba12229f36bb436635a04ac646d5 Mon Sep 17 00:00:00 2001 From: Naxess <30292137+Naxesss@users.noreply.github.com> Date: Thu, 13 May 2021 06:00:21 +0200 Subject: [PATCH 175/205] Also refresh when interpreted difficulty changes --- osu.Game/Screens/Edit/Verify/IssueList.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Verify/IssueList.cs b/osu.Game/Screens/Edit/Verify/IssueList.cs index 03cd22ad54..407c1e3bc7 100644 --- a/osu.Game/Screens/Edit/Verify/IssueList.cs +++ b/osu.Game/Screens/Edit/Verify/IssueList.cs @@ -83,7 +83,10 @@ namespace osu.Game.Screens.Edit.Verify { base.LoadComplete(); - verify.HiddenIssueTypes.BindCollectionChanged((_, __) => refresh(), runOnceImmediately: true); + verify.InterpretedDifficulty.BindValueChanged(_ => refresh()); + verify.HiddenIssueTypes.BindCollectionChanged((_, __) => refresh()); + + refresh(); } private void refresh() From a38cb61b085329cf1712899674ace8c8de68a2d0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 May 2021 13:02:55 +0900 Subject: [PATCH 176/205] Remove duplicated call to `base.GetDrawableComponent` --- osu.Game/Skinning/DefaultSkin.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Skinning/DefaultSkin.cs b/osu.Game/Skinning/DefaultSkin.cs index 55f34ba1c6..d6ca5e9009 100644 --- a/osu.Game/Skinning/DefaultSkin.cs +++ b/osu.Game/Skinning/DefaultSkin.cs @@ -92,10 +92,10 @@ namespace osu.Game.Skinning return skinnableTargetWrapper; } - return null; + break; } - return base.GetDrawableComponent(component); + return null; } public override IBindable GetConfig(TLookup lookup) From 2bf8635ffd5d44210b6d6638482c089bbd3f611a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 May 2021 13:03:23 +0900 Subject: [PATCH 177/205] Move field upwards in class --- osu.Game/Skinning/Editor/SkinBlueprintContainer.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Skinning/Editor/SkinBlueprintContainer.cs b/osu.Game/Skinning/Editor/SkinBlueprintContainer.cs index 9890b8e8b6..09bbf7ffd5 100644 --- a/osu.Game/Skinning/Editor/SkinBlueprintContainer.cs +++ b/osu.Game/Skinning/Editor/SkinBlueprintContainer.cs @@ -19,6 +19,8 @@ namespace osu.Game.Skinning.Editor { private readonly Drawable target; + private readonly List> targetComponents = new List>(); + public SkinBlueprintContainer(Drawable target) { this.target = target; @@ -30,8 +32,6 @@ namespace osu.Game.Skinning.Editor SelectedItems.BindTo(editor.SelectedComponents); } - private readonly List> targetComponents = new List>(); - protected override void LoadComplete() { base.LoadComplete(); From 469a7f5d2a07c434aface5cff1a1fdae987c5072 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 May 2021 13:04:17 +0900 Subject: [PATCH 178/205] Reorder fields in `SkinEditor` --- osu.Game/Skinning/Editor/SkinEditor.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game/Skinning/Editor/SkinEditor.cs b/osu.Game/Skinning/Editor/SkinEditor.cs index a00557ee4e..72dacad310 100644 --- a/osu.Game/Skinning/Editor/SkinEditor.cs +++ b/osu.Game/Skinning/Editor/SkinEditor.cs @@ -22,19 +22,19 @@ namespace osu.Game.Skinning.Editor { public const double TRANSITION_DURATION = 500; + public readonly BindableList SelectedComponents = new BindableList(); + + protected override bool StartHidden => true; + private readonly Drawable targetScreen; private OsuTextFlowContainer headerText; - protected override bool StartHidden => true; - - public readonly BindableList SelectedComponents = new BindableList(); + private Bindable currentSkin; [Resolved] private SkinManager skins { get; set; } - private Bindable currentSkin; - [Resolved] private OsuColour colours { get; set; } From 992a052426da407f794bb3d241d30338d51a8ce8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 May 2021 13:07:06 +0900 Subject: [PATCH 179/205] Remove stray comment --- osu.Game/Skinning/Skin.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game/Skinning/Skin.cs b/osu.Game/Skinning/Skin.cs index 10481e4efd..1fb1e3b99f 100644 --- a/osu.Game/Skinning/Skin.cs +++ b/osu.Game/Skinning/Skin.cs @@ -39,8 +39,6 @@ namespace osu.Game.Skinning { SkinInfo = skin; - // may be null for default skin. - // we may want to move this to some kind of async operation in the future. foreach (SkinnableTarget skinnableTarget in Enum.GetValues(typeof(SkinnableTarget))) { From 47948d7b34c860e17d73579ac4ee6e91d69e3506 Mon Sep 17 00:00:00 2001 From: Naxess <30292137+Naxesss@users.noreply.github.com> Date: Thu, 13 May 2021 06:08:48 +0200 Subject: [PATCH 180/205] Set default for bindable in object initializer Fixes the CI failure. --- osu.Game/Screens/Edit/Verify/VisibilitySection.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Edit/Verify/VisibilitySection.cs b/osu.Game/Screens/Edit/Verify/VisibilitySection.cs index cb7deb8566..f942621d2a 100644 --- a/osu.Game/Screens/Edit/Verify/VisibilitySection.cs +++ b/osu.Game/Screens/Edit/Verify/VisibilitySection.cs @@ -37,10 +37,10 @@ namespace osu.Game.Screens.Edit.Verify { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - LabelText = issueType.ToString() + LabelText = issueType.ToString(), + Current = { Default = !hiddenIssueTypes.Contains(issueType) } }; - checkbox.Current.Default = !hiddenIssueTypes.Contains(issueType); checkbox.Current.SetDefault(); checkbox.Current.BindValueChanged(state => { From c93ed541f3571aa3d9991949d75f29b74fa46def Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 May 2021 13:09:33 +0900 Subject: [PATCH 181/205] Add xmldoc and tidy up logic in `Skin` --- osu.Game/Skinning/Skin.cs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/osu.Game/Skinning/Skin.cs b/osu.Game/Skinning/Skin.cs index 1fb1e3b99f..64c848fbfa 100644 --- a/osu.Game/Skinning/Skin.cs +++ b/osu.Game/Skinning/Skin.cs @@ -61,11 +61,19 @@ namespace osu.Game.Skinning } } + /// + /// Remove all stored customisations for the provided target. + /// + /// The target container to reset. public void ResetDrawableTarget(SkinnableElementTargetContainer targetContainer) { DrawableComponentInfo.Remove(targetContainer.Target); } + /// + /// Update serialised information for the provided target. + /// + /// The target container to serialise to this skin. public void UpdateDrawableTarget(SkinnableElementTargetContainer targetContainer) { DrawableComponentInfo[targetContainer.Target] = targetContainer.CreateSerialisedChildren().ToArray(); @@ -76,10 +84,7 @@ namespace osu.Game.Skinning switch (component) { case SkinnableTargetComponent target: - - var skinnableTarget = target.Target; - - if (!DrawableComponentInfo.TryGetValue(skinnableTarget, out var skinnableInfo)) + if (!DrawableComponentInfo.TryGetValue(target.Target, out var skinnableInfo)) return null; return new SkinnableTargetWrapper From 581e7940c7331ab89f45cc6234089417c1e5bdd6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 May 2021 13:13:22 +0900 Subject: [PATCH 182/205] Add xmldoc to `SkinnableElementTargetContainer` --- osu.Game/Skinning/SkinnableElementTargetContainer.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/osu.Game/Skinning/SkinnableElementTargetContainer.cs b/osu.Game/Skinning/SkinnableElementTargetContainer.cs index b900fdf3e0..f447800a33 100644 --- a/osu.Game/Skinning/SkinnableElementTargetContainer.cs +++ b/osu.Game/Skinning/SkinnableElementTargetContainer.cs @@ -26,6 +26,9 @@ namespace osu.Game.Skinning Target = target; } + /// + /// Reload all components in this container from the current skin. + /// public void Reload() { ClearInternal(); @@ -43,6 +46,12 @@ namespace osu.Game.Skinning } } + /// + /// Add a new skinnable component to this target. + /// + /// The component to add. + /// Thrown when attempting to add an element to a target which is not supported by the current skin. + /// Thrown if the provided instance is not a . public void Add(ISkinnableComponent component) { if (content == null) From 3b862798e985edc8ff51ba99b4d2961ccdede31b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 May 2021 13:14:49 +0900 Subject: [PATCH 183/205] Standardise naming of methods related to SkinnableInfo --- osu.Game/Extensions/DrawableExtensions.cs | 4 ++-- osu.Game/Screens/Play/HUD/SkinnableInfo.cs | 4 ++-- osu.Game/Skinning/Skin.cs | 2 +- osu.Game/Skinning/SkinnableElementTargetContainer.cs | 7 +++++-- 4 files changed, 10 insertions(+), 7 deletions(-) diff --git a/osu.Game/Extensions/DrawableExtensions.cs b/osu.Game/Extensions/DrawableExtensions.cs index 0ec96a876e..2ac6e6ff22 100644 --- a/osu.Game/Extensions/DrawableExtensions.cs +++ b/osu.Game/Extensions/DrawableExtensions.cs @@ -46,9 +46,9 @@ namespace osu.Game.Extensions public static Vector2 ScreenSpaceDeltaToParentSpace(this Drawable drawable, Vector2 delta) => drawable.Parent.ToLocalSpace(drawable.Parent.ToScreenSpace(Vector2.Zero) + delta); - public static SkinnableInfo CreateSerialisedInformation(this Drawable component) => new SkinnableInfo(component); + public static SkinnableInfo CreateSkinnableInfo(this Drawable component) => new SkinnableInfo(component); - public static void ApplySerialisedInformation(this Drawable component, SkinnableInfo info) + public static void ApplySkinnableInfo(this Drawable component, SkinnableInfo info) { // todo: can probably make this better via deserialisation directly using a common interface. component.Position = info.Position; diff --git a/osu.Game/Screens/Play/HUD/SkinnableInfo.cs b/osu.Game/Screens/Play/HUD/SkinnableInfo.cs index 2e9235df97..678885d096 100644 --- a/osu.Game/Screens/Play/HUD/SkinnableInfo.cs +++ b/osu.Game/Screens/Play/HUD/SkinnableInfo.cs @@ -56,7 +56,7 @@ namespace osu.Game.Screens.Play.HUD if (component is Container container) { foreach (var child in container.OfType().OfType()) - Children.Add(child.CreateSerialisedInformation()); + Children.Add(child.CreateSkinnableInfo()); } } @@ -67,7 +67,7 @@ namespace osu.Game.Screens.Play.HUD public Drawable CreateInstance() { Drawable d = (Drawable)Activator.CreateInstance(Type); - d.ApplySerialisedInformation(this); + d.ApplySkinnableInfo(this); return d; } } diff --git a/osu.Game/Skinning/Skin.cs b/osu.Game/Skinning/Skin.cs index 64c848fbfa..4890524b90 100644 --- a/osu.Game/Skinning/Skin.cs +++ b/osu.Game/Skinning/Skin.cs @@ -76,7 +76,7 @@ namespace osu.Game.Skinning /// The target container to serialise to this skin. public void UpdateDrawableTarget(SkinnableElementTargetContainer targetContainer) { - DrawableComponentInfo[targetContainer.Target] = targetContainer.CreateSerialisedChildren().ToArray(); + DrawableComponentInfo[targetContainer.Target] = targetContainer.CreateSkinnableInfo().ToArray(); } public virtual Drawable GetDrawableComponent(ISkinComponent component) diff --git a/osu.Game/Skinning/SkinnableElementTargetContainer.cs b/osu.Game/Skinning/SkinnableElementTargetContainer.cs index f447800a33..2aea8c0281 100644 --- a/osu.Game/Skinning/SkinnableElementTargetContainer.cs +++ b/osu.Game/Skinning/SkinnableElementTargetContainer.cs @@ -64,8 +64,11 @@ namespace osu.Game.Skinning components.Add(component); } - public IEnumerable CreateSerialisedChildren() => - components.Select(d => ((Drawable)d).CreateSerialisedInformation()); + /// + /// Serialise all children as . + /// + /// The serialised content. + public IEnumerable CreateSkinnableInfo() => components.Select(d => ((Drawable)d).CreateSkinnableInfo()); protected override void SkinChanged(ISkinSource skin, bool allowFallback) { From db19617b8b69a9b9102b8594ed34d881f36d5ede Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 May 2021 13:16:20 +0900 Subject: [PATCH 184/205] Add `JsonConstructor` attribute to `SkinnableTargetWrapper` --- osu.Game/Skinning/SkinnableTargetWrapper.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Skinning/SkinnableTargetWrapper.cs b/osu.Game/Skinning/SkinnableTargetWrapper.cs index 0d16775f51..2e2ce32a6f 100644 --- a/osu.Game/Skinning/SkinnableTargetWrapper.cs +++ b/osu.Game/Skinning/SkinnableTargetWrapper.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using Newtonsoft.Json; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -11,6 +12,7 @@ namespace osu.Game.Skinning /// A container which is serialised and can encapsulate multiple skinnable elements into a single return type (for consumption via . /// Will also optionally apply default cross-element layout dependencies when initialised from a non-deserialised source. /// + [Serializable] public class SkinnableTargetWrapper : Container, ISkinSerialisable { private readonly Action applyDefaults; @@ -25,6 +27,7 @@ namespace osu.Game.Skinning this.applyDefaults = applyDefaults; } + [JsonConstructor] public SkinnableTargetWrapper() { RelativeSizeAxes = Axes.Both; From 23e284b8b31cc59189382d16cb06a72cf16fb00d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 May 2021 13:34:03 +0900 Subject: [PATCH 185/205] Change default skin editor shortcut to Ctrl+Shift+S Avoids a conflict with song select's random rewind functionality. As mentioned in #12776. --- osu.Game/Input/Bindings/GlobalActionContainer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs index ce945f3bf8..c8227c0887 100644 --- a/osu.Game/Input/Bindings/GlobalActionContainer.cs +++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs @@ -48,7 +48,7 @@ namespace osu.Game.Input.Bindings new KeyBinding(new[] { InputKey.Control, InputKey.O }, GlobalAction.ToggleSettings), new KeyBinding(new[] { InputKey.Control, InputKey.D }, GlobalAction.ToggleBeatmapListing), new KeyBinding(new[] { InputKey.Control, InputKey.N }, GlobalAction.ToggleNotifications), - new KeyBinding(new[] { InputKey.Shift, InputKey.F2 }, GlobalAction.ToggleSkinEditor), + new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.S }, GlobalAction.ToggleSkinEditor), new KeyBinding(InputKey.Escape, GlobalAction.Back), new KeyBinding(InputKey.ExtraMouseButton1, GlobalAction.Back), From cdcbaf4291e4bbf20b552a305244be92336dbe80 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 May 2021 13:45:10 +0900 Subject: [PATCH 186/205] Tidy up specification of `SettingsSection` --- .../Screens/Edit/EditorRoundedScreenSettingsSection.cs | 7 ++++--- osu.Game/Screens/Edit/Verify/InterpretationSection.cs | 2 +- osu.Game/Screens/Edit/Verify/VisibilitySection.cs | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Edit/EditorRoundedScreenSettingsSection.cs b/osu.Game/Screens/Edit/EditorRoundedScreenSettingsSection.cs index 87ed98a439..e17114ebcb 100644 --- a/osu.Game/Screens/Edit/EditorRoundedScreenSettingsSection.cs +++ b/osu.Game/Screens/Edit/EditorRoundedScreenSettingsSection.cs @@ -15,8 +15,9 @@ namespace osu.Game.Screens.Edit { private const int header_height = 50; - protected FillFlowContainer Flow; - protected abstract string Header { get; } + protected abstract string HeaderText { get; } + + protected FillFlowContainer Flow { get; private set; } [BackgroundDependencyLoader] private void load(OverlayColourProvider colours) @@ -36,7 +37,7 @@ namespace osu.Game.Screens.Edit { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - Text = Header, + Text = HeaderText, Font = new FontUsage(size: 25, weight: "bold") } }, diff --git a/osu.Game/Screens/Edit/Verify/InterpretationSection.cs b/osu.Game/Screens/Edit/Verify/InterpretationSection.cs index 1bdb528473..4dca94aacf 100644 --- a/osu.Game/Screens/Edit/Verify/InterpretationSection.cs +++ b/osu.Game/Screens/Edit/Verify/InterpretationSection.cs @@ -14,7 +14,7 @@ namespace osu.Game.Screens.Edit.Verify [Resolved] private VerifyScreen verify { get; set; } - protected override string Header => "Interpretation"; + protected override string HeaderText => "Interpretation"; private Bindable interpretedDifficulty; diff --git a/osu.Game/Screens/Edit/Verify/VisibilitySection.cs b/osu.Game/Screens/Edit/Verify/VisibilitySection.cs index f942621d2a..7ebfe586a7 100644 --- a/osu.Game/Screens/Edit/Verify/VisibilitySection.cs +++ b/osu.Game/Screens/Edit/Verify/VisibilitySection.cs @@ -24,7 +24,7 @@ namespace osu.Game.Screens.Edit.Verify private BindableList hiddenIssueTypes; - protected override string Header => "Visibility"; + protected override string HeaderText => "Visibility"; [BackgroundDependencyLoader] private void load(OverlayColourProvider colours) From c6648112e59f8a5c3429af35f72b99d8ceceb2c2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 May 2021 13:51:41 +0900 Subject: [PATCH 187/205] Simplify binding flow in `InterpretationSection` --- .../Screens/Edit/Verify/InterpretationSection.cs | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/osu.Game/Screens/Edit/Verify/InterpretationSection.cs b/osu.Game/Screens/Edit/Verify/InterpretationSection.cs index 4dca94aacf..7de87737f5 100644 --- a/osu.Game/Screens/Edit/Verify/InterpretationSection.cs +++ b/osu.Game/Screens/Edit/Verify/InterpretationSection.cs @@ -2,7 +2,6 @@ // 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.Game.Beatmaps; using osu.Game.Overlays.Settings; @@ -16,23 +15,16 @@ namespace osu.Game.Screens.Edit.Verify protected override string HeaderText => "Interpretation"; - private Bindable interpretedDifficulty; - [BackgroundDependencyLoader] private void load() { - interpretedDifficulty = verify.InterpretedDifficulty.GetBoundCopy(); - - var dropdown = new SettingsEnumDropdown + Flow.Add(new SettingsEnumDropdown { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - TooltipText = "Affects checks that depend on difficulty level" - }; - - dropdown.Current.BindTo(interpretedDifficulty); - - Flow.Add(dropdown); + TooltipText = "Affects checks that depend on difficulty level", + Current = verify.InterpretedDifficulty.GetBoundCopy() + }); } } } From b81f86bd4d9c088dce4a7c49fb16b768ccc3e669 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 May 2021 13:53:27 +0900 Subject: [PATCH 188/205] Move DI resolution to inside BDL parameters --- osu.Game/Screens/Edit/Verify/InterpretationSection.cs | 5 +---- osu.Game/Screens/Edit/Verify/VisibilitySection.cs | 5 +---- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/osu.Game/Screens/Edit/Verify/InterpretationSection.cs b/osu.Game/Screens/Edit/Verify/InterpretationSection.cs index 7de87737f5..9548f8aaa9 100644 --- a/osu.Game/Screens/Edit/Verify/InterpretationSection.cs +++ b/osu.Game/Screens/Edit/Verify/InterpretationSection.cs @@ -10,13 +10,10 @@ namespace osu.Game.Screens.Edit.Verify { internal class InterpretationSection : EditorRoundedScreenSettingsSection { - [Resolved] - private VerifyScreen verify { get; set; } - protected override string HeaderText => "Interpretation"; [BackgroundDependencyLoader] - private void load() + private void load(VerifyScreen verify) { Flow.Add(new SettingsEnumDropdown { diff --git a/osu.Game/Screens/Edit/Verify/VisibilitySection.cs b/osu.Game/Screens/Edit/Verify/VisibilitySection.cs index 7ebfe586a7..d049436376 100644 --- a/osu.Game/Screens/Edit/Verify/VisibilitySection.cs +++ b/osu.Game/Screens/Edit/Verify/VisibilitySection.cs @@ -12,9 +12,6 @@ namespace osu.Game.Screens.Edit.Verify { internal class VisibilitySection : EditorRoundedScreenSettingsSection { - [Resolved] - private VerifyScreen verify { get; set; } - private readonly IssueType[] configurableIssueTypes = { IssueType.Warning, @@ -27,7 +24,7 @@ namespace osu.Game.Screens.Edit.Verify protected override string HeaderText => "Visibility"; [BackgroundDependencyLoader] - private void load(OverlayColourProvider colours) + private void load(OverlayColourProvider colours, VerifyScreen verify) { hiddenIssueTypes = verify.HiddenIssueTypes.GetBoundCopy(); From e0e9106921fc1b4c30c19c56645e19cd696f455f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 May 2021 14:54:52 +0900 Subject: [PATCH 189/205] Enable autoplay in skin editor tests --- osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs | 2 ++ .../Visual/Gameplay/TestSceneSkinEditorMultipleSkins.cs | 9 ++++----- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs index 73da76df78..a0b27755b7 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs @@ -19,6 +19,8 @@ namespace osu.Game.Tests.Visual.Gameplay [Resolved] private SkinManager skinManager { get; set; } + protected override bool Autoplay => true; + [SetUpSteps] public override void SetUpSteps() { diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditorMultipleSkins.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditorMultipleSkins.cs index c7c93b8892..245e190b1f 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditorMultipleSkins.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditorMultipleSkins.cs @@ -1,13 +1,11 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Testing; using osu.Game.Rulesets; -using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Scoring; using osu.Game.Screens.Play; @@ -32,12 +30,13 @@ namespace osu.Game.Tests.Visual.Gameplay SetContents(() => { var ruleset = new OsuRuleset(); + var mods = new[] { ruleset.GetAutoplayMod() }; var working = CreateWorkingBeatmap(ruleset.RulesetInfo); - var beatmap = working.GetPlayableBeatmap(ruleset.RulesetInfo); + var beatmap = working.GetPlayableBeatmap(ruleset.RulesetInfo, mods); - var drawableRuleset = ruleset.CreateDrawableRulesetWith(beatmap); + var drawableRuleset = ruleset.CreateDrawableRulesetWith(beatmap, mods); - var hudOverlay = new HUDOverlay(drawableRuleset, Array.Empty()) + var hudOverlay = new HUDOverlay(drawableRuleset, mods) { Anchor = Anchor.Centre, Origin = Anchor.Centre, From 5818ed4c8c66bb749a83e700c16fde831ba56b4c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 May 2021 16:41:36 +0900 Subject: [PATCH 190/205] Remove unused DI resolution --- osu.Game/Skinning/LegacyAccuracyCounter.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/osu.Game/Skinning/LegacyAccuracyCounter.cs b/osu.Game/Skinning/LegacyAccuracyCounter.cs index 3eea5d22d5..493107172f 100644 --- a/osu.Game/Skinning/LegacyAccuracyCounter.cs +++ b/osu.Game/Skinning/LegacyAccuracyCounter.cs @@ -12,9 +12,6 @@ namespace osu.Game.Skinning { public class LegacyAccuracyCounter : GameplayAccuracyCounter, ISkinnableComponent { - [Resolved] - private ISkinSource skin { get; set; } - public LegacyAccuracyCounter() { Anchor = Anchor.TopRight; From 19223ba01359b87ebb55b838d47fe6846fd58db4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 May 2021 16:42:13 +0900 Subject: [PATCH 191/205] Remove left-over debug logging --- osu.Game/Skinning/SkinManager.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs index 91f3d0c7cf..fbe23482d7 100644 --- a/osu.Game/Skinning/SkinManager.cs +++ b/osu.Game/Skinning/SkinManager.cs @@ -194,9 +194,6 @@ namespace osu.Game.Skinning ReplaceFile(skin.SkinInfo, oldFile, streamContent, oldFile.Filename); else AddFile(skin.SkinInfo, streamContent, filename); - - Logger.Log($"Saving out {filename} with {json.Length} bytes"); - Logger.Log(json); } } } From 9dfa48b22ed523f67bc4b29e7a497127b28d6280 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 May 2021 16:42:43 +0900 Subject: [PATCH 192/205] Fix incorrect exception text --- osu.Game/Skinning/SkinnableElementTargetContainer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Skinning/SkinnableElementTargetContainer.cs b/osu.Game/Skinning/SkinnableElementTargetContainer.cs index 2aea8c0281..c68dce4109 100644 --- a/osu.Game/Skinning/SkinnableElementTargetContainer.cs +++ b/osu.Game/Skinning/SkinnableElementTargetContainer.cs @@ -58,7 +58,7 @@ namespace osu.Game.Skinning throw new NotSupportedException("Attempting to add a new component to a target container which is not supported by the current skin."); if (!(component is Drawable drawable)) - throw new ArgumentException("Provided argument must be of type {nameof(ISkinnableComponent)}.", nameof(drawable)); + throw new ArgumentException($"Provided argument must be of type {nameof(Drawable)}.", nameof(drawable)); content.Add(drawable); components.Add(component); From dd6a06a302a7f38c3889829068589b1260cf944c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 May 2021 16:43:42 +0900 Subject: [PATCH 193/205] Reword xmldoc to read better --- osu.Game/Skinning/SkinnableTargetWrapper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Skinning/SkinnableTargetWrapper.cs b/osu.Game/Skinning/SkinnableTargetWrapper.cs index 2e2ce32a6f..d8ad008448 100644 --- a/osu.Game/Skinning/SkinnableTargetWrapper.cs +++ b/osu.Game/Skinning/SkinnableTargetWrapper.cs @@ -20,7 +20,7 @@ namespace osu.Game.Skinning /// /// Construct a wrapper with defaults that should be applied once. /// - /// A function with default to apply after the initial layout (ie. consuming autosize) + /// A function to apply the default layout. public SkinnableTargetWrapper(Action applyDefaults) : this() { From cdcd31b546ac41806c0f7559d65dd9b24e6323d1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 May 2021 17:03:17 +0900 Subject: [PATCH 194/205] Replace `ISkinSerialisable` with `IsEditable` property --- osu.Game/Screens/Play/HUD/SkinnableInfo.cs | 4 ++-- .../Skinning/Editor/SkinBlueprintContainer.cs | 8 ++++++++ osu.Game/Skinning/Editor/SkinComponentToolbox.cs | 3 +++ osu.Game/Skinning/ISkinSerialisable.cs | 15 --------------- osu.Game/Skinning/ISkinnableComponent.cs | 8 +++++++- osu.Game/Skinning/SkinManager.cs | 1 - osu.Game/Skinning/SkinnableTargetWrapper.cs | 4 +++- 7 files changed, 23 insertions(+), 20 deletions(-) delete mode 100644 osu.Game/Skinning/ISkinSerialisable.cs diff --git a/osu.Game/Screens/Play/HUD/SkinnableInfo.cs b/osu.Game/Screens/Play/HUD/SkinnableInfo.cs index 678885d096..a7c5c33f7d 100644 --- a/osu.Game/Screens/Play/HUD/SkinnableInfo.cs +++ b/osu.Game/Screens/Play/HUD/SkinnableInfo.cs @@ -15,7 +15,7 @@ using osuTK; namespace osu.Game.Screens.Play.HUD { /// - /// Serialised information governing custom changes to an . + /// Serialised information governing custom changes to an . /// [Serializable] public class SkinnableInfo : IJsonSerializable @@ -55,7 +55,7 @@ namespace osu.Game.Screens.Play.HUD if (component is Container container) { - foreach (var child in container.OfType().OfType()) + foreach (var child in container.OfType().OfType()) Children.Add(child.CreateSkinnableInfo()); } } diff --git a/osu.Game/Skinning/Editor/SkinBlueprintContainer.cs b/osu.Game/Skinning/Editor/SkinBlueprintContainer.cs index 09bbf7ffd5..31f89ad0c2 100644 --- a/osu.Game/Skinning/Editor/SkinBlueprintContainer.cs +++ b/osu.Game/Skinning/Editor/SkinBlueprintContainer.cs @@ -81,6 +81,14 @@ namespace osu.Game.Skinning.Editor } } + protected override void AddBlueprintFor(ISkinnableComponent item) + { + if (!item.IsEditable) + return; + + base.AddBlueprintFor(item); + } + protected override SelectionHandler CreateSelectionHandler() => new SkinSelectionHandler(); protected override SelectionBlueprint CreateBlueprintFor(ISkinnableComponent component) diff --git a/osu.Game/Skinning/Editor/SkinComponentToolbox.cs b/osu.Game/Skinning/Editor/SkinComponentToolbox.cs index a000204062..068b2058a6 100644 --- a/osu.Game/Skinning/Editor/SkinComponentToolbox.cs +++ b/osu.Game/Skinning/Editor/SkinComponentToolbox.cs @@ -78,6 +78,9 @@ namespace osu.Game.Skinning.Editor Debug.Assert(instance != null); + if (!((ISkinnableComponent)instance).IsEditable) + return null; + return new ToolboxComponentButton(instance); } catch diff --git a/osu.Game/Skinning/ISkinSerialisable.cs b/osu.Game/Skinning/ISkinSerialisable.cs deleted file mode 100644 index d1777512af..0000000000 --- a/osu.Game/Skinning/ISkinSerialisable.cs +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Framework.Graphics; - -namespace osu.Game.Skinning -{ - /// - /// Denotes a drawable component which should be serialised as part of a skin. - /// Use for components which should be mutable by the user / editor. - /// - public interface ISkinSerialisable : IDrawable - { - } -} diff --git a/osu.Game/Skinning/ISkinnableComponent.cs b/osu.Game/Skinning/ISkinnableComponent.cs index e44c7be13d..65ac2d5849 100644 --- a/osu.Game/Skinning/ISkinnableComponent.cs +++ b/osu.Game/Skinning/ISkinnableComponent.cs @@ -1,12 +1,18 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.Graphics; + namespace osu.Game.Skinning { /// /// Denotes a drawable which, as a drawable, can be adjusted via skinning specifications. /// - public interface ISkinnableComponent : ISkinSerialisable + public interface ISkinnableComponent : IDrawable { + /// + /// Whether this component should be editable by an end user. + /// + bool IsEditable => true; } } diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs index fbe23482d7..7b27a7a816 100644 --- a/osu.Game/Skinning/SkinManager.cs +++ b/osu.Game/Skinning/SkinManager.cs @@ -19,7 +19,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.OpenGL.Textures; using osu.Framework.Graphics.Textures; using osu.Framework.IO.Stores; -using osu.Framework.Logging; using osu.Framework.Platform; using osu.Framework.Testing; using osu.Framework.Utils; diff --git a/osu.Game/Skinning/SkinnableTargetWrapper.cs b/osu.Game/Skinning/SkinnableTargetWrapper.cs index d8ad008448..28e8d585fc 100644 --- a/osu.Game/Skinning/SkinnableTargetWrapper.cs +++ b/osu.Game/Skinning/SkinnableTargetWrapper.cs @@ -13,8 +13,10 @@ namespace osu.Game.Skinning /// Will also optionally apply default cross-element layout dependencies when initialised from a non-deserialised source. /// [Serializable] - public class SkinnableTargetWrapper : Container, ISkinSerialisable + public class SkinnableTargetWrapper : Container, ISkinnableComponent { + public bool IsEditable => false; + private readonly Action applyDefaults; /// From 7921dc7ece64f04d37e8e96630a4d0f677d29fe9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 May 2021 17:06:00 +0900 Subject: [PATCH 195/205] Rename `ISkinnableComponent` to `ISkinnableDrawable` --- .../Play/HUD/DefaultAccuracyCounter.cs | 2 +- .../Screens/Play/HUD/DefaultComboCounter.cs | 2 +- .../Screens/Play/HUD/DefaultHealthDisplay.cs | 2 +- .../Screens/Play/HUD/DefaultScoreCounter.cs | 2 +- .../Screens/Play/HUD/LegacyComboCounter.cs | 2 +- osu.Game/Screens/Play/HUD/SkinnableInfo.cs | 4 ++-- osu.Game/Skinning/Editor/SkinBlueprint.cs | 4 ++-- .../Skinning/Editor/SkinBlueprintContainer.cs | 20 +++++++++---------- .../Skinning/Editor/SkinComponentToolbox.cs | 4 ++-- osu.Game/Skinning/Editor/SkinEditor.cs | 4 ++-- .../Skinning/Editor/SkinSelectionHandler.cs | 10 +++++----- ...ableComponent.cs => ISkinnableDrawable.cs} | 2 +- osu.Game/Skinning/ISkinnableTarget.cs | 6 +++--- osu.Game/Skinning/LegacyAccuracyCounter.cs | 2 +- osu.Game/Skinning/LegacyHealthDisplay.cs | 2 +- osu.Game/Skinning/LegacyScoreCounter.cs | 2 +- .../SkinnableElementTargetContainer.cs | 8 ++++---- osu.Game/Skinning/SkinnableTargetWrapper.cs | 2 +- 18 files changed, 40 insertions(+), 40 deletions(-) rename osu.Game/Skinning/{ISkinnableComponent.cs => ISkinnableDrawable.cs} (90%) diff --git a/osu.Game/Screens/Play/HUD/DefaultAccuracyCounter.cs b/osu.Game/Screens/Play/HUD/DefaultAccuracyCounter.cs index d8dff89b29..45ba05e036 100644 --- a/osu.Game/Screens/Play/HUD/DefaultAccuracyCounter.cs +++ b/osu.Game/Screens/Play/HUD/DefaultAccuracyCounter.cs @@ -7,7 +7,7 @@ using osu.Game.Skinning; namespace osu.Game.Screens.Play.HUD { - public class DefaultAccuracyCounter : GameplayAccuracyCounter, ISkinnableComponent + public class DefaultAccuracyCounter : GameplayAccuracyCounter, ISkinnableDrawable { [Resolved(canBeNull: true)] private HUDOverlay hud { get; set; } diff --git a/osu.Game/Screens/Play/HUD/DefaultComboCounter.cs b/osu.Game/Screens/Play/HUD/DefaultComboCounter.cs index 13bd045fc0..c4575c5ad0 100644 --- a/osu.Game/Screens/Play/HUD/DefaultComboCounter.cs +++ b/osu.Game/Screens/Play/HUD/DefaultComboCounter.cs @@ -12,7 +12,7 @@ using osu.Game.Skinning; namespace osu.Game.Screens.Play.HUD { - public class DefaultComboCounter : RollingCounter, ISkinnableComponent + public class DefaultComboCounter : RollingCounter, ISkinnableDrawable { [Resolved(canBeNull: true)] private HUDOverlay hud { get; set; } diff --git a/osu.Game/Screens/Play/HUD/DefaultHealthDisplay.cs b/osu.Game/Screens/Play/HUD/DefaultHealthDisplay.cs index 241777244b..ed297f0ffc 100644 --- a/osu.Game/Screens/Play/HUD/DefaultHealthDisplay.cs +++ b/osu.Game/Screens/Play/HUD/DefaultHealthDisplay.cs @@ -17,7 +17,7 @@ using osu.Game.Skinning; namespace osu.Game.Screens.Play.HUD { - public class DefaultHealthDisplay : HealthDisplay, IHasAccentColour, ISkinnableComponent + public class DefaultHealthDisplay : HealthDisplay, IHasAccentColour, ISkinnableDrawable { /// /// The base opacity of the glow. diff --git a/osu.Game/Screens/Play/HUD/DefaultScoreCounter.cs b/osu.Game/Screens/Play/HUD/DefaultScoreCounter.cs index bd18050dfb..16e3642181 100644 --- a/osu.Game/Screens/Play/HUD/DefaultScoreCounter.cs +++ b/osu.Game/Screens/Play/HUD/DefaultScoreCounter.cs @@ -8,7 +8,7 @@ using osu.Game.Skinning; namespace osu.Game.Screens.Play.HUD { - public class DefaultScoreCounter : GameplayScoreCounter, ISkinnableComponent + public class DefaultScoreCounter : GameplayScoreCounter, ISkinnableDrawable { public DefaultScoreCounter() : base(6) diff --git a/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs b/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs index 2565faf423..d64513d41e 100644 --- a/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs +++ b/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs @@ -15,7 +15,7 @@ namespace osu.Game.Screens.Play.HUD /// /// Uses the 'x' symbol and has a pop-out effect while rolling over. /// - public class LegacyComboCounter : CompositeDrawable, ISkinnableComponent + public class LegacyComboCounter : CompositeDrawable, ISkinnableDrawable { public Bindable Current { get; } = new BindableInt { MinValue = 0, }; diff --git a/osu.Game/Screens/Play/HUD/SkinnableInfo.cs b/osu.Game/Screens/Play/HUD/SkinnableInfo.cs index a7c5c33f7d..e08044b14c 100644 --- a/osu.Game/Screens/Play/HUD/SkinnableInfo.cs +++ b/osu.Game/Screens/Play/HUD/SkinnableInfo.cs @@ -15,7 +15,7 @@ using osuTK; namespace osu.Game.Screens.Play.HUD { /// - /// Serialised information governing custom changes to an . + /// Serialised information governing custom changes to an . /// [Serializable] public class SkinnableInfo : IJsonSerializable @@ -55,7 +55,7 @@ namespace osu.Game.Screens.Play.HUD if (component is Container container) { - foreach (var child in container.OfType().OfType()) + foreach (var child in container.OfType().OfType()) Children.Add(child.CreateSkinnableInfo()); } } diff --git a/osu.Game/Skinning/Editor/SkinBlueprint.cs b/osu.Game/Skinning/Editor/SkinBlueprint.cs index b8dfdbad0a..4be9299699 100644 --- a/osu.Game/Skinning/Editor/SkinBlueprint.cs +++ b/osu.Game/Skinning/Editor/SkinBlueprint.cs @@ -13,7 +13,7 @@ using osuTK; namespace osu.Game.Skinning.Editor { - public class SkinBlueprint : SelectionBlueprint + public class SkinBlueprint : SelectionBlueprint { private Container box; @@ -26,7 +26,7 @@ namespace osu.Game.Skinning.Editor protected override bool ShouldBeAlive => (drawable.IsAlive && Item.IsPresent) || (AlwaysShowWhenSelected && State == SelectionState.Selected); - public SkinBlueprint(ISkinnableComponent component) + public SkinBlueprint(ISkinnableDrawable component) : base(component) { } diff --git a/osu.Game/Skinning/Editor/SkinBlueprintContainer.cs b/osu.Game/Skinning/Editor/SkinBlueprintContainer.cs index 31f89ad0c2..c0cc2ab40e 100644 --- a/osu.Game/Skinning/Editor/SkinBlueprintContainer.cs +++ b/osu.Game/Skinning/Editor/SkinBlueprintContainer.cs @@ -15,11 +15,11 @@ using osu.Game.Screens.Edit.Compose.Components; namespace osu.Game.Skinning.Editor { - public class SkinBlueprintContainer : BlueprintContainer + public class SkinBlueprintContainer : BlueprintContainer { private readonly Drawable target; - private readonly List> targetComponents = new List>(); + private readonly List> targetComponents = new List>(); public SkinBlueprintContainer(Drawable target) { @@ -49,7 +49,7 @@ namespace osu.Game.Skinning.Editor foreach (var targetContainer in targetContainers) { - var bindableList = new BindableList { BindTarget = targetContainer.Components }; + var bindableList = new BindableList { BindTarget = targetContainer.Components }; bindableList.BindCollectionChanged(componentsChanged, true); targetComponents.Add(bindableList); @@ -61,27 +61,27 @@ namespace osu.Game.Skinning.Editor switch (e.Action) { case NotifyCollectionChangedAction.Add: - foreach (var item in e.NewItems.Cast()) + foreach (var item in e.NewItems.Cast()) AddBlueprintFor(item); break; case NotifyCollectionChangedAction.Remove: case NotifyCollectionChangedAction.Reset: - foreach (var item in e.OldItems.Cast()) + foreach (var item in e.OldItems.Cast()) RemoveBlueprintFor(item); break; case NotifyCollectionChangedAction.Replace: - foreach (var item in e.OldItems.Cast()) + foreach (var item in e.OldItems.Cast()) RemoveBlueprintFor(item); - foreach (var item in e.NewItems.Cast()) + foreach (var item in e.NewItems.Cast()) AddBlueprintFor(item); break; } } - protected override void AddBlueprintFor(ISkinnableComponent item) + protected override void AddBlueprintFor(ISkinnableDrawable item) { if (!item.IsEditable) return; @@ -89,9 +89,9 @@ namespace osu.Game.Skinning.Editor base.AddBlueprintFor(item); } - protected override SelectionHandler CreateSelectionHandler() => new SkinSelectionHandler(); + protected override SelectionHandler CreateSelectionHandler() => new SkinSelectionHandler(); - protected override SelectionBlueprint CreateBlueprintFor(ISkinnableComponent component) + protected override SelectionBlueprint CreateBlueprintFor(ISkinnableDrawable component) => new SkinBlueprint(component); } } diff --git a/osu.Game/Skinning/Editor/SkinComponentToolbox.cs b/osu.Game/Skinning/Editor/SkinComponentToolbox.cs index 068b2058a6..8536cba139 100644 --- a/osu.Game/Skinning/Editor/SkinComponentToolbox.cs +++ b/osu.Game/Skinning/Editor/SkinComponentToolbox.cs @@ -56,7 +56,7 @@ namespace osu.Game.Skinning.Editor Spacing = new Vector2(20) }; - var skinnableTypes = typeof(OsuGame).Assembly.GetTypes().Where(t => typeof(ISkinnableComponent).IsAssignableFrom(t)).ToArray(); + var skinnableTypes = typeof(OsuGame).Assembly.GetTypes().Where(t => typeof(ISkinnableDrawable).IsAssignableFrom(t)).ToArray(); foreach (var type in skinnableTypes) { @@ -78,7 +78,7 @@ namespace osu.Game.Skinning.Editor Debug.Assert(instance != null); - if (!((ISkinnableComponent)instance).IsEditable) + if (!((ISkinnableDrawable)instance).IsEditable) return null; return new ToolboxComponentButton(instance); diff --git a/osu.Game/Skinning/Editor/SkinEditor.cs b/osu.Game/Skinning/Editor/SkinEditor.cs index 72dacad310..bf9a6464d0 100644 --- a/osu.Game/Skinning/Editor/SkinEditor.cs +++ b/osu.Game/Skinning/Editor/SkinEditor.cs @@ -22,7 +22,7 @@ namespace osu.Game.Skinning.Editor { public const double TRANSITION_DURATION = 500; - public readonly BindableList SelectedComponents = new BindableList(); + public readonly BindableList SelectedComponents = new BindableList(); protected override bool StartHidden => true; @@ -165,7 +165,7 @@ namespace osu.Game.Skinning.Editor private void placeComponent(Type type) { - if (!(Activator.CreateInstance(type) is ISkinnableComponent component)) + if (!(Activator.CreateInstance(type) is ISkinnableDrawable component)) throw new InvalidOperationException("Attempted to instantiate a component for placement which was not an {typeof(ISkinnableComponent)}."); getTarget(SkinnableTarget.MainHUDComponents)?.Add(component); diff --git a/osu.Game/Skinning/Editor/SkinSelectionHandler.cs b/osu.Game/Skinning/Editor/SkinSelectionHandler.cs index cf5ece03e9..8ca98c794f 100644 --- a/osu.Game/Skinning/Editor/SkinSelectionHandler.cs +++ b/osu.Game/Skinning/Editor/SkinSelectionHandler.cs @@ -14,7 +14,7 @@ using osuTK; namespace osu.Game.Skinning.Editor { - public class SkinSelectionHandler : SelectionHandler + public class SkinSelectionHandler : SelectionHandler { public override bool HandleRotation(float angle) { @@ -36,7 +36,7 @@ namespace osu.Game.Skinning.Editor return true; } - public override bool HandleMovement(MoveSelectionEvent moveEvent) + public override bool HandleMovement(MoveSelectionEvent moveEvent) { foreach (var c in SelectedBlueprints) { @@ -57,7 +57,7 @@ namespace osu.Game.Skinning.Editor SelectionBox.CanReverse = false; } - protected override void DeleteItems(IEnumerable items) + protected override void DeleteItems(IEnumerable items) { foreach (var i in items) { @@ -66,7 +66,7 @@ namespace osu.Game.Skinning.Editor } } - protected override IEnumerable GetContextMenuItemsForSelection(IEnumerable> selection) + protected override IEnumerable GetContextMenuItemsForSelection(IEnumerable> selection) { yield return new OsuMenuItem("Anchor") { @@ -131,7 +131,7 @@ namespace osu.Game.Skinning.Editor public class AnchorMenuItem : TernaryStateMenuItem { - public AnchorMenuItem(Anchor anchor, IEnumerable> selection, Action action) + public AnchorMenuItem(Anchor anchor, IEnumerable> selection, Action action) : base(anchor.ToString(), getNextState, MenuItemType.Standard, action) { } diff --git a/osu.Game/Skinning/ISkinnableComponent.cs b/osu.Game/Skinning/ISkinnableDrawable.cs similarity index 90% rename from osu.Game/Skinning/ISkinnableComponent.cs rename to osu.Game/Skinning/ISkinnableDrawable.cs index 65ac2d5849..d42b6f71b0 100644 --- a/osu.Game/Skinning/ISkinnableComponent.cs +++ b/osu.Game/Skinning/ISkinnableDrawable.cs @@ -8,7 +8,7 @@ namespace osu.Game.Skinning /// /// Denotes a drawable which, as a drawable, can be adjusted via skinning specifications. /// - public interface ISkinnableComponent : IDrawable + public interface ISkinnableDrawable : IDrawable { /// /// Whether this component should be editable by an end user. diff --git a/osu.Game/Skinning/ISkinnableTarget.cs b/osu.Game/Skinning/ISkinnableTarget.cs index ab3c24a1e2..3773b5be24 100644 --- a/osu.Game/Skinning/ISkinnableTarget.cs +++ b/osu.Game/Skinning/ISkinnableTarget.cs @@ -6,7 +6,7 @@ using osu.Framework.Bindables; namespace osu.Game.Skinning { /// - /// Denotes a container which can house s. + /// Denotes a container which can house s. /// public interface ISkinnableTarget { @@ -18,7 +18,7 @@ namespace osu.Game.Skinning /// /// A bindable list of components which are being tracked by this skinnable target. /// - IBindableList Components { get; } + IBindableList Components { get; } /// /// Reload this target from the current skin. @@ -28,6 +28,6 @@ namespace osu.Game.Skinning /// /// Add the provided item to this target. /// - void Add(ISkinnableComponent drawable); + void Add(ISkinnableDrawable drawable); } } diff --git a/osu.Game/Skinning/LegacyAccuracyCounter.cs b/osu.Game/Skinning/LegacyAccuracyCounter.cs index 493107172f..16562d9571 100644 --- a/osu.Game/Skinning/LegacyAccuracyCounter.cs +++ b/osu.Game/Skinning/LegacyAccuracyCounter.cs @@ -10,7 +10,7 @@ using osuTK; namespace osu.Game.Skinning { - public class LegacyAccuracyCounter : GameplayAccuracyCounter, ISkinnableComponent + public class LegacyAccuracyCounter : GameplayAccuracyCounter, ISkinnableDrawable { public LegacyAccuracyCounter() { diff --git a/osu.Game/Skinning/LegacyHealthDisplay.cs b/osu.Game/Skinning/LegacyHealthDisplay.cs index dfb6ca1a64..c601adc3a0 100644 --- a/osu.Game/Skinning/LegacyHealthDisplay.cs +++ b/osu.Game/Skinning/LegacyHealthDisplay.cs @@ -16,7 +16,7 @@ using osuTK.Graphics; namespace osu.Game.Skinning { - public class LegacyHealthDisplay : HealthDisplay, ISkinnableComponent + public class LegacyHealthDisplay : HealthDisplay, ISkinnableDrawable { private const double epic_cutoff = 0.5; diff --git a/osu.Game/Skinning/LegacyScoreCounter.cs b/osu.Game/Skinning/LegacyScoreCounter.cs index 8aa48da453..64ea03d59c 100644 --- a/osu.Game/Skinning/LegacyScoreCounter.cs +++ b/osu.Game/Skinning/LegacyScoreCounter.cs @@ -8,7 +8,7 @@ using osuTK; namespace osu.Game.Skinning { - public class LegacyScoreCounter : GameplayScoreCounter, ISkinnableComponent + public class LegacyScoreCounter : GameplayScoreCounter, ISkinnableDrawable { protected override double RollingDuration => 1000; protected override Easing RollingEasing => Easing.Out; diff --git a/osu.Game/Skinning/SkinnableElementTargetContainer.cs b/osu.Game/Skinning/SkinnableElementTargetContainer.cs index c68dce4109..2a76d3bf7c 100644 --- a/osu.Game/Skinning/SkinnableElementTargetContainer.cs +++ b/osu.Game/Skinning/SkinnableElementTargetContainer.cs @@ -17,9 +17,9 @@ namespace osu.Game.Skinning public SkinnableTarget Target { get; } - public IBindableList Components => components; + public IBindableList Components => components; - private readonly BindableList components = new BindableList(); + private readonly BindableList components = new BindableList(); public SkinnableElementTargetContainer(SkinnableTarget target) { @@ -41,7 +41,7 @@ namespace osu.Game.Skinning LoadComponentAsync(content, wrapper => { AddInternal(wrapper); - components.AddRange(wrapper.Children.OfType()); + components.AddRange(wrapper.Children.OfType()); }); } } @@ -52,7 +52,7 @@ namespace osu.Game.Skinning /// The component to add. /// Thrown when attempting to add an element to a target which is not supported by the current skin. /// Thrown if the provided instance is not a . - public void Add(ISkinnableComponent component) + public void Add(ISkinnableDrawable component) { if (content == null) throw new NotSupportedException("Attempting to add a new component to a target container which is not supported by the current skin."); diff --git a/osu.Game/Skinning/SkinnableTargetWrapper.cs b/osu.Game/Skinning/SkinnableTargetWrapper.cs index 28e8d585fc..58ecac71ab 100644 --- a/osu.Game/Skinning/SkinnableTargetWrapper.cs +++ b/osu.Game/Skinning/SkinnableTargetWrapper.cs @@ -13,7 +13,7 @@ namespace osu.Game.Skinning /// Will also optionally apply default cross-element layout dependencies when initialised from a non-deserialised source. /// [Serializable] - public class SkinnableTargetWrapper : Container, ISkinnableComponent + public class SkinnableTargetWrapper : Container, ISkinnableDrawable { public bool IsEditable => false; From 106fa97a11080329a6097430590866d827dfe1a5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 May 2021 17:07:38 +0900 Subject: [PATCH 196/205] Rename `SkinnableElementTargetContainer` to `SkinnableTargetContainer` --- osu.Game/Screens/Play/HUDOverlay.cs | 4 ++-- osu.Game/Skinning/Editor/SkinEditor.cs | 4 ++-- osu.Game/Skinning/Skin.cs | 4 ++-- ...eElementTargetContainer.cs => SkinnableTargetContainer.cs} | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) rename osu.Game/Skinning/{SkinnableElementTargetContainer.cs => SkinnableTargetContainer.cs} (94%) diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index 887346b5df..a10e91dae8 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -68,7 +68,7 @@ namespace osu.Game.Screens.Play private bool holdingForHUD; - private readonly SkinnableElementTargetContainer mainComponents; + private readonly SkinnableTargetContainer mainComponents; private IEnumerable hideTargets => new Drawable[] { visibilityContainer, KeyCounter, topRightElements }; @@ -97,7 +97,7 @@ namespace osu.Game.Screens.Play RelativeSizeAxes = Axes.Both, Children = new Drawable[] { - mainComponents = new SkinnableElementTargetContainer(SkinnableTarget.MainHUDComponents) + mainComponents = new SkinnableTargetContainer(SkinnableTarget.MainHUDComponents) { RelativeSizeAxes = Axes.Both, }, diff --git a/osu.Game/Skinning/Editor/SkinEditor.cs b/osu.Game/Skinning/Editor/SkinEditor.cs index bf9a6464d0..67285987ef 100644 --- a/osu.Game/Skinning/Editor/SkinEditor.cs +++ b/osu.Game/Skinning/Editor/SkinEditor.cs @@ -181,7 +181,7 @@ namespace osu.Game.Skinning.Editor private void revert() { - SkinnableElementTargetContainer[] targetContainers = targetScreen.ChildrenOfType().ToArray(); + SkinnableTargetContainer[] targetContainers = targetScreen.ChildrenOfType().ToArray(); foreach (var t in targetContainers) { @@ -194,7 +194,7 @@ namespace osu.Game.Skinning.Editor private void save() { - SkinnableElementTargetContainer[] targetContainers = targetScreen.ChildrenOfType().ToArray(); + SkinnableTargetContainer[] targetContainers = targetScreen.ChildrenOfType().ToArray(); foreach (var t in targetContainers) currentSkin.Value.UpdateDrawableTarget(t); diff --git a/osu.Game/Skinning/Skin.cs b/osu.Game/Skinning/Skin.cs index 4890524b90..3a16cb5818 100644 --- a/osu.Game/Skinning/Skin.cs +++ b/osu.Game/Skinning/Skin.cs @@ -65,7 +65,7 @@ namespace osu.Game.Skinning /// Remove all stored customisations for the provided target. /// /// The target container to reset. - public void ResetDrawableTarget(SkinnableElementTargetContainer targetContainer) + public void ResetDrawableTarget(SkinnableTargetContainer targetContainer) { DrawableComponentInfo.Remove(targetContainer.Target); } @@ -74,7 +74,7 @@ namespace osu.Game.Skinning /// Update serialised information for the provided target. /// /// The target container to serialise to this skin. - public void UpdateDrawableTarget(SkinnableElementTargetContainer targetContainer) + public void UpdateDrawableTarget(SkinnableTargetContainer targetContainer) { DrawableComponentInfo[targetContainer.Target] = targetContainer.CreateSkinnableInfo().ToArray(); } diff --git a/osu.Game/Skinning/SkinnableElementTargetContainer.cs b/osu.Game/Skinning/SkinnableTargetContainer.cs similarity index 94% rename from osu.Game/Skinning/SkinnableElementTargetContainer.cs rename to osu.Game/Skinning/SkinnableTargetContainer.cs index 2a76d3bf7c..daf54277fd 100644 --- a/osu.Game/Skinning/SkinnableElementTargetContainer.cs +++ b/osu.Game/Skinning/SkinnableTargetContainer.cs @@ -11,7 +11,7 @@ using osu.Game.Screens.Play.HUD; namespace osu.Game.Skinning { - public class SkinnableElementTargetContainer : SkinReloadableDrawable, ISkinnableTarget + public class SkinnableTargetContainer : SkinReloadableDrawable, ISkinnableTarget { private SkinnableTargetWrapper content; @@ -21,7 +21,7 @@ namespace osu.Game.Skinning private readonly BindableList components = new BindableList(); - public SkinnableElementTargetContainer(SkinnableTarget target) + public SkinnableTargetContainer(SkinnableTarget target) { Target = target; } From 0959e7156a27a29b4a7a8fa041e418e4cc84f577 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 May 2021 17:22:05 +0900 Subject: [PATCH 197/205] Remove outdated TODO --- osu.Game/Skinning/SkinManager.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs index 7b27a7a816..5793edda30 100644 --- a/osu.Game/Skinning/SkinManager.cs +++ b/osu.Game/Skinning/SkinManager.cs @@ -180,7 +180,6 @@ namespace osu.Game.Skinning foreach (var drawableInfo in skin.DrawableComponentInfo) { - // todo: the OfType() call can be removed with better IDrawable support. string json = JsonConvert.SerializeObject(drawableInfo.Value, new JsonSerializerSettings { Formatting = Formatting.Indented }); using (var streamContent = new MemoryStream(Encoding.UTF8.GetBytes(json))) From 3ea469813c8cc95396d020a1f145cf0d2c67040e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 May 2021 17:25:51 +0900 Subject: [PATCH 198/205] Use interface in place of `SkinnableTargetContainer` --- osu.Game/Skinning/ISkinnableTarget.cs | 11 +++++++++++ osu.Game/Skinning/Skin.cs | 4 ++-- osu.Game/Skinning/SkinnableTargetContainer.cs | 9 --------- 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/osu.Game/Skinning/ISkinnableTarget.cs b/osu.Game/Skinning/ISkinnableTarget.cs index 3773b5be24..ba3a9fe228 100644 --- a/osu.Game/Skinning/ISkinnableTarget.cs +++ b/osu.Game/Skinning/ISkinnableTarget.cs @@ -1,7 +1,12 @@ // 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.Bindables; +using osu.Framework.Graphics; +using osu.Game.Extensions; +using osu.Game.Screens.Play.HUD; namespace osu.Game.Skinning { @@ -20,6 +25,12 @@ namespace osu.Game.Skinning /// IBindableList Components { get; } + /// + /// Serialise all children as . + /// + /// The serialised content. + IEnumerable CreateSkinnableInfo() => Components.Select(d => ((Drawable)d).CreateSkinnableInfo()); + /// /// Reload this target from the current skin. /// diff --git a/osu.Game/Skinning/Skin.cs b/osu.Game/Skinning/Skin.cs index 3a16cb5818..0ceca3925e 100644 --- a/osu.Game/Skinning/Skin.cs +++ b/osu.Game/Skinning/Skin.cs @@ -65,7 +65,7 @@ namespace osu.Game.Skinning /// Remove all stored customisations for the provided target. /// /// The target container to reset. - public void ResetDrawableTarget(SkinnableTargetContainer targetContainer) + public void ResetDrawableTarget(ISkinnableTarget targetContainer) { DrawableComponentInfo.Remove(targetContainer.Target); } @@ -74,7 +74,7 @@ namespace osu.Game.Skinning /// Update serialised information for the provided target. /// /// The target container to serialise to this skin. - public void UpdateDrawableTarget(SkinnableTargetContainer targetContainer) + public void UpdateDrawableTarget(ISkinnableTarget targetContainer) { DrawableComponentInfo[targetContainer.Target] = targetContainer.CreateSkinnableInfo().ToArray(); } diff --git a/osu.Game/Skinning/SkinnableTargetContainer.cs b/osu.Game/Skinning/SkinnableTargetContainer.cs index daf54277fd..52e86f5d86 100644 --- a/osu.Game/Skinning/SkinnableTargetContainer.cs +++ b/osu.Game/Skinning/SkinnableTargetContainer.cs @@ -2,12 +2,9 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; using System.Linq; using osu.Framework.Bindables; using osu.Framework.Graphics; -using osu.Game.Extensions; -using osu.Game.Screens.Play.HUD; namespace osu.Game.Skinning { @@ -64,12 +61,6 @@ namespace osu.Game.Skinning components.Add(component); } - /// - /// Serialise all children as . - /// - /// The serialised content. - public IEnumerable CreateSkinnableInfo() => components.Select(d => ((Drawable)d).CreateSkinnableInfo()); - protected override void SkinChanged(ISkinSource skin, bool allowFallback) { base.SkinChanged(skin, allowFallback); From ebce3fd3c7d408541b39b2fa37df45d82d057a08 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 May 2021 17:29:11 +0900 Subject: [PATCH 199/205] Use `ScheduleAfterChildren` to better match comment --- osu.Game/Skinning/SkinnableTargetWrapper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Skinning/SkinnableTargetWrapper.cs b/osu.Game/Skinning/SkinnableTargetWrapper.cs index 58ecac71ab..664e8da480 100644 --- a/osu.Game/Skinning/SkinnableTargetWrapper.cs +++ b/osu.Game/Skinning/SkinnableTargetWrapper.cs @@ -40,7 +40,7 @@ namespace osu.Game.Skinning base.LoadComplete(); // schedule is required to allow children to run their LoadComplete and take on their correct sizes. - Schedule(() => applyDefaults?.Invoke(this)); + ScheduleAfterChildren(() => applyDefaults?.Invoke(this)); } } } From 01bc71acd2a24c3fa5993aab6487f4fdee7d7062 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 May 2021 18:40:28 +0900 Subject: [PATCH 200/205] Improve ability to parse xmldoc of `SkinnableTargetWrapper` Co-authored-by: Dan Balasescu --- osu.Game/Skinning/SkinnableTargetWrapper.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Skinning/SkinnableTargetWrapper.cs b/osu.Game/Skinning/SkinnableTargetWrapper.cs index 664e8da480..395de590cf 100644 --- a/osu.Game/Skinning/SkinnableTargetWrapper.cs +++ b/osu.Game/Skinning/SkinnableTargetWrapper.cs @@ -9,8 +9,8 @@ using osu.Framework.Graphics.Containers; namespace osu.Game.Skinning { /// - /// A container which is serialised and can encapsulate multiple skinnable elements into a single return type (for consumption via . - /// Will also optionally apply default cross-element layout dependencies when initialised from a non-deserialised source. + /// A container which groups the elements of a into a single object. + /// Optionally also applies a default layout to the elements. /// [Serializable] public class SkinnableTargetWrapper : Container, ISkinnableDrawable From 2f025f196723c0711e1573962515cabb892daae7 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 13 May 2021 18:51:23 +0900 Subject: [PATCH 201/205] SkinnableTargetWrapper -> SkinnableTargetComponentsContainer --- osu.Game/Skinning/DefaultSkin.cs | 2 +- osu.Game/Skinning/LegacySkin.cs | 2 +- osu.Game/Skinning/Skin.cs | 2 +- ...rapper.cs => SkinnableTargetComponentsContainer.cs} | 10 +++++----- osu.Game/Skinning/SkinnableTargetContainer.cs | 4 ++-- 5 files changed, 10 insertions(+), 10 deletions(-) rename osu.Game/Skinning/{SkinnableTargetWrapper.cs => SkinnableTargetComponentsContainer.cs} (73%) diff --git a/osu.Game/Skinning/DefaultSkin.cs b/osu.Game/Skinning/DefaultSkin.cs index d6ca5e9009..65e8fd1b82 100644 --- a/osu.Game/Skinning/DefaultSkin.cs +++ b/osu.Game/Skinning/DefaultSkin.cs @@ -47,7 +47,7 @@ namespace osu.Game.Skinning switch (target.Target) { case SkinnableTarget.MainHUDComponents: - var skinnableTargetWrapper = new SkinnableTargetWrapper(container => + var skinnableTargetWrapper = new SkinnableTargetComponentsContainer(container => { var score = container.OfType().FirstOrDefault(); var accuracy = container.OfType().FirstOrDefault(); diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index 27f6fcdf97..a6f8f45c0f 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -332,7 +332,7 @@ namespace osu.Game.Skinning { case SkinnableTarget.MainHUDComponents: - var skinnableTargetWrapper = new SkinnableTargetWrapper(container => + var skinnableTargetWrapper = new SkinnableTargetComponentsContainer(container => { var score = container.OfType().FirstOrDefault(); var accuracy = container.OfType().FirstOrDefault(); diff --git a/osu.Game/Skinning/Skin.cs b/osu.Game/Skinning/Skin.cs index 0ceca3925e..2944c7a8ec 100644 --- a/osu.Game/Skinning/Skin.cs +++ b/osu.Game/Skinning/Skin.cs @@ -87,7 +87,7 @@ namespace osu.Game.Skinning if (!DrawableComponentInfo.TryGetValue(target.Target, out var skinnableInfo)) return null; - return new SkinnableTargetWrapper + return new SkinnableTargetComponentsContainer { ChildrenEnumerable = skinnableInfo.Select(i => i.CreateInstance()) }; diff --git a/osu.Game/Skinning/SkinnableTargetWrapper.cs b/osu.Game/Skinning/SkinnableTargetComponentsContainer.cs similarity index 73% rename from osu.Game/Skinning/SkinnableTargetWrapper.cs rename to osu.Game/Skinning/SkinnableTargetComponentsContainer.cs index 395de590cf..2107ca7a8b 100644 --- a/osu.Game/Skinning/SkinnableTargetWrapper.cs +++ b/osu.Game/Skinning/SkinnableTargetComponentsContainer.cs @@ -9,11 +9,11 @@ using osu.Framework.Graphics.Containers; namespace osu.Game.Skinning { /// - /// A container which groups the elements of a into a single object. - /// Optionally also applies a default layout to the elements. + /// A container which groups the components of a into a single object. + /// Optionally also applies a default layout to the components. /// [Serializable] - public class SkinnableTargetWrapper : Container, ISkinnableDrawable + public class SkinnableTargetComponentsContainer : Container, ISkinnableDrawable { public bool IsEditable => false; @@ -23,14 +23,14 @@ namespace osu.Game.Skinning /// Construct a wrapper with defaults that should be applied once. /// /// A function to apply the default layout. - public SkinnableTargetWrapper(Action applyDefaults) + public SkinnableTargetComponentsContainer(Action applyDefaults) : this() { this.applyDefaults = applyDefaults; } [JsonConstructor] - public SkinnableTargetWrapper() + public SkinnableTargetComponentsContainer() { RelativeSizeAxes = Axes.Both; } diff --git a/osu.Game/Skinning/SkinnableTargetContainer.cs b/osu.Game/Skinning/SkinnableTargetContainer.cs index 52e86f5d86..a4d7f621eb 100644 --- a/osu.Game/Skinning/SkinnableTargetContainer.cs +++ b/osu.Game/Skinning/SkinnableTargetContainer.cs @@ -10,7 +10,7 @@ namespace osu.Game.Skinning { public class SkinnableTargetContainer : SkinReloadableDrawable, ISkinnableTarget { - private SkinnableTargetWrapper content; + private SkinnableTargetComponentsContainer content; public SkinnableTarget Target { get; } @@ -31,7 +31,7 @@ namespace osu.Game.Skinning ClearInternal(); components.Clear(); - content = CurrentSkin.GetDrawableComponent(new SkinnableTargetComponent(Target)) as SkinnableTargetWrapper; + content = CurrentSkin.GetDrawableComponent(new SkinnableTargetComponent(Target)) as SkinnableTargetComponentsContainer; if (content != null) { From e5f765d1a8ef42e9a12fe94dbe23f3d464d1972d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 May 2021 19:06:58 +0900 Subject: [PATCH 202/205] Fix broken exception message --- osu.Game/Skinning/Editor/SkinEditor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Skinning/Editor/SkinEditor.cs b/osu.Game/Skinning/Editor/SkinEditor.cs index 67285987ef..cb27a84a75 100644 --- a/osu.Game/Skinning/Editor/SkinEditor.cs +++ b/osu.Game/Skinning/Editor/SkinEditor.cs @@ -166,7 +166,7 @@ namespace osu.Game.Skinning.Editor private void placeComponent(Type type) { if (!(Activator.CreateInstance(type) is ISkinnableDrawable component)) - throw new InvalidOperationException("Attempted to instantiate a component for placement which was not an {typeof(ISkinnableComponent)}."); + throw new InvalidOperationException($"Attempted to instantiate a component for placement which was not an {typeof(ISkinnableDrawable)}."); getTarget(SkinnableTarget.MainHUDComponents)?.Add(component); From 1e23c535070102c326ca609f56b0d68765bcc25f Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 13 May 2021 21:59:38 +0900 Subject: [PATCH 203/205] Fix inspection --- osu.Game/Skinning/SkinnableDrawable.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game/Skinning/SkinnableDrawable.cs b/osu.Game/Skinning/SkinnableDrawable.cs index 77f7cf5c3f..fc2730ca44 100644 --- a/osu.Game/Skinning/SkinnableDrawable.cs +++ b/osu.Game/Skinning/SkinnableDrawable.cs @@ -23,7 +23,7 @@ namespace osu.Game.Skinning /// Whether the drawable component should be centered in available space. /// Defaults to true. /// - public bool CentreComponent { get; set; } = true; + public bool CentreComponent = true; public new Axes AutoSizeAxes { @@ -42,7 +42,8 @@ namespace osu.Game.Skinning /// A function to create the default skin implementation of this element. /// A conditional to decide whether to allow fallback to the default implementation if a skinned element is not present. /// How (if at all) the should be resize to fit within our own bounds. - public SkinnableDrawable(ISkinComponent component, Func defaultImplementation = null, Func allowFallback = null, ConfineMode confineMode = ConfineMode.NoScaling) + public SkinnableDrawable(ISkinComponent component, Func defaultImplementation = null, Func allowFallback = null, + ConfineMode confineMode = ConfineMode.NoScaling) : this(component, allowFallback, confineMode) { createDefault = defaultImplementation; From 4cf4817ad2abc61de9f754b7cd9e714bd19b74e0 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 13 May 2021 22:11:58 +0900 Subject: [PATCH 204/205] Remove redundant parens --- osu.Game/Skinning/Editor/SkinSelectionHandler.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Skinning/Editor/SkinSelectionHandler.cs b/osu.Game/Skinning/Editor/SkinSelectionHandler.cs index f7eec4d661..79dd45c07c 100644 --- a/osu.Game/Skinning/Editor/SkinSelectionHandler.cs +++ b/osu.Game/Skinning/Editor/SkinSelectionHandler.cs @@ -124,7 +124,7 @@ namespace osu.Game.Skinning.Editor { var drawable = (Drawable)item; - var previousAnchor = (drawable.AnchorPosition); + var previousAnchor = drawable.AnchorPosition; drawable.Anchor = anchor; drawable.Position -= drawable.AnchorPosition - previousAnchor; } From 6c12cae105a98f0d313d16f00b2cdfa9c4960bfb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 May 2021 22:25:11 +0900 Subject: [PATCH 205/205] Remove unnecessary property --- osu.Game/Skinning/Editor/SkinBlueprint.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/osu.Game/Skinning/Editor/SkinBlueprint.cs b/osu.Game/Skinning/Editor/SkinBlueprint.cs index f7509d7b8f..7a5547296b 100644 --- a/osu.Game/Skinning/Editor/SkinBlueprint.cs +++ b/osu.Game/Skinning/Editor/SkinBlueprint.cs @@ -23,11 +23,6 @@ namespace osu.Game.Skinning.Editor private Drawable drawable => (Drawable)Item; - /// - /// Whether the blueprint should be shown even when the is not alive. - /// - protected virtual bool AlwaysShowWhenSelected => true; - protected override bool ShouldBeAlive => drawable.IsAlive && Item.IsPresent; [Resolved]