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) {