Skip to content
Draft
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -406,4 +406,6 @@ dist/

# End of https://www.toptal.com/developers/gitignore/api/csharp

run.bat
run.bat

.idea/
2 changes: 1 addition & 1 deletion PolyMod.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
</RestoreAdditionalProjectSources>
<Configurations>IL2CPP</Configurations>
<RootNamespace>PolyMod</RootNamespace>
<Version>1.2.17</Version>
<Version>1.3.0-pre-android-1</Version>
<PolytopiaVersion>2.17.2.16299</PolytopiaVersion>
<Authors>PolyModdingTeam</Authors>
<Description>The Battle of Polytopia's mod loader.</Description>
Expand Down
Binary file added resources/dystopia_icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
89 changes: 89 additions & 0 deletions src/Android/AndroidHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
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));
}

/// <summary>
/// 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.
/// </summary>
[HarmonyPrefix]
[HarmonyPatch(typeof(GameManager), nameof(GameManager.IsMultiplayerEnabled), MethodType.Getter)]
public static bool GameManager_IsMultiplayerEnabled(ref bool __result)
{
__result = true;
return false;
}

/// <summary>
/// Replace the Android login flow to skip Google Play Games SDK entirely.
/// Uses deviceUniqueIdentifier as the auth code for the Polydystopia backend.
/// </summary>
[HarmonyPrefix]
[HarmonyPatch(typeof(PolytopiaBackendAdapter), "LoginPlatformAndroid")]
public static bool LoginPlatformAndroid_Prefix(
ref Il2CppSystem.Threading.Tasks.Task<ServerResponse<PolytopiaToken>> __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;
}

/// <summary>
/// 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.
/// </summary>
[HarmonyPrefix]
[HarmonyPatch(typeof(AnalyticsManager), nameof(AnalyticsManager.IsAnalyticsEnabled))]
private static bool AnalyticsManager_IsAnalyticsEnabled(ref bool __result)
{
__result = false;
return false;
}

/// <summary>
/// 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. isFirebaseInitialized deliberately stays false:
/// pretending Firebase is up could wake isFirebaseInitialized-guarded code paths
/// (e.g. HandleOpenedThroughNotification on every app resume).
/// </summary>
[HarmonyPrefix]
[HarmonyPatch(typeof(FirebaseMessagingManager), nameof(FirebaseMessagingManager.Init))]
private static bool FirebaseMessagingManager_Init()
{
return false;
}

/// <summary>
/// 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.
/// </summary>
[HarmonyPrefix]
[HarmonyPatch(typeof(FirebaseMessagingManager), nameof(FirebaseMessagingManager.InitAsync))]
private static bool FirebaseMessagingManager_InitAsync(ref Il2CppSystem.Threading.Tasks.Task __result)
{
__result = Il2CppSystem.Threading.Tasks.Task.CompletedTask;
return false;
}
}
9 changes: 9 additions & 0 deletions src/Managers/Compatibility.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,15 @@ internal static class Compatibility
internal static bool shouldResetSettings = false;
private static bool sawSignatureWarning;

/// <summary>
/// Whether all loaded mods are client only. If at least one non client only mod exists this returns false.
/// </summary>
/// <returns></returns>
public static bool IsClientOnly()
{
return Registry.mods.Select(modPair => modPair.Value).All(mod => mod.client);
}

/// <summary>
/// Hashes the signatures of all loaded mods to create a checksum.
/// </summary>
Expand Down
94 changes: 92 additions & 2 deletions src/Managers/Visual.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ public record SkinInfo(int idx, string id, SkinData? skinData);

/// <summary>A dictionary of custom widths for basic popups.</summary>
public static Dictionary<int, int> basicPopupWidths = new();
/// <summary>Original font sizes of popup buttons, so relayouts rescale from the prefab size.</summary>
private static readonly Dictionary<int, float> popupButtonFontSizes = new();
/// <summary>Original scroll viewport bottom offsets of popups, so relayouts don't compound.</summary>
private static readonly Dictionary<int, float> popupScrollBottoms = new();
/// <summary>Represents information about a unit prefab.</summary>
public struct UnitPrefabInfo
{
Expand Down Expand Up @@ -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<BasicPopupLegacy>();
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<List<UITextButton>> 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<RectTransform>();
int popupId = popup.GetInstanceID();
if (!popupScrollBottoms.ContainsKey(popupId))
popupScrollBottoms[popupId] = viewport.offsetMin.y;
viewport.offsetMin = new Vector2(viewport.offsetMin.x, popupScrollBottoms[popupId] + extra);
}
}

/// <summary>Sets the attacker's tribe before a unit attacks.</summary>
Expand Down Expand Up @@ -809,12 +892,19 @@ private static void WeaponGFX_SetSkin(WeaponGFX __instance, SkinType skinType)
}
}

/// <summary>Removes a popup's custom width when it is hidden.</summary>
/// <summary>Removes a popup's custom width and cached button font sizes when it is hidden.</summary>
[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<BasicPopupLegacy>();
if (legacy != null && legacy.buttonContainer != null && legacy.buttonContainer.Buttons != null)
{
foreach (UITextButton button in legacy.buttonContainer.Buttons)
popupButtonFontSizes.Remove(button.GetInstanceID());
}
}

[HarmonyPrefix]
Expand Down
Loading
Loading