From 6f69e5a9935569580d8c68d4c5b282e0e8340b3f Mon Sep 17 00:00:00 2001 From: Denis Pakhorukov Date: Mon, 16 Oct 2023 02:44:40 +0300 Subject: [PATCH] #260 Add enhanced loading overlay --- .../Multiplayer/MultiplayerConnectionTests.cs | 15 +++- ...ch.cs => MultiplayerStatusOverlayPatch.cs} | 8 +- .../Multiplayer/MultiplayerTools.cs | 4 +- .../Commands/Overlay/ShowLoadOverlay.cs | 10 --- .../Multiplayer/Commands/World/LoadWorld.cs | 4 +- .../MultiplayerJoinRequestController.cs | 4 +- .../MultiplayerServerController.cs | 14 +++- .../Multiplayer/Players/MultiplayerPlayers.cs | 2 + .../Multiplayer/UI/LoadOverlay.cs | 30 -------- .../UI/Overlays/MultiplayerStatusOverlay.cs | 73 +++++++++++++++++++ .../Multiplayer/World/WorldManager.cs | 57 +++++++++++++-- 11 files changed, 162 insertions(+), 59 deletions(-) rename src/MultiplayerMod.Test/Multiplayer/{LoadOverlayPatch.cs => MultiplayerStatusOverlayPatch.cs} (52%) delete mode 100644 src/MultiplayerMod/Multiplayer/Commands/Overlay/ShowLoadOverlay.cs delete mode 100644 src/MultiplayerMod/Multiplayer/UI/LoadOverlay.cs create mode 100644 src/MultiplayerMod/Multiplayer/UI/Overlays/MultiplayerStatusOverlay.cs diff --git a/src/MultiplayerMod.Test/Multiplayer/MultiplayerConnectionTests.cs b/src/MultiplayerMod.Test/Multiplayer/MultiplayerConnectionTests.cs index 9f75fd8b..5f4847c2 100644 --- a/src/MultiplayerMod.Test/Multiplayer/MultiplayerConnectionTests.cs +++ b/src/MultiplayerMod.Test/Multiplayer/MultiplayerConnectionTests.cs @@ -97,6 +97,13 @@ public void EstablishingTwoPlayersConnection() { // Event: the client finished loading and the game is started clientRuntime.StartGame(); + // Lists must be equal and the client player must be in loading state + AssertPlayersAreEqual(hostRuntime, clientRuntime); + Assert.AreEqual(expected: PlayerState.Loading, actual: clientPlayer.State); + + // Unity transitions into the next frame + UnityTestRuntime.NextFrame(); + // Lists must be equal and the client player must be ready AssertPlayersAreEqual(hostRuntime, clientRuntime); Assert.AreEqual(expected: PlayerState.Ready, actual: clientPlayer.State); @@ -158,7 +165,13 @@ private static void AssertPlayersAreEqual(TestRuntime runtimeA, TestRuntime runt private static Harmony SetupEnvironment() { UnityTestRuntime.Install(); var harmony = new Harmony("MultiplayerConnectionTests"); - PatchesSetup.Install(harmony, new List { typeof(WorldManagerPatch), typeof(LoadOverlayPatch) }); + PatchesSetup.Install( + harmony, + new List { + typeof(WorldManagerPatch), + typeof(MultiplayerStatusOverlayPatch) + } + ); return harmony; } diff --git a/src/MultiplayerMod.Test/Multiplayer/LoadOverlayPatch.cs b/src/MultiplayerMod.Test/Multiplayer/MultiplayerStatusOverlayPatch.cs similarity index 52% rename from src/MultiplayerMod.Test/Multiplayer/LoadOverlayPatch.cs rename to src/MultiplayerMod.Test/Multiplayer/MultiplayerStatusOverlayPatch.cs index 98d49c24..9dc46206 100644 --- a/src/MultiplayerMod.Test/Multiplayer/LoadOverlayPatch.cs +++ b/src/MultiplayerMod.Test/Multiplayer/MultiplayerStatusOverlayPatch.cs @@ -1,16 +1,16 @@ using HarmonyLib; using JetBrains.Annotations; -using MultiplayerMod.Multiplayer.UI; +using MultiplayerMod.Multiplayer.UI.Overlays; namespace MultiplayerMod.Test.Multiplayer; [UsedImplicitly] -[HarmonyPatch(typeof(LoadOverlay))] -public class LoadOverlayPatch { +[HarmonyPatch(typeof(MultiplayerStatusOverlay))] +public class MultiplayerStatusOverlayPatch { [UsedImplicitly] [HarmonyPrefix] - [HarmonyPatch(nameof(LoadOverlay.Show))] + [HarmonyPatch(nameof(MultiplayerStatusOverlay.Show))] private static bool Show() { return false; } diff --git a/src/MultiplayerMod.Test/Multiplayer/MultiplayerTools.cs b/src/MultiplayerMod.Test/Multiplayer/MultiplayerTools.cs index 3a237de1..1dd62b8f 100644 --- a/src/MultiplayerMod.Test/Multiplayer/MultiplayerTools.cs +++ b/src/MultiplayerMod.Test/Multiplayer/MultiplayerTools.cs @@ -51,8 +51,8 @@ public static TestRuntime CreateTestRuntime(MultiplayerMode mode, string playerN .AddType() .AddType(); - var configurer = new MultiplayerCommandsConfigurer(); - configurer.Configure(builder); + new MultiplayerCommandsConfigurer().Configure(builder); + new UnityTaskSchedulerConfigurer().Configure(builder); var container = builder.Build(); var runtime = container.Get(); diff --git a/src/MultiplayerMod/Multiplayer/Commands/Overlay/ShowLoadOverlay.cs b/src/MultiplayerMod/Multiplayer/Commands/Overlay/ShowLoadOverlay.cs deleted file mode 100644 index c34820b6..00000000 --- a/src/MultiplayerMod/Multiplayer/Commands/Overlay/ShowLoadOverlay.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System; -using MultiplayerMod.Multiplayer.UI; - -namespace MultiplayerMod.Multiplayer.Commands.Overlay; - -[Serializable] -[MultiplayerCommand(Type = MultiplayerCommandType.System)] -public class ShowLoadOverlay : MultiplayerCommand { - public override void Execute(MultiplayerCommandContext context) => LoadOverlay.Show("Waiting for players..."); -} diff --git a/src/MultiplayerMod/Multiplayer/Commands/World/LoadWorld.cs b/src/MultiplayerMod/Multiplayer/Commands/World/LoadWorld.cs index c88a25c2..2f566aac 100644 --- a/src/MultiplayerMod/Multiplayer/Commands/World/LoadWorld.cs +++ b/src/MultiplayerMod/Multiplayer/Commands/World/LoadWorld.cs @@ -15,6 +15,8 @@ public LoadWorld(string worldName, byte[] data) { this.data = data; } - public override void Execute(MultiplayerCommandContext context) => WorldManager.LoadWorldSave(worldName, data); + public override void Execute(MultiplayerCommandContext context) { + context.Runtime.Dependencies.Get().RequestWorldLoad(worldName, data); + } } diff --git a/src/MultiplayerMod/Multiplayer/CoreOperations/MultiplayerJoinRequestController.cs b/src/MultiplayerMod/Multiplayer/CoreOperations/MultiplayerJoinRequestController.cs index 0d2b75a5..0c6c630b 100644 --- a/src/MultiplayerMod/Multiplayer/CoreOperations/MultiplayerJoinRequestController.cs +++ b/src/MultiplayerMod/Multiplayer/CoreOperations/MultiplayerJoinRequestController.cs @@ -3,7 +3,7 @@ using MultiplayerMod.Core.Events; using MultiplayerMod.Multiplayer.CoreOperations.Events; using MultiplayerMod.Multiplayer.Players.Events; -using MultiplayerMod.Multiplayer.UI; +using MultiplayerMod.Multiplayer.UI.Overlays; using MultiplayerMod.Network; namespace MultiplayerMod.Multiplayer.CoreOperations; @@ -23,7 +23,7 @@ public MultiplayerJoinRequestController(EventDispatcher events, IMultiplayerClie private void OnMultiplayerJoinRequested(MultiplayerJoinRequestedEvent @event) { events.Dispatch(new MultiplayerModeSelectedEvent(MultiplayerMode.Client)); - LoadOverlay.Show($"Connecting to {@event.HostName}..."); + MultiplayerStatusOverlay.Show($"Connecting to {@event.HostName}..."); client.Connect(@event.Endpoint); } diff --git a/src/MultiplayerMod/Multiplayer/CoreOperations/MultiplayerServerController.cs b/src/MultiplayerMod/Multiplayer/CoreOperations/MultiplayerServerController.cs index b645234c..e96d0fb7 100644 --- a/src/MultiplayerMod/Multiplayer/CoreOperations/MultiplayerServerController.cs +++ b/src/MultiplayerMod/Multiplayer/CoreOperations/MultiplayerServerController.cs @@ -2,7 +2,8 @@ using MultiplayerMod.Core.Dependency; using MultiplayerMod.Core.Events; using MultiplayerMod.Multiplayer.CoreOperations.Events; -using MultiplayerMod.Multiplayer.UI; +using MultiplayerMod.Multiplayer.Players.Events; +using MultiplayerMod.Multiplayer.UI.Overlays; using MultiplayerMod.Network; namespace MultiplayerMod.Multiplayer.CoreOperations; @@ -12,10 +13,12 @@ public class MultiplayerServerController { private readonly IMultiplayerServer server; private readonly IMultiplayerClient client; + private readonly EventDispatcher events; public MultiplayerServerController(IMultiplayerServer server, IMultiplayerClient client, EventDispatcher events) { this.server = server; this.client = client; + this.events = events; events.Subscribe(OnGameStarted); events.Subscribe(OnGameQuit); @@ -32,7 +35,14 @@ private void OnGameStarted(GameStartedEvent @event) { if (@event.Multiplayer.Mode != MultiplayerMode.Host) return; - LoadOverlay.Show("Starting host..."); + MultiplayerStatusOverlay.Show("Starting host..."); + events.Subscribe( + (_, subscription) => { + MultiplayerStatusOverlay.Close(); + subscription.Cancel(); + } + ); + server.Start(); } diff --git a/src/MultiplayerMod/Multiplayer/Players/MultiplayerPlayers.cs b/src/MultiplayerMod/Multiplayer/Players/MultiplayerPlayers.cs index 2168c1ee..62259610 100644 --- a/src/MultiplayerMod/Multiplayer/Players/MultiplayerPlayers.cs +++ b/src/MultiplayerMod/Multiplayer/Players/MultiplayerPlayers.cs @@ -22,6 +22,8 @@ public class MultiplayerPlayers : IEnumerable { public IEnumerator GetEnumerator() => players.Values.GetEnumerator(); + public int Count => players.Values.Count; + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); public MultiplayerPlayer Current => players[currentPlayerId]; diff --git a/src/MultiplayerMod/Multiplayer/UI/LoadOverlay.cs b/src/MultiplayerMod/Multiplayer/UI/LoadOverlay.cs deleted file mode 100644 index d90c8155..00000000 --- a/src/MultiplayerMod/Multiplayer/UI/LoadOverlay.cs +++ /dev/null @@ -1,30 +0,0 @@ -using MultiplayerMod.Core.Events; -using MultiplayerMod.ModRuntime.StaticCompatibility; -using MultiplayerMod.Multiplayer.Players.Events; - -namespace MultiplayerMod.Multiplayer.UI; - -public static class LoadOverlay { - - private static bool active; - - public static void Show(string text) { - if (active) - return; - - LoadingOverlay.Load(() => { }); - LoadingOverlay.instance.GetComponentInChildren().text = text; - - var eventDispatcher = Dependencies.Get(); - eventDispatcher.Subscribe( - (_, subscription) => { - LoadingOverlay.Clear(); - subscription.Cancel(); - active = false; - } - ); - - active = true; - } - -} diff --git a/src/MultiplayerMod/Multiplayer/UI/Overlays/MultiplayerStatusOverlay.cs b/src/MultiplayerMod/Multiplayer/UI/Overlays/MultiplayerStatusOverlay.cs new file mode 100644 index 00000000..2d6696ef --- /dev/null +++ b/src/MultiplayerMod/Multiplayer/UI/Overlays/MultiplayerStatusOverlay.cs @@ -0,0 +1,73 @@ +using JetBrains.Annotations; +using MultiplayerMod.Core.Dependency; +using MultiplayerMod.Core.Events; +using MultiplayerMod.Core.Scheduling; +using MultiplayerMod.ModRuntime.StaticCompatibility; +using TMPro; +using UnityEngine; +using UnityEngine.SceneManagement; + +namespace MultiplayerMod.Multiplayer.UI.Overlays; + +public class MultiplayerStatusOverlay { + + public static string Text { + get => overlay?.text ?? ""; + set { + if (overlay == null) + return; + + overlay.text = value; + overlay.textComponent.text = value; + } + } + + private LocText textComponent = null!; + private string text = ""; + + [InjectDependency, UsedImplicitly] + private UnityTaskScheduler scheduler = null!; + + [InjectDependency, UsedImplicitly] + private EventDispatcher events = null!; + + private static MultiplayerStatusOverlay? overlay; + + private MultiplayerStatusOverlay() { + SceneManager.sceneLoaded += OnPostLoadScene; + Dependencies.Get().Inject(this); + CreateOverlay(); + } + + private void CreateOverlay() { + LoadingOverlay.Load(() => { }); + textComponent = LoadingOverlay.instance.GetComponentInChildren(); + textComponent.alignment = TextAlignmentOptions.Top; + textComponent.margin = new Vector4(0, -21.0f, 0, 0); + textComponent.text = text; + + var rect = textComponent.gameObject.GetComponent(); + rect.sizeDelta = new Vector2(Screen.width, 0); + + var scale = LoadingOverlay.instance.GetComponentInParent().GetCanvasScale(); + ScreenResize.Instance.OnResize += () => rect.sizeDelta = new Vector2(Screen.width / scale, 0); + } + + private void Dispose() { + SceneManager.sceneLoaded -= OnPostLoadScene; + LoadingOverlay.Clear(); + } + + private void OnPostLoadScene(Scene scene, LoadSceneMode mode) => scheduler.Run(CreateOverlay); + + public static void Show(string text) { + overlay ??= new MultiplayerStatusOverlay(); + Text = text; + } + + public static void Close() { + overlay?.Dispose(); + overlay = null; + } + +} diff --git a/src/MultiplayerMod/Multiplayer/World/WorldManager.cs b/src/MultiplayerMod/Multiplayer/World/WorldManager.cs index 82c47db6..c8783717 100644 --- a/src/MultiplayerMod/Multiplayer/World/WorldManager.cs +++ b/src/MultiplayerMod/Multiplayer/World/WorldManager.cs @@ -1,14 +1,18 @@ using System.IO; +using System.Linq; using JetBrains.Annotations; using MultiplayerMod.Core.Dependency; +using MultiplayerMod.Core.Events; using MultiplayerMod.Core.Extensions; +using MultiplayerMod.Core.Scheduling; using MultiplayerMod.ModRuntime.Context; using MultiplayerMod.ModRuntime.StaticCompatibility; -using MultiplayerMod.Multiplayer.Commands.Overlay; using MultiplayerMod.Multiplayer.Commands.Speed; using MultiplayerMod.Multiplayer.Commands.World; using MultiplayerMod.Multiplayer.CoreOperations.PlayersManagement.Commands; using MultiplayerMod.Multiplayer.Players; +using MultiplayerMod.Multiplayer.Players.Events; +using MultiplayerMod.Multiplayer.UI.Overlays; using MultiplayerMod.Network; namespace MultiplayerMod.Multiplayer.World; @@ -18,34 +22,73 @@ public class WorldManager { private readonly IMultiplayerServer server; private readonly MultiplayerGame multiplayer; + private readonly EventDispatcher events; + private readonly UnityTaskScheduler scheduler; - public WorldManager(IMultiplayerServer server, MultiplayerGame multiplayer) { + public WorldManager( + IMultiplayerServer server, + MultiplayerGame multiplayer, + EventDispatcher events, + UnityTaskScheduler scheduler + ) { this.server = server; this.multiplayer = multiplayer; + this.events = events; + this.scheduler = scheduler; } public void Sync() { + SetupStatusOverlay(); + var resume = !SpeedControlScreen.Instance.IsPaused; server.Send(new PauseGame()); multiplayer.Objects.SynchronizeWithTracker(); multiplayer.Players.ForEach(it => server.Send(new ChangePlayerStateCommand(it.Id, PlayerState.Loading))); server.Send(new ChangePlayerStateCommand(multiplayer.Players.Current.Id, PlayerState.Ready)); - server.Send(new ShowLoadOverlay()); server.Send(new LoadWorld(WorldName, GetWorldSave()), MultiplayerCommandOptions.SkipHost); + if (resume) server.Send(new ResumeGame()); } - public static void LoadWorldSave(string worldName, byte[] data) { + private void SetupStatusOverlay() { + MultiplayerStatusOverlay.Show("Waiting for players..."); + events.Subscribe( + (_, subscription) => { + var players = multiplayer.Players; + if (players.Ready) { + MultiplayerStatusOverlay.Close(); + subscription.Cancel(); + } + var readyPlayersCount = players.Count(it => it.State == PlayerState.Ready); + var playerList = string.Join("\n", players.Select(it => $"{it.Profile.PlayerName}: {it.State}")); + var statusText = $"Waiting for players ({readyPlayersCount}/{players.Count} ready)...\n{playerList}"; + MultiplayerStatusOverlay.Text = statusText; + } + ); + } + + public void RequestWorldLoad(string name, byte[] data) { + MultiplayerStatusOverlay.Show($"Loading {name}..."); + events.Subscribe( + (_, subscription) => { + MultiplayerStatusOverlay.Close(); + subscription.Cancel(); + } + ); + scheduler.Run(() => LoadWorldSave(name, data)); + } + + private void LoadWorldSave(string name, byte[] data) { var savePath = SaveLoader.GetCloudSavesDefault() ? SaveLoader.GetCloudSavePrefix() : SaveLoader.GetSavePrefixAndCreateFolder(); - var path = Path.Combine(savePath, worldName, $"{worldName}.sav"); + var path = Path.Combine(savePath, name, $"{name}.sav"); Directory.CreateDirectory(Path.GetDirectoryName(path)!); - using (var writer = new BinaryWriter(File.OpenWrite(path))) { + using (var writer = new BinaryWriter(File.OpenWrite(path))) writer.Write(data); - } + LoadScreen.DoLoad(path); }