diff --git a/PolyMod.csproj b/PolyMod.csproj
index e598697..cb82dca 100644
--- a/PolyMod.csproj
+++ b/PolyMod.csproj
@@ -11,7 +11,7 @@
IL2CPP
PolyMod
- 1.2.12
+ 1.3.0-pre
2.16.5.15700
PolyModdingTeam
The Battle of Polytopia's mod loader.
diff --git a/src/Multiplayer/Client.cs b/src/Multiplayer/Client.cs
new file mode 100644
index 0000000..0792bf8
--- /dev/null
+++ b/src/Multiplayer/Client.cs
@@ -0,0 +1,457 @@
+using HarmonyLib;
+using Il2CppMicrosoft.AspNetCore.SignalR.Client;
+using PolyMod.Multiplayer.ViewModels;
+using Polytopia.Data;
+using PolytopiaBackendBase;
+using PolytopiaBackendBase.Common;
+using PolytopiaBackendBase.Game;
+using PolytopiaBackendBase.Game.BindingModels;
+using UnityEngine;
+using Newtonsoft.Json;
+
+namespace PolyMod.Multiplayer;
+
+public static class Client
+{
+ internal const string DEFAULT_SERVER_URL = "https://dev.polydystopia.xyz";
+ internal const string LOCAL_SERVER_URL = "http://localhost:5051/";
+ private const string GldMarker = "##GLD:";
+ internal static bool allowGldMods = false;
+
+ // Cache parsed GLD by game Seed to handle rewinds/reloads
+ private static readonly Dictionary _gldCache = new();
+ private static readonly Dictionary _versionCache = new(); // Seed -> modGldVersion
+
+ internal static void Init()
+ {
+ Harmony.CreateAndPatchAll(typeof(Client));
+ BuildConfig buildConfig = BuildConfigHelper.GetSelectedBuildConfig();
+ buildConfig.buildServerURL = BuildServerURL.Custom;
+ buildConfig.customServerURL = LOCAL_SERVER_URL;
+
+ Plugin.logger.LogInfo($"Multiplayer> Server URL set to: {Plugin.config.backendUrl}");
+ Plugin.logger.LogInfo("Multiplayer> GLD patches applied");
+ }
+
+ [HarmonyPostfix]
+ [HarmonyPatch(typeof(MultiplayerScreen), nameof(MultiplayerScreen.Show))]
+ public static void MultiplayerScreen_Show(MultiplayerScreen __instance)
+ {
+ __instance.multiplayerSelectionScreen.TournamentsButton.gameObject.SetActive(false);
+ }
+
+ [HarmonyPostfix]
+ [HarmonyPatch(typeof(StartScreen), nameof(StartScreen.Start))]
+ private static void StartScreen_Start(StartScreen __instance)
+ {
+ __instance.highscoreButton.gameObject.SetActive(false);
+ __instance.weeklyChallengesButton.gameObject.SetActive(false);
+ }
+
+ [HarmonyPostfix]
+ [HarmonyPatch(typeof(SystemInfo), nameof(SystemInfo.deviceUniqueIdentifier), MethodType.Getter)]
+ public static void SteamClient_get_SteamId(ref string __result)
+ {
+ if (Plugin.config.overrideDeviceId != string.Empty)
+ {
+ __result = Plugin.config.overrideDeviceId;
+ }
+ }
+
+
+ // ///
+ // /// After GameState deserialization, check for trailing GLD version ID and set mockedGameLogicData.
+ // /// The server appends "##GLD:" + modGldVersion (int) after the normal serialized data.
+ // ///
+ // [HarmonyPostfix]
+ // [HarmonyPatch(typeof(GameState), nameof(GameState.Deserialize))]
+ // private static void Deserialize_Postfix(GameState __instance, BinaryReader __0)
+ // {
+ // if(!allowGldMods) return;
+
+ // Plugin.logger?.LogDebug("Deserialize_Postfix: Entered");
+
+ // try
+ // {
+ // var reader = __0;
+ // if (reader == null)
+ // {
+ // Plugin.logger?.LogWarning("Deserialize_Postfix: reader is null");
+ // return;
+ // }
+
+ // var position = reader.BaseStream.Position;
+ // var length = reader.BaseStream.Length;
+ // var remaining = length - position;
+
+ // Plugin.logger?.LogDebug($"Deserialize_Postfix: Stream position={position}, length={length}, remaining={remaining}");
+
+ // // Check if there's more data after normal deserialization
+ // if (position >= length)
+ // {
+ // Plugin.logger?.LogDebug("Deserialize_Postfix: No trailing data (position >= length)");
+
+ // var sd = __instance.Seed;
+ // if (_gldCache.TryGetValue(sd, out var cachedGld))
+ // {
+ // __instance.mockedGameLogicData = cachedGld;
+ // var cachedVersion = _versionCache.GetValueOrDefault(sd, -1);
+ // Plugin.logger?.LogInfo($"Deserialize_Postfix: Applied cached GLD for Seed={sd}, ModGldVersion={cachedVersion}");
+ // }
+ // return;
+ // }
+
+ // Plugin.logger?.LogDebug($"Deserialize_Postfix: Found {remaining} bytes of trailing data, attempting to read marker");
+
+ // var marker = reader.ReadString();
+ // Plugin.logger?.LogDebug($"Deserialize_Postfix: Read marker string: '{marker}'");
+
+ // if (marker != GldMarker)
+ // {
+ // Plugin.logger?.LogDebug($"Deserialize_Postfix: Marker mismatch - expected '{GldMarker}', got '{marker}'");
+ // return;
+ // }
+
+ // Plugin.logger?.LogInfo($"Deserialize_Postfix: Found GLD marker '{GldMarker}'");
+
+ // var modGldVersion = reader.ReadInt32();
+ // Plugin.logger?.LogInfo($"Deserialize_Postfix: Found embedded ModGldVersion: {modGldVersion}");
+
+ // Plugin.logger?.LogDebug($"Deserialize_Postfix: Fetching GLD from server for version {modGldVersion}");
+ // var gldJson = FetchGldById(modGldVersion);
+ // if (string.IsNullOrEmpty(gldJson))
+ // {
+ // Plugin.logger?.LogError($"Deserialize_Postfix: Failed to fetch GLD for ModGldVersion: {modGldVersion}");
+ // return;
+ // }
+
+ // Plugin.logger?.LogDebug($"Deserialize_Postfix: Parsing GLD JSON ({gldJson.Length} chars)");
+
+ // var customGld = new GameLogicData();
+ // customGld.Parse(gldJson);
+ // __instance.mockedGameLogicData = customGld;
+
+ // // Cache for subsequent deserializations (rewinds, reloads)
+ // var seed = __instance.Seed;
+ // _gldCache[seed] = customGld;
+ // _versionCache[seed] = modGldVersion;
+
+ // Plugin.logger?.LogInfo($"Deserialize_Postfix: Successfully set mockedGameLogicData from ModGldVersion: {modGldVersion}, cached for Seed={seed}");
+ // }
+ // catch (EndOfStreamException)
+ // {
+ // Plugin.logger?.LogDebug("Deserialize_Postfix: EndOfStreamException - no trailing data");
+ // }
+ // catch (Exception ex)
+ // {
+ // Plugin.logger?.LogError($"Deserialize_Postfix: Exception: {ex.GetType().Name}: {ex.Message}");
+ // Plugin.logger?.LogDebug($"Deserialize_Postfix: Stack trace: {ex.StackTrace}");
+ // }
+ // }
+
+ // ///
+ // /// Fetch GLD from server using ModGldVersion ID
+ // ///
+ // private static string? FetchGldById(int modGldVersion)
+ // {
+ // if(!allowGldMods) return null;
+ // try
+ // {
+ // using var client = new HttpClient();
+ // var url = $"{Plugin.config.backendUrl.TrimEnd('/')}/api/mods/gld/{modGldVersion}";
+ // Plugin.logger?.LogDebug($"FetchGldById: Requesting URL: {url}");
+
+ // var response = client.GetAsync(url).Result;
+ // Plugin.logger?.LogDebug($"FetchGldById: Response status: {response.StatusCode}");
+
+ // if (response.IsSuccessStatusCode)
+ // {
+ // var gld = response.Content.ReadAsStringAsync().Result;
+ // Plugin.logger?.LogInfo($"FetchGldById: Successfully fetched mod GLD ({gld.Length} chars)");
+ // return gld;
+ // }
+
+ // var errorContent = response.Content.ReadAsStringAsync().Result;
+ // Plugin.logger?.LogError($"FetchGldById: Failed with status {response.StatusCode}: {errorContent}");
+ // }
+ // catch (Exception ex)
+ // {
+ // Plugin.logger?.LogError($"FetchGldById: Exception: {ex.GetType().Name}: {ex.Message}");
+ // if (ex.InnerException != null)
+ // {
+ // Plugin.logger?.LogError($"FetchGldById: Inner exception: {ex.InnerException.Message}");
+ // }
+ // }
+ // return null;
+ // }
+
+ [HarmonyPrefix]
+ [HarmonyPatch(typeof(ClientBase), nameof(ClientBase.SendCommand))]
+ private static bool ClientBase_SendCommand(
+ ClientBase __instance,
+ CommandBase command)
+ {
+
+ Plugin.logger.LogInfo("Multiplayer> ClientBase_SendCommand");
+ Il2CppSystem.Threading.Tasks.Task> task = new();
+ var taskCompletionSource = new Il2CppSystem.Threading.Tasks.TaskCompletionSource>();
+
+ _ = HandleSendCommandModded(taskCompletionSource, __instance, command);
+
+ task = taskCompletionSource.Task;
+
+ return false;
+ }
+
+ private static async System.Threading.Tasks.Task HandleSendCommandModded(
+ Il2CppSystem.Threading.Tasks.TaskCompletionSource> tcs,
+ ClientBase client,
+ CommandBase command)
+ {
+ try
+ {
+ if (!client.CurrentGameId.HasValue)
+ {
+ Console.Write("Tried to perform and send command but no GameId was set");
+ return;
+ }
+ if (!ClientActionManager.CanReceiveCommand(command, client.GameState))
+ {
+ Console.Write("Tried to send invalid command");
+ return;
+ }
+ uint currentResetId = client.resets;
+ int count = client.GameState.CommandStack.Count;
+ var list = new Il2CppSystem.Collections.Generic.List();
+ list.Add(command);
+ client.ActionManager.ExecuteCommands(list);
+ await client.SendCommandToServer(command, count);
+
+ var serializedGameState = SerializationHelpers.ToByteArray(client.GameState, client.GameState.Version);
+
+ var succ = GameStateSummary.FromGameStateByteArray(serializedGameState,
+ out GameStateSummary stateSummary, out var gameState);
+
+ var serializedGameSummary = SerializationHelpers.ToByteArray(stateSummary, gameState.Version);
+
+ var setupGameDataViewModel = new SetupGameStateViewModel
+ {
+ gameId = client.gameId.ToString(),
+ serializedGameState = serializedGameState,
+ serializedGameSummary = serializedGameSummary,
+ gameSettingsJson = ""
+ };
+
+ var setupData = System.Text.Json.JsonSerializer.Serialize(setupGameDataViewModel);
+
+ var serverResponse = await PolytopiaBackendAdapter.Instance.HubConnection.InvokeAsync>(
+ "UpdateGameStateModded",
+ setupData,
+ Il2CppSystem.Threading.CancellationToken.None
+ );
+ tcs.SetResult(serverResponse);
+ }
+ catch (Exception ex)
+ {
+ Plugin.logger.LogError("Multiplayer> Error during HandleSendCommandModded: " + ex.Message);
+ tcs.SetException(new Il2CppSystem.Exception(ex.Message));
+ }
+ }
+
+ [HarmonyPrefix]
+ [HarmonyPatch(typeof(BackendAdapter), nameof(BackendAdapter.StartLobbyGame))]
+ private static bool BackendAdapter_StartLobbyGame_Modded(
+ ref Il2CppSystem.Threading.Tasks.Task> __result,
+ BackendAdapter __instance,
+ StartLobbyBindingModel model)
+ {
+ Plugin.logger.LogInfo("Multiplayer> BackendAdapter_StartLobbyGame_Modded");
+ var taskCompletionSource = new Il2CppSystem.Threading.Tasks.TaskCompletionSource>();
+
+ _ = HandleStartLobbyGameModded(taskCompletionSource, __instance, model);
+
+ __result = taskCompletionSource.Task;
+
+ return false;
+ }
+
+ private static async System.Threading.Tasks.Task HandleStartLobbyGameModded(
+ Il2CppSystem.Threading.Tasks.TaskCompletionSource> tcs,
+ BackendAdapter instance,
+ StartLobbyBindingModel model)
+ {
+ try
+ {
+ var lobbyResponse = await PolytopiaBackendAdapter.Instance.GetLobby(new GetLobbyBindingModel
+ {
+ LobbyId = model.LobbyId
+ });
+
+ Plugin.logger.LogInfo($"Multiplayer> Lobby processed {lobbyResponse.Success}");
+ LobbyGameViewModel lobbyGameViewModel = lobbyResponse.Data;
+ Plugin.logger.LogInfo("Multiplayer> Lobby received");
+
+ (byte[] serializedGameState, string gameSettingsJson) = CreateMultiplayerGame(
+ lobbyGameViewModel,
+ VersionManager.GameVersion,
+ VersionManager.GameLogicDataVersion
+ );
+
+ Plugin.logger.LogInfo("Multiplayer> GameState and Settiings created");
+
+ var succ = GameStateSummary.FromGameStateByteArray(serializedGameState,
+ out GameStateSummary stateSummary, out var gameState);
+
+ var serializedGameSummary = SerializationHelpers.ToByteArray(stateSummary, gameState.Version);
+ var setupGameDataViewModel = new SetupGameStateViewModel
+ {
+ lobbyId = lobbyGameViewModel.Id.ToString(),
+ serializedGameState = serializedGameState,
+ serializedGameSummary = serializedGameSummary,
+ gameSettingsJson = gameSettingsJson
+ };
+
+ var setupData = System.Text.Json.JsonSerializer.Serialize(setupGameDataViewModel);
+
+ var serverResponse = await instance.HubConnection.InvokeAsync>(
+ "StartLobbyGameModded",
+ setupData,
+ Il2CppSystem.Threading.CancellationToken.None
+ );
+ Plugin.logger.LogInfo("Multiplayer> Invoked StartLobbyGameModded");
+ tcs.SetResult(serverResponse);
+ }
+ catch (Exception ex)
+ {
+ Plugin.logger.LogError("Multiplayer> Error during HandleStartLobbyGameModded: " + ex.Message);
+ tcs.SetException(new Il2CppSystem.Exception(ex.Message));
+ }
+ }
+
+ public static (byte[] serializedGameState, string gameSettingsJson) CreateMultiplayerGame(LobbyGameViewModel lobby,
+ int gameVersion, int gameLogicVersion)
+ {
+ var lobbyMapSize = lobby.MapSize;
+ var settings = new GameSettings();
+ settings.ApplyLobbySettings(lobby);
+ if (settings.LiveGamePreset)
+ {
+ settings.SetLiveModePreset();
+ }
+ foreach (var participatorViewModel in lobby.Participators)
+ {
+ var humanPlayer = new PlayerData
+ {
+ type = PlayerDataType.LocalUser,
+ state = PlayerDataFriendshipState.Accepted,
+ knownTribe = true,
+ tribe = (TribeType)participatorViewModel.SelectedTribe,
+ tribeMix = (TribeType)participatorViewModel.SelectedTribe,
+ skinType = (SkinType)participatorViewModel.SelectedTribeSkin,
+ defaultName = participatorViewModel.GetNameInternal()
+ };
+ humanPlayer.profile.id = participatorViewModel.UserId;
+ humanPlayer.profile.SetName(participatorViewModel.GetNameInternal());
+ SerializationHelpers.FromByteArray(participatorViewModel.AvatarStateData, out var avatarState);
+ humanPlayer.profile.avatarState = avatarState;
+
+ settings.AddPlayer(humanPlayer);
+ }
+
+ foreach (var botDifficulty in lobby.Bots)
+ {
+ var botGuid = Il2CppSystem.Guid.NewGuid();
+
+ var botPlayer = new PlayerData
+ {
+ type = PlayerDataType.Bot,
+ state = PlayerDataFriendshipState.Accepted,
+ knownTribe = true,
+ tribe = Enum.GetValues().Where(t => t != TribeType.None)
+ .OrderBy(x => Il2CppSystem.Guid.NewGuid()).First()
+ };
+ ;
+ botPlayer.botDifficulty = (BotDifficulty)botDifficulty;
+ botPlayer.skinType = SkinType.Default;
+ botPlayer.defaultName = "Bot" + botGuid;
+ botPlayer.profile.id = botGuid;
+
+ settings.AddPlayer(botPlayer);
+ }
+
+ GameState gameState = new GameState()
+ {
+ Version = gameVersion,
+ Settings = settings,
+ PlayerStates = new Il2CppSystem.Collections.Generic.List()
+ };
+
+ for (int index = 0; index < settings.GetPlayerCount(); ++index)
+ {
+ PlayerData player = settings.GetPlayer(index);
+ if (player.type != PlayerDataType.Bot)
+ {
+ var nullableGuid = new Il2CppSystem.Nullable(player.profile.id);
+ if (!nullableGuid.HasValue)
+ {
+ throw new Exception("GUID was not set properly!");
+ }
+ PlayerState playerState = new PlayerState()
+ {
+ Id = (byte)(index + 1),
+ AccountId = nullableGuid,
+ AutoPlay = player.type == PlayerDataType.Bot,
+ UserName = player.GetNameInternal(),
+ tribe = player.tribe,
+ tribeMix = player.tribeMix,
+ hasChosenTribe = true,
+ skinType = player.skinType
+ };
+ gameState.PlayerStates.Add(playerState);
+ Plugin.logger.LogInfo($"Multiplayer> Created player: {playerState}");
+ }
+ else
+ {
+ GameStateUtils.AddAIOpponent(gameState, GameStateUtils.GetRandomPickableTribe(gameState),
+ GameSettings.HandicapFromDifficulty(player.botDifficulty), player.skinType);
+ }
+ }
+
+ GameStateUtils.SetPlayerColors(gameState);
+ GameStateUtils.AddNaturePlayer(gameState);
+
+ Plugin.logger.LogInfo("Multiplayer> Creating world...");
+
+ ushort num = (ushort)Math.Max(lobbyMapSize,
+ (int)MapDataExtensions.GetMinimumMapSize(gameState.PlayerCount));
+ gameState.Map = new MapData(num, num);
+ MapGeneratorSettings generatorSettings = settings.GetMapGeneratorSettings();
+ new MapGenerator().Generate(gameState, generatorSettings);
+
+ Plugin.logger.LogInfo($"Multiplayer> Creating initial state for {gameState.PlayerCount} players...");
+
+ foreach (PlayerState player in gameState.PlayerStates)
+ {
+ foreach (PlayerState otherPlayer in gameState.PlayerStates)
+ player.aggressions[otherPlayer.Id] = 0;
+
+ if (player.Id != byte.MaxValue && gameState.GameLogicData.TryGetData(player.tribe, out TribeData tribeData))
+ {
+ player.Currency = tribeData.startingStars;
+ TileData tile = gameState.Map.GetTile(player.startTile);
+ UnitState unitState = ActionUtils.TrainUnitScored(gameState, player, tile, tribeData.startingUnit);
+ unitState.attacked = false;
+ unitState.moved = false;
+ }
+ }
+
+ Plugin.logger.LogInfo("Multiplayer> Session created successfully");
+
+ gameState.CommandStack.Add((CommandBase)new StartMatchCommand((byte)1));
+
+ var serializedGameState = SerializationHelpers.ToByteArray(gameState, gameState.Version);
+
+ return (serializedGameState,
+ JsonConvert.SerializeObject(gameState.Settings));
+ }
+}
diff --git a/src/Multiplayer/SerializationUtils.cs b/src/Multiplayer/SerializationUtils.cs
new file mode 100644
index 0000000..ea32c6b
--- /dev/null
+++ b/src/Multiplayer/SerializationUtils.cs
@@ -0,0 +1,319 @@
+using HarmonyLib;
+using Polytopia.Data;
+using PolytopiaBackendBase.Common;
+
+namespace PolyMod.Multiplayer;
+
+public static class SerializationUtils
+{
+ internal static void Init()
+ {
+ Harmony.CreateAndPatchAll(typeof(SerializationUtils));
+ }
+
+ // [HarmonyPrefix]
+ // [HarmonyPatch(typeof(GamePlayerSummary), nameof(GamePlayerSummary.Serialize))]
+ // public static bool GamePlayerSummary_Serialize(GamePlayerSummary __instance, Il2CppSystem.IO.BinaryWriter writer, int version)
+ // {
+ // Plugin.logger.LogInfo("Multiplayer> GamePlayerSummary_Serialize");
+ // var memoryStream = new Il2CppSystem.IO.MemoryStream();
+ // var binaryWriter = new Il2CppSystem.IO.BinaryWriter(memoryStream);
+ // binaryWriter.Write(__instance.Id);
+ // binaryWriter.Write(__instance.PolytopiaId.ToString());
+ // binaryWriter.Write(__instance.UserName ?? "");
+ // binaryWriter.Write((int)__instance.TribeType);
+ // binaryWriter.Write(__instance.AutoPlay);
+ // binaryWriter.Write(__instance.HasChosenTribe);
+ // binaryWriter.Write(__instance.Handicap);
+ // binaryWriter.Write(__instance.IsDead);
+ // if (version >= 86)
+ // {
+ // binaryWriter.Write((int)__instance.SkinType);
+ // }
+ // writer.Write((int)memoryStream.Length);
+ // memoryStream.WriteTo(writer.BaseStream);
+ // return false;
+ // }
+
+ // [HarmonyPrefix]
+ // [HarmonyPatch(typeof(GamePlayerSummary), nameof(GamePlayerSummary.Deserialize))]
+ // public static bool GamePlayerSummary_Deserialize(GamePlayerSummary __instance, Il2CppSystem.IO.BinaryReader reader, int version)
+ // {
+ // Plugin.logger.LogInfo("Multiplayer> GamePlayerSummary_Deserialize");
+ // int num = reader.ReadInt32();
+ // long position = reader.BaseStream.Position;
+ // __instance.Id = reader.ReadByte();
+ // string g = reader.ReadString();
+ // Il2CppSystem.Guid parsed;
+ // Il2CppSystem.Nullable nullableGuid;
+ // if (Il2CppSystem.Guid.TryParse(g, out parsed))
+ // nullableGuid = new Il2CppSystem.Nullable(parsed);
+ // else
+ // nullableGuid = new Il2CppSystem.Nullable();
+ // __instance.PolytopiaId = nullableGuid;
+ // __instance.UserName = reader.ReadString();
+ // __instance.TribeType = (TribeType)reader.ReadInt32();
+ // __instance.AutoPlay = reader.ReadBoolean();
+ // __instance.HasChosenTribe = reader.ReadBoolean();
+ // __instance.Handicap = reader.ReadInt32();
+ // __instance.IsDead = reader.ReadBoolean();
+ // if (version >= 86)
+ // {
+ // __instance.SkinType = (SkinType)reader.ReadInt32();
+ // }
+ // reader.BaseStream.Position = position + num;
+ // return false;
+ // }
+
+ // [HarmonyPrefix]
+ // [HarmonyPatch(typeof(PlayerState), nameof(PlayerState.Serialize))]
+ // public static bool PlayerState_Serialize(PlayerState __instance, Il2CppSystem.IO.BinaryWriter writer, int version)
+ // {
+ // writer.Write(__instance.Id);
+ // if (__instance.UserName == null)
+ // {
+ // __instance.UserName = "";
+ // }
+ // writer.Write(__instance.UserName);
+ // writer.Write(__instance.AccountId.ToString());
+ // writer.Write(__instance.AutoPlay);
+ // __instance.startTile.Serialize(writer, version);
+ // writer.Write((ushort)__instance.tribe);
+ // writer.Write(__instance.hasChosenTribe);
+ // writer.Write(__instance.handicap);
+ // if (version < 113)
+ // {
+ // writer.Write((ushort)((__instance.aggressions != null) ? ((uint)__instance.aggressions.Count) : 0u));
+ // foreach (Il2CppSystem.Collections.Generic.KeyValuePair aggression in __instance.aggressions)
+ // {
+ // writer.Write(aggression.Key);
+ // writer.Write(aggression.Value);
+ // }
+ // }
+ // writer.Write(__instance.currency);
+ // writer.Write(__instance.score);
+ // writer.Write(__instance.endScore);
+ // writer.Write((ushort)__instance.cities);
+ // writer.Write((ushort)((__instance.availableTech != null) ? ((uint)__instance.availableTech.Count) : 0u));
+ // if (__instance.availableTech != null)
+ // {
+ // for (int i = 0; i < __instance.availableTech.Count; i++)
+ // {
+ // writer.Write((ushort)__instance.availableTech[i]);
+ // }
+ // }
+ // writer.Write((ushort)((__instance.knownPlayers != null) ? ((uint)__instance.knownPlayers.Count) : 0u));
+ // if (__instance.knownPlayers != null)
+ // {
+ // for (int j = 0; j < __instance.knownPlayers.Count; j++)
+ // {
+ // writer.Write(__instance.knownPlayers[j]);
+ // }
+ // }
+ // ushort num = (ushort)((__instance.tasks != null) ? ((uint)__instance.tasks.Count) : 0u);
+ // writer.Write(num);
+ // for (int k = 0; k < num; k++)
+ // {
+ // TaskBase.SerializeTask(__instance.tasks[k], writer, version);
+ // }
+ // writer.Write(__instance.kills);
+ // writer.Write(__instance.casualities);
+ // writer.Write(__instance.wipeOuts);
+ // writer.Write(__instance.color);
+ // writer.Write((byte)__instance.tribeMix);
+ // writer.Write((ushort)((__instance.builtUniqueImprovements != null) ? ((uint)__instance.builtUniqueImprovements.Count) : 0u));
+ // if (__instance.builtUniqueImprovements != null)
+ // {
+ // for (int l = 0; l < __instance.builtUniqueImprovements.Count; l++)
+ // {
+ // writer.Write((short)__instance.builtUniqueImprovements[l]);
+ // }
+ // }
+ // if (version < 60)
+ // {
+ // return false;
+ // }
+ // writer.Write((ushort)__instance.relations.Count);
+ // foreach (Il2CppSystem.Collections.Generic.KeyValuePair relation in __instance.relations)
+ // {
+ // writer.Write(relation.Key);
+ // relation.Value.Serialize(writer, version);
+ // }
+ // writer.Write((ushort)__instance.messages.Count);
+ // foreach (DiplomacyMessage message in __instance.messages)
+ // {
+ // message.Serialize(writer, version);
+ // }
+ // writer.Write(__instance.killerId);
+ // writer.Write(__instance.killedTurn);
+ // if (version < 70)
+ // {
+ // return false;
+ // }
+ // writer.Write(__instance.resignedAtCommandIndex);
+ // writer.Write(__instance.wipedAtCommandIndex);
+ // if (version < 86)
+ // {
+ // return false;
+ // }
+ // writer.Write((ushort)__instance.skinType);
+ // if (version >= 93)
+ // {
+ // writer.Write(__instance.resignedTurn);
+ // if (version >= 121)
+ // {
+ // writer.Write((int)__instance.climate);
+ // }
+ // }
+ // return false;
+ // }
+
+ // [HarmonyPrefix]
+ // [HarmonyPatch(typeof(PlayerState), nameof(PlayerState.Deserialize))]
+ // public static bool Deserialize(PlayerState __instance, Il2CppSystem.IO.BinaryReader reader, int version)
+ // {
+ // __instance.Id = reader.ReadByte();
+ // __instance.UserName = reader.ReadString();
+ // string g = reader.ReadString();
+ // Il2CppSystem.Guid parsed;
+ // Il2CppSystem.Nullable nullableGuid;
+ // if (Il2CppSystem.Guid.TryParse(g, out parsed))
+ // nullableGuid = new Il2CppSystem.Nullable(parsed);
+ // else
+ // nullableGuid = new Il2CppSystem.Nullable();
+ // __instance.AccountId = nullableGuid;
+ // __instance.AutoPlay = reader.ReadBoolean();
+ // __instance.startTile = new WorldCoordinates(reader, version);
+ // __instance.tribe = (TribeType)reader.ReadUInt16();
+ // __instance.climate = __instance.tribe;
+ // __instance.hasChosenTribe = reader.ReadBoolean();
+ // __instance.handicap = reader.ReadInt32();
+ // if (version < 113)
+ // {
+ // int num = reader.ReadUInt16();
+ // for (int i = 0; i < num; i++)
+ // {
+ // byte key = reader.ReadByte();
+ // __instance.aggressions[key] = reader.ReadInt32();
+ // }
+ // }
+ // __instance.currency = reader.ReadInt32();
+ // __instance.score = reader.ReadUInt32();
+ // __instance.endScore = reader.ReadUInt32();
+ // __instance.cities = reader.ReadUInt16();
+ // int num2 = reader.ReadUInt16();
+ // if (__instance.availableTech == null || __instance.availableTech.Count < num2)
+ // {
+ // __instance.availableTech = new Il2CppSystem.Collections.Generic.List(num2);
+ // }
+ // for (int j = 0; j < num2; j++)
+ // {
+ // if (j < __instance.availableTech.Count)
+ // {
+ // __instance.availableTech[j] = (TechData.Type)reader.ReadUInt16();
+ // }
+ // else
+ // {
+ // __instance.availableTech.Add((TechData.Type)reader.ReadUInt16());
+ // }
+ // }
+ // int num3 = reader.ReadUInt16();
+ // if (__instance.knownPlayers == null || __instance.knownPlayers.Count < num3)
+ // {
+ // __instance.knownPlayers = new Il2CppSystem.Collections.Generic.List();
+ // }
+ // for (int k = 0; k < num3; k++)
+ // {
+ // if (k < __instance.knownPlayers.Count)
+ // {
+ // __instance.knownPlayers[k] = reader.ReadByte();
+ // }
+ // else
+ // {
+ // __instance.knownPlayers.Add(reader.ReadByte());
+ // }
+ // }
+ // ushort num4 = reader.ReadUInt16();
+ // if (__instance.tasks == null)
+ // {
+ // __instance.tasks = new Il2CppSystem.Collections.Generic.List(num4);
+ // }
+ // else
+ // {
+ // __instance.tasks.Clear();
+ // }
+ // for (int l = 0; l < num4; l++)
+ // {
+ // __instance.tasks.Add(TaskBase.DeserializeTask(reader, version));
+ // }
+ // __instance.kills = reader.ReadUInt32();
+ // __instance.casualities = reader.ReadUInt32();
+ // __instance.wipeOuts = reader.ReadUInt32();
+ // __instance.color = reader.ReadInt32();
+ // __instance.tribeMix = (TribeType)reader.ReadByte();
+ // ushort num5 = reader.ReadUInt16();
+ // if (__instance.builtUniqueImprovements == null)
+ // {
+ // __instance.builtUniqueImprovements = new Il2CppSystem.Collections.Generic.List(num5);
+ // }
+ // else
+ // {
+ // __instance.builtUniqueImprovements.Clear();
+ // }
+ // for (int m = 0; m < num5; m++)
+ // {
+ // __instance.builtUniqueImprovements.Add((ImprovementData.Type)reader.ReadInt16());
+ // }
+ // if (__instance.color == -1 && version < 86)
+ // {
+ // __instance.color = __instance.GetPlayerColor(version, __instance.tribe);
+ // }
+ // if (version < 60)
+ // {
+ // return false;
+ // }
+ // __instance.relations.Clear();
+ // ushort num6 = reader.ReadUInt16();
+ // for (int n = 0; n < num6; n++)
+ // {
+ // byte key2 = reader.ReadByte();
+ // DiplomacyRelation diplomacyRelation = new DiplomacyRelation();
+ // diplomacyRelation.Deserialize(reader, version);
+ // __instance.relations[key2] = diplomacyRelation;
+ // }
+ // __instance.messages.Clear();
+ // ushort num7 = reader.ReadUInt16();
+ // for (int num8 = 0; num8 < num7; num8++)
+ // {
+ // DiplomacyMessage diplomacyMessage = new DiplomacyMessage();
+ // diplomacyMessage.Deserialize(reader, version);
+ // __instance.messages.Add(diplomacyMessage);
+ // }
+ // __instance.killerId = reader.ReadByte();
+ // __instance.killedTurn = reader.ReadUInt32();
+ // if (version < 70)
+ // {
+ // return false;
+ // }
+ // __instance.resignedAtCommandIndex = reader.ReadInt32();
+ // __instance.wipedAtCommandIndex = reader.ReadInt32();
+ // if (version < 86)
+ // {
+ // return false;
+ // }
+ // __instance.skinType = (SkinType)reader.ReadUInt16();
+ // if (__instance.color == -1)
+ // {
+ // __instance.color = __instance.GetPlayerColor(version, __instance.tribe, __instance.skinType);
+ // }
+ // if (version >= 93)
+ // {
+ // __instance.resignedTurn = reader.ReadInt32();
+ // if (version >= 121)
+ // {
+ // __instance.climate = (TribeType)reader.ReadInt32();
+ // }
+ // }
+ // return false;
+ // }
+}
\ No newline at end of file
diff --git a/src/Multiplayer/ViewModels/IMonoServerResponseData.cs b/src/Multiplayer/ViewModels/IMonoServerResponseData.cs
new file mode 100644
index 0000000..3b0a835
--- /dev/null
+++ b/src/Multiplayer/ViewModels/IMonoServerResponseData.cs
@@ -0,0 +1,5 @@
+namespace PolyMod.Multiplayer.ViewModels;
+
+public interface IMonoServerResponseData
+{
+}
\ No newline at end of file
diff --git a/src/Multiplayer/ViewModels/SetupGameStateViewModel.cs b/src/Multiplayer/ViewModels/SetupGameStateViewModel.cs
new file mode 100644
index 0000000..63467a9
--- /dev/null
+++ b/src/Multiplayer/ViewModels/SetupGameStateViewModel.cs
@@ -0,0 +1,14 @@
+
+using Tesla;
+
+namespace PolyMod.Multiplayer.ViewModels;
+public class SetupGameStateViewModel : IMonoServerResponseData
+{
+ public string gameId { get; set; } = "";
+ public string lobbyId { get; set; } = "";
+
+ public byte[] serializedGameState { get; set; } = new byte[0];
+ public byte[] serializedGameSummary { get; set; } = new byte[0];
+
+ public string gameSettingsJson { get; set; } = "";
+}
\ No newline at end of file
diff --git a/src/Plugin.cs b/src/Plugin.cs
index 96ae942..597609c 100644
--- a/src/Plugin.cs
+++ b/src/Plugin.cs
@@ -24,7 +24,9 @@ internal record PolyConfig(
bool debug = false,
bool autoUpdate = true,
bool updatePrerelease = false,
- bool allowUnsafeIndexes = false
+ bool allowUnsafeIndexes = false,
+ string backendUrl = Multiplayer.Client.DEFAULT_SERVER_URL,
+ string overrideDeviceId = ""
);
///
@@ -132,6 +134,8 @@ public override void Load()
Hub.Init();
Main.Init();
+ Multiplayer.SerializationUtils.Init();
+ Multiplayer.Client.Init();
}
///