diff --git a/HKMP/Api/Client/ClientAddon.cs b/HKMP/Api/Client/ClientAddon.cs index d36ac934..4d9fa5e3 100644 --- a/HKMP/Api/Client/ClientAddon.cs +++ b/HKMP/Api/Client/ClientAddon.cs @@ -10,7 +10,18 @@ public abstract class ClientAddon : Addon.Addon { /// /// The client API interface. /// - protected IClientApi ClientApi { get; private set; } + private IClientApi _clientApi; + + /// + protected IClientApi ClientApi { + get { + if (this is TogglableClientAddon { Disabled: true }) { + throw new InvalidOperationException("Addon is disabled, cannot use Client API in this state"); + } + + return _clientApi; + } + } /// /// The logger for logging information. @@ -37,7 +48,7 @@ public abstract class ClientAddon : Addon.Addon { /// /// The client API instance. internal void InternalInitialize(IClientApi clientApi) { - ClientApi = clientApi; + _clientApi = clientApi; Initialize(clientApi); } diff --git a/HKMP/Api/Client/ClientAddonManager.cs b/HKMP/Api/Client/ClientAddonManager.cs index 5427d1e9..437ea573 100644 --- a/HKMP/Api/Client/ClientAddonManager.cs +++ b/HKMP/Api/Client/ClientAddonManager.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using Hkmp.Api.Client.Networking; +using Hkmp.Game.Settings; using Hkmp.Logging; using Hkmp.Networking.Packet.Data; @@ -27,6 +28,11 @@ internal class ClientAddonManager { /// private readonly ClientApi _clientApi; + /// + /// The mod settings instance for storing disabled addons. + /// + private readonly ModSettings _modSettings; + /// /// A list of all loaded addons, the order is important as it is the exact order /// in which we sent it to the server and are expected to act on when receiving a response. @@ -49,8 +55,10 @@ static ClientAddonManager() { /// Construct the addon manager with the client API. /// /// The client API instance. - public ClientAddonManager(ClientApi clientApi) { + /// The mod setting instance. + public ClientAddonManager(ClientApi clientApi, ModSettings modSettings) { _clientApi = clientApi; + _modSettings = modSettings; _addons = new List(); _networkedAddons = new Dictionary<(string, string), ClientAddon>(); @@ -93,6 +101,14 @@ public void LoadAddons() { continue; } + // Check if this addon was saved in the mod settings as disabled and then re-disable it + if ( + addon is TogglableClientAddon togglableAddon && + _modSettings.DisabledAddons.Contains(addon.GetName()) + ) { + togglableAddon.Disabled = true; + } + _addons.Add(addon); if (addon.NeedsNetwork) { @@ -122,12 +138,22 @@ public List GetNetworkedAddonData() { var addonData = new List(); foreach (var addon in _networkedAddons.Values) { + if (addon is TogglableClientAddon { Disabled: true }) { + continue; + } + addonData.Add(new AddonData(addon.GetName(), addon.GetVersion())); } return addonData; } + /// + /// Get a read-only list of all loaded addons. + /// + /// A read-only list of instances. + public IReadOnlyList GetLoadedAddons() => _addons; + /// /// Updates the order of all networked addons according to the given order. /// @@ -137,9 +163,9 @@ public void UpdateNetworkedAddonOrder(byte[] addonOrder) { // The order of the addons in our local list should stay the same // between connection and obtaining the addon order from the server - foreach (var addon in _addons) { - // Skip all non-networked addons - if (!addon.NeedsNetwork) { + foreach (var addon in _networkedAddons.Values) { + // Skip addons that are disabled + if (addon is TogglableClientAddon { Disabled: true }) { continue; } @@ -172,6 +198,52 @@ public void ClearNetworkedAddonIds() { } } + /// + /// Try to enable the addon with the given name. + /// + /// The name of the addon to enable. + /// True if the addon with the given name was enabled; otherwise false. + public bool TryEnableAddon(string addonName) { + foreach (var addon in _addons) { + if (addon.GetName() == addonName) { + if (addon is not TogglableClientAddon togglableAddon) { + return false; + } + + togglableAddon.Disabled = false; + + _modSettings.DisabledAddons.Remove(addon.GetName()); + + return true; + } + } + + return false; + } + + /// + /// Try to disable the addon with the given name. + /// + /// The name of the addon to disable. + /// True if the addon with the given name was disable; otherwise false. + public bool TryDisableAddon(string addonName) { + foreach (var addon in _addons) { + if (addon.GetName() == addonName) { + if (addon is not TogglableClientAddon togglableAddon) { + return false; + } + + togglableAddon.Disabled = true; + + _modSettings.DisabledAddons.Add(addon.GetName()); + + return true; + } + } + + return false; + } + /// /// Register an addon class from outside of HKMP. /// diff --git a/HKMP/Api/Client/IClientManager.cs b/HKMP/Api/Client/IClientManager.cs index cf9109e3..53ed9f15 100644 --- a/HKMP/Api/Client/IClientManager.cs +++ b/HKMP/Api/Client/IClientManager.cs @@ -28,6 +28,11 @@ public interface IClientManager { /// IReadOnlyCollection Players { get; } + /// + /// Disconnect the local client from the server. + /// + void Disconnect(); + /// /// Get a specific player by their ID. /// diff --git a/HKMP/Api/Client/TogglableClientAddon.cs b/HKMP/Api/Client/TogglableClientAddon.cs new file mode 100644 index 00000000..6b813e70 --- /dev/null +++ b/HKMP/Api/Client/TogglableClientAddon.cs @@ -0,0 +1,51 @@ +using System; + +namespace Hkmp.Api.Client; + +/// +/// Abstract class for a client addon that can be toggled. Extends . +/// +public abstract class TogglableClientAddon : ClientAddon { + /// + /// Whether this addon is disabled, meaning network is restricted + /// + private bool _disabled; + + /// + public bool Disabled { + get => _disabled; + internal set { + var valueChanged = _disabled != value; + + _disabled = value; + + if (!valueChanged) { + return; + } + + if (value) { + try { + OnDisable(); + } catch (Exception e) { + Logger.Error($"Exception was thrown while calling OnDisable for addon '{GetName()}':\n{e}"); + } + } else { + try { + OnEnable(); + } catch (Exception e) { + Logger.Error($"Exception was thrown while calling OnEnable for addon '{GetName()}':\n{e}"); + } + } + } + } + + /// + /// Callback method for when this addon gets enabled. + /// + protected abstract void OnEnable(); + + /// + /// Callback method for when this addon gets disabled. + /// + protected abstract void OnDisable(); +} diff --git a/HKMP/Game/Client/ClientManager.cs b/HKMP/Game/Client/ClientManager.cs index 1d255a00..14742e19 100644 --- a/HKMP/Game/Client/ClientManager.cs +++ b/HKMP/Game/Client/ClientManager.cs @@ -185,10 +185,11 @@ ModSettings modSettings _commandManager = new ClientCommandManager(); var eventAggregator = new EventAggregator(); - RegisterCommands(); var clientApi = new ClientApi(this, _commandManager, uiManager, netClient, eventAggregator); - _addonManager = new ClientAddonManager(clientApi); + _addonManager = new ClientAddonManager(clientApi, _modSettings); + + RegisterCommands(); ModHooks.FinishedLoadingModsHook += _addonManager.LoadAddons; @@ -229,7 +230,7 @@ ModSettings modSettings UiManager.InternalChatBox.ChatInputEvent += OnChatInput; - netClient.ConnectEvent += response => uiManager.OnSuccessfulConnect(); + netClient.ConnectEvent += _ => uiManager.OnSuccessfulConnect(); netClient.ConnectFailedEvent += OnConnectFailed; // Register the Hero Controller Start, which is when the local player spawns @@ -266,6 +267,7 @@ ModSettings modSettings private void RegisterCommands() { _commandManager.RegisterCommand(new ConnectCommand(this)); _commandManager.RegisterCommand(new HostCommand(_serverManager)); + _commandManager.RegisterCommand(new AddonCommand(_addonManager, _netClient)); } /// @@ -296,44 +298,43 @@ public void Connect(string address, int port, string username) { ); } - /// - /// Disconnect the local client from the server. - /// - /// Whether to tell the server we are disconnecting. - public void Disconnect(bool sendDisconnect = true) { + /// + public void Disconnect() { if (_netClient.IsConnected) { - if (sendDisconnect) { - // First send the server that we are disconnecting - Logger.Info("Sending PlayerDisconnect packet"); - _netClient.UpdateManager.SetPlayerDisconnect(); - } + // Send the server that we are disconnecting + Logger.Info("Sending PlayerDisconnect packet"); + _netClient.UpdateManager.SetPlayerDisconnect(); - // Then actually disconnect - _netClient.Disconnect(); + InternalDisconnect(); + } + } - // Let the player manager know we disconnected - _playerManager.OnDisconnect(); + /// + /// Internal logic for disconnecting from the server. + /// + private void InternalDisconnect() { + _netClient.Disconnect(); - // Clear the player data dictionary - _playerData.Clear(); + // Let the player manager know we disconnected + _playerManager.OnDisconnect(); - _uiManager.OnClientDisconnect(); + // Clear the player data dictionary + _playerData.Clear(); - _addonManager.ClearNetworkedAddonIds(); + _uiManager.OnClientDisconnect(); - // Check whether the game is in the pause menu and reset timescale to 0 in that case - if (UIManager.instance.uiState.Equals(UIState.PAUSED)) { - PauseManager.SetTimeScale(0); - } + _addonManager.ClearNetworkedAddonIds(); - try { - DisconnectEvent?.Invoke(); - } catch (Exception e) { - Logger.Warn( - $"Exception thrown while invoking Disconnect event:\n{e}"); - } - } else { - Logger.Warn("Could not disconnect client, it was not connected"); + // Check whether the game is in the pause menu and reset timescale to 0 in that case + if (UIManager.instance.uiState.Equals(UIState.PAUSED)) { + PauseManager.SetTimeScale(0); + } + + try { + DisconnectEvent?.Invoke(); + } catch (Exception e) { + Logger.Warn( + $"Exception thrown while invoking Disconnect event:\n{e}"); } } @@ -358,8 +359,12 @@ private void OnConnectFailed(ConnectFailedResult result) { var addonVersion = addonData.Version; var message = $" {addonName} v{addonVersion}"; - if (_addonManager.TryGetNetworkedAddon(addonName, addonVersion, out _)) { - message += " (installed)"; + if (_addonManager.TryGetNetworkedAddon(addonName, addonVersion, out var addon)) { + if (addon is TogglableClientAddon { Disabled: true }) { + message += " (disabled)"; + } else { + message += " (installed)"; + } } else { message += " (missing)"; } @@ -521,7 +526,7 @@ private void OnDisconnect(ServerClientDisconnect disconnect) { } // Disconnect without sending the server that we disconnect, because the server knows that already - Disconnect(false); + InternalDisconnect(); } /// diff --git a/HKMP/Game/Command/Client/AddonCommand.cs b/HKMP/Game/Command/Client/AddonCommand.cs new file mode 100644 index 00000000..89814b6a --- /dev/null +++ b/HKMP/Game/Command/Client/AddonCommand.cs @@ -0,0 +1,101 @@ +using System; +using System.Linq; +using Hkmp.Api.Client; +using Hkmp.Api.Command.Client; +using Hkmp.Networking.Client; +using Hkmp.Ui; + +namespace Hkmp.Game.Command.Client; + +/// +/// Command for managing client-side addons, such as enabling and disabling them. +/// +internal class AddonCommand : IClientCommand { + /// + public string Trigger => "/addon"; + + /// + public string[] Aliases => Array.Empty(); + + /// + /// The client addon manager instance. + /// + private readonly ClientAddonManager _addonManager; + + /// + /// The net client instance. + /// + private readonly NetClient _netClient; + + public AddonCommand(ClientAddonManager addonManager, NetClient netClient) { + _addonManager = addonManager; + _netClient = netClient; + } + + /// + public void Execute(string[] arguments) { + if (arguments.Length < 2) { + SendUsage(); + return; + } + + var action = arguments[1]; + + if (action == "list") { + var message = "Loaded addons: "; + message += string.Join( + ", ", + _addonManager.GetLoadedAddons().Select(addon => { + var msg = $"{addon.GetName()} {addon.GetVersion()}"; + if (addon is TogglableClientAddon {Disabled: true }) { + msg += " (disabled)"; + } + + return msg; + }) + ); + + UiManager.InternalChatBox.AddMessage(message); + return; + } + + if ((action != "enable" && action != "disable") || arguments.Length < 3) { + SendUsage(); + return; + } + + if (_netClient.IsConnected || _netClient.IsConnecting) { + UiManager.InternalChatBox.AddMessage("Cannot toggle addons while connecting or connected to a server."); + return; + } + + if (action == "enable") { + for (var i = 2; i < arguments.Length; i++) { + var addonName = arguments[i]; + + if (_addonManager.TryEnableAddon(addonName)) { + UiManager.InternalChatBox.AddMessage($"Successfully enabled '{addonName}'"); + } else { + UiManager.InternalChatBox.AddMessage($"Could not enable addon '{addonName}'"); + } + } + } else if (action == "disable") { + for (var i = 2; i < arguments.Length; i++) { + var addonName = arguments[i]; + + if (_addonManager.TryDisableAddon(addonName)) { + UiManager.InternalChatBox.AddMessage($"Successfully disabled '{addonName}'"); + } else { + UiManager.InternalChatBox.AddMessage($"Could not disable addon '{addonName}'"); + } + } + } + } + + /// + /// Sends the command usage to the chat box. + /// + private void SendUsage() { + UiManager.InternalChatBox.AddMessage($"Usage: {Trigger} [addon(s)]"); + } +} diff --git a/HKMP/Game/Settings/ModSettings.cs b/HKMP/Game/Settings/ModSettings.cs index d0e3b443..fc92a93d 100644 --- a/HKMP/Game/Settings/ModSettings.cs +++ b/HKMP/Game/Settings/ModSettings.cs @@ -1,4 +1,5 @@ -using Newtonsoft.Json; +using System.Collections.Generic; +using Newtonsoft.Json; using Newtonsoft.Json.Converters; using UnityEngine; @@ -50,6 +51,11 @@ internal class ModSettings { /// public bool AutoConnectWhenHosting { get; set; } = true; + /// + /// Set of addon names for addons that are disabled by the user. + /// + public HashSet DisabledAddons { get; set; } = new(); + /// /// The last used server settings in a hosted server. /// diff --git a/HKMP/Networking/Client/NetClient.cs b/HKMP/Networking/Client/NetClient.cs index dc5b4229..88c7d010 100644 --- a/HKMP/Networking/Client/NetClient.cs +++ b/HKMP/Networking/Client/NetClient.cs @@ -60,6 +60,11 @@ internal class NetClient : INetClient { /// Boolean denoting whether the client is connected to a server. /// public bool IsConnected { get; private set; } + + /// + /// Boolean denoting whether the client is currently attempting connection. + /// + public bool IsConnecting { get; private set; } /// /// Cancellation token source for the task for the update manager. @@ -94,6 +99,7 @@ private void OnConnect(LoginResponse loginResponse) { ThreadUtil.RunActionOnMainThread(() => { ConnectEvent?.Invoke(loginResponse); }); IsConnected = true; + IsConnecting = false; } /// @@ -113,6 +119,7 @@ private void OnConnectFailed(ConnectFailedResult result) { UpdateManager?.StopUpdates(); IsConnected = false; + IsConnecting = false; // Request cancellation for the update task _updateTaskTokenSource?.Cancel(); @@ -201,6 +208,8 @@ public void Connect( string authKey, List addonData ) { + IsConnecting = true; + try { _udpNetClient.Connect(address, port); } catch (SocketException e) {