diff --git a/HKMP/Game/Client/ClientManager.cs b/HKMP/Game/Client/ClientManager.cs index a823b59..cef36c8 100644 --- a/HKMP/Game/Client/ClientManager.cs +++ b/HKMP/Game/Client/ClientManager.cs @@ -242,35 +242,23 @@ ModSettings modSettings packetManager.RegisterClientPacketHandler(ClientPacketId.ChatMessage, OnChatMessage); // Register handlers for events from UI - uiManager.ConnectInterface.ConnectButtonPressed += (address, port, username, autoConnect) => { + uiManager.RequestClientConnectEvent += (address, port, username, autoConnect) => { _autoConnect = autoConnect; Connect(address, port, username); }; - uiManager.ConnectInterface.DisconnectButtonPressed += Disconnect; - uiManager.SettingsInterface.OnTeamRadioButtonChange += InternalChangeTeam; - uiManager.SettingsInterface.OnSkinIdChange += InternalChangeSkin; + uiManager.RequestClientDisconnectEvent += Disconnect; + + // uiManager.SettingsInterface.OnTeamRadioButtonChange += InternalChangeTeam; + // uiManager.SettingsInterface.OnSkinIdChange += InternalChangeSkin; UiManager.InternalChatBox.ChatInputEvent += OnChatInput; netClient.ConnectEvent += _ => uiManager.OnSuccessfulConnect(); netClient.ConnectFailedEvent += OnConnectFailed; - // Register the Hero Controller Start, which is when the local player spawns - On.HeroController.Start += (orig, self) => { - // Execute the original method - orig(self); - // If we are connect to a server, add a username to the player object - if (netClient.IsConnected) { - _playerManager.AddNameToPlayer( - HeroController.instance.gameObject, - _username, - _playerManager.LocalPlayerTeam - ); - } - }; - - // Register handlers for scene change and player update + // Register handlers for various things UnityEngine.SceneManagement.SceneManager.activeSceneChanged += OnSceneChange; + On.HeroController.Start += OnHeroControllerStart; On.HeroController.Update += OnPlayerUpdate; // Register client connect and timeout handler @@ -480,39 +468,22 @@ private void OnClientConnect(LoginResponse loginResponse) { // First relay the addon order from the login response to the addon manager _addonManager.UpdateNetworkedAddonOrder(loginResponse.AddonOrder); - // We should only be able to connect during a gameplay scene, - // which is when the player is spawned already, so we can add the username - _playerManager.AddNameToPlayer(HeroController.instance.gameObject, _username, - _playerManager.LocalPlayerTeam); - - Logger.Info("Client is connected, sending Hello packet"); + _netClient.UpdateManager.SetHelloServerData(_username); + } + + /// + /// Callback method for when the HeroController is started so we can add the username to the player object. + /// + private void OnHeroControllerStart(On.HeroController.orig_Start orig, HeroController self) { + orig(self); - // If we are in a non-gameplay scene, we transmit that we are not active yet - var currentSceneName = SceneUtil.GetCurrentSceneName(); - if (SceneUtil.IsNonGameplayScene(currentSceneName)) { - Logger.Error( - $"Client connected during a non-gameplay scene named {currentSceneName}, this should never happen!"); - return; + if (_netClient.IsConnected) { + _playerManager.AddNameToPlayer( + HeroController.instance.gameObject, + _username, + _playerManager.LocalPlayerTeam + ); } - - var transform = HeroController.instance.transform; - var position = transform.position; - - Logger.Info("Sending Hello packet"); - - _netClient.UpdateManager.SetHelloServerData( - _username, - SceneUtil.GetCurrentSceneName(), - new Vector2(position.x, position.y), - transform.localScale.x > 0, - (ushort) AnimationManager.GetCurrentAnimationClip() - ); - - // Since we are probably in the pause menu when we connect, set the timescale so the game - // is running while paused - PauseManager.SetTimeScale(1.0f); - - UiManager.InternalChatBox.AddMessage("You are connected to the server"); } /// @@ -525,6 +496,7 @@ private void OnHelloClient(HelloClient helloClient) { // If this was not an auto-connect, we set save data. Otherwise, we know we already have the save data. if (!_autoConnect) { _saveManager.SetSaveWithData(helloClient.CurrentSave); + _uiManager.EnterGameFromMultiplayerMenu(); } // Fill the player data dictionary with the info from the packet @@ -553,6 +525,8 @@ private void OnDisconnect(ServerClientDisconnect disconnect) { } else if (disconnect.Reason == DisconnectReason.Shutdown) { UiManager.InternalChatBox.AddMessage("You are disconnected from the server (server is shutting down)"); } + + _uiManager.ReturnToMainMenuFromGame(); // Disconnect without sending the server that we disconnect, because the server knows that already InternalDisconnect(); @@ -917,7 +891,7 @@ private void OnServerSettingsUpdated(ServerSettingsUpdate update) { _playerManager.ResetAllTeams(); } - _uiManager.OnTeamSettingChange(); + // _uiManager.OnTeamSettingChange(); } // If the allow skins setting changed and it is no longer allowed, we reset all existing skins @@ -952,12 +926,10 @@ private void OnSceneChange(Scene oldScene, Scene newScene) { // Reset the status of whether we determined the scene host or not _sceneHostDetermined = false; - // Ignore scene changes from and to non-gameplay scenes - if (SceneUtil.IsNonGameplayScene(oldScene.name)) { - return; + // If the old scene is a gameplay scene, we need to notify the server that we left + if (!SceneUtil.IsNonGameplayScene(oldScene.name)) { + _netClient.UpdateManager.SetLeftScene(); } - - _netClient.UpdateManager.SetLeftScene(); } /// @@ -991,7 +963,6 @@ private void OnPlayerUpdate(On.HeroController.orig_Update orig, HeroController s if (_sceneChanged) { _sceneChanged = false; - // Set some default values for the packet variables in case we don't have a HeroController instance // This might happen when we are in a non-gameplay scene without the knight var position = Vector2.Zero; @@ -1049,7 +1020,10 @@ private void OnTimeout() { return; } - Logger.Info("Connection to server timed out, disconnecting"); + Logger.Info("Connection to server timed out, moving to main menu"); + + _uiManager.ReturnToMainMenuFromGame(); + UiManager.InternalChatBox.AddMessage("You are disconnected from the server (server timed out)"); Disconnect(); diff --git a/HKMP/Game/Client/Save/SaveDataMapping.cs b/HKMP/Game/Client/Save/SaveDataMapping.cs index 7f593c1..1eb442f 100644 --- a/HKMP/Game/Client/Save/SaveDataMapping.cs +++ b/HKMP/Game/Client/Save/SaveDataMapping.cs @@ -2,6 +2,7 @@ using System.Linq; using Hkmp.Collection; using Hkmp.Logging; +using Hkmp.Util; using Newtonsoft.Json; namespace Hkmp.Game.Client.Save; @@ -10,6 +11,30 @@ namespace Hkmp.Game.Client.Save; /// Serializable data class that stores mappings for what scene data should be synchronised and their indices used for networking. /// internal class SaveDataMapping { + /// + /// The file path of the embedded resource file for save data. + /// + private const string SaveDataFilePath = "Hkmp.Resource.save-data.json"; + + /// + /// The static instance of the mapping. + /// + [JsonIgnore] + private static SaveDataMapping _instance; + + /// + [JsonIgnore] + public static SaveDataMapping Instance { + get { + if (_instance == null) { + _instance = FileUtil.LoadObjectFromEmbeddedJson(SaveDataFilePath); + _instance.Initialize(); + } + + return _instance; + } + } + /// /// Dictionary mapping player data values to booleans indicating whether they should be synchronised. /// diff --git a/HKMP/Game/Client/Save/SaveManager.cs b/HKMP/Game/Client/Save/SaveManager.cs index 7e51b53..2fe4279 100644 --- a/HKMP/Game/Client/Save/SaveManager.cs +++ b/HKMP/Game/Client/Save/SaveManager.cs @@ -21,20 +21,15 @@ namespace Hkmp.Game.Client.Save; /// Class that manages save data synchronisation. /// internal class SaveManager { - /// - /// The file path of the embedded resource file for save data. - /// - private const string SaveDataFilePath = "Hkmp.Resource.save-data.json"; - /// /// The index of the save data entry for the warp. /// private const ushort SaveWarpIndex = ushort.MaxValue; /// - /// The save data instances that contains mappings for what to sync and their indices. + /// The save data instance that contains mappings for what to sync and their indices. /// - private static readonly SaveDataMapping SaveDataMapping; + private static SaveDataMapping SaveDataMapping => SaveDataMapping.Instance; /// /// The net client instance to send save updates. @@ -82,14 +77,6 @@ public SaveManager(NetClient netClient, PacketManager packetManager, EntityManag _bsCompHashes = new Dictionary(); } - /// - /// Static constructor to load and initialize the save data mapping. - /// - static SaveManager() { - SaveDataMapping = FileUtil.LoadObjectFromEmbeddedJson(SaveDataFilePath); - SaveDataMapping.Initialize(); - } - /// /// Initializes the save manager by loading the save data json. /// diff --git a/HKMP/Game/GameManager.cs b/HKMP/Game/GameManager.cs index 4d53dbf..dd91b2e 100644 --- a/HKMP/Game/GameManager.cs +++ b/HKMP/Game/GameManager.cs @@ -37,7 +37,6 @@ public GameManager(ModSettings modSettings) { var serverServerSettings = modSettings.ServerSettings; var uiManager = new UiManager( - clientServerSettings, modSettings, netClient ); diff --git a/HKMP/Game/Server/ModServerManager.cs b/HKMP/Game/Server/ModServerManager.cs index cb37e1c..b855fe3 100644 --- a/HKMP/Game/Server/ModServerManager.cs +++ b/HKMP/Game/Server/ModServerManager.cs @@ -22,11 +22,11 @@ UiManager uiManager ModHooks.FinishedLoadingModsHook += AddonManager.LoadAddons; // Register handlers for UI events - uiManager.ConnectInterface.StartHostButtonPressed += port => { + uiManager.RequestServerStartHostEvent += port => { CurrentSaveData = SaveManager.GetCurrentSaveData(); Start(port); }; - uiManager.ConnectInterface.StopHostButtonPressed += Stop; + uiManager.RequestServerStopHostEvent += Stop; // Register application quit handler ModHooks.ApplicationQuitHook += Stop; diff --git a/HKMP/Game/Server/ServerManager.cs b/HKMP/Game/Server/ServerManager.cs index 4b3a82b..2cf37c2 100644 --- a/HKMP/Game/Server/ServerManager.cs +++ b/HKMP/Game/Server/ServerManager.cs @@ -253,11 +253,6 @@ private void OnHelloServer(ushort id, HelloServer helloServer) { return; } - playerData.CurrentScene = helloServer.SceneName; - playerData.Position = helloServer.Position; - playerData.Scale = helloServer.Scale; - playerData.AnimationId = helloServer.AnimationClipId; - var clientInfo = new List<(ushort, string)>(); foreach (var idPlayerDataPair in _playerData) { @@ -1288,7 +1283,7 @@ private void OnChatMessage(ushort id, ChatMessage chatMessage) { /// /// The ID of the player. /// The SaveUpdate packet data. - private void OnSaveUpdate(ushort id, SaveUpdate packet) { + protected virtual void OnSaveUpdate(ushort id, SaveUpdate packet) { if (!_playerData.TryGetValue(id, out var playerData)) { Logger.Debug($"Could not process save update from unknown player ID: {id}"); return; @@ -1300,6 +1295,9 @@ private void OnSaveUpdate(ushort id, SaveUpdate packet) { Logger.Info(" Player is not scene host, not broadcasting update"); return; } + + // The save update is valid so we store it in our current save + CurrentSaveData[packet.SaveDataIndex] = packet.Value; foreach (var idPlayerDataPair in _playerData) { var otherId = idPlayerDataPair.Key; diff --git a/HKMP/Game/Settings/ModSettings.cs b/HKMP/Game/Settings/ModSettings.cs index fc92a93..cff1167 100644 --- a/HKMP/Game/Settings/ModSettings.cs +++ b/HKMP/Game/Settings/ModSettings.cs @@ -14,12 +14,6 @@ internal class ModSettings { /// public string AuthKey { get; set; } = null; - /// - /// The key to hide the HKMP UI. - /// - [JsonConverter(typeof(StringEnumConverter))] - public KeyCode HideUiKey { get; set; } = KeyCode.RightAlt; - /// /// The key to open the chat. /// diff --git a/HKMP/Networking/Client/ClientUpdateManager.cs b/HKMP/Networking/Client/ClientUpdateManager.cs index ccc8d64..31a33f6 100644 --- a/HKMP/Networking/Client/ClientUpdateManager.cs +++ b/HKMP/Networking/Client/ClientUpdateManager.cs @@ -358,26 +358,12 @@ public void SetSkinUpdate(byte skinId) { /// Set hello server data in the current packet. /// /// The username of the player. - /// The name of the current scene of the player. - /// The position of the player. - /// The scale of the player. - /// The animation clip ID of the player. - public void SetHelloServerData( - string username, - string sceneName, - Vector2 position, - bool scale, - ushort animationClipId - ) { + public void SetHelloServerData(string username) { lock (Lock) { CurrentUpdatePacket.SetSendingPacketData( ServerPacketId.HelloServer, new HelloServer { - Username = username, - SceneName = sceneName, - Position = position, - Scale = scale, - AnimationClipId = animationClipId + Username = username } ); } diff --git a/HKMP/Networking/Packet/Data/HelloServer.cs b/HKMP/Networking/Packet/Data/HelloServer.cs index eb3ea61..f3f1bb2 100644 --- a/HKMP/Networking/Packet/Data/HelloServer.cs +++ b/HKMP/Networking/Packet/Data/HelloServer.cs @@ -1,6 +1,4 @@ -using Hkmp.Math; - -namespace Hkmp.Networking.Packet.Data; +namespace Hkmp.Networking.Packet.Data; /// /// Packet data for the hello server data. @@ -17,45 +15,13 @@ internal class HelloServer : IPacketData { /// public string Username { get; set; } - /// - /// The name of the current scene of the player. - /// - public string SceneName { get; set; } - - /// - /// The position of the player. - /// - public Vector2 Position { get; set; } - - /// - /// The scale of the player. - /// - public bool Scale { get; set; } - - /// - /// The animation clip ID of the player. - /// - public ushort AnimationClipId { get; set; } - /// public void WriteData(IPacket packet) { packet.Write(Username); - packet.Write(SceneName); - - packet.Write(Position); - packet.Write(Scale); - - packet.Write(AnimationClipId); } /// public void ReadData(IPacket packet) { Username = packet.ReadString(); - SceneName = packet.ReadString(); - - Position = packet.ReadVector2(); - Scale = packet.ReadBool(); - - AnimationClipId = packet.ReadUShort(); } } diff --git a/HKMP/Ui/ConnectInterface.cs b/HKMP/Ui/ConnectInterface.cs index 559b37c..77dfec3 100644 --- a/HKMP/Ui/ConnectInterface.cs +++ b/HKMP/Ui/ConnectInterface.cs @@ -14,11 +14,6 @@ namespace Hkmp.Ui; /// Class for creating and managing the connect interface. /// internal class ConnectInterface { - /// - /// The address to connect to the local device. - /// - private const string LocalhostAddress = "127.0.0.1"; - /// /// The indent of some text elements. /// @@ -34,21 +29,11 @@ internal class ConnectInterface { /// private const string ConnectingText = "Connecting..."; - /// - /// The text of the connection button while connected. - /// - private const string DisconnectText = "Disconnect"; - /// /// The text of the host button while not hosting. /// private const string StartHostingText = "Start Hosting"; - /// - /// The text of the host button while hosting. - /// - private const string StopHostingText = "Stop Hosting"; - /// /// The time in seconds to hide the feedback text after it appeared. /// @@ -64,10 +49,10 @@ internal class ConnectInterface { /// private readonly ComponentGroup _connectGroup; - /// - /// The component group of the client settings UI. - /// - private readonly ComponentGroup _settingsGroup; + // /// + // /// The component group of the client settings UI. + // /// + // private readonly ComponentGroup _settingsGroup; /// /// The username input component. @@ -107,32 +92,22 @@ internal class ConnectInterface { /// /// Event that is executed when the connect button is pressed. /// - public event Action ConnectButtonPressed; - - /// - /// Event that is executed when the disconnect button is pressed. - /// - public event Action DisconnectButtonPressed; + public event Action ConnectButtonPressed; /// /// Event that is executed when the start hosting button is pressed. /// - public event Action StartHostButtonPressed; - - /// - /// The event that is executed when the stop hosting button is pressed. - /// - public event Action StopHostButtonPressed; + public event Action StartHostButtonPressed; public ConnectInterface( ModSettings modSettings, - ComponentGroup connectGroup, - ComponentGroup settingsGroup + ComponentGroup connectGroup + // ComponentGroup settingsGroup ) { _modSettings = modSettings; _connectGroup = connectGroup; - _settingsGroup = settingsGroup; + // _settingsGroup = settingsGroup; CreateConnectUi(); } @@ -142,7 +117,7 @@ ComponentGroup settingsGroup /// public void OnClientDisconnect() { _connectionButton.SetText(ConnectText); - _connectionButton.SetOnPress(() => OnConnectButtonPressed()); + _connectionButton.SetOnPress(OnConnectButtonPressed); _connectionButton.SetInteractable(true); } @@ -153,9 +128,8 @@ public void OnSuccessfulConnect() { // Let the user know that the connection was successful SetFeedbackText(Color.green, "Successfully connected"); - // Reset the connection button with the disconnect text and callback - _connectionButton.SetText(DisconnectText); - _connectionButton.SetOnPress(OnDisconnectButtonPressed); + // Reset the connection button with the disconnect text + _connectionButton.SetText(ConnectText); _connectionButton.SetInteractable(true); } @@ -200,7 +174,7 @@ public void OnFailedConnect(ConnectFailedResult result) { private void CreateConnectUi() { // Now we can start adding individual components to our UI // Keep track of current x and y of objects we want to place - var x = 1920f - 210f; + var x = 1920f / 2f; var y = 1080f - 100f; const float labelHeight = 20f; @@ -285,15 +259,15 @@ private void CreateConnectUi() { y -= ButtonComponent.DefaultHeight + 8f; - var settingsButton = new ButtonComponent( - _connectGroup, - new Vector2(x, y), - "Settings" - ); - settingsButton.SetOnPress(() => { - _connectGroup.SetActive(false); - _settingsGroup.SetActive(true); - }); + // var settingsButton = new ButtonComponent( + // _connectGroup, + // new Vector2(x, y), + // "Settings" + // ); + // settingsButton.SetOnPress(() => { + // _connectGroup.SetActive(false); + // _settingsGroup.SetActive(true); + // }); y -= ButtonComponent.DefaultHeight + 8f; @@ -312,8 +286,7 @@ private void CreateConnectUi() { /// /// Callback method for when the connect button is pressed. /// - /// Whether to execute this routine based on auto-connecting from hosting. - private void OnConnectButtonPressed(bool autoConnect = false) { + private void OnConnectButtonPressed() { var address = _addressInput.GetInput(); if (address.Length == 0) { @@ -335,14 +308,7 @@ private void OnConnectButtonPressed(bool autoConnect = false) { Logger.Debug($"Connect button pressed, address: {address}:{port}"); - var username = _usernameInput.GetInput(); - if (username.Length == 0 || username.Length > 20) { - if (username.Length > 20) { - SetFeedbackText(Color.red, "Failed to connect:\nUsername is too long"); - } else if (username.Length == 0) { - SetFeedbackText(Color.red, "Failed to connect:\nYou must enter a username"); - } - + if (!ValidateUsername(out var username)) { return; } @@ -358,22 +324,7 @@ private void OnConnectButtonPressed(bool autoConnect = false) { _connectionButton.SetText(ConnectingText); _connectionButton.SetInteractable(false); - ConnectButtonPressed?.Invoke(address, port, username, autoConnect); - } - - /// - /// Callback method for when the disconnect button is pressed. - /// - private void OnDisconnectButtonPressed() { - // Disconnect the client - DisconnectButtonPressed?.Invoke(); - - // Let the user know that the connection was successful - SetFeedbackText(Color.green, "Successfully disconnected"); - - _connectionButton.SetText(ConnectText); - _connectionButton.SetOnPress(() => OnConnectButtonPressed()); - _connectionButton.SetInteractable(true); + ConnectButtonPressed?.Invoke(address, port, username); } /// @@ -389,40 +340,13 @@ private void OnStartButtonPressed() { return; } - - // Start the server in networkManager - StartHostButtonPressed?.Invoke(port); - - _serverButton.SetText(StopHostingText); - _serverButton.SetOnPress(OnStopButtonPressed); - - // If the setting for automatically connecting when hosting is enabled, - // we connect the client to itself as well - if (_modSettings.AutoConnectWhenHosting) { - _addressInput.SetInput(LocalhostAddress); - - OnConnectButtonPressed(true); - - // Let the user know that the server has been started - SetFeedbackText(Color.green, "Successfully connected to hosted server"); - } else { - // Let the user know that the server has been started - SetFeedbackText(Color.green, "Successfully started server"); + + if (!ValidateUsername(out var username)) { + return; } - } - - /// - /// Callback method for when the stop hosting button is pressed. - /// - private void OnStopButtonPressed() { - // Stop the server in networkManager - StopHostButtonPressed?.Invoke(); - - _serverButton.SetText(StartHostingText); - _serverButton.SetOnPress(OnStartButtonPressed); - // Let the user know that the server has been stopped - SetFeedbackText(Color.green, "Successfully stopped server"); + // Start the server in networkManager + StartHostButtonPressed?.Invoke(username, port); } /// @@ -451,4 +375,19 @@ private IEnumerator WaitHideFeedbackText() { _feedbackText.SetActive(false); } + + private bool ValidateUsername(out string username) { + username = _usernameInput.GetInput(); + if (username.Length == 0 || username.Length > 20) { + if (username.Length > 20) { + SetFeedbackText(Color.red, "Failed to connect:\nUsername is too long"); + } else if (username.Length == 0) { + SetFeedbackText(Color.red, "Failed to connect:\nYou must enter a username"); + } + + return false; + } + + return true; + } } diff --git a/HKMP/Ui/UiManager.cs b/HKMP/Ui/UiManager.cs index 9941af0..920e835 100644 --- a/HKMP/Ui/UiManager.cs +++ b/HKMP/Ui/UiManager.cs @@ -1,4 +1,7 @@ -using GlobalEnums; +using System; +using System.Collections; +using System.Collections.Generic; +using GlobalEnums; using Hkmp.Api.Client; using Hkmp.Game.Settings; using Hkmp.Networking.Client; @@ -9,6 +12,7 @@ using UnityEngine.EventSystems; using UnityEngine.UI; using Logger = Hkmp.Logging.Logger; +using Object = UnityEngine.Object; namespace Hkmp.Ui; @@ -35,6 +39,26 @@ internal class UiManager : IUiManager { /// The font size of sub text. /// public const int SubTextFontSize = 22; + + /// + /// The address to connect to the local device. + /// + private const string LocalhostAddress = "127.0.0.1"; + + /// + /// Expression for the GameManager instance. + /// + private static GameManager GM => GameManager.instance; + + /// + /// Expression for the UIManager instance. + /// + private static UIManager UM => UIManager.instance; + + /// + /// Expression for the InputHandler instance. + /// + private static InputHandler IH => InputHandler.Instance; /// /// The global GameObject in which all UI is created. @@ -45,37 +69,50 @@ internal class UiManager : IUiManager { /// The chat box instance. /// internal static ChatBox InternalChatBox; - + /// - /// The connect interface. + /// Event that is fired when a server is requested to be hosted from the UI. /// - public ConnectInterface ConnectInterface { get; } + public event Action RequestServerStartHostEvent; /// - /// The client settings interface. + /// Event that is fired when a server is requested to be stopped. /// - public ClientSettingsInterface SettingsInterface { get; } + public event Action RequestServerStopHostEvent; /// - /// The mod settings. + /// Event that is fired when a connection is requested with the given username, IP, port and whether it was a + /// connection from hosting. /// - private readonly ModSettings _modSettings; + public event Action RequestClientConnectEvent; /// - /// The ping interface. + /// Event that is fired when a disconnect is requested. /// - private readonly PingInterface _pingInterface; + public event Action RequestClientDisconnectEvent; + + // /// + // /// The client settings interface. + // /// + // public ClientSettingsInterface SettingsInterface { get; } /// - /// Whether the UI is hidden by the key-bind. + /// The connect interface. /// - private bool _isUiHiddenByKeyBind; - + private readonly ConnectInterface _connectInterface; + /// - /// Whether the game is in a state where we normally show the pause menu UI for example in a gameplay - /// scene in the HK pause menu. + /// The ping interface. /// - private bool _canShowPauseUi; + private readonly PingInterface _pingInterface; + + private readonly ComponentGroup _pauseMenuGroup; + + private GameObject _backButtonObj; + + private List _originalBackTriggers; + + private Action _hostSaveSlotSelectedAction; #endregion @@ -87,12 +124,9 @@ internal class UiManager : IUiManager { #endregion public UiManager( - ServerSettings clientServerSettings, ModSettings modSettings, NetClient netClient ) { - _modSettings = modSettings; - // First we create a gameObject that will hold all other objects of the UI UiGameObject = new GameObject(); @@ -114,6 +148,7 @@ NetClient netClient var canvasScaler = UiGameObject.AddComponent(); canvasScaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize; canvasScaler.referenceResolution = new Vector2(1920f, 1080f); + canvasScaler.screenMatchMode = CanvasScaler.ScreenMatchMode.Expand; UiGameObject.AddComponent(); @@ -122,15 +157,16 @@ NetClient netClient var uiGroup = new ComponentGroup(); var pauseMenuGroup = new ComponentGroup(false, uiGroup); + _pauseMenuGroup = pauseMenuGroup; var connectGroup = new ComponentGroup(parent: pauseMenuGroup); - var settingsGroup = new ComponentGroup(parent: pauseMenuGroup); + // var settingsGroup = new ComponentGroup(parent: pauseMenuGroup); - ConnectInterface = new ConnectInterface( + _connectInterface = new ConnectInterface( modSettings, - connectGroup, - settingsGroup + connectGroup + // settingsGroup ); var inGameGroup = new ComponentGroup(parent: uiGroup); @@ -147,32 +183,21 @@ NetClient netClient netClient ); - SettingsInterface = new ClientSettingsInterface( - modSettings, - clientServerSettings, - settingsGroup, - connectGroup, - _pingInterface - ); + // SettingsInterface = new ClientSettingsInterface( + // modSettings, + // clientServerSettings, + // settingsGroup, + // connectGroup, + // _pingInterface + // ); // Register callbacks to make sure the UI is hidden and shown at correct times On.UIManager.SetState += (orig, self, state) => { orig(self, state); if (state == UIState.PAUSED) { - // Only show UI in gameplay scenes - if (!SceneUtil.IsNonGameplayScene(SceneUtil.GetCurrentSceneName())) { - _canShowPauseUi = true; - - pauseMenuGroup.SetActive(!_isUiHiddenByKeyBind); - } - inGameGroup.SetActive(false); } else { - pauseMenuGroup.SetActive(false); - - _canShowPauseUi = false; - // Only show chat box UI in gameplay scenes if (!SceneUtil.IsNonGameplayScene(SceneUtil.GetCurrentSceneName())) { inGameGroup.SetActive(true); @@ -180,18 +205,10 @@ NetClient netClient } }; UnityEngine.SceneManagement.SceneManager.activeSceneChanged += (oldScene, newScene) => { - if (SceneUtil.IsNonGameplayScene(newScene.name)) { - eventSystem.enabled = false; - - _canShowPauseUi = false; - - pauseMenuGroup.SetActive(false); - inGameGroup.SetActive(false); - } else { - eventSystem.enabled = true; - - inGameGroup.SetActive(true); - } + var isNonGamePlayScene = SceneUtil.IsNonGameplayScene(newScene.name); + + eventSystem.enabled = !isNonGamePlayScene; + inGameGroup.SetActive(!isNonGamePlayScene); }; // The game is automatically unpaused when the knight dies, so we need @@ -199,83 +216,341 @@ NetClient netClient // TODO: this still gives issues, since it displays the cursor while we are supposed to be unpaused ModHooks.AfterPlayerDeadHook += () => { pauseMenuGroup.SetActive(false); }; - MonoBehaviourUtil.Instance.OnUpdateEvent += () => { CheckKeyBinds(uiGroup); }; + ModHooks.LanguageGetHook += (key, sheet, orig) => { + if (key == "StartMultiplayerBtn" && sheet == "MainMenu") { + return "Start Multiplayer"; + } + + if (key == "MODAL_PROGRESS" && sheet == "MainMenu" && netClient.IsConnected) { + return "You will be disconnected"; + } + + return orig; + }; + + On.UIManager.UIGoToMainMenu += (orig, self) => { + orig(self); + + TryAddMultiOption(); + }; + + TryAddMultiOption(); + + var achievementsMenuControls = UM.achievementsMenuScreen.gameObject.FindGameObjectInChildren("Controls"); + if (achievementsMenuControls == null) { + Logger.Warn("achievementsMenuControls is null"); + return; + } + + var achievementsBackBtn = achievementsMenuControls.FindGameObjectInChildren("BackButton"); + if (achievementsBackBtn == null) { + Logger.Warn("achievementsBackBtn is null"); + return; + } + + _backButtonObj = Object.Instantiate(achievementsBackBtn, UiGameObject.transform); + _backButtonObj.SetActive(false); + + var eventTrigger = _backButtonObj.GetComponent(); + eventTrigger.triggers.Clear(); + + ChangeBtnTriggers(eventTrigger, () => UIManager.instance.StartCoroutine(ReturnToMainMenu())); + + _connectInterface.StartHostButtonPressed += (username, port) => { + _hostSaveSlotSelectedAction = SaveSlotSelectedCallback; + + On.GameManager.StartNewGame += OnStartNewGame; + On.GameManager.ContinueGame += OnContinueGame; + + void SaveSlotSelectedCallback() { + RequestServerStartHostEvent?.Invoke(port); + RequestClientConnectEvent?.Invoke(LocalhostAddress, port, username, true); + + On.GameManager.StartNewGame -= OnStartNewGame; + On.GameManager.ContinueGame -= OnContinueGame; + } + + UM.StartCoroutine(GoToSaveMenu()); + }; + + _connectInterface.ConnectButtonPressed += (address, port, username) => { + RequestClientConnectEvent?.Invoke(address, port, username, false); + }; + + On.UIManager.ReturnToMainMenu += (orig, self) => { + RequestClientDisconnectEvent?.Invoke(); + RequestServerStopHostEvent?.Invoke(); + + return orig(self); + }; } + + /// + /// Enter the game with the current PlayerData from the multiplayer menu. This assumes that the PlayerData + /// instance is populated with values already. + /// + public void EnterGameFromMultiplayerMenu() { + IH.StopUIInput(); - #region Internal UI manager methods + _pauseMenuGroup.SetActive(false); + _backButtonObj.SetActive(false); + UM.uiAudioPlayer.PlayStartGame(); + if (MenuStyles.Instance) { + MenuStyles.Instance.StopAudio(); + } + + GM.ContinueGame(); + } + /// - /// Callback method for when the client successfully connects. + /// Return to the main menu from in-game. Used whenever the player disconnects from the current server. /// - public void OnSuccessfulConnect() { - ConnectInterface.OnSuccessfulConnect(); - _pingInterface.SetEnabled(true); - SettingsInterface.OnSuccessfulConnect(); + public void ReturnToMainMenuFromGame() { + IH.StopUIInput(); + + UM.StartCoroutine(GM.ReturnToMainMenu( + GameManager.ReturnToMainMenuSaveModes.DontSave, + _ => { + UM.StartCoroutine(UM.HideCurrentMenu()); + } + )); + } + + /// + /// Callback method for when a new game is started. This is used to check when to start a hosted server from + /// the save menu. + /// + private void OnStartNewGame(On.GameManager.orig_StartNewGame orig, GameManager self, bool permaDeathMode, bool bossRushMode) { + orig(self, permaDeathMode, bossRushMode); + _hostSaveSlotSelectedAction.Invoke(); } /// - /// Callback method for when the client fails to connect. + /// Callback method for when a save file is continued. This is used to check when to start a hosted server from + /// the save menu. /// - /// The result of the failed connection. - public void OnFailedConnect(ConnectFailedResult result) { - ConnectInterface.OnFailedConnect(result); + private void OnContinueGame(On.GameManager.orig_ContinueGame orig, GameManager self) { + orig(self); + _hostSaveSlotSelectedAction.Invoke(); } /// - /// Callback method for when the client disconnects. + /// Try to add the multiplayer option to the menu screen. Will not add the option if is already exists. /// - public void OnClientDisconnect() { - ConnectInterface.OnClientDisconnect(); - _pingInterface.SetEnabled(false); - SettingsInterface.OnDisconnect(); + private void TryAddMultiOption() { + Logger.Info("AddMultiOption called"); + + var btnParent = UM.mainMenuButtons.gameObject; + if (btnParent == null) { + Logger.Info("btnParent is null"); + return; + } + + if (btnParent.FindGameObjectInChildren("StartMultiplayerButton") != null) { + Logger.Info("Multiplayer button is already present"); + return; + } + + var startGameBtn = UM.mainMenuButtons.startButton.gameObject; + if (startGameBtn == null) { + Logger.Info("startGameBtn is null"); + return; + } + + var startMultiBtn = Object.Instantiate(startGameBtn, btnParent.transform); + if (startMultiBtn == null) { + Logger.Info("startMultiBtn is null"); + return; + } + + startMultiBtn.name = "StartMultiplayerButton"; + startMultiBtn.transform.SetSiblingIndex(1); + + var autoLocalize = startMultiBtn.GetComponent(); + autoLocalize.textKey = "StartMultiplayerBtn"; + autoLocalize.RefreshTextFromLocalization(); + + // Fix navigation for buttons + var startMultiBtnMenuBtn = startMultiBtn.GetComponent(); + if (startMultiBtnMenuBtn != null) { + var nav = UM.mainMenuButtons.startButton.navigation; + nav.selectOnDown = startMultiBtnMenuBtn; + UM.mainMenuButtons.startButton.navigation = nav; + + nav = UM.mainMenuButtons.optionsButton.navigation; + nav.selectOnUp = startMultiBtnMenuBtn; + UM.mainMenuButtons.optionsButton.navigation = nav; + + nav = startMultiBtnMenuBtn.navigation; + nav.selectOnUp = UM.mainMenuButtons.startButton; + startMultiBtnMenuBtn.navigation = nav; + } + + var eventTrigger = startMultiBtn.GetComponent(); + eventTrigger.triggers.Clear(); + + ChangeBtnTriggers(eventTrigger, () => UM.StartCoroutine(GoToMultiplayerMenu())); } /// - /// Callback method for when the team setting in the changes. + /// Coroutine to go to the multiplayer menu of the main menu. /// - public void OnTeamSettingChange() { - SettingsInterface.OnTeamSettingChange(); + private IEnumerator GoToMultiplayerMenu() { + IH.StopUIInput(); + + if (UM.menuState == MainMenuState.MAIN_MENU) { + UM.StartCoroutine(ReflectionHelper.CallMethod(UM, "FadeOutSprite", UM.gameTitle)); + UM.subtitleFSM.SendEvent("FADE OUT"); + yield return UM.StartCoroutine(UM.FadeOutCanvasGroup(UM.mainMenuScreen)); + } else if (UM.menuState == MainMenuState.SAVE_PROFILES) { + yield return UM.StartCoroutine(UM.HideSaveProfileMenu()); + } + + IH.StartUIInput(); + + _pauseMenuGroup.SetActive(true); + _backButtonObj.SetActive(true); } /// - /// Check key-binds to show/hide the UI. + /// Coroutine to go back to the main menu from the multiplayer menu. /// - /// The component group for the entire UI. - private void CheckKeyBinds(ComponentGroup uiGroup) { - if (Input.GetKeyDown((KeyCode) _modSettings.HideUiKey)) { - // Only allow UI toggling within the pause menu, otherwise the chat input might interfere - if (_canShowPauseUi) { - _isUiHiddenByKeyBind = !_isUiHiddenByKeyBind; + /// + private IEnumerator ReturnToMainMenu() { + IH.StopUIInput(); + + _pauseMenuGroup.SetActive(false); + _backButtonObj.SetActive(false); + + UM.gameTitle.gameObject.SetActive(true); + UM.mainMenuScreen.gameObject.SetActive(true); + + if (MenuStyles.Instance) { + MenuStyles.Instance.UpdateTitle(); + } - Logger.Debug($"UI is now {(_isUiHiddenByKeyBind ? "hidden" : "shown")}"); + UM.StartCoroutine(ReflectionHelper.CallMethod(UM, "FadeInSprite", UM.gameTitle)); + UM.subtitleFSM.SendEvent("FADE IN"); - uiGroup.SetActive(!_isUiHiddenByKeyBind); - } + yield return UM.StartCoroutine(UM.FadeInCanvasGroup(UM.mainMenuScreen)); + + UM.mainMenuScreen.interactable = true; + + IH.StartUIInput(); + + yield return null; + + UM.mainMenuButtons.HighlightDefault(); + + UM.menuState = MainMenuState.MAIN_MENU; + } + + /// + /// Coroutine to go to the saves menu from the multiplayer menu. Used whenever the user selects to host a server. + /// + private IEnumerator GoToSaveMenu() { + _pauseMenuGroup.SetActive(false); + _backButtonObj.SetActive(false); + + yield return UM.GoToProfileMenu(); + + var saveProfilesBackBtn = UM.saveProfileControls.gameObject.FindGameObjectInChildren("BackButton"); + if (saveProfilesBackBtn == null) { + Logger.Info("saveProfilesBackBtn is null"); + yield break; } + + var eventTrigger = saveProfilesBackBtn.GetComponent(); + _originalBackTriggers = eventTrigger.triggers; + + eventTrigger.triggers = new List(); + ChangeBtnTriggers(eventTrigger, () => { + On.GameManager.StartNewGame -= OnStartNewGame; + On.GameManager.ContinueGame -= OnContinueGame; + + UM.StartCoroutine(GoToMultiplayerMenu()); + + eventTrigger.triggers = _originalBackTriggers; + }); + } + + /// + /// Change the triggers on a button with the given event trigger. + /// + /// The event trigger of the button to change. + /// The action that should be executed whenever the button is triggered. + private void ChangeBtnTriggers(EventTrigger eventTrigger, Action action) { + var entry = new EventTrigger.Entry { + eventID = EventTriggerType.Submit + }; + entry.callback.AddListener(_ => action.Invoke()); + eventTrigger.triggers.Add(entry); + + var entry2 = new EventTrigger.Entry { + eventID = EventTriggerType.PointerClick + }; + entry2.callback.AddListener(_ => action.Invoke()); + eventTrigger.triggers.Add(entry2); + } + + #region Internal UI manager methods + + /// + /// Callback method for when the client successfully connects. + /// + public void OnSuccessfulConnect() { + _connectInterface.OnSuccessfulConnect(); + _pingInterface.SetEnabled(true); + // SettingsInterface.OnSuccessfulConnect(); } + /// + /// Callback method for when the client fails to connect. + /// + /// The result of the failed connection. + public void OnFailedConnect(ConnectFailedResult result) { + _connectInterface.OnFailedConnect(result); + } + + /// + /// Callback method for when the client disconnects. + /// + public void OnClientDisconnect() { + _connectInterface.OnClientDisconnect(); + _pingInterface.SetEnabled(false); + // SettingsInterface.OnDisconnect(); + } + + // /// + // /// Callback method for when the team setting in the changes. + // /// + // public void OnTeamSettingChange() { + // SettingsInterface.OnTeamSettingChange(); + // } + #endregion #region IUiManager methods /// public void DisableTeamSelection() { - SettingsInterface.OnAddonSetTeamSelection(false); + // SettingsInterface.OnAddonSetTeamSelection(false); } /// public void EnableTeamSelection() { - SettingsInterface.OnAddonSetTeamSelection(true); + // SettingsInterface.OnAddonSetTeamSelection(true); } /// public void DisableSkinSelection() { - SettingsInterface.OnAddonSetSkinSelection(false); + // SettingsInterface.OnAddonSetSkinSelection(false); } /// public void EnableSkinSelection() { - SettingsInterface.OnAddonSetSkinSelection(true); + // SettingsInterface.OnAddonSetSkinSelection(true); } #endregion diff --git a/HKMPServer/ConsoleServerManager.cs b/HKMPServer/ConsoleServerManager.cs index 6bd6c6e..9e5aad2 100644 --- a/HKMPServer/ConsoleServerManager.cs +++ b/HKMPServer/ConsoleServerManager.cs @@ -1,7 +1,12 @@ using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; using Hkmp.Game.Server; using Hkmp.Game.Settings; +using Hkmp.Logging; using Hkmp.Networking.Packet; +using Hkmp.Networking.Packet.Data; using Hkmp.Networking.Server; using HkmpServer.Command; using HkmpServer.Logging; @@ -11,10 +16,25 @@ namespace HkmpServer { /// Specialization of the server manager for the console program. /// internal class ConsoleServerManager : ServerManager { + /// + /// Name of the file used to store save data. + /// + private const string SaveFileName = "save.dat"; + /// /// The logger class for logging to console. /// private readonly ConsoleLogger _consoleLogger; + + /// + /// Lock object for asynchronous access to the save file. + /// + private readonly object _saveFileLock = new object(); + + /// + /// The absolute file path of the save file. + /// + private string _saveFilePath; public ConsoleServerManager( NetServer netServer, @@ -35,6 +55,8 @@ ConsoleLogger consoleLogger Stop(); }; + + InitializeSaveFile(); } /// @@ -45,5 +67,101 @@ protected override void RegisterCommands() { CommandManager.RegisterCommand(new ConsoleSettingsCommand(this, InternalServerSettings)); CommandManager.RegisterCommand(new LogCommand(_consoleLogger)); } + + /// + protected override void OnSaveUpdate(ushort id, SaveUpdate packet) { + base.OnSaveUpdate(id, packet); + + // After the server manager has processed the save update, we write the current save data to file + WriteToSaveFile(CurrentSaveData); + } + + /// + /// Initialize the save file by either reading it from disk or creating a new one and writing it to disk. + /// + /// Thrown when the directory of the assembly could not be found. + private void InitializeSaveFile() { + // We first try to get the entry assembly in case the executing assembly was + // embedded in the standalone server + var assembly = Assembly.GetEntryAssembly(); + if (assembly == null) { + // If the entry assembly doesn't exist, we fall back on the executing assembly + assembly = Assembly.GetExecutingAssembly(); + } + + var currentPath = Path.GetDirectoryName(assembly.Location); + if (currentPath == null) { + throw new Exception("Could not get directory of assembly for save file"); + } + + lock (_saveFileLock) { + _saveFilePath = Path.Combine(currentPath, SaveFileName); + + // If the file exists, simply read it into the current save data for the server + // Otherwise, create an empty dictionary for save data and save it to file + if (File.Exists(_saveFilePath) && TryReadSaveFile(out var saveData)) { + CurrentSaveData = saveData; + } else { + CurrentSaveData = new Dictionary(); + + WriteToSaveFile(CurrentSaveData); + } + } + } + + /// + /// Try to read the save data in the save file into the given dictionary. + /// + /// The save data in a dictionary if it was read, otherwise null. + /// true if the save file could be read, false otherwise. + private bool TryReadSaveFile(out Dictionary saveData) { + lock (_saveFileLock) { + // Read the raw bytes from the file + var bytes = File.ReadAllBytes(_saveFilePath); + + try { + // We use the Packet class to easily read the raw bytes in the data + var packet = new Packet(bytes); + + // Then we use the CurrentSave class to parse the packet into the desired format + var currentSave = new CurrentSave(); + currentSave.ReadData(packet); + + saveData = currentSave.SaveData; + return true; + } catch (Exception e) { + Logger.Error($"Could not read the save data from file:\n{e}"); + } + + saveData = null; + return false; + } + } + + /// + /// Write the save data in the given dictionary to the save file. + /// + /// The dictionary containing the save data. + private void WriteToSaveFile(Dictionary saveData) { + lock (_saveFileLock) { + try { + // We use the Packet class to easily write the data to raw bytes + var packet = new Packet(); + + // Then we use the CurrentSave class to write the data into the packet as bytes + var currentSave = new CurrentSave { + SaveData = saveData + }; + currentSave.WriteData(packet); + + // And finally obtain the byte array to write to file + var bytes = packet.ToArray(); + + File.WriteAllBytes(_saveFilePath, bytes); + } catch (Exception e) { + Logger.Error($"Exception occurred while writing to save file:\n{e}"); + } + } + } } }