From 3967b7c5b1e5e5bcd56d54b2bafc055cad7939aa Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 9 Jun 2025 13:36:47 +0200 Subject: [PATCH 1/8] Minor stuff --- PolyMod.csproj | 2 +- src/Managers/Multiplayer.cs | 40 +++++++++++++++++++++++++++++++++++++ src/Plugin.cs | 4 +++- 3 files changed, 44 insertions(+), 2 deletions(-) create mode 100644 src/Managers/Multiplayer.cs diff --git a/PolyMod.csproj b/PolyMod.csproj index f138f69..ded9a2e 100644 --- a/PolyMod.csproj +++ b/PolyMod.csproj @@ -10,7 +10,7 @@ https://polymod.dev/nuget/v3/index.json; IL2CPP - 1.1.8 + 1.3.0-pre 2.13.0.14218 PolyModdingTeam The Battle of Polytopia's mod loader. diff --git a/src/Managers/Multiplayer.cs b/src/Managers/Multiplayer.cs new file mode 100644 index 0000000..26da745 --- /dev/null +++ b/src/Managers/Multiplayer.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using HarmonyLib; + +namespace PolyMod.Managers +{ + public static class Multiplayer + { + internal static void Init() + { + Harmony.CreateAndPatchAll(typeof(Multiplayer)); + BuildConfigHelper.GetSelectedBuildConfig().buildServerURL = BuildServerURL.Custom; + BuildConfigHelper.GetSelectedBuildConfig().customServerURL = Plugin.config.backendUrl; + } + + [HarmonyPostfix] + [HarmonyPatch(typeof(MultiplayerScreen), nameof(MultiplayerScreen.Show))] + public static void MultiplayerScreen_Show(MultiplayerScreen __instance) + { + __instance.multiplayerSelectionScreen.TournamentsButton.gameObject.SetActive(false); + } + + + [HarmonyPostfix] + [HarmonyPatch(typeof(ProfileScreen), nameof(ProfileScreen.Start))] + public static void ProfileScreen_Start(ProfileScreen __instance) + { + } + + [HarmonyPostfix] + [HarmonyPatch(typeof(StartScreen), nameof(StartScreen.Start))] + private static void StartScreen_Start(StartScreen __instance) + { + __instance.highscoreButton.gameObject.SetActive(false); + __instance.weeklyChallengesButton.gameObject.SetActive(false); + } + } +} \ No newline at end of file diff --git a/src/Plugin.cs b/src/Plugin.cs index 86a49ea..8595134 100644 --- a/src/Plugin.cs +++ b/src/Plugin.cs @@ -10,7 +10,8 @@ namespace PolyMod; public partial class Plugin : BepInEx.Unity.IL2CPP.BasePlugin { internal record PolyConfig( - bool debug = false + bool debug = false, + string backendUrl = "http://127.0.0.1:8090" ); internal const int AUTOIDX_STARTS_FROM = 1000; @@ -59,6 +60,7 @@ public override void Load() Hub.Init(); Main.Init(); + Multiplayer.Init(); } internal static Stream GetResource(string id) From e77df1f2b6c86936560a817d6be82965842280e5 Mon Sep 17 00:00:00 2001 From: Maksym Muraviov$ Date: Sun, 15 Mar 2026 18:04:00 +0100 Subject: [PATCH 2/8] Added dystopia stuff --- src/Managers/Multiplayer.cs | 179 +++++++++++++++++++++++++++++++----- src/Plugin.cs | 2 +- 2 files changed, 158 insertions(+), 23 deletions(-) diff --git a/src/Managers/Multiplayer.cs b/src/Managers/Multiplayer.cs index 26da745..4cf586d 100644 --- a/src/Managers/Multiplayer.cs +++ b/src/Managers/Multiplayer.cs @@ -2,39 +2,174 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using BepInEx.Logging; using HarmonyLib; +using Polytopia.Data; -namespace PolyMod.Managers +namespace PolyMod.Managers; + +public static class Multiplayer { - public static class Multiplayer + internal const string DEFAULT_SERVER_URL = "https://dev.polydystopia.xyz"; + private const string GldMarker = "##GLD:"; + + // 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() { - internal static void Init() - { - Harmony.CreateAndPatchAll(typeof(Multiplayer)); - BuildConfigHelper.GetSelectedBuildConfig().buildServerURL = BuildServerURL.Custom; - BuildConfigHelper.GetSelectedBuildConfig().customServerURL = Plugin.config.backendUrl; - } + Harmony.CreateAndPatchAll(typeof(Multiplayer)); + BuildConfig buildConfig = BuildConfigHelper.GetSelectedBuildConfig(); + buildConfig.buildServerURL = BuildServerURL.Custom; + buildConfig.customServerURL = Plugin.config.backendUrl; + + Plugin.logger.LogInfo($"Polydystopia> Server URL set to: {Plugin.config.backendUrl}"); + Plugin.logger.LogInfo("Polydystopia> 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(MultiplayerScreen), nameof(MultiplayerScreen.Show))] - public static void MultiplayerScreen_Show(MultiplayerScreen __instance) + [HarmonyPostfix] + [HarmonyPatch(typeof(ProfileScreen), nameof(ProfileScreen.Start))] + public static void ProfileScreen_Start(ProfileScreen __instance) + { + } + + [HarmonyPostfix] + [HarmonyPatch(typeof(StartScreen), nameof(StartScreen.Start))] + private static void StartScreen_Start(StartScreen __instance) + { + __instance.highscoreButton.gameObject.SetActive(false); + __instance.weeklyChallengesButton.gameObject.SetActive(false); + } + + /// + /// 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) + { + Plugin.logger?.LogDebug("Deserialize_Postfix: Entered"); + + try { - __instance.multiplayerSelectionScreen.TournamentsButton.gameObject.SetActive(false); - } + 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}"); - [HarmonyPostfix] - [HarmonyPatch(typeof(ProfileScreen), nameof(ProfileScreen.Start))] - public static void ProfileScreen_Start(ProfileScreen __instance) + // 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) + { + 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}"); - [HarmonyPostfix] - [HarmonyPatch(typeof(StartScreen), nameof(StartScreen.Start))] - private static void StartScreen_Start(StartScreen __instance) + 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) { - __instance.highscoreButton.gameObject.SetActive(false); - __instance.weeklyChallengesButton.gameObject.SetActive(false); + 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; } -} \ No newline at end of file +} diff --git a/src/Plugin.cs b/src/Plugin.cs index 7986ed1..e88ab34 100644 --- a/src/Plugin.cs +++ b/src/Plugin.cs @@ -22,7 +22,7 @@ public partial class Plugin : BepInEx.Unity.IL2CPP.BasePlugin /// Whether to include pre-release versions when updating. internal record PolyConfig( bool debug = false, - string backendUrl = "http://127.0.0.1:8090", + string backendUrl = Multiplayer.DEFAULT_SERVER_URL, bool autoUpdate = true, bool updatePrerelease = false, bool allowUnsafeIndexes = false From 21b16a1f22e97d32680d17f8182cc59b778a786c Mon Sep 17 00:00:00 2001 From: Maksym Muraviov$ Date: Sun, 15 Mar 2026 18:33:06 +0100 Subject: [PATCH 3/8] Added bool which lets u disable gld mods --- src/Managers/Multiplayer.cs | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/src/Managers/Multiplayer.cs b/src/Managers/Multiplayer.cs index 4cf586d..e9e76bb 100644 --- a/src/Managers/Multiplayer.cs +++ b/src/Managers/Multiplayer.cs @@ -1,8 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using BepInEx.Logging; using HarmonyLib; using Polytopia.Data; @@ -12,6 +7,7 @@ public static class Multiplayer { internal const string DEFAULT_SERVER_URL = "https://dev.polydystopia.xyz"; 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(); @@ -35,13 +31,6 @@ public static void MultiplayerScreen_Show(MultiplayerScreen __instance) __instance.multiplayerSelectionScreen.TournamentsButton.gameObject.SetActive(false); } - - [HarmonyPostfix] - [HarmonyPatch(typeof(ProfileScreen), nameof(ProfileScreen.Start))] - public static void ProfileScreen_Start(ProfileScreen __instance) - { - } - [HarmonyPostfix] [HarmonyPatch(typeof(StartScreen), nameof(StartScreen.Start))] private static void StartScreen_Start(StartScreen __instance) @@ -58,6 +47,8 @@ private static void StartScreen_Start(StartScreen __instance) [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 @@ -143,6 +134,7 @@ private static void Deserialize_Postfix(GameState __instance, BinaryReader __0) /// private static string? FetchGldById(int modGldVersion) { + if(!allowGldMods) return null; try { using var client = new HttpClient(); From fc1d5494c20d1826aace2dcd3ced6aeb973c4458 Mon Sep 17 00:00:00 2001 From: Maksym Muraviov$ Date: Tue, 24 Mar 2026 17:47:03 +0100 Subject: [PATCH 4/8] Started implementing client side creation of GameState --- .gitignore | 4 +- src/Managers/Multiplayer.cs | 218 +++++++++++++++++++++- src/Plugin.cs | 5 +- src/ViewModels/IMonoServerResponseData.cs | 5 + src/ViewModels/SetupGameDataViewModel.cs | 10 + 5 files changed, 236 insertions(+), 6 deletions(-) create mode 100644 src/ViewModels/IMonoServerResponseData.cs create mode 100644 src/ViewModels/SetupGameDataViewModel.cs diff --git a/.gitignore b/.gitignore index 6c789ad..a42fb76 100644 --- a/.gitignore +++ b/.gitignore @@ -404,4 +404,6 @@ build/ dist/ *.spec -# End of https://www.toptal.com/developers/gitignore/api/csharp \ No newline at end of file +# End of https://www.toptal.com/developers/gitignore/api/csharp + +run.bat \ No newline at end of file diff --git a/src/Managers/Multiplayer.cs b/src/Managers/Multiplayer.cs index e9e76bb..bcb5088 100644 --- a/src/Managers/Multiplayer.cs +++ b/src/Managers/Multiplayer.cs @@ -1,11 +1,20 @@ using HarmonyLib; +using Il2CppMicrosoft.AspNetCore.SignalR.Client; +using PolyMod.ViewModels; using Polytopia.Data; +using PolytopiaBackendBase; +using PolytopiaBackendBase.Common; +using PolytopiaBackendBase.Game; +using PolytopiaBackendBase.Game.BindingModels; +using UnityEngine; +using Newtonsoft.Json; namespace PolyMod.Managers; public static class Multiplayer { 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; @@ -18,10 +27,10 @@ internal static void Init() Harmony.CreateAndPatchAll(typeof(Multiplayer)); BuildConfig buildConfig = BuildConfigHelper.GetSelectedBuildConfig(); buildConfig.buildServerURL = BuildServerURL.Custom; - buildConfig.customServerURL = Plugin.config.backendUrl; + buildConfig.customServerURL = LOCAL_SERVER_URL; - Plugin.logger.LogInfo($"Polydystopia> Server URL set to: {Plugin.config.backendUrl}"); - Plugin.logger.LogInfo("Polydystopia> GLD patches applied"); + Plugin.logger.LogInfo($"Server URL set to: {Plugin.config.backendUrl}"); + Plugin.logger.LogInfo("GLD patches applied"); } [HarmonyPostfix] @@ -39,6 +48,17 @@ private static void StartScreen_Start(StartScreen __instance) __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. @@ -164,4 +184,196 @@ private static void Deserialize_Postfix(GameState __instance, BinaryReader __0) } return null; } + + [HarmonyPrefix] + [HarmonyPatch(typeof(BackendAdapter), nameof(BackendAdapter.StartLobbyGame))] + private static bool BackendAdapter_StartLobbyGame( + ref Il2CppSystem.Threading.Tasks.Task> __result, + BackendAdapter __instance, + StartLobbyBindingModel model) + { + Plugin.logger.LogInfo("BackendAdapter_StartLobbyGame"); + _ = HandleStartLobbyGameAsync(__instance, model); + return true; + } + + private static async Task HandleStartLobbyGameAsync(BackendAdapter instance, StartLobbyBindingModel model) + { + try + { + var lobbyResponse = await PolytopiaBackendAdapter.Instance.GetLobby(new GetLobbyBindingModel + { + LobbyId = model.LobbyId + }); + Plugin.logger.LogInfo($"Lobby processed {lobbyResponse.Success}"); + LobbyGameViewModel lobbyGameViewModel = lobbyResponse.Data; + Plugin.logger.LogInfo("Lobby received"); + + (byte[] serializedGameState, string gameSettingsJson) = CreateMultiplayerGame( + lobbyGameViewModel, + VersionManager.GameVersion, + VersionManager.GameLogicDataVersion + ); + + Plugin.logger.LogInfo("Game data created"); + + var setupGameDataViewModel = new SetupGameDataViewModel + { + lobbyId = lobbyGameViewModel.Id.ToString(), + serializedGameState = serializedGameState, + gameSettingsJson = gameSettingsJson + }; + + var setupData = System.Text.Json.JsonSerializer.Serialize(setupGameDataViewModel); + + var serverResponse = await instance.HubConnection.InvokeAsync>( + "SetupGameData", + setupData, + Il2CppSystem.Threading.CancellationToken.None + ); + + Plugin.logger.LogInfo("Setup complete: " + serverResponse.Success); + } + catch (Exception ex) + { + Plugin.logger.LogInfo("Error: " + ex.Message); + } + } + + public static (byte[] serializedGameState, string gameSettingsJson) CreateMultiplayerGame(LobbyGameViewModel lobby, + int gameVersion, int gameLogicVersion) + { + Console.Write(1); + Console.Write(lobby == null); + var lobbyMapSize = lobby.MapSize; + Console.Write(11); + var settings = new GameSettings(); + Console.Write(111); + settings.ApplyLobbySettings(lobby); + Console.Write(111); + if (settings.LiveGamePreset) + { + settings.SetLiveModePreset(); + } + Console.Write(3); + foreach (var participatorViewModel in lobby.Participators) + { + Console.Write(4); + if (participatorViewModel.SelectedTribe == 0) participatorViewModel.SelectedTribe = 2; //TODO: Remove later + + 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); + Console.Write(5); + } + Console.Write(6); + 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; //TODO + 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) + { + Il2CppSystem.Nullable nullableGuid = new() + { + value = player.profile.id + }; + 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($"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("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($"Creating initial state for {gameState.PlayerCount} players..."); + + foreach (PlayerState playerState3 in gameState.PlayerStates) + { + foreach (PlayerState playerState4 in gameState.PlayerStates) + playerState3.aggressions[playerState4.Id] = 0; + if (playerState3.Id != byte.MaxValue) + { + playerState3.Currency = 55; + TribeData data3; + UnitData data4; + if (gameState.GameLogicData.TryGetData(playerState3.tribe, out data3) && + gameState.GameLogicData.TryGetData(data3.startingUnit.type, out data4)) + { + TileData tile = gameState.Map.GetTile(playerState3.startTile); + UnitState unitState = ActionUtils.TrainUnitScored(gameState, playerState3, tile, data4); + unitState.attacked = false; + unitState.moved = false; + } + } + } + + Plugin.logger.LogInfo("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/Plugin.cs b/src/Plugin.cs index e88ab34..a336d7e 100644 --- a/src/Plugin.cs +++ b/src/Plugin.cs @@ -22,10 +22,11 @@ public partial class Plugin : BepInEx.Unity.IL2CPP.BasePlugin /// Whether to include pre-release versions when updating. internal record PolyConfig( bool debug = false, - string backendUrl = Multiplayer.DEFAULT_SERVER_URL, bool autoUpdate = true, bool updatePrerelease = false, - bool allowUnsafeIndexes = false + bool allowUnsafeIndexes = false, + string backendUrl = Multiplayer.DEFAULT_SERVER_URL, + string overrideDeviceId = "" ); /// diff --git a/src/ViewModels/IMonoServerResponseData.cs b/src/ViewModels/IMonoServerResponseData.cs new file mode 100644 index 0000000..18e613c --- /dev/null +++ b/src/ViewModels/IMonoServerResponseData.cs @@ -0,0 +1,5 @@ +namespace PolyMod.ViewModels; + +public interface IMonoServerResponseData +{ +} \ No newline at end of file diff --git a/src/ViewModels/SetupGameDataViewModel.cs b/src/ViewModels/SetupGameDataViewModel.cs new file mode 100644 index 0000000..09de35c --- /dev/null +++ b/src/ViewModels/SetupGameDataViewModel.cs @@ -0,0 +1,10 @@ + +namespace PolyMod.ViewModels; +public class SetupGameDataViewModel : IMonoServerResponseData +{ + public string lobbyId { get; set; } + + public byte[] serializedGameState { get; set; } + + public string gameSettingsJson { get; set; } +} \ No newline at end of file From 3db6ce2f6c38de76db8534b8ec82411abcdf1c41 Mon Sep 17 00:00:00 2001 From: Maksym Muraviov$ Date: Tue, 24 Mar 2026 18:06:13 +0100 Subject: [PATCH 5/8] fixed malformed player account id --- src/Managers/Multiplayer.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Managers/Multiplayer.cs b/src/Managers/Multiplayer.cs index bcb5088..24aa892 100644 --- a/src/Managers/Multiplayer.cs +++ b/src/Managers/Multiplayer.cs @@ -313,10 +313,11 @@ public static (byte[] serializedGameState, string gameSettingsJson) CreateMultip PlayerData player = settings.GetPlayer(index); if (player.type != PlayerDataType.Bot) { - Il2CppSystem.Nullable nullableGuid = new() + var nullableGuid = new Il2CppSystem.Nullable(player.profile.id); + if (!nullableGuid.HasValue) { - value = player.profile.id - }; + throw new Exception("GUID was not set properly!"); + } PlayerState playerState = new PlayerState() { Id = (byte)(index + 1), From 03498e8a1bbb54031de19694936f526e75b3b5f2 Mon Sep 17 00:00:00 2001 From: Maksym Muraviov$ Date: Wed, 25 Mar 2026 11:27:04 +0100 Subject: [PATCH 6/8] Moved stuff a bit and finished implementing client side creation of game state --- src/{Managers => Multiplayer}/Multiplayer.cs | 101 +++++++++--------- .../ViewModels/IMonoServerResponseData.cs | 2 +- .../ViewModels/SetupGameDataViewModel.cs | 2 +- src/Plugin.cs | 4 +- 4 files changed, 54 insertions(+), 55 deletions(-) rename src/{Managers => Multiplayer}/Multiplayer.cs (82%) rename src/{ => Multiplayer}/ViewModels/IMonoServerResponseData.cs (51%) rename src/{ => Multiplayer}/ViewModels/SetupGameDataViewModel.cs (82%) diff --git a/src/Managers/Multiplayer.cs b/src/Multiplayer/Multiplayer.cs similarity index 82% rename from src/Managers/Multiplayer.cs rename to src/Multiplayer/Multiplayer.cs index 24aa892..a06e021 100644 --- a/src/Managers/Multiplayer.cs +++ b/src/Multiplayer/Multiplayer.cs @@ -1,6 +1,6 @@ using HarmonyLib; using Il2CppMicrosoft.AspNetCore.SignalR.Client; -using PolyMod.ViewModels; +using PolyMod.Multiplayer.ViewModels; using Polytopia.Data; using PolytopiaBackendBase; using PolytopiaBackendBase.Common; @@ -9,9 +9,9 @@ using UnityEngine; using Newtonsoft.Json; -namespace PolyMod.Managers; +namespace PolyMod.Multiplayer; -public static class Multiplayer +public static class Client { internal const string DEFAULT_SERVER_URL = "https://dev.polydystopia.xyz"; internal const string LOCAL_SERVER_URL = "http://localhost:5051/"; @@ -24,13 +24,13 @@ public static class Multiplayer internal static void Init() { - Harmony.CreateAndPatchAll(typeof(Multiplayer)); + Harmony.CreateAndPatchAll(typeof(Client)); BuildConfig buildConfig = BuildConfigHelper.GetSelectedBuildConfig(); buildConfig.buildServerURL = BuildServerURL.Custom; buildConfig.customServerURL = LOCAL_SERVER_URL; - Plugin.logger.LogInfo($"Server URL set to: {Plugin.config.backendUrl}"); - Plugin.logger.LogInfo("GLD patches applied"); + Plugin.logger.LogInfo($"Multiplayer> Server URL set to: {Plugin.config.backendUrl}"); + Plugin.logger.LogInfo("Multiplayer> GLD patches applied"); } [HarmonyPostfix] @@ -187,17 +187,25 @@ private static void Deserialize_Postfix(GameState __instance, BinaryReader __0) [HarmonyPrefix] [HarmonyPatch(typeof(BackendAdapter), nameof(BackendAdapter.StartLobbyGame))] - private static bool BackendAdapter_StartLobbyGame( + private static bool BackendAdapter_StartLobbyGame_Modded( ref Il2CppSystem.Threading.Tasks.Task> __result, BackendAdapter __instance, StartLobbyBindingModel model) { - Plugin.logger.LogInfo("BackendAdapter_StartLobbyGame"); - _ = HandleStartLobbyGameAsync(__instance, model); - return true; + 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 Task HandleStartLobbyGameAsync(BackendAdapter instance, StartLobbyBindingModel model) + private static async System.Threading.Tasks.Task HandleStartLobbyGameModded( + Il2CppSystem.Threading.Tasks.TaskCompletionSource> tcs, + BackendAdapter instance, + StartLobbyBindingModel model) { try { @@ -205,9 +213,10 @@ private static async Task HandleStartLobbyGameAsync(BackendAdapter instance, Sta { LobbyId = model.LobbyId }); - Plugin.logger.LogInfo($"Lobby processed {lobbyResponse.Success}"); + + Plugin.logger.LogInfo($"Multiplayer> Lobby processed {lobbyResponse.Success}"); LobbyGameViewModel lobbyGameViewModel = lobbyResponse.Data; - Plugin.logger.LogInfo("Lobby received"); + Plugin.logger.LogInfo("Multiplayer> Lobby received"); (byte[] serializedGameState, string gameSettingsJson) = CreateMultiplayerGame( lobbyGameViewModel, @@ -215,7 +224,7 @@ private static async Task HandleStartLobbyGameAsync(BackendAdapter instance, Sta VersionManager.GameLogicDataVersion ); - Plugin.logger.LogInfo("Game data created"); + Plugin.logger.LogInfo("Multiplayer> GameState and Settiings created"); var setupGameDataViewModel = new SetupGameDataViewModel { @@ -226,48 +235,40 @@ private static async Task HandleStartLobbyGameAsync(BackendAdapter instance, Sta var setupData = System.Text.Json.JsonSerializer.Serialize(setupGameDataViewModel); - var serverResponse = await instance.HubConnection.InvokeAsync>( - "SetupGameData", + var serverResponse = await instance.HubConnection.InvokeAsync>( + "StartLobbyGameModded", setupData, Il2CppSystem.Threading.CancellationToken.None ); - - Plugin.logger.LogInfo("Setup complete: " + serverResponse.Success); + Plugin.logger.LogInfo("Multiplayer> Invoked StartLobbyGameModded"); + tcs.SetResult(serverResponse); } catch (Exception ex) { - Plugin.logger.LogInfo("Error: " + ex.Message); + 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) { - Console.Write(1); - Console.Write(lobby == null); var lobbyMapSize = lobby.MapSize; - Console.Write(11); var settings = new GameSettings(); - Console.Write(111); settings.ApplyLobbySettings(lobby); - Console.Write(111); if (settings.LiveGamePreset) { settings.SetLiveModePreset(); } - Console.Write(3); foreach (var participatorViewModel in lobby.Participators) { - Console.Write(4); - if (participatorViewModel.SelectedTribe == 0) participatorViewModel.SelectedTribe = 2; //TODO: Remove later - var humanPlayer = new PlayerData { type = PlayerDataType.LocalUser, state = PlayerDataFriendshipState.Accepted, knownTribe = true, tribe = (TribeType)participatorViewModel.SelectedTribe, - tribeMix = (TribeType)participatorViewModel.SelectedTribe, //? + tribeMix = (TribeType)participatorViewModel.SelectedTribe, skinType = (SkinType)participatorViewModel.SelectedTribeSkin, defaultName = participatorViewModel.GetNameInternal() }; @@ -277,9 +278,8 @@ public static (byte[] serializedGameState, string gameSettingsJson) CreateMultip humanPlayer.profile.avatarState = avatarState; settings.AddPlayer(humanPlayer); - Console.Write(5); } - Console.Write(6); + foreach (var botDifficulty in lobby.Bots) { var botGuid = Il2CppSystem.Guid.NewGuid(); @@ -294,7 +294,7 @@ public static (byte[] serializedGameState, string gameSettingsJson) CreateMultip }; ; botPlayer.botDifficulty = (BotDifficulty)botDifficulty; - botPlayer.skinType = SkinType.Default; //TODO + botPlayer.skinType = SkinType.Default; botPlayer.defaultName = "Bot" + botGuid; botPlayer.profile.id = botGuid; @@ -330,7 +330,7 @@ public static (byte[] serializedGameState, string gameSettingsJson) CreateMultip skinType = player.skinType }; gameState.PlayerStates.Add(playerState); - Plugin.logger.LogInfo($"Created player: {playerState}"); + Plugin.logger.LogInfo($"Multiplayer> Created player: {playerState}"); } else { @@ -341,35 +341,34 @@ public static (byte[] serializedGameState, string gameSettingsJson) CreateMultip GameStateUtils.SetPlayerColors(gameState); GameStateUtils.AddNaturePlayer(gameState); - Plugin.logger.LogInfo("Creating world..."); + + 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($"Creating initial state for {gameState.PlayerCount} players..."); - foreach (PlayerState playerState3 in gameState.PlayerStates) + Plugin.logger.LogInfo($"Multiplayer> Creating initial state for {gameState.PlayerCount} players..."); + + foreach (PlayerState player in gameState.PlayerStates) { - foreach (PlayerState playerState4 in gameState.PlayerStates) - playerState3.aggressions[playerState4.Id] = 0; - if (playerState3.Id != byte.MaxValue) + foreach (PlayerState otherPlayer in gameState.PlayerStates) + player.aggressions[otherPlayer.Id] = 0; + + if (player.Id != byte.MaxValue && gameState.GameLogicData.TryGetData(player.tribe, out TribeData tribeData)) { - playerState3.Currency = 55; - TribeData data3; - UnitData data4; - if (gameState.GameLogicData.TryGetData(playerState3.tribe, out data3) && - gameState.GameLogicData.TryGetData(data3.startingUnit.type, out data4)) - { - TileData tile = gameState.Map.GetTile(playerState3.startTile); - UnitState unitState = ActionUtils.TrainUnitScored(gameState, playerState3, tile, data4); - unitState.attacked = false; - unitState.moved = false; - } + 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("Session created successfully"); + Plugin.logger.LogInfo("Multiplayer> Session created successfully"); + gameState.CommandStack.Add((CommandBase)new StartMatchCommand((byte)1)); var serializedGameState = SerializationHelpers.ToByteArray(gameState, gameState.Version); diff --git a/src/ViewModels/IMonoServerResponseData.cs b/src/Multiplayer/ViewModels/IMonoServerResponseData.cs similarity index 51% rename from src/ViewModels/IMonoServerResponseData.cs rename to src/Multiplayer/ViewModels/IMonoServerResponseData.cs index 18e613c..3b0a835 100644 --- a/src/ViewModels/IMonoServerResponseData.cs +++ b/src/Multiplayer/ViewModels/IMonoServerResponseData.cs @@ -1,4 +1,4 @@ -namespace PolyMod.ViewModels; +namespace PolyMod.Multiplayer.ViewModels; public interface IMonoServerResponseData { diff --git a/src/ViewModels/SetupGameDataViewModel.cs b/src/Multiplayer/ViewModels/SetupGameDataViewModel.cs similarity index 82% rename from src/ViewModels/SetupGameDataViewModel.cs rename to src/Multiplayer/ViewModels/SetupGameDataViewModel.cs index 09de35c..68f43f1 100644 --- a/src/ViewModels/SetupGameDataViewModel.cs +++ b/src/Multiplayer/ViewModels/SetupGameDataViewModel.cs @@ -1,5 +1,5 @@ -namespace PolyMod.ViewModels; +namespace PolyMod.Multiplayer.ViewModels; public class SetupGameDataViewModel : IMonoServerResponseData { public string lobbyId { get; set; } diff --git a/src/Plugin.cs b/src/Plugin.cs index a336d7e..924944c 100644 --- a/src/Plugin.cs +++ b/src/Plugin.cs @@ -25,7 +25,7 @@ internal record PolyConfig( bool autoUpdate = true, bool updatePrerelease = false, bool allowUnsafeIndexes = false, - string backendUrl = Multiplayer.DEFAULT_SERVER_URL, + string backendUrl = Multiplayer.Client.DEFAULT_SERVER_URL, string overrideDeviceId = "" ); @@ -134,7 +134,7 @@ public override void Load() Hub.Init(); Main.Init(); - Multiplayer.Init(); + Multiplayer.Client.Init(); } /// From 06ba9b2496ea870593f362bb199a28a2d17a4536 Mon Sep 17 00:00:00 2001 From: Maksym Muraviov$ Date: Fri, 27 Mar 2026 17:59:37 +0100 Subject: [PATCH 7/8] unhardcoded backend url --- src/Multiplayer/{Multiplayer.cs => Client.cs} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename src/Multiplayer/{Multiplayer.cs => Client.cs} (99%) diff --git a/src/Multiplayer/Multiplayer.cs b/src/Multiplayer/Client.cs similarity index 99% rename from src/Multiplayer/Multiplayer.cs rename to src/Multiplayer/Client.cs index a06e021..4ff1a93 100644 --- a/src/Multiplayer/Multiplayer.cs +++ b/src/Multiplayer/Client.cs @@ -27,7 +27,7 @@ internal static void Init() Harmony.CreateAndPatchAll(typeof(Client)); BuildConfig buildConfig = BuildConfigHelper.GetSelectedBuildConfig(); buildConfig.buildServerURL = BuildServerURL.Custom; - buildConfig.customServerURL = LOCAL_SERVER_URL; + buildConfig.customServerURL = Plugin.config.backendUrl; Plugin.logger.LogInfo($"Multiplayer> Server URL set to: {Plugin.config.backendUrl}"); Plugin.logger.LogInfo("Multiplayer> GLD patches applied"); From 9a9f213eb7a382039d51e43e54db3f1ddf1389b8 Mon Sep 17 00:00:00 2001 From: Maksym Muraviov$ Date: Sun, 5 Apr 2026 22:15:24 +0200 Subject: [PATCH 8/8] added half working modded games --- src/Multiplayer/Client.cs | 286 ++++++++++------ src/Multiplayer/SerializationUtils.cs | 319 ++++++++++++++++++ .../ViewModels/SetupGameDataViewModel.cs | 10 - .../ViewModels/SetupGameStateViewModel.cs | 14 + src/Plugin.cs | 1 + 5 files changed, 516 insertions(+), 114 deletions(-) create mode 100644 src/Multiplayer/SerializationUtils.cs delete mode 100644 src/Multiplayer/ViewModels/SetupGameDataViewModel.cs create mode 100644 src/Multiplayer/ViewModels/SetupGameStateViewModel.cs diff --git a/src/Multiplayer/Client.cs b/src/Multiplayer/Client.cs index 4ff1a93..0792bf8 100644 --- a/src/Multiplayer/Client.cs +++ b/src/Multiplayer/Client.cs @@ -27,7 +27,7 @@ internal static void Init() Harmony.CreateAndPatchAll(typeof(Client)); BuildConfig buildConfig = BuildConfigHelper.GetSelectedBuildConfig(); buildConfig.buildServerURL = BuildServerURL.Custom; - buildConfig.customServerURL = Plugin.config.backendUrl; + buildConfig.customServerURL = LOCAL_SERVER_URL; Plugin.logger.LogInfo($"Multiplayer> Server URL set to: {Plugin.config.backendUrl}"); Plugin.logger.LogInfo("Multiplayer> GLD patches applied"); @@ -59,130 +59,203 @@ public static void SteamClient_get_SteamId(ref string __result) } - /// - /// 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; + // /// + // /// 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; + // } - 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}"); + [HarmonyPrefix] + [HarmonyPatch(typeof(ClientBase), nameof(ClientBase.SendCommand))] + private static bool ClientBase_SendCommand( + ClientBase __instance, + CommandBase command) + { - // Check if there's more data after normal deserialization - if (position >= length) - { - Plugin.logger?.LogDebug("Deserialize_Postfix: No trailing data (position >= length)"); + Plugin.logger.LogInfo("Multiplayer> ClientBase_SendCommand"); + Il2CppSystem.Threading.Tasks.Task> task = new(); + var taskCompletionSource = new Il2CppSystem.Threading.Tasks.TaskCompletionSource>(); - 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; - } + _ = HandleSendCommandModded(taskCompletionSource, __instance, command); - Plugin.logger?.LogDebug($"Deserialize_Postfix: Found {remaining} bytes of trailing data, attempting to read marker"); + task = taskCompletionSource.Task; - var marker = reader.ReadString(); - Plugin.logger?.LogDebug($"Deserialize_Postfix: Read marker string: '{marker}'"); + return false; + } - if (marker != GldMarker) + private static async System.Threading.Tasks.Task HandleSendCommandModded( + Il2CppSystem.Threading.Tasks.TaskCompletionSource> tcs, + ClientBase client, + CommandBase command) + { + try + { + if (!client.CurrentGameId.HasValue) { - Plugin.logger?.LogDebug($"Deserialize_Postfix: Marker mismatch - expected '{GldMarker}', got '{marker}'"); + Console.Write("Tried to perform and send command but no GameId was set"); 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)) + if (!ClientActionManager.CanReceiveCommand(command, client.GameState)) { - Plugin.logger?.LogError($"Deserialize_Postfix: Failed to fetch GLD for ModGldVersion: {modGldVersion}"); + 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); - Plugin.logger?.LogDebug($"Deserialize_Postfix: Parsing GLD JSON ({gldJson.Length} chars)"); + var serializedGameState = SerializationHelpers.ToByteArray(client.GameState, client.GameState.Version); - var customGld = new GameLogicData(); - customGld.Parse(gldJson); - __instance.mockedGameLogicData = customGld; + var succ = GameStateSummary.FromGameStateByteArray(serializedGameState, + out GameStateSummary stateSummary, out var gameState); - // Cache for subsequent deserializations (rewinds, reloads) - var seed = __instance.Seed; - _gldCache[seed] = customGld; - _versionCache[seed] = modGldVersion; + var serializedGameSummary = SerializationHelpers.ToByteArray(stateSummary, gameState.Version); - 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 setupGameDataViewModel = new SetupGameStateViewModel { - var gld = response.Content.ReadAsStringAsync().Result; - Plugin.logger?.LogInfo($"FetchGldById: Successfully fetched mod GLD ({gld.Length} chars)"); - return gld; - } + gameId = client.gameId.ToString(), + serializedGameState = serializedGameState, + serializedGameSummary = serializedGameSummary, + gameSettingsJson = "" + }; + + var setupData = System.Text.Json.JsonSerializer.Serialize(setupGameDataViewModel); - var errorContent = response.Content.ReadAsStringAsync().Result; - Plugin.logger?.LogError($"FetchGldById: Failed with status {response.StatusCode}: {errorContent}"); + var serverResponse = await PolytopiaBackendAdapter.Instance.HubConnection.InvokeAsync>( + "UpdateGameStateModded", + setupData, + Il2CppSystem.Threading.CancellationToken.None + ); + tcs.SetResult(serverResponse); } 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}"); - } + Plugin.logger.LogError("Multiplayer> Error during HandleSendCommandModded: " + ex.Message); + tcs.SetException(new Il2CppSystem.Exception(ex.Message)); } - return null; } [HarmonyPrefix] @@ -226,10 +299,15 @@ private static async System.Threading.Tasks.Task HandleStartLobbyGameModded( Plugin.logger.LogInfo("Multiplayer> GameState and Settiings created"); - var setupGameDataViewModel = new SetupGameDataViewModel + 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 }; 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/SetupGameDataViewModel.cs b/src/Multiplayer/ViewModels/SetupGameDataViewModel.cs deleted file mode 100644 index 68f43f1..0000000 --- a/src/Multiplayer/ViewModels/SetupGameDataViewModel.cs +++ /dev/null @@ -1,10 +0,0 @@ - -namespace PolyMod.Multiplayer.ViewModels; -public class SetupGameDataViewModel : IMonoServerResponseData -{ - public string lobbyId { get; set; } - - public byte[] serializedGameState { get; set; } - - public string gameSettingsJson { get; set; } -} \ 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 924944c..597609c 100644 --- a/src/Plugin.cs +++ b/src/Plugin.cs @@ -134,6 +134,7 @@ public override void Load() Hub.Init(); Main.Init(); + Multiplayer.SerializationUtils.Init(); Multiplayer.Client.Init(); }