This directory contains the first editable skeleton for the extracted Android compatibility MOD / Harmony patcher. It is intentionally not a copy of the old full game source.
Current implementation (STS2AndroidPortCompat):
ModEntryexposes the same unmanaged entrypoints used by the reference launcher (InitializeGodotSharp,Apply).PlatformPatchesdisables desktop Steam/Sentry/platform paths.RunHistoryPatcheskeeps persistedPlatformType.Steamrun histories readable without loading desktopsteam_api64: only history-screen identity lookups use the Android/null-platform ID and cached-name fallback, while Steam transport, lobby, and invite behavior remains untouched.ReleaseInfoPatchesreadsrelease_info.jsonfrom the imported private payload atOS.GetDataDir()/game/release_info.json.AndroidSettingsBridgereads extra-settings JSON fromOS.GetDataDir()/default/1/settings.savewithout requiring PCSettingsSaveto contain Android-only fields.AndroidSettingsPatchesmaps companion JSON fields that also exist in the PCSettingsSave(aspect_ratio,vsync,msaa,fps_limit,fullscreen), maps companionmod_settings.mods_enabled/mod_list/ legacydisabled_modsinto the runtimeModSettings, and merges Android-only JSON keys back after PCSettingsSaveserialization would drop them.DisplaySettingsPatchesapplies Android-only companion fields for FPS, global content scale, UI font scale, and landscape orientation. It is the sole coordinator for root-windowContentScaleMode,ContentScaleAspect, andContentScaleSize; logical layout always usesCanvasItems, with the ownership orderFixedAspect > UiScaleAuto. Auto uses the UI-scale target and fixed aspect uses its corresponding fixed target.fullscreen_render_sizenever owns or replaces that logical target and Java no longer forwards it as Godot--resolution; changing it in the in-game settings immediately resizes only the root renderer render target. After all high-levelContentScale*setters finish, the coordinator appliesRenderingServer.ViewportSetRenderDirectToScreen(false),ViewportSetSize(), andViewportSetGlobalCanvasTransform(). The sceneWindow, its input transform, and the AndroidSurfacestay unchanged. Do not useSurfaceHolder.setFixedSize()orViewportAttachToScreen()for this path.0x0restores both the native attachment-sized render target and the base canvas transform. A non-zero preset is a minimum reference rectangle: the effective target keeps the current native attachment aspect and uses Expand-style coverage (for example, native2400x1080plus1280x720becomes1600x720). The custom longest-dimension cap ismax(4096, native longest dimension). Root-windowSizeChanged, application resume, and consistency repair reapply the renderer state after logical setters. Ownership is published before any compare-before-set Window mutation, reentrant requests are coalesced, and application resume schedules one deferred runtime apply instead of rebuilding the viewport from focus notifications.UiScalePatchesonly supplies the Auto target and requests a single-flight recalculation; it never writesContentScale*directly.global_scaleremains an independentContentScaleFactorunder every owner, and UI font scale remains independent. Each resume generation performs one deferred consistency check and at most one compare-before-set repair; stale targets are rejected by revision and a failed final check only logs a warning instead of entering a viewport rebuild loop.MerchantLayoutPatchesanimates the shop panel's anchor-relative offset against the inventory's live Control height, not the rootContentScaleSize. Global scale and UI scale can be applied before opening, after opening, or during the animation without a stale absolute Y target pushing the bottom row offscreen. Item sizes, purchase behavior, and the original easing remain unchanged; this does not shrink oversized content to fit arbitrarily high zoom levels.MobileHandLayoutPatchesapplies the companionshow_more_hand_card_text/show_more_hand_card_text_lift_height_percenthand lift as a Harmony post-layout offset without rebuilding the game body.DevTools/hosts the file-based Java bridge for the in-game overlay. It is started independently from optional version-specific feature patches, writes alauncher/devtools/host.jsonready marker, and answers protocol-2 requests in their own atomicresponse-<uuid>.jsonfiles (while retaining legacyresponse.jsoncompatibility): reflection inspector, collapsible Godot scene tree, nested Godot object / node property inspection, and temporary GDScript execution with non-Nil result capture, companion settings runtime apply, and overlay quick-restart.QuickRestartPatchesadds the built-in Android retry button on the pause menu whenquick_sl_enabledis true and no external Quick Restart UI mod is loaded; it waits for pending run-save work, awaits saved-run setup before loading the new run, and fades back in on failure so async restart errors do not leave a permanent black transition screen.ExternalSettingsPatchesadds a fallback in-game settings row that opens the Java companion settings shell, redirects game Quit back to the settings shell, and applies the companionpending_unlock_all.flagcommand.ModLoaderPatchesredirects local mods toOS.GetDataDir()/modsand skips Steam mod enumeration. Runtime manifest aliases accept only file basenames and never follow symlink files or directories. Rundotnet run --project tests/ModManifestAlias.Tests/ModManifestAlias.Tests.csprojfor isolated traversal, Unicode-name and symlink regressions (no game assemblies required).ModelDbInitPatchkeeps early vanilla models in a shadow registry until phase 1. Canonical-dictionary keyed reads cover generic, Type, ID and category lookups without patching shared generic method bodies. Existing canonical values and original casts/errors take precedence. Publication preserves object identity and removes the temporary read prefixes.DeferredModPatchQueueprotects Android/Mono from user-MOD patches that eagerly initialize STS2 UI/Godot/model types or read ModelDb before essential startup. It covers directPatchProcessor.Patch(), the per-target privatePatchClassProcessor.ProcessPatchJob()path used byHarmony.PatchAll(), andHarmonyTargetMethods/HarmonyTargetMethodfactories whose target discovery reads ModelDb. It also defers resource types whose static initialization reads models or consumes modded pools, including helper and iterator calls. ID-only discovery and safe registration remain immediate; unsafe jobs retain their original Harmony owner, patch lists, ordering, and per-target prepare/cleanup flow and replay once after model/network type initialization. A syntheticsts2fixture regression is available throughtools/test-deferred-mod-patch-queue.sh; it covers direct patches, PatchAll jobs, target factories, resource cctors, pool registration/freeze boundaries, failure isolation and duplicate-flush idempotence.ShaderCompatibilityPatchesloadsport_compat.pckand applies the mobile shader replacements copied from the old port whenshader_compatibility_modeis enabled; it intentionally keeps the originalcanvas_group_mask_blur.gdshadercard/Ancient-card face shader and does not ship the old mobile substitute because it can render Ancient card faces solid white.AndroidSettingsMergepreservesandroid_compat_pack_enabledwhen the game serializes settings, including an explicitfalse. A later launch must not silently re-enable compatibility because the PC serializer omitted this key. Consumer round-trip:tests/AndroidSettingsMerge.Tests.TouchInputPatchesadds the first touch-friendly card-play cancellation path for releases outside the play zone / untargeted releases.MobileTapPreviewPatchesadds a first-pass tap-to-lift card preview flow using companiontouch_lift_preview/touch_lift_retap_actionsettings.MobileReactionButtonPatchesinjects the Android reaction-wheel button while reusing the payload's reaction synchronizer. Android detection accepts either the mobile feature tag or the Android OS name, and networking lifecycle hooks refresh visibility immediately. Visible multiplayer lobby/player containers and ready/wait overlays keep the button available before the run starts. Because the compat assembly uses plainMicrosoft.NET.Sdkwithout the game's Godot source-generated virtual callback dispatch, the dynamic button connectsControl.gui_inputandTimer.timeoutsignals explicitly; anNGame._Inputpostfix forwards active-pointer drag and release events. Button and wheel centers are resolved in viewport coordinates with the full CanvasItem transforms, then the wheel correction is converted back into its parent's coordinates. Before dispatch, that viewport center is also inverted throughNReactionContainer.GetGlobalTransformWithCanvas()so the payload'sDoLocalReactionreceives control-space coordinates for both the local animation and existing normalized network message. On first show, the compat path captures every anchored wedge's live neutral position and refreshes the payload's_defaultPositionafter responsive wheel-size changes; selection still moves one wedge radially, but deselection and a full sweep return all eight wedges to stable baselines. Diagnostics cover platform, setting, network, scene, geometry, press, wheel requested/actual centers, alignment error, dispatch viewport/control positions, stale payload wedge delta, reset correction, release/react, and failures while single-player stays hidden. The 250ms visibility timer queries a smallMobileReactionSurfaceTrackerindex, not the scene tree. The index seeds once and followsSceneTree.NodeAdded/NodeRemoved; hidden ancestors and current scene/overlay ownership remain part of visibility checks. Disabling the setting or leaving the tree disconnects and clears the index; reentry seeds it again. Do not restore recursiveGetChildren()polling: it allocates native-array/name wrappers even in single-player with the button hidden. Regression coverage lives intests/MobileReactionButton.Tests.AndroidInputCompatPatchesbridges Android back-button, two-finger inspect right-click, and trigger-axis controller compatibility into original input.ExtendedMultiplayerRoomPatcheskeeps the original multiplayer synchronizers authoritative while extending client room UI beyond the four prebuilt slots: treasure rooms create and lay out one holder per synchronized relic, use a bounded default focus target, spread award/fight hands around the screen, and rest sites create one ordered character container per player. A commercial- code-free synthetic regression is available throughtools/test-extended-multiplayer-rooms.sh. Older payloads without_relicContaineruse the existingContainerchild; default-focus protection does not depend on that optional field. Run the synthetic runner and nativeFramePreparation.Testswith both default and-p:LegacyTreasureShape=trueshapes. These fixtures do not replace real-game multiplayer verification of animation/completion and network synchronizers.LanMultiplayerPatchesbridges companion LAN settings while leaving the originalMessageTypesID assignment andNetMessageBusserialization/deserialization untouched. It adds configured compatibility mod names to multiplayer checks, honors persistent/custom LAN player IDs, replaces the no-Steam join screen with host/port input, and hosts ENet games with the configured player capacity. The v0.111 target passesPeerVersionInfo.LocalDefault()into the original host/client services and lets the original transport-levelHandshakeManagerown version, ModelDb hash, and MOD compatibility validation.- Layout scale subscriptions are owned by scene-node lifetime: detach on
TreeExiting, restore on reentry, and ignore repeated Ready registration. Old rooms and event layouts no longer need a later scale change to be collectible. - Main-menu scaling records neutral button/logo geometry per node. Repeated Ready
or scale notifications do not accumulate logo offsets; returning to 100% restores
anchors, offsets, grow directions, size flags and logo position without changing
the root display-scale owner. Native coverage:
tests/FramePreparation.Tests. - Font scaling distinguishes inherited sizes from existing explicit overrides. Returning to 100% removes only scaler-created overrides and releases the old baseline, so later theme changes and scaling cycles use current font sizes. Existing or externally replaced overrides and auto-size bounds remain owned by their original caller; font fallback selection is unchanged.
- Disabling preload gates only
PreloadManager.LoadAssets; the outer asset-set operation still performs cache and missed-set eviction. The resource disposal guard and protected warm-cache scope remain unchanged; no forced GC is added. - Learned warm assets use one atomic schema-2 snapshot capped at 512 paths and scoped to the launch profile, payload/compat assemblies, and ordered loaded-MOD identities/file metadata. Unscoped legacy lists or mismatched contexts are relearned.
- Optional combat animation warmup samples a separate native
SpineSpriteclone without scripts, signal connections, groups, or child autoplay nodes. It never triggers or rewinds the live creature animator; trigger-driven VFX/audio are no longer covered by animation warmup. Unsupported Spine APIs skip the preview. - Known Waterfall Giant/Orobas backgrounds only bound excessive preprocessing of
standard continuous ambient particles to one lifetime plus the original phase.
Burst, one-shot, trail, sub-emitter, custom-material and long-lived effects remain
unchanged. This mitigation is not evidence that reported crashes were OOM.
Synthetic lifetime/cache/particle regressions:
tools/test-resource-safety.sh.
Build locally from the parent repository after configuring .env:
../tools/android/build-port-mod.shOr build the schema-2 family compatibility pack from this submodule with local environment variables:
export DOTNET_BIN=/path/to/dotnet
export STS2_ORIGINAL_V1080_REFERENCE_DIR=/path/to/original-v0.108.0/bin/Debug
./tools/build-compat-matrix.sh --target v0.108.0
# Historical V1090 names identify the shared v0.109.x target; use the latest v0.109.1 gate.
export STS2_ORIGINAL_V1090_REFERENCE_DIR=/path/to/original-v0.109.1/bin/Debug
./tools/build-compat-matrix.sh --target v0.109.0
# Shared v0.110.x public-beta API/protocol target.
# Keep the historical V1100 variable/flavor name; point it at the latest v0.110.1 gate.
export STS2_ORIGINAL_V1100_REFERENCE_DIR=/path/to/original-v0.110.1/bin/Debug
./tools/build-compat-matrix.sh --target v0.110.0
# Current v0.111.0 public-beta API/handshake target.
export STS2_ORIGINAL_V1110_REFERENCE_DIR=/path/to/original-v0.111.0/bin/Debug
./tools/build-compat-matrix.sh --target v0.111.0tools/build-compat-pack.sh is the legacy schema-1 path; use it only when also providing a matching COMPAT_MANIFEST.
The patched Godot runtime expects STS2Mobile.dll / STS2Mobile.ModEntry; the parent build script builds this skeleton under that assembly name and copies it into android/assets/dotnet_bcl/.