From 3967b7c5b1e5e5bcd56d54b2bafc055cad7939aa Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 9 Jun 2025 13:36:47 +0200 Subject: [PATCH 01/34] 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 02/34] 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 03/34] 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 04/34] 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 05/34] 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 06/34] 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 07/34] 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 08/34] 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(); } From f556cc81f5cc2e60b83a5eb8175017ecffd39cb4 Mon Sep 17 00:00:00 2001 From: Demonofpower Date: Sun, 12 Apr 2026 21:28:38 +0200 Subject: [PATCH 09/34] first android multiplayer edits --- src/Multiplayer/Multiplayer.cs | 51 ++++++++++++++++++++++++++++++++-- 1 file changed, 49 insertions(+), 2 deletions(-) diff --git a/src/Multiplayer/Multiplayer.cs b/src/Multiplayer/Multiplayer.cs index a06e021..be0921b 100644 --- a/src/Multiplayer/Multiplayer.cs +++ b/src/Multiplayer/Multiplayer.cs @@ -8,6 +8,7 @@ using PolytopiaBackendBase.Game.BindingModels; using UnityEngine; using Newtonsoft.Json; +using PolytopiaBackendBase.Auth; namespace PolyMod.Multiplayer; @@ -27,10 +28,53 @@ 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; + + // Update BackendUri and HttpClient.BaseAddress since PolytopiaBackendAdapter.Instance + // was statically initialized before plugins load, so it still points to polytopia-prod.net + var uri = new Il2CppSystem.Uri(Plugin.config.backendUrl); + PolytopiaBackendAdapter.Instance.UseBackendUri(uri); + PolytopiaBackendAdapter.Instance.BackendHttpClient.BaseAddress = uri; Plugin.logger.LogInfo($"Multiplayer> Server URL set to: {Plugin.config.backendUrl}"); - Plugin.logger.LogInfo("Multiplayer> GLD patches applied"); + } + + /// + /// On Android, bypass multiplayer requirements that depend on + /// Google Play login, push notifications, and purchases — none of which work + /// when running as a wrapper app with a different package identity. + /// + [HarmonyPrefix] + [HarmonyPatch(typeof(GameManager), nameof(GameManager.IsMultiplayerEnabled), MethodType.Getter)] + public static bool GameManager_IsMultiplayerEnabled(ref bool __result) + { + if (Application.platform != RuntimePlatform.Android) return true; + __result = true; + return false; + } + + /// + /// Replace the Android login flow to skip Google Play Games SDK entirely. + /// Uses deviceUniqueIdentifier as the auth code for the Polydystopia backend. + /// + [HarmonyPrefix] + [HarmonyPatch(typeof(PolytopiaBackendAdapter), "LoginPlatformAndroid")] + public static bool LoginPlatformAndroid_Prefix( + ref Il2CppSystem.Threading.Tasks.Task> __result, + PolytopiaBackendAdapter __instance) + { + if (Application.platform != RuntimePlatform.Android) return true; + + // Mark social login as cached so the post-login flow doesn't bail out + __instance.HasSocialLoginCached = true; + + var model = new LoginGooglePlayBindingModel(); + model.AuthCode = SystemInfo.deviceUniqueIdentifier; + model.DeviceId = SystemInfo.deviceUniqueIdentifier; + + Plugin.logger.LogInfo($"Multiplayer> Android login with DeviceId: {model.DeviceId}"); + __result = __instance.LoginGooglePlay(model); + return false; } [HarmonyPostfix] @@ -192,6 +236,9 @@ private static bool BackendAdapter_StartLobbyGame_Modded( BackendAdapter __instance, StartLobbyBindingModel model) { + // On Android, let the game's original StartLobbyGame handle it + if (Application.platform == RuntimePlatform.Android) return true; + Plugin.logger.LogInfo("Multiplayer> BackendAdapter_StartLobbyGame_Modded"); var taskCompletionSource = new Il2CppSystem.Threading.Tasks.TaskCompletionSource>(); From e67a7282381ff42efd191d05fd54dae3e282f476 Mon Sep 17 00:00:00 2001 From: Maksym Muraviov$ Date: Sun, 3 May 2026 21:23:34 +0200 Subject: [PATCH 10/34] Implemented working online battles with mods --- src/Multiplayer/Client.cs | 51 ++- src/Multiplayer/SerializationUtils.cs | 361 +++--------------- .../ViewModels/IMonoServerResponseData.cs | 5 - ...ewModel.cs => ModdedGameStateViewModel.cs} | 7 +- 4 files changed, 108 insertions(+), 316 deletions(-) delete mode 100644 src/Multiplayer/ViewModels/IMonoServerResponseData.cs rename src/Multiplayer/ViewModels/{SetupGameStateViewModel.cs => ModdedGameStateViewModel.cs} (69%) diff --git a/src/Multiplayer/Client.cs b/src/Multiplayer/Client.cs index 0792bf8..aba7195 100644 --- a/src/Multiplayer/Client.cs +++ b/src/Multiplayer/Client.cs @@ -234,14 +234,25 @@ private static async System.Threading.Tasks.Task HandleSendCommandModded( var serializedGameSummary = SerializationHelpers.ToByteArray(stateSummary, gameState.Version); - var setupGameDataViewModel = new SetupGameStateViewModel + + client.GameState.TryGetPlayer(client.GameState.CurrentPlayer, out PlayerState playerState); + var currentPlayerId = ""; + if(playerState.AccountId.HasValue) + { + currentPlayerId = playerState.AccountId.Value.ToString(); + } + var setupGameDataViewModel = new ModdedGameStateViewModel { gameId = client.gameId.ToString(), serializedGameState = serializedGameState, serializedGameSummary = serializedGameSummary, - gameSettingsJson = "" + gameSettingsJson = "", + currentPlayerId = currentPlayerId, + IsEndTurnCommand = command.GetCommandType() == CommandType.EndTurn }; + + var setupData = System.Text.Json.JsonSerializer.Serialize(setupGameDataViewModel); var serverResponse = await PolytopiaBackendAdapter.Instance.HubConnection.InvokeAsync>( @@ -303,7 +314,7 @@ private static async System.Threading.Tasks.Task HandleStartLobbyGameModded( out GameStateSummary stateSummary, out var gameState); var serializedGameSummary = SerializationHelpers.ToByteArray(stateSummary, gameState.Version); - var setupGameDataViewModel = new SetupGameStateViewModel + var setupGameDataViewModel = new ModdedGameStateViewModel { lobbyId = lobbyGameViewModel.Id.ToString(), serializedGameState = serializedGameState, @@ -346,7 +357,7 @@ public static (byte[] serializedGameState, string gameSettingsJson) CreateMultip state = PlayerDataFriendshipState.Accepted, knownTribe = true, tribe = (TribeType)participatorViewModel.SelectedTribe, - tribeMix = (TribeType)participatorViewModel.SelectedTribe, + tribeMix = TribeType.None, // TribeMix is byte too skinType = (SkinType)participatorViewModel.SelectedTribeSkin, defaultName = participatorViewModel.GetNameInternal() }; @@ -454,4 +465,36 @@ public static (byte[] serializedGameState, string gameSettingsJson) CreateMultip return (serializedGameState, JsonConvert.SerializeObject(gameState.Settings)); } + + // FIX FOR NATURE PLAYER. BOTS ARENT IMPLEMENTED YET + + [HarmonyPrefix] + [HarmonyPatch(typeof(GameState), nameof(GameState.EndPlayerTurn))] + private static bool GameState_EndPlayerTurn(GameState __instance, bool newTurn = false) + { + Console.Write("GameState_EndPlayerTurn"); + __instance.CurrentPlayerIndex++; + if (__instance.CurrentPlayerIndex >=__instance. PlayerStates.Count) + { + __instance.CurrentPlayerIndex = 0; + newTurn = true; + } + + var currentPlayer = __instance.PlayerStates[__instance.CurrentPlayerIndex]; + if (!currentPlayer.IsAlive(__instance)) + { + __instance.EndPlayerTurn(newTurn); + } + else if (newTurn) + { + __instance.CurrentTurn++; + } + + if(currentPlayer.AutoPlay) + { + __instance.CommandStack.Add(new EndTurnCommand(currentPlayer.Id)); + } + Console.Write("finished"); + return false; + } } diff --git a/src/Multiplayer/SerializationUtils.cs b/src/Multiplayer/SerializationUtils.cs index ea32c6b..e91a46b 100644 --- a/src/Multiplayer/SerializationUtils.cs +++ b/src/Multiplayer/SerializationUtils.cs @@ -11,309 +11,64 @@ 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.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(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; - // } + [HarmonyPostfix] + [HarmonyPatch(typeof(PlayerState), nameof(PlayerState.Deserialize))] + public static void PlayerState_Deserialize(PlayerState __instance, Il2CppSystem.IO.BinaryReader reader, int version) + { + __instance.climate = __instance.tribe; + } } \ No newline at end of file diff --git a/src/Multiplayer/ViewModels/IMonoServerResponseData.cs b/src/Multiplayer/ViewModels/IMonoServerResponseData.cs deleted file mode 100644 index 3b0a835..0000000 --- a/src/Multiplayer/ViewModels/IMonoServerResponseData.cs +++ /dev/null @@ -1,5 +0,0 @@ -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/ModdedGameStateViewModel.cs similarity index 69% rename from src/Multiplayer/ViewModels/SetupGameStateViewModel.cs rename to src/Multiplayer/ViewModels/ModdedGameStateViewModel.cs index 63467a9..176a997 100644 --- a/src/Multiplayer/ViewModels/SetupGameStateViewModel.cs +++ b/src/Multiplayer/ViewModels/ModdedGameStateViewModel.cs @@ -1,8 +1,5 @@ - -using Tesla; - namespace PolyMod.Multiplayer.ViewModels; -public class SetupGameStateViewModel : IMonoServerResponseData +public class ModdedGameStateViewModel { public string gameId { get; set; } = ""; public string lobbyId { get; set; } = ""; @@ -11,4 +8,6 @@ public class SetupGameStateViewModel : IMonoServerResponseData public byte[] serializedGameSummary { get; set; } = new byte[0]; public string gameSettingsJson { get; set; } = ""; + public string currentPlayerId { get; set; } = ""; + public bool IsEndTurnCommand { get; set; } = false; } \ No newline at end of file From 12bf062ecd549fef09eb96d91429125ef7e88b63 Mon Sep 17 00:00:00 2001 From: Maksym Muraviov$ Date: Fri, 19 Jun 2026 22:24:20 +0200 Subject: [PATCH 11/34] removed all references to old start screen --- src/Multiplayer/Client.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Multiplayer/Client.cs b/src/Multiplayer/Client.cs index aba7195..e7ef4bd 100644 --- a/src/Multiplayer/Client.cs +++ b/src/Multiplayer/Client.cs @@ -34,18 +34,18 @@ internal static void Init() } [HarmonyPostfix] - [HarmonyPatch(typeof(MultiplayerScreen), nameof(MultiplayerScreen.Show))] - public static void MultiplayerScreen_Show(MultiplayerScreen __instance) + [HarmonyPatch(typeof(MultiplayerSelectionScreen), nameof(MultiplayerSelectionScreen.Show))] + public static void MultiplayerScreen_Show(MultiplayerSelectionScreen __instance) { - __instance.multiplayerSelectionScreen.TournamentsButton.gameObject.SetActive(false); + __instance.TournamentsButton.gameObject.SetActive(false); } [HarmonyPostfix] - [HarmonyPatch(typeof(StartScreen), nameof(StartScreen.Start))] - private static void StartScreen_Start(StartScreen __instance) + [HarmonyPatch(typeof(StartScreen_UI2), nameof(StartScreen_UI2.RunLayout))] + private static void StartScreen_Start(StartScreen_UI2 __instance) { __instance.highscoreButton.gameObject.SetActive(false); - __instance.weeklyChallengesButton.gameObject.SetActive(false); + __instance.weeklyChallengeButton.gameObject.SetActive(false); } [HarmonyPostfix] From 9b5d3aecb5fc3e84141e7f16b75577b2d213e1dd Mon Sep 17 00:00:00 2001 From: Maksym Muraviov$ Date: Mon, 6 Jul 2026 21:33:00 +0200 Subject: [PATCH 12/34] minor stuff --- src/Multiplayer/Client.cs | 136 ++------------------------------------ 1 file changed, 5 insertions(+), 131 deletions(-) diff --git a/src/Multiplayer/Client.cs b/src/Multiplayer/Client.cs index e7ef4bd..add1b28 100644 --- a/src/Multiplayer/Client.cs +++ b/src/Multiplayer/Client.cs @@ -42,7 +42,7 @@ public static void MultiplayerScreen_Show(MultiplayerSelectionScreen __instance) [HarmonyPostfix] [HarmonyPatch(typeof(StartScreen_UI2), nameof(StartScreen_UI2.RunLayout))] - private static void StartScreen_Start(StartScreen_UI2 __instance) + private static void StartScreen_UI2_RunLayout(StartScreen_UI2 __instance) { __instance.highscoreButton.gameObject.SetActive(false); __instance.weeklyChallengeButton.gameObject.SetActive(false); @@ -59,132 +59,6 @@ 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; - - // 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( @@ -196,14 +70,14 @@ private static bool ClientBase_SendCommand( Il2CppSystem.Threading.Tasks.Task> task = new(); var taskCompletionSource = new Il2CppSystem.Threading.Tasks.TaskCompletionSource>(); - _ = HandleSendCommandModded(taskCompletionSource, __instance, command); + _ = ClientBase_SendCommand_Async(taskCompletionSource, __instance, command); task = taskCompletionSource.Task; return false; } - private static async System.Threading.Tasks.Task HandleSendCommandModded( + private static async System.Threading.Tasks.Task ClientBase_SendCommand_Async( Il2CppSystem.Threading.Tasks.TaskCompletionSource> tcs, ClientBase client, CommandBase command) @@ -279,14 +153,14 @@ private static bool BackendAdapter_StartLobbyGame_Modded( Plugin.logger.LogInfo("Multiplayer> BackendAdapter_StartLobbyGame_Modded"); var taskCompletionSource = new Il2CppSystem.Threading.Tasks.TaskCompletionSource>(); - _ = HandleStartLobbyGameModded(taskCompletionSource, __instance, model); + _ = BackendAdapter_StartLobbyGame_Async(taskCompletionSource, __instance, model); __result = taskCompletionSource.Task; return false; } - private static async System.Threading.Tasks.Task HandleStartLobbyGameModded( + private static async System.Threading.Tasks.Task BackendAdapter_StartLobbyGame_Async( Il2CppSystem.Threading.Tasks.TaskCompletionSource> tcs, BackendAdapter instance, StartLobbyBindingModel model) From 56c1f1145135560aa9d66bb5c4826b6a9a26af0d Mon Sep 17 00:00:00 2001 From: Demonofpower Date: Mon, 6 Jul 2026 22:12:44 +0200 Subject: [PATCH 13/34] minor button arrange fix --- src/Multiplayer/Multiplayer.cs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/Multiplayer/Multiplayer.cs b/src/Multiplayer/Multiplayer.cs index 81c6355..fe855fe 100644 --- a/src/Multiplayer/Multiplayer.cs +++ b/src/Multiplayer/Multiplayer.cs @@ -92,6 +92,20 @@ private static void StartScreen_UI2_HideButtons(StartScreen_UI2 __instance) __instance.weeklyChallengeButton.gameObject.SetActive(false); } + [HarmonyPostfix] + [HarmonyPatch(typeof(StartScreen_UI2), nameof(StartScreen_UI2.RunLayout))] + private static void StartScreen_UI2_ReflowRoundButtons(StartScreen_UI2 __instance, ScreenBase_UI2.ScreenSize screenSize) + { + // RunLayout adds all four round buttons to a UITable unconditionally, so hiding the highscore button leaves a gap. Re-run the row without it so the rest recenter. + UITable table = new(); + table.AddCell(__instance.settingsButton); + table.AddCell(__instance.throneRoomButton); + table.AddCell(__instance.aboutButton); + table.SetBottom(screenSize.safeRect.Bottom + __instance.settingsButton.GetHalfHeight() + 15f); + table.margin = 20f; + table.RunLayout(); + } + [HarmonyPostfix] [HarmonyPatch(typeof(SystemInfo), nameof(SystemInfo.deviceUniqueIdentifier), MethodType.Getter)] public static void SteamClient_get_SteamId(ref string __result) From dc1d176509747d76d1e60e2cb15297fe22e6f241 Mon Sep 17 00:00:00 2001 From: Demonofpower Date: Thu, 9 Jul 2026 22:04:03 +0200 Subject: [PATCH 14/34] minor android refactoring --- src/Android/AndroidHandler.cs | 75 ++++++++++++++++++++++++++++++++++ src/Multiplayer/Multiplayer.cs | 45 ++------------------ src/Plugin.cs | 2 + 3 files changed, 80 insertions(+), 42 deletions(-) create mode 100644 src/Android/AndroidHandler.cs diff --git a/src/Android/AndroidHandler.cs b/src/Android/AndroidHandler.cs new file mode 100644 index 0000000..f8a9c7f --- /dev/null +++ b/src/Android/AndroidHandler.cs @@ -0,0 +1,75 @@ +using HarmonyLib; +using PolytopiaBackendBase; +using PolytopiaBackendBase.Auth; +using UnityEngine; + +namespace PolyMod.Android; + +public static class AndroidHandler +{ + internal static void Init() + { + if (Application.platform != RuntimePlatform.Android) return; + + Harmony.CreateAndPatchAll(typeof(AndroidHandler)); + } + + /// + /// On Android, bypass multiplayer requirements that depend on + /// Google Play login, push notifications, and purchases — none of which work + /// when running as a wrapper app with a different package identity. + /// + [HarmonyPrefix] + [HarmonyPatch(typeof(GameManager), nameof(GameManager.IsMultiplayerEnabled), MethodType.Getter)] + public static bool GameManager_IsMultiplayerEnabled(ref bool __result) + { + __result = true; + return false; + } + + /// + /// Replace the Android login flow to skip Google Play Games SDK entirely. + /// Uses deviceUniqueIdentifier as the auth code for the Polydystopia backend. + /// + [HarmonyPrefix] + [HarmonyPatch(typeof(PolytopiaBackendAdapter), "LoginPlatformAndroid")] + public static bool LoginPlatformAndroid_Prefix( + ref Il2CppSystem.Threading.Tasks.Task> __result, + PolytopiaBackendAdapter __instance) + { + // Mark social login as cached so the post-login flow doesn't bail out + __instance.HasSocialLoginCached = true; + + var model = new LoginGooglePlayBindingModel(); + model.AuthCode = SystemInfo.deviceUniqueIdentifier; + model.DeviceId = SystemInfo.deviceUniqueIdentifier; + + Plugin.logger.LogInfo($"Multiplayer> Android login with DeviceId: {model.DeviceId}"); + __result = __instance.LoginGooglePlay(model); + return false; + } + + /// + /// On android Firebase cannot initialize inside the launcher process (its config lives in the game APK's resources, and the native lib may be unreachable there). + /// We try to skip Firebase completely. + /// + [HarmonyPrefix] + [HarmonyPatch(typeof(AnalyticsManager), nameof(AnalyticsManager.IsAnalyticsEnabled))] + private static bool AnalyticsManager_IsAnalyticsEnabled(ref bool __result) + { + __result = false; + return false; + } + + /// + /// On android Firebase cannot initialize inside the launcher process (its config lives in the game APK's resources, and the native lib may be unreachable there). + /// We try to skip Firebase completely. + /// + [HarmonyPrefix] + [HarmonyPatch(typeof(FirebaseMessagingManager), nameof(FirebaseMessagingManager.Init))] + private static bool FirebaseMessagingManager_Init(FirebaseMessagingManager __instance) + { + __instance.isFirebaseInitialized = true; + return false; + } +} \ No newline at end of file diff --git a/src/Multiplayer/Multiplayer.cs b/src/Multiplayer/Multiplayer.cs index fe855fe..2058aa0 100644 --- a/src/Multiplayer/Multiplayer.cs +++ b/src/Multiplayer/Multiplayer.cs @@ -39,44 +39,6 @@ internal static void Init() Plugin.logger.LogInfo($"Multiplayer> Server URL set to: {Plugin.config.backendUrl}"); } - /// - /// On Android, bypass multiplayer requirements that depend on - /// Google Play login, push notifications, and purchases — none of which work - /// when running as a wrapper app with a different package identity. - /// - [HarmonyPrefix] - [HarmonyPatch(typeof(GameManager), nameof(GameManager.IsMultiplayerEnabled), MethodType.Getter)] - public static bool GameManager_IsMultiplayerEnabled(ref bool __result) - { - if (Application.platform != RuntimePlatform.Android) return true; - __result = true; - return false; - } - - /// - /// Replace the Android login flow to skip Google Play Games SDK entirely. - /// Uses deviceUniqueIdentifier as the auth code for the Polydystopia backend. - /// - [HarmonyPrefix] - [HarmonyPatch(typeof(PolytopiaBackendAdapter), "LoginPlatformAndroid")] - public static bool LoginPlatformAndroid_Prefix( - ref Il2CppSystem.Threading.Tasks.Task> __result, - PolytopiaBackendAdapter __instance) - { - if (Application.platform != RuntimePlatform.Android) return true; - - // Mark social login as cached so the post-login flow doesn't bail out - __instance.HasSocialLoginCached = true; - - var model = new LoginGooglePlayBindingModel(); - model.AuthCode = SystemInfo.deviceUniqueIdentifier; - model.DeviceId = SystemInfo.deviceUniqueIdentifier; - - Plugin.logger.LogInfo($"Multiplayer> Android login with DeviceId: {model.DeviceId}"); - __result = __instance.LoginGooglePlay(model); - return false; - } - [HarmonyPostfix] [HarmonyPatch(typeof(MultiplayerSelectionScreen), nameof(MultiplayerSelectionScreen.Awake))] public static void MultiplayerSelectionScreen_Awake(MultiplayerSelectionScreen __instance) @@ -98,9 +60,9 @@ private static void StartScreen_UI2_ReflowRoundButtons(StartScreen_UI2 __instanc { // RunLayout adds all four round buttons to a UITable unconditionally, so hiding the highscore button leaves a gap. Re-run the row without it so the rest recenter. UITable table = new(); - table.AddCell(__instance.settingsButton); - table.AddCell(__instance.throneRoomButton); - table.AddCell(__instance.aboutButton); + table.AddCell(__instance.settingsButton.Cast()); + table.AddCell(__instance.throneRoomButton.Cast()); + table.AddCell(__instance.aboutButton.Cast()); table.SetBottom(screenSize.safeRect.Bottom + __instance.settingsButton.GetHalfHeight() + 15f); table.margin = 20f; table.RunLayout(); @@ -116,7 +78,6 @@ 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. diff --git a/src/Plugin.cs b/src/Plugin.cs index 924944c..25a496c 100644 --- a/src/Plugin.cs +++ b/src/Plugin.cs @@ -3,6 +3,7 @@ using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; +using PolyMod.Android; using PolyMod.Managers; using UnityEngine; @@ -135,6 +136,7 @@ public override void Load() Main.Init(); Multiplayer.Client.Init(); + AndroidHandler.Init(); } /// From b70e321ca58a55a4f86ef116237c8391bb58cad6 Mon Sep 17 00:00:00 2001 From: Demonofpower Date: Thu, 9 Jul 2026 22:08:53 +0200 Subject: [PATCH 15/34] added default values to class vars to fix pipeline --- src/Multiplayer/ViewModels/SetupGameDataViewModel.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Multiplayer/ViewModels/SetupGameDataViewModel.cs b/src/Multiplayer/ViewModels/SetupGameDataViewModel.cs index 68f43f1..41274bd 100644 --- a/src/Multiplayer/ViewModels/SetupGameDataViewModel.cs +++ b/src/Multiplayer/ViewModels/SetupGameDataViewModel.cs @@ -2,9 +2,9 @@ namespace PolyMod.Multiplayer.ViewModels; public class SetupGameDataViewModel : IMonoServerResponseData { - public string lobbyId { get; set; } + public string lobbyId { get; set; } = string.Empty; - public byte[] serializedGameState { get; set; } + public byte[] serializedGameState { get; set; } = Array.Empty(); - public string gameSettingsJson { get; set; } + public string gameSettingsJson { get; set; } = string.Empty; } \ No newline at end of file From 19ac56e6b633b3727be8c533ebac7acde2b863a0 Mon Sep 17 00:00:00 2001 From: Demonofpower Date: Thu, 9 Jul 2026 22:19:09 +0200 Subject: [PATCH 16/34] more firebase fixes --- src/Android/AndroidHandler.cs | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/src/Android/AndroidHandler.cs b/src/Android/AndroidHandler.cs index f8a9c7f..18a8d9f 100644 --- a/src/Android/AndroidHandler.cs +++ b/src/Android/AndroidHandler.cs @@ -63,13 +63,27 @@ private static bool AnalyticsManager_IsAnalyticsEnabled(ref bool __result) /// /// On android Firebase cannot initialize inside the launcher process (its config lives in the game APK's resources, and the native lib may be unreachable there). - /// We try to skip Firebase completely. + /// We try to skip Firebase completely. isFirebaseInitialized deliberately stays false: + /// pretending Firebase is up could wake isFirebaseInitialized-guarded code paths + /// (e.g. HandleOpenedThroughNotification on every app resume). /// [HarmonyPrefix] [HarmonyPatch(typeof(FirebaseMessagingManager), nameof(FirebaseMessagingManager.Init))] - private static bool FirebaseMessagingManager_Init(FirebaseMessagingManager __instance) + private static bool FirebaseMessagingManager_Init() + { + return false; + } + + /// + /// RequestPushNotificationPermissions (the push-notification row in LoginDetails) calls + /// InitAsync directly, bypassing Init — with isFirebaseInitialized kept false that would + /// still reach Firebase, so hand back a completed task instead. + /// + [HarmonyPrefix] + [HarmonyPatch(typeof(FirebaseMessagingManager), nameof(FirebaseMessagingManager.InitAsync))] + private static bool FirebaseMessagingManager_InitAsync(ref Il2CppSystem.Threading.Tasks.Task __result) { - __instance.isFirebaseInitialized = true; + __result = Il2CppSystem.Threading.Tasks.Task.CompletedTask; return false; } } \ No newline at end of file From a76a1031f04a06394f63f58a23274603e56261d8 Mon Sep 17 00:00:00 2001 From: Demonofpower Date: Thu, 9 Jul 2026 23:05:24 +0200 Subject: [PATCH 17/34] bumped version to see in logs on android --- PolyMod.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PolyMod.csproj b/PolyMod.csproj index 2c165a4..783658f 100644 --- a/PolyMod.csproj +++ b/PolyMod.csproj @@ -11,7 +11,7 @@ IL2CPP PolyMod - 1.3.0-pre + 1.3.0-pre-android-1 2.17.2.16299 PolyModdingTeam The Battle of Polytopia's mod loader. From f43a1c82dcf00ed57cb3878153275cea3f960c30 Mon Sep 17 00:00:00 2001 From: Demonofpower Date: Wed, 22 Jul 2026 22:17:42 +0200 Subject: [PATCH 18/34] multiplayer refactoring + added client only helper --- src/Managers/Compatibility.cs | 9 ++ src/Multiplayer/ModMultiplayer.cs | 228 ++++++++++++++++++++++++++++++ src/Multiplayer/Multiplayer.cs | 197 +------------------------- src/Plugin.cs | 2 + 4 files changed, 241 insertions(+), 195 deletions(-) create mode 100644 src/Multiplayer/ModMultiplayer.cs diff --git a/src/Managers/Compatibility.cs b/src/Managers/Compatibility.cs index 1d55bb7..9359df3 100644 --- a/src/Managers/Compatibility.cs +++ b/src/Managers/Compatibility.cs @@ -21,6 +21,15 @@ internal static class Compatibility internal static bool shouldResetSettings = false; private static bool sawSignatureWarning; + /// + /// Whether all loaded mods are client only. If at least one non client only mod exists this returns false. + /// + /// + public static bool IsClientOnly() + { + return Registry.mods.Select(modPair => modPair.Value).All(mod => mod.client); + } + /// /// Hashes the signatures of all loaded mods to create a checksum. /// diff --git a/src/Multiplayer/ModMultiplayer.cs b/src/Multiplayer/ModMultiplayer.cs new file mode 100644 index 0000000..67fe011 --- /dev/null +++ b/src/Multiplayer/ModMultiplayer.cs @@ -0,0 +1,228 @@ +using HarmonyLib; +using Il2CppMicrosoft.AspNetCore.SignalR.Client; +using Newtonsoft.Json; +using PolyMod.Managers; +using PolyMod.Multiplayer.ViewModels; +using Polytopia.Data; +using PolytopiaBackendBase; +using PolytopiaBackendBase.Common; +using PolytopiaBackendBase.Game; +using PolytopiaBackendBase.Game.BindingModels; +using UnityEngine; + +namespace PolyMod.Multiplayer; + +public class ModMultiplayer +{ + internal static void Init() + { + if (Compatibility.IsClientOnly()) + { + Plugin.logger?.LogInfo($"All loaded mods are client only. Skipping modded multiplayer initialization."); + + return; + } + + Plugin.logger?.LogInfo($"Starting modded multiplayer initialization."); + + Harmony.CreateAndPatchAll(typeof(ModMultiplayer)); + + + Plugin.logger?.LogInfo($"Finished modded multiplayer initialization."); + } + + [HarmonyPrefix] + [HarmonyPatch(typeof(BackendAdapter), nameof(BackendAdapter.StartLobbyGame))] + private static bool BackendAdapter_StartLobbyGame_Modded( + ref Il2CppSystem.Threading.Tasks.Task> __result, + BackendAdapter __instance, + StartLobbyBindingModel model) + { + // On Android, let the game's original StartLobbyGame handle it + if (Application.platform == RuntimePlatform.Android) 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 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 Settings 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>( + "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)); + } +} \ No newline at end of file diff --git a/src/Multiplayer/Multiplayer.cs b/src/Multiplayer/Multiplayer.cs index 2058aa0..1520742 100644 --- a/src/Multiplayer/Multiplayer.cs +++ b/src/Multiplayer/Multiplayer.cs @@ -84,6 +84,7 @@ public static void SteamClient_get_SteamId(ref string __result) /// [HarmonyPostfix] [HarmonyPatch(typeof(GameState), nameof(GameState.Deserialize))] + [Obsolete("This will be succeeded by ModMultiplayer in the future.")] private static void Deserialize_Postfix(GameState __instance, BinaryReader __0) { if(!allowGldMods) return; @@ -171,6 +172,7 @@ private static void Deserialize_Postfix(GameState __instance, BinaryReader __0) /// /// Fetch GLD from server using ModGldVersion ID /// + [Obsolete("This will be succeeded by ModMultiplayer in the future.")] private static string? FetchGldById(int modGldVersion) { if(!allowGldMods) return null; @@ -203,199 +205,4 @@ private static void Deserialize_Postfix(GameState __instance, BinaryReader __0) } return null; } - - [HarmonyPrefix] - [HarmonyPatch(typeof(BackendAdapter), nameof(BackendAdapter.StartLobbyGame))] - private static bool BackendAdapter_StartLobbyGame_Modded( - ref Il2CppSystem.Threading.Tasks.Task> __result, - BackendAdapter __instance, - StartLobbyBindingModel model) - { - // On Android, let the game's original StartLobbyGame handle it - if (Application.platform == RuntimePlatform.Android) 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 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 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>( - "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/Plugin.cs b/src/Plugin.cs index 25a496c..fc5ceb6 100644 --- a/src/Plugin.cs +++ b/src/Plugin.cs @@ -5,6 +5,7 @@ using BepInEx.Logging; using PolyMod.Android; using PolyMod.Managers; +using PolyMod.Multiplayer; using UnityEngine; namespace PolyMod; @@ -136,6 +137,7 @@ public override void Load() Main.Init(); Multiplayer.Client.Init(); + ModMultiplayer.Init(); AndroidHandler.Init(); } From 1d09078f20010da2467904aa20c78c7c532a853b Mon Sep 17 00:00:00 2001 From: Demonofpower Date: Sat, 8 Aug 2026 21:00:09 +0200 Subject: [PATCH 19/34] linux crash fix --- src/Plugin.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Plugin.cs b/src/Plugin.cs index fc5ceb6..b9fcd22 100644 --- a/src/Plugin.cs +++ b/src/Plugin.cs @@ -170,8 +170,9 @@ internal static void UpdateConsole() { ConsoleManager.CreateConsole(); } - else + else if (OperatingSystem.IsWindows()) { + // BepInEx's Unix console driver throws unsupported on detach. Off-Windows there is no separate console window, so there is nothing to detach. ConsoleManager.DetachConsole(); } } From 0fbafab1c36f810187d35e9742736a82c061dc11 Mon Sep 17 00:00:00 2001 From: Demonofpower Date: Sat, 8 Aug 2026 21:16:12 +0200 Subject: [PATCH 20/34] added dystopia server changer --- resources/dystopia_icon.png | Bin 0 -> 95320 bytes src/Managers/Visual.cs | 94 ++++++++++++++- src/Multiplayer/Dystopia.cs | 234 ++++++++++++++++++++++++++++++++++++ src/Plugin.cs | 3 +- 4 files changed, 328 insertions(+), 3 deletions(-) create mode 100644 resources/dystopia_icon.png create mode 100644 src/Multiplayer/Dystopia.cs diff --git a/resources/dystopia_icon.png b/resources/dystopia_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..512162a33c13f239f257b02c92620e45f3cc2da8 GIT binary patch literal 95320 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 Wh&2oZH75e#m%Ox!RE>mL=>Gw2fBiZD literal 0 HcmV?d00001 diff --git a/src/Managers/Visual.cs b/src/Managers/Visual.cs index bccc5fc..d786dec 100644 --- a/src/Managers/Visual.cs +++ b/src/Managers/Visual.cs @@ -55,6 +55,10 @@ public record SkinInfo(int idx, string id, SkinData? skinData); /// A dictionary of custom widths for basic popups. public static Dictionary basicPopupWidths = new(); + /// Original font sizes of popup buttons, so relayouts rescale from the prefab size. + private static readonly Dictionary popupButtonFontSizes = new(); + /// Original scroll viewport bottom offsets of popups, so relayouts don't compound. + private static readonly Dictionary popupScrollBottoms = new(); /// Represents information about a unit prefab. public struct UnitPrefabInfo { @@ -778,7 +782,86 @@ private static void UpdateWidth(PopupBase __instance) { int id = __instance.GetInstanceID(); if (basicPopupWidths.ContainsKey(id)) - __instance.rectTransform.SetWidth(basicPopupWidths[id]); + { + float maxWidth = UIManager.GetUIWidth() - 40f; + float width = Mathf.Min(basicPopupWidths[id], maxWidth); + __instance.rectTransform.SetWidth(width); + LayoutPopupButtons(__instance, width); + } + } + + private const float BUTTON_GAP = 10f; + private const float BUTTON_ROW_PADDING = 40f; + + private static void LayoutPopupButtons(PopupBase popup, float popupWidth) + { + var legacy = popup.TryCast(); + if (legacy == null || legacy.buttonContainer == null) return; + var buttons = legacy.buttonContainer.Buttons; + if (buttons == null || buttons.Length < 2) return; + + float rowWidth = popupWidth - BUTTON_ROW_PADDING; + + foreach (UITextButton button in buttons) + { + int id = button.GetInstanceID(); + if (!popupButtonFontSizes.ContainsKey(id)) + popupButtonFontSizes[id] = button.FontSize; + else + button.FontSize = popupButtonFontSizes[id]; + button.UpdateSize(); + float width = button.rectTransform.GetWidth(); + if (width > rowWidth) + { + button.FontSize *= rowWidth / width; + button.UpdateSize(); + } + } + + List> rows = new(); + float cursor = 0f; + foreach (UITextButton button in buttons) + { + float width = button.rectTransform.GetWidth(); + if (rows.Count == 0 || cursor + width > rowWidth) + { + rows.Add(new()); + cursor = 0f; + } + rows[^1].Add(button); + cursor += width + BUTTON_GAP; + } + + float rowHeight = buttons[0].rectTransform.GetHeight() + BUTTON_GAP; + for (int r = 0; r < rows.Count; r++) + { + float total = -BUTTON_GAP; + foreach (UITextButton button in rows[r]) + total += button.rectTransform.GetWidth() + BUTTON_GAP; + float x = -total / 2f; + float y = (rows.Count - 1 - r) * rowHeight; + foreach (UITextButton button in rows[r]) + { + button.rectTransform.anchorMin = new Vector2(0.5f, 0.5f); + button.rectTransform.anchorMax = new Vector2(0.5f, 0.5f); + button.rectTransform.pivot = new Vector2(0f, 0.5f); + button.rectTransform.anchoredPosition = new Vector2(x, y); + x += button.rectTransform.GetWidth() + BUTTON_GAP; + } + } + + float extra = (rows.Count - 1) * rowHeight; + float maxHeight = (ScreenManager.SafeHeight - 20f) * UICanvasScalerHelper.GetInvertedUIScale(); + popup.rectTransform.SetHeight(Mathf.Min(popup.rectTransform.GetHeight() + extra, maxHeight)); + + if (popup.scrollRect != null) + { + var viewport = popup.scrollRect.GetComponent(); + int popupId = popup.GetInstanceID(); + if (!popupScrollBottoms.ContainsKey(popupId)) + popupScrollBottoms[popupId] = viewport.offsetMin.y; + viewport.offsetMin = new Vector2(viewport.offsetMin.x, popupScrollBottoms[popupId] + extra); + } } /// Sets the attacker's tribe before a unit attacks. @@ -809,12 +892,19 @@ private static void WeaponGFX_SetSkin(WeaponGFX __instance, SkinType skinType) } } - /// Removes a popup's custom width when it is hidden. + /// Removes a popup's custom width and cached button font sizes when it is hidden. [HarmonyPostfix] [HarmonyPatch(typeof(PopupBase), nameof(PopupBase.Hide))] private static void PopupBase_Hide(PopupBase __instance) { basicPopupWidths.Remove(__instance.GetInstanceID()); + popupScrollBottoms.Remove(__instance.GetInstanceID()); + var legacy = __instance.TryCast(); + if (legacy != null && legacy.buttonContainer != null && legacy.buttonContainer.Buttons != null) + { + foreach (UITextButton button in legacy.buttonContainer.Buttons) + popupButtonFontSizes.Remove(button.GetInstanceID()); + } } [HarmonyPrefix] diff --git a/src/Multiplayer/Dystopia.cs b/src/Multiplayer/Dystopia.cs new file mode 100644 index 0000000..9848f3d --- /dev/null +++ b/src/Multiplayer/Dystopia.cs @@ -0,0 +1,234 @@ +using Cpp2IL.Core.Extensions; +using HarmonyLib; +using Il2CppInterop.Runtime; +using PolyMod.Managers; +using PolytopiaBackendBase; +using TMPro; +using UnityEngine; + +namespace PolyMod.Multiplayer; + +/// +/// Adds a "Dystopia" start-screen button with a popup to switch the backend server at runtime. +/// +public static class Dystopia +{ + private const int POPUP_WIDTH = 1400; + + internal const string OFFICIAL_SERVER_URL = "https://polytopia-prod.net/"; + + private record ServerEntry(string name, string url, bool official = false); + + private static readonly ServerEntry[] SERVERS = + { + new("Official", OFFICIAL_SERVER_URL, official: true), + new("Dystopia", "https://polydystopia.xyz"), + new("Dystopia Dev", "https://dev.polydystopia.xyz"), + }; + + /// + /// Default backend: the official server, except on Android + /// + internal static string DefaultServerUrl() + { + return Application.platform == RuntimePlatform.Android + ? Client.DEFAULT_SERVER_URL + : OFFICIAL_SERVER_URL; + } + + private static UIRoundButton_UI2? dystopiaButton = null; + + internal static void Init() + { + Harmony.CreateAndPatchAll(typeof(Dystopia)); + } + + [HarmonyPostfix] + [HarmonyPatch(typeof(StartScreen_UI2), nameof(StartScreen_UI2.Init))] + private static void StartScreen_UI2_Init(StartScreen_UI2 __instance, RectTransform transform) + { + if (dystopiaButton != null) + { + UnityEngine.Object.Destroy(dystopiaButton.gameObject); + } + dystopiaButton = UILibrary.NewRoundButton(transform).SetStyle(UIButtonBase_UI2.ButtonStyle.Suggested); + dystopiaButton.bg.sprite = Visual.BuildSprite(Plugin.GetResource("dystopia_icon.png").ReadBytes()); + dystopiaButton.OnClickedSignal.Add(DelegateSupport.ConvertDelegate(ShowDystopiaPopup)); + } + + [HarmonyPostfix] + [HarmonyPatch(typeof(StartScreen_UI2), nameof(StartScreen_UI2.RunLayout))] + private static void StartScreen_UI2_RunLayout(StartScreen_UI2 __instance, ScreenBase_UI2.ScreenSize screenSize) + { + if (dystopiaButton == null) + { + Plugin.logger.LogWarning("Dystopia button is null when running layout!"); + return; + } + dystopiaButton.iconContainer.gameObject.SetActive(false); + dystopiaButton.outline.gameObject.SetActive(false); + dystopiaButton.bg.color = Color.white; + dystopiaButton.Text = "Dystopia"; + float num = 50f; + + dystopiaButton.SetPosition(screenSize.safeRect.Right - (num * 4.0f), screenSize.safeRect.Top - num); + } + + internal static void ShowDystopiaPopup() + { + string current = Normalize(Plugin.config.backendUrl); + + BasicPopupLegacy popup = Visual.GetBasicPopupLegacy(); + popup.Header = "Dystopia"; + popup.Description = $"Current server:\n{Plugin.config.backendUrl}"; + + List buttons = new() + { + new("buttons.back"), + }; + foreach (ServerEntry server in SERVERS) + { + bool isActive = Normalize(server.url) == current; + // The official server rejects our DeviceId login, so it stays greyed out on Android. + bool isDisabled = isActive || (server.official && Application.platform == RuntimePlatform.Android); + string url = server.url; + buttons.Add(new( + isActive ? server.name + " (active)" : server.name, + isDisabled ? PopupBase.PopupButtonData.States.Disabled : PopupBase.PopupButtonData.States.None, + DelegateSupport.ConvertDelegate((System.Action)(() => SwitchServer(url))) + )); + } + buttons.Add(new( + "Custom...", + callback: DelegateSupport.ConvertDelegate(ShowCustomServerPopup) + )); + popup.buttonData = buttons.ToArray(); + popup.ShowSetWidth(POPUP_WIDTH); + } + + private static void ShowCustomServerPopup() + { + SearchFriendCodePopup popup = PopupManager.GetPopup("dystopiaCustomServerPopup"); + TMP_InputField input = popup.inputfield; + + input.onSubmit = new TMP_InputField.SubmitEvent(); + input.onEndEdit = new TMP_InputField.SubmitEvent(); + input.onValueChanged = new TMP_InputField.OnChangeEvent(); + input.onSelect = new TMP_InputField.SelectionEvent(); + input.onDeselect = new TMP_InputField.SelectionEvent(); + input.contentType = TMP_InputField.ContentType.Standard; + input.characterLimit = 200; + + var placeholder = input.placeholder != null ? input.placeholder.TryCast() : null; + if (placeholder != null) + { + placeholder.text = "Server URL or IP"; + } + + void OnConnect() + { + string url = input.text?.Trim() ?? ""; + if (url.Length == 0) + { + return; + } + if (!url.StartsWith("http://") && !url.StartsWith("https://")) + { + url = "https://" + url; + } + if (!System.Uri.TryCreate(url, System.UriKind.Absolute, out _)) + { + NotificationManager.Notify("Invalid server URL"); + return; + } + popup.Hide(); + SwitchServer(url); + } + + popup.Show(); + popup.Header = "Custom server"; + popup.Description = "Enter the server URL or IP:"; + popup.buttonContainer.ResetContainer(); + popup.buttonData = new PopupBase.PopupButtonData[] + { + new("buttons.back"), + new( + "Connect", + callback: DelegateSupport.ConvertDelegate(OnConnect), + closesPopup: false + ), + }; + input.SetTextWithoutNotify(Plugin.config.backendUrl); + } + + private static async void SwitchServer(string url) + { + Plugin.logger.LogInfo($"Dystopia> Switching server to {url}"); + Plugin.config = Plugin.config with { backendUrl = url }; + Plugin.WriteConfig(); + + PolytopiaBackendAdapter adapter = PolytopiaBackendAdapter.Instance; + + await adapter.CloseConnection(); + + BuildConfig buildConfig = BuildConfigHelper.GetSelectedBuildConfig(); + buildConfig.buildServerURL = BuildServerURL.Custom; + buildConfig.customServerURL = url; + + adapter.UseBackendUri(new Il2CppSystem.Uri(url)); + adapter.UseHttpClient(); + + PurgeServerCaches(); + + adapter.ConnectionStatus = ConnectionStatus.None; + BackendEvents.BackendConnectionChanged(ConnectionStatus.Disconnected, false); + + GameManager.GetLoginManager().Login(false, true); + Plugin.logger.LogInfo($"Dystopia> Reconnect to {url} initiated"); + } + + private static void PurgeServerCaches() + { + try + { + var remote = GameManager.GetRemoteGameDataManager(); + remote.gameDataCache.Clear(); + remote.matchmakingGameDataCache?.Clear(); + remote.gameIdCache.Clear(); + remote.hasLoadedGameDataCache = false; + + var lobbies = GameManager.GetLobbyManager(); + lobbies.cachedLobbies.Clear(); + lobbies.hasCachedLobbies = false; + + AccountManager.currentPlayerData = null; + AccountManager.friends = null; + AccountManager.friendViewModels = null; + AccountManager.playersStatuses?.Clear(); + AccountManager.ClearCachedPlayerId(); + + DeleteIfExists(Paths.GetUserProfileCachePath()); + DeleteIfExists(Paths.GetStartupDataPath()); + + GameManager.GetWeeklyChallengeModel().ClearData(); + GameManager.ActionableGamesCount = 0; + } + catch (System.Exception e) + { + Plugin.logger.LogWarning($"Dystopia> Failed to purge some server caches: {e}"); + } + } + + private static void DeleteIfExists(string path) + { + if (File.Exists(path)) + { + File.Delete(path); + } + } + + private static string Normalize(string url) + { + return url.TrimEnd('/').ToLowerInvariant(); + } +} diff --git a/src/Plugin.cs b/src/Plugin.cs index b9fcd22..148e2ae 100644 --- a/src/Plugin.cs +++ b/src/Plugin.cs @@ -119,7 +119,7 @@ public override void Load() } catch { - config = new(); + config = new(backendUrl: Multiplayer.Dystopia.DefaultServerUrl()); } WriteConfig(); UpdateConsole(); @@ -138,6 +138,7 @@ public override void Load() Main.Init(); Multiplayer.Client.Init(); ModMultiplayer.Init(); + Multiplayer.Dystopia.Init(); AndroidHandler.Init(); } From 459b574fc16656323cbb14ea2ca106287fbfdb3a Mon Sep 17 00:00:00 2001 From: Demonofpower Date: Sat, 8 Aug 2026 21:17:38 +0200 Subject: [PATCH 21/34] minor refactoring --- src/Plugin.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Plugin.cs b/src/Plugin.cs index 148e2ae..ee514f8 100644 --- a/src/Plugin.cs +++ b/src/Plugin.cs @@ -136,9 +136,9 @@ public override void Load() Hub.Init(); Main.Init(); - Multiplayer.Client.Init(); + Client.Init(); ModMultiplayer.Init(); - Multiplayer.Dystopia.Init(); + Dystopia.Init(); AndroidHandler.Init(); } From 2b78e8dc3773c60a3f12bf714725c1371a3547f1 Mon Sep 17 00:00:00 2001 From: Demonofpower Date: Sat, 8 Aug 2026 22:29:33 +0200 Subject: [PATCH 22/34] added mod lobby creation flags in createlobby backend call --- src/Multiplayer/ModMultiplayer.cs | 44 +++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/src/Multiplayer/ModMultiplayer.cs b/src/Multiplayer/ModMultiplayer.cs index 67fe011..115c09f 100644 --- a/src/Multiplayer/ModMultiplayer.cs +++ b/src/Multiplayer/ModMultiplayer.cs @@ -1,6 +1,7 @@ using HarmonyLib; using Il2CppMicrosoft.AspNetCore.SignalR.Client; using Newtonsoft.Json; +using Newtonsoft.Json.Linq; using PolyMod.Managers; using PolyMod.Multiplayer.ViewModels; using Polytopia.Data; @@ -31,6 +32,49 @@ internal static void Init() Plugin.logger?.LogInfo($"Finished modded multiplayer initialization."); } + [HarmonyPrefix] + [HarmonyPatch(typeof(BackendAdapter), nameof(BackendAdapter.CreateLobby))] + private static bool BackendAdapter_CreateLobby( + ref Il2CppSystem.Threading.Tasks.Task> __result, + BackendAdapter __instance, + CreateLobbyBindingModel model) + { + Plugin.logger.LogInfo("Multiplayer> BackendAdapter_CreateLobby"); + var taskCompletionSource = new Il2CppSystem.Threading.Tasks.TaskCompletionSource>(); + + _ = HandleCreateLobbyModded(taskCompletionSource, __instance, model); + + __result = taskCompletionSource.Task; + + return false; + } + + private static async System.Threading.Tasks.Task HandleCreateLobbyModded( + Il2CppSystem.Threading.Tasks.TaskCompletionSource> tcs, + BackendAdapter instance, + CreateLobbyBindingModel model) + { + try + { + var payload = JObject.FromObject(model); + payload["IsModded"] = new JValue(true); + payload["Checksum"] = new JValue(Compatibility.checksum); + + var serverResponse = await instance.HubConnection.InvokeAsync>( + "CreateLobby", + payload, + Il2CppSystem.Threading.CancellationToken.None + ); + Plugin.logger.LogInfo("Multiplayer> Invoked CreateLobby with mod info"); + tcs.SetResult(serverResponse); + } + catch (Exception ex) + { + Plugin.logger.LogError("Multiplayer> Error during HandleCreateLobbyModded: " + ex.Message); + tcs.SetException(new Il2CppSystem.Exception(ex.Message)); + } + } + [HarmonyPrefix] [HarmonyPatch(typeof(BackendAdapter), nameof(BackendAdapter.StartLobbyGame))] private static bool BackendAdapter_StartLobbyGame_Modded( From dd7ee10fef15046b6678ad08aed03dec0b7c46e6 Mon Sep 17 00:00:00 2001 From: Demonofpower Date: Mon, 10 Aug 2026 12:14:10 +0200 Subject: [PATCH 23/34] modded multiplayer?? --- src/Managers/Compatibility.cs | 3 +- src/Multiplayer/Dystopia.cs | 18 +- src/Multiplayer/ModMultiplayer.cs | 100 ++- src/Multiplayer/ModdedClient.cs | 616 ++++++++++++++++++ src/Multiplayer/SerializationUtils.cs | 42 ++ .../ViewModels/ModdedGameStateViewModel.cs | 33 + .../ViewModels/SetupGameDataViewModel.cs | 8 +- src/Plugin.cs | 2 +- 8 files changed, 798 insertions(+), 24 deletions(-) create mode 100644 src/Multiplayer/ModdedClient.cs create mode 100644 src/Multiplayer/SerializationUtils.cs create mode 100644 src/Multiplayer/ViewModels/ModdedGameStateViewModel.cs diff --git a/src/Managers/Compatibility.cs b/src/Managers/Compatibility.cs index 9359df3..d2cde96 100644 --- a/src/Managers/Compatibility.cs +++ b/src/Managers/Compatibility.cs @@ -27,7 +27,8 @@ internal static class Compatibility /// public static bool IsClientOnly() { - return Registry.mods.Select(modPair => modPair.Value).All(mod => mod.client); + return Registry.mods.Select(modPair => modPair.Value) + .All(mod => mod.client || mod.id == "polytopia" || mod.status != Mod.Status.Success); } /// diff --git a/src/Multiplayer/Dystopia.cs b/src/Multiplayer/Dystopia.cs index 9848f3d..3bf7811 100644 --- a/src/Multiplayer/Dystopia.cs +++ b/src/Multiplayer/Dystopia.cs @@ -17,25 +17,16 @@ public static class Dystopia internal const string OFFICIAL_SERVER_URL = "https://polytopia-prod.net/"; - private record ServerEntry(string name, string url, bool official = false); + private record ServerEntry(string name, string url, bool official = false, bool disabled = false); private static readonly ServerEntry[] SERVERS = { new("Official", OFFICIAL_SERVER_URL, official: true), - new("Dystopia", "https://polydystopia.xyz"), + // Disabled until the production Dystopia server goes online. + new("Dystopia", "https://polydystopia.xyz", disabled: true), new("Dystopia Dev", "https://dev.polydystopia.xyz"), }; - /// - /// Default backend: the official server, except on Android - /// - internal static string DefaultServerUrl() - { - return Application.platform == RuntimePlatform.Android - ? Client.DEFAULT_SERVER_URL - : OFFICIAL_SERVER_URL; - } - private static UIRoundButton_UI2? dystopiaButton = null; internal static void Init() @@ -89,8 +80,7 @@ internal static void ShowDystopiaPopup() foreach (ServerEntry server in SERVERS) { bool isActive = Normalize(server.url) == current; - // The official server rejects our DeviceId login, so it stays greyed out on Android. - bool isDisabled = isActive || (server.official && Application.platform == RuntimePlatform.Android); + bool isDisabled = isActive || server.official || server.disabled; string url = server.url; buttons.Add(new( isActive ? server.name + " (active)" : server.name, diff --git a/src/Multiplayer/ModMultiplayer.cs b/src/Multiplayer/ModMultiplayer.cs index 115c09f..05d6deb 100644 --- a/src/Multiplayer/ModMultiplayer.cs +++ b/src/Multiplayer/ModMultiplayer.cs @@ -27,7 +27,8 @@ internal static void Init() Plugin.logger?.LogInfo($"Starting modded multiplayer initialization."); Harmony.CreateAndPatchAll(typeof(ModMultiplayer)); - + SerializationUtils.Init(); + ModdedClient.Init(); Plugin.logger?.LogInfo($"Finished modded multiplayer initialization."); } @@ -82,9 +83,6 @@ private static bool BackendAdapter_StartLobbyGame_Modded( BackendAdapter __instance, StartLobbyBindingModel model) { - // On Android, let the game's original StartLobbyGame handle it - if (Application.platform == RuntimePlatform.Android) return true; - Plugin.logger.LogInfo("Multiplayer> BackendAdapter_StartLobbyGame_Modded"); var taskCompletionSource = new Il2CppSystem.Threading.Tasks.TaskCompletionSource>(); @@ -119,11 +117,29 @@ private static async System.Threading.Tasks.Task HandleStartLobbyGameModded( Plugin.logger.LogInfo("Multiplayer> GameState and Settings created"); + var serializedGameSummary = Array.Empty(); + var initialCommandCount = -1; + string? currentPlayerId = null; + if (GameStateSummary.FromGameStateByteArray(serializedGameState, out GameStateSummary stateSummary, + out GameState initialState)) + { + serializedGameSummary = SerializationHelpers.ToByteArray(stateSummary, initialState.Version); + initialCommandCount = initialState.CommandStack.Count; + currentPlayerId = GameStateUtils.GetCurrentPlayerAccountId(initialState).ToString(); + if (currentPlayerId == "00000000-0000-0000-0000-000000000000") + { + currentPlayerId = null; + } + } + var setupGameDataViewModel = new SetupGameDataViewModel { lobbyId = lobbyGameViewModel.Id.ToString(), serializedGameState = serializedGameState, - gameSettingsJson = gameSettingsJson + serializedGameSummary = serializedGameSummary, + gameSettingsJson = gameSettingsJson, + initialCommandCount = initialCommandCount, + currentPlayerId = currentPlayerId }; var setupData = System.Text.Json.JsonSerializer.Serialize(setupGameDataViewModel); @@ -134,6 +150,13 @@ private static async System.Threading.Tasks.Task HandleStartLobbyGameModded( Il2CppSystem.Threading.CancellationToken.None ); Plugin.logger.LogInfo("Multiplayer> Invoked StartLobbyGameModded"); + + if (serverResponse != null && serverResponse.Success) + { + ModdedClient.RegisterModdedGame(lobbyGameViewModel.Id.ToString(), Compatibility.checksum); + ModdedClient.SetShadowState(lobbyGameViewModel.Id.ToString(), serializedGameState); + } + tcs.SetResult(serverResponse); } catch (Exception ex) @@ -143,6 +166,64 @@ private static async System.Threading.Tasks.Task HandleStartLobbyGameModded( } } + /// + /// Joining a modded lobby must carry the local mod checksum so the server can block mismatched mod sets before they corrupt a game. + /// + [HarmonyPrefix] + [HarmonyPatch(typeof(BackendAdapter), nameof(BackendAdapter.RespondToLobbyInvitation))] + private static bool BackendAdapter_RespondToLobbyInvitation( + ref Il2CppSystem.Threading.Tasks.Task> __result, + BackendAdapter __instance, + RespondToLobbyInvitation model) + { + Plugin.logger.LogInfo("Multiplayer> BackendAdapter_RespondToLobbyInvitation"); + var taskCompletionSource = new Il2CppSystem.Threading.Tasks.TaskCompletionSource>(); + + _ = HandleRespondToLobbyInvitationModded(taskCompletionSource, __instance, model); + + __result = taskCompletionSource.Task; + + return false; + } + + private static async System.Threading.Tasks.Task HandleRespondToLobbyInvitationModded( + Il2CppSystem.Threading.Tasks.TaskCompletionSource> tcs, + BackendAdapter instance, + RespondToLobbyInvitation model) + { + try + { + var payload = JObject.FromObject(model); + payload["Checksum"] = new JValue(Compatibility.checksum); + + var serverResponse = await instance.HubConnection.InvokeAsync>( + "RespondToLobbyInvitation", + payload, + Il2CppSystem.Threading.CancellationToken.None + ); + + if (serverResponse != null && !serverResponse.Success && + serverResponse.ErrorCode == ErrorCode.StateProhibitsOperation) + { + Plugin.logger.LogWarning("Multiplayer> Lobby join blocked: mod set mismatch"); + PopupManager.GetBasicPopupWithData(new( + Localization.Get("polymod.signature.mismatch"), + Localization.Get("polymod.signature.incompatible"), + new(new PopupBase.PopupButtonData[] { + new("OK") + }) + )).Show(); + } + + tcs.SetResult(serverResponse); + } + catch (Exception ex) + { + Plugin.logger.LogError("Multiplayer> Error during HandleRespondToLobbyInvitationModded: " + ex.Message); + tcs.SetException(new Il2CppSystem.Exception(ex.Message)); + } + } + public static (byte[] serializedGameState, string gameSettingsJson) CreateMultiplayerGame(LobbyGameViewModel lobby, int gameVersion, int gameLogicVersion) { @@ -155,13 +236,16 @@ public static (byte[] serializedGameState, string gameSettingsJson) CreateMultip } foreach (var participatorViewModel in lobby.Participators) { + if (participatorViewModel.InvitationState != PlayerInvitationState.Accepted) continue; + + var tribe = (TribeType)participatorViewModel.SelectedTribe; var humanPlayer = new PlayerData { type = PlayerDataType.LocalUser, state = PlayerDataFriendshipState.Accepted, knownTribe = true, - tribe = (TribeType)participatorViewModel.SelectedTribe, - tribeMix = (TribeType)participatorViewModel.SelectedTribe, + tribe = tribe, + tribeMix = (int)tribe < byte.MaxValue ? tribe : TribeType.None, skinType = (SkinType)participatorViewModel.SelectedTribeSkin, defaultName = participatorViewModel.GetNameInternal() }; @@ -264,6 +348,8 @@ public static (byte[] serializedGameState, string gameSettingsJson) CreateMultip gameState.CommandStack.Add((CommandBase)new StartMatchCommand((byte)1)); + new ActionManager(gameState).Update(); + var serializedGameState = SerializationHelpers.ToByteArray(gameState, gameState.Version); return (serializedGameState, diff --git a/src/Multiplayer/ModdedClient.cs b/src/Multiplayer/ModdedClient.cs new file mode 100644 index 0000000..8120104 --- /dev/null +++ b/src/Multiplayer/ModdedClient.cs @@ -0,0 +1,616 @@ +using HarmonyLib; +using Il2CppMicrosoft.AspNetCore.SignalR.Client; +using PolyMod.Managers; +using PolyMod.Multiplayer.ViewModels; +using Polytopia.Data; +using PolytopiaBackendBase; +using PolytopiaBackendBase.Game; +using PolytopiaBackendBase.Game.BindingModels; +using UnityEngine; + +namespace PolyMod.Multiplayer; + +/// +/// Client-authoritative command flow for modded games. +/// The server never evaluates modded game state. The acting client executes commands (plus auto-play follow-up turns) the way the vanilla server would, on a shadow copy of the authoritative state and uploads the result via UpdateGameStateModded. Other clients receive the commands through the vanilla OnCommand relay. +/// +public static class ModdedClient +{ + private static readonly HashSet _moddedGameIds = new(); + private static readonly Dictionary _shadowStates = new(); + private static readonly SemaphoreSlim _sendLock = new(1, 1); + private const string EmptyGuid = "00000000-0000-0000-0000-000000000000"; + + internal static void Init() + { + Harmony.CreateAndPatchAll(typeof(ModdedClient)); + } + + internal static void RegisterModdedGame(string gameId, string? checksum) + { + lock (_moddedGameIds) + { + _moddedGameIds.Add(gameId.ToLowerInvariant()); + } + + try + { + File.WriteAllText(SignaturesPath(gameId), checksum ?? Compatibility.checksum); + } + catch (Exception e) + { + Plugin.logger.LogWarning($"Multiplayer> Could not write signatures for {gameId}: {e.Message}"); + } + } + + internal static bool IsModdedGame(string gameId) + { + lock (_moddedGameIds) + { + if (_moddedGameIds.Contains(gameId.ToLowerInvariant())) return true; + } + + return File.Exists(SignaturesPath(gameId)); + } + + internal static void SetShadowState(string gameId, byte[] stateBytes) + { + lock (_shadowStates) + { + _shadowStates[gameId.ToLowerInvariant()] = stateBytes; + } + } + + private static byte[]? GetShadowState(string gameId) + { + lock (_shadowStates) + { + return _shadowStates.TryGetValue(gameId.ToLowerInvariant(), out var bytes) ? bytes : null; + } + } + + private static void DropShadowState(string gameId) + { + lock (_shadowStates) + { + _shadowStates.Remove(gameId.ToLowerInvariant()); + } + } + + private static string SignaturesPath(string gameId) => + Path.Combine(Application.persistentDataPath, $"{gameId}.signatures"); + + /// + /// Whenever the client subscribes to a game, ask the server whether it is modded so the send path can route accordingly. + /// Also covers games created/joined on another device. + /// + [HarmonyPostfix] + [HarmonyPatch(typeof(BackendAdapter), nameof(BackendAdapter.SubscribeToGame))] + private static void BackendAdapter_SubscribeToGame(BackendAdapter __instance, SubscribeToGameBindingModel model) + { + _ = FetchModdedGameInfo(__instance, model.GameId); + } + + private static async System.Threading.Tasks.Task FetchModdedGameInfo(BackendAdapter adapter, Il2CppSystem.Guid gameIdGuid) + { + var gameId = gameIdGuid.ToString(); + try + { + var json = await adapter.HubConnection.InvokeAsync( + "GetModdedGameInfo", + gameId, + Il2CppSystem.Threading.CancellationToken.None + ); + + using var doc = System.Text.Json.JsonDocument.Parse(json); + var root = doc.RootElement; + if (!root.TryGetProperty("isModded", out var isModdedProperty) || !isModdedProperty.GetBoolean()) + { + return; + } + + string? checksum = root.TryGetProperty("checksum", out var checksumProperty) + ? checksumProperty.GetString() + : null; + + RegisterModdedGame(gameId, checksum); + Plugin.logger.LogInfo($"Multiplayer> Game {gameId} is modded"); + + if (checksum != null && checksum != Compatibility.checksum) + { + Plugin.logger.LogWarning($"Multiplayer> Mod checksum mismatch for game {gameId}"); + PopupManager.GetBasicPopupWithData(new( + Localization.Get("polymod.signature.mismatch"), + Localization.Get("polymod.signature.incompatible"), + new(new PopupBase.PopupButtonData[] { + new("OK") + }) + )).Show(); + return; + } + + if (GetShadowState(gameId) == null) + { + await FetchShadowState(gameIdGuid); + } + } + catch (Exception e) + { + Plugin.logger.LogWarning($"Multiplayer> Could not fetch modded game info for {gameId}: {e.Message}"); + } + } + + /// + /// Fetches the server's stored authoritative state into the shadow cache. + /// + private static async System.Threading.Tasks.Task FetchShadowState(Il2CppSystem.Guid gameIdGuid) + { + try + { + var gameResponse = await PolytopiaBackendAdapter.Instance.JoinGameHttp(new JoinGameBindingModel + { + GameId = gameIdGuid + }); + + byte[]? stateBytes = gameResponse?.Data?.CurrentGameStateData ?? gameResponse?.Data?.InitialGameStateData; + if (stateBytes == null) + { + Plugin.logger.LogWarning($"Multiplayer> Could not fetch state for game {gameIdGuid}"); + return null; + } + + SetShadowState(gameIdGuid.ToString(), stateBytes); + return stateBytes; + } + catch (Exception e) + { + Plugin.logger.LogWarning($"Multiplayer> Could not fetch state for game {gameIdGuid}: {e.Message}"); + return null; + } + } + + /// + /// The modded replacement for the vanilla send. T + /// he client has already executed the command locally, recompute it (plus auto-play follow-ups) on the shadow state and upload the result instead of calling the vanilla SendCommand hub method. + /// + [HarmonyPrefix] + [HarmonyPatch(typeof(BackendAdapter), nameof(BackendAdapter.SendCommand))] + private static bool BackendAdapter_SendCommand( + ref Il2CppSystem.Threading.Tasks.Task> __result, + SendCommandBindingModel model) + { + if (!IsModdedGame(model.GameId.ToString())) return true; + + var taskCompletionSource = + new Il2CppSystem.Threading.Tasks.TaskCompletionSource>(); + + _ = HandleModdedSendCommand(taskCompletionSource, model); + + __result = taskCompletionSource.Task; + + return false; + } + + private static async System.Threading.Tasks.Task HandleModdedSendCommand( + Il2CppSystem.Threading.Tasks.TaskCompletionSource> tcs, + SendCommandBindingModel model) + { + await _sendLock.WaitAsync(); + try + { + var gameIdGuid = model.GameId; + var gameId = gameIdGuid.ToString(); + int expectedIndex = model.Command.CommandIndex; + + if (!CommandBase.FromByteArray(model.Command.SerializedData, out var command, out _)) + { + Plugin.logger.LogError("Multiplayer> Could not deserialize own command"); + tcs.SetResult(FailedResponse()); + return; + } + + var shadow = GetShadowState(gameId); + if (shadow == null || CountCommands(shadow) != expectedIndex) + { + shadow = await FetchShadowState(gameIdGuid); + } + + if (shadow == null) + { + tcs.SetResult(FailedResponse()); + return; + } + + var (update, followUps, newStateBytes) = + ComputeModdedUpdate(shadow, gameId, command, null, expectedIndex); + if (update == null) + { + DropShadowState(gameId); + tcs.SetResult(FailedResponse()); + return; + } + + var uploaded = await UploadModdedUpdate(update); + if (!uploaded) + { + DropShadowState(gameId); + tcs.SetResult(FailedResponse()); + return; + } + + SetShadowState(gameId, newStateBytes!); + FeedFollowUpsToLiveClient(gameId, followUps); + tcs.SetResult(SuccessResponse()); + } + catch (Exception ex) + { + Plugin.logger.LogError("Multiplayer> Error during HandleModdedSendCommand: " + ex.Message); + tcs.SetException(new Il2CppSystem.Exception(ex.Message)); + } + finally + { + _sendLock.Release(); + } + } + + /// + /// Auto-play follow-up commands (resigned players' turns) computed on the shadow are fed to the live client exactly like a vanilla server push. + /// + private static void FeedFollowUpsToLiveClient(string gameId, List followUps) + { + if (followUps.Count == 0) return; + + var client = GameManager.Client; + if (client == null || !client.CurrentGameId.HasValue || + client.CurrentGameId.Value.ToString() != gameId) + { + return; + } + + var followUpCommands = new Il2CppSystem.Collections.Generic.List(); + foreach (var serialized in followUps) + { + if (CommandBase.FromByteArray(serialized, out var followUpCommand, out _)) + { + followUpCommands.Add(followUpCommand); + } + } + + if (followUpCommands.Count > 0) + { + _ = client.ReceiveCommand(followUpCommands); + } + } + + private static int CountCommands(byte[] stateBytes) + { + if (!SerializationHelpers.FromByteArray(stateBytes, out GameState state)) + { + return -1; + } + + return state.CommandStack.Count; + } + + /// + /// Runs the command on a copy of the authoritative state. + /// Returns the serialized follow-up commands and the new state separately. + /// + internal static (ModdedGameStateViewModel? update, List followUps, byte[]? newStateBytes) + ComputeModdedUpdate(byte[] preStateBytes, string gameId, CommandBase command, + string? resignedAccountId, int expectedFirstIndex = -1) + { + var noFollowUps = new List(); + + if (!SerializationHelpers.FromByteArray(preStateBytes, out GameState stateCopy)) + { + Plugin.logger.LogError("Multiplayer> Could not deserialize shadow state"); + return (null, noFollowUps, null); + } + + new ActionManager(stateCopy).Update(); + + int version = stateCopy.Version; + int firstIndex = stateCopy.CommandStack.Count; + if (expectedFirstIndex >= 0 && firstIndex != expectedFirstIndex) + { + Plugin.logger.LogWarning( + $"Multiplayer> Shadow state out of sync ({firstIndex} commands, expected {expectedFirstIndex})"); + return (null, noFollowUps, null); + } + + var commandList = new Il2CppSystem.Collections.Generic.List(); + commandList.Add(command); + var result = GameStateUtils.PerformCommands(stateCopy, commandList, out _, out _); + if (result == null || !result.Success) + { + Plugin.logger.LogError("Multiplayer> Command execution failed on shadow state"); + return (null, noFollowUps, null); + } + + int newCount = stateCopy.CommandStack.Count; + var uploadCommands = new List(); + var followUps = new List(); + bool isEndTurn = false; + + for (int i = firstIndex; i < newCount; i++) + { + var executed = stateCopy.CommandStack[i]; + if (executed.GetCommandType() == CommandType.EndTurn) + { + isEndTurn = true; + } + + var serialized = CommandBase.ToByteArray(executed, version); + uploadCommands.Add(new ModdedCommandViewModel { serializedData = serialized, commandIndex = i }); + if (i > firstIndex) + { + followUps.Add(serialized); + } + } + + string? currentPlayerId = GameStateUtils.GetCurrentPlayerAccountId(stateCopy).ToString(); + if (currentPlayerId == EmptyGuid) + { + currentPlayerId = null; + } + + var newStateBytes = SerializationHelpers.ToByteArray(stateCopy, stateCopy.Version); + + var summaryBytes = Array.Empty(); + if (GameStateSummary.FromGameStateByteArray(newStateBytes, out GameStateSummary summary, + out GameState summaryState)) + { + summaryBytes = SerializationHelpers.ToByteArray(summary, summaryState.Version); + } + + var update = new ModdedGameStateViewModel + { + gameId = gameId, + commands = uploadCommands, + serializedGameState = newStateBytes, + serializedGameSummary = summaryBytes, + newCommandCount = newCount, + currentPlayerId = currentPlayerId, + isEndTurn = isEndTurn, + isGameEnded = stateCopy.CurrentState == GameState.State.Ended, + resignedPlayerId = resignedAccountId + }; + + return (update, followUps, newStateBytes); + } + + internal static async System.Threading.Tasks.Task UploadModdedUpdate(ModdedGameStateViewModel update) + { + try + { + var payload = System.Text.Json.JsonSerializer.Serialize(update); + var response = await PolytopiaBackendAdapter.Instance.HubConnection + .InvokeAsync>( + "UpdateGameStateModded", + payload, + Il2CppSystem.Threading.CancellationToken.None + ); + + if (response != null && response.Success) + { + return true; + } + + Plugin.logger.LogWarning($"Multiplayer> UpdateGameStateModded rejected: {response?.ErrorMessage}"); + } + catch (Exception e) + { + Plugin.logger.LogError($"Multiplayer> UpdateGameStateModded failed: {e.Message}"); + } + + return false; + } + + /// + /// Relayed commands from other players are applied to the shadow so it tracks the authoritative state without refetching. + /// + [HarmonyPrefix] + [HarmonyPatch(typeof(PolytopiaBackendAdapter), nameof(PolytopiaBackendAdapter.OnCommand))] + private static void PolytopiaBackendAdapter_OnCommand(CommandArrayViewModel model) + { + try + { + var gameId = model.GameId.ToString(); + if (!IsModdedGame(gameId)) return; + + var shadow = GetShadowState(gameId); + if (shadow == null) return; + + if (!SerializationHelpers.FromByteArray(shadow, out GameState state)) + { + DropShadowState(gameId); + return; + } + + var actionManager = new ActionManager(state); + actionManager.Update(); + foreach (var commandViewModel in model.Commands) + { + int index = commandViewModel.CommandIndex; + if (index >= 0 && index < state.CommandStack.Count) + { + continue; + } + + if (index > state.CommandStack.Count) + { + DropShadowState(gameId); + return; + } + + if (!CommandBase.FromByteArray(commandViewModel.SerializedData, out var command, out _) || + !actionManager.ExecuteCommand(command, out _)) + { + DropShadowState(gameId); + return; + } + } + + SetShadowState(gameId, SerializationHelpers.ToByteArray(state, state.Version)); + } + catch (Exception e) + { + Plugin.logger.LogWarning($"Multiplayer> Could not apply relayed commands to shadow: {e.Message}"); + DropShadowState(model.GameId.ToString()); + } + } + + /// + /// Vanilla resign asks the server to build the resign command, which the relay server cannot do for modded games. Build and execute it on the shadow state instead. + /// + [HarmonyPrefix] + [HarmonyPatch(typeof(BackendAdapter), nameof(BackendAdapter.Resign))] + private static bool BackendAdapter_Resign( + ref Il2CppSystem.Threading.Tasks.Task> __result, + ResignBindingModel model) + { + if (!IsModdedGame(model.GameId.ToString())) return true; + + var taskCompletionSource = + new Il2CppSystem.Threading.Tasks.TaskCompletionSource>(); + + _ = HandleModdedResign(taskCompletionSource, model.GameId); + + __result = taskCompletionSource.Task; + + return false; + } + + private static async System.Threading.Tasks.Task HandleModdedResign( + Il2CppSystem.Threading.Tasks.TaskCompletionSource> tcs, + Il2CppSystem.Guid gameIdGuid) + { + await _sendLock.WaitAsync(); + try + { + var gameId = gameIdGuid.ToString(); + var ownAccountId = AccountManager.PlayerAccountId; + + var shadow = GetShadowState(gameId) ?? await FetchShadowState(gameIdGuid); + if (shadow == null || + !SerializationHelpers.FromByteArray(shadow, out GameState state) || + !state.TryGetPlayer(ownAccountId, out PlayerState ownPlayer)) + { + tcs.SetResult(FailedResponse()); + return; + } + + var resignCommand = new ResignCommand(state.CurrentPlayer, ownPlayer.Id, 0, false); + var (update, followUps, newStateBytes) = + ComputeModdedUpdate(shadow, gameId, resignCommand, ownAccountId.ToString()); + if (update == null) + { + DropShadowState(gameId); + tcs.SetResult(FailedResponse()); + return; + } + + var uploaded = await UploadModdedUpdate(update); + if (!uploaded) + { + DropShadowState(gameId); + tcs.SetResult(FailedResponse()); + return; + } + + SetShadowState(gameId, newStateBytes!); + + var client = GameManager.Client; + if (client != null && client.CurrentGameId.HasValue && + client.CurrentGameId.Value.ToString() == gameId) + { + var commands = new Il2CppSystem.Collections.Generic.List(); + commands.Add(resignCommand); + foreach (var serialized in followUps) + { + if (CommandBase.FromByteArray(serialized, out var followUpCommand, out _)) + { + commands.Add(followUpCommand); + } + } + + _ = client.ReceiveCommand(commands); + } + + tcs.SetResult(SuccessResponse()); + } + catch (Exception ex) + { + Plugin.logger.LogError("Multiplayer> Error during HandleModdedResign: " + ex.Message); + tcs.SetException(new Il2CppSystem.Exception(ex.Message)); + } + finally + { + _sendLock.Release(); + } + } + + /// + /// Skipping is not supported yet for modded games. + /// Returns true to suppress errors. + /// + [HarmonyPrefix] + [HarmonyPatch(typeof(BackendAdapter), nameof(BackendAdapter.SkipTurn))] + private static bool BackendAdapter_SkipTurn( + ref Il2CppSystem.Threading.Tasks.Task> __result, + SkipTurnBindingModel model) + { + if (!IsModdedGame(model.GameId.ToString())) return true; + + Plugin.logger.LogInfo("Multiplayer> SkipTurn is not supported for modded games yet"); + + var taskCompletionSource = + new Il2CppSystem.Threading.Tasks.TaskCompletionSource>(); + taskCompletionSource.SetResult(SuccessResponse()); + __result = taskCompletionSource.Task; + + return false; + } + + /// + ///Matchmaking is blocked while gameplay mods are loaded. + /// + [HarmonyPrefix] + [HarmonyPatch(typeof(BackendAdapter), nameof(BackendAdapter.SubmitMatchmakingRequest))] + private static bool BackendAdapter_SubmitMatchmakingRequest( + ref Il2CppSystem.Threading.Tasks.Task> __result) + { + Plugin.logger.LogWarning("Multiplayer> Matchmaking blocked: gameplay mods are loaded"); + PopupManager.GetBasicPopupWithData(new( + "Matchmaking disabled", + "Matchmaking is unavailable while gameplay mods are loaded.", + new(new PopupBase.PopupButtonData[] { + new("OK") + }) + )).Show(); + + var taskCompletionSource = + new Il2CppSystem.Threading.Tasks.TaskCompletionSource>(); + taskCompletionSource.SetResult(new ServerResponse + { + Success = false, + ErrorCode = ErrorCode.StateProhibitsOperation, + ErrorMessage = "Matchmaking is disabled while gameplay mods are loaded." + }); + __result = taskCompletionSource.Task; + + return false; + } + + private static ServerResponse SuccessResponse() => + new() { Success = true, Data = new ResponseViewModel() }; + + private static ServerResponse FailedResponse() => + new() + { + Success = false, + ErrorCode = ErrorCode.StateProhibitsOperation, + ErrorMessage = "Modded game operation failed." + }; +} diff --git a/src/Multiplayer/SerializationUtils.cs b/src/Multiplayer/SerializationUtils.cs new file mode 100644 index 0000000..e6d3da8 --- /dev/null +++ b/src/Multiplayer/SerializationUtils.cs @@ -0,0 +1,42 @@ +using HarmonyLib; +using Polytopia.Data; +using PolytopiaBackendBase.Common; + +namespace PolyMod.Multiplayer; + +/// +/// Serialization fixes for custom (modded) content. +/// +/// GamePlayerSummary stores the tribe as a single byte, which custom tribe ids (>= 1000) overflow into garbage. +/// Rewriting the record in managed code is not an option. Reading Il2CppSystem.Nullable members (PolytopiaId) from managed patches returns corrupted guids. +/// Instead the tribe is clamped to None before the untouched native serializer runs and menu summaries show a generic icon for custom tribes, nothing more. +/// +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) + { + if ((int)__instance.TribeType >= byte.MaxValue) + { + __instance.TribeType = TribeType.None; + } + + return true; + } + + [HarmonyPostfix] + [HarmonyPatch(typeof(PlayerState), nameof(PlayerState.Deserialize))] + public static void PlayerState_Deserialize(PlayerState __instance, Il2CppSystem.IO.BinaryReader reader, int version) + { + if ((int)__instance.tribe >= Plugin.AUTOIDX_STARTS_FROM) + { + __instance.climate = __instance.tribe; + } + } +} diff --git a/src/Multiplayer/ViewModels/ModdedGameStateViewModel.cs b/src/Multiplayer/ViewModels/ModdedGameStateViewModel.cs new file mode 100644 index 0000000..9fa949b --- /dev/null +++ b/src/Multiplayer/ViewModels/ModdedGameStateViewModel.cs @@ -0,0 +1,33 @@ +namespace PolyMod.Multiplayer.ViewModels; + +/// +/// Payload of the UpdateGameStateModded hub method. +/// The locally executed command batch plus everything the server would normally compute itself (new state, summary, turn metadata). +/// +public class ModdedGameStateViewModel +{ + public string gameId { get; set; } = string.Empty; + + public List commands { get; set; } = new(); + + public byte[] serializedGameState { get; set; } = Array.Empty(); + + public byte[] serializedGameSummary { get; set; } = Array.Empty(); + + public int newCommandCount { get; set; } = -1; + + public string? currentPlayerId { get; set; } + + public bool isEndTurn { get; set; } + + public bool isGameEnded { get; set; } + + public string? resignedPlayerId { get; set; } +} + +public class ModdedCommandViewModel +{ + public byte[] serializedData { get; set; } = Array.Empty(); + + public int commandIndex { get; set; } = -1; +} diff --git a/src/Multiplayer/ViewModels/SetupGameDataViewModel.cs b/src/Multiplayer/ViewModels/SetupGameDataViewModel.cs index 41274bd..689d29e 100644 --- a/src/Multiplayer/ViewModels/SetupGameDataViewModel.cs +++ b/src/Multiplayer/ViewModels/SetupGameDataViewModel.cs @@ -6,5 +6,11 @@ public class SetupGameDataViewModel : IMonoServerResponseData public byte[] serializedGameState { get; set; } = Array.Empty(); + public byte[] serializedGameSummary { get; set; } = Array.Empty(); + public string gameSettingsJson { get; set; } = string.Empty; -} \ No newline at end of file + + public int initialCommandCount { get; set; } = -1; + + public string? currentPlayerId { get; set; } +} diff --git a/src/Plugin.cs b/src/Plugin.cs index ee514f8..8cba2b9 100644 --- a/src/Plugin.cs +++ b/src/Plugin.cs @@ -119,7 +119,7 @@ public override void Load() } catch { - config = new(backendUrl: Multiplayer.Dystopia.DefaultServerUrl()); + config = new(); } WriteConfig(); UpdateConsole(); From 5c433c9167e722894da844f2766bd03964869e4a Mon Sep 17 00:00:00 2001 From: Demonofpower Date: Mon, 10 Aug 2026 13:13:53 +0200 Subject: [PATCH 24/34] minor fix --- src/Android/AndroidHandler.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Android/AndroidHandler.cs b/src/Android/AndroidHandler.cs index 18a8d9f..0e3730d 100644 --- a/src/Android/AndroidHandler.cs +++ b/src/Android/AndroidHandler.cs @@ -43,6 +43,7 @@ public static bool LoginPlatformAndroid_Prefix( var model = new LoginGooglePlayBindingModel(); model.AuthCode = SystemInfo.deviceUniqueIdentifier; model.DeviceId = SystemInfo.deviceUniqueIdentifier; + model.GameVersion = new Il2CppSystem.Nullable(VersionManager.GameVersion); Plugin.logger.LogInfo($"Multiplayer> Android login with DeviceId: {model.DeviceId}"); __result = __instance.LoginGooglePlay(model); From e26b5d8312d9ad2af8d2f5fc56bd283ac9943b19 Mon Sep 17 00:00:00 2001 From: Demonofpower Date: Mon, 10 Aug 2026 14:43:23 +0200 Subject: [PATCH 25/34] minor fix + version bump --- PolyMod.csproj | 2 +- src/Managers/Compatibility.cs | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/PolyMod.csproj b/PolyMod.csproj index 783658f..5b7340e 100644 --- a/PolyMod.csproj +++ b/PolyMod.csproj @@ -11,7 +11,7 @@ IL2CPP PolyMod - 1.3.0-pre-android-1 + 1.3.0-pre-android-multi-1 2.17.2.16299 PolyModdingTeam The Battle of Polytopia's mod loader. diff --git a/src/Managers/Compatibility.cs b/src/Managers/Compatibility.cs index d2cde96..4d39d70 100644 --- a/src/Managers/Compatibility.cs +++ b/src/Managers/Compatibility.cs @@ -162,6 +162,25 @@ private static bool StartScreen_OnResumeButtonClick(StartScreen_UI2 __instance) return CheckSignatures(__instance.OnResumeButtonLongPress, LocalSaveFileUtils.GetSaveFiles(PolytopiaBackendBase.Game.GameType.SinglePlayer)[0]); } + /// + /// Checks the signature of a multiplayer game before opening it. + /// Blocks on mismatch. + /// + [HarmonyPrefix] + [HarmonyPatch(typeof(GameManager), nameof(GameManager.OpenMultiplayerGame))] + private static bool GameManager_OpenMultiplayerGame( + ref Il2CppSystem.Threading.Tasks.Task __result, + Il2CppSystem.Guid gameId) + { + if (CheckSignatures(null!, gameId)) return true; + + var taskCompletionSource = new Il2CppSystem.Threading.Tasks.TaskCompletionSource(); + taskCompletionSource.SetResult(false); + __result = taskCompletionSource.Task; + + return false; + } + /// /// Deletes the signature file of a pass-and-play game when it is deleted. /// From f870ea4ae0fa22ffd0356dc538c5660bc9b53586 Mon Sep 17 00:00:00 2001 From: Demonofpower Date: Mon, 10 Aug 2026 22:41:45 +0200 Subject: [PATCH 26/34] moved dystopia button into section in multiplayer tab --- resources/localization.json | 40 +++++++++++ src/Multiplayer/Dystopia.cs | 136 +++++++++++++++++++++++------------- 2 files changed, 128 insertions(+), 48 deletions(-) diff --git a/resources/localization.json b/resources/localization.json index 3b19f1e..4b1ca28 100644 --- a/resources/localization.json +++ b/resources/localization.json @@ -1,4 +1,44 @@ { + "polymod_dystopia": { + "English": "Dystopia", + "German (Germany)": "Dystopia" + }, + "polymod_dystopia_server": { + "English": "Server", + "German (Germany)": "Server" + }, + "polymod_dystopia_connected": { + "English": "connected", + "German (Germany)": "verbunden" + }, + "polymod_dystopia_disconnected": { + "English": "disconnected", + "German (Germany)": "getrennt" + }, + "polymod_dystopia_modded": { + "English": "modded", + "German (Germany)": "gemoddet" + }, + "polymod_dystopia_vanilla": { + "English": "vanilla", + "German (Germany)": "vanilla" + }, + "polymod_dystopia_modded_description": { + "English": "Gameplay Mods active\nChecksum: {0}\n\nOnly players with the exact same mods can join your games. The official backend is unavailable while gameplay mods are loaded.", + "German (Germany)": "Gameplay-Mods aktiv\nPrüfsumme: {0}\n\nNur Spieler mit exakt denselben Mods können deinen Spielen beitreten. Der offizielle Server ist mit geladenen Gameplay-Mods nicht verfügbar." + }, + "polymod_dystopia_vanilla_description": { + "English": "No gameplay mods loaded — vanilla multiplayer.", + "German (Germany)": "Keine Gameplay-Mods geladen — Vanilla-Multiplayer." + }, + "polymod_dystopia_connect": { + "English": "Connect to {0}", + "German (Germany)": "Mit {0} verbinden" + }, + "polymod_dystopia_custom": { + "English": "Custom server...", + "German (Germany)": "Eigener Server..." + }, "polymod_hub": { "English": "PolyMod Hub", "Russian": "Центр PolyMod", diff --git a/src/Multiplayer/Dystopia.cs b/src/Multiplayer/Dystopia.cs index 3bf7811..3d288fc 100644 --- a/src/Multiplayer/Dystopia.cs +++ b/src/Multiplayer/Dystopia.cs @@ -1,20 +1,16 @@ -using Cpp2IL.Core.Extensions; using HarmonyLib; using Il2CppInterop.Runtime; using PolyMod.Managers; using PolytopiaBackendBase; using TMPro; -using UnityEngine; namespace PolyMod.Multiplayer; /// -/// Adds a "Dystopia" start-screen button with a popup to switch the backend server at runtime. +/// Integrates the Dystopia backend switcher natively into the multiplayer tab as a section at the bottom of the game list /// public static class Dystopia { - private const int POPUP_WIDTH = 1400; - internal const string OFFICIAL_SERVER_URL = "https://polytopia-prod.net/"; private record ServerEntry(string name, string url, bool official = false, bool disabled = false); @@ -27,73 +23,117 @@ private record ServerEntry(string name, string url, bool official = false, bool new("Dystopia Dev", "https://dev.polydystopia.xyz"), }; - private static UIRoundButton_UI2? dystopiaButton = null; - internal static void Init() { Harmony.CreateAndPatchAll(typeof(Dystopia)); } [HarmonyPostfix] - [HarmonyPatch(typeof(StartScreen_UI2), nameof(StartScreen_UI2.Init))] - private static void StartScreen_UI2_Init(StartScreen_UI2 __instance, RectTransform transform) + [HarmonyPatch(typeof(MultiplayerScreen), nameof(MultiplayerScreen.AddHotSeatGames))] + private static void MultiplayerScreen_AddHotSeatGames(MultiplayerScreen __instance) { - if (dystopiaButton != null) + try + { + AddServerSection(__instance); + } + catch (System.Exception e) { - UnityEngine.Object.Destroy(dystopiaButton.gameObject); + Plugin.logger.LogWarning($"Dystopia> Failed to add server section: {e}"); } - dystopiaButton = UILibrary.NewRoundButton(transform).SetStyle(UIButtonBase_UI2.ButtonStyle.Suggested); - dystopiaButton.bg.sprite = Visual.BuildSprite(Plugin.GetResource("dystopia_icon.png").ReadBytes()); - dystopiaButton.OnClickedSignal.Add(DelegateSupport.ConvertDelegate(ShowDystopiaPopup)); } - [HarmonyPostfix] - [HarmonyPatch(typeof(StartScreen_UI2), nameof(StartScreen_UI2.RunLayout))] - private static void StartScreen_UI2_RunLayout(StartScreen_UI2 __instance, ScreenBase_UI2.ScreenSize screenSize) + private static void AddServerSection(MultiplayerScreen screen) { - if (dystopiaButton == null) + string current = Normalize(Plugin.config.backendUrl); + + screen.AddHeader("polymod.dystopia", useExtraSpacer: true); + + string connectionState = Localization.Get(PolytopiaBackendAdapter.Instance.IsConnected + ? "polymod.dystopia.connected" + : "polymod.dystopia.disconnected"); + + MultiplayerInfoRow infoRow = screen.AddInfoRow(); + infoRow.header.text = $"{Localization.Get("polymod.dystopia.server")}: {CurrentServerName()} ({connectionState})"; + infoRow.description.text = Compatibility.IsClientOnly() + ? Localization.Get("polymod.dystopia.vanilla.description") + : string.Format(Localization.Get("polymod.dystopia.modded.description"), ShortChecksum()); + + foreach (ServerEntry server in SERVERS) { - Plugin.logger.LogWarning("Dystopia button is null when running layout!"); - return; + // The active server is named in the info row; the official backend stays blocked + // (the info row explains why). + if (server.official || server.disabled || Normalize(server.url) == current) continue; + + string url = server.url; + AddServerButton(screen, + string.Format(Localization.Get("polymod.dystopia.connect"), server.name), + () => SwitchServer(url)); } - dystopiaButton.iconContainer.gameObject.SetActive(false); - dystopiaButton.outline.gameObject.SetActive(false); - dystopiaButton.bg.color = Color.white; - dystopiaButton.Text = "Dystopia"; - float num = 50f; - dystopiaButton.SetPosition(screenSize.safeRect.Right - (num * 4.0f), screenSize.safeRect.Top - num); + AddServerButton(screen, Localization.Get("polymod.dystopia.custom"), ShowCustomServerPopup); } - internal static void ShowDystopiaPopup() + private static void AddServerButton(MultiplayerScreen screen, string text, System.Action action) { - string current = Normalize(Plugin.config.backendUrl); + // The private no-arg AddButtonRow returns the actual row (the public string overload + // returns the prefab by mistake) and clears callbacks on reused rows. + ButtonRow row = screen.AddButtonRow(); + row.buttonComp.text = text; + row.buttonComp.OnClickedSignal.Add(DelegateSupport.ConvertDelegate(action)); + } - BasicPopupLegacy popup = Visual.GetBasicPopupLegacy(); - popup.Header = "Dystopia"; - popup.Description = $"Current server:\n{Plugin.config.backendUrl}"; + [HarmonyPostfix] + [HarmonyPatch(typeof(MultiplayerSelectionScreen), nameof(MultiplayerSelectionScreen.OnEnable))] + private static void MultiplayerSelectionScreen_OnEnable(MultiplayerSelectionScreen __instance) + { + UpdateStatusHeader(__instance); + } - List buttons = new() + [HarmonyPostfix] + [HarmonyPatch(typeof(MultiplayerSelectionScreen), nameof(MultiplayerSelectionScreen.OnBackendConnectionChanged))] + private static void MultiplayerSelectionScreen_OnBackendConnectionChanged(MultiplayerSelectionScreen __instance) + { + UpdateStatusHeader(__instance); + } + + private static void UpdateStatusHeader(MultiplayerSelectionScreen screen) + { + try { - new("buttons.back"), - }; + UIHorizontalList list = screen.ScreenSelectionList; + if (list == null) return; + + if (string.IsNullOrEmpty(list.HeaderKey)) + { + // Activates and sizes the header slot; the text itself is overridden below. + list.HeaderKey = "polymod.dystopia"; + } + + string moddedState = Compatibility.IsClientOnly() + ? Localization.Get("polymod.dystopia.vanilla") + : $"{Localization.Get("polymod.dystopia.modded")} {ShortChecksum()}"; + list.header.Text = $"{CurrentServerName()} · {moddedState}"; + } + catch (System.Exception e) + { + Plugin.logger.LogWarning($"Dystopia> Failed to update status header: {e}"); + } + } + + private static string CurrentServerName() + { + string current = Normalize(Plugin.config.backendUrl); foreach (ServerEntry server in SERVERS) { - bool isActive = Normalize(server.url) == current; - bool isDisabled = isActive || server.official || server.disabled; - string url = server.url; - buttons.Add(new( - isActive ? server.name + " (active)" : server.name, - isDisabled ? PopupBase.PopupButtonData.States.Disabled : PopupBase.PopupButtonData.States.None, - DelegateSupport.ConvertDelegate((System.Action)(() => SwitchServer(url))) - )); + if (Normalize(server.url) == current) return server.name; } - buttons.Add(new( - "Custom...", - callback: DelegateSupport.ConvertDelegate(ShowCustomServerPopup) - )); - popup.buttonData = buttons.ToArray(); - popup.ShowSetWidth(POPUP_WIDTH); + + return Plugin.config.backendUrl; + } + + private static string ShortChecksum() + { + return Compatibility.checksum.Length >= 8 ? Compatibility.checksum[..8] : Compatibility.checksum; } private static void ShowCustomServerPopup() From ac7056ad0552e05696dd9b74d8633ecc1d3e863d Mon Sep 17 00:00:00 2001 From: Demonofpower Date: Tue, 11 Aug 2026 11:23:05 +0200 Subject: [PATCH 27/34] fixed pipeline --- src/Multiplayer/ModMultiplayer.cs | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/Multiplayer/ModMultiplayer.cs b/src/Multiplayer/ModMultiplayer.cs index 05d6deb..1d6a5c3 100644 --- a/src/Multiplayer/ModMultiplayer.cs +++ b/src/Multiplayer/ModMultiplayer.cs @@ -151,7 +151,13 @@ private static async System.Threading.Tasks.Task HandleStartLobbyGameModded( ); Plugin.logger.LogInfo("Multiplayer> Invoked StartLobbyGameModded"); - if (serverResponse != null && serverResponse.Success) + if (serverResponse == null) + { + tcs.SetException(new Il2CppSystem.Exception("No response from StartLobbyGameModded.")); + return; + } + + if (serverResponse.Success) { ModdedClient.RegisterModdedGame(lobbyGameViewModel.Id.ToString(), Compatibility.checksum); ModdedClient.SetShadowState(lobbyGameViewModel.Id.ToString(), serializedGameState); @@ -202,8 +208,13 @@ private static async System.Threading.Tasks.Task HandleRespondToLobbyInvitationM Il2CppSystem.Threading.CancellationToken.None ); - if (serverResponse != null && !serverResponse.Success && - serverResponse.ErrorCode == ErrorCode.StateProhibitsOperation) + if (serverResponse == null) + { + tcs.SetException(new Il2CppSystem.Exception("No response from RespondToLobbyInvitation.")); + return; + } + + if (!serverResponse.Success && serverResponse.ErrorCode == ErrorCode.StateProhibitsOperation) { Plugin.logger.LogWarning("Multiplayer> Lobby join blocked: mod set mismatch"); PopupManager.GetBasicPopupWithData(new( From c6dfc2f557f5c122240cab53f08369b5cebdbc96 Mon Sep 17 00:00:00 2001 From: Maksym Muraviov$ Date: Sat, 19 Sep 2026 09:54:07 +0200 Subject: [PATCH 28/34] Revert "Merge branch 'dev1.3' into dev1.3-multiplayer" This reverts commit 07ccd88439f29c408cb395ae3e99a65155ca3a82, reversing changes made to ac7056ad0552e05696dd9b74d8633ecc1d3e863d. --- PolyMod.csproj | 2 +- src/Multiplayer/Client.cs | 168 -------------- src/Multiplayer/Multiplayer.cs | 208 ++++++++++++++++++ .../ViewModels/IMonoServerResponseData.cs | 5 + src/Plugin.cs | 2 - 5 files changed, 214 insertions(+), 171 deletions(-) delete mode 100644 src/Multiplayer/Client.cs create mode 100644 src/Multiplayer/Multiplayer.cs create mode 100644 src/Multiplayer/ViewModels/IMonoServerResponseData.cs diff --git a/PolyMod.csproj b/PolyMod.csproj index 2c165a4..5b7340e 100644 --- a/PolyMod.csproj +++ b/PolyMod.csproj @@ -11,7 +11,7 @@ IL2CPP PolyMod - 1.3.0-pre + 1.3.0-pre-android-multi-1 2.17.2.16299 PolyModdingTeam The Battle of Polytopia's mod loader. diff --git a/src/Multiplayer/Client.cs b/src/Multiplayer/Client.cs deleted file mode 100644 index f9ebd14..0000000 --- a/src/Multiplayer/Client.cs +++ /dev/null @@ -1,168 +0,0 @@ -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; -using PolytopiaBackendBase.Auth; - -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 = Plugin.config.backendUrl; - - // Update BackendUri and HttpClient.BaseAddress since PolytopiaBackendAdapter.Instance - // was statically initialized before plugins load, so it still points to polytopia-prod.net - var uri = new Il2CppSystem.Uri(Plugin.config.backendUrl); - PolytopiaBackendAdapter.Instance.UseBackendUri(uri); - PolytopiaBackendAdapter.Instance.BackendHttpClient.BaseAddress = uri; - - Plugin.logger.LogInfo($"Multiplayer> Server URL set to: {Plugin.config.backendUrl}"); - } - - [HarmonyPostfix] - [HarmonyPatch(typeof(MultiplayerSelectionScreen), nameof(MultiplayerSelectionScreen.Awake))] - public static void MultiplayerSelectionScreen_Awake(MultiplayerSelectionScreen __instance) - { - __instance.TournamentsButton.gameObject.SetActive(false); - } - - [HarmonyPostfix] - [HarmonyPatch(typeof(StartScreen_UI2), nameof(StartScreen_UI2.Init))] - private static void StartScreen_UI2_HideButtons(StartScreen_UI2 __instance) - { - __instance.highscoreButton.gameObject.SetActive(false); - __instance.weeklyChallengeButton.gameObject.SetActive(false); - } - - [HarmonyPostfix] - [HarmonyPatch(typeof(StartScreen_UI2), nameof(StartScreen_UI2.RunLayout))] - private static void StartScreen_UI2_ReflowRoundButtons(StartScreen_UI2 __instance, ScreenBase_UI2.ScreenSize screenSize) - { - // RunLayout adds all four round buttons to a UITable unconditionally, so hiding the highscore button leaves a gap. Re-run the row without it so the rest recenter. - UITable table = new(); - table.AddCell(__instance.settingsButton.Cast()); - table.AddCell(__instance.throneRoomButton.Cast()); - table.AddCell(__instance.aboutButton.Cast()); - table.SetBottom(screenSize.safeRect.Bottom + __instance.settingsButton.GetHalfHeight() + 15f); - table.margin = 20f; - table.RunLayout(); - } - - [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))] - [Obsolete("This will be succeeded by ModMultiplayer in the future.")] - private static void Deserialize_Postfix(GameState __instance, BinaryReader __0) - { - - Plugin.logger.LogInfo("Multiplayer> ClientBase_SendCommand"); - Il2CppSystem.Threading.Tasks.Task> task = new(); - var taskCompletionSource = new Il2CppSystem.Threading.Tasks.TaskCompletionSource>(); - - _ = ClientBase_SendCommand_Async(taskCompletionSource, __instance, command); - - task = taskCompletionSource.Task; - - return false; - } - - /// - /// Fetch GLD from server using ModGldVersion ID - /// - [Obsolete("This will be succeeded by ModMultiplayer in the future.")] - private static string? FetchGldById(int modGldVersion) - { - 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); - - - client.GameState.TryGetPlayer(client.GameState.CurrentPlayer, out PlayerState playerState); - var currentPlayerId = ""; - if(playerState.AccountId.HasValue) - { - currentPlayerId = playerState.AccountId.Value.ToString(); - } - var setupGameDataViewModel = new ModdedGameStateViewModel - { - gameId = client.gameId.ToString(), - serializedGameState = serializedGameState, - serializedGameSummary = serializedGameSummary, - gameSettingsJson = "", - currentPlayerId = currentPlayerId, - IsEndTurnCommand = command.GetCommandType() == CommandType.EndTurn - }; - - - - 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)); - } - } -} diff --git a/src/Multiplayer/Multiplayer.cs b/src/Multiplayer/Multiplayer.cs new file mode 100644 index 0000000..1520742 --- /dev/null +++ b/src/Multiplayer/Multiplayer.cs @@ -0,0 +1,208 @@ +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; +using PolytopiaBackendBase.Auth; + +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 = Plugin.config.backendUrl; + + // Update BackendUri and HttpClient.BaseAddress since PolytopiaBackendAdapter.Instance + // was statically initialized before plugins load, so it still points to polytopia-prod.net + var uri = new Il2CppSystem.Uri(Plugin.config.backendUrl); + PolytopiaBackendAdapter.Instance.UseBackendUri(uri); + PolytopiaBackendAdapter.Instance.BackendHttpClient.BaseAddress = uri; + + Plugin.logger.LogInfo($"Multiplayer> Server URL set to: {Plugin.config.backendUrl}"); + } + + [HarmonyPostfix] + [HarmonyPatch(typeof(MultiplayerSelectionScreen), nameof(MultiplayerSelectionScreen.Awake))] + public static void MultiplayerSelectionScreen_Awake(MultiplayerSelectionScreen __instance) + { + __instance.TournamentsButton.gameObject.SetActive(false); + } + + [HarmonyPostfix] + [HarmonyPatch(typeof(StartScreen_UI2), nameof(StartScreen_UI2.Init))] + private static void StartScreen_UI2_HideButtons(StartScreen_UI2 __instance) + { + __instance.highscoreButton.gameObject.SetActive(false); + __instance.weeklyChallengeButton.gameObject.SetActive(false); + } + + [HarmonyPostfix] + [HarmonyPatch(typeof(StartScreen_UI2), nameof(StartScreen_UI2.RunLayout))] + private static void StartScreen_UI2_ReflowRoundButtons(StartScreen_UI2 __instance, ScreenBase_UI2.ScreenSize screenSize) + { + // RunLayout adds all four round buttons to a UITable unconditionally, so hiding the highscore button leaves a gap. Re-run the row without it so the rest recenter. + UITable table = new(); + table.AddCell(__instance.settingsButton.Cast()); + table.AddCell(__instance.throneRoomButton.Cast()); + table.AddCell(__instance.aboutButton.Cast()); + table.SetBottom(screenSize.safeRect.Bottom + __instance.settingsButton.GetHalfHeight() + 15f); + table.margin = 20f; + table.RunLayout(); + } + + [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))] + [Obsolete("This will be succeeded by ModMultiplayer in the future.")] + 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 + /// + [Obsolete("This will be succeeded by ModMultiplayer in the future.")] + 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; + } +} 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/Plugin.cs b/src/Plugin.cs index 34ef29c..8cba2b9 100644 --- a/src/Plugin.cs +++ b/src/Plugin.cs @@ -140,8 +140,6 @@ public override void Load() ModMultiplayer.Init(); Dystopia.Init(); AndroidHandler.Init(); - Multiplayer.SerializationUtils.Init(); - Multiplayer.Client.Init(); } /// From fed1962503237e14ba75c8b1c407c9a6a36fcda6 Mon Sep 17 00:00:00 2001 From: Maksym Muraviov$ Date: Sat, 19 Sep 2026 10:31:06 +0200 Subject: [PATCH 29/34] feat: removed autoupdate --- src/Managers/AutoUpdate.cs | 182 ------------------------------------- src/Managers/Hub.cs | 40 +------- src/Plugin.cs | 6 -- 3 files changed, 1 insertion(+), 227 deletions(-) delete mode 100644 src/Managers/AutoUpdate.cs diff --git a/src/Managers/AutoUpdate.cs b/src/Managers/AutoUpdate.cs deleted file mode 100644 index 6384616..0000000 --- a/src/Managers/AutoUpdate.cs +++ /dev/null @@ -1,182 +0,0 @@ -using System.Diagnostics; -using System.IO.Compression; -using System.Text.Json; -using HarmonyLib; -using UnityEngine; - -namespace PolyMod.Managers; - -/// -/// Manages the automatic update process for PolyMod. -/// -internal static class AutoUpdate -{ - /// - /// Checks for updates when the start screen is shown. - /// - [HarmonyPostfix] - [HarmonyPatch(typeof(StartScreen_UI2), nameof(StartScreen_UI2.OnShow))] - private static void StartScreen_UI2_OnShow() - { - if (!Plugin.config.autoUpdate) return; - if (Environment.GetEnvironmentVariable("WINEPREFIX") != null) - { - Plugin.logger.LogError("Autoupdate is not supported on Wine!"); - return; - } - HttpClient client = new(); - client.DefaultRequestHeaders.Add("User-Agent", "PolyMod"); - try - { - // Fetch release information from GitHub API - var json = JsonDocument.Parse( - client.GetAsync("https://api.github.com/repos/PolyModdingTeam/PolyMod/releases").UnwrapAsync() - .Content.ReadAsStringAsync().UnwrapAsync() - ); - JsonElement? latest = null; - for (int i = 0; i < json.RootElement.GetArrayLength(); i++) - { - var release = json.RootElement[i]; - if (release.GetProperty("prerelease").GetBoolean() && !Plugin.config.updatePrerelease) continue; - latest = release; - break; - } - string newVersion = latest?.GetProperty("tag_name").GetString()!.TrimStart('v')!; - if (newVersion.IsVersionOlderOrEqual(Plugin.VERSION)) return; - - // Determine the correct BepInEx URL for the current OS - string os = Application.platform switch - { - RuntimePlatform.WindowsPlayer => "win", - RuntimePlatform.LinuxPlayer => "linux", - RuntimePlatform.OSXPlayer => "macos", - _ => "unknown", - }; - if (os == "unknown") - { - Plugin.logger.LogError("Unsupported platform for autoupdate!"); - return; - } - string bepinex_url = client - .GetAsync("https://polymod.dev/data/bepinex.txt").UnwrapAsync() - .Content.ReadAsStringAsync().UnwrapAsync() - .Replace("{os}", os); - - // The actual update logic - void Update() - { - Time.timeScale = 0; - - // Download new PolyMod DLL and BepInEx files - File.WriteAllBytes( - Path.Combine(Plugin.BASE_PATH, "PolyMod.new.dll"), - client.GetAsync(latest?.GetProperty("assets")[0].GetProperty("browser_download_url").GetString()!).UnwrapAsync() - .Content.ReadAsByteArrayAsync().UnwrapAsync() - ); - using ZipArchive bepinex = new(client.GetAsync(bepinex_url).UnwrapAsync().Content.ReadAsStream()); - bepinex.ExtractToDirectory(Path.Combine(Plugin.BASE_PATH, "New"), overwriteFiles: true); - - // Create and run the appropriate update script for the OS - ProcessStartInfo info = new() - { - WorkingDirectory = Path.Combine(Plugin.BASE_PATH), - CreateNoWindow = true, - }; - if (Application.platform == RuntimePlatform.WindowsPlayer) - { - string batchPath = Path.Combine(Plugin.BASE_PATH, "update.bat"); - File.WriteAllText(batchPath, $@" - @echo off - echo Waiting for Polytopia.exe to exit... - :waitloop - tasklist | findstr /I ""Polytopia.exe"" >nul - if not errorlevel 1 ( - timeout /T 1 >nul - goto waitloop - ) - - echo Updating... - robocopy ""New"" . /E /MOVE /NFL /NDL /NJH /NJS /NP >nul - rmdir /S /Q ""New"" - del /F /Q ""BepInEx\plugins\PolyMod.dll"" - move /Y ""PolyMod.new.dll"" ""BepInEx\plugins\PolyMod.dll"" - - echo Launching game... - start steam://rungameid/874390 - timeout /T 3 /NOBREAK >nul - exit - "); - info.FileName = "cmd.exe"; - info.Arguments = $"/C start \"\" \"{batchPath}\""; - info.WorkingDirectory = Plugin.BASE_PATH; - info.CreateNoWindow = true; - info.UseShellExecute = false; - } - if (Application.platform == RuntimePlatform.LinuxPlayer || Application.platform == RuntimePlatform.OSXPlayer) - { - string bashPath = Path.Combine(Plugin.BASE_PATH, "update.sh"); - File.WriteAllText(bashPath, $@" - #!/bin/bash - - echo ""Waiting for Polytopia to exit..."" - while pgrep -x ""Polytopia"" > /dev/null; do - sleep 1 - done - - echo ""Updating..."" - mv New/* . && rm -rf New - rm -f BepInEx/plugins/PolyMod.dll - mv -f PolyMod.new.dll BepInEx/plugins/PolyMod.dll - - echo ""Launching game..."" - xdg-open steam://rungameid/874390 & - - sleep 3 - exit 0 - "); - - System.Diagnostics.Process chmod = new System.Diagnostics.Process(); - chmod.StartInfo.FileName = "chmod"; - chmod.StartInfo.Arguments = $"+x \"{bashPath}\""; - chmod.StartInfo.UseShellExecute = false; - chmod.StartInfo.CreateNoWindow = true; - chmod.Start(); - chmod.WaitForExit(); - - info.FileName = "/bin/bash"; - info.Arguments = $"\"{bashPath}\""; - info.WorkingDirectory = Plugin.BASE_PATH; - info.CreateNoWindow = true; - info.UseShellExecute = false; - } - Process.Start(info); - Application.Quit(); - } - - // Show a popup to the user asking if they want to update - PopupManager.GetBasicPopupWithData(new( - Localization.Get("polymod.autoupdate"), - Localization.Get("polymod.autoupdate.description"), - new(new PopupBase.PopupButtonData[] { - new( - "polymod.autoupdate.update", - PopupBase.PopupButtonData.States.None, - (Il2CppSystem.Action)Update - ) - })) - ).Show(); - } - catch (Exception e) - { - Plugin.logger.LogError($"Failed to check updates: {e.Message}"); - } - } - - /// - /// Initializes the AutoUpdate manager by patching the necessary methods. - /// - internal static void Init() - { - Harmony.CreateAndPatchAll(typeof(AutoUpdate)); - } -} \ No newline at end of file diff --git a/src/Managers/Hub.cs b/src/Managers/Hub.cs index 57da0b6..75ff255 100644 --- a/src/Managers/Hub.cs +++ b/src/Managers/Hub.cs @@ -366,25 +366,13 @@ internal static PopupButtonData[] CreateConfigPopupButtonData() new Il2CppSystem.Object[] { Localization.Get("polymod.debug", new Il2CppSystem.Object[]{}).ToUpperInvariant() } ); - string autoUpdateButtonName = Localization.Get( - Plugin.config.autoUpdate ? "polymod.hub.config.disable" : "polymod.hub.config.enable", - new Il2CppSystem.Object[] { Localization.Get("polymod.autoupdate", - new Il2CppSystem.Object[]{}).ToUpperInvariant() } - ); - string includeAlphasButtonName = Localization.Get( - Plugin.config.updatePrerelease ? "polymod.hub.config.disable" : "polymod.hub.config.enable", - new Il2CppSystem.Object[] { Localization.Get("polymod.autoupdate.alpha", - new Il2CppSystem.Object[]{}).ToUpperInvariant() } - ); popupButtons.Add(new PopupButtonData(debugButtonName, PopupButtonData.States.None, DelegateSupport.ConvertDelegate(OnDebugButtonClicked), -1, true, null)); - popupButtons.Add(new PopupButtonData(autoUpdateButtonName, PopupButtonData.States.None, DelegateSupport.ConvertDelegate(OnAutoUpdateButtonClicked), -1, true, null)); - popupButtons.Add(new PopupButtonData(includeAlphasButtonName, Plugin.config.autoUpdate ? PopupButtonData.States.None : PopupButtonData.States.Disabled, DelegateSupport.ConvertDelegate(OnIncludeAlphasButtonClicked), -1, true, null)); } return popupButtons.ToArray(); void OnDebugButtonClicked() { - Plugin.config = new(debug: !Plugin.config.debug, autoUpdate: Plugin.config.autoUpdate, updatePrerelease: Plugin.config.updatePrerelease); + Plugin.config = new(debug: !Plugin.config.debug); Plugin.WriteConfig(); Plugin.UpdateConsole(); NotificationManager.Notify(Localization.Get( @@ -395,32 +383,6 @@ void OnDebugButtonClicked() isConfigPopupActive = false; } - void OnAutoUpdateButtonClicked() - { - Plugin.config = new(debug: Plugin.config.debug, autoUpdate: !Plugin.config.autoUpdate, updatePrerelease: Plugin.config.updatePrerelease); - Plugin.WriteConfig(); - Plugin.UpdateConsole(); - NotificationManager.Notify(Localization.Get( - "polymod.config.setto", - new Il2CppSystem.Object[] { Localization.Get("polymod.autoupdate", - new Il2CppSystem.Object[]{}), Plugin.config.autoUpdate } - )); - isConfigPopupActive = false; - } - - void OnIncludeAlphasButtonClicked() - { - Plugin.config = new(debug: Plugin.config.debug, autoUpdate: Plugin.config.autoUpdate, updatePrerelease: !Plugin.config.updatePrerelease); - Plugin.WriteConfig(); - Plugin.UpdateConsole(); - NotificationManager.Notify(Localization.Get( - "polymod.config.setto", - new Il2CppSystem.Object[] { Localization.Get("polymod.autoupdate.alpha", - new Il2CppSystem.Object[]{}), Plugin.config.updatePrerelease } - )); - isConfigPopupActive = false; - } - void OnUpdateSpritesButtonClicked() { UpdateSpriteInfos(); diff --git a/src/Plugin.cs b/src/Plugin.cs index 8cba2b9..30dc1de 100644 --- a/src/Plugin.cs +++ b/src/Plugin.cs @@ -20,12 +20,8 @@ public partial class Plugin : BepInEx.Unity.IL2CPP.BasePlugin /// Represents the configuration for PolyMod. /// /// Whether to enable debug mode. - /// Whether to automatically update PolyMod. - /// Whether to include pre-release versions when updating. internal record PolyConfig( bool debug = false, - bool autoUpdate = true, - bool updatePrerelease = false, bool allowUnsafeIndexes = false, string backendUrl = Multiplayer.Client.DEFAULT_SERVER_URL, string overrideDeviceId = "" @@ -126,8 +122,6 @@ public override void Load() logger = Log; ConfigFile.CoreConfig[new("Logging.Disk", "WriteUnityLog")].BoxedValue = true; - AutoUpdate.Init(); - Compatibility.Init(); Audio.Init(); From 7344e2ec2cee96cd6cbda5ba8d8cb17812d8e688 Mon Sep 17 00:00:00 2001 From: Maksym Muraviov$ Date: Sat, 19 Sep 2026 20:15:21 +0200 Subject: [PATCH 30/34] feat: added named pipe stuff and popup on invalid install --- resources/localization.json | 12 ++++++++++ src/Loader.cs | 5 +++++ src/Managers/Compatibility.cs | 33 ++++++++++++++++++++++++++- src/Plugin.cs | 42 ++++++++++++++++++++++++++++++++--- 4 files changed, 88 insertions(+), 4 deletions(-) diff --git a/resources/localization.json b/resources/localization.json index 4b1ca28..38b6009 100644 --- a/resources/localization.json +++ b/resources/localization.json @@ -282,5 +282,17 @@ "English": "PROCEED", "Russian": "ПРОДОЛЖИТЬ", "Elyrion": "πrȱ#∑∑Δ" + }, + "polymod_title_invalid": { + "English": "Invalid Launch", + "Russian": "Неверный Запуск" + }, + "polymod_description_invalid": { + "English": "PolyMod launch was invalid. Please, open the game via the PolyMod Launcher or proceed to our site to install it :)", + "Russian": "PolyMod запущен неккоректно. Пожалуйста, откройте игру через PolyMod Launcher или перейдите на наш сайт и установите его :)" + }, + "buttons_download_launcher": { + "English": "OUR SITE", + "Russian": "НАШ САЙТ" } } diff --git a/src/Loader.cs b/src/Loader.cs index 69fe92b..0460fd0 100644 --- a/src/Loader.cs +++ b/src/Loader.cs @@ -346,6 +346,11 @@ ref manifest Plugin.logger.LogError($"Mod id {manifest.id} is invalid in {modContainer}"); continue; } + if (Plugin.config.disabledMods.Contains(manifest.id)) + { + Plugin.logger.LogError($"Mod {manifest.id} is disabled, skipping"); + continue; + } if (manifest.version == null) { Plugin.logger.LogError($"Mod version not found in {modContainer}"); diff --git a/src/Managers/Compatibility.cs b/src/Managers/Compatibility.cs index 4d39d70..9cb6c64 100644 --- a/src/Managers/Compatibility.cs +++ b/src/Managers/Compatibility.cs @@ -1,7 +1,7 @@ using HarmonyLib; +using Il2CppInterop.Runtime; using System.Text; using UnityEngine; -using UnityEngine.EventSystems; namespace PolyMod.Managers; @@ -20,6 +20,7 @@ internal static class Compatibility /// internal static bool shouldResetSettings = false; private static bool sawSignatureWarning; + private const string downloadLink = "https://polymod.dev/download/"; /// /// Whether all loaded mods are client only. If at least one non client only mod exists this returns false. @@ -97,6 +98,36 @@ private static bool CheckSignatures(Action action, Il2CppSystem.Guid gameId) [HarmonyPatch(typeof(StartScreen_UI2), nameof(StartScreen_UI2.OnShow))] private static void StartScreen_UI2_OnShow() { + if(!Plugin.ValidLaunch) + { + PopupManager.GetBasicPopupWithData( + new( + Localization.Get("polymod.title.invalid"), + Localization.Get("polymod.description.invalid"), + new PopupBase.PopupButtonData[] { + new( + "buttons.exitgame", + PopupBase.PopupButtonData.States.None, + callback: (Il2CppSystem.Action)Application.Quit, + closesPopup: false + ), + new( + "buttons.download.launcher", + PopupBase.PopupButtonData.States.None, + callback: DelegateSupport.ConvertDelegate(OpenSite), + closesPopup: false + ) + } + ) + ).Show(); + + void OpenSite() + { + NativeHelpers.OpenURL(downloadLink, false); + Application.Quit(); + } + return; + } string lastChecksum = checksum; try { diff --git a/src/Plugin.cs b/src/Plugin.cs index 30dc1de..80d5b6e 100644 --- a/src/Plugin.cs +++ b/src/Plugin.cs @@ -1,4 +1,5 @@ -using System.Reflection; +using System.IO.Pipes; +using System.Reflection; using System.Text.Json; using BepInEx; using BepInEx.Configuration; @@ -16,6 +17,10 @@ namespace PolyMod; [BepInPlugin("com.polymod", "PolyMod", VERSION)] public partial class Plugin : BepInEx.Unity.IL2CPP.BasePlugin { + internal static string? Token { get; set; } + internal static bool ValidLaunch { get; set; } = false; + internal static bool Disabled { get; set; } = false; + /// /// Represents the configuration for PolyMod. /// @@ -25,7 +30,10 @@ internal record PolyConfig( bool allowUnsafeIndexes = false, string backendUrl = Multiplayer.Client.DEFAULT_SERVER_URL, string overrideDeviceId = "" - ); + ) + { + public List disabledMods { get; init; } = new(); + }; /// /// The starting index for automatically assigned IDs. @@ -122,8 +130,12 @@ public override void Load() logger = Log; ConfigFile.CoreConfig[new("Logging.Disk", "WriteUnityLog")].BoxedValue = true; - Compatibility.Init(); + CaptureLaunchData(); + if(Disabled) + return; + + Compatibility.Init(); Audio.Init(); Loc.Init(); Visual.Init(); @@ -136,6 +148,30 @@ public override void Load() AndroidHandler.Init(); } + internal static void CaptureLaunchData() + { + try + { + using var client = new NamedPipeClientStream( + ".", "PolyMod.Launcher", PipeDirection.In, PipeOptions.CurrentUserOnly); + client.Connect(2000); + + using var reader = new StreamReader(client); + var line = reader.ReadLine(); + if (string.IsNullOrEmpty(line)) return; + + using var doc = JsonDocument.Parse(line); + var root = doc.RootElement; + Plugin.ValidLaunch = root.GetProperty("validLaunch").GetBoolean(); + Plugin.Disabled = root.GetProperty("disabled").GetBoolean(); + Plugin.Token = root.GetProperty("token").GetString() ?? ""; + } + catch + { + Plugin.ValidLaunch = false; + } + } + /// /// Gets a resource stream from the assembly. /// From 0cdc58d6ed2334e235c4a2f4174ed3a8ca14609d Mon Sep 17 00:00:00 2001 From: Maksym Muraviov$ Date: Thu, 24 Sep 2026 11:34:45 +0200 Subject: [PATCH 31/34] feat: removed launcher --- installer/build.py | 19 ----- installer/icon.ico | Bin 116829 -> 0 bytes installer/main.py | 154 ------------------------------------- installer/requirements.txt | 4 - 4 files changed, 177 deletions(-) delete mode 100644 installer/build.py delete mode 100644 installer/icon.ico delete mode 100644 installer/main.py delete mode 100644 installer/requirements.txt diff --git a/installer/build.py b/installer/build.py deleted file mode 100644 index 8b802ae..0000000 --- a/installer/build.py +++ /dev/null @@ -1,19 +0,0 @@ -import os -import PyInstaller.__main__ - -ROOT = os.path.abspath(os.getcwd()) - -PyInstaller.__main__.run( - [ - "--noconfirm", - "--onefile", - "--windowed", - "--icon", - ROOT + "/icon.ico", - "--name", - "PolyMod", - "--add-data", - ROOT + "/icon.ico:.", - ROOT + "/main.py", - ] -) diff --git a/installer/icon.ico b/installer/icon.ico deleted file mode 100644 index f52643a3ebe810f2156f072e7a17e4d7c7bd2821..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 116829 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�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 nc1Sy&CHsV}2K()>_6l00000NkvXXu0mjfcyCBQ diff --git a/installer/main.py b/installer/main.py deleted file mode 100644 index e1fad1b..0000000 --- a/installer/main.py +++ /dev/null @@ -1,154 +0,0 @@ -import os -import io -import sys -import shutil -import zipfile -import requests -import threading -import subprocess -import customtkinter -import CTkMessagebox as messagebox - -OS = { - "linux": "linux", - "linux2": "linux", - "win32": "win", - "darwin": "macos", -}[sys.platform] -BEPINEX = "https://polymod.dev/data/bepinex.txt" -POLYMOD = "https://api.github.com/repos/PolyModdingTeam/PolyMod/releases" - - -def resource_path(path): - try: - base_path = sys._MEIPASS - except Exception: - base_path = os.path.abspath(".") - return os.path.join(base_path, path) - - -def to_zip(request: requests.Response): - return zipfile.ZipFile(io.BytesIO(request.content)) - - -def browse(): - global path_entry - path_entry.delete(0, customtkinter.END) - path_entry.insert(0, customtkinter.filedialog.askdirectory()) - - -def prepare(target): - global progress_bar - path = path_entry.get() - try: - if "Polytopia_Data" not in os.listdir(path): - raise FileNotFoundError - except FileNotFoundError: - messagebox.CTkMessagebox( - title="Error", - message="The folder does not exist or is not valid!", - icon="cancel", - width=100, - height=50 - ) - return - path_entry.configure(state=customtkinter.DISABLED) - browse_button.configure(state=customtkinter.DISABLED) - prerelease_checkbox.destroy() - install_button.destroy() - uninstall_button.destroy() - progress_bar = customtkinter.CTkProgressBar(app, determinate_speed=50 / 2) - progress_bar.grid(column=0, row=1, columnspan=2, padx=5, pady=5) - progress_bar.set(0) - threading.Thread(target=target, daemon=True, args=(path, )).start() - - -def install(path): - to_zip( - requests.get( - requests.get(BEPINEX).text.strip().replace("{os}", OS) - ) - ).extractall(path) - progress_bar.step() - - for release in requests.get(POLYMOD).json(): - if release["prerelease"] and not prerelease_checkbox.get(): continue - latest = release - break - open(path + "/BepInEx/plugins/PolyMod.dll", "wb").write( - requests.get(latest["assets"][0]["browser_download_url"]).content - ) - progress_bar.step() - - customtkinter.CTkButton(app, text="Launch", command=lambda: launch(path)).grid( - column=0, row=2, columnspan=2, padx=5, pady=5 - ) - - -def uninstall(path): - dirs = [ - "BepInEx", - "dotnet", - ] - files = [ - ".doorstop_version", - "changelog.txt", - "doorstop_config.ini", - "winhttp.dll", # windows - "libdoorstop.so", # linux - "libdoorstop.dylib", # mac - "run_bepinex.sh", # linux + mac - ] - for dir in dirs: - shutil.rmtree(path + "/" + dir, True) - progress_bar.step() - for file in files: - try: - os.remove(path + "/" + file) - except FileNotFoundError: - ... - progress_bar.step() - customtkinter.CTkButton(app, text="Quit", command=quit).grid( - column=0, row=2, columnspan=2, padx=5, pady=5 - ) - - -def launch(path): - if sys.platform != "win32": - subprocess.check_call(f"chmod +x {path}/run_bepinex.sh", shell=True) - subprocess.check_call(f"{path}/run_bepinex.sh {path}/Polytopia.*", shell=True) - subprocess.check_call(f"xdg-open https://docs.bepinex.dev/articles/advanced/steam_interop.html", shell=True) - else: - subprocess.check_call(f"start steam://rungameid/874390", shell=True) - quit() - - -def quit(): - app.destroy() - sys.exit() - - -app = customtkinter.CTk() -app.title("PolyMod") -if OS != "linux": - app.iconbitmap(default=resource_path("icon.ico")) -app.resizable(False, False) - -path_entry = customtkinter.CTkEntry( - app, placeholder_text="Game path", width=228) -browse_button = customtkinter.CTkButton( - app, text="Browse", command=browse, width=1) -prerelease_checkbox = customtkinter.CTkCheckBox( - app, text="Prerelease", width=1) -install_button = customtkinter.CTkButton( - app, text="Install", command=lambda: prepare(install)) -uninstall_button = customtkinter.CTkButton( - app, text="Uninstall", command=lambda: prepare(uninstall)) - -path_entry.grid(column=0, row=0, padx=5, pady=5) -browse_button.grid(column=1, row=0, padx=(0, 5), pady=5) -prerelease_checkbox.grid(column=0, row=1, columnspan=2, padx=5, pady=5) -install_button.grid(column=0, row=2, columnspan=2, padx=5, pady=5) -uninstall_button.grid(column=0, row=3, columnspan=2, padx=5, pady=5) - -app.mainloop() diff --git a/installer/requirements.txt b/installer/requirements.txt deleted file mode 100644 index c396099..0000000 --- a/installer/requirements.txt +++ /dev/null @@ -1,4 +0,0 @@ -requests -pyinstaller -customtkinter -CTkMessagebox \ No newline at end of file From 5717c336945cd56c056bb795cf56fc9ee7b77203 Mon Sep 17 00:00:00 2001 From: Maksym Muraviov$ Date: Thu, 24 Sep 2026 12:52:44 +0200 Subject: [PATCH 32/34] feat: removed basic popup legacy from config --- src/Managers/Hub.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Managers/Hub.cs b/src/Managers/Hub.cs index 75ff255..fad736e 100644 --- a/src/Managers/Hub.cs +++ b/src/Managers/Hub.cs @@ -278,11 +278,11 @@ internal static void DumpData() /// internal static void ShowConfigPopup() { - BasicPopupLegacy polymodPopup = Visual.GetBasicPopupLegacy(); + BasicPopup polymodPopup = PopupManager.GetBasicPopup(); polymodPopup.Header = Localization.Get("polymod.hub.config"); polymodPopup.Description = ""; polymodPopup.buttonData = CreateConfigPopupButtonData(); - polymodPopup.ShowSetWidth(POPUP_WIDTH); + polymodPopup.Show(); } internal static void ShowPolyModHub() From ec1572a2c85eac4fc14db749889e81130531bbbb Mon Sep 17 00:00:00 2001 From: Maksym Muraviov$ Date: Thu, 24 Sep 2026 12:57:46 +0200 Subject: [PATCH 33/34] feat: removed installer.yml --- .github/workflows/installer.yml | 43 --------------------------------- 1 file changed, 43 deletions(-) delete mode 100644 .github/workflows/installer.yml diff --git a/.github/workflows/installer.yml b/.github/workflows/installer.yml deleted file mode 100644 index f75f773..0000000 --- a/.github/workflows/installer.yml +++ /dev/null @@ -1,43 +0,0 @@ -name: Installer -on: - push: - paths: - - 'installer/**' - pull_request: - paths: - - 'installer/**' -jobs: - job: - defaults: - run: - working-directory: ./installer - strategy: - fail-fast: false - matrix: - os: [windows-latest, ubuntu-22.04, macos-13] - name: Build on ${{ matrix.os }} - runs-on: ${{ matrix.os }} - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 - with: - python-version: '3.11' - - name: Install requirements - run: pip install -r requirements.txt - - name: Build - run: python build.py - - uses: actions/upload-artifact@v4 - with: - name: ${{ matrix.os }} - path: installer/dist/ - merge: - name: Merge artifacts - runs-on: ubuntu-latest - needs: job - steps: - - uses: actions/upload-artifact/merge@v4 - with: - name: artifacts - delete-merged: true - separate-directories: true - From 97b749aad20708f7e0f903939dd03dd709460791 Mon Sep 17 00:00:00 2001 From: Maksym Muraviov$ Date: Thu, 1 Oct 2026 12:04:44 +0200 Subject: [PATCH 34/34] feat: android check --- src/Managers/Compatibility.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Managers/Compatibility.cs b/src/Managers/Compatibility.cs index 9cb6c64..57d2811 100644 --- a/src/Managers/Compatibility.cs +++ b/src/Managers/Compatibility.cs @@ -98,7 +98,7 @@ private static bool CheckSignatures(Action action, Il2CppSystem.Guid gameId) [HarmonyPatch(typeof(StartScreen_UI2), nameof(StartScreen_UI2.OnShow))] private static void StartScreen_UI2_OnShow() { - if(!Plugin.ValidLaunch) + if(!Plugin.ValidLaunch && Application.platform != RuntimePlatform.Android) { PopupManager.GetBasicPopupWithData( new(