diff --git a/BloonsTD6 Mod Helper/Api/Coop/MessageUtils.cs b/BloonsTD6 Mod Helper/Api/Coop/MessageUtils.cs index 4ef07687b..9853ac4ad 100644 --- a/BloonsTD6 Mod Helper/Api/Coop/MessageUtils.cs +++ b/BloonsTD6 Mod Helper/Api/Coop/MessageUtils.cs @@ -1,42 +1,54 @@ -using Newtonsoft.Json; +using System; +using System.Text; +using Newtonsoft.Json; using NinjaKiwi.NKMulti; using UnhollowerBaseLib; namespace BTD_Mod_Helper.Api.Coop { + /// + /// Utility functions used for sending messages over the network. + /// public class MessageUtils { - public static Message CreateMessage(T objectToSend, string code = "") where T : Il2CppSystem.Object + /// + /// Creates a message to be sent over the network. + /// The message will be serialized as JSON. + /// + /// The object to be sent. The object's properties will be serialized. + /// Unique code for your specific message. + public static Message CreateMessageEx(T objectToSend, string code = "") { var json = JsonConvert.SerializeObject(objectToSend); - var serialize = Il2CppSystem.Text.Encoding.Default.GetBytes(json); + var serialize = Encoding.Default.GetBytes(json); code = string.IsNullOrEmpty(code) ? MelonMain.coopMessageCode : code; return new Message(code, serialize); - - //throw new System.Exception("This code was broken in BTD6 update 27.0"); - // commented code below broke in update 27.0 - //Il2CppStructArray serialize = SerialisationUtil.Serialise(objectToSend); - - // this code is commented because code above is broken - /*code = string.IsNullOrEmpty(code) ? MelonMain.coopMessageCode : code; - return new Message(code, serialize);*/ } + /// + /// Reads a message sent from the network. + /// Assumes message is sent as JSON. (via ) + /// + /// Raw bytes received from the network. public static T ReadMessage(Il2CppStructArray serializedMessage) { - var modMessage = Il2CppSystem.Text.Encoding.Default.GetString(serializedMessage); + var modMessage = Encoding.Default.GetString(serializedMessage); return JsonConvert.DeserializeObject(modMessage); - - //throw new System.Exception("This code was broken in BTD6 update 27.0"); - // commented code below broke in update 27.0 - //return SerialisationUtil.Deserialise(serializedMessage); } + /// + /// Reads a message sent from the network. + /// Assumes message is sent as JSON. (via ) + /// + /// Message received from the network. public static T ReadMessage(Message message) { - //throw new System.Exception("This code was broken in BTD6 update 27.0"); - // commented code below broke in update 27.0 return ReadMessage(message.bytes); } + + #region Backwards Binary Compatibility + [Obsolete($"For backwards compatibility reasons only, please use {nameof(CreateMessageEx)}")] + public static Message CreateMessage(T objectToSend, string code = "") where T : Il2CppSystem.Object => CreateMessage(objectToSend, code); + #endregion } } \ No newline at end of file diff --git a/BloonsTD6 Mod Helper/BloonsTD6 Mod Helper.csproj b/BloonsTD6 Mod Helper/BloonsTD6 Mod Helper.csproj index 95001025e..3df152cdb 100644 --- a/BloonsTD6 Mod Helper/BloonsTD6 Mod Helper.csproj +++ b/BloonsTD6 Mod Helper/BloonsTD6 Mod Helper.csproj @@ -12,6 +12,8 @@ v4.7.2 512 true + latest + true @@ -19,7 +21,6 @@ BloonsTD6;TRACE full AnyCPU - 7.3 prompt obj\BloonsTD6 - Debug\BloonsTD6 Mod Helper.xml true @@ -31,7 +32,6 @@ true pdbonly AnyCPU - 7.3 prompt bin\BloonsTD6 - Release\BloonsTD6 Mod Helper.xml true @@ -123,6 +123,8 @@ + + diff --git a/BloonsTD6 Mod Helper/BloonsTD6Mod.cs b/BloonsTD6 Mod Helper/BloonsTD6Mod.cs index 14e07f990..fc3312d37 100644 --- a/BloonsTD6 Mod Helper/BloonsTD6Mod.cs +++ b/BloonsTD6 Mod Helper/BloonsTD6Mod.cs @@ -21,6 +21,7 @@ using Assets.Scripts.Unity.Display; using Assets.Scripts.Simulation.Towers.Behaviors.Attack; using Assets.Scripts.Simulation.Towers.Behaviors.Abilities; +using BTD_Mod_Helper.Api.Coop; namespace BTD_Mod_Helper { @@ -41,22 +42,73 @@ public static void AddTowerToGame(TowerModel newTowerModel, TowerDetailsModel to Game.instance.model.AddTowerToGame(newTowerModel, towerDetailsModel); } + #region Networking Hooks - #region Misc Hooks + /// + /// Executed once the user has connected to a game session. + /// Note: Only invoked if == true. + /// + /// The interface used to interact with the game. + public virtual void OnConnected(NKMultiGameInterface nkGi) + { + } + + /// + /// Executed once the user has tried to connect to a server, but failed to do so. + /// Note: Only invoked if == true. + /// + /// The interface used to interact with the game. + public virtual void OnConnectFail(NKMultiGameInterface nkGi) + { + } + + /// + /// Executed once the player has disconnected from a server. + /// Note: Only invoked if == true. + /// + /// The interface used to interact with the game. + public virtual void OnDisconnected(NKMultiGameInterface nkGi) + { + } + + /// + /// Executed when a new client has joined the game session. + /// Note: Only invoked if == true. + /// + /// The interface used to interact with the game. + /// Index of the peer in question. + public virtual void OnPeerConnected(NKMultiGameInterface nkGi, int peerId) + { + } + + /// + /// Executed when a new client has left the game session. + /// Note: Only invoked if == true. + /// + /// The interface used to interact with the game. + /// Index of the peer in question. + public virtual void OnPeerDisconnected(NKMultiGameInterface nkGi, int peerId) + { + } /// /// Acts on a Network message that's been sent to the client ///
- /// Use Game.instance.GetNKgI().ReadMessage<YOUR_CLASS_NAME>(message) to get back the same object/class you sent. + /// Use to read back the message you sent. ///
/// If this is one of your messages and you're consuming and acting on it, return true. /// Otherwise, return false. Seriously. + /// Note: Only invoked if == true. ///
public virtual bool ActOnMessage(Message message) { return false; } + #endregion + + + #region Misc Hooks /// /// Called when the player's ProfileModel is loaded. diff --git a/BloonsTD6 Mod Helper/Extensions/CoopExtensions/NKMultiGameInterfaceExt.cs b/BloonsTD6 Mod Helper/Extensions/CoopExtensions/NKMultiGameInterfaceExt.cs index a8d067773..2f072e01f 100644 --- a/BloonsTD6 Mod Helper/Extensions/CoopExtensions/NKMultiGameInterfaceExt.cs +++ b/BloonsTD6 Mod Helper/Extensions/CoopExtensions/NKMultiGameInterfaceExt.cs @@ -1,4 +1,6 @@ -using Assets.Scripts.Unity; +using System; +using Assets.Scripts.Unity; +using Assets.Scripts.Unity.UI_New.InGame; using BTD_Mod_Helper.Api.Coop; using NinjaKiwi.NKMulti; using UnhollowerBaseLib; @@ -8,28 +10,42 @@ namespace BTD_Mod_Helper.Extensions public static class NKMultiGameInterfaceExt { /// - /// Send a Message to all players in the lobby + /// Returns true if the player is a host in a co-op game. + /// Works for both lobby and in-game. /// - /// Message to send - public static void SendMessage(this NKMultiGameInterface nkGI, Message message) + public static bool IsCoOpHost(this NKMultiGameInterface nkGi) { - nkGI.relayConnection.Writer.Write(message); + var inGame = InGame.instance; + var game = Game.instance; + + // Check lobby first. + var connection = game?.GetCoopLobbyConnection(); + if (connection != null) + return connection.IsAuthority; + + // Then check game. + if (inGame == null || !inGame.IsCoOpReady()) + return nkGi.PeerID == 1; // The game does this! for lobby etc. Checked in IDA. + + return inGame.coopGame.IsAuthority(); } + /// + /// Send a Message to all players in the lobby + /// + /// Message to send + public static void SendMessage(this NKMultiGameInterface nkGI, Message message) => nkGI.relayConnection.Writer.Write(message); + /// /// Convert an object to json and send it players or a player in the lobby /// - /// Object you want to send + /// Object you want to send. The properties of the object will be serialised as JSON. /// The id of the peer you want the message to go to. Leave null if you want to send to all players /// Coop code used to distinguish this message from others. Like a lock and key for reading messages - public static void SendMessage(this NKMultiGameInterface nkGI, T objectToSend, byte? peerId = null, string code = "") where T : Il2CppSystem.Object + public static void SendMessageEx(this NKMultiGameInterface nkGI, T objectToSend, byte? peerId = null, string code = "") { - Message message = MessageUtils.CreateMessage(objectToSend, code); - - var str = Il2CppSystem.Text.Encoding.Default.GetString(message.bytes); - MelonLoader.MelonLogger.Msg($"str: {str}"); - - if (peerId.HasValue && peerId != null) + var message = MessageUtils.CreateMessageEx(objectToSend, code); + if (peerId.HasValue && peerId.HasValue) nkGI.SendToPeer(peerId.Value, message); else nkGI.relayConnection.Writer.Write(message); @@ -43,12 +59,8 @@ public static void SendMessage(this NKMultiGameInterface nkGI, T objectToSend /// Coop code used to distinguish this message from others. Like a lock and key for reading messages public static void SendMessage(this NKMultiGameInterface nkGI, Il2CppSystem.String objectToSend, byte? peerId = null, string code = "") { - Message message = MessageUtils.CreateMessage(objectToSend, code); - - var str = Il2CppSystem.Text.Encoding.Default.GetString(message.bytes); - MelonLoader.MelonLogger.Msg($"str: {str}"); - - if (peerId.HasValue && peerId != null) + var message = MessageUtils.CreateMessage(objectToSend, code); + if (peerId.HasValue && peerId.HasValue) nkGI.SendToPeer(peerId.Value, message); else nkGI.relayConnection.Writer.Write(message); @@ -82,9 +94,21 @@ public static Chat_Message ReadChatMessage(this NKMultiGameInterface nkGI, Messa { if (message.Code != Chat_Message.chatCoopCode) return null; + string json = nkGI.ReadMessage(message.bytes); Chat_Message deserialized = Game.instance.GetJsonSerializer().DeserializeJson(json); return deserialized; } + + #region Backwards Binary Compatibility + /// + /// Convert an object to json and send it players or a player in the lobby + /// + /// Object you want to send. The properties of the object will be serialised as JSON. + /// The id of the peer you want the message to go to. Leave null if you want to send to all players + /// Coop code used to distinguish this message from others. Like a lock and key for reading messages + [Obsolete($"For backwards compatibility reasons only, please use {nameof(SendMessageEx)}")] + public static void SendMessage(this NKMultiGameInterface nkGI, T objectToSend, byte? peerId = null, string code = "") where T : Il2CppSystem.Object => SendMessageEx(nkGI, objectToSend, peerId, code); + #endregion } } \ No newline at end of file diff --git a/BloonsTD6 Mod Helper/Extensions/GameExt.cs b/BloonsTD6 Mod Helper/Extensions/GameExt.cs index 36c54b86c..fc84c018f 100644 --- a/BloonsTD6 Mod Helper/Extensions/GameExt.cs +++ b/BloonsTD6 Mod Helper/Extensions/GameExt.cs @@ -13,12 +13,27 @@ using NinjaKiwi.NKMulti; using NinjaKiwi.Players.Files.SaveStrategies; using System.Collections.Generic; +using Assets.Scripts.Unity.UI_New.Coop; +using NinjaKiwi.LiNK.Lobbies; using UnhollowerBaseLib; namespace BTD_Mod_Helper.Extensions { public static partial class GameExt { + /// + /// Returns the current lobby screen. + /// + public static CoopLobbyScreen? GetCoopLobbyScreen(this Game game) + { + return (game.GetMenuManager()?.currMenu?.Item2?.TryCast()); + } + + /// + /// Returns the current lobby connection. + /// + public static LobbyConnection? GetCoopLobbyConnection(this Game game) => game.GetCoopLobbyScreen()?.lobbyConnection; + /// /// Returns the directory where the Player's Profile.save file is located. /// Not set until after reaching the Main Menu for the first time diff --git a/BloonsTD6 Mod Helper/Extensions/InGameExt.cs b/BloonsTD6 Mod Helper/Extensions/InGameExt.cs index f286412a2..8e0d967cb 100644 --- a/BloonsTD6 Mod Helper/Extensions/InGameExt.cs +++ b/BloonsTD6 Mod Helper/Extensions/InGameExt.cs @@ -13,6 +13,21 @@ namespace BTD_Mod_Helper.Extensions { public static partial class InGameExt { + /// + /// Returns true if the initial co-op handshake has finished and user has co-op game details. + /// + /// The game. + public static bool IsCoOpReady(this InGame? inGame) + { + if (inGame == null) + return false; + + if (!inGame.IsCoop) + return false; + + return inGame.coopGame != null; + } + /// /// Custom API method that changes the game's round set to a custom RoundSetModel. /// diff --git a/BloonsTD6 Mod Helper/Patches/Player/NKMultiGameInterface_Connect.cs b/BloonsTD6 Mod Helper/Patches/Player/NKMultiGameInterface_Connect.cs new file mode 100644 index 000000000..0c1ff1fb7 --- /dev/null +++ b/BloonsTD6 Mod Helper/Patches/Player/NKMultiGameInterface_Connect.cs @@ -0,0 +1,33 @@ +using System; +using HarmonyLib; +using MelonLoader; +using NinjaKiwi.NKMulti; +using Task = Il2CppSystem.Threading.Tasks.Task; + +namespace BTD_Mod_Helper.Patches.Player +{ + [HarmonyPatch(typeof(NKMultiGameInterface), nameof(NKMultiGameInterface.Connect))] + internal class NKMultiGameInterface_Connect + { + [HarmonyPostfix] + public static void Postfix(NKMultiGameInterface __instance, ref Task __result) => __result.ContinueWith(new Action(task => OnConnectTaskFinished(__instance, task))); + + private static void OnConnectTaskFinished(NKMultiGameInterface instance, Task obj) + { + if (instance.IsConnected) + { + MelonMain.PerformHook(mod => { mod.OnConnected(instance); }); + instance.add_PeerConnectedEvent(new Action((peerId) => OnPeerConnected(instance, peerId))); + instance.add_PeerDisconnectedEvent(new Action((peerId) => OnPeerDisconnected(instance, peerId))); + } + else + { + MelonMain.PerformHook(mod => { mod.OnConnectFail(instance); }); + } + } + + private static void OnPeerDisconnected(NKMultiGameInterface nkGi, int peerId) => MelonMain.PerformHook(mod => { mod.OnPeerDisconnected(nkGi, peerId); }); + + private static void OnPeerConnected(NKMultiGameInterface nkGi, int peerId) => MelonMain.PerformHook(mod => { mod.OnPeerConnected(nkGi, peerId); }); + } +} diff --git a/BloonsTD6 Mod Helper/Patches/Player/NKMultiGameInterface_Disconnect.cs b/BloonsTD6 Mod Helper/Patches/Player/NKMultiGameInterface_Disconnect.cs new file mode 100644 index 000000000..2d6bf9107 --- /dev/null +++ b/BloonsTD6 Mod Helper/Patches/Player/NKMultiGameInterface_Disconnect.cs @@ -0,0 +1,12 @@ +using HarmonyLib; +using NinjaKiwi.NKMulti; + +namespace BTD_Mod_Helper.Patches.Player +{ + [HarmonyPatch(typeof(NKMultiGameInterface), nameof(NKMultiGameInterface.Disconnect))] + public class NKMultiGameInterface_Disconnect + { + [HarmonyPostfix] + public static void Postfix(NKMultiGameInterface __instance) => MelonMain.PerformHook(mod => { mod.OnDisconnected(__instance); }); + } +} diff --git a/BloonsTD6 Mod Helper/Patches/Player/NKMultiGameInterface_Update.cs b/BloonsTD6 Mod Helper/Patches/Player/NKMultiGameInterface_Update.cs index 18cfc86a5..cb8f7a45b 100644 --- a/BloonsTD6 Mod Helper/Patches/Player/NKMultiGameInterface_Update.cs +++ b/BloonsTD6 Mod Helper/Patches/Player/NKMultiGameInterface_Update.cs @@ -6,10 +6,22 @@ namespace BTD_Mod_Helper.Patches [HarmonyPatch(typeof(NKMultiGameInterface), nameof(NKMultiGameInterface.Update))] internal class NKMultiGameInterface_Update { - [HarmonyPostfix] - internal static void Postfix(ref NKMultiGameInterface __instance) + [HarmonyPriority(Priority.HigherThanNormal)] + [HarmonyPrefix] + internal static void Prefix(ref NKMultiGameInterface __instance) { SessionData.Instance.NkGI = __instance; + if (__instance.relayConnection == null) + return; + + // There exist some game states where send/receive might not be called + // on a regular basis, e.g. co-op menu, leading to lost messages. + + // In addition, it seems the game will also clear the receive queue in some + // cases. I think it's in this update method, but I (Sewer) have not confirmed this. + // In any case, this should help prevent lost messages. + __instance.relayConnection.Receive(); + __instance.relayConnection.Send(); } } } \ No newline at end of file