From f556cc81f5cc2e60b83a5eb8175017ecffd39cb4 Mon Sep 17 00:00:00 2001 From: Demonofpower Date: Sun, 12 Apr 2026 21:28:38 +0200 Subject: [PATCH 1/7] 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 56c1f1145135560aa9d66bb5c4826b6a9a26af0d Mon Sep 17 00:00:00 2001 From: Demonofpower Date: Mon, 6 Jul 2026 22:12:44 +0200 Subject: [PATCH 2/7] 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 3/7] 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 4/7] 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 5/7] 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 6/7] 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 7/7] 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(); }