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