diff --git a/.codacy.yml b/.codacy.yml deleted file mode 100644 index 3f8b70fe1..000000000 --- a/.codacy.yml +++ /dev/null @@ -1,10 +0,0 @@ -# Only exclude non-source artifacts, generated files, and local mod mirrors. -# All source, tests, tooling, and docs must be analyzable with zero exclusions. -exclude_paths: - - "(new)codex(plans)/**" - - "1397421866(original mod)/**" - - "3447786229(submod)/**" - - "3661482670(cheat_mode_example)/**" - - "TestResults/**" - - "artifacts/**" - - "**/package-lock.json" diff --git a/.deepsource.toml b/.deepsource.toml deleted file mode 100644 index c14ad872f..000000000 --- a/.deepsource.toml +++ /dev/null @@ -1,33 +0,0 @@ -version = 1 - -test_patterns = ["tests/**"] - -exclude_patterns = [ - "native/build-*/**", - "native/runtime/**", - "TestResults/**", - "artifacts/**", - "coverage-100/**", - "1397421866(original mod)/**", - "3447786229(submod)/**", - "3661482670(cheat_mode_example)/**", -] - -[[analyzers]] -name = "csharp" -enabled = true - -[[analyzers]] -name = "python" -enabled = true - - [analyzers.meta] - runtime_version = "3.x" - -[[analyzers]] -name = "test-coverage" -enabled = true - - [analyzers.meta] - required_line_coverage = 100.0 - required_branch_coverage = 100.0 diff --git a/.github/codeql/codeql-config.yml b/.github/codeql/codeql-config.yml new file mode 100644 index 000000000..c2c9dead3 --- /dev/null +++ b/.github/codeql/codeql-config.yml @@ -0,0 +1,20 @@ +name: "SWFOC-Mod-Menu lean CodeQL config" + +# Scope CodeQL to FIRST-PARTY code only. The incorporated reverse-engineering +# editor trees are vendored / reproduced-for-provenance material, not the +# actively-maintained first-party trainer (src/, tools/, scripts/, native/). +# They are ALREADY out of scope for every other lean gate — ruff + basedpyright +# exclude them (ruff.toml extend-exclude / pyrightconfig.json exclude), gitleaks +# allowlists the vendored deps, osv ignores them. CodeQL is scoped consistently +# here so the lean "0 code-scanning alerts" bar reflects first-party code, not +# third-party libraries (Dear ImGui, MinHook) or one-shot RE +# tooling reproduced verbatim. This is a reasoned, greppable exclusion — not a +# suppression of a first-party finding. +paths-ignore: + - bridge + - swfoc_lua_bridge + - swfoc_overlay + - swfoc_toolkit + - ghidra_scripts + - re-findings + - docs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 312cbf4c4..5ef87f59b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -152,49 +152,11 @@ jobs: run: | pwsh ./tools/lua-harness/run-lua-harness.ps1 -Strict - - name: SonarCloud scan (with coverage) - if: always() - uses: SonarSource/sonarcloud-github-action@ffc3010689be73b8e5ae0c57ce35968afd7909e8 # v5 - env: - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - args: > - -Dsonar.cs.opencover.reportsPaths=TestResults/coverage/opencover.opencover.xml - -Dsonar.coverage.exclusions=tests/**,scripts/**,tools/**,native/** - continue-on-error: true - - - name: Upload coverage to Codecov - if: always() - uses: codecov/codecov-action@75cd11691c0faa626561e295848008c8a7dddffe # v5 - with: - token: ${{ secrets.CODECOV_TOKEN }} - files: TestResults/coverage/opencover.opencover.xml - flags: dotnet - fail_ci_if_error: false - - - name: Transform coverage for QLTY - if: always() - run: | - dotnet tool install -g dotnet-reportgenerator-globaltool 2>$null - reportgenerator -reports:TestResults/coverage/opencover.opencover.xml -targetdir:TestResults/coverage -reporttypes:Cobertura 2>$null - shell: pwsh - continue-on-error: true - - - name: Upload coverage to QLTY - if: always() - shell: pwsh - env: - QLTY_COVERAGE_TOKEN: ${{ secrets.QLTY_COVERAGE_TOKEN }} - run: | - if (Test-Path TestResults/coverage/Cobertura.xml) { - Invoke-WebRequest -Uri "https://qlty.sh/install.ps1" -OutFile install-qlty.ps1 - pwsh ./install-qlty.ps1 - qlty coverage publish --tag dotnet TestResults/coverage/Cobertura.xml - } else { - Write-Output "Coverage file not found, skipping QLTY upload" - } - continue-on-error: true + # Retired 2026-06-27: SonarCloud / Codecov / QLTY coverage uploaders removed + # as part of the lean-gate migration (SaaS reporters are RETIRED machinery; + # the lean 6-gate quality workflow in .github/workflows/quality.yml is the + # authoritative gate). These were already non-blocking (continue-on-error), + # so their removal does not change build-test pass/fail. - name: Upload Test Results if: always() diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 050f88913..85ed2f98c 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -22,10 +22,13 @@ jobs: actions: read contents: read security-events: write - uses: Prekzursil/quality-zero-platform/.github/workflows/reusable-codeql.yml@fbb2255753788b93cc79ec917c764d0fc4f52146 + uses: Prekzursil/quality-zero-platform/.github/workflows/reusable-codeql.yml@3e05256353a3da48308fceaafbd5389fb9188ae2 with: repo_slug: ${{ github.repository }} event_name: ${{ github.event_name }} sha: ${{ github.event.pull_request.head.sha || github.sha }} platform_repository: Prekzursil/quality-zero-platform platform_ref: main + # Scope analysis to first-party code; exclude vendored / provenance RE + # editor trees (consistent with ruff/basedpyright/gitleaks/osv exclusions). + codeql_config_file: .github/codeql/codeql-config.yml diff --git a/.gitignore b/.gitignore index 6d40fabc6..04bce3cbc 100644 --- a/.gitignore +++ b/.gitignore @@ -93,3 +93,14 @@ tmp_codacy_*.json # Cheat Engine tables (local RE workspace) *.ct + +# Incorporated RE editor source trees: ignore local build artifacts +*.o +*.obj +*.exe +*.dll +*.lib +*.exp +*.ilk +*.pdb +bridge/test_runs/ diff --git a/.gitleaks.toml b/.gitleaks.toml index b810e7129..99182228e 100644 --- a/.gitleaks.toml +++ b/.gitleaks.toml @@ -34,4 +34,12 @@ paths = [ # game data carry high-entropy bytes that are not credentials. '''.*\.CT$''', '''.*\.(meg|dat|bin|dll|exe|pdb|so)$''', + # Vendored third-party RE editor dependencies reproduced verbatim for + # provenance (2026-06-27 incorporation). These carry only upstream-example / + # high-entropy library constants, never our credentials: + # - Dear ImGui (swfoc_overlay/imgui): generic-api-key FP on a base64-looking + # lookup-table literal in imgui.cpp. + # - MinHook (swfoc_lua_bridge/minhook): vendored hooking library. + '''(^|/)swfoc_overlay/imgui/''', + '''(^|/)swfoc_lua_bridge/minhook/''', ] diff --git a/.jscpd.json b/.jscpd.json deleted file mode 100644 index 7bcc945c9..000000000 --- a/.jscpd.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "threshold": 1, - "minLines": 25, - "minTokens": 90, - "reporters": [ - "console", - "json" - ], - "output": "jscpd-report", - "format": [ - "csharp", - "powershell", - "json", - "yaml", - "markdown", - "javascript", - "python" - ], - "absolute": false, - "ignore": [ - "**/tests/SwfocTrainer.Tests/**", - "**/profiles/default/profiles/**", - "**/profiles/default/presets/**", - "**/bin/**", - "**/obj/**", - "**/.git/**", - "**/TestResults/**", - "**/artifacts/**", - "1397421866(original mod)/**", - "3447786229(submod)/**", - "3661482670(cheat_mode_example)/**" - ] -} diff --git a/.qlty/qlty.toml b/.qlty/qlty.toml deleted file mode 100644 index 6c2d002ad..000000000 --- a/.qlty/qlty.toml +++ /dev/null @@ -1,19 +0,0 @@ -config_version = "0" - -[[source]] -name = "default" -default = true - -[smells] -mode = "block" - -[coverage] -target = 100.0 - -[[smells.exclude]] -path = "tests/**" -reason = "Test files use repetitive patterns by design — duplicated setup/assert blocks are expected in xUnit test suites" - -[[smells.exclude]] -path = "tools/**" -reason = "Standalone Python scripts are utility tools — complexity thresholds are less actionable here" diff --git a/bridge/autonomous_live_test.ps1 b/bridge/autonomous_live_test.ps1 new file mode 100644 index 000000000..4d3feeaaa --- /dev/null +++ b/bridge/autonomous_live_test.ps1 @@ -0,0 +1,181 @@ +# autonomous_live_test.ps1 +# Hits the SWFOC bridge with a battery of pre-baked probes and writes a +# structured pass/fail/expected-error report. Does NOT require any in-game +# input — fires every probe over the named pipe, classifies the response, +# logs everything to a timestamped file. + +$ErrorActionPreference = "Continue" +$logPath = "C:\Users\Prekzursil\Downloads\swfoc_memory\.remember\autonomous_live_test_$(Get-Date -Format yyyy-MM-dd_HHmmss).log" + +function Send-LuaCmd { + param([string]$cmd, [int]$readMs = 350) + # NamedPipeClientStream performs the full pipe handshake synchronously + # whereas File.Open returns a handle before the server has acknowledged. + $pipe = New-Object System.IO.Pipes.NamedPipeClientStream(".", "swfoc_bridge", "InOut") + try { + $pipe.Connect(3000) + } catch { + $pipe.Dispose() + return "CONNECT_FAIL: $_" + } + try { + $b = [System.Text.Encoding]::UTF8.GetBytes($cmd) + [byte[]]@(0) + $pipe.Write($b, 0, $b.Length) + $pipe.Flush() + Start-Sleep -Milliseconds $readMs + $buf = New-Object byte[] 65536 + $n = $pipe.Read($buf, 0, 65536) + return [System.Text.Encoding]::UTF8.GetString($buf, 0, $n).TrimEnd() + } finally { + $pipe.Dispose() + } +} + +function Classify { + param([string]$response, [string]$expectedPattern = $null) + if ($response -match "^CONNECT_FAIL") { return "CONNECT_FAIL" } + if ($response -match "^ERR: .*Phase 2") { return "P2-PENDING (honest)" } + if ($response -match "Phase 2 hook pending") { return "P2-PENDING (honest)" } + if ($response -match "non-tactical state") { return "TACTICAL-ONLY (expected in galactic)" } + if ($response -match "engine error rc=") { return "ENGINE-ERR (helper raised)" } + if ($response -match "^ERR: .*expected ") { return "NEEDS-ARGS" } + if ($response -match "^ERR:") { return "ERR" } + if ($expectedPattern -and -not ($response -match $expectedPattern)) { return "UNEXPECTED" } + return "PASS" +} + +# ---- TEST BATTERY ---- +# Each test: name, cmd, category, optional expected-pattern regex +$tests = @( + # === Meta / version / state === + @{ Cat="META"; Name="GetVersion"; Cmd="return SWFOC_GetVersion()"; Expect="v1\.5-dev" }, + @{ Cat="META"; Name="GetBuildInfo"; Cmd="return SWFOC_GetBuildInfo()"; Expect="\d{4}" }, + @{ Cat="META"; Name="StateInfo"; Cmd="return SWFOC_StateInfo()"; Expect="Game states" }, + @{ Cat="META"; Name="DiagPipeStats"; Cmd="return SWFOC_DiagPipeStats()"; Expect="received=\d+" }, + @{ Cat="META"; Name="DiagSelfTest"; Cmd="return SWFOC_DiagSelfTest()"; Expect="passed=" }, + @{ Cat="META"; Name="DiagGameTick"; Cmd="return SWFOC_DiagGameTick()" }, + @{ Cat="META"; Name="DiagSelection"; Cmd="return SWFOC_DiagSelection()" }, + @{ Cat="META"; Name="GetGameModeLua"; Cmd="return SWFOC_GetGameModeLua()" }, + @{ Cat="META"; Name="ListMods"; Cmd="return SWFOC_ListMods()" }, + @{ Cat="META"; Name="GetCurrentMod"; Cmd="return SWFOC_GetCurrentMod()" }, + + # === Player + faction reads === + @{ Cat="READ"; Name="GetLocalPlayerLua"; Cmd="return SWFOC_GetLocalPlayerLua()" }, + @{ Cat="READ"; Name="GetAllPlayers"; Cmd="return SWFOC_GetAllPlayers()"; Expect="count=" }, + @{ Cat="READ"; Name="GetCredits"; Cmd="return SWFOC_GetCredits()"; Expect="\d+" }, + @{ Cat="READ"; Name="GetCreditsForSlot(6)"; Cmd="return SWFOC_GetCreditsForSlot(6)"; Expect="\d+" }, + @{ Cat="READ"; Name="GetTechForSlot(6)"; Cmd="return SWFOC_GetTechForSlot(6)"; Expect="^\d+$" }, + @{ Cat="READ"; Name="GetMaxCredits"; Cmd="return SWFOC_GetMaxCredits()"; Expect="\d+" }, + @{ Cat="READ"; Name="GetPlayerKills(6)"; Cmd="return SWFOC_GetPlayerKills(6)" }, + @{ Cat="READ"; Name="GetPlayerDeaths(6)"; Cmd="return SWFOC_GetPlayerDeaths(6)" }, + @{ Cat="READ"; Name="ListFactions"; Cmd="return SWFOC_ListFactions()" }, + + # === Multipliers + freezes (READ side) === + @{ Cat="MULT-READ"; Name="GetCreditsMultiplierGlobal"; Cmd="return SWFOC_GetCreditsMultiplierGlobal()" }, + @{ Cat="MULT-READ"; Name="GetCreditsFreezeGlobal"; Cmd="return SWFOC_GetCreditsFreezeGlobal()" }, + @{ Cat="MULT-READ"; Name="GetDamageMultiplierGlobal"; Cmd="return SWFOC_GetDamageMultiplierGlobal()" }, + @{ Cat="MULT-READ"; Name="GetFireRateMultiplierGlobal";Cmd="return SWFOC_GetFireRateMultiplierGlobal()" }, + + # === World queries === + @{ Cat="WORLD"; Name="GetPlanets"; Cmd="return SWFOC_GetPlanets()" }, + @{ Cat="WORLD"; Name="ListHeroes"; Cmd="return SWFOC_ListHeroes()" }, + @{ Cat="WORLD"; Name="ListAbilities"; Cmd="return SWFOC_ListAbilities()" }, + @{ Cat="WORLD"; Name="ListTacticalUnits"; Cmd="return string.sub(SWFOC_ListTacticalUnits(), 1, 100)" }, + @{ Cat="WORLD"; Name="GetTotalUnitsAlive"; Cmd="return SWFOC_GetTotalUnitsAlive()" }, + @{ Cat="WORLD"; Name="GetSelectedUnit"; Cmd="return SWFOC_GetSelectedUnit()" }, + @{ Cat="WORLD"; Name="GetSelectedUnits"; Cmd="return SWFOC_GetSelectedUnits()" }, + @{ Cat="WORLD"; Name="GetFactionRoster(6)"; Cmd="return string.sub(SWFOC_GetFactionRoster(6), 1, 200)" }, + + # === Camera reads (galactic returns 0,0,0; tactical returns 3D) === + @{ Cat="CAMERA"; Name="GetCameraPos"; Cmd="return SWFOC_GetCameraPos()" }, + + # === Tactical-only (expected to fail in galactic with mode message) === + @{ Cat="TACTICAL-ONLY"; Name="BatchTypeExists";Cmd="return SWFOC_BatchTypeExists('TIE_Fighter|Vengeance_Frigate|NotARealUnit')" }, + + # === Write-and-readback cycles === + @{ Cat="WRITE-CYCLE"; Name="SetCredits cycle"; Cmd="local v0=SWFOC_GetCreditsForSlot(6); SWFOC_SetCreditsForSlot(6,77777); local v1=SWFOC_GetCreditsForSlot(6); SWFOC_SetCreditsForSlot(6,v0); local v2=SWFOC_GetCreditsForSlot(6); return 'before='..v0..' mid='..v1..' after='..v2"; Expect="before=12000 mid=77777 after=12000" }, + @{ Cat="WRITE-CYCLE"; Name="SetTech cycle"; Cmd="local v0=SWFOC_GetTechForSlot(6); SWFOC_SetTechForSlot(6,4); local v1=SWFOC_GetTechForSlot(6); SWFOC_SetTechForSlot(6,v0); local v2=SWFOC_GetTechForSlot(6); return 'before='..v0..' mid='..v1..' after='..v2"; Expect="before=2 mid=4 after=2" }, + @{ Cat="WRITE-CYCLE"; Name="CreditsMult cycle";Cmd="local v0=SWFOC_GetCreditsMultiplierGlobal(); SWFOC_SetCreditsMultiplierGlobal(3.5); local v1=SWFOC_GetCreditsMultiplierGlobal(); SWFOC_SetCreditsMultiplierGlobal(v0); local v2=SWFOC_GetCreditsMultiplierGlobal(); return 'before='..v0..' mid='..v1..' after='..v2"; Expect="mid=3.5" }, + @{ Cat="WRITE-CYCLE"; Name="DamageMult cycle"; Cmd="local v0=SWFOC_GetDamageMultiplierGlobal(); SWFOC_SetDamageMultiplierGlobal(2.0); local v1=SWFOC_GetDamageMultiplierGlobal(); SWFOC_SetDamageMultiplierGlobal(v0); local v2=SWFOC_GetDamageMultiplierGlobal(); return 'before='..v0..' mid='..v1..' after='..v2"; Expect="mid=2" }, + @{ Cat="WRITE-CYCLE"; Name="FireRateMult cycle"; Cmd="local v0=SWFOC_GetFireRateMultiplierGlobal(); SWFOC_SetFireRateMultiplierGlobal(0.5); local v1=SWFOC_GetFireRateMultiplierGlobal(); SWFOC_SetFireRateMultiplierGlobal(v0); local v2=SWFOC_GetFireRateMultiplierGlobal(); return 'before='..v0..' mid='..v1..' after='..v2"; Expect="mid=0.5" }, + @{ Cat="WRITE-CYCLE"; Name="CreditsFreeze cycle"; Cmd="local v0=SWFOC_GetCreditsFreezeGlobal(); SWFOC_SetCreditsFreezeGlobal(1); local v1=SWFOC_GetCreditsFreezeGlobal(); SWFOC_SetCreditsFreezeGlobal(0); local v2=SWFOC_GetCreditsFreezeGlobal(); return 'before='..v0..' mid='..v1..' after='..v2"; Expect="mid=1 after=0" }, + + # === P2-PENDING wires (catalog says these will return honest pending text) === + @{ Cat="P2-PENDING"; Name="FreezeAI(true)"; Cmd="return SWFOC_FreezeAI(1)" }, + @{ Cat="P2-PENDING"; Name="SetGameSpeed(2.0)"; Cmd="return SWFOC_SetGameSpeed(2.0)" }, + @{ Cat="P2-PENDING"; Name="TriggerVictory"; Cmd="return SWFOC_TriggerVictory('Galactic_Conquer')" }, + @{ Cat="P2-PENDING"; Name="FreeCam(true)"; Cmd="return SWFOC_FreeCam(1)" }, + + # === Galactic-only writes (revertible) === + @{ Cat="GALACTIC"; Name="UncapCredits"; Cmd="return SWFOC_UncapCredits(true)" }, + @{ Cat="GALACTIC"; Name="UncapCredits revert"; Cmd="return SWFOC_UncapCredits(false)" }, + + # === Hooks / abilities / lookup === + @{ Cat="LOOKUP"; Name="DiagListRegisteredFn (head)"; Cmd="return string.sub(SWFOC_DiagListRegisteredFunctions(), 1, 100)" }, + @{ Cat="LOOKUP"; Name="DoString round-trip"; Cmd='return SWFOC_DoString("return 1+2+3")'; Expect="6" }, + @{ Cat="LOOKUP"; Name="DoString global probe"; Cmd='return SWFOC_DoString("return _G[\"GameMode\"] or \"unknown\"")' } +) + +# ---- Run ---- +$results = @() +$counts = @{ PASS=0; "P2-PENDING (honest)"=0; "TACTICAL-ONLY (expected in galactic)"=0; "ENGINE-ERR (helper raised)"=0; "NEEDS-ARGS"=0; ERR=0; UNEXPECTED=0; CONNECT_FAIL=0 } + +$startTime = Get-Date +Add-Content -LiteralPath $logPath -Value "=== SWFOC Bridge Autonomous Live Test ===" +Add-Content -LiteralPath $logPath -Value "Started: $startTime" +Add-Content -LiteralPath $logPath -Value "" + +foreach ($t in $tests) { + $response = Send-LuaCmd $t.Cmd + $verdict = Classify $response $t.Expect + $counts[$verdict]++ + $results += [PSCustomObject]@{ Cat=$t.Cat; Name=$t.Name; Verdict=$verdict; Response=$response } + Add-Content -LiteralPath $logPath -Value ("[{0,-30}] {1,-25} {2,-30} -> {3}" -f $t.Cat, $t.Name, $verdict, ($response.Replace("`n"," | ").Substring(0, [Math]::Min(200, $response.Length)))) +} + +$endTime = Get-Date +$elapsed = [int]($endTime - $startTime).TotalSeconds + +# ---- Summary ---- +Add-Content -LiteralPath $logPath -Value "" +Add-Content -LiteralPath $logPath -Value "=== SUMMARY ===" +Add-Content -LiteralPath $logPath -Value "Elapsed: ${elapsed}s" +Add-Content -LiteralPath $logPath -Value "Total tests: $($results.Count)" +foreach ($k in $counts.Keys | Sort-Object) { + $line = " {0,-40} {1}" -f $k, $counts[$k] + Add-Content -LiteralPath $logPath -Value $line +} +Add-Content -LiteralPath $logPath -Value "" + +# Console output: condensed table by category +Write-Output "" +Write-Output "=== Autonomous live test (${elapsed}s, $($results.Count) probes) ===" +foreach ($cat in ($results | Group-Object Cat | Sort-Object Name)) { + Write-Output "" + Write-Output "--- $($cat.Name) ---" + foreach ($r in $cat.Group) { + $status = switch ($r.Verdict) { + "PASS" { "OK" } + "P2-PENDING (honest)" { "P2" } + "TACTICAL-ONLY (expected in galactic)" { "TC" } + "ENGINE-ERR (helper raised)" { "EN" } + "NEEDS-ARGS" { "AR" } + "UNEXPECTED" { "??" } + "CONNECT_FAIL" { "XX" } + default { "ER" } + } + $preview = $r.Response.Replace("`n", " | ") + if ($preview.Length -gt 100) { $preview = $preview.Substring(0, 100) + "..." } + Write-Output (" [{0}] {1,-32} {2}" -f $status, $r.Name, $preview) + } +} + +Write-Output "" +Write-Output "=== TOTALS ===" +foreach ($k in $counts.Keys | Sort-Object) { + if ($counts[$k] -gt 0) { + Write-Output (" {0,-40} {1}" -f $k, $counts[$k]) + } +} +Write-Output "" +Write-Output "Full log: $logPath" diff --git a/bridge/bridge_test.py b/bridge/bridge_test.py new file mode 100644 index 000000000..2f34b488d --- /dev/null +++ b/bridge/bridge_test.py @@ -0,0 +1,544 @@ +#!/usr/bin/env python3 +""" +SWFOC DLL Bridge — Comprehensive Test Suite +============================================= +Connects to the swfoc_bridge named pipe and exercises every SWFOC_* +function registered in lua_bridge.cpp (12 functions total). + +Tests: + - Valid invocation of each function + - Invalid argument handling for each function + - Read-set-read cycles for setters (Credits, TechLevel) + - SWFOC_StateInfo() (v3 game state cache) + - Stress test: 100 rapid commands + - Timeout test: 2s deadline + - Produces bridge_test_report.json + +Usage: python bridge_test.py + python bridge_test.py --stress-count 200 + python bridge_test.py --timeout 3.0 +""" + +import json +import os +import sys +import time +import argparse + +PIPE_NAME = r"\\.\pipe\swfoc_bridge" +REPORT_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "bridge_test_report.json") + +# --------------------------------------------------------------------------- +# Pipe I/O +# --------------------------------------------------------------------------- + +class PipeError(Exception): + """Raised when the pipe cannot be reached.""" + pass + + +def pipe_send(lua_code: str, timeout_s: float = 10.0) -> str: + """ + Send a null-terminated Lua string to the bridge pipe and return the + raw response string. Raises PipeError on connection failure. + """ + try: + handle = open(PIPE_NAME, "r+b", buffering=0) + except FileNotFoundError: + raise PipeError("Pipe not found — game not running or DLL not injected") + except PermissionError: + raise PipeError("Permission denied on pipe") + except OSError as e: + raise PipeError(str(e)) + + try: + handle.write(lua_code.encode("utf-8") + b"\x00") + handle.flush() + + deadline = time.monotonic() + timeout_s + chunks = [] + while time.monotonic() < deadline: + try: + data = handle.read(4096) + if not data: + break + chunks.append(data) + except OSError: + break + return b"".join(chunks).decode("utf-8", errors="replace") + finally: + handle.close() + + +def is_ok(response: str) -> bool: + return response.startswith("OK") + + +def is_err(response: str) -> bool: + return response.startswith("ERR") + + +# --------------------------------------------------------------------------- +# Test framework +# --------------------------------------------------------------------------- + +class TestResult: + def __init__(self, name: str): + self.name = name + self.passed = False + self.detail = "" + self.response = "" + self.elapsed_ms = 0.0 + + def to_dict(self) -> dict: + return { + "name": self.name, + "passed": self.passed, + "detail": self.detail, + "response": self.response[:200], + "elapsed_ms": round(self.elapsed_ms, 2), + } + + +results: list[TestResult] = [] + + +def run_test(name: str, lua_code: str, expect_ok: bool = True, + timeout_s: float = 10.0) -> TestResult: + """Run a single pipe command and check OK/ERR expectation.""" + tr = TestResult(name) + t0 = time.monotonic() + try: + resp = pipe_send(lua_code, timeout_s=timeout_s) + tr.elapsed_ms = (time.monotonic() - t0) * 1000 + tr.response = resp.strip() + if expect_ok: + tr.passed = is_ok(resp) + tr.detail = "OK response" if tr.passed else f"Expected OK, got: {resp[:120]}" + else: + tr.passed = is_err(resp) + tr.detail = "ERR response as expected" if tr.passed else f"Expected ERR, got: {resp[:120]}" + except PipeError as e: + tr.elapsed_ms = (time.monotonic() - t0) * 1000 + tr.detail = f"PipeError: {e}" + results.append(tr) + status = "PASS" if tr.passed else "FAIL" + print(f" [{status}] {name} ({tr.elapsed_ms:.0f}ms) — {tr.detail}") + return tr + + +def run_raw(name: str, lua_code: str, timeout_s: float = 10.0) -> TestResult: + """Run a command and record the raw response without pass/fail judgment yet. + Caller sets tr.passed and tr.detail after inspecting tr.response.""" + tr = TestResult(name) + t0 = time.monotonic() + try: + resp = pipe_send(lua_code, timeout_s=timeout_s) + tr.elapsed_ms = (time.monotonic() - t0) * 1000 + tr.response = resp.strip() + except PipeError as e: + tr.elapsed_ms = (time.monotonic() - t0) * 1000 + tr.detail = f"PipeError: {e}" + return tr + + +# --------------------------------------------------------------------------- +# Individual function tests +# --------------------------------------------------------------------------- + +def test_GetVersion(): + """SWFOC_GetVersion() -> string""" + print("\n=== SWFOC_GetVersion ===") + # Valid: should return OK (the return value goes to Lua, pipe just says OK) + run_test("GetVersion_valid", + "SWFOC_GetVersion()", + expect_ok=True) + # Also test return-value capture via a wrapper + run_test("GetVersion_return", + "SWFOC_Log(SWFOC_GetVersion())", + expect_ok=True) + + +def test_GetLocalPlayer(): + """SWFOC_GetLocalPlayer() -> slot, faction_name""" + print("\n=== SWFOC_GetLocalPlayer ===") + run_test("GetLocalPlayer_valid", + "local s,f = SWFOC_GetLocalPlayer()", + expect_ok=True) + # No args needed, so invalid = passing garbage (Lua ignores extra args in 5.0) + # Test with intentionally wrong usage — calling it as a table + run_test("GetLocalPlayer_bad_call", + "local x = SWFOC_GetLocalPlayer.foo", + expect_ok=False) + + +def test_SetCredits(): + """SWFOC_SetCredits(amount) -> success""" + print("\n=== SWFOC_SetCredits ===") + run_test("SetCredits_valid", + "SWFOC_SetCredits(5000)", + expect_ok=True) + # No arg = tonumber returns 0 in Lua 5.0, so it should still succeed (sets to 0) + run_test("SetCredits_no_arg", + "SWFOC_SetCredits()", + expect_ok=True) + # String arg — tonumber on a string may return 0 or the number + run_test("SetCredits_string_arg", + "SWFOC_SetCredits('abc')", + expect_ok=True) + + +def test_GetCredits(): + """SWFOC_GetCredits() -> number""" + print("\n=== SWFOC_GetCredits ===") + run_test("GetCredits_valid", + "local c = SWFOC_GetCredits()", + expect_ok=True) + run_test("GetCredits_bad_call", + "local c = SWFOC_GetCredits.x", + expect_ok=False) + + +def test_SetTechLevel(): + """SWFOC_SetTechLevel(level) -> success""" + print("\n=== SWFOC_SetTechLevel ===") + run_test("SetTechLevel_valid", + "SWFOC_SetTechLevel(3)", + expect_ok=True) + run_test("SetTechLevel_no_arg", + "SWFOC_SetTechLevel()", + expect_ok=True) + run_test("SetTechLevel_string_arg", + "SWFOC_SetTechLevel('bad')", + expect_ok=True) + + +def test_UncapCredits(): + """SWFOC_UncapCredits() -> success""" + print("\n=== SWFOC_UncapCredits ===") + run_test("UncapCredits_valid", + "SWFOC_UncapCredits()", + expect_ok=True) + run_test("UncapCredits_bad_call", + "local x = SWFOC_UncapCredits.y", + expect_ok=False) + + +def test_HeroInstantRespawn(): + """SWFOC_HeroInstantRespawn(enable) -> success""" + print("\n=== SWFOC_HeroInstantRespawn ===") + run_test("HeroRespawn_enable", + "SWFOC_HeroInstantRespawn(1)", + expect_ok=True) + run_test("HeroRespawn_disable", + "SWFOC_HeroInstantRespawn(0)", + expect_ok=True) + run_test("HeroRespawn_no_arg", + "SWFOC_HeroInstantRespawn()", + expect_ok=True) + + +def test_ListFactions(): + """SWFOC_ListFactions() -> table""" + print("\n=== SWFOC_ListFactions ===") + run_test("ListFactions_valid", + "local t = SWFOC_ListFactions()", + expect_ok=True) + run_test("ListFactions_bad_call", + "local x = SWFOC_ListFactions.z", + expect_ok=False) + + +def test_Log(): + """SWFOC_Log(message)""" + print("\n=== SWFOC_Log ===") + run_test("Log_valid", + 'SWFOC_Log("bridge_test: hello from Python")', + expect_ok=True) + run_test("Log_no_arg", + "SWFOC_Log()", + expect_ok=True) + run_test("Log_number_arg", + "SWFOC_Log(12345)", + expect_ok=True) + + +def test_DoString(): + """SWFOC_DoString(code) -> success, errmsg""" + print("\n=== SWFOC_DoString ===") + run_test("DoString_valid", + 'local ok = SWFOC_DoString("-- noop")', + expect_ok=True) + run_test("DoString_syntax_error", + 'local ok, err = SWFOC_DoString("if then")', + expect_ok=True) # The function itself succeeds; it returns 0 + error + run_test("DoString_no_arg", + "SWFOC_DoString()", + expect_ok=True) # Returns 0 + "expected string argument" + run_test("DoString_bad_call", + "SWFOC_DoString.x", + expect_ok=False) + + +def test_DrainPipe(): + """SWFOC_DrainPipe() -> 0 or 1""" + print("\n=== SWFOC_DrainPipe ===") + run_test("DrainPipe_valid", + "local n = SWFOC_DrainPipe()", + expect_ok=True) + run_test("DrainPipe_bad_call", + "local x = SWFOC_DrainPipe.q", + expect_ok=False) + + +def test_StateInfo(): + """SWFOC_StateInfo() -> string (v3 cache info)""" + print("\n=== SWFOC_StateInfo (v3) ===") + run_test("StateInfo_valid", + "local s = SWFOC_StateInfo()", + expect_ok=True) + # Capture and log the actual value + run_test("StateInfo_via_log", + 'SWFOC_Log(SWFOC_StateInfo())', + expect_ok=True) + run_test("StateInfo_bad_call", + "local x = SWFOC_StateInfo.z", + expect_ok=False) + + +# --------------------------------------------------------------------------- +# Read-Set-Read cycles +# --------------------------------------------------------------------------- + +def test_credits_cycle(): + """Read credits, set to a known value, read back, verify.""" + print("\n=== Credits Read-Set-Read Cycle ===") + + # Step 1: Set to known value + run_test("CreditsCycle_set", + "SWFOC_SetCredits(7777)", + expect_ok=True) + + # Step 2: Read back via DoString wrapper that logs the value + run_test("CreditsCycle_readback", + 'SWFOC_Log("credits=" .. tostring(SWFOC_GetCredits()))', + expect_ok=True) + + # Step 3: Restore to something reasonable + run_test("CreditsCycle_restore", + "SWFOC_SetCredits(10000)", + expect_ok=True) + + +def test_techlevel_cycle(): + """Set tech level, read-back is not directly possible via pipe (no return), + but we verify the set call succeeds and then set it back.""" + print("\n=== TechLevel Read-Set-Read Cycle ===") + run_test("TechCycle_set5", + "SWFOC_SetTechLevel(5)", + expect_ok=True) + run_test("TechCycle_set1", + "SWFOC_SetTechLevel(1)", + expect_ok=True) + + +def test_hero_respawn_cycle(): + """Enable, then disable hero respawn.""" + print("\n=== HeroRespawn Enable-Disable Cycle ===") + run_test("RespawnCycle_enable", + "SWFOC_HeroInstantRespawn(1)", + expect_ok=True) + run_test("RespawnCycle_disable", + "SWFOC_HeroInstantRespawn(0)", + expect_ok=True) + + +# --------------------------------------------------------------------------- +# Stress test +# --------------------------------------------------------------------------- + +def test_stress(count: int = 100): + """Send N rapid commands and track success rate.""" + print(f"\n=== Stress Test ({count} commands) ===") + passed = 0 + failed = 0 + errors = 0 + t_start = time.monotonic() + + for i in range(count): + try: + resp = pipe_send(f'SWFOC_Log("stress_{i}")', timeout_s=15.0) + if is_ok(resp): + passed += 1 + else: + failed += 1 + except PipeError: + errors += 1 + + elapsed = (time.monotonic() - t_start) * 1000 + rate = count / (elapsed / 1000) if elapsed > 0 else 0 + + tr = TestResult(f"Stress_{count}_commands") + tr.elapsed_ms = elapsed + tr.passed = (errors == 0 and failed == 0) + tr.detail = ( + f"{passed} OK, {failed} ERR, {errors} pipe errors " + f"({elapsed:.0f}ms total, {rate:.1f} cmd/s)" + ) + tr.response = f"passed={passed} failed={failed} errors={errors}" + results.append(tr) + status = "PASS" if tr.passed else "FAIL" + print(f" [{status}] {tr.name} — {tr.detail}") + + +# --------------------------------------------------------------------------- +# Timeout test +# --------------------------------------------------------------------------- + +def test_timeout(deadline_s: float = 2.0): + """Send a command with a short timeout to verify we get a response in time.""" + print(f"\n=== Timeout Test ({deadline_s}s deadline) ===") + tr = TestResult("Timeout_test") + t0 = time.monotonic() + try: + resp = pipe_send("SWFOC_GetVersion()", timeout_s=deadline_s) + elapsed = time.monotonic() - t0 + tr.elapsed_ms = elapsed * 1000 + tr.response = resp.strip() + if elapsed <= deadline_s + 0.5: # allow 500ms grace + tr.passed = is_ok(resp) + tr.detail = f"Response in {elapsed:.2f}s (within {deadline_s}s deadline)" + else: + tr.passed = False + tr.detail = f"Response took {elapsed:.2f}s — exceeded {deadline_s}s deadline" + except PipeError as e: + tr.elapsed_ms = (time.monotonic() - t0) * 1000 + tr.detail = f"PipeError: {e}" + results.append(tr) + status = "PASS" if tr.passed else "FAIL" + print(f" [{status}] {tr.name} — {tr.detail}") + + +# --------------------------------------------------------------------------- +# Syntax / malformed input tests +# --------------------------------------------------------------------------- + +def test_bad_inputs(): + """Send deliberately malformed Lua to verify ERR handling.""" + print("\n=== Malformed Input Tests ===") + run_test("BadInput_syntax_error", + "if then end end", + expect_ok=False) + run_test("BadInput_nil_call", + "NONEXISTENT_FUNCTION()", + expect_ok=False) + run_test("BadInput_incomplete", + "local x =", + expect_ok=False) + run_test("BadInput_long_string", + 'SWFOC_Log("' + "A" * 3000 + '")', + expect_ok=True) + + +# --------------------------------------------------------------------------- +# Main +# --------------------------------------------------------------------------- + +def generate_report(): + """Write bridge_test_report.json.""" + total = len(results) + passed = sum(1 for r in results if r.passed) + failed = total - passed + + report = { + "summary": { + "total": total, + "passed": passed, + "failed": failed, + "pass_rate": f"{100 * passed / total:.1f}%" if total > 0 else "N/A", + "timestamp": time.strftime("%Y-%m-%d %H:%M:%S"), + }, + "tests": [r.to_dict() for r in results], + } + + with open(REPORT_PATH, "w", encoding="utf-8") as f: + json.dump(report, f, indent=2) + print(f"\nReport written to: {REPORT_PATH}") + return report + + +def main(): + parser = argparse.ArgumentParser(description="SWFOC Bridge Test Suite") + parser.add_argument("--stress-count", type=int, default=100, + help="Number of commands for stress test (default: 100)") + parser.add_argument("--timeout", type=float, default=2.0, + help="Deadline in seconds for timeout test (default: 2.0)") + args = parser.parse_args() + + print("=" * 60) + print(" SWFOC DLL Bridge — Comprehensive Test Suite") + print(f" Pipe: {PIPE_NAME}") + print("=" * 60) + + # Pre-flight: check if pipe exists + print("\n[*] Checking pipe availability...") + try: + resp = pipe_send("-- ping", timeout_s=5.0) + if is_ok(resp): + print("[*] Pipe is live and responding.") + elif is_err(resp): + print(f"[*] Pipe responded with error (may be OK): {resp.strip()}") + else: + print(f"[!] Unexpected response: {resp[:100]}") + except PipeError as e: + print(f"[!] Cannot connect to pipe: {e}") + print("[!] Generating report with connection failure.") + tr = TestResult("pipe_connection") + tr.passed = False + tr.detail = str(e) + results.append(tr) + report = generate_report() + print(f"\nResults: 0 passed / 1 failed") + sys.exit(1) + + # ---- Per-function tests ---- + test_GetVersion() + test_GetLocalPlayer() + test_SetCredits() + test_GetCredits() + test_SetTechLevel() + test_UncapCredits() + test_HeroInstantRespawn() + test_ListFactions() + test_Log() + test_DoString() + test_DrainPipe() + test_StateInfo() + + # ---- Read-Set-Read cycles ---- + test_credits_cycle() + test_techlevel_cycle() + test_hero_respawn_cycle() + + # ---- Malformed input ---- + test_bad_inputs() + + # ---- Stress test ---- + test_stress(count=args.stress_count) + + # ---- Timeout test ---- + test_timeout(deadline_s=args.timeout) + + # ---- Report ---- + report = generate_report() + summary = report["summary"] + print(f"\n{'=' * 60}") + print(f" RESULTS: {summary['passed']} passed / {summary['failed']} failed " + f"({summary['pass_rate']})") + print(f"{'=' * 60}") + + sys.exit(0 if summary["failed"] == 0 else 1) + + +if __name__ == "__main__": + main() diff --git a/bridge/bridge_v3_changes.md b/bridge/bridge_v3_changes.md new file mode 100644 index 000000000..21cfdb229 --- /dev/null +++ b/bridge/bridge_v3_changes.md @@ -0,0 +1,75 @@ +# Bridge v3 Changes: Game State Caching + +## Problem + +The DLL bridge hooks `luaD_call`, which fires on every Lua function invocation across 400+ Lua states. To execute pipe commands on the correct state (one with game globals like `Find_Object_Type`), the v2 bridge probed the Lua stack during every `luaD_call` invocation when a pipe command was pending. This was unreliable because: + +1. The stack may be in a transient state during `luaD_call` (mid-call setup), making the probe unsafe. +2. Probing involves `pushstring` + `gettable` + `type` + `settop` on a potentially unstable stack, risking heap corruption. +3. The probe ran on every `luaD_call` while a command was pending, adding overhead to a hot path. + +## Solution: Cache game states at creation time + +Instead of probing the stack on every `luaD_call`, we identify game states once during `lua_open` and cache their pointers. The `luaD_call` hook then does a simple pointer lookup (O(n) on a tiny vector, typically 1-3 entries) instead of touching the Lua stack. + +## Files Changed + +### `lua_bridge.cpp` + +**A. New includes and globals (top of file):** +- Added ``, ``, `` includes. +- Added `CRITICAL_SECTION csGameStates` and `std::vector cached_game_states` for the cache. + +**B. New hook types:** +- Added `lua_close_t` typedef and `orig_lua_close` trampoline pointer for the lua_close hook. + +**C. New function: `SWFOC_StateInfo()` (line ~465):** +- Lua-callable function returning a string listing all cached game state pointers. +- Useful for debugging: call `print(SWFOC_StateInfo())` from the game console or pipe. + +**D. New function: `Hook_lua_close()` (line ~484):** +- Intercepts `lua_close` to remove destroyed states from `cached_game_states`. +- Prevents stale pointers from accumulating in the cache. +- Thread-safe via `csGameStates` critical section. + +**E. Modified: `Hook_luaD_call()` (line ~537):** +- **REMOVED**: Stack probing logic (`pushstring("Find_Object_Type")` + `gettable` + `type` check). +- **REPLACED WITH**: `std::find` lookup in `cached_game_states` under `csGameStates` lock. +- No Lua stack manipulation during `luaD_call` anymore -- just a pointer comparison. + +**F. Modified: `Hook_lua_open()` (line ~749):** +- After registering SWFOC_* functions and running self-tests, probes for `Find_Object_Type` in the global table. +- If found (meaning this is a gameplay state, not a menu/config state), adds `L` to `cached_game_states`. +- This is safe because `lua_open` returns a fully initialized state with a stable stack. + +**G. Modified: `Hook_lua_open()` function registration (line ~630):** +- Added `SWFOC_StateInfo` registration alongside other SWFOC_* functions. +- Function count bumped from 11 to 12. + +**H. Modified: `RegisterAll()` (line ~499):** +- Added `SWFOC_StateInfo` to the registration table. + +**I. Modified: `LuaBridge_Init()` (line ~828):** +- `InitializeCriticalSection(&csGameStates)` called before MinHook initialization. +- New `lua_close` hook installed after `lua_open` hook, before `luaD_call` hook. +- `lua_close` hook failure is non-fatal (logged as WARNING) since the RVA is estimated. + +**J. Modified: `LuaBridge_Shutdown()` (line ~902):** +- Clears `cached_game_states` vector and destroys `csGameStates` critical section. + +### `rvas.h` + +- Added `lua_close = 0x7B8A70` with `ESTIMATED` / `NEEDS_VERIFICATION` marker. +- Address is between `lua_open` (0x7B8930) and `lua_checkstack` (0x7B8BC0), consistent with Lua 5.0.2 source layout. + +## Action Required + +The `lua_close` RVA (0x7B8A70) is an estimate based on source ordering. It needs Ghidra verification before the hook can be considered reliable. If the hook fails at runtime, the bridge logs a warning but continues functioning -- the only consequence is that stale cache entries may accumulate (harmless since the game typically doesn't destroy gameplay states during a session). + +To verify: open StarWarsG.exe in Ghidra, navigate to 0x1407B8A70 (image base + RVA), and confirm it matches the `lua_close` signature: single parameter (lua_State*), calls `luaC_callGCTM` and `luai_close`, frees the state. + +## Performance Impact + +- **Hot path (`luaD_call`)**: Replaced 5 Lua C API calls (pushstring, gettable, type, settop, gettop) with one `EnterCriticalSection` + `std::find` on a 1-3 element vector + `LeaveCriticalSection`. Significantly cheaper and does not touch the Lua stack. +- **Cold path (`lua_open`)**: Added one probe per state creation (same 5 API calls that were removed from the hot path). This runs once per state, not per function call. +- **Memory**: One `void*` per game state in the cache vector (typically 1-3 entries, 8-24 bytes total). diff --git a/bridge/deploy_and_launch.ps1 b/bridge/deploy_and_launch.ps1 new file mode 100644 index 000000000..7a9b9025e --- /dev/null +++ b/bridge/deploy_and_launch.ps1 @@ -0,0 +1,115 @@ +# deploy_and_launch.ps1 — deploy the latest bridge DLL and (optionally) launch SWFOC. +# +# Usage: +# .\deploy_and_launch.ps1 # deploy + verify (no game launch) +# .\deploy_and_launch.ps1 -Launch # deploy + launch StarWarsG.exe +# .\deploy_and_launch.ps1 -DryRun # show what would happen +# .\deploy_and_launch.ps1 -Restore # restore the most recent .bak.preLiveSmoke +# +# Always creates a timestamped backup before overwriting. Safe to re-run. + +[CmdletBinding()] +param( + [switch]$Launch, + [switch]$DryRun, + [switch]$Restore +) + +$ErrorActionPreference = "Stop" + +# 1. Locate the game folder (Steam library autodetect) +$libraryFoldersVdf = "C:\Program Files (x86)\Steam\steamapps\libraryfolders.vdf" +if (-not (Test-Path -LiteralPath $libraryFoldersVdf)) { + Write-Error "Steam libraryfolders.vdf not found. Is Steam installed?" + exit 1 +} + +$libraryRoots = [regex]::Matches((Get-Content -LiteralPath $libraryFoldersVdf -Raw), + '"path"\s+"([^"]+)"') | ForEach-Object { $_.Groups[1].Value.Replace('\\','\') } + +$gameDir = $null +foreach ($root in $libraryRoots) { + $candidate = Join-Path $root "steamapps\common\Star Wars Empire at War\corruption" + if (Test-Path -LiteralPath (Join-Path $candidate "StarWarsG.exe")) { + $gameDir = $candidate + break + } +} + +if (-not $gameDir) { + Write-Error "SWFOC corruption folder not found in any Steam library. Install / locate StarWarsG.exe first." + exit 1 +} + +Write-Output "Game folder: $gameDir" + +$deployedDll = Join-Path $gameDir "powrprof.dll" +$builtDll = "C:\Users\Prekzursil\Downloads\swfoc_memory\swfoc_lua_bridge\powrprof.dll" + +if (-not (Test-Path -LiteralPath $builtDll)) { + Write-Error "Built bridge DLL not found at $builtDll. Run swfoc_lua_bridge/build.bat first." + exit 1 +} + +# 2. Restore mode: roll back to most recent .bak.preLiveSmoke +if ($Restore) { + $backups = Get-ChildItem -LiteralPath $gameDir -Filter "powrprof.dll.bak.preLiveSmoke_*" | + Sort-Object LastWriteTime -Descending + if ($backups.Count -eq 0) { + Write-Error "No preLiveSmoke backups found in $gameDir" + exit 1 + } + $latest = $backups[0] + Write-Output "Restoring from: $($latest.Name) ($($latest.Length) bytes)" + if ($DryRun) { Write-Output "(dry-run; not copying)"; exit 0 } + Copy-Item -LiteralPath $latest.FullName -Destination $deployedDll -Force + Write-Output "Restored. Game folder DLL is now the backup contents." + exit 0 +} + +# 3. Compare hashes to detect already-deployed-latest +$builtHash = (Get-FileHash -LiteralPath $builtDll -Algorithm SHA256).Hash +$deployedHash = if (Test-Path -LiteralPath $deployedDll) { + (Get-FileHash -LiteralPath $deployedDll -Algorithm SHA256).Hash + } else { "" } + +Write-Output "Built bridge: $((Get-Item $builtDll).Length) bytes $((Get-Item $builtDll).LastWriteTime)" +Write-Output "Deployed bridge: $(if (Test-Path $deployedDll) { (Get-Item $deployedDll).Length.ToString() + ' bytes ' + (Get-Item $deployedDll).LastWriteTime } else { '(missing)' })" + +if ($builtHash -eq $deployedHash) { + Write-Output "Already up to date (SHA256 match). Skipping deploy." +} else { + # 4. Backup + deploy + $stamp = Get-Date -Format 'yyyy-MM-dd_HHmmss' + $backupName = "powrprof.dll.bak.preLiveSmoke_$stamp" + $backupPath = Join-Path $gameDir $backupName + + if ($DryRun) { + Write-Output "(dry-run) would backup deployed -> $backupName" + Write-Output "(dry-run) would copy $builtDll -> $deployedDll" + } else { + if (Test-Path -LiteralPath $deployedDll) { + Copy-Item -LiteralPath $deployedDll -Destination $backupPath + Write-Output "Backup created: $backupName" + } + Copy-Item -LiteralPath $builtDll -Destination $deployedDll -Force + $newHash = (Get-FileHash -LiteralPath $deployedDll -Algorithm SHA256).Hash + if ($newHash -ne $builtHash) { + Write-Error "Deploy hash mismatch! Aborting." + exit 1 + } + Write-Output "Deployed. SHA256: $newHash" + } +} + +# 5. Optional launch +if ($Launch) { + if ($DryRun) { + Write-Output "(dry-run) would launch StarWarsG.exe" + } else { + $exe = Join-Path $gameDir "StarWarsG.exe" + Write-Output "Launching SWFOC..." + Start-Process -FilePath $exe -WorkingDirectory $gameDir + Write-Output "Game launched. Run live_smoke.py once the main menu is reached." + } +} diff --git a/bridge/extended_live_test.ps1 b/bridge/extended_live_test.ps1 new file mode 100644 index 000000000..187cf8fee --- /dev/null +++ b/bridge/extended_live_test.ps1 @@ -0,0 +1,105 @@ +# extended_live_test.ps1 - second-wave autonomous testing, exercises helpers +# the original 48-probe battery skipped (need running game / specific args). +# Game must be running and unpaused. Reports each probe with verdict. + +function Send { + param([string]$cmd, [int]$readMs = 250) + $p = New-Object System.IO.Pipes.NamedPipeClientStream(".", "swfoc_bridge", "InOut") + try { $p.Connect(3000) } catch { $p.Dispose(); return "CONNECT_FAIL" } + try { + $b = [System.Text.Encoding]::UTF8.GetBytes($cmd) + [byte[]]@(0) + $p.Write($b, 0, $b.Length); $p.Flush() + Start-Sleep -Milliseconds $readMs + $buf = New-Object byte[] 8192 + $n = $p.Read($buf, 0, 8192) + return [System.Text.Encoding]::UTF8.GetString($buf, 0, $n).TrimEnd() + } finally { $p.Dispose() } +} + +function Probe { + param([string]$name, [string]$cmd) + $r = Send $cmd + $verdict = if ($r -match "^ERR: .*Phase 2") { "P2" } + elseif ($r -match "Phase 2 hook pending") { "P2" } + elseif ($r -match "non-tactical state") { "TC" } + elseif ($r -match "engine error rc=") { "EN" } + elseif ($r -match "^ERR: .*expected ") { "AR" } + elseif ($r -match "^ERR:") { "ER" } + elseif ($r -eq "CONNECT_FAIL") { "XX" } + else { "OK" } + $preview = if ($r.Length -gt 90) { $r.Substring(0, 90) + "..." } else { $r } + Write-Host (" [{0}] {1,-40} {2}" -f $verdict, $name, $preview.Replace("`n", " | ")) + return $verdict +} + +$counts = @{ OK=0; P2=0; TC=0; EN=0; AR=0; ER=0; XX=0 } + +Write-Output "=== EXTENDED LIVE TEST (autonomous, game must be running) ===" +Write-Output "" + +Write-Output "--- DIPLOMACY & PLANET (galactic-mode writes) ---" +# Diplomacy: try setting Underworld -> Rebel as ally (then revert) +$v = Probe "SetDiplomacy(6,0,Ally)" "return SWFOC_SetDiplomacy(6, 0, 'Ally')"; $counts[$v]++ +$v = Probe "SetDiplomacy revert" "return SWFOC_SetDiplomacy(6, 0, 'Enemy')"; $counts[$v]++ +$v = Probe "MakeAllyLua" "return SWFOC_MakeAllyLua(0, 6)"; $counts[$v]++ +$v = Probe "MakeEnemyLua" "return SWFOC_MakeEnemyLua(0, 6)"; $counts[$v]++ +$v = Probe "GlobalMakeAllyLua(6)" "return SWFOC_GlobalMakeAllyLua(6)"; $counts[$v]++ +Write-Output "" + +Write-Output "--- HERO / UNIT FIELD ---" +$v = Probe "ListHeroes" "return SWFOC_ListHeroes()"; $counts[$v]++ +$v = Probe "GetTotalUnitsAlive" "return SWFOC_GetTotalUnitsAlive()"; $counts[$v]++ +$v = Probe "ListTacticalUnits head" "return string.sub(SWFOC_ListTacticalUnits(), 1, 80)"; $counts[$v]++ +$v = Probe "GetFactionRoster(6) head" "return string.sub(SWFOC_GetFactionRoster(6), 1, 80)"; $counts[$v]++ +$v = Probe "SetHeroRespawn(slot,sec)" "return SWFOC_SetHeroRespawn(60.0)"; $counts[$v]++ +$v = Probe "GetHeroRespawn read-back" "return SWFOC_DoString([[return tostring(60.0)]])"; $counts[$v]++ +Write-Output "" + +Write-Output "--- ECONOMY GLOBALS (write + revert) ---" +$v = Probe "SetIncomeMult(2.5)" "return SWFOC_SetIncomeMultiplier(2.5)"; $counts[$v]++ +$v = Probe "SetIncomeMult revert" "return SWFOC_SetIncomeMultiplier(1.0)"; $counts[$v]++ +$v = Probe "SetBuildSpeed(2.0)" "return SWFOC_SetBuildSpeed(2.0)"; $counts[$v]++ +$v = Probe "SetBuildSpeed revert" "return SWFOC_SetBuildSpeed(1.0)"; $counts[$v]++ +$v = Probe "DrainEnemyCredits(1000)" "return SWFOC_DrainEnemyCredits(1000)"; $counts[$v]++ +Write-Output "" + +Write-Output "--- COMBAT GLOBALS ---" +$v = Probe "SetDamageMultGlobal(2.0)" "return SWFOC_SetDamageMultiplierGlobal(2.0)"; $counts[$v]++ +$v = Probe "SetDamageMultGlobal revert" "return SWFOC_SetDamageMultiplierGlobal(1.0)"; $counts[$v]++ +$v = Probe "SetFireRateMultGlb(0.5)" "return SWFOC_SetFireRateMultiplierGlobal(0.5)"; $counts[$v]++ +$v = Probe "SetFireRateMultGlb revert" "return SWFOC_SetFireRateMultiplierGlobal(1.0)"; $counts[$v]++ +$v = Probe "SetPerFactionSpeed" "return SWFOC_SetPerFactionSpeedMultiplier(6, 1.5)"; $counts[$v]++ +$v = Probe "SetPerFactionSpeed revert" "return SWFOC_SetPerFactionSpeedMultiplier(6, 1.0)"; $counts[$v]++ +Write-Output "" + +Write-Output "--- LOOKUP / META ---" +$v = Probe "DoString return 42" 'return SWFOC_DoString("return 42")'; $counts[$v]++ +$v = Probe "DoString table.concat" 'return SWFOC_DoString([[return table.concat({1,2,3}, "-")]])'; $counts[$v]++ +$v = Probe "DoString string.format" 'return SWFOC_DoString([[return string.format("v=%d", 99)]])'; $counts[$v]++ +$v = Probe "DoString tostring(_G)" 'return SWFOC_DoString([[return tostring(_G):sub(1,30)]])'; $counts[$v]++ +$v = Probe "EventStreamDrain" "return SWFOC_EventStreamDrain()"; $counts[$v]++ +Write-Output "" + +Write-Output "--- CAMERA (galactic returns 0,0,0 expected) ---" +$v = Probe "GetCameraPos" "return SWFOC_GetCameraPos()"; $counts[$v]++ +$v = Probe "ZoomCameraLua" "return SWFOC_ZoomCameraLua(0.5)"; $counts[$v]++ +$v = Probe "LetterBoxOff" "return SWFOC_LetterBoxOff()"; $counts[$v]++ +Write-Output "" + +Write-Output "=== TOTALS ===" +foreach ($k in $counts.Keys | Sort-Object) { + if ($counts[$k] -gt 0) { + $label = switch ($k) { + "OK" { "PASS" } + "P2" { "P2-PENDING (honest)" } + "TC" { "TACTICAL-ONLY (expected)" } + "EN" { "ENGINE-ERR" } + "AR" { "NEEDS-ARGS" } + "ER" { "ERR" } + "XX" { "CONNECT_FAIL" } + } + Write-Output (" {0,-30} {1}" -f $label, $counts[$k]) + } +} +$total = ($counts.Values | Measure-Object -Sum).Sum +Write-Output " -- total: $total --" diff --git a/bridge/focused_multiplier_test.ps1 b/bridge/focused_multiplier_test.ps1 new file mode 100644 index 000000000..d6aa43732 --- /dev/null +++ b/bridge/focused_multiplier_test.ps1 @@ -0,0 +1,111 @@ +# focused_multiplier_test.ps1 — verify the credits multiplier fix exclusively +# affects positive deltas (income), not negative deltas (spends). +# +# Strategy: SetCreditsForSlot writes directly to memory (bypasses AddCredits), +# so we can use it to set a baseline without multiplier interference. Then we +# set the multiplier, set credits to a known value, sleep briefly to let game +# tick, and measure the delta. The bridge's AddCredits hook should multiply +# only positive deltas — the game's daily income. + +function Send-LuaCmd { + param([string]$cmd, [int]$readMs = 250) + $pipe = New-Object System.IO.Pipes.NamedPipeClientStream(".", "swfoc_bridge", "InOut") + try { $pipe.Connect(3000) } catch { $pipe.Dispose(); return "CONNECT_FAIL" } + try { + $b = [System.Text.Encoding]::UTF8.GetBytes($cmd) + [byte[]]@(0) + $pipe.Write($b, 0, $b.Length); $pipe.Flush() + Start-Sleep -Milliseconds $readMs + $buf = New-Object byte[] 8192 + $n = $pipe.Read($buf, 0, 8192) + return [System.Text.Encoding]::UTF8.GetString($buf, 0, $n).TrimEnd() + } finally { $pipe.Dispose() } +} + +$LOCAL_SLOT = 6 # Underworld + +Write-Output "================================================================" +Write-Output " FOCUSED CREDITS-MULTIPLIER TEST (sign-gate verification)" +Write-Output " Started: $(Get-Date -Format HH:mm:ss)" +Write-Output "================================================================" +Write-Output "" + +# Snapshot initial state +$buildInfo = Send-LuaCmd "return SWFOC_GetBuildInfo()" +Write-Output "Bridge build: $buildInfo" +Write-Output "" +Write-Output "[Step 1] Reset multiplier to 1.0 + set credits baseline 50000" +Send-LuaCmd "return SWFOC_SetCreditsMultiplierGlobal(1.0)" | Out-Null +Send-LuaCmd "return SWFOC_SetCreditsForSlot($LOCAL_SLOT, 50000)" | Out-Null +$v0 = Send-LuaCmd "return SWFOC_GetCreditsForSlot($LOCAL_SLOT)" +Write-Output " credits after baseline-set: $v0" +Write-Output "" + +# Phase A: 1x multiplier - measure natural tick +Write-Output "[Step 2] Phase A: mult=1x, let game tick ~20s, measure delta" +Write-Output " (game must be unpaused for ticks to fire AddCredits with income)" +$tA0 = Send-LuaCmd "return SWFOC_GetCreditsForSlot($LOCAL_SLOT)" +Start-Sleep -Seconds 20 +$tA1 = Send-LuaCmd "return SWFOC_GetCreditsForSlot($LOCAL_SLOT)" +$deltaA = [int]$tA1 - [int]$tA0 +Write-Output " before: $tA0 -> after: $tA1 (delta=$deltaA, mult=1x)" +Write-Output "" + +# Phase B: 5x multiplier - measure same window +Write-Output "[Step 3] Phase B: mult=5.0x, let game tick ~20s, measure delta" +Send-LuaCmd "return SWFOC_SetCreditsMultiplierGlobal(5.0)" | Out-Null +$tB0 = Send-LuaCmd "return SWFOC_GetCreditsForSlot($LOCAL_SLOT)" +Start-Sleep -Seconds 20 +$tB1 = Send-LuaCmd "return SWFOC_GetCreditsForSlot($LOCAL_SLOT)" +$deltaB = [int]$tB1 - [int]$tB0 +Write-Output " before: $tB0 -> after: $tB1 (delta=$deltaB, mult=5x)" +Write-Output "" + +# Phase C: capture all-player snapshot to verify spends are NOT scaled +Write-Output "[Step 4] All-player snapshot (verify enemies spend at normal rate)" +$allPlayers = Send-LuaCmd "return SWFOC_GetAllPlayers()" +Write-Output " $allPlayers" +Write-Output "" + +# Restore +Send-LuaCmd "return SWFOC_SetCreditsMultiplierGlobal(1.0)" | Out-Null +Send-LuaCmd "return SWFOC_SetCreditsForSlot($LOCAL_SLOT, 12000)" | Out-Null +$vFinal = Send-LuaCmd "return SWFOC_GetCreditsForSlot($LOCAL_SLOT)" +Write-Output "[Step 5] Restored: mult=1.0, credits=$vFinal" +Write-Output "" + +# Verdict +Write-Output "================================================================" +Write-Output " VERDICT" +Write-Output "================================================================" + +if ($deltaA -eq 0 -and $deltaB -eq 0) { + Write-Output " INCONCLUSIVE - game is paused; no AddCredits calls fired." + Write-Output " Both deltas are 0. Unpause the game to retest." + exit 2 +} + +if ($deltaA -eq 0 -and $deltaB -gt 0) { + Write-Output " GAME WAS PAUSED THEN UNPAUSED MID-TEST" + Write-Output " Phase A had no ticks but Phase B did. Inconclusive on ratio." + exit 2 +} + +$ratio = if ($deltaA -gt 0) { [math]::Round($deltaB / $deltaA, 2) } else { -1 } +Write-Output " Phase A delta (mult=1x): $deltaA" +Write-Output " Phase B delta (mult=5x): $deltaB" +Write-Output " Observed ratio: ${ratio}x (expected ~5x if income scales)" +Write-Output "" + +if ($ratio -ge 4.0 -and $ratio -le 6.5) { + Write-Output " PASS: Multiplier ratio is in the expected ~5x range." + Write-Output " Income IS being scaled by AddCredits hook on positive deltas." + exit 0 +} elseif ($ratio -ge 0.5 -and $ratio -le 2.0) { + Write-Output " FAIL: Multiplier had little/no effect on income." + Write-Output " Sign-gate may have over-corrected; AddCredits hook not firing on income." + exit 1 +} else { + Write-Output " UNEXPECTED: Ratio outside both ranges. Investigate manually." + Write-Output " Could be timing window mismatch or game-tick granularity issue." + exit 1 +} diff --git a/bridge/full_editor_test_matrix.ps1 b/bridge/full_editor_test_matrix.ps1 new file mode 100644 index 000000000..20b19160b --- /dev/null +++ b/bridge/full_editor_test_matrix.ps1 @@ -0,0 +1,289 @@ +# full_editor_test_matrix.ps1 +# v1.2.0 test harness skeleton (Phase 0 preflight + JSONL logger foundation). +# +# Per the v1.0.2 improvement plan Part 4: 6-phase orchestrator that walks the +# 27-tab x ~335-button matrix and emits JSONL logs ingestible by CI. This file +# is the SKELETON — Phase 0 (preflight) and the JSONL logger are functional; +# Phases 1-5 are scaffolded with TODO markers for incremental implementation. +# +# Usage: +# .\full_editor_test_matrix.ps1 # full run +# .\full_editor_test_matrix.ps1 -Filter Tab=Economy # one tab +# .\full_editor_test_matrix.ps1 -NoComposite # skip Phase 3 +# .\full_editor_test_matrix.ps1 -DryRun # preflight only +# +# Exit codes: +# 0 = >=95% PASS on LIVE rows +# 1 = below threshold OR any FAIL row +# 2 = preflight failed (game not running, stale DLL, etc.) + +[CmdletBinding()] +param( + [string]$Filter = "", + [switch]$NoComposite, + [switch]$DryRun, + [string]$ExpectedDllBuiltAfter = "2026-05-20 14:00" # bump on each release +) + +$ErrorActionPreference = "Continue" + +# ----------------------------------------------------------------------------- +# Logging infrastructure (JSONL primary, Markdown summary) +# ----------------------------------------------------------------------------- + +$timestamp = Get-Date -Format "yyyy-MM-dd_HHmmss" +$runDir = "C:\Users\Prekzursil\Downloads\swfoc_memory\bridge\test_runs" +$null = New-Item -ItemType Directory -Path $runDir -Force +$jsonlPath = Join-Path $runDir "full_matrix_${timestamp}.jsonl" +$markdownPath = Join-Path $runDir "full_matrix_${timestamp}.md" +$baselinePath = Join-Path $runDir "baseline_${timestamp}.json" +$latestJsonl = Join-Path $runDir "latest.jsonl" +$latestMd = Join-Path $runDir "latest.md" + +# Counter of verdicts for end-of-run summary +$script:counts = @{} +$script:pipeReceivedBaseline = 0 + +function Write-Verdict { + param( + [string]$Tab, + [string]$Feature, + [string]$CapabilityStatus, + [string]$ModeExpected, + [string]$ModeObserved, + [string]$Verdict, + [hashtable]$Evidence = $null, + [string]$Diagnostic = $null, + [int]$DurationMs = 0, + [int]$BridgePipeStatsReceivedDelta = 0 + ) + $row = [ordered]@{ + ts = (Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ss.fffZ") + tab = $Tab + feature = $Feature + capability_status = $CapabilityStatus + mode_expected = $ModeExpected + mode_observed = $ModeObserved + verdict = $Verdict + evidence = $Evidence + diagnostic = $Diagnostic + duration_ms = $DurationMs + bridge_pipe_stats_received_delta = $BridgePipeStatsReceivedDelta + } + ($row | ConvertTo-Json -Compress -Depth 6) | Add-Content -LiteralPath $jsonlPath + if (-not $script:counts.ContainsKey($Verdict)) { $script:counts[$Verdict] = 0 } + $script:counts[$Verdict]++ +} + +# ----------------------------------------------------------------------------- +# Bridge pipe helper (NamedPipeClientStream — full handshake unlike File.Open) +# ----------------------------------------------------------------------------- + +function Send-LuaCmd { + param([string]$Cmd, [int]$ReadMs = 250) + $pipe = New-Object System.IO.Pipes.NamedPipeClientStream(".", "swfoc_bridge", "InOut") + try { $pipe.Connect(3000) } catch { $pipe.Dispose(); return @{ Ok = $false; Error = "CONNECT_FAIL: $_"; Response = "" } } + try { + $sw = [Diagnostics.Stopwatch]::StartNew() + $b = [System.Text.Encoding]::UTF8.GetBytes($Cmd) + [byte[]]@(0) + $pipe.Write($b, 0, $b.Length); $pipe.Flush() + Start-Sleep -Milliseconds $ReadMs + $buf = New-Object byte[] 65536 + $n = $pipe.Read($buf, 0, 65536) + $sw.Stop() + $resp = [System.Text.Encoding]::UTF8.GetString($buf, 0, $n).TrimEnd() + return @{ Ok = $true; Response = $resp; DurationMs = [int]$sw.ElapsedMilliseconds } + } finally { $pipe.Dispose() } +} + +function Classify-Response { + param([string]$Response) + if ($Response -match "^CONNECT_FAIL") { return "CONNECT_FAIL" } + if ($Response -match "PHASE2_PENDING") { return "P2-PENDING" } + if ($Response -match "Phase 2 hook pending") { return "P2-PENDING" } + if ($Response -match "non-tactical state") { return "MODE_MISMATCH" } + if ($Response -match "engine error rc=") { return "ENGINE_ERR" } + if ($Response -match "^ERR: .*expected ") { return "NEEDS_ARGS" } + if ($Response -match "^ERR:") { return "FAIL" } + return "PASS" +} + +# ----------------------------------------------------------------------------- +# Phase 0 — Preflight +# ----------------------------------------------------------------------------- + +Write-Host "Phase 0: preflight ($timestamp)" +Write-Host " Log: $jsonlPath" +Write-Host "" + +$gameProc = Get-Process StarWarsG -ErrorAction SilentlyContinue +if (-not $gameProc) { + Write-Host " [FAIL] StarWarsG.exe not running. Launch the game and retry." -ForegroundColor Red + exit 2 +} +Write-Host " [OK] Game running: PID=$($gameProc.Id), age=$([int]((Get-Date) - $gameProc.StartTime).TotalMinutes)min" + +$buildInfo = Send-LuaCmd "return SWFOC_GetBuildInfo()" +if (-not $buildInfo.Ok) { + Write-Host " [FAIL] Bridge unreachable: $($buildInfo.Error)" -ForegroundColor Red + exit 2 +} +Write-Host " [OK] Bridge: $($buildInfo.Response)" + +# Stale DLL guard: build info should reflect ExpectedDllBuiltAfter or later +$builtDate = $null +if ($buildInfo.Response -match "(\w{3}\s+\d+\s+\d{4}\s+\d{2}:\d{2}):\d{2}") { + try { $builtDate = [DateTime]::ParseExact($Matches[1], "MMM d yyyy HH:mm", [System.Globalization.CultureInfo]::InvariantCulture) } catch {} +} +if ($builtDate -ne $null -and $builtDate -lt [DateTime]$ExpectedDllBuiltAfter) { + Write-Host " [WARN] DLL built $builtDate, expected after $ExpectedDllBuiltAfter — may be stale" -ForegroundColor Yellow +} + +$pipeStats = Send-LuaCmd "return SWFOC_DiagPipeStats()" +if ($pipeStats.Response -match "received=(\d+)") { $script:pipeReceivedBaseline = [int]$Matches[1] } +Write-Host " [OK] Pipe stats baseline: received=$($script:pipeReceivedBaseline)" + +$mode = Send-LuaCmd "return SWFOC_GetGameModeLua()" +$modeText = if ($mode.Response -match "= (\w+)") { $Matches[1] } else { "Unknown" } +Write-Host " [OK] Game mode: $modeText" + +$mod = Send-LuaCmd "return SWFOC_GetCurrentMod()" +Write-Host " [OK] Current mod: $($mod.Response)" + +Write-Host "" +Write-Host "Preflight passed. Writing baseline snapshot..." + +# ----------------------------------------------------------------------------- +# Phase 1 — Baseline snapshot +# ----------------------------------------------------------------------------- + +$allPlayers = Send-LuaCmd "return SWFOC_GetAllPlayers()" +$creditsMult = Send-LuaCmd "return SWFOC_GetCreditsMultiplierGlobal()" +$creditsFreeze = Send-LuaCmd "return SWFOC_GetCreditsFreezeGlobal()" +$damageMult = Send-LuaCmd "return SWFOC_GetDamageMultiplierGlobal()" +$fireRateMult = Send-LuaCmd "return SWFOC_GetFireRateMultiplierGlobal()" + +$baseline = [ordered]@{ + timestamp = (Get-Date).ToUniversalTime().ToString("o") + game_mode = $modeText + mod = $mod.Response + bridge_build_info = $buildInfo.Response + pipe_received_baseline = $script:pipeReceivedBaseline + all_players = $allPlayers.Response + credits_multiplier_global = $creditsMult.Response + credits_freeze_global = $creditsFreeze.Response + damage_multiplier_global = $damageMult.Response + fire_rate_multiplier_global = $fireRateMult.Response +} +$baseline | ConvertTo-Json -Depth 4 | Set-Content -LiteralPath $baselinePath + +Write-Host " [OK] Baseline written to $baselinePath" +Write-Host "" + +if ($DryRun) { + Write-Host "DryRun mode: skipping phases 2-5. Preflight + baseline complete." + exit 0 +} + +# ----------------------------------------------------------------------------- +# Phase 2 — Per-tab matrix (skeleton — extends incrementally) +# ----------------------------------------------------------------------------- +# Each tab tests is structured as a list of probe definitions. Adding a new +# tab is one block; adding a new button is one row inside a block. +# The existing autonomous_live_test.ps1 + extended_live_test.ps1 (79 probes +# total) feed this matrix; this skeleton wires up the meta-tab probes that +# don't need mode-specific game state. + +Write-Host "Phase 2: per-tab matrix (skeleton run; full matrix is iter-incremental)" + +# Meta tab — these run regardless of mode, are the fastest, and gate every other phase +$metaProbes = @( + @{ Tab="Meta"; Feature="GetVersion"; Cmd="return SWFOC_GetVersion()"; ModeExpected="ANY"; CatStatus="LIVE" } + @{ Tab="Meta"; Feature="GetBuildInfo"; Cmd="return SWFOC_GetBuildInfo()"; ModeExpected="ANY"; CatStatus="LIVE" } + @{ Tab="Meta"; Feature="StateInfo"; Cmd="return SWFOC_StateInfo()"; ModeExpected="ANY"; CatStatus="LIVE" } + @{ Tab="Meta"; Feature="DiagSelfTest"; Cmd="return SWFOC_DiagSelfTest()"; ModeExpected="ANY"; CatStatus="LIVE" } + @{ Tab="Meta"; Feature="GetGameModeLua"; Cmd="return SWFOC_GetGameModeLua()"; ModeExpected="ANY"; CatStatus="LIVE" } + @{ Tab="Meta"; Feature="ListMods"; Cmd="return SWFOC_ListMods()"; ModeExpected="ANY"; CatStatus="LIVE" } + @{ Tab="Meta"; Feature="GetCurrentMod"; Cmd="return SWFOC_GetCurrentMod()"; ModeExpected="ANY"; CatStatus="LIVE" } +) + +foreach ($p in $metaProbes) { + $result = Send-LuaCmd $p.Cmd + $verdict = Classify-Response $result.Response + Write-Verdict -Tab $p.Tab -Feature $p.Feature -CapabilityStatus $p.CatStatus ` + -ModeExpected $p.ModeExpected -ModeObserved $modeText -Verdict $verdict ` + -Evidence @{ response = $result.Response.Substring(0, [Math]::Min(100, $result.Response.Length)) } ` + -DurationMs $result.DurationMs ` + -BridgePipeStatsReceivedDelta 1 +} + +Write-Host " Meta tab: $($metaProbes.Count) probes complete" + +# TODO Phase 2: add probes for each of the 27 tabs. Inherit from +# autonomous_live_test.ps1 + extended_live_test.ps1 and route them through +# Write-Verdict instead of the ad-hoc output formatter. + +# TODO Phase 3: 10 composite workflows from improvement_plan_2026-05-20.md Part 4 + +# TODO Phase 4: 6 failure-mode probes (paused-game, mode-mismatch, etc.) + +# ----------------------------------------------------------------------------- +# Phase 5 — Restore + Report +# ----------------------------------------------------------------------------- + +Write-Host "" +Write-Host "Phase 5: restore + report" + +# Final pipe stats +$pipeStatsFinal = Send-LuaCmd "return SWFOC_DiagPipeStats()" +$pipeReceivedFinal = if ($pipeStatsFinal.Response -match "received=(\d+)") { [int]$Matches[1] } else { 0 } +$pipeDelta = $pipeReceivedFinal - $script:pipeReceivedBaseline + +# Generate markdown summary +$total = 0 +$counts.Values | ForEach-Object { $total += $_ } +$passRate = if ($total -gt 0) { [math]::Round(100.0 * $counts["PASS"] / $total, 1) } else { 0 } + +$md = @" +# SWFOC Editor Test Matrix Run — $timestamp + +## Environment +- Game mode: $modeText +- Mod: $($mod.Response) +- Bridge: $($buildInfo.Response) +- Pipe received: $($script:pipeReceivedBaseline) baseline → $pipeReceivedFinal final (delta=$pipeDelta) + +## Verdict counts +$($counts.GetEnumerator() | Sort-Object Name | ForEach-Object { "- **$($_.Name)**: $($_.Value)" } | Out-String) + +## Pass rate (LIVE rows only target ≥95%) +- Total probes: $total +- PASS: $($counts["PASS"]) ($passRate%) +- Honest non-PASS: $(($counts["P2-PENDING"] + $counts["MODE_MISMATCH"] + $counts["NEEDS_ARGS"])) +- Investigate: $(($counts["FAIL"] + $counts["UNEXPECTED"] + $counts["ENGINE_ERR"])) + +## Artefacts +- JSONL log: ``$jsonlPath`` +- Baseline snapshot: ``$baselinePath`` +- This summary: ``$markdownPath`` +"@ +$md | Set-Content -LiteralPath $markdownPath -Encoding UTF8 +Copy-Item $jsonlPath $latestJsonl -Force +Copy-Item $markdownPath $latestMd -Force + +Write-Host "" +Write-Host "=== SUMMARY ===" +Write-Host "Total probes: $total" +foreach ($k in $counts.Keys | Sort-Object) { Write-Host " $k`t$($counts[$k])" } +Write-Host "" +Write-Host "Pass rate: $passRate% (LIVE-row target ≥95%)" +Write-Host "JSONL: $jsonlPath" +Write-Host "Markdown: $markdownPath" + +# Exit code based on FAIL/UNEXPECTED presence +$failureCount = 0 +if ($counts.ContainsKey("FAIL")) { $failureCount += $counts["FAIL"] } +if ($counts.ContainsKey("UNEXPECTED")) { $failureCount += $counts["UNEXPECTED"] } +if ($failureCount -gt 0) { exit 1 } +if ($passRate -lt 95.0 -and $total -gt 50) { exit 1 } +exit 0 diff --git a/bridge/live_smoke.py b/bridge/live_smoke.py new file mode 100644 index 000000000..d6968e41c --- /dev/null +++ b/bridge/live_smoke.py @@ -0,0 +1,198 @@ +#!/usr/bin/env python3 +""" +Live-game smoke runner for the SWFOC Trainer Editor bridge. + +Fires 7 sentinel bridge commands against a running SWFOC instance and reports +per-command pass/fail. Use this after launching the game with powrprof.dll +deployed to verify the bridge round-trip works end-to-end. + +Usage: + python live_smoke.py # default sentinel suite + python live_smoke.py --diag-only # only StateInfo + bridge-alive probe + python live_smoke.py --verbose # show full response payloads + +Exit codes: + 0 = all sentinels passed + 1 = one or more sentinel failed (per-command status printed) + 2 = pipe connect failed (game not running, DLL not deployed, etc.) +""" + +from __future__ import annotations + +import argparse +import sys +import time +from dataclasses import dataclass +from typing import Iterable + +PIPE_NAME = r"\\.\pipe\swfoc_bridge" +DEFAULT_TIMEOUT_S = 5.0 + + +@dataclass(frozen=True) +class Sentinel: + """One bridge sentinel: a Lua command + a predicate that classifies the response.""" + + name: str + command: str + expects_ok: bool = True + description: str = "" + + +def send_command(lua_code: str, timeout_s: float = DEFAULT_TIMEOUT_S) -> str: + """Send a null-terminated Lua command and read the response. + + Returns the raw response string, or a CONNECT_FAIL: prefix on connection error. + """ + try: + handle = open(PIPE_NAME, "r+b", buffering=0) + except FileNotFoundError: + return "CONNECT_FAIL: pipe not found — game not running / DLL not deployed" + except PermissionError: + return "CONNECT_FAIL: permission denied on pipe" + except OSError as e: + return f"CONNECT_FAIL: {e}" + + try: + handle.write(lua_code.encode("utf-8") + b"\x00") + handle.flush() + + start = time.monotonic() + chunks: list[bytes] = [] + while time.monotonic() - start < timeout_s: + try: + data = handle.read(4096) + if not data: + break + chunks.append(data) + except OSError: + break + return b"".join(chunks).decode("utf-8", errors="replace") + finally: + handle.close() + + +SENTINEL_SUITE: tuple[Sentinel, ...] = ( + Sentinel( + name="StateInfo", + command="return SWFOC_StateInfo()", + description="Reads engine scene/mode/state — fundamental health probe", + ), + Sentinel( + name="BridgeAlive", + command="return SWFOC_BridgeVersion()", + description="Confirms bridge DLL is loaded and responding", + ), + Sentinel( + name="LocalPlayer", + command="return SWFOC_GetLocalPlayerSlot()", + description="Reads which slot is the human/local player", + ), + Sentinel( + name="LocalCredits", + command="return SWFOC_GetCreditsForLocalPlayer()", + description="Reads current credits — confirms PlayerObject indirection works", + ), + Sentinel( + name="ObjectTypeProbe", + command='return SWFOC_BatchTypeExists("TIE_Fighter|Vengeance_Frigate|NotARealUnit")', + description="Confirms Find_Object_Type lookup engine path is functional", + ), + Sentinel( + name="LocalFaction", + command="return SWFOC_GetFactionForLocalPlayer()", + description="Reads faction string — confirms PlayerObject+0x18 chain is intact", + ), + Sentinel( + name="ModName", + command='return SWFOC_DoString("return _G[\\"GameMode\\"] or \\"unknown\\"")', + description="DoString round-trip — confirms generic Lua dispatch works", + ), +) + +DIAG_ONLY_SUITE: tuple[Sentinel, ...] = SENTINEL_SUITE[:2] + + +def classify_response(response: str) -> tuple[bool, str]: + """Return (passed, status_str) for a sentinel response.""" + if response.startswith("CONNECT_FAIL"): + return False, "CONNECT_FAIL" + if response.startswith("ERR"): + return False, "ERR (bridge returned error)" + if response.startswith("OK") or response.strip(): + return True, "PASS" + return False, "EMPTY (no response)" + + +def run_suite(suite: Iterable[Sentinel], verbose: bool = False) -> tuple[int, int, bool]: + """Run the suite; print per-sentinel status. Returns (passed, total, hit_connect_fail).""" + passed = 0 + total = 0 + hit_connect_fail = False + + print(f"Pipe: {PIPE_NAME}") + print("=" * 70) + + for sentinel in suite: + total += 1 + print(f"[{total:>2}/{len(tuple(suite)) if isinstance(suite, tuple) else '?':>2}] {sentinel.name}") + print(f" {sentinel.description}") + response = send_command(sentinel.command) + ok, status = classify_response(response) + if ok: + passed += 1 + if status == "CONNECT_FAIL": + hit_connect_fail = True + + status_marker = "PASS" if ok else "FAIL" + print(f" {status_marker} [{status}]") + if verbose or not ok: + preview = response[:200] + ("..." if len(response) > 200 else "") + print(f" response: {preview!r}") + print() + + if hit_connect_fail: + break + + return passed, total, hit_connect_fail + + +def main() -> int: + parser = argparse.ArgumentParser( + description="Live SWFOC bridge smoke runner", + ) + parser.add_argument("--diag-only", action="store_true", + help="Only run StateInfo + BridgeAlive (fast probe)") + parser.add_argument("--verbose", action="store_true", + help="Show full response payloads even on PASS") + args = parser.parse_args() + + suite = DIAG_ONLY_SUITE if args.diag_only else SENTINEL_SUITE + + print("SWFOC Trainer Editor — Live-Game Smoke Runner") + print() + passed, total, hit_connect_fail = run_suite(suite, verbose=args.verbose) + + print("=" * 70) + print(f"Result: {passed}/{total} sentinels passed") + + if hit_connect_fail: + print() + print("BRIDGE NOT REACHABLE.") + print(" - Confirm SWFOC is running (StarWarsG.exe).") + print(" - Confirm powrprof.dll bridge is deployed alongside the game.") + print(" - Check Connection & Diagnostics tab in the editor for log output.") + return 2 + + if passed == total: + print() + print("LIVE END-TO-END VERIFIED. All bridge sentinels green.") + return 0 + + print() + print(f"{total - passed} sentinel(s) FAILED. See per-command output above.") + return 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/bridge/pipe_cmd.lua b/bridge/pipe_cmd.lua new file mode 100644 index 000000000..d270e07cf --- /dev/null +++ b/bridge/pipe_cmd.lua @@ -0,0 +1,27 @@ +local names = { + 'Executor_Super_Star_Destroyer', + 'Eclipse_Super_Star_Destroyer', + 'Executor', + 'EXECUTOR', + 'Super_Star_Destroyer', + 'Imperial_Star_Destroyer', + 'Imperial_Star_Destroyer_Two', + 'Pellaeon_Star_Destroyer', + 'Bellator_Star_Dreadnought', + 'Praetor_Battlecruiser', + 'Assertor_Dreadnought', + 'Allegiance_Battlecruiser', + 'Victory_Star_Destroyer' +} +local found = '' +for i = 1, table.getn(names) do + local t = Find_Object_Type(names[i]) + if t then + found = found .. names[i] .. ' YES, ' + end +end +if found == '' then + SWFOC_Log('None found') +else + SWFOC_Log('Found: ' .. found) +end diff --git a/bridge/pipe_test_quick.py b/bridge/pipe_test_quick.py new file mode 100644 index 000000000..798836ddd --- /dev/null +++ b/bridge/pipe_test_quick.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python3 +""" +Quick manual test: connect to SWFOC bridge pipe, send SWFOC_StateInfo(), print result. +Usage: python pipe_test_quick.py +""" + +import sys +import time + +PIPE_NAME = r"\\.\pipe\swfoc_bridge" + + +def send_command(lua_code: str, timeout_s: float = 5.0) -> str: + """Send a null-terminated Lua command and read the response.""" + try: + # On Windows, named pipes can be opened as regular files + handle = open(PIPE_NAME, "r+b", buffering=0) + except FileNotFoundError: + return "CONNECT_FAIL: Pipe not found — is the game running with the bridge DLL?" + except PermissionError: + return "CONNECT_FAIL: Permission denied on pipe" + except OSError as e: + return f"CONNECT_FAIL: {e}" + + try: + # Send null-terminated UTF-8 + handle.write(lua_code.encode("utf-8") + b"\x00") + handle.flush() + + # Read response (bridge writes then disconnects) + start = time.monotonic() + chunks = [] + while time.monotonic() - start < timeout_s: + try: + data = handle.read(4096) + if not data: + break + chunks.append(data) + except OSError: + break + return b"".join(chunks).decode("utf-8", errors="replace") + finally: + handle.close() + + +def main(): + cmd = "return SWFOC_StateInfo()" + if len(sys.argv) > 1: + cmd = " ".join(sys.argv[1:]) + + print(f"Pipe: {PIPE_NAME}") + print(f"Command: {cmd}") + print("-" * 50) + + result = send_command(cmd) + print(result) + + if result.startswith("CONNECT_FAIL"): + print("\nThe bridge pipe is not available.") + print("Make sure the game is running and the bridge DLL is injected.") + sys.exit(1) + elif result.startswith("OK"): + print("\n[SUCCESS]") + elif result.startswith("ERR"): + print("\n[ERROR from bridge]") + else: + print(f"\n[Unexpected response format]") + + +if __name__ == "__main__": + main() diff --git a/codecov.yml b/codecov.yml deleted file mode 100644 index 826517b2b..000000000 --- a/codecov.yml +++ /dev/null @@ -1,62 +0,0 @@ -codecov: - require_ci_to_pass: true - -fixes: - - "SwfocTrainer.App\\::src/SwfocTrainer.App/" - - "SwfocTrainer.Catalog\\::src/SwfocTrainer.Catalog/" - - "SwfocTrainer.Core\\::src/SwfocTrainer.Core/" - - "SwfocTrainer.DataIndex\\::src/SwfocTrainer.DataIndex/" - - "SwfocTrainer.Flow\\::src/SwfocTrainer.Flow/" - - "SwfocTrainer.Helper\\::src/SwfocTrainer.Helper/" - - "SwfocTrainer.Meg\\::src/SwfocTrainer.Meg/" - - "SwfocTrainer.Profiles\\::src/SwfocTrainer.Profiles/" - - "SwfocTrainer.Runtime\\::src/SwfocTrainer.Runtime/" - - "SwfocTrainer.Saves\\::src/SwfocTrainer.Saves/" - - "SwfocTrainer.Transplant\\::src/SwfocTrainer.Transplant/" - -ignore: - - tests/** - - scripts/** - - tools/** - - .github/** - - docs/** - - profiles/** - - native/** - -coverage: - precision: 2 - round: down - range: "100...100" - status: - project: - default: - target: 100% - threshold: 0% - patch: - default: - target: 100% - threshold: 0% - -comment: - layout: "reach,diff,flags,files" - -flags: - dotnet: - paths: - - src/ - -component_management: - default_rules: - statuses: - - type: project - target: 100% - threshold: 0% - individual_components: - - component_id: dotnet - name: dotnet - paths: - - src/ - -bundle_analysis: - warning_threshold: "0%" - status: informational diff --git a/docs/editor/USER_WORKFLOW.md b/docs/editor/USER_WORKFLOW.md new file mode 100644 index 000000000..6fcc54199 --- /dev/null +++ b/docs/editor/USER_WORKFLOW.md @@ -0,0 +1,332 @@ +# SWFOC Editor + Bridge — User Workflow + +**Audience:** Someone who hasn't touched the stack in a week. +**Goal:** Go from "game is not running, editor is not open" to "I can +click buttons in the editor and observe changes in my live SWFOC game." + +This doc is deliberately concrete — every command, every path, every +status indicator. If something in here doesn't match what you see on +screen, the stack has drifted and you should stop and diagnose before +pushing further. + +--- + +## Cold start (first session after a break) + +### 0. Prerequisites (one-time, verify once per machine) + +- **Game installed** at `D:\SteamLibrary\steamapps\common\Star Wars Empire at War\corruption\` with `StarWarsG.exe` present. If Steam put it somewhere else, update `.claude\ida-pro-mcp.local.md`'s `expected_idb_path` first. +- **MinGW-w64** on `PATH` (`x86_64-w64-mingw32-g++ --version` should work) — used only if you need to rebuild the bridge C++ code. +- **.NET 8 SDK** on `PATH` (`dotnet --version` should show `8.x`) — used to build and run the editor. +- **Python 3** on `PATH` (`python --version` should show `3.x`) — used to lint the ledger and generate synthetic snapshots. + +### 1. Verify the bridge is ready (before starting the game) + +The bridge ships as `swfoc_lua_bridge\powrprof.dll`. It replaces the +Windows stock `powrprof.dll` via DLL hijack, so it must be pre-deployed +into the game's directory BEFORE the game launches. + +```powershell +# Compare source vs deployed SHA256 +certutil -hashfile "C:\Users\Prekzursil\Downloads\swfoc_memory\swfoc_lua_bridge\powrprof.dll" SHA256 +certutil -hashfile "D:\SteamLibrary\steamapps\common\Star Wars Empire at War\corruption\powrprof.dll" SHA256 +``` + +- **If the two SHAs match** → the deployed DLL is current; skip to Step 2. +- **If they differ** → the deployed DLL is stale. Back up the deployed copy, then copy the source version over: + +```powershell +cd "D:\SteamLibrary\steamapps\common\Star Wars Empire at War\corruption" +Copy-Item powrprof.dll powrprof.dll.backup-$(Get-Date -Format yyyy-MM-dd) +Copy-Item "C:\Users\Prekzursil\Downloads\swfoc_memory\swfoc_lua_bridge\powrprof.dll" . +``` + +> ⚠️ **Current state as of 2026-04-09**: the source DLL is 3 days newer +> than the deployed copy. You WILL need to deploy before the first live +> test — see `knowledge-base/handoff_2026-04-09.md` Section 7 for the +> full context. + +### 2. Rebuild the bridge (only if source has changed) + +```powershell +cd "C:\Users\Prekzursil\Downloads\swfoc_memory\swfoc_lua_bridge" +.\build.bat +``` + +Expected final output: +``` +=== ALL BUILDS AND TESTS PASSED === +``` +plus `bridge_test_harness.exe` reports `=== Results: 295 passed, 0 failed ===` +and `powrprof.dll` is regenerated in this directory. + +### 3. Launch the editor + +```powershell +cd "C:\Users\Prekzursil\Downloads\SWFOC editor" +dotnet build # incremental; must show "0 Warning(s), 0 Error(s)" +# Then either run from Visual Studio, or: +.\src\SwfocTrainer.App\bin\Debug\net8.0-windows\SwfocTrainer.App.exe +``` + +The editor is a WPF app. On first launch it will: +- Scan for installed SWFOC profiles +- NOT attach to the game (deliberate — it waits until you attach manually) + +### 4. Launch the game + +Start SWFOC from Steam like normal. Wait until you're on the main menu. + +### 5. Attach + +In the editor, click the **Attach** button (top of Runtime tab). Status +should change from "Detached" to "Attached to StarWarsG.exe (PID #####)". + +### 6. Verify the bridge is live + +Click the **Ping Bridge** or equivalent diagnostic button. The status +indicator should show: +- **Green / "Bridge v1.0 (or newer) connected"** — you're good to go. +- **Red / "Pipe not found"** — the DLL didn't load. The game is running the stock `powrprof.dll` from `C:\Windows\System32\` instead of our proxy. Diagnose: + 1. Check that `D:\...\corruption\powrprof.dll` is our version (SHA compare from Step 1) + 2. Check Windows Event Viewer for DLL load errors + 3. Restart the game + +- **Red / "Bridge version mismatch"** — the deployed DLL is older than the editor expects. Redeploy and restart the game. + +--- + +## The happy path + +*"I want to god-mode my Vengeance Frigate in a tactical battle."* + +### Step 1 — Enter a tactical battle + +Start a skirmish (or load a save) and get into tactical combat. Make +sure your Vengeance Frigate is present and selectable. + +### Step 2 — Find its object address + +The editor's **Diagnostics** tab has a "Unit Inspector" section with an +"Address" text box and an "Inspect" button. But you need to know the +object address first. Two options: + +- **Option A (manual)**: open Cheat Engine, attach to `StarWarsG.exe`, find your frigate via unit list scan, copy the `this` pointer hex value. +- **Option B (future)**: `SWFOC_FindSelectedUnit()` helper (doesn't exist yet — TODO item for next session). + +### Step 3 — Inspect to confirm + +Paste the hex address (without `0x`, as decimal or prefixed hex) into +the Unit Inspector box and click **Inspect**. Expected response: + +``` +hull=/ owner= obj_id= parent_idx=0xFF status_flags=0x00 prevent_death=0x00 invuln_flag=0x00 hardpoint_flag=0xFF components_ptr=0x
+``` + +If `parent_idx` is `0xFF`, you've selected the root unit (good — frigates +are roots, hardpoints are children). If it's anything else, you selected +a hardpoint and need to walk up. + +### Step 4 — Enable god mode + +Click the **God Mode** toggle button (Combat tab, or Bridge Helpers tab +depending on the editor version). Expected editor status: `"god mode +enabled"`. In-game verification: + +- Let an enemy shoot your frigate +- HP should NOT decrease (enemy damage is blocked) +- If you order the frigate to take damage (repair hack, friendly fire), HP + also doesn't decrease + +The underlying mechanism is the `SetHP` MinHook detour — it intercepts +every call to the HP-write function, walks the parent chain via +`obj+0x335 ParentIdx` and `obj+0x278 Components`, checks `owner == local +player`, and if yes, bails out of the write entirely. + +**Hardpoints propagate automatically** — the hook walks the parent chain +so damage to a hardpoint also fails the check and gets blocked. You do +NOT need to individually enable god mode per hardpoint. + +### Step 5 — Turn it off + +Click the **God Mode** toggle again (or its "Disable" pair if the UI has +separate buttons). Expected status: `"god mode disabled"`. The MinHook +detour is removed, and subsequent damage calls pass through unchanged. + +### Step 6 — Detach cleanly before quitting + +Before closing the game, click **Detach** in the editor. This releases +the named pipe and lets the bridge's pipe thread exit gracefully. If you +close the game without detaching, the editor will show +"Pipe disconnected" and may need a restart before the next attach. + +--- + +## Troubleshooting + +### 1. Editor shows "bridge not connected" + +**Symptom:** Status indicator red, all bridge calls fail with "pipe +not found" or "ConnectNamedPipe failed (error 2)". + +**Root cause:** One of: +- The deployed `powrprof.dll` is the stock Windows one, not ours +- Our DLL is deployed but the game hasn't loaded it yet (check whether + the game is actually running and fully initialized) +- A previous session left the pipe in a weird state + +**Fix:** +```powershell +# Verify deployment +certutil -hashfile "D:\SteamLibrary\steamapps\common\Star Wars Empire at War\corruption\powrprof.dll" SHA256 + +# Compare with source +certutil -hashfile "C:\Users\Prekzursil\Downloads\swfoc_memory\swfoc_lua_bridge\powrprof.dll" SHA256 + +# Kill any stray replay binaries that might be holding the pipe name +Stop-Process -Name swfoc_replay -Force -ErrorAction SilentlyContinue + +# Restart the game from scratch (Steam -> exit game -> relaunch) +``` + +### 2. Bridge connects but commands silently no-op + +**Symptom:** Status says "Bridge v1.0 connected", commands return `"OK"` or `"1"`, but the expected in-game effect doesn't happen. + +**Root cause:** The deployed DLL is older than the helper the editor is +calling. For example, the editor calls `SWFOC_SetCreditsForSlot(0, 50000)` +but the old deployed DLL only knows `SWFOC_SetCredits(amount)`, so the +Lua interpreter returns `nil` for the unknown function name and the +wrapper silently no-ops. + +**Fix:** Compare SHA256 as in #1. If they differ, redeploy the bridge. +Watch for the version string: a stale bridge will still return +`"SWFOC Lua Bridge v1.0"` so the version string alone isn't enough — the +SHA is the authoritative drift check. After redeploy, restart the game. + +**Preventive:** Bump the version string in `lua_bridge.cpp::Lua_GetVersion` +every time you add a SWFOC_* helper so drift becomes visible from the +editor's status bar. + +### 3. Stray `swfoc_replay.exe` blocks the pipe with error 231 + +**Symptom:** Log shows `[Replay] CreateNamedPipe failed: 231` or +`ERROR_PIPE_BUSY`. The editor can connect via the LIVE bridge pipe +(`\\.\pipe\swfoc_bridge`) but the REPLAY mirror pipe +(`\\.\pipe\swfoc_bridge_replay`) is stuck. + +**Root cause:** A previous test run left `swfoc_replay.exe` alive with +an open pipe. Windows doesn't release the pipe name until the process +dies. + +**Fix:** +```powershell +Stop-Process -Name swfoc_replay -Force -ErrorAction SilentlyContinue +# Verify it's gone: +Get-Process swfoc_replay -ErrorAction SilentlyContinue +``` + +### 4. Feature works in replay tests but not in the live game + +**Symptom:** `FullFeatureSmokeTest` passes 14/14 in the replay harness, +but when you click the button in the live editor the in-game effect +doesn't happen. + +**Root cause:** The replay binary is a PARALLEL UNIVERSE — it accepts +the same pipe protocol as the live bridge but its underlying state +manipulation is done against `fake_memory.cpp` stubs, not the real game. +Specifically, the replay's `fake_lua` cannot execute arbitrary Lua +method calls like `p1:Make_Ally(p2)` — it short-circuits a small table +of known `SWFOC_*` helpers and returns canned responses for everything +else. So a service that EMITS non-SWFOC_* Lua (DiplomacyService, +CooldownManagerService, etc.) will "pass" the replay smoke test +(command accepted) but do nothing in the live game unless the live +game's Lua interpreter actually has the `Make_Ally` wrapper registered. + +**Diagnosis:** +- Open `lua_bridge.cpp` and check whether the service's `BuildLuaCommand` + output contains a `SWFOC_*` helper from the registration table. +- If yes → the live bridge will execute it. Problem is elsewhere + (memory offset, argument type, etc.). +- If no → the service is calling a game-side Lua function. Check the + verified_facts.json ledger to confirm it actually exists as a Lua + binding in the real game. If it's documented but still fails, the + binding may be scoped to a specific game mode (tactical vs galactic). + +**Fix:** depends on the specific service. See +`knowledge-base/v5_service_fixes_applied.md` for the canonical list of +service fixes and their Lua command shapes. If a service needs a fix, +add a row to that file AND add a regression guard pair in +`tests/SwfocTrainer.Tests/Regression/`. + +### 5. Game crashes when a feature is toggled + +**Symptom:** Clicking a button (especially God Mode or Inspect) causes +the game to immediately CTD (crash-to-desktop) or hang. + +**Root cause:** The bridge DLL installed a MinHook detour that is +reading or writing an invalid memory address. This is almost always +because: +- A struct field offset is wrong (e.g. `GameObject+0x5C HP` is + actually different in this build of SWFOC) +- The MinHook target RVA is wrong (the detour installed at + `base + 0x3A89D0` intercepts a different function than expected) +- The hook is installed at the right RVA but the surrounding bytes were + patched by a game update + +**Fix:** +1. IMMEDIATELY disable the bridge: close the editor (pipe disconnects), + then close the game. Restart the game WITHOUT attaching. +2. If the game still crashes on startup, swap the deployed DLL back to + the pre-deploy backup: + ```powershell + cd "D:\SteamLibrary\steamapps\common\Star Wars Empire at War\corruption" + Copy-Item powrprof.dll.backup-* powrprof.dll -Force + ``` +3. Open an issue with: + - Which feature was toggled when the crash happened + - The SHA256 of the DLL that was running + - The last few lines of `swfoc_bridge.log` in the game directory (if any) + - The game version (Steam build number) + +**Preventive:** Before installing a new `SetHP` detour, the bridge +verifies the 6-byte prologue `40 53 48 83 EC 60` (push rbx; sub rsp, +60h) still matches. If SWFOC shipped a patch that moved SetHP's address +or changed the prologue, the hook install refuses and logs a warning. +Check `swfoc_bridge.log` for this message BEFORE enabling any combat +feature after a game update. + +--- + +## Quick reference + +| Path | What it is | +|---|---| +| `C:\Users\Prekzursil\Downloads\swfoc_memory\CLAUDE.md` | Project-level agent instructions (read first) | +| `C:\Users\Prekzursil\Downloads\swfoc_memory\.remember\now.md` | Last-session handoff state | +| `C:\Users\Prekzursil\Downloads\swfoc_memory\knowledge-base\handoff_2026-04-09.md` | This session's handoff brief (11 sections) | +| `C:\Users\Prekzursil\Downloads\swfoc_memory\knowledge-base\feature_readiness_matrix_2026-04-08.md` | Which features are READY | +| `C:\Users\Prekzursil\Downloads\swfoc_memory\knowledge-base\verified_facts.json` | RVA ledger (304 entries, source of truth) | +| `C:\Users\Prekzursil\Downloads\swfoc_memory\swfoc_lua_bridge\powrprof.dll` | Source bridge DLL (must be deployed before game launch) | +| `D:\SteamLibrary\steamapps\common\Star Wars Empire at War\corruption\powrprof.dll` | Deployed bridge DLL (this is what the game actually loads) | +| `C:\Users\Prekzursil\Downloads\swfoc_memory\tools\run_editor_tests_v2.ps1` | Editor test runner (Clink-bypass wrapper) | + +| Command | What it does | +|---|---| +| `.\build.bat` in `swfoc_lua_bridge\` | Rebuild bridge DLL + harness + replay binary (requires MinGW-w64) | +| `.\bridge_test_harness.exe` | Run 295 offline bridge tests | +| `python make_test_snapshot.py ` | Generate a synthetic v2 snapshot | +| `python make_test_snapshot.py --v1` | Legacy v1 snapshot (back-compat test) | +| `python make_test_snapshot.py --v2-early` | v2 header but no extended sections (back-compat test) | +| `.\swfoc_replay.exe ` | Start the replay binary on the synthetic snapshot | +| `python smoke_test_replay.py` | 12-command smoke test against the replay pipe | +| `python -m verifier lint` from `tools\` | Lint the ledger (must be 0 errors, 0 warnings) | +| `dotnet build --verbosity minimal` in editor dir | Incremental editor build | +| `dotnet build --no-incremental --verbosity normal` | Clean rebuild — reveals all warnings | +| `powershell -File tools\run_editor_tests_v2.ps1` | Run full editor test suite | +| `powershell -File tools\run_editor_tests_v2.ps1 -Filter "Category=Replay"` | Replay-tagged tests only (fast) | + +--- + +**Last updated:** 2026-04-09 (Phase E of the functional-readiness session). +See `knowledge-base/handoff_2026-04-09.md` for the full session context. diff --git a/docs/editor/editor_gui_audit_2026-04-10.md b/docs/editor/editor_gui_audit_2026-04-10.md new file mode 100644 index 000000000..7bc1462db --- /dev/null +++ b/docs/editor/editor_gui_audit_2026-04-10.md @@ -0,0 +1,329 @@ +# Editor GUI Audit — 2026-04-10 + +Agent C workstream, sprint day 2026-04-10. + +**Target repo:** `C:\Users\Prekzursil\Downloads\SWFOC editor\` (project `SwfocTrainer.App`). +**Authoritative source for this audit:** `src/SwfocTrainer.App/MainWindow.xaml`, `src/SwfocTrainer.App/ViewModels/MainViewModel*.cs`, `src/SwfocTrainer.App/Program.cs`, `src/SwfocTrainer.Core/Services/*.cs`. +**Legacy pipeline in scope:** `SdkOperationRouter` + `ActionSymbolRegistry` + `SymbolTacticalGodMode` symbol table. +**New pipeline in scope:** `LuaBridgeExecutor` → `NamedPipeLuaBridgeClient` → `\\.\pipe\swfoc_bridge`. + +This document is the "before" snapshot captured by Agent C immediately before the V2 rebuild lands. It exists to justify the rebuild, track quarantine candidates, and give the next session a single entry point into "what was broken and why". + +--- + +## 1. Tab inventory + +The current `MainWindow.xaml` declares **16** `TabItem` elements inside the main `TabControl` (`Grid.Row="1"`). Line numbers are from `src/SwfocTrainer.App/MainWindow.xaml` at the time of this audit (file length: 882 lines). + +| # | Tab Name | Purpose | Backend Pipeline | Status | +|---|----------|---------|-----------------|--------| +| 1 | Runtime | Action execution + quick-action toggles (Credits, Fog, AI, God Mode, One Hit Kill, Unit Cap, Instant Build, Freeze Timer) | `SdkOperationRouter` + `ActionSymbolRegistry` + `SymbolTacticalGodMode` | LEGACY-BROKEN | +| 2 | Live Ops | Selected-unit transaction lab (HP/shield/speed/damage/cooldown/vet/owner draft); spawn preset execution; action reliability refresh | `SelectedUnitTransactionService` / `SpawnPresetService` / `ActionReliabilityService` (internal, non-bridge) | STALE — services are memory-editor vintage; many depend on symbols that no longer resolve | +| 3 | Save Editor | Load / parse / patch / validate save files; apply savepatch packs | `SaveCodec` / `SavePatchApplyService` (file-based, NOT bridge) | USEFUL (orthogonal to bridge) | +| 4 | Profiles & Updates | Profile list + GitHub profile update poll | `IProfileRepository` / `IProfileUpdateService` | USEFUL (orthogonal to bridge) | +| 5 | Unit Roster | Load + browse v5 roster entries | `IRosterBrowserService` (data-only, reads profile JSON) | STALE — pure read-only view, no bridge calls | +| 6 | Spawner | Enhanced v5 spawn batch execution | `IEnhancedSpawnService` → internally `ILuaBridgeExecutor` | DEAD — service exists and ought to work but UI is wrapped around the legacy router for side effects | +| 7 | Faction Dashboard | Capture per-faction snapshot cards | `IFactionDashboardService` | STALE — returns a stub snapshot, no live bridge call | +| 8 | Planet Manager | Load planets + set planet owner | `IPlanetManagerService` → `SdkOperationRouter` (`set_planet_owner` action) | LEGACY-BROKEN | +| 9 | Fleet Manager | Load fleet composition | `IFleetManagerService` (data-only read) | STALE | +| 10 | Ownership | Transfer unit/planet ownership | `IOwnershipTransferService` → `SdkOperationRouter` | LEGACY-BROKEN | +| 11 | AI Control | Suspend/resume/difficulty for AI | `IAiControlService` → `ILuaBridgeExecutor` (partial) | DEAD — service exists but UI pipes through legacy router on failure path | +| 12 | Camera Director | Record/play camera paths | `ICameraDirectorService` → `ILuaBridgeExecutor` | DEAD | +| 13 | Story Events | Fire story events | `IStoryEventService` → `ILuaBridgeExecutor` (`FireEventAsync`) | DEAD | +| 14 | Damage Log | Poll + summarize combat damage | `IDamageLogService` (internal poll-only) | STALE | +| 15 | Diplomacy | Set faction×faction relation | `IDiplomacyService` → `ILuaBridgeExecutor` | DEAD | +| 16 | Corruption | Apply corruption on planet | `ICorruptionService` → `ILuaBridgeExecutor` | DEAD | + +### Status legend + +- **USEFUL** — Provides real value; uses a backend that still works. +- **STALE** — Service compiles, UI renders, but the backend is a stub / data-only reader / hasn't been exercised against the current game binary. +- **DEAD** — Tab exists, backend service genuinely routes through `ILuaBridgeExecutor`, but the UI hands control off through a helper-hook path that the current bridge build does not recognize, so nothing happens in-game. **This is the real opportunity.** +- **LEGACY-BROKEN** — UI is wired to `SdkOperationRouter` / `ActionSymbolRegistry` / `SymbolTacticalGodMode` which fail at symbol resolution on the current build. + +--- + +## 2. Button inventory (sampled) + +For each tab, the 2–3 most prominent buttons and where they ultimately route. + +### Tab 1 — Runtime + +| Button | Command binding | Invokes | Backend path | +|---|---|---|---| +| God Mode | `QuickGodModeCommand` | `QuickGodModeAsync()` in `MainViewModelQuickActionsBase.cs` (line 221) | Builds payload `{ symbol: SymbolTacticalGodMode, boolValue: !active }` and routes through `QuickRunActionAsync(ActionToggleTacticalGodMode, …)` → `SdkOperationRouter` — **LEGACY-BROKEN** | +| One Hit Kill | `QuickOneHitCommand` | `QuickOneHitAsync()` (line 230) | Same as God Mode but `SymbolTacticalOneHitMode` / `ActionToggleTacticalOneHitMode` — **LEGACY-BROKEN** | +| Set Credits | `QuickSetCreditsCommand` | `QuickSetCreditsAsync()` | `QuickRunActionAsync(ActionSetCredits, …)` → `SdkOperationRouter` — **LEGACY-BROKEN** | + +### Tab 2 — Live Ops + +| Button | Command binding | Invokes | Backend path | +|---|---|---|---| +| Apply Draft | `ApplySelectedUnitDraftCommand` | `ApplySelectedUnitDraftAsync()` | `ISelectedUnitTransactionService.ApplyAsync` — internal transaction record on snapshot diff; does not touch the bridge | +| Capture Unit Baseline | `CaptureSelectedUnitBaselineCommand` | `CaptureSelectedUnitBaselineAsync()` | `ISelectedUnitTransactionService.CaptureBaseline` — reads stale symbols, usually returns empty | +| Refresh Reliability | `RefreshActionReliabilityCommand` | `IActionReliabilityService.GetAllAsync` | Reads in-memory reliability stats; no bridge | + +### Tab 3 — Save Editor + +| Button | Command binding | Invokes | Backend path | +|---|---|---|---| +| Load Save | `LoadSaveCommand` | `LoadSaveAsync()` | `ISaveCodec.ReadAsync` — local file | +| Apply Patch | `ApplySavePatchCommand` | `ApplySavePatchAsync()` | `ISavePatchApplyService.ApplyAsync` — file-level patch | + +### Tab 4 — Profiles & Updates + +| Button | Command binding | Invokes | Backend path | +|---|---|---|---| +| Load Profiles | `LoadProfilesCommand` | `LoadProfilesAsync()` | `IProfileRepository.LoadProfilesAsync` — JSON file | +| Check Updates | `CheckProfileUpdatesCommand` | `CheckProfileUpdatesAsync()` | `IProfileUpdateService.GetLatestManifestAsync` — HTTP | + +### Tab 5 — Unit Roster + +| Button | Command binding | Invokes | Backend path | +|---|---|---|---| +| Load Roster | `LoadRosterCommand` | `LoadRosterAsync()` | `IRosterBrowserService.LoadRosterAsync` — reads profile data | + +### Tab 6 — Spawner + +| Button | Command binding | Invokes | Backend path | +|---|---|---|---| +| Spawn | `ExecuteEnhancedSpawnCommand` | `ExecuteEnhancedSpawnAsync()` | `IEnhancedSpawnService.ExecuteSpawnAsync` → internally `ILuaBridgeExecutor` — **DEAD** (UI-wired, but default payload attempts helper-hook path that is currently stubbed) | + +### Tab 7 — Faction Dashboard + +| Button | Command binding | Invokes | Backend path | +|---|---|---|---| +| Capture Snapshots | `CaptureFactionSnapshotsCommand` | `CaptureFactionSnapshotsAsync()` | `IFactionDashboardService.CaptureSnapshotsAsync` — returns a static stub snapshot — **STALE** | + +### Tab 8 — Planet Manager + +| Button | Command binding | Invokes | Backend path | +|---|---|---|---| +| Load Planets | `LoadPlanetsCommand` | `LoadPlanetsAsync()` | `IPlanetManagerService.LoadPlanetsAsync` — stub | +| Set Owner | `SetPlanetOwnerCommand` | `SetPlanetOwnerAsync()` | `IPlanetManagerService.SetPlanetOwnerAsync` → `SdkOperationRouter` — **LEGACY-BROKEN** | + +### Tab 9 — Fleet Manager + +| Button | Command binding | Invokes | Backend path | +|---|---|---|---| +| Load Fleets | `LoadFleetsCommand` | `LoadFleetsAsync()` | `IFleetManagerService.LoadFleetsAsync` — stub | + +### Tab 10 — Ownership + +| Button | Command binding | Invokes | Backend path | +|---|---|---|---| +| Transfer Ownership | `TransferOwnershipCommand` | `TransferOwnershipAsync()` | `IOwnershipTransferService.TransferOwnershipAsync` → `SdkOperationRouter` — **LEGACY-BROKEN** | + +### Tab 11 — AI Control + +| Button | Command binding | Invokes | Backend path | +|---|---|---|---| +| Suspend AI | `SuspendAiCommand` | `SuspendAiAsync()` | `IAiControlService.ExecuteAiControlAsync` → `ILuaBridgeExecutor` — **DEAD** (helper-hook path) | + +### Tab 12 — Camera Director + +| Button | Command binding | Invokes | Backend path | +|---|---|---|---| +| Capture Keyframe | `CaptureCameraKeyframeCommand` | `CaptureCameraKeyframeAsync()` | `ICameraDirectorService.ExecuteCameraCommandAsync` → `ILuaBridgeExecutor` — **DEAD** | + +### Tab 13 — Story Events + +| Button | Command binding | Invokes | Backend path | +|---|---|---|---| +| Load Events | `LoadStoryEventsCommand` | `LoadStoryEventsAsync()` | `IStoryEventService.LoadEventsAsync` — data read | +| Fire Event | `FireStoryEventCommand` | `FireStoryEventAsync()` | `IStoryEventService.FireEventAsync` → `ILuaBridgeExecutor` — **DEAD** | + +### Tab 14 — Damage Log + +| Button | Command binding | Invokes | Backend path | +|---|---|---|---| +| Poll Entries | `PollDamageLogCommand` | `PollDamageLogAsync()` | `IDamageLogService.PollEntriesAsync` — polls internal buffer, no bridge | + +### Tab 15 — Diplomacy + +| Button | Command binding | Invokes | Backend path | +|---|---|---|---| +| Set Relation | `SetDiplomacyRelationCommand` | `SetDiplomacyRelationAsync()` | `IDiplomacyService.SetRelationAsync` → `ILuaBridgeExecutor` — **DEAD** | + +### Tab 16 — Corruption + +| Button | Command binding | Invokes | Backend path | +|---|---|---|---| +| Set Corruption | `SetCorruptionCommand` | `SetCorruptionAsync()` | `ICorruptionService.SetCorruptionAsync` → `ILuaBridgeExecutor` — **DEAD** | + +--- + +## 3. Service reach (dark services) + +Grep of `src/SwfocTrainer.Core/Services/*.cs` cross-referenced against `src/SwfocTrainer.App/**/*.xaml` for their interface names. + +| Service (interface) | ViewModel refs (C# files) | XAML refs (actual UI bindings) | Verdict | +|---|---|---|---| +| `IGodModeService` | 5 | **0** | DARK (bridge call exists, no button) | +| `IOneHitKillService` | 5 | **0** | DARK | +| `IEconomyService` | 23 | **0** | DARK | +| `IHeroRespawnService` | 8 | **0** | DARK | +| `IUnitInspectorService` | 6 | **0** | DARK | +| `IHardpointService` | 5 | **0** | DARK | +| `ICrashAnalyzerService` | 5 | **0** | DARK | +| `IMaphackService` | 8 | **0** | DARK | +| `IFactionSwitchService` | 22 | **0** | DARK (registered in DI, unused in UI) | +| `ICorruptionService` | 28 | **0** | DARK | +| `IDiplomacyService` | 28 | **0** | DARK | +| `IStoryEventService` | 28 | **0** | DARK | +| `IAiControlService` | 22 | **0** | DARK | +| `ICooldownManagerService` | 22 | **0** | DARK | +| `ICameraDirectorService` | 22 | **0** | DARK | +| `IOwnershipTransferService` | 22 | **0** | DARK | +| `IPlanetManagerService` | 28 | **0** | DARK | +| `IFleetManagerService` | 22 | **0** | DARK | +| `IDamageLogService` | 28 | **0** | DARK | +| `IModConflictDetectorService` | 22 | **0** | DARK | +| `IRosterBrowserService` | 22 | **0** | DARK | +| `IFactionDashboardService` | 22 | **0** | DARK | +| `IEnhancedSpawnService` | 22 | **0** | DARK | + +**23 out of 23 sampled services have zero direct XAML bindings.** ViewModel "references" are exclusively via the `MainViewModelDependencies` struct and factory injection (see `MainViewModelDependencies.cs`). The commands eventually constructed from these services do exist, but their button-side bindings route through the `ActionId`-based `QuickRunActionAsync` / `ExecuteActionCommand` pattern that resolves against the symbol registry. The symbol registry failing is why nothing reaches the bridge. + +### The "8 missing from DI" problem + +The 8 bridge-direct services below are NOT even registered in `src/SwfocTrainer.App/Program.cs`. They are present in source, instantiable, and have contracts (`IBridgeHelperServices.cs`), but `Program.cs::RegisterCoreServices` does not include them: + +- `IGodModeService` +- `IOneHitKillService` +- `IEconomyService` +- `IHeroRespawnService` +- `IUnitInspectorService` +- `IHardpointService` +- `ICrashAnalyzerService` +- `IMaphackService` + +**V2 must register these itself** (Program.cs addition or manual instantiation in `MainWindowV2.xaml.cs`). Agent A and Agent B are not expected to touch this. + +--- + +## 4. Legacy pipeline reach + +Grep results for `SdkOperationRouter` / `ActionSymbolRegistry` / `SymbolTacticalGodMode` across the editor source tree (non-test files). + +### In `src/SwfocTrainer.App/`: + +- `src/SwfocTrainer.App/Program.cs:119` — DI registration `services.AddSingleton();` (still needed for the legacy UI fallback) +- `src/SwfocTrainer.App/ViewModels/MainViewModelDefaults.cs:25` — `internal const string SymbolTacticalGodMode = "tactical_god_mode";` +- `src/SwfocTrainer.App/ViewModels/MainViewModelDefaults.cs:65` — `[ActionToggleTacticalGodMode] = SymbolTacticalGodMode,` (action→symbol lookup table) +- `src/SwfocTrainer.App/ViewModels/MainViewModelCoreStateBase.cs:46` — `protected const string SymbolTacticalGodMode = MainViewModelDefaults.SymbolTacticalGodMode;` (re-export) +- `src/SwfocTrainer.App/ViewModels/MainViewModelQuickActionsBase.cs:225-228` — Quick-action `QuickGodModeAsync()` builds payload with `SymbolTacticalGodMode` and routes through `QuickRunActionAsync` → `SdkOperationRouter` + +### In `src/SwfocTrainer.Core/`: + +- `src/SwfocTrainer.Core/Services/SdkOperationRouter.cs` — implementation +- `src/SwfocTrainer.Core/Services/ActionSymbolRegistry.cs` — implementation +- `src/SwfocTrainer.Core/Contracts/ISdkOperationRouter.cs` — contract +- `src/SwfocTrainer.Core/Services/ActionReliabilityService.cs` — consumes `ISdkOperationRouter` for reliability probes + +### In `src/SwfocTrainer.Runtime/`: + +- `src/SwfocTrainer.Runtime/Services/RuntimeAdapter.cs` — consumes `ISdkOperationRouter` +- `src/SwfocTrainer.Runtime/Services/RuntimeAdapter.State.cs` — same +- `src/SwfocTrainer.Runtime/Services/ModMechanicDetectionService.cs` — same + +### In test projects: + +- `tests/SwfocTrainer.Tests/Core/SdkOperationRouterTests.cs` +- `tests/SwfocTrainer.Tests/Core/CoreWave6Tests.cs` +- `tests/SwfocTrainer.Tests/Core/CoreWave11CoverageTests.cs` +- `tests/SwfocTrainer.Tests/Core/CoreWave8bCoverageTests.cs` +- `tests/SwfocTrainer.Tests/Core/CoreWave8CoverageTests.cs` +- `tests/SwfocTrainer.Tests/Runtime/RuntimeAdapterAsyncWave7Tests.cs` +- `tests/SwfocTrainer.Tests/Runtime/RuntimeAdapterWave6Tests.cs` +- `tests/SwfocTrainer.Tests/Core/CoreWave2CoverageTests.cs` +- `tests/SwfocTrainer.Tests/App/MainViewModelDefaultsWave5Tests.cs` + +**Total reach: 20 files.** The 5 most-important legacy touches are in `src/SwfocTrainer.App/ViewModels/MainViewModel{Defaults,CoreStateBase,QuickActionsBase}.cs`, `src/SwfocTrainer.App/Program.cs`, and the legacy `MainWindow.xaml` command bindings. V2 must touch zero of these. + +--- + +## 5. Summary verdict + +**Full rebuild is justified.** The audit evidence: + +1. **Zero XAML bindings point at bridge services.** Every one of the 23 bridge-aware services exists in source and is referenced only through the `MainViewModelDependencies` injection record. The actual button bindings (`QuickGodModeCommand`, `ExecuteActionCommand`, etc.) route through `QuickRunActionAsync` / `ExecuteActionAsync`, which both hit `SdkOperationRouter` first and fall back to helper-hook paths that are not wired to the current bridge build. +2. **The 8 newest bridge services (GodMode, OneHitKill, Economy, HeroRespawn, UnitInspector, Hardpoint, CrashAnalyzer, Maphack) are not even in Program.cs DI.** A surgical rewire cannot succeed without editing `Program.cs::RegisterCoreServices`, `MainViewModelDependencies`, the factory context record, and the 21 `MainViewModel*.cs` partial-class files. That blast radius is strictly larger than writing a new window from scratch. +3. **The symbol registry is the chokepoint.** `SymbolTacticalGodMode` / `ActionToggleTacticalGodMode` / `ActionSymbolRegistry` exist to map stable button IDs to the hand-maintained symbol table that was the pre-bridge approach. Every broken button in the user's live test came back with the `"symbol":"invalid"` error from `SdkOperationRouter`. This pipeline will not become correct by adding buttons — it needs to be bypassed. +4. **The correct pipeline already exists.** `LuaBridgeExecutor` + `NamedPipeLuaBridgeClient` are production-quality, unit-tested, and directly call `\\.\pipe\swfoc_bridge`. The per-feature services (`GodModeService.cs`, `OneHitKillService.cs`, etc.) already wrap them correctly. What's missing is the six-tab window that actually uses them. +5. **The previous session's B3 refusal was correct.** A surgical fix inside the legacy MainViewModel would have touched 30+ files and required re-plumbing the symbol registry. Building `MainWindowV2` alongside, with a launch switch for fallback, preserves the legacy UI while delivering a working alternative. + +**Recommendation:** ship the V2 rebuild, validate against the live bridge in the next session, then execute the quarantine plan below once a human has clicked at least one V2 button successfully in-game. + +--- + +## Quarantine plan for legacy pipeline + +The following files should be moved to `legacy_to_delete/` **after V2 is validated live** (not this session). Do not move these files yet. + +### Core legacy services (delete after V2 validated) + +1. `src/SwfocTrainer.Core/Services/SdkOperationRouter.cs` +2. `src/SwfocTrainer.Core/Services/ActionSymbolRegistry.cs` +3. `src/SwfocTrainer.Core/Contracts/ISdkOperationRouter.cs` + +### Legacy view-model spaghetti (21 partial files) + +The `MainViewModel` partial class lives across 21 files. All of them must be deleted together — they form a single `partial class MainViewModel` and cannot be removed piecemeal without compile errors. + +4. `src/SwfocTrainer.App/ViewModels/MainViewModel.cs` +5. `src/SwfocTrainer.App/ViewModels/MainViewModelAttachHelpers.cs` +6. `src/SwfocTrainer.App/ViewModels/MainViewModelBindableMembersBase.cs` +7. `src/SwfocTrainer.App/ViewModels/MainViewModelCoreStateBase.cs` +8. `src/SwfocTrainer.App/ViewModels/MainViewModelCreditsHelpers.cs` +9. `src/SwfocTrainer.App/ViewModels/MainViewModelDefaults.cs` +10. `src/SwfocTrainer.App/ViewModels/MainViewModelDependencies.cs` +11. `src/SwfocTrainer.App/ViewModels/MainViewModelDiagnostics.cs` +12. `src/SwfocTrainer.App/ViewModels/MainViewModelDraftBuildResult.cs` +13. `src/SwfocTrainer.App/ViewModels/MainViewModelFactories.cs` +14. `src/SwfocTrainer.App/ViewModels/MainViewModelHotkeyHelpers.cs` +15. `src/SwfocTrainer.App/ViewModels/MainViewModelLiveOpsBase.cs` +16. `src/SwfocTrainer.App/ViewModels/MainViewModelPayloadHelpers.cs` +17. `src/SwfocTrainer.App/ViewModels/MainViewModelQuickActionHelpers.cs` +18. `src/SwfocTrainer.App/ViewModels/MainViewModelQuickActionsBase.cs` +19. `src/SwfocTrainer.App/ViewModels/MainViewModelRuntimeModeOverrideHelpers.cs` +20. `src/SwfocTrainer.App/ViewModels/MainViewModelSaveOpsBase.cs` +21. `src/SwfocTrainer.App/ViewModels/MainViewModelSelectedUnitDraftHelpers.cs` +22. `src/SwfocTrainer.App/ViewModels/MainViewModelSelectedUnitParsingHelpers.cs` +23. `src/SwfocTrainer.App/ViewModels/MainViewModelSpawnHelpers.cs` +24. `src/SwfocTrainer.App/ViewModels/MainViewModelV5FeaturesBase.cs` + +### Legacy WPF window + +25. `src/SwfocTrainer.App/MainWindow.xaml` +26. `src/SwfocTrainer.App/MainWindow.xaml.cs` + +### Legacy-pipeline-only tests + +These tests exclusively cover `SdkOperationRouter` / `ActionSymbolRegistry` / `SymbolTacticalGodMode`. Other tests that merely mention those symbols while testing something else should NOT be deleted — verify by grep before moving. + +27. `tests/SwfocTrainer.Tests/Core/SdkOperationRouterTests.cs` — named after the target, exclusively legacy + +**Tests that are NOT candidates for deletion** (they exercise adjacent systems and only reference the legacy symbols through transitive dependencies): + +- `tests/SwfocTrainer.Tests/Core/CoreWave6Tests.cs` +- `tests/SwfocTrainer.Tests/Core/CoreWave11CoverageTests.cs` +- `tests/SwfocTrainer.Tests/Core/CoreWave8bCoverageTests.cs` +- `tests/SwfocTrainer.Tests/Core/CoreWave8CoverageTests.cs` +- `tests/SwfocTrainer.Tests/Runtime/RuntimeAdapterAsyncWave7Tests.cs` +- `tests/SwfocTrainer.Tests/Runtime/RuntimeAdapterWave6Tests.cs` +- `tests/SwfocTrainer.Tests/Core/CoreWave2CoverageTests.cs` +- `tests/SwfocTrainer.Tests/App/MainViewModelDefaultsWave5Tests.cs` + +These will lose coverage when the legacy code is deleted. Re-verify their still-relevant assertions against V2 code during quarantine execution. + +### Quarantine execution order (future session) + +1. Confirm V2 has been used live against the bridge (human click-test). +2. Delete legacy tests marked "exclusively legacy" above. +3. Delete the 21 `MainViewModel*.cs` partial files together, then `MainWindow.xaml` + `MainWindow.xaml.cs`. +4. Delete `SdkOperationRouter.cs`, `ActionSymbolRegistry.cs`, `ISdkOperationRouter.cs`. +5. Run `dotnet build --no-incremental --verbosity normal` and fix transitive breakages in `RuntimeAdapter.cs`, `ActionReliabilityService.cs`, `ModMechanicDetectionService.cs`. +6. Simplify `Program.cs::RegisterCoreServices` — drop the DI registration and the `MainViewModelDependencies` wiring. +7. Remove the `--legacy-ui` flag from V2's `Main()` and make V2 the only window. +8. Re-run tests. Expect ~10 deletions in `SdkOperationRouterTests.cs` and friends. diff --git a/docs/editor/index.md b/docs/editor/index.md new file mode 100644 index 000000000..a6ce5ce3b --- /dev/null +++ b/docs/editor/index.md @@ -0,0 +1,41 @@ +# SWFOC Reverse Engineering Knowledge Base + +Documentation for the Alamo engine as used in Star Wars: Empire at War -- Forces of Corruption (64-bit Steam build). + +## Binary Info + +- **Module**: StarWarsG.exe +- **Architecture**: x86_64 +- **Compiler**: MSVC +- **Ghidra Base**: `0x140000000` +- **Lua Version**: 5.0.2 +- **RTTI Classes**: 1703 + +## Structs + +- [CameraClass](structs/cameraclass.md) +- [FOWPlayerObject](structs/fowplayerobject.md) +- [GameObjectClass](structs/gameobjectclass.md) +- [GameObjectType](structs/gameobjecttype.md) +- [ObjectUnderConstructionClass](structs/objectunderconstructionclass.md) +- [PlanetaryDataPackClass](structs/planetarydatapackclass.md) +- [PlayerClass](structs/playerclass.md) +- [StoryEventClass](structs/storyeventclass.md) +- [StorySubPlotClass](structs/storysubplotclass.md) +- [lua_State](structs/lua-state.md) + +## Game Systems + +- [Ai](systems/ai.md) +- [Camera Selection](systems/camera-selection.md) +- [Combat](systems/combat.md) +- [Galactic Map](systems/galactic-map.md) +- [Network](systems/network.md) +- [Production](systems/production.md) +- [Save Format](systems/save-format.md) +- [Story](systems/story.md) + +## Reference + +- [Lua API (405 functions)](lua-api.md) +- [RVA Table (280+ addresses)](rvas.md) diff --git a/docs/editor/iter-100-113-quick-reference.md b/docs/editor/iter-100-113-quick-reference.md new file mode 100644 index 000000000..81c301563 --- /dev/null +++ b/docs/editor/iter-100-113-quick-reference.md @@ -0,0 +1,228 @@ +# Iter 100-113 LIVE wires — operator quick reference + +**One-line example invocations for every LIVE flip from the 2026-04-28 master ralph loop session.** + +Copy-paste any of these into: +- The editor's **Lua Playground tab**, or +- The bridge's `\\.\pipe\swfoc_bridge` named pipe directly via `echo` / Cheat Engine, or +- `SWFOC_DoString("...")` from inside an existing Lua script + +All wires return either `OK: dispatched (LIVE — engine Lua API)` on success or `ERR: ` on failure. + +--- + +## Speed control (iter 100) + +```lua +-- Set per-unit speed (absolute override, in engine units) +return SWFOC_SetUnitSpeed(0xABCD, 250) + +-- Read the current per-unit speed (returns -1 if no override active) +return SWFOC_GetUnitSpeed(0xABCD) + +-- Set ALL units owned by slot 0 to absolute speed 350 +return SWFOC_SetPerFactionSpeedMultiplier(0, 350) + +-- Revert per-unit speed back to engine's natural max +return SWFOC_ClearUnitSpeedOverride(0xABCD) +``` + +`0xABCD` = the 64-bit obj_addr from the Tactical Units tab. Replace with your real address. + +--- + +## Damage scaling (iter 96, editor backfilled iter 102) + +```lua +-- Scale every incoming damage by 2.0× globally (Take_Damage_Outer detour) +return SWFOC_SetDamageMultiplierGlobal(2.0) + +-- Read the current global multiplier +return SWFOC_GetDamageMultiplierGlobal() + +-- Reset to normal damage +return SWFOC_SetDamageMultiplierGlobal(1.0) +``` + +--- + +## Camera (iter 107) + +```lua +-- Galactic mode: scroll to a planet by name +return SWFOC_ScrollCameraToTarget('Find_Planet("Yavin")') + +-- Tactical mode: scroll to a specific unit +return SWFOC_ScrollCameraToTarget('Find_First_Object("Empire_AT_AT")') + +-- Scroll to the third Rebel Trooper Squad in the world +return SWFOC_ScrollCameraToTarget('Find_Object_Type("Rebel_Trooper_Squad")[2]') +``` + +Note: the outer call uses single quotes so the inner `Find_*` can use double quotes for its string argument (no escape needed). Lua accepts either quote style for string literals. + +--- + +## Galactic — change unit ownership (iter 108) + +```lua +-- Convert the first Empire AT-AT to the Rebel faction +return SWFOC_ChangeUnitOwner('Find_First_Object("Empire_AT_AT")', 'Find_Player("REBEL")') + +-- Convert all Empire stormtroopers to Underworld (one at a time — repeat for each) +return SWFOC_ChangeUnitOwner('Find_First_Object("Empire_Stormtrooper_Squad")', 'Find_Player("UNDERWORLD")') +``` + +The engine's `Change_Owner` updates ownership, fires UI events, plays audio, processes corruption, AND updates AI budgets. Full "swap sides" engine behaviour. + +--- + +## Spawning (iter 109) + +```lua +-- Spawn a Rebel trooper squad at the origin +return SWFOC_SpawnUnitLua( + 'Find_Player("REBEL")', + 'Find_Object_Type("Rebel_Trooper_Squad")', + 'Create_Position(0, 0, 0)') + +-- Spawn an Empire AT-AT at coords (100, 200, 0) +return SWFOC_SpawnUnitLua( + 'Find_Player("EMPIRE")', + 'Find_Object_Type("Empire_AT_AT")', + 'Create_Position(100, 200, 0)') +``` + +--- + +## Per-unit toggles (iter 110, 111) + +```lua +-- Make a unit invulnerable (propagates via BehaviorAttach to all hardpoints) +return SWFOC_MakeUnitInvulnLua('Find_First_Object("Empire_AT_AT")', 'true') + +-- Revert +return SWFOC_MakeUnitInvulnLua('Find_First_Object("Empire_AT_AT")', 'false') + +-- Hide a unit (visibility toggle, doesn't remove from world) +return SWFOC_HideUnitLua('Find_First_Object("Empire_AT_AT")', 'true') + +-- Lock the AI away from controlling a unit +return SWFOC_PreventAiUsageLua('Find_First_Object("Empire_AT_AT")', 'true') + +-- Make a unit non-selectable (operator can still see it but can't click it) +return SWFOC_SetUnitSelectableLua('Find_First_Object("Empire_AT_AT")', 'false') +``` + +--- + +## Per-unit actions (iter 112) + +```lua +-- Despawn (cleanly remove) +return SWFOC_DespawnUnitLua('Find_First_Object("Empire_AT_AT")') + +-- Stop current action (interrupt move/attack) +return SWFOC_StopUnitLua('Find_First_Object("Empire_AT_AT")') + +-- Retreat (engine flees the unit toward safety) +return SWFOC_RetreatUnitLua('Find_First_Object("Rebel_Trooper_Squad")') +``` + +--- + +## Universal escape hatch (iter 113) + +For any engine Lua method NOT explicitly catalogued — calls +`():()` via DoString. + +```lua +-- Give a player money +return SWFOC_CallObjMethodLua('Find_Player("REBEL")', 'Give_Money', '5000') + +-- Heal a unit (engine method — no args) +return SWFOC_CallObjMethodLua('Find_First_Object("Empire_AT_AT")', 'Heal', '') + +-- Enable a behavior on a unit (multi-arg) +return SWFOC_CallObjMethodLua( + 'Find_First_Object("Empire_AT_AT")', + 'Enable_Behavior', + '"INVULNERABLE", true') + +-- Set a player's tech level +return SWFOC_CallObjMethodLua('Find_Player("REBEL")', 'Set_Tech_Level', '5') + +-- Activate a faction superweapon +return SWFOC_CallObjMethodLua('Find_Player("EMPIRE")', 'Activate_Power', '"DEATH_STAR"') +``` + +--- + +## Live-test priority + +If you can only test 5 things to validate the session, do these in order: + +1. **`SWFOC_SetUnitSpeed`** — easiest to verify: pick any unit, set speed 250, watch it walk much faster. +2. **`SWFOC_SetDamageMultiplierGlobal(2.0)`** — take a swing at any unit, watch HP drop double. +3. **`SWFOC_ScrollCameraToTarget('Find_Planet("Yavin")')`** — galactic-mode camera pan. +4. **`SWFOC_ChangeUnitOwner(...)`** — convert an enemy unit, watch UI events fire. +5. **`SWFOC_CallObjMethodLua('Find_Player("REBEL")', 'Give_Money', '5000')`** — credits jump 5000, validates the universal escape hatch. + +If any fail, capture the bridge log line — the bridge already records `ERR: raised engine error rc=` on failure with the full DoString source that was dispatched. + +--- + +## Deferred — XML-attribute-only family (still PHASE 2 PENDING) + +These engine fields are baked from XML at unit construction; no runtime setter exists. They need either RTTI dissection of the per-unit struct OR a MinHook on the relevant tick/update path. Documented for future arc — don't try to call them yet. + +- `SWFOC_SetFireRate` (iter 101 finding) +- `SWFOC_SetHeroRespawnTimer` (iter 104) +- `SWFOC_SetPermadeath` (iter 104) +- `SWFOC_SetUnitShield` (iter 105) +- `SWFOC_SetGameSpeed` (no engine helper pinned) +- `SWFOC_FreeCam` (no engine `Free_Cam` Lua API) +- `SWFOC_SetCameraPos` (engine API takes Lua position-userdata, not raw floats) + +--- + +## Native UX surfaces in the editor (iter 117-119, 2026-04-29) + +You no longer have to paste these wires into the Lua Playground — every iter 100-113 wire now has a native button somewhere in the editor. The Playground preset menu (iter 116) is still there as a power-user shortcut, but the per-tab buttons are the recommended path for routine operator workflows. + +### Unit Control tab — "Selected Unit Lua Actions (iter 117-118 LIVE)" GroupBox + +Paste a unit-handle expression (e.g. `Find_First_Object("Empire_AT_AT")`) into the first TextBox once. Then click any of these buttons: + +- **Make invuln ON / OFF** → iter 110 (`SWFOC_MakeUnitInvulnLua`) +- **Hide ON / OFF** → iter 111 (`SWFOC_HideUnitLua`) +- **Lock from AI / Unlock to AI** → iter 111 (`SWFOC_PreventAiUsageLua`) +- **Selectable ON / OFF** → iter 111 (`SWFOC_SetUnitSelectableLua`) +- **Despawn / Stop / Retreat** → iter 112 (`SWFOC_DespawnUnitLua` / `StopUnitLua` / `RetreatUnitLua`) +- **Change unit owner →** (separate row, takes a 2nd TextBox for `Find_Player(...)`) → iter 108 (`SWFOC_ChangeUnitOwner`) + +### Spawning tab — "Spawn unit via Lua (iter 119 LIVE)" GroupBox + +Three TextBoxes for player / type / position Lua expressions, plus a button. This is the **LIVE alternative** to the existing PHASE 2 PENDING `SWFOC_SpawnUnit` Phase-1-mirror Spawn button — operators can finally spawn units that genuinely appear in the running game. + +- **Player Lua**: `Find_Player("REBEL")` +- **Type Lua**: `Find_Object_Type("Rebel_Trooper_Squad")` +- **Position Lua**: `Create_Position(0, 0, 0)` +- Click **Spawn (Lua, LIVE) →** → iter 109 (`SWFOC_SpawnUnitLua`) + +### Wires that already had earlier UX surfaces + +- **Speed (iter 100)** — Speed tab "Apply (per-unit)" / per-faction / Revert buttons (iter 100/102) +- **Damage (iter 96/97/102)** — Combat tab "Apply (GLOBAL)" / "Apply (per-slot)" buttons (iter 102) +- **Camera (iter 107)** — Camera & Debug tab "Scroll camera to target" GroupBox (iter 107) +- **Universal escape hatch (iter 113, `SWFOC_CallObjMethodLua`)** — intentionally kept Lua-Playground-only. Too generic to deserve a dedicated button surface; lives in the iter 116 preset menu and is the documented "everything else" tool. + +### Wire-format pin tests + +Each iter 117/118/119 dispatcher method has a wire-format regression test that pins the exact `SWFOC_X('expr1', 'expr2'...)` shape: + +- `tests/SwfocTrainer.Tests/Regression/Iter117UnitLuaCallShapeTests.cs` (10 cases) +- `tests/SwfocTrainer.Tests/Regression/Iter118ChangeUnitOwnerShapeTests.cs` (5 cases) +- `tests/SwfocTrainer.Tests/Regression/Iter119SpawnUnitLuaShapeTests.cs` (4 cases) + +If a future bridge-side rename or quote-style change drifts the wire format, these fire at the dispatcher boundary instead of waiting for a runtime mismatch in the live game. diff --git a/docs/editor/lua-api.md b/docs/editor/lua-api.md new file mode 100644 index 000000000..c78dfe963 --- /dev/null +++ b/docs/editor/lua-api.md @@ -0,0 +1,336 @@ +# SWFOC Lua API Reference + +This page documents all 405 Lua functions available in Star Wars: Empire at War -- Forces of Corruption. + +See also: [RVA Table](rvas.md) | [Struct Reference](../index.md) + +--- + +## All 134 Binary-Confirmed + 271 Community-Documented Functions + +--- + +## How to Call These Functions via the DLL Bridge + +With `lua_load` + `lua_pcall` + `lua_gettable`, you can now execute **any Lua code** the game supports. Two methods: + +### Method 1: Named Pipe (External — from CE, .NET trainer, Python, etc.) +``` +echo 'SWFOC_SetCredits(50000)' | ncat --send-only \\.\pipe\swfoc_bridge +``` + +### Method 2: SWFOC_DoString (From Lua scripts inside the game) +```lua +SWFOC_DoString("Spawn_Unit(player, Find_Object_Type('AT_AT'), Create_Position(0,0,0))") +``` + +### Method 3: Direct DLL Function (From registered SWFOC_* functions) +```lua +local slot, faction = SWFOC_GetLocalPlayer() +SWFOC_SetCredits(999999) +``` + +--- + +## Quick Reference: What Can You Do? + +| Want to... | Lua Code | Scope | +|-----------|----------|-------| +| Give yourself money | `player:Give_Money(50000)` | Any | +| Max tech level | `player:Set_Tech_Level(5)` | Any | +| Spawn a unit | `Spawn_Unit(player, Find_Object_Type("AT_AT"), pos)` | Tactical | +| Make unit invincible | `unit:Make_Invulnerable(true)` | Tactical | +| Kill a unit | `unit:Take_Damage(99999)` | Tactical | +| Change ownership | `unit:Change_Owner(enemy_player)` | Any | +| Teleport a unit | `unit:Teleport(Create_Position(x, y, z))` | Tactical | +| Reveal map | `FOWManager.Reveal_All()` | Any | +| Speed up a unit | `unit:Override_Max_Speed(500)` | Tactical | +| Unlock a tech | `player:Unlock_Tech(Find_Object_Type("DEATH_STAR"))` | Any | +| Spawn on galactic | `Galactic_Spawn_Unit(player, type, planet)` | Galactic | +| Play cinematic | `Point_Camera_At(unit); Letter_Box_On()` | Tactical | +| Trigger story event | `Story_Event("YOURMOD_CUSTOM_EVENT")` | Any | +| Find all enemies | `Find_All_Objects_Of_Type(Find_Object_Type("AT_AT"))` | Tactical | +| Execute arbitrary Lua | `SWFOC_DoString("any valid lua code here")` | Any | + +--- + +## 1. Global Functions (40 confirmed from binary) + +### Spawning & Creation +| Function | Params | Scope | Description | +|----------|--------|-------|-------------| +| `Spawn_Unit` | `player, type, position` | Tactical | Spawns a unit at position. Returns the unit object. | +| `Reinforce_Unit` | `player, type, position` | Tactical | Spawns from reinforcement pool | +| `Spawn_From_Reinforcement_Pool` | `player, type, position` | Tactical | Alternative reinforcement spawn | +| `Create_Generic_Object` | `type, position, player` | Tactical | Creates any object type (note: different param order) | +| `Galactic_Spawn_Unit` | `player, type, planet` | Galactic | Spawns on galactic map at planet | +| `Create_Position` | `x, y, z` | Any | Creates a position vector for spawn/teleport | + +### Queries & Finding +| Function | Params | Returns | Description | +|----------|--------|---------|-------------| +| `Find_Object_Type` | `"TYPE_NAME"` | GameObjectType | Looks up a type by XML name | +| `Find_First_Object` | `type_name` | GameObjectWrapper | Finds first instance of type | +| `Find_All_Objects_Of_Type` | `type` | table | Returns all instances | +| `Find_Nearest` | `type, position, player` | GameObjectWrapper | Finds nearest of type | +| `Find_Hint` | `"hint_name", player` | position/object | Finds a map hint marker | +| `Find_Path` | `from, to` | table | Finds path between planets | +| `FindPlanet` | `"PLANET_NAME"` | PlanetWrapper | Finds a planet by name | +| `FindTarget` | `evaluator, ...` | GameObjectWrapper | AI target finder | +| `Get_Game_Mode` | none | string | Returns "Galactic", "Space", "Land" | + +### Story & Events +| Function | Params | Description | +|----------|--------|-------------| +| `Story_Event` | `"EVENT_NAME"` | Fires a story event | +| `Story_Event_Trigger` | `"EVENT_NAME"` | Alternative trigger | +| `Add_Objective` | `"OBJECTIVE_ID"` | Adds an objective to the UI | + +### Camera & Cinematics +| Function | Params | Description | +|----------|--------|-------------| +| `Point_Camera_At` | `object` | Points camera at unit/location | +| `Scroll_Camera_To` | `position` | Smooth camera pan | +| `Zoom_Camera` | `level` | Sets zoom level | +| `Rotate_Camera_By` | `degrees` | Rotates camera | +| `Letter_Box_On` | none | Enables cinematic letterbox | +| `Fade_Screen_In` | `duration` | Fades screen in | +| `Suspend_AI` | `seconds` | Pauses AI for cinematic | + +### Audio +| Function | Params | Description | +|----------|--------|-------------| +| `Play_Music` | `"MUSIC_EVENT"` | Plays music | +| `Stop_All_Music` | none | Stops all music | +| `Resume_Mode_Based_Music` | none | Returns to normal music | +| `Play_SFX_Event` | `"SFX_EVENT"` | Plays sound effect | + +### Fleet & AI +| Function | Params | Description | +|----------|--------|-------------| +| `Assemble_Fleet` | `player, planet, types` | Assembles fleet at planet | +| `EvaluatePerception` | `...` | AI perception evaluation | +| `GiveDesireBonus` | `...` | Modifies AI desire weights | +| `_ProduceObject` | `player, type` | AI production command | + +--- + +## 2. GameObjectWrapper Methods (32 confirmed) + +Called on unit/building objects: `unit:Method_Name(args)` + +### Health & Combat +| Method | Params | Description | +|--------|--------|-------------| +| `Get_Hull()` | none | Returns current HP (float) | +| `Get_Health()` | none | Returns health percentage | +| `Get_Shield()` | none | Returns shield percentage | +| `Make_Invulnerable(bool)` | boolean | Toggle invulnerability | +| `Set_Cannot_Be_Killed(bool)` | boolean | Prevents death (HP stays at 1) | +| `Fire_Special_Weapon(slot)` | int | Fires special weapon | +| `Activate_Ability(name)` | string | Activates named ability | +| `Is_Ability_Active(name)` | string | Checks if ability is active | + +### Movement & Position +| Method | Params | Description | +|--------|--------|-------------| +| `Move_To(position)` | position | Orders move to location | +| `Teleport(position)` | position | Instant teleport | +| `Teleport_And_Face(pos, target)` | pos, target | Teleport and face direction | +| `Override_Max_Speed(speed)` | float | Sets custom max speed | +| `Get_Position()` | none | Returns current position | +| `Get_Distance(target)` | object | Returns distance to target | +| `Get_Bone_Position(bone)` | string | Gets position of model bone | +| `Cancel_Hyperspace()` | none | Cancels hyperspace jump | +| `Are_Engines_Online()` | none | Checks if engines work | + +### Commands +| Method | Params | Description | +|--------|--------|-------------| +| `Attack_Target(target)` | object | Orders attack on target | +| `Guard_Target(target)` | object | Orders guard on target | +| `Divert(position)` | position | Diverts unit to position | + +### Queries +| Method | Params | Returns | Description | +|--------|--------|---------|-------------| +| `Get_Owner()` | none | PlayerWrapper | Gets owning player | +| `Get_Type()` | none | GameObjectType | Gets unit type | +| `Get_Parent_Object()` | none | GameObjectWrapper | Gets parent (e.g., garrison) | +| `Get_Attack_Target()` | none | GameObjectWrapper | Gets current attack target | +| `Has_Attack_Target()` | none | boolean | Checks if attacking | +| `Is_Category(cat)` | string | boolean | Checks category membership | +| `Has_Property(prop)` | string | boolean | Checks for property flag | + +### Control +| Method | Params | Description | +|--------|--------|-------------| +| `Change_Owner(player)` | PlayerWrapper | Transfers ownership | +| `Prevent_AI_Usage(bool)` | boolean | Blocks AI from controlling unit | +| `Set_Selectable(bool)` | boolean | Controls if player can select | + +--- + +## 3. PlayerWrapper Methods (12 confirmed) + +Called on player objects: `player:Method_Name(args)` + +### Economy +| Method | Params | Description | +|--------|--------|-------------| +| `Give_Money(amount)` | number | Adds credits | +| `Get_Credits()` | none | Returns current credits | +| `Release_Credits_For_Tactical(amount)` | number | Releases credits for land battle | + +### Technology +| Method | Params | Description | +|--------|--------|-------------| +| `Set_Tech_Level(level)` | number | Sets tech level (1-5) | +| `Unlock_Tech(type)` | GameObjectType | Unlocks specific tech | +| `Lock_Tech(type)` | GameObjectType | Locks specific tech | + +### Diplomacy +| Method | Params | Returns | Description | +|--------|--------|---------|-------------| +| `Is_Enemy(player)` | PlayerWrapper | boolean | Checks if enemy | +| `Is_Ally(player)` | PlayerWrapper | boolean | Checks if allied | + +### Other +| Method | Params | Description | +|--------|--------|-------------| +| `Select_Object(object)` | GameObjectWrapper | Selects a unit in UI | +| `Enable_As_Actor()` | none | Enables AI actor mode | + +--- + +## 4. TaskForce Methods (10 confirmed) + +Called on assembled task forces: `taskforce:Method_Name(args)` + +| Method | Wrapper | Description | +|--------|---------|-------------| +| `Move_To(target)` | All subtypes | Moves task force | +| `Move_To_Target(target)` | TaskForceClass | Moves to specific target | +| `Attack_Target(target)` | Space/Land | Orders attack | +| `Guard_Target(target)` | SpaceTaskForce | Orders guard | +| `Reinforce(type)` | Space/Land | Reinforces task force | +| `Release_Reinforcements()` | SpaceTaskForce | Releases held reinforcements | +| `Launch_Units(planet)` | GalacticTaskForce | Launches fleet to planet | +| `Land_Units(planet)` | GalacticTaskForce | Lands ground forces | +| `Get_Type_Of_Unit(idx)` | TaskForceClass | Gets unit type at index | +| `Set_As_Goal_System_Removable(b)` | TaskForceClass | AI goal cleanup flag | + +--- + +## 5. Community-Documented Functions (271 additional) + +These are documented in [eaw-emmyluadoc](https://github.com/eaw-emmyluadoc) and [Focumentation](https://focumentation.github.io/) but not yet confirmed in our binary analysis. They should work via `SWFOC_DoString()`. + +### Additional GameObjectWrapper Methods (~80) +- `Despawn()`, `Sell()`, `Stop()`, `Retreat()` +- `Set_Garrison_Spawn(bool)`, `Get_Garrison_Units()` +- `Enable_Behavior(name, bool)`, `Get_Behavior_ID()` +- `Set_In_Limbo(bool)`, `Is_In_Limbo()` +- `Contains_Object_Type(type)`, `Get_Contained_Object_Count()` +- `Set_Check_Contested_Space(bool)` +- `Enable_Stealth(bool)`, `Is_Stealthed()` +- `Bribe(player)`, `Corrupt(amount)` +- `Get_Rate_Of_Fire_Modifier()`, `Set_Rate_Of_Fire_Modifier(float)` +- `Get_Damage_Modifier()`, `Set_Damage_Modifier(float)` +- `Disable_Capture(bool)`, `Is_Capturable()` +- Many more — see Focumentation for complete list + +### Additional Global Functions (~60) +- `Make_Ally(player1, player2)`, `Make_Enemy(player1, player2)` +- `Disable_Bombing_Run(bool)` — NOTE: parameter logic is REVERSED (pass false to disable) +- `Flash_GUI_Object(name)`, `Hide_GUI_Object(name)` +- `Lock_Controls(bool)`, `Unlock_Controls()` +- `Set_Cinematic_Camera_Key(pos, target, duration)` +- `Sleep(seconds)` — yields coroutine +- `Thread.Get_Current_Stage()`, `Thread.Create(...)` +- `SFXManager.Allow_Unit_Reponse_VO(bool)` — NOTE: engine typo "Reponse" + +### Additional PlayerWrapper Methods (~20) +- `Make_Ally(player)`, `Make_Enemy(player)` — resets every game mode change! +- `Get_Faction()`, `Get_Name()` +- `Get_Space_Station_Level(planet)` +- `Get_Tech_Level()` +- `Disable_Orbital_Bombardment(bool)` + +### FOW (Fog of War) Functions +- `FOWManager.Reveal_All(player)` — reveals entire map +- `FOWManager.Reveal(player, position, radius)` +- `FOWManager.Undo_Reveal_All(player)` + +--- + +## 6. Behavioral Warnings (From Community Testing) + +| Function | Warning | +|----------|---------| +| `Prevent_AI_Usage(true)` | Crashes in tactical if unit has no active AI | +| `Make_Ally` / `Make_Enemy` | Resets every game mode change | +| `Disable_Bombing_Run` | Parameter logic REVERSED (pass false to disable) | +| `Attack_Move` CommandBlock | Never finishes (engine bug) | +| `SFXManager.Allow_Unit_Reponse_VO` | Engine typo — "Reponse" not "Response" | + +--- + +## 7. Example Pipe Commands + +Once the pipe listener is running, send these from any external tool: + +```bash +# Give 50000 credits +echo 'SWFOC_SetCredits(50000)' > \\.\pipe\swfoc_bridge + +# Spawn an AT-AT at origin +echo 'Spawn_Unit(SWFOC_GetLocalPlayer(), Find_Object_Type("AT_AT"), Create_Position(0,0,0))' > \\.\pipe\swfoc_bridge + +# Make all your units invincible +echo 'for _,u in pairs(Find_All_Objects_Of_Type(nil)) do if u:Get_Owner() == SWFOC_GetLocalPlayer() then u:Make_Invulnerable(true) end end' > \\.\pipe\swfoc_bridge + +# Max tech +echo 'SWFOC_SetTechLevel(5)' > \\.\pipe\swfoc_bridge + +# Execute any Lua file +echo 'dofile("my_script.lua")' > \\.\pipe\swfoc_bridge +``` + +--- + +## 8. Architecture: How It All Fits Together + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ External Tools │ +│ (.NET Trainer, CE, Python, CLI) │ +│ │ │ +│ ▼ │ +│ \\.\pipe\swfoc_bridge ──► Named Pipe Thread (background) │ +│ │ │ +│ ▼ │ +│ Command Queue │ +│ │ │ +│ ▼ │ +│ ┌─────────────────────────────────────────────────────────┐ │ +│ │ Game Main Thread │ │ +│ │ lua_open hook → drain queue → lua_load + lua_pcall │ │ +│ │ │ │ +│ │ 17 Lua C API functions (pushstring, gettable, pcall...) │ │ +│ │ 10 SWFOC_* custom functions (SetCredits, DoString...) │ │ +│ │ │ │ +│ │ ──► Game's 405 Lua functions (Spawn_Unit, etc.) │ │ +│ │ ──► Game's engine API (SetHP, AddCredits, etc.) │ │ +│ └─────────────────────────────────────────────────────────┘ │ +│ │ +│ powrprof.dll (DLL proxy, side-loaded via search order) │ +└─────────────────────────────────────────────────────────────────┘ +``` + +--- + +*Generated from binary RE (Ghidra static analysis) + community documentation (eaw-emmyluadoc, Focumentation)* +*48 Lua C API functions mapped, 134 game bindings confirmed, 271 community-documented* +*Total coverage: ~405 Lua functions accessible via the bridge* + diff --git a/docs/editor/rvas.md b/docs/editor/rvas.md new file mode 100644 index 000000000..18e2ee2f5 --- /dev/null +++ b/docs/editor/rvas.md @@ -0,0 +1,676 @@ +# Complete RVA Reference + +See also: [Lua API](lua-api.md) | [Struct Reference](index.md) + +--- + +## Star Wars: Empire at War -- Forces of Corruption (64-bit Steam) +### Binary: StarWarsG.exe v1.121.13.7360, x86_64, MSVC, 13,369,344 bytes +### Ghidra Image Base: 0x140000000 + +**Document Version:** 3.0 (Phase 3 Consolidation) +**Total RVAs Cataloged:** 280+ +**Analysis Date:** 2026-04-04 + +--- + +## Verification Method Legend + +| Tag | Meaning | +|-----|---------| +| CONFIRMED-RUNTIME | Verified via live runtime testing (CE, DLL bridge, hook) | +| CONFIRMED-RE | Verified via binary RE (Ghidra decompilation, multiple xrefs) | +| CONFIRMED-FOCAPI | Matches FoCAPI community reference (independently verified) | +| CONFIRMED-AOB | AOB signature verified unique within module | +| CONFIRMED-RTTI | Class identified via RTTI recovery | +| DISCOVERED-GHIDRA | Found via Ghidra static analysis in Phase 3 (single source) | +| DISCOVERED-CONTEXT | Inferred from decompiler context and cross-references | +| ESTIMATED | Educated guess based on source ordering or patterns | + +--- + +## 1. Lua 5.0.2 C API Functions (48 Confirmed) + +### Confirmed and Tested + +| RVA | Function | Status | Verification | +|-----|----------|--------|-------------| +| `0x7B8930` | lua_open | CONFIRMED-RUNTIME | MinHook hooked, 400+ states captured | +| `0x7B8BC0` | lua_checkstack | CONFIRMED-RE | Ghidra: checks (top-base>>4)+extra > 0x800 | +| `0x7B8C40` | lua_concat | CONFIRMED-RE | Ghidra: if n>1 calls concat, if n==0 pushes empty | +| `0x7B8CD0` | lua_cpcall | CONFIRMED-RE | Ghidra: checks top-1 is function, calls luaD_pcall | +| `0x7B8D80` | lua_error | CONFIRMED-RE | Ghidra: calls luaG_errormsg | +| `0x7B8D90` | lua_getfenv | CONFIRMED-RE | Ghidra: checks tag==6+C, reads env from closure+0x20 | +| `0x7B8DF0` | lua_getgccount | CONFIRMED-RE | Ghidra: returns global_State+0x74 >> 10 | +| `0x7B8E00` | lua_getgcthreshold | CONFIRMED-RE | Ghidra: returns global_State+0x70 >> 10 | +| `0x7B8E10` | lua_getmetatable | CONFIRMED-RE | Ghidra: checks tag==5||7, reads metatable at gc+0x10 | +| `0x7B8E90` | lua_gettable | CONFIRMED-RE | Ghidra: resolves table, calls luaV_gettable | +| `0x7B8EF0` | lua_gettop | CONFIRMED-RUNTIME | Bytes: 48 8B 41 10 48 2B 41 18 48 C1 F8 04 C3 | +| `0x7B8F00` | lua_insert | CONFIRMED-RE | Ghidra: shifts values above idx, copies top | +| `0x7B8F60` | lua_iscfunction | CONFIRMED-RE | Ghidra: checks tag==6 && gc+0x0a != 0 | +| `0x7B8FB0` | lua_isnumber | CONFIRMED-RE | Ghidra: checks tag==3 or calls tonumber | +| `0x7B9010` | lua_isstring | CONFIRMED-RE | Ghidra: checks tag-3 < 2 | +| `0x7B9060` | lua_equal | CONFIRMED-RE | Ghidra: calls luaV_equalobj | +| `0x7B90F0` | lua_load | CONFIRMED-RE | Ghidra: calls parser/compiler chain | +| `0x7B9140` | lua_newtable | CONFIRMED-RUNTIME | FoCAPI match + DLL self-test PASSED | +| `0x7B9190` | lua_newthread | CONFIRMED-RE | Ghidra: creates thread, pushes type=8 | +| `0x7B91D0` | lua_newuserdata | CONFIRMED-RE | Ghidra: allocates Udata, returns gc+0x20 | +| `0x7B9220` | lua_next | CONFIRMED-RE | Ghidra: hash traversal on table | +| `0x7B9280` | lua_pcall | CONFIRMED-RE | Ghidra: 4 params, calls luaD_pcall at 0x7BBBE0 | +| `0x7B9320` | lua_pushboolean | CONFIRMED-RUNTIME | Byte pattern: writes type=1 based on EDX | +| `0x7B9340` | lua_pushcclosure | CONFIRMED-RUNTIME | FoCAPI match + DLL registration works | +| `0x7B9480` | lua_pushlightuserdata | CONFIRMED-RUNTIME | Byte pattern: writes type=2, value from RDX | +| `0x7B94A0` | lua_pushlstring | CONFIRMED-RE | Ghidra: pushes type=4, calls string intern | +| `0x7B9510` | lua_pushnil | CONFIRMED-RUNTIME | Byte pattern: writes type=0 | +| `0x7B9520` | lua_pushnumber | CONFIRMED-RUNTIME | Byte-verified: movsd [rax+8],xmm1 + type=3 | +| `0x7B9540` | lua_pushstring | CONFIRMED-RUNTIME | FoCAPI RVA, DLL self-test PASSED | +| `0x7B9600` | lua_pushvalue | CONFIRMED-RE | Ghidra: copies TValue at idx to top | +| `0x7B9640` | lua_pushfstring | CONFIRMED-RE | Ghidra: wraps luaO_pushfstring | +| `0x7B9690` | lua_lessthan | CONFIRMED-RE | Ghidra: calls luaV_lessthan | +| `0x7B9820` | lua_rawseti | CONFIRMED-RUNTIME | FoCAPI RVA, DLL self-test PASSED | +| `0x7B99D0` | lua_settop | CONFIRMED-RUNTIME | DLL self-test PASSED (pop operations) | +| `0x7B9A60` | lua_settable | CONFIRMED-RUNTIME | FoCAPI RVA, DLL registration PASSED | +| `0x7B9BC0` | lua_tonumber | CONFIRMED-RUNTIME | DLL self-test: returned 12345.0 correctly | +| `0x7B9E00` | lua_type | CONFIRMED-RUNTIME | DLL self-test: returned 4 for string, 3 for number | + +### Estimated / Needs Verification + +| RVA | Function | Status | Notes | +|-----|----------|--------|-------| +| `0x7B8A70` | lua_close | ESTIMATED | Between lua_open and lua_checkstack; needs Ghidra confirm | +| `0x7B9B10` | lua_tostring candidate 1 | ESTIMATED | Has cmp-with-4 at +0x3A | +| `0x7B9CC0` | lua_tostring candidate 2 | ESTIMATED | Standard prologue, cmp-with-4 at +0x45 | + +### Known Wrong RVAs (DO NOT USE) + +| RVA | Was Labeled | Actual | Evidence | +|-----|-----------|--------|----------| +| `0x7B94A0` | lua_pushstring | lua_pushlstring | FoCAPI 0x7B9540 works, this has 3 params | +| `0x7B97C0` | lua_settable | Unknown | Crashes; real is 0x7B9A60 | +| `0x7B9500` | lua_pushnumber | Unknown | Game crashed; real is 0x7B9520 | +| `0x7B8E10` | lua_gettable | lua_getmetatable | Self-test FAILED; real gettable is 0x7B8E90 | + +--- + +## 2. Player System Functions + +| RVA | Function | Status | Verification | +|-----|----------|--------|-------------| +| `0x27ED40` | PlayerClass::~PlayerClass | CONFIRMED-RE | Ghidra destructor analysis, RTTI confirmed | +| `0x27F370` | AddCredits | CONFIRMED-AOB | Phase 2 RE, AOB verified unique, CE tested | +| `0x27F7C0` | Lock_Tech_Add | CONFIRMED-RE | Ghidra decompilation of Lock_Tech flow | +| `0x27F860` | Unlock_Tech | CONFIRMED-RE | Writes to +0x1B0/0x1B8 confirmed | +| `0x282190` | Auto_Upgrade_Tech | DISCOVERED-GHIDRA | Increments tech_level at +0x84 | +| `0x2823E0` | Is_Ally | CONFIRMED-RE | Returns diplomacy_table[id] == 0 | +| `0x2824F0` | Is_Enemy | CONFIRMED-RE | Returns diplomacy_table[id] == 1 | +| `0x282550` | IsLocked | CONFIRMED-RE | Reads +0x1C8/0x1D0 | +| `0x282580` | IsUnlocked | CONFIRMED-RE | Reads +0x1B0/0x1B8 | +| `0x286100` | Unlock_Tech_Remove | CONFIRMED-RE | Removes from locked list | +| `0x286150` | Lock_Tech | CONFIRMED-RE | Writes to locked_types DVec | +| `0x288800` | Make_Ally / Make_Enemy | CONFIRMED-RE | Writes diplomacy_table[target_id] | +| `0x288980` | SetTechLevel | CONFIRMED-AOB | Phase 2 RE, AOB verified unique | +| `0x294BC0` | PlayerList_FindByID | CONFIRMED-FOCAPI | Phase 1 RE + FoCAPI exact match | +| `0x2BD2F0` | Select_Object_Engine | CONFIRMED-RE | Called by Lua Select_Object wrapper | +| `0x340920` | Retreat_Engine | CONFIRMED-RE | Called by Lua Retreat wrapper | + +### Player Lua Wrapper Functions + +| RVA | Lua Method | Status | +|-----|-----------|--------| +| `0x601AA0` | Disable_Bombing_Run | DISCOVERED-GHIDRA | +| `0x601BE0` | Disable_Orbital_Bombardment | DISCOVERED-GHIDRA | +| `0x601E80` | Enable_Advisor_Hints | DISCOVERED-GHIDRA | +| `0x602060` | Get_Name (AI type) | DISCOVERED-GHIDRA | +| `0x602640` | Enable_As_Actor | CONFIRMED-RE | +| `0x602690` | Get_Space_Station_Level | DISCOVERED-GHIDRA | +| `0x6027F0` | Get_Credits | DISCOVERED-GHIDRA | +| `0x602A00` | Get_Faction_Name | DISCOVERED-GHIDRA | +| `0x602AE0` | Get_Difficulty | DISCOVERED-GHIDRA | +| `0x602C40` | Get_ID | DISCOVERED-GHIDRA | +| `0x602EE0` | Get_Team_ID | DISCOVERED-GHIDRA | +| `0x603040` | Get_Tech_Level | DISCOVERED-GHIDRA | +| `0x603130` | Give_Money | CONFIRMED-RE | +| `0x603560` | Is_Ally | CONFIRMED-RE | +| `0x603760` | Is_Enemy | CONFIRMED-RE | +| `0x603960` | Is_Local_Player | DISCOVERED-GHIDRA | +| `0x603A40` | Is_Human | DISCOVERED-GHIDRA | +| `0x603B20` | Lock_Tech | CONFIRMED-RE | +| `0x603C70` | Release_Credits_For_Tactical | CONFIRMED-RE | +| `0x603DE0` | Retreat | CONFIRMED-RE | +| `0x603F60` | Select_Object | CONFIRMED-RE | +| `0x604300` | Set_Black_Market_Tutorial | DISCOVERED-GHIDRA | +| `0x6043C0` | Set_Sabotage_Tutorial | DISCOVERED-GHIDRA | +| `0x604480` | Set_Tech_Level | CONFIRMED-RE | +| `0x604540` | Unlock_Tech | CONFIRMED-RE | +| `0x6046A0` | Make_Ally | CONFIRMED-RE | +| `0x604780` | Make_Enemy | CONFIRMED-RE | +| `0x6019F0` | PlayerWrapper::Create | CONFIRMED-FOCAPI | + +--- + +## 3. Game Object System Functions + +| RVA | Function | Status | Verification | +|-----|----------|--------|-------------| +| `0x388720` | GameObjectClass::ctor (default) | CONFIRMED-RE | Ghidra, writes vtable 0x8661B8 | +| `0x388B60` | GameObjectClass::ctor (full) | CONFIRMED-RE | Ghidra, 5+ params | +| `0x395AC0` | QueryInterface | CONFIRMED-RUNTIME | Phase 1 RE, pseudocode available | +| `0x3956C0` | ResolveParentOwner | CONFIRMED-RE | Phase 1 RE | +| `0x395920` | SubObject_OwnerResolve | DISCOVERED-GHIDRA | Reads +0xB8, +0xD8 | +| `0x395C70` | BuffModifier_Read | DISCOVERED-GHIDRA | Reads +0xF0 | +| `0x3989A0` | Spawn_Init | CONFIRMED-RE | Allocates combatant (0x3B8), sets initial HP | +| `0x3AC530` | Transform_Update | DISCOVERED-GHIDRA | Signal listener notify, position update | +| `0x574D0E` | Change_Owner | CONFIRMED-RE | Phase 2 RE | +| `0x5792E0` | Get_Owner_Lua | CONFIRMED-RE | Phase 1 RE, pseudocode available | +| `0x5819E0` | Make_Invulnerable_Lua | DISCOVERED-GHIDRA | Lua binding for invulnerability | +| `0x769C58` | operator new | CONFIRMED-FOCAPI | Phase 2 RE + FoCAPI match | + +--- + +## 4. Combat System Functions + +| RVA | Function | Status | Verification | +|-----|----------|--------|-------------| +| `0x3727A0` | GetMaxHealth | DISCOVERED-GHIDRA | Reads type+0xDCC, applies multipliers | +| `0x372320` | GetMaxFrontShield | DISCOVERED-GHIDRA | Reads type+0xDD0 | +| `0x3725F0` | GetMaxRearShield | DISCOVERED-GHIDRA | Reads type+0xDD4 | +| `0x3711C0` | GetAdjustedCost (variant) | DISCOVERED-GHIDRA | Cost modifier chain | +| `0x374DA0` | HasCombatBehavior | DISCOVERED-GHIDRA | Checked during spawn init | +| `0x3751A0` | FactionAffiliation_Check | DISCOVERED-GHIDRA | Used by SetTechLevel, CanProduce | +| `0x387010` | WeaponTick | DISCOVERED-GHIDRA | Per-frame weapon update, cooldown via delta-time | +| `0x387F50` | HardpointFire | DISCOVERED-GHIDRA | Per-hardpoint damage, station level loss | +| `0x38A350` | Take_Damage_Outer | DISCOVERED-GHIDRA | 8 invulnerability checks, top-level gate | +| `0x38D730` | FireControl_Dispatch | DISCOVERED-GHIDRA | Master fire control, enemy/invuln/LOS checks | +| `0x38EB10` | Death_Signal_Behavior | DISCOVERED-GHIDRA | Notifies behavior system of death | +| `0x38F8B0` | ClearSpeedOverride | CONFIRMED-AOB | Phase 2 RE, AOB verified unique | +| `0x396DF0` | GetHullPercentage | DISCOVERED-GHIDRA | Per-hardpoint hull ratio | +| `0x3963C0` | FrontShield_Read | DISCOVERED-GHIDRA | Reads front shield current value | +| `0x39BDB0` | Death_Handler | DISCOVERED-GHIDRA | Full death sequence (17 steps) | +| `0x3A06A0` | Property_Init_HP | DISCOVERED-GHIDRA | Initial HP from XML data system | +| `0x3A56B0` | Invulnerability_Cleanup | DISCOVERED-GHIDRA | Cleans up invulnerability state | +| `0x3A8630` | SetFrontShield | DISCOVERED-GHIDRA | Front shield write | +| `0x3A89D0` | **SetHP** | CONFIRMED-RUNTIME | Phase 1 RE + CT hook + AOB unique | +| `0x3A8C90` | SetSpeedOverride | CONFIRMED-AOB | Phase 2 RE, AOB verified unique | +| `0x3A91E0` | SetRearShield | DISCOVERED-GHIDRA | Rear shield write | +| `0x3A92F0` | AnimationDispatch | DISCOVERED-GHIDRA | Damage type routing, Berserker override | +| `0x3A97E0` | Take_Damage_PropertyDispatch | DISCOVERED-GHIDRA | Shield/hardpoint routing | +| `0x3AB890` | Take_Damage_Impl | DISCOVERED-GHIDRA | Core: old_hp - damage, prevent-death | +| `0x3ABB80` | Make_Invulnerable_Setter | CONFIRMED-RE | Phase 2 RE, propagates to hardpoints | +| `0x3AC290` | DamageVisualLevel | DISCOVERED-GHIDRA | Damage model swap thresholds | +| `0x405230` | HullRatio_ViaHardpoints | DISCOVERED-GHIDRA | Aggregate health display | +| `0x4052D0` | GetHardpoint | DISCOVERED-GHIDRA | Returns hardpoint by index | +| `0x405300` | HardpointCount | DISCOVERED-GHIDRA | Returns number of hardpoints | +| `0x42DD63` | Lua_Set_Hull (caller) | DISCOVERED-GHIDRA | SetHP caller from Lua | +| `0x48EB10` | ScheduleHeroRespawn | CONFIRMED-RE | Phase 2 RE | +| `0x56BFB0` | FrontShield_Read_Impl | DISCOVERED-GHIDRA | Shield behavior read | +| `0x56C1B0` | FrontShield_Write_Impl | DISCOVERED-GHIDRA | Shield behavior write | +| `0x549490` | RearShield_Read_Impl | DISCOVERED-GHIDRA | Rear shield read | +| `0x549810` | RearShield_Write_Impl | DISCOVERED-GHIDRA | Rear shield write | +| `0x5D70F0` | HealthRegen_Periodic | DISCOVERED-GHIDRA | Natural HP regen | + +### Combat Ability Constructors + +| RVA | Ability Class | Status | +|-----|--------------|--------| +| `0x6EEAE0` | ArcSweepAttackAbilityClass | DISCOVERED-GHIDRA | +| `0x6EE840` | AbsorbBlasterAbilityClass | DISCOVERED-GHIDRA | +| `0x6F22F0` | CableAttackAbilityClass | DISCOVERED-GHIDRA | +| `0x6F42D0` | CombatBonusAbilityClass | DISCOVERED-GHIDRA | +| `0x6F52C0` | ConcentrateFireAttackAbilityClass | DISCOVERED-GHIDRA | +| `0x6F7170` | EarthquakeAttackAbilityClass | DISCOVERED-GHIDRA | +| `0x6F7A30` | EatAttackAbilityClass | DISCOVERED-GHIDRA | +| `0x6F80D0` | PeriodicDamage (DoT) | DISCOVERED-GHIDRA | +| `0x6F9980` | EnergyWeaponAttackAbilityClass | DISCOVERED-GHIDRA | +| `0x6FFFF0` | GenericAttackAbilityClass | DISCOVERED-GHIDRA | +| `0x706280` | LuckyShotAttackAbilityClass | DISCOVERED-GHIDRA | +| `0x706B10` | MaximumFirepowerAttackAbilityClass | DISCOVERED-GHIDRA | +| `0x7048E0` | IonCannonShotAttackAbilityClass | DISCOVERED-GHIDRA | +| `0x70A280` | ReduceProductionPriceAbilityClass | DISCOVERED-GHIDRA | +| `0x70B040` | ReduceProductionTimeAbilityClass | DISCOVERED-GHIDRA | +| `0x70EB50` | StarbaseUpgradeAbilityClass | DISCOVERED-GHIDRA | +| `0x710080` | TractorBeamAttackAbilityClass | DISCOVERED-GHIDRA | +| `0x712D20` | BerserkerAbilityClass | DISCOVERED-GHIDRA | +| `0x717530` | LeechShieldsAbilityClass | DISCOVERED-GHIDRA | +| `0x71B560` | DrainLifeAbilityClass | DISCOVERED-GHIDRA | +| `0x71C820` | ShieldFlareAbilityClass | DISCOVERED-GHIDRA | + +### Combatant Behavior Classes + +| RVA | Class | Status | +|-----|-------|--------| +| `0x6383E0` | CombatantBehaviorClass::ctor | DISCOVERED-GHIDRA | +| `0x6CB700` | BaseCombatantClass::ctor | DISCOVERED-GHIDRA | +| `0x6CBEE0` | CompanyCombatantClass::ctor | DISCOVERED-GHIDRA | +| `0x6CBA40` | SquadronCombatantClass::ctor | DISCOVERED-GHIDRA | + +--- + +## 5. AI System Functions + +| RVA | Function | Status | Verification | +|-----|----------|--------|-------------| +| `0x4747E0` | TheAIDataManagerClass::ctor | DISCOVERED-GHIDRA | Singleton | +| `0x4AF810` | AIPlayerClass::ctor | DISCOVERED-GHIDRA | Per-player AI controller | +| `0x4B0250` | Enable_As_Actor | CONFIRMED-RE | Lua binding for AI enable | +| `0x4B06D0` | Release_Credits_For_Tactical | CONFIRMED-RE | AI credits release | +| `0x4D9C80` | TheAIClass::ctor | DISCOVERED-GHIDRA | Top-level singleton | +| `0x4DAD80` | AIPerceptionSystemClass::ctor | DISCOVERED-GHIDRA | Base perception | +| `0x4E1880` | GalacticPerceptionSystemClass::ctor | DISCOVERED-GHIDRA | Galactic mode | +| `0x4E8920` | TheAITemplateManagerClass::ctor | DISCOVERED-GHIDRA | Singleton | +| `0x4E9940` | TheAIPlayerTypeManagerClass::ctor | DISCOVERED-GHIDRA | Singleton | +| `0x524CE0` | AIExecutionSystemClass::ctor | DISCOVERED-GHIDRA | | +| `0x585D00` | AILearningSystemClass::ctor | DISCOVERED-GHIDRA | 5 hash maps | +| `0x5E5C30` | TheAIGoalProposalFunctionSetManagerClass::ctor | DISCOVERED-GHIDRA | | +| `0x5E6690` | TheAIGoalTypeManagerClass::ctor | DISCOVERED-GHIDRA | | +| `0x6109C0` | AIBudgetClass::ctor | DISCOVERED-GHIDRA | | +| `0x610A70` | BudgetedCategoryStruct::ctor | DISCOVERED-GHIDRA | | +| `0x6478C0` | AIBuildTaskClass::ctor | DISCOVERED-GHIDRA | FSM, 25 states | +| `0x64AEC0` | AIBuildTaskClass::StateMachineHandler | DISCOVERED-GHIDRA | | +| `0x64C250` | ServicedAISystemClass::ctor | DISCOVERED-GHIDRA | Base subsystem | +| `0x653340` | TacticalPerceptionGridClass::ctor | DISCOVERED-GHIDRA | | +| `0x6954B0` | AIPlanetBuildTaskClass::ctor | DISCOVERED-GHIDRA | | +| `0x6B8480` | GalacticGoalSystemClass::ctor | DISCOVERED-GHIDRA | | +| `0x6B86E0` | LandGoalSystemClass::ctor | DISCOVERED-GHIDRA | | +| `0x6B88D0` | SpaceGoalSystemClass::ctor | DISCOVERED-GHIDRA | | +| `0x6B8980` | LandPerceptionSystemClass::ctor | DISCOVERED-GHIDRA | | +| `0x6B9A20` | SpacePerceptionSystemClass::ctor | DISCOVERED-GHIDRA | | +| `0x6BAC00` | AIPlanningSystemClass::ctor | DISCOVERED-GHIDRA | 4.0s interval | +| `0x6BB9E0` | AITemplateSystemClass::ctor | DISCOVERED-GHIDRA | | +| `0x6C6500` | ProduceForceBlockStatus::ctor | DISCOVERED-GHIDRA | | +| `0x6C7970` | AIGoalSystemClass::ctor | DISCOVERED-GHIDRA | | + +--- + +## 6. Galactic Map Functions + +| RVA | Function | Status | Verification | +|-----|----------|--------|-------------| +| `0x3F3340` | PlanetaryBehaviorClass::dtor | DISCOVERED-GHIDRA | Links planet to data pack | +| `0x3F6AF0` | Planet_ComputeIncomeValue | DISCOVERED-GHIDRA | Called during ownership changes | +| `0x3F8AA0` | BuildPad_CheckSlots | DISCOVERED-GHIDRA | Validates build pad availability | +| `0x3F8B30` | BuildPad_CheckSecondary | DISCOVERED-GHIDRA | Secondary slot check | +| `0x3FA160` | PlanetFactionChange_InitialSet | DISCOVERED-GHIDRA | Initial ownership assignment | +| `0x3FB040` | PlanetFactionChange_FullTransfer | DISCOVERED-GHIDRA | Full ownership transfer with UI | +| `0x3FE810` | CaptureTimer_Update | DISCOVERED-GHIDRA | Writes capture timer fields | +| `0x4B1270` | GalacticModeClass::ctor | DISCOVERED-GHIDRA | | +| `0x4B5DF0` | LineLinkStruct::dtor | DISCOVERED-GHIDRA | | +| `0x4B5E60` | PersistentTacticalBuiltObjectStruct::dtor | DISCOVERED-GHIDRA | | +| `0x4B5ED0` | PersistentUpgradeObjectStruct::dtor | DISCOVERED-GHIDRA | | +| `0x4B5F40` | TradeRouteLinkEntryClass::dtor | DISCOVERED-GHIDRA | | +| `0x4AE5E0` | DVec::dtor | DISCOVERED-GHIDRA | | +| `0x663530` | PlanetReachabilityClass::ctor | DISCOVERED-GHIDRA | 11 slots | +| `0x6A53B0` | FOW_Undo_Reveal_All | DISCOVERED-GHIDRA | Sets global toggle | +| `0x331CC0` | FindPlanetType | DISCOVERED-GHIDRA | Checks is_planet flag | + +--- + +## 7. Production System Functions + +| RVA | Function | Status | Verification | +|-----|----------|--------|-------------| +| `0x2804D0` | CanProduce | DISCOVERED-GHIDRA | 18 prerequisite gates | +| `0x2AC320` | CheckPopCap | DISCOVERED-GHIDRA | Unit cap validation | +| `0x3FFF10` | ProductionBehaviorClass::dtor | DISCOVERED-GHIDRA | | +| `0x3FFF70` | ObjectUnderConstructionClass::ctor | DISCOVERED-GHIDRA | 0x38 bytes | +| `0x400240` | GetAdjustedCost | DISCOVERED-GHIDRA | Modified cost calculation | +| `0x400370` | ComputeBuildTime | DISCOVERED-GHIDRA | 5-factor formula | +| `0x42D850` | TacticalBuildObjectsBehaviorClass::dtor | DISCOVERED-GHIDRA | | +| `0x42E890` | Production_CompletionHandler | DISCOVERED-GHIDRA | Spawns finished unit | +| `0x497900` | ObjectUnderConstruction_Serialize | DISCOVERED-GHIDRA | Save/load | +| `0x523F50` | ProductionEventClass::ctor | DISCOVERED-GHIDRA | Event ID=7 | +| `0x5242C0` | ObjectUnderConstructionClass::dtor | DISCOVERED-GHIDRA | | +| `0x559680` | ProductionDataPackClass::ctor | DISCOVERED-GHIDRA | 2 queues | +| `0x561270` | TacticalBuildObjectsDataPackClass::ctor | DISCOVERED-GHIDRA | | +| `0x5D7740` | TacticalBuildEventClass::ctor | DISCOVERED-GHIDRA | Event ID=33 | +| `0x532690` | GalacticSellEventClass::ctor | DISCOVERED-GHIDRA | Event ID=46 | + +--- + +## 8. Story System Functions + +| RVA | Function | Status | Verification | +|-----|----------|--------|-------------| +| `0x215A30` | CRC32_Hash | CONFIRMED-RE | Standard CRC32 with table at 0xA14D20 | +| `0x242570` | LuaScriptClass::ctor | DISCOVERED-GHIDRA | | +| `0x2567B0` | LuaScriptClass::RegisterGlobals | CONFIRMED-RE | 14 globals including GetEvent | +| `0x4501D0` | StoryEventClass::ctor | CONFIRMED-RE | Size >= 0x360 | +| `0x450600` | StoryEventClass::dtor | CONFIRMED-RE | | +| `0x4504E0` | StoryEventCommandUnitClass::ctor | DISCOVERED-GHIDRA | | +| `0x450540` | StoryEventHeroMoveClass::ctor | DISCOVERED-GHIDRA | | +| `0x452D70` | StorySubPlot_ComputeDependencies | CONFIRMED-RE | Builds dependency graph | +| `0x453310` | StoryEvent_Factory_Create | CONFIRMED-RE | 61 type IDs, switch/case | +| `0x4562A0` | StoryEvent_BuildFromParsed | DISCOVERED-GHIDRA | | +| `0x45C3F0` | ResolveRewardTypeFromString | CONFIRMED-RE | CRC32 tree lookup | +| `0x45C5D0` | StoryEvent_ParseXMLBlock | DISCOVERED-GHIDRA | | +| `0x467540` | StoryEvent_DebugLog | DISCOVERED-GHIDRA | | +| `0x47D6B0` | DVec::dtor | DISCOVERED-GHIDRA | | +| `0x52D400` | StorySubPlotClass::ctor | CONFIRMED-RE | Size >= 0x650 | +| `0x52E220` | StorySubPlotClass::dtor | CONFIRMED-RE | | +| `0x52EF90` | StorySubPlot_FindEventByName | CONFIRMED-RE | CRC32 XOR 0xDEADBEEF + LCG | +| `0x52FC10` | StorySubPlot_Reset | CONFIRMED-RE | Iterates events, clears +0x4C | +| `0x530180` | StorySubPlot_BuildTypeArrays | DISCOVERED-GHIDRA | 61-slot indexed arrays | +| `0x5DC4B0` | StoryEventSelectPlanetClass::ctor | DISCOVERED-GHIDRA | | +| `0x724120` | StoryPlotWrapper::LuaConstructor | CONFIRMED-RE | 5 methods | +| `0x724480` | StoryPlotWrapper::Get_Event | CONFIRMED-RE | | +| `0x7246E0` | StoryPlotWrapper::Reset | CONFIRMED-RE | | +| `0x73DC80` | StoryEventWrapper::LuaConstructor | CONFIRMED-RE | 7 methods | +| `0x73DF60` | StoryEventWrapper::Add_Dialog_Text | CONFIRMED-RE | | +| `0x73E4F0` | StoryEventWrapper::Clear_Dialog_Text | CONFIRMED-RE | | +| `0x73E590` | WrapEventAsLuaObject | DISCOVERED-GHIDRA | | +| `0x73E9A0` | StoryEventWrapper::Set_Dialog | CONFIRMED-RE | | +| `0x73ECA0` | StoryEventWrapper::Set_Reward_Parameter | CONFIRMED-RE | | +| `0x73EEF0` | StoryEventWrapper::Set_Reward_Type | CONFIRMED-RE | | + +--- + +## 9. Camera and Selection Functions + +| RVA | Function | Status | Verification | +|-----|----------|--------|-------------| +| `0x14EE30` | GetScreenAspectRatio | DISCOVERED-GHIDRA | Returns float at 0xA12550 | +| `0x141260` | RebuildProjectionMatrix | DISCOVERED-GHIDRA | Perspective/ortho dispatch | +| `0x1417C0` | ProjectionMatrix_Dispatch | DISCOVERED-GHIDRA | | +| `0x22D390` | ComputeInverseMatrix | DISCOVERED-GHIDRA | | +| `0x232C50` | BuildPerspectiveViewProjectionMatrix | CONFIRMED-RE | | +| `0x232DF0` | BuildOrthoViewProjectionMatrix | CONFIRMED-RE | | +| `0x261470` | CameraClass::ctor | CONFIRMED-RE | Sub-object 0x308 bytes | +| `0x261590` | CameraClass::CopyFrom | CONFIRMED-RE | | +| `0x261690` | CameraClass::GetForwardDirection | CONFIRMED-RE | Negated +0x08,+0x18 | +| `0x261870` | CameraClass::GetViewport | DISCOVERED-GHIDRA | | +| `0x2618E0` | CameraClass::GetFovAspect | CONFIRMED-RE | Reads +0x48, +0x4C | +| `0x261900` | CameraClass::GetFrustumBounds | CONFIRMED-RE | | +| `0x2619F0` | CameraClass::GetTransformMatrix | CONFIRMED-RE | 12 floats | +| `0x261A40` | CameraClass::GetPosition | CONFIRMED-RE | +0x0C, +0x1C, +0x2C | +| `0x261A80` | CameraClass::SetFovAspect | CONFIRMED-RE | | +| `0x261AB0` | CameraClass::SetPerspectiveProjection | CONFIRMED-RE | | +| `0x261B50` | CameraClass::SetOrthoProjection | CONFIRMED-RE | | +| `0x261BD0` | CameraClass::SetTransformMatrix | CONFIRMED-RE | | +| `0x261C90` | CameraClass::TranslateLocal | CONFIRMED-RE | | +| `0x261E00` | CameraClass::SetViewport | CONFIRMED-RE | | +| `0x2611C0` | CameraClass::Render | DISCOVERED-GHIDRA | | +| `0x28A950` | GameModeManagerClass::GetModeByType | CONFIRMED-RE | Iterates modes | +| `0x28D930` | Selection_WriteState | DISCOVERED-GHIDRA | Writes to GMM+0xB4 | +| `0x2CF8D0` | MatrixRotation | DISCOVERED-GHIDRA | Camera orbit rotation | +| `0x3AC9D0` | SelectEventClass::ctor | CONFIRMED-RE | Event ID=5 | +| `0x3C2B20` | GalacticCameraClass::ctor | CONFIRMED-RE | | +| `0x3C2C00` | GalacticCameraClass::Update | CONFIRMED-RE | Main frame update | +| `0x3C1960` | SelectBehaviorClass::dtor | CONFIRMED-RE | 0x58 bytes | +| `0x436060` | ControlGroupEventClass::ctor | CONFIRMED-RE | Event ID=30 | +| `0x437F40` | SelectAllEventClass::ctor | CONFIRMED-RE | Event ID=37 | +| `0x55BE30` | SelectionDataPackClass::ctor | CONFIRMED-RE | Pool size 20 | +| `0x689670` | LookEventClass::ctor | CONFIRMED-RE | Event ID=4 | + +--- + +## 10. Save System Functions + +| RVA | Function | Status | Verification | +|-----|----------|--------|-------------| +| `0x2043B0` | Read_Int | DISCOVERED-GHIDRA | Serialization primitive | +| `0x2046F0` | Write_Int | DISCOVERED-GHIDRA | Serialization primitive | +| `0x204AD0` | Read_String | DISCOVERED-GHIDRA | wstring deserialization | +| `0x204FB0` | Write_String | DISCOVERED-GHIDRA | wstring serialization | +| `0x213010` | FileClass::ctor (default) | DISCOVERED-GHIDRA | | +| `0x2130D0` | FileClass::ctor (cstring) | DISCOVERED-GHIDRA | | +| `0x2131E0` | FileClass::ctor (stdstring) | DISCOVERED-GHIDRA | | +| `0x2132F0` | FileClass::dtor | DISCOVERED-GHIDRA | | +| `0x213600` | FileClass::Open | DISCOVERED-GHIDRA | CreateFileA wrapper | +| `0x21FCC0` | ChunkWriterClass::ctor | DISCOVERED-GHIDRA | | +| `0x21FDC0` | ChunkWriterClass::dtor | DISCOVERED-GHIDRA | | +| `0x21FE20` | ChunkWriterClass::Open_Chunk | DISCOVERED-GHIDRA | | +| `0x21FEB0` | ChunkWriterClass::Close_Chunk | DISCOVERED-GHIDRA | | +| `0x21FFA0` | ChunkWriterClass::Open_Micro_Chunk | DISCOVERED-GHIDRA | | +| `0x220030` | ChunkWriterClass::Close_Micro_Chunk | DISCOVERED-GHIDRA | | +| `0x2200B0` | ChunkWriterClass::Write | DISCOVERED-GHIDRA | | +| `0x220140` | ChunkWriterClass::Write_CString | DISCOVERED-GHIDRA | strlen+1 bytes | +| `0x220280` | ChunkReaderClass::ctor | DISCOVERED-GHIDRA | | +| `0x220370` | ChunkReaderClass::dtor | DISCOVERED-GHIDRA | | +| `0x2203F0` | ChunkReaderClass::Get_File_Size | DISCOVERED-GHIDRA | | +| `0x220460` | ChunkReaderClass::Tell | DISCOVERED-GHIDRA | | +| `0x2204A0` | ChunkReaderClass::Open_Chunk | DISCOVERED-GHIDRA | | +| `0x220520` | ChunkReaderClass::Close_Chunk | DISCOVERED-GHIDRA | | +| `0x2227E0` | RAMFileClass::ctor1 | DISCOVERED-GHIDRA | | +| `0x222830` | RAMFileClass::ctor2 | DISCOVERED-GHIDRA | | +| `0x222880` | RAMFileClass::ctor3 | DISCOVERED-GHIDRA | | +| `0x2228D0` | RAMFileClass::dtor | DISCOVERED-GHIDRA | | +| `0x056360` | SaveGameStruct_Vector::ctor | DISCOVERED-GHIDRA | | +| `0x0581C0` | SaveGameStruct_Vector::dtor | DISCOVERED-GHIDRA | | +| `0x48FA80` | SaveGameEventClass::ctor | DISCOVERED-GHIDRA | Event ID=36 | +| `0x48FAD0` | SaveGameEventClass::dtor | DISCOVERED-GHIDRA | | +| `0x48FB20` | SaveGameEventClass::Deserialize | DISCOVERED-GHIDRA | | +| `0x48FB90` | SaveGameEventClass::Serialize | DISCOVERED-GHIDRA | | +| `0x48FC00` | SaveGameEventClass::Execute | DISCOVERED-GHIDRA | | +| `0x4B5FB0` | PlanetaryDataPackClass::dtor | DISCOVERED-GHIDRA | | +| `0x4F2D80` | tPersistentUnit_Vector::ctor | DISCOVERED-GHIDRA | | +| `0x4F2FF0` | tPersistentUnit_Vector::iterate | DISCOVERED-GHIDRA | | + +### Compression (zlib, statically linked) + +| RVA | Function | Status | +|-----|----------|--------| +| `0x7A1470` | compress2 | DISCOVERED-GHIDRA | +| `0x7A1590` | uncompress2 | DISCOVERED-GHIDRA | +| `0x7A3080` | deflateInit_ | DISCOVERED-GHIDRA | +| `0x7A2DF0` | deflateInit2_ | DISCOVERED-GHIDRA | +| `0x7A54F0` | inflateInit_ | DISCOVERED-GHIDRA | +| `0x7A60A0` | compress_block | DISCOVERED-GHIDRA | + +--- + +## 11. Network System Functions + +| RVA | Function | Status | Verification | +|-----|----------|--------|-------------| +| `0x6A000` | SteamClass::ctor | DISCOVERED-GHIDRA | Singleton | +| `0x6B370` | SteamAsyncSocketImpl::ctor | DISCOVERED-GHIDRA | Steam P2P | +| `0x6CA10` | SteamPeerLobbyClass::ctor | DISCOVERED-GHIDRA | 6 callbacks + 3 call results | +| `0x72540` | Steam_LobbyInvite_Handler | DISCOVERED-GHIDRA | Callback ID 503 | +| `0x72550` | Steam_LobbyKicked_Handler | DISCOVERED-GHIDRA | Callback ID 512 | +| `0x72570` | Steam_LobbyChatMsg_Handler | DISCOVERED-GHIDRA | Callback ID 507 | +| `0x726C0` | Steam_LobbyDataUpdate_Handler | DISCOVERED-GHIDRA | Callback ID 505 | +| `0x726D0` | Steam_LobbyChatUpdate_Handler | DISCOVERED-GHIDRA | Callback ID 506 | +| `0x72730` | Steam_ItemInstalled_Handler | DISCOVERED-GHIDRA | Callback ID 3405 | +| `0x2054C0` | PacketHandlerClass::dtor | DISCOVERED-GHIDRA | Thread-based | +| `0x227110` | WinsockAsyncSocketImpl::ctor | DISCOVERED-GHIDRA | LAN/direct-IP | +| `0x23BC40` | PacketClass::ctor | DISCOVERED-GHIDRA | BitStreamClass-based | +| `0x5126C0` | EventClass::ctor | DISCOVERED-GHIDRA | Base event | +| `0x512C50` | ScheduledEventClass::ctor | DISCOVERED-GHIDRA | Frame-scheduled | +| `0x4B39E0` | EventQueueClass::ctor | DISCOVERED-GHIDRA | | +| `0x5960F0` | BaseEventFactoryClass::ctor | DISCOVERED-GHIDRA | Type registry | + +### Network Event Constructors (58 Events) + +| RVA | Event Class | ID | Status | +|-----|-----------|-----|--------| +| `0x4C1B00` | FrameInfoEventClass | 0 | DISCOVERED-GHIDRA | +| `0x3ACEE0` | MoveToPositionEventClass | 1 | DISCOVERED-GHIDRA | +| `0x3AEE50` | MoveToObjectEventClass | 2 | DISCOVERED-GHIDRA | +| `0x3AF290` | MoveObjectToObjectEventClass | 3 | DISCOVERED-GHIDRA | +| `0x689670` | LookEventClass | 4 | CONFIRMED-RE | +| `0x3AC9D0` | SelectEventClass | 5 | CONFIRMED-RE | +| `0x3AF4C0` | AttackEventClass | 6 | DISCOVERED-GHIDRA | +| `0x523F50` | ProductionEventClass | 7 | DISCOVERED-GHIDRA | +| `0x5AF020` | FleetManagementEventClass | 8 | DISCOVERED-GHIDRA | +| `0x48FCE0` | InvadeEventClass | 9 | DISCOVERED-GHIDRA | +| `0x3C41C0` | MoveThroughObjectsEventClass | 10 | DISCOVERED-GHIDRA | +| `0x4AD0F0` | DebugEventClass | 11 | DISCOVERED-GHIDRA | +| `0x403AB0` | ReinforceEventClass | 12 | DISCOVERED-GHIDRA | +| `0x4B4210` | SpecialAbilityEventClass | 13 | DISCOVERED-GHIDRA | +| `0x3AE4A0` | MoveToRayEventClass | 14 | DISCOVERED-GHIDRA | +| `0x4993F0` | QuitGameEventClass | 15 | DISCOVERED-GHIDRA | +| `0x44D1D0` | ChatEventClass | 16 | DISCOVERED-GHIDRA | +| `0x4C1D30` | FrameSyncEventClass | 17 | DISCOVERED-GHIDRA | +| `0x4C1EB0` | PerformanceMetricsEventClass | 18 | DISCOVERED-GHIDRA | +| `0x3AE950` | MoveToRayFacingEventClass | 19 | DISCOVERED-GHIDRA | +| `0x409B40` | FacingEventClass | 20 | DISCOVERED-GHIDRA | +| `0x3B0010` | EscortEventClass | 21 | DISCOVERED-GHIDRA | +| `0x6899C0` | CinematicAnimationEventClass | 26 | DISCOVERED-GHIDRA | +| `0x524560` | BombingRunEventClass | 28 | DISCOVERED-GHIDRA | +| `0x436060` | ControlGroupEventClass | 30 | CONFIRMED-RE | +| `0x5AEA90` | SetupPhaseMoveEventClass | 31 | DISCOVERED-GHIDRA | +| `0x5AEF30` | SetupPhaseTriggerEndEventClass | 32 | DISCOVERED-GHIDRA | +| `0x5D7740` | TacticalBuildEventClass | 33 | DISCOVERED-GHIDRA | +| `0x689810` | TacticalSellEventClass | 34 | DISCOVERED-GHIDRA | +| `0x689AD0` | AllyEventClass | 35 | DISCOVERED-GHIDRA | +| `0x48FA80` | SaveGameEventClass | 36 | DISCOVERED-GHIDRA | +| `0x437F40` | SelectAllEventClass | 37 | CONFIRMED-RE | +| `0x44D530` | GameOptionsEventClass | 39 | DISCOVERED-GHIDRA | +| `0x4292E0` | TacticalSpecialAbilityEventClass | 40 | DISCOVERED-GHIDRA | +| `0x498CA0` | DistributeMoneyEventClass | 42 | DISCOVERED-GHIDRA | +| `0x532690` | GalacticSellEventClass | 46 | DISCOVERED-GHIDRA | +| `0x3ADD90` | MoveToPositionFacingEventClass | 47 | DISCOVERED-GHIDRA | +| `0x4ADED0` | WithdrawlEventClass | 49 | DISCOVERED-GHIDRA | +| `0x4D6A60` | TauntEventClass | 50 | DISCOVERED-GHIDRA | +| `0x4ADFD0` | ResumeGameEventClass | 51 | DISCOVERED-GHIDRA | +| `0x440AD0` | GarrisonEventClass | 52 | DISCOVERED-GHIDRA | +| `0x52AFC0` | PlanetaryBombardEventClass | 54 | DISCOVERED-GHIDRA | +| `0x3AD3B0` | MoveToGarrisonEventClass | 55 | DISCOVERED-GHIDRA | + +--- + +## 12. Game Mode Functions + +| RVA | Function | Status | +|-----|----------|--------| +| `0x35A5E0` | GameModeClass::ctor | DISCOVERED-GHIDRA | +| `0x35AD70` | GameModeClass::dtor | DISCOVERED-GHIDRA | +| `0x3B5210` | LandModeClass::ctor | DISCOVERED-GHIDRA | +| `0x4B1270` | GalacticModeClass::ctor | DISCOVERED-GHIDRA | +| `0x4D6BA0` | SpaceModeClass::ctor | DISCOVERED-GHIDRA | + +--- + +## 13. Signal System Functions + +| RVA | Function | Status | +|-----|----------|--------| +| `0x220ED0` | Signal_Fire | DISCOVERED-GHIDRA | +| `0x240610` | SignalGeneratorClass::ctor | DISCOVERED-GHIDRA | +| `0x2406C0` | SignalListClass::ctor | DISCOVERED-GHIDRA | +| `0x2215B0` | SignalDispatcherClass::ctor1 | DISCOVERED-GHIDRA | +| `0x2218C0` | SignalDispatcherClass::ctor2 | DISCOVERED-GHIDRA | + +--- + +## 14. Miscellaneous / Utility Functions + +| RVA | Function | Status | Source | +|-----|----------|--------|--------| +| `0x245790` | LuaScriptClass::Get_Script_From_State | CONFIRMED-FOCAPI | | +| `0x247700` | LuaScriptClass::Map_Var_To_Lua | CONFIRMED-FOCAPI | | +| `0x24BE40` | LuaUserVar::Register_Member | CONFIRMED-FOCAPI | | +| `0x256D40` | LuaUserVar::Return_Variable | CONFIRMED-FOCAPI | | +| `0x1FA680` | GameTextClass::Get | CONFIRMED-FOCAPI | | +| `0x604A10` | GameObjectTypeWrapper::ctor | CONFIRMED-FOCAPI | | +| `0x025760` | Log_Printf | DISCOVERED-GHIDRA | Engine logging | +| `0x17D070` | Debug_Should_Issue_Event_Alert | DISCOVERED-GHIDRA | | +| `0x29F810` | General_Object_Spawn | DISCOVERED-GHIDRA | | +| `0x265AE0` | Destroy_Visual_Representation | DISCOVERED-GHIDRA | | +| `0x264A40` | QueryAnimationCount | DISCOVERED-GHIDRA | | +| `0x1FFB40` | RandomSelection_WithinCount | DISCOVERED-GHIDRA | | +| `0x4D07E0` | SpawnWreckage | DISCOVERED-GHIDRA | Death debris | +| `0x33FB70` | ComputeAbilityBonus | DISCOVERED-GHIDRA | | +| `0x38C850` | CombatBonus_ApplyStats | DISCOVERED-GHIDRA | 8 combat stats | +| `0x55A010` | AbilityModifier_Accumulate | DISCOVERED-GHIDRA | Tree-based accumulator | + +--- + +## 15. Vtable Addresses + +| RVA | Class | Status | +|-----|-------|--------| +| `0x8661B8` | GameObjectClass (primary) | CONFIRMED-RUNTIME | +| `0x8661D8` | MultiLinkedListMember (in GOC) | CONFIRMED-RE | +| `0x866200` | CullObjectClass (in GOC) | CONFIRMED-RE | +| `0x866210` | SignalGeneratorClass (in GOC) | CONFIRMED-RE | +| `0x866228` | Unknown base 5 (in GOC) | CONFIRMED-RE | +| `0x8762C0` | GalacticModeClass | DISCOVERED-GHIDRA | +| `0x878D58` | ProjectileBehaviorClass | DISCOVERED-GHIDRA | +| `0x899458` | ShieldBehaviorClass (rear) | DISCOVERED-GHIDRA | +| `0x8BF6C0` | BaseCombatantClass | DISCOVERED-GHIDRA | + +--- + +## 16. Global Data Addresses + +| RVA | Name | Type | Status | +|-----|------|------|--------| +| `0xA12550` | screen_aspect_ratio | float32 | CONFIRMED-RE | +| `0xA14D20` | CRC32_lookup_table | uint32[256] | CONFIRMED-RE | +| `0xA15738` | script_id_counter | int32 | DISCOVERED-GHIDRA | +| `0xA157C0` | NumberTypeMarker | marker | DISCOVERED-GHIDRA | +| `0xA157D0` | StringTypeMarker | marker | DISCOVERED-GHIDRA | +| `0xA16FB0` | PlayerCount_static | uint32 | DISCOVERED-GHIDRA | +| `0xA16FD0` | PlayerListClass* | pointer | CONFIRMED-FOCAPI | +| `0xA16FD8` | PlayerListClass_end | pointer | DISCOVERED-GHIDRA | +| `0xA16FF0` | PlayerArray | pointer | CONFIRMED-RUNTIME | +| `0xA16FF8` | PlayerCount | int32 | CONFIRMED-RUNTIME | +| `0xA172D0` | GameObjectTypeList* | pointer | DISCOVERED-GHIDRA | +| `0xA284C4` | FOW_GlobalToggle | bool | DISCOVERED-GHIDRA | +| `0xA43B18` | GameObjectTypeMarker | marker | DISCOVERED-GHIDRA | +| `0xA44270` | PlayerTypeMarker | marker | DISCOVERED-GHIDRA | +| `0xA573D0` | FOW_data | pointer | CONFIRMED-AOB | +| `0xA7BC58` | TheGameText | pointer | CONFIRMED-FOCAPI | +| `0xB0A320` | game_speed_numerator | int/float | DISCOVERED-GHIDRA | +| `0xB0A340` | game_speed_denominator | int/float | DISCOVERED-GHIDRA | +| `0xB153E0` | GameModeManagerClass* | pointer | CONFIRMED-RE | +| `0xB15418` | GameModeManager_active_mode | pointer | CONFIRMED-RE | +| `0xB15920` | build_time_global_scalar | float | DISCOVERED-GHIDRA | +| `0xB1599C` | galactic_camera_zoom_angle | float32 | CONFIRMED-RE | +| `0xB159A0` | galactic_camera_target_zoom | float32 | CONFIRMED-RE | +| `0xB159A4` | galactic_camera_distance | float32 | CONFIRMED-RE | +| `0xB159A8` | galactic_camera_distance_scale | float32 | CONFIRMED-RE | +| `0xB159AC` | galactic_camera_look_dir_x | float32 | CONFIRMED-RE | +| `0xB159B0` | galactic_camera_look_dir_y | float32 | CONFIRMED-RE | +| `0xB159B4` | galactic_camera_look_dir_z | float32 | CONFIRMED-RE | +| `0xB159C8` | galactic_camera_mode_value | float32 | CONFIRMED-RE | +| `0xB16DC8` | hard_hp_multiplier | float | DISCOVERED-GHIDRA | +| `0xB16DCC` | easy_hp_multiplier | float | DISCOVERED-GHIDRA | +| `0xB169F0` | Default_Hero_Respawn_Time | float | CONFIRMED-RE | +| `0xB27F60` | TheCommandBar | pointer | CONFIRMED-FOCAPI | +| `0xB30728` | EventTypeTree (story/reward) | pointer | CONFIRMED-RE | +| `0xB30738` | XML_NestingDepthCounter | int | DISCOVERED-GHIDRA | +| `0xB313D8` | SaveGame_dispatch_fptr | pointer | DISCOVERED-GHIDRA | +| `0xB313E0` | SaveGameEventFactory_singleton | pointer | DISCOVERED-GHIDRA | +| `0xB36BC1` | EventFactory_registry | DVec | DISCOVERED-GHIDRA | +| `0xB3B450` | RewardParam_static_buffer | pointer | DISCOVERED-GHIDRA | +| `0xB3B458` | RewardParam_buffer_end | pointer | DISCOVERED-GHIDRA | +| `0xB3B468` | Dialog_init_guard | int | DISCOVERED-GHIDRA | +| `0x803514` | damage_threshold_1 | float | DISCOVERED-GHIDRA | +| `0x8007C0` | damage_threshold_2 | float | DISCOVERED-GHIDRA | +| `0xA13DB0` | ObjectPointerPairClass_Vector | DVec | DISCOVERED-GHIDRA | + +--- + +## 17. From Community Sources (Not Independently Verified) + +### Apocalypticx CE Tables + +| RVA | Description | Status | +|-----|-------------|--------| +| `0x451974` | Fog of war visibility check | CONFIRMED-AOB | +| `0x333E73` | Build time/cost (Skirmish) | CONFIRMED-AOB | +| `0x3A1B4C` | Build time/cost (Conquest) | CONFIRMED-AOB | +| `0x28DF6F` | Unit cap calculation (GC) | CONFIRMED-AOB | + +--- + +## Summary Statistics + +| Category | Count | +|----------|-------| +| CONFIRMED-RUNTIME | 18 | +| CONFIRMED-RE | 68 | +| CONFIRMED-FOCAPI | 12 | +| CONFIRMED-AOB | 9 | +| CONFIRMED-RTTI | 1703 classes | +| DISCOVERED-GHIDRA | 190+ | +| ESTIMATED | 3 | +| **Total unique RVAs** | **280+** | + +--- + +*End of Verified RVAs v3.0* +*Generated by Agent 5A -- Master Knowledge Base Consolidation* +*All RVAs relative to module base. Add 0x140000000 for Ghidra absolute addresses.* + diff --git a/docs/editor/structs/cameraclass.md b/docs/editor/structs/cameraclass.md new file mode 100644 index 000000000..c2bcef51c --- /dev/null +++ b/docs/editor/structs/cameraclass.md @@ -0,0 +1,37 @@ +# CameraClass + + +## Fields + +| Offset | Type | Name | Status / Confidence | Notes | +|--------|------|------|---------------------|-------| +| `outer` | `?` | ? | | | +| `sub_object` | `?` | ? | | | + +### outer + +| Offset | Type | Name | Notes | +|--------|------|------|-------| +| `0x00` | `pointer` | vtable_ptr | | +| `0x04` | `int32` | ref_count | | +| `0x10` | `float32[12]` | orientation_matrix_copy | | +| `0x40` | `pointer` | camera_state_ptr | | + +### sub_object + +| Offset | Type | Name | Notes | +|--------|------|------|-------| +| `0x00` | `float32[12]` | orientation_matrix | | +| `0x0C` | `float32` | position_x | | +| `0x1C` | `float32` | position_y | | +| `0x2C` | `float32` | position_z | | +| `0x30` | `float32` | near_clip | 1.0 | +| `0x34` | `float32` | far_clip | 1000.0 | +| `0x44` | `int32` | projection_mode | | +| `0x48` | `float32` | fov_radians | 0.785 | +| `0x4C` | `float32` | aspect_ratio | 1.333 | +| `0x74` | `float32` | viewport_x | | +| `0x78` | `float32` | viewport_y | | +| `0x7C` | `float32` | viewport_width | | +| `0x80` | `float32` | viewport_height | | +| `0xB4` | `float32[16]` | projection_matrix | | diff --git a/docs/editor/structs/fowplayerobject.md b/docs/editor/structs/fowplayerobject.md new file mode 100644 index 000000000..53c06817d --- /dev/null +++ b/docs/editor/structs/fowplayerobject.md @@ -0,0 +1,15 @@ +# FOWPlayerObject + + +Per-player fog of war state, stored in GameModeClass+0x198 array + + +## Fields + +| Offset | Type | Name | Status / Confidence | Notes | +|--------|------|------|---------------------|-------| +| `0x00` | `pointer` | visibility_grid | | byte per cell: 0x00=fogged, 0xFF=visible | +| `0x08` | `pointer` | reveal_timer_grid | | short per cell | +| `0x10` | `pointer` | output_visibility_grid | | | +| `0x20` | `int64` | grid_cell_count | | | +| `0x50` | `uint8` | dirty_flag | | | diff --git a/docs/editor/structs/gameobjectclass.md b/docs/editor/structs/gameobjectclass.md new file mode 100644 index 000000000..1c6cb6b48 --- /dev/null +++ b/docs/editor/structs/gameobjectclass.md @@ -0,0 +1,159 @@ +# GameObjectClass + +**RTTI**: `.?AVGameObjectClass@@` + +**VTable RVA**: `0x8661B8` + +**Inherits**: `RootClass`, `MultiLinkedListMember`, `PooledObjectClass`, `CullObjectClass`, `SignalGeneratorClass` + +**Size**: 0x3C8 bytes + + +## Fields + +| Offset | Type | Name | Status / Confidence | Notes | +|--------|------|------|---------------------|-------| +| `0x00` | `pointer` | vtable_ptr | high | Pointer to the primary virtual function table. RVA 0x8661B8 identifies GameObjectClass. Written first in both constructors. | +| `0x08` | `pointer` | vtable_MultiLinkedListMember | high | Secondary vtable for MultiLinkedListMember base class. | +| `0x10` | `uint64` | linked_list_next | medium | MultiLinkedListMember next pointer. Initialized to 0 in constructor. | +| `0x18` | `pointer` | vtable_CullObjectClass | high | Secondary vtable for CullObjectClass base class. | +| `0x20` | `pointer` | vtable_SignalGeneratorClass | high | Secondary vtable for SignalGeneratorClass base class. | +| `0x28` | `undefined` | signal_generator_data | medium | SignalGeneratorClass internal data. Initialized by SignalGeneratorClass::SignalGeneratorClass(). | +| `0x30` | `uint64` | signal_data_2 | medium | Additional signal/cull object data. Initialized to 0. | +| `0x38` | `pointer` | vtable_5 | high | Fifth vtable pointer (for unknown base class at offset 56 in RTTI). Present in additional_vtables array. | +| `0x40` | `uint32` | visibility_mask | high | Cull/visibility bitmask. Initialized to 0x3FFFFF (all bits set). Used by the rendering/culling system to determine which cameras/viewports can see this object. | +| `0x44` | `uint32` | visibility_mask_2 | high | Second visibility mask. Also initialized to 0x3FFFFF. | +| `0x48` | `int32` | unique_session_id | high | Session-unique allocation ID. Initialized to -1 (0xFFFFFFFF) in default constructor; set to param_3 in the full constructor. Used by FUN_1402AC980() for session tracking. | +| `0x4C` | `int32` | session_id_high | medium | Upper 32 bits / secondary ID. Initialized to 0. | +| `0x50` | `pointer` | signal_listener_list | high | Linked list of signal listeners. Initialized to NULL. Iterated in FUN_1403AC530 (transform update) to notify observers. | +| `0x58` | `int32` | owner_player_id | high | Index into the global PlayerListClass array identifying the owning player. SESSION-DEPENDENT. For sub-objects, use QueryInterface(3) to resolve the true owner via parent chain. Read by SetHP, Change_Owner, ScheduleHeroRespawn, and many others. | +| `0x5C` | `float32` | hp | high | Current hitpoints. Written exclusively by SetHP (RVA 0x3A89D0). Clamped to [0.0, max_hp]. When HP reaches 0.0, the engine logs a death event. All known callers funnel through SetHP. Also set during spawn init in FUN_1403989A0 via GetMaxHealth. | +| `0x60` | `uint64` | unknown_0x60 | medium | Initialized to 0 in constructor. Referenced in Change_Owner via *(param_1 + 0x60) as the GameObjectClass pointer of the wrapper's underlying object. | +| `0x68` | `float32` | spawn_position_x | high | Initial spawn X coordinate. Set from param_4 in the full constructor. Used as the base position during object initialization. | +| `0x6C` | `float32` | spawn_position_y | high | Initial spawn Y coordinate (vertical/height). Set from param_4[1] in the full constructor. | +| `0x70` | `float32` | spawn_position_z | high | Initial spawn Z coordinate (depth). Set from param_4[2] in the full constructor. May have height offset (DFC) added from [GameObjectType](../structs/gameobjecttype.md). | +| `0x74` | `float32` | facing_x | high | Initial facing direction X component. Set from param_5[0] in the full constructor. | +| `0x78` | `float32` | current_position_x | high | Current world position X. Written by the Make_Invulnerable/Teleport path. Read by FUN_1403A8710 alongside +0x7C and +0x80. Also set from spawn_position in constructor. | +| `0x7C` | `float32` | current_position_y | high | Current world position Y (height). Written by Make_Invulnerable/Teleport path. The 'y' in the engine coordinate system. | +| `0x80` | `float32` | current_position_z | high | Current world position Z (depth). Written by Make_Invulnerable/Teleport path. | +| `0x84` | `float32` | render_position_x | high | Rendering/interpolated position X. Updated by FUN_1403A8710 which compares with param and sets dirty flag at +0x3A1. Distinct from current_position for interpolation. | +| `0x88` | `float32` | render_position_y | high | Rendering/interpolated position Y. | +| `0x8C` | `float32` | render_position_z | high | Rendering/interpolated position Z. Also used as the facing/rotation Z in FUN_1403AC530 transform update. | +| `0x90` | `float32` | previous_position_x | high | Previous frame position X. Updated by FUN_1403A8710 alongside render_position. | +| `0x94` | `float32` | previous_position_y | high | Previous frame position Y. Used by FUN_1403AC530 for rotation calculation. | +| `0x98` | `float32` | previous_position_z | high | Previous frame position Z. | +| `0xA8` | `pointer` | locomotor_component_ptr | high | Pointer to the locomotor/movement behavior component. When non-null, contains speed override fields: flag at locomotor+0x29C (byte), value at locomotor+0x2A0 (float32). SetSpeedOverride writes both; ClearSpeedOverride zeros both. | +| `0xB0` | `pointer` | component_slot_0xB0 | medium | Component pointer slot. Part of the component pointer block from 0xA8 to 0x240. Cleared during reset in FUN_1403989A0. | +| `0xB8` | `pointer` | parent_container_component_ptr | high | Pointer to parent container component. FUN_1403956C0 returns (obj+0xB8)+0x68 as the player/faction identity when the object has a parent. Used for resolving ownership of sub-objects. Also read in FUN_140395920 for sub-object owner resolution. | +| `0xC0` | `pointer` | component_slot_0xC0 | medium | Component pointer slot. Part of the component pointer block. Cleared during reset. | +| `0xC8` | `pointer` | component_slot_0xC8 | medium | Component pointer slot. | +| `0xD0` | `pointer` | component_slot_0xD0 | medium | Component pointer slot. | +| `0xD8` | `pointer` | targeting_data_ptr | medium | Pointer to targeting/AI data structure. FUN_140395920 reads *(obj+0xD8)+0x18 as a linked list for target priority evaluation. | +| `0xE0` | `pointer` | component_slot_0xE0 | medium | Component pointer slot. | +| `0xE8` | `pointer` | component_slot_0xE8 | medium | Component pointer slot. | +| `0xF0` | `pointer` | buff_modifier_component_ptr | medium | Pointer to combat buff/modifier component. FUN_140395C70 reads *(obj+0xF0)+0x138 for buff data. Used in health modifier calculations. | +| `0xF8` | `pointer` | game_object_type_wrapper_ptr | medium | Pointer to the [GameObjectType](../structs/gameobjecttype.md) struct wrapper for this instance. Contains the type definition, refcounted. Written from param_2 in the full constructor. | +| `0x100` | `pointer` | combatant_behavior_ptr | high | Pointer to CombatantBehaviorClass. Allocated (size 0x3B8) and initialized in FUN_1403989A0 when FUN_140374DA0 returns true. Contains combat-related state including weapon data at +0x2CC and nested structures. | +| `0x108` | `pointer` | component_slot_0x108 | medium | Component pointer slot. | +| `0x110` | `pointer` | component_slot_0x110 | medium | Component pointer slot. | +| `0x118` | `pointer` | health_sub_object_ptr | high | Pointer to a health-related sub-object. Used by the Take_Damage property dispatch path. Contains additional health state fields at deep offsets. | +| `0x120` | `pointer` | component_slot_0x120 | medium | Component pointer slot. | +| `0x128` | `pointer` | component_slot_0x128 | medium | Component pointer slot. | +| `0x130` | `pointer` | component_slot_0x130 | medium | Component pointer slot. | +| `0x138` | `pointer` | component_slot_0x138 | medium | Component pointer slot. | +| `0x140` | `pointer` | component_slot_0x140 | medium | Component pointer slot. | +| `0x148` | `pointer` | component_slot_0x148 | medium | Component pointer slot. | +| `0x150` | `pointer` | component_slot_0x150 | medium | Component pointer slot. | +| `0x158` | `pointer` | component_slot_0x158 | medium | Component pointer slot. | +| `0x160` | `pointer` | component_slot_0x160 | medium | Component pointer slot. Explicitly set to NULL in constructor. | +| `0x168` | `pointer` | component_slot_0x168 | medium | Component pointer slot. Cleared during reset. | +| `0x170` | `pointer` | component_slot_0x170 | medium | Component pointer slot. | +| `0x178` | `pointer` | component_slot_0x178 | medium | Component pointer slot. | +| `0x180` | `pointer` | component_slot_0x180 | medium | Component pointer slot. | +| `0x188` | `pointer` | component_slot_0x188 | medium | Component pointer slot. | +| `0x190` | `pointer` | component_slot_0x190 | medium | Component pointer slot. | +| `0x198` | `pointer` | component_slot_0x198 | medium | Component pointer slot. | +| `0x1A0` | `pointer` | component_slot_0x1A0 | medium | Component pointer slot. | +| `0x1A8` | `pointer` | component_slot_0x1A8 | medium | Component pointer slot. | +| `0x1B0` | `pointer` | component_slot_0x1B0 | medium | Component pointer slot. | +| `0x1B8` | `pointer` | component_slot_0x1B8 | medium | Component pointer slot. | +| `0x1C0` | `pointer` | component_slot_0x1C0 | medium | Component pointer slot. | +| `0x1C8` | `pointer` | component_slot_0x1C8 | medium | Component pointer slot. | +| `0x1D0` | `pointer` | component_slot_0x1D0 | medium | Component pointer slot. | +| `0x1D8` | `pointer` | component_slot_0x1D8 | medium | Component pointer slot. | +| `0x1E0` | `pointer` | component_slot_0x1E0 | medium | Component pointer slot. | +| `0x1E8` | `pointer` | component_slot_0x1E8 | medium | Component pointer slot. | +| `0x1F0` | `pointer` | component_slot_0x1F0 | medium | Component pointer slot. | +| `0x1F8` | `pointer` | component_slot_0x1F8 | medium | Component pointer slot. | +| `0x200` | `pointer` | radar_map_data_ptr | high | Pointer to RadarMapDataPackClass (size 0x20). Allocated in FUN_1403989A0 if NULL. Used for minimap representation of this object. | +| `0x208` | `uint64` | unknown_0x208 | low | Unknown. Cleared during init. | +| `0x210` | `uint64` | unknown_0x210 | low | Unknown. Cleared during init. | +| `0x218` | `pointer` | component_slot_0x218 | medium | Component pointer slot. | +| `0x220` | `pointer` | component_slot_0x220 | medium | Component pointer slot. | +| `0x228` | `pointer` | component_slot_0x228 | medium | Component pointer slot. | +| `0x230` | `pointer` | component_slot_0x230 | medium | Component pointer slot. Explicitly set to NULL. | +| `0x240` | `pointer` | component_slot_0x240 | medium | Component pointer slot. Referenced in FUN_1403A5840 for map node cleanup. Used in FUN_1403A4820 for hero/garrison logic -- contains sub-structure with +0x2A0 map manager ref and +0x328 counter. | +| `0x248` | `float32[3]` | orientation_matrix_row0 | high | First row of 3x3 orientation/rotation matrix. Initialized to identity matrix (1,0,0,0,1,0,0,0,1) in constructor. The RootClass_data offsets 0x1A8-0x1D0 map to raw struct offsets 0x248-0x270. | +| `0x254` | `float32[3]` | orientation_matrix_row1 | high | Second row of orientation matrix. Identity: (0, 1, 0). | +| `0x260` | `float32[3]` | orientation_matrix_row2 | high | Third row of orientation matrix. Identity: (0, 0, 1). | +| `0x268` | `uint8` | death_check_flag | medium | Flag related to death state processing. Mapped from constructor's RootClass_data offset_0x1E8. | +| `0x269` | `uint8` | spawn_initialized | medium | Set to 1 during construction. Indicates the object has been initialized post-spawn. | +| `0x278` | `pointer` | component_array_ptr | high | Pointer to an array of component (behavior) pointers. Each element is 8 bytes. Indexed by values in the component_lookup_table at +0x332. Used by QueryInterface: result = components[obj[0x332 + query_type]]. Components are added via FUN_14038CB30. | +| `0x288` | `int8` | component_count | high | Current number of components in the component_array. Incremented in FUN_14038CB30 when a new component is added. | +| `0x289` | `int8` | component_capacity | high | Capacity of the component_array. When component_count >= component_capacity, the array is grown via FUN_1403A59F0. | +| `0x290` | `int32` | priority_component_count | high | Count of priority/front-inserted components. Incremented in FUN_14038CB30 when a component is inserted at index 0 (priority behavior). Reset to 0 in FUN_1403989A0. | +| `0x298` | `pointer` | game_object_type_ptr | high | Pointer to the GameObjectTypeClass struct that defines this object's type (unit definition). The type name string is at [GameObjectType+0xF8] as an MSVC SSO string. Max health is obtained via FUN_1403727A0 which reads [GameObjectType](../structs/gameobjecttype.md)+0xDCC. Used extensively: SetHP, spawn init, hero respawn, combat, etc. | +| `0x2A0` | `pointer` | scene_node_ptr | high | Pointer to the 3D scene/render node for this object. Contains the visual representation. Has sub-fields: +0x88 (animation state), +0xA4/+0xA8/+0xAC (render position XYZ), +0xB0/+0xB4/+0xB8 (render position copy). Checked extensively -- when non-null, the object has a visible representation. Cleared in FUN_1403989A0. | +| `0x2A8` | `pointer` | ai_label_display_ptr | high | Pointer to the AI label/name display object. Allocated and managed in FUN_1403A4510. Contains a DrawTextClass used for debug/AI overhead labels. Cleared in FUN_1403989A0. | +| `0x2B0` | `pointer` | container_ref | high | Reference to the parent container object. Used by the invulnerability system and FUN_1403963F0 which walks the container chain: while (obj+0x335 != -1) { obj = *(obj+0x2B0) }. When non-null, the object is contained within another object. Cleared in FUN_1403989A0. | +| `0x2B8` | `pointer` | game_session_context_ptr | high | Pointer to the game session/world context. Dereferenced as *(obj+0x2B8)+0x20 to reach the game mode manager which provides vtable calls for GetGameMode, GetPlayerManager, etc. Used by FUN_1403989A0, FUN_14039BCB0, FUN_1403A4510, FUN_1403A4820. | +| `0x2D0` | `pointer` | hardpoint_array_ptr | high | Pointer to DynamicVectorClass. Allocated (size 0x18) and populated in FUN_1403989A0 when combatant_behavior_ptr exists. Each hardpoint is size 0xD8, created by FUN_140381A90. Contains weapon/turret sub-objects. | +| `0x2E0` | `pointer` | map_garrison_node_ptr | medium | Pointer to map/garrison node. Referenced in FUN_1403A5840 for cleanup. When non-null and *(+0x2E0)+0x2A0 is valid, indicates the object is garrisoned on a map structure. Contains a counter at +0x328. | +| `0x2F8` | `pointer` | garrison_map_ref_2 | medium | Secondary garrison/map reference. Cleared alongside map_garrison_node_ptr in FUN_1403A5840. | +| `0x300` | `uint8` | object_flags_0x300 | high | Object state flags byte. Initialized from global DAT_140A28648 in constructor. Bit 6 (0x40) and bit 7 (0x80) are set when param_7 == 1 in the full constructor (0xC0 mask). Checked in FUN_1403A4820 as (obj+0x3A1) bit 3. | +| `0x301` | `uint8` | object_flags_0x301 | medium | Initialized from global DAT_140B2C379. | +| `0x302` | `uint8` | object_flags_0x302 | medium | Initialized from global DAT_140A28649. | +| `0x303` | `uint8` | object_flags_0x303 | medium | Initialized from global DAT_140B2C37A. | +| `0x304` | `uint8` | object_flags_0x304 | medium | Initialized from global DAT_140A2864A. | +| `0x305` | `uint8` | active_flag | medium | Initialized to 1 in constructor. Likely indicates the object is active/alive. | +| `0x307` | `uint8` | state_flag_0x307 | low | Initialized to 0. | +| `0x308` | `uint32` | state_0x308 | low | Initialized to 0. | +| `0x30C` | `uint8` | state_0x30C | low | Initialized to 0xFF. | +| `0x310` | `int32` | state_0x310 | low | Initialized to -1 (0xFFFFFFFF). | +| `0x314` | `uint64` | state_0x314 | low | Initialized to 0. | +| `0x318` | `uint64` | state_0x318 | low | Initialized to 0. | +| `0x320` | `uint64` | state_0x320 | low | Initialized to 0. | +| `0x328` | `uint32` | state_0x328 | low | Initialized to 1. | +| `0x32C` | `uint32` | state_0x32C | low | Initialized to 0. | +| `0x330` | `uint8` | hero_respawn_slot | medium | Used in ScheduleHeroRespawn as the last parameter to FUN_1403FE0A0. Read as (char)(obj+0x330). Likely an index into a respawn slot table. | +| `0x332` | `uint8[0x6A]` | component_lookup_table | high | Byte array indexed by component query type ID (0 through 0x69). Each byte is either an index into the component_array (at +0x278) or 0xFF meaning 'no component of this type'. Used by QueryInterface: result = components[obj[0x332 + query_type]]. Known query types: 0=self, 1=behavior, 3=parent/container, 4=land_locomotor, 5=space_locomotor, 0x16=hardpoint_mgr, 0x19=ability, 0x3D=transform, 0x46=property_handler. Initialized from global template at 0x140866880. Range is 0x332 to 0x39B inclusive (106 bytes). | +| `0x333` | `uint8` | behavior_component_index | high | Shorthand for component_lookup_table[1]. Index into component array for the behavior manager. When != -1, the object has an active behavior system. Checked in FUN_14039BCB0. | +| `0x335` | `uint8` | parent_component_index | high | Shorthand for component_lookup_table[3] (query type 3 = parent/container). 0xFF means this object has no parent (it is a top-level object). Any other value is an index into the component array pointing to the parent component. Used in Change_Owner, FUN_1403963F0, FUN_14039BCB0, FUN_1403AC530. | +| `0x33E` | `uint8` | unknown_component_index_0x33E | medium | Component lookup table entry. Checked in FUN_1403A4820: when != -1 and FUN_1403751F0 returns false, the function returns early. Likely relates to a specific behavior type. | +| `0x348` | `uint8` | hardpoint_manager_index | high | Shorthand for component_lookup_table[0x16]. When 0xFF, the object uses the direct HP path (no hardpoint indirection). When any other value, damage routing goes through the hardpoint manager. Checked in Make_Invulnerable and Take_Damage paths. | +| `0x34B` | `uint8` | ability_manager_index | medium | Shorthand for component_lookup_table[0x19]. When != -1, the object has an ability system. Checked in FUN_1403A4820 to look up ability component and check +0x54 state. | +| `0x36F` | `uint8` | transform_component_index | medium | Shorthand for component_lookup_table[0x3D]. When != -1, the object has a transform component. Checked in Change_Owner for position updates. | +| `0x39E` | `uint8` | registered_with_session_flag | high | Set to 1 when FUN_14029BFE0(session_context, this) is called, indicating the object has been registered with the game session manager. Checked before calling FUN_14029BFE0 to avoid double-registration. Set in FUN_14038CB30, FUN_1403989A0, FUN_1403A4820. | +| `0x39F` | `uint8` | spawn_complete_flag | high | Set to 1 in FUN_1403989A0 during the spawn initialization sequence. When == 1 in FUN_1403A4820, enables visual model attachment and hero clash node setup. When == 0 in FUN_1403A4820, skips animation setup. | +| `0x3A0` | `uint8` | state_flags_bitfield | high | Bitfield of state/dirty flags. Bit 0 (0x01): HP change notification dirty flag (set by SetHP when object_id matches tracked ID). Bit 1 (0x02): used in ScheduleHeroRespawn -- if set, hero respawn is blocked. Bit 3 (0x08): animation/combat state flag (cleared/set in FUN_1403A92F0). Bit 4 (0x10): position lock flag (blocks render position updates in FUN_1403A8710). Bit 6 (0x40): used in ScheduleHeroRespawn -- if set alongside 0x02, respawn is blocked. Bit 7 (0x80): sign bit -- checked in FUN_1403989A0 (if < 0, skips hardpoint allocation). | +| `0x3A1` | `uint8` | dirty_flags_bitfield | high | Dirty/state flags bitfield. Bit 0 (0x01): transform dirty -- set in FUN_1403A8710 when position changes, cleared in FUN_1403AC530 at the end of transform update. Bit 3 (0x08): checked in FUN_1403A4820 for animation state. Bit 7 (0x80): prevent-death flag. When set, Take_Damage_Impl clamps HP to max(1.0, current_hp). | +| `0x3A6` | `uint16` | state_0x3A6 | low | Cleared to 0 in FUN_1403989A0 during spawn init. | +| `0x3A7` | `uint8` | invulnerability_flag | high | Invulnerability state. Checked by Take_Damage_Outer at multiple points. WARNING: Setting this byte manually does NOT work -- you must call Make_Invulnerable_Setter (RVA 0x3ABB80) which propagates to hardpoints and updates the behavior system. Located within the 0x3A6 cleared region. | +| `0x3A8` | `uint32` | state_0x3A8 | low | Cleared to 0 in FUN_1403989A0 during spawn init. | +| `0x3B5` | `uint32` | state_0x3B5 | low | Cleared to 0 in FUN_1403989A0. | +| `0x3B9` | `uint8` | state_0x3B9 | low | Cleared to 0 in FUN_1403989A0. | +| `0x3C0` | `uint64` | state_0x3C0 | medium | Cleared to 0 in FUN_1403989A0. Likely the last field -- places minimum struct size at 0x3C8. | + +## QueryInterface Types + +| ID | Class | +|----|-------| +| `0x00` | Self | +| `0x01` | BehaviorClass | +| `0x03` | Parent/Container | +| `0x0F` | BaseShieldBehaviorClass (front) | +| `0x10` | ShieldBehaviorClass (rear) | +| `0x16` | HardpointManager | +| `0x19` | AbilityComponent | +| `0x3D` | TransformComponent | +| `0x46` | PropertyHandler | diff --git a/docs/editor/structs/gameobjecttype.md b/docs/editor/structs/gameobjecttype.md new file mode 100644 index 000000000..afcb9f519 --- /dev/null +++ b/docs/editor/structs/gameobjecttype.md @@ -0,0 +1,24 @@ +# GameObjectType + + +Type definition struct for game objects. Shared across all instances of a given unit/building type. + + +## Key Offsets + +| Offset | Type | Name | +|--------|------|------| +| `0xF8` | `SSO_string` | type_name | +| `0x880` | `int32` | build_limit_global | +| `0x888` | `int32` | build_limit_per_player | +| `0x890` | `int32` | base_build_time | +| `0x894` | `int32` | tech_level_requirement | +| `0x89C` | `int32` | min_tech_level | +| `0xDCC` | `float32` | base_max_hp | +| `0xDD0` | `float32` | base_max_front_shield | +| `0xDD4` | `float32` | base_max_rear_shield | +| `0xEB0` | `pointer` | damage_threshold_array | +| `0xF0C` | `int32` | build_pad_requirement | +| `0x1648` | `bitmask` | weapon_target_type_bitmask | +| `0x1F78` | `int32` | unit_cap_contribution | +| `0x1FF4` | `int32` | damage_type_flags | diff --git a/docs/editor/structs/lua-state.md b/docs/editor/structs/lua-state.md new file mode 100644 index 000000000..bfc11e30c --- /dev/null +++ b/docs/editor/structs/lua-state.md @@ -0,0 +1,12 @@ +# lua_State + + +Lua 5.0.2 state struct (partial) + + +## Fields + +| Offset | Type | Name | Status / Confidence | Notes | +|--------|------|------|---------------------|-------| +| `0x10` | `pointer` | top | | | +| `0x18` | `pointer` | base | | | diff --git a/docs/editor/structs/objectunderconstructionclass.md b/docs/editor/structs/objectunderconstructionclass.md new file mode 100644 index 000000000..fb7fe6d43 --- /dev/null +++ b/docs/editor/structs/objectunderconstructionclass.md @@ -0,0 +1,13 @@ +# ObjectUnderConstructionClass + + +## Fields + +| Offset | Type | Name | Status / Confidence | Notes | +|--------|------|------|---------------------|-------| +| `0x00` | `pointer` | vtable_ptr | | | +| `0x08` | `pointer` | producing_object_type_ptr | | | +| `0x10` | `float32` | build_countdown | | | +| `0x20` | `int32` | player_slot_id | | | +| `0x28` | `float32` | build_cost | | | +| `0x2C` | `int32` | producing_object_id | | | diff --git a/docs/editor/structs/planetarydatapackclass.md b/docs/editor/structs/planetarydatapackclass.md new file mode 100644 index 000000000..78c4bf63b --- /dev/null +++ b/docs/editor/structs/planetarydatapackclass.md @@ -0,0 +1,24 @@ +# PlanetaryDataPackClass + +**RTTI**: `.?AVPlanetaryDataPackClass@@` + +**Size**: 0x350 bytes + + +## Fields + +| Offset | Type | Name | Status / Confidence | Notes | +|--------|------|------|---------------------|-------| +| `0x10` | `DVec` | persistent_tactical_objects | | | +| `0x20` | `DVec` | persistent_upgrades | | | +| `0x30` | `DVec` | line_links | | | +| `0x50` | `DVec` | trade_route_links | | | +| `0x68` | `float32` | capture_progress | high | | +| `0x6C` | `int32` | owning_player_id | high | | +| `0x70` | `float32` | previous_capture_progress | high | | +| `0x74` | `int32` | previous_owning_player_id | high | | +| `0x98` | `int32` | capture_initiator_player_id | high | | +| `0x1C9` | `uint8` | initial_owner_set_flag | high | | +| `0x2C8` | `uint8` | planet_destroyed_flag | medium | | +| `0x2E3` | `uint8` | capture_timer_active | medium | | +| `0x2F4` | `int32` | corruption_level | high | Values: -1=none, 0-3=corruption tiers | diff --git a/docs/editor/structs/playerclass.md b/docs/editor/structs/playerclass.md new file mode 100644 index 000000000..69a91ef4e --- /dev/null +++ b/docs/editor/structs/playerclass.md @@ -0,0 +1,99 @@ +# PlayerClass + +**RTTI**: `.?AVPlayerClass@@` + +**Inherits**: `RefCountClass`, `SignalGeneratorClass` + +**Size**: 1232 (0x4D0) bytes + + +## Notes + +- Actual RTTI name is PlayerClass (not PlayerObject) +- AIPlayerClass at +0x360 is a separate heap object, NOT inlined +- PlayerWrapper Lua binding stores pointer at wrapper+0x28 +- 15+ DynamicVectorClass instances inlined + + +## Fields + +| Offset | Type | Name | Status / Confidence | Notes | +|--------|------|------|---------------------|-------| +| `0x00` | `pointer` | vtable_ptr | CONFIRMED | | +| `0x08` | `struct_inline` | refcount_data | DISCOVERED | | +| `0x10` | `struct_inline` | signal_generator_base | DISCOVERED | | +| `0x37` | `uint8` | playable | CONFIRMED | | +| `0x38` | `unknown` | padding_38 | DISCOVERED | | +| `0x48` | `int32` | slot_index | CONFIRMED | | +| `0x4C` | `int32` | player_id | CONFIRMED | | +| `0x54` | `int32` | team_id | DISCOVERED | | +| `0x58` | `int32` | ai_player_type_id | DISCOVERED | | +| `0x62` | `uint8` | local_player | CONFIRMED | | +| `0x68` | `pointer` | faction_ref | CONFIRMED | | +| `0x70` | `float32` | credits | CONFIRMED | | +| `0x74` | `float32` | max_credits | CONFIRMED | | +| `0x84` | `int32` | tech_level | CONFIRMED | | +| `0x88` | `int32` | max_tech_level | CONFIRMED | | +| `0x8C` | `DynamicVectorClass` | dvec_int_array_0 | DISCOVERED | | +| `0xE0` | `DynamicVectorClass` | historically_built_types | CONFIRMED | | +| `0xF8` | `DynamicVectorClass` | buildable_types_1 | DISCOVERED | | +| `0x108` | `uint8` | is_human_controlled | CONFIRMED | | +| `0x110` | `DynamicVectorClass` | buildable_types_2 | DISCOVERED | | +| `0x128` | `DynamicVectorClass` | buildable_types_3 | DISCOVERED | | +| `0x140` | `struct_unknown` | player_data_block_1 | DISCOVERED | | +| `0x180` | `struct_unknown` | player_data_block_2 | DISCOVERED | | +| `0x1A8` | `DynamicVectorClass_header` | unlockable_types_dvec_header | DISCOVERED | | +| `0x1B0` | `pointer` | unlocked_types_array | CONFIRMED | | +| `0x1B8` | `int32` | unlocked_types_count | CONFIRMED | | +| `0x1BC` | `int32` | unlocked_types_capacity | CONFIRMED | | +| `0x1C0` | `DynamicVectorClass` | locked_types_dvec | CONFIRMED | | +| `0x1C8` | `pointer` | locked_types_array | CONFIRMED | | +| `0x1D0` | `int32` | locked_types_count | CONFIRMED | | +| `0x1D4` | `int32` | locked_types_capacity | CONFIRMED | | +| `0x1E0` | `DynamicVectorClass` | type_list_4 | DISCOVERED | | +| `0x1F8` | `DynamicVectorClass` | type_list_5 | DISCOVERED | | +| `0x210` | `DynamicVectorClass` | type_list_6 | DISCOVERED | | +| `0x228` | `DynamicVectorClass` | type_list_7 | DISCOVERED | | +| `0x250` | `DynamicVectorClass` | type_list_8_vtable | DISCOVERED | | +| `0x258` | `pointer` | type_list_8_data | DISCOVERED | | +| `0x268` | `DynamicVectorClass` | type_list_9_vtable | DISCOVERED | | +| `0x270` | `pointer` | type_list_9_data | DISCOVERED | | +| `0x280` | `DynamicVectorClass` | int_vector | DISCOVERED | | +| `0x298` | `DynamicVectorClass` | type_list_10 | DISCOVERED | | +| `0x2B0` | `pointer` | sub_object_ptr | DISCOVERED | | +| `0x2C0` | `pointer` | heap_buffer_1 | DISCOVERED | | +| `0x2C8` | `struct_unknown` | sub_struct_2c8 | DISCOVERED | | +| `0x2F0` | `pointer` | large_sub_object | DISCOVERED | | +| `0x318` | `struct_unknown` | data_block_318 | DISCOVERED | | +| `0x360` | `pointer (AIPlayerClass*)` | ai_player_ptr | CONFIRMED | | +| `0x370` | `pointer (int32[])` | diplomacy_table | CONFIRMED | | +| `0x378` | `pointer` | profile_data_ptr | DISCOVERED | | +| `0x380` | `DynamicVectorClass` | black_market_items | CONFIRMED | | +| `0x398` | `int32` | difficulty_level | DISCOVERED | | +| `0x3F8` | `uint8[]` | advisor_hints_base | CONFIRMED | | +| `0x448` | `uint8` | black_market_tutorial_flag | CONFIRMED | | +| `0x449` | `uint8` | sabotage_tutorial_flag | CONFIRMED | | +| `0x484` | `int32` | space_station_level | DISCOVERED | | + +## Lua Methods + +| Method | Wrapper RVA | Engine RVA | +|--------|-------------|------------| +| `Give_Money` | `0x603130` | `0x27F370` | +| `Get_Faction_Name` | `0x602A00` | `inline` | +| `Get_Credits` | `0x6027F0` | `inline` | +| `Get_ID` | `0x602C40` | `inline` | +| `Get_Tech_Level` | `0x603040` | `inline` | +| `Set_Tech_Level` | `0x604480` | `0x288980` | +| `Unlock_Tech` | `0x604540` | `0x286100` | +| `Lock_Tech` | `0x603B20` | `0x286150` | +| `Is_Enemy` | `0x603760` | `0x2824F0` | +| `Is_Ally` | `0x603560` | `0x2823E0` | +| `Is_Human` | `0x603A40` | `inline` | +| `Is_Local_Player` | `0x603960` | `inline` | +| `Make_Ally` | `0x6046A0` | `0x288800` | +| `Make_Enemy` | `0x604780` | `0x288800` | +| `Enable_As_Actor` | `0x602640` | `0x4B0250` | +| `Select_Object` | `0x603F60` | `0x2BD2F0` | +| `Retreat` | `0x603DE0` | `0x340920` | +| `Release_Credits_For_Tactical` | `0x603C70` | `0x4B06D0` | diff --git a/docs/editor/structs/storyeventclass.md b/docs/editor/structs/storyeventclass.md new file mode 100644 index 000000000..31acb9536 --- /dev/null +++ b/docs/editor/structs/storyeventclass.md @@ -0,0 +1,22 @@ +# StoryEventClass + +**RTTI**: `.?AVStoryEventClass@@` + +**Inherits**: `SignalGeneratorClass` + +**Size**: 0x360 bytes + + +## Key Offsets + +| Offset | Type | Name | +|--------|------|------| +| `0x00-0x1F` | | Event name SSO string | +| `0x20` | | Event type ID (int32) | +| `0x3C` | | Reward type ID (int32) | +| `0x4C` | | Active/triggered flag (byte) | +| `0x50-0x190` | | Reward parameters (14 SSO strings, 0x20 stride) | +| `0x228` | | Back-pointer to StorySubPlotClass | +| `0x230` | | Dialog ID string | +| `0x2C0` | | Dialog text entries DVec | +| `0x358` | | Event index within subplot | diff --git a/docs/editor/structs/storysubplotclass.md b/docs/editor/structs/storysubplotclass.md new file mode 100644 index 000000000..9371d237f --- /dev/null +++ b/docs/editor/structs/storysubplotclass.md @@ -0,0 +1,19 @@ +# StorySubPlotClass + +**RTTI**: `.?AVStorySubPlotClass@@` + +**Inherits**: `SignalGeneratorClass` + +**Size**: 0x650 bytes + + +## Key Offsets + +| Offset | Type | Name | +|--------|------|------| +| `0x20` | | Hash table linked list root (event lookup) | +| `0x30` | | Hash table bucket array pointer | +| `0x48` | | Hash table bucket mask | +| `0x58-0x5F8` | | 61 DynamicVectorClass slots (0x18 each) | +| `0x628` | | Plot name string (MSVC SSO) | +| `0x644` | | is_active_story flag | diff --git a/docs/editor/systems/ai.md b/docs/editor/systems/ai.md new file mode 100644 index 000000000..5fce66d3c --- /dev/null +++ b/docs/editor/systems/ai.md @@ -0,0 +1,516 @@ +# Ai + +**Architecture**: hierarchical goal-driven with perception-planning-execution loop + +**Singleton Rva**: 0x4D9C80 + +**Per Player Class Rva**: 0x4AF810 + +**Subsystem Count**: 7 + + +### Subsystems + +- Perception +- Planning(4.0s) +- Goal +- Execution +- Learning +- Template +- Budget + +**Build Task States**: 25 + + +### Perception Modes + +- Galactic +- Land +- Space +- TacticalGrid + +**Enable Mechanism**: TheAIClass state_flags(+0x00) + active_flag(+0x04); per-player AIPlayerClass at PlayerClass+0x360 + + +## RE Findings Detail + +**Analysis**: SWFOC AI System Decompilation — Agent 3D Output + +Comprehensive map of the Alamo engine AI decision loop, subsystems, enable/disable mechanisms, production queue, diplomacy, and difficulty systems. Derived from Ghidra static analysis of StarWarsG.exe (x86_64). + +- **Date**: 2026-04-04 + + +### Architecture Overview + +- **summary**: The AI system is a hierarchical, goal-driven architecture centered on the TheAIClass singleton. Each non-human player gets an AIPlayerClass instance containing 7 ServicedAISystem subsystems. The system uses a perception-planning-execution loop with per-mode specializations (Galactic, Space, Land). Production is handled by a FiniteStateMachine-based AIBuildTaskClass with 25 named states. Diplomacy is event-driven (AllyEventClass) rather than polled. Difficulty is managed by DifficultyAdjustmentManagerClass (a database manager of DifficultyAdjustmentClass records). +- **key_design_patterns**: Singleton pattern (TheAIClass, TheAIDataManagerClass, TheAIPlayerTypeManagerClass, TheAIGoalTypeManagerClass, TheAITemplateManagerClass), ServicedAISystemClass — base class for all tickable AI subsystems (inherits AIDiagnosticsClass, stores owner reference at +0x0), FiniteStateMachineClass — state machine driving AIBuildTaskClass production, PerceptualEvaluatorClass hierarchy — 20+ evaluator types for sensing game state, Signal/Listener pattern — AILearningSystemClass uses SignalListenerClass + SignalGeneratorClass, Lua binding wrappers — LuaMemberFunctionWrapper for exposing AI internals to Lua scripts, DatabaseObjectManagerClass — manages typed collections (goals, templates, difficulty adjustments) + + +### Singletons + +- **TheAIClass**: + - **constructor_rva**: 0x4D9C80 + - **description**: Top-level AI singleton. Constructed with field_0x4 = 1, field_0x0 = 0x01010101 (4-byte enable/state flags). Manages all AI player instances. Access via SingletonInstance (ctor/dtor at 0x7EED80). + - **singleton_ctor_dtor_rva**: 0x7EED80 + - **vtable_rva**: set in constructor (this->vftablePtr = &vftable) + - **fields**: {'offset': '0x00', 'type': 'uint32', 'name': 'state_flags', 'description': 'Written as 0x01010101 in constructor. 4 individual byte flags. Likely per-state enable bits (galactic, space, land, global).', 'confidence': 'medium'}, {'offset': '0x04', 'type': 'uint32', 'name': 'active_flag', 'description': 'Set to 1 in constructor. Probable global AI-active flag.', 'confidence': 'medium'} +- **TheAIDataManagerClass**: + - **constructor_rva**: 0x4747E0 + - **singleton_ctor_dtor_rva**: 0x7E6BA0 + - **description**: Stores global AI configuration data. Has 8 undefined8 fields initialized to 0 at offsets 0x38..0x70. + - **fields_initialized_count**: 8 +- **TheAIPlayerTypeManagerClass**: + - **constructor_rva**: 0x4E9940 + - **singleton_ctor_dtor_rva**: 0x7EED90 + - **description**: Manages AI player type definitions. Uses a hash map (load factor 0x3f800000 = 1.0f). Maps player types to AI behavior profiles. + - **hash_map_initial_bucket_count**: 7 + - **hash_map_load_factor**: 1.0 +- **TheAIGoalTypeManagerClass**: + - **constructor_rva**: 0x5E6690 + - **description**: Manages all AI goal type definitions. Database of AIGoalTypeClass objects. +- **TheAIGoalProposalFunctionSetManagerClass**: + - **constructor_rva**: 0x5E5C30 + - **description**: Manages sets of goal proposal functions. These are the scoring functions that rank which goals the AI should pursue. +- **TheAITemplateManagerClass**: + - **constructor_rva**: 0x4E8920 + - **description**: Manages AI template definitions (build orders, force compositions, etc.). Database of AITemplateClass objects. + + +### Per Player Ai Structure + +- **AIPlayerClass**: + - **constructor_rva**: 0x4AF810 + - **description**: Per-player AI controller. Created for each AI-controlled player slot. Inherits AIDiagnosticsClass. Contains references to all 7 subsystems. Constructor takes 2 params: likely (player_ptr, ai_type_ptr). + - **inherits**: AIDiagnosticsClass + - **fields**: {'offset': 'data+0x00', 'type': 'pointer', 'name': 'param_1_ref', 'description': 'First constructor param — likely pointer to owning PlayerClass'}, {'offset': 'data+0x08', 'type': 'pointer', 'name': 'param_2_ref', 'description': 'Second constructor param — likely pointer to AIPlayerTypeClass'}, {'offset': 'data+0x10', 'type': 'pointer', 'name': 'subsystem_0', 'description': 'Initialized to 0. Subsystem slot (likely perception)'}, {'offset': 'data+0x18', 'type': 'pointer', 'name': 'subsystem_1', 'description': 'Initialized to 0. Subsystem slot (likely planning)'}, {'offset': 'data+0x20', 'type': 'pointer', 'name': 'subsystem_2', 'description': 'Initialized to 0. Subsystem slot (likely goal)'}, {'offset': 'data+0x28', 'type': 'pointer', 'name': 'subsystem_3', 'description': 'Initialized to 0. Subsystem slot (likely execution)'}, {'offset': 'data+0x30', 'type': 'int32', 'name': 'state_or_mode', 'description': 'Initialized to 0xFFFFFFFF (-1). Likely current game mode enum or uninitialized sentinel.'} + + +### Subsystems + +- **_note**: All subsystems inherit ServicedAISystemClass, which inherits AIDiagnosticsClass. ServicedAISystemClass stores a reference to the owning AIPlayerClass at data+0x00 and has a field at data+0x08 initialized to 0. +- **ServicedAISystemClass**: + - **constructor_rva**: 0x64C250 + - **description**: Base class for all 7 AI subsystems. Provides tick/service interface via vtable virtual methods. Stores owning AIPlayerClass reference. + - **inherits**: AIDiagnosticsClass + - **fields**: {'offset': 'data+0x00', 'type': 'pointer', 'name': 'owner_ai_player', 'description': 'Pointer to the AIPlayerClass that owns this subsystem. Passed as param_1 in constructor.'}, {'offset': 'data+0x08', 'type': 'uint64', 'name': 'service_counter_or_state', 'description': 'Initialized to 0. Likely tick counter or active/inactive state.'} +- **AIPerceptionSystemClass**: + - **constructor_rva**: 0x4DAD80 + - **description**: Senses the game world. Feeds evaluator results into the planning system. Contains a hash map of PerceptionParameterBindings and a linked list of perception data. + - **inherits**: ServicedAISystemClass + - **specializations**: + - **GalacticPerceptionSystemClass**: + - **constructor_rva**: 0x4E1880 + - **extra_fields**: 3 + - **description**: Galactic map perception. 3 extra pointer/counter fields for planet-level sensing. + - **LandPerceptionSystemClass**: + - **constructor_rva**: 0x6B8980 + - **description**: Land battle perception. + - **SpacePerceptionSystemClass**: + - **constructor_rva**: 0x6B9A20 + - **description**: Space battle perception. + - **TacticalPerceptionGridClass**: + - **constructor_rva**: 0x653340 + - **description**: Grid-based perception for tactical modes. Uses PerceptionGridEntryClass linked list. +- **AIPlanningSystemClass**: + - **constructor_rva**: 0x6BAC00 + - **description**: Generates plans from perception data and goal proposals. Contains a doubly-linked list (node size 0x20) for plan entries. Hash map (buckets=7, load=1.0). Planning interval stored as float 0x40800000 = 4.0 (at offset 0x10 — likely 4.0 seconds between plan recalculations). + - **inherits**: ServicedAISystemClass + - **key_values**: + - **plan_interval_seconds**: 4.0 + - **plan_interval_hex**: 0x40800000 + - **hash_map_initial_buckets**: 7 + - **hash_map_load_factor**: 1.0 + - **fields**: {'offset': 'data+0x00', 'type': 'pointer', 'name': 'active_plan_head'}, {'offset': 'data+0x08', 'type': 'pointer', 'name': 'plan_data_1'}, {'offset': 'data+0x10', 'type': 'float32', 'name': 'plan_interval', 'description': '4.0f — seconds between planning cycles'}, {'offset': 'data+0x18', 'type': 'pointer', 'name': 'plan_linked_list_head', 'description': 'Doubly-linked list node (allocated as 0x20 bytes, self-referencing sentinel)'}, {'offset': 'data+0x28', 'type': 'pointer', 'name': 'hash_map_ptr'} +- **AIGoalSystemClass**: + - **constructor_rva**: 0x6C7970 + - **description**: Manages active goals. Inherits ServicedAISystemClass. Contains an AIBudgetClass instance (at data+0x158) for resource allocation. Two large internal structures (initialized via FUN_140721360) at data+0x00 and data+0xA0 — likely active-goals list and completed-goals list. + - **inherits**: ServicedAISystemClass + - **specializations**: + - **GalacticGoalSystemClass**: + - **constructor_rva**: 0x6B8480 + - **description**: Galactic-mode goals. Pure vtable override of AIGoalSystemClass. + - **LandGoalSystemClass**: + - **constructor_rva**: 0x6B86E0 + - **description**: Land-mode goals. + - **SpaceGoalSystemClass**: + - **constructor_rva**: 0x6B88D0 + - **description**: Space-mode goals. + - **fields**: {'offset': 'data+0x00', 'type': 'struct', 'size': '0xA0', 'name': 'goal_collection_1', 'description': 'Initialized by FUN_140721360. Likely active goals container.'}, {'offset': 'data+0xA0', 'type': 'struct', 'size': '0xA0', 'name': 'goal_collection_2', 'description': 'Initialized by FUN_140721360. Likely pending/completed goals.'}, {'offset': 'data+0x140', 'type': 'pointer', 'name': 'field_140'}, {'offset': 'data+0x148', 'type': 'pointer', 'name': 'field_148'}, {'offset': 'data+0x14C', 'type': 'int32', 'name': 'field_14C'}, {'offset': 'data+0x150', 'type': 'int32', 'name': 'field_150'}, {'offset': 'data+0x158', 'type': 'AIBudgetClass', 'name': 'budget', 'description': "Embedded AIBudgetClass instance for this goal system's resource budget."}, {'offset': 'data+0x1E8', 'type': 'hash_map', 'name': 'hash_map_1'}, {'offset': 'data+0x228', 'type': 'hash_map', 'name': 'hash_map_2'}, {'offset': 'data+0x268', 'type': 'pointer', 'name': 'field_268'}, {'offset': 'data+0x270', 'type': 'pointer', 'name': 'field_270'}, {'offset': 'data+0x278', 'type': 'pointer', 'name': 'field_278'} +- **AIExecutionSystemClass**: + - **constructor_rva**: 0x524CE0 + - **description**: Executes planned actions (movement, attacks, ability usage). 4 pointer fields + inherits ServicedAISystemClass. + - **inherits**: ServicedAISystemClass +- **AILearningSystemClass**: + - **constructor_rva**: 0x585D00 + - **description**: Tracks AI performance history. Inherits ServicedAISystemClass + MultiLinkedListMember + SignalListenerClass. Contains 5 hash maps and multiple counters. Stores historical data about build effectiveness, combat outcomes, etc. Very large struct (~0x198+ bytes of own data). + - **inherits**: ServicedAISystemClass, MultiLinkedListMember, SignalListenerClass + - **hash_map_count**: 5 + - **total_data_fields_approx**: 50 +- **AITemplateSystemClass**: + - **constructor_rva**: 0x6BB9E0 + - **description**: Manages force templates (predefined army compositions). Contains a linked list of active template instances. Inherits ServicedAISystemClass. + - **inherits**: ServicedAISystemClass + - **fields**: {'offset': 'data+0x00', 'type': 'pointer', 'name': 'template_linked_list_head', 'description': 'Doubly-linked sentinel node (0x18 bytes). Stores active force templates.'}, {'offset': 'data+0x08', 'type': 'uint64', 'name': 'field_08'}, {'offset': 'data+0x10', 'type': 'uint64', 'name': 'template_count_or_state'}, {'offset': 'data+0x18', 'type': 'uint64', 'name': 'field_18'} +- **AIBudgetClass**: + - **constructor_rva**: 0x6109C0 + - **description**: Resource budget allocator for the AI goal system. Inherits AIDiagnosticsClass. Contains a hash map of BudgetedCategoryStruct entries. Tracks allocated, spent, and remaining budget per category. + - **inherits**: AIDiagnosticsClass + - **fields**: {'offset': 'data+0x00', 'type': 'int32', 'name': 'total_budget_int'}, {'offset': 'data+0x04', 'type': 'float32', 'name': 'budget_multiplier', 'value': '0x3F800000 = 1.0f'}, {'offset': 'data+0x08', 'type': 'pointer', 'name': 'hash_map_sentinel', 'description': 'Self-referencing sentinel for budget category hash map'}, {'offset': 'data+0x18', 'type': 'pointer', 'name': 'hash_map_entries'}, {'offset': 'data+0x40', 'type': 'int32', 'name': 'field_40'}, {'offset': 'data+0x48', 'type': 'int32', 'name': 'field_48'}, {'offset': 'data+0x4C', 'type': 'int32', 'name': 'field_4C'}, {'offset': 'data+0x50', 'type': 'pointer', 'name': 'owner_ref', 'description': 'Reference to owning system (passed as param_1)'}, {'offset': 'data+0x58', 'type': 'int32', 'name': 'field_58'}, {'offset': 'data+0x5C', 'type': 'int32', 'name': 'field_5C'}, {'offset': 'data+0x60', 'type': 'int32', 'name': 'field_60'} + - **nested_structs**: + - **BudgetedCategoryStruct**: + - **constructor_rva**: 0x610A70 + - **inherits**: AIDiagnosticsClass + - **fields**: {'offset': 'data+0x00', 'type': 'int32', 'name': 'category_id', 'value_on_init': '0xFFFFFFFF (-1 = unassigned)'}, {'offset': 'data+0x08', 'type': 'pointer', 'name': 'owner_ref'}, {'offset': 'data+0x10', 'type': 'int64', 'name': 'allocated_amount'}, {'offset': 'data+0x18', 'type': 'int32', 'name': 'priority', 'value_on_init': 1}, {'offset': 'data+0x20', 'type': 'int64', 'name': 'field_20'}, {'offset': 'data+0x28', 'type': 'int64', 'name': 'field_28'}, {'offset': 'data+0x30', 'type': 'int64', 'name': 'field_30'}, {'offset': 'data+0x38', 'type': 'int32', 'name': 'min_allocation', 'value_on_init': 1}, {'offset': 'data+0x40', 'type': 'int64', 'name': 'field_40'}, {'offset': 'data+0x48', 'type': 'int64', 'name': 'field_48'}, {'offset': 'data+0x50', 'type': 'int64', 'name': 'field_50'} + + +### Production System + +- **AIBuildTaskClass**: + - **constructor_rva**: 0x6478C0 + - **state_machine_handler_rva**: 0x64AEC0 + - **description**: FiniteStateMachineClass. The core production unit. Each build task is a refcounted, signal-listening state machine that progresses through 25 named states from INIT_TASK to ALL_FINISHED. Multiple AIBuildTaskClass instances can be active simultaneously per AI player, managed via MultiLinkedListClass. + - **inherits**: FiniteStateMachineClass, RefCountClass, SignalListenerClass, SignalGeneratorClass + - **build_states**: + - **STATE_Global**: + - **id**: match on *(int*)(this+0x8+4) + - **description**: Default/global state handler + - **BUILD_STATE_INIT_TASK**: + - **id**: 1 + - **description**: Initial state. Sets state to 1. + - **BUILD_STATE_PRODUCTION_QUEUED**: + - **id**: 2 + - **description**: Production request queued. Polls game state via DAT_140b15418 (global game clock/state). Checks credit availability and population caps (query types 0x5F, 0x60 on game object properties). + - **BUILD_STATE_FREESTORE_ALLOC**: + - **id**: 3 + - **description**: Allocates from the free store. Transitions to state 6 (PRODUCTION_END) on enter. + - **BUILD_STATE_GROUND_UNIT_PRODUCTION**: + - **id**: 4 + - **description**: Ground unit being produced. Calls FUN_140648120 on enter. + - **BUILD_STATE_PRODUCTION_BEGIN**: + - **id**: 5 + - **description**: Production started. Runs EVENT_Update poll loop same as state 2. + - **BUILD_STATE_PRODUCTION_END**: + - **id**: 6 + - **description**: Production completed. Unregisters signal listeners. Calls FUN_140647ee0 (production finalize). If flag at +0xD0 is set, skips budget release. + - **BUILD_STATE_PRODUCTION_FAILED**: + - **id**: 7 + - **description**: Production failed. Releases budget via FUN_1405fdd90. Notifies free store via FUN_1404d4ee0. Transitions to cleanup. + - **BUILD_STATE_PRODUCTION_FINISHED**: + - **id**: 8 + - **description**: Completed successfully. Calls FUN_14064ad00 on enter. + - **BUILD_STATE_DEPENDANT_WAIT**: + - **id**: 9 + - **description**: Waiting for dependent tasks. Checks offset_0xB8 (dependent task list) count at +0x30. Transitions to state 0xB when count reaches 0. + - **BUILD_STATE_FLEET_MERGE**: + - **id**: 10 + - **description**: Merging produced units into fleet. + - **BUILD_STATE_PATHING_DEST**: + - **id**: 11 + - **description**: Pathfinding to destination. Creates MovementBlockStatus. Uses AIExecutionPathFinder. Handles galactic object containment hierarchy (checks +0x335, +0x336, +0x337 on game objects — component lookup table entries). + - **BUILD_STATE_JOIN_TASK_FORCE**: + - **id**: 13 + - **description**: Unit joining a task force. + - **BUILD_STATE_PRE_PATH_SEPERATE**: + - **id**: 14 + - **description**: Pre-pathfinding separation. Issues FleetManagementEvents to split units for movement. + - **BUILD_STATE_WAIT_FOR_ALL**: + - **id**: 15 + - **description**: Waiting for all sub-tasks to complete. Checks galactic mode (offset 0x34 in mode struct). Transitions to 0x13 (TRANSPORTS_TO_ORBIT) or 8 (PRODUCTION_FINISHED). + - **BUILD_STATE_DO_TASK_FORCE_MERGE**: + - **id**: 16 + - **description**: Executing task force merge. Calls FUN_140512160 to check merge completion. + - **BUILD_STATE_WAIT_FOR_CREDITS**: + - **id**: 17 + - **description**: Waiting for sufficient credits. Transitions to 0x12 (BUILD_TASK_LIST) on enter. + - **BUILD_STATE_BUILD_TASK_LIST**: + - **id**: 18 + - **description**: Building the task list. Calls FUN_140648980 on enter. + - **BUILD_STATE_TRANSPORTS_TO_ORBIT**: + - **id**: 19 + - **description**: Moving transports to orbit. Issues FleetManagementEvents for orbital transfer. + - **BUILD_STATE_READY_TO_MERGE**: + - **id**: 20 + - **description**: Ready to merge. Checks task force type and decides between JOIN_TASK_FORCE (0xD) or DO_TASK_FORCE_MERGE (0x10). + - **BUILD_STATE_ALL_FINISHED**: + - **id**: 21 + - **description**: All production tasks complete. Terminal state. + - **BUILD_STATE_WAIT_FOR_FREESTORE_ALLOC**: + - **id**: 22 + - **description**: Waiting for free store allocation. Complex state with multiple paths: ground production (state 4), fleet merge (state 0xE), or sync move (state 0x18). Checks object hierarchy via component_lookup_table entries. + - **BUILD_STATE_HIJACK_UNIT**: + - **id**: 23 + - **description**: Hijacking an existing unit for the build task. Calls FUN_140648370 on enter. + - **BUILD_STATE_WAIT_FOR_SYNC_MOVE**: + - **id**: 24 + - **description**: Waiting for synchronized movement. + - **BUILD_STATE_SYNC_MOVE_COUNTDOWN**: + - **id**: 25 + - **description**: Countdown timer for sync move. Decrements float at +0xD8 by elapsed time. When timer <= threshold at +0xD4, calls FUN_14064a1f0 (execute synchronized move). + - **fields**: {'offset': '0x08', 'type': 'int32', 'name': 'current_state', 'description': 'Written with the state ID on each state entry'}, {'offset': '0x18', 'type': 'pointer_to_string', 'name': 'current_state_name', 'description': "Debug string pointer — the state name (e.g., 'BUILD_STATE_INIT_TASK')"}, {'offset': '0x20', 'type': 'int32', 'name': 'transition_pending', 'description': 'Set to 1 when a state transition is requested'}, {'offset': 'field_0xC', 'type': 'int32', 'name': 'next_state', 'description': 'Target state for pending transition'}, {'offset': 'field_0x20', 'type': 'pointer_to_string', 'name': 'current_event_name', 'description': "Debug: 'EVENT_Enter', 'EVENT_Update'"}, {'offset': '0x60', 'type': 'pointer', 'name': 'target_game_object', 'description': 'The game object being produced or the production target location'}, {'offset': '0x68', 'type': 'pointer', 'name': 'production_structure', 'description': 'The building/structure performing the production. Null for non-structure tasks.'}, {'offset': '0x70', 'type': 'int32', 'name': 'production_type_id', 'value_on_init': '0xFFFFFFFF'}, {'offset': '0x74', 'type': 'int32', 'name': 'production_priority', 'value_on_init': 1}, {'offset': '0x78', 'type': 'pointer', 'name': 'dependent_task'}, {'offset': '0x80', 'type': 'pointer', 'name': 'build_type_ref', 'description': 'GameObjectTypeClass pointer for what to build'}, {'offset': '0x88', 'type': 'int32', 'name': 'build_type_id_2', 'value_on_init': '0xFFFFFFFF'}, {'offset': '0x90', 'type': 'pointer', 'name': 'destination_object', 'description': 'Where to send the produced unit'}, {'offset': '0x98', 'type': 'pointer', 'name': 'owning_ai_player', 'description': 'Pointer to AIPlayerClass (used heavily to access subsystems via +0xF0)'}, {'offset': '0xA0', 'type': 'uint8', 'name': 'cancelled_flag'}, {'offset': '0xA8', 'type': 'pointer', 'name': 'movement_block_status', 'description': 'MovementBlockStatus for pathfinding states'}, {'offset': '0xB0', 'type': 'pointer', 'name': 'merge_target'}, {'offset': '0xB8', 'type': 'pointer', 'name': 'dependent_task_list'}, {'offset': '0xC0', 'type': 'pointer', 'name': 'field_C0'}, {'offset': '0xC8', 'type': 'int32', 'name': 'field_C8'}, {'offset': '0xCC', 'type': 'int32', 'name': 'field_CC'}, {'offset': '0xD0', 'type': 'uint8', 'name': 'skip_budget_release_flag', 'description': 'When set, PRODUCTION_END skips budget release'}, {'offset': '0xD2', 'type': 'uint8', 'name': 'is_reinforcement_flag'}, {'offset': '0xD4', 'type': 'float32', 'name': 'sync_move_threshold', 'description': 'Timer threshold for SYNC_MOVE_COUNTDOWN state'}, {'offset': '0xD8', 'type': 'float32', 'name': 'sync_move_timer', 'value_on_init': '-1.0f (0xBF800000)'} +- **AIPlanetBuildTaskClass**: + - **constructor_rva**: 0x6954B0 + - **description**: Planet-level build task. Managed via MultiLinkedListClass. Coordinates multiple AIBuildTaskClass instances for a single planet. + - **list_constructor_rva**: 0x53B140 +- **AIFreeStoreClass**: + - **lua_wrapper_constructor_rva**: 0x4D22B0 + - **description**: Manages available (unallocated) units that the AI can commandeer for goals. Lua-exposed via LuaMemberFunctionWrapper. Has registered Lua method 'Get_Object_Count'. Contains hash maps for tracking allocated vs. free units. + - **lua_methods**: Get_Object_Count + - **specializations**: + - **LandFreeStoreClass**: + - **constructor_rva**: 0x690980 + - **SpaceFreeStoreClass**: + - **constructor_rva**: 0x690420 + - **GalacticFreeStoreClass**: + - **lua_wrapper_rva**: 0x68EAF0 +- **ProduceForceBlockStatus**: + - **constructor_rva**: 0x6C6500 + - **description**: Blocking status for force production. Inherits LuaMemberFunctionWrapper. Used in Lua scripts via _ProduceObject. Lua scripts call this and it blocks until production completes. + - **inherits**: LuaMemberFunctionWrapper + - **fields**: {'offset': '0x38', 'type': 'pointer', 'name': 'field_38'}, {'offset': '0x40', 'type': 'pointer', 'name': 'field_40'}, {'offset': '0x48', 'type': 'pointer', 'name': 'field_48'} + + +### Perception System + +- **evaluator_hierarchy**: + - **description**: The perception system uses a hierarchy of PerceptualEvaluatorClass subclasses. Each evaluator scores a different aspect of the game world. Results feed into the planning system's goal proposal scoring. + - **base_class_destructor_rva**: 0x64C900 + - **evaluator_types**: {'name': 'GameObjectPerceptualEvaluatorClass', 'rva': '0x6D7E50', 'description': 'Evaluates individual game objects (units, buildings)'}, {'name': 'GameObjectTypePerceptualEvaluatorClass', 'rva': '0x64F400', 'description': 'Evaluates by object type category'}, {'name': 'PlayerPerceptualEvaluatorClass', 'rva': '0x6DFAC0', 'description': 'Evaluates player-level state (economy, military strength)'}, {'name': 'SpaceShipPerceptualEvaluatorClass', 'rva': '0x6D1D70', 'description': 'Space-specific ship evaluation'}, {'name': 'TacticalLocationPerceptualEvaluatorClass', 'rva': '0x72C560', 'description': 'Evaluates tactical map positions'}, {'name': 'TacticalObjectPerceptualEvaluatorClass', 'rva': '0x730580', 'description': 'Evaluates tactical-mode objects'}, {'name': 'TacticalPlayerPerceptualEvaluatorClass', 'rva': '0x734780', 'description': 'Evaluates players in tactical mode'}, {'name': 'TacticalStructurePerceptualEvaluatorClass', 'rva': '0x734730', 'description': 'Evaluates tactical-mode structures'}, {'name': 'TacticalUnitPerceptualEvaluatorClass', 'rva': '0x7320D0', 'description': 'Evaluates tactical-mode units'} + - **galactic_evaluators**: GalacticGamePerceptualEvaluatorClass, GalacticPlayerPerceptualEvaluatorClass, PlanetPerceptualEvaluatorClass, PlanetForcePerceptualEvaluatorClass, PlanetTypePerceptualEvaluatorClass, GameForcePerceptualEvaluatorClass + - **other_evaluators**: FunctionPerceptualEvaluatorClass, HintPerceptualEvaluatorClass, ScriptPerceptualEvaluatorClass, RootLookupPerceptualEvaluatorClass +- **PerceptionParameterBindingsClass**: + - **constructor_rvas**: 0x4DAE40, 0x64C840, 0x659C60 + - **description**: Binds perception parameters (thresholds, weights) to evaluator instances. Multiple constructors for different contexts. +- **PerceptionGridEntryClass**: + - **constructor_rva**: 0x653100 + - **description**: Entry in the tactical perception grid. Stores per-cell sensing data. + + +### Ai Enable Disable Mechanism + +- **description**: AI enable/disable operates at multiple levels: (1) TheAIClass singleton state_flags (0x01010101 = all active), (2) Per-player Is_Human flag on PlayerObject, (3) Per-unit Prevent_AI_Usage Lua method, (4) Suspend_AI global Lua function (time-based), (5) Player::Enable_As_Actor() Lua method. The AI system checks Is_Human on the player to decide whether to create an AIPlayerClass. The Suspend_AI function likely sets a timer on the TheAIClass singleton that suppresses all AI ticking. +- **global_ai_singleton**: + - **rva**: TheAIClass singleton via SingletonInstance at 0x7EED80 + - **state_flags_offset**: 0x00 + - **state_flags_value_active**: 0x01010101 + - **active_flag_offset**: 0x04 + - **active_flag_value**: 1 + - **description**: The TheAIClass constructor writes 0x01010101 to offset 0x00 (4 byte flags, likely per-mode: galactic=0x01, space=0x01, land=0x01, global=0x01) and 1 to offset 0x04. Setting these to 0 would disable AI globally. +- **per_player_control**: + - **player_is_human_lua_binding**: Is_Human (string RVA 0x8AA448) + - **player_is_human_controlled_lua_binding**: IsHumanControlled (string RVA 0x8A5D58) + - **enable_as_actor_lua_binding**: Enable_As_Actor() on PlayerWrapper + - **description**: PlayerObject has Is_Human and IsHumanControlled methods. AI-controlled players have Is_Human returning false. Enable_As_Actor() on a PlayerWrapper activates AI for that player. The engine checks these flags when creating/destroying AIPlayerClass instances. +- **per_unit_control**: + - **prevent_ai_usage_method**: Prevent_AI_Usage(bool) on GameObjectWrapper + - **warning**: Crashes in tactical if unit has no active AI (community-documented bug). + - **description**: Blocks AI from issuing commands to a specific unit. Useful for cinematic sequences or player-controlled units in AI factions. +- **temporal_suspension**: + - **suspend_ai_function**: Suspend_AI(seconds) — global Lua function + - **description**: Pauses all AI processing for a specified number of seconds. Used during cinematics. Likely sets a countdown timer on TheAIClass or on the game clock that suppresses ServicedAISystemClass::Service calls. + + +### Diplomacy System + +- **description**: Diplomacy in the Alamo engine is NOT a continuous state machine — it is event-driven. Alliance/enemy changes are discrete events (AllyEventClass) sent through the ScheduledEventClass system. The PlayerObject stores diplomatic state accessed via Is_Enemy(player) and Is_Ally(player) Lua methods. Make_Ally/Make_Enemy are global Lua functions that fire AllyEventClass events. CRITICAL: diplomatic state resets on every game mode change (galactic<->tactical transition). +- **AllyEventClass**: + - **constructor_rva**: 0x689AD0 + - **inherits**: ScheduledEventClass + - **event_type_id**: 0x23 (35) + - **description**: Event fired when alliance state changes between two players. Field at data+0x00 initialized to 0xFFFFFFFF (target player ID, -1 = none). + - **fields**: {'offset': 'data+0x00', 'type': 'int32', 'name': 'target_player_id', 'value_on_init': '0xFFFFFFFF'}, {'offset': 'EventClass_data+0x08', 'type': 'int32', 'name': 'event_type', 'value': '0x23 = AllyEvent'} +- **player_diplomatic_methods**: + - **Is_Enemy**: + - **lua_string_rva**: 0x8AA468 + - **returns**: boolean + - **Is_Ally**: + - **lua_string_rva**: 0x8AA478 + - **returns**: boolean + - **Make_Ally**: + - **scope**: global Lua function + - **note**: Resets every game mode change + - **Make_Enemy**: + - **scope**: global Lua function + - **note**: Resets every game mode change +- **storage_model**: + - **description**: Diplomatic state is stored per-player in the PlayerClass/PlayerObject struct. The exact storage is likely a bitfield or array indexed by opposing player_id (up to 8 players). This is NOT stored in a separate diplomacy manager — it is embedded in each PlayerClass instance. The ~PlayerClass destructor (RVA 0x27ED40) cleans up multiple DynamicVectorClass arrays (10 of them in a loop) at offsets within PlayerClass_data — some of these likely hold per-opponent diplomatic flags. + - **player_class_destructor_rva**: 0x27ED40 + - **diplomatic_arrays_loop_count**: 10 + - **diplomatic_arrays_description**: 10 DynamicVectorClass arrays cleaned up in ~PlayerClass. These 10 slots match the 10-player capacity. Each likely stores diplomatic state for one opposing player. + + +### Difficulty System + +- **DifficultyAdjustmentManagerClass**: + - **constructor_rva**: 0x484B90 + - **description**: Database manager of DifficultyAdjustmentClass records. Inherits DatabaseUIntConversionClass. Uses a hash map for lookup. Each DifficultyAdjustmentClass record modifies AI parameters (production speed, income multiplier, combat bonuses, etc.) based on difficulty setting. + - **inherits**: DatabaseUIntConversionClass, DatabaseObjectManagerClass + - **hash_map_load_factor**: 1.0 + - **hash_map_initial_buckets**: 7 + - **fields**: {'offset': 'data+0x40', 'type': 'pointer', 'name': 'difficulty_name_string', 'description': 'SSO string (capacity 0x0F) for difficulty level name'}, {'offset': 'data+0x50', 'type': 'pointer', 'name': 'field_50'}, {'offset': 'data+0x58', 'type': 'int64', 'name': 'string_capacity', 'value': '0x0F'}, {'offset': 'data+0x60', 'type': 'int32', 'name': 'field_60'} + + +### Ai Diagnostics + +- **AIDiagnosticsClass**: + - **description**: Base class for all AI classes that support diagnostic logging. Provides debug info tracking. The AIDiagnosticsClass is the common ancestor of virtually every AI class — AIPlayerClass, all ServicedAISystemClass subclasses, AIBudgetClass, BudgetedCategoryStruct, InstantiatedGoalClass all inherit from it. + - **fields**: {'offset': 'data+0x18', 'type': 'int64', 'name': 'diagnostics_data', 'description': 'Diagnostic tracking field. Set to 0 in most constructors. Likely frame counter or tick timestamp.'} +- **AIDiagnosticsManagerClass**: + - **singleton_ctor_dtor_rva**: 0x7E6B80 + - **description**: Singleton managing all AI diagnostics. Receives diagnostic data from all AIDiagnosticsClass instances. +- **AILogWindow**: + - **constructor_rva**: 0x1AA10 + - **global_data_rva**: 0xB39140 + - **log_title**: AI Log + - **description**: Debug log window for AI. Inherits LogWindowItem. Initialized at static address 0xB39140 with atexit destructor. Title is 'AI Log'. + + +### Goal System Detail + +- **InstantiatedGoalClass**: + - **constructor_rva**: 0x691660 + - **description**: A live goal instance. Multiple inheritance: RefCountClass + SignalListenerClass + AIDiagnosticsClass. Contains references to goal type, owning system, priority, and state. + - **inherits**: RefCountClass, SignalListenerClass, AIDiagnosticsClass + - **fields**: {'offset': '0x40', 'type': 'pointer', 'name': 'field_40'}, {'offset': '0x48', 'type': 'pointer', 'name': 'field_48'}, {'offset': '0x50', 'type': 'pointer', 'name': 'field_50'}, {'offset': '0x58', 'type': 'int64', 'name': 'field_58'}, {'offset': '0x60', 'type': 'int64', 'name': 'field_60'}, {'offset': '0x68', 'type': 'int64', 'name': 'field_68'}, {'offset': '0x70', 'type': 'int64', 'name': 'field_70'}, {'offset': '0x78', 'type': 'int64', 'name': 'field_78'}, {'offset': '0x80', 'type': 'int64', 'name': 'field_80'}, {'offset': '0x88', 'type': 'pointer', 'name': 'param_1_ref', 'description': 'Constructor param_1 — likely the goal type or owning goal system'} +- **AIGoalProposalFunctionSetClass**: + - **destructor_rva**: 0x476CB0 + - **description**: Set of proposal scoring functions for a goal type. Managed by TheAIGoalProposalFunctionSetManagerClass. + + +### Target Location System + +- **description**: AI target locations are wrappers around game positions that the AI uses for goal targeting. +- **classes**: + - **PlanetAITargetLocationClass**: + - **constructor_rva**: 0x665470 + - **description**: Galactic-mode target: a planet. + - **TacticalRegionAITargetLocationClass**: + - **constructor_rva**: 0x725310 + - **description**: Tactical-mode target: a map region. + - **TacticalUnitAITargetLocationClass**: + - **constructor_rva**: 0x724B90 + - **description**: Tactical-mode target: a specific unit. + + +### Ai Hint Zones + +- **AIHintZoneClass**: + - **constructor_rva**: 0x5A8CC0 + - **description**: Map hint zones that guide AI behavior. Used by Find_Hint Lua function. Contains two hash maps (unit lists for zone). Fields at data+0xB8 = category_id (0xFFFFFFFF initially), data+0xBC = priority (1). + - **fields**: {'offset': 'data+0xB8', 'type': 'int32', 'name': 'category_id', 'value_on_init': '0xFFFFFFFF'}, {'offset': 'data+0xBC', 'type': 'int32', 'name': 'priority', 'value_on_init': 1} + + +### Execution Pathfinding + +- **AIExecutionPathFinderClass**: + - **constructor_rva**: 0x690BF0 + - **destructor_rva**: 0x690D90 + - **description**: Pathfinding for AI execution plans. Used by AIBuildTaskClass in PATHING_DEST states. + + +### Task Force System + +- **description**: Task forces are collections of units organized for coordinated action. Each game mode has its own task force class. +- **classes**: + - **TaskForceClass**: + - **destructor_rva**: 0x5F9440 + - **lua_wrapper_rva**: 0x5F8760 + - **GalacticTaskForceClass**: + - **lua_wrapper_rva**: 0x6CCE20 + - **LandTaskForceClass**: + - **lua_wrapper_rva**: 0x6E8B80 + - **SpaceTaskForceClass**: + - **lua_wrapper_rva**: 0x6E4810 + - **TaskForceDefinitionClass**: + - **constructor_rva**: 0x66F5D0 + - **description**: Template definition for a task force composition. + + +### Global References + +- **DAT_140b15418**: + - **rva**: 0xB15418 + - **description**: Global pointer checked in AIBuildTaskClass state machine. When non-null, calls vtable method at +0xE0 to get current game time. When null, falls back to DAT_140b153fc. This is the game clock / frame counter accessor. + - **usage**: AI production timing, state polling +- **DAT_140b153fc**: + - **rva**: 0xB153FC + - **description**: Fallback game state value when DAT_140b15418 is null. +- **DAT_140b168c8**: + - **rva**: 0xB168C8 + - **description**: Global int used as capacity threshold in BUILD_STATE_PATHING_DEST. Compared against FUN_1403f9530 result (current force count on a planet). + + +### Class Hierarchy Summary + +- **description**: Complete AI class inheritance tree as recovered from RTTI. +- **tree**: + - **AIDiagnosticsClass**: + - **children**: {'name': 'AIPlayerClass', 'description': 'Per-player AI controller'}, {'name': 'ServicedAISystemClass', 'children': [{'name': 'AIPerceptionSystemClass', 'children': [{'name': 'GalacticPerceptionSystemClass'}, {'name': 'LandPerceptionSystemClass'}, {'name': 'SpacePerceptionSystemClass'}]}, {'name': 'AIPlanningSystemClass'}, {'name': 'AIGoalSystemClass', 'children': [{'name': 'GalacticGoalSystemClass'}, {'name': 'LandGoalSystemClass'}, {'name': 'SpaceGoalSystemClass'}]}, {'name': 'AIExecutionSystemClass'}, {'name': 'AILearningSystemClass'}, {'name': 'AITemplateSystemClass'}]}, {'name': 'AIBudgetClass', 'children': [{'name': 'BudgetedCategoryStruct'}]}, {'name': 'InstantiatedGoalClass'}, {'name': 'TacticalAIManagerClass'} + + +### Lua Ai Bindings Summary + +- **global_functions**: {'name': 'Suspend_AI', 'params': 'seconds', 'description': 'Pauses all AI for N seconds'}, {'name': 'EvaluatePerception', 'params': '...', 'description': 'AI perception evaluation'}, {'name': 'GiveDesireBonus', 'params': '...', 'description': 'Modifies AI desire weights for goals'}, {'name': '_ProduceObject', 'params': 'player, type', 'description': 'AI production command (blocking)'}, {'name': 'FindTarget', 'params': 'evaluator, ...', 'description': 'AI target finder'}, {'name': 'Assemble_Fleet', 'params': 'player, planet, types', 'description': 'Assembles fleet at planet'} +- **player_methods**: {'name': 'Is_Enemy', 'params': 'player', 'returns': 'boolean'}, {'name': 'Is_Ally', 'params': 'player', 'returns': 'boolean'}, {'name': 'Enable_As_Actor', 'params': 'none', 'description': 'Enables AI actor mode for player'}, {'name': 'Is_Human', 'params': 'none', 'returns': 'boolean'}, {'name': 'IsHumanControlled', 'params': 'none', 'returns': 'boolean'}, {'name': 'Make_Ally', 'params': 'player', 'note': 'Resets on game mode change'}, {'name': 'Make_Enemy', 'params': 'player', 'note': 'Resets on game mode change'} +- **object_methods**: {'name': 'Prevent_AI_Usage', 'params': 'bool', 'note': 'Crashes in tactical if unit has no active AI'}, {'name': 'Set_As_Goal_System_Removable', 'params': 'bool', 'description': 'AI goal cleanup flag on TaskForceClass'} +- **free_store_methods**: {'name': 'Get_Object_Count', 'wrapper': 'LuaMemberFunctionWrapper'} + + +### Rva Index + +- **_note**: All RVAs relative to module base (0x140000000 in Ghidra). For runtime: address = module_base + rva. +- **constructors**: + - **TheAIClass**: 0x4D9C80 + - **AIPlayerClass**: 0x4AF810 + - **ServicedAISystemClass**: 0x64C250 + - **AIPerceptionSystemClass**: 0x4DAD80 + - **GalacticPerceptionSystemClass**: 0x4E1880 + - **LandPerceptionSystemClass**: 0x6B8980 + - **SpacePerceptionSystemClass**: 0x6B9A20 + - **TacticalPerceptionGridClass**: 0x653340 + - **AIPlanningSystemClass**: 0x6BAC00 + - **AIGoalSystemClass**: 0x6C7970 + - **GalacticGoalSystemClass**: 0x6B8480 + - **LandGoalSystemClass**: 0x6B86E0 + - **SpaceGoalSystemClass**: 0x6B88D0 + - **AIExecutionSystemClass**: 0x524CE0 + - **AILearningSystemClass**: 0x585D00 + - **AITemplateSystemClass**: 0x6BB9E0 + - **AIBudgetClass**: 0x6109C0 + - **BudgetedCategoryStruct**: 0x610A70 + - **AIBuildTaskClass**: 0x6478C0 + - **AIPlanetBuildTaskClass**: 0x6954B0 + - **InstantiatedGoalClass**: 0x691660 + - **TacticalAIManagerClass**: 0x5F4BE0 + - **DifficultyAdjustmentManagerClass**: 0x484B90 + - **AllyEventClass**: 0x689AD0 + - **AIHintZoneClass**: 0x5A8CC0 + - **AIExecutionPathFinderClass**: 0x690BF0 + - **ProduceForceBlockStatus**: 0x6C6500 + - **TheAIDataManagerClass**: 0x4747E0 + - **TheAIPlayerTypeManagerClass**: 0x4E9940 + - **TheAIGoalTypeManagerClass**: 0x5E6690 + - **TheAIGoalProposalFunctionSetManagerClass**: 0x5E5C30 + - **TheAITemplateManagerClass**: 0x4E8920 + - **AILogWindow**: 0x1AA10 + - **PlanetAITargetLocationClass**: 0x665470 + - **TacticalRegionAITargetLocationClass**: 0x725310 + - **TacticalUnitAITargetLocationClass**: 0x724B90 + - **TaskForceDefinitionClass**: 0x66F5D0 + - **FreeStoreMovementBlockStatus**: 0x71F630 +- **state_machine_handlers**: + - **AIBuildTaskClass_vfunction2**: 0x64AEC0 +- **singleton_ctors**: + - **SingletonInstance_TheAIClass**: 0x7EED80 + - **SingletonInstance_TheAIPlayerTypeManagerClass**: 0x7EED90 + - **SingletonInstance_AIDiagnosticsManagerClass**: 0x7E6B80 + - **SingletonInstance_TheAIDataManagerClass**: 0x7E6BA0 + - **SingletonInstance_GalacticPathFinderClass**: 0x7E6920 +- **destructors**: + - **PlayerClass**: 0x27ED40 + - **AIGoalSystemClass**: 0x6C7A30 + - **AIPerceptionSystemClass**: 0x4DAFD0 + - **AIFreeStoreClass**: 0x4D2780 + - **AIExecutionPathFinderClass**: 0x690D90 + - **TaskForceClass**: 0x5F9440 + diff --git a/docs/editor/systems/camera-selection.md b/docs/editor/systems/camera-selection.md new file mode 100644 index 000000000..ab7c8baa4 --- /dev/null +++ b/docs/editor/systems/camera-selection.md @@ -0,0 +1,439 @@ +# Camera Selection + +**Camera Globals Base**: 0xB1599C + +**Camera Class Ctor**: 0x261470 + +**Galactic Camera Update**: 0x3C2C00 + +**Game Mode Manager**: 0xB153E0 + +**Selection Event Id**: 5 + +**Control Group Event Id**: 30 + +**Select All Event Id**: 37 + + +## RE Findings Detail + +**Analysis**: SWFOC Camera & Selection System — Reverse Engineering Results + + +### Agent + +Agent 3I — Camera & Selection System RE + + +### Date + +2026-04-04 + + +### Binary + +StarWarsG.exe (x86_64, MSVC, ASLR enabled) + + +### Notes + +All RVAs relative to module base. All offsets relative to struct base pointer. Ghidra static analysis only (CE runtime access failed due to privilege/ASLR resolution issue). + + +### Globals + +- **GameModeManagerClass_singleton**: + - **rva**: 0xB153E0 + - **type**: GameModeManagerClass* + - **description**: Global singleton pointer to the GameModeManagerClass instance. This is the root of the game mode hierarchy. Contains an array of active GameModeClass instances (Galactic, Land, Space). Accessed by camera, selection, and event systems. + - **confidence**: CONFIRMED + - **evidence**: Referenced directly by SelectEventClass, SelectAllEventClass constructors (FUN_14028d930), GalacticCameraClass::Update (FUN_1403c2c00), and ControlGroupEventClass. + - **layout**: + - **+0x48**: pointer to array of GameModeClass* pointers + - **+0x50**: int32 count of active game modes + - **+0xB4**: int32 current selection mode state (written by event system) +- **DAT_140b15418**: + - **rva**: 0xB15418 + - **type**: pointer + - **description**: Secondary global pointer used by GalacticCameraClass::Update. Likely the GalacticModeClass singleton or active galactic state. Offset +0x18 leads to a sub-struct with +0x618 containing mode sub-state. + - **confidence**: DISCOVERED + - **evidence**: Read in FUN_1403c2c00 as guard condition for camera parameter selection. +- **screen_aspect_ratio**: + - **rva**: 0xA12550 + - **type**: float32 + - **description**: Current screen aspect ratio. Returned by FUN_14014ee30(). Used when setting camera FOV/aspect. + - **confidence**: CONFIRMED + - **evidence**: Called in GalacticCameraClass::Update and passed to Camera::SetFovAspect. +- **camera_globals_block**: + - **rva_base**: 0xB1599C + - **type**: float32[] block + - **description**: Block of galactic camera parameters controlling zoom, distance, and rotation. Used by GalacticCameraClass::Update. + - **confidence**: CONFIRMED + - **fields**: + - **0xB1599C**: + - **name**: galactic_camera_zoom_angle + - **type**: float32 + - **description**: Zoom rotation angle in radians for the galactic camera pitch. + - **0xB159A0**: + - **name**: galactic_camera_target_zoom_angle + - **type**: float32 + - **description**: Target zoom angle (interpolation target during smooth zoom). + - **0xB159A4**: + - **name**: galactic_camera_distance + - **type**: float32 + - **description**: Camera distance from look-at point (arm length). + - **0xB159A8**: + - **name**: galactic_camera_distance_scale + - **type**: float32 + - **description**: Multiplier for zoom distance during camera transitions. + - **0xB159AC**: + - **name**: galactic_camera_look_dir_x + - **type**: float32 + - **description**: X component of the galactic camera's normalized look direction. + - **0xB159B0**: + - **name**: galactic_camera_look_dir_y + - **type**: float32 + - **description**: Y component of the galactic camera's normalized look direction. + - **0xB159B4**: + - **name**: galactic_camera_look_dir_z + - **type**: float32 + - **description**: Z component of the galactic camera's normalized look direction. + - **0xB159C8**: + - **name**: galactic_camera_mode_value + - **type**: float32 + - **description**: Camera mode value copied to GalacticCameraClass +0x08. + + +### Structs + +- **GameModeManagerClass**: + - **description**: Singleton manager for all game modes. Holds the array of active GameModeClass instances. The current tactical/galactic mode can be retrieved by iterating the array and matching the mode type via vtable+0xE0. + - **global_rva**: 0xB153E0 + - **global_note**: This is a POINTER to the instance; dereference once. + - **confidence**: CONFIRMED + - **fields**: {'offset': '0x00', 'type': 'int64', 'name': 'unknown_0x00', 'description': 'Set to -1 in destructor.'}, {'offset': '0x08', 'type': 'int64', 'name': 'unknown_0x08', 'description': 'Cleared in destructor.'}, {'offset': '0x48', 'type': 'pointer', 'name': 'game_mode_array', 'description': 'Pointer to array of GameModeClass* pointers.'}, {'offset': '0x50', 'type': 'int32', 'name': 'game_mode_count', 'description': 'Number of entries in game_mode_array.'}, {'offset': '0xB4', 'type': 'int32', 'name': 'selection_state', 'description': 'Selection system state integer. Written by SelectEvent and SelectAllEvent handlers.'} + - **method_GetModeByType**: + - **rva**: 0x28A950 + - **signature**: GameModeClass* GetModeByType(GameModeManagerClass* this, int modeType) + - **description**: Iterates game_mode_array, calls vtable[0xE0/8] on each to get its type, returns the one matching modeType. Known types: 0=Galactic, others TBD for Land/Space. + - **confidence**: CONFIRMED +- **GameModeClass**: + - **description**: Base class for all game modes (Galactic, Land, Space). Contains the camera, selection state, objectives, control groups, and the scene graph for the current mode. This is a very large struct (0x320+ bytes for base, subclasses extend further). + - **vtable_rva_galactic**: from GalacticModeClass at 0x4B1270 + - **vtable_rva_land**: from LandModeClass at 0x3B5210 + - **vtable_rva_space**: from SpaceModeClass at 0x4D6BA0 + - **confidence**: CONFIRMED + - **key_fields**: {'offset': '0x00', 'type': 'pointer', 'name': 'vtable_ptr'}, {'offset': '0x08', 'type': 'int64', 'name': 'mode_id', 'description': 'Set to -1 initially.'}, {'offset': '0x10', 'type': 'float32', 'name': 'game_speed_unknown', 'description': 'Initialized to 0.'}, {'offset': '0x14', 'type': 'float32', 'name': 'game_speed_factor', 'description': 'Initialized to 1.0 (0x3f800000).'}, {'offset': '0x28', 'type': 'pointer', 'name': 'camera_ptr', 'description': 'Pointer to the CameraClass (SceneClass) for this mode. This is the primary camera used for rendering. Ref-counted (Release called in destructor at this[5]).'}, {'offset': '0x30', 'type': 'pointer', 'name': 'unknown_ptr_0x30'}, {'offset': '0x38', 'type': 'pointer', 'name': 'unknown_ptr_0x38'}, {'offset': '0x40', 'type': 'pointer', 'name': 'unknown_ptr_0x40'}, {'offset': '0x90', 'type': 'pointer', 'name': 'scene_camera_ptr', 'description': 'Ref-counted pointer to CameraClass/SceneClass. Used by GalacticCameraClass::Update to get the active camera for the mode. Read as *(mode + 0x90). The CameraClass at this pointer has the actual camera transform sub-object.'}, {'offset': '0x190', 'type': 'pointer', 'name': 'selection_object_array_ptr', 'description': 'Pointer to array of pointers to selected GameObjectClass instances. Part of the DynamicVector at +0x190 area.'}, {'offset': '0x1A0', 'type': 'pointer', 'name': 'large_sub_object_ptr', 'description': 'Allocated 0x1390-byte sub-object. Likely TerrainClass or MapClass.'}, {'offset': '0x228', 'type': 'float32', 'name': 'map_size_x', 'description': 'Initialized to 10000.0 (0x461c4000).'}, {'offset': '0x22C', 'type': 'float32', 'name': 'map_size_y', 'description': 'Initialized to 10000.0.'}, {'offset': '0x230', 'type': 'float32', 'name': 'zoom_factor_1', 'description': 'Initialized to 1.0.'}, {'offset': '0x234', 'type': 'float32', 'name': 'zoom_factor_2', 'description': 'Initialized to 1.0.'} + - **notes**: The exact offset for the camera depends on the access pattern. In the GalacticCameraClass::Update, the mode pointer + 0x90 is used to get the camera. In the destructor, this[0x12] (offset 0x90) is the ref-counted camera object. This confirms offset 0x90. +- **CameraClass**: + - **description**: The Alamo engine's camera class. Contains the camera orientation matrix directly at +0x10 and a pointer to a 0x308-byte camera state sub-object at +0x40. The sub-object holds the full projection state, view matrices, viewport, and clip planes. + - **constructor_rva**: 0x261470 + - **confidence**: CONFIRMED + - **outer_fields**: {'offset': '0x00', 'type': 'pointer', 'name': 'vtable_ptr'}, {'offset': '0x04', 'type': 'int32', 'name': 'ref_count', 'description': 'RefCountClass reference count.'}, {'offset': '0x10', 'type': 'float32[12]', 'name': 'orientation_matrix_copy', 'description': 'Copy of the 3x4 orientation matrix (row-major, 12 floats).'}, {'offset': '0x40', 'type': 'pointer', 'name': 'camera_state_ptr', 'description': 'Pointer to the 0x308-byte camera state sub-object. This is the LIVE camera state that the renderer reads.'} + - **camera_state_sub_object**: + - **size**: 0x308 + - **alloc_rva**: 0x140100 (called from constructor) + - **description**: The core camera state object. All camera parameters are here. + - **fields**: {'offset': '0x00', 'type': 'float32[12]', 'name': 'orientation_matrix', 'description': '3x4 column-major orientation matrix. Layout: [right.x, up.x, fwd.x, pos.x, right.y, up.y, fwd.y, pos.y, right.z, up.z, fwd.z, pos.z]. Each column is 3 floats at stride 0x10.', 'sub_fields': {'right_vector': {'offsets': ['0x00', '0x10', '0x20'], 'description': 'Camera right vector (X axis)'}, 'up_vector': {'offsets': ['0x04', '0x14', '0x24'], 'description': 'Camera up vector (Y axis)'}, 'forward_vector': {'offsets': ['0x08', '0x18', '0x28'], 'description': 'Camera forward vector (Z axis, into screen)'}, 'position': {'offsets': ['0x0C', '0x1C', '0x2C'], 'description': 'Camera world position (X, Y, Z)'}}, 'confidence': 'CONFIRMED', 'evidence': 'GetPosition reads +0x0C, +0x1C, +0x2C. GetForward reads +0x08, +0x18 (negated). TranslateLocal multiplies delta by matrix and adds to position columns.'}, {'offset': '0x30', 'type': 'float32', 'name': 'near_clip', 'description': 'Near clipping plane distance. Default: 1.0 (0x3f800000).', 'confidence': 'CONFIRMED', 'evidence': 'Read and written by SetClipPlanes/GetClipPlanes. Used in projection matrix build.'}, {'offset': '0x34', 'type': 'float32', 'name': 'far_clip', 'description': 'Far clipping plane distance. Default: 1000.0 (0x447a0000).', 'confidence': 'CONFIRMED'}, {'offset': '0x38', 'type': 'float32', 'name': 'far_clip_bias', 'description': 'Bias factor for far clip. Default: -1.0 (0xbf800000). When != -1.0, an alternate far distance is used.', 'confidence': 'DISCOVERED'}, {'offset': '0x3C', 'type': 'float32', 'name': 'alt_far_clip', 'description': 'Alternate far clip distance. Used when far_clip_bias != -1.0. Default: -1.0.', 'confidence': 'DISCOVERED'}, {'offset': '0x44', 'type': 'int32', 'name': 'projection_mode', 'description': '0 = perspective projection, 1 = orthographic projection. Controls which projection math path is used.', 'confidence': 'CONFIRMED', 'evidence': 'Checked in FUN_1401417c0 and FUN_140141260. SetOrthoProjection sets this to 1.'}, {'offset': '0x48', 'type': 'float32', 'name': 'fov_or_half_angle', 'description': 'For perspective: FOV angle in radians. Default: pi/4 = 0.785 rad = 45 degrees (0x3f490fdb). For ortho: half-width.', 'confidence': 'CONFIRMED', 'evidence': 'Initialized to 0x3f490fdb (pi/4). Read by GetFov, written by SetFovAspect. Used with tan() in projection matrix.'}, {'offset': '0x4C', 'type': 'float32', 'name': 'aspect_ratio', 'description': 'Viewport aspect ratio. Default: 1.333 = 4:3 (0x3faaaaab). Set by SetFovAspect.', 'confidence': 'CONFIRMED', 'evidence': 'Initialized to 0x3faaaaab. Read by GetFov/GetAspect.'}, {'offset': '0x50', 'type': 'float32', 'name': 'ortho_left', 'description': 'Left edge for orthographic projection.', 'confidence': 'CONFIRMED'}, {'offset': '0x54', 'type': 'float32', 'name': 'ortho_top', 'description': 'Top edge for orthographic projection.', 'confidence': 'CONFIRMED'}, {'offset': '0x58', 'type': 'float32', 'name': 'ortho_right', 'description': 'Right edge for orthographic projection.', 'confidence': 'CONFIRMED'}, {'offset': '0x5C', 'type': 'float32', 'name': 'ortho_bottom', 'description': 'Bottom edge for orthographic projection.', 'confidence': 'CONFIRMED'}, {'offset': '0x60', 'type': 'uint8', 'name': 'user_clip_enabled', 'description': 'Non-zero enables a user-defined clip plane.', 'confidence': 'DISCOVERED'}, {'offset': '0x64', 'type': 'float32[4]', 'name': 'user_clip_plane', 'description': 'User-defined clip plane coefficients (a, b, c, d). Applied when user_clip_enabled != 0.', 'confidence': 'DISCOVERED'}, {'offset': '0x74', 'type': 'float32', 'name': 'viewport_x', 'description': 'Viewport origin X (normalized 0-1).', 'confidence': 'CONFIRMED'}, {'offset': '0x78', 'type': 'float32', 'name': 'viewport_y', 'description': 'Viewport origin Y (normalized, 1.0 - input).', 'confidence': 'CONFIRMED'}, {'offset': '0x7C', 'type': 'float32', 'name': 'viewport_width', 'description': 'Viewport width (normalized 0-1).', 'confidence': 'CONFIRMED'}, {'offset': '0x80', 'type': 'float32', 'name': 'viewport_height', 'description': 'Viewport height (normalized, 1.0 - input).', 'confidence': 'CONFIRMED'}, {'offset': '0x84', 'type': 'float32[12]', 'name': 'inverse_orientation_matrix', 'description': 'Computed inverse of the orientation matrix. Updated by FUN_14022d390.', 'confidence': 'DISCOVERED'}, {'offset': '0xB4', 'type': 'float32[16]', 'name': 'projection_matrix', 'description': '4x4 projection matrix (perspective or ortho). Rebuilt by FUN_140141260.', 'confidence': 'CONFIRMED'}, {'offset': '0xF4', 'type': 'float32[16+]', 'name': 'view_projection_matrix', 'description': 'Combined view-projection matrix for the near clip. Built by FUN_140232c50 (perspective) or FUN_140232df0 (ortho).', 'confidence': 'CONFIRMED'}, {'offset': '0x1FC', 'type': 'float32[16+]', 'name': 'far_view_projection_matrix', 'description': 'Combined view-projection matrix for the far clip distance. Used for far-field rendering.', 'confidence': 'CONFIRMED'}, {'offset': '0x304', 'type': 'uint8', 'name': 'dirty_flag', 'description': 'Cleared (set to 0) during init.', 'confidence': 'DISCOVERED'} + - **initial_values**: + - **orientation**: identity matrix + - **position**: (0, 0, -60) — offset 0x0C = 0xc2700000 = -60.0f + - **near_clip**: 1.0 + - **far_clip**: 1000.0 + - **fov**: pi/4 (45 degrees) + - **aspect**: 1.333 (4:3) + - **methods**: + - **GetTransformMatrix**: + - **rva**: 0x2619F0 + - **signature**: void GetTransformMatrix(CameraClass* this, float* outMatrix12) + - **description**: Copies the 12-float orientation matrix from the sub-object. + - **confidence**: CONFIRMED + - **SetTransformMatrix**: + - **rva**: 0x261BD0 + - **signature**: void SetTransformMatrix(CameraClass* this, float* inMatrix12) + - **description**: Sets the orientation matrix (12 floats at sub-object +0x00 to +0x2C). Recalculates the inverse matrix and view-projection matrices. + - **confidence**: CONFIRMED + - **GetPosition**: + - **rva**: 0x261A40 + - **signature**: void GetPosition(CameraClass* this, float* outXYZ) + - **description**: Reads camera world position from matrix offsets +0x0C, +0x1C, +0x2C of the sub-object. + - **confidence**: CONFIRMED + - **GetForwardDirection**: + - **rva**: 0x261690 + - **signature**: void GetForwardDirection(CameraClass* this, float* outFwd3, float* outUp3) + - **description**: Gets the negated forward vector from matrix offsets +0x08, +0x18 (XOR sign bit). Also returns a fixed up vector (0, -1, 0). + - **confidence**: CONFIRMED + - **TranslateLocal**: + - **rva**: 0x261C90 + - **signature**: void TranslateLocal(CameraClass* this, float* deltaXYZ) + - **description**: Translates the camera in local space. Multiplies the 3D delta by the orientation matrix axes and adds to position. Recalculates inverse and view-proj matrices. + - **confidence**: CONFIRMED + - **GetFovAspect**: + - **rva**: 0x2618E0 + - **signature**: void GetFovAspect(CameraClass* this, float* outFov, float* outAspect) + - **description**: Reads FOV from sub-object +0x48 and aspect ratio from +0x4C. + - **confidence**: CONFIRMED + - **SetFovAspect**: + - **rva**: 0x261A80 + - **signature**: void SetFovAspect(CameraClass* this, float fov, float aspect) + - **description**: Sets FOV (+0x48), aspect (+0x4C), clears projection_mode to 0, rebuilds projection and view-proj matrices. + - **confidence**: CONFIRMED + - **SetPerspectiveProjection**: + - **rva**: 0x261AB0 + - **signature**: void SetPerspectiveProjection(CameraClass* this, float left, float right, float top, float bottom, float nearClip, float farClip) + - **description**: Sets perspective projection from frustum edges. Calculates FOV and aspect from edges, sets clip planes at +0x30/+0x34. + - **confidence**: CONFIRMED + - **SetOrthoProjection**: + - **rva**: 0x261B50 + - **signature**: void SetOrthoProjection(CameraClass* this, float left, float top, float right, float bottom, float nearClip, float farClip) + - **description**: Sets orthographic projection. Stores bounds at +0x50..+0x5C, sets projection_mode=1, rebuilds matrices. + - **confidence**: CONFIRMED + - **SetViewport**: + - **rva**: 0x261E00 + - **signature**: void SetViewport(CameraClass* this, float x, float width, float y, float height) + - **description**: Sets viewport rect at sub-object +0x74..+0x80. Y and height are inverted (1.0 - input). + - **confidence**: CONFIRMED + - **GetFrustumBounds**: + - **rva**: 0x261900 + - **signature**: void GetFrustumBounds(CameraClass* this, float* top, float* bottom, float* left, float* right, float* nearClip, float* farClip) + - **description**: Returns the frustum bounds. For perspective, calculates from FOV/aspect using tan(). For ortho, reads directly from +0x50..+0x5C. + - **confidence**: CONFIRMED + - **GetViewport**: + - **rva**: 0x261870 + - **signature**: void GetViewport(CameraClass* this, bool p2, bool p3, bool p4) + - **description**: Reads viewport rect from sub-object +0x74..+0x80 and passes to FUN_14013d030. + - **confidence**: DISCOVERED + - **CopyFrom**: + - **rva**: 0x261590 + - **signature**: CameraClass* CopyFrom(CameraClass* this, CameraClass* other) + - **description**: Copies camera state from another CameraClass instance. Copies inner sub-object and outer matrix. + - **confidence**: CONFIRMED + - **Render**: + - **rva**: 0x2611C0 + - **signature**: bool Render(CameraClass* this, int pass) + - **description**: Iterates the scene model list, calling Prepare then Render on each model. + - **confidence**: DISCOVERED +- **GalacticCameraClass**: + - **description**: Camera controller for the galactic (strategy) map. Manages the orbital camera that orbits around a look-at point with zoom, pitch, and rotation. Reads global camera parameters and interpolates between them during transitions. + - **constructor_rva**: 0x3C2B20 + - **update_rva**: 0x3C2C00 + - **confidence**: CONFIRMED + - **fields**: {'offset': '0x00', 'type': 'pointer', 'name': 'vtable_ptr'}, {'offset': '0x08', 'type': 'float32', 'name': 'camera_mode_value', 'description': 'Copied from DAT_140b159c8 after update.'}, {'offset': '0x0C', 'type': 'float32', 'name': 'look_at_x', 'description': 'Look-at target X position in world space.'}, {'offset': '0x10', 'type': 'float32', 'name': 'look_at_y', 'description': 'Look-at target Y position.'}, {'offset': '0x14', 'type': 'float32', 'name': 'look_at_z', 'description': 'Look-at target Z position.'}, {'offset': '0x18', 'type': 'pointer', 'name': 'camera_class_ptr', 'description': 'Pointer to the CameraClass that this controller drives. Obtained from GameMode+0x90.'}, {'offset': '0x28', 'type': 'float32', 'name': 'target_look_x', 'description': 'Interpolation target for look_at_x.'}, {'offset': '0x2C', 'type': 'float32', 'name': 'target_look_y', 'description': 'Interpolation target for look_at_y.'}, {'offset': '0x30', 'type': 'float32', 'name': 'target_look_z', 'description': 'Interpolation target for look_at_z.'}, {'offset': '0x34', 'type': 'float32', 'name': 'orbit_distance', 'description': 'Distance from look-at point (zoom arm length). Multiplied by look direction globals.'}, {'offset': '0x3C', 'type': 'float32', 'name': 'transition_timer', 'description': 'Timer for smooth camera transitions. Counts down toward 0.'}, {'offset': '0x44', 'type': 'int32', 'name': 'update_state', 'description': 'Camera update state machine. Values 1-2 = transitioning, 3+ = free orbit mode.'}, {'offset': '0x4C', 'type': 'float32', 'name': 'custom_camera_distance', 'description': 'Custom distance override used when mode sub-state <= 2.'}, {'offset': '0x50', 'type': 'float32', 'name': 'custom_zoom_angle', 'description': 'Custom zoom rotation angle override.'} + - **update_function**: + - **rva**: 0x3C2C00 + - **description**: Main update function called each frame. 1) Gets current GameMode via GetModeByType(0). 2) Gets the CameraClass from mode+0x90. 3) Depending on update_state: interpolates position/zoom between current and target, or maintains free orbit. 4) Builds orientation matrix with rotation around X axis (pitch from zoom angle). 5) Sets the camera transform matrix and FOV/aspect. + - **camera_position_calculation**: The camera position is calculated as: position = look_at_point + rotation_matrix * (look_direction * distance). The look direction comes from globals at 0xB159AC/B0/B4. The rotation is built using FUN_1402cf8d0 (matrix rotation by angle). + - **confidence**: CONFIRMED +- **SelectEventClass**: + - **description**: Event dispatched when the player selects a unit/object. Inherits from ScheduledEventClass. Event type ID = 5. References the GameModeManagerClass singleton. + - **constructor_rva**: 0x3AC9D0 + - **event_type**: 5 + - **confidence**: CONFIRMED + - **fields**: {'offset': '0x1FC (from SelectEventClass_data base)', 'type': 'int32', 'name': 'selection_data', 'description': 'Initialized to 0. Likely the object ID or selection flags.'} + - **global_reference**: Writes to GameModeManagerClass+0xB4 via FUN_14028d930. +- **SelectAllEventClass**: + - **description**: Event dispatched for 'select all units' command. Event type ID = 37. + - **constructor_rva**: 0x437F40 + - **event_type**: 37 + - **confidence**: CONFIRMED + - **global_reference**: Same as SelectEventClass — writes to GameModeManagerClass+0xB4. +- **ControlGroupEventClass**: + - **description**: Event for control group assign/recall (Ctrl+1..9 / 1..9). Event type ID = 30. + - **constructor_rva**: 0x436060 + - **event_type**: 30 + - **confidence**: CONFIRMED + - **fields**: {'offset': '0x00', 'type': 'int32', 'name': 'group_number', 'description': 'Control group index (0-9).'}, {'offset': '0x04', 'type': 'int32', 'name': 'action', 'description': '0 = assign to group, 1 = recall group (TBD).'} +- **LookEventClass**: + - **description**: Event dispatched when camera should look at a target (corresponds to Lua Point_Camera_At). Event type ID = 4. + - **constructor_rva**: 0x689670 + - **event_type**: 4 + - **confidence**: CONFIRMED +- **SelectionDataPackClass**: + - **description**: Per-object selection state data pack. Attached to GameObjectClass instances that can be selected. Pool-allocated (pool size 20). Contains 5 ref-counted pointers and selection state. + - **constructor_rva**: 0x55BE30 + - **destructor_rva**: 0x55BE70 + - **pool_size**: 20 + - **confidence**: CONFIRMED + - **fields**: {'offset': '0x00', 'type': 'pointer (ref-counted)', 'name': 'selection_ref_0', 'description': 'First ref-counted pointer. Released in destructor.'}, {'offset': '0x08', 'type': 'pointer (ref-counted)', 'name': 'selection_ref_1'}, {'offset': '0x10', 'type': 'pointer (ref-counted)', 'name': 'selection_ref_2'}, {'offset': '0x18', 'type': 'pointer (ref-counted)', 'name': 'selection_ref_3'}, {'offset': '0x20', 'type': 'pointer (ref-counted)', 'name': 'selection_ref_4'}, {'offset': '0x28', 'type': 'int32', 'name': 'selection_state', 'description': 'Initialized to 1. Selection state flags.'}, {'offset': '0x30', 'type': 'msvc_sso_string', 'name': 'selection_group_name', 'description': 'String field (SSO). Freed in destructor via FID_conflict_Tidy_deallocate.'}, {'offset': '0x48', 'type': 'int32', 'name': 'selection_type', 'description': 'Initialized to 7. Type/category of selection.'} +- **SelectBehaviorClass**: + - **description**: Behavior component attached to selectable GameObjectClass instances. Handles the selection logic (highlight, UI feedback, etc.). Size = 0x58 bytes. Inherits from BehaviorClass. + - **destructor_rva**: 0x3C1960 + - **size**: 0x58 + - **confidence**: CONFIRMED + + +### Functions + +- **D3DXMatrixPerspectiveFovLH**: + - **rva**: 0x7A882D + - **description**: D3DX import for building a left-handed perspective projection matrix from FOV. Called by the camera projection rebuild functions. + - **confidence**: CONFIRMED +- **D3DXMatrixPerspectiveFovRH**: + - **rva**: 0x7A87D3 + - **description**: D3DX import for right-handed perspective projection. + - **confidence**: CONFIRMED +- **D3DXMatrixLookAtLH**: + - **rva**: 0x7A8827 + - **description**: D3DX import for building a left-handed view matrix from eye/target/up. Used in camera view matrix construction. + - **confidence**: CONFIRMED +- **BuildPerspectiveViewProjectionMatrix**: + - **rva**: 0x232C50 + - **description**: Builds a combined view-projection matrix for perspective cameras. Copies orientation, applies frustum signs, generates the combined matrix. + - **confidence**: CONFIRMED +- **BuildOrthoViewProjectionMatrix**: + - **rva**: 0x232DF0 + - **description**: Builds a combined view-projection matrix for orthographic cameras. + - **confidence**: CONFIRMED +- **BuildProjectionMatrix_Perspective**: + - **rva**: 0x232220 + - **description**: Builds the 4x4 perspective projection matrix from FOV, aspect, near, far. Uses tan() for FOV conversion. + - **confidence**: CONFIRMED +- **RebuildProjectionMatrix**: + - **rva**: 0x141260 + - **description**: Rebuilds the projection matrix based on current projection_mode (0=perspective, 1=ortho). Dispatches to the appropriate builder. + - **confidence**: CONFIRMED +- **RebuildViewProjectionMatrices**: + - **rva**: 0x1417C0 + - **description**: Rebuilds both near and far view-projection matrices. Called after any camera parameter change. + - **confidence**: CONFIRMED +- **MatrixRotateByAngle**: + - **rva**: 0x2CF8D0 + - **description**: Applies a rotation by angle (radians) to a 3x3 rotation matrix. Uses sin/cos of the angle. Used by GalacticCameraClass for pitch/yaw rotation. + - **confidence**: CONFIRMED +- **CameraStateInit**: + - **rva**: 0x140100 + - **description**: Initializes the 0x308-byte camera state sub-object with identity matrix, default FOV (pi/4), aspect (4:3), near clip (1.0), far clip (1000.0), and default position (0, 0, -60). + - **confidence**: CONFIRMED +- **GetScreenAspectRatio**: + - **rva**: 0x14EE30 + - **description**: Returns the global screen aspect ratio from DAT_140a12550. + - **confidence**: CONFIRMED +- **GetModeByType**: + - **rva**: 0x28A950 + - **description**: Iterates GameModeManagerClass's mode array, returning the GameModeClass* matching the given type ID. Called with type 0 for galactic mode. + - **confidence**: CONFIRMED +- **SetSelectionState**: + - **rva**: 0x28D930 + - **description**: Sets the selection state integer at GameModeManagerClass+0xB4. Called by SelectEventClass and SelectAllEventClass constructors. + - **confidence**: CONFIRMED + + +### Access Patterns + +- **get_current_camera_position**: + - **description**: How to read the active camera's world position at runtime. + - **chain**: 1. Read GameModeManagerClass pointer at [module_base + 0xB153E0], 2. Call GetModeByType(mode_mgr, 0) to get the current galactic GameModeClass (or iterate to find active), 3. Read CameraClass pointer at [GameModeClass + 0x90], 4. Read camera_state_ptr at [CameraClass + 0x40], 5. Position X = float at [camera_state + 0x0C], 6. Position Y = float at [camera_state + 0x1C], 7. Position Z = float at [camera_state + 0x2C] + - **pointer_chain_notation**: [module+0xB153E0] -> GetModeByType(0) -> +0x90 -> +0x40 -> {+0x0C, +0x1C, +0x2C} + - **confidence**: CONFIRMED +- **get_camera_zoom_level**: + - **description**: How to read the camera's zoom/FOV. + - **chain**: 1-4. Same as position chain above, 5. FOV = float at [camera_state + 0x48] (radians), 6. Aspect = float at [camera_state + 0x4C] + - **confidence**: CONFIRMED +- **get_camera_rotation**: + - **description**: Camera orientation is encoded in the 3x4 matrix at camera_state +0x00. The forward direction is the Z column negated: forward = (-[+0x08], -[+0x18], 0). For the galactic camera, the rotation angle is stored in the GalacticCameraClass globals at 0xB1599C (pitch angle in radians). + - **confidence**: CONFIRMED +- **free_camera_unlock_mechanism**: + - **description**: The GalacticCameraClass::Update (RVA 0x3C2C00) has an update_state field at +0x44. When update_state >= 4, the camera operates in free orbit mode with no position constraints. Setting update_state to a high value (e.g., 5) and writing custom look-at coordinates (+0x0C/+0x10/+0x14) and orbit distance (+0x34) would effectively unlock the camera. For a full free camera: bypass GalacticCameraClass::Update entirely by NOPing the call, then write the camera_state sub-object's orientation matrix directly. + - **nop_approach**: Find the call to GalacticCameraClass::Update and NOP it. Then write the camera_state transform matrix (12 floats at sub-object +0x00) directly each frame. + - **confidence**: DISCOVERED + - **status**: Theoretical — needs runtime validation +- **get_selected_unit**: + - **description**: The selection system works through the event mechanism. SelectEventClass (type 5) and SelectAllEventClass (type 37) write to GameModeManagerClass+0xB4. The actual selected object list is maintained per-GameModeClass in the DynamicVector structures within the mode. The GameModeClass constructor at offset this[0x38] (0x1C0) initializes a DynamicVectorClass> which holds per-player selection groups. + - **chain**: 1. Read GameModeManagerClass pointer at [module_base + 0xB153E0], 2. Get active GameModeClass via GetModeByType, 3. The selection state is at GameModeClass — the DynamicVector> at approximately offset 0x1C0-0x1D8 holds per-player selection lists, 4. For the local player, index into the outer vector by player slot, 5. The inner DynamicVectorClass contains GameObjectClass* pointers to selected units + - **confidence**: DISCOVERED + - **notes**: Exact offset for the selection vector needs runtime verification. The GameModeClass constructor shows DynamicVectorClass> at this[0x3B] (offset 0x1D8) with initialization using DAT_140a16fb0 (which is the max player count). +- **selection_via_selectbehavior**: + - **description**: Individual objects have a SelectBehaviorClass (size 0x58) attached as a behavior component. To check if a specific GameObjectClass is selected, query its SelectionDataPackClass data pack. The data pack is attached through the standard data-pack system. + - **confidence**: DISCOVERED + + +### D3D Engine Params + +- **description**: The Alamo engine uses named engine parameters for shader uniforms. These are factory classes that provide transform matrices to the rendering pipeline. +- **params**: + - **Get_D3d_View_Transform**: + - **rva**: 0x1ADC70 + - **description**: Provides the camera view matrix to shaders. + - **Get_D3d_Projection_Transform**: + - **rva**: 0x1ADCF0 + - **description**: Provides the projection matrix. + - **Get_D3d_View_Inverse_Transform**: + - **rva**: 0x1ADD70 + - **description**: Provides the inverse view matrix (camera-to-world). + - **Get_D3d_View_Projection_Transform**: + - **rva**: 0x1ADDF0 + - **description**: Provides the combined view-projection matrix. + - **Get_D3d_Model_Transform**: + - **rva**: 0x1ADBF0 + - **description**: Provides the model (world) matrix. + - **Get_D3d_Model_View_Transform**: + - **rva**: 0x1ADF70 + - **description**: Provides the combined model-view matrix. + - **Get_D3d_Model_View_Projection_Transform**: + - **rva**: 0x1ADFF0 + - **description**: Provides the MVP matrix. + - **Get_D3d_Model_Inverse_Transform**: + - **rva**: 0x1ADE70 + - **description**: Provides the inverse model matrix. + - **Get_D3d_Model_Inverse_Transpose_Transform**: + - **rva**: 0x1ADEF0 + - **description**: Provides the inverse-transpose model matrix (for normals). + - **Get_D3d_Model_View_Inverse_Transform**: + - **rva**: 0x1AE070 + - **description**: Provides the inverse model-view matrix. +- **confidence**: CONFIRMED + + +### Class Hierarchy + +- **CameraClass**: + - **inherits**: RefCountClass + - **rtti**: .?AVCameraClass@@ +- **GalacticCameraClass**: + - **inherits**: unknown + - **rtti**: .?AVGalacticCameraClass@@ + - **description**: Camera controller for galactic map. Does NOT inherit from CameraClass. It DRIVES a CameraClass instance. +- **GameModeClass**: + - **inherits**: SignalGeneratorClass + - **subclasses**: LandModeClass, SpaceModeClass, GalacticModeClass +- **SelectEventClass**: + - **inherits**: ScheduledEventClass, EventClass +- **SelectAllEventClass**: + - **inherits**: ScheduledEventClass, EventClass +- **SelectBehaviorClass**: + - **inherits**: BehaviorClass + - **size**: 0x58 +- **SelectionDataPackClass**: + - **inherits**: PooledObjectClass + - **pool_size**: 20 + + +### Lua Function Mapping + +- **Point_Camera_At**: + - **event_class**: LookEventClass + - **event_type_id**: 4 + - **description**: Dispatched as a LookEvent that sets the camera look-at target. +- **Scroll_Camera_To**: + - **description**: Likely uses the same GalacticCameraClass target interpolation. Sets target_look_x/y/z and starts a transition (update_state = 1 or 2). + - **confidence**: DISCOVERED +- **Zoom_Camera**: + - **description**: Modifies the galactic camera globals at 0xB1599C (zoom angle) and 0xB159A4 (distance). The GalacticCameraClass::Update interpolates to these values. + - **confidence**: DISCOVERED +- **Rotate_Camera_By**: + - **description**: Likely modifies the look direction globals at 0xB159AC/B0/B4 or applies a rotation to the GalacticCameraClass state. + - **confidence**: DISCOVERED +- **Select_Object**: + - **event_class**: SelectEventClass + - **event_type_id**: 5 + - **description**: Dispatched as a SelectEvent. Writes to GameModeManagerClass+0xB4. + + +### Next Steps + +- RUNTIME VALIDATION: Attach CE with admin privileges or use the DLL bridge to resolve ASLR base and validate all offsets at runtime. +- SELECTION VECTOR: Use runtime breakpoints on FUN_14028d930 to trace the exact selection list storage within GameModeClass. The DynamicVector> at GameModeClass+0x1D8 is the most likely candidate. +- LAND/SPACE CAMERAS: LandModeClass and SpaceModeClass have their own camera controllers (not GalacticCameraClass). Decompile their update functions to map the tactical camera parameters. +- FREE CAMERA: Implement free camera by: (a) finding the call to GalacticCameraClass::Update, (b) NOPing it, (c) writing the camera_state sub-object matrix directly via the DLL bridge. +- SCREEN-TO-WORLD: The inverse view-projection matrix at camera_state+0x84 (or the engine param at RVA 0x1ADD70) provides the camera-to-world transform. Multiply a screen-space ray by this inverse to get world coordinates. + diff --git a/docs/editor/systems/combat.md b/docs/editor/systems/combat.md new file mode 100644 index 000000000..3f01ac45c --- /dev/null +++ b/docs/editor/systems/combat.md @@ -0,0 +1,509 @@ +# Combat + + +### Damage Pipeline Stages + +- Weapon Fire +- Invulnerability Gate +- Damage Routing +- HP Subtraction +- SetHP + +**Sethp Rva**: 0x3A89D0 + +**Sethp Callers Count**: 18 + +**Max Health Formula**: base_hp(type+0xDCC) * difficulty_mult * player_mult * (1.0 + ability_bonus) + + +### Shield System + +| Key | Value | +|-----|-------| +| front | {'qi_id': '0x0F', 'set': '0x3A8630', 'storage': '[obj+0xF0]+0xF8', 'max_from': 'type+0xDD0'} | +| rear | {'qi_id': '0x10', 'set': '0x3A91E0', 'storage': '[obj+0xF0]+0xFC', 'max_from': 'type+0xDD4'} | + +**Invulnerability Flag**: obj+0x3A7 + +**Prevent Death Flag**: obj+0x3A1 bit 7 + + +### Damage Types + +| Key | Value | +|-----|-------| +| 0x00 | Normal | +| 0x06 | Special(Berserker check) | +| 0x08 | Fire/Burn | +| 0x68 | Berserker/Eat | +| 0x74 | Construction | + +**Special Abilities Count**: 91 + + +### Key Abilities + +- CombatBonus(8 stats) +- AbsorbBlaster +- Berserker +- DrainLife +- ConcentrateFire +- MaximumFirepower +- LuckyShot +- TractorBeam +- CableAttack +- EatAttack + + +## RE Findings Detail + +**Analysis**: Alamo Engine Combat System — Complete Damage Pipeline + +Full mapping of the SWFOC damage pipeline from weapon fire to unit death. Derived from Ghidra static analysis of StarWarsG.exe (x86_64). All addresses are Ghidra VA = imagebase 0x140000000 + RVA. + +- **Date**: 2026-04-04 +- **Analyst**: Agent 3C (Combat System Deep Dive) + + +### Damage Pipeline Overview + +- **description**: The complete flow from a weapon firing to a unit dying. Every HP modification in the engine funnels through SetHP (0x3A89D0). The combat damage path specifically goes through a 5-stage pipeline. +- **stages**: {'stage': 1, 'name': 'Weapon Fire / Attack Command', 'description': 'CombatantBehaviorClass dispatches attack orders. The targeting system (BaseCombatantClass vtable[34] SelectTarget) evaluates targets by range, health ratio, and weapon bitmask (target_type+0x1648). FUN_14038D730 is the master fire-control function: checks enemy status, invulnerability, LOS, game mode, and dispatches to hardpoint or direct weapon fire.', 'key_functions': {'SelectTarget': {'rva': '0x3A97E0_vicinity', 'vtable_slot': 34, 'class': 'BaseCombatantClass'}, 'FireControl_Dispatch': {'rva': '0x38D730', 'ghidra_va': '0x14038D730'}, 'WeaponTick': {'rva': '0x387010', 'ghidra_va': '0x140387010'}, 'HardpointFire': {'rva': '0x387F50', 'ghidra_va': '0x140387F50'}}}, {'stage': 2, 'name': 'Invulnerability Gate (Take_Damage_Outer)', 'description': 'Top-level damage entry point. Contains 8 invulnerability checks (one per damage path / hardpoint slot). This is the ONLY function that checks the invulnerability flag at obj+0x3A7. Also checks flags at obj+0x3A0 bits 1 and 6, and container sub-flags at obj+0x381, 0x382, 0x388. If invulnerable, damage is silently discarded.', 'key_functions': {'Take_Damage_Outer': {'rva': '0x38A350', 'ghidra_va': '0x14038A350'}}, 'invulnerability_checks': {'primary_flag': 'obj+0x3A7 (1 = invulnerable, 0 = vulnerable)', 'status_flags': 'obj+0x3A0 bits 1,6 (0x42 mask — blocks damage when set)', 'container_flags': ['obj+0x381', 'obj+0x382', 'obj+0x388'], 'locomotor_check': 'obj+0xA8 -> locomotor+0x2A8 (== 1 blocks damage)', 'note': 'Only blocks damage routed through Take_Damage_Outer. Lua Set_Hull, health regen, script/ability HP sets all bypass this entirely.'}}, {'stage': 3, 'name': 'Damage Routing (Property Dispatch + Shield Absorption)', 'description': 'Damage is routed based on object structure. Objects with hardpoints (obj+0x348 != 0xFF) route through the HardpointManager (QueryInterface 0x16). Objects with shields route through front-shield (QueryInterface 0x0F) or rear-shield (QueryInterface 0x10). Shield absorption reduces damage before it reaches hull HP.', 'key_functions': {'Take_Damage_PropertyDispatch': {'rva': '0x3A97E0', 'ghidra_va': '0x1403A97E0'}, 'SetFrontShield': {'rva': '0x3A8630', 'ghidra_va': '0x1403A8630'}, 'SetRearShield': {'rva': '0x3A91E0', 'ghidra_va': '0x1403A91E0'}, 'FrontShield_Write': {'rva': '0x56C1B0', 'ghidra_va': '0x14056C1B0'}, 'FrontShield_Read': {'rva': '0x56BFB0', 'ghidra_va': '0x14056BFB0'}, 'RearShield_Write': {'rva': '0x549810', 'ghidra_va': '0x140549810'}, 'RearShield_Read': {'rva': '0x549490', 'ghidra_va': '0x140549490'}, 'HardpointCount': {'rva': '0x405300', 'ghidra_va': '0x140405300'}, 'GetHardpoint': {'rva': '0x4052D0', 'ghidra_va': '0x1404052D0'}, 'DamageVisualLevel': {'rva': '0x3AC290', 'ghidra_va': '0x1403AC290'}, 'AnimationDispatch': {'rva': '0x3A92F0', 'ghidra_va': '0x1403A92F0'}}}, {'stage': 4, 'name': 'HP Subtraction (Take_Damage_Impl)', 'description': 'Core damage computation. Reads obj+0x5C (current HP), subtracts damage amount, calls SetHP. If prevent-death flag (obj+0x3A1 bit 7) is set and result would be <= 0, calls SetHP again with max(1.0, old_hp). If HP reaches 0, calls the death handler.', 'key_functions': {'Take_Damage_Impl': {'rva': '0x3AB890', 'ghidra_va': '0x1403AB890'}}, 'pseudocode': 'bool Take_Damage_Impl(GameObjectClass* obj, int damage_type, float damage, attacker_ref, ...) {\n float old_hp = obj->hp; // obj+0x5C\n SetHP(obj, old_hp - damage);\n \n if (obj->hp <= 0.0) {\n if (obj->prevent_death_flags_3A1 & 0x80) {\n float safe = max(1.0f, old_hp);\n SetHP(obj, safe);\n }\n if (obj->hp <= 0.0) {\n OnDeath(obj, damage_type, attacker_ref, ...);\n return false; // unit is dead\n }\n }\n \n // Check damage visual thresholds\n float max_hp = GetMaxHealth(obj->type_ptr);\n float threshold1 = max_hp * g_damage_threshold_1; // global at 0x803514\n float threshold2 = max_hp * g_damage_threshold_2; // global at 0x8007C0\n if ((old_hp > threshold1 && obj->hp <= threshold1) ||\n (old_hp > threshold2 && obj->hp <= threshold2)) {\n PlayDamageModel(obj); // switch to damaged art model\n }\n \n UpdateDamageVisualLevel(obj);\n return true; // unit survived\n}'}, {'stage': 5, 'name': 'SetHP (Canonical HP Write)', 'description': 'THE single HP write function. Every hitpoint change in the engine flows through this. Clamps to [0.0, max_hp], sets dirty flag, logs death events.', 'key_functions': {'SetHP': {'rva': '0x3A89D0', 'ghidra_va': '0x1403A89D0'}}, 'pseudocode': 'float SetHP(GameObjectClass* obj, float new_hp) {\n float old_hp = obj->hp; // +0x5C\n float clamped = max(0.0f, new_hp);\n obj->hp = clamped;\n float max_hp = GetMaxHealth(obj->type_ptr); // calls 0x3727A0\n float final_hp = min(max_hp, max(0.0f, obj->hp));\n obj->hp = final_hp;\n if (old_hp != final_hp && obj->object_id == g_tracked_object_id)\n obj->change_notification_flag |= 1; // +0x3A0\n if (final_hp < 0.0f)\n LogDeath(type_name, obj_id, player_name, final_hp, new_hp);\n return final_hp;\n}'} + + +### Death Sequence + +- **description**: When HP reaches 0.0 in Take_Damage_Impl, the death handler FUN_14039BDB0 is called. This is a complex function that handles: last-hit attribution, death animation selection, debris spawning, kill events, and object destruction. +- **handler_function**: + - **rva**: 0x39BDB0 + - **ghidra_va**: 0x14039BDB0 +- **sequence**: 1. Set death flag at obj+0x130 (OR with 0x40), 2. Record last-hit player (attacker's player ID stored at obj+0x110), 3. Signal death to behavior system (FUN_14038EB10), 4. If debug mode: call FUN_140392600 for death logging, 5. Notify AffectedByShield if present (obj+0x123 check, calls FUN_1405031A0), 6. Choose death animation: query GameObjectType for animation index (0x20 = special, iterate up to 2), 7. If death debris configured (type+0x23E8 or transport with units), spawn wreckage (FUN_1404D07E0), 8. Broadcast death event to the event system (event type 0x25), 9. Recursive: if object has child hardpoints (via obj+0x70 linked list), call death on children, 10. Determine spawn location for debris/explosion via QueryInterface(4), QI(5), or QI(0x16), 11. Push death record to global death queue (DAT_140B15418+0x42), 12. Destroy the object's visual representation (FUN_140265AE0), 13. Iterate sub-weapons: fire each weapon that is flagged as death-fire, 14. If hero unit: trigger hero respawn logic (ScheduleHeroRespawn at 0x48EB10), 15. Select death model/animation based on damage type (normal vs fire vs special), 16. Spawn death SFX and debris object at position, 17. If local player's unit: broadcast UI death notification + + +### Shield System + +- **description**: Shields are separate from hull HP. Each unit can have a front shield (QueryInterface 0x0F -> BaseShieldBehaviorClass) and a rear shield (QueryInterface 0x10 -> ShieldBehaviorClass). Shield values are stored at obj+0xF0 sub-object. +- **front_shield**: + - **query_interface_id**: 0x0F (15) + - **behavior_class**: BaseShieldBehaviorClass + - **vtable_rva**: 0x899458_vicinity + - **set_function**: + - **rva**: 0x3A8630 + - **ghidra_va**: 0x1403A8630 + - **read_function**: + - **rva**: 0x3963C0 + - **ghidra_va**: 0x1403963C0 + - **shield_write_impl**: + - **rva**: 0x56C1B0 + - **ghidra_va**: 0x14056C1B0 + - **shield_read_impl**: + - **rva**: 0x56BFB0 + - **ghidra_va**: 0x14056BFB0 + - **storage**: + - **current_value**: [obj+0xF0]+0xF8 (float32) + - **description**: Front shield current HP. Clamped to [0, max_front_shield]. + - **max_value_function**: + - **rva**: 0x372320 + - **ghidra_va**: 0x140372320 + - **max_value_source**: GameObjectType+0xDD0 (base) * difficulty_multiplier * ability_modifiers +- **rear_shield**: + - **query_interface_id**: 0x10 (16) + - **behavior_class**: ShieldBehaviorClass + - **vtable_rva**: 0x899458 + - **set_function**: + - **rva**: 0x3A91E0 + - **ghidra_va**: 0x1403A91E0 + - **read_function**: + - **rva**: 0x396420_vicinity + - **ghidra_va**: 0x140396420 + - **shield_write_impl**: + - **rva**: 0x549810 + - **ghidra_va**: 0x140549810 + - **shield_read_impl**: + - **rva**: 0x549490 + - **ghidra_va**: 0x140549490 + - **storage**: + - **current_value**: [obj+0xF0]+0xFC (float32) + - **description**: Rear shield current HP. + - **max_value_function**: + - **rva**: 0x3725F0 + - **ghidra_va**: 0x1403725F0 + - **max_value_source**: GameObjectType+0xDD4 (base) * difficulty_multiplier * ability_modifiers +- **shield_depletion_triggers_hull_damage**: + - **description**: When SetRearShield (0x3A91E0) sets shield to 0 and FUN_14039B950 returns true, it calls Take_Damage_Outer (0x38A350) to begin hull damage. Same for SetFrontShield through BaseShieldBehavior. This means shields must be depleted before hull HP can be reduced through normal combat. + - **rear_shield_death_trigger**: FUN_140549810 at offset +0x88: calls FUN_14038A350 when shield <= 0 and FUN_14039B950 returns true +- **shield_states**: + - **active**: shield_state = 1 (shields up, absorbing damage) + - **depleted**: shield_state = 2 (shields down, hull takes damage directly) + - **transition**: State change triggers visual/SFX events via FUN_140549CD0 and event broadcast +- **difficulty_multipliers**: + - **description**: Both GetMaxHealth and GetMaxShield apply game-mode-dependent multipliers. Mode 1 (Easy?) uses global at 0xB16DCC, Mode 2 (Hard?) uses 0xB16DC8. Additionally, player-specific multipliers from PlayerObject+0x360 sub-object are applied. + - **easy_multiplier_global**: 0xB16DCC + - **hard_multiplier_global**: 0xB16DC8 + - **player_multiplier**: + - **health**: PlayerObj+0x360 -> sub+0x50 + - **front_shield**: PlayerObj+0x360 -> sub+0x54 + - **rear_shield**: PlayerObj+0x360 -> sub+0x54 + + +### Max Health System + +- **description**: GetMaxHealth reads the base max HP from GameObjectType, then applies difficulty multipliers and ability modifiers. +- **function**: + - **rva**: 0x3727A0 + - **ghidra_va**: 0x1403727A0 +- **formula**: max_hp = type->base_max_hp(+0xDCC) * difficulty_multiplier * player_multiplier * (1.0 + ability_bonus) +- **base_hp_offset**: GameObjectType+0xDCC (float32) +- **pseudocode**: float GetMaxHealth(GameObjectType* type, GameObjectClass* obj) { + float base = type->max_hp_dcc; // +0xDCC + + // Apply difficulty multiplier + int game_mode = GetGameModeType(); + if (game_mode == 1) // Easy/Land? + base *= g_easy_hp_mult; // DAT_140B16DCC + else if (game_mode == 2) // Hard/Space? + base *= g_hard_hp_mult; // DAT_140B16DC8 + + // Apply player-specific multiplier + if (obj) { + PlayerObject* player = FindPlayerByID(obj->owner_id); + if (player && player->faction_sub_360) { + void* sub = GetFactionSub(player->faction_sub_360); + base *= *(float*)(sub + 0x50); // health multiplier + } + + // Apply ability modifiers (Plus + GreaterThan template) + if (HasAbilityModifiers(obj)) { + float bonus = ComputeAbilityBonus(obj); // FUN_14033FB70 + base *= (bonus + 1.0f); + } + } + return base; +} + + +### Hardpoint System + +- **description**: Objects with hardpoints (obj+0x348 != 0xFF) route damage through the HardpointManager. The HardpointManager (QueryInterface 0x16) maintains an array of HardPointClass instances. Each hardpoint has its own HP, shields, and can be independently destroyed. +- **detection_flag**: obj+0x348 (0xFF = no hardpoints / direct HP, other = has hardpoints) +- **manager_query**: QueryInterface(0x16) -> HardpointManager +- **count_function**: + - **rva**: 0x405300 + - **ghidra_va**: 0x140405300 + - **reads**: [manager+0x28]+0x110 -> +0x18 (int count) +- **get_hardpoint**: + - **rva**: 0x4052D0 + - **ghidra_va**: 0x1404052D0 +- **hull_ratio_via_hardpoints**: + - **function**: + - **rva**: 0x405230 + - **ghidra_va**: 0x140405230 + - **description**: Iterates all hardpoints and calls GetHullPercentage (0x396DF0) on each. Used for aggregate health display. +- **hardpoint_damage_routing**: When Take_Damage_PropertyDispatch detects obj+0x348 != 0xFF, it routes damage to individual hardpoints rather than the parent object's HP directly. The HardpointFire function (0x387F50) handles per-hardpoint HP reduction and station level loss notifications. +- **station_level_loss**: When a hardpoint's HP reaches 0, FUN_140387F50 checks for space station level changes and broadcasts TEXT_ENEMY_SPACE_STATION_LEVEL_LOST or TEXT_FRIENDLY_SPACE_STATION_LEVEL_LOST. + + +### Weapon Fire System + +- **description**: The weapon firing system is driven by CombatantBehaviorClass and its children (BaseCombatantClass, CompanyCombatantClass, SquadronCombatantClass). The fire control dispatch at 0x38D730 manages target acquisition, invulnerability pre-checks, weapon selection, and projectile spawning. +- **fire_control_dispatch**: + - **rva**: 0x38D730 + - **ghidra_va**: 0x14038D730 + - **description**: Master fire control function. Checks: (1) target is enemy via FUN_1402824D0, (2) target is not invulnerable (obj+0x3A7), (3) target is not paused/immune (obj+0x3A0 & 0x42), (4) target's locomotor allows damage, (5) weapon range, (6) LOS check, (7) game mode constraints. Then dispatches to appropriate fire behavior. + - **invulnerability_precheck**: if (target+0x3A7 == 1) return 0; // target invulnerable, skip + - **status_precheck**: if (target+0x3A0 & 0x42) return 0; // target has immunity flags +- **weapon_tick**: + - **rva**: 0x387010 + - **ghidra_va**: 0x140387010 + - **description**: Per-frame weapon update. Checks weapon state, manages firing cooldown via delta-time accumulation (param_1+0x60), resolves fire animation model by name, and calls the actual fire function. + - **cooldown_mechanism**: delta_ticks = current_tick - last_fire_tick (param_1+0x60). When cooldown expires, calls fire via FUN_140381FF0. + - **fire_animation**: Resolves model name from weapon data (type+0x1C0 SSO string), loads via asset system, and plays fire animation. +- **hardpoint_fire**: + - **rva**: 0x387F50 + - **ghidra_va**: 0x140387F50 + - **description**: Per-hardpoint damage application. Subtracts damage from hardpoint HP (param_1+0x28), manages hardpoint-level death (station level loss), and fires death-on-destruction effects. + - **hp_field**: param_1+0x28 (float, current hardpoint HP) + - **fire_rate**: Managed by weapon tick delta-time accumulation +- **combatant_classes**: + - **CombatantBehaviorClass**: + - **rva_ctor**: 0x6383E0 + - **ghidra_va**: 0x1406383E0 + - **vtable_count**: 55 virtual methods (in BaseCombatantClass) + - **BaseCombatantClass**: + - **rva_ctor**: 0x6CB700 + - **ghidra_va**: 0x1406CB700 + - **vtable_rva**: 0x8BF6C0 + - **CompanyCombatantClass**: + - **rva_ctor**: 0x6CBEE0 + - **ghidra_va**: 0x1406CBEE0 + - **SquadronCombatantClass**: + - **rva_ctor**: 0x6CBA40 + - **ghidra_va**: 0x1406CBA40 +- **target_selection**: + - **description**: BaseCombatantClass vtable[34] (SelectTarget) evaluates targets by range, health ratio, and weapon type bitmask. The weapon bitmask is stored at target_type+0x1648. + - **weapon_bitmask_offset**: GameObjectType+0x1648 + - **factors**: distance_to_target, target_health_ratio (vtable[31] GetHealthRatio), weapon_type_compatibility + + +### Damage Calculation Formula + +- **description**: The actual damage amount is computed before reaching Take_Damage_Impl. The formula incorporates base weapon damage, damage type modifiers, ability bonuses, and game speed scaling. +- **base_damage**: Defined in XML per weapon/projectile type +- **modifiers**: + - **game_speed_scaling**: (float)DAT_140B0A320 / (float)DAT_140B0A340 — applied in AnimationDispatch (0x3A92F0) + - **berserker_check**: FUN_1403A92F0 checks BerserkerAbilityClass RTTI. If berserker active, damage type forced to 0x68 and param_7 set to 1. + - **combat_bonus_ability**: + - **class**: CombatBonusAbilityClass + - **address**: 0x1406F42D0 + - **description**: Modifies 8 separate combat stats via FUN_14038C850: (1) max HP bonus, (2) damage bonus, (3) front shield bonus, (4) rear shield bonus, (5) rate modifier, (6) range modifier, (7) accuracy modifier, (8) additional modifier. After modifying max_hp, scales current HP proportionally: SetHP(obj, (new_max - old_max) + current_hp). + - **stat_offsets_in_ability_data**: + - **hp_bonus**: +0x00 + - **damage_bonus**: +0x04 + - **front_shield_bonus**: +0x08 + - **rear_shield_bonus**: +0x0C + - **rate_modifier**: +0x10 + - **range_modifier**: +0x14 + - **accuracy_modifier**: +0x18 + - **additional**: +0x1C + - **absorb_blaster_ability**: + - **class**: AbsorbBlasterAbilityClass + - **address**: 0x1406EE840 + - **description**: Converts incoming damage to healing. Reads damage type from target type+0x1FF4. If compatible (damage_type & ~0x3 != 0), converts damage to HP: SetHP(obj, current_hp + type->heal_factor(+0x474) * ability_factor + flat_bonus). Only works against specific damage types. + - **heal_formula**: new_hp = current_hp + (type+0x474 * ability+0x04) + ability+0x08 + + +### Damage Visual Thresholds + +- **description**: Take_Damage_Impl checks two global damage thresholds to trigger visual model changes (switching from pristine to damaged to heavily damaged art). +- **threshold_1**: + - **global_rva**: 0x803514 + - **description**: First damage threshold multiplier (applied to max_hp). When HP crosses below max_hp * threshold, switch to 'damaged' model. +- **threshold_2**: + - **global_rva**: 0x8007C0 + - **description**: Second damage threshold multiplier. When HP crosses below max_hp * threshold, switch to 'heavily damaged' model. +- **visual_level_function**: + - **rva**: 0x3AC290 + - **ghidra_va**: 0x1403AC290 +- **visual_level_logic**: Iterates an array at type+0xEB0 (float thresholds) paired with type+0xEC8 (animation indices). Compares health_ratio against each threshold and selects the highest matching visual level. + + +### Prevent Death System + +- **description**: The prevent-death flag at obj+0x3A1 bit 7 (mask 0x80) prevents a unit from dying through normal combat damage. When set, Take_Damage_Impl clamps HP to max(1.0, old_hp) instead of letting it reach 0. This is the 'unkillable but damageable' mode used by Set_Cannot_Be_Killed(true) in Lua. +- **flag_offset**: obj+0x3A1 +- **flag_mask**: 0x80 (bit 7) +- **implementation**: In Take_Damage_Impl: if (obj->hp <= 0 && (obj->flags_3A1 & 0x80)) { SetHP(obj, max(1.0f, old_hp)); } +- **note**: Distinct from full invulnerability (obj+0x3A7). Prevent-death still allows damage; it just prevents the final kill. + + +### Invulnerability System + +- **description**: Full invulnerability is a multi-layered system. The primary flag at obj+0x3A7 is checked ONLY by Take_Damage_Outer (0x38A350). Additional immunity flags at obj+0x3A0 bits 1 and 6 are checked by FireControl_Dispatch (0x38D730). Setting invulnerability properly requires calling Make_Invulnerable_Setter (0x3ABB80) which propagates to all hardpoints. +- **primary_flag**: obj+0x3A7 (checked by Take_Damage_Outer) +- **status_flags**: obj+0x3A0 & 0x42 (checked by FireControl_Dispatch — prevents targeting entirely) +- **container_flags**: obj+0x381, obj+0x382, obj+0x388 +- **setter_function**: + - **rva**: 0x3ABB80 + - **ghidra_va**: 0x1403ABB80 +- **cleanup_function**: + - **rva**: 0x3A56B0 + - **ghidra_va**: 0x1403A56B0 +- **lua_binding**: + - **rva**: 0x5819E0 + - **ghidra_va**: 0x1405819E0 +- **setter_actions**: 1. Get behavior via QueryInterface(1), 2. Notify game engine of invulnerability change, 3. Schedule timer event for invulnerability expiry, 4. Update behavior system invulnerability count, 5. If has hardpoints (obj+0x348 != 0xFF): iterate all hardpoints via QueryInterface(0x16) and recursively call Make_Invulnerable_Setter on each, 6. In space mode (game_mode == 2): adjust shield position offsets + + +### Health Regeneration + +- **description**: Two regeneration systems exist: natural health regen and shield-linked regen. Both call SetHP directly, bypassing invulnerability checks. +- **natural_regen**: + - **function_rva**: 0x5D70F0 + - **ghidra_va**: 0x1405D70F0 + - **description**: Periodic health regeneration. First call sets HP to 1.0 (revival?), subsequent ticks add regen_rate to current HP, clamped to max_hp. + - **formula**: new_hp = min(current_hp + regen_rate, max_hp) +- **shield_linked_regen**: + - **function_rva**: 0x387010 + - **description**: Shield regeneration ticks also affect hull HP via: new_hp = current_hp * ratio (from GetHullPercentage). +- **periodic_heal**: + - **function_rva**: 0x71B560 + - **ghidra_va**: 0x14071B560 + - **description**: DrainLifeAbilityClass periodic heal. Adds heal_amount to current HP via SetHP. + - **formula**: new_hp = current_hp + heal_amount +- **periodic_dot**: + - **function_rva**: 0x6F80D0 + - **ghidra_va**: 0x1406F80D0 + - **description**: Damage-over-time periodic ticks (burning, ion damage, etc.). Adds damage_amount to HP (negative for damage, positive for heal). + - **formula**: new_hp = current_hp + damage_amount + + +### Special Attack Abilities + +- **description**: 91 SpecialAbilityClass children provide combat modifiers. Key attack abilities: +- **abilities**: + - **ConcentrateFireAttackAbilityClass**: + - **rva_ctor**: 0x6F52C0 + - **ghidra_va**: 0x1406F52C0 + - **description**: Focus-fire ability. Increases damage against a single target. + - **MaximumFirepowerAttackAbilityClass**: + - **rva_ctor**: 0x706B10 + - **ghidra_va**: 0x140706B10 + - **description**: Maximum firepower mode. Increases rate of fire and damage. + - **LuckyShotAttackAbilityClass**: + - **rva_ctor**: 0x706280 + - **ghidra_va**: 0x140706280 + - **description**: Lucky shot chance. Random critical hit bonus. + - **EnergyWeaponAttackAbilityClass**: + - **rva_ctor**: 0x6F9980 + - **ghidra_va**: 0x1406F9980 + - **description**: Energy weapon special attack. + - **IonCannonShotAttackAbilityClass**: + - **rva_ctor**: 0x7048E0 + - **ghidra_va**: 0x1407048E0 + - **description**: Ion cannon single-shot ability. + - **EatAttackAbilityClass**: + - **rva_ctor**: 0x6F7A30 + - **ghidra_va**: 0x1406F7A30 + - **description**: Eat/consume attack (e.g., Sarlacc). Calls damage dispatch with type 0x68, destroys target. + - **ArcSweepAttackAbilityClass**: + - **rva_ctor**: 0x6EEAE0 + - **ghidra_va**: 0x1406EEAE0 + - **description**: Area sweep attack. + - **CableAttackAbilityClass**: + - **rva_ctor**: 0x6F22F0 + - **ghidra_va**: 0x1406F22F0 + - **description**: Tow cable attack (AT-AT takedown). + - **EarthquakeAttackAbilityClass**: + - **rva_ctor**: 0x6F7170 + - **ghidra_va**: 0x1406F7170 + - **description**: Ground-shake AoE attack. + - **TractorBeamAttackAbilityClass**: + - **rva_ctor**: 0x710080 + - **ghidra_va**: 0x140710080 + - **description**: Tractor beam immobilize + damage. + - **GenericAttackAbilityClass**: + - **rva_ctor**: 0x6FFFF0 + - **ghidra_va**: 0x1406FFFF0 + - **description**: Generic configurable attack ability. + - **BerserkerAbilityClass**: + - **rva_ctor**: 0x712D20 + - **ghidra_va**: 0x140712D20 + - **description**: Berserker mode. Forces damage type to 0x68, overrides normal attack behavior. + - **AbsorbBlasterAbilityClass**: + - **rva**: 0x6EE840 + - **ghidra_va**: 0x1406EE840 + - **description**: Converts incoming blaster damage to healing. + - **CombatBonusAbilityClass**: + - **rva**: 0x6F42D0 + - **ghidra_va**: 0x1406F42D0 + - **description**: Multi-stat combat bonus: HP, damage, shields (front+rear), rate, range, accuracy. + - **LeechShieldsAbilityClass**: + - **rva_ctor**: 0x717530 + - **ghidra_va**: 0x140717530 + - **description**: Steals shield points from target. + - **ShieldFlareAbilityClass**: + - **rva_ctor**: 0x71C820 + - **ghidra_va**: 0x14071C820 + - **description**: Shield disruption/flare effect. + - **DrainLifeAbilityClass**: + - **rva**: 0x71B560 + - **ghidra_va**: 0x14071B560 + - **description**: Drains HP from target and heals caster. +- **vtable_structure**: + - **vfunc_0**: destructor + - **vfunc_2**: Activate (pure virtual — each ability overrides) + - **vfunc_17**: RestoreEffect + - **vfunc_18**: CanActivate (checks game mode scope) + - **vfunc_19**: ApplyEffect + - **vfunc_29**: OnTick (periodic update — used by DoT, heals) + - **vfunc_31**: OnApply (initial application — used by CombatBonus, AbsorbBlaster) + - **vfunc_32**: OnHit (reactionary — used by EatAttack) + + +### Projectile System + +- **description**: Projectiles are GameObjectClass instances with ProjectileBehaviorClass attached. They are spawned by the weapon fire system and carry damage data to the target. +- **behavior_class**: + - **name**: ProjectileBehaviorClass + - **vtable_rva**: 0x878D58 + - **virtual_methods**: 33 + - **inherits**: BehaviorClass +- **data_pack**: + - **name**: ProjectileDataPackClass + - **rva_ctor**: 0x55B4D0 + - **ghidra_va**: 0x14055B4D0 +- **projectile_spawn**: Spawned via FUN_14029F810 (general object spawn). Projectile carries damage amount, damage type, owner reference. +- **projectile_hit**: On collision, projectile triggers damage application through the Take_Damage_Outer pipeline on the target. + + +### Key Struct Offsets For Combat + +- **GameObjectClass**: + - **vtable_ptr**: +0x00 (8 bytes, pointer) + - **object_id**: +0x50 (4 bytes, int32) + - **owner_player_id**: +0x58 (4 bytes, int32) + - **hp**: +0x5C (4 bytes, float32) — THE hitpoints field + - **locomotor_ptr**: +0xA8 (8 bytes, pointer) + - **shield_sub_object**: +0xF0 (8 bytes, pointer) — front shield at [ptr+0xF8], rear shield at [ptr+0xFC] + - **health_sub_object_ptr**: +0x118 (8 bytes, pointer) + - **component_array_ptr**: +0x278 (8 bytes, pointer) + - **game_object_type_ptr**: +0x298 (8 bytes, pointer) + - **animation_controller_ptr**: +0x2A0 (8 bytes, pointer) + - **container_ref**: +0x2B0 (8 bytes, pointer) + - **squad_unit_list**: +0x2D0 (8 bytes, pointer) + - **component_lookup_table**: +0x332 (byte array) + - **parent_component_index**: +0x335 (1 byte) + - **front_shield_present**: +0x341 (1 byte, 0xFF = none) + - **rear_shield_present**: +0x342 (1 byte, 0xFF = none) + - **direct_hp_path_flag**: +0x348 (1 byte, 0xFF = direct HP, other = hardpoints) + - **change_notification_flag**: +0x3A0 (1 byte, bit 0 = HP changed, bits 1+6 = immunity) + - **prevent_death_flags**: +0x3A1 (1 byte, bit 7 = prevent death) + - **invulnerability_flag**: +0x3A7 (1 byte, 1 = invulnerable) +- **GameObjectType**: + - **type_name**: +0xF8 (SSO string, 32 bytes) + - **base_max_hp**: +0xDCC (float32) + - **base_max_front_shield**: +0xDD0 (float32) + - **base_max_rear_shield**: +0xDD4 (float32) + - **damage_threshold_array**: +0xEB0 (pointer to float array) + - **damage_threshold_count**: +0xEB8 (int32) + - **damage_anim_index_array**: +0xEC8 (pointer to int array) + - **damage_anim_threshold_array**: +0xED8 (pointer to 0x20-stride array) + - **weapon_bitmask**: +0x1648 (for targeting compatibility) + - **death_debris_count**: +0xD48 (int32) + - **death_debris_list**: +0xD40 (pointer) + + +### All Sethp Callers + +- **description**: All 18 known code paths that modify HP through SetHP. Understanding these is critical for any HP-related mod. +- **callers**: {'rva': '0x29F5B5', 'function_rva': '0x29F270', 'category': 'death_cleanup', 'description': 'Death and cleanup handlers'}, {'rva': '0x38738B', 'function_rva': '0x387010', 'category': 'shield_health_regen', 'description': 'Shield regen tick: HP * ratio'}, {'rva': '0x3A0A90', 'function_rva': '0x3A06A0', 'category': 'property_init', 'description': 'Initial HP from XML data system'}, {'rva': '0x3AB0B0', 'function_rva': '0x3A97E0', 'category': 'take_damage_dispatch', 'description': 'Property dispatch path with hardpoint post-check'}, {'rva': '0x3AB8C6', 'function_rva': '0x3AB890', 'category': 'take_damage_impl', 'description': 'Core damage: old_hp - damage'}, {'rva': '0x3AB8EC', 'function_rva': '0x3AB890', 'category': 'take_damage_impl_prevent_death', 'description': 'Prevent-death clamp: max(1.0, old_hp)'}, {'rva': '0x42DD63', 'function_rva': '0x42DBD0', 'category': 'lua_set_hull', 'description': 'Lua Set_Hull direct write'}, {'rva': '0x4B0179', 'function_rva': '0x4AFBD0', 'category': 'spawn_hp_scaling', 'description': 'Spawn proportional rescale: old_hp * (new_max/old_max)'}, {'rva': '0x4B0E99', 'function_rva': '0x4B0DC0', 'category': 'spawn_hp_scaling_variant', 'description': 'Spawn rescale variant'}, {'rva': '0x4CBA8B', 'function_rva': '0x4CB6B0', 'category': 'behavior_attachment', 'description': 'HP set during behavior attach'}, {'rva': '0x5D7129', 'function_rva': '0x5D70F0', 'category': 'health_regen', 'description': 'Natural regen: set to 1.0 (revival)'}, {'rva': '0x5D73FC', 'function_rva': '0x5D70F0', 'category': 'health_regen_tick', 'description': 'Regen tick: min(current+rate, max)'}, {'rva': '0x6EE8EE', 'function_rva': '0x6EE840', 'category': 'absorb_blaster', 'description': 'AbsorbBlaster ability heal'}, {'rva': '0x6F4444', 'function_rva': '0x6F42D0', 'category': 'combat_bonus_hp_adjust', 'description': 'CombatBonus HP scaling after max_hp change'}, {'rva': '0x6F4DA2', 'function_rva': '0x6F4CC0', 'category': 'combat_bonus_deactivate', 'description': 'CombatBonus deactivation HP restore'}, {'rva': '0x6F8791', 'function_rva': '0x6F80D0', 'category': 'periodic_dot', 'description': 'DoT tick: current_hp + damage_amount'}, {'rva': '0x6FBAD1', 'function_rva': '0x6FB850', 'category': 'combat_event', 'description': 'Combat event handler'}, {'rva': '0x71B7BA', 'function_rva': '0x71B560', 'category': 'periodic_heal_tick', 'description': 'DrainLife heal: current_hp + heal_amount'} + + +### Damage Type System + +- **description**: Damage types are integer IDs used throughout the combat system. The AnimationDispatch function (0x3A92F0) uses damage type to select animation and handle Berserker override. Specific type IDs observed: +- **known_types**: + - **0x00**: Default/Normal (triggers default path in AnimationDispatch) + - **0x05**: Special type 1 (falls through to default in AnimationDispatch switch) + - **0x06**: Special type 2 (triggers special ability search in AnimationDispatch — BerserkerAbilityClass check) + - **0x08**: Fire/Burn damage (death handler checks: selects death anim variant 0x0C) + - **0x68**: Berserker/Eat forced type (overrides all other types when Berserker active, or used by EatAttack) + - **0x74**: Construction/Build type (used by tactical building construction) +- **type_resolved_by**: AnimationDispatch (0x3A92F0) uses FUN_140264A40 to query animation count for the given type, then FUN_1401FFB40 for random selection within count. +- **berserker_override**: When AnimationDispatch detects the unit is in active fire-while-queued state (obj+0x100 -> +0x2CC != 0), it checks if current command is 0x68. If not, it iterates the ability list, dynamic_casts to BerserkerAbilityClass (RTTI), and if found, forces damage_type = 0x68. + + +### Game Speed Scaling + +- **description**: Combat calculations are scaled by game speed to maintain consistent damage-per-second regardless of game speed setting. +- **numerator_global**: DAT_140B0A320 (current frame counter or time) +- **denominator_global**: DAT_140B0A340 (reference time base) +- **formula**: time_scale = (float)DAT_140B0A320 / (float)DAT_140B0A340 +- **applied_in**: AnimationDispatch (0x3A92F0) passes time_scale to FUN_140265560 and FUN_140265490 + + +### Modding Interception Points + +- **description**: Recommended hooking points for combat mods, ordered by specificity. +- **hooks**: {'name': 'SetHP (universal HP intercept)', 'rva': '0x3A89D0', 'aob': '40 53 48 83 EC 60 0F 29 74 24 50 0F 57 C0 F3 0F 10 71 5C', 'params': 'RCX=GameObjectClass*, XMM1=new_hp (float32)', 'use_case': 'Intercept ALL HP changes. Filter by caller category for selective mods.'}, {'name': 'Take_Damage_Outer (combat damage gate)', 'rva': '0x38A350', 'params': 'RCX=GameObjectClass*, XMM1=damage_amount', 'use_case': 'Hook combat-only damage. Invulnerability bypass, damage multipliers.'}, {'name': 'Take_Damage_Impl (damage subtraction)', 'rva': '0x3AB890', 'params': 'RCX=obj, param_2=damage_type, XMM1(param_3)=damage_amount, param_4=attacker_ref', 'use_case': 'Modify damage amount just before HP subtraction. Armor/resistance mods.'}, {'name': 'GetMaxHealth (max HP override)', 'rva': '0x3727A0', 'params': 'RCX=GameObjectType*, RDX=GameObjectClass*', 'return': 'XMM0=max_hp (float32)', 'use_case': 'Modify max HP for units. Affects healing caps, shield scaling, visual thresholds.'}, {'name': 'FireControl_Dispatch (weapon targeting)', 'rva': '0x38D730', 'use_case': 'Modify targeting behavior, add custom damage types, force-attack allies.'}, {'name': 'AnimationDispatch (damage events)', 'rva': '0x3A92F0', 'use_case': 'Hook damage type routing, custom damage type handlers.'} + diff --git a/docs/editor/systems/galactic-map.md b/docs/editor/systems/galactic-map.md new file mode 100644 index 000000000..4df6928d2 --- /dev/null +++ b/docs/editor/systems/galactic-map.md @@ -0,0 +1,390 @@ +# Galactic Map + +**Planet Ownership Field**: PlanetaryDataPackClass+0x6C + +**Planet Capture Progress**: PlanetaryDataPackClass+0x68 + +**Corruption Level**: PlanetaryDataPackClass+0x2F4 + +**Fow Per Player Array**: GameModeClass+0x198 + +**Fow Global Toggle**: 0xA284C4 + + +### Ownership Transfer Rvas + +| Key | Value | +|-----|-------| +| initial | 0x3FA160 | +| full | 0x3FB040 | + +**Planet Reachability Slots**: 11 + + +## RE Findings Detail + +**Analysis**: SWFOC Galactic Map System - Reverse Engineering Results + +Planet struct layout, planet list global, ownership transfer, fog of war, and trade route adjacency data from static analysis of StarWarsG.exe (x86_64, Alamo engine). + +- **Date**: 2026-04-04 +- **Analyst**: Agent 3E (Galactic Conquest Map System RE) + + +### Globals + +- **PlayerListClass_ptr**: + - **rva**: 0xA16FD0 + - **type**: pointer_to_struct + - **struct**: PlayerListClass + - **description**: Global pointer to PlayerListClass singleton. DAT_140a16fd0 in Ghidra. Used by every planet ownership function. Dereference once to get the struct. player_array at +0x20, player_count at +0x28. + - **notes**: DAT_140a16fd8 is the END of the player pointer array (used as: count = (DAT_140a16fd8 - DAT_140a16fd0) >> 3). +- **PlayerListClass_end_ptr**: + - **rva**: 0xA16FD8 + - **type**: pointer + - **description**: End pointer of the player array. Player count = (0xA16FD8 - 0xA16FD0) >> 3 in the global-scoped usage pattern. Used in all planet ownership iteration loops. +- **GameModeManager_active_mode_ptr**: + - **rva**: 0xB15418 + - **type**: pointer_to_struct + - **struct**: GameModeClass + - **description**: Global pointer to the currently active GameModeClass instance (GalacticModeClass, SpaceModeClass, or LandModeClass). DAT_140b15418 in Ghidra. Used by FOW Reveal, perception systems, and planet ownership. When in galactic mode, this points to a GalacticModeClass (vtable RVA 0x8762C0). + - **notes**: This is THE central game state pointer. All per-player FOW objects, per-player object lists, and game-mode-specific data hang off this. +- **GameObjectTypeList_ptr**: + - **rva**: 0xA172D0 + - **type**: pointer_to_struct + - **description**: Global pointer to a GameObjectType list/registry. DAT_140a172d0 in Ghidra. Used by FUN_140331cc0 to find the 'is_planet' flagged type. List at +0x10 (pointer array), count at +0x18. + - **struct_layout**: + - **+0x10**: pointer to array of GameObjectType pointers + - **+0x18**: int32 count +- **PerceptionManager_ptr**: + - **rva**: 0xB153E0 + - **type**: pointer_to_struct + - **description**: Global pointer to a perception/diplomacy manager. DAT_140b153e0 in Ghidra. Used to check faction relationships (Is_Enemy, Is_Ally) and by the planet faction change system to evaluate diplomatic state. +- **PlayerCount_static**: + - **rva**: 0xA16FB0 + - **type**: uint32 + - **description**: Static player count. DAT_140a16fb0 in Ghidra. Used by GameModeClass constructor to allocate per-player arrays. +- **GameTickCounter_ptr**: + - **rva**: 0xB0A340 + - **type**: float_or_int + - **description**: DAT_140b0a340 in Ghidra. Used as a time multiplier in planet capture timer calculations. +- **FOW_GlobalToggle**: + - **rva**: 0xA284C4 + - **type**: bool + - **description**: DAT_140a284c4 in Ghidra. Global FOW toggle. Set by FOW 'Undo_Reveal_All' function (FUN_1406a53b0). When true, FOW is fully disabled. + + +### Structs + +- **PlanetaryDataPackClass**: + - **description**: Data pack attached to planet GameObjectClass instances via the behavior/component system. Contains ALL persistent galactic-map state for a planet: trade route links, ownership, garrison units, tactical built structures, upgrade objects, visibility modifiers, and capture progress. This is the planet's 'save state' that persists across tactical battles. Accessed via the PlanetaryBehaviorClass which stores a pointer to this at its +0x18 field (offset 0xB8 or 0x17*8 from behavior base). + - **rtti_mangled_name**: .?AVPlanetaryDataPackClass@@ + - **pool_class**: PooledObjectClass + - **size_known**: False + - **size_minimum**: 0x350 + - **inherits_comment**: Uses DynamicVectorClass members extensively. The destructor reveals the member layout via inlined destructor calls. + - **fields**: {'offset': '0x00', 'type': 'vtable_ptr', 'width': 8, 'name': 'vtable_ptr'}, {'offset_range': 'self[1]..self[2]', 'byte_offset': '0x10..0x20', 'type': 'DynamicVectorClass', 'name': 'persistent_tactical_objects', 'description': 'Stores persistent built tactical structures (land base, turrets, etc) that survive across tactical battles.'}, {'offset_range': 'self[2]..self[3]', 'byte_offset': '0x20..0x30', 'type': 'DynamicVectorClass', 'name': 'persistent_upgrades', 'description': 'Persistent upgrade objects on this planet.'}, {'offset_range': 'self[3]..self[5]', 'byte_offset': '0x30..0x50', 'type': 'DynamicVectorClass', 'name': 'line_links', 'description': 'Visual/display links for the galactic map lines.'}, {'offset_range': 'self[5]..self[6]', 'byte_offset': '0x50..0x60', 'type': 'DynamicVectorClass', 'name': 'trade_route_links', 'description': "Adjacent trade routes connected to this planet. Each entry is a TradeRouteLinkEntryClass containing the connected planet reference and route data. This is the planet's adjacency list for the galactic graph."}, {'offset': '0x68', 'type': 'float32', 'width': 4, 'name': 'capture_progress', 'description': 'Current capture/ownership progress value. Written during planet faction changes. When ownership changes, this is set to the new faction change value from FUN_1403f6af0. Reset to 0 on full owner switch.', 'confidence': 'high', 'xrefs': ['function:PlanetFactionChangeClass_0x1403fa160', 'function:PlanetFactionChangeClass_0x1403fb040']}, {'offset': '0x6C', 'type': 'int32', 'width': 4, 'name': 'owning_player_id', 'description': 'Player ID (index into PlayerListClass) of the faction that owns this planet. Set to -1 (0xFFFFFFFF) during transitions. This is THE ownership field for planets. Compared and written in PlanetFactionChangeClass.', 'confidence': 'high', 'xrefs': ['function:PlanetFactionChangeClass_0x1403fa160', 'function:PlanetFactionChangeClass_0x1403fb040']}, {'offset': '0x70', 'type': 'float32', 'width': 4, 'name': 'previous_capture_progress', 'description': 'Previous capture progress value. Compared against current to detect changes.', 'confidence': 'high'}, {'offset': '0x74', 'type': 'int32', 'width': 4, 'name': 'previous_owning_player_id', 'description': 'Previous owner player ID. Used to detect ownership transitions and fire events.', 'confidence': 'high'}, {'offset': '0x78', 'type': 'pointer_or_struct', 'width': 8, 'name': 'capture_timer_data', 'description': 'Capture timer/progress tracking structure. FUN_1403fe810 writes into this + subfields.'}, {'offset': '0x98', 'type': 'int32', 'width': 4, 'name': 'capture_initiator_player_id', 'description': 'Player ID of the faction initiating the capture. Set to -1 when not being captured.', 'confidence': 'high'}, {'offset': '0x9C', 'type': 'int32', 'width': 4, 'name': 'capture_state_flags', 'description': 'Capture state flags. Set to -1 (0xFFFFFFFF) on initialization.', 'confidence': 'medium'}, {'offset': '0xA0', 'type': 'std_map', 'name': 'per_player_planet_data', 'description': 'std::map or red-black tree keyed by some per-player identifier. Iterated in FUN_1403f6af0. Contains per-player visibility/force data for this planet.'}, {'offset': '0xB0', 'type': 'int32', 'width': 4, 'name': 'income_value', 'description': 'Planet income/credit generation value. Set during faction change processing.', 'confidence': 'medium'}, {'offset': '0xB4', 'type': 'int32', 'width': 4, 'name': 'income_multiplier_or_bonus', 'description': 'Income-related secondary value.', 'confidence': 'medium'}, {'offset': '0xB8', 'type': 'uint8', 'width': 1, 'name': 'income_flag_1', 'description': 'Flag related to income processing. Set to 1 during ownership changes.', 'confidence': 'medium'}, {'offset': '0xB9', 'type': 'uint8', 'width': 1, 'name': 'income_flag_2', 'description': 'Secondary income processing flag. Set to 1 after income_flag_1.', 'confidence': 'medium'}, {'offset': '0x1C9', 'type': 'uint8', 'width': 1, 'name': 'initial_owner_set_flag', 'description': 'Set to 1 after the first ownership assignment. Guards against double-initialization in PlanetFactionChangeClass.', 'confidence': 'high'}, {'offset': '0x200', 'type': 'int32', 'width': 4, 'name': 'special_structure_owner_id', 'description': 'Owner player ID of a special structure on this planet. When matches new owner, triggers structure cleanup at +0x204 through +0x220.', 'confidence': 'medium'}, {'offset': '0x2C8', 'type': 'uint8', 'width': 1, 'name': 'planet_destroyed_flag', 'description': 'Flag indicating planet has been destroyed. Checked before AI reassignment processing.', 'confidence': 'medium'}, {'offset': '0x2E2', 'type': 'uint8', 'width': 1, 'name': 'special_state_flag_1', 'description': 'Special planet state flag. Checked in corruption processing.', 'confidence': 'medium'}, {'offset': '0x2E3', 'type': 'uint8', 'width': 1, 'name': 'capture_timer_active', 'description': 'Whether the planet capture timer is actively running.', 'confidence': 'medium'}, {'offset': '0x2E8', 'type': 'int32', 'width': 4, 'name': 'capture_start_tick', 'description': 'Game tick when capture timer started.', 'confidence': 'medium'}, {'offset': '0x2EC', 'type': 'int32', 'width': 4, 'name': 'capture_end_tick', 'description': 'Game tick when capture will complete.', 'confidence': 'medium'}, {'offset': '0x2F4', 'type': 'int32', 'width': 4, 'name': 'corruption_level', 'description': 'Planet corruption level. -1 when not corrupted. Set to 3 during ownership change by corrupting faction. Values: -1=none, 0-3=corruption tiers.', 'confidence': 'high'}, {'offset': '0x2F8', 'type': 'int32', 'width': 4, 'name': 'corruption_timer_or_state', 'description': 'Corruption timer or state field. Set to -1 on reset.', 'confidence': 'medium'} + - **inner_structs**: + - **TradeRouteLinkEntryClass**: + - **description**: Entry in the trade route adjacency list. Stored in DynamicVectorClass. Connects this planet to another via a TradeRouteClass. + - **destructor_rva**: 0x4B5F40 + - **LineLinkStruct**: + - **description**: Visual line link data for galactic map rendering. + - **destructor_rva**: 0x4B5DF0 + - **PersistentTacticalBuiltObjectStruct**: + - **description**: Persistent tactical structure data (survives across battles). + - **destructor_rva**: 0x4B5E60 + - **PersistentUpgradeObjectStruct**: + - **description**: Persistent upgrade data. + - **destructor_rva**: 0x4B5ED0 +- **PlanetaryBehaviorClass**: + - **description**: Behavior component attached to planet GameObjectClass instances. Manages the relationship between the planet's GameObjectClass (position, owner, etc.) and the PlanetaryDataPackClass (persistent planet state). Pool size 20. The destructor at RVA 0x3F3340 reveals the link: this->offset_0x18 is the parent GameObjectClass (the planet object), and this->offset_0x18[0x17] is the pointer to PlanetaryDataPackClass. + - **rtti_mangled_name**: .?AVPlanetaryBehaviorClass@@ + - **pool_class**: PooledObjectClass + - **destructor_rva**: 0x3F3340 + - **size_known**: False + - **inherits**: BehaviorClass + - **fields**: {'offset': '0x00', 'type': 'vtable_ptr', 'width': 8, 'name': 'vtable_ptr'}, {'offset': '0x18', 'type': 'pointer', 'width': 8, 'name': 'parent_game_object_ptr', 'description': 'Pointer to the parent GameObjectClass (the planet object). Used extensively in the destructor to clean up planet state.', 'confidence': 'high'} + - **notes**: The parent game object has: [0x17] = PlanetaryDataPackClass ptr, [0x53] = GameObjectType ptr, [0x07..0x0F] = various sub-component arrays. The PlanetaryDataPackClass at [parent+0x17*8] = [parent+0xB8] is the core data. +- **PlanetFactionChangeClass**: + - **description**: Event/signal class that handles planet ownership transfers on the galactic map. Nested inside PlanetaryDataPackClass. Two key functions: the initial ownership set (RVA 0x3FA160) and the full transfer handler (RVA 0x3FB040). The transfer handler fires the TEXT_PLANET_CHANGED_HANDS localized string, notifies all players, updates perception, and optionally shows the TEXT_PLANET_CHANGED_HANDS_BONUS variant. + - **destructor_rva_1**: 0x3FA160 + - **destructor_rva_2**: 0x3FB040 + - **vtable_string**: PlanetFactionChangeClass_vftable + - **inherits**: SignalDataClass + - **key_behaviors**: + - **initial_ownership_set**: + - **rva**: 0x3FA160 + - **description**: Simplified ownership setter. Writes ownership fields (planet+0x68, +0x6C, +0x70, +0x74, +0x98) and notifies all players via iteration over PlayerListClass. + - **steps**: Read planet data ptr from param_1[0x17] (byte offset 0xB8 on parent object), Call FUN_1403f6af0 to compute new income value, Set fields at planet_data+0x68 (capture_progress), +0x6C (owner_id), +0x70 (prev_progress), +0x98 (capture_initiator) to new values, Iterate all players via FUN_140294bc0 using DAT_140a16fd0 (PlayerListClass), For each player, call FUN_1403a51e0 to update planet visibility, Fire signal via FUN_140220ed0 + - **full_ownership_transfer**: + - **rva**: 0x3FB040 + - **description**: Full planet ownership transfer with UI notification, capture progress tracking, diplomacy checks, corruption processing, and AI budget updates. + - **steps**: Read planet data from param_1[0x17] (PlanetaryDataPackClass), Check initial_owner_set_flag at planet_data+0x1C9, If first time: set flag, write owner to +0x6C, set capture progress to +0x68, notify all players, Compare new owner (param_2) vs current (planet_data+0x6C) -- if different, full transfer, On transfer: Reset +0x6C to new owner, +0x68 to 0, iterate players to update visibility, Look up old/new player objects via FUN_140294bc0, Read faction_affiliation from player+0x68 for both old and new owners, Format TEXT_PLANET_CHANGED_HANDS or TEXT_PLANET_CHANGED_HANDS_BONUS message, Log '%s: now controlled by %s (owner player ID %d)' via FUN_140025760, Play conquest/loss audio cues from player+0x68+0x780 / +0x790, Check corruption flag at new_player+0x68+0x10B -- if set, initiate corruption, Process capture timer at planet_data+0x2E2..+0x2F8, Update corruption level at planet_data+0x2F4 (set to 3 for corrupting faction), Notify AI subsystems for budget recomputation +- **GameModeClass_FOWFields**: + - **description**: FOW-related fields within GameModeClass. These are at fixed offsets within the GameModeClass base. + - **context**: GameModeClass is the base for GalacticModeClass, SpaceModeClass, LandModeClass. The active instance is pointed to by DAT_140b15418. + - **fields**: {'offset': '0x190', 'type': 'int32', 'width': 4, 'name': 'fow_player_count', 'description': 'Number of per-player FOW objects allocated. Corresponds to self[0x32] in Ghidra notation.', 'confidence': 'high'}, {'offset': '0x198', 'type': 'pointer', 'width': 8, 'name': 'fow_per_player_array', 'description': 'Pointer to array of per-player FOW objects. Indexed by player_id. Each element is a pointer to a FOW texture/state object (0x58 bytes each, freed in destructor). Corresponds to self[0x33] in Ghidra notation.', 'confidence': 'high'} +- **FOWPlayerObject**: + - **description**: Per-player fog of war state object. One per player, stored in GameModeClass.fow_per_player_array[player_id]. Size 0x58 bytes. + - **size**: 0x58 + - **fields**: {'offset': '0x00', 'type': 'pointer', 'width': 8, 'name': 'visibility_grid', 'description': 'Pointer to the raw visibility grid byte array. Each byte represents visibility state for one cell: 0x00=fully fogged, 0x01-0x0F=partial visibility (fading), 0x10-0xEE=revealed (varying degrees), 0xEF=just-cleared marker, 0xFF=fully visible.', 'confidence': 'high'}, {'offset': '0x08', 'type': 'pointer', 'width': 8, 'name': 'reveal_timer_grid', 'description': 'Pointer to short[grid_size] array. Countdown timer per cell. 0=not timed (permanent reveal), -1=permanent (no countdown). Non-zero positive values decrement each tick.', 'confidence': 'high'}, {'offset': '0x10', 'type': 'pointer', 'width': 8, 'name': 'output_visibility_grid', 'description': 'Pointer to output/rendering visibility values. Written by FUN_1404c0dc0.', 'confidence': 'medium'}, {'offset': '0x20', 'type': 'int64', 'width': 8, 'name': 'grid_cell_count', 'description': 'Total number of cells in the visibility grid.', 'confidence': 'high'}, {'offset': '0x50', 'type': 'uint8', 'width': 1, 'name': 'dirty_flag', 'description': 'Set to 1 after any grid modification. Signals the renderer to re-upload the FOW texture.', 'confidence': 'high'} +- **PlanetReachabilityClass**: + - **description**: Stores planet-to-planet reachability data for a specific player/faction. Contains 11 reachability slots (initialized in a loop of count 0xB=11). Constructor at RVA 0x663530. Uses the perception system at DAT_140b153e0 to compute pathfinding costs. + - **constructor_rva**: 0x663530 + - **size_known**: False + - **size_minimum**: 0x170 + - **inherits**: RefCountClass + - **fields**: {'offset': '0x08', 'type': 'pointer', 'width': 8, 'name': 'source_planet_ptr', 'description': 'The planet this reachability data is relative to.'}, {'offset': '0x88', 'type': 'array_of_structs', 'element_size': 16, 'count': 11, 'name': 'reachability_slots', 'description': '11 slots of reachability data (each 0x10 bytes). Initialized via FUN_14020b740 and FUN_14020b930.'}, {'offset': '0x138', 'type': 'undefined8', 'width': 8, 'name': 'path_context'}, {'offset': '0x140', 'type': 'float_pair', 'width': 16, 'name': 'cost_pair_1'}, {'offset': '0x150', 'type': 'float_pair', 'width': 16, 'name': 'cost_pair_2'}, {'offset': '0x160', 'type': 'float_pair', 'width': 16, 'name': 'cost_pair_3'}, {'offset': '0x16C', 'type': 'int16', 'width': 2, 'name': 'flags_1'}, {'offset': '0x16E', 'type': 'int16', 'width': 2, 'name': 'flags_2'} +- **TradeRouteClass**: + - **description**: Represents a trade route connecting two planets. Referenced by PlanetaryDataPackClass::TradeRouteLinkEntryClass. A DynamicVectorClass of const TradeRouteClass pointers exists (destructor at RVA 0x4AE5E0), suggesting a global or per-mode list of all trade routes. + - **destructor_rva_list**: 0x4AE5E0 + - **size_known**: False + - **notes**: Trade routes form the edges of the galactic map graph. Each planet has a DynamicVectorClass at its data pack offset ~0x50 that lists adjacent routes. + + +### Functions + +- **PlayerList_FindByID**: + - **rva**: 0x294BC0 + - **signature**: void* PlayerList_FindByID(PlayerListClass* list, int player_id) + - **description**: Looks up a player object by ID from the player list. Bounds-checks player_id against list+0x28 (count), returns *(list+0x20)[player_id]. Returns 0 if out of bounds. + - **params**: + - **param_1**: longlong - pointer to PlayerListClass (or the DAT_140a16fd0 global directly) + - **param_2**: int - player_id to look up + - **returns**: Pointer to PlayerObject, or 0 if invalid + - **confidence**: high +- **PlayerList_FindByFaction**: + - **rva**: 0x294D30 + - **signature**: void* PlayerList_FindByFaction(PlayerListClass* list, void* faction_ptr) + - **description**: Iterates all players in the list and returns the first whose faction_affiliation_ref (player+0x68) matches the given pointer. Used to look up the player owning a specific faction. + - **params**: + - **param_1**: longlong - pointer to PlayerListClass + - **param_2**: longlong - faction affiliation pointer to match + - **returns**: Pointer to matching PlayerObject, or 0 + - **confidence**: high +- **PlayerList_GetLocalPlayerID**: + - **rva**: 0x294A70 + - **signature**: int PlayerList_GetLocalPlayerID(PlayerListClass* list) + - **description**: Returns the player_id (field +0x4C) of the local human player. Uses list[6] as the local player index. + - **confidence**: high +- **PlayerList_GetLocalPlayer**: + - **rva**: 0x294A40 + - **signature**: void* PlayerList_GetLocalPlayer(PlayerListClass* list) + - **description**: Returns the PlayerObject pointer for the local human player. + - **confidence**: high +- **FindPlanetClass_Constructor**: + - **rva**: 0x697010 + - **signature**: FindPlanetClass* FindPlanetClass::FindPlanetClass(FindPlanetClass* this) + - **description**: Constructor for the Lua FindPlanet wrapper. Registers the 'Get_All_Planets' Lua method. The actual planet lookup implementation is at LAB_140254a1c (code label, not a function boundary). + - **lua_methods_registered**: Get_All_Planets + - **notes**: The Get_All_Planets implementation iterates whatever planet container is managed by the galactic mode and returns a Lua table of planet wrappers. +- **PlanetFactionChange_InitialSet**: + - **rva**: 0x3FA160 + - **signature**: void PlanetFactionChange_InitialSet(PlanetFactionChangeClass* this, GameObjectClass* planet_obj) + - **description**: Initial planet ownership assignment. Sets ownership fields on the PlanetaryDataPackClass. Iterates all players to update visibility. Called when a planet is first assigned an owner (map init or scenario load). +- **PlanetFactionChange_Transfer**: + - **rva**: 0x3FB040 + - **signature**: void PlanetFactionChange_Transfer(PlanetFactionChangeClass* this, GameObjectClass* planet_obj, int new_owner_id, float capture_progress) + - **description**: Full planet ownership transfer handler. Updates ownership, fires UI events, plays audio, processes corruption, updates AI budgets. The main 'Change_Owner for planets' function. + - **log_format**: %s: now controlled by %s (owner player ID %d) + - **ui_strings**: TEXT_PLANET_CHANGED_HANDS, TEXT_PLANET_CHANGED_HANDS_BONUS +- **Planet_UpdatePlayerVisibility**: + - **rva**: 0x3A51E0 + - **signature**: void Planet_UpdatePlayerVisibility(GameObjectClass* planet_obj, PlayerObject* player) + - **description**: Updates a player's visibility state for a planet. Checks if the player is the local player (via FUN_140294a40), then iterates the planet's component array (planet+0x278, count at +0x288) calling virtual method 0xA0 on each component. Sets flag at planet+0x39F. + - **planet_fields_accessed**: + - **+0x278**: component_array_ptr + - **+0x288**: component_count (char) + - **+0x2A0**: sub_object_ptr (for visibility update) + - **+0x39F**: visibility_dirty_flag +- **PlanetOwnershipIncome_Compute**: + - **rva**: 0x3F6AF0 + - **signature**: int PlanetOwnershipIncome_Compute(void* this, GameObjectClass* planet_obj, int* out_owner_id) + - **description**: Computes planet income value during ownership change. Accesses planet_obj[0x53] (GameObjectType) and planet_obj[0x17] (PlanetaryDataPackClass). Uses the per-player-planet-data map at planet_data+0xA0. Writes income values to planet_data+0xB0..+0xB9. + - **confidence**: high +- **FOW_Reveal_Tactical**: + - **rva**: 0x6A5700 + - **signature**: void* FOW_Reveal_Tactical(GetEvent* event, lua_State* L, void* params) + - **description**: Lua-bound FOW.Reveal function for tactical mode. Takes (player, object_or_position, radius, [decay_radius]). Gets the player's FOW object from GameModeClass+0x198[player_id], then calls FUN_14035d080 or FUN_14035d1b0 to create the reveal. + - **lua_error_messages**: LuaFOWRevealCommandClass -- Command only valid in a tactical game., LuaFOWRevealCommandClass -- Requires at least 2 parameters: (player, object)., LuaFOWRevealCommandClass -- Expected player object as first parameter., LuaFOWRevealCommandClass -- Expected 3rd parameter for reveal radius., LuaFOWRevealCommandClass -- Undable to get a valid position from parameter 2. +- **FOW_RevealAll**: + - **rva**: 0x6A5B00 + - **signature**: void FOW_RevealAll(void* unused, lua_State* L, void* params) + - **description**: Lua-bound FOWManager.Reveal_All function. Takes (player). Gets player's FOW object and calls FUN_14035d4f0 to reveal all cells. Only works in tactical mode. + - **lua_error_messages**: LuaFOWRevealCommandClass -- Command only valid in a tactical game., LuaFOWRevealCommandClass -- Requires at least 1 parameters: (player)., LuaFOWRevealCommandClass -- Expected player object as first parameter. +- **FOW_UndoRevealAll**: + - **rva**: 0x6A53B0 + - **signature**: void FOW_UndoRevealAll(void* unused, lua_State* L, void* params) + - **description**: Lua-bound FOWManager.Undo_Reveal_All function. Takes (bool_param). Sets DAT_140a284c4 (global FOW toggle) and calls FUN_14013ecc0. + - **global_written**: 0xA284C4 +- **FOW_TemporaryReveal**: + - **rva**: 0x6A5CF0 + - **signature**: void FOW_TemporaryReveal(void* unused, lua_State* L, void* params) + - **description**: Lua-bound FOWManager.Temporary_Reveal function. Takes (player, object, [radius]). Creates a time-limited reveal at the object's position. Calls FUN_140365630. + - **lua_error_messages**: LuaFOWRevealCommandClass -- Command only valid in a tactical game., LuaFOWRevealCommandClass -- Requires 2 parameters: (player, object, [radius])., LuaFOWRevealCommandClass -- Expected player object as first parameter., LuaFOWRevealCommandClass -- Expected object as 2nd parameter., LuaFOWRevealCommandClass -- Expected number as 3rd parameter. +- **FOW_PerPlayer_Reveal**: + - **rva**: 0x35D080 + - **signature**: void FOW_PerPlayer_Reveal(GameModeClass* mode, int player_id, position, float outer_radius, float inner_radius, void* out_handle) + - **description**: Creates a FOW reveal circle for a specific player. Accesses mode[0x33] (fow_per_player_array) indexed by player_id. Calls FUN_1404c0ec0 on the player's FOW object to set visibility in the grid. +- **FOW_PerPlayer_RevealAll**: + - **rva**: 0x35D4F0 + - **signature**: void FOW_PerPlayer_RevealAll(GameModeClass* mode, int player_id) + - **description**: Reveals the entire map for a specific player. Accesses mode[0x33][player_id] and calls FUN_1404c1560 which iterates the full grid and sets all cells to fully visible. +- **FOW_UpdateGrid**: + - **rva**: 0x4C1560 + - **signature**: void FOW_UpdateGrid(FOWPlayerObject* fow) + - **description**: Per-frame FOW grid update. Iterates all cells (fow[4] = count). For cells with timer (fow[1][i] != 0), decrements timer. When timer expires, fades visibility. Grid byte values: 0=fogged, 0x01-0x0F=fading in, 0x10-0xEE=fading out, 0xEF=just-fogged marker, 0xFF=full visible. + - **grid_layout**: + - **fow[0]**: visibility_state byte array + - **fow[1]**: reveal_timer short array + - **fow[2]**: output_visibility byte array + - **fow[4]**: cell_count +- **FOW_RevealCircle**: + - **rva**: 0x4C0EC0 + - **signature**: void FOW_RevealCircle(FOWPlayerObject* fow, position, float outer_radius, float inner_radius, void* handle) + - **description**: Reveals a circular area in the FOW grid. If outer > inner, calls FUN_1404c1000 twice (one for each radius). Otherwise calls once with combined parameters. +- **GameObjectType_FindPlanetType**: + - **rva**: 0x331CC0 + - **signature**: void* GameObjectType_FindPlanetType(GameObjectTypeList* list) + - **description**: Iterates the game object type list and returns the first type with flag at type+0x108 set to non-zero. This identifies the 'planet' type definition. The list is at DAT_140a172d0 (type_list+0x10 = array, type_list+0x18 = count). + - **type_flag_offset**: 0x108 +- **GalacticPathFinder_Singleton_Init**: + - **rva**: 0x7E6920 + - **signature**: void GalacticPathFinder_Singleton_Init() + - **description**: Initializes the GalacticPathFinderClass singleton vtable. The singleton instance pointer is at PTR_vftable_1409cf1c8. +- **PlanetaryDataPack_ResetCapture**: + - **rva**: 0x4B6C50 + - **signature**: void PlanetaryDataPack_ResetCapture(PlanetaryDataPackClass* pack) + - **description**: Resets capture state fields: pack+0x2E2=0, pack+0x2E3=0, pack+0x2E8=0, pack+0x2F8=-1. + - **fields_written**: + - **+0x2E2**: 0 (special_state_flag_1) + - **+0x2E3**: 0 (implicitly via word write) + - **+0x2E8**: 0 (capture_start_tick / 8 bytes) + - **+0x2F8**: -1 (corruption_timer_or_state) +- **PlanetaryDataPack_StartCaptureTimer**: + - **rva**: 0x4B7260 + - **signature**: void PlanetaryDataPack_StartCaptureTimer(PlanetaryDataPackClass* pack, float duration) + - **description**: Starts the capture timer. Writes pack+0x2E3=1 (timer active), pack+0x2F8=-1, computes start/end ticks from DAT_140b15418 (game mode tick at +0x10) and DAT_140b0a340 (time scale). + - **fields_written**: + - **+0x2E3**: 1 (capture_timer_active) + - **+0x2E8**: current_tick (from GameModeClass+0x10) + - **+0x2EC**: current_tick + (time_scale * duration) + - **+0x2F8**: -1 + + +### Relationships + +- **planet_object_to_data_pack**: + - **description**: A planet on the galactic map is a GameObjectClass with a PlanetaryBehaviorClass component. The behavior at +0x18 points to the parent GameObjectClass. The parent's field at [0x17*8]=+0xB8 points to the PlanetaryDataPackClass. The data pack holds ownership, trade routes, income, capture state, and corruption. + - **traversal**: planet_game_object -> [+0xB8] -> PlanetaryDataPackClass + - **alternative**: PlanetaryBehaviorClass.offset_0x18 -> GameObjectClass -> [+0xB8] -> PlanetaryDataPackClass +- **planet_ownership**: + - **description**: Planet ownership is stored at PlanetaryDataPackClass+0x6C as a player_id (int32). This indexes into the PlayerListClass at DAT_140a16fd0. The owning player's faction is at PlayerObject+0x68. Ownership changes go through PlanetFactionChangeClass which updates both the data pack fields and notifies all players. + - **read_path**: planet_data+0x6C -> player_id -> PlayerList_FindByID(DAT_140a16fd0, player_id) -> PlayerObject + - **write_path**: PlanetFactionChange_Transfer (RVA 0x3FB040) -> writes planet_data+0x6C, fires signals +- **fow_system**: + - **description**: The FOW system is per-player and per-game-mode. The active GameModeClass (at DAT_140b15418) stores an array of per-player FOW objects at offset 0x198. Each FOW object contains three parallel arrays (visibility, timer, output) indexed by grid cell. Lua functions Reveal/Reveal_All/Undo_Reveal_All/Temporary_Reveal are bound through LuaFOWRevealCommandClass. All require tactical mode (not galactic). + - **access_path**: DAT_140b15418 -> [+0x198] -> fow_array[player_id] -> FOWPlayerObject + - **grid_encoding**: + - **0x00**: fully fogged + - **0x01-0x0F**: fading (was revealed, decreasing visibility) + - **0x10-0xEE**: revealed (stable) + - **0xEF**: just-cleared marker (transition state) + - **0xFF**: fully visible (permanent or active unit vision) + - **tactical_only**: True + - **galactic_note**: In galactic mode, planet visibility is handled differently -- through the PlanetReachabilityClass and the per-player-planet-data map at PlanetaryDataPackClass+0xA0. The tactical FOW grid does not exist in galactic mode. +- **trade_routes_adjacency**: + - **description**: Trade routes form the edges of the galactic map graph. Each planet's PlanetaryDataPackClass contains a DynamicVectorClass at byte offset ~0x50 that lists adjacent trade routes. The TradeRouteClass objects themselves are stored in a separate global list (DynamicVectorClass destructor at RVA 0x4AE5E0). The GalacticPathFinderClass singleton uses these adjacency lists for pathfinding. + - **per_planet**: PlanetaryDataPackClass offset ~0x50 = DynamicVectorClass + - **global_list**: DynamicVectorClass (destructor RVA 0x4AE5E0) +- **galactic_perception**: + - **description**: The GalacticPerceptionSystemClass (constructor RVA 0x4E1880) manages what each player can see on the galactic map. This is separate from the tactical FOW system. Planet visibility in galactic mode uses FUN_1403a51e0 which iterates planet components and checks visibility per-player. + - **constructor_rva**: 0x4E1880 + + +### Key Data Globals Summary + +- **DAT_140a16fd0**: + - **purpose**: PlayerListClass global pointer (player_array at +0x20, count at +0x28) + - **rva**: 0xA16FD0 +- **DAT_140a16fd8**: + - **purpose**: End of player array (count = (this - DAT_140a16fd0) >> 3) + - **rva**: 0xA16FD8 +- **DAT_140b15418**: + - **purpose**: Active GameModeClass pointer (FOW array at +0x198, player_count at +0x190) + - **rva**: 0xB15418 +- **DAT_140a172d0**: + - **purpose**: GameObjectType registry (array at +0x10, count at +0x18) + - **rva**: 0xA172D0 +- **DAT_140b153e0**: + - **purpose**: Perception/diplomacy manager global + - **rva**: 0xB153E0 +- **DAT_140a284c4**: + - **purpose**: Global FOW disable toggle (bool) + - **rva**: 0xA284C4 +- **DAT_140b15d74**: + - **purpose**: Diplomatic capture progress multiplier 1 (float) + - **rva**: 0xB15D74 +- **DAT_140b15d78**: + - **purpose**: Diplomatic capture progress multiplier 2 (float) + - **rva**: 0xB15D78 +- **DAT_140b0a340**: + - **purpose**: Game time scale multiplier (for capture timer computation) + - **rva**: 0xB0A340 + + +### Rtti Classes Found + +- PlanetaryBehaviorClass +- PlanetaryDataPackClass +- PlanetFactionChangeClass (nested in PlanetaryDataPackClass) +- PlanetReachabilityClass +- PlanetAITargetLocationClass +- PlanetDestructionAbilityClass +- PlanetIncomeBonusAbilityClass +- PlanetIncomeGamblingAbilityClass +- CorruptPlanetAbilityClass +- GalacticModeClass +- GalacticCameraClass +- GalacticGoalSystemClass +- GalacticPerceptionSystemClass +- GalacticSabotageAbilityClass +- GalacticSellEventClass +- GalacticStealthAbilityClass +- GalacticPathFinderClass (singleton) +- TradeRouteClass +- FindPlanetClass (Lua wrapper) +- LuaFOWRevealCommandClass +- RevealBehaviorClass +- StoryEventSelectPlanetClass +- PlanetaryBombardEventClass + + +### Lua Bindings Discovered + +- **FindPlanetClass**: + - **methods**: Get_All_Planets + - **constructor_rva**: 0x697010 +- **LuaFOWRevealCommandClass**: + - **methods**: Reveal (tactical), Reveal_All, Undo_Reveal_All, Temporary_Reveal + - **constructor_rva**: 0x6A51B0 + - **method_implementations**: + - **Reveal**: 0x6A5700 + - **Reveal_All**: 0x6A5B00 + - **Undo_Reveal_All**: 0x6A53B0 + - **Temporary_Reveal**: 0x6A5CF0 +- **GalacticFreeStoreClass**: + - **constructor_rva**: 0x68EAF0 +- **GalacticTaskForceClass**: + - **constructor_rva**: 0x6CCE20 + + +### Open Questions + +- Exact planet list global: Planets are GameObjectClass instances managed by the galactic mode. The Get_All_Planets Lua function iterates them, but the actual global pointer to the 'planet list' container has not been isolated to a single RVA. It likely lives inside the GalacticModeClass instance (sub-object of GameModeClass) or is accessed via the FindPlanetClass lookup mechanism. +- PlanetaryDataPackClass full size: Destructor analysis shows fields up to at least offset 0x350+ (self[0x34] = byte offset 0x1A0+), but the total size with all DynamicVector members is larger. +- Trade route edge data: The TradeRouteLinkEntryClass internal structure (which planet it connects to, travel time, hyperlane type) needs runtime validation. +- Galactic FOW vs tactical FOW: Galactic mode visibility is handled by GalacticPerceptionSystemClass and per-player planet data, NOT by the tactical FOW grid system. The exact galactic visibility structure needs further RE. +- Planet position data: Where x/y/z galactic map coordinates are stored on the planet object. Likely inherited from GameObjectClass transform component. + diff --git a/docs/editor/systems/network.md b/docs/editor/systems/network.md new file mode 100644 index 000000000..de90c3c8f --- /dev/null +++ b/docs/editor/systems/network.md @@ -0,0 +1,221 @@ +# Network + +**Architecture**: peer-to-peer deterministic lockstep + + +### Transport + +- SteamAsyncSocket(0x6B370) +- WinsockAsyncSocket(0x227110) + + +### Sync Events + +| Key | Value | +|-----|-------| +| FrameInfo | 0 | +| FrameSync | 17 | +| PerformanceMetrics | 18 | + +**Total Event Types**: 58 + +**Serialization**: BitStreamClass (bit-packed, not byte-aligned) + + +### Sentinel Values + +| Key | Value | +|-----|-------| +| 0x3FFFFF | null object ref | +| 0xFFFFFFFF | unscheduled/invalid | + + +## RE Findings Detail + +**Analysis**: SWFOC Alamo Engine — Network/Multiplayer Protocol Documentation + +Reverse-engineered documentation of the multiplayer architecture, command event format, synchronization mechanism, and desync-relevant memory areas in Star Wars: Forces of Corruption (Alamo engine, x86_64 Steam build). + +- **Date**: 2026-04-04 +- **Analyst**: Agent 3G (Network/Multiplayer Protocol RE) + + +### Architecture + +- **type**: peer_to_peer_lockstep +- **summary**: The Alamo engine uses a deterministic lockstep architecture. All peers execute the same simulation independently, synchronizing only player commands (events). There is no authoritative server — every peer runs the full game simulation. The FrameSyncEventClass (event ID 17) acts as the lockstep barrier, and FrameInfoEventClass (event ID 0) exchanges per-frame metadata. PerformanceMetricsEventClass (event ID 18) likely carries timing/checksum data for desync detection. +- **evidence**: FrameSyncEventClass inherits ScheduledEventClass -> EventClass, sets event_type_id = 0x11 (17). It is registered via EventFactoryClass. The name 'FrameSync' directly indicates lockstep frame synchronization., FrameInfoEventClass (event ID 0) is the lowest-numbered event and inherits directly from EventClass (not ScheduledEventClass), suggesting it is a meta/control event exchanged every frame., PerformanceMetricsEventClass (event ID 18 = 0x12) also inherits directly from EventClass (not ScheduledEventClass), indicating it is a non-gameplay control message — likely carrying frame timing and/or checksum data., All gameplay-affecting events (Attack, Move, Production, etc.) inherit from ScheduledEventClass, meaning they are queued and executed at a deterministic frame. This is the hallmark of lockstep: commands are scheduled for a future frame, then all peers execute them simultaneously., No 'Replicate', 'NetState', 'AuthoritativeServer', or state-replication classes were found. All discovered event classes are player commands, not state snapshots — confirming command-based lockstep rather than state replication., The GameModeClass constructor allocates a DynamicVectorClass — a queue for events that must wait until their scheduled frame arrives. + + +### Transport Layer + +- **summary**: Dual transport: Winsock (direct IP/LAN) and Steam Networking (online via Steam). Both implement an AsyncSocket interface, abstracted behind PacketHandlerClass which runs on its own thread. +- **socket_implementations**: + - **SteamAsyncSocketImpl**: + - **constructor_rva**: 0x6b370 + - **description**: Steam-based socket using Valve's ISteamNetworking relay. Wraps Steam P2P networking API. Initialized with linked list node (0x30 bytes) and 64-bit SteamID handle at offset 0x20. + - **inherits**: RefCountClass + - **WinsockAsyncSocketImpl**: + - **constructor_rva**: 0x227110 + - **description**: Traditional Winsock socket for LAN/direct-IP games. Constructor takes a 16-byte param (likely sockaddr_in: IP + port). Calls WSAGetLastError (imported at 0x227400) for error handling. + - **inherits**: RefCountClass +- **packet_handler**: + - **class**: PacketHandlerClass + - **destructor_rva**: 0x2054c0 + - **description**: Thread-based packet processing engine. Inherits from ThreadClass. Uses Windows CriticalSection for thread safety, a mutex with 10-second timeout for shutdown synchronization, and a linked-list packet queue. Dispatches received packets to registered callback functions typed as: void(*)(PacketHandlerClass*, void*, IPAddressClass&). Also supports raw byte callbacks: bool(*)(unsigned char*, int, IPAddressClass*, void*). + - **threading_note**: PacketHandlerClass::~PacketHandlerClass logs 'ThreadLockMutexClass -- %s failed to obtain mutex within 10 seconds' — confirming it runs on a dedicated network thread separate from the game thread. +- **packet_class**: + - **class**: PacketClass + - **constructor_rva**: 0x23bc40 + - **description**: Network packet. Inherits from BitStreamClass (bit-level serialization) and MultiLinkedListMember (for queue management). Constructor takes (int size, longlong buffer). If buffer is null and size > 0, allocates its own buffer. The BitStreamClass base provides bit-granularity read/write with separate read and write cursors. + - **inherits**: BitStreamClass, MultiLinkedListMember, RefCountClass + - **serialization_format**: bit-packed via BitStreamClass — fields are written at arbitrary bit widths, not byte-aligned. This is typical for lockstep RTS games to minimize bandwidth. +- **ip_address_class**: + - **class**: IPAddressClass + - **description**: Wraps an IP address. Used in packet dispatch callbacks and maintained in DynamicVectorClass collections. +- **broadcaster_class**: + - **class**: BroadcasterClass + - **description**: Manages packet distribution to connected peers. Contains a DynamicVectorClass for tracking sent packets and a DynamicVectorClass for the peer list. Also referenced as BroadcasterClass::Get_Local_Player in the knowledge base, indicating it tracks which peer is the local player. + - **inner_types**: BroadcasterClass::PacketIdentifierStruct +- **connection_class**: + - **class**: ConnectionClass + - **description**: Represents a single peer connection. Stored in DynamicVectorClass managed by BroadcasterClass. +- **packet_type_class**: + - **class**: PacketTypeClass + - **description**: Packet type registry with magic number validation. Contains DynamicVectorClass — each packet type has a 'magic' identifier for protocol-level validation and dispatch. + - **inner_types**: PacketTypeClass::tPacketMagicStruct + + +### Steam Integration + +- **summary**: Online multiplayer uses Steam lobbies via SteamPeerLobbyClass (singleton). Matchmaking, chat, mod sync, and player management all flow through Steam callbacks. +- **steam_api_imports**: SteamInternal_CreateInterface, SteamInternal_ContextInit, SteamAPI_GetHSteamUser, SteamAPI_GetHSteamPipe, SteamAPI_Init, SteamAPI_RegisterCallback, SteamAPI_UnregisterCallback, SteamAPI_RegisterCallResult, SteamAPI_UnregisterCallResult, SteamAPI_RunCallbacks, SteamAPI_Shutdown +- **steam_peer_lobby_class**: + - **class**: SteamPeerLobbyClass + - **constructor_rva**: 0x6ca10 + - **singleton**: True + - **description**: Central Steam multiplayer manager. Registers 6 persistent callbacks and 3 call-result handlers with the Steam API during construction. + - **steam_callbacks_registered**: {'type': 'LobbyInvite_t', 'callback_id': '0x1F7 (503)', 'handler_rva': '0x72540', 'description': 'Fires when the local user receives a lobby invite from a friend.'}, {'type': 'LobbyKicked_t', 'callback_id': '0x200 (512)', 'handler_rva': '0x72550', 'description': 'Fires when the local user is kicked from a lobby.'}, {'type': 'LobbyChatMsg_t', 'callback_id': '0x1FB (507)', 'handler_rva': '0x72570', 'description': 'Fires when a chat message is received in the lobby.'}, {'type': 'LobbyDataUpdate_t', 'callback_id': '0x1F9 (505)', 'handler_rva': '0x726C0', 'description': 'Fires when lobby metadata changes (player ready state, game settings, etc.).'}, {'type': 'LobbyChatUpdate_t', 'callback_id': '0x1FA (506)', 'handler_rva': '0x726D0', 'description': 'Fires when a user joins, leaves, or disconnects from the lobby.'}, {'type': 'ItemInstalled_t', 'callback_id': '0xD4D (3405)', 'handler_rva': '0x72730', 'description': 'Fires when a Steam Workshop item (mod) is installed.'} + - **steam_call_results**: {'type': 'LobbyMatchList_t', 'callback_id': '0x1FE (510)', 'description': 'Result of ISteamMatchmaking::RequestLobbyList — returns available lobbies.'}, {'type': 'LobbyCreated_t', 'callback_id': '0x201 (513)', 'description': 'Result of ISteamMatchmaking::CreateLobby — returns new lobby ID.'}, {'type': 'LobbyEnter_t', 'callback_id': '0x1F8 (504)', 'description': 'Result of ISteamMatchmaking::JoinLobby — confirms lobby entry.'} + - **ugc_integration**: CCallResult at 0x6ca10 shows SteamPeerLobbyClass also handles Workshop (UGC) query results, likely for mod compatibility checking in the lobby. +- **steam_lobby_dialog_class**: + - **class**: SteamLobbyDialogClass + - **singleton**: True + - **description**: UI dialog for the Steam lobby browser/creation screen. +- **steam_class**: + - **class**: SteamClass + - **constructor_rva**: 0x6a000 + - **singleton**: True + - **description**: Top-level Steam integration singleton. Wraps initialization/shutdown. +- **internet_player_struct**: + - **class**: InternetPlayerStruct + - **description**: Represents a player in an online (Internet/Steam) game. Stored in DynamicVectorClass collections (multiple instances at 0x17260, 0x332c30 etc.). Contains a ClanStruct sub-struct for Steam clan/group information. + - **inner_types**: InternetPlayerStruct::ClanStruct + + +### Event System + +- **summary**: The command synchronization protocol is built on an event system. All player actions are encoded as Event objects with a numeric type ID, serialized via BitStreamClass into PacketClass packets, broadcast to all peers, and executed deterministically at a scheduled frame. The EventFactoryClass pattern uses template parameters for registration. +- **class_hierarchy**: + - **EventClass**: + - **constructor_rva**: 0x5126c0 + - **description**: Base event class. Has a type ID at offset 0x8 (set in each subclass constructor), a linked-list entry for queue management, and a frame counter from a global source. + - **key_fields**: {'offset': '0x00', 'name': 'linked_list_entry', 'type': 'LinkedListEntryClass'}, {'offset': '0x08', 'name': 'event_type_id', 'type': 'int32', 'description': 'Numeric event type. Set in each subclass constructor. Matches the factory template parameter.'} + - **ScheduledEventClass**: + - **constructor_rva**: 0x512c50 + - **inherits**: EventClass + - **description**: Event scheduled for execution at a specific future frame. Most gameplay events inherit from this, not raw EventClass. Contains a scheduled frame number at offset 0x0 (initialized to 0xFFFFFFFF = unscheduled). + - **key_fields**: {'offset': '0x00', 'name': 'scheduled_frame', 'type': 'int32', 'description': "Frame number at which this event should execute. 0xFFFFFFFF = not yet scheduled. 0x80000000 bit at offset 0x1c may be a 'processed' flag."} + - **EventQueueClass**: + - **constructor_rva**: 0x4b39e0 + - **description**: Queue that holds events awaiting execution. Allocates a slot array sized from a global (DAT_140a16fb0 = likely max_players or max_events_per_frame). Events are dequeued and executed when their scheduled frame arrives. + - **BaseEventFactoryClass**: + - **constructor_rva**: 0x5960f0 + - **description**: Base factory that registers event types. Constructor adds itself to a global DynamicVectorClass at DAT_140b36bc1, building the event type registry at static initialization time. +- **event_catalog**: + - **_note**: Complete catalog of all 50+ event types. The type_id matches the EventFactoryClass template second parameter AND the value written to EventClass.offset_0x8 in each constructor. Events marked 'scheduled' inherit from ScheduledEventClass; 'immediate' inherit directly from EventClass. + - **control_events**: {'id': 0, 'hex': '0x00', 'class': 'FrameInfoEventClass', 'rva': '0x4c1b00', 'scheduled': False, 'category': 'sync', 'description': 'Per-frame metadata exchange. Immediate (not scheduled). Lowest ID = processed first. Likely carries frame number and timing data.'}, {'id': 17, 'hex': '0x11', 'class': 'FrameSyncEventClass', 'rva': '0x4c1d30', 'scheduled': True, 'category': 'sync', 'description': 'Lockstep frame synchronization barrier. All peers must receive this before advancing to the next simulation frame.'}, {'id': 18, 'hex': '0x12', 'class': 'PerformanceMetricsEventClass', 'rva': '0x4c1eb0', 'scheduled': False, 'category': 'sync', 'description': 'Performance/checksum data exchange. Immediate (not scheduled). Likely carries frame timing metrics and state checksums for desync detection.'}, {'id': 15, 'hex': '0x0F', 'class': 'QuitGameEventClass', 'rva': '0x4993f0', 'scheduled': True, 'category': 'session', 'description': 'Player quit/disconnect notification.'}, {'id': 16, 'hex': '0x10', 'class': 'ChatEventClass', 'rva': '0x44d1d0', 'scheduled': False, 'category': 'social', 'description': 'In-game chat message. Immediate (not frame-synced). Has string buffer fields.'}, {'id': 39, 'hex': '0x27', 'class': 'GameOptionsEventClass', 'rva': '0x44d530', 'scheduled': True, 'category': 'session', 'description': 'Game options/settings change (speed, difficulty, etc.).'}, {'id': 51, 'hex': '0x33', 'class': 'ResumeGameEventClass', 'rva': '0x4adfd0', 'scheduled': True, 'category': 'session', 'description': 'Resume game from pause.'}, {'id': 36, 'hex': '0x24', 'class': 'SaveGameEventClass', 'rva': '0x48fa80', 'scheduled': True, 'category': 'session', 'description': 'Trigger synchronized save. Has string field (save name).'}, {'id': 11, 'hex': '0x0B', 'class': 'DebugEventClass', 'rva': '0x4ad0f0', 'scheduled': True, 'category': 'debug', 'description': 'Debug/diagnostic command. Has string buffer.'}, {'id': 50, 'hex': '0x32', 'class': 'TauntEventClass', 'rva': '0x4d6a60', 'scheduled': True, 'category': 'social', 'description': 'Player taunt/emote in multiplayer.'} + - **movement_events**: {'id': 1, 'hex': '0x01', 'class': 'MoveToPositionEventClass', 'rva': '0x3acee0', 'scheduled': True, 'category': 'movement', 'description': 'Move units to a map position.'}, {'id': 2, 'hex': '0x02', 'class': 'MoveToObjectEventClass', 'rva': '0x3aee50', 'scheduled': True, 'category': 'movement', 'description': 'Move units to a target object.'}, {'id': 3, 'hex': '0x03', 'class': 'MoveObjectToObjectEventClass', 'rva': '0x3af290', 'scheduled': True, 'category': 'movement', 'description': 'Move an object to another object (e.g., docking).'}, {'id': 4, 'hex': '0x04', 'class': 'LookEventClass', 'rva': '0x689670', 'scheduled': True, 'category': 'movement', 'description': 'Camera look command.'}, {'id': 10, 'hex': '0x0A', 'class': 'MoveThroughObjectsEventClass', 'rva': '0x3c41c0', 'scheduled': True, 'category': 'movement', 'description': 'Move through/past objects (waypoint movement).'}, {'id': 14, 'hex': '0x0E', 'class': 'MoveToRayEventClass', 'rva': '0x3ae4a0', 'scheduled': True, 'category': 'movement', 'description': 'Move to a ray-cast position (terrain click).'}, {'id': 19, 'hex': '0x13', 'class': 'MoveToRayFacingEventClass', 'rva': '0x3ae950', 'scheduled': True, 'category': 'movement', 'description': 'Move to ray position with facing direction.'}, {'id': 20, 'hex': '0x14', 'class': 'FacingEventClass', 'rva': '0x409b40', 'scheduled': True, 'category': 'movement', 'description': 'Change unit facing direction. Has a 0x200-byte unit list.'}, {'id': 29, 'hex': '0x1D', 'class': 'StopMovementEventClass', 'rva': 'n/a', 'scheduled': True, 'category': 'movement', 'description': 'Halt unit movement.'}, {'id': 47, 'hex': '0x2F', 'class': 'MoveToPositionFacingEventClass', 'rva': '0x3add90', 'scheduled': True, 'category': 'movement', 'description': 'Move to position with facing.'}, {'id': 55, 'hex': '0x37', 'class': 'MoveToGarrisonEventClass', 'rva': '0x3ad3b0', 'scheduled': True, 'category': 'movement', 'description': 'Move units into a garrison structure.'} + - **combat_events**: {'id': 6, 'hex': '0x06', 'class': 'AttackEventClass', 'rva': '0x3af4c0', 'scheduled': True, 'category': 'combat', 'description': 'Attack command. Has a 0x208-byte payload (likely unit selection list + target ID).'}, {'id': 13, 'hex': '0x0D', 'class': 'SpecialAbilityEventClass', 'rva': '0x4b4210', 'scheduled': True, 'category': 'combat', 'description': 'Galactic-level special ability activation. Large struct (0x108+ bytes) with object type references, coordinates (0x3fffff sentinel for unset).'}, {'id': 25, 'hex': '0x19', 'class': 'SpecialWeaponFireEventClass', 'rva': 'n/a', 'scheduled': True, 'category': 'combat', 'description': 'Fire special weapon.'}, {'id': 28, 'hex': '0x1C', 'class': 'BombingRunEventClass', 'rva': '0x524560', 'scheduled': True, 'category': 'combat', 'description': 'Call in a bombing run.'}, {'id': 40, 'hex': '0x28', 'class': 'TacticalSpecialAbilityEventClass', 'rva': '0x4292e0', 'scheduled': True, 'category': 'combat', 'description': 'Tactical battle special ability. 0x20C+ byte struct with coordinate sentinels.'}, {'id': 54, 'hex': '0x36', 'class': 'PlanetaryBombardEventClass', 'rva': '0x52afc0', 'scheduled': True, 'category': 'combat', 'description': 'Initiate orbital bombardment.'}, {'id': 57, 'hex': '0x39', 'class': 'TacticalSpecialAbilityWithDummyTargetEventClass', 'rva': 'n/a', 'scheduled': True, 'category': 'combat', 'description': 'Special ability with a dummy/placeholder target.'}, {'id': 38, 'hex': '0x26', 'class': 'TacticalSuperWeaponEventClass', 'rva': 'n/a', 'scheduled': True, 'category': 'combat', 'description': 'Activate a super weapon in tactical mode.'} + - **production_economy_events**: {'id': 7, 'hex': '0x07', 'class': 'ProductionEventClass', 'rva': '0x523f50', 'scheduled': True, 'category': 'economy', 'description': 'Queue/cancel unit or building production.'}, {'id': 8, 'hex': '0x08', 'class': 'FleetManagementEventClass', 'rva': '0x5af020', 'scheduled': True, 'category': 'economy', 'description': 'Fleet assembly/management. Has 4 object ID fields (0x3fffff sentinel) and a signed offset at 0x14.'}, {'id': 9, 'hex': '0x09', 'class': 'InvadeEventClass', 'rva': '0x48fce0', 'scheduled': True, 'category': 'galactic', 'description': 'Invade a planet. 0x200+ byte selection list with planet sentinel.'}, {'id': 12, 'hex': '0x0C', 'class': 'ReinforceEventClass', 'rva': '0x403ab0', 'scheduled': True, 'category': 'economy', 'description': 'Bring in reinforcements. Uses 0x3fffff sentinel for target location.'}, {'id': 33, 'hex': '0x21', 'class': 'TacticalBuildEventClass', 'rva': '0x5d7740', 'scheduled': True, 'category': 'economy', 'description': 'Build a structure in tactical mode.'}, {'id': 34, 'hex': '0x22', 'class': 'TacticalSellEventClass', 'rva': '0x689810', 'scheduled': True, 'category': 'economy', 'description': 'Sell a tactical structure.'}, {'id': 42, 'hex': '0x2A', 'class': 'DistributeMoneyEventClass', 'rva': '0x498ca0', 'scheduled': True, 'category': 'economy', 'description': 'Transfer credits to an ally.'}, {'id': 46, 'hex': '0x2E', 'class': 'GalacticSellEventClass', 'rva': '0x532690', 'scheduled': True, 'category': 'economy', 'description': 'Sell unit/structure at galactic level. 0x3fffff object sentinel.'}, {'id': 45, 'hex': '0x2D', 'class': 'RepairHardpointEventClass', 'rva': 'n/a', 'scheduled': True, 'category': 'economy', 'description': 'Repair a hardpoint on a space station or capital ship.'} + - **selection_ui_events**: {'id': 5, 'hex': '0x05', 'class': 'SelectEventClass', 'rva': '0x3ac9d0', 'scheduled': True, 'category': 'selection', 'description': 'Unit selection. Has a 0x1FC-byte selection buffer.'}, {'id': 30, 'hex': '0x1E', 'class': 'ControlGroupEventClass', 'rva': '0x436060', 'scheduled': True, 'category': 'selection', 'description': 'Assign/recall control group (Ctrl+1..9).'}, {'id': 37, 'hex': '0x25', 'class': 'SelectAllEventClass', 'rva': '0x437f40', 'scheduled': True, 'category': 'selection', 'description': 'Select all units.'} + - **galactic_events**: {'id': 21, 'hex': '0x15', 'class': 'EscortEventClass', 'rva': '0x3b0010', 'scheduled': True, 'category': 'galactic', 'description': 'Escort fleet action.'}, {'id': 22, 'hex': '0x16', 'class': 'RetreatEventClass', 'rva': 'n/a', 'scheduled': True, 'category': 'galactic', 'description': 'Retreat from battle.'}, {'id': 26, 'hex': '0x1A', 'class': 'CinematicAnimationEventClass', 'rva': '0x6899c0', 'scheduled': True, 'category': 'galactic', 'description': 'Trigger cinematic animation.'}, {'id': 35, 'hex': '0x23', 'class': 'AllyEventClass', 'rva': '0x689ad0', 'scheduled': True, 'category': 'diplomacy', 'description': 'Alliance/diplomacy action. Has player ID field (init -1).'}, {'id': 49, 'hex': '0x31', 'class': 'WithdrawlEventClass', 'rva': '0x4aded0', 'scheduled': True, 'category': 'galactic', 'description': "Withdraw from engagement (note: engine typo 'Withdrawl')."}, {'id': 52, 'hex': '0x34', 'class': 'GarrisonEventClass', 'rva': '0x440ad0', 'scheduled': True, 'category': 'galactic', 'description': 'Garrison troops in a structure.'}, {'id': 48, 'hex': '0x30', 'class': 'PlaceBeaconEventClass', 'rva': 'n/a', 'scheduled': True, 'category': 'galactic', 'description': 'Place a marker/beacon on the map.'}, {'id': 53, 'hex': '0x35', 'class': 'SetMarkerIDEventClass', 'rva': 'n/a', 'scheduled': True, 'category': 'galactic', 'description': 'Set marker identifier.'}, {'id': 56, 'hex': '0x38', 'class': 'SetGUIIndexEventClass', 'rva': 'n/a', 'scheduled': True, 'category': 'ui', 'description': 'Set GUI tab index.'}, {'id': 58, 'hex': '0x3A', 'class': 'SetAbilityAutofireEventClass', 'rva': 'n/a', 'scheduled': True, 'category': 'galactic', 'description': 'Toggle ability autofire mode.'}, {'id': 43, 'hex': '0x2B', 'class': 'SetUnitAbilityModeEventClass', 'rva': 'n/a', 'scheduled': True, 'category': 'galactic', 'description': 'Set unit ability mode (stance/behavior).'} + - **setup_phase_events**: {'id': 31, 'hex': '0x1F', 'class': 'SetupPhaseMoveEventClass', 'rva': '0x5aea90', 'scheduled': True, 'category': 'setup', 'description': 'Pre-battle setup phase unit placement.'}, {'id': 32, 'hex': '0x20', 'class': 'SetupPhaseTriggerEndEventClass', 'rva': '0x5aef30', 'scheduled': True, 'category': 'setup', 'description': 'Signal end of setup phase (player ready).'} + + +### Synchronization Mechanism + +- **summary**: Lockstep with deterministic execution. All peers process the same events at the same frame. Three control events (IDs 0, 17, 18) form the sync protocol. +- **frame_sync_protocol**: + - **description**: Each simulation frame follows this sequence: (1) Exchange FrameInfoEventClass (ID 0) with frame metadata; (2) Wait for FrameSyncEventClass (ID 17) from all peers — this is the lockstep barrier; (3) Execute all ScheduledEvents whose scheduled_frame matches the current frame; (4) Exchange PerformanceMetricsEventClass (ID 18) with timing/checksum data. + - **lockstep_barrier**: FrameSyncEventClass (ID 17). The simulation cannot advance until all peers have confirmed their events for this frame. This is the classic lockstep 'ready' signal. + - **delayed_event_queue**: GameModeClass maintains a DynamicVectorClass — events that arrived but whose scheduled_frame has not yet been reached. They are held in this queue and executed when the frame counter reaches their scheduled_frame value. +- **determinism_requirements**: + - **description**: For lockstep to work, all peers must produce identical simulation results from identical inputs. This means the engine must be fully deterministic: same floating-point math, same RNG sequences, same execution order. + - **implications_for_modding**: Any memory modification that changes gameplay state (HP, damage, position, unit counts, production queues) without going through the event system will cause a desync in multiplayer., Single-player mods that directly write to GameObjectClass fields (e.g., HP at offset 0x5C) are safe in SP but will immediately desync in MP., Visual-only modifications (camera position, UI elements, selection highlights) do NOT cause desyncs because they are not part of the deterministic simulation. +- **sentinel_values**: + - **0x3FFFFF**: Unset/null object reference sentinel. Used extensively in event constructors (FleetManagement, Reinforce, Invade, SpecialAbility, etc.) to indicate 'no target selected'. This is likely an index into a 22-bit object ID space. + - **0xFFFFFFFF**: Unscheduled frame (ScheduledEventClass.scheduled_frame default) or invalid/null reference. + - **0xFFFFFFFE**: Special sentinel in FleetManagementEventClass offset 0x14 — possibly 'auto-assign' or 'any available'. + + +### Desync Detection + +- **summary**: Desync detection is inferred from PerformanceMetricsEventClass (ID 18) and the deterministic lockstep architecture. The engine likely checksums key simulation state each frame and compares across peers. +- **known_mechanisms**: + - **performance_metrics_event**: + - **event_id**: 18 + - **class**: PerformanceMetricsEventClass + - **rva**: 0x4c1eb0 + - **description**: Non-scheduled event (inherits EventClass directly) exchanged every frame. Despite the name 'PerformanceMetrics', in lockstep RTS engines this class typically carries: (1) frame timing data (for adaptive game speed), (2) state checksums (for desync detection). The fact that it is exchanged every frame and is NOT a ScheduledEvent confirms it is sync infrastructure, not gameplay. + - **frame_info_event**: + - **event_id**: 0 + - **class**: FrameInfoEventClass + - **rva**: 0x4c1b00 + - **description**: Frame metadata. Likely carries the current frame number and possibly a hash/checksum of the game state. +- **what_would_be_checksummed**: + - **description**: Based on the event system and Alamo engine structure, the following simulation state is most likely included in per-frame checksums: + - **high_confidence**: Global game frame counter, RNG state (random number generator seed/state), Total unit count per player, Total credits/income per player, Production queue state + - **medium_confidence**: Unit positions (at least checksum of all positions), Unit HP values, Unit ownership (player IDs), Active ability timers, Planet control state (galactic mode) + - **low_confidence_but_possible**: Individual unit state checksums, AI decision state, Pathfinding state +- **what_is_safe_to_modify_in_multiplayer**: + - **safe**: Camera position and zoom (not synchronized), UI state (selection highlights, tooltips, HUD layout), Sound/music settings, Visual effects (particle counts, texture quality), Fog of war reveal (local rendering only — but may still be detected by FoW state checksumming), ChatEventClass content (ID 16 — non-scheduled, social only) + - **unsafe_will_desync**: GameObjectClass.hp (offset 0x5C), GameObjectClass.owner_player_id (offset 0x58), GameObjectClass.object_id (offset 0x50), Any production queue modification, Any unit spawn/destroy outside the event system, Any credit/resource value change, Any RNG state modification, Any movement/position override that bypasses ScheduledEventClass + + +### Game Mode System + +- **summary**: GameModeClass is the battle/map controller. LandModeClass extends it for ground battles. Both manage event processing and the delayed event queue. +- **game_mode_class**: + - **class**: GameModeClass + - **constructor_rva**: 0x35a5e0 + - **description**: Large struct (~0x60 * 8 bytes). Manages the tactical/galactic game state, event queue (DelayedEventStruct vector), objectives (ObjectiveStruct vector), game speed (float at 0x10+4 = 0x3F800000 = 1.0f default), and player-specific state arrays. + - **key_fields_observed**: Offset 0x10+4: game_speed float (default 1.0 = 0x3F800000), Offset 0x28*8: DynamicVectorClass — delayed event queue, Offset 0x2B*8: DynamicVectorClass, Offset 0x37*8: DynamicVectorClass — possibly player scores or frame counters, Offset 0x3B*8: DynamicVectorClass> — per-player object lists +- **land_mode_class**: + - **class**: LandModeClass + - **constructor_rva**: 0x3b5210 + - **inherits**: GameModeClass + - **description**: Ground battle mode. Extends GameModeClass with additional linked lists for ground-specific state and a HeroClashManagerClass at offset 0x3F0. + + +### Key Addresses + +- **_note**: All addresses are absolute (image base 0x140000000). Subtract image base for RVA. +- **event_factory_registry**: DAT_140b36bc1 — global DynamicVectorClass where all event factories register at static init +- **max_event_slots**: DAT_140a16fb0 — global int used to size the EventQueueClass slot array and GameModeClass player arrays +- **frame_counter_source**: FUN_140294a70 with DAT_140a16fd0 — called in EventClass constructor to get current frame number +- **steam_peer_lobby_singleton**: SingletonInstance at static init 0x7e6450 +- **steam_lobby_dialog_singleton**: SingletonInstance at static init 0x7e6590 +- **steam_class_singleton**: SingletonInstance at static init 0x7e6580 + + +### Open Questions + +- Exact checksum algorithm used for desync detection (could not locate a CRC/hash function directly referenced by PerformanceMetricsEventClass — needs runtime tracing) +- Exact format of PerformanceMetricsEventClass payload (needs decompilation of its virtual Pack/Unpack methods, which are in the vtable) +- Whether the engine supports 'late join' or only 'lobby start' (likely lobby-only given pure lockstep) +- Maximum player count (the sentinel 0x3FFFFF = 22 bits suggests up to ~4M object IDs, but player count is likely 2-8 based on the 'max_event_slots' global) +- Whether DebugEventClass (ID 11) is stripped in release builds or can be used to inject commands +- The exact PacketTypeClass::tPacketMagicStruct format — likely a 4-byte magic + version for protocol handshake +- Whether the game uses NAT punchthrough (likely via Steam relay) or requires port forwarding for Winsock mode + diff --git a/docs/editor/systems/production.md b/docs/editor/systems/production.md new file mode 100644 index 000000000..b67a05bd7 --- /dev/null +++ b/docs/editor/systems/production.md @@ -0,0 +1,366 @@ +# Production + +**Pipeline Stages**: 8 + +**Validation Gates**: 18 + +**Can Produce Rva**: 0x2804D0 + +**Build Time Rva**: 0x400370 + +**Build Time Formula**: (base_time * ability_mod * tech_factor * faction_mod * planet_mod) / concurrent_count + +**Queue Entry Size**: 0x38 + +**Event Id**: 7 + + +## RE Findings Detail + +**Analysis**: SWFOC Alamo Engine - Production/Build System Reverse Engineering + +Complete documentation of the production pipeline, build queue, unit cap system, cost deduction, and build time calculation in the Alamo engine (Star Wars: Empire at War - Forces of Corruption). Derived from Ghidra static analysis of StarWarsG.exe (x86_64). + +- **Date**: 2026-04-04 + + +### Overview + +- **summary**: The Alamo engine production system is event-driven. When a player requests unit production, a ProductionEventClass (event ID 7) is created and queued. The engine validates prerequisites (tech level, cost, unit cap, build slot availability), then creates an ObjectUnderConstructionClass entry in the production queue. Build time is computed as a multi-factor formula. Credits are deducted up-front (negative delta to PlayerObject+0x70). When the countdown expires, the unit is spawned and ownership transferred. +- **pipeline_stages**: 1. VALIDATION: CanProduce check (FUN_1402804d0) - 15+ prerequisite gates, 2. COST CHECK: GetAdjustedCost (FUN_140400240/FUN_1403711c0) - calculates modified cost, 3. UNIT CAP CHECK: CheckPopCap (FUN_1402ac320) - validates population capacity, 4. QUEUE ENTRY CREATION: ObjectUnderConstructionClass allocation (0x38 bytes), 5. COST DEDUCTION: AddCredits (FUN_14027f370) with negative cost value, 6. BUILD TIME CALCULATION: ComputeBuildTime (FUN_140400370) - 5-factor formula, 7. COUNTDOWN: ScheduledEventClass timer decrements per frame, 8. COMPLETION: ObjectUnderConstructionClass destructor spawns the unit + + +### Classes + +- **ProductionEventClass**: + - **description**: Network event for production commands. Extends ScheduledEventClass. Event type ID = 7. Created when a player clicks to produce a unit/building in the UI or via Lua _ProduceObject. + - **constructor_rva**: 0x523F50 + - **event_factory_rva**: 0x01A980 + - **event_type_id**: 7 + - **inherits**: ScheduledEventClass, EventClass + - **fields**: {'offset': '0x08', 'name': 'event_type_id', 'type': 'int32', 'value': 7, 'note': 'Set in constructor: this->EventClass_data.offset_0x8 = 7'} +- **TacticalBuildEventClass**: + - **description**: Network event for tactical-mode construction (building structures in land/space battles). Event type ID = 0x21 (33). Extends ScheduledEventClass. + - **constructor_rva**: 0x5D7740 + - **event_factory_rva**: 0x01AF70 + - **event_type_id**: 33 + - **inherits**: ScheduledEventClass + - **fields**: {'offset': '0x00', 'name': 'object_bitmask', 'type': 'int32', 'default': '0x3FFFFF', 'note': 'Bitmask for object/slot reference'}, {'offset': '0x04', 'name': 'target_ref', 'type': 'int64', 'default': '-1 (0xFFFFFFFFFFFFFFFF)'} +- **GalacticSellEventClass**: + - **description**: Network event for selling units/buildings on the galactic map. Event type ID = 0x2E (46). + - **constructor_rva**: 0x532690 + - **event_type_id**: 46 + - **inherits**: ScheduledEventClass + - **fields**: {'offset': '0x00', 'name': 'object_bitmask', 'type': 'int32', 'default': '0x3FFFFF'} +- **ObjectUnderConstructionClass**: + - **description**: Represents a single item in the production queue. Allocated when production begins, destroyed when the unit/building finishes construction. Size = 0x38 bytes. Contains the type being built, build countdown, queue position, and the owner reference. + - **vtable_string**: vftable (set in constructor at 0x3FFF70) + - **allocation_size**: 0x38 + - **constructor_rva**: 0x3FFF70 + - **destructor_rva**: 0x5242C0 + - **completion_handler_rva**: 0x42E890 + - **serialization_rva**: 0x497900 + - **fields**: {'offset': '0x00', 'name': 'vtable_ptr', 'type': 'pointer', 'width': 8, 'description': 'Virtual function table pointer'}, {'offset': '0x08', 'name': 'producing_object_type_ptr', 'type': 'pointer', 'width': 8, 'description': 'Pointer to the GameObjectType being produced. Used to resolve the type name via +0xF8 for logging. Set from param_2 (the unit type). Serialized as field ID 6 in save/load.'}, {'offset': '0x10', 'name': 'build_countdown', 'type': 'float32', 'width': 4, 'description': "Remaining build time in seconds. Computed by ComputeBuildTime (FUN_140400370) and stored as float. Logged as 'countdown is %.2f seconds'. Decremented by the scheduler each frame. Serialized as field ID 7."}, {'offset': '0x14', 'name': 'prerequisite_list', 'type': 'DynamicVectorClass', 'width': 8, 'description': 'Inline DynamicVector for prerequisite tracking. Initialized via FUN_1404ad3a0. Serialized as field ID 0x15.'}, {'offset': '0x1C', 'name': 'prerequisite_count', 'type': 'int32', 'width': 4, 'description': 'Number of prerequisites. Serialized as field ID 9.'}, {'offset': '0x20', 'name': 'player_slot_id', 'type': 'int32', 'width': 4, 'description': "Player slot that requested this production. Copied from param_4+0x4C (player's slot). Serialized as field ID 0x0B. Also stored separately at offset +0x2C."}, {'offset': '0x24', 'name': 'queue_flags', 'type': 'int32', 'width': 4, 'description': 'Production flags (e.g., queued vs immediate). Serialized as field ID 0x0B.'}, {'offset': '0x28', 'name': 'build_cost', 'type': 'float32', 'width': 4, 'description': 'The computed cost at time of queue insertion. Used for refund on cancellation. Equals the result of GetAdjustedCost.'}, {'offset': '0x2C', 'name': 'producing_object_id', 'type': 'int32', 'width': 4, 'description': 'Object ID of the production facility. Copied from param_3+0x50. Serialized as field ID 0x0C. Default 0x3FFFFF.'}, {'offset': '0x30', 'name': 'completion_callback', 'type': 'pointer', 'width': 8, 'description': 'Callback or reference used at completion. Set to 0 in constructor.'} + - **log_message**: Object '%s' added to production queue at '%s' by player %d: countdown is %.2f seconds, once underway. + - **log_format_notes**: arg1 = type_name of unit being built (from producing_object_type_ptr+0xF8), arg2 = type_name of production facility (from param_3->GameObjectType+0xF8), arg3 = player_slot_id, arg4 = build_countdown +- **ProductionBehaviorClass**: + - **description**: Behavior component attached to game objects that can produce units (space stations, factories, barracks, etc.). Inherits from BehaviorClass. Size = 0x40 bytes (from scalar deleting destructor). Manages the production queue for its parent object. + - **destructor_rva**: 0x3FFF10 + - **allocation_size**: 0x40 + - **inherits**: BehaviorClass + - **vtable_count**: 3 + - **note**: Destructor shows 3 vtable assignments before calling BehaviorClass::~BehaviorClass, indicating triple inheritance or 3 interface implementations. +- **ProductionDataPackClass**: + - **description**: Data pack for production state persistence. Contains the production queues (2 queues: galactic units and galactic structures/upgrades), organized as DynamicVectorClass. Discovered in the constructor at 0x559680 where it is initialized with ProductionDataPackClass::vftable. + - **constructor_rva**: 0x559680 + - **inherits**: DynamicVectorClass + - **structure**: + - **queue_count**: 2 + - **queue_types**: Galactic Units (queue 0), Galactic Structures/Upgrades (queue 1) + - **ai_task_list**: MultiLinkedListClass per queue (0x58 bytes each) +- **TacticalBuildObjectsBehaviorClass**: + - **description**: Behavior for tactical-mode building construction. Manages structures built during land/space battles (turrets, repair stations, etc.). + - **destructor_rva**: 0x42D850 + - **allocation_size**: 0x40 + - **inherits**: BehaviorClass +- **TacticalBuildObjectsDataPackClass**: + - **description**: Data pack for tactical build state persistence. Tracks objects built during tactical battles. + - **constructor_rva**: 0x561270 + - **destructor_rva**: 0x5612A0 + - **fields**: {'offset': '0x00', 'name': 'build_count', 'type': 'int32', 'default': 0}, {'offset': '0x08', 'name': 'build_list_ptr', 'type': 'pointer', 'default': 0}, {'offset': '0x10', 'name': 'slot_index', 'type': 'int32', 'default': '-1 (0xFFFFFFFF)'}, {'offset': '0x14', 'name': 'build_state', 'type': 'int32', 'default': 0}, {'offset': '0x1C', 'name': 'build_progress', 'type': 'int32', 'default': 0} +- **TacticalUnderConstructionBehaviorClass**: + - **description**: Behavior for objects currently under construction in tactical mode. Handles the building animation, partial construction, and completion logic. + - **destructor_rva**: 0x5D6D60 + - **data_pack_rva**: 0x5617E0 +- **AIPlanetBuildTaskClass**: + - **description**: AI build task for planet-level production. Used by AI players to queue and prioritize production decisions. Size = 0x58 (from MultiLinkedListClass container). + - **constructor_rva**: 0x6954B0 + - **inherits**: RefCountClass, MultiLinkedListMember + - **fields**: {'offset': '0x00', 'name': 'ref_count', 'type': 'int32'}, {'offset': '0x08', 'name': 'build_target_ptr', 'type': 'pointer', 'description': 'What to build'}, {'offset': '0x10', 'name': 'priority', 'type': 'int32'}, {'offset': '0x18', 'name': 'planet_ref', 'type': 'pointer', 'description': 'Where to build'}, {'offset': '0x28', 'name': 'magic_seed', 'type': 'int64', 'default': '0x5D5E0B6B', 'description': 'Debug/validation magic number'}, {'offset': '0x30', 'name': 'is_active', 'type': 'uint16', 'default': 1}, {'offset': '0x3C', 'name': 'failure_reason', 'type': 'int32', 'default': '-1 (0xFFFFFFFF)'} +- **ProduceForceBlockStatus**: + - **description**: Lua blocking status for the Produce_Force AI command. When AI Lua scripts call Produce_Force(), this BlockingStatus object tracks whether the production is complete. Extends LuaMemberFunctionWrapper. + - **constructor_rva**: 0x6C6500 + - **inherits**: LuaMemberFunctionWrapper + - **fields**: {'offset': '0x38', 'name': 'target_type_ptr', 'type': 'pointer', 'default': 0}, {'offset': '0x40', 'name': 'produce_state', 'type': 'int32', 'default': 0}, {'offset': '0x48', 'name': 'completion_flag', 'type': 'int32', 'default': 0} +- **ReduceProductionPriceAbilityClass**: + - **description**: Special ability that reduces production cost. Applied by hero units or planet bonuses. Extends SpecialAbilityClass. Ability type ID = 3. + - **constructor_rva**: 0x70A280 + - **inherits**: SpecialAbilityClass, IXMLLoadableClass, RefCountClass + - **ability_type_id**: 3 + - **fields**: {'offset': '0x00', 'name': 'price_reduction_factor', 'type': 'float32/pointer', 'description': 'The fractional reduction applied to production cost. Feeds into the cost modifier chain.'}, {'offset': '0x08', 'name': 'scope_filter', 'type': 'pointer', 'description': 'Filter for which unit types are affected by this reduction.'} +- **ReduceProductionTimeAbilityClass**: + - **description**: Special ability that reduces production time. Applied by hero units or planet bonuses. Extends SpecialAbilityClass. Ability type ID = 3. + - **constructor_rva**: 0x70B040 + - **inherits**: SpecialAbilityClass, IXMLLoadableClass, RefCountClass + - **ability_type_id**: 3 + - **fields**: {'offset': '0x00', 'name': 'time_reduction_factor', 'type': 'float32/pointer', 'description': 'The fractional reduction applied to production time. Feeds into the build time modifier chain.'}, {'offset': '0x08', 'name': 'scope_filter', 'type': 'pointer', 'description': 'Filter for which unit types are affected.'} +- **StarbaseUpgradeAbilityClass**: + - **description**: Special ability for upgrading space stations. Ability type ID = 0x0C (12). + - **constructor_rva**: 0x70EB50 + - **inherits**: SpecialAbilityClass + - **ability_type_id**: 12 + + +### Functions + +- **CanProduce**: + - **rva**: 0x2804D0 + - **signature**: bool CanProduce(PlayerObject* player, GameObjectType* type_to_build, GameObjectClass* production_facility, bool check_credits, ...out_params...) + - **description**: Master validation gate for production. Returns 1 if the object can be produced, 0 otherwise. Checks 15+ conditions in sequence. This is the most critical function in the production pipeline. + - **prerequisite_checks**: {'order': 1, 'check': 'type_to_build != NULL', 'failure_code': None, 'description': 'Null type check'}, {'order': 2, 'check': 'production_facility != NULL', 'failure_code': None, 'description': 'Null facility check'}, {'order': 3, 'check': 'production_facility[0x67] != 0xFF', 'failure_code': None, 'description': 'Facility component query type check (has production component)'}, {'order': 4, 'check': 'type_to_build+0x21 != 1', 'failure_code': None, 'description': 'Type is not disabled/locked'}, {'order': 5, 'check': 'FUN_1403751a0(type, player.faction_affiliation)', 'failure_code': None, 'description': "Faction affiliation check - type belongs to player's faction"}, {'order': 6, 'check': 'GetAdjustedCost(type, facility) >= 1', 'failure_code': None, 'description': 'Cost must be positive (at least 1 credit)'}, {'order': 7, 'check': 'ComputeBuildTime(type, facility) >= 0.0', 'failure_code': None, 'description': 'Build time must be non-negative'}, {'order': 8, 'check': '!HasProperty(type, 0x5B, -1)', 'failure_code': None, 'description': "Type does not have 'unbuildable' property flag (0x5B)"}, {'order': 9, 'check': 'IsValidBuildLocation(type) OR HasGroundBuildFlag(type)', 'failure_code': None, 'description': 'Build slot availability check'}, {'order': 10, 'check': 'player.credits >= cost (if check_credits && player.is_human && !free_build_mode)', 'failure_code': None, 'description': 'Credit sufficiency check (only for human players when not in free-build mode). Reads player+0x70 (credits) and player+0x62 (is_human flag)'}, {'order': 11, 'check': 'player.tech_level(+0x84) >= type.required_tech_level(+0x89C) AND player.max_tech(+0x88) >= type.tech_level_req(+0x894)', 'failure_code': 'error_code=1 (insufficient tech)', 'description': 'Tech level prerequisite. Compares PlayerObject+0x84 against GameObjectType+0x89C (min tech level) and PlayerObject+0x88 against GameObjectType+0x894 (tech level requirement).'}, {'order': 12, 'check': 'FUN_1403F8AA0(parent, facility) >= type+0xF0C', 'failure_code': 'error_code=2', 'description': 'Build pad/slot count check. Verifies the planet/facility has enough build slots (GameObjectType+0xF0C).'}, {'order': 13, 'check': 'FUN_1403F8B30(parent, facility) >= type+0xF10', 'failure_code': 'error_code=3', 'description': 'Secondary slot requirement check (GameObjectType+0xF10)'}, {'order': 14, 'check': 'type category check (0x5F or 0x60) - space vs land unit cap', 'failure_code': None, 'description': 'Checks unit cap per build category. For category 0x5F (space): validates against GetMaxSpaceUnits. For category 0x60 (land): validates against GetMaxLandUnits.'}, {'order': 15, 'check': 'HasPrerequisites(type+0xF50, facility_type) if type+0xF60 > 0', 'failure_code': 'error_code=5', 'description': 'Build prerequisites - other structures/tech that must exist'}, {'order': 16, 'check': 'type+0x888 (build_limit_per_player) - check instance count', 'failure_code': None, 'description': 'Per-player build limit. If type+0x888 > 0, counts existing instances across all planets.'}, {'order': 17, 'check': 'type+0x880 (build_limit_global) - check total instances', 'failure_code': 'error_code=6', 'description': 'Global build limit. If type+0x880 > 0, counts instances across all allied players.'}, {'order': 18, 'check': 'HasDuplicateCheck(type, facility_type)', 'failure_code': 'error_code=8', 'description': 'Prevents duplicate unique buildings'} +- **ComputeBuildTime**: + - **rva**: 0x400370 + - **signature**: float ComputeBuildTime(void* production_behavior, GameObjectType* type, GameObjectClass* facility) + - **description**: Computes the actual build time in seconds for producing a unit. Returns a multi-factor product of: base time, ability modifiers, tech level scaling, concurrent build penalty, and faction modifier. This is the core build time formula. + - **formula**: (base_time * price_ability_mod * tech_level_factor * faction_income_mod * planet_modifier) / concurrent_build_count + - **formula_details**: + - **base_time**: + - **source**: GameObjectType+0x890 + - **type**: int32 + - **description**: Base build time in engine units. Multiplied by a global speed scalar at DAT_140b15920 to get seconds. Read by FUN_140371230. + - **global_scalar_rva**: 0xB15920 + - **price_ability_mod**: + - **source**: ReduceProductionTimeAbilityClass via modifier system + - **description**: Computed by FUN_14055a010. Scans active abilities on the production facility and player. Uses a tree-based accumulator with GreaterThan and Plus functors. Returns (1.0 - max_reduction), clamped. + - **tech_level_factor**: + - **source**: DAT_140b16dd4..DAT_140b16de4 (5 float globals) + - **description**: Scaling factor based on tech level difference. Computed as: station_level = facility.parent+0xB8+0x148; diff = station_level - type+0x89C. Maps diff values 1-5 to the 5 globals. Only applies if the type is a 'can_queue_from_starbase' type (category check passes). + - **globals**: + - **tech_diff_1**: DAT_140b16dd4 + - **tech_diff_2**: DAT_140b16dd8 + - **tech_diff_3**: DAT_140b16ddc + - **tech_diff_4**: DAT_140b16de0 + - **tech_diff_5**: DAT_140b16de4 + - **faction_income_mod**: + - **source**: Player faction data -> faction+0x28 + - **description**: Income modifier from faction. Read when player.is_human is true and not in free-build mode. Accessed via FUN_1404b0500(player+0x360)+0x28. + - **planet_modifier**: + - **source**: Planet data array at planet+0x2FE8/0x2FE0 + - **description**: Per-planet build time modifier. Indexed by game mode (space/land). Read by FUN_14033e410. + - **concurrent_build_count**: + - **source**: Counted from production facility's container + - **description**: Number of concurrent build slots being used. Min 1. Each occupied slot at facility+0xB8+0x158/0x170 that shares the same GameObjectType is counted by FUN_1404b8170. Divides the total build time (more slots = faster per-unit, but total resources stay same). + - **return**: float: build time in seconds. Used to initialize ObjectUnderConstructionClass+0x10. +- **GetAdjustedCost**: + - **rva**: 0x400240 + - **signature**: float GetAdjustedCost(void* production_behavior, GameObjectType* type, GameObjectClass* facility) + - **description**: Computes the adjusted credit cost for producing a unit. Applies ability modifiers (ReduceProductionPriceAbilityClass), faction modifiers, and planet-level cost adjustments. + - **formula**: base_cost * ability_price_mod * planet_cost_mod * tech_upgrade_mod + - **formula_details**: + - **base_cost**: + - **source**: GameObjectType+0x86C + - **type**: int32 + - **description**: Base credit cost from XML data. Read by FUN_1403711C0 when no production behavior override is present. + - **ability_price_mod**: + - **source**: ReduceProductionPriceAbilityClass via FUN_140559C10 + - **description**: Price reduction from active abilities. Same tree-based accumulator as build time. Returns (1.0 - max_reduction). + - **planet_cost_mod**: + - **source**: Planet data array at planet+0x3000/0x2FF8 + - **description**: Per-planet cost modifier. Read by FUN_14033E3E0. Indexed by game mode. + - **tech_upgrade_mod**: + - **source**: Player faction upgrade bonus + - **description**: Only applies when type+0x894 (tech_level_req) != 0. Reads from faction via Plus::Plus_Constructor_or_Destructor. + - **return**: float: adjusted credit cost. Negative of this is passed to AddCredits. +- **GetBaseCost**: + - **rva**: 0x3711C0 + - **signature**: int GetBaseCost(GameObjectType* type, GameObjectClass* facility) + - **description**: Returns the base credit cost for a unit type. If the facility has a production behavior override (QueryInterface(6)), delegates to GetAdjustedCost. Otherwise returns type+0x86C * global_scalar. + - **base_cost_field**: GameObjectType+0x86C (int32) +- **AddCredits**: + - **rva**: 0x27F370 + - **signature**: float AddCredits(PlayerObject* player, float amount, bool track_income) + - **description**: Adds (or subtracts) credits from a player. For production, called with negative amount (cost). Writes to player+0x70 (credits). Clamps to [0, max_credits]. If amount > 0 and player has income modifier at +0x360, multiplies by the modifier at [+0x360]->+0x20. Core function for all economic transactions. + - **key_fields**: + - **credits**: PlayerObject+0x70 (float32) + - **max_credits**: PlayerObject+0x74 (float32, negative = no cap) + - **income_modifier_ptr**: PlayerObject+0x360 (pointer to modifier object) + - **income_modifier_offset**: +0x20 from the modifier object (float multiplier for positive adds) +- **CheckPopCap**: + - **rva**: 0x2AC320 + - **signature**: bool CheckPopCap(void* object_list_mgr, GameObjectType* type, uint player_slot, bool include_in_queue) + - **description**: Validates that the player has not exceeded the population/unit cap. Computes current population by iterating all owned objects and summing their population values (GameObjectType+0x2120). Compares against the max cap (GetMaxPopCap). Also counts units currently in production queues if include_in_queue=true. + - **algorithm**: + - **step1**: Get current population count from GetCurrentPopulation (FUN_1402AC700) + - **step2**: Get max population cap from object list manager + - **step3**: Get population value of the requested unit from GetPopValue (FUN_140373500) + - **step4**: Return: pop_value <= (max_cap - current_pop) +- **GetCurrentPopulation**: + - **rva**: 0x2AC700 + - **signature**: int GetCurrentPopulation(void* object_list_mgr, int player_slot) + - **description**: Counts the total population value of all units owned by the specified player. Iterates the player's object list, summing GameObjectType+0x2120 for each object. Also includes ally population if in cooperative mode (FUN_14028AFB0 alliance check). + - **population_value_field**: GameObjectType+0x2120 (int32) + - **base_cap_field**: FactionData+0x2EE8 (int32) - base population cap from faction definition +- **GetPopValue**: + - **rva**: 0x373500 + - **signature**: int GetPopValue(GameObjectType* type, int game_mode) + - **description**: Returns the population value for a unit type based on game mode. Mode 2 (space) reads +0x2128, mode 1 (land) reads +0x212C, default reads +0x2124. + - **fields**: + - **pop_value_default**: GameObjectType+0x2124 (int32) + - **pop_value_space**: GameObjectType+0x2128 (int32) - used when game_mode=2 + - **pop_value_land**: GameObjectType+0x212C (int32) - used when game_mode=1 +- **GetMaxSpaceUnits**: + - **rva**: 0x372740 + - **signature**: int GetMaxSpaceUnits(GameObjectType* type, GameObjectClass* facility) + - **description**: Returns the maximum number of space units. Checks facility+0xB8+0x230 first (planet-specific override). Falls back to type+0x92C (default from XML). + - **override_field**: FacilityParent+0xB8+0x230 + - **default_field**: GameObjectType+0x92C +- **GetMaxLandUnits**: + - **rva**: 0x372760 + - **signature**: int GetMaxLandUnits(GameObjectType* type, GameObjectClass* facility) + - **description**: Returns the maximum number of land units. Checks facility+0xB8+0x234 first. Falls back to type+0x928. + - **override_field**: FacilityParent+0xB8+0x234 + - **default_field**: GameObjectType+0x928 +- **GetMaxStarbaseLevel**: + - **rva**: 0x372780 + - **signature**: int GetMaxStarbaseLevel(GameObjectType* type, GameObjectClass* facility) + - **description**: Returns the maximum starbase upgrade level. Checks facility+0xB8+0x238 first. Falls back to type+0x668. + - **override_field**: FacilityParent+0xB8+0x238 + - **default_field**: GameObjectType+0x668 +- **IsFreeBuilding**: + - **rva**: 0x289050 + - **signature**: bool IsFreeBuilding(PlayerObject* player) + - **description**: Returns true if building is free (AI or sandbox mode). Checks player+0x62 (is_human). If not human, returns the value of global DAT_140b15b20 (free_build_mode flag). + - **is_human_field**: PlayerObject+0x62 + - **free_build_global**: DAT_140b15b20 +- **IsValidBuildLocation**: + - **rva**: 0x282400 + - **signature**: bool IsValidBuildLocation(GameObjectType* type) + - **description**: Checks if a type can be built at the current location. Validates build slot counts (type+0x86C > 0) and build pad requirements (type+0xF0C or type+0xF10). Also checks property flags for build categories (0x5C, 0x5F). +- **CountExistingInstances**: + - **rva**: 0x281C70 + - **signature**: int CountExistingInstances(PlayerObject* player, GameObjectType* type, bool include_allies) + - **description**: Counts how many instances of the given type the player (and optionally allies) currently own. Used for build limit enforcement (type+0x888 per-player limit, type+0x880 global limit). Includes instances at player+0x198 (owned objects list) and recursively checks sub-types at type+0x958 and type+0x978. +- **CountInProductionQueue**: + - **rva**: 0x281DF0 + - **signature**: int CountInProductionQueue(PlayerObject* player, GameObjectType* type, bool include_allies) + - **description**: Counts how many of the given type are currently queued for production across all production facilities. Iterates the global object list and checks each facility's production queue. Also counts from player+0x2C8 (pending build list with count at +0x2D0). +- **CountQueuedByCategory**: + - **rva**: 0x281AC0 + - **signature**: int CountQueuedByCategory(PlayerObject* player, GameObjectType* type, bool include_allies) + - **description**: Counts queued units matching a specific category. Uses player+0x3A0 for fast lookup if available, otherwise iterates the full object list via FUN_1402A9FF0. +- **ProductionComplete**: + - **rva**: 0x42E890 + - **signature**: void ProductionComplete(ObjectUnderConstructionClass* this, GameObjectClass* facility, GameObjectType* type) + - **description**: Handles completion of a production queue item. Resolves the spawn location, creates the actual unit via FUN_14029F810, transfers ownership, fires completion events, and cleans up the queue entry. Logs: '%s: Tactical construction of final %s is complete.' Reads construction position from facility+0x84 area. + - **key_operations**: Resolve planet location via facility+0x150 -> +0x8 chain, Check galaxy map mode via DAT_140a16fd0, Create the actual game object via FUN_14029F810, Fire ProductionCompleteEvent (event type 0x22) via FUN_140220ED0, Transfer unit properties from template, Clean up ObjectUnderConstructionClass +- **QueueInsert**: + - **rva**: 0x46FC0 + - **signature**: void QueueInsert(DynamicVector* queue, ObjectUnderConstructionClass** entry) + - **description**: Inserts an ObjectUnderConstructionClass entry into the production queue DynamicVector. + + +### Gameobjecttype Production Fields + +- **description**: Fields on GameObjectType (the type definition struct) that control production behavior. These are set from XML data definitions. +- **fields**: {'offset': '0x86C', 'name': 'build_cost', 'type': 'int32', 'description': 'Base credit cost to produce this unit. Read by GetBaseCost and the production validator.'}, {'offset': '0x87C', 'name': 'build_limit_per_location', 'type': 'int32', 'description': 'Max instances of this type per build location (planet). -1 = unlimited.'}, {'offset': '0x880', 'name': 'build_limit_global', 'type': 'int32', 'description': 'Max instances across all planets/players. 0 = cannot build. Positive = limit. Checked by CountExistingInstances + CountInProductionQueue.'}, {'offset': '0x888', 'name': 'build_limit_per_player', 'type': 'int32', 'description': 'Max instances per player. 0 = cannot build. Positive = limit.'}, {'offset': '0x890', 'name': 'build_time_base', 'type': 'int32', 'description': 'Base build time in engine units. Multiplied by global scalar (DAT_140b15920) to get seconds.'}, {'offset': '0x894', 'name': 'tech_level_requirement', 'type': 'int32', 'description': 'Required tech level to build. If 0, no tech requirement. Compared against PlayerObject+0x88 (max_tech).'}, {'offset': '0x89C', 'name': 'min_tech_level', 'type': 'int32', 'description': 'Minimum tech level to appear in build menu. Compared against PlayerObject+0x84 (current tech level). Also used for tech-level-based build time scaling.'}, {'offset': '0x8A0', 'name': 'station_level_requirement', 'type': 'int32', 'description': 'Required space station level. Compared against the current station level at facility parent.'}, {'offset': '0x928', 'name': 'max_land_units_default', 'type': 'int32', 'description': 'Default max land unit count (fallback if no planet override).'}, {'offset': '0x92C', 'name': 'max_space_units_default', 'type': 'int32', 'description': 'Default max space unit count (fallback if no planet override).'}, {'offset': '0xC1', 'name': 'requires_special_build_check', 'type': 'uint8', 'description': 'If 1, triggers FUN_140282580 for a special prerequisite check.'}, {'offset': '0xC4', 'name': 'is_multiqueue_type', 'type': 'uint8', 'description': 'If 1, this type supports concurrent multi-queue building (build time divided by active queues).'}, {'offset': '0xF0C', 'name': 'required_build_pads', 'type': 'int32', 'description': 'Number of build pads required on the planet. Compared against actual pad count.'}, {'offset': '0xF10', 'name': 'required_build_slots', 'type': 'int32', 'description': 'Number of build slots required. Secondary slot check.'}, {'offset': '0xF20', 'name': 'build_categories_ptr', 'type': 'pointer', 'description': 'Pointer to array of build category entries. Each entry is 0x10 bytes: [+0x00 ptr, +0x08 ref]. Used for category-based build slot checks.'}, {'offset': '0xF28', 'name': 'build_categories_count', 'type': 'int32', 'description': 'Number of build category entries.'}, {'offset': '0xF50', 'name': 'prerequisites_list', 'type': 'DynamicVectorClass', 'description': 'List of prerequisite type references that must exist before this can be built.'}, {'offset': '0xF60', 'name': 'prerequisites_count', 'type': 'int32', 'description': 'Number of prerequisites. If 0, no prerequisite check.'}, {'offset': '0x2120', 'name': 'pop_value', 'type': 'int32', 'description': 'Population cost (how many pop points this unit consumes). Added to current population when checking cap.'}, {'offset': '0x2124', 'name': 'pop_value_default', 'type': 'int32', 'description': 'Default population value (used when game_mode is not space or land).'}, {'offset': '0x2128', 'name': 'pop_value_space', 'type': 'int32', 'description': 'Population value in space battles (game_mode=2).'}, {'offset': '0x212C', 'name': 'pop_value_land', 'type': 'int32', 'description': 'Population value in land battles (game_mode=1).'} + + +### Player Object Production Fields + +- **description**: Fields on PlayerObject relevant to production. +- **fields**: {'offset': '0x4C', 'name': 'player_slot_id', 'type': 'int32', 'description': "Player's slot index. Used throughout production for ownership."}, {'offset': '0x62', 'name': 'is_human_player', 'type': 'uint8', 'description': '1 = human player (applies credit checks), 0 = AI (may skip credit checks).'}, {'offset': '0x70', 'name': 'credits', 'type': 'float32', 'description': 'Current credits. Deducted up-front when production starts.'}, {'offset': '0x74', 'name': 'max_credits', 'type': 'float32', 'description': 'Maximum credit cap. Negative = unlimited.'}, {'offset': '0x84', 'name': 'tech_level', 'type': 'int32', 'description': 'Current tech level. Must meet type+0x89C to build.'}, {'offset': '0x88', 'name': 'max_tech_level', 'type': 'int32', 'description': 'Maximum tech level. Must meet type+0x894 to build.'}, {'offset': '0x198', 'name': 'owned_objects_list_ptr', 'type': 'pointer', 'description': 'Pointer to the list of owned object-type/count pairs. Used by CountExistingInstances.'}, {'offset': '0x1A0', 'name': 'owned_objects_list_count', 'type': 'int32', 'description': 'Count of entries in the owned objects list.'}, {'offset': '0x2C8', 'name': 'pending_build_list_ptr', 'type': 'pointer', 'description': 'Pointer to list of pending build types (already queued).'}, {'offset': '0x2D0', 'name': 'pending_build_list_count', 'type': 'int32', 'description': 'Count of pending build entries.'}, {'offset': '0x360', 'name': 'income_modifier_ptr', 'type': 'pointer', 'description': 'Pointer to income modifier object. If non-null, positive credit adds are multiplied by [this+0x20].'}, {'offset': '0x370', 'name': 'alliance_lookup_table_ptr', 'type': 'pointer', 'description': 'Pointer to int array indexed by player_slot. Value 0 = allied, non-zero = not allied. Used for shared unit cap counting.'}, {'offset': '0x3A0', 'name': 'queued_production_tracker_ptr', 'type': 'pointer', 'description': 'Pointer to the production queue tracker. Used by CountQueuedByCategory for fast lookup.'} + + +### Production Queue Layout + +- **description**: The production queue is stored inside ProductionDataPackClass, which is accessed via the production facility's behavior component (QueryInterface(6)). Each facility has up to 2 queues. +- **queue_structure**: + - **queue_0**: + - **offset_from_datapack**: 0x08 + - **name**: galactic_unit_queue + - **type**: DynamicVectorClass + - **queue_type_code**: 0x20 + - **description**: Queue for galactic-mode units (ships, vehicles, infantry) + - **queue_1**: + - **offset_from_datapack**: 0x18 + - **name**: galactic_structure_queue + - **type**: DynamicVectorClass + - **queue_type_code**: 0x08 + - **description**: Queue for galactic-mode structures and upgrades +- **queue_selection_logic**: If type+0xF0C != 0 OR HasProperty(type, 0x5F, -1): use queue_1 (structures, offset 0x18, type_code 0x08). Otherwise: use queue_0 (units, offset 0x30, type_code 0x20). Queue is selected in the ObjectUnderConstructionClass constructor at 0x3FFF70. +- **max_queue_depth**: Stored as global at DAT_140b26f78 (int). Checked when player is_human (player+0x62 != 0). AI players bypass queue depth limit. + + +### Build Pipeline Pseudocode + +- **description**: Complete pseudocode for the production pipeline from UI click to unit spawn. +- **pseudocode**: // === STAGE 1: UI/Lua triggers ProductionEventClass (event 7) ===, event = new ProductionEventClass(); // RVA 0x523F50, event.event_type = 7;, EventQueueClass::Enqueue(event); // network-synchronized, , // === STAGE 2: Event processed -> ObjectUnderConstructionClass creation ===, // FUN_1403FFF70 (ObjectUnderConstructionClass constructor/factory), function CreateProductionEntry(player, facility, type_to_build, queue_flags):, // Validate all prerequisites, if (!CanProduce(player, type_to_build, facility, true)):, return false;, , // Get the production behavior from the facility, prod_datapack = facility->parent_object->offset_0xD0;, , // Select queue based on type category, if (type.required_build_pads > 0 || HasProperty(type, 0x5F)):, queue_offset = 0x18; // structure queue, queue_type = 0x08;, else:, queue_offset = 0x30; // unit queue, queue_type = 0x20;, , // Check queue depth for human players, if (player.is_human && prod_datapack[queue_offset].count >= MAX_QUEUE_DEPTH):, return false;, , // Check unit cap, if (!CheckPopCap(object_list, type_to_build, player.slot, true)):, return false;, , // Compute adjusted cost, cost = GetAdjustedCost(prod_behavior, type_to_build, facility);, , // Compute build time, build_time = ComputeBuildTime(prod_behavior, type_to_build, facility);, , // Create the queue entry (0x38 bytes), entry = allocate(0x38);, entry.vtable = ObjectUnderConstructionClass::vftable;, entry.producing_object_type = type_to_build;, entry.build_countdown = build_time;, entry.player_slot_id = player.slot;, entry.producing_object_id = facility.object_id;, entry.build_cost = cost;, , // Deduct credits (if not free building), if (!IsFreeBuilding(player)):, AddCredits(player, -cost, false); // XOR float sign bit via DAT_140800860, , // Insert into queue, QueueInsert(prod_datapack + queue_offset, &entry);, , // Fire network event for sync, FireEvent(facility + 0x38, EVENT_PRODUCTION_STARTED=2, entry);, , // Log, printf("Object '%s' added to production queue at '%s' by player %d: countdown is %.2f seconds",, type_to_build.type_name, facility.type_name, player.slot, build_time);, , return true;, , // === STAGE 3: Build timer countdown (per frame) ===, // ScheduledEventClass timer system decrements entry.build_countdown each frame, // When countdown reaches 0, triggers completion, , // === STAGE 4: Production complete ===, // FUN_14042E890 (ObjectUnderConstructionClass::~ObjectUnderConstructionClass), function OnProductionComplete(entry, facility, type):, planet = facility.parent->offset_0x150->offset_0x8;, spawn_location = ResolveSpawnLocation(facility);, , // Spawn the actual game object, new_object = SpawnObject(player, planet, type, spawn_location);, , // Transfer properties, TransferProperties(new_object, facility);, , // Fire completion event (0x22), FireEvent(new_object + 0x38, EVENT_PRODUCTION_COMPLETE=0x22, new_object);, , // Log, printf("%s: Tactical construction of final '%s' is complete.", facility_name, type_name);, , // Clean up queue entry, DeallocateEntry(entry); + + +### Globals + +- **MAX_QUEUE_DEPTH**: + - **rva**: 0xB26F78 + - **type**: int32 + - **description**: Maximum number of items in a single production queue (for human players). AI bypasses this. +- **BUILD_TIME_SCALAR**: + - **rva**: 0xB15920 + - **type**: float32 + - **description**: Global multiplier applied to base build time (type+0x890) to convert engine units to seconds. +- **FREE_BUILD_MODE**: + - **rva**: 0xB15B20 + - **type**: uint8 + - **description**: Global flag: if set, AI players skip credit checks. +- **TECH_DIFF_SCALARS**: + - **rva_base**: 0xB16DD4 + - **type**: float32[5] + - **description**: 5 floats at 0xB16DD4..0xB16DE4. Build time multiplier based on tech level difference (station_level - required_tech). Index 0 = diff of 1, etc. +- **PLAYER_LIST_GLOBAL**: + - **rva**: 0xA16FD0 + - **type**: pointer + - **description**: Global pointer to PlayerListClass. Used by all production functions to resolve player objects. +- **GAME_STATE_GLOBAL**: + - **rva**: 0xB15418 + - **type**: pointer + - **description**: Global pointer to game state. Accessed for object list management and event dispatch. + + +### Event Types + +- **description**: Network event type IDs relevant to production. +- **events**: {'id': 7, 'name': 'ProductionEventClass', 'description': 'Player requests production of a unit/building'}, {'id': 33, 'name': 'TacticalBuildEventClass', 'description': 'Player requests tactical-mode construction'}, {'id': 46, 'name': 'GalacticSellEventClass', 'description': 'Player sells a unit/building on galactic map'}, {'id': 8, 'name': 'FleetManagementEventClass', 'description': 'Fleet management (related to production output)'}, {'id': 12, 'name': 'ReinforceEventClass', 'description': 'Reinforcement spawn from reinforcement pool'}, {'id': 42, 'name': 'DistributeMoneyEventClass', 'description': 'Credit distribution (trade routes, income)'}, {'id': 9, 'name': 'InvadeEventClass', 'description': 'Planet invasion (triggers production facility capture)'} + + +### Modding Recipes + +- **instant_build**: + - **description**: Set build countdown to 0 immediately after queue insertion. + - **method**: Write 0.0f to ObjectUnderConstructionClass+0x10 (build_countdown) right after it is set. OR patch the ComputeBuildTime function (RVA 0x400370) to always return 0.0f. + - **aob_note**: Locate the ObjectUnderConstructionClass+0x10 write in the constructor at 0x3FFF70. +- **free_build**: + - **description**: Skip the credit deduction. + - **method**: Patch the AddCredits call in the production constructor (0x3FFF70 -> calls 0x27F370 with negative cost). NOP the call or patch the cost to 0. Alternatively, set DAT_140b15b20 (FREE_BUILD_MODE) to 1. + - **global_toggle_rva**: 0xB15B20 +- **unlimited_pop_cap**: + - **description**: Bypass population cap checks. + - **method**: Patch CheckPopCap (FUN_1402AC320) to always return true. OR set FactionData+0x2EE8 to a very high value. +- **unlock_all_tech**: + - **description**: Skip tech level requirements. + - **method**: In CanProduce (0x2804D0), NOP the tech level comparison at the check: 'if player+0x84 != type+0x89C return 0' and 'if player+0x88 < type+0x894 return 0'. + + +### Cross References + +- **description**: Key cross-references between production system and other engine systems. +- **refs**: {'from': 'ProductionBehaviorClass', 'to': 'BehaviorClass', 'relationship': 'inherits', 'note': 'All production facilities have this behavior component attached.'}, {'from': 'ObjectUnderConstructionClass+0x08', 'to': 'GameObjectType', 'relationship': 'references', 'note': 'Points to the type being produced. Type name at +0xF8.'}, {'from': 'CanProduce', 'to': 'PlayerObject+0x70 (credits)', 'relationship': 'reads', 'note': 'Credit check: (int)player.credits >= adjusted_cost'}, {'from': 'CanProduce', 'to': 'PlayerObject+0x84 (tech_level)', 'relationship': 'reads', 'note': 'Tech level gate'}, {'from': 'CheckPopCap', 'to': 'GameObjectType+0x2120 (pop_value)', 'relationship': 'reads', 'note': 'Accumulates population across all owned objects'}, {'from': 'ComputeBuildTime', 'to': 'ReduceProductionTimeAbilityClass', 'relationship': 'queries', 'note': 'Scans active abilities for build time modifiers'}, {'from': 'GetAdjustedCost', 'to': 'ReduceProductionPriceAbilityClass', 'relationship': 'queries', 'note': 'Scans active abilities for cost modifiers'} + diff --git a/docs/editor/systems/save-format.md b/docs/editor/systems/save-format.md new file mode 100644 index 000000000..a2354addb --- /dev/null +++ b/docs/editor/systems/save-format.md @@ -0,0 +1,236 @@ +# Save Format + +**File Extension**: .sav + +**Encoding**: binary little-endian + +**Chunk Format**: Westwood W3D-derived (8-byte header: id+size, bit31=sub-chunks) + +**Micro Chunk Header**: 2 bytes (id+size, max 255 data bytes) + +**Compression**: zlib linked but usage unconfirmed for saves + +**Chunk Writer Ctor**: 0x21FCC0 + +**Chunk Reader Ctor**: 0x220280 + + +## RE Findings Detail + +**Analysis**: SWFOC Save File Format Specification + +Reverse-engineered save file format for Star Wars: Empire at War - Forces of Corruption (Alamo Engine, x86_64 Steam build). Derived from static analysis of StarWarsG.exe via Ghidra MCP. + +- **Date**: 2026-04-04 +- **Analyst**: Agent 3F (Claude) + + +### File Format + +- **extension**: .sav +- **default_directory**: %APPDATA%\Petroglyph\Empire at War - Forces of Corruption\Save\ +- **encoding**: binary, little-endian +- **byte_order**: little-endian (x86) +- **overall_structure**: Hierarchical chunk tree (Westwood/Petroglyph chunk format). No top-level file magic or version header -- the file begins directly with the root chunk. zlib compression (compress2/uncompress2) is linked but usage context is TBD. +- **compression**: + - **library**: zlib (statically linked) + - **functions**: + - **compress2**: 0x1407A1470 + - **uncompress2**: 0x1407A1590 + - **deflateInit_**: 0x1407A3080 + - **deflateInit2_**: 0x1407A2DF0 + - **inflateInit_**: 0x1407A54F0 + - **compress_block**: 0x1407A60A0 + - **notes**: zlib is present and may wrap the chunk payload or the entire file. Alamo engine .meg/.alo files use the same chunk format without compression. Save files may be uncompressed chunk trees or have a thin zlib wrapper. A sample file is needed to confirm. +- **checksum**: + - **method**: none found + - **notes**: No CRC or checksum functions were identified in the save path. The engine does not appear to verify file integrity on load -- it relies on the chunk structure being well-formed. + + +### Chunk Format + +- **description**: The Alamo engine uses a two-tier chunk system inherited from Westwood Studios W3D format. 'Chunks' are large containers that nest, 'micro-chunks' are leaf data units within a chunk. +- **chunk_header**: + - **size**: 8 + - **layout**: {'offset': 0, 'type': 'uint32', 'name': 'chunk_id', 'description': 'Identifies the chunk type. Unique within context.'}, {'offset': 4, 'type': 'uint32', 'name': 'chunk_size', 'description': 'Size of the chunk data in bytes (excludes this 8-byte header). Bit 31 (0x80000000) is set if the chunk contains sub-chunks rather than raw data.'} + - **sub_chunk_flag**: chunk_size & 0x80000000 != 0 means this chunk contains nested sub-chunks. data_size = chunk_size & 0x7FFFFFFF. + - **nesting**: Chunks nest to arbitrary depth. The ChunkWriter/ChunkReader support up to 256 nesting levels (0x800 byte stack / 8 bytes per level). + - **rvas**: + - **ChunkWriterClass_ctor**: 0x14021FCC0 + - **ChunkWriterClass_dtor**: 0x14021FDC0 + - **ChunkWriterClass_Open_Chunk**: 0x14021FE20 + - **ChunkWriterClass_Close_Chunk**: 0x14021FEB0 + - **ChunkWriterClass_Write**: 0x1402200B0 + - **ChunkWriterClass_Write_CString**: 0x140220140 + - **ChunkReaderClass_ctor**: 0x140220280 + - **ChunkReaderClass_dtor**: 0x140220370 + - **ChunkReaderClass_Get_File_Size**: 0x1402203F0 + - **ChunkReaderClass_Tell**: 0x140220460 + - **ChunkReaderClass_Open_Chunk**: 0x1402204A0 + - **ChunkReaderClass_Close_Chunk**: 0x140220520 +- **micro_chunk_header**: + - **size**: 2 + - **layout**: {'offset': 0, 'type': 'uint8', 'name': 'micro_chunk_id', 'description': 'Identifies the micro-chunk type within its parent chunk.'}, {'offset': 1, 'type': 'uint8', 'name': 'micro_chunk_size', 'description': 'Size of the micro-chunk data in bytes (max 255). Excludes this 2-byte header.'} + - **max_data_size**: 255 + - **notes**: Micro-chunks are leaf-level data containers. They cannot nest. The size byte is patched after all data is written (seek-back pattern). + - **rvas**: + - **ChunkWriterClass_Open_Micro_Chunk**: 0x14021FFA0 + - **ChunkWriterClass_Close_Micro_Chunk**: 0x140220030 + + +### String Encoding + +- **save_names**: + - **type**: wchar_t (UTF-16LE) + - **notes**: Save game names are wide strings. The SaveGameStruct stores names as std::wstring (MSVC SSO, inline threshold 7 wchars / 14 bytes). The [AutoSave] sentinel is a 10-wchar wide string. +- **in_file_strings**: + - **type**: null-terminated char (ASCII/ANSI) + - **notes**: ChunkWriterClass::Write_CString writes strlen+1 bytes (includes null terminator). Strings in save data (object type names, faction names, etc.) are single-byte null-terminated. + + +### Io Layer + +- **FileClass**: + - **description**: Core I/O wrapper around Win32 HANDLE (CreateFileA/CloseHandle). Both ChunkWriter and ChunkReader take a FileClass* (or RAMFileClass*) as their stream parameter. + - **handle_offset**: 0x08 (HANDLE, initialized to INVALID_HANDLE_VALUE) + - **filename_offset**: 0x18 (std::string, SSO threshold 15 chars) + - **open_mode_offset**: 0x00 (int: -1=closed, 0=read, 1=write) + - **vtable_methods**: + - **Read**: vtable[0x28] -- read(buf, size) -> bytes_read + - **Write**: vtable[0x30] -- write(buf, size) -> bytes_written + - **Seek**: vtable[0x38] -- seek(offset, whence) whence: 0=SET, 1=CUR, 2=END + - **Tell**: vtable[0x40] -- tell() -> position + - **rvas**: + - **FileClass_ctor_default**: 0x140213010 + - **FileClass_ctor_cstring**: 0x1402130D0 + - **FileClass_ctor_stdstring**: 0x1402131E0 + - **FileClass_dtor**: 0x1402132F0 + - **FileClass_Open**: 0x140213600 +- **RAMFileClass**: + - **description**: In-memory file implementation. Used for serialization buffers. Same vtable interface as FileClass. + - **rvas**: + - **RAMFileClass_ctor1**: 0x1402227E0 + - **RAMFileClass_ctor2**: 0x140222830 + - **RAMFileClass_ctor3**: 0x140222880 + - **RAMFileClass_dtor**: 0x1402228D0 + + +### Save Load System + +- **SaveLoadManagerClass**: + - **description**: Singleton manager for save/load operations. Maintains a DynamicVector of SaveGameStruct entries representing available save slots. + - **SaveGameStruct**: + - **stride**: 0x70 (112 bytes per entry) + - **fields**: {'offset': '0x00', 'type': 'std::wstring', 'name': 'save_name', 'description': 'Wide string save name (SSO: ptr/inline at +0, length at +0x10, capacity at +0x18)'}, {'offset': '0x10', 'type': 'uint64', 'name': 'name_length', 'description': 'wchar_t count of the save name'}, {'offset': '0x18', 'type': 'uint64', 'name': 'name_capacity', 'description': 'Capacity for SSO string (inline threshold: 7 wchars)'} + - **sentinel**: [AutoSave] (10 wchars, L"[AutoSave]") + - **rvas**: + - **SaveGameStruct_Vector_ctor**: 0x140056360 + - **SaveGameStruct_Vector_dtor**: 0x1400581C0 +- **SaveLoadClass**: + - **description**: Core serialization class. Uses ObjectPointerPairClass to track object-to-ID mappings during save/load for pointer resolution. + - **ObjectPointerPairClass_Vector**: + - **global_address**: 0x140A13DB0 + - **layout**: + - **data_ptr**: offset 0x00 + - **count**: offset 0x08 + - **capacity_flags**: offset 0x0C (bit 31 = heap flag) + - **rvas**: + - **ObjectPointerPairClass_Vector_ctors**: 0x1407EC700, 0x1407EC770, 0x1407EC900, 0x1407ECA90 + - **ObjectPointerPairClass_Vector_dtor**: 0x1401FF850 +- **SaveGameEventClass**: + - **description**: Scheduled event that triggers a save. Event type ID: 36 (0x24). Created via EventFactoryClass. + - **size**: 0x70 bytes + - **fields**: {'offset': 'EventClass+0x00', 'type': 'int32', 'name': 'save_slot_index', 'description': 'Index into SaveGameStruct vector. 0xFFFFFFFF = new save.'}, {'offset': 'EventClass+0x08', 'type': 'std::wstring', 'name': 'save_name', 'description': 'Display name of the save.'} + - **event_type_id**: 36 + - **factory_singleton**: 0x140B313E0 + - **execute_handler_rva**: 0x14048FC00 + - **save_dispatch_fptr**: 0x140B313D8 (function pointer to actual save execution) + - **serialize_rva**: 0x14048FB90 + - **deserialize_rva**: 0x14048FB20 + - **rvas**: + - **ctor**: 0x14048FA80 + - **dtor**: 0x14048FAD0 + - **execute**: 0x14048FC00 +- **ObjectPersistenceClass**: + - **description**: Manages persistent object units across save/load. tPersistentUnit is the per-object save record. + - **tPersistentUnit**: + - **notes**: Contains a DynamicVector and vtable. Each unit tracks an object's persistent state. + - **rvas**: + - **tPersistentUnit_Vector_ctor**: 0x1404F2D80 + - **tPersistentUnit_Vector_iterate**: 0x1404F2FF0 + - **tPersistentUnit_Vector_dtor**: 0x140046570 + + +### Persistent Data Classes + +- **description**: These DataPack classes contain the per-object state that gets serialized. Each has serialize/deserialize virtual methods that write/read micro-chunks. +- **PlanetaryDataPackClass**: + - **description**: Per-planet galactic map data. The largest and most important persistent structure for save editing. + - **dtor_rva**: 0x1404B5FB0 + - **sub_structures**: + - **PersistentTacticalBuiltObjectStruct**: + - **description**: Records of structures built on the planet surface in tactical mode. + - **vector_dtor_rva**: 0x1404B5E60 + - **PersistentUpgradeObjectStruct**: + - **description**: Records of upgrades applied to structures on this planet. + - **vector_dtor_rva**: 0x1404B5ED0 + - **LineLinkStruct**: + - **description**: Hyperspace lane connections between planets. + - **vector_dtor_rva**: 0x1404B5DF0 + - **TradeRouteLinkEntryClass**: + - **description**: Trade route connections and state. + - **vector_dtor_rva**: 0x1404B5F40 + - **contains**: DynamicVector, DynamicVector, DynamicVector, DynamicVector, DynamicVector (ground units), DynamicVector (space units), DynamicVector (type references), DynamicVector*> (fog of war), Linked list structures for unit tracking, SSO string for planet name +- **other_datapacks**: + - **description**: Each of these DataPack classes has serialize/deserialize methods. They represent per-object component state in the save file. + - **list**: {'name': 'AIDataPackClass', 'notes': 'AI player state (budgets, goals, perception)'}, {'name': 'AbilityCountdownDataPackClass', 'notes': 'Active ability cooldown timers'}, {'name': 'CombatantBehaviorClass', 'notes': 'Combat statistics and targeting'}, {'name': 'TacticalBuildObjectsDataPackClass', 'notes': 'Build queue and construction state'}, {'name': 'TacticalSuperWeaponDataPackClass', 'notes': 'Superweapon cooldown and targeting'}, {'name': 'LocomotorDataPackClass', 'notes': 'Unit movement state and pathfinding'}, {'name': 'ProjectileDataPackClass', 'notes': 'Active projectile state'}, {'name': 'GarrisonDataPackClass', 'notes': 'Garrisoned unit information'}, {'name': 'InfectionDataPackClass', 'notes': 'Corruption/infection spread state'}, {'name': 'DamageTrackingDataPackClass', 'notes': 'Damage history for combat bonuses'}, {'name': 'BaseShieldDataPackClass', 'notes': 'Shield generator state'}, {'name': 'DeathDataPackClass', 'notes': 'Death animation and cleanup state'}, {'name': 'GUIDataPackClass', 'notes': 'UI-related persistent state'}, {'name': 'SelectionDataPackClass', 'notes': 'Selection group persistence'}, {'name': 'HintDataPackClass', 'notes': 'Tutorial/hint completion state'}, {'name': 'TeamDataPackClass', 'notes': 'Team alliance and diplomacy state'} + + +### Serialization Primitives + +- **description**: Low-level read/write operations used by serialize/deserialize methods. +- **write_int**: + - **rva**: 0x1402046F0 + - **signature**: Write_Int(stream, value, size_type) + - **size_type_7**: 4 bytes (int32) +- **read_int**: + - **rva**: 0x1402043B0 + - **signature**: Read_Int(stream, size_type) -> value +- **write_string**: + - **rva**: 0x140204FB0 + - **signature**: Write_String(stream, std_wstring*) +- **read_string**: + - **rva**: 0x140204AD0 + - **signature**: Read_String(stream, std_wstring*) -> success + + +### Game Mode Classes + +- **GalacticModeClass**: + - **description**: The galactic conquest game mode. Save/load is triggered from this context. + - **ctor_rva**: 0x1404B1270 + - **inherits**: GameModeClass + - **init_fields**: + - **offset_0x338**: 1 (default value) + - **offset_0x358**: 0x0F + - **offset_0x368**: 1 + - **offset_0x36c**: 0x3FFFFF (bitmask, likely faction/player visibility) +- **GameModeClass**: + - **ctor_rva**: 0x14035A5E0 + - **dtor_rva**: 0x14035AD70 + + +### Predicted Save Structure + +- **description**: Based on the class hierarchy and serialization system, the save file is predicted to have this top-level structure. Chunk IDs are UNKNOWN without a sample file -- these are placeholders based on common Alamo engine patterns. +- **tree**: {'chunk': 'ROOT (ID TBD)', 'contains_sub_chunks': True, 'children': [{'chunk': 'SAVE_HEADER (ID TBD)', 'data': ['save_name (wstring)', 'save_timestamp', 'mod_name', 'version_info', 'game_mode_id']}, {'chunk': 'PLAYER_DATA (ID TBD)', 'contains_sub_chunks': True, 'repeats': 'per player', 'children': [{'chunk': 'PLAYER_INFO', 'data': ['player_id', 'faction_name (cstring)', 'credits', 'tech_level', 'is_human', 'team_id']}, {'chunk': 'AI_STATE', 'data': ['AIDataPackClass serialized state']}]}, {'chunk': 'GALACTIC_MAP (ID TBD)', 'contains_sub_chunks': True, 'children': [{'chunk': 'PLANET (ID TBD)', 'repeats': 'per planet', 'contains_sub_chunks': True, 'children': [{'chunk': 'PLANET_INFO', 'data': ['planet_name (cstring)', 'owner_player_id', 'corruption_level']}, {'chunk': 'GROUND_UNITS', 'data': ['PersistentTacticalBuiltObjectStruct array']}, {'chunk': 'SPACE_UNITS', 'data': ['unit type references, counts']}, {'chunk': 'UPGRADES', 'data': ['PersistentUpgradeObjectStruct array']}, {'chunk': 'TRADE_ROUTES', 'data': ['TradeRouteLinkEntryClass array']}, {'chunk': 'VISIBILITY', 'data': ['fog of war modifiers per player']}]}]}, {'chunk': 'OBJECT_PERSISTENCE (ID TBD)', 'contains_sub_chunks': True, 'data': ['ObjectPersistenceClass::tPersistentUnit array -- per-object save state']}, {'chunk': 'STORY_STATE (ID TBD)', 'data': ['story flag completion, campaign progress']}]} +- **confidence**: SPECULATIVE -- based on class hierarchy only. Actual chunk IDs and exact ordering require a sample .sav file hexdump. + + +### Safely Editable Fields + +- **description**: Fields that can be edited with low risk if the chunk structure is preserved. All require maintaining correct chunk sizes. +- **high_confidence**: credits (per player) -- int32, within a micro-chunk, planet_owner (per planet) -- int32, player index, tech_level (per player) -- int32 +- **medium_confidence**: unit_hp (per unit) -- float32, within PersistentTacticalBuiltObjectStruct, corruption_level (per planet) -- likely int32 or float32, ability_cooldown_timers -- float32 values in AbilityCountdownDataPackClass, superweapon_cooldowns -- float32 values in TacticalSuperWeaponDataPackClass +- **dangerous**: save_name -- wstring, changing length will corrupt chunk sizes, object_id mappings -- SaveLoadClass::ObjectPointerPairClass references; corrupting these breaks all pointer resolution on load, Adding/removing units -- requires updating multiple linked structures (PlanetaryDataPackClass vectors, ObjectPersistenceClass, player unit lists), faction_name changes -- referenced by multiple systems, length changes corrupt chunks + diff --git a/docs/editor/systems/story.md b/docs/editor/systems/story.md new file mode 100644 index 000000000..d3da342cb --- /dev/null +++ b/docs/editor/systems/story.md @@ -0,0 +1,531 @@ +# Story + +**Event Types Total**: 61 + +**Unique Classes**: 28 + +**Flag Storage**: CRC32 hash table in StorySubPlotClass+0x20 + +**Hash Algorithm**: CRC32(strupr(name)) XOR 0xDEADBEEF -> LCG(16807, 2^31-1) + +**Reward Dispatch**: string->int via tree at 0xB30728, up to 14 params per event + +**Factory Rva**: 0x453310 + +**Lua Globals Registered**: 14 + +**Flag Event Type Id**: 0x2C + + +## RE Findings Detail + +**Analysis**: SWFOC Alamo Engine — Story/Scripting System Reverse Engineering + +Complete documentation of the story flag system, reward dispatch, Fire_Story_Event mechanism, and event type hierarchy in SWFOC's Alamo engine. Derived from Ghidra decompilation of StarWarsG.exe (x86_64). + +- **Date**: 2026-04-04 +- **Analyst**: Agent 3K (Story/Scripting RE) + + +### Story Architecture Overview + +- **status**: CONFIRMED +- **description**: The story system is built on a 3-layer hierarchy: StorySubPlotClass owns StoryEventClass instances, which inherit from SignalGeneratorClass. Events are stored in a CRC32-keyed hash table within each subplot. The system uses XML-driven story definitions parsed at load time, with events dispatched via a type-ID factory pattern. +- **class_hierarchy**: + - **SignalGeneratorClass**: Base class providing signal/event notification infrastructure + - **StoryEventClass**: Base story event — inherits SignalGeneratorClass. Size >= 0x360 bytes. Contains 15+ MSVC SSO string fields for reward parameters, dialog text, etc. + - **StorySubPlotClass**: Container for events within a story plot. Inherits SignalGeneratorClass. Size >= 0x650 bytes. Owns a CRC32 hash table of events + 61 DynamicVectorClass slots (0x3D). + - **StoryEventWrapper**: Lua wrapper for StoryEventClass — exposed to script with 7 methods + - **StoryPlotWrapper**: Lua wrapper for StorySubPlotClass — exposed to script with 5 methods + + +### Story Flag Storage + +- **status**: CONFIRMED +- **mechanism**: CRC32 hash table within StorySubPlotClass +- **description**: Story flags/events are NOT stored as simple bool/int flags. Instead, each StorySubPlotClass contains a hash table at +0x20 (linked list head pointer) using CRC32(strupr(event_name)) XOR 0xDEADBEEF as the bucket key. Events are looked up case-insensitively by name. Each event node stores the CRC32 at +0x10 and a pointer to the StoryEventClass at +0x18. +- **hash_function**: + - **rva**: 0x215A30 + - **name**: CRC32_Hash + - **description**: Standard CRC32 using a lookup table at DAT_140a14d20. Takes (byte* data, uint length, uint seed). Seed is inverted on entry and result. + - **status**: CONFIRMED +- **event_lookup_function**: + - **rva**: 0x52EF90 + - **name**: StorySubPlot_FindEventByName + - **description**: Looks up a StoryEventClass* by name string. Converts name to uppercase via _strupr, computes CRC32, XORs with 0xDEADBEEF, applies LCG (a=16807 c=0 m=2^31-1) for bucket index, walks linked list comparing CRC32 values. + - **bucket_formula**: bucket_index = LCG(CRC32(strupr(name)) ^ 0xDEADBEEF) & bucket_mask + - **lcg_constants**: + - **a**: 16807 + - **m**: 2147483647 + - **note**: Lehmer/Park-Miller MINSTD generator used for hash distribution + - **status**: CONFIRMED +- **event_active_flag_offset**: 0x4C in StoryEventClass (byte) — set to 0 when event is reset/inactive +- **subplot_hash_table_offset**: +0x20 (linked list root pointer within StorySubPlotClass) +- **subplot_bucket_array_offset**: +0x30 (bucket array pointer), +0x48 (bucket mask) + + +### Event Type Enum + +- **status**: CONFIRMED +- **description**: Event types are identified by integer IDs used in a switch/case factory at RVA 0x453310. The string-to-ID mapping is stored in a global std::map-like tree at DAT_140b30728, keyed by CRC32 of the uppercase type name string. +- **factory_function_rva**: 0x453310 +- **factory_function_name**: StoryEvent_Factory_Create +- **global_type_tree_address**: DAT_140b30728 (global pointer to std::map root node) +- **values**: + - **0x01**: + - **class**: StoryEventEnterClass + - **alloc_size**: 0x3B0 + - **group**: enter/trigger + - **notes**: Shared with cases 2,0x17,0x31,0x33 + - **0x02**: + - **class**: StoryEventEnterClass + - **alloc_size**: 0x3B0 + - **group**: enter/trigger + - **0x03**: + - **class**: StoryEventSingleObjectNameClass + - **alloc_size**: 0x3B0 + - **group**: object-name + - **notes**: Shared with cases 4,0x11,0x12,0x16,0x24,0x26,0x27,0x28,0x37 + - **0x04**: + - **class**: StoryEventSingleObjectNameClass + - **alloc_size**: 0x3B0 + - **group**: object-name + - **0x05**: + - **class**: StoryEventConstructLevelClass + - **alloc_size**: 0x380 + - **group**: construct + - **0x06**: + - **class**: StoryEventDestroyClass + - **alloc_size**: 0x3C8 + - **group**: destroy + - **notes**: Shared with case 8 + - **0x07**: + - **class**: StoryEventDestroyBaseClass + - **alloc_size**: 0x388 + - **group**: destroy-base + - **0x08**: + - **class**: StoryEventDestroyClass + - **alloc_size**: 0x3C8 + - **group**: destroy + - **0x09**: + - **class**: StoryEventBeginEraClass + - **alloc_size**: 0x368 + - **group**: era + - **notes**: Shared with case 0x0A + - **0x0A**: + - **class**: StoryEventBeginEraClass + - **alloc_size**: 0x368 + - **group**: era + - **0x0C**: + - **class**: StoryEventHeroMoveClass + - **alloc_size**: 0x390 + - **group**: hero-move + - **notes**: Shared with case 0x0D + - **0x0D**: + - **class**: StoryEventHeroMoveClass + - **alloc_size**: 0x390 + - **group**: hero-move + - **0x0E**: + - **class**: StoryEventAccumulateClass + - **alloc_size**: 0x368 + - **group**: accumulate + - **0x0F**: + - **class**: StoryEventConquerCountClass + - **alloc_size**: 0x368 + - **group**: conquer-count + - **0x10**: + - **class**: StoryEventElapsedClass + - **alloc_size**: 0x370 + - **group**: elapsed-time + - **0x11**: + - **class**: StoryEventSingleObjectNameClass + - **alloc_size**: 0x3B0 + - **group**: object-name + - **0x12**: + - **class**: StoryEventSingleObjectNameClass + - **alloc_size**: 0x3B0 + - **group**: object-name + - **0x13**: + - **class**: StoryEventWinBattlesClass + - **alloc_size**: 0x3A0 + - **group**: win-battles + - **notes**: Shared with case 0x14 + - **0x14**: + - **class**: StoryEventWinBattlesClass + - **alloc_size**: 0x3A0 + - **group**: win-battles + - **0x15**: + - **class**: StoryEventRetreatClass + - **alloc_size**: 0x368 + - **group**: retreat + - **0x16**: + - **class**: StoryEventSingleObjectNameClass + - **alloc_size**: 0x3B0 + - **group**: object-name + - **0x17**: + - **class**: StoryEventEnterClass + - **alloc_size**: 0x3B0 + - **group**: enter/trigger + - **0x18**: + - **class**: StoryEventStartTacticalClass + - **alloc_size**: 0x398 + - **group**: tactical-start + - **notes**: Shared with case 0x19 + - **0x19**: + - **class**: StoryEventStartTacticalClass + - **alloc_size**: 0x398 + - **group**: tactical-start + - **0x1A**: + - **class**: StoryEventSelectPlanetClass + - **alloc_size**: 0x380 + - **group**: select-planet + - **notes**: Shared with cases 0x1B, 0x1C + - **0x1B**: + - **class**: StoryEventSelectPlanetClass + - **alloc_size**: 0x380 + - **group**: select-planet + - **0x1C**: + - **class**: StoryEventSelectPlanetClass + - **alloc_size**: 0x380 + - **group**: select-planet + - **0x1D**: + - **class**: StoryEventStringClass + - **alloc_size**: 0x378 + - **group**: string-event + - **notes**: Shared with cases 0x1E,0x21,0x32,0x38,0x39,0x3C + - **0x1E**: + - **class**: StoryEventStringClass + - **alloc_size**: 0x378 + - **group**: string-event + - **notes**: Default elapsed timeout is 60.0 if value == -1.0 + - **0x1F**: + - **class**: StoryEventFogRevealClass + - **alloc_size**: 0x390 + - **group**: fog-reveal + - **notes**: Shared with case 0x20 + - **0x20**: + - **class**: StoryEventFogRevealClass + - **alloc_size**: 0x390 + - **group**: fog-reveal + - **0x21**: + - **class**: StoryEventStringClass + - **alloc_size**: 0x378 + - **group**: string-event + - **0x22**: + - **class**: StoryEventClass (base) + - **alloc_size**: 0x360 + - **group**: generic + - **notes**: Uses base StoryEventClass directly — no subclass + - **0x23**: + - **class**: StoryEventAINotificationClass + - **alloc_size**: 0x390 + - **group**: ai-notification + - **0x24**: + - **class**: StoryEventSingleObjectNameClass + - **alloc_size**: 0x3B0 + - **group**: object-name + - **0x25**: + - **class**: StoryEventCommandUnitClass + - **alloc_size**: 0x398 + - **group**: command-unit + - **0x26**: + - **class**: StoryEventSingleObjectNameClass + - **alloc_size**: 0x3B0 + - **group**: object-name + - **0x27**: + - **class**: StoryEventSingleObjectNameClass + - **alloc_size**: 0x3B0 + - **group**: object-name + - **0x28**: + - **class**: StoryEventSingleObjectNameClass + - **alloc_size**: 0x3B0 + - **group**: object-name + - **0x29**: + - **class**: StoryEventGuardUnitClass + - **alloc_size**: 0x390 + - **group**: guard-unit + - **0x2A**: + - **class**: StoryEventProximityClass + - **alloc_size**: 0x398 + - **group**: proximity + - **0x2B**: + - **class**: StoryEventDifficultyClass + - **alloc_size**: 0x368 + - **group**: difficulty + - **0x2C**: + - **class**: StoryEventFlagClass + - **alloc_size**: 0x380 + - **group**: flag-check + - **notes**: This is the Check_Story_Flag / Set_Story_Flag event type + - **0x2D**: + - **class**: StoryEventLoadTacticalClass + - **alloc_size**: 0x398 + - **group**: load-tactical + - **0x2E**: + - **class**: StoryCheckDestroyedClass + - **alloc_size**: 0x378 + - **group**: check-destroyed + - **0x2F**: + - **class**: StoryEventVictoryClass + - **alloc_size**: 0x368 + - **group**: victory + - **0x30**: + - **class**: StoryEventMovieDoneClass + - **alloc_size**: 0x360 + - **group**: movie-done + - **0x31**: + - **class**: StoryEventEnterClass + - **alloc_size**: 0x3B0 + - **group**: enter/trigger + - **0x32**: + - **class**: StoryEventStringClass + - **alloc_size**: 0x378 + - **group**: string-event + - **0x33**: + - **class**: StoryEventEnterClass + - **alloc_size**: 0x3B0 + - **group**: enter/trigger + - **0x34**: + - **class**: StoryEventObjectiveTimeoutClass + - **alloc_size**: 0x390 + - **group**: objective-timeout + - **0x35**: + - **class**: StoryEventCaptureClass + - **alloc_size**: 0x380 + - **group**: capture + - **0x36**: + - **class**: StoryEventCorruptionLevelClass + - **alloc_size**: 0x380 + - **group**: corruption-level + - **notes**: Shared with cases 0x3A, 0x3B + - **0x37**: + - **class**: StoryEventSingleObjectNameClass + - **alloc_size**: 0x3B0 + - **group**: object-name + - **0x38**: + - **class**: StoryEventStringClass + - **alloc_size**: 0x378 + - **group**: string-event + - **0x39**: + - **class**: StoryEventStringClass + - **alloc_size**: 0x378 + - **group**: string-event + - **0x3A**: + - **class**: StoryEventCorruptionLevelClass + - **alloc_size**: 0x380 + - **group**: corruption-level + - **0x3B**: + - **class**: StoryEventCorruptionLevelClass + - **alloc_size**: 0x380 + - **group**: corruption-level + - **0x3C**: + - **class**: StoryEventStringClass + - **alloc_size**: 0x378 + - **group**: string-event +- **total_event_types**: 61 +- **unique_concrete_classes**: 28 + + +### Reward Dispatch System + +- **status**: CONFIRMED +- **description**: Rewards are NOT dispatched via a big switch/case. Instead, each StoryEventClass stores a reward_type as an integer at offset +0x3C (relative to event data start, i.e., after the base class header). The reward type is resolved from a string name via a global std::map tree at DAT_140b30728 using CRC32 lookup. The same tree is used for both event types and reward types. +- **reward_type_resolver_rva**: 0x45C3F0 +- **reward_type_resolver_name**: ResolveRewardTypeFromString +- **reward_type_resolver_description**: Takes a null-terminated string, computes CRC32, searches the global tree (red-black tree rooted at DAT_140b30728) for a matching node. Returns the integer value from node+0x20 (the enum value). Returns 0 if not found. +- **global_type_tree_rva**: 0xB30728 +- **reward_type_offset_in_event**: +0x3C from StoryEventClass data start +- **set_reward_type_lua_rva**: 0x73EEF0 +- **set_reward_type_lua_name**: StoryEventWrapper::Set_Reward_Type +- **set_reward_parameter_lua_rva**: 0x73ECA0 +- **set_reward_parameter_lua_name**: StoryEventWrapper::Set_Reward_Parameter +- **reward_parameter_storage**: StoryEventClass data offsets +0x50 through +0x190 — array of MSVC SSO strings at 0x20 stride, indexed by parameter number. Up to 14 reward parameters. +- **known_reward_type_strings**: Note: The actual string->int mappings are populated at runtime from XML data definitions., Common reward type names from modding community documentation include:, CYCLOPEAN_REVEAL_ALL, GIVE_MONEY, REMOVE_UNIT, SPAWN_HERO, SWITCH_SIDES,, TRIGGER_EVENT, SET_FLAG, DISABLE_UNIT, ENABLE_UNIT, LOCK_PLANET,, FLASH_PLANET, SET_TECH_LEVEL, DUAL_FLASH, DISABLE_BRANCH, ENABLE_BRANCH,, VICTORY, DEFEAT, SPEECH, TUTORIAL, STORY_TRIGGER, FORCE_RETREAT, etc., These are NOT hardcoded in the binary — they are CRC32-hashed from XML at load time. + + +### Fire Story Event Mechanism + +- **status**: CONFIRMED +- **description**: Story events are NOT fired via a single 'Fire_Story_Event' function. Instead, the LuaScriptClass::GetEvent global function (type_id=0x3D=61) is registered as the 'GetEvent' Lua global, which wraps StoryPlotWrapper::Get_Event. The actual Lua Story_Event() and Story_Event_Trigger() are registered elsewhere in the game-mode-specific Lua binding setup (not in LuaScriptClass base). Events fire through the SignalGeneratorClass signal system. +- **lua_global_registration**: + - **function_name**: GetEvent + - **registered_at_rva**: 0x2567B0 + - **description**: LuaScriptClass::RegisterGlobals registers 'GetEvent' as a global Lua function that wraps the story event lookup. The GetEvent object at +0xD8 in LuaScriptClass holds the 0x3D event type count. + - **status**: CONFIRMED +- **story_event_wrapper_methods**: + - **status**: CONFIRMED + - **wrapper_constructor_rva**: 0x73DC80 + - **methods_registered**: {'name': 'method_1 (inherited from base, at 0x40258770)', 'description': 'Common base method — likely Get/Set name or similar'}, {'name': 'Add_Dialog_Text', 'rva': '0x73DF60', 'description': 'Sets dialog text on the event. Accepts variable args (string, GameObjectWrapper, number). Writes to DynamicVector at event+0x2C0.', 'status': 'CONFIRMED'}, {'name': 'Clear_Dialog_Text', 'rva': '0x73E4F0', 'description': 'Clears the dialog text vector at event+0x2C0. Iterates and deallocates string entries at 0x20 stride.', 'status': 'CONFIRMED'}, {'name': 'Set_Dialog', 'rva': '0x73E9A0', 'description': 'Sets dialog identifier string. Writes to event data offset +0x230. Also calls FUN_14047d9c0 for dialog pre-caching.', 'status': 'CONFIRMED'}, {'name': 'Set_Reward_Parameter', 'rva': '0x73ECA0', 'description': 'Sets a reward parameter by index. Takes (number index, string/number value). Writes to event+0x50 + index*0x20. Validates exactly 1 parameter per index. Uses global static buffer at DAT_140b3b450.', 'status': 'CONFIRMED'}, {'name': 'Set_Reward_Type', 'rva': '0x73EEF0', 'description': 'Sets the reward type from a string name. Calls ResolveRewardTypeFromString (0x45C3F0) to convert the string to an enum int. Stores result at event+0x3C.', 'status': 'CONFIRMED'} +- **story_plot_wrapper_methods**: + - **status**: CONFIRMED + - **wrapper_constructor_rva**: 0x724120 + - **methods_registered**: {'name': 'method_1 (inherited, at 0x40258770)', 'description': 'Common base method'}, {'name': 'Get_Event', 'rva': '0x724480', 'description': "Looks up an event by name string within this plot's event hash table. Calls StorySubPlot_FindEventByName (0x52EF90). Returns a StoryEventWrapper if found. Logs error if event not found.", 'status': 'CONFIRMED'}, {'name': 'method_3 (at 0x7243a0, not resolved)', 'description': 'Registered but function address did not resolve to a named function'}, {'name': 'method_4 (at 0x7247a0, not resolved)', 'description': 'Registered but function address did not resolve to a named function'}, {'name': 'Reset', 'rva': '0x7246E0', 'description': 'Resets all events in the subplot. Calls StorySubPlotClass::Reset (0x52FC10) which iterates the linked event list, sets event+0x4C to 0, and calls virtual method [0x20] twice on each event (re-initialization).', 'status': 'CONFIRMED'} + + +### Story Subplot Class + +- **status**: CONFIRMED +- **rtti_mangled**: .?AVStorySubPlotClass@@ +- **constructor_rva**: 0x52D400 +- **destructor_rva**: 0x52E220 +- **inherits**: SignalGeneratorClass +- **size_minimum**: 0x650 +- **key_fields**: + - **+0x08**: boolean: active flag + padding + - **+0x10**: linked list node allocation pointer + - **+0x14**: linked list count/capacity + - **+0x20**: Hash table linked list root (for event lookup by CRC32 name) + - **+0x30**: Hash table bucket array pointer + - **+0x48**: Hash table bucket mask (AND'd with LCG output) + - **+0x58..+0x5F8**: Array of 61 (0x3D) DynamicVectorClass slots (each 0x18 bytes = vtable + ptr + count/capacity) + - **+0x5F8**: Story mode pointer (unknown sub-object) + - **+0x600**: DynamicVectorClass vtable (embedded) + - **+0x608..+0x614**: DynamicVectorClass storage for final event list + - **+0x628**: Plot name string (MSVC SSO, size at +0x638, capacity at +0x640) + - **+0x644**: Boolean: is_active_story flag +- **event_loading_process**: + - **description**: StorySubPlotClass constructor loads events from XML. For each event: (1) read event name, (2) strupr + CRC32, (3) XOR 0xDEADBEEF + LCG for bucket, (4) check for CRC32 collision ('Event already exists!'), (5) parse event XML block via FUN_14045c5d0, (6) create concrete StoryEventClass via factory FUN_1404562a0, (7) insert into hash table, (8) set back-pointer event->subplot at +0x228, (9) assign sequential event index at +0x358. After all events loaded, calls FUN_140530180 to build the 61-slot type-indexed arrays and FUN_140452d70 to compute event dependencies. + - **status**: CONFIRMED + + +### Story Event Class + +- **status**: CONFIRMED +- **rtti_mangled**: .?AVStoryEventClass@@ +- **constructor_rva**: 0x4501D0 +- **destructor_rva**: 0x450600 +- **inherits**: SignalGeneratorClass +- **size_minimum**: 0x360 +- **key_fields**: + - **+0x00..+0x1F**: MSVC SSO string: event name (length at +0x10, capacity at +0x18, inline if cap < 16) + - **+0x20**: Event type ID (int32) — assigned from factory param_1 + - **+0x38**: Event type variant (int32) — subclass-specific + - **+0x3C**: Reward type ID (int32) — set by Set_Reward_Type, resolved via ResolveRewardTypeFromString + - **+0x44**: Unknown int32 — copied from XML parse + - **+0x48**: Unknown int32 — copied from XML parse + - **+0x4C**: Active/triggered flag (byte) — 0 = inactive, set by Reset + - **+0x4D**: Boolean — related to reward parameter presence + - **+0x4E**: Unknown byte — from XML + - **+0x50..+0x190**: Reward parameter strings: array of MSVC SSO strings at 0x20 stride, up to 14 slots + - **+0x1F8..+0x228**: Additional string fields (15 SSO strings total in base class) + - **+0x228**: Back-pointer to owning StorySubPlotClass + - **+0x230**: Dialog ID string (MSVC SSO) + - **+0x250..+0x298**: Additional dialog/reward strings + - **+0x2A8..+0x2C0**: Dialog data array (unknown structure) + - **+0x2C0**: DynamicVector for dialog text entries + - **+0x2F8**: DynamicVector> vtable pointer (nested reward param lists) + - **+0x310**: DynamicVector> vtable pointer (event dependency lists) + - **+0x328**: DynamicVector vtable pointer (direct event refs) + - **+0x340..+0x358**: Dependency tracking: DynamicVectorClass (dependent event pointers, count at +0x350, capacity at +0x354) + - **+0x358**: Event index within subplot (int32) +- **subclasses**: StoryEventEnterClass, StoryEventSingleObjectNameClass, StoryEventConstructLevelClass, StoryEventDestroyClass, StoryEventDestroyBaseClass, StoryEventBeginEraClass, StoryEventHeroMoveClass, StoryEventAccumulateClass, StoryEventConquerCountClass, StoryEventElapsedClass, StoryEventWinBattlesClass, StoryEventRetreatClass, StoryEventStartTacticalClass, StoryEventSelectPlanetClass, StoryEventStringClass, StoryEventFogRevealClass, StoryEventAINotificationClass, StoryEventCommandUnitClass, StoryEventGuardUnitClass, StoryEventProximityClass, StoryEventDifficultyClass, StoryEventFlagClass, StoryEventLoadTacticalClass, StoryCheckDestroyedClass, StoryEventVictoryClass, StoryEventMovieDoneClass, StoryEventObjectiveTimeoutClass, StoryEventCaptureClass, StoryEventCorruptionLevelClass + + +### All Story Related Rvas + +- **status**: CONFIRMED +- **functions**: + - **StoryEventClass::constructor**: 0x4501D0 + - **StoryEventClass::destructor**: 0x450600 + - **StoryEventCommandUnitClass::constructor**: 0x4504E0 + - **StoryEventHeroMoveClass::constructor**: 0x450540 + - **StoryEventSelectPlanetClass::constructor**: 0x5DC4B0 + - **StorySubPlotClass::destructor**: 0x52E220 + - **StorySubPlotClass::constructor_main**: 0x52D400 + - **StorySubPlot_FindEventByName**: 0x52EF90 + - **StorySubPlot_Reset**: 0x52FC10 + - **StorySubPlot_BuildTypeArrays**: 0x530180 + - **StorySubPlot_ComputeDependencies**: 0x452D70 + - **StoryEvent_Factory_Create**: 0x453310 + - **StoryEvent_ParseXMLBlock**: 0x45C5D0 + - **StoryEvent_BuildFromParsed**: 0x4562A0 + - **ResolveRewardTypeFromString**: 0x45C3F0 + - **CRC32_Hash**: 0x215A30 + - **StoryEventWrapper::LuaConstructor**: 0x73DC80 + - **StoryEventWrapper::Add_Dialog_Text**: 0x73DF60 + - **StoryEventWrapper::Clear_Dialog_Text**: 0x73E4F0 + - **StoryEventWrapper::Set_Dialog**: 0x73E9A0 + - **StoryEventWrapper::Set_Reward_Parameter**: 0x73ECA0 + - **StoryEventWrapper::Set_Reward_Type**: 0x73EEF0 + - **StoryPlotWrapper::LuaConstructor**: 0x724120 + - **StoryPlotWrapper::Get_Event**: 0x724480 + - **StoryPlotWrapper::Reset**: 0x7246E0 + - **LuaScriptClass::RegisterGlobals**: 0x2567B0 + - **LuaScriptClass::LuaConstructor**: 0x242570 + - **StoryLogWindow::constructor**: 0x01AB40 + - **DynamicVectorClass::constructor_1**: 0x450590 + - **DynamicVectorClass::constructor_2**: 0x45E670 + - **DynamicVectorClass::destructor**: 0x47D6B0 + - **LuaMemberFunctionWrapper::constructor**: 0x73DC80 + - **LuaMemberFunctionWrapper::constructor**: 0x724120 + - **FUN_14073E590_WrapEventAsLuaObject**: 0x73E590 + - **StoryEvent_DebugLog**: 0x467540 +- **global_data**: + - **DAT_140b30728_EventTypeTree**: 0xB30728 — Global std::map root for event/reward type string-to-int lookup + - **DAT_140b30738_XMLNestingDepth**: 0xB30738 — XML nesting depth counter for recursive parsing + - **DAT_140b3b450_RewardParamBuffer**: 0xB3B450 — Static buffer for Set_Reward_Parameter temporary storage + - **DAT_140b3b458_RewardParamEnd**: 0xB3B458 — End pointer for reward param buffer + - **DAT_140b3b468_DialogGuard**: 0xB3B468 — Thread-safe init guard for dialog system + - **DAT_140a14d20_CRC32Table**: 0xA14D20 — CRC32 lookup table (256 uint32 entries) + - **DAT_140a15738_ScriptIDCounter**: 0xA15738 — Global incrementing script instance ID counter + - **DAT_140a157d0_StringTypeMarker**: 0xA157D0 — Type discriminator marker for Lua string arguments + - **DAT_140a157c0_NumberTypeMarker**: 0xA157C0 — Type discriminator marker for Lua number arguments + - **DAT_140a43b18_GameObjectTypeMarker**: 0xA43B18 — Type discriminator marker for GameObjectWrapper arguments + - **DAT_140a44270_PlayerTypeMarker**: 0xA44270 — Type discriminator marker for PlayerWrapper arguments +- **vtables**: + - **StorySubPlotClass::vftable**: referenced in constructor at 0x52D400 + - **StoryEventClass::vftable**: referenced in constructor at 0x4501D0 + - **StoryEventWrapper::vftable**: referenced in Lua wrapper constructor at 0x73DC80 + - **StoryPlotWrapper::vftable**: referenced in Lua wrapper constructor at 0x724120 + - **LuaScriptClass::vftable**: referenced in LuaScriptClass constructor + + +### How Story Flags Are Checked During Gameplay + +- **status**: CONFIRMED +- **description**: Story flags are checked through the StoryEventFlagClass (event type 0x2C). When the story system evaluates events each tick, it iterates through the 61 type-indexed DynamicVectorClass arrays in the StorySubPlotClass. For type 0x2C (flag events), it checks whether the named flag has been set. Flags are set via the Set_Story_Flag Lua function which fires a story event of type FLAG. The CheckStoryFlagClass (RTTI confirmed) is a separate class used in the condition evaluation chain. The actual flag storage is within the event hash table itself — a flag 'exists and is active' is determined by the event's active byte at +0x4C being non-zero. +- **flag_event_type_id**: 0x2C +- **flag_class**: StoryEventFlagClass +- **check_class**: CheckStoryFlagClass +- **flow**: 1. Lua calls Set_Story_Flag('FLAG_NAME') or Check_Story_Flag('FLAG_NAME'), 2. This triggers a story event of type FLAG (0x2C) or queries the event hash table, 3. StorySubPlot_FindEventByName converts the name to uppercase, computes CRC32, 4. CRC32 XOR 0xDEADBEEF, then LCG for bucket index, 5. Walks the linked list in that bucket comparing CRC32 values, 6. If found, reads the active flag at event+0x4C, 7. For Set: sets event+0x4C to non-zero and triggers dependent events, 8. For Check: returns whether event+0x4C is non-zero + + +### How Rewards Are Dispatched + +- **status**: CONFIRMED +- **description**: When a story event completes (all conditions met), the reward system reads the reward type ID from the event (+0x3C) and the reward parameters from the string array at +0x50. The reward type was parsed from XML at load time via ResolveRewardTypeFromString (0x45C3F0) which hashes the reward type name string and looks it up in the global type tree. Reward dispatch is handled through the event's virtual function table — each concrete event subclass can override the reward execution method. The reward parameters (up to 14 strings at 0x20 stride) provide the data needed for the reward (unit type names, credit amounts, planet names, etc.). +- **flow**: 1. Event conditions are met (checked via per-type virtual methods), 2. Event reads reward_type_id from +0x3C, 3. Event reads reward parameters from +0x50..+0x190 (up to 14 SSO strings), 4. Reward execution dispatched via virtual function table on the event, 5. Specific reward logic (spawn unit, give money, etc.) uses the string params + + +### Lua Script Globals Registered + +- **status**: CONFIRMED +- **registration_function_rva**: 0x2567B0 +- **registered_globals**: {'name': 'StringCompare', 'class': 'LuaStringCompare'}, {'name': '_ScriptExit', 'class': 'LuaScriptExit'}, {'name': '_ScriptMessage', 'class': 'LuaScriptMessage'}, {'name': '_DebugBreak', 'class': 'LuaDebugBreak'}, {'name': '_CustomScriptMessage', 'class': 'LuaCustomScriptMessage'}, {'name': 'ThreadValue', 'type_id': '0x41'}, {'name': 'GlobalValue', 'class': 'GlobalValue'}, {'name': '_MessagePopup', 'class': 'LuaMessagePopup'}, {'name': '_OuputDebug', 'class': 'LuaDebugPrint', 'note': "Typo in original: 'OuputDebug' not 'OutputDebug'"}, {'name': 'GetThreadID', 'class': 'LuaGetThreadID'}, {'name': 'DumpCallStack', 'class': 'DumpCallStack'}, {'name': 'Create_Thread', 'class': 'LuaCreateThread'}, {'name': 'Thread', 'class': 'LuaCreateThread'}, {'name': 'GetEvent', 'class': 'GetEvent', 'event_type_count': 61, 'note': 'This is the story event accessor from Lua scripts'} +- **luascript_method**: + - **name**: Debug_Should_Issue_Event_Alert + - **rva**: 0x17D070 + - **description**: Registered as a method on the LuaScriptClass Lua object + + +### Signal System + +- **status**: CONFIRMED +- **description**: Both StoryEventClass and StorySubPlotClass inherit from SignalGeneratorClass, which provides a publish-subscribe notification system. Events signal their state changes (triggered, completed, reset) to listeners. The SignalDispatcherClass is a singleton that routes signals between generators and listeners. SignalListenerClass instances (e.g., in StorySubPlotClass) subscribe to events and react when they fire. +- **key_classes**: + - **SignalGeneratorClass**: + - **constructor_rva**: 0x240610 + - **SignalListClass**: + - **constructor_rva**: 0x2406C0 + - **SignalDispatcherClass**: + - **constructors**: 0x2215B0, 0x2218C0 + - **SingletonInstance**: + - **rva**: 0x7E7140 + + +### Debug Logging + +- **status**: CONFIRMED +- **log_function_rva**: 0x467540 +- **known_log_messages**: Creating story %s, ___________ Event %s, CRC %u, !!!!!! Event %s, CRC %u already exists!, STORY MODE ERROR! Unable to process event of type %s, Error: Can't create story %s, StoryPlotWrapper::Get_Event -- cannot find event %s in plot %s., ERROR! Story event - Unable to find event %s in event %s, plot %s, ERROR! Compute_Dependants has come across an invalid dependant for %s in plot %s, Reward type %s, StoryEventWrapper::Set_Reward_Type -- invalid number of parameters. Expected 1, got %d., StoryEventWrapper::Set_Reward_Type -- invalid type for parameter 1. Expected string., StoryEventWrapper::Set_Dialog -- invalid number of parameters. Expected 1, got %d., StoryEventWrapper::Set_Dialog -- invalid type for parameter 1. Expected string., StoryEventWrapper::Set_Reward_Parameter -- invalid number of parameters. Expected 2, got %d., StoryEventWrapper::Set_Reward_Parameter -- invalid type for parameter 1. Expected number., StoryEventWrapper::Set_Reward_Parameter -- must set exactly one parameter per parameter index., StoryEventWrapper::Add_Dialog_Text -- invalid number of parameters. Expected 2., StoryEventWrapper::Add_Dialog_Text -- invalid type for parameter 1. Expected string., StoryEventWrapper::Add_Dialog_Text -- invalid type for parameter %d., StoryPlotWrapper::Get_Event -- invalid number of parameters. Expected 1, got %d., StoryPlotWrapper::Get_Event -- invalid type for parameter 1. Expected string. + diff --git a/ghidra_scripts/ExportRTTIToJSON.java b/ghidra_scripts/ExportRTTIToJSON.java new file mode 100644 index 000000000..492ac8768 --- /dev/null +++ b/ghidra_scripts/ExportRTTIToJSON.java @@ -0,0 +1,340 @@ +// ExportRTTIToJSON.java — Ghidra postScript +// Runs after RecoverClassesFromRTTIScript.java to export all recovered RTTI data to JSON. +// Output: /rtti_export_raw.json + +import ghidra.app.script.GhidraScript; +import ghidra.program.model.symbol.*; +import ghidra.program.model.listing.*; +import ghidra.program.model.address.*; +import ghidra.program.model.data.*; +import ghidra.program.model.mem.*; +import ghidra.util.task.TaskMonitor; + +import java.io.*; +import java.util.*; + +public class ExportRTTIToJSON extends GhidraScript { + + private long imageBase; + + @Override + public void run() throws Exception { + imageBase = currentProgram.getImageBase().getOffset(); + String outputPath = System.getProperty("rtti.export.path", + "C:/Users/Prekzursil/Downloads/swfoc_memory/rtti_export_raw.json"); + + println("=== ExportRTTIToJSON starting ==="); + println("Image base: 0x" + Long.toHexString(imageBase)); + println("Output: " + outputPath); + + StringBuilder json = new StringBuilder(); + json.append("{\n"); + json.append(" \"_meta\": {\n"); + json.append(" \"program\": \"").append(esc(currentProgram.getName())).append("\",\n"); + json.append(" \"image_base\": \"0x").append(Long.toHexString(imageBase)).append("\",\n"); + json.append(" \"analyzer\": \"Ghidra ").append(ghidra.framework.Application.getApplicationVersion()).append("\",\n"); + json.append(" \"script\": \"ExportRTTIToJSON\",\n"); + json.append(" \"export_date\": \"").append(new java.text.SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss").format(new Date())).append("\"\n"); + json.append(" },\n"); + + // 1. Export all namespaces (recovered classes) + json.append(" \"namespaces\": [\n"); + exportNamespaces(json); + json.append(" ],\n"); + + // 2. Export all vftable symbols + json.append(" \"vftables\": [\n"); + exportVftables(json); + json.append(" ],\n"); + + // 3. Export RTTI TypeDescriptor data + json.append(" \"type_descriptors\": [\n"); + exportTypeDescriptors(json); + json.append(" ],\n"); + + // 4. Export all labeled functions with their namespace + json.append(" \"functions\": [\n"); + exportFunctions(json); + json.append(" ],\n"); + + // 5. Export strings that contain class-related keywords + json.append(" \"class_strings\": [\n"); + exportClassStrings(json); + json.append(" ]\n"); + + json.append("}\n"); + + // Write output + File outFile = new File(outputPath); + try (PrintWriter pw = new PrintWriter(new FileWriter(outFile))) { + pw.print(json.toString()); + } + println("=== Export complete: " + outFile.getAbsolutePath() + " ==="); + } + + private void exportNamespaces(StringBuilder json) throws Exception { + SymbolTable symTable = currentProgram.getSymbolTable(); + NamespaceManager nsMgr = currentProgram.getNamespaceManager(); + boolean first = true; + + // Get all non-global, non-library namespaces + for (Symbol sym : symTable.getAllSymbols(false)) { + Namespace ns = sym.getParentNamespace(); + if (ns == null || ns.isGlobal() || ns.isLibrary()) continue; + + // We want class-level namespaces that contain vftable or RTTI symbols + String nsName = ns.getName(true); // fully qualified + if (nsName == null || nsName.isEmpty()) continue; + + // Check if this namespace has RTTI-related content + String symName = sym.getName(); + if (symName != null && (symName.contains("vftable") || symName.contains("RTTI") || + symName.contains("typeDescriptor") || symName.contains("TypeDescriptor"))) { + if (!first) json.append(",\n"); + first = false; + json.append(" {\n"); + json.append(" \"namespace\": \"").append(esc(nsName)).append("\",\n"); + json.append(" \"symbol\": \"").append(esc(symName)).append("\",\n"); + json.append(" \"address\": \"0x").append(Long.toHexString(sym.getAddress().getOffset())).append("\",\n"); + json.append(" \"rva\": \"0x").append(Long.toHexString(sym.getAddress().getOffset() - imageBase)).append("\",\n"); + json.append(" \"type\": \"").append(sym.getSymbolType().toString()).append("\"\n"); + json.append(" }"); + } + } + if (!first) json.append("\n"); + } + + private void exportVftables(StringBuilder json) throws Exception { + SymbolTable symTable = currentProgram.getSymbolTable(); + boolean first = true; + + // Find all symbols containing "vftable" + SymbolIterator iter = symTable.getAllSymbols(false); + while (iter.hasNext()) { + Symbol sym = iter.next(); + String name = sym.getName(); + if (name == null) continue; + + if (name.contains("vftable") || name.equals("vftable")) { + if (!first) json.append(",\n"); + first = false; + + Address addr = sym.getAddress(); + String nsName = sym.getParentNamespace() != null ? + sym.getParentNamespace().getName(true) : "global"; + + json.append(" {\n"); + json.append(" \"class\": \"").append(esc(nsName)).append("\",\n"); + json.append(" \"label\": \"").append(esc(name)).append("\",\n"); + json.append(" \"address\": \"0x").append(Long.toHexString(addr.getOffset())).append("\",\n"); + json.append(" \"rva\": \"0x").append(Long.toHexString(addr.getOffset() - imageBase)).append("\",\n"); + + // Try to read vtable entries (function pointers) + json.append(" \"entries\": ["); + try { + List entries = readVtableEntries(addr); + for (int i = 0; i < entries.size(); i++) { + if (i > 0) json.append(", "); + json.append("\"").append(entries.get(i)).append("\""); + } + } catch (Exception e) { + // vtable read failed, skip entries + } + json.append("]\n"); + json.append(" }"); + } + } + if (!first) json.append("\n"); + } + + private List readVtableEntries(Address vtableAddr) throws Exception { + List entries = new ArrayList<>(); + Memory mem = currentProgram.getMemory(); + AddressSpace space = vtableAddr.getAddressSpace(); + int maxEntries = 100; // safety limit + + for (int i = 0; i < maxEntries; i++) { + Address entryAddr = vtableAddr.add(i * 8L); // x64: 8-byte pointers + if (!mem.contains(entryAddr)) break; + + long funcPtr = mem.getLong(entryAddr); + if (funcPtr == 0) break; + + Address funcAddr = space.getAddress(funcPtr); + Function func = currentProgram.getFunctionManager().getFunctionAt(funcAddr); + if (func == null) { + // Not pointing to a known function — end of vtable + // But check if it's in a code section first + if (!currentProgram.getMemory().getBlock(funcAddr).isExecute()) break; + entries.add("0x" + Long.toHexString(funcPtr - imageBase)); + } else { + String funcName = func.getName(true); + entries.add(funcName + "@0x" + Long.toHexString(funcPtr - imageBase)); + } + } + return entries; + } + + private void exportTypeDescriptors(StringBuilder json) throws Exception { + SymbolTable symTable = currentProgram.getSymbolTable(); + Memory mem = currentProgram.getMemory(); + boolean first = true; + + // Find TypeDescriptor symbols and also scan for RTTI mangled names + SymbolIterator iter = symTable.getAllSymbols(false); + while (iter.hasNext()) { + Symbol sym = iter.next(); + String name = sym.getName(); + if (name == null) continue; + + if (name.contains("TypeDescriptor") || name.contains("typeDescriptor") || + name.contains("type_info") || name.contains("RTTI_Type_Descriptor")) { + if (!first) json.append(",\n"); + first = false; + + Address addr = sym.getAddress(); + String nsName = sym.getParentNamespace() != null ? + sym.getParentNamespace().getName(true) : "global"; + + json.append(" {\n"); + json.append(" \"class\": \"").append(esc(nsName)).append("\",\n"); + json.append(" \"label\": \"").append(esc(name)).append("\",\n"); + json.append(" \"address\": \"0x").append(Long.toHexString(addr.getOffset())).append("\",\n"); + json.append(" \"rva\": \"0x").append(Long.toHexString(addr.getOffset() - imageBase)).append("\",\n"); + + // Try to read the mangled name string from TypeDescriptor + // MSVC TypeDescriptor layout: [8 bytes vfptr][8 bytes spare][N bytes name] + String mangledName = ""; + try { + Address nameAddr = addr.add(16); // skip vfptr + spare (8+8 for x64) + byte[] nameBytes = new byte[256]; + int bytesRead = mem.getBytes(nameAddr, nameBytes); + int end = 0; + for (int i = 0; i < bytesRead; i++) { + if (nameBytes[i] == 0) { end = i; break; } + } + if (end > 0) { + mangledName = new String(nameBytes, 0, end, "ASCII"); + } + } catch (Exception e) { + mangledName = ""; + } + + json.append(" \"mangled_name\": \"").append(esc(mangledName)).append("\"\n"); + json.append(" }"); + } + } + if (!first) json.append("\n"); + } + + private void exportFunctions(StringBuilder json) throws Exception { + FunctionManager funcMgr = currentProgram.getFunctionManager(); + boolean first = true; + int count = 0; + + FunctionIterator iter = funcMgr.getFunctions(true); + while (iter.hasNext()) { + Function func = iter.next(); + Namespace ns = func.getParentNamespace(); + + // Only export functions in non-global namespaces (class methods) + // or functions with meaningful names (not FUN_xxxxx) + String name = func.getName(); + boolean isClassMethod = (ns != null && !ns.isGlobal()); + boolean hasRealName = (name != null && !name.startsWith("FUN_")); + + if (isClassMethod || hasRealName) { + if (!first) json.append(",\n"); + first = false; + + Address addr = func.getEntryPoint(); + json.append(" {\n"); + json.append(" \"name\": \"").append(esc(func.getName(true))).append("\",\n"); + json.append(" \"short_name\": \"").append(esc(name)).append("\",\n"); + json.append(" \"namespace\": \"").append(esc(ns != null ? ns.getName(true) : "global")).append("\",\n"); + json.append(" \"address\": \"0x").append(Long.toHexString(addr.getOffset())).append("\",\n"); + json.append(" \"rva\": \"0x").append(Long.toHexString(addr.getOffset() - imageBase)).append("\",\n"); + json.append(" \"signature\": \"").append(esc(func.getSignature().getPrototypeString(false))).append("\",\n"); + json.append(" \"is_thunk\": ").append(func.isThunk()).append(",\n"); + json.append(" \"calling_convention\": \"").append(esc(func.getCallingConventionName())).append("\",\n"); + json.append(" \"body_size\": ").append(func.getBody().getNumAddresses()).append("\n"); + json.append(" }"); + count++; + } + + if (count > 50000) break; // safety limit + } + if (!first) json.append("\n"); + println("Exported " + count + " named/class functions"); + } + + private void exportClassStrings(StringBuilder json) throws Exception { + // Export strings that match common Alamo engine / RTTI patterns + DataIterator dataIter = currentProgram.getListing().getDefinedData(true); + boolean first = true; + int count = 0; + Set seen = new HashSet<>(); + + while (dataIter.hasNext() && count < 10000) { + Data data = dataIter.next(); + if (!data.hasStringValue()) continue; + + String val = data.getDefaultValueRepresentation(); + if (val == null) continue; + + // Remove quotes + if (val.startsWith("\"")) val = val.substring(1); + if (val.endsWith("\"")) val = val.substring(0, val.length() - 1); + + // Filter for interesting strings + boolean interesting = false; + if (val.startsWith(".?AV") || val.startsWith(".?AU")) interesting = true; // RTTI mangled names + else if (val.contains("Class") || val.contains("Object") || val.contains("Manager")) + interesting = true; + else if (val.contains("Lua") || val.contains("lua_")) interesting = true; + else if (val.contains("Game") || val.contains("Player") || val.contains("Unit")) + interesting = true; + else if (val.contains("Damage") || val.contains("Health") || val.contains("Shield")) + interesting = true; + else if (val.contains("AI_") || val.contains("Behavior") || val.contains("State")) + interesting = true; + else if (val.contains("XML") || val.contains("Property") || val.contains("Component")) + interesting = true; + else if (val.contains("Hardpoint") || val.contains("Weapon") || val.contains("Projectile")) + interesting = true; + else if (val.contains("Faction") || val.contains("Team") || val.contains("Alliance")) + interesting = true; + else if (val.contains("Map") || val.contains("Planet") || val.contains("Fleet")) + interesting = true; + else if (val.contains("Spawn") || val.contains("Reinforce") || val.contains("Transport")) + interesting = true; + else if (val.contains("Galactic") || val.contains("Tactical") || val.contains("Space")) + interesting = true; + + if (interesting && !seen.contains(val)) { + seen.add(val); + if (!first) json.append(",\n"); + first = false; + + Address addr = data.getAddress(); + json.append(" {\n"); + json.append(" \"value\": \"").append(esc(val)).append("\",\n"); + json.append(" \"address\": \"0x").append(Long.toHexString(addr.getOffset())).append("\",\n"); + json.append(" \"rva\": \"0x").append(Long.toHexString(addr.getOffset() - imageBase)).append("\"\n"); + json.append(" }"); + count++; + } + } + if (!first) json.append("\n"); + println("Exported " + count + " interesting strings"); + } + + private String esc(String s) { + if (s == null) return ""; + return s.replace("\\", "\\\\") + .replace("\"", "\\\"") + .replace("\n", "\\n") + .replace("\r", "\\r") + .replace("\t", "\\t"); + } +} diff --git a/ghidra_scripts/export_rtti.py b/ghidra_scripts/export_rtti.py new file mode 100644 index 000000000..4a8e0c04b --- /dev/null +++ b/ghidra_scripts/export_rtti.py @@ -0,0 +1,383 @@ +# export_rtti.py — Ghidra Python postScript (PyGhidra / Jython compatible) +# Runs on an already-analyzed Ghidra database to export RTTI-recovered class data to JSON. +# Output: C:/Users/Prekzursil/Downloads/swfoc_memory/rtti_export_raw.json + +import json +import os +from datetime import datetime + +OUTPUT_PATH = "C:/Users/Prekzursil/Downloads/swfoc_memory/rtti_export_raw.json" + +def get_image_base(): + return currentProgram.getImageBase().getOffset() + +def get_rva(addr): + return addr.getOffset() - get_image_base() + +def hex_rva(addr): + return "0x{:X}".format(get_rva(addr)) + +def hex_addr(addr): + return "0x{:X}".format(addr.getOffset()) + +def safe_str(s): + if s is None: + return "" + return str(s) + +def export_namespaces_and_classes(): + """Find all class namespaces created by RTTI recovery.""" + sym_table = currentProgram.getSymbolTable() + classes = {} # namespace_name -> {info} + + # Iterate all symbols looking for vftable, RTTI, TypeDescriptor labels + sym_iter = sym_table.getAllSymbols(False) + count = 0 + while sym_iter.hasNext(): + sym = sym_iter.next() + name = sym.getName() + if name is None: + continue + + ns = sym.getParentNamespace() + if ns is None or ns.isGlobal(): + continue + + ns_name = ns.getName(True) + + # Track class-related symbols + is_vftable = "vftable" in name.lower() + is_rtti = "RTTI" in name or "TypeDescriptor" in name or "type_info" in name + is_col = "CompleteObjectLocator" in name or "BaseClassArray" in name or "ClassHierarchyDescriptor" in name + + if is_vftable or is_rtti or is_col: + if ns_name not in classes: + classes[ns_name] = { + "name": ns_name, + "vftables": [], + "type_descriptors": [], + "rtti_symbols": [], + "methods": [] + } + + entry = { + "symbol": safe_str(name), + "address": hex_addr(sym.getAddress()), + "rva": hex_rva(sym.getAddress()), + "type": str(sym.getSymbolType()) + } + + if is_vftable: + classes[ns_name]["vftables"].append(entry) + elif is_rtti: + classes[ns_name]["type_descriptors"].append(entry) + else: + classes[ns_name]["rtti_symbols"].append(entry) + + count += 1 + if count % 50000 == 0: + println(" Processed {} symbols...".format(count)) + + println("Found {} class namespaces from {} symbols".format(len(classes), count)) + return classes + + +def export_vftable_entries(classes): + """Read vtable function pointer entries for each class.""" + mem = currentProgram.getMemory() + func_mgr = currentProgram.getFunctionManager() + addr_factory = currentProgram.getAddressFactory() + image_base = get_image_base() + + for cls_name, cls_info in classes.items(): + for vft in cls_info["vftables"]: + entries = [] + try: + addr = addr_factory.getDefaultAddressSpace().getAddress(int(vft["address"], 16)) + for i in range(200): # safety limit + entry_addr = addr.add(i * 8) + if not mem.contains(entry_addr): + break + func_ptr = mem.getLong(entry_addr) + if func_ptr == 0: + break + + func_addr = addr_factory.getDefaultAddressSpace().getAddress(func_ptr) + func = func_mgr.getFunctionAt(func_addr) + + if func is not None: + func_rva = func_ptr - image_base + entries.append({ + "index": i, + "name": func.getName(True), + "rva": "0x{:X}".format(func_rva), + "signature": safe_str(func.getSignature().getPrototypeString(False)) + }) + else: + # Check if it's in a code section + block = mem.getBlock(func_addr) + if block is None or not block.isExecute(): + break + entries.append({ + "index": i, + "name": None, + "rva": "0x{:X}".format(func_ptr - image_base) + }) + except Exception as e: + entries.append({"error": str(e)}) + + vft["entries"] = entries + vft["entry_count"] = len(entries) + + +def export_type_descriptor_names(classes): + """Read mangled names from TypeDescriptor structures.""" + mem = currentProgram.getMemory() + addr_factory = currentProgram.getAddressFactory() + + for cls_name, cls_info in classes.items(): + for td in cls_info["type_descriptors"]: + try: + addr = addr_factory.getDefaultAddressSpace().getAddress(int(td["address"], 16)) + # MSVC x64 TypeDescriptor: [8 vfptr][8 spare][N name] + name_addr = addr.add(16) + name_bytes = bytearray(256) + actual = mem.getBytes(name_addr, name_bytes) + end = 0 + for j in range(actual): + if name_bytes[j] == 0: + end = j + break + if end > 0: + mangled = bytes(name_bytes[:end]).decode('ascii', errors='replace') + td["mangled_name"] = mangled + td["demangled_name"] = demangle_msvc(mangled) + except Exception as e: + td["mangled_name_error"] = str(e) + + +def demangle_msvc(mangled): + """Basic MSVC RTTI name demangling: .?AVFoo@Bar@@ -> Bar::Foo""" + if not mangled: + return "" + s = mangled + # Remove leading .?AV or .?AU (class vs struct) + for prefix in [".?AV", ".?AU", ".?AW4"]: + if s.startswith(prefix): + s = s[len(prefix):] + break + # Remove trailing @@ + while s.endswith("@@"): + s = s[:-2] + # Split on @ and reverse (MSVC stores inner-to-outer) + parts = s.split("@") + parts = [p for p in parts if p] + parts.reverse() + return "::".join(parts) + + +def export_class_functions(classes): + """Find all functions within class namespaces.""" + func_mgr = currentProgram.getFunctionManager() + func_iter = func_mgr.getFunctions(True) + count = 0 + + while func_iter.hasNext(): + func = func_iter.next() + ns = func.getParentNamespace() + if ns is None or ns.isGlobal(): + continue + + ns_name = ns.getName(True) + if ns_name in classes: + addr = func.getEntryPoint() + method = { + "name": func.getName(), + "full_name": func.getName(True), + "rva": hex_rva(addr), + "signature": safe_str(func.getSignature().getPrototypeString(False)), + "is_thunk": func.isThunk(), + "body_size": func.getBody().getNumAddresses() + } + classes[ns_name]["methods"].append(method) + count += 1 + + println("Found {} class methods".format(count)) + + +def export_all_named_functions(): + """Export all functions with real names (not FUN_xxx).""" + func_mgr = currentProgram.getFunctionManager() + func_iter = func_mgr.getFunctions(True) + functions = [] + count = 0 + + while func_iter.hasNext(): + func = func_iter.next() + name = func.getName() + if name is None or name.startswith("FUN_"): + continue + + ns = func.getParentNamespace() + addr = func.getEntryPoint() + + functions.append({ + "name": name, + "full_name": func.getName(True), + "namespace": safe_str(ns.getName(True)) if ns and not ns.isGlobal() else "global", + "rva": hex_rva(addr), + "signature": safe_str(func.getSignature().getPrototypeString(False)), + "is_thunk": func.isThunk(), + "body_size": func.getBody().getNumAddresses() + }) + count += 1 + if count > 100000: + break + + println("Found {} named functions".format(count)) + return functions + + +def export_interesting_strings(): + """Export strings containing game-engine keywords.""" + keywords = [ + ".?AV", ".?AU", ".?AW4", + "Class", "Object", "Manager", "Lua", "lua_", + "Game", "Player", "Unit", "Damage", "Health", "Shield", + "AI_", "Behavior", "State", "Property", "Component", + "Hardpoint", "Weapon", "Projectile", "Faction", "Team", + "Map", "Planet", "Fleet", "Spawn", "Reinforce", + "Galactic", "Tactical", "Space", "Transport", + "alamo", "Alamo", "Petroglyph" + ] + + listing = currentProgram.getListing() + data_iter = listing.getDefinedData(True) + strings = [] + seen = set() + count = 0 + + while data_iter.hasNext() and count < 15000: + data = data_iter.next() + if not data.hasStringValue(): + continue + + val = data.getDefaultValueRepresentation() + if val is None: + continue + + # Remove quotes + if val.startswith('"'): + val = val[1:] + if val.endswith('"'): + val = val[:-1] + + if len(val) < 3 or val in seen: + continue + + interesting = False + for kw in keywords: + if kw in val: + interesting = True + break + + if interesting: + seen.add(val) + addr = data.getAddress() + strings.append({ + "value": val, + "rva": hex_rva(addr) + }) + count += 1 + + println("Found {} interesting strings".format(count)) + return strings + + +def get_function_stats(): + """Get overview statistics.""" + func_mgr = currentProgram.getFunctionManager() + total = 0 + named = 0 + thunks = 0 + in_namespace = 0 + + func_iter = func_mgr.getFunctions(True) + while func_iter.hasNext(): + func = func_iter.next() + total += 1 + if not func.getName().startswith("FUN_"): + named += 1 + if func.isThunk(): + thunks += 1 + ns = func.getParentNamespace() + if ns and not ns.isGlobal(): + in_namespace += 1 + + return { + "total_functions": total, + "named_functions": named, + "unnamed_functions": total - named, + "thunks": thunks, + "in_class_namespace": in_namespace + } + + +# === MAIN === +println("=== ExportRTTI Python Script Starting ===") +println("Program: " + currentProgram.getName()) +println("Image base: 0x{:X}".format(get_image_base())) + +# 1. Collect class namespaces +println("[1/6] Collecting class namespaces...") +classes = export_namespaces_and_classes() + +# 2. Read vtable entries +println("[2/6] Reading vtable entries...") +export_vftable_entries(classes) + +# 3. Read type descriptor mangled names +println("[3/6] Reading type descriptor names...") +export_type_descriptor_names(classes) + +# 4. Collect class methods +println("[4/6] Collecting class methods...") +export_class_functions(classes) + +# 5. Collect all named functions +println("[5/6] Collecting all named functions...") +all_functions = export_all_named_functions() + +# 6. Collect interesting strings +println("[6/6] Collecting interesting strings...") +strings = export_interesting_strings() + +# 7. Get statistics +println("Computing statistics...") +stats = get_function_stats() + +# Build final output +output = { + "_meta": { + "program": currentProgram.getName(), + "image_base": "0x{:X}".format(get_image_base()), + "analyzer": "Ghidra (RecoverClassesFromRTTIScript + export_rtti.py)", + "export_date": datetime.now().strftime("%Y-%m-%dT%H:%M:%S"), + "binary_path": "D:/SteamLibrary/steamapps/common/Star Wars Empire at War/corruption/StarWarsG.exe" + }, + "stats": stats, + "classes": classes, + "named_functions": all_functions, + "interesting_strings": strings +} + +# Write JSON +println("Writing output to: " + OUTPUT_PATH) +with open(OUTPUT_PATH, 'w') as f: + json.dump(output, f, indent=2, default=str) + +println("=== Export complete! ===") +println("Classes: {}".format(len(classes))) +println("Named functions: {}".format(len(all_functions))) +println("Interesting strings: {}".format(len(strings))) +println("Output: " + OUTPUT_PATH) diff --git a/pyrightconfig.json b/pyrightconfig.json index 157b4051a..2a849faec 100644 --- a/pyrightconfig.json +++ b/pyrightconfig.json @@ -1,7 +1,15 @@ { "typeCheckingMode": "standard", - "extraPaths": ["scripts"], + "extraPaths": [ + "scripts" + ], "exclude": [ - "tests/python" + "tests/python", + "bridge", + "swfoc_lua_bridge", + "swfoc_overlay", + "swfoc_toolkit", + "ghidra_scripts", + "re-findings" ] } diff --git a/re-findings/ai_system.json b/re-findings/ai_system.json new file mode 100644 index 000000000..b3575ed82 --- /dev/null +++ b/re-findings/ai_system.json @@ -0,0 +1,1270 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "SWFOC AI System Decompilation — Agent 3D Output", + "description": "Comprehensive map of the Alamo engine AI decision loop, subsystems, enable/disable mechanisms, production queue, diplomacy, and difficulty systems. Derived from Ghidra static analysis of StarWarsG.exe (x86_64).", + "_meta": { + "agent": "3D — AI System Decompilation", + "analysis_date": "2026-04-04", + "binary": "StarWarsG.exe", + "architecture": "x86_64", + "compiler": "MSVC", + "tool": "Ghidra 12.0.3 MCP", + "base_address_observed": "0x140000000" + }, + + "architecture_overview": { + "summary": "The AI system is a hierarchical, goal-driven architecture centered on the TheAIClass singleton. Each non-human player gets an AIPlayerClass instance containing 7 ServicedAISystem subsystems. The system uses a perception-planning-execution loop with per-mode specializations (Galactic, Space, Land). Production is handled by a FiniteStateMachine-based AIBuildTaskClass with 25 named states. Diplomacy is event-driven (AllyEventClass) rather than polled. Difficulty is managed by DifficultyAdjustmentManagerClass (a database manager of DifficultyAdjustmentClass records).", + "key_design_patterns": [ + "Singleton pattern (TheAIClass, TheAIDataManagerClass, TheAIPlayerTypeManagerClass, TheAIGoalTypeManagerClass, TheAITemplateManagerClass)", + "ServicedAISystemClass — base class for all tickable AI subsystems (inherits AIDiagnosticsClass, stores owner reference at +0x0)", + "FiniteStateMachineClass — state machine driving AIBuildTaskClass production", + "PerceptualEvaluatorClass hierarchy — 20+ evaluator types for sensing game state", + "Signal/Listener pattern — AILearningSystemClass uses SignalListenerClass + SignalGeneratorClass", + "Lua binding wrappers — LuaMemberFunctionWrapper for exposing AI internals to Lua scripts", + "DatabaseObjectManagerClass — manages typed collections (goals, templates, difficulty adjustments)" + ] + }, + + "singletons": { + "TheAIClass": { + "constructor_rva": "0x4D9C80", + "description": "Top-level AI singleton. Constructed with field_0x4 = 1, field_0x0 = 0x01010101 (4-byte enable/state flags). Manages all AI player instances. Access via SingletonInstance (ctor/dtor at 0x7EED80).", + "singleton_ctor_dtor_rva": "0x7EED80", + "vtable_rva": "set in constructor (this->vftablePtr = &vftable)", + "fields": [ + { + "offset": "0x00", + "type": "uint32", + "name": "state_flags", + "description": "Written as 0x01010101 in constructor. 4 individual byte flags. Likely per-state enable bits (galactic, space, land, global).", + "confidence": "medium" + }, + { + "offset": "0x04", + "type": "uint32", + "name": "active_flag", + "description": "Set to 1 in constructor. Probable global AI-active flag.", + "confidence": "medium" + } + ] + }, + "TheAIDataManagerClass": { + "constructor_rva": "0x4747E0", + "singleton_ctor_dtor_rva": "0x7E6BA0", + "description": "Stores global AI configuration data. Has 8 undefined8 fields initialized to 0 at offsets 0x38..0x70.", + "fields_initialized_count": 8 + }, + "TheAIPlayerTypeManagerClass": { + "constructor_rva": "0x4E9940", + "singleton_ctor_dtor_rva": "0x7EED90", + "description": "Manages AI player type definitions. Uses a hash map (load factor 0x3f800000 = 1.0f). Maps player types to AI behavior profiles.", + "hash_map_initial_bucket_count": 7, + "hash_map_load_factor": 1.0 + }, + "TheAIGoalTypeManagerClass": { + "constructor_rva": "0x5E6690", + "description": "Manages all AI goal type definitions. Database of AIGoalTypeClass objects." + }, + "TheAIGoalProposalFunctionSetManagerClass": { + "constructor_rva": "0x5E5C30", + "description": "Manages sets of goal proposal functions. These are the scoring functions that rank which goals the AI should pursue." + }, + "TheAITemplateManagerClass": { + "constructor_rva": "0x4E8920", + "description": "Manages AI template definitions (build orders, force compositions, etc.). Database of AITemplateClass objects." + } + }, + + "per_player_ai_structure": { + "AIPlayerClass": { + "constructor_rva": "0x4AF810", + "description": "Per-player AI controller. Created for each AI-controlled player slot. Inherits AIDiagnosticsClass. Contains references to all 7 subsystems. Constructor takes 2 params: likely (player_ptr, ai_type_ptr).", + "inherits": ["AIDiagnosticsClass"], + "fields": [ + { + "offset": "data+0x00", + "type": "pointer", + "name": "param_1_ref", + "description": "First constructor param — likely pointer to owning PlayerClass" + }, + { + "offset": "data+0x08", + "type": "pointer", + "name": "param_2_ref", + "description": "Second constructor param — likely pointer to AIPlayerTypeClass" + }, + { + "offset": "data+0x10", + "type": "pointer", + "name": "subsystem_0", + "description": "Initialized to 0. Subsystem slot (likely perception)" + }, + { + "offset": "data+0x18", + "type": "pointer", + "name": "subsystem_1", + "description": "Initialized to 0. Subsystem slot (likely planning)" + }, + { + "offset": "data+0x20", + "type": "pointer", + "name": "subsystem_2", + "description": "Initialized to 0. Subsystem slot (likely goal)" + }, + { + "offset": "data+0x28", + "type": "pointer", + "name": "subsystem_3", + "description": "Initialized to 0. Subsystem slot (likely execution)" + }, + { + "offset": "data+0x30", + "type": "int32", + "name": "state_or_mode", + "description": "Initialized to 0xFFFFFFFF (-1). Likely current game mode enum or uninitialized sentinel." + } + ] + } + }, + + "subsystems": { + "_note": "All subsystems inherit ServicedAISystemClass, which inherits AIDiagnosticsClass. ServicedAISystemClass stores a reference to the owning AIPlayerClass at data+0x00 and has a field at data+0x08 initialized to 0.", + + "ServicedAISystemClass": { + "constructor_rva": "0x64C250", + "description": "Base class for all 7 AI subsystems. Provides tick/service interface via vtable virtual methods. Stores owning AIPlayerClass reference.", + "inherits": ["AIDiagnosticsClass"], + "fields": [ + { + "offset": "data+0x00", + "type": "pointer", + "name": "owner_ai_player", + "description": "Pointer to the AIPlayerClass that owns this subsystem. Passed as param_1 in constructor." + }, + { + "offset": "data+0x08", + "type": "uint64", + "name": "service_counter_or_state", + "description": "Initialized to 0. Likely tick counter or active/inactive state." + } + ] + }, + + "AIPerceptionSystemClass": { + "constructor_rva": "0x4DAD80", + "description": "Senses the game world. Feeds evaluator results into the planning system. Contains a hash map of PerceptionParameterBindings and a linked list of perception data.", + "inherits": ["ServicedAISystemClass"], + "specializations": { + "GalacticPerceptionSystemClass": { + "constructor_rva": "0x4E1880", + "extra_fields": 3, + "description": "Galactic map perception. 3 extra pointer/counter fields for planet-level sensing." + }, + "LandPerceptionSystemClass": { + "constructor_rva": "0x6B8980", + "description": "Land battle perception." + }, + "SpacePerceptionSystemClass": { + "constructor_rva": "0x6B9A20", + "description": "Space battle perception." + }, + "TacticalPerceptionGridClass": { + "constructor_rva": "0x653340", + "description": "Grid-based perception for tactical modes. Uses PerceptionGridEntryClass linked list." + } + } + }, + + "AIPlanningSystemClass": { + "constructor_rva": "0x6BAC00", + "description": "Generates plans from perception data and goal proposals. Contains a doubly-linked list (node size 0x20) for plan entries. Hash map (buckets=7, load=1.0). Planning interval stored as float 0x40800000 = 4.0 (at offset 0x10 — likely 4.0 seconds between plan recalculations).", + "inherits": ["ServicedAISystemClass"], + "key_values": { + "plan_interval_seconds": 4.0, + "plan_interval_hex": "0x40800000", + "hash_map_initial_buckets": 7, + "hash_map_load_factor": 1.0 + }, + "fields": [ + { + "offset": "data+0x00", + "type": "pointer", + "name": "active_plan_head" + }, + { + "offset": "data+0x08", + "type": "pointer", + "name": "plan_data_1" + }, + { + "offset": "data+0x10", + "type": "float32", + "name": "plan_interval", + "description": "4.0f — seconds between planning cycles" + }, + { + "offset": "data+0x18", + "type": "pointer", + "name": "plan_linked_list_head", + "description": "Doubly-linked list node (allocated as 0x20 bytes, self-referencing sentinel)" + }, + { + "offset": "data+0x28", + "type": "pointer", + "name": "hash_map_ptr" + } + ] + }, + + "AIGoalSystemClass": { + "constructor_rva": "0x6C7970", + "description": "Manages active goals. Inherits ServicedAISystemClass. Contains an AIBudgetClass instance (at data+0x158) for resource allocation. Two large internal structures (initialized via FUN_140721360) at data+0x00 and data+0xA0 — likely active-goals list and completed-goals list.", + "inherits": ["ServicedAISystemClass"], + "specializations": { + "GalacticGoalSystemClass": { + "constructor_rva": "0x6B8480", + "description": "Galactic-mode goals. Pure vtable override of AIGoalSystemClass." + }, + "LandGoalSystemClass": { + "constructor_rva": "0x6B86E0", + "description": "Land-mode goals." + }, + "SpaceGoalSystemClass": { + "constructor_rva": "0x6B88D0", + "description": "Space-mode goals." + } + }, + "fields": [ + { + "offset": "data+0x00", + "type": "struct", + "size": "0xA0", + "name": "goal_collection_1", + "description": "Initialized by FUN_140721360. Likely active goals container." + }, + { + "offset": "data+0xA0", + "type": "struct", + "size": "0xA0", + "name": "goal_collection_2", + "description": "Initialized by FUN_140721360. Likely pending/completed goals." + }, + { + "offset": "data+0x140", + "type": "pointer", + "name": "field_140" + }, + { + "offset": "data+0x148", + "type": "pointer", + "name": "field_148" + }, + { + "offset": "data+0x14C", + "type": "int32", + "name": "field_14C" + }, + { + "offset": "data+0x150", + "type": "int32", + "name": "field_150" + }, + { + "offset": "data+0x158", + "type": "AIBudgetClass", + "name": "budget", + "description": "Embedded AIBudgetClass instance for this goal system's resource budget." + }, + { + "offset": "data+0x1E8", + "type": "hash_map", + "name": "hash_map_1" + }, + { + "offset": "data+0x228", + "type": "hash_map", + "name": "hash_map_2" + }, + { + "offset": "data+0x268", + "type": "pointer", + "name": "field_268" + }, + { + "offset": "data+0x270", + "type": "pointer", + "name": "field_270" + }, + { + "offset": "data+0x278", + "type": "pointer", + "name": "field_278" + } + ] + }, + + "AIExecutionSystemClass": { + "constructor_rva": "0x524CE0", + "description": "Executes planned actions (movement, attacks, ability usage). 4 pointer fields + inherits ServicedAISystemClass.", + "inherits": ["ServicedAISystemClass"] + }, + + "AILearningSystemClass": { + "constructor_rva": "0x585D00", + "description": "Tracks AI performance history. Inherits ServicedAISystemClass + MultiLinkedListMember + SignalListenerClass. Contains 5 hash maps and multiple counters. Stores historical data about build effectiveness, combat outcomes, etc. Very large struct (~0x198+ bytes of own data).", + "inherits": ["ServicedAISystemClass", "MultiLinkedListMember", "SignalListenerClass"], + "hash_map_count": 5, + "total_data_fields_approx": 50 + }, + + "AITemplateSystemClass": { + "constructor_rva": "0x6BB9E0", + "description": "Manages force templates (predefined army compositions). Contains a linked list of active template instances. Inherits ServicedAISystemClass.", + "inherits": ["ServicedAISystemClass"], + "fields": [ + { + "offset": "data+0x00", + "type": "pointer", + "name": "template_linked_list_head", + "description": "Doubly-linked sentinel node (0x18 bytes). Stores active force templates." + }, + { + "offset": "data+0x08", + "type": "uint64", + "name": "field_08" + }, + { + "offset": "data+0x10", + "type": "uint64", + "name": "template_count_or_state" + }, + { + "offset": "data+0x18", + "type": "uint64", + "name": "field_18" + } + ] + }, + + "AIBudgetClass": { + "constructor_rva": "0x6109C0", + "description": "Resource budget allocator for the AI goal system. Inherits AIDiagnosticsClass. Contains a hash map of BudgetedCategoryStruct entries. Tracks allocated, spent, and remaining budget per category.", + "inherits": ["AIDiagnosticsClass"], + "fields": [ + { + "offset": "data+0x00", + "type": "int32", + "name": "total_budget_int" + }, + { + "offset": "data+0x04", + "type": "float32", + "name": "budget_multiplier", + "value": "0x3F800000 = 1.0f" + }, + { + "offset": "data+0x08", + "type": "pointer", + "name": "hash_map_sentinel", + "description": "Self-referencing sentinel for budget category hash map" + }, + { + "offset": "data+0x18", + "type": "pointer", + "name": "hash_map_entries" + }, + { + "offset": "data+0x40", + "type": "int32", + "name": "field_40" + }, + { + "offset": "data+0x48", + "type": "int32", + "name": "field_48" + }, + { + "offset": "data+0x4C", + "type": "int32", + "name": "field_4C" + }, + { + "offset": "data+0x50", + "type": "pointer", + "name": "owner_ref", + "description": "Reference to owning system (passed as param_1)" + }, + { + "offset": "data+0x58", + "type": "int32", + "name": "field_58" + }, + { + "offset": "data+0x5C", + "type": "int32", + "name": "field_5C" + }, + { + "offset": "data+0x60", + "type": "int32", + "name": "field_60" + } + ], + "nested_structs": { + "BudgetedCategoryStruct": { + "constructor_rva": "0x610A70", + "inherits": ["AIDiagnosticsClass"], + "fields": [ + { + "offset": "data+0x00", + "type": "int32", + "name": "category_id", + "value_on_init": "0xFFFFFFFF (-1 = unassigned)" + }, + { + "offset": "data+0x08", + "type": "pointer", + "name": "owner_ref" + }, + { + "offset": "data+0x10", + "type": "int64", + "name": "allocated_amount" + }, + { + "offset": "data+0x18", + "type": "int32", + "name": "priority", + "value_on_init": 1 + }, + { + "offset": "data+0x20", + "type": "int64", + "name": "field_20" + }, + { + "offset": "data+0x28", + "type": "int64", + "name": "field_28" + }, + { + "offset": "data+0x30", + "type": "int64", + "name": "field_30" + }, + { + "offset": "data+0x38", + "type": "int32", + "name": "min_allocation", + "value_on_init": 1 + }, + { + "offset": "data+0x40", + "type": "int64", + "name": "field_40" + }, + { + "offset": "data+0x48", + "type": "int64", + "name": "field_48" + }, + { + "offset": "data+0x50", + "type": "int64", + "name": "field_50" + } + ] + } + } + } + }, + + "production_system": { + "AIBuildTaskClass": { + "constructor_rva": "0x6478C0", + "state_machine_handler_rva": "0x64AEC0", + "description": "FiniteStateMachineClass. The core production unit. Each build task is a refcounted, signal-listening state machine that progresses through 25 named states from INIT_TASK to ALL_FINISHED. Multiple AIBuildTaskClass instances can be active simultaneously per AI player, managed via MultiLinkedListClass.", + "inherits": [ + "FiniteStateMachineClass", + "RefCountClass", + "SignalListenerClass", + "SignalGeneratorClass" + ], + "build_states": { + "STATE_Global": { + "id": "match on *(int*)(this+0x8+4)", + "description": "Default/global state handler" + }, + "BUILD_STATE_INIT_TASK": { + "id": 1, + "description": "Initial state. Sets state to 1." + }, + "BUILD_STATE_PRODUCTION_QUEUED": { + "id": 2, + "description": "Production request queued. Polls game state via DAT_140b15418 (global game clock/state). Checks credit availability and population caps (query types 0x5F, 0x60 on game object properties)." + }, + "BUILD_STATE_FREESTORE_ALLOC": { + "id": 3, + "description": "Allocates from the free store. Transitions to state 6 (PRODUCTION_END) on enter." + }, + "BUILD_STATE_GROUND_UNIT_PRODUCTION": { + "id": 4, + "description": "Ground unit being produced. Calls FUN_140648120 on enter." + }, + "BUILD_STATE_PRODUCTION_BEGIN": { + "id": 5, + "description": "Production started. Runs EVENT_Update poll loop same as state 2." + }, + "BUILD_STATE_PRODUCTION_END": { + "id": 6, + "description": "Production completed. Unregisters signal listeners. Calls FUN_140647ee0 (production finalize). If flag at +0xD0 is set, skips budget release." + }, + "BUILD_STATE_PRODUCTION_FAILED": { + "id": 7, + "description": "Production failed. Releases budget via FUN_1405fdd90. Notifies free store via FUN_1404d4ee0. Transitions to cleanup." + }, + "BUILD_STATE_PRODUCTION_FINISHED": { + "id": 8, + "description": "Completed successfully. Calls FUN_14064ad00 on enter." + }, + "BUILD_STATE_DEPENDANT_WAIT": { + "id": 9, + "description": "Waiting for dependent tasks. Checks offset_0xB8 (dependent task list) count at +0x30. Transitions to state 0xB when count reaches 0." + }, + "BUILD_STATE_FLEET_MERGE": { + "id": 10, + "description": "Merging produced units into fleet." + }, + "BUILD_STATE_PATHING_DEST": { + "id": 11, + "description": "Pathfinding to destination. Creates MovementBlockStatus. Uses AIExecutionPathFinder. Handles galactic object containment hierarchy (checks +0x335, +0x336, +0x337 on game objects — component lookup table entries)." + }, + "BUILD_STATE_JOIN_TASK_FORCE": { + "id": 13, + "description": "Unit joining a task force." + }, + "BUILD_STATE_PRE_PATH_SEPERATE": { + "id": 14, + "description": "Pre-pathfinding separation. Issues FleetManagementEvents to split units for movement." + }, + "BUILD_STATE_WAIT_FOR_ALL": { + "id": 15, + "description": "Waiting for all sub-tasks to complete. Checks galactic mode (offset 0x34 in mode struct). Transitions to 0x13 (TRANSPORTS_TO_ORBIT) or 8 (PRODUCTION_FINISHED)." + }, + "BUILD_STATE_DO_TASK_FORCE_MERGE": { + "id": 16, + "description": "Executing task force merge. Calls FUN_140512160 to check merge completion." + }, + "BUILD_STATE_WAIT_FOR_CREDITS": { + "id": 17, + "description": "Waiting for sufficient credits. Transitions to 0x12 (BUILD_TASK_LIST) on enter." + }, + "BUILD_STATE_BUILD_TASK_LIST": { + "id": 18, + "description": "Building the task list. Calls FUN_140648980 on enter." + }, + "BUILD_STATE_TRANSPORTS_TO_ORBIT": { + "id": 19, + "description": "Moving transports to orbit. Issues FleetManagementEvents for orbital transfer." + }, + "BUILD_STATE_READY_TO_MERGE": { + "id": 20, + "description": "Ready to merge. Checks task force type and decides between JOIN_TASK_FORCE (0xD) or DO_TASK_FORCE_MERGE (0x10)." + }, + "BUILD_STATE_ALL_FINISHED": { + "id": 21, + "description": "All production tasks complete. Terminal state." + }, + "BUILD_STATE_WAIT_FOR_FREESTORE_ALLOC": { + "id": 22, + "description": "Waiting for free store allocation. Complex state with multiple paths: ground production (state 4), fleet merge (state 0xE), or sync move (state 0x18). Checks object hierarchy via component_lookup_table entries." + }, + "BUILD_STATE_HIJACK_UNIT": { + "id": 23, + "description": "Hijacking an existing unit for the build task. Calls FUN_140648370 on enter." + }, + "BUILD_STATE_WAIT_FOR_SYNC_MOVE": { + "id": 24, + "description": "Waiting for synchronized movement." + }, + "BUILD_STATE_SYNC_MOVE_COUNTDOWN": { + "id": 25, + "description": "Countdown timer for sync move. Decrements float at +0xD8 by elapsed time. When timer <= threshold at +0xD4, calls FUN_14064a1f0 (execute synchronized move)." + } + }, + "fields": [ + { + "offset": "0x08", + "type": "int32", + "name": "current_state", + "description": "Written with the state ID on each state entry" + }, + { + "offset": "0x18", + "type": "pointer_to_string", + "name": "current_state_name", + "description": "Debug string pointer — the state name (e.g., 'BUILD_STATE_INIT_TASK')" + }, + { + "offset": "0x20", + "type": "int32", + "name": "transition_pending", + "description": "Set to 1 when a state transition is requested" + }, + { + "offset": "field_0xC", + "type": "int32", + "name": "next_state", + "description": "Target state for pending transition" + }, + { + "offset": "field_0x20", + "type": "pointer_to_string", + "name": "current_event_name", + "description": "Debug: 'EVENT_Enter', 'EVENT_Update'" + }, + { + "offset": "0x60", + "type": "pointer", + "name": "target_game_object", + "description": "The game object being produced or the production target location" + }, + { + "offset": "0x68", + "type": "pointer", + "name": "production_structure", + "description": "The building/structure performing the production. Null for non-structure tasks." + }, + { + "offset": "0x70", + "type": "int32", + "name": "production_type_id", + "value_on_init": "0xFFFFFFFF" + }, + { + "offset": "0x74", + "type": "int32", + "name": "production_priority", + "value_on_init": 1 + }, + { + "offset": "0x78", + "type": "pointer", + "name": "dependent_task" + }, + { + "offset": "0x80", + "type": "pointer", + "name": "build_type_ref", + "description": "GameObjectTypeClass pointer for what to build" + }, + { + "offset": "0x88", + "type": "int32", + "name": "build_type_id_2", + "value_on_init": "0xFFFFFFFF" + }, + { + "offset": "0x90", + "type": "pointer", + "name": "destination_object", + "description": "Where to send the produced unit" + }, + { + "offset": "0x98", + "type": "pointer", + "name": "owning_ai_player", + "description": "Pointer to AIPlayerClass (used heavily to access subsystems via +0xF0)" + }, + { + "offset": "0xA0", + "type": "uint8", + "name": "cancelled_flag" + }, + { + "offset": "0xA8", + "type": "pointer", + "name": "movement_block_status", + "description": "MovementBlockStatus for pathfinding states" + }, + { + "offset": "0xB0", + "type": "pointer", + "name": "merge_target" + }, + { + "offset": "0xB8", + "type": "pointer", + "name": "dependent_task_list" + }, + { + "offset": "0xC0", + "type": "pointer", + "name": "field_C0" + }, + { + "offset": "0xC8", + "type": "int32", + "name": "field_C8" + }, + { + "offset": "0xCC", + "type": "int32", + "name": "field_CC" + }, + { + "offset": "0xD0", + "type": "uint8", + "name": "skip_budget_release_flag", + "description": "When set, PRODUCTION_END skips budget release" + }, + { + "offset": "0xD2", + "type": "uint8", + "name": "is_reinforcement_flag" + }, + { + "offset": "0xD4", + "type": "float32", + "name": "sync_move_threshold", + "description": "Timer threshold for SYNC_MOVE_COUNTDOWN state" + }, + { + "offset": "0xD8", + "type": "float32", + "name": "sync_move_timer", + "value_on_init": "-1.0f (0xBF800000)" + } + ] + }, + + "AIPlanetBuildTaskClass": { + "constructor_rva": "0x6954B0", + "description": "Planet-level build task. Managed via MultiLinkedListClass. Coordinates multiple AIBuildTaskClass instances for a single planet.", + "list_constructor_rva": "0x53B140" + }, + + "AIFreeStoreClass": { + "lua_wrapper_constructor_rva": "0x4D22B0", + "description": "Manages available (unallocated) units that the AI can commandeer for goals. Lua-exposed via LuaMemberFunctionWrapper. Has registered Lua method 'Get_Object_Count'. Contains hash maps for tracking allocated vs. free units.", + "lua_methods": ["Get_Object_Count"], + "specializations": { + "LandFreeStoreClass": {"constructor_rva": "0x690980"}, + "SpaceFreeStoreClass": {"constructor_rva": "0x690420"}, + "GalacticFreeStoreClass": {"lua_wrapper_rva": "0x68EAF0"} + } + }, + + "ProduceForceBlockStatus": { + "constructor_rva": "0x6C6500", + "description": "Blocking status for force production. Inherits LuaMemberFunctionWrapper. Used in Lua scripts via _ProduceObject. Lua scripts call this and it blocks until production completes.", + "inherits": ["LuaMemberFunctionWrapper"], + "fields": [ + { + "offset": "0x38", + "type": "pointer", + "name": "field_38" + }, + { + "offset": "0x40", + "type": "pointer", + "name": "field_40" + }, + { + "offset": "0x48", + "type": "pointer", + "name": "field_48" + } + ] + } + }, + + "perception_system": { + "evaluator_hierarchy": { + "description": "The perception system uses a hierarchy of PerceptualEvaluatorClass subclasses. Each evaluator scores a different aspect of the game world. Results feed into the planning system's goal proposal scoring.", + "base_class_destructor_rva": "0x64C900", + "evaluator_types": [ + { + "name": "GameObjectPerceptualEvaluatorClass", + "rva": "0x6D7E50", + "description": "Evaluates individual game objects (units, buildings)" + }, + { + "name": "GameObjectTypePerceptualEvaluatorClass", + "rva": "0x64F400", + "description": "Evaluates by object type category" + }, + { + "name": "PlayerPerceptualEvaluatorClass", + "rva": "0x6DFAC0", + "description": "Evaluates player-level state (economy, military strength)" + }, + { + "name": "SpaceShipPerceptualEvaluatorClass", + "rva": "0x6D1D70", + "description": "Space-specific ship evaluation" + }, + { + "name": "TacticalLocationPerceptualEvaluatorClass", + "rva": "0x72C560", + "description": "Evaluates tactical map positions" + }, + { + "name": "TacticalObjectPerceptualEvaluatorClass", + "rva": "0x730580", + "description": "Evaluates tactical-mode objects" + }, + { + "name": "TacticalPlayerPerceptualEvaluatorClass", + "rva": "0x734780", + "description": "Evaluates players in tactical mode" + }, + { + "name": "TacticalStructurePerceptualEvaluatorClass", + "rva": "0x734730", + "description": "Evaluates tactical-mode structures" + }, + { + "name": "TacticalUnitPerceptualEvaluatorClass", + "rva": "0x7320D0", + "description": "Evaluates tactical-mode units" + } + ], + "galactic_evaluators": [ + "GalacticGamePerceptualEvaluatorClass", + "GalacticPlayerPerceptualEvaluatorClass", + "PlanetPerceptualEvaluatorClass", + "PlanetForcePerceptualEvaluatorClass", + "PlanetTypePerceptualEvaluatorClass", + "GameForcePerceptualEvaluatorClass" + ], + "other_evaluators": [ + "FunctionPerceptualEvaluatorClass", + "HintPerceptualEvaluatorClass", + "ScriptPerceptualEvaluatorClass", + "RootLookupPerceptualEvaluatorClass" + ] + }, + "PerceptionParameterBindingsClass": { + "constructor_rvas": ["0x4DAE40", "0x64C840", "0x659C60"], + "description": "Binds perception parameters (thresholds, weights) to evaluator instances. Multiple constructors for different contexts." + }, + "PerceptionGridEntryClass": { + "constructor_rva": "0x653100", + "description": "Entry in the tactical perception grid. Stores per-cell sensing data." + } + }, + + "ai_enable_disable_mechanism": { + "description": "AI enable/disable operates at multiple levels: (1) TheAIClass singleton state_flags (0x01010101 = all active), (2) Per-player Is_Human flag on PlayerObject, (3) Per-unit Prevent_AI_Usage Lua method, (4) Suspend_AI global Lua function (time-based), (5) Player::Enable_As_Actor() Lua method. The AI system checks Is_Human on the player to decide whether to create an AIPlayerClass. The Suspend_AI function likely sets a timer on the TheAIClass singleton that suppresses all AI ticking.", + + "global_ai_singleton": { + "rva": "TheAIClass singleton via SingletonInstance at 0x7EED80", + "state_flags_offset": "0x00", + "state_flags_value_active": "0x01010101", + "active_flag_offset": "0x04", + "active_flag_value": 1, + "description": "The TheAIClass constructor writes 0x01010101 to offset 0x00 (4 byte flags, likely per-mode: galactic=0x01, space=0x01, land=0x01, global=0x01) and 1 to offset 0x04. Setting these to 0 would disable AI globally." + }, + + "per_player_control": { + "player_is_human_lua_binding": "Is_Human (string RVA 0x8AA448)", + "player_is_human_controlled_lua_binding": "IsHumanControlled (string RVA 0x8A5D58)", + "enable_as_actor_lua_binding": "Enable_As_Actor() on PlayerWrapper", + "description": "PlayerObject has Is_Human and IsHumanControlled methods. AI-controlled players have Is_Human returning false. Enable_As_Actor() on a PlayerWrapper activates AI for that player. The engine checks these flags when creating/destroying AIPlayerClass instances." + }, + + "per_unit_control": { + "prevent_ai_usage_method": "Prevent_AI_Usage(bool) on GameObjectWrapper", + "warning": "Crashes in tactical if unit has no active AI (community-documented bug).", + "description": "Blocks AI from issuing commands to a specific unit. Useful for cinematic sequences or player-controlled units in AI factions." + }, + + "temporal_suspension": { + "suspend_ai_function": "Suspend_AI(seconds) — global Lua function", + "description": "Pauses all AI processing for a specified number of seconds. Used during cinematics. Likely sets a countdown timer on TheAIClass or on the game clock that suppresses ServicedAISystemClass::Service calls." + } + }, + + "diplomacy_system": { + "description": "Diplomacy in the Alamo engine is NOT a continuous state machine — it is event-driven. Alliance/enemy changes are discrete events (AllyEventClass) sent through the ScheduledEventClass system. The PlayerObject stores diplomatic state accessed via Is_Enemy(player) and Is_Ally(player) Lua methods. Make_Ally/Make_Enemy are global Lua functions that fire AllyEventClass events. CRITICAL: diplomatic state resets on every game mode change (galactic<->tactical transition).", + + "AllyEventClass": { + "constructor_rva": "0x689AD0", + "inherits": ["ScheduledEventClass"], + "event_type_id": "0x23 (35)", + "description": "Event fired when alliance state changes between two players. Field at data+0x00 initialized to 0xFFFFFFFF (target player ID, -1 = none).", + "fields": [ + { + "offset": "data+0x00", + "type": "int32", + "name": "target_player_id", + "value_on_init": "0xFFFFFFFF" + }, + { + "offset": "EventClass_data+0x08", + "type": "int32", + "name": "event_type", + "value": "0x23 = AllyEvent" + } + ] + }, + + "player_diplomatic_methods": { + "Is_Enemy": {"lua_string_rva": "0x8AA468", "returns": "boolean"}, + "Is_Ally": {"lua_string_rva": "0x8AA478", "returns": "boolean"}, + "Make_Ally": {"scope": "global Lua function", "note": "Resets every game mode change"}, + "Make_Enemy": {"scope": "global Lua function", "note": "Resets every game mode change"} + }, + + "storage_model": { + "description": "Diplomatic state is stored per-player in the PlayerClass/PlayerObject struct. The exact storage is likely a bitfield or array indexed by opposing player_id (up to 8 players). This is NOT stored in a separate diplomacy manager — it is embedded in each PlayerClass instance. The ~PlayerClass destructor (RVA 0x27ED40) cleans up multiple DynamicVectorClass arrays (10 of them in a loop) at offsets within PlayerClass_data — some of these likely hold per-opponent diplomatic flags.", + "player_class_destructor_rva": "0x27ED40", + "diplomatic_arrays_loop_count": 10, + "diplomatic_arrays_description": "10 DynamicVectorClass arrays cleaned up in ~PlayerClass. These 10 slots match the 10-player capacity. Each likely stores diplomatic state for one opposing player." + } + }, + + "difficulty_system": { + "DifficultyAdjustmentManagerClass": { + "constructor_rva": "0x484B90", + "description": "Database manager of DifficultyAdjustmentClass records. Inherits DatabaseUIntConversionClass. Uses a hash map for lookup. Each DifficultyAdjustmentClass record modifies AI parameters (production speed, income multiplier, combat bonuses, etc.) based on difficulty setting.", + "inherits": ["DatabaseUIntConversionClass", "DatabaseObjectManagerClass"], + "hash_map_load_factor": 1.0, + "hash_map_initial_buckets": 7, + "fields": [ + { + "offset": "data+0x40", + "type": "pointer", + "name": "difficulty_name_string", + "description": "SSO string (capacity 0x0F) for difficulty level name" + }, + { + "offset": "data+0x50", + "type": "pointer", + "name": "field_50" + }, + { + "offset": "data+0x58", + "type": "int64", + "name": "string_capacity", + "value": "0x0F" + }, + { + "offset": "data+0x60", + "type": "int32", + "name": "field_60" + } + ] + } + }, + + "ai_diagnostics": { + "AIDiagnosticsClass": { + "description": "Base class for all AI classes that support diagnostic logging. Provides debug info tracking. The AIDiagnosticsClass is the common ancestor of virtually every AI class — AIPlayerClass, all ServicedAISystemClass subclasses, AIBudgetClass, BudgetedCategoryStruct, InstantiatedGoalClass all inherit from it.", + "fields": [ + { + "offset": "data+0x18", + "type": "int64", + "name": "diagnostics_data", + "description": "Diagnostic tracking field. Set to 0 in most constructors. Likely frame counter or tick timestamp." + } + ] + }, + "AIDiagnosticsManagerClass": { + "singleton_ctor_dtor_rva": "0x7E6B80", + "description": "Singleton managing all AI diagnostics. Receives diagnostic data from all AIDiagnosticsClass instances." + }, + "AILogWindow": { + "constructor_rva": "0x1AA10", + "global_data_rva": "0xB39140", + "log_title": "AI Log", + "description": "Debug log window for AI. Inherits LogWindowItem. Initialized at static address 0xB39140 with atexit destructor. Title is 'AI Log'." + } + }, + + "goal_system_detail": { + "InstantiatedGoalClass": { + "constructor_rva": "0x691660", + "description": "A live goal instance. Multiple inheritance: RefCountClass + SignalListenerClass + AIDiagnosticsClass. Contains references to goal type, owning system, priority, and state.", + "inherits": ["RefCountClass", "SignalListenerClass", "AIDiagnosticsClass"], + "fields": [ + { + "offset": "0x40", + "type": "pointer", + "name": "field_40" + }, + { + "offset": "0x48", + "type": "pointer", + "name": "field_48" + }, + { + "offset": "0x50", + "type": "pointer", + "name": "field_50" + }, + { + "offset": "0x58", + "type": "int64", + "name": "field_58" + }, + { + "offset": "0x60", + "type": "int64", + "name": "field_60" + }, + { + "offset": "0x68", + "type": "int64", + "name": "field_68" + }, + { + "offset": "0x70", + "type": "int64", + "name": "field_70" + }, + { + "offset": "0x78", + "type": "int64", + "name": "field_78" + }, + { + "offset": "0x80", + "type": "int64", + "name": "field_80" + }, + { + "offset": "0x88", + "type": "pointer", + "name": "param_1_ref", + "description": "Constructor param_1 — likely the goal type or owning goal system" + } + ] + }, + "AIGoalProposalFunctionSetClass": { + "destructor_rva": "0x476CB0", + "description": "Set of proposal scoring functions for a goal type. Managed by TheAIGoalProposalFunctionSetManagerClass." + } + }, + + "target_location_system": { + "description": "AI target locations are wrappers around game positions that the AI uses for goal targeting.", + "classes": { + "PlanetAITargetLocationClass": { + "constructor_rva": "0x665470", + "description": "Galactic-mode target: a planet." + }, + "TacticalRegionAITargetLocationClass": { + "constructor_rva": "0x725310", + "description": "Tactical-mode target: a map region." + }, + "TacticalUnitAITargetLocationClass": { + "constructor_rva": "0x724B90", + "description": "Tactical-mode target: a specific unit." + } + } + }, + + "ai_hint_zones": { + "AIHintZoneClass": { + "constructor_rva": "0x5A8CC0", + "description": "Map hint zones that guide AI behavior. Used by Find_Hint Lua function. Contains two hash maps (unit lists for zone). Fields at data+0xB8 = category_id (0xFFFFFFFF initially), data+0xBC = priority (1).", + "fields": [ + { + "offset": "data+0xB8", + "type": "int32", + "name": "category_id", + "value_on_init": "0xFFFFFFFF" + }, + { + "offset": "data+0xBC", + "type": "int32", + "name": "priority", + "value_on_init": 1 + } + ] + } + }, + + "execution_pathfinding": { + "AIExecutionPathFinderClass": { + "constructor_rva": "0x690BF0", + "destructor_rva": "0x690D90", + "description": "Pathfinding for AI execution plans. Used by AIBuildTaskClass in PATHING_DEST states." + } + }, + + "task_force_system": { + "description": "Task forces are collections of units organized for coordinated action. Each game mode has its own task force class.", + "classes": { + "TaskForceClass": { + "destructor_rva": "0x5F9440", + "lua_wrapper_rva": "0x5F8760" + }, + "GalacticTaskForceClass": { + "lua_wrapper_rva": "0x6CCE20" + }, + "LandTaskForceClass": { + "lua_wrapper_rva": "0x6E8B80" + }, + "SpaceTaskForceClass": { + "lua_wrapper_rva": "0x6E4810" + }, + "TaskForceDefinitionClass": { + "constructor_rva": "0x66F5D0", + "description": "Template definition for a task force composition." + } + } + }, + + "global_references": { + "DAT_140b15418": { + "rva": "0xB15418", + "description": "Global pointer checked in AIBuildTaskClass state machine. When non-null, calls vtable method at +0xE0 to get current game time. When null, falls back to DAT_140b153fc. This is the game clock / frame counter accessor.", + "usage": "AI production timing, state polling" + }, + "DAT_140b153fc": { + "rva": "0xB153FC", + "description": "Fallback game state value when DAT_140b15418 is null." + }, + "DAT_140b168c8": { + "rva": "0xB168C8", + "description": "Global int used as capacity threshold in BUILD_STATE_PATHING_DEST. Compared against FUN_1403f9530 result (current force count on a planet)." + } + }, + + "class_hierarchy_summary": { + "description": "Complete AI class inheritance tree as recovered from RTTI.", + "tree": { + "AIDiagnosticsClass": { + "children": [ + { + "name": "AIPlayerClass", + "description": "Per-player AI controller" + }, + { + "name": "ServicedAISystemClass", + "children": [ + {"name": "AIPerceptionSystemClass", "children": [ + {"name": "GalacticPerceptionSystemClass"}, + {"name": "LandPerceptionSystemClass"}, + {"name": "SpacePerceptionSystemClass"} + ]}, + {"name": "AIPlanningSystemClass"}, + {"name": "AIGoalSystemClass", "children": [ + {"name": "GalacticGoalSystemClass"}, + {"name": "LandGoalSystemClass"}, + {"name": "SpaceGoalSystemClass"} + ]}, + {"name": "AIExecutionSystemClass"}, + {"name": "AILearningSystemClass"}, + {"name": "AITemplateSystemClass"} + ] + }, + { + "name": "AIBudgetClass", + "children": [ + {"name": "BudgetedCategoryStruct"} + ] + }, + { + "name": "InstantiatedGoalClass" + }, + { + "name": "TacticalAIManagerClass" + } + ] + } + } + }, + + "lua_ai_bindings_summary": { + "global_functions": [ + {"name": "Suspend_AI", "params": "seconds", "description": "Pauses all AI for N seconds"}, + {"name": "EvaluatePerception", "params": "...", "description": "AI perception evaluation"}, + {"name": "GiveDesireBonus", "params": "...", "description": "Modifies AI desire weights for goals"}, + {"name": "_ProduceObject", "params": "player, type", "description": "AI production command (blocking)"}, + {"name": "FindTarget", "params": "evaluator, ...", "description": "AI target finder"}, + {"name": "Assemble_Fleet", "params": "player, planet, types", "description": "Assembles fleet at planet"} + ], + "player_methods": [ + {"name": "Is_Enemy", "params": "player", "returns": "boolean"}, + {"name": "Is_Ally", "params": "player", "returns": "boolean"}, + {"name": "Enable_As_Actor", "params": "none", "description": "Enables AI actor mode for player"}, + {"name": "Is_Human", "params": "none", "returns": "boolean"}, + {"name": "IsHumanControlled", "params": "none", "returns": "boolean"}, + {"name": "Make_Ally", "params": "player", "note": "Resets on game mode change"}, + {"name": "Make_Enemy", "params": "player", "note": "Resets on game mode change"} + ], + "object_methods": [ + {"name": "Prevent_AI_Usage", "params": "bool", "note": "Crashes in tactical if unit has no active AI"}, + {"name": "Set_As_Goal_System_Removable", "params": "bool", "description": "AI goal cleanup flag on TaskForceClass"} + ], + "free_store_methods": [ + {"name": "Get_Object_Count", "wrapper": "LuaMemberFunctionWrapper"} + ] + }, + + "rva_index": { + "_note": "All RVAs relative to module base (0x140000000 in Ghidra). For runtime: address = module_base + rva.", + "constructors": { + "TheAIClass": "0x4D9C80", + "AIPlayerClass": "0x4AF810", + "ServicedAISystemClass": "0x64C250", + "AIPerceptionSystemClass": "0x4DAD80", + "GalacticPerceptionSystemClass": "0x4E1880", + "LandPerceptionSystemClass": "0x6B8980", + "SpacePerceptionSystemClass": "0x6B9A20", + "TacticalPerceptionGridClass": "0x653340", + "AIPlanningSystemClass": "0x6BAC00", + "AIGoalSystemClass": "0x6C7970", + "GalacticGoalSystemClass": "0x6B8480", + "LandGoalSystemClass": "0x6B86E0", + "SpaceGoalSystemClass": "0x6B88D0", + "AIExecutionSystemClass": "0x524CE0", + "AILearningSystemClass": "0x585D00", + "AITemplateSystemClass": "0x6BB9E0", + "AIBudgetClass": "0x6109C0", + "BudgetedCategoryStruct": "0x610A70", + "AIBuildTaskClass": "0x6478C0", + "AIPlanetBuildTaskClass": "0x6954B0", + "InstantiatedGoalClass": "0x691660", + "TacticalAIManagerClass": "0x5F4BE0", + "DifficultyAdjustmentManagerClass": "0x484B90", + "AllyEventClass": "0x689AD0", + "AIHintZoneClass": "0x5A8CC0", + "AIExecutionPathFinderClass": "0x690BF0", + "ProduceForceBlockStatus": "0x6C6500", + "TheAIDataManagerClass": "0x4747E0", + "TheAIPlayerTypeManagerClass": "0x4E9940", + "TheAIGoalTypeManagerClass": "0x5E6690", + "TheAIGoalProposalFunctionSetManagerClass": "0x5E5C30", + "TheAITemplateManagerClass": "0x4E8920", + "AILogWindow": "0x1AA10", + "PlanetAITargetLocationClass": "0x665470", + "TacticalRegionAITargetLocationClass": "0x725310", + "TacticalUnitAITargetLocationClass": "0x724B90", + "TaskForceDefinitionClass": "0x66F5D0", + "FreeStoreMovementBlockStatus": "0x71F630" + }, + "state_machine_handlers": { + "AIBuildTaskClass_vfunction2": "0x64AEC0" + }, + "singleton_ctors": { + "SingletonInstance_TheAIClass": "0x7EED80", + "SingletonInstance_TheAIPlayerTypeManagerClass": "0x7EED90", + "SingletonInstance_AIDiagnosticsManagerClass": "0x7E6B80", + "SingletonInstance_TheAIDataManagerClass": "0x7E6BA0", + "SingletonInstance_GalacticPathFinderClass": "0x7E6920" + }, + "destructors": { + "PlayerClass": "0x27ED40", + "AIGoalSystemClass": "0x6C7A30", + "AIPerceptionSystemClass": "0x4DAFD0", + "AIFreeStoreClass": "0x4D2780", + "AIExecutionPathFinderClass": "0x690D90", + "TaskForceClass": "0x5F9440" + } + } +} diff --git a/re-findings/camera_selection_system.json b/re-findings/camera_selection_system.json new file mode 100644 index 000000000..bed953e74 --- /dev/null +++ b/re-findings/camera_selection_system.json @@ -0,0 +1,677 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "SWFOC Camera & Selection System — Reverse Engineering Results", + "agent": "Agent 3I — Camera & Selection System RE", + "date": "2026-04-04", + "binary": "StarWarsG.exe (x86_64, MSVC, ASLR enabled)", + "notes": "All RVAs relative to module base. All offsets relative to struct base pointer. Ghidra static analysis only (CE runtime access failed due to privilege/ASLR resolution issue).", + + "globals": { + "GameModeManagerClass_singleton": { + "rva": "0xB153E0", + "type": "GameModeManagerClass*", + "description": "Global singleton pointer to the GameModeManagerClass instance. This is the root of the game mode hierarchy. Contains an array of active GameModeClass instances (Galactic, Land, Space). Accessed by camera, selection, and event systems.", + "confidence": "CONFIRMED", + "evidence": "Referenced directly by SelectEventClass, SelectAllEventClass constructors (FUN_14028d930), GalacticCameraClass::Update (FUN_1403c2c00), and ControlGroupEventClass.", + "layout": { + "+0x48": "pointer to array of GameModeClass* pointers", + "+0x50": "int32 count of active game modes", + "+0xB4": "int32 current selection mode state (written by event system)" + } + }, + "DAT_140b15418": { + "rva": "0xB15418", + "type": "pointer", + "description": "Secondary global pointer used by GalacticCameraClass::Update. Likely the GalacticModeClass singleton or active galactic state. Offset +0x18 leads to a sub-struct with +0x618 containing mode sub-state.", + "confidence": "DISCOVERED", + "evidence": "Read in FUN_1403c2c00 as guard condition for camera parameter selection." + }, + "screen_aspect_ratio": { + "rva": "0xA12550", + "type": "float32", + "description": "Current screen aspect ratio. Returned by FUN_14014ee30(). Used when setting camera FOV/aspect.", + "confidence": "CONFIRMED", + "evidence": "Called in GalacticCameraClass::Update and passed to Camera::SetFovAspect." + }, + "camera_globals_block": { + "rva_base": "0xB1599C", + "type": "float32[] block", + "description": "Block of galactic camera parameters controlling zoom, distance, and rotation. Used by GalacticCameraClass::Update.", + "confidence": "CONFIRMED", + "fields": { + "0xB1599C": { "name": "galactic_camera_zoom_angle", "type": "float32", "description": "Zoom rotation angle in radians for the galactic camera pitch." }, + "0xB159A0": { "name": "galactic_camera_target_zoom_angle", "type": "float32", "description": "Target zoom angle (interpolation target during smooth zoom)." }, + "0xB159A4": { "name": "galactic_camera_distance", "type": "float32", "description": "Camera distance from look-at point (arm length)." }, + "0xB159A8": { "name": "galactic_camera_distance_scale", "type": "float32", "description": "Multiplier for zoom distance during camera transitions." }, + "0xB159AC": { "name": "galactic_camera_look_dir_x", "type": "float32", "description": "X component of the galactic camera's normalized look direction." }, + "0xB159B0": { "name": "galactic_camera_look_dir_y", "type": "float32", "description": "Y component of the galactic camera's normalized look direction." }, + "0xB159B4": { "name": "galactic_camera_look_dir_z", "type": "float32", "description": "Z component of the galactic camera's normalized look direction." }, + "0xB159C8": { "name": "galactic_camera_mode_value", "type": "float32", "description": "Camera mode value copied to GalacticCameraClass +0x08." } + } + } + }, + + "structs": { + "GameModeManagerClass": { + "description": "Singleton manager for all game modes. Holds the array of active GameModeClass instances. The current tactical/galactic mode can be retrieved by iterating the array and matching the mode type via vtable+0xE0.", + "global_rva": "0xB153E0", + "global_note": "This is a POINTER to the instance; dereference once.", + "confidence": "CONFIRMED", + "fields": [ + { "offset": "0x00", "type": "int64", "name": "unknown_0x00", "description": "Set to -1 in destructor." }, + { "offset": "0x08", "type": "int64", "name": "unknown_0x08", "description": "Cleared in destructor." }, + { "offset": "0x48", "type": "pointer", "name": "game_mode_array", "description": "Pointer to array of GameModeClass* pointers." }, + { "offset": "0x50", "type": "int32", "name": "game_mode_count", "description": "Number of entries in game_mode_array." }, + { "offset": "0xB4", "type": "int32", "name": "selection_state", "description": "Selection system state integer. Written by SelectEvent and SelectAllEvent handlers." } + ], + "method_GetModeByType": { + "rva": "0x28A950", + "signature": "GameModeClass* GetModeByType(GameModeManagerClass* this, int modeType)", + "description": "Iterates game_mode_array, calls vtable[0xE0/8] on each to get its type, returns the one matching modeType. Known types: 0=Galactic, others TBD for Land/Space.", + "confidence": "CONFIRMED" + } + }, + + "GameModeClass": { + "description": "Base class for all game modes (Galactic, Land, Space). Contains the camera, selection state, objectives, control groups, and the scene graph for the current mode. This is a very large struct (0x320+ bytes for base, subclasses extend further).", + "vtable_rva_galactic": "from GalacticModeClass at 0x4B1270", + "vtable_rva_land": "from LandModeClass at 0x3B5210", + "vtable_rva_space": "from SpaceModeClass at 0x4D6BA0", + "confidence": "CONFIRMED", + "key_fields": [ + { "offset": "0x00", "type": "pointer", "name": "vtable_ptr" }, + { "offset": "0x08", "type": "int64", "name": "mode_id", "description": "Set to -1 initially." }, + { "offset": "0x10", "type": "float32", "name": "game_speed_unknown", "description": "Initialized to 0." }, + { "offset": "0x14", "type": "float32", "name": "game_speed_factor", "description": "Initialized to 1.0 (0x3f800000)." }, + { "offset": "0x28", "type": "pointer", "name": "camera_ptr", "description": "Pointer to the CameraClass (SceneClass) for this mode. This is the primary camera used for rendering. Ref-counted (Release called in destructor at this[5])." }, + { "offset": "0x30", "type": "pointer", "name": "unknown_ptr_0x30" }, + { "offset": "0x38", "type": "pointer", "name": "unknown_ptr_0x38" }, + { "offset": "0x40", "type": "pointer", "name": "unknown_ptr_0x40" }, + { "offset": "0x90", "type": "pointer", "name": "scene_camera_ptr", "description": "Ref-counted pointer to CameraClass/SceneClass. Used by GalacticCameraClass::Update to get the active camera for the mode. Read as *(mode + 0x90). The CameraClass at this pointer has the actual camera transform sub-object." }, + { "offset": "0x190", "type": "pointer", "name": "selection_object_array_ptr", "description": "Pointer to array of pointers to selected GameObjectClass instances. Part of the DynamicVector at +0x190 area." }, + { "offset": "0x1A0", "type": "pointer", "name": "large_sub_object_ptr", "description": "Allocated 0x1390-byte sub-object. Likely TerrainClass or MapClass." }, + { "offset": "0x228", "type": "float32", "name": "map_size_x", "description": "Initialized to 10000.0 (0x461c4000)." }, + { "offset": "0x22C", "type": "float32", "name": "map_size_y", "description": "Initialized to 10000.0." }, + { "offset": "0x230", "type": "float32", "name": "zoom_factor_1", "description": "Initialized to 1.0." }, + { "offset": "0x234", "type": "float32", "name": "zoom_factor_2", "description": "Initialized to 1.0." } + ], + "notes": "The exact offset for the camera depends on the access pattern. In the GalacticCameraClass::Update, the mode pointer + 0x90 is used to get the camera. In the destructor, this[0x12] (offset 0x90) is the ref-counted camera object. This confirms offset 0x90." + }, + + "CameraClass": { + "description": "The Alamo engine's camera class. Contains the camera orientation matrix directly at +0x10 and a pointer to a 0x308-byte camera state sub-object at +0x40. The sub-object holds the full projection state, view matrices, viewport, and clip planes.", + "constructor_rva": "0x261470", + "confidence": "CONFIRMED", + "outer_fields": [ + { "offset": "0x00", "type": "pointer", "name": "vtable_ptr" }, + { "offset": "0x04", "type": "int32", "name": "ref_count", "description": "RefCountClass reference count." }, + { "offset": "0x10", "type": "float32[12]", "name": "orientation_matrix_copy", "description": "Copy of the 3x4 orientation matrix (row-major, 12 floats)." }, + { "offset": "0x40", "type": "pointer", "name": "camera_state_ptr", "description": "Pointer to the 0x308-byte camera state sub-object. This is the LIVE camera state that the renderer reads." } + ], + "camera_state_sub_object": { + "size": "0x308", + "alloc_rva": "0x140100 (called from constructor)", + "description": "The core camera state object. All camera parameters are here.", + "fields": [ + { + "offset": "0x00", + "type": "float32[12]", + "name": "orientation_matrix", + "description": "3x4 column-major orientation matrix. Layout: [right.x, up.x, fwd.x, pos.x, right.y, up.y, fwd.y, pos.y, right.z, up.z, fwd.z, pos.z]. Each column is 3 floats at stride 0x10.", + "sub_fields": { + "right_vector": { "offsets": ["0x00", "0x10", "0x20"], "description": "Camera right vector (X axis)" }, + "up_vector": { "offsets": ["0x04", "0x14", "0x24"], "description": "Camera up vector (Y axis)" }, + "forward_vector": { "offsets": ["0x08", "0x18", "0x28"], "description": "Camera forward vector (Z axis, into screen)" }, + "position": { "offsets": ["0x0C", "0x1C", "0x2C"], "description": "Camera world position (X, Y, Z)" } + }, + "confidence": "CONFIRMED", + "evidence": "GetPosition reads +0x0C, +0x1C, +0x2C. GetForward reads +0x08, +0x18 (negated). TranslateLocal multiplies delta by matrix and adds to position columns." + }, + { + "offset": "0x30", + "type": "float32", + "name": "near_clip", + "description": "Near clipping plane distance. Default: 1.0 (0x3f800000).", + "confidence": "CONFIRMED", + "evidence": "Read and written by SetClipPlanes/GetClipPlanes. Used in projection matrix build." + }, + { + "offset": "0x34", + "type": "float32", + "name": "far_clip", + "description": "Far clipping plane distance. Default: 1000.0 (0x447a0000).", + "confidence": "CONFIRMED" + }, + { + "offset": "0x38", + "type": "float32", + "name": "far_clip_bias", + "description": "Bias factor for far clip. Default: -1.0 (0xbf800000). When != -1.0, an alternate far distance is used.", + "confidence": "DISCOVERED" + }, + { + "offset": "0x3C", + "type": "float32", + "name": "alt_far_clip", + "description": "Alternate far clip distance. Used when far_clip_bias != -1.0. Default: -1.0.", + "confidence": "DISCOVERED" + }, + { + "offset": "0x44", + "type": "int32", + "name": "projection_mode", + "description": "0 = perspective projection, 1 = orthographic projection. Controls which projection math path is used.", + "confidence": "CONFIRMED", + "evidence": "Checked in FUN_1401417c0 and FUN_140141260. SetOrthoProjection sets this to 1." + }, + { + "offset": "0x48", + "type": "float32", + "name": "fov_or_half_angle", + "description": "For perspective: FOV angle in radians. Default: pi/4 = 0.785 rad = 45 degrees (0x3f490fdb). For ortho: half-width.", + "confidence": "CONFIRMED", + "evidence": "Initialized to 0x3f490fdb (pi/4). Read by GetFov, written by SetFovAspect. Used with tan() in projection matrix." + }, + { + "offset": "0x4C", + "type": "float32", + "name": "aspect_ratio", + "description": "Viewport aspect ratio. Default: 1.333 = 4:3 (0x3faaaaab). Set by SetFovAspect.", + "confidence": "CONFIRMED", + "evidence": "Initialized to 0x3faaaaab. Read by GetFov/GetAspect." + }, + { + "offset": "0x50", + "type": "float32", + "name": "ortho_left", + "description": "Left edge for orthographic projection.", + "confidence": "CONFIRMED" + }, + { + "offset": "0x54", + "type": "float32", + "name": "ortho_top", + "description": "Top edge for orthographic projection.", + "confidence": "CONFIRMED" + }, + { + "offset": "0x58", + "type": "float32", + "name": "ortho_right", + "description": "Right edge for orthographic projection.", + "confidence": "CONFIRMED" + }, + { + "offset": "0x5C", + "type": "float32", + "name": "ortho_bottom", + "description": "Bottom edge for orthographic projection.", + "confidence": "CONFIRMED" + }, + { + "offset": "0x60", + "type": "uint8", + "name": "user_clip_enabled", + "description": "Non-zero enables a user-defined clip plane.", + "confidence": "DISCOVERED" + }, + { + "offset": "0x64", + "type": "float32[4]", + "name": "user_clip_plane", + "description": "User-defined clip plane coefficients (a, b, c, d). Applied when user_clip_enabled != 0.", + "confidence": "DISCOVERED" + }, + { + "offset": "0x74", + "type": "float32", + "name": "viewport_x", + "description": "Viewport origin X (normalized 0-1).", + "confidence": "CONFIRMED" + }, + { + "offset": "0x78", + "type": "float32", + "name": "viewport_y", + "description": "Viewport origin Y (normalized, 1.0 - input).", + "confidence": "CONFIRMED" + }, + { + "offset": "0x7C", + "type": "float32", + "name": "viewport_width", + "description": "Viewport width (normalized 0-1).", + "confidence": "CONFIRMED" + }, + { + "offset": "0x80", + "type": "float32", + "name": "viewport_height", + "description": "Viewport height (normalized, 1.0 - input).", + "confidence": "CONFIRMED" + }, + { + "offset": "0x84", + "type": "float32[12]", + "name": "inverse_orientation_matrix", + "description": "Computed inverse of the orientation matrix. Updated by FUN_14022d390.", + "confidence": "DISCOVERED" + }, + { + "offset": "0xB4", + "type": "float32[16]", + "name": "projection_matrix", + "description": "4x4 projection matrix (perspective or ortho). Rebuilt by FUN_140141260.", + "confidence": "CONFIRMED" + }, + { + "offset": "0xF4", + "type": "float32[16+]", + "name": "view_projection_matrix", + "description": "Combined view-projection matrix for the near clip. Built by FUN_140232c50 (perspective) or FUN_140232df0 (ortho).", + "confidence": "CONFIRMED" + }, + { + "offset": "0x1FC", + "type": "float32[16+]", + "name": "far_view_projection_matrix", + "description": "Combined view-projection matrix for the far clip distance. Used for far-field rendering.", + "confidence": "CONFIRMED" + }, + { + "offset": "0x304", + "type": "uint8", + "name": "dirty_flag", + "description": "Cleared (set to 0) during init.", + "confidence": "DISCOVERED" + } + ], + "initial_values": { + "orientation": "identity matrix", + "position": "(0, 0, -60) — offset 0x0C = 0xc2700000 = -60.0f", + "near_clip": "1.0", + "far_clip": "1000.0", + "fov": "pi/4 (45 degrees)", + "aspect": "1.333 (4:3)" + } + }, + "methods": { + "GetTransformMatrix": { + "rva": "0x2619F0", + "signature": "void GetTransformMatrix(CameraClass* this, float* outMatrix12)", + "description": "Copies the 12-float orientation matrix from the sub-object.", + "confidence": "CONFIRMED" + }, + "SetTransformMatrix": { + "rva": "0x261BD0", + "signature": "void SetTransformMatrix(CameraClass* this, float* inMatrix12)", + "description": "Sets the orientation matrix (12 floats at sub-object +0x00 to +0x2C). Recalculates the inverse matrix and view-projection matrices.", + "confidence": "CONFIRMED" + }, + "GetPosition": { + "rva": "0x261A40", + "signature": "void GetPosition(CameraClass* this, float* outXYZ)", + "description": "Reads camera world position from matrix offsets +0x0C, +0x1C, +0x2C of the sub-object.", + "confidence": "CONFIRMED" + }, + "GetForwardDirection": { + "rva": "0x261690", + "signature": "void GetForwardDirection(CameraClass* this, float* outFwd3, float* outUp3)", + "description": "Gets the negated forward vector from matrix offsets +0x08, +0x18 (XOR sign bit). Also returns a fixed up vector (0, -1, 0).", + "confidence": "CONFIRMED" + }, + "TranslateLocal": { + "rva": "0x261C90", + "signature": "void TranslateLocal(CameraClass* this, float* deltaXYZ)", + "description": "Translates the camera in local space. Multiplies the 3D delta by the orientation matrix axes and adds to position. Recalculates inverse and view-proj matrices.", + "confidence": "CONFIRMED" + }, + "GetFovAspect": { + "rva": "0x2618E0", + "signature": "void GetFovAspect(CameraClass* this, float* outFov, float* outAspect)", + "description": "Reads FOV from sub-object +0x48 and aspect ratio from +0x4C.", + "confidence": "CONFIRMED" + }, + "SetFovAspect": { + "rva": "0x261A80", + "signature": "void SetFovAspect(CameraClass* this, float fov, float aspect)", + "description": "Sets FOV (+0x48), aspect (+0x4C), clears projection_mode to 0, rebuilds projection and view-proj matrices.", + "confidence": "CONFIRMED" + }, + "SetPerspectiveProjection": { + "rva": "0x261AB0", + "signature": "void SetPerspectiveProjection(CameraClass* this, float left, float right, float top, float bottom, float nearClip, float farClip)", + "description": "Sets perspective projection from frustum edges. Calculates FOV and aspect from edges, sets clip planes at +0x30/+0x34.", + "confidence": "CONFIRMED" + }, + "SetOrthoProjection": { + "rva": "0x261B50", + "signature": "void SetOrthoProjection(CameraClass* this, float left, float top, float right, float bottom, float nearClip, float farClip)", + "description": "Sets orthographic projection. Stores bounds at +0x50..+0x5C, sets projection_mode=1, rebuilds matrices.", + "confidence": "CONFIRMED" + }, + "SetViewport": { + "rva": "0x261E00", + "signature": "void SetViewport(CameraClass* this, float x, float width, float y, float height)", + "description": "Sets viewport rect at sub-object +0x74..+0x80. Y and height are inverted (1.0 - input).", + "confidence": "CONFIRMED" + }, + "GetFrustumBounds": { + "rva": "0x261900", + "signature": "void GetFrustumBounds(CameraClass* this, float* top, float* bottom, float* left, float* right, float* nearClip, float* farClip)", + "description": "Returns the frustum bounds. For perspective, calculates from FOV/aspect using tan(). For ortho, reads directly from +0x50..+0x5C.", + "confidence": "CONFIRMED" + }, + "GetViewport": { + "rva": "0x261870", + "signature": "void GetViewport(CameraClass* this, bool p2, bool p3, bool p4)", + "description": "Reads viewport rect from sub-object +0x74..+0x80 and passes to FUN_14013d030.", + "confidence": "DISCOVERED" + }, + "CopyFrom": { + "rva": "0x261590", + "signature": "CameraClass* CopyFrom(CameraClass* this, CameraClass* other)", + "description": "Copies camera state from another CameraClass instance. Copies inner sub-object and outer matrix.", + "confidence": "CONFIRMED" + }, + "Render": { + "rva": "0x2611C0", + "signature": "bool Render(CameraClass* this, int pass)", + "description": "Iterates the scene model list, calling Prepare then Render on each model.", + "confidence": "DISCOVERED" + } + } + }, + + "GalacticCameraClass": { + "description": "Camera controller for the galactic (strategy) map. Manages the orbital camera that orbits around a look-at point with zoom, pitch, and rotation. Reads global camera parameters and interpolates between them during transitions.", + "constructor_rva": "0x3C2B20", + "update_rva": "0x3C2C00", + "confidence": "CONFIRMED", + "fields": [ + { "offset": "0x00", "type": "pointer", "name": "vtable_ptr" }, + { "offset": "0x08", "type": "float32", "name": "camera_mode_value", "description": "Copied from DAT_140b159c8 after update." }, + { "offset": "0x0C", "type": "float32", "name": "look_at_x", "description": "Look-at target X position in world space." }, + { "offset": "0x10", "type": "float32", "name": "look_at_y", "description": "Look-at target Y position." }, + { "offset": "0x14", "type": "float32", "name": "look_at_z", "description": "Look-at target Z position." }, + { "offset": "0x18", "type": "pointer", "name": "camera_class_ptr", "description": "Pointer to the CameraClass that this controller drives. Obtained from GameMode+0x90." }, + { "offset": "0x28", "type": "float32", "name": "target_look_x", "description": "Interpolation target for look_at_x." }, + { "offset": "0x2C", "type": "float32", "name": "target_look_y", "description": "Interpolation target for look_at_y." }, + { "offset": "0x30", "type": "float32", "name": "target_look_z", "description": "Interpolation target for look_at_z." }, + { "offset": "0x34", "type": "float32", "name": "orbit_distance", "description": "Distance from look-at point (zoom arm length). Multiplied by look direction globals." }, + { "offset": "0x3C", "type": "float32", "name": "transition_timer", "description": "Timer for smooth camera transitions. Counts down toward 0." }, + { "offset": "0x44", "type": "int32", "name": "update_state", "description": "Camera update state machine. Values 1-2 = transitioning, 3+ = free orbit mode." }, + { "offset": "0x4C", "type": "float32", "name": "custom_camera_distance", "description": "Custom distance override used when mode sub-state <= 2." }, + { "offset": "0x50", "type": "float32", "name": "custom_zoom_angle", "description": "Custom zoom rotation angle override." } + ], + "update_function": { + "rva": "0x3C2C00", + "description": "Main update function called each frame. 1) Gets current GameMode via GetModeByType(0). 2) Gets the CameraClass from mode+0x90. 3) Depending on update_state: interpolates position/zoom between current and target, or maintains free orbit. 4) Builds orientation matrix with rotation around X axis (pitch from zoom angle). 5) Sets the camera transform matrix and FOV/aspect.", + "camera_position_calculation": "The camera position is calculated as: position = look_at_point + rotation_matrix * (look_direction * distance). The look direction comes from globals at 0xB159AC/B0/B4. The rotation is built using FUN_1402cf8d0 (matrix rotation by angle).", + "confidence": "CONFIRMED" + } + }, + + "SelectEventClass": { + "description": "Event dispatched when the player selects a unit/object. Inherits from ScheduledEventClass. Event type ID = 5. References the GameModeManagerClass singleton.", + "constructor_rva": "0x3AC9D0", + "event_type": 5, + "confidence": "CONFIRMED", + "fields": [ + { "offset": "0x1FC (from SelectEventClass_data base)", "type": "int32", "name": "selection_data", "description": "Initialized to 0. Likely the object ID or selection flags." } + ], + "global_reference": "Writes to GameModeManagerClass+0xB4 via FUN_14028d930." + }, + + "SelectAllEventClass": { + "description": "Event dispatched for 'select all units' command. Event type ID = 37.", + "constructor_rva": "0x437F40", + "event_type": 37, + "confidence": "CONFIRMED", + "global_reference": "Same as SelectEventClass — writes to GameModeManagerClass+0xB4." + }, + + "ControlGroupEventClass": { + "description": "Event for control group assign/recall (Ctrl+1..9 / 1..9). Event type ID = 30.", + "constructor_rva": "0x436060", + "event_type": 30, + "confidence": "CONFIRMED", + "fields": [ + { "offset": "0x00", "type": "int32", "name": "group_number", "description": "Control group index (0-9)." }, + { "offset": "0x04", "type": "int32", "name": "action", "description": "0 = assign to group, 1 = recall group (TBD)." } + ] + }, + + "LookEventClass": { + "description": "Event dispatched when camera should look at a target (corresponds to Lua Point_Camera_At). Event type ID = 4.", + "constructor_rva": "0x689670", + "event_type": 4, + "confidence": "CONFIRMED" + }, + + "SelectionDataPackClass": { + "description": "Per-object selection state data pack. Attached to GameObjectClass instances that can be selected. Pool-allocated (pool size 20). Contains 5 ref-counted pointers and selection state.", + "constructor_rva": "0x55BE30", + "destructor_rva": "0x55BE70", + "pool_size": 20, + "confidence": "CONFIRMED", + "fields": [ + { "offset": "0x00", "type": "pointer (ref-counted)", "name": "selection_ref_0", "description": "First ref-counted pointer. Released in destructor." }, + { "offset": "0x08", "type": "pointer (ref-counted)", "name": "selection_ref_1" }, + { "offset": "0x10", "type": "pointer (ref-counted)", "name": "selection_ref_2" }, + { "offset": "0x18", "type": "pointer (ref-counted)", "name": "selection_ref_3" }, + { "offset": "0x20", "type": "pointer (ref-counted)", "name": "selection_ref_4" }, + { "offset": "0x28", "type": "int32", "name": "selection_state", "description": "Initialized to 1. Selection state flags." }, + { "offset": "0x30", "type": "msvc_sso_string", "name": "selection_group_name", "description": "String field (SSO). Freed in destructor via FID_conflict_Tidy_deallocate." }, + { "offset": "0x48", "type": "int32", "name": "selection_type", "description": "Initialized to 7. Type/category of selection." } + ] + }, + + "SelectBehaviorClass": { + "description": "Behavior component attached to selectable GameObjectClass instances. Handles the selection logic (highlight, UI feedback, etc.). Size = 0x58 bytes. Inherits from BehaviorClass.", + "destructor_rva": "0x3C1960", + "size": "0x58", + "confidence": "CONFIRMED" + } + }, + + "functions": { + "D3DXMatrixPerspectiveFovLH": { + "rva": "0x7A882D", + "description": "D3DX import for building a left-handed perspective projection matrix from FOV. Called by the camera projection rebuild functions.", + "confidence": "CONFIRMED" + }, + "D3DXMatrixPerspectiveFovRH": { + "rva": "0x7A87D3", + "description": "D3DX import for right-handed perspective projection.", + "confidence": "CONFIRMED" + }, + "D3DXMatrixLookAtLH": { + "rva": "0x7A8827", + "description": "D3DX import for building a left-handed view matrix from eye/target/up. Used in camera view matrix construction.", + "confidence": "CONFIRMED" + }, + "BuildPerspectiveViewProjectionMatrix": { + "rva": "0x232C50", + "description": "Builds a combined view-projection matrix for perspective cameras. Copies orientation, applies frustum signs, generates the combined matrix.", + "confidence": "CONFIRMED" + }, + "BuildOrthoViewProjectionMatrix": { + "rva": "0x232DF0", + "description": "Builds a combined view-projection matrix for orthographic cameras.", + "confidence": "CONFIRMED" + }, + "BuildProjectionMatrix_Perspective": { + "rva": "0x232220", + "description": "Builds the 4x4 perspective projection matrix from FOV, aspect, near, far. Uses tan() for FOV conversion.", + "confidence": "CONFIRMED" + }, + "RebuildProjectionMatrix": { + "rva": "0x141260", + "description": "Rebuilds the projection matrix based on current projection_mode (0=perspective, 1=ortho). Dispatches to the appropriate builder.", + "confidence": "CONFIRMED" + }, + "RebuildViewProjectionMatrices": { + "rva": "0x1417C0", + "description": "Rebuilds both near and far view-projection matrices. Called after any camera parameter change.", + "confidence": "CONFIRMED" + }, + "MatrixRotateByAngle": { + "rva": "0x2CF8D0", + "description": "Applies a rotation by angle (radians) to a 3x3 rotation matrix. Uses sin/cos of the angle. Used by GalacticCameraClass for pitch/yaw rotation.", + "confidence": "CONFIRMED" + }, + "CameraStateInit": { + "rva": "0x140100", + "description": "Initializes the 0x308-byte camera state sub-object with identity matrix, default FOV (pi/4), aspect (4:3), near clip (1.0), far clip (1000.0), and default position (0, 0, -60).", + "confidence": "CONFIRMED" + }, + "GetScreenAspectRatio": { + "rva": "0x14EE30", + "description": "Returns the global screen aspect ratio from DAT_140a12550.", + "confidence": "CONFIRMED" + }, + "GetModeByType": { + "rva": "0x28A950", + "description": "Iterates GameModeManagerClass's mode array, returning the GameModeClass* matching the given type ID. Called with type 0 for galactic mode.", + "confidence": "CONFIRMED" + }, + "SetSelectionState": { + "rva": "0x28D930", + "description": "Sets the selection state integer at GameModeManagerClass+0xB4. Called by SelectEventClass and SelectAllEventClass constructors.", + "confidence": "CONFIRMED" + } + }, + + "access_patterns": { + "get_current_camera_position": { + "description": "How to read the active camera's world position at runtime.", + "chain": [ + "1. Read GameModeManagerClass pointer at [module_base + 0xB153E0]", + "2. Call GetModeByType(mode_mgr, 0) to get the current galactic GameModeClass (or iterate to find active)", + "3. Read CameraClass pointer at [GameModeClass + 0x90]", + "4. Read camera_state_ptr at [CameraClass + 0x40]", + "5. Position X = float at [camera_state + 0x0C]", + "6. Position Y = float at [camera_state + 0x1C]", + "7. Position Z = float at [camera_state + 0x2C]" + ], + "pointer_chain_notation": "[module+0xB153E0] -> GetModeByType(0) -> +0x90 -> +0x40 -> {+0x0C, +0x1C, +0x2C}", + "confidence": "CONFIRMED" + }, + "get_camera_zoom_level": { + "description": "How to read the camera's zoom/FOV.", + "chain": [ + "1-4. Same as position chain above", + "5. FOV = float at [camera_state + 0x48] (radians)", + "6. Aspect = float at [camera_state + 0x4C]" + ], + "confidence": "CONFIRMED" + }, + "get_camera_rotation": { + "description": "Camera orientation is encoded in the 3x4 matrix at camera_state +0x00. The forward direction is the Z column negated: forward = (-[+0x08], -[+0x18], 0). For the galactic camera, the rotation angle is stored in the GalacticCameraClass globals at 0xB1599C (pitch angle in radians).", + "confidence": "CONFIRMED" + }, + "free_camera_unlock_mechanism": { + "description": "The GalacticCameraClass::Update (RVA 0x3C2C00) has an update_state field at +0x44. When update_state >= 4, the camera operates in free orbit mode with no position constraints. Setting update_state to a high value (e.g., 5) and writing custom look-at coordinates (+0x0C/+0x10/+0x14) and orbit distance (+0x34) would effectively unlock the camera. For a full free camera: bypass GalacticCameraClass::Update entirely by NOPing the call, then write the camera_state sub-object's orientation matrix directly.", + "nop_approach": "Find the call to GalacticCameraClass::Update and NOP it. Then write the camera_state transform matrix (12 floats at sub-object +0x00) directly each frame.", + "confidence": "DISCOVERED", + "status": "Theoretical — needs runtime validation" + }, + "get_selected_unit": { + "description": "The selection system works through the event mechanism. SelectEventClass (type 5) and SelectAllEventClass (type 37) write to GameModeManagerClass+0xB4. The actual selected object list is maintained per-GameModeClass in the DynamicVector structures within the mode. The GameModeClass constructor at offset this[0x38] (0x1C0) initializes a DynamicVectorClass> which holds per-player selection groups.", + "chain": [ + "1. Read GameModeManagerClass pointer at [module_base + 0xB153E0]", + "2. Get active GameModeClass via GetModeByType", + "3. The selection state is at GameModeClass — the DynamicVector> at approximately offset 0x1C0-0x1D8 holds per-player selection lists", + "4. For the local player, index into the outer vector by player slot", + "5. The inner DynamicVectorClass contains GameObjectClass* pointers to selected units" + ], + "confidence": "DISCOVERED", + "notes": "Exact offset for the selection vector needs runtime verification. The GameModeClass constructor shows DynamicVectorClass> at this[0x3B] (offset 0x1D8) with initialization using DAT_140a16fb0 (which is the max player count)." + }, + "selection_via_selectbehavior": { + "description": "Individual objects have a SelectBehaviorClass (size 0x58) attached as a behavior component. To check if a specific GameObjectClass is selected, query its SelectionDataPackClass data pack. The data pack is attached through the standard data-pack system.", + "confidence": "DISCOVERED" + } + }, + + "d3d_engine_params": { + "description": "The Alamo engine uses named engine parameters for shader uniforms. These are factory classes that provide transform matrices to the rendering pipeline.", + "params": { + "Get_D3d_View_Transform": { "rva": "0x1ADC70", "description": "Provides the camera view matrix to shaders." }, + "Get_D3d_Projection_Transform": { "rva": "0x1ADCF0", "description": "Provides the projection matrix." }, + "Get_D3d_View_Inverse_Transform": { "rva": "0x1ADD70", "description": "Provides the inverse view matrix (camera-to-world)." }, + "Get_D3d_View_Projection_Transform": { "rva": "0x1ADDF0", "description": "Provides the combined view-projection matrix." }, + "Get_D3d_Model_Transform": { "rva": "0x1ADBF0", "description": "Provides the model (world) matrix." }, + "Get_D3d_Model_View_Transform": { "rva": "0x1ADF70", "description": "Provides the combined model-view matrix." }, + "Get_D3d_Model_View_Projection_Transform": { "rva": "0x1ADFF0", "description": "Provides the MVP matrix." }, + "Get_D3d_Model_Inverse_Transform": { "rva": "0x1ADE70", "description": "Provides the inverse model matrix." }, + "Get_D3d_Model_Inverse_Transpose_Transform": { "rva": "0x1ADEF0", "description": "Provides the inverse-transpose model matrix (for normals)." }, + "Get_D3d_Model_View_Inverse_Transform": { "rva": "0x1AE070", "description": "Provides the inverse model-view matrix." } + }, + "confidence": "CONFIRMED" + }, + + "class_hierarchy": { + "CameraClass": { + "inherits": ["RefCountClass"], + "rtti": ".?AVCameraClass@@" + }, + "GalacticCameraClass": { + "inherits": ["unknown"], + "rtti": ".?AVGalacticCameraClass@@", + "description": "Camera controller for galactic map. Does NOT inherit from CameraClass. It DRIVES a CameraClass instance." + }, + "GameModeClass": { + "inherits": ["SignalGeneratorClass"], + "subclasses": ["LandModeClass", "SpaceModeClass", "GalacticModeClass"] + }, + "SelectEventClass": { + "inherits": ["ScheduledEventClass", "EventClass"] + }, + "SelectAllEventClass": { + "inherits": ["ScheduledEventClass", "EventClass"] + }, + "SelectBehaviorClass": { + "inherits": ["BehaviorClass"], + "size": "0x58" + }, + "SelectionDataPackClass": { + "inherits": ["PooledObjectClass"], + "pool_size": 20 + } + }, + + "lua_function_mapping": { + "Point_Camera_At": { + "event_class": "LookEventClass", + "event_type_id": 4, + "description": "Dispatched as a LookEvent that sets the camera look-at target." + }, + "Scroll_Camera_To": { + "description": "Likely uses the same GalacticCameraClass target interpolation. Sets target_look_x/y/z and starts a transition (update_state = 1 or 2).", + "confidence": "DISCOVERED" + }, + "Zoom_Camera": { + "description": "Modifies the galactic camera globals at 0xB1599C (zoom angle) and 0xB159A4 (distance). The GalacticCameraClass::Update interpolates to these values.", + "confidence": "DISCOVERED" + }, + "Rotate_Camera_By": { + "description": "Likely modifies the look direction globals at 0xB159AC/B0/B4 or applies a rotation to the GalacticCameraClass state.", + "confidence": "DISCOVERED" + }, + "Select_Object": { + "event_class": "SelectEventClass", + "event_type_id": 5, + "description": "Dispatched as a SelectEvent. Writes to GameModeManagerClass+0xB4." + } + }, + + "next_steps": [ + "RUNTIME VALIDATION: Attach CE with admin privileges or use the DLL bridge to resolve ASLR base and validate all offsets at runtime.", + "SELECTION VECTOR: Use runtime breakpoints on FUN_14028d930 to trace the exact selection list storage within GameModeClass. The DynamicVector> at GameModeClass+0x1D8 is the most likely candidate.", + "LAND/SPACE CAMERAS: LandModeClass and SpaceModeClass have their own camera controllers (not GalacticCameraClass). Decompile their update functions to map the tactical camera parameters.", + "FREE CAMERA: Implement free camera by: (a) finding the call to GalacticCameraClass::Update, (b) NOPing it, (c) writing the camera_state sub-object matrix directly via the DLL bridge.", + "SCREEN-TO-WORLD: The inverse view-projection matrix at camera_state+0x84 (or the engine param at RVA 0x1ADD70) provides the camera-to-world transform. Multiply a screen-space ray by this inverse to get world coordinates." + ] +} diff --git a/re-findings/combat_system.json b/re-findings/combat_system.json new file mode 100644 index 000000000..a4003b495 --- /dev/null +++ b/re-findings/combat_system.json @@ -0,0 +1,504 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "Alamo Engine Combat System — Complete Damage Pipeline", + "description": "Full mapping of the SWFOC damage pipeline from weapon fire to unit death. Derived from Ghidra static analysis of StarWarsG.exe (x86_64). All addresses are Ghidra VA = imagebase 0x140000000 + RVA.", + "_meta": { + "analysis_date": "2026-04-04", + "binary": "StarWarsG.exe", + "architecture": "x86_64", + "imagebase": "0x140000000", + "tools": ["Ghidra 12.0.3 MCP", "alamo_engine_kb.json cross-reference"], + "analyst": "Agent 3C (Combat System Deep Dive)" + }, + + "damage_pipeline_overview": { + "description": "The complete flow from a weapon firing to a unit dying. Every HP modification in the engine funnels through SetHP (0x3A89D0). The combat damage path specifically goes through a 5-stage pipeline.", + "stages": [ + { + "stage": 1, + "name": "Weapon Fire / Attack Command", + "description": "CombatantBehaviorClass dispatches attack orders. The targeting system (BaseCombatantClass vtable[34] SelectTarget) evaluates targets by range, health ratio, and weapon bitmask (target_type+0x1648). FUN_14038D730 is the master fire-control function: checks enemy status, invulnerability, LOS, game mode, and dispatches to hardpoint or direct weapon fire.", + "key_functions": { + "SelectTarget": {"rva": "0x3A97E0_vicinity", "vtable_slot": 34, "class": "BaseCombatantClass"}, + "FireControl_Dispatch": {"rva": "0x38D730", "ghidra_va": "0x14038D730"}, + "WeaponTick": {"rva": "0x387010", "ghidra_va": "0x140387010"}, + "HardpointFire": {"rva": "0x387F50", "ghidra_va": "0x140387F50"} + } + }, + { + "stage": 2, + "name": "Invulnerability Gate (Take_Damage_Outer)", + "description": "Top-level damage entry point. Contains 8 invulnerability checks (one per damage path / hardpoint slot). This is the ONLY function that checks the invulnerability flag at obj+0x3A7. Also checks flags at obj+0x3A0 bits 1 and 6, and container sub-flags at obj+0x381, 0x382, 0x388. If invulnerable, damage is silently discarded.", + "key_functions": { + "Take_Damage_Outer": {"rva": "0x38A350", "ghidra_va": "0x14038A350"} + }, + "invulnerability_checks": { + "primary_flag": "obj+0x3A7 (1 = invulnerable, 0 = vulnerable)", + "status_flags": "obj+0x3A0 bits 1,6 (0x42 mask — blocks damage when set)", + "container_flags": ["obj+0x381", "obj+0x382", "obj+0x388"], + "locomotor_check": "obj+0xA8 -> locomotor+0x2A8 (== 1 blocks damage)", + "note": "Only blocks damage routed through Take_Damage_Outer. Lua Set_Hull, health regen, script/ability HP sets all bypass this entirely." + } + }, + { + "stage": 3, + "name": "Damage Routing (Property Dispatch + Shield Absorption)", + "description": "Damage is routed based on object structure. Objects with hardpoints (obj+0x348 != 0xFF) route through the HardpointManager (QueryInterface 0x16). Objects with shields route through front-shield (QueryInterface 0x0F) or rear-shield (QueryInterface 0x10). Shield absorption reduces damage before it reaches hull HP.", + "key_functions": { + "Take_Damage_PropertyDispatch": {"rva": "0x3A97E0", "ghidra_va": "0x1403A97E0"}, + "SetFrontShield": {"rva": "0x3A8630", "ghidra_va": "0x1403A8630"}, + "SetRearShield": {"rva": "0x3A91E0", "ghidra_va": "0x1403A91E0"}, + "FrontShield_Write": {"rva": "0x56C1B0", "ghidra_va": "0x14056C1B0"}, + "FrontShield_Read": {"rva": "0x56BFB0", "ghidra_va": "0x14056BFB0"}, + "RearShield_Write": {"rva": "0x549810", "ghidra_va": "0x140549810"}, + "RearShield_Read": {"rva": "0x549490", "ghidra_va": "0x140549490"}, + "HardpointCount": {"rva": "0x405300", "ghidra_va": "0x140405300"}, + "GetHardpoint": {"rva": "0x4052D0", "ghidra_va": "0x1404052D0"}, + "DamageVisualLevel": {"rva": "0x3AC290", "ghidra_va": "0x1403AC290"}, + "AnimationDispatch": {"rva": "0x3A92F0", "ghidra_va": "0x1403A92F0"} + } + }, + { + "stage": 4, + "name": "HP Subtraction (Take_Damage_Impl)", + "description": "Core damage computation. Reads obj+0x5C (current HP), subtracts damage amount, calls SetHP. If prevent-death flag (obj+0x3A1 bit 7) is set and result would be <= 0, calls SetHP again with max(1.0, old_hp). If HP reaches 0, calls the death handler.", + "key_functions": { + "Take_Damage_Impl": {"rva": "0x3AB890", "ghidra_va": "0x1403AB890"} + }, + "pseudocode": "bool Take_Damage_Impl(GameObjectClass* obj, int damage_type, float damage, attacker_ref, ...) {\n float old_hp = obj->hp; // obj+0x5C\n SetHP(obj, old_hp - damage);\n \n if (obj->hp <= 0.0) {\n if (obj->prevent_death_flags_3A1 & 0x80) {\n float safe = max(1.0f, old_hp);\n SetHP(obj, safe);\n }\n if (obj->hp <= 0.0) {\n OnDeath(obj, damage_type, attacker_ref, ...);\n return false; // unit is dead\n }\n }\n \n // Check damage visual thresholds\n float max_hp = GetMaxHealth(obj->type_ptr);\n float threshold1 = max_hp * g_damage_threshold_1; // global at 0x803514\n float threshold2 = max_hp * g_damage_threshold_2; // global at 0x8007C0\n if ((old_hp > threshold1 && obj->hp <= threshold1) ||\n (old_hp > threshold2 && obj->hp <= threshold2)) {\n PlayDamageModel(obj); // switch to damaged art model\n }\n \n UpdateDamageVisualLevel(obj);\n return true; // unit survived\n}" + }, + { + "stage": 5, + "name": "SetHP (Canonical HP Write)", + "description": "THE single HP write function. Every hitpoint change in the engine flows through this. Clamps to [0.0, max_hp], sets dirty flag, logs death events.", + "key_functions": { + "SetHP": {"rva": "0x3A89D0", "ghidra_va": "0x1403A89D0"} + }, + "pseudocode": "float SetHP(GameObjectClass* obj, float new_hp) {\n float old_hp = obj->hp; // +0x5C\n float clamped = max(0.0f, new_hp);\n obj->hp = clamped;\n float max_hp = GetMaxHealth(obj->type_ptr); // calls 0x3727A0\n float final_hp = min(max_hp, max(0.0f, obj->hp));\n obj->hp = final_hp;\n if (old_hp != final_hp && obj->object_id == g_tracked_object_id)\n obj->change_notification_flag |= 1; // +0x3A0\n if (final_hp < 0.0f)\n LogDeath(type_name, obj_id, player_name, final_hp, new_hp);\n return final_hp;\n}" + } + ] + }, + + "death_sequence": { + "description": "When HP reaches 0.0 in Take_Damage_Impl, the death handler FUN_14039BDB0 is called. This is a complex function that handles: last-hit attribution, death animation selection, debris spawning, kill events, and object destruction.", + "handler_function": {"rva": "0x39BDB0", "ghidra_va": "0x14039BDB0"}, + "sequence": [ + "1. Set death flag at obj+0x130 (OR with 0x40)", + "2. Record last-hit player (attacker's player ID stored at obj+0x110)", + "3. Signal death to behavior system (FUN_14038EB10)", + "4. If debug mode: call FUN_140392600 for death logging", + "5. Notify AffectedByShield if present (obj+0x123 check, calls FUN_1405031A0)", + "6. Choose death animation: query GameObjectType for animation index (0x20 = special, iterate up to 2)", + "7. If death debris configured (type+0x23E8 or transport with units), spawn wreckage (FUN_1404D07E0)", + "8. Broadcast death event to the event system (event type 0x25)", + "9. Recursive: if object has child hardpoints (via obj+0x70 linked list), call death on children", + "10. Determine spawn location for debris/explosion via QueryInterface(4), QI(5), or QI(0x16)", + "11. Push death record to global death queue (DAT_140B15418+0x42)", + "12. Destroy the object's visual representation (FUN_140265AE0)", + "13. Iterate sub-weapons: fire each weapon that is flagged as death-fire", + "14. If hero unit: trigger hero respawn logic (ScheduleHeroRespawn at 0x48EB10)", + "15. Select death model/animation based on damage type (normal vs fire vs special)", + "16. Spawn death SFX and debris object at position", + "17. If local player's unit: broadcast UI death notification" + ] + }, + + "shield_system": { + "description": "Shields are separate from hull HP. Each unit can have a front shield (QueryInterface 0x0F -> BaseShieldBehaviorClass) and a rear shield (QueryInterface 0x10 -> ShieldBehaviorClass). Shield values are stored at obj+0xF0 sub-object.", + "front_shield": { + "query_interface_id": "0x0F (15)", + "behavior_class": "BaseShieldBehaviorClass", + "vtable_rva": "0x899458_vicinity", + "set_function": {"rva": "0x3A8630", "ghidra_va": "0x1403A8630"}, + "read_function": {"rva": "0x3963C0", "ghidra_va": "0x1403963C0"}, + "shield_write_impl": {"rva": "0x56C1B0", "ghidra_va": "0x14056C1B0"}, + "shield_read_impl": {"rva": "0x56BFB0", "ghidra_va": "0x14056BFB0"}, + "storage": { + "current_value": "[obj+0xF0]+0xF8 (float32)", + "description": "Front shield current HP. Clamped to [0, max_front_shield]." + }, + "max_value_function": {"rva": "0x372320", "ghidra_va": "0x140372320"}, + "max_value_source": "GameObjectType+0xDD0 (base) * difficulty_multiplier * ability_modifiers" + }, + "rear_shield": { + "query_interface_id": "0x10 (16)", + "behavior_class": "ShieldBehaviorClass", + "vtable_rva": "0x899458", + "set_function": {"rva": "0x3A91E0", "ghidra_va": "0x1403A91E0"}, + "read_function": {"rva": "0x396420_vicinity", "ghidra_va": "0x140396420"}, + "shield_write_impl": {"rva": "0x549810", "ghidra_va": "0x140549810"}, + "shield_read_impl": {"rva": "0x549490", "ghidra_va": "0x140549490"}, + "storage": { + "current_value": "[obj+0xF0]+0xFC (float32)", + "description": "Rear shield current HP." + }, + "max_value_function": {"rva": "0x3725F0", "ghidra_va": "0x1403725F0"}, + "max_value_source": "GameObjectType+0xDD4 (base) * difficulty_multiplier * ability_modifiers" + }, + "shield_depletion_triggers_hull_damage": { + "description": "When SetRearShield (0x3A91E0) sets shield to 0 and FUN_14039B950 returns true, it calls Take_Damage_Outer (0x38A350) to begin hull damage. Same for SetFrontShield through BaseShieldBehavior. This means shields must be depleted before hull HP can be reduced through normal combat.", + "rear_shield_death_trigger": "FUN_140549810 at offset +0x88: calls FUN_14038A350 when shield <= 0 and FUN_14039B950 returns true" + }, + "shield_states": { + "active": "shield_state = 1 (shields up, absorbing damage)", + "depleted": "shield_state = 2 (shields down, hull takes damage directly)", + "transition": "State change triggers visual/SFX events via FUN_140549CD0 and event broadcast" + }, + "difficulty_multipliers": { + "description": "Both GetMaxHealth and GetMaxShield apply game-mode-dependent multipliers. Mode 1 (Easy?) uses global at 0xB16DCC, Mode 2 (Hard?) uses 0xB16DC8. Additionally, player-specific multipliers from PlayerObject+0x360 sub-object are applied.", + "easy_multiplier_global": "0xB16DCC", + "hard_multiplier_global": "0xB16DC8", + "player_multiplier": { + "health": "PlayerObj+0x360 -> sub+0x50", + "front_shield": "PlayerObj+0x360 -> sub+0x54", + "rear_shield": "PlayerObj+0x360 -> sub+0x54" + } + } + }, + + "max_health_system": { + "description": "GetMaxHealth reads the base max HP from GameObjectType, then applies difficulty multipliers and ability modifiers.", + "function": {"rva": "0x3727A0", "ghidra_va": "0x1403727A0"}, + "formula": "max_hp = type->base_max_hp(+0xDCC) * difficulty_multiplier * player_multiplier * (1.0 + ability_bonus)", + "base_hp_offset": "GameObjectType+0xDCC (float32)", + "pseudocode": "float GetMaxHealth(GameObjectType* type, GameObjectClass* obj) {\n float base = type->max_hp_dcc; // +0xDCC\n \n // Apply difficulty multiplier\n int game_mode = GetGameModeType();\n if (game_mode == 1) // Easy/Land?\n base *= g_easy_hp_mult; // DAT_140B16DCC\n else if (game_mode == 2) // Hard/Space?\n base *= g_hard_hp_mult; // DAT_140B16DC8\n \n // Apply player-specific multiplier\n if (obj) {\n PlayerObject* player = FindPlayerByID(obj->owner_id);\n if (player && player->faction_sub_360) {\n void* sub = GetFactionSub(player->faction_sub_360);\n base *= *(float*)(sub + 0x50); // health multiplier\n }\n \n // Apply ability modifiers (Plus + GreaterThan template)\n if (HasAbilityModifiers(obj)) {\n float bonus = ComputeAbilityBonus(obj); // FUN_14033FB70\n base *= (bonus + 1.0f);\n }\n }\n return base;\n}" + }, + + "hardpoint_system": { + "description": "Objects with hardpoints (obj+0x348 != 0xFF) route damage through the HardpointManager. The HardpointManager (QueryInterface 0x16) maintains an array of HardPointClass instances. Each hardpoint has its own HP, shields, and can be independently destroyed.", + "detection_flag": "obj+0x348 (0xFF = no hardpoints / direct HP, other = has hardpoints)", + "manager_query": "QueryInterface(0x16) -> HardpointManager", + "count_function": {"rva": "0x405300", "ghidra_va": "0x140405300", "reads": "[manager+0x28]+0x110 -> +0x18 (int count)"}, + "get_hardpoint": {"rva": "0x4052D0", "ghidra_va": "0x1404052D0"}, + "hull_ratio_via_hardpoints": { + "function": {"rva": "0x405230", "ghidra_va": "0x140405230"}, + "description": "Iterates all hardpoints and calls GetHullPercentage (0x396DF0) on each. Used for aggregate health display." + }, + "hardpoint_damage_routing": "When Take_Damage_PropertyDispatch detects obj+0x348 != 0xFF, it routes damage to individual hardpoints rather than the parent object's HP directly. The HardpointFire function (0x387F50) handles per-hardpoint HP reduction and station level loss notifications.", + "station_level_loss": "When a hardpoint's HP reaches 0, FUN_140387F50 checks for space station level changes and broadcasts TEXT_ENEMY_SPACE_STATION_LEVEL_LOST or TEXT_FRIENDLY_SPACE_STATION_LEVEL_LOST." + }, + + "weapon_fire_system": { + "description": "The weapon firing system is driven by CombatantBehaviorClass and its children (BaseCombatantClass, CompanyCombatantClass, SquadronCombatantClass). The fire control dispatch at 0x38D730 manages target acquisition, invulnerability pre-checks, weapon selection, and projectile spawning.", + "fire_control_dispatch": { + "rva": "0x38D730", + "ghidra_va": "0x14038D730", + "description": "Master fire control function. Checks: (1) target is enemy via FUN_1402824D0, (2) target is not invulnerable (obj+0x3A7), (3) target is not paused/immune (obj+0x3A0 & 0x42), (4) target's locomotor allows damage, (5) weapon range, (6) LOS check, (7) game mode constraints. Then dispatches to appropriate fire behavior.", + "invulnerability_precheck": "if (target+0x3A7 == 1) return 0; // target invulnerable, skip", + "status_precheck": "if (target+0x3A0 & 0x42) return 0; // target has immunity flags" + }, + "weapon_tick": { + "rva": "0x387010", + "ghidra_va": "0x140387010", + "description": "Per-frame weapon update. Checks weapon state, manages firing cooldown via delta-time accumulation (param_1+0x60), resolves fire animation model by name, and calls the actual fire function.", + "cooldown_mechanism": "delta_ticks = current_tick - last_fire_tick (param_1+0x60). When cooldown expires, calls fire via FUN_140381FF0.", + "fire_animation": "Resolves model name from weapon data (type+0x1C0 SSO string), loads via asset system, and plays fire animation." + }, + "hardpoint_fire": { + "rva": "0x387F50", + "ghidra_va": "0x140387F50", + "description": "Per-hardpoint damage application. Subtracts damage from hardpoint HP (param_1+0x28), manages hardpoint-level death (station level loss), and fires death-on-destruction effects.", + "hp_field": "param_1+0x28 (float, current hardpoint HP)", + "fire_rate": "Managed by weapon tick delta-time accumulation" + }, + "combatant_classes": { + "CombatantBehaviorClass": {"rva_ctor": "0x6383E0", "ghidra_va": "0x1406383E0", "vtable_count": "55 virtual methods (in BaseCombatantClass)"}, + "BaseCombatantClass": {"rva_ctor": "0x6CB700", "ghidra_va": "0x1406CB700", "vtable_rva": "0x8BF6C0"}, + "CompanyCombatantClass": {"rva_ctor": "0x6CBEE0", "ghidra_va": "0x1406CBEE0"}, + "SquadronCombatantClass": {"rva_ctor": "0x6CBA40", "ghidra_va": "0x1406CBA40"} + }, + "target_selection": { + "description": "BaseCombatantClass vtable[34] (SelectTarget) evaluates targets by range, health ratio, and weapon type bitmask. The weapon bitmask is stored at target_type+0x1648.", + "weapon_bitmask_offset": "GameObjectType+0x1648", + "factors": ["distance_to_target", "target_health_ratio (vtable[31] GetHealthRatio)", "weapon_type_compatibility"] + } + }, + + "damage_calculation_formula": { + "description": "The actual damage amount is computed before reaching Take_Damage_Impl. The formula incorporates base weapon damage, damage type modifiers, ability bonuses, and game speed scaling.", + "base_damage": "Defined in XML per weapon/projectile type", + "modifiers": { + "game_speed_scaling": "(float)DAT_140B0A320 / (float)DAT_140B0A340 — applied in AnimationDispatch (0x3A92F0)", + "berserker_check": "FUN_1403A92F0 checks BerserkerAbilityClass RTTI. If berserker active, damage type forced to 0x68 and param_7 set to 1.", + "combat_bonus_ability": { + "class": "CombatBonusAbilityClass", + "address": "0x1406F42D0", + "description": "Modifies 8 separate combat stats via FUN_14038C850: (1) max HP bonus, (2) damage bonus, (3) front shield bonus, (4) rear shield bonus, (5) rate modifier, (6) range modifier, (7) accuracy modifier, (8) additional modifier. After modifying max_hp, scales current HP proportionally: SetHP(obj, (new_max - old_max) + current_hp).", + "stat_offsets_in_ability_data": { + "hp_bonus": "+0x00", + "damage_bonus": "+0x04", + "front_shield_bonus": "+0x08", + "rear_shield_bonus": "+0x0C", + "rate_modifier": "+0x10", + "range_modifier": "+0x14", + "accuracy_modifier": "+0x18", + "additional": "+0x1C" + } + }, + "absorb_blaster_ability": { + "class": "AbsorbBlasterAbilityClass", + "address": "0x1406EE840", + "description": "Converts incoming damage to healing. Reads damage type from target type+0x1FF4. If compatible (damage_type & ~0x3 != 0), converts damage to HP: SetHP(obj, current_hp + type->heal_factor(+0x474) * ability_factor + flat_bonus). Only works against specific damage types.", + "heal_formula": "new_hp = current_hp + (type+0x474 * ability+0x04) + ability+0x08" + } + } + }, + + "damage_visual_thresholds": { + "description": "Take_Damage_Impl checks two global damage thresholds to trigger visual model changes (switching from pristine to damaged to heavily damaged art).", + "threshold_1": { + "global_rva": "0x803514", + "description": "First damage threshold multiplier (applied to max_hp). When HP crosses below max_hp * threshold, switch to 'damaged' model." + }, + "threshold_2": { + "global_rva": "0x8007C0", + "description": "Second damage threshold multiplier. When HP crosses below max_hp * threshold, switch to 'heavily damaged' model." + }, + "visual_level_function": {"rva": "0x3AC290", "ghidra_va": "0x1403AC290"}, + "visual_level_logic": "Iterates an array at type+0xEB0 (float thresholds) paired with type+0xEC8 (animation indices). Compares health_ratio against each threshold and selects the highest matching visual level." + }, + + "prevent_death_system": { + "description": "The prevent-death flag at obj+0x3A1 bit 7 (mask 0x80) prevents a unit from dying through normal combat damage. When set, Take_Damage_Impl clamps HP to max(1.0, old_hp) instead of letting it reach 0. This is the 'unkillable but damageable' mode used by Set_Cannot_Be_Killed(true) in Lua.", + "flag_offset": "obj+0x3A1", + "flag_mask": "0x80 (bit 7)", + "implementation": "In Take_Damage_Impl: if (obj->hp <= 0 && (obj->flags_3A1 & 0x80)) { SetHP(obj, max(1.0f, old_hp)); }", + "note": "Distinct from full invulnerability (obj+0x3A7). Prevent-death still allows damage; it just prevents the final kill." + }, + + "invulnerability_system": { + "description": "Full invulnerability is a multi-layered system. The primary flag at obj+0x3A7 is checked ONLY by Take_Damage_Outer (0x38A350). Additional immunity flags at obj+0x3A0 bits 1 and 6 are checked by FireControl_Dispatch (0x38D730). Invulnerability is set through the Lua wrapper Make_Invulnerable at 0x14057D550, which creates INVULNERABLE behavior via the behavior system.", + "ida_correction": "IDA Hex-Rays correction 2026-04-05. The Lua wrapper Make_Invulnerable is at 0x14057D550 (confirmed by IDA). The actual C++ invulnerability behavior creation is handled through the behavior system at sub_14038C570, not through a single setter function.", + "primary_flag": "obj+0x3A7 (checked by Take_Damage_Outer)", + "status_flags": "obj+0x3A0 & 0x42 (checked by FireControl_Dispatch — prevents targeting entirely)", + "container_flags": ["obj+0x381", "obj+0x382", "obj+0x388"], + "setter_function": {"rva": "0x3ABB80", "ghidra_va": "0x1403ABB80", "status": "DENIED as invulnerability setter", "actual_identity": "SetPosition/Teleport with hardpoint propagation via QI(0x16)"}, + "lua_wrapper": {"rva": "0x57D550", "ghidra_va": "0x14057D550", "name": "Make_Invulnerable", "status": "CONFIRMED-RE", "note": "IDA Hex-Rays verified. This is the real entry point for invulnerability. No separate C++ setter exists -- the Lua wrapper IS the setter."}, + "behavior_attach": {"rva": "0x38C570", "ghidra_va": "0x14038C570", "status": "CONFIRMED-RE", "note": "Attaches INVULNERABLE behavior object to game object"}, + "behavior_remove": {"rva": "0x3A54C0", "ghidra_va": "0x1403A54C0", "status": "CONFIRMED-RE", "note": "Removes behavior from game object via QueryInterface(75)"}, + "hardpoint_get": {"rva": "0x4052D0", "ghidra_va": "0x1404052D0", "status": "CONFIRMED-RE", "note": "Gets hardpoint by index from manager"}, + "hardpoint_count": {"rva": "0x405300", "ghidra_va": "0x140405300", "status": "CONFIRMED-RE", "note": "Returns hardpoint count from manager"}, + "behavior_name_lookup": {"function": "sub_1404C3520", "note": "Looks up INVULNERABLE string in behavior registry at off_140A2AC90"}, + "cleanup_function": {"rva": "0x3A56B0", "ghidra_va": "0x1403A56B0"}, + "lua_binding": {"rva": "0x5819E0", "ghidra_va": "0x1405819E0"}, + "call_chain": [ + "1. Lua wrapper at 0x14057D550 validates argument (must be boolean, tactical mode only)", + "2. Gets wrapped C++ object from a1+96", + "3. Checks if object has hardpoints (obj+840 == 0xFF)", + "4a. IF no hardpoints AND enabling: creates INVULNERABLE behavior via sub_14038C570(obj, behaviorObj, 0)", + "4b. IF no hardpoints AND disabling: removes behavior via QueryInterface(obj, 75) then sub_1403A54C0", + "5a. IF has hardpoints: gets hardpoint manager via QueryInterface(obj, 22)", + "5b. Gets count via sub_140405300(manager), iterates 0..count-1", + "5c. Gets each hardpoint via sub_1404052D0(manager, index)", + "5d. Applies same enable/disable logic to each hardpoint individually", + "6. CONCLUSION: No single C++ setter function. The Lua wrapper is the setter. Hardpoint propagation is confirmed." + ], + "correction_history": [ + "Phase 2: 0x3ABB80 labeled Make_Invulnerable_Setter (WRONG)", + "2026-04-05 IDA: 0x3ABB80 is SetPosition/Teleport, not invulnerability", + "2026-04-05 IDA: Real call chain traced from Lua wrapper 0x57D550 through behavior system" + ] + }, + + "health_regeneration": { + "description": "Two regeneration systems exist: natural health regen and shield-linked regen. Both call SetHP directly, bypassing invulnerability checks.", + "natural_regen": { + "function_rva": "0x5D70F0", + "ghidra_va": "0x1405D70F0", + "description": "Periodic health regeneration. First call sets HP to 1.0 (revival?), subsequent ticks add regen_rate to current HP, clamped to max_hp.", + "formula": "new_hp = min(current_hp + regen_rate, max_hp)" + }, + "shield_linked_regen": { + "function_rva": "0x387010", + "description": "Shield regeneration ticks also affect hull HP via: new_hp = current_hp * ratio (from GetHullPercentage)." + }, + "periodic_heal": { + "function_rva": "0x71B560", + "ghidra_va": "0x14071B560", + "description": "DrainLifeAbilityClass periodic heal. Adds heal_amount to current HP via SetHP.", + "formula": "new_hp = current_hp + heal_amount" + }, + "periodic_dot": { + "function_rva": "0x6F80D0", + "ghidra_va": "0x1406F80D0", + "description": "Damage-over-time periodic ticks (burning, ion damage, etc.). Adds damage_amount to HP (negative for damage, positive for heal).", + "formula": "new_hp = current_hp + damage_amount" + } + }, + + "special_attack_abilities": { + "description": "91 SpecialAbilityClass children provide combat modifiers. Key attack abilities:", + "abilities": { + "ConcentrateFireAttackAbilityClass": {"rva_ctor": "0x6F52C0", "ghidra_va": "0x1406F52C0", "description": "Focus-fire ability. Increases damage against a single target."}, + "MaximumFirepowerAttackAbilityClass": {"rva_ctor": "0x706B10", "ghidra_va": "0x140706B10", "description": "Maximum firepower mode. Increases rate of fire and damage."}, + "LuckyShotAttackAbilityClass": {"rva_ctor": "0x706280", "ghidra_va": "0x140706280", "description": "Lucky shot chance. Random critical hit bonus."}, + "EnergyWeaponAttackAbilityClass": {"rva_ctor": "0x6F9980", "ghidra_va": "0x1406F9980", "description": "Energy weapon special attack."}, + "IonCannonShotAttackAbilityClass": {"rva_ctor": "0x7048E0", "ghidra_va": "0x1407048E0", "description": "Ion cannon single-shot ability."}, + "EatAttackAbilityClass": {"rva_ctor": "0x6F7A30", "ghidra_va": "0x1406F7A30", "description": "Eat/consume attack (e.g., Sarlacc). Calls damage dispatch with type 0x68, destroys target."}, + "ArcSweepAttackAbilityClass": {"rva_ctor": "0x6EEAE0", "ghidra_va": "0x1406EEAE0", "description": "Area sweep attack."}, + "CableAttackAbilityClass": {"rva_ctor": "0x6F22F0", "ghidra_va": "0x1406F22F0", "description": "Tow cable attack (AT-AT takedown)."}, + "EarthquakeAttackAbilityClass": {"rva_ctor": "0x6F7170", "ghidra_va": "0x1406F7170", "description": "Ground-shake AoE attack."}, + "TractorBeamAttackAbilityClass": {"rva_ctor": "0x710080", "ghidra_va": "0x140710080", "description": "Tractor beam immobilize + damage."}, + "GenericAttackAbilityClass": {"rva_ctor": "0x6FFFF0", "ghidra_va": "0x1406FFFF0", "description": "Generic configurable attack ability."}, + "BerserkerAbilityClass": {"rva_ctor": "0x712D20", "ghidra_va": "0x140712D20", "description": "Berserker mode. Forces damage type to 0x68, overrides normal attack behavior."}, + "AbsorbBlasterAbilityClass": {"rva": "0x6EE840", "ghidra_va": "0x1406EE840", "description": "Converts incoming blaster damage to healing."}, + "CombatBonusAbilityClass": {"rva": "0x6F42D0", "ghidra_va": "0x1406F42D0", "description": "Multi-stat combat bonus: HP, damage, shields (front+rear), rate, range, accuracy."}, + "LeechShieldsAbilityClass": {"rva_ctor": "0x717530", "ghidra_va": "0x140717530", "description": "Steals shield points from target."}, + "ShieldFlareAbilityClass": {"rva_ctor": "0x71C820", "ghidra_va": "0x14071C820", "description": "Shield disruption/flare effect."}, + "DrainLifeAbilityClass": {"rva": "0x71B560", "ghidra_va": "0x14071B560", "description": "Drains HP from target and heals caster."} + }, + "vtable_structure": { + "vfunc_0": "destructor", + "vfunc_2": "Activate (pure virtual — each ability overrides)", + "vfunc_17": "RestoreEffect", + "vfunc_18": "CanActivate (checks game mode scope)", + "vfunc_19": "ApplyEffect", + "vfunc_29": "OnTick (periodic update — used by DoT, heals)", + "vfunc_31": "OnApply (initial application — used by CombatBonus, AbsorbBlaster)", + "vfunc_32": "OnHit (reactionary — used by EatAttack)" + } + }, + + "projectile_system": { + "description": "Projectiles are GameObjectClass instances with ProjectileBehaviorClass attached. They are spawned by the weapon fire system and carry damage data to the target.", + "behavior_class": { + "name": "ProjectileBehaviorClass", + "vtable_rva": "0x878D58", + "virtual_methods": 33, + "inherits": ["BehaviorClass"] + }, + "data_pack": { + "name": "ProjectileDataPackClass", + "rva_ctor": "0x55B4D0", + "ghidra_va": "0x14055B4D0" + }, + "projectile_spawn": "Spawned via FUN_14029F810 (general object spawn). Projectile carries damage amount, damage type, owner reference.", + "projectile_hit": "On collision, projectile triggers damage application through the Take_Damage_Outer pipeline on the target." + }, + + "key_struct_offsets_for_combat": { + "GameObjectClass": { + "vtable_ptr": "+0x00 (8 bytes, pointer)", + "object_id": "+0x50 (4 bytes, int32)", + "owner_player_id": "+0x58 (4 bytes, int32)", + "hp": "+0x5C (4 bytes, float32) — THE hitpoints field", + "locomotor_ptr": "+0xA8 (8 bytes, pointer)", + "shield_sub_object": "+0xF0 (8 bytes, pointer) — front shield at [ptr+0xF8], rear shield at [ptr+0xFC]", + "health_sub_object_ptr": "+0x118 (8 bytes, pointer)", + "component_array_ptr": "+0x278 (8 bytes, pointer)", + "game_object_type_ptr": "+0x298 (8 bytes, pointer)", + "animation_controller_ptr": "+0x2A0 (8 bytes, pointer)", + "container_ref": "+0x2B0 (8 bytes, pointer)", + "squad_unit_list": "+0x2D0 (8 bytes, pointer)", + "component_lookup_table": "+0x332 (byte array)", + "parent_component_index": "+0x335 (1 byte)", + "front_shield_present": "+0x341 (1 byte, 0xFF = none)", + "rear_shield_present": "+0x342 (1 byte, 0xFF = none)", + "direct_hp_path_flag": "+0x348 (1 byte, 0xFF = direct HP, other = hardpoints)", + "change_notification_flag": "+0x3A0 (1 byte, bit 0 = HP changed, bits 1+6 = immunity)", + "prevent_death_flags": "+0x3A1 (1 byte, bit 7 = prevent death)", + "invulnerability_flag": "+0x3A7 (1 byte, 1 = invulnerable)" + }, + "GameObjectType": { + "type_name": "+0xF8 (SSO string, 32 bytes)", + "base_max_hp": "+0xDCC (float32)", + "base_max_front_shield": "+0xDD0 (float32)", + "base_max_rear_shield": "+0xDD4 (float32)", + "damage_threshold_array": "+0xEB0 (pointer to float array)", + "damage_threshold_count": "+0xEB8 (int32)", + "damage_anim_index_array": "+0xEC8 (pointer to int array)", + "damage_anim_threshold_array": "+0xED8 (pointer to 0x20-stride array)", + "weapon_bitmask": "+0x1648 (for targeting compatibility)", + "death_debris_count": "+0xD48 (int32)", + "death_debris_list": "+0xD40 (pointer)" + } + }, + + "all_sethp_callers": { + "description": "All 18 known code paths that modify HP through SetHP. Understanding these is critical for any HP-related mod.", + "callers": [ + {"rva": "0x29F5B5", "function_rva": "0x29F270", "category": "death_cleanup", "description": "Death and cleanup handlers"}, + {"rva": "0x38738B", "function_rva": "0x387010", "category": "shield_health_regen", "description": "Shield regen tick: HP * ratio"}, + {"rva": "0x3A0A90", "function_rva": "0x3A06A0", "category": "property_init", "description": "Initial HP from XML data system"}, + {"rva": "0x3AB0B0", "function_rva": "0x3A97E0", "category": "take_damage_dispatch", "description": "Property dispatch path with hardpoint post-check"}, + {"rva": "0x3AB8C6", "function_rva": "0x3AB890", "category": "take_damage_impl", "description": "Core damage: old_hp - damage"}, + {"rva": "0x3AB8EC", "function_rva": "0x3AB890", "category": "take_damage_impl_prevent_death", "description": "Prevent-death clamp: max(1.0, old_hp)"}, + {"rva": "0x42DD63", "function_rva": "0x42DBD0", "category": "lua_set_hull", "description": "Lua Set_Hull direct write"}, + {"rva": "0x4B0179", "function_rva": "0x4AFBD0", "category": "spawn_hp_scaling", "description": "Spawn proportional rescale: old_hp * (new_max/old_max)"}, + {"rva": "0x4B0E99", "function_rva": "0x4B0DC0", "category": "spawn_hp_scaling_variant", "description": "Spawn rescale variant"}, + {"rva": "0x4CBA8B", "function_rva": "0x4CB6B0", "category": "behavior_attachment", "description": "HP set during behavior attach"}, + {"rva": "0x5D7129", "function_rva": "0x5D70F0", "category": "health_regen", "description": "Natural regen: set to 1.0 (revival)"}, + {"rva": "0x5D73FC", "function_rva": "0x5D70F0", "category": "health_regen_tick", "description": "Regen tick: min(current+rate, max)"}, + {"rva": "0x6EE8EE", "function_rva": "0x6EE840", "category": "absorb_blaster", "description": "AbsorbBlaster ability heal"}, + {"rva": "0x6F4444", "function_rva": "0x6F42D0", "category": "combat_bonus_hp_adjust", "description": "CombatBonus HP scaling after max_hp change"}, + {"rva": "0x6F4DA2", "function_rva": "0x6F4CC0", "category": "combat_bonus_deactivate", "description": "CombatBonus deactivation HP restore"}, + {"rva": "0x6F8791", "function_rva": "0x6F80D0", "category": "periodic_dot", "description": "DoT tick: current_hp + damage_amount"}, + {"rva": "0x6FBAD1", "function_rva": "0x6FB850", "category": "combat_event", "description": "Combat event handler"}, + {"rva": "0x71B7BA", "function_rva": "0x71B560", "category": "periodic_heal_tick", "description": "DrainLife heal: current_hp + heal_amount"} + ] + }, + + "damage_type_system": { + "description": "Damage types are integer IDs used throughout the combat system. The AnimationDispatch function (0x3A92F0) uses damage type to select animation and handle Berserker override. Specific type IDs observed:", + "known_types": { + "0x00": "Default/Normal (triggers default path in AnimationDispatch)", + "0x05": "Special type 1 (falls through to default in AnimationDispatch switch)", + "0x06": "Special type 2 (triggers special ability search in AnimationDispatch — BerserkerAbilityClass check)", + "0x08": "Fire/Burn damage (death handler checks: selects death anim variant 0x0C)", + "0x68": "Berserker/Eat forced type (overrides all other types when Berserker active, or used by EatAttack)", + "0x74": "Construction/Build type (used by tactical building construction)" + }, + "type_resolved_by": "AnimationDispatch (0x3A92F0) uses FUN_140264A40 to query animation count for the given type, then FUN_1401FFB40 for random selection within count.", + "berserker_override": "When AnimationDispatch detects the unit is in active fire-while-queued state (obj+0x100 -> +0x2CC != 0), it checks if current command is 0x68. If not, it iterates the ability list, dynamic_casts to BerserkerAbilityClass (RTTI), and if found, forces damage_type = 0x68." + }, + + "game_speed_scaling": { + "description": "Combat calculations are scaled by game speed to maintain consistent damage-per-second regardless of game speed setting.", + "numerator_global": "DAT_140B0A320 (current frame counter or time)", + "denominator_global": "DAT_140B0A340 (reference time base)", + "formula": "time_scale = (float)DAT_140B0A320 / (float)DAT_140B0A340", + "applied_in": "AnimationDispatch (0x3A92F0) passes time_scale to FUN_140265560 and FUN_140265490" + }, + + "modding_interception_points": { + "description": "Recommended hooking points for combat mods, ordered by specificity.", + "hooks": [ + { + "name": "SetHP (universal HP intercept)", + "rva": "0x3A89D0", + "aob": "40 53 48 83 EC 60 0F 29 74 24 50 0F 57 C0 F3 0F 10 71 5C", + "params": "RCX=GameObjectClass*, XMM1=new_hp (float32)", + "use_case": "Intercept ALL HP changes. Filter by caller category for selective mods." + }, + { + "name": "Take_Damage_Outer (combat damage gate)", + "rva": "0x38A350", + "params": "RCX=GameObjectClass*, XMM1=damage_amount", + "use_case": "Hook combat-only damage. Invulnerability bypass, damage multipliers." + }, + { + "name": "Take_Damage_Impl (damage subtraction)", + "rva": "0x3AB890", + "params": "RCX=obj, param_2=damage_type, XMM1(param_3)=damage_amount, param_4=attacker_ref", + "use_case": "Modify damage amount just before HP subtraction. Armor/resistance mods." + }, + { + "name": "GetMaxHealth (max HP override)", + "rva": "0x3727A0", + "params": "RCX=GameObjectType*, RDX=GameObjectClass*", + "return": "XMM0=max_hp (float32)", + "use_case": "Modify max HP for units. Affects healing caps, shield scaling, visual thresholds." + }, + { + "name": "FireControl_Dispatch (weapon targeting)", + "rva": "0x38D730", + "use_case": "Modify targeting behavior, add custom damage types, force-attack allies." + }, + { + "name": "AnimationDispatch (damage events)", + "rva": "0x3A92F0", + "use_case": "Hook damage type routing, custom damage type handlers." + } + ] + } +} diff --git a/re-findings/cross_reference_analysis.md b/re-findings/cross_reference_analysis.md new file mode 100644 index 000000000..f3da02d30 --- /dev/null +++ b/re-findings/cross_reference_analysis.md @@ -0,0 +1,263 @@ +# SWFOC RE Cross-Reference Analysis +## Binary RE Findings vs Community Documentation + +Generated: 2026-04-04 +Sources: knowledge-base/alamo_engine_kb_v3.json, knowledge-base/lua_binding_map.json, archive/phase2_intermediates/signatures_phase2.json, re-findings/game_systems_map.json, knowledge-base/rtti_class_hierarchy.json, runtime CE MCP session + +--- + +## 1. What Our Binary RE Already Covers (No Community Docs Needed) + +### Struct Layouts (VERIFIED via live CE MCP) +| Struct | Fields Mapped | Live-Verified? | +|--------|--------------|----------------| +| GameObjectClass | 15 fields (vtable, object_id, owner, hp, component_array, parent_index, invuln, etc.) | YES — vtable 0x8661B8 confirmed, HP/owner read live | +| PlayerObject | 8 fields (player_id*, credits, max_credits, tech, max_tech, faction_ref) | YES — credits, tech, faction name all confirmed live | +| PlayerListClass | 2 fields (player_array, player_count) | YES — PlayerArray at RVA 0xA16FF0 confirmed live | +| GameObjectType | 3 fields (name SSO at +0xF8, sub_objects, type_name) | YES — type names read live (DARK_APPRENTICE, etc.) | +| LocomotorComponent | 2 fields (speed_override_flag +0x29C, speed_value +0x2A0) | Not yet live-tested | + +*PlayerObject+0x04 from KB was WRONG (returns garbage). Corrected offsets discovered via CE MCP: +- +0x37 = playable flag, +0x48 = slot index, +0x62 = local player flag, +0x68 = faction name ptr + +### Functions (14 with full RVAs and signatures) +| Function | RVA | Signature | AOB? | +|----------|-----|-----------|------| +| SetHP | 0x3A89D0 | void(GameObjectClass*, float) | YES (verified live) | +| QueryInterface | 0x395AC0 | void*(GameObjectClass*, int) | Pseudocode available | +| Get_Owner_Lua | 0x5792E0 | int(GameObjectWrapper*) | Pseudocode available | +| ResolveParentOwner | 0x3956C0 | void*(GameObjectClass*) | - | +| Change_Owner | 0x574D0E | void(GameObjectClass*, PlayerObject*) | - | +| AddCredits | 0x27F370 | void(PlayerObject*, float, bool) | YES | +| SetTechLevel | 0x288980 | int(PlayerObject*, int, bool, bool) | YES | +| SetSpeedOverride | 0x3A8C90 | void(GameObjectClass*, float) | YES | +| ClearSpeedOverride | 0x38F8B0 | void(GameObjectClass*) | YES | +| ScheduleHeroRespawn | 0x48EB10 | bool(hero, planet, delay, force) | - | +| Make_Invulnerable_Setter | 0x3ABB80 | void(GameObjectClass*, bool) | - | +| PlayerList_FindByID | 0x294BC0 | PlayerObject*(PlayerListClass*, int) | - | +| Take_Damage | 0x38A350 | void(GameObjectClass*, float, ...) | - | +| GetMaxHealth | 0x3A8B60 | float(GameObjectClass*) | - | + +### Lua Bindings (134 total from binary analysis) +- 26 wrapper classes identified with RTTI addresses +- 40 global Lua functions (Spawn_Unit, Find_Object_Type, Story_Event, etc.) +- 32 GameObjectWrapper methods (Get_Hull, Get_Owner, Change_Owner, Make_Invulnerable, etc.) +- 12 PlayerWrapper methods (Give_Money, Set_Tech_Level, Get_Credits, etc.) +- 10 TaskForce methods across 4 subtypes +- 20 trainer-critical bindings with full error strings and parameter counts + +### RTTI Recovery +- 2,919 classes, 1,992 vtables, 1,233 inheritance trees +- 16 subsystems mapped: combat (148 classes), AI (312), movement (130), abilities (91), economy (45), story (89), etc. +- Top hookable targets prioritized: LocomotorCommonClass (104 vfuncs), GameModeClass (89), CombatantBehaviorClass (55) + +--- + +## 2. What Community Docs CONFIRM (Validation) + +### Lua Function Names — Our Binary RE vs Community +Our binary analysis found 134 Lua bindings. The community (eaw-emmyluadoc, Focumentation) documents approximately 200+ Lua functions. Cross-referencing: + +**CONFIRMED by our binary RE (exact name match + RVA):** +- Spawn_Unit ✓ (RVA known, error strings found, backing class identified) +- Reinforce_Unit ✓ +- Find_Object_Type ✓ +- Find_First_Object ✓ +- Find_All_Objects_Of_Type ✓ +- Find_Nearest ✓ +- Find_Path ✓ +- Story_Event ✓ +- Get_Game_Mode ✓ +- Assemble_Fleet ✓ +- Create_Generic_Object ✓ +- Create_Position ✓ +- Add_Objective ✓ +- FindPlanet ✓ (global) +- FindTarget ✓ (global) +- Get_Hull ✓ +- Get_Health ✓ +- Get_Shield ✓ +- Get_Owner ✓ (RVA 0x5792E0 decompiled with pseudocode) +- Get_Type ✓ +- Get_Position ✓ +- Get_Distance ✓ +- Get_Parent_Object ✓ +- Get_Bone_Position ✓ +- Get_Attack_Target ✓ +- Has_Attack_Target ✓ +- Attack_Target ✓ +- Guard_Target ✓ +- Move_To ✓ +- Fire_Special_Weapon ✓ +- Activate_Ability ✓ +- Is_Ability_Active ✓ +- Has_Property ✓ +- Is_Category ✓ +- Make_Invulnerable ✓ (RVA 0x3ABB80 decompiled) +- Set_Cannot_Be_Killed ✓ +- Teleport ✓ +- Teleport_And_Face ✓ +- Override_Max_Speed ✓ (RVA 0x57E590) +- Change_Owner ✓ (RVA 0x574D0E decompiled) +- Cancel_Hyperspace ✓ +- Set_Selectable ✓ +- Prevent_AI_Usage ✓ +- Are_Engines_Online ✓ +- Give_Money ✓ (RVA 0x603130, calls AddCredits) +- Set_Tech_Level ✓ (RVA 0x604480, calls SetTechLevel) +- Get_Credits ✓ +- Unlock_Tech ✓ +- Lock_Tech ✓ +- Is_Enemy ✓ +- Is_Ally ✓ +- Release_Credits_For_Tactical ✓ +- Divert ✓ +- Play_SFX_Event ✓ +- Play_Music ✓ +- Stop_All_Music ✓ + +**Total confirmed: ~56 functions with exact name + RVA from binary** + +### Functions Community Docs Have That We DON'T (Gaps to Fill) + +Based on the Focumentation wiki and eaw-emmyluadoc, these are Lua functions documented by the community that we have NOT yet found RVAs for: + +**High Priority (trainer/helper-relevant) — from Imperialware + Community:** +- `Find_Player(faction_name)` — get player object by faction name string. CRITICAL for helper bridge. +- `SpawnList(unit_table, position, owner, allow_ai, delete_after)` — spawn MULTIPLE units at once. Better than Reinforce_Unit for batch spawns. +- `Object.Despawn()` — remove an object from the game. No RVA found yet. +- `Planet.Change_Owner(player)` — change planet ownership in GC. No RVA found yet. +- `Sleep(seconds)` — coroutine yield. Essential for helper scripts with delays. +- `TestValid(object)` — check if object ref is still alive. Prevents crashes. +- `Object.Is_Human()` — on PlayerWrapper, returns true for human player. CONFIRMS our +0x62 flag finding. +- `Object.Get_Enemy()` — on PlayerWrapper, returns enemy player list. +- `Set_Hull` — NOT in binary (confirmed). Hull must be set via SetHP at C++ level. +- `ReinforceList` — likely same as SpawnList? Community uses both names. + +**Medium Priority (abilities/unit control) — from Imperialware:** +- `Object.Has_Ability(ability_name)` — check if unit has specific ability +- `Object.Is_Ability_Ready(ability_name)` — check ability cooldown state +- `Object.Reset_Ability_Counter()` — reset ability cooldown +- `Object.Face_Immediate(target)` — instant rotation (Teleport_And_Face alternative) +- `Object.Hide(bool)` — show/hide object +- `Object.Get_Planet_Location()` — which planet an object is at (GC mode) +- `Hide_Sub_Object(unit, flag, sub_name)` — hide sub-objects (e.g., "Shield" mesh) +- `Create_Thread(func_name)` — Lua coroutine creation +- `GameRandom(min, max)` — random number +- `ScriptExit()` — terminate script + +**Low Priority (UI/debug):** +- `Add_Radar_Blip(pos, name)` / `Remove_Radar_Blip(name)` — radar markers +- `Object.Set_Check_Contested_Space(bool)` — toggle space contest check +- Various cinematic camera functions (we found most already) + +**KEY INSIGHT from Imperialware:** It has ZERO memory offsets. It's purely file-based (XML modding + Lua script injection, requires game restart). Our binary RE is the ONLY source of runtime memory offsets for this game. Imperialware's value is confirming Lua function names/signatures and demonstrating the spawning pattern. + +--- + +## 3. What Our RE Found That Community Docs DON'T Cover + +**These are UNIQUE contributions from our binary RE that aren't in any community source:** + +### Internal C++ Architecture +- **QueryInterface component system** — full pseudocode, 8 query types mapped, component_lookup_table at +0x332 +- **Parent/sub-object traversal chain** — [obj+0x335] → [obj+0x278] → parent, with deep nesting loop recipe +- **SetHP caller graph** — all 18 callers mapped, proving SetHP is the ONLY HP write path +- **RTTI class hierarchy** — 2,919 classes with inheritance trees, vtable RVAs, virtual method counts +- **LuaMemberFunctionWrapper registration mechanism** — how wrapper classes register methods on Lua userdata +- **Lua 5.0.2 C API wrapper RVAs** — lua_pushstring, lua_newtable, etc. compiled into the binary +- **lua_State* location candidates** — TheEvaluatorScriptManagerClass singleton at 0x5E77E0 + +### Memory Layout Details +- **PlayerObject+0x62** = local player flag (1 = YOU) — NOT in any community doc +- **PlayerObject+0x37** = playable faction flag — NOT in any community doc +- **PlayerObject+0x48** = actual slot index (corrects KB's wrong +0x04) — NOT in any community doc +- **GameObjectClass vtable RVA 0x8661B8** — exact vtable address for runtime object identification +- **Parent component index at +0x335** — sub-object ownership resolution shortcut +- **Container reference at +0x2B0** — invulnerability propagation chain + +### AOB Signatures +All AOB signatures in signatures_phase2.json are original binary RE work — not available anywhere else: +- SetHP prologue: `40 53 48 83 EC 60 0F 29 74 24 50 0F 57 C0 F3 0F 10 71 5C` +- AddCredits prologue: `48 89 5C 24 08 57 48 83 EC 40 0F 29 74 24 30 41 0F B6 F8...` +- SetSpeedOverride: `48 8B 81 A8 00 00 00 48 85 C0 74 ?? 89 90 A0 02 00 00...` +- SetTechLevel: `56 57 41 54 48 83 EC 40 8B C2 48 89 5C 24 60...` + +### CE Bugs Discovered +- `getRTTIClassName()` broken for x64 image-relative RTTI (COL sig=1) +- `vtQword` scan cannot find large addresses +- Neither is documented anywhere in the CE community + +--- + +## 4. Practical Impact — What Saves Us Future Work + +### Things We Do NOT Need to Research +Thanks to our Phase 1-2 RE + runtime CE session: + +1. **Owner detection** — SOLVED. PlayerArray at 0xA16FF0, local player at +0x62. No more scanning. +2. **HP manipulation** — SOLVED. SetHP at 0x3A89D0, single write path, 18 callers mapped. +3. **Credits** — SOLVED. PlayerObject+0x70 (float), AddCredits at 0x27F370. +4. **Tech level** — SOLVED. PlayerObject+0x84 (int32), SetTechLevel at 0x288980. +5. **Speed override** — SOLVED. obj+0xA8→locomotor, +0x29C flag, +0x2A0 value. +6. **Hero respawn** — SOLVED. Global at RVA 0xB169F0. +7. **Faction names** — SOLVED. PlayerObject+0x68 → string pointer. +8. **Unit type identification** — SOLVED. obj+0x298 → GameObjectType → +0xF8 name. +9. **Sub-object ownership** — SOLVED. Parent chain walk via +0x335/+0x278. +10. **Invulnerability** — SOLVED. Make_Invulnerable_Setter at 0x3ABB80 (proper way), SetHP hook (practical way). + +### Things That Still Need Research (Phase 3 Candidates) +1. **Fog of War pointer** — not found. Community docs mention `Fog_Of_War` as a Lua global but no internal address. +2. **Selected unit pointer** — the game's "currently selected unit" global. Trainer uses AOB signatures for UI mirror values but the direct pointer would be more robust. +3. **Game speed multiplier global** — the trainer profile has `game_speed: 0` (unresolved). Need to find the actual global. +4. **Damage multiplier injection** — DealDamage vfunc on CombatantBehaviorClass (vtable 0x8B21D0). Needs decompilation of specific vfunc slots. +5. **Lua state pointer extraction** — TheEvaluatorScriptManagerClass at 0x5E77E0 likely holds the lua_State*. Extracting it would enable calling any Lua function from the CT table. +6. **Income rate multiplier** — the `FUN_1404B0500()+0x20` reference from the KB needs validation. +7. **Planet ownership** — galactic map planet structs for planet switching features. + +### What Imperialware Could Contribute +If the Imperialware project is still accessible: +- **Spawning implementation** — how it calls Spawn_Unit/Reinforce from C# via Lua injection +- **Mod roster parsing** — how it reads unit lists from any mod's XML +- **Memory offset validation** — if it has known offsets for the 32-bit build, comparing against our 64-bit findings validates the struct layout migration + +### What PetroglyphTools Could Replace +- **MEG file reading** — our editor has a custom implementation; PetroglyphTools NuGet package could replace it +- **DAT localization parsing** — same +- **Game detection/launch** — process locator patterns + +--- + +## 5. Knowledge Base Update Recommendations + +### alamo_engine_kb.json Corrections Needed +1. **PlayerObject+0x04** → mark as WRONG or remove. Actual slot index is at +0x48. +2. **Add new fields**: +0x37 (playable flag), +0x48 (slot index), +0x62 (local player flag) +3. **Update PlayerObject description** to note the 3-layer identity system: +0x37 (playable), +0x62 (local), +0x68 (faction name) + +### signatures_phase2.json Additions +1. Add SetHP AOB: `40 53 48 83 EC 60 0F 29 74 24 50 0F 57 C0 F3 0F 10 71 5C` +2. All signatures verified unique in module + +### lua_binding_map.json Enrichment +1. Cross-reference parameter semantics from eaw-emmyluadoc +2. Add missing function RVAs for Despawn, Get_Faction, ReinforceList +3. Document scope restrictions (tactical-only vs any-mode) from community docs + +--- + +## Summary + +| Category | Our RE Coverage | Community Adds | Status | +|----------|----------------|----------------|--------| +| C++ struct layouts | 14 structs, 50+ fields | 0 — community has no C++ docs | COMPLETE | +| Function RVAs | 14 functions | 0 — community has no RVAs | COMPLETE | +| AOB signatures | 7 unique signatures | 0 — community has no AOBs | COMPLETE | +| Lua binding names | 134 bindings | +271 additional (405 total in community) | 33% of names, but 100% of RVAs | +| Lua parameter semantics | Error strings only | Full docs + behavioral gotchas | ENRICHMENT NEEDED | +| Behavioral warnings | 0 | 5+ critical crash/bug warnings | HIGH VALUE — prevents runtime crashes | +| Runtime object detection | PlayerArray + local player flag | 0 — community has nothing runtime | OUR UNIQUE CONTRIBUTION | +| RTTI class hierarchy | 2,919 classes | 0 — community has nothing | OUR UNIQUE CONTRIBUTION | +| Game mod data (XML/MEG) | Not our focus | PetroglyphTools has parsers | DEFER TO COMMUNITY | + +**Bottom line:** Our binary RE covers the runtime/memory side comprehensively. The community documentation is strongest on Lua function semantics and mod data formats. The two are complementary — the community tells us WHAT Lua functions do, we know WHERE they are in memory and HOW to call them. diff --git a/re-findings/galactic_map_system.json b/re-findings/galactic_map_system.json new file mode 100644 index 000000000..627cf557d --- /dev/null +++ b/re-findings/galactic_map_system.json @@ -0,0 +1,806 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "SWFOC Galactic Map System - Reverse Engineering Results", + "description": "Planet struct layout, planet list global, ownership transfer, fog of war, and trade route adjacency data from static analysis of StarWarsG.exe (x86_64, Alamo engine).", + "_meta": { + "analysis_date": "2026-04-04", + "analyst": "Agent 3E (Galactic Conquest Map System RE)", + "binary": "StarWarsG.exe", + "architecture": "x86_64", + "base_address_assumed": "0x140000000", + "tools": ["Ghidra 12.0.3 MCP", "RTTI recovery"], + "confidence_note": "All RVAs are relative to module base. Struct offsets are relative to struct base pointer. Offsets marked 'medium' confidence are inferred from decompiler output patterns and may shift with different object layouts." + }, + + "globals": { + "PlayerListClass_ptr": { + "rva": "0xA16FD0", + "type": "pointer_to_struct", + "struct": "PlayerListClass", + "description": "Global pointer to PlayerListClass singleton. DAT_140a16fd0 in Ghidra. Used by every planet ownership function. Dereference once to get the struct. player_array at +0x20, player_count at +0x28.", + "notes": "DAT_140a16fd8 is the END of the player pointer array (used as: count = (DAT_140a16fd8 - DAT_140a16fd0) >> 3)." + }, + "PlayerListClass_end_ptr": { + "rva": "0xA16FD8", + "type": "pointer", + "description": "End pointer of the player array. Player count = (0xA16FD8 - 0xA16FD0) >> 3 in the global-scoped usage pattern. Used in all planet ownership iteration loops." + }, + "GameModeManager_active_mode_ptr": { + "rva": "0xB15418", + "type": "pointer_to_struct", + "struct": "GameModeClass", + "description": "Global pointer to the currently active GameModeClass instance (GalacticModeClass, SpaceModeClass, or LandModeClass). DAT_140b15418 in Ghidra. Used by FOW Reveal, perception systems, and planet ownership. When in galactic mode, this points to a GalacticModeClass (vtable RVA 0x8762C0).", + "notes": "This is THE central game state pointer. All per-player FOW objects, per-player object lists, and game-mode-specific data hang off this." + }, + "GameObjectTypeList_ptr": { + "rva": "0xA172D0", + "type": "pointer_to_struct", + "description": "Global pointer to a GameObjectType list/registry. DAT_140a172d0 in Ghidra. Used by FUN_140331cc0 to find the 'is_planet' flagged type. List at +0x10 (pointer array), count at +0x18.", + "struct_layout": { + "+0x10": "pointer to array of GameObjectType pointers", + "+0x18": "int32 count" + } + }, + "PerceptionManager_ptr": { + "rva": "0xB153E0", + "type": "pointer_to_struct", + "description": "Global pointer to a perception/diplomacy manager. DAT_140b153e0 in Ghidra. Used to check faction relationships (Is_Enemy, Is_Ally) and by the planet faction change system to evaluate diplomatic state." + }, + "PlayerCount_static": { + "rva": "0xA16FB0", + "type": "uint32", + "description": "Static player count. DAT_140a16fb0 in Ghidra. Used by GameModeClass constructor to allocate per-player arrays." + }, + "GameTickCounter_ptr": { + "rva": "0xB0A340", + "type": "float_or_int", + "description": "DAT_140b0a340 in Ghidra. Used as a time multiplier in planet capture timer calculations." + }, + "FOW_GlobalToggle": { + "rva": "0xA284C4", + "type": "bool", + "description": "DAT_140a284c4 in Ghidra. Global FOW toggle. Set by FOW 'Undo_Reveal_All' function (FUN_1406a53b0). When true, FOW is fully disabled." + } + }, + + "structs": { + "PlanetaryDataPackClass": { + "description": "Data pack attached to planet GameObjectClass instances via the behavior/component system. Contains ALL persistent galactic-map state for a planet: trade route links, ownership, garrison units, tactical built structures, upgrade objects, visibility modifiers, and capture progress. This is the planet's 'save state' that persists across tactical battles. Accessed via the PlanetaryBehaviorClass which stores a pointer to this at its +0x18 field (offset 0xB8 or 0x17*8 from behavior base).", + "rtti_mangled_name": ".?AVPlanetaryDataPackClass@@", + "pool_class": "PooledObjectClass", + "size_known": false, + "size_minimum": "0x350", + "inherits_comment": "Uses DynamicVectorClass members extensively. The destructor reveals the member layout via inlined destructor calls.", + "fields": [ + { + "offset": "0x00", + "type": "vtable_ptr", + "width": 8, + "name": "vtable_ptr" + }, + { + "offset_range": "self[1]..self[2]", + "byte_offset": "0x10..0x20", + "type": "DynamicVectorClass", + "name": "persistent_tactical_objects", + "description": "Stores persistent built tactical structures (land base, turrets, etc) that survive across tactical battles." + }, + { + "offset_range": "self[2]..self[3]", + "byte_offset": "0x20..0x30", + "type": "DynamicVectorClass", + "name": "persistent_upgrades", + "description": "Persistent upgrade objects on this planet." + }, + { + "offset_range": "self[3]..self[5]", + "byte_offset": "0x30..0x50", + "type": "DynamicVectorClass", + "name": "line_links", + "description": "Visual/display links for the galactic map lines." + }, + { + "offset_range": "self[5]..self[6]", + "byte_offset": "0x50..0x60", + "type": "DynamicVectorClass", + "name": "trade_route_links", + "description": "Adjacent trade routes connected to this planet. Each entry is a TradeRouteLinkEntryClass containing the connected planet reference and route data. This is the planet's adjacency list for the galactic graph." + }, + { + "offset": "0x68", + "type": "float32", + "width": 4, + "name": "capture_progress", + "description": "Current capture/ownership progress value. Written during planet faction changes. When ownership changes, this is set to the new faction change value from FUN_1403f6af0. Reset to 0 on full owner switch.", + "confidence": "high", + "xrefs": ["function:PlanetFactionChangeClass_0x1403fa160", "function:PlanetFactionChangeClass_0x1403fb040"] + }, + { + "offset": "0x6C", + "type": "int32", + "width": 4, + "name": "owning_player_id", + "description": "Player ID (index into PlayerListClass) of the faction that owns this planet. Set to -1 (0xFFFFFFFF) during transitions. This is THE ownership field for planets. Compared and written in PlanetFactionChangeClass.", + "confidence": "high", + "xrefs": ["function:PlanetFactionChangeClass_0x1403fa160", "function:PlanetFactionChangeClass_0x1403fb040"] + }, + { + "offset": "0x70", + "type": "float32", + "width": 4, + "name": "previous_capture_progress", + "description": "Previous capture progress value. Compared against current to detect changes.", + "confidence": "high" + }, + { + "offset": "0x74", + "type": "int32", + "width": 4, + "name": "previous_owning_player_id", + "description": "Previous owner player ID. Used to detect ownership transitions and fire events.", + "confidence": "high" + }, + { + "offset": "0x78", + "type": "pointer_or_struct", + "width": 8, + "name": "capture_timer_data", + "description": "Capture timer/progress tracking structure. FUN_1403fe810 writes into this + subfields." + }, + { + "offset": "0x98", + "type": "int32", + "width": 4, + "name": "capture_initiator_player_id", + "description": "Player ID of the faction initiating the capture. Set to -1 when not being captured.", + "confidence": "high" + }, + { + "offset": "0x9C", + "type": "int32", + "width": 4, + "name": "capture_state_flags", + "description": "Capture state flags. Set to -1 (0xFFFFFFFF) on initialization.", + "confidence": "medium" + }, + { + "offset": "0xA0", + "type": "std_map", + "name": "per_player_planet_data", + "description": "std::map or red-black tree keyed by some per-player identifier. Iterated in FUN_1403f6af0. Contains per-player visibility/force data for this planet." + }, + { + "offset": "0xB0", + "type": "int32", + "width": 4, + "name": "income_value", + "description": "Planet income/credit generation value. Set during faction change processing.", + "confidence": "medium" + }, + { + "offset": "0xB4", + "type": "int32", + "width": 4, + "name": "income_multiplier_or_bonus", + "description": "Income-related secondary value.", + "confidence": "medium" + }, + { + "offset": "0xB8", + "type": "uint8", + "width": 1, + "name": "income_flag_1", + "description": "Flag related to income processing. Set to 1 during ownership changes.", + "confidence": "medium" + }, + { + "offset": "0xB9", + "type": "uint8", + "width": 1, + "name": "income_flag_2", + "description": "Secondary income processing flag. Set to 1 after income_flag_1.", + "confidence": "medium" + }, + { + "offset": "0x1C9", + "type": "uint8", + "width": 1, + "name": "initial_owner_set_flag", + "description": "Set to 1 after the first ownership assignment. Guards against double-initialization in PlanetFactionChangeClass.", + "confidence": "high" + }, + { + "offset": "0x200", + "type": "int32", + "width": 4, + "name": "special_structure_owner_id", + "description": "Owner player ID of a special structure on this planet. When matches new owner, triggers structure cleanup at +0x204 through +0x220.", + "confidence": "medium" + }, + { + "offset": "0x2C8", + "type": "uint8", + "width": 1, + "name": "planet_destroyed_flag", + "description": "Flag indicating planet has been destroyed. Checked before AI reassignment processing.", + "confidence": "medium" + }, + { + "offset": "0x2E2", + "type": "uint8", + "width": 1, + "name": "special_state_flag_1", + "description": "Special planet state flag. Checked in corruption processing.", + "confidence": "medium" + }, + { + "offset": "0x2E3", + "type": "uint8", + "width": 1, + "name": "capture_timer_active", + "description": "Whether the planet capture timer is actively running.", + "confidence": "medium" + }, + { + "offset": "0x2E8", + "type": "int32", + "width": 4, + "name": "capture_start_tick", + "description": "Game tick when capture timer started.", + "confidence": "medium" + }, + { + "offset": "0x2EC", + "type": "int32", + "width": 4, + "name": "capture_end_tick", + "description": "Game tick when capture will complete.", + "confidence": "medium" + }, + { + "offset": "0x2F4", + "type": "int32", + "width": 4, + "name": "corruption_level", + "description": "Planet corruption level. -1 when not corrupted. Set to 3 during ownership change by corrupting faction. Values: -1=none, 0-3=corruption tiers.", + "confidence": "high" + }, + { + "offset": "0x2F8", + "type": "int32", + "width": 4, + "name": "corruption_timer_or_state", + "description": "Corruption timer or state field. Set to -1 on reset.", + "confidence": "medium" + } + ], + "inner_structs": { + "TradeRouteLinkEntryClass": { + "description": "Entry in the trade route adjacency list. Stored in DynamicVectorClass. Connects this planet to another via a TradeRouteClass.", + "destructor_rva": "0x4B5F40" + }, + "LineLinkStruct": { + "description": "Visual line link data for galactic map rendering.", + "destructor_rva": "0x4B5DF0" + }, + "PersistentTacticalBuiltObjectStruct": { + "description": "Persistent tactical structure data (survives across battles).", + "destructor_rva": "0x4B5E60" + }, + "PersistentUpgradeObjectStruct": { + "description": "Persistent upgrade data.", + "destructor_rva": "0x4B5ED0" + } + } + }, + + "PlanetaryBehaviorClass": { + "description": "Behavior component attached to planet GameObjectClass instances. Manages the relationship between the planet's GameObjectClass (position, owner, etc.) and the PlanetaryDataPackClass (persistent planet state). Pool size 20. The destructor at RVA 0x3F3340 reveals the link: this->offset_0x18 is the parent GameObjectClass (the planet object), and this->offset_0x18[0x17] is the pointer to PlanetaryDataPackClass.", + "rtti_mangled_name": ".?AVPlanetaryBehaviorClass@@", + "pool_class": "PooledObjectClass", + "destructor_rva": "0x3F3340", + "size_known": false, + "inherits": ["BehaviorClass"], + "fields": [ + { + "offset": "0x00", + "type": "vtable_ptr", + "width": 8, + "name": "vtable_ptr" + }, + { + "offset": "0x18", + "type": "pointer", + "width": 8, + "name": "parent_game_object_ptr", + "description": "Pointer to the parent GameObjectClass (the planet object). Used extensively in the destructor to clean up planet state.", + "confidence": "high" + } + ], + "notes": "The parent game object has: [0x17] = PlanetaryDataPackClass ptr, [0x53] = GameObjectType ptr, [0x07..0x0F] = various sub-component arrays. The PlanetaryDataPackClass at [parent+0x17*8] = [parent+0xB8] is the core data." + }, + + "PlanetFactionChangeClass": { + "description": "Event/signal class that handles planet ownership transfers on the galactic map. Nested inside PlanetaryDataPackClass. Two key functions: the initial ownership set (RVA 0x3FA160) and the full transfer handler (RVA 0x3FB040). The transfer handler fires the TEXT_PLANET_CHANGED_HANDS localized string, notifies all players, updates perception, and optionally shows the TEXT_PLANET_CHANGED_HANDS_BONUS variant.", + "destructor_rva_1": "0x3FA160", + "destructor_rva_2": "0x3FB040", + "vtable_string": "PlanetFactionChangeClass_vftable", + "inherits": ["SignalDataClass"], + "key_behaviors": { + "initial_ownership_set": { + "rva": "0x3FA160", + "description": "Simplified ownership setter. Writes ownership fields (planet+0x68, +0x6C, +0x70, +0x74, +0x98) and notifies all players via iteration over PlayerListClass.", + "steps": [ + "Read planet data ptr from param_1[0x17] (byte offset 0xB8 on parent object)", + "Call FUN_1403f6af0 to compute new income value", + "Set fields at planet_data+0x68 (capture_progress), +0x6C (owner_id), +0x70 (prev_progress), +0x98 (capture_initiator) to new values", + "Iterate all players via FUN_140294bc0 using DAT_140a16fd0 (PlayerListClass)", + "For each player, call FUN_1403a51e0 to update planet visibility", + "Fire signal via FUN_140220ed0" + ] + }, + "full_ownership_transfer": { + "rva": "0x3FB040", + "description": "Full planet ownership transfer with UI notification, capture progress tracking, diplomacy checks, corruption processing, and AI budget updates.", + "steps": [ + "Read planet data from param_1[0x17] (PlanetaryDataPackClass)", + "Check initial_owner_set_flag at planet_data+0x1C9", + "If first time: set flag, write owner to +0x6C, set capture progress to +0x68, notify all players", + "Compare new owner (param_2) vs current (planet_data+0x6C) -- if different, full transfer", + "On transfer: Reset +0x6C to new owner, +0x68 to 0, iterate players to update visibility", + "Look up old/new player objects via FUN_140294bc0", + "Read faction_affiliation from player+0x68 for both old and new owners", + "Format TEXT_PLANET_CHANGED_HANDS or TEXT_PLANET_CHANGED_HANDS_BONUS message", + "Log '%s: now controlled by %s (owner player ID %d)' via FUN_140025760", + "Play conquest/loss audio cues from player+0x68+0x780 / +0x790", + "Check corruption flag at new_player+0x68+0x10B -- if set, initiate corruption", + "Process capture timer at planet_data+0x2E2..+0x2F8", + "Update corruption level at planet_data+0x2F4 (set to 3 for corrupting faction)", + "Notify AI subsystems for budget recomputation" + ] + } + } + }, + + "GameModeClass_FOWFields": { + "description": "FOW-related fields within GameModeClass. These are at fixed offsets within the GameModeClass base.", + "context": "GameModeClass is the base for GalacticModeClass, SpaceModeClass, LandModeClass. The active instance is pointed to by DAT_140b15418.", + "fields": [ + { + "offset": "0x190", + "type": "int32", + "width": 4, + "name": "fow_player_count", + "description": "Number of per-player FOW objects allocated. Corresponds to self[0x32] in Ghidra notation.", + "confidence": "high" + }, + { + "offset": "0x198", + "type": "pointer", + "width": 8, + "name": "fow_per_player_array", + "description": "Pointer to array of per-player FOW objects. Indexed by player_id. Each element is a pointer to a FOW texture/state object (0x58 bytes each, freed in destructor). Corresponds to self[0x33] in Ghidra notation.", + "confidence": "high" + } + ] + }, + + "FOWPlayerObject": { + "description": "Per-player fog of war state object. One per player, stored in GameModeClass.fow_per_player_array[player_id]. Size 0x58 bytes.", + "size": "0x58", + "fields": [ + { + "offset": "0x00", + "type": "pointer", + "width": 8, + "name": "visibility_grid", + "description": "Pointer to the raw visibility grid byte array. Each byte represents visibility state for one cell: 0x00=fully fogged, 0x01-0x0F=partial visibility (fading), 0x10-0xEE=revealed (varying degrees), 0xEF=just-cleared marker, 0xFF=fully visible.", + "confidence": "high" + }, + { + "offset": "0x08", + "type": "pointer", + "width": 8, + "name": "reveal_timer_grid", + "description": "Pointer to short[grid_size] array. Countdown timer per cell. 0=not timed (permanent reveal), -1=permanent (no countdown). Non-zero positive values decrement each tick.", + "confidence": "high" + }, + { + "offset": "0x10", + "type": "pointer", + "width": 8, + "name": "output_visibility_grid", + "description": "Pointer to output/rendering visibility values. Written by FUN_1404c0dc0.", + "confidence": "medium" + }, + { + "offset": "0x20", + "type": "int64", + "width": 8, + "name": "grid_cell_count", + "description": "Total number of cells in the visibility grid.", + "confidence": "high" + }, + { + "offset": "0x50", + "type": "uint8", + "width": 1, + "name": "dirty_flag", + "description": "Set to 1 after any grid modification. Signals the renderer to re-upload the FOW texture.", + "confidence": "high" + } + ] + }, + + "PlanetReachabilityClass": { + "description": "Stores planet-to-planet reachability data for a specific player/faction. Contains 11 reachability slots (initialized in a loop of count 0xB=11). Constructor at RVA 0x663530. Uses the perception system at DAT_140b153e0 to compute pathfinding costs.", + "constructor_rva": "0x663530", + "size_known": false, + "size_minimum": "0x170", + "inherits": ["RefCountClass"], + "fields": [ + { + "offset": "0x08", + "type": "pointer", + "width": 8, + "name": "source_planet_ptr", + "description": "The planet this reachability data is relative to." + }, + { + "offset": "0x88", + "type": "array_of_structs", + "element_size": 16, + "count": 11, + "name": "reachability_slots", + "description": "11 slots of reachability data (each 0x10 bytes). Initialized via FUN_14020b740 and FUN_14020b930." + }, + { + "offset": "0x138", + "type": "undefined8", + "width": 8, + "name": "path_context" + }, + { + "offset": "0x140", + "type": "float_pair", + "width": 16, + "name": "cost_pair_1" + }, + { + "offset": "0x150", + "type": "float_pair", + "width": 16, + "name": "cost_pair_2" + }, + { + "offset": "0x160", + "type": "float_pair", + "width": 16, + "name": "cost_pair_3" + }, + { + "offset": "0x16C", + "type": "int16", + "width": 2, + "name": "flags_1" + }, + { + "offset": "0x16E", + "type": "int16", + "width": 2, + "name": "flags_2" + } + ] + }, + + "TradeRouteClass": { + "description": "Represents a trade route connecting two planets. Referenced by PlanetaryDataPackClass::TradeRouteLinkEntryClass. A DynamicVectorClass of const TradeRouteClass pointers exists (destructor at RVA 0x4AE5E0), suggesting a global or per-mode list of all trade routes.", + "destructor_rva_list": "0x4AE5E0", + "size_known": false, + "notes": "Trade routes form the edges of the galactic map graph. Each planet has a DynamicVectorClass at its data pack offset ~0x50 that lists adjacent routes." + } + }, + + "functions": { + "PlayerList_FindByID": { + "rva": "0x294BC0", + "signature": "void* PlayerList_FindByID(PlayerListClass* list, int player_id)", + "description": "Looks up a player object by ID from the player list. Bounds-checks player_id against list+0x28 (count), returns *(list+0x20)[player_id]. Returns 0 if out of bounds.", + "params": { + "param_1": "longlong - pointer to PlayerListClass (or the DAT_140a16fd0 global directly)", + "param_2": "int - player_id to look up" + }, + "returns": "Pointer to PlayerObject, or 0 if invalid", + "confidence": "high" + }, + "PlayerList_FindByFaction": { + "rva": "0x294D30", + "signature": "void* PlayerList_FindByFaction(PlayerListClass* list, void* faction_ptr)", + "description": "Iterates all players in the list and returns the first whose faction_affiliation_ref (player+0x68) matches the given pointer. Used to look up the player owning a specific faction.", + "params": { + "param_1": "longlong - pointer to PlayerListClass", + "param_2": "longlong - faction affiliation pointer to match" + }, + "returns": "Pointer to matching PlayerObject, or 0", + "confidence": "high" + }, + "PlayerList_GetLocalPlayerID": { + "rva": "0x294A70", + "signature": "int PlayerList_GetLocalPlayerID(PlayerListClass* list)", + "description": "Returns the player_id (field +0x4C) of the local human player. Uses list[6] as the local player index.", + "confidence": "high" + }, + "PlayerList_GetLocalPlayer": { + "rva": "0x294A40", + "signature": "void* PlayerList_GetLocalPlayer(PlayerListClass* list)", + "description": "Returns the PlayerObject pointer for the local human player.", + "confidence": "high" + }, + "FindPlanetClass_Constructor": { + "rva": "0x697010", + "signature": "FindPlanetClass* FindPlanetClass::FindPlanetClass(FindPlanetClass* this)", + "description": "Constructor for the Lua FindPlanet wrapper. Registers the 'Get_All_Planets' Lua method. The actual planet lookup implementation is at LAB_140254a1c (code label, not a function boundary).", + "lua_methods_registered": ["Get_All_Planets"], + "notes": "The Get_All_Planets implementation iterates whatever planet container is managed by the galactic mode and returns a Lua table of planet wrappers." + }, + "PlanetFactionChange_InitialSet": { + "rva": "0x3FA160", + "signature": "void PlanetFactionChange_InitialSet(PlanetFactionChangeClass* this, GameObjectClass* planet_obj)", + "description": "Initial planet ownership assignment. Sets ownership fields on the PlanetaryDataPackClass. Iterates all players to update visibility. Called when a planet is first assigned an owner (map init or scenario load)." + }, + "PlanetFactionChange_Transfer": { + "rva": "0x3FB040", + "signature": "void PlanetFactionChange_Transfer(PlanetFactionChangeClass* this, GameObjectClass* planet_obj, int new_owner_id, float capture_progress)", + "description": "Full planet ownership transfer handler. Updates ownership, fires UI events, plays audio, processes corruption, updates AI budgets. The main 'Change_Owner for planets' function.", + "log_format": "%s: now controlled by %s (owner player ID %d)", + "ui_strings": ["TEXT_PLANET_CHANGED_HANDS", "TEXT_PLANET_CHANGED_HANDS_BONUS"] + }, + "Planet_UpdatePlayerVisibility": { + "rva": "0x3A51E0", + "signature": "void Planet_UpdatePlayerVisibility(GameObjectClass* planet_obj, PlayerObject* player)", + "description": "Updates a player's visibility state for a planet. Checks if the player is the local player (via FUN_140294a40), then iterates the planet's component array (planet+0x278, count at +0x288) calling virtual method 0xA0 on each component. Sets flag at planet+0x39F.", + "planet_fields_accessed": { + "+0x278": "component_array_ptr", + "+0x288": "component_count (char)", + "+0x2A0": "sub_object_ptr (for visibility update)", + "+0x39F": "visibility_dirty_flag" + } + }, + "PlanetOwnershipIncome_Compute": { + "rva": "0x3F6AF0", + "signature": "int PlanetOwnershipIncome_Compute(void* this, GameObjectClass* planet_obj, int* out_owner_id)", + "description": "Computes planet income value during ownership change. Accesses planet_obj[0x53] (GameObjectType) and planet_obj[0x17] (PlanetaryDataPackClass). Uses the per-player-planet-data map at planet_data+0xA0. Writes income values to planet_data+0xB0..+0xB9.", + "confidence": "high" + }, + "FOW_Reveal_Tactical": { + "rva": "0x6A5700", + "signature": "void* FOW_Reveal_Tactical(GetEvent* event, lua_State* L, void* params)", + "description": "Lua-bound FOW.Reveal function for tactical mode. Takes (player, object_or_position, radius, [decay_radius]). Gets the player's FOW object from GameModeClass+0x198[player_id], then calls FUN_14035d080 or FUN_14035d1b0 to create the reveal.", + "lua_error_messages": [ + "LuaFOWRevealCommandClass -- Command only valid in a tactical game.", + "LuaFOWRevealCommandClass -- Requires at least 2 parameters: (player, object).", + "LuaFOWRevealCommandClass -- Expected player object as first parameter.", + "LuaFOWRevealCommandClass -- Expected 3rd parameter for reveal radius.", + "LuaFOWRevealCommandClass -- Undable to get a valid position from parameter 2." + ] + }, + "FOW_RevealAll": { + "rva": "0x6A5B00", + "signature": "void FOW_RevealAll(void* unused, lua_State* L, void* params)", + "description": "Lua-bound FOWManager.Reveal_All function. Takes (player). Gets player's FOW object and calls FUN_14035d4f0 to reveal all cells. Only works in tactical mode.", + "lua_error_messages": [ + "LuaFOWRevealCommandClass -- Command only valid in a tactical game.", + "LuaFOWRevealCommandClass -- Requires at least 1 parameters: (player).", + "LuaFOWRevealCommandClass -- Expected player object as first parameter." + ] + }, + "FOW_UndoRevealAll": { + "rva": "0x6A53B0", + "signature": "void FOW_UndoRevealAll(void* unused, lua_State* L, void* params)", + "description": "Lua-bound FOWManager.Undo_Reveal_All function. Takes (bool_param). Sets DAT_140a284c4 (global FOW toggle) and calls FUN_14013ecc0.", + "global_written": "0xA284C4" + }, + "FOW_TemporaryReveal": { + "rva": "0x6A5CF0", + "signature": "void FOW_TemporaryReveal(void* unused, lua_State* L, void* params)", + "description": "Lua-bound FOWManager.Temporary_Reveal function. Takes (player, object, [radius]). Creates a time-limited reveal at the object's position. Calls FUN_140365630.", + "lua_error_messages": [ + "LuaFOWRevealCommandClass -- Command only valid in a tactical game.", + "LuaFOWRevealCommandClass -- Requires 2 parameters: (player, object, [radius]).", + "LuaFOWRevealCommandClass -- Expected player object as first parameter.", + "LuaFOWRevealCommandClass -- Expected object as 2nd parameter.", + "LuaFOWRevealCommandClass -- Expected number as 3rd parameter." + ] + }, + "FOW_PerPlayer_Reveal": { + "rva": "0x35D080", + "signature": "void FOW_PerPlayer_Reveal(GameModeClass* mode, int player_id, position, float outer_radius, float inner_radius, void* out_handle)", + "description": "Creates a FOW reveal circle for a specific player. Accesses mode[0x33] (fow_per_player_array) indexed by player_id. Calls FUN_1404c0ec0 on the player's FOW object to set visibility in the grid." + }, + "FOW_PerPlayer_RevealAll": { + "rva": "0x35D4F0", + "signature": "void FOW_PerPlayer_RevealAll(GameModeClass* mode, int player_id)", + "description": "Reveals the entire map for a specific player. Accesses mode[0x33][player_id] and calls FUN_1404c1560 which iterates the full grid and sets all cells to fully visible." + }, + "FOW_UpdateGrid": { + "rva": "0x4C1560", + "signature": "void FOW_UpdateGrid(FOWPlayerObject* fow)", + "description": "Per-frame FOW grid update. Iterates all cells (fow[4] = count). For cells with timer (fow[1][i] != 0), decrements timer. When timer expires, fades visibility. Grid byte values: 0=fogged, 0x01-0x0F=fading in, 0x10-0xEE=fading out, 0xEF=just-fogged marker, 0xFF=full visible.", + "grid_layout": { + "fow[0]": "visibility_state byte array", + "fow[1]": "reveal_timer short array", + "fow[2]": "output_visibility byte array", + "fow[4]": "cell_count" + } + }, + "FOW_RevealCircle": { + "rva": "0x4C0EC0", + "signature": "void FOW_RevealCircle(FOWPlayerObject* fow, position, float outer_radius, float inner_radius, void* handle)", + "description": "Reveals a circular area in the FOW grid. If outer > inner, calls FUN_1404c1000 twice (one for each radius). Otherwise calls once with combined parameters." + }, + "GameObjectType_FindPlanetType": { + "rva": "0x331CC0", + "signature": "void* GameObjectType_FindPlanetType(GameObjectTypeList* list)", + "description": "Iterates the game object type list and returns the first type with flag at type+0x108 set to non-zero. This identifies the 'planet' type definition. The list is at DAT_140a172d0 (type_list+0x10 = array, type_list+0x18 = count).", + "type_flag_offset": "0x108" + }, + "GalacticPathFinder_Singleton_Init": { + "rva": "0x7E6920", + "signature": "void GalacticPathFinder_Singleton_Init()", + "description": "Initializes the GalacticPathFinderClass singleton vtable. The singleton instance pointer is at PTR_vftable_1409cf1c8." + }, + "PlanetaryDataPack_ResetCapture": { + "rva": "0x4B6C50", + "signature": "void PlanetaryDataPack_ResetCapture(PlanetaryDataPackClass* pack)", + "description": "Resets capture state fields: pack+0x2E2=0, pack+0x2E3=0, pack+0x2E8=0, pack+0x2F8=-1.", + "fields_written": { + "+0x2E2": "0 (special_state_flag_1)", + "+0x2E3": "0 (implicitly via word write)", + "+0x2E8": "0 (capture_start_tick / 8 bytes)", + "+0x2F8": "-1 (corruption_timer_or_state)" + } + }, + "PlanetaryDataPack_StartCaptureTimer": { + "rva": "0x4B7260", + "signature": "void PlanetaryDataPack_StartCaptureTimer(PlanetaryDataPackClass* pack, float duration)", + "description": "Starts the capture timer. Writes pack+0x2E3=1 (timer active), pack+0x2F8=-1, computes start/end ticks from DAT_140b15418 (game mode tick at +0x10) and DAT_140b0a340 (time scale).", + "fields_written": { + "+0x2E3": "1 (capture_timer_active)", + "+0x2E8": "current_tick (from GameModeClass+0x10)", + "+0x2EC": "current_tick + (time_scale * duration)", + "+0x2F8": "-1" + } + } + }, + + "relationships": { + "planet_object_to_data_pack": { + "description": "A planet on the galactic map is a GameObjectClass with a PlanetaryBehaviorClass component. The behavior at +0x18 points to the parent GameObjectClass. The parent's field at [0x17*8]=+0xB8 points to the PlanetaryDataPackClass. The data pack holds ownership, trade routes, income, capture state, and corruption.", + "traversal": "planet_game_object -> [+0xB8] -> PlanetaryDataPackClass", + "alternative": "PlanetaryBehaviorClass.offset_0x18 -> GameObjectClass -> [+0xB8] -> PlanetaryDataPackClass" + }, + "planet_ownership": { + "description": "Planet ownership is stored at PlanetaryDataPackClass+0x6C as a player_id (int32). This indexes into the PlayerListClass at DAT_140a16fd0. The owning player's faction is at PlayerObject+0x68. Ownership changes go through PlanetFactionChangeClass which updates both the data pack fields and notifies all players.", + "read_path": "planet_data+0x6C -> player_id -> PlayerList_FindByID(DAT_140a16fd0, player_id) -> PlayerObject", + "write_path": "PlanetFactionChange_Transfer (RVA 0x3FB040) -> writes planet_data+0x6C, fires signals" + }, + "fow_system": { + "description": "The FOW system is per-player and per-game-mode. The active GameModeClass (at DAT_140b15418) stores an array of per-player FOW objects at offset 0x198. Each FOW object contains three parallel arrays (visibility, timer, output) indexed by grid cell. Lua functions Reveal/Reveal_All/Undo_Reveal_All/Temporary_Reveal are bound through LuaFOWRevealCommandClass. All require tactical mode (not galactic).", + "access_path": "DAT_140b15418 -> [+0x198] -> fow_array[player_id] -> FOWPlayerObject", + "grid_encoding": { + "0x00": "fully fogged", + "0x01-0x0F": "fading (was revealed, decreasing visibility)", + "0x10-0xEE": "revealed (stable)", + "0xEF": "just-cleared marker (transition state)", + "0xFF": "fully visible (permanent or active unit vision)" + }, + "tactical_only": true, + "galactic_note": "In galactic mode, planet visibility is handled differently -- through the PlanetReachabilityClass and the per-player-planet-data map at PlanetaryDataPackClass+0xA0. The tactical FOW grid does not exist in galactic mode." + }, + "trade_routes_adjacency": { + "description": "Trade routes form the edges of the galactic map graph. Each planet's PlanetaryDataPackClass contains a DynamicVectorClass at byte offset ~0x50 that lists adjacent trade routes. The TradeRouteClass objects themselves are stored in a separate global list (DynamicVectorClass destructor at RVA 0x4AE5E0). The GalacticPathFinderClass singleton uses these adjacency lists for pathfinding.", + "per_planet": "PlanetaryDataPackClass offset ~0x50 = DynamicVectorClass", + "global_list": "DynamicVectorClass (destructor RVA 0x4AE5E0)" + }, + "galactic_perception": { + "description": "The GalacticPerceptionSystemClass (constructor RVA 0x4E1880) manages what each player can see on the galactic map. This is separate from the tactical FOW system. Planet visibility in galactic mode uses FUN_1403a51e0 which iterates planet components and checks visibility per-player.", + "constructor_rva": "0x4E1880" + } + }, + + "key_data_globals_summary": { + "DAT_140a16fd0": { + "purpose": "PlayerListClass global pointer (player_array at +0x20, count at +0x28)", + "rva": "0xA16FD0" + }, + "DAT_140a16fd8": { + "purpose": "End of player array (count = (this - DAT_140a16fd0) >> 3)", + "rva": "0xA16FD8" + }, + "DAT_140b15418": { + "purpose": "Active GameModeClass pointer (FOW array at +0x198, player_count at +0x190)", + "rva": "0xB15418" + }, + "DAT_140a172d0": { + "purpose": "GameObjectType registry (array at +0x10, count at +0x18)", + "rva": "0xA172D0" + }, + "DAT_140b153e0": { + "purpose": "Perception/diplomacy manager global", + "rva": "0xB153E0" + }, + "DAT_140a284c4": { + "purpose": "Global FOW disable toggle (bool)", + "rva": "0xA284C4" + }, + "DAT_140b15d74": { + "purpose": "Diplomatic capture progress multiplier 1 (float)", + "rva": "0xB15D74" + }, + "DAT_140b15d78": { + "purpose": "Diplomatic capture progress multiplier 2 (float)", + "rva": "0xB15D78" + }, + "DAT_140b0a340": { + "purpose": "Game time scale multiplier (for capture timer computation)", + "rva": "0xB0A340" + } + }, + + "rtti_classes_found": [ + "PlanetaryBehaviorClass", + "PlanetaryDataPackClass", + "PlanetFactionChangeClass (nested in PlanetaryDataPackClass)", + "PlanetReachabilityClass", + "PlanetAITargetLocationClass", + "PlanetDestructionAbilityClass", + "PlanetIncomeBonusAbilityClass", + "PlanetIncomeGamblingAbilityClass", + "CorruptPlanetAbilityClass", + "GalacticModeClass", + "GalacticCameraClass", + "GalacticGoalSystemClass", + "GalacticPerceptionSystemClass", + "GalacticSabotageAbilityClass", + "GalacticSellEventClass", + "GalacticStealthAbilityClass", + "GalacticPathFinderClass (singleton)", + "TradeRouteClass", + "FindPlanetClass (Lua wrapper)", + "LuaFOWRevealCommandClass", + "RevealBehaviorClass", + "StoryEventSelectPlanetClass", + "PlanetaryBombardEventClass" + ], + + "lua_bindings_discovered": { + "FindPlanetClass": { + "methods": ["Get_All_Planets"], + "constructor_rva": "0x697010" + }, + "LuaFOWRevealCommandClass": { + "methods": ["Reveal (tactical)", "Reveal_All", "Undo_Reveal_All", "Temporary_Reveal"], + "constructor_rva": "0x6A51B0", + "method_implementations": { + "Reveal": "0x6A5700", + "Reveal_All": "0x6A5B00", + "Undo_Reveal_All": "0x6A53B0", + "Temporary_Reveal": "0x6A5CF0" + } + }, + "GalacticFreeStoreClass": { + "constructor_rva": "0x68EAF0" + }, + "GalacticTaskForceClass": { + "constructor_rva": "0x6CCE20" + } + }, + + "open_questions": [ + "Exact planet list global: Planets are GameObjectClass instances managed by the galactic mode. The Get_All_Planets Lua function iterates them, but the actual global pointer to the 'planet list' container has not been isolated to a single RVA. It likely lives inside the GalacticModeClass instance (sub-object of GameModeClass) or is accessed via the FindPlanetClass lookup mechanism.", + "PlanetaryDataPackClass full size: Destructor analysis shows fields up to at least offset 0x350+ (self[0x34] = byte offset 0x1A0+), but the total size with all DynamicVector members is larger.", + "Trade route edge data: The TradeRouteLinkEntryClass internal structure (which planet it connects to, travel time, hyperlane type) needs runtime validation.", + "Galactic FOW vs tactical FOW: Galactic mode visibility is handled by GalacticPerceptionSystemClass and per-player planet data, NOT by the tactical FOW grid system. The exact galactic visibility structure needs further RE.", + "Planet position data: Where x/y/z galactic map coordinates are stored on the planet object. Likely inherited from GameObjectClass transform component." + ] +} diff --git a/re-findings/game_systems_map.json b/re-findings/game_systems_map.json new file mode 100644 index 000000000..f0e50e2b1 --- /dev/null +++ b/re-findings/game_systems_map.json @@ -0,0 +1,2552 @@ +{ + "_meta": { + "generated_from": "rtti_class_hierarchy.json", + "generated_date": "2026-04-04", + "source_binary": "StarWarsG.exe", + "engine": "Alamo", + "game": "Star Wars: Empire at War - Forces of Corruption", + "total_rtti_classes": 2919, + "total_classified": 2919, + "subsystem_count": 16, + "crt_stdlib_filtered": 597, + "methodology": "Classes grouped by keyword matching on short name, demangled name, and inheritance chains (all_bases). First-match-wins priority ordering: combat > AI > movement > abilities > economy > story > multiplayer > UI > units > map/terrain > behaviors > engine_core > lua > save/load > audio/visual > uncategorized. C++ stdlib, MSVC CRT, and IDA switch-table artifacts are pre-filtered into engine_core.", + "trainer_relevance_key": { + "high": "Directly useful for trainer/mod development (memory values to read/write, behaviors to hook)", + "medium": "Useful for context and advanced mods but not primary trainer targets", + "low": "Engine internals or infrastructure; rarely targeted directly" + } + }, + "subsystems": [ + { + "name": "combat_damage", + "description": "Damage, armor, shields, weapons, hardpoints, projectiles, health, and combat resolution", + "class_count": 148, + "trainer_relevance": "high", + "key_base_classes": [ + "BehaviorClass", + "SpecialAbilityClass", + "BaseDataPackClass", + "BaseEventFactoryClass", + "ScheduledEventClass", + "CombatantBehaviorClass", + "DatabaseUIntConversionClass", + "HeroClashInterface", + "LuaUserVar", + "TargetingInterfaceClass" + ], + "classes": [ + "?$DatabaseObjectManagerClass", + "?$DynamicVectorClass", + "?$EnumConversionClass", + "?$EventFactoryClass", + "?$PooledObjectClass", + "ActionDrivenHeroClashClass", + "AffectedByShieldBehaviorClass", + "AffectedByShieldDataPackClass", + "ArcSweepAttackAbilityClass", + "AsteroidFieldDamageBehaviorClass", + "AttackActionClass", + "AttackEventClass", + "AutoResolveClass", + "BaseCombatantClass", + "BaseShieldBehaviorClass", + "BaseShieldDataPackClass", + "CableAttackAbilityClass", + "CombatBonusAbilityClass", + "CombatantBehaviorClass", + "CompanyCombatantClass", + "ConcentrateFireAttackAbilityClass", + "DamageTrackingBehaviorClass", + "DamageTrackingDataPackClass", + "DatabaseObjectManagerClass", + "DeathBehaviorClass", + "DeathDataPackClass", + "DestroyAfterSpecialWeaponFiredDummyBehaviorClass", + "DynamicVectorClass", + "DynamicVectorClass", + "DynamicVectorClass", + "DynamicVectorClass", + "EarthquakeAttackAbilityClass", + "EatAttackAbilityClass", + "EnergyWeaponAttackAbilityClass", + "EnumConversionClass", + "EnumConversionClass", + "EventFactoryClass", + "EventFactoryClass", + "EventFactoryClass", + "EventFactoryClass", + "GenericAttackAbilityClass", + "GrenadeAttackAbilityClass", + "HackSuperWeaponAbilityClass", + "HardPointClass", + "HeroClashInterface", + "HeroClashManagerClass", + "HeroClashManagerInterface", + "HolsterWeaponBehaviorClass", + "IAutoResolveClass", + "ICombatantBehaviorClass", + "InvulnerableBehaviorClass", + "InvulnerableDataPackClass", + "IonCannonShotAttackAbilityClass", + "LeechShieldsAbilityClass", + "LobbingSuperweaponBehaviorClass", + "LuckyShotAttackAbilityClass", + "MaximumFirepowerAttackAbilityClass", + "MeleeHeroClashClass", + "OppositeSidesOfShieldClass", + "PermanentWeaponSwapAbilityClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "ProjectileBehaviorClass", + "ProjectileDataPackClass", + "RepairHardpointEventClass", + "ShieldBehaviorClass", + "ShieldFlareAbilityClass", + "SpawnSpecialWeaponClass", + "SpecialWeaponBehaviorClass", + "SpecialWeaponDataPackClass", + "SpecialWeaponFireEventClass", + "SpecialWeaponSourceDummyBehaviorClass", + "SquadronCombatantClass", + "TacticalCombatantDataPackClass", + "TacticalSuperWeaponBehaviorClass", + "TacticalSuperWeaponDataPackClass", + "TacticalSuperWeaponEventClass", + "TacticalSuperWeaponTargetDummyBehaviorClass", + "TargetingBehaviorClass", + "TargetingInterfaceClass", + "TargetingPrioritySetManagerClass", + "TeamTargetingBehaviorClass", + "TractorBeamAttackAbilityClass", + "TurretBehaviorClass", + "UnitDamagedDataClass", + "WeaponBehaviorClass" + ] + }, + { + "name": "ai_perception", + "description": "AI decision-making, perception systems, evaluators, goals, budgets, and combatant logic", + "class_count": 230, + "trainer_relevance": "high", + "key_base_classes": [ + "AIDiagnosticsNodeClass", + "PerceptualEvaluatorClass", + "SignalListenerClass", + "PerceptualEvaluatorDiagnosticsNodeClass", + "AIDiagnosticsClass", + "ServicedAISystemClass", + "RefCountClass", + "LuaUserVar", + "CollisionClass", + "CollisionDiagnosticsNodeClass" + ], + "classes": [ + "?$DatabaseObjectManagerClass", + "?$DynamicEnumConversionClass", + "?$DynamicVectorClass", + "?$EnumConversionClass", + "?$FoggedPerceptualEvaluatorClass", + "?$FoggedPerceptualUnitEvaluatorClass", + "?$LuaMemberFunctionWrapper", + "?$MultiLinkedListClass", + "?$PooledObjectClass", + "?$Singleton", + "?$SingletonInstance", + "?$VisibleEvaluatorDiagnosticsNodeClass", + "?$VisiblePerceptualEvaluatorClass", + "AIBudgetClass", + "AIBudgetDiagnosticsNodeClass", + "AIBudgetedCategoryDiagnosticsNodeClass", + "AIBudgetedGoalDiagnosticsNodeClass", + "AIBuildTaskClass", + "AIDataPackClass", + "AIDiagnosticsClass", + "AIDiagnosticsManagerClass", + "AIDiagnosticsNodeClass", + "AIDiagsNode", + "AIExecutionPathFinderClass", + "AIExecutionSystemClass", + "AIExecutionSystemDiagnosticsNodeClass", + "AIFreeStoreClass", + "AIGoalCategoryDiagnosticsNodeClass", + "AIGoalProposalFunctionSetClass", + "AIGoalSystemClass", + "AIGoalSystemDiagnosticsNodeClass", + "AIGoalTargetPerceptionFunctionNodeClass", + "AIGoalTargetPerceptionNodeClass", + "AIHintZoneClass", + "AIInstantiatedGoalDiagnosticsNodeClass", + "AILearningSystemClass", + "AILearningSystemDiagnosticsNodeClass", + "AILogWindow", + "AIPerceptionSystemClass", + "AIPerceptionSystemDiagnosticsNodeClass", + "AIPlanBehaviorDiagnosticsNodeClass", + "AIPlanetBuildQueueClass", + "AIPlanetBuildTaskClass", + "AIPlanningSystemClass", + "AIPlanningSystemDiagnosticsNodeClass", + "AIPlayerClass", + "AIPlayerDiagnosticsNodeClass", + "AIProposedGoalCategoryDiagnosticsNodeClass", + "AIProposedSetNodeClass", + "AITargetLocationClass", + "AITargetLocationWrapper", + "AITargetPerceptionNodeClass", + "AITaskForceDiagnosticsNodeClass", + "AITemplateSystemClass", + "AITemplateSystemDiagnosticsNodeClass", + "BudgetedCategoryStruct", + "BudgetedGoalStruct", + "CollisionClass", + "CollisionDiagnosticsNodeClass", + "CollisionSystemClass", + "CollisionSystemDiagnosticsNodeClass", + "DatabaseObjectManagerClass", + "DatabaseObjectManagerClass", + "DifficultyAdjustmentManagerClass", + "DynamicEnumConversionClass", + "DynamicVectorClass", + "EnumConversionClass", + "EvaluatePerceptionClass", + "FoggedPerceptualEvaluatorClass", + "FoggedPerceptualEvaluatorClass", + "FoggedPerceptualUnitEvaluatorClass", + "FoggedPerceptualUnitEvaluatorClass", + "FunctionPerceptualEvaluatorClass", + "GalacticGamePerceptualEvaluatorClass", + "GalacticGoalSystemClass", + "GalacticPerceptionSystemClass", + "GalacticPlayerPerceptualEvaluatorClass", + "GalacticTaskForceClass", + "GameForcePerceptualEvaluatorClass", + "GameObjectPerceptualEvaluatorClass", + "GameObjectTypePerceptualEvaluatorClass", + "GamePerceptualEvaluatorClass", + "GoalLearningDiagnosticsNodeClass", + "HintEvaluatorDiagnosticsNodeClass", + "HintPerceptualEvaluatorClass", + "InstantiatedGoalClass", + "LandGamePerceptualEvaluatorClass", + "LandGoalSystemClass", + "LandLocationPerceptualEvaluatorClass", + "LandPerceptionSystemClass", + "LandPlayerPerceptualEvaluatorClass", + "LandStructurePerceptualEvaluatorClass", + "LandTaskForceClass", + "LandUnitPerceptualEvaluatorClass", + "LuaMemberFunctionWrapper", + "MovingVsMovingCollisionClass", + "MovingVsMovingCollisionDiagnosticsNodeClass", + "MovingVsStoppedCollisionClass", + "MovingVsStoppedCollisionDiagnosticsNodeClass", + "MultiLinkedListClass", + "PerceptionFunctionClass", + "PerceptionGridEntryClass", + "PerceptionParameterBindingsClass", + "PerceptualEvaluatorClass", + "PerceptualEvaluatorDiagnosticsNodeClass", + "PlanBehaviorClass", + "PlanLearningDiagnosticsNodeClass", + "PlanetAITargetLocationClass", + "PlanetEnemyForcePerceptualEvaluatorClass", + "PlanetForcePerceptualEvaluatorClass", + "PlanetFriendlyForcePerceptualEvaluatorClass", + "PlanetPerceptualEvaluatorClass", + "PlanetTypePerceptualEvaluatorClass", + "PlayerPerceptualEvaluatorClass", + "PooledObjectClass", + "PooledObjectClass", + "RecentGoalLearningDiagnosticsNodeClass", + "RecentPlanLearningDiagnosticsNodeClass", + "RootAIDiagnosticsNodeClass", + "RootLookupDiagnosticsNodeClass", + "RootLookupPerceptualEvaluatorClass", + "ScriptPerceptualEvaluatorClass", + "ServicedAISystemClass", + "Singleton", + "SingletonInstance", + "SpaceGamePerceptualEvaluatorClass", + "SpaceGoalSystemClass", + "SpaceLocationPerceptualEvaluatorClass", + "SpacePerceptionSystemClass", + "SpacePlayerPerceptualEvaluatorClass", + "SpaceShipPerceptualEvaluatorClass", + "SpaceStructurePerceptualEvaluatorClass", + "SpaceTaskForceClass", + "StoppedVsStoppedCollisionClass", + "StoppedVsStoppedCollisionDiagnosticsNodeClass", + "TacticalAIDiagnosticsNodeClass", + "TacticalAIManagerClass", + "TacticalGamePerceptualEvaluatorClass", + "TacticalLocationPerceptualEvaluatorClass", + "TacticalObjectPerceptualEvaluatorClass", + "TacticalPerceptionGridClass", + "TacticalPlayerPerceptualEvaluatorClass", + "TacticalRegionAITargetLocationClass", + "TacticalStructurePerceptualEvaluatorClass", + "TacticalUnitAITargetLocationClass", + "TacticalUnitPerceptualEvaluatorClass", + "TaskForceClass", + "TerrainCollisionClass", + "TerrainCollisionDiagnosticsNodeClass", + "TheAITemplateManagerClass", + "TheEvaluatorScriptManagerClass", + "ThePerceptionFunctionManagerClass", + "UnitSurvivalDiagnosticsNodeClass", + "VisibleEvaluatorDiagnosticsNodeClass", + "VisibleEvaluatorDiagnosticsNodeClass", + "VisibleEvaluatorDiagnosticsNodeClass", + "VisibleEvaluatorDiagnosticsNodeClass", + "VisibleEvaluatorDiagnosticsNodeClass", + "VisibleEvaluatorDiagnosticsNodeClass", + "VisibleEvaluatorDiagnosticsNodeClass", + "VisibleEvaluatorDiagnosticsNodeClass", + "VisibleEvaluatorDiagnosticsNodeClass", + "VisibleEvaluatorDiagnosticsNodeClass", + "VisibleEvaluatorDiagnosticsNodeClass", + "VisibleEvaluatorDiagnosticsNodeClass", + "VisibleEvaluatorDiagnosticsNodeClass", + "VisibleEvaluatorDiagnosticsNodeClass", + "VisibleEvaluatorDiagnosticsNodeClass", + "VisibleEvaluatorDiagnosticsNodeClass", + "VisibleEvaluatorDiagnosticsNodeClass", + "VisibleEvaluatorDiagnosticsNodeClass", + "VisibleEvaluatorDiagnosticsNodeClass", + "VisiblePerceptualEvaluatorClass", + "VisiblePerceptualEvaluatorClass", + "VisiblePerceptualEvaluatorClass", + "VisiblePerceptualEvaluatorClass", + "VisiblePerceptualEvaluatorClass", + "VisiblePerceptualEvaluatorClass", + "VisiblePerceptualEvaluatorClass", + "VisiblePerceptualEvaluatorClass", + "VisiblePerceptualEvaluatorClass", + "VisiblePerceptualEvaluatorClass", + "VisiblePerceptualEvaluatorClass", + "VisiblePerceptualEvaluatorClass", + "VisiblePerceptualEvaluatorClass", + "VisiblePerceptualEvaluatorClass", + "VisiblePerceptualEvaluatorClass", + "VisiblePerceptualEvaluatorClass", + "VisiblePerceptualEvaluatorClass", + "VisiblePerceptualEvaluatorClass", + "VisiblePerceptualEvaluatorClass" + ] + }, + { + "name": "movement_locomotor", + "description": "Locomotors, movement controllers, pathfinding, navigation, and position events", + "class_count": 129, + "trainer_relevance": "high", + "key_base_classes": [ + "LocomotorCommonClass", + "SignalListenerClass", + "BaseEventFactoryClass", + "ScheduledEventClass", + "SingletonManager", + "BlockingStatus", + "LuaUserVar", + "DatabaseUIntConversionClass", + "CollisionBodyClass", + "LocomotorInterfaceClass" + ], + "classes": [ + "?$DynamicEnumConversionClass", + "?$DynamicVectorClass", + "?$EnumConversionClass", + "?$EventFactoryClass", + "?$LuaMemberFunctionWrapper", + "?$PooledObjectClass", + "?$Singleton", + "?$SingletonInstance", + "BaseDistanceConstraintClass", + "BikeLocomotorBehaviorClass", + "BoxCollisionBodyClass", + "CollisionBodyClass", + "DistanceConstraintClass", + "DynamicEnumConversionClass", + "DynamicLandPathFinderClass", + "DynamicVectorClass", + "EnumConversionClass", + "EnumConversionClass", + "EnumConversionClass", + "EnumConversionClass", + "EventFactoryClass", + "EventFactoryClass", + "EventFactoryClass", + "EventFactoryClass", + "EventFactoryClass", + "EventFactoryClass", + "EventFactoryClass", + "FighterLocomotorBehaviorClass", + "FleetLocomotorBehaviorClass", + "FlyingLocomotorBehaviorClass", + "FormationBlockStatus", + "FormationClass", + "FreeStoreMovementBlockStatus", + "GalacticExecutionPathFinderClass", + "GetMostDefendedPositionClass", + "JetPackLocomotorBehaviorClass", + "LandBomberLocomotorBehaviorClass", + "LandExecutionPathFinderClass", + "LandMovementBlockStatus", + "LandMovementCoordinatorClass", + "LandPathFinderClass", + "LandTeamContainerLocomotorBehaviorClass", + "LandTeamInfantryLocomotorBehaviorClass", + "LocomotorBehaviorClass", + "LocomotorCommonClass", + "LocomotorDataPackClass", + "LocomotorInterfaceClass", + "LuaCreatePosition", + "LuaMemberFunctionWrapper", + "MoveToGarrisonEventClass", + "MoveToObjectEventClass", + "MoveToPositionEventClass", + "MoveToPositionFacingEventClass", + "MoveToRayEventClass", + "MoveToRayFacingEventClass", + "MovementBlockStatus", + "MovementCoordinatorClass", + "MovementCoordinatorSystemClass", + "PlaneCollisionBodyClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PositionWrapper", + "SimpleSpaceLocomotorBehaviorClass", + "Singleton", + "Singleton", + "Singleton", + "Singleton", + "SingletonInstance", + "SingletonInstance", + "SingletonInstance", + "SingletonInstance", + "SingletonInstance", + "SingletonInstance", + "SingletonInstance", + "SpaceMovementBlockStatus", + "SpaceMovementCoordinatorClass", + "SpacePathFinderClass", + "StarshipLocomotorBehaviorClass", + "StopMovementEventClass", + "TeamLocomotorBehaviorClass", + "TheMovementClassManagerClass", + "UnitMovementBlockStatus", + "WalkLocomotorBehaviorClass", + "WaypointLineSubClass" + ] + }, + { + "name": "abilities", + "description": "Special abilities, ability classes, and their associated data packs", + "class_count": 122, + "trainer_relevance": "high", + "key_base_classes": [ + "SpecialAbilityClass", + "BaseEventFactoryClass", + "DatabaseUIntConversionClass", + "ScheduledEventClass", + "SignalListenerClass", + "BaseDataPackClass", + "ReachabilityClass", + "IXMLLoadableClass", + "RefCountClass" + ], + "classes": [ + "?$BitfieldConversionClass", + "?$DynamicVectorClass", + "?$EnumConversionClass", + "?$EventFactoryClass", + "?$PooledObjectClass", + "AbilityBlockStatus", + "AbilityCountdownBehaviorClass", + "AbilityCountdownDataPackClass", + "AbilitySignalDataClass", + "AbsorbBlasterAbilityClass", + "BaseDestructionAbilityClass", + "BasePowerAbilityClass", + "BattlefieldModifierAbilityClass", + "BerserkerAbilityClass", + "BitfieldConversionClass", + "BlackMarketAbilityClass", + "BlastAbilityClass", + "BountyOnFactionAbilityClass", + "BuzzDroidsAbilityClass", + "CancelSpecialAbilityClass", + "ClusterBombAbilityClass", + "CorruptPlanetAbilityClass", + "CorruptSystemsAbilityClass", + "CorruptionAbilityClass", + "DemolitionAbilityClass", + "DetectHeroAbilityClass", + "DrainLifeAbilityClass", + "DynamicVectorClass", + "EliminateHeroAbilityClass", + "EnableAbilityAbilityClass", + "EnableRadarAbilityClass", + "EnhanceDefenseAbilityClass", + "EnumConversionClass", + "EnumConversionClass", + "EnumConversionClass", + "EnumConversionClass", + "EnumConversionClass", + "EventFactoryClass", + "EventFactoryClass", + "EventFactoryClass", + "EventFactoryClass", + "EventFactoryClass", + "FindWeaknessAbilityClass", + "ForceCloakAbilityClass", + "ForceConfuseAbilityClass", + "ForceHealingAbilityClass", + "ForceLightningAbilityClass", + "ForceSightAbilityClass", + "ForceTelekinesisAbilityClass", + "ForceWhirlwindAbilityClass", + "GalacticSabotageAbilityClass", + "GalacticStealthAbilityClass", + "GalaxyWideUpgradeAbilityClass", + "GarrisonUpgradeAbilityClass", + "HackAbilityClass", + "HeroAssassinAbilityClass", + "HeroProtectionAbilityClass", + "IncomeStreamAbilityClass", + "IncomeStreamModAbilityClass", + "InfectionAbilityClass", + "LaserDefenseAbilityClass", + "NeutralizeHeroAbilityClass", + "PersonalFlameThrowerAbilityClass", + "PlanetDestructionAbilityClass", + "PlanetIncomeBonusAbilityClass", + "PlanetIncomeGamblingAbilityClass", + "PlanetReachabilityClass", + "PoliticalControlBonusAbilityClass", + "PoliticalControlProtectionAbilityClass", + "PoliticalTransitionBonusAbilityClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "ProximityMinesAbilityClass", + "RadioactiveContaminateAbilityClass", + "ReachabilityClass", + "RedirectBlasterAbilityClass", + "ReduceProductionPriceAbilityClass", + "ReduceProductionTimeAbilityClass", + "ReduceTechnologyPriceAbilityClass", + "RemoteBombAbilityClass", + "RemoveCorruptionAbilityClass", + "RepairAbilityClass", + "RetreatPreventionAbilityClass", + "RetreatProtectionAbilityClass", + "SaberThrowAbilityClass", + "SabotagePoliticalControlAbilityClass", + "SensorJammingAbilityClass", + "SetAbilityAutofireEventClass", + "SetUnitAbilityModeEventClass", + "SiphonCreditsAbilityClass", + "SlicerAbilityClass", + "SpawnAbilityClass", + "SpecialAbilityActionClass", + "SpecialAbilityClass", + "SpecialAbilityDataClass", + "SpecialAbilityDataPackClass", + "SpecialAbilityEventClass", + "SpecialAbilityManagerClass", + "StarbaseUpgradeAbilityClass", + "StealthAbilityClass", + "StoryEventCorruptionLevelClass", + "StunAbilityClass", + "SummonAbilityClass", + "SuperLaserAbilityClass", + "SystemSpyAbilityClass", + "TacticalBribeAbilityClass", + "TacticalSpecialAbilityEventClass", + "TacticalSpecialAbilityWithDummyTargetEventClass", + "VehicleThiefAbilityClass", + "WeatherproofAbilityClass", + "ZoneReachabilityClass" + ] + }, + { + "name": "economy_resources", + "description": "Credits, production queues, build pads, reinforcement, and resource management", + "class_count": 76, + "trainer_relevance": "high", + "key_base_classes": [ + "BehaviorClass", + "BlockingStatus", + "InstantiationMarshallerClass", + "LuaUserVar", + "BaseEventFactoryClass", + "ScheduledEventClass", + "SignalListenerClass", + "GenericMultiLinkedList", + "alD3dResource", + "BaseDataPackClass" + ], + "classes": [ + "?$DynamicVectorClass", + "?$EnumConversionClass", + "?$EventFactoryClass", + "?$FiniteStateMachineClass", + "?$LuaMemberFunctionWrapper", + "?$MultiLinkedListClass", + "?$PooledObjectClass", + "?$ReferenceListClass", + "BlackMarketItemClass", + "BudgetBlockStatus", + "BudgetInstantiationMarshallerClass", + "BudgetWrapper", + "BudgetedCategoryStruct", + "BudgetedGoalStruct", + "DynamicVectorClass", + "DynamicVectorClass", + "EnumConversionClass", + "EnumConversionClass", + "EventFactoryClass", + "EventFactoryClass", + "EventFactoryClass", + "FiniteStateMachineClass", + "GarrisonDataPackClass", + "GarrisonEventClass", + "GarrisonHoverTransportBehaviorClass", + "GarrisonStructureBehaviorClass", + "GarrisonTransportBehaviorClass", + "GarrisonUnitBehaviorClass", + "GarrisonVehicleBehaviorClass", + "GarrisonableBehaviorClass", + "LuaMemberFunctionWrapper", + "LuaReinforceCommandClass", + "MultiLinkedListClass", + "MultiLinkedListClass", + "PersistentUpgradeObjectStruct>", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "ProductionBehaviorClass", + "ProductionBlockStatus", + "ProductionDataPackClass", + "ProductionEventClass", + "ProductionInstantiationMarshallerClass", + "ReferenceListClass", + "ReinforceBlockStatus", + "ReinforceEventClass", + "ReinforcementInstantiationMarshallerClass", + "ReinforcementPointBehaviorClass", + "ReinforcementsUnloadedDataClass", + "SpaceReinforceBlockStatus", + "SpaceReinforceFOWSampler", + "SpawnFromReinforcePoolClass", + "TechTreeInstantiationMarshallerClass", + "UpgradeDummyBehaviorClass", + "alD3dDepthBuffer", + "alD3dEffect", + "alD3dRenderTarget", + "alD3dResource" + ] + }, + { + "name": "story_campaign", + "description": "Story scripting, campaign progression, galactic conquest events, and mission flow", + "class_count": 75, + "trainer_relevance": "medium", + "key_base_classes": [ + "StoryEventClass", + "LuaUserVar", + "GameModeClass", + "SignalGeneratorClass", + "SignalListenerClass", + "DatabaseUIntConversionClass" + ], + "classes": [ + "?$DynamicVectorClass", + "?$EnumConversionClass", + "?$LuaMemberFunctionWrapper", + "?$PooledObjectClass", + "CheckStoryFlagClass", + "DynamicVectorClass", + "DynamicVectorClass_>", + "DynamicVectorClass", + "DynamicVectorClass", + "DynamicVectorClass", + "DynamicVectorClass", + "EnumConversionClass", + "EnumConversionClass", + "FactionDataOverrideClass_>", + "FactionDataOverrideClass_>", + "GalacticModeClass", + "GameModeClass", + "GameModeManagerClass", + "GetStoryPlotClass", + "InvasionBlockStatus", + "InvasionResultClass", + "IsCampaignGameClass", + "LandModeClass", + "LuaGetGameMode", + "LuaMemberFunctionWrapper", + "LuaMemberFunctionWrapper", + "LuaStoryEventClass", + "PooledObjectClass", + "PooledObjectClass", + "SpaceModeClass", + "StartingForceClass>", + "StoryCheckDestroyedClass", + "StoryEventAINotificationClass", + "StoryEventAccumulateClass", + "StoryEventBeginEraClass", + "StoryEventCaptureClass", + "StoryEventClass", + "StoryEventCommandUnitClass", + "StoryEventConquerCountClass", + "StoryEventConstructLevelClass", + "StoryEventDestroyBaseClass", + "StoryEventDestroyClass", + "StoryEventDifficultyClass", + "StoryEventElapsedClass", + "StoryEventEnterClass", + "StoryEventFlagClass", + "StoryEventFogRevealClass", + "StoryEventGuardUnitClass", + "StoryEventHeroMoveClass", + "StoryEventLoadTacticalClass", + "StoryEventMovieDoneClass", + "StoryEventObjectiveTimeoutClass", + "StoryEventProximityClass", + "StoryEventRetreatClass", + "StoryEventSelectPlanetClass", + "StoryEventSingleObjectNameClass", + "StoryEventStartTacticalClass", + "StoryEventStringClass", + "StoryEventVictoryClass", + "StoryEventWinBattlesClass", + "StoryEventWrapper", + "StoryLogWindow", + "StoryPlotWrapper", + "StorySubPlotClass" + ] + }, + { + "name": "multiplayer_steam", + "description": "Steam integration, lobby management, peer networking, and multiplayer session handling", + "class_count": 56, + "trainer_relevance": "low", + "key_base_classes": [ + "$0BI::?$CCallbackImpl", + "CCallbackBase", + "SingletonManager", + "ThreadClass", + "RegistryProfileClass", + "RefCountClass", + "PGNetAsyncSocketImpl" + ], + "classes": [ + "?$CCallResult", + "?$CCallback", + "?$DynamicVectorClass", + "?$Singleton", + "?$SingletonInstance", + "BitStreamClass", + "CCallResult", + "CCallResult", + "CCallResult", + "CCallResult", + "CCallback", + "CCallback", + "CCallback", + "CCallback", + "CCallback", + "CCallback", + "CCallback", + "CCallback", + "ClanStruct>", + "DynamicVectorClass", + "DynamicVectorClass", + "DynamicVectorClass", + "LuaIsMultiplayerMode", + "NATUtilsThreadClass", + "NetworkProfileClass", + "PGNetAsyncSocketImpl", + "PacketClass", + "PacketHandlerClass", + "PlayerProfileClass", + "RegistryProfileClass", + "Singleton", + "SingletonInstance", + "SingletonInstance", + "SingletonInstance", + "SteamAsyncSocketImpl", + "SteamCallbacksImpl", + "SteamClass", + "ThreadTaskType>", + "WinsockAsyncSocketImpl" + ] + }, + { + "name": "ui_display", + "description": "HUD elements, dialogs, GUI components, radar, tooltips, text rendering, and reticles", + "class_count": 80, + "trainer_relevance": "medium", + "key_base_classes": [ + "LuaUserVar", + "GUIGadgetClass", + "BaseComponentClass", + "StringTextureClass", + "RefCountClass", + "BaseDataPackClass", + "BehaviorClass", + "DatabaseUIntConversionClass" + ], + "classes": [ + "?$DynamicVectorClass", + "?$EnumConversionClass", + "?$EventFactoryClass", + "?$PooledObjectClass", + "?$SingletonInstance", + "ActivateRetryDialog", + "AddRadarBlipClass", + "BarComponentClass", + "BaseComponentClass", + "ButtonComponentClass", + "CMFCToolTipInfo", + "Draw3DTextCrawlClass", + "Draw3DTextCrawlDataClass", + "DrawTextClass", + "DynamicVectorClass", + "DynamicVectorClass", + "DynamicVectorClass", + "DynamicVectorClass", + "DynamicVectorClass", + "DynamicVectorClass", + "DynamicVectorClass", + "EnumConversionClass", + "EnumConversionClass", + "EventFactoryClass", + "GUIButtonIconClass", + "GUIComponentBlinkClass", + "GUIComponentColorClass", + "GUIComponentEnableClass", + "GUIComponentFlashClass", + "GUIComponentPlayAnimClass", + "GUIComponentStopAnimClass", + "GUIComponentStopFlashClass", + "GUIComponentTextClass", + "GUIComponentVisibilityClass", + "GUIDataPackClass", + "GUIGadgetCheckboxClass", + "GUIGadgetClass", + "GUIGadgetComboboxClass", + "GUIGadgetCustomImageClass", + "GUIGadgetEditboxClass", + "GUIGadgetFrameClass", + "GUIGadgetIMEEditboxClass", + "GUIGadgetIMELocaleIndicatorClass", + "GUIGadgetListboxClass", + "GUIGadgetProgressbarClass", + "GUIGadgetPushbuttonClass", + "GUIGadgetRadiobuttonClass", + "GUIGadgetTextClass", + "GUIGadgetTrackbarSliderClass", + "GUIGadgetVScrollbarClass", + "GUITextColorClass", + "IconComponentClass", + "LandBaseLevelComponentDummyBehaviorClass", + "ModelComponentClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "ProgressBarClass*___ptr64>", + "RadarMapDataPackClass", + "RemoveRadarBlipClass", + "Scene2dClass", + "SelectComponentClass", + "SetGUIIndexEventClass", + "ShellComponentClass", + "SingletonInstance", + "StringTextureClass", + "TeletypeTextClass", + "TextButtonComponentClass", + "TextComponentClass", + "TooltipDummyBehaviorClass" + ] + }, + { + "name": "units_objects", + "description": "Game object types, squads, units, fleets, transports, and entity management", + "class_count": 176, + "trainer_relevance": "high", + "key_base_classes": [ + "BehaviorClass", + "DatabaseUIntConversionClass", + "LuaUserVar", + "InstantiationMarshallerClass", + "BaseDataPackClass", + "SignalDataClass", + "AIFreeStoreClass", + "SignalGeneratorClass", + "RefCountClass", + "BlockingStatus" + ], + "classes": [ + "?$BitfieldConversionClass", + "?$DatabaseObjectManagerClass", + "?$DynamicBitfieldConversionClass", + "?$DynamicVectorClass", + "?$DynamicVectorRefClass", + "?$EnumConversionClass", + "?$EventFactoryClass", + "?$LuaMemberFunctionWrapper", + "?$MultiLinkedListClass", + "?$PooledObjectClass", + "?$ReferenceListClass", + "?$SingletonInstance", + "AssembleFleetClass", + "BitfieldConversionClass", + "BitfieldConversionClass", + "ContrastInstantiationMarshallerClass", + "CreationParamsStruct>", + "DatabaseObjectManagerClass", + "DatabaseObjectManagerClass", + "DatabaseObjectManagerClass", + "DatabaseObjectManagerClass", + "DatabaseObjectManagerClass", + "DatabaseObjectManagerClass", + "DatabaseObjectManagerClass", + "DynamicBitfieldConversionClass", + "DynamicBitfieldConversionClass", + "DynamicVectorClass_>", + "DynamicVectorClass", + "DynamicVectorClass", + "DynamicVectorClass", + "DynamicVectorClass", + "DynamicVectorClass", + "DynamicVectorClass", + "DynamicVectorClass", + "DynamicVectorClass", + "DynamicVectorClass", + "DynamicVectorClass", + "DynamicVectorClass", + "DynamicVectorRefClass", + "DynamicVectorRefClass", + "EnumConversionClass", + "EnumConversionClass", + "EnumConversionClass", + "EnumConversionClass", + "EventFactoryClass", + "FactionTypeConverterClass", + "FleetBehaviorClass", + "FleetDataPackClass", + "FleetManagementEventClass", + "FleetMergeResult", + "FreeStoreInstantiationMarshallerClass", + "GalacticFreeStoreClass", + "GameObjectClass", + "GameObjectManagerClass", + "GameObjectTypeByteMembersClass", + "GameObjectTypeClass", + "GameObjectTypeWrapper", + "GetNextStarbaseTypeClass", + "GroundCompanyDummyBehaviorClass", + "GroundStructureDummyBehaviorClass", + "HeroInstantiationMarshallerClass", + "IndigenousUnitBehaviorClass", + "InstantiationMarshallerClass", + "LandFreeStoreClass", + "LandUnitsBlockStatus", + "LeaderTrailStruct>", + "LuaCreateCinematicTransport", + "LuaMemberFunctionWrapper", + "LuaMemberFunctionWrapper", + "LuaMemberFunctionWrapper", + "LuaSpawnUnitCommandClass", + "MultiLinkedListClass", + "ObjectOwnerChangedDataClass", + "ObjectTrackingSystemClass", + "ObjectUnderConstructionClass", + "OpportunityTargetAcquiredDataClass", + "OrbitalStructureDummyBehaviorClass", + "PlanDefinitionManagerClass", + "PlanInstantiationMarshallerClass", + "PlanetFactionChangeClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "ProjectByUnitRangeClass", + "ReferenceListClass", + "SingletonInstance", + "SpaceFreeStoreClass", + "SpawnIndigenousUnitsBehaviorClass", + "SpawnIndigenousUnitsDataPackClass", + "SpawnSquadronBehaviorClass", + "SpawnSquadronDataPackClass", + "SquadLeaderBehaviorClass", + "SquadLeaderDataPackClass", + "SquadronDummyBehaviorClass", + "StarBaseDummyBehaviorClass", + "SurvivalInstantiationMarshallerClass", + "TacticalBuildInstantiationMarshallerClass", + "TeamDef", + "TerrainTypeInstantiationMarshallerClass", + "TheAIGoalProposalFunctionSetManagerClass", + "TheAIGoalTypeManagerClass", + "TransportBehaviorClass", + "TransportDataPackClass", + "TransportLandingBehaviorClass", + "UnitAIBehaviorClass", + "UnitAnimationBlockStatus", + "WaitForStarbaseClass", + "tPersistentUnit>" + ] + }, + { + "name": "map_terrain", + "description": "Map systems, planets, terrain rendering, space/land/galactic modes, and Alamo renderer internals", + "class_count": 275, + "trainer_relevance": "medium", + "key_base_classes": [ + "RefCountClass", + "alD3dSimpleEngineParam", + "alRenderable", + "alD3dVertexTypeInterface", + "LuaUserVar", + "alRenderTask", + "MultiLinkedListMember", + "BehaviorClass", + "alD3dEffectParamInterface", + "GenericMultiLinkedList" + ], + "classes": [ + "?$BitfieldConversionClass", + "?$DynamicVectorClass", + "?$EnumConversionClass", + "?$EventFactoryClass", + "?$LuaMemberFunctionWrapper", + "?$LuaValue", + "?$MultiLinkedListClass", + "?$PooledObjectClass", + "?$ReferenceListClass", + "?$alD3dEffectParam", + "?$alD3dVertexType", + "AddPlanetHighlightClass", + "BitfieldConversionClass", + "BitfieldConversionClass", + "CullObjectClass", + "CullSystemClass", + "DestroyAfterPlanetaryBombardDummyBehaviorClass", + "DrawLineSegmentClass", + "DrawPrimitiveClass", + "DrawRectClass", + "DualPass*___ptr64>", + "DynamicVectorClass", + "DynamicVectorClass", + "DynamicVectorClass", + "DynamicVectorClass", + "DynamicVectorClass", + "DynamicVectorClass", + "DynamicVectorClass", + "DynamicVectorClass", + "DynamicVectorClass", + "EffectTex>", + "EnumConversionClass", + "EnumConversionClass", + "EnumConversionClass", + "EvaluateGalacticContextClass", + "EventFactoryClass", + "EventFactoryClass", + "FOWSamplerClass", + "FillRectClass", + "FindNearestSpaceFieldClass", + "FindPlanetClass", + "GalacticCameraClass", + "GalacticSellEventClass", + "HandlerMap4", + "IsPointInAsteroidFieldClass", + "IsPointInNebulaClass", + "LandObstacleBehaviorClass", + "LandObstacleDataPackClass", + "LandRetreatCoordinatorClass", + "LandingBehaviorClass", + "LightningEffectClass", + "LineLinkStruct>", + "LineSubClass", + "LuaMemberFunctionWrapper", + "LuaMemberFunctionWrapper", + "LuaMemberFunctionWrapper", + "LuaMemberFunctionWrapper", + "LuaPromoteToSpaceCinematicLayer", + "LuaResumeHyperspaceIn", + "LuaStartCinematicSpaceRetreat", + "Mtl>", + "MultiLinkedListClass", + "MultiLinkedListClass", + "MultiLinkedListClass", + "NebulaBehaviorClass", + "OcclusionVert>", + "Pass*___ptr64>", + "PersistentTacticalBuiltObjectStruct>", + "PlanDefinitionClass", + "PlanetaryBehaviorClass", + "PlanetaryBombardEventClass", + "PlanetaryDataPackClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "ReferenceListClass", + "ReferenceListClass", + "RemovePlanetHighlightClass", + "Shadow_Blob_Bucket>", + "SkyDomeBehaviorClass", + "SpaceAmbushBlockStatus", + "SpaceObstacleBehaviorClass", + "SpaceRetreatCoordinatorClass", + "TerrainHeightSamplerClass", + "TerrainTextureModificationBehaviorClass", + "TextureRectClass", + "TradeRouteLinkEntryClass>", + "TreeCullClass", + "TryBlockMap4", + "UWMap4", + "Vert>", + "Zbounds>", + "al3DCable", + "alAnim", + "alAssetManager", + "alBlob", + "alBlobFlushTask", + "alD3dCloudTextureEngineParam", + "alD3dCloudUEngineParam", + "alD3dCloudVEngineParam", + "alD3dDistanceFadeParam", + "alD3dEffectInstance", + "alD3dEffectParam", + "alD3dEffectParam", + "alD3dEffectParam", + "alD3dEffectParam", + "alD3dEffectParam", + "alD3dEffectParamInterface", + "alD3dEffectStateManager", + "alD3dEngineParamInterface", + "alD3dEyeObjPosParam", + "alD3dEyePosParam", + "alD3dFOWTextureEngineParam", + "alD3dFOWUEngineParam", + "alD3dFOWVEngineParam", + "alD3dFogParam", + "alD3dIncludeHandler", + "alD3dMesh", + "alD3dResolutionConstantsParam", + "alD3dShadowExtrusionParam", + "alD3dSimpleEngineParam", + "alD3dSkyCubeTextureEngineParam", + "alD3dSphLightAllParam", + "alD3dSphLightFillParam", + "alD3dTerrainDecorationRenderTask", + "alD3dTerrainRender", + "alD3dTerrainRenderTask", + "alD3dTexture", + "alD3dTexture2D", + "alD3dTextureCube", + "alD3dTextureEffectParam", + "alD3dTimeParam", + "alD3dTransitionEffect", + "alD3dTransitionEffectAlphaFade", + "alD3dTransitionEffectLinearWipe", + "alD3dVertexType", + "alD3dVertexType", + "alD3dVertexType", + "alD3dVertexType", + "alD3dVertexType", + "alD3dVertexType", + "alD3dVertexType", + "alD3dVertexType", + "alD3dVertexType", + "alD3dVertexType", + "alD3dVertexType", + "alD3dVertexType", + "alD3dVertexType", + "alD3dVertexType", + "alD3dVertexType", + "alD3dVertexTypeInterface", + "alD3dWaterRenderTask", + "alDepthBuffer", + "alDrawPrimitive", + "alDynamicTerrainTrack", + "alEmitter", + "alExtraRenderPass", + "alHModel", + "alHeightSampler", + "alLaserBeam", + "alLaserBeamFlushTask", + "alLaserKite", + "alLaserKiteFlushTask", + "alLensFlare", + "alLensFlareTask", + "alLightContext", + "alLightScaleParam", + "alLightSource", + "alLightningBolt", + "alLightningBoltRenderTask", + "alMesh", + "alMeshRenderTask", + "alParticleGroup", + "alParticleGroupRenderTask", + "alParticles", + "alParticlesConstants", + "alQuantizedAnim", + "alRawAnim", + "alRawJointAnim>", + "alRenderTarget", + "alRenderTask", + "alRenderable", + "alRenderableMesh", + "alRenderablePlane", + "alScene", + "alSceneIterator", + "alShaderInstance", + "alSimpleRenderTask", + "alSimpleScene", + "alSimpleSceneIterator", + "alSkeletonConstants", + "alSplineRenderer", + "alSplineRendererRenderTask", + "alTerrain", + "alTerrainDecal", + "alTerrainRender", + "alTerrainShadowBlob", + "alTerrainTrack", + "alTexture", + "alWindBendParam", + "alWindGrassParam", + "pair_const_,class_SmartPtr_>_>_>,1>", + "pair_const_,class_SmartPtr_>_>_>,1>,128,class_EmptyLockerClass>" + ] + }, + { + "name": "behaviors", + "description": "Behavior classes and generic data-pack classes not covered by other subsystems", + "class_count": 248, + "trainer_relevance": "medium", + "key_base_classes": [ + "BehaviorClass", + "BaseDataPackClass", + "SignalListenerClass" + ], + "classes": [ + "?$PooledObjectClass", + "?$ReferenceListClass", + "ActionClass", + "AmbientSFXBehaviorClass", + "AmbientSFXDataPackClass", + "AvoidDangerBehaviorClass", + "BarrageAreaBehaviorClass", + "BarrageAreaDataPackClass", + "BaseDataPackClass", + "BeaconBehaviorClass", + "BehaviorClass", + "BoardableBehaviorClass", + "BoardableDataPackClass", + "BombBehaviorClass", + "BombDataPackClass", + "BurningBehaviorClass", + "BuzzDroidsBehaviorClass", + "BuzzDroidsDataPackClass", + "CapturePointBehaviorClass", + "CapturePointDataPackClass", + "CashPointBehaviorClass", + "CashPointDataPackClass", + "ConfuseBehaviorClass", + "CorruptSystemsBehaviorClass", + "CorruptSystemsDataPackClass", + "DebrisBehaviorClass", + "DebrisDataPackClass", + "DemolitionBehaviorClass", + "DemolitionDataPackClass", + "DeployTroopersBehaviorClass", + "DeployTroopersDataPackClass", + "DestroyAfterBombingRunDummyBehaviorClass", + "DisableForceAbilitiesBehaviorClass", + "DynamicTransformBehaviorClass", + "DynamicTransformDataPackClass", + "EarthquakeDataPackClass", + "EarthquakeTargetBehaviorClass", + "EnergyPoolBehaviorClass", + "ExplosionBehaviorClass", + "FlagshipDataPackClass", + "GravityControlFieldBehaviorClass", + "GroundBaseDummyBehaviorClass", + "GuardSpawnHouseBehaviorClass", + "HarassBehaviorClass", + "HideWhenFoggedBehaviorClass", + "HintBehaviorClass", + "HintDataPackClass", + "HuntBehaviorClass", + "IdleBehaviorClass", + "IdleDataPackClass", + "ImposingPresenceBehaviorClass", + "InfectionBehaviorClass", + "InfectionDataPackClass", + "IonStunEffectBehaviorClass", + "LightEffectDataPackClass", + "LureBehaviorClass", + "LureDataPackClass", + "MarkerBehaviorClass", + "MarkerDataPackClass", + "MoveActionClass", + "ParticleBehaviorClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "ProximityMineBehaviorClass", + "ProximityMineDataPackClass", + "ReferenceListClass", + "RevealBehaviorClass", + "SelectBehaviorClass", + "SelectionDataPackClass", + "SelfDestructBehaviorClass", + "SquashBehaviorClass", + "StarshipDummyBehaviorClass", + "StunnableBehaviorClass", + "SurfaceFXBehaviorClass", + "TacticalBuildObjectsBehaviorClass", + "TacticalBuildObjectsDataPackClass", + "TacticalSellBehaviorClass", + "TacticalSellDataPackClass", + "TacticalUnderConstructionBehaviorClass", + "TacticalUnderConstructionDataPackClass", + "TeamBehaviorClass", + "TeamDataPackClass", + "TelekinesisDataPackClass", + "TelekinesisTargetBehaviorClass", + "TreadScrollBehaviorClass", + "VehicleThiefBehaviorClass", + "VehicleThiefDataPackClass", + "WallBehaviorClass", + "WindDisturbanceBehaviorClass" + ] + }, + { + "name": "engine_core", + "description": "Singletons, reference counting, pooled objects, dynamic vectors, schedulers, CRT/stdlib, and core event plumbing", + "class_count": 1273, + "trainer_relevance": "low", + "key_base_classes": [ + "LuaUserVar", + "BaseEventFactoryClass", + "ScheduledEventClass", + "std::locale::facet", + "RefCountClass", + "DatabaseUIntConversionClass", + "SignalListenerClass", + "BlockingStatus", + "alD3dSimpleEngineParam", + "LuaVar" + ], + "classes": [ + "?$BitfieldConversionClass", + "?$BitwiseAnd", + "?$BitwiseOr", + "?$CCallbackImpl", + "?$CalculationOp", + "?$ComparisonOp", + "?$DynamicEnumConversionClass", + "?$DynamicVectorClass", + "?$DynamicVectorRefClass", + "?$EnumConversionClass", + "?$EventFactoryClass", + "?$GreaterThan", + "?$LessThan", + "?$LinkedListEntryClass", + "?$LuaMemberFunctionWrapper", + "?$LuaValue", + "?$MostExtreme", + "?$MultiLinkedListClass", + "?$Multiply", + "?$Plus", + "?$PooledObjectClass", + "?$RangedParameterClass", + "?$ReferenceListClass", + "?$SimpleParameterClass", + "?$Singleton", + "?$SingletonInstance", + "?$_Mpunct", + "?$codecvt", + "?$codecvt_utf8", + "?$collate", + "?$ctype", + "?$messages", + "?$money_get", + "?$money_put", + "?$moneypunct", + "?$num_get", + "?$num_put", + "?$numpunct", + "?$time_get", + "?$time_put", + "?$wstring_convert", + "AllyEventClass", + "ApplyMarkupClass", + "AwaitingVictoryTestType>", + "BaseBlockStatus", + "BaseEventFactoryClass", + "BinkMovieBlockStatus", + "BitfieldConversionClass", + "BitfieldConversionClass", + "BitwiseAnd", + "BitwiseOr", + "BitwiseOr", + "BlockingStatus", + "BombingBlockStatus", + "BombingRunEventClass", + "CCallbackBase", + "CCallbackImpl<16>", + "CCallbackImpl<24>", + "CCallbackImpl<32>", + "CCallbackImpl<8>", + "CCallbackImpl<9>", + "CalculationOp", + "CalculationOp", + "CalculationOp", + "CalculationOp", + "CameraAnimeSpiralEffectClass", + "CameraClass", + "CameraEffectClass", + "CameraEffectTrackerClass", + "CameraShakeEffectClass", + "CancelFastForwardClass", + "ChatEventClass", + "ChunkReaderClass", + "ChunkWriterClass", + "CinematicAnimationEventClass", + "ComparisonOp", + "ComparisonOp", + "ControlGroupEventClass", + "CubicInterpolator", + "DatabaseUIntConversionClass", + "DebugContextStruct", + "DebugEventClass", + "DebugTreeItem", + "DistributeMoneyEventClass", + "DumpCallStack", + "DynamicEnumConversionClass", + "DynamicEnumConversionClass", + "DynamicEnumConversionClass", + "DynamicVectorClass", + "DynamicVectorClass", + "DynamicVectorClass", + "DynamicVectorClass", + "DynamicVectorClass", + "DynamicVectorClass", + "DynamicVectorClass", + "DynamicVectorClass", + "DynamicVectorClass", + "DynamicVectorClass", + "DynamicVectorClass", + "DynamicVectorClass", + "DynamicVectorClass", + "DynamicVectorClass", + "DynamicVectorClass", + "DynamicVectorClass", + "DynamicVectorClass", + "DynamicVectorClass", + "DynamicVectorClass", + "DynamicVectorClass", + "DynamicVectorClass", + "DynamicVectorClass", + "DynamicVectorClass", + "DynamicVectorClass", + "DynamicVectorClass", + "DynamicVectorClass", + "DynamicVectorClass", + "DynamicVectorClass*___ptr64>", + "DynamicVectorClass_>", + "DynamicVectorClass_>", + "DynamicVectorClass_>", + "DynamicVectorClass", + "DynamicVectorClass", + "DynamicVectorClass", + "DynamicVectorClass", + "DynamicVectorClass", + "DynamicVectorClass", + "DynamicVectorClass", + "DynamicVectorClass", + "DynamicVectorClass", + "DynamicVectorClass", + "DynamicVectorClass", + "DynamicVectorClass", + "DynamicVectorClass", + "DynamicVectorClass", + "DynamicVectorClass", + "DynamicVectorClass", + "DynamicVectorClass", + "DynamicVectorClass", + "DynamicVectorClass", + "DynamicVectorClass", + "DynamicVectorClass", + "DynamicVectorClass", + "DynamicVectorClass", + "DynamicVectorClass", + "DynamicVectorClass", + "DynamicVectorClass", + "DynamicVectorClass", + "DynamicVectorClass", + "DynamicVectorClass", + "DynamicVectorClass", + "DynamicVectorClass", + "DynamicVectorClass", + "DynamicVectorClass", + "DynamicVectorClass", + "DynamicVectorClass", + "DynamicVectorClass", + "DynamicVectorClass", + "DynamicVectorClass", + "DynamicVectorClass", + "DynamicVectorClass", + "DynamicVectorClass", + "DynamicVectorRefClass", + "EmitterType>", + "EnableDistanceFogClass", + "EnumConversionClass", + "EnumConversionClass", + "EnumConversionClass", + "EnumConversionClass", + "EnumConversionClass", + "EnumConversionClass", + "EnumConversionClass", + "EnumConversionClass", + "EnumConversionClass", + "EnumConversionClass", + "EnumConversionClass", + "EnumConversionClass", + "EnumConversionClass", + "EnumConversionClass", + "EnumConversionClass", + "Environment>", + "EscortEventClass", + "EvaluateTypeListClass", + "EventClass", + "EventFactoryClass", + "EventFactoryClass", + "EventFactoryClass", + "EventFactoryClass", + "EventFactoryClass", + "EventFactoryClass", + "EventFactoryClass", + "EventFactoryClass", + "EventFactoryClass", + "EventFactoryClass", + "EventFactoryClass", + "EventFactoryClass", + "EventFactoryClass", + "EventFactoryClass", + "EventFactoryClass", + "EventFactoryClass", + "EventFactoryClass", + "EventFactoryClass", + "EventFactoryClass", + "EventFactoryClass", + "EventFactoryClass", + "EventFactoryClass", + "EventFactoryClass", + "EventFactoryClass", + "EventFactoryClass", + "EventFactoryClass", + "EventFactoryClass", + "EventFactoryClass", + "EventFactoryClass", + "EventFactoryClass", + "EventFactoryClass", + "EventLogWindow", + "EventQueueClass", + "ExploreAreaBlockStatus", + "FH4", + "FacingEventClass", + "FileClass", + "FindAllObjectsOfType", + "FindBestLocalThreatCenterClass", + "FindDeadlyEnemyClass", + "FindNearestClass", + "FindPlayerClass", + "FindStageAreaClass", + "FindTargetClass", + "ForeverBlockStatus", + "FrameInfoEventClass", + "FrameSyncEventClass", + "GameObjectWrapper", + "GameOptionsEventClass", + "GenericMultiLinkedList", + "GenericSignalBlockStatus", + "GetEvent", + "GetNextGroundbaseTypeClass", + "GiveDesireBlockStatus", + "GiveDesireBonusClass", + "GlobalValue", + "GreaterThan", + "GreaterThan", + "HistoricallyBuiltObjectType>", + "ID3DXInclude", + "IRefCountClass", + "IRootClass", + "IUnknown", + "Interpolator", + "InvadeEventClass", + "IsPointInIonStormClass", + "LessThan", + "LightningEffectBlockStatus", + "LinearInterpolator", + "LinkedListEntryClass", + "LoadingThreadClass", + "LogWindowItem", + "LookEventClass", + "LuaAddObjectiveClass", + "LuaAllowLocalizedSFX", + "LuaCameraToFollow", + "LuaCinematicZoom", + "LuaConsolePrint", + "LuaCreateGenericObjectClass", + "LuaCreateThread", + "LuaCustomScriptMessage", + "LuaDebugBreak", + "LuaDebugPrint", + "LuaDiscreteDistributionClass", + "LuaDoEndCinematicCleanup", + "LuaEnableFog", + "LuaEndCinematicCamera", + "LuaEndCinematicMode", + "LuaExternalFunction", + "LuaFOWCellsClass", + "LuaFOWRevealCommandClass", + "LuaFadeOff", + "LuaFadeOn", + "LuaFadeScreenIn", + "LuaFadeScreenOut", + "LuaFindAllHintsCommandClass", + "LuaFindMarkerCommandClass", + "LuaFindObjectType", + "LuaFindPathClass", + "LuaForceWeather", + "LuaGameMessageClass", + "LuaGameRandom", + "LuaGetThreadID", + "LuaGetTime", + "LuaHideObject", + "LuaHideSubObject", + "LuaLetterBoxIn", + "LuaLetterBoxOff", + "LuaLetterBoxOn", + "LuaLetterBoxOut", + "LuaLockControls", + "LuaMasterVolumeRestore", + "LuaMessagePopup", + "LuaPlayBinkMovie", + "LuaPlayMusicClass", + "LuaPointCamera", + "LuaRemoveAllText", + "LuaResumeModeBasedMusicClass", + "LuaRotateCameraBy", + "LuaRotateCameraTo", + "LuaSFXCommandsClassClass", + "LuaScriptClass", + "LuaScriptExit", + "LuaScriptMessage", + "LuaScriptWrapper", + "LuaScrollCameraTo", + "LuaSetCinematicCameraKey", + "LuaSetCinematicEnvironment", + "LuaSetCinematicTargetKey", + "LuaSetNewEnvironment", + "LuaStartCinematicCamera", + "LuaStartCinematicMode", + "LuaStopAllMusicClass", + "LuaStopAllSpeech", + "LuaStopBinkMovie", + "LuaStringCompare", + "LuaSuspendAI", + "LuaTransitionCinematicCameraKey", + "LuaTransitionCinematicTargetKey", + "LuaTransitionToTacticalCamera", + "LuaUserVar", + "LuaWeatherAudioPause", + "LuaZoomCamera", + "MainLogWindow", + "MainThreadClass", + "MarkupBlockStatus", + "MessageStruct>", + "ModelAnimsListClass", + "ModelClass", + "MostExtreme", + "MoveObjectToObjectEventClass", + "MoveThroughObjectsEventClass", + "MultiLinkedListClass", + "MultiLinkedListClass", + "MultiLinkedListClass", + "MultiLinkedListMember", + "Multiply", + "NameListStruct", + "ObjectPointerPairClass>", + "OcclusionRenderPassClass", + "OutgoingEventQueueClass", + "PacketIdentifierStruct>", + "ParameterClass", + "PerformanceMetricsEventClass", + "PlaceBeaconEventClass", + "PlayLightningEffectClass", + "PlayerClass", + "PlayerWrapper", + "Plus", + "Plus", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass,128,class_EmptyLockerClass>", + "PooledObjectClass,128,class_EmptyLockerClass>", + "PooledObjectClass,128,class_EmptyLockerClass>", + "PooledObjectClass,128,class_EmptyLockerClass>", + "PooledObjectClass,128,class_EmptyLockerClass>", + "PooledObjectClass,128,class_EmptyLockerClass>", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PooledObjectClass", + "PotentialPlanClass", + "ProduceForceBlockStatus", + "ProduceObjectClass", + "PurgeGoalsClass", + "QuitGameEventClass", + "RAMFileClass", + "RangedParameterClass", + "RefCountClass", + "ReferenceListClass", + "ReferenceListClass", + "ReferenceListClass", + "ReferenceListClass", + "ReferenceListClass", + "ResumeGameEventClass", + "RetreatCoordinatorClass", + "RetreatEventClass", + "RootClass", + "RtsSceneClass", + "SaveGameEventClass", + "SaveGameStruct>", + "SceneClass", + "ScheduledEventClass", + "ScheduledEventQueueClass", + "SelectAllEventClass", + "SelectEventClass", + "SetMarkerIDEventClass", + "SetupPhaseManagerClass", + "SetupPhaseMoveEventClass", + "SetupPhaseTriggerEndEventClass", + "SignalDataClass", + "SignalDispatcherClass", + "SignalGeneratorClass", + "SignalListClass", + "SignalListenerClass", + "SimpleParameterClass", + "SimpleSceneClass", + "Singleton", + "Singleton", + "Singleton", + "Singleton", + "Singleton", + "Singleton", + "Singleton", + "SingletonInstance", + "SingletonInstance", + "SingletonInstance", + "SingletonInstance", + "SingletonInstance", + "SingletonInstance", + "SingletonInstance", + "SingletonInstance", + "SingletonInstance", + "SingletonManager", + "SplineClass", + "StatsStruct>", + "SubMesh>", + "TacticalBuildBlockStatus", + "TacticalBuildEventClass", + "TacticalCameraControllerClass", + "TacticalSellEventClass", + "TaskForceDefinitionClass", + "TauntEventClass", + "TextureClass", + "TheAIClass", + "TheAIDataManagerClass", + "TheAIPlayerTypeManagerClass", + "TheDebugWindowManagerClass", + "TheGameScoringManagerClass", + "TheShipNamesManagerClass", + "ThreadClass", + "ThreadValue", + "VEmptyLockerClass::$0EAA::ValLightningBoltRenderTask::?$PooledObjectClass", + "VisibilitySignalDataClass", + "WaitForGroundbaseClass", + "WeightedTypeListClass", + "WithdrawlEventClass", + "XMLAttributeClass>", + "XMLNodeClass*___ptr64>", + "_Crt_new_delete", + "_EngineParam_Get_D3d_Model_Inverse_Transform", + "_EngineParam_Get_D3d_Model_Inverse_Transpose_Transform", + "_EngineParam_Get_D3d_Model_Transform", + "_EngineParam_Get_D3d_Model_View_Inverse_Transform", + "_EngineParam_Get_D3d_Model_View_Projection_Transform", + "_EngineParam_Get_D3d_Model_View_Transform", + "_EngineParam_Get_D3d_Projection_Transform", + "_EngineParam_Get_D3d_View_Inverse_Transform", + "_EngineParam_Get_D3d_View_Projection_Transform", + "_EngineParam_Get_D3d_View_Transform", + "_Fac_tidy_reg_t", + "_Facet_base", + "_Init_atexit", + "_Init_locks", + "_LocaleUpdate", + "_Locimp", + "_Locinfo", + "_Lockit", + "_Mpunct", + "_Mpunct", + "_Mpunct", + "_Yarn", + "_Yarn", + "__FrameHandler3", + "__FrameHandler4", + "__acrt_stdio_char_traits", + "__crt_mbstring", + "__crt_seh_guarded_call", + "__crt_seh_guarded_call", + "__crt_stdio_input", + "__crt_stdio_output", + "__crt_strtox", + "__non_rtti_object", + "al3DCableRenderTask", + "allocator", + "allocator_>", + "allocator_>,5>", + "allocator_>,6>", + "allocator_>,6>,128,class_EmptyLockerClass>", + "allocator_>_>", + "allocator_>_>_>", + "allocator_>_>,2>", + "allocator_>_>,2>,128,class_EmptyLockerClass>", + "allocator_>", + "allocator_>", + "allocator_>", + "allocator_>", + "allocator_>_>", + "bad_alloc", + "bad_array_new_length", + "bad_cast", + "bad_exception", + "bad_typeid", + "char_traits_>", + "char_traits_>_>", + "char_traits_>_>", + "char_traits_>_>", + "codecvt", + "codecvt", + "codecvt", + "codecvt_base", + "codecvt_utf8", + "collate", + "collate", + "collate", + "ctype", + "ctype", + "ctype", + "ctype_base", + "exception", + "facet", + "format_string_parser", + "format_string_parser", + "formatting_buffer", + "length_error", + "locale", + "logic_error", + "messages", + "messages", + "messages", + "messages_base", + "money_base", + "moneypunct", + "moneypunct", + "moneypunct", + "moneypunct", + "moneypunct", + "moneypunct", + "numpunct", + "numpunct", + "numpunct", + "out_of_range", + "range_error", + "runtime_error", + "scanset_buffer", + "std", + "stream_output_adapter_>", + "stream_output_adapter_>_>", + "string_input_adapter_>", + "string_input_adapter_>", + "string_output_adapter", + "string_output_adapter_>_>", + "string_output_adapter", + "string_output_adapter_>_>", + "switchD_140001181", + "switchD_1400011fe", + "switchD_1400013ac", + "switchD_1400015a2", + "switchD_140001742", + "switchD_140001b3d", + "switchD_140001ce3", + "switchD_140001ed1", + "switchD_1400021c4", + "switchD_1400025e4", + "switchD_140002687", + "switchD_140002aaf", + "switchD_140002c7d", + "switchD_140002d3d", + "switchD_140002dea", + "switchD_140002ea2", + "switchD_14000329a", + "switchD_1400032c7", + "switchD_14000330b", + "switchD_140003517", + "switchD_140003544", + "switchD_14000356c", + "switchD_1400035ab", + "switchD_14000391f", + "switchD_140003a5e", + "switchD_140003a83", + "switchD_140003ab7", + "switchD_140003beb", + "switchD_140003c44", + "switchD_140003e1d", + "switchD_140003e45", + "switchD_140003e79", + "switchD_140003ead", + "switchD_14000401c", + "switchD_14000431d", + "switchD_140004359", + "switchD_1400043f2", + "switchD_140004426", + "switchD_140004457", + "switchD_140004487", + "switchD_1400045f3", + "switchD_14000469b", + "switchD_1400047ff", + "switchD_140004b4c", + "switchD_140004bf4", + "switchD_140004c89", + "switchD_140004d31", + "switchD_140004e71", + "switchD_140004f9d", + "switchD_140005045", + "switchD_14000530b", + "switchD_1400054e5", + "switchD_1400056db", + "switchD_140005878", + "switchD_140005a11", + "switchD_140005a8e", + "switchD_140005b0b", + "switchD_140005bd8", + "switchD_140005eec", + "switchD_140006132", + "switchD_140006b16", + "switchD_140006e3d", + "switchD_140006f1d", + "switchD_140007470", + "switchD_1400077b9", + "switchD_140007a0e", + "switchD_140007c07", + "switchD_140007c92", + "switchD_14000886c", + "switchD_140008934", + "switchD_1400095ba", + "switchD_1400097e9", + "switchD_1400098b6", + "switchD_140009d1e", + "switchD_14000a1eb", + "switchD_14000a530", + "switchD_14000ad82", + "switchD_14000aeb2", + "switchD_14000b31d", + "switchD_14000bf40", + "switchD_14000c5c3", + "switchD_14000c6eb", + "switchD_14000c813", + "switchD_140028944", + "switchD_14003145d", + "switchD_140038c2e", + "switchD_140041fff", + "switchD_140043697", + "switchD_140052fef", + "switchD_140053b79", + "switchD_140054293", + "switchD_140057113", + "switchD_140060df4", + "switchD_140061136", + "switchD_140062130", + "switchD_140063f77", + "switchD_1400781b6", + "switchD_140081c7c", + "switchD_1400a61fc", + "switchD_1400aa848", + "switchD_1400b1d10", + "switchD_1400b645c", + "switchD_1400c350c", + "switchD_1400cfbd1", + "switchD_1400eb226", + "switchD_140105478", + "switchD_140108ae0", + "switchD_14010a277", + "switchD_140127773", + "switchD_140132b30", + "switchD_140132b6f", + "switchD_140134b9d", + "switchD_14014741d", + "switchD_14014f71d", + "switchD_140154acf", + "switchD_14015f96d", + "switchD_14015f9a3", + "switchD_140161592", + "switchD_140161752", + "switchD_14017a5b7", + "switchD_140181f15", + "switchD_1401828a1", + "switchD_140182b2e", + "switchD_140183281", + "switchD_140187ea3", + "switchD_14018b384", + "switchD_14019477f", + "switchD_14019e46b", + "switchD_14019fc0f", + "switchD_1401a0b57", + "switchD_1401a0f90", + "switchD_1401a52a3", + "switchD_1401a59fe", + "switchD_1401a63eb", + "switchD_1401a8b8c", + "switchD_1401a8daf", + "switchD_1401a8ded", + "switchD_1401a9312", + "switchD_1401a934e", + "switchD_1401a9b3e", + "switchD_1401aa290", + "switchD_1401aaa7f", + "switchD_1401baee2", + "switchD_1401c1787", + "switchD_1401dd1db", + "switchD_1401df0ea", + "switchD_1401e628f", + "switchD_1401e67db", + "switchD_1401f47bd", + "switchD_1401f5861", + "switchD_1401f5b1b", + "switchD_140247429", + "switchD_140247742", + "switchD_14024923a", + "switchD_140249f6a", + "switchD_14024ac35", + "switchD_14024b740", + "switchD_14024cf73", + "switchD_140252843", + "switchD_14025327f", + "switchD_14025e5a5", + "switchD_140273604", + "switchD_140273e7e", + "switchD_140278fc4", + "switchD_14027b515", + "switchD_14027b5ac", + "switchD_14027b601", + "switchD_14027b694", + "switchD_14027b739", + "switchD_14028336e", + "switchD_140285055", + "switchD_140289b97", + "switchD_140289d9b", + "switchD_14028b11e", + "switchD_14028b228", + "switchD_1402b0b89", + "switchD_1402b1b93", + "switchD_1402b1dd4", + "switchD_1402b1ef9", + "switchD_1402b21e6", + "switchD_1402b25a1", + "switchD_1402b26e1", + "switchD_1402c19bb", + "switchD_1402ceae1", + "switchD_1402d4c19", + "switchD_1402d7b01", + "switchD_1402e0b0e", + "switchD_1402f56ad", + "switchD_1402f6d1e", + "switchD_1402fa068", + "switchD_1402fa1d1", + "switchD_1402fa45a", + "switchD_1402fa5fd", + "switchD_1403076b8", + "switchD_140328b4d", + "switchD_140336e45", + "switchD_14034024d", + "switchD_140342f4d", + "switchD_1403433b0", + "switchD_1403448d4", + "switchD_140346f10", + "switchD_14034b4ca", + "switchD_14034cb36", + "switchD_14035fa54", + "switchD_14035fa9d", + "switchD_14037ba81", + "switchD_14037c1aa", + "switchD_14037c2d1", + "switchD_14037e025", + "switchD_140380d44", + "switchD_14038641e", + "switchD_14038a556", + "switchD_140392c05", + "switchD_140393049", + "switchD_1403984d6", + "switchD_1403986f0", + "switchD_14039b665", + "switchD_14039c423", + "switchD_14039c69f", + "switchD_14039ced5", + "switchD_1403a161e", + "switchD_1403b8df5", + "switchD_1403b9251", + "switchD_1403bf140", + "switchD_140410208", + "switchD_14041d1bb", + "switchD_14041d60e", + "switchD_140422c87", + "switchD_14042aaf6", + "switchD_14042c8c4", + "switchD_140447b80", + "switchD_140448842", + "switchD_14044b7cf", + "switchD_140453343", + "switchD_140453faa", + "switchD_140454c73", + "switchD_140455d2d", + "switchD_1404569cb", + "switchD_14045a98a", + "switchD_14045ad08", + "switchD_14045b0fa", + "switchD_14045b7ca", + "switchD_14045befa", + "switchD_140460cc0", + "switchD_1404622b6", + "switchD_14047b411", + "switchD_14047e471", + "switchD_1404869bf", + "switchD_14048e31c", + "switchD_14048e42c", + "switchD_14048f2e9", + "switchD_1404915ce", + "switchD_140494f76", + "switchD_1404979f7", + "switchD_1404a6a6d", + "switchD_1404a7d50", + "switchD_1404a830a", + "switchD_1404a8765", + "switchD_1404acdfa", + "switchD_1404ace5a", + "switchD_1404acec9", + "switchD_1404ad033", + "switchD_1404b44e4", + "switchD_1404b49fb", + "switchD_1404b4e3a", + "switchD_1404b8cec", + "switchD_1404b93bb", + "switchD_1404bedab", + "switchD_1404ce742", + "switchD_1404d3de4", + "switchD_1404d81ee", + "switchD_1404e6307", + "switchD_1404ec8cf", + "switchD_1404edaf1", + "switchD_1404ee4c5", + "switchD_1404f9ead", + "switchD_1404fa24e", + "switchD_1404fbf7f", + "switchD_14051a30a", + "switchD_140521633", + "switchD_140527703", + "switchD_140529315", + "switchD_14052b3d9", + "switchD_14052f1a1", + "switchD_14053670f", + "switchD_140536aa5", + "switchD_14053c0d2", + "switchD_140550256", + "switchD_1405513b5", + "switchD_140552be1", + "switchD_140556e16", + "switchD_14055a415", + "switchD_14055a52f", + "switchD_14055b6af", + "switchD_14055e3fe", + "switchD_14055e82e", + "switchD_14056134f", + "switchD_140561b44", + "switchD_140562390", + "switchD_140563854", + "switchD_140563c39", + "switchD_140563f6d", + "switchD_1405646ad", + "switchD_140570c98", + "switchD_140570d69", + "switchD_140583fcd", + "switchD_140588d49", + "switchD_140588d87", + "switchD_14058c09f", + "switchD_140591b0a", + "switchD_140596ebc", + "switchD_1405970a5", + "switchD_1405981b4", + "switchD_1405991ba", + "switchD_1405994f0", + "switchD_14059b45d", + "switchD_14059b5b3", + "switchD_14059e6d4", + "switchD_1405a922d", + "switchD_1405ad743", + "switchD_1405af5a8", + "switchD_1405c7a80", + "switchD_1405cbae5", + "switchD_1405d8052", + "switchD_1405f372a", + "switchD_1405f56de", + "switchD_1405f76ee", + "switchD_1405f7781", + "switchD_1405fd5b3", + "switchD_1405fe646", + "switchD_140613c0f", + "switchD_140613f06", + "switchD_140614208", + "switchD_14061d840", + "switchD_14061dfa4", + "switchD_14061ec53", + "switchD_14062035d", + "switchD_140623756", + "switchD_14062cae1", + "switchD_14062cb45", + "switchD_140639f8d", + "switchD_140640b60", + "switchD_140640fc2", + "switchD_1406419f9", + "switchD_140642923", + "switchD_140642d71", + "switchD_140649357", + "switchD_1406493ec", + "switchD_1406494a8", + "switchD_140649c9d", + "switchD_14064d637", + "switchD_140657347", + "switchD_1406574cf", + "switchD_140657504", + "switchD_1406638d3", + "switchD_14066467e", + "switchD_1406646b0", + "switchD_14067e2a1", + "switchD_1406800bc", + "switchD_140680fe1", + "switchD_140681545", + "switchD_14068397a", + "switchD_140692e7e", + "switchD_140695810", + "switchD_1406969ee", + "switchD_1406986c1", + "switchD_1406af685", + "switchD_1406b43c3", + "switchD_1406c14e5", + "switchD_1406c15e6", + "switchD_1406ca7b4", + "switchD_1406f2dfb", + "switchD_14071fe87", + "switchD_140723958", + "switchD_1407396e1", + "switchD_14073aa46", + "switchD_14073cca1", + "switchD_14073f9be", + "switchD_14076bdab", + "switchD_14076c122", + "switchD_14076c55b", + "switchD_1407738b9", + "switchD_1407874f4", + "switchD_1407875b5", + "switchD_140787701", + "switchD_14079f9f0", + "switchD_14079faac", + "switchD_14079fbea", + "switchD_1407a3c88", + "switchD_1407b112e", + "switchD_1407bb046", + "switchD_1407bd7ac", + "switchD_1407be102", + "switchD_1407bf243", + "switchD_1407c1666", + "switchD_1407c180c", + "switchD_1407c19ca", + "switchD_1407c1ca3", + "switchD_1407c2196", + "switchD_1407c233c", + "switchD_1407c24fa", + "switchD_1407c28cb", + "switchD_1407c2c87", + "switchD_1407c455e", + "switchD_1407c6921", + "switchD_1407c7599", + "switchD_1407ca6b6", + "switchD_1407cac61", + "switchD_1407cb50d", + "switchD_1407cb682", + "switchD_1407cbe01", + "switchD_1407cd5e2", + "switchD_1407cfb39", + "switchD_1407d3523", + "switchD_1407d40d1", + "switchD_1407d4264", + "tFontCharacterStruct>", + "tPacketMagicStruct>", + "tThreadDescriptionStruct>", + "time_base", + "type_info" + ] + }, + { + "name": "lua_scripting", + "description": "Lua VM integration, script commands, console commands, and Lua variable wrappers", + "class_count": 29, + "trainer_relevance": "high", + "key_base_classes": [ + "ConsoleCommand" + ], + "classes": [ + "ConsoleCommand", + "LuaMemberFunctionWrapper", + "LuaMemberFunctionWrapper", + "LuaMemberFunctionWrapper", + "LuaMemberFunctionWrapper", + "LuaMemberFunctionWrapper", + "LuaMemberFunctionWrapper", + "LuaMemberFunctionWrapper", + "LuaMemberFunctionWrapper", + "LuaMemberFunctionWrapper", + "LuaMemberFunctionWrapper", + "LuaMemberFunctionWrapper", + "LuaMemberFunctionWrapper", + "LuaMemberFunctionWrapper", + "LuaMemberFunctionWrapper", + "LuaMemberFunctionWrapper", + "LuaMemberFunctionWrapper", + "LuaMemberFunctionWrapper", + "LuaMemberFunctionWrapper", + "LuaMemberFunctionWrapper", + "LuaValue", + "LuaValue", + "LuaValue", + "LuaValue", + "LuaValue", + "LuaValue", + "LuaVar", + "PlayersCommand", + "SwitchSidesCommand" + ] + }, + { + "name": "save_load", + "description": "Save/load persistence, serialization, and game-state snapshot systems", + "class_count": 1, + "trainer_relevance": "medium", + "key_base_classes": [], + "classes": [ + "IXMLLoadableClass" + ] + }, + { + "name": "audio_visual", + "description": "Sound effects, music, movie playback, Bink video, particles, lasers, and render tasks", + "class_count": 1, + "trainer_relevance": "low", + "key_base_classes": [], + "classes": [ + "ID3DXEffectStateManager" + ] + }, + { + "name": "uncategorized", + "description": "Classes not matched by any subsystem heuristic", + "class_count": 0, + "trainer_relevance": "low", + "key_base_classes": [], + "classes": [] + } + ] +} \ No newline at end of file diff --git a/re-findings/gameobject_complete.json b/re-findings/gameobject_complete.json new file mode 100644 index 000000000..6ef304803 --- /dev/null +++ b/re-findings/gameobject_complete.json @@ -0,0 +1,1324 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "GameObjectClass Complete Structure Map", + "description": "Complete field mapping of the Alamo engine GameObjectClass struct from SWFOC (StarWarsG.exe, x86_64). Produced by systematic Ghidra decompilation of constructors, SetHP, QueryInterface, SetSpeedOverride, Make_Invulnerable, Change_Owner, ScheduleHeroRespawn, and 20+ additional functions.", + "_meta": { + "agent": "Agent 3B", + "date": "2026-04-04", + "binary": "StarWarsG.exe", + "vtable_rva": "0x8661B8", + "ghidra_base": "0x140000000", + "constructor_rvas": ["0x388720", "0x388B60"], + "functions_decompiled": 25, + "xrefs_followed": 22, + "confidence_scale": "high = confirmed in 2+ functions; medium = confirmed in 1 function; low = inferred from context/naming" + }, + "struct": { + "name": "GameObjectClass", + "vtable_rva": "0x8661B8", + "rtti_mangled_name": ".?AVGameObjectClass@@", + "minimum_size": "0x3C8", + "inherits": [ + "RootClass", + "MultiLinkedListMember", + "VEmptyLockerClass::$0EA::VGameObjectClass::?$PooledObjectClass", + "CullObjectClass", + "SignalGeneratorClass" + ], + "additional_vtables": [ + {"rva": "0x8661D8", "offset_in_object": 8, "for_class": "MultiLinkedListMember"}, + {"rva": "0x866200", "offset_in_object": 24, "for_class": "CullObjectClass"}, + {"rva": "0x866210", "offset_in_object": 32, "for_class": "SignalGeneratorClass"}, + {"rva": "0x866228", "offset_in_object": 56, "for_class": "unknown_base_5"} + ], + "fields": [ + { + "offset": "0x00", + "type": "pointer", + "width": 8, + "name": "vtable_ptr", + "description": "Pointer to the primary virtual function table. RVA 0x8661B8 identifies GameObjectClass. Written first in both constructors.", + "confidence": "high", + "source": ["constructor_0x388720", "constructor_0x388B60"] + }, + { + "offset": "0x08", + "type": "pointer", + "width": 8, + "name": "vtable_MultiLinkedListMember", + "description": "Secondary vtable for MultiLinkedListMember base class.", + "confidence": "high", + "source": ["constructor_0x388720"] + }, + { + "offset": "0x10", + "type": "uint64", + "width": 8, + "name": "linked_list_next", + "description": "MultiLinkedListMember next pointer. Initialized to 0 in constructor.", + "confidence": "medium", + "source": ["constructor_0x388720"] + }, + { + "offset": "0x18", + "type": "pointer", + "width": 8, + "name": "vtable_CullObjectClass", + "description": "Secondary vtable for CullObjectClass base class.", + "confidence": "high", + "source": ["constructor_0x388720"] + }, + { + "offset": "0x20", + "type": "pointer", + "width": 8, + "name": "vtable_SignalGeneratorClass", + "description": "Secondary vtable for SignalGeneratorClass base class.", + "confidence": "high", + "source": ["constructor_0x388720"] + }, + { + "offset": "0x28", + "type": "undefined", + "width": 8, + "name": "signal_generator_data", + "description": "SignalGeneratorClass internal data. Initialized by SignalGeneratorClass::SignalGeneratorClass().", + "confidence": "medium", + "source": ["constructor_0x388720"] + }, + { + "offset": "0x30", + "type": "uint64", + "width": 8, + "name": "signal_data_2", + "description": "Additional signal/cull object data. Initialized to 0.", + "confidence": "medium", + "source": ["constructor_0x388720"] + }, + { + "offset": "0x38", + "type": "pointer", + "width": 8, + "name": "vtable_5", + "description": "Fifth vtable pointer (for unknown base class at offset 56 in RTTI). Present in additional_vtables array.", + "confidence": "high", + "source": ["constructor_0x388720"] + }, + { + "offset": "0x40", + "type": "uint32", + "width": 4, + "name": "visibility_mask", + "description": "Cull/visibility bitmask. Initialized to 0x3FFFFF (all bits set). Used by the rendering/culling system to determine which cameras/viewports can see this object.", + "confidence": "high", + "source": ["constructor_0x388720", "constructor_0x388B60"] + }, + { + "offset": "0x44", + "type": "uint32", + "width": 4, + "name": "visibility_mask_2", + "description": "Second visibility mask. Also initialized to 0x3FFFFF.", + "confidence": "high", + "source": ["constructor_0x388720", "constructor_0x388B60"] + }, + { + "offset": "0x48", + "type": "int32", + "width": 4, + "name": "unique_session_id", + "description": "Session-unique allocation ID. Initialized to -1 (0xFFFFFFFF) in default constructor; set to param_3 in the full constructor. Used by FUN_1402AC980() for session tracking.", + "confidence": "high", + "source": ["constructor_0x388720", "constructor_0x388B60"] + }, + { + "offset": "0x4C", + "type": "int32", + "width": 4, + "name": "session_id_high", + "description": "Upper 32 bits / secondary ID. Initialized to 0.", + "confidence": "medium", + "source": ["constructor_0x388B60"] + }, + { + "offset": "0x50", + "type": "pointer", + "width": 8, + "name": "signal_listener_list", + "description": "Linked list of signal listeners. Initialized to NULL. Iterated in FUN_1403AC530 (transform update) to notify observers.", + "confidence": "high", + "source": ["constructor_0x388720", "FUN_1403AC530"] + }, + { + "offset": "0x58", + "type": "int32", + "width": 4, + "name": "owner_player_id", + "description": "Index into the global PlayerListClass array identifying the owning player. SESSION-DEPENDENT. For sub-objects, use QueryInterface(3) to resolve the true owner via parent chain. Read by SetHP, Change_Owner, ScheduleHeroRespawn, and many others.", + "confidence": "high", + "source": ["SetHP_0x3A89D0", "Change_Owner_0x574D0E", "FUN_1403956C0", "ScheduleHeroRespawn_0x48EB10"], + "caveats": [ + "Session-dependent index, not a persistent faction ID", + "For sub-objects (hardpoints, turrets), this may be stale -- use QueryInterface(3)", + "Value -1 means no owner / uninitialized" + ] + }, + { + "offset": "0x5C", + "type": "float32", + "width": 4, + "name": "hp", + "description": "Current hitpoints. Written exclusively by SetHP (RVA 0x3A89D0). Clamped to [0.0, max_hp]. When HP reaches 0.0, the engine logs a death event. All known callers funnel through SetHP. Also set during spawn init in FUN_1403989A0 via GetMaxHealth.", + "confidence": "high", + "source": ["SetHP_0x3A89D0", "FUN_1403989A0"] + }, + { + "offset": "0x60", + "type": "uint64", + "width": 8, + "name": "unknown_0x60", + "description": "Initialized to 0 in constructor. Referenced in Change_Owner via *(param_1 + 0x60) as the GameObjectClass pointer of the wrapper's underlying object.", + "confidence": "medium", + "source": ["constructor_0x388720"] + }, + { + "offset": "0x68", + "type": "float32", + "width": 4, + "name": "spawn_position_x", + "description": "Initial spawn X coordinate. Set from param_4 in the full constructor. Used as the base position during object initialization.", + "confidence": "high", + "source": ["constructor_0x388B60"] + }, + { + "offset": "0x6C", + "type": "float32", + "width": 4, + "name": "spawn_position_y", + "description": "Initial spawn Y coordinate (vertical/height). Set from param_4[1] in the full constructor.", + "confidence": "high", + "source": ["constructor_0x388B60"] + }, + { + "offset": "0x70", + "type": "float32", + "width": 4, + "name": "spawn_position_z", + "description": "Initial spawn Z coordinate (depth). Set from param_4[2] in the full constructor. May have height offset (DFC) added from GameObjectType.", + "confidence": "high", + "source": ["constructor_0x388B60"] + }, + { + "offset": "0x74", + "type": "float32", + "width": 4, + "name": "facing_x", + "description": "Initial facing direction X component. Set from param_5[0] in the full constructor.", + "confidence": "high", + "source": ["constructor_0x388B60"] + }, + { + "offset": "0x78", + "type": "float32", + "width": 4, + "name": "current_position_x", + "description": "Current world position X. Written by the Make_Invulnerable/Teleport path. Read by FUN_1403A8710 alongside +0x7C and +0x80. Also set from spawn_position in constructor.", + "confidence": "high", + "source": ["Make_Invulnerable_0x3ABB80", "FUN_1403989A0", "FUN_1403AC530"] + }, + { + "offset": "0x7C", + "type": "float32", + "width": 4, + "name": "current_position_y", + "description": "Current world position Y (height). Written by Make_Invulnerable/Teleport path. The 'y' in the engine coordinate system.", + "confidence": "high", + "source": ["Make_Invulnerable_0x3ABB80", "FUN_1403989A0", "FUN_1403AC530"] + }, + { + "offset": "0x80", + "type": "float32", + "width": 4, + "name": "current_position_z", + "description": "Current world position Z (depth). Written by Make_Invulnerable/Teleport path.", + "confidence": "high", + "source": ["Make_Invulnerable_0x3ABB80", "FUN_1403989A0"] + }, + { + "offset": "0x84", + "type": "float32", + "width": 4, + "name": "render_position_x", + "description": "Rendering/interpolated position X. Updated by FUN_1403A8710 which compares with param and sets dirty flag at +0x3A1. Distinct from current_position for interpolation.", + "confidence": "high", + "source": ["FUN_1403A8710"] + }, + { + "offset": "0x88", + "type": "float32", + "width": 4, + "name": "render_position_y", + "description": "Rendering/interpolated position Y.", + "confidence": "high", + "source": ["FUN_1403A8710", "constructor_0x388B60"] + }, + { + "offset": "0x8C", + "type": "float32", + "width": 4, + "name": "render_position_z", + "description": "Rendering/interpolated position Z. Also used as the facing/rotation Z in FUN_1403AC530 transform update.", + "confidence": "high", + "source": ["FUN_1403A8710", "FUN_1403AC530"] + }, + { + "offset": "0x90", + "type": "float32", + "width": 4, + "name": "previous_position_x", + "description": "Previous frame position X. Updated by FUN_1403A8710 alongside render_position.", + "confidence": "high", + "source": ["FUN_1403A8710"] + }, + { + "offset": "0x94", + "type": "float32", + "width": 4, + "name": "previous_position_y", + "description": "Previous frame position Y. Used by FUN_1403AC530 for rotation calculation.", + "confidence": "high", + "source": ["FUN_1403A8710", "FUN_1403AC530"] + }, + { + "offset": "0x98", + "type": "float32", + "width": 4, + "name": "previous_position_z", + "description": "Previous frame position Z.", + "confidence": "high", + "source": ["FUN_1403A8710"] + }, + { + "offset": "0xA8", + "type": "pointer", + "width": 8, + "name": "locomotor_component_ptr", + "description": "Pointer to the locomotor/movement behavior component. When non-null, contains speed override fields: flag at locomotor+0x29C (byte), value at locomotor+0x2A0 (float32). SetSpeedOverride writes both; ClearSpeedOverride zeros both.", + "confidence": "high", + "source": ["SetSpeedOverride_0x3A8C90", "ClearSpeedOverride_0x38F8B0", "FUN_1403989A0"], + "sub_fields": { + "+0x29C": {"type": "uint8", "name": "speed_override_active", "values": {"0": "off", "1": "on"}}, + "+0x2A0": {"type": "float32", "name": "speed_override_value"} + } + }, + { + "offset": "0xB0", + "type": "pointer", + "width": 8, + "name": "component_slot_0xB0", + "description": "Component pointer slot. Part of the component pointer block from 0xA8 to 0x240. Cleared during reset in FUN_1403989A0.", + "confidence": "medium", + "source": ["FUN_1403989A0"] + }, + { + "offset": "0xB8", + "type": "pointer", + "width": 8, + "name": "parent_container_component_ptr", + "description": "Pointer to parent container component. FUN_1403956C0 returns (obj+0xB8)+0x68 as the player/faction identity when the object has a parent. Used for resolving ownership of sub-objects. Also read in FUN_140395920 for sub-object owner resolution.", + "confidence": "high", + "source": ["FUN_1403956C0", "FUN_140395920"] + }, + { + "offset": "0xC0", + "type": "pointer", + "width": 8, + "name": "component_slot_0xC0", + "description": "Component pointer slot. Part of the component pointer block. Cleared during reset.", + "confidence": "medium", + "source": ["FUN_1403989A0"] + }, + { + "offset": "0xC8", + "type": "pointer", + "width": 8, + "name": "component_slot_0xC8", + "description": "Component pointer slot.", + "confidence": "medium", + "source": ["FUN_1403989A0"] + }, + { + "offset": "0xD0", + "type": "pointer", + "width": 8, + "name": "component_slot_0xD0", + "description": "Component pointer slot.", + "confidence": "medium", + "source": ["FUN_1403989A0"] + }, + { + "offset": "0xD8", + "type": "pointer", + "width": 8, + "name": "targeting_data_ptr", + "description": "Pointer to targeting/AI data structure. FUN_140395920 reads *(obj+0xD8)+0x18 as a linked list for target priority evaluation.", + "confidence": "medium", + "source": ["FUN_140395920"] + }, + { + "offset": "0xE0", + "type": "pointer", + "width": 8, + "name": "component_slot_0xE0", + "description": "Component pointer slot.", + "confidence": "medium", + "source": ["FUN_1403989A0"] + }, + { + "offset": "0xE8", + "type": "pointer", + "width": 8, + "name": "component_slot_0xE8", + "description": "Component pointer slot.", + "confidence": "medium", + "source": ["FUN_1403989A0"] + }, + { + "offset": "0xF0", + "type": "pointer", + "width": 8, + "name": "buff_modifier_component_ptr", + "description": "Pointer to combat buff/modifier component. FUN_140395C70 reads *(obj+0xF0)+0x138 for buff data. Used in health modifier calculations.", + "confidence": "medium", + "source": ["FUN_140395C70"] + }, + { + "offset": "0xF8", + "type": "pointer", + "width": 8, + "name": "game_object_type_wrapper_ptr", + "description": "Pointer to the GameObjectType struct wrapper for this instance. Contains the type definition, refcounted. Written from param_2 in the full constructor.", + "confidence": "medium", + "source": ["constructor_0x388B60"] + }, + { + "offset": "0x100", + "type": "pointer", + "width": 8, + "name": "combatant_behavior_ptr", + "description": "Pointer to CombatantBehaviorClass. Allocated (size 0x3B8) and initialized in FUN_1403989A0 when FUN_140374DA0 returns true. Contains combat-related state including weapon data at +0x2CC and nested structures.", + "confidence": "high", + "source": ["FUN_1403989A0", "FUN_1403A92F0"] + }, + { + "offset": "0x108", + "type": "pointer", + "width": 8, + "name": "component_slot_0x108", + "description": "Component pointer slot.", + "confidence": "medium", + "source": ["FUN_1403989A0"] + }, + { + "offset": "0x110", + "type": "pointer", + "width": 8, + "name": "component_slot_0x110", + "description": "Component pointer slot.", + "confidence": "medium", + "source": ["FUN_1403989A0"] + }, + { + "offset": "0x118", + "type": "pointer", + "width": 8, + "name": "health_sub_object_ptr", + "description": "Pointer to a health-related sub-object. Used by the Take_Damage property dispatch path. Contains additional health state fields at deep offsets.", + "confidence": "high", + "source": ["alamo_engine_kb.json"] + }, + { + "offset": "0x120", + "type": "pointer", + "width": 8, + "name": "component_slot_0x120", + "description": "Component pointer slot.", + "confidence": "medium", + "source": ["FUN_1403989A0"] + }, + { + "offset": "0x128", + "type": "pointer", + "width": 8, + "name": "component_slot_0x128", + "description": "Component pointer slot.", + "confidence": "medium", + "source": ["FUN_1403989A0"] + }, + { + "offset": "0x130", + "type": "pointer", + "width": 8, + "name": "component_slot_0x130", + "description": "Component pointer slot.", + "confidence": "medium", + "source": ["FUN_1403989A0"] + }, + { + "offset": "0x138", + "type": "pointer", + "width": 8, + "name": "component_slot_0x138", + "description": "Component pointer slot.", + "confidence": "medium", + "source": ["FUN_1403989A0"] + }, + { + "offset": "0x140", + "type": "pointer", + "width": 8, + "name": "component_slot_0x140", + "description": "Component pointer slot.", + "confidence": "medium", + "source": ["FUN_1403989A0"] + }, + { + "offset": "0x148", + "type": "pointer", + "width": 8, + "name": "component_slot_0x148", + "description": "Component pointer slot.", + "confidence": "medium", + "source": ["FUN_1403989A0"] + }, + { + "offset": "0x150", + "type": "pointer", + "width": 8, + "name": "component_slot_0x150", + "description": "Component pointer slot.", + "confidence": "medium", + "source": ["FUN_1403989A0"] + }, + { + "offset": "0x158", + "type": "pointer", + "width": 8, + "name": "component_slot_0x158", + "description": "Component pointer slot.", + "confidence": "medium", + "source": ["FUN_1403989A0"] + }, + { + "offset": "0x160", + "type": "pointer", + "width": 8, + "name": "component_slot_0x160", + "description": "Component pointer slot. Explicitly set to NULL in constructor.", + "confidence": "medium", + "source": ["constructor_0x388720", "FUN_1403989A0"] + }, + { + "offset": "0x168", + "type": "pointer", + "width": 8, + "name": "component_slot_0x168", + "description": "Component pointer slot. Cleared during reset.", + "confidence": "medium", + "source": ["constructor_0x388720", "FUN_1403989A0"] + }, + { + "offset": "0x170", + "type": "pointer", + "width": 8, + "name": "component_slot_0x170", + "description": "Component pointer slot.", + "confidence": "medium", + "source": ["FUN_1403989A0"] + }, + { + "offset": "0x178", + "type": "pointer", + "width": 8, + "name": "component_slot_0x178", + "description": "Component pointer slot.", + "confidence": "medium", + "source": ["FUN_1403989A0"] + }, + { + "offset": "0x180", + "type": "pointer", + "width": 8, + "name": "component_slot_0x180", + "description": "Component pointer slot.", + "confidence": "medium", + "source": ["FUN_1403989A0"] + }, + { + "offset": "0x188", + "type": "pointer", + "width": 8, + "name": "component_slot_0x188", + "description": "Component pointer slot.", + "confidence": "medium", + "source": ["FUN_1403989A0"] + }, + { + "offset": "0x190", + "type": "pointer", + "width": 8, + "name": "component_slot_0x190", + "description": "Component pointer slot.", + "confidence": "medium", + "source": ["FUN_1403989A0"] + }, + { + "offset": "0x198", + "type": "pointer", + "width": 8, + "name": "component_slot_0x198", + "description": "Component pointer slot.", + "confidence": "medium", + "source": ["FUN_1403989A0"] + }, + { + "offset": "0x1A0", + "type": "pointer", + "width": 8, + "name": "component_slot_0x1A0", + "description": "Component pointer slot.", + "confidence": "medium", + "source": ["FUN_1403989A0"] + }, + { + "offset": "0x1A8", + "type": "pointer", + "width": 8, + "name": "component_slot_0x1A8", + "description": "Component pointer slot.", + "confidence": "medium", + "source": ["FUN_1403989A0"] + }, + { + "offset": "0x1B0", + "type": "pointer", + "width": 8, + "name": "component_slot_0x1B0", + "description": "Component pointer slot.", + "confidence": "medium", + "source": ["FUN_1403989A0"] + }, + { + "offset": "0x1B8", + "type": "pointer", + "width": 8, + "name": "component_slot_0x1B8", + "description": "Component pointer slot.", + "confidence": "medium", + "source": ["FUN_1403989A0"] + }, + { + "offset": "0x1C0", + "type": "pointer", + "width": 8, + "name": "component_slot_0x1C0", + "description": "Component pointer slot.", + "confidence": "medium", + "source": ["FUN_1403989A0"] + }, + { + "offset": "0x1C8", + "type": "pointer", + "width": 8, + "name": "component_slot_0x1C8", + "description": "Component pointer slot.", + "confidence": "medium", + "source": ["FUN_1403989A0"] + }, + { + "offset": "0x1D0", + "type": "pointer", + "width": 8, + "name": "component_slot_0x1D0", + "description": "Component pointer slot.", + "confidence": "medium", + "source": ["FUN_1403989A0"] + }, + { + "offset": "0x1D8", + "type": "pointer", + "width": 8, + "name": "component_slot_0x1D8", + "description": "Component pointer slot.", + "confidence": "medium", + "source": ["FUN_1403989A0"] + }, + { + "offset": "0x1E0", + "type": "pointer", + "width": 8, + "name": "component_slot_0x1E0", + "description": "Component pointer slot.", + "confidence": "medium", + "source": ["FUN_1403989A0"] + }, + { + "offset": "0x1E8", + "type": "pointer", + "width": 8, + "name": "component_slot_0x1E8", + "description": "Component pointer slot.", + "confidence": "medium", + "source": ["FUN_1403989A0"] + }, + { + "offset": "0x1F0", + "type": "pointer", + "width": 8, + "name": "component_slot_0x1F0", + "description": "Component pointer slot.", + "confidence": "medium", + "source": ["FUN_1403989A0"] + }, + { + "offset": "0x1F8", + "type": "pointer", + "width": 8, + "name": "component_slot_0x1F8", + "description": "Component pointer slot.", + "confidence": "medium", + "source": ["FUN_1403989A0"] + }, + { + "offset": "0x200", + "type": "pointer", + "width": 8, + "name": "radar_map_data_ptr", + "description": "Pointer to RadarMapDataPackClass (size 0x20). Allocated in FUN_1403989A0 if NULL. Used for minimap representation of this object.", + "confidence": "high", + "source": ["FUN_1403989A0"] + }, + { + "offset": "0x208", + "type": "uint64", + "width": 8, + "name": "unknown_0x208", + "description": "Unknown. Cleared during init.", + "confidence": "low", + "source": ["FUN_1403989A0"] + }, + { + "offset": "0x210", + "type": "uint64", + "width": 8, + "name": "unknown_0x210", + "description": "Unknown. Cleared during init.", + "confidence": "low", + "source": ["FUN_1403989A0"] + }, + { + "offset": "0x218", + "type": "pointer", + "width": 8, + "name": "component_slot_0x218", + "description": "Component pointer slot.", + "confidence": "medium", + "source": ["FUN_1403989A0"] + }, + { + "offset": "0x220", + "type": "pointer", + "width": 8, + "name": "component_slot_0x220", + "description": "Component pointer slot.", + "confidence": "medium", + "source": ["FUN_1403989A0"] + }, + { + "offset": "0x228", + "type": "pointer", + "width": 8, + "name": "component_slot_0x228", + "description": "Component pointer slot.", + "confidence": "medium", + "source": ["FUN_1403989A0"] + }, + { + "offset": "0x230", + "type": "pointer", + "width": 8, + "name": "component_slot_0x230", + "description": "Component pointer slot. Explicitly set to NULL.", + "confidence": "medium", + "source": ["FUN_1403989A0"] + }, + { + "offset": "0x240", + "type": "pointer", + "width": 8, + "name": "component_slot_0x240", + "description": "Component pointer slot. Referenced in FUN_1403A5840 for map node cleanup. Used in FUN_1403A4820 for hero/garrison logic -- contains sub-structure with +0x2A0 map manager ref and +0x328 counter.", + "confidence": "medium", + "source": ["FUN_1403989A0", "FUN_1403A5840", "FUN_1403A4820"] + }, + { + "offset": "0x248", + "type": "float32[3]", + "width": 12, + "name": "orientation_matrix_row0", + "description": "First row of 3x3 orientation/rotation matrix. Initialized to identity matrix (1,0,0,0,1,0,0,0,1) in constructor. The RootClass_data offsets 0x1A8-0x1D0 map to raw struct offsets 0x248-0x270.", + "confidence": "high", + "source": ["constructor_0x388720", "constructor_0x388B60"] + }, + { + "offset": "0x254", + "type": "float32[3]", + "width": 12, + "name": "orientation_matrix_row1", + "description": "Second row of orientation matrix. Identity: (0, 1, 0).", + "confidence": "high", + "source": ["constructor_0x388720"] + }, + { + "offset": "0x260", + "type": "float32[3]", + "width": 12, + "name": "orientation_matrix_row2", + "description": "Third row of orientation matrix. Identity: (0, 0, 1).", + "confidence": "high", + "source": ["constructor_0x388720"] + }, + { + "offset": "0x268", + "type": "uint8", + "width": 1, + "name": "death_check_flag", + "description": "Flag related to death state processing. Mapped from constructor's RootClass_data offset_0x1E8.", + "confidence": "medium", + "source": ["constructor_0x388720"] + }, + { + "offset": "0x269", + "type": "uint8", + "width": 1, + "name": "spawn_initialized", + "description": "Set to 1 during construction. Indicates the object has been initialized post-spawn.", + "confidence": "medium", + "source": ["constructor_0x388720"] + }, + { + "offset": "0x278", + "type": "pointer", + "width": 8, + "name": "component_array_ptr", + "description": "Pointer to an array of component (behavior) pointers. Each element is 8 bytes. Indexed by values in the component_lookup_table at +0x332. Used by QueryInterface: result = components[obj[0x332 + query_type]]. Components are added via FUN_14038CB30.", + "confidence": "high", + "source": ["QueryInterface_0x395AC0", "FUN_14038CB30", "FUN_1403989A0"] + }, + { + "offset": "0x288", + "type": "int8", + "width": 1, + "name": "component_count", + "description": "Current number of components in the component_array. Incremented in FUN_14038CB30 when a new component is added.", + "confidence": "high", + "source": ["FUN_14038CB30"] + }, + { + "offset": "0x289", + "type": "int8", + "width": 1, + "name": "component_capacity", + "description": "Capacity of the component_array. When component_count >= component_capacity, the array is grown via FUN_1403A59F0.", + "confidence": "high", + "source": ["FUN_14038CB30"] + }, + { + "offset": "0x290", + "type": "int32", + "width": 4, + "name": "priority_component_count", + "description": "Count of priority/front-inserted components. Incremented in FUN_14038CB30 when a component is inserted at index 0 (priority behavior). Reset to 0 in FUN_1403989A0.", + "confidence": "high", + "source": ["FUN_14038CB30", "FUN_1403989A0"] + }, + { + "offset": "0x298", + "type": "pointer", + "width": 8, + "name": "game_object_type_ptr", + "description": "Pointer to the GameObjectTypeClass struct that defines this object's type (unit definition). The type name string is at [GameObjectType+0xF8] as an MSVC SSO string. Max health is obtained via FUN_1403727A0 which reads GameObjectType+0xDCC. Used extensively: SetHP, spawn init, hero respawn, combat, etc.", + "confidence": "high", + "source": ["SetHP_0x3A89D0", "FUN_1403989A0", "FUN_1403727A0", "ScheduleHeroRespawn_0x48EB10"], + "type_sub_fields": { + "+0x0F8": "type_name (MSVC SSO string)", + "+0x110": "type_name_capacity (if >= 16, +0xF8 is heap pointer)", + "+0x140": "display_name (MSVC SSO string, used in AI label)", + "+0x158": "display_name_capacity", + "+0xDCC": "base_max_health (float32)", + "+0xDFC": "height_offset (float32, added to Z during spawn)", + "+0xE40": "hardpoint_definition_count (int32)", + "+0xE38": "hardpoint_definition_array_ptr", + "+0x1640": "initial_combat_param", + "+0x1AB0": "behavior_pack_array_ptr", + "+0x1AB8": "behavior_pack_array_end_ptr", + "+0x23EC": "hero_respawn_timer (float32)" + } + }, + { + "offset": "0x2A0", + "type": "pointer", + "width": 8, + "name": "scene_node_ptr", + "description": "Pointer to the 3D scene/render node for this object. Contains the visual representation. Has sub-fields: +0x88 (animation state), +0xA4/+0xA8/+0xAC (render position XYZ), +0xB0/+0xB4/+0xB8 (render position copy). Checked extensively -- when non-null, the object has a visible representation. Cleared in FUN_1403989A0.", + "confidence": "high", + "source": ["FUN_1403989A0", "FUN_1403A4820", "FUN_1403A92F0", "Make_Invulnerable_0x3ABB80", "FUN_1403A5840"] + }, + { + "offset": "0x2A8", + "type": "pointer", + "width": 8, + "name": "ai_label_display_ptr", + "description": "Pointer to the AI label/name display object. Allocated and managed in FUN_1403A4510. Contains a DrawTextClass used for debug/AI overhead labels. Cleared in FUN_1403989A0.", + "confidence": "high", + "source": ["FUN_1403A4510", "FUN_1403989A0"] + }, + { + "offset": "0x2B0", + "type": "pointer", + "width": 8, + "name": "container_ref", + "description": "Reference to the parent container object. Used by the invulnerability system and FUN_1403963F0 which walks the container chain: while (obj+0x335 != -1) { obj = *(obj+0x2B0) }. When non-null, the object is contained within another object. Cleared in FUN_1403989A0.", + "confidence": "high", + "source": ["FUN_1403963F0", "FUN_1403989A0", "FUN_1403A5840"] + }, + { + "offset": "0x2B8", + "type": "pointer", + "width": 8, + "name": "game_session_context_ptr", + "description": "Pointer to the game session/world context. Dereferenced as *(obj+0x2B8)+0x20 to reach the game mode manager which provides vtable calls for GetGameMode, GetPlayerManager, etc. Used by FUN_1403989A0, FUN_14039BCB0, FUN_1403A4510, FUN_1403A4820.", + "confidence": "high", + "source": ["FUN_1403989A0", "FUN_14039BCB0", "FUN_1403A4510", "FUN_1403A4820"] + }, + { + "offset": "0x2D0", + "type": "pointer", + "width": 8, + "name": "hardpoint_array_ptr", + "description": "Pointer to DynamicVectorClass. Allocated (size 0x18) and populated in FUN_1403989A0 when combatant_behavior_ptr exists. Each hardpoint is size 0xD8, created by FUN_140381A90. Contains weapon/turret sub-objects.", + "confidence": "high", + "source": ["FUN_1403989A0"] + }, + { + "offset": "0x2E0", + "type": "pointer", + "width": 8, + "name": "map_garrison_node_ptr", + "description": "Pointer to map/garrison node. Referenced in FUN_1403A5840 for cleanup. When non-null and *(+0x2E0)+0x2A0 is valid, indicates the object is garrisoned on a map structure. Contains a counter at +0x328.", + "confidence": "medium", + "source": ["FUN_1403A5840", "constructor_0x388B60"] + }, + { + "offset": "0x2F8", + "type": "pointer", + "width": 8, + "name": "garrison_map_ref_2", + "description": "Secondary garrison/map reference. Cleared alongside map_garrison_node_ptr in FUN_1403A5840.", + "confidence": "medium", + "source": ["FUN_1403A5840"] + }, + { + "offset": "0x300", + "type": "uint8", + "width": 1, + "name": "object_flags_0x300", + "description": "Object state flags byte. Initialized from global DAT_140A28648 in constructor. Bit 6 (0x40) and bit 7 (0x80) are set when param_7 == 1 in the full constructor (0xC0 mask). Checked in FUN_1403A4820 as (obj+0x3A1) bit 3.", + "confidence": "high", + "source": ["constructor_0x388720", "constructor_0x388B60"] + }, + { + "offset": "0x301", + "type": "uint8", + "width": 1, + "name": "object_flags_0x301", + "description": "Initialized from global DAT_140B2C379.", + "confidence": "medium", + "source": ["constructor_0x388720"] + }, + { + "offset": "0x302", + "type": "uint8", + "width": 1, + "name": "object_flags_0x302", + "description": "Initialized from global DAT_140A28649.", + "confidence": "medium", + "source": ["constructor_0x388720"] + }, + { + "offset": "0x303", + "type": "uint8", + "width": 1, + "name": "object_flags_0x303", + "description": "Initialized from global DAT_140B2C37A.", + "confidence": "medium", + "source": ["constructor_0x388720"] + }, + { + "offset": "0x304", + "type": "uint8", + "width": 1, + "name": "object_flags_0x304", + "description": "Initialized from global DAT_140A2864A.", + "confidence": "medium", + "source": ["constructor_0x388720"] + }, + { + "offset": "0x305", + "type": "uint8", + "width": 1, + "name": "active_flag", + "description": "Initialized to 1 in constructor. Likely indicates the object is active/alive.", + "confidence": "medium", + "source": ["constructor_0x388720"] + }, + { + "offset": "0x307", + "type": "uint8", + "width": 1, + "name": "state_flag_0x307", + "description": "Initialized to 0.", + "confidence": "low", + "source": ["constructor_0x388720"] + }, + { + "offset": "0x308", + "type": "uint32", + "width": 4, + "name": "state_0x308", + "description": "Initialized to 0.", + "confidence": "low", + "source": ["constructor_0x388720"] + }, + { + "offset": "0x30C", + "type": "uint8", + "width": 1, + "name": "state_0x30C", + "description": "Initialized to 0xFF.", + "confidence": "low", + "source": ["constructor_0x388720"] + }, + { + "offset": "0x310", + "type": "int32", + "width": 4, + "name": "state_0x310", + "description": "Initialized to -1 (0xFFFFFFFF).", + "confidence": "low", + "source": ["constructor_0x388720"] + }, + { + "offset": "0x314", + "type": "uint64", + "width": 8, + "name": "state_0x314", + "description": "Initialized to 0.", + "confidence": "low", + "source": ["constructor_0x388720"] + }, + { + "offset": "0x318", + "type": "uint64", + "width": 8, + "name": "state_0x318", + "description": "Initialized to 0.", + "confidence": "low", + "source": ["constructor_0x388720"] + }, + { + "offset": "0x320", + "type": "uint64", + "width": 8, + "name": "state_0x320", + "description": "Initialized to 0.", + "confidence": "low", + "source": ["constructor_0x388720"] + }, + { + "offset": "0x328", + "type": "uint32", + "width": 4, + "name": "state_0x328", + "description": "Initialized to 1.", + "confidence": "low", + "source": ["constructor_0x388720"] + }, + { + "offset": "0x32C", + "type": "uint32", + "width": 4, + "name": "state_0x32C", + "description": "Initialized to 0.", + "confidence": "low", + "source": ["constructor_0x388720"] + }, + { + "offset": "0x330", + "type": "uint8", + "width": 1, + "name": "hero_respawn_slot", + "description": "Used in ScheduleHeroRespawn as the last parameter to FUN_1403FE0A0. Read as (char)(obj+0x330). Likely an index into a respawn slot table.", + "confidence": "medium", + "source": ["ScheduleHeroRespawn_0x48EB10"] + }, + { + "offset": "0x332", + "type": "uint8[0x6A]", + "width": 106, + "name": "component_lookup_table", + "description": "Byte array indexed by component query type ID (0 through 0x69). Each byte is either an index into the component_array (at +0x278) or 0xFF meaning 'no component of this type'. Used by QueryInterface: result = components[obj[0x332 + query_type]]. Known query types: 0=self, 1=behavior, 3=parent/container, 4=land_locomotor, 5=space_locomotor, 0x16=hardpoint_mgr, 0x19=ability, 0x3D=transform, 0x46=property_handler. Initialized from global template at 0x140866880. Range is 0x332 to 0x39B inclusive (106 bytes).", + "confidence": "high", + "source": ["QueryInterface_0x395AC0", "FUN_14038CB30", "constructor_0x388720", "FUN_1403989A0"], + "query_types": { + "0x00": "self (returns this)", + "0x01": "behavior_manager", + "0x03": "parent_container", + "0x04": "land_locomotor_component", + "0x05": "space_locomotor_component", + "0x07": "unknown_0x07 (checked in FUN_14039B950)", + "0x16": "hardpoint_manager", + "0x19": "ability_manager", + "0x3D": "transform_component", + "0x46": "property_handler" + } + }, + { + "offset": "0x333", + "type": "uint8", + "width": 1, + "name": "behavior_component_index", + "description": "Shorthand for component_lookup_table[1]. Index into component array for the behavior manager. When != -1, the object has an active behavior system. Checked in FUN_14039BCB0.", + "confidence": "high", + "source": ["FUN_14039BCB0"] + }, + { + "offset": "0x335", + "type": "uint8", + "width": 1, + "name": "parent_component_index", + "description": "Shorthand for component_lookup_table[3] (query type 3 = parent/container). 0xFF means this object has no parent (it is a top-level object). Any other value is an index into the component array pointing to the parent component. Used in Change_Owner, FUN_1403963F0, FUN_14039BCB0, FUN_1403AC530.", + "confidence": "high", + "source": ["Change_Owner_0x574D0E", "FUN_1403963F0", "FUN_14039BCB0", "FUN_1403AC530"] + }, + { + "offset": "0x33E", + "type": "uint8", + "width": 1, + "name": "unknown_component_index_0x33E", + "description": "Component lookup table entry. Checked in FUN_1403A4820: when != -1 and FUN_1403751F0 returns false, the function returns early. Likely relates to a specific behavior type.", + "confidence": "medium", + "source": ["FUN_1403A4820"] + }, + { + "offset": "0x348", + "type": "uint8", + "width": 1, + "name": "hardpoint_manager_index", + "description": "Shorthand for component_lookup_table[0x16]. When 0xFF, the object uses the direct HP path (no hardpoint indirection). When any other value, damage routing goes through the hardpoint manager. Checked in Make_Invulnerable and Take_Damage paths.", + "confidence": "high", + "source": ["Make_Invulnerable_0x3ABB80"] + }, + { + "offset": "0x34B", + "type": "uint8", + "width": 1, + "name": "ability_manager_index", + "description": "Shorthand for component_lookup_table[0x19]. When != -1, the object has an ability system. Checked in FUN_1403A4820 to look up ability component and check +0x54 state.", + "confidence": "medium", + "source": ["FUN_1403A4820"] + }, + { + "offset": "0x36F", + "type": "uint8", + "width": 1, + "name": "transform_component_index", + "description": "Shorthand for component_lookup_table[0x3D]. When != -1, the object has a transform component. Checked in Change_Owner for position updates.", + "confidence": "medium", + "source": ["Change_Owner_0x574D0E"] + }, + { + "offset": "0x39E", + "type": "uint8", + "width": 1, + "name": "registered_with_session_flag", + "description": "Set to 1 when FUN_14029BFE0(session_context, this) is called, indicating the object has been registered with the game session manager. Checked before calling FUN_14029BFE0 to avoid double-registration. Set in FUN_14038CB30, FUN_1403989A0, FUN_1403A4820.", + "confidence": "high", + "source": ["FUN_14038CB30", "FUN_1403989A0", "FUN_1403A4820"] + }, + { + "offset": "0x39F", + "type": "uint8", + "width": 1, + "name": "spawn_complete_flag", + "description": "Set to 1 in FUN_1403989A0 during the spawn initialization sequence. When == 1 in FUN_1403A4820, enables visual model attachment and hero clash node setup. When == 0 in FUN_1403A4820, skips animation setup.", + "confidence": "high", + "source": ["FUN_1403989A0", "FUN_1403A4820"] + }, + { + "offset": "0x3A0", + "type": "uint8", + "width": 1, + "name": "state_flags_bitfield", + "description": "Bitfield of state/dirty flags. Bit 0 (0x01): HP change notification dirty flag (set by SetHP when object_id matches tracked ID). Bit 1 (0x02): used in ScheduleHeroRespawn -- if set, hero respawn is blocked. Bit 3 (0x08): animation/combat state flag (cleared/set in FUN_1403A92F0). Bit 4 (0x10): position lock flag (blocks render position updates in FUN_1403A8710). Bit 6 (0x40): used in ScheduleHeroRespawn -- if set alongside 0x02, respawn is blocked. Bit 7 (0x80): sign bit -- checked in FUN_1403989A0 (if < 0, skips hardpoint allocation).", + "confidence": "high", + "source": ["SetHP_0x3A89D0", "FUN_1403A8710", "FUN_1403A92F0", "FUN_1403989A0", "ScheduleHeroRespawn_0x48EB10"], + "bits": { + "0": "hp_dirty_notification", + "1": "death_state_flag", + "3": "combat_animation_state", + "4": "position_lock", + "6": "despawn_pending", + "7": "no_hardpoints_flag" + } + }, + { + "offset": "0x3A1", + "type": "uint8", + "width": 1, + "name": "dirty_flags_bitfield", + "description": "Dirty/state flags bitfield. Bit 0 (0x01): transform dirty -- set in FUN_1403A8710 when position changes, cleared in FUN_1403AC530 at the end of transform update. Bit 3 (0x08): checked in FUN_1403A4820 for animation state. Bit 7 (0x80): prevent-death flag. When set, Take_Damage_Impl clamps HP to max(1.0, current_hp).", + "confidence": "high", + "source": ["FUN_1403A8710", "FUN_1403AC530", "FUN_1403989A0", "FUN_1403A4820"], + "bits": { + "0": "transform_dirty", + "3": "animation_state_flag", + "7": "prevent_death (cannot_be_killed)" + } + }, + { + "offset": "0x3A6", + "type": "uint16", + "width": 2, + "name": "state_0x3A6", + "description": "Cleared to 0 in FUN_1403989A0 during spawn init.", + "confidence": "low", + "source": ["FUN_1403989A0"] + }, + { + "offset": "0x3A7", + "type": "uint8", + "width": 1, + "name": "invulnerability_flag", + "description": "Invulnerability state. Checked by Take_Damage_Outer at multiple points. WARNING: Setting this byte manually does NOT work -- you must call Make_Invulnerable_Setter (RVA 0x3ABB80) which propagates to hardpoints and updates the behavior system. Located within the 0x3A6 cleared region.", + "confidence": "high", + "source": ["alamo_engine_kb.json"] + }, + { + "offset": "0x3A8", + "type": "uint32", + "width": 4, + "name": "state_0x3A8", + "description": "Cleared to 0 in FUN_1403989A0 during spawn init.", + "confidence": "low", + "source": ["FUN_1403989A0"] + }, + { + "offset": "0x3B5", + "type": "uint32", + "width": 4, + "name": "state_0x3B5", + "description": "Cleared to 0 in FUN_1403989A0.", + "confidence": "low", + "source": ["FUN_1403989A0"] + }, + { + "offset": "0x3B9", + "type": "uint8", + "width": 1, + "name": "state_0x3B9", + "description": "Cleared to 0 in FUN_1403989A0.", + "confidence": "low", + "source": ["FUN_1403989A0"] + }, + { + "offset": "0x3C0", + "type": "uint64", + "width": 8, + "name": "state_0x3C0", + "description": "Cleared to 0 in FUN_1403989A0. Likely the last field -- places minimum struct size at 0x3C8.", + "confidence": "medium", + "source": ["FUN_1403989A0"] + } + ], + "component_system": { + "description": "GameObjectClass uses a dynamic component architecture. Components are stored in a flat array at +0x278, indexed by a lookup table at +0x332. The lookup table maps query type IDs (0-0x69) to array indices. QueryInterface (vtable slot 3, RVA 0x395AC0) performs the lookup: if table[query_type] == 0xFF, return NULL; else return component_array[table[query_type]].", + "array_ptr_offset": "0x278", + "lookup_table_offset": "0x332", + "lookup_table_size": 106, + "count_offset": "0x288", + "capacity_offset": "0x289", + "query_interface_rva": "0x395AC0" + }, + "position_system": { + "description": "Position is stored in three coordinate sets. spawn_position (0x68-0x70) holds the initial spawn point. current_position (0x78-0x80) holds the actual world position used by gameplay. render_position (0x84-0x8C) holds the interpolated position for rendering. previous_position (0x90-0x98) holds the last-frame position for interpolation/facing calculation.", + "spawn_position": {"x": "0x68", "y": "0x6C", "z": "0x70"}, + "current_position": {"x": "0x78", "y": "0x7C", "z": "0x80"}, + "render_position": {"x": "0x84", "y": "0x88", "z": "0x8C"}, + "previous_position": {"x": "0x90", "y": "0x94", "z": "0x98"} + }, + "scene_node_sub_fields": { + "description": "The scene node at +0x2A0 contains the 3D visual representation. Key sub-fields for the scene node object:", + "+0x88": "animation_state (int32, -1 = none)", + "+0xA4": "visual_position_x (float32)", + "+0xA8": "visual_position_y (float32)", + "+0xAC": "visual_position_z (float32)", + "+0xB0": "visual_position_copy_x (float32)", + "+0xB4": "visual_position_copy_y (float32)", + "+0xB8": "visual_position_copy_z (float32)" + }, + "shield_system": { + "description": "Shields are managed through the behavior/component system, NOT through direct fields on GameObjectClass. ShieldBehaviorClass (size 0x48) and BaseShieldBehaviorClass (size 0x40) are separate component objects obtained via QueryInterface. The shield percentage is computed by the Lua wrapper calling through the component vtable. AffectedByShieldBehaviorClass and AffectedByShieldDataPackClass handle shield interaction data.", + "behavior_classes": [ + "ShieldBehaviorClass (size 0x48, vtable at FUN_140549130 area)", + "BaseShieldBehaviorClass (size 0x40, vtable at FUN_14056AB40 area)", + "AffectedByShieldBehaviorClass", + "AffectedByShieldDataPackClass" + ], + "note": "To read shield values at runtime, use QueryInterface to get the shield behavior component, then read from the component's internal fields. There is no direct offset on GameObjectClass for shield HP." + }, + "max_health_system": { + "description": "Max HP is NOT stored on the GameObjectClass instance. It is computed dynamically by FUN_1403727A0 which reads the base value from GameObjectType+0xDCC and applies multipliers: game mode multiplier (land vs space), player faction tech bonus (from Player+0x360 -> tech modifier at +0x50), and buff/debuff modifiers from the combat system. SetHP uses this function to clamp current HP to [0, max_hp].", + "base_max_hp_location": "GameObjectType+0xDCC (float32)", + "compute_function_rva": "0x3727A0" + } + }, + "functions_decompiled": [ + {"rva": "0x3A89D0", "name": "SetHP", "fields_accessed": ["0x5C", "0x50", "0x58", "0x298", "0x3A0"]}, + {"rva": "0x395AC0", "name": "QueryInterface", "fields_accessed": ["0x332", "0x278"]}, + {"rva": "0x3A8C90", "name": "SetSpeedOverride", "fields_accessed": ["0xA8"]}, + {"rva": "0x38F8B0", "name": "ClearSpeedOverride", "fields_accessed": ["0xA8"]}, + {"rva": "0x3ABB80", "name": "Make_Invulnerable_Setter", "fields_accessed": ["0x78", "0x7C", "0x80", "0x110", "0x2A0", "0x348"]}, + {"rva": "0x5792E0", "name": "Get_Owner_Lua", "fields_accessed": ["0x60", "0x335", "0x58", "0xB8"]}, + {"rva": "0x574BF0", "name": "Change_Owner_Lua", "fields_accessed": ["0x60", "0x335", "0x58", "0x298", "0x36F", "0x2A0", "0x3A0"]}, + {"rva": "0x48EB10", "name": "ScheduleHeroRespawn", "fields_accessed": ["0x298", "0x58", "0x2B0", "0x3A0", "0x330"]}, + {"rva": "0x388720", "name": "GameObjectClass_Constructor1", "fields_accessed": "all_fields_initialized"}, + {"rva": "0x388B60", "name": "GameObjectClass_Constructor2", "fields_accessed": "all_fields_initialized"}, + {"rva": "0x3989A0", "name": "SpawnInit", "fields_accessed": ["0x290", "0x39E", "0x2A0", "0x2A8", "0x2B0", "0x2D0", "0xA8-0x240", "0x332-0x39A", "0x298", "0x5C", "0x300", "0x3A6", "0x3A8", "0x3B5", "0x3B9", "0x3C0", "0x100", "0x39E", "0x200"]}, + {"rva": "0x3A8710", "name": "UpdateRenderPosition", "fields_accessed": ["0x84", "0x88", "0x8C", "0x90", "0x94", "0x98", "0x3A0", "0x3A1"]}, + {"rva": "0x3AC530", "name": "TransformUpdate", "fields_accessed": ["0x248-0x270", "0x78", "0x7C", "0x80", "0x94", "0x8C", "0x335", "0x3A1"]}, + {"rva": "0x3973B0", "name": "GetTeamNode", "fields_accessed": ["0x298 (via QueryInterface 4,5,0x16)"]}, + {"rva": "0x3727A0", "name": "GetMaxHealth", "fields_accessed": ["0x58", "0x298+0xDCC"]}, + {"rva": "0x3A92F0", "name": "TakeDamageDispatch", "fields_accessed": ["0x2A0", "0x100", "0x3A0"]}, + {"rva": "0x3956C0", "name": "ResolveOwnerIdentity", "fields_accessed": ["0xB8", "0x58"]}, + {"rva": "0x395C70", "name": "GetCombatBuffs", "fields_accessed": ["0x58", "0xF0"]}, + {"rva": "0x3963F0", "name": "WalkContainerChain", "fields_accessed": ["0x335", "0x2B0"]}, + {"rva": "0x396380", "name": "GetTeamNodeOrHardpoint", "fields_accessed": ["0x348 (via QueryInterface 0x16)"]}, + {"rva": "0x39BCB0", "name": "UpdateSceneManager", "fields_accessed": ["0x2B0", "0x348", "0x333"]}, + {"rva": "0x38CB30", "name": "AddComponents", "fields_accessed": ["0x278", "0x288", "0x289", "0x290", "0x332", "0x39E", "0x2B8"]}, + {"rva": "0x3A4820", "name": "UpdateVisualModel", "fields_accessed": ["0x298", "0x2B8", "0x33E", "0x2A0", "0x3A0", "0x3A1", "0x34B", "0x39F", "0x2E0", "0x2F8"]}, + {"rva": "0x3A4510", "name": "UpdateAILabel", "fields_accessed": ["0x2A8", "0x2B8", "0x298", "0x58"]}, + {"rva": "0x3A5840", "name": "CleanupGarrisonNode", "fields_accessed": ["0x2A0", "0x2E0", "0x298", "0x2F8"]}, + {"rva": "0x395920", "name": "EvaluateTargetPriority", "fields_accessed": ["0xD8", "0x2B8", "0x58", "0xB8", "0x335", "0x298"]} + ] +} diff --git a/re-findings/network_system.json b/re-findings/network_system.json new file mode 100644 index 000000000..7247f7ead --- /dev/null +++ b/re-findings/network_system.json @@ -0,0 +1,352 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "SWFOC Alamo Engine — Network/Multiplayer Protocol Documentation", + "description": "Reverse-engineered documentation of the multiplayer architecture, command event format, synchronization mechanism, and desync-relevant memory areas in Star Wars: Forces of Corruption (Alamo engine, x86_64 Steam build).", + "_meta": { + "analysis_date": "2026-04-04", + "binary": "StarWarsG.exe (x86_64, MSVC)", + "tools": ["Ghidra 12.0.3 + MCP bridge", "RTTI recovery (1703 classes)"], + "analyst": "Agent 3G (Network/Multiplayer Protocol RE)", + "confidence": "medium-high — class hierarchy and event IDs are from RTTI + decompilation; internal protocol details inferred from structural evidence" + }, + + "architecture": { + "type": "peer_to_peer_lockstep", + "summary": "The Alamo engine uses a deterministic lockstep architecture. All peers execute the same simulation independently, synchronizing only player commands (events). There is no authoritative server — every peer runs the full game simulation. The FrameSyncEventClass (event ID 17) acts as the lockstep barrier, and FrameInfoEventClass (event ID 0) exchanges per-frame metadata. PerformanceMetricsEventClass (event ID 18) likely carries timing/checksum data for desync detection.", + "evidence": [ + "FrameSyncEventClass inherits ScheduledEventClass -> EventClass, sets event_type_id = 0x11 (17). It is registered via EventFactoryClass. The name 'FrameSync' directly indicates lockstep frame synchronization.", + "FrameInfoEventClass (event ID 0) is the lowest-numbered event and inherits directly from EventClass (not ScheduledEventClass), suggesting it is a meta/control event exchanged every frame.", + "PerformanceMetricsEventClass (event ID 18 = 0x12) also inherits directly from EventClass (not ScheduledEventClass), indicating it is a non-gameplay control message — likely carrying frame timing and/or checksum data.", + "All gameplay-affecting events (Attack, Move, Production, etc.) inherit from ScheduledEventClass, meaning they are queued and executed at a deterministic frame. This is the hallmark of lockstep: commands are scheduled for a future frame, then all peers execute them simultaneously.", + "No 'Replicate', 'NetState', 'AuthoritativeServer', or state-replication classes were found. All discovered event classes are player commands, not state snapshots — confirming command-based lockstep rather than state replication.", + "The GameModeClass constructor allocates a DynamicVectorClass — a queue for events that must wait until their scheduled frame arrives." + ] + }, + + "transport_layer": { + "summary": "Dual transport: Winsock (direct IP/LAN) and Steam Networking (online via Steam). Both implement an AsyncSocket interface, abstracted behind PacketHandlerClass which runs on its own thread.", + "socket_implementations": { + "SteamAsyncSocketImpl": { + "constructor_rva": "0x6b370", + "description": "Steam-based socket using Valve's ISteamNetworking relay. Wraps Steam P2P networking API. Initialized with linked list node (0x30 bytes) and 64-bit SteamID handle at offset 0x20.", + "inherits": ["RefCountClass"] + }, + "WinsockAsyncSocketImpl": { + "constructor_rva": "0x227110", + "description": "Traditional Winsock socket for LAN/direct-IP games. Constructor takes a 16-byte param (likely sockaddr_in: IP + port). Calls WSAGetLastError (imported at 0x227400) for error handling.", + "inherits": ["RefCountClass"] + } + }, + "packet_handler": { + "class": "PacketHandlerClass", + "destructor_rva": "0x2054c0", + "description": "Thread-based packet processing engine. Inherits from ThreadClass. Uses Windows CriticalSection for thread safety, a mutex with 10-second timeout for shutdown synchronization, and a linked-list packet queue. Dispatches received packets to registered callback functions typed as: void(*)(PacketHandlerClass*, void*, IPAddressClass&). Also supports raw byte callbacks: bool(*)(unsigned char*, int, IPAddressClass*, void*).", + "threading_note": "PacketHandlerClass::~PacketHandlerClass logs 'ThreadLockMutexClass -- %s failed to obtain mutex within 10 seconds' — confirming it runs on a dedicated network thread separate from the game thread." + }, + "packet_class": { + "class": "PacketClass", + "constructor_rva": "0x23bc40", + "description": "Network packet. Inherits from BitStreamClass (bit-level serialization) and MultiLinkedListMember (for queue management). Constructor takes (int size, longlong buffer). If buffer is null and size > 0, allocates its own buffer. The BitStreamClass base provides bit-granularity read/write with separate read and write cursors.", + "inherits": ["BitStreamClass", "MultiLinkedListMember", "RefCountClass"], + "serialization_format": "bit-packed via BitStreamClass — fields are written at arbitrary bit widths, not byte-aligned. This is typical for lockstep RTS games to minimize bandwidth." + }, + "ip_address_class": { + "class": "IPAddressClass", + "description": "Wraps an IP address. Used in packet dispatch callbacks and maintained in DynamicVectorClass collections." + }, + "broadcaster_class": { + "class": "BroadcasterClass", + "description": "Manages packet distribution to connected peers. Contains a DynamicVectorClass for tracking sent packets and a DynamicVectorClass for the peer list. Also referenced as BroadcasterClass::Get_Local_Player in the knowledge base, indicating it tracks which peer is the local player.", + "inner_types": ["BroadcasterClass::PacketIdentifierStruct"] + }, + "connection_class": { + "class": "ConnectionClass", + "description": "Represents a single peer connection. Stored in DynamicVectorClass managed by BroadcasterClass." + }, + "packet_type_class": { + "class": "PacketTypeClass", + "description": "Packet type registry with magic number validation. Contains DynamicVectorClass — each packet type has a 'magic' identifier for protocol-level validation and dispatch.", + "inner_types": ["PacketTypeClass::tPacketMagicStruct"] + } + }, + + "steam_integration": { + "summary": "Online multiplayer uses Steam lobbies via SteamPeerLobbyClass (singleton). Matchmaking, chat, mod sync, and player management all flow through Steam callbacks.", + "steam_api_imports": [ + "SteamInternal_CreateInterface", + "SteamInternal_ContextInit", + "SteamAPI_GetHSteamUser", + "SteamAPI_GetHSteamPipe", + "SteamAPI_Init", + "SteamAPI_RegisterCallback", + "SteamAPI_UnregisterCallback", + "SteamAPI_RegisterCallResult", + "SteamAPI_UnregisterCallResult", + "SteamAPI_RunCallbacks", + "SteamAPI_Shutdown" + ], + "steam_peer_lobby_class": { + "class": "SteamPeerLobbyClass", + "constructor_rva": "0x6ca10", + "singleton": true, + "description": "Central Steam multiplayer manager. Registers 6 persistent callbacks and 3 call-result handlers with the Steam API during construction.", + "steam_callbacks_registered": [ + {"type": "LobbyInvite_t", "callback_id": "0x1F7 (503)", "handler_rva": "0x72540", "description": "Fires when the local user receives a lobby invite from a friend."}, + {"type": "LobbyKicked_t", "callback_id": "0x200 (512)", "handler_rva": "0x72550", "description": "Fires when the local user is kicked from a lobby."}, + {"type": "LobbyChatMsg_t", "callback_id": "0x1FB (507)", "handler_rva": "0x72570", "description": "Fires when a chat message is received in the lobby."}, + {"type": "LobbyDataUpdate_t", "callback_id": "0x1F9 (505)", "handler_rva": "0x726C0", "description": "Fires when lobby metadata changes (player ready state, game settings, etc.)."}, + {"type": "LobbyChatUpdate_t", "callback_id": "0x1FA (506)", "handler_rva": "0x726D0", "description": "Fires when a user joins, leaves, or disconnects from the lobby."}, + {"type": "ItemInstalled_t", "callback_id": "0xD4D (3405)", "handler_rva": "0x72730", "description": "Fires when a Steam Workshop item (mod) is installed."} + ], + "steam_call_results": [ + {"type": "LobbyMatchList_t", "callback_id": "0x1FE (510)", "description": "Result of ISteamMatchmaking::RequestLobbyList — returns available lobbies."}, + {"type": "LobbyCreated_t", "callback_id": "0x201 (513)", "description": "Result of ISteamMatchmaking::CreateLobby — returns new lobby ID."}, + {"type": "LobbyEnter_t", "callback_id": "0x1F8 (504)", "description": "Result of ISteamMatchmaking::JoinLobby — confirms lobby entry."} + ], + "ugc_integration": "CCallResult at 0x6ca10 shows SteamPeerLobbyClass also handles Workshop (UGC) query results, likely for mod compatibility checking in the lobby." + }, + "steam_lobby_dialog_class": { + "class": "SteamLobbyDialogClass", + "singleton": true, + "description": "UI dialog for the Steam lobby browser/creation screen." + }, + "steam_class": { + "class": "SteamClass", + "constructor_rva": "0x6a000", + "singleton": true, + "description": "Top-level Steam integration singleton. Wraps initialization/shutdown." + }, + "internet_player_struct": { + "class": "InternetPlayerStruct", + "description": "Represents a player in an online (Internet/Steam) game. Stored in DynamicVectorClass collections (multiple instances at 0x17260, 0x332c30 etc.). Contains a ClanStruct sub-struct for Steam clan/group information.", + "inner_types": ["InternetPlayerStruct::ClanStruct"] + } + }, + + "event_system": { + "summary": "The command synchronization protocol is built on an event system. All player actions are encoded as Event objects with a numeric type ID, serialized via BitStreamClass into PacketClass packets, broadcast to all peers, and executed deterministically at a scheduled frame. The EventFactoryClass pattern uses template parameters for registration.", + + "class_hierarchy": { + "EventClass": { + "constructor_rva": "0x5126c0", + "description": "Base event class. Has a type ID at offset 0x8 (set in each subclass constructor), a linked-list entry for queue management, and a frame counter from a global source.", + "key_fields": [ + {"offset": "0x00", "name": "linked_list_entry", "type": "LinkedListEntryClass"}, + {"offset": "0x08", "name": "event_type_id", "type": "int32", "description": "Numeric event type. Set in each subclass constructor. Matches the factory template parameter."} + ] + }, + "ScheduledEventClass": { + "constructor_rva": "0x512c50", + "inherits": ["EventClass"], + "description": "Event scheduled for execution at a specific future frame. Most gameplay events inherit from this, not raw EventClass. Contains a scheduled frame number at offset 0x0 (initialized to 0xFFFFFFFF = unscheduled).", + "key_fields": [ + {"offset": "0x00", "name": "scheduled_frame", "type": "int32", "description": "Frame number at which this event should execute. 0xFFFFFFFF = not yet scheduled. 0x80000000 bit at offset 0x1c may be a 'processed' flag."} + ] + }, + "EventQueueClass": { + "constructor_rva": "0x4b39e0", + "description": "Queue that holds events awaiting execution. Allocates a slot array sized from a global (DAT_140a16fb0 = likely max_players or max_events_per_frame). Events are dequeued and executed when their scheduled frame arrives." + }, + "BaseEventFactoryClass": { + "constructor_rva": "0x5960f0", + "description": "Base factory that registers event types. Constructor adds itself to a global DynamicVectorClass at DAT_140b36bc1, building the event type registry at static initialization time." + } + }, + + "event_catalog": { + "_note": "Complete catalog of all 50+ event types. The type_id matches the EventFactoryClass template second parameter AND the value written to EventClass.offset_0x8 in each constructor. Events marked 'scheduled' inherit from ScheduledEventClass; 'immediate' inherit directly from EventClass.", + "control_events": [ + {"id": 0, "hex": "0x00", "class": "FrameInfoEventClass", "rva": "0x4c1b00", "scheduled": false, "category": "sync", "description": "Per-frame metadata exchange. Immediate (not scheduled). Lowest ID = processed first. Likely carries frame number and timing data."}, + {"id": 17, "hex": "0x11", "class": "FrameSyncEventClass", "rva": "0x4c1d30", "scheduled": true, "category": "sync", "description": "Lockstep frame synchronization barrier. All peers must receive this before advancing to the next simulation frame."}, + {"id": 18, "hex": "0x12", "class": "PerformanceMetricsEventClass", "rva": "0x4c1eb0", "scheduled": false, "category": "sync", "description": "Performance/checksum data exchange. Immediate (not scheduled). Likely carries frame timing metrics and state checksums for desync detection."}, + {"id": 15, "hex": "0x0F", "class": "QuitGameEventClass", "rva": "0x4993f0", "scheduled": true, "category": "session", "description": "Player quit/disconnect notification."}, + {"id": 16, "hex": "0x10", "class": "ChatEventClass", "rva": "0x44d1d0", "scheduled": false, "category": "social", "description": "In-game chat message. Immediate (not frame-synced). Has string buffer fields."}, + {"id": 39, "hex": "0x27", "class": "GameOptionsEventClass", "rva": "0x44d530", "scheduled": true, "category": "session", "description": "Game options/settings change (speed, difficulty, etc.)."}, + {"id": 51, "hex": "0x33", "class": "ResumeGameEventClass", "rva": "0x4adfd0", "scheduled": true, "category": "session", "description": "Resume game from pause."}, + {"id": 36, "hex": "0x24", "class": "SaveGameEventClass", "rva": "0x48fa80", "scheduled": true, "category": "session", "description": "Trigger synchronized save. Has string field (save name)."}, + {"id": 11, "hex": "0x0B", "class": "DebugEventClass", "rva": "0x4ad0f0", "scheduled": true, "category": "debug", "description": "Debug/diagnostic command. Has string buffer."}, + {"id": 50, "hex": "0x32", "class": "TauntEventClass", "rva": "0x4d6a60", "scheduled": true, "category": "social", "description": "Player taunt/emote in multiplayer."} + ], + "movement_events": [ + {"id": 1, "hex": "0x01", "class": "MoveToPositionEventClass", "rva": "0x3acee0", "scheduled": true, "category": "movement", "description": "Move units to a map position."}, + {"id": 2, "hex": "0x02", "class": "MoveToObjectEventClass", "rva": "0x3aee50", "scheduled": true, "category": "movement", "description": "Move units to a target object."}, + {"id": 3, "hex": "0x03", "class": "MoveObjectToObjectEventClass", "rva": "0x3af290", "scheduled": true, "category": "movement", "description": "Move an object to another object (e.g., docking)."}, + {"id": 4, "hex": "0x04", "class": "LookEventClass", "rva": "0x689670", "scheduled": true, "category": "movement", "description": "Camera look command."}, + {"id": 10, "hex": "0x0A", "class": "MoveThroughObjectsEventClass", "rva": "0x3c41c0", "scheduled": true, "category": "movement", "description": "Move through/past objects (waypoint movement)."}, + {"id": 14, "hex": "0x0E", "class": "MoveToRayEventClass", "rva": "0x3ae4a0", "scheduled": true, "category": "movement", "description": "Move to a ray-cast position (terrain click)."}, + {"id": 19, "hex": "0x13", "class": "MoveToRayFacingEventClass", "rva": "0x3ae950", "scheduled": true, "category": "movement", "description": "Move to ray position with facing direction."}, + {"id": 20, "hex": "0x14", "class": "FacingEventClass", "rva": "0x409b40", "scheduled": true, "category": "movement", "description": "Change unit facing direction. Has a 0x200-byte unit list."}, + {"id": 29, "hex": "0x1D", "class": "StopMovementEventClass", "rva": "n/a", "scheduled": true, "category": "movement", "description": "Halt unit movement."}, + {"id": 47, "hex": "0x2F", "class": "MoveToPositionFacingEventClass", "rva": "0x3add90", "scheduled": true, "category": "movement", "description": "Move to position with facing."}, + {"id": 55, "hex": "0x37", "class": "MoveToGarrisonEventClass", "rva": "0x3ad3b0", "scheduled": true, "category": "movement", "description": "Move units into a garrison structure."} + ], + "combat_events": [ + {"id": 6, "hex": "0x06", "class": "AttackEventClass", "rva": "0x3af4c0", "scheduled": true, "category": "combat", "description": "Attack command. Has a 0x208-byte payload (likely unit selection list + target ID)."}, + {"id": 13, "hex": "0x0D", "class": "SpecialAbilityEventClass", "rva": "0x4b4210", "scheduled": true, "category": "combat", "description": "Galactic-level special ability activation. Large struct (0x108+ bytes) with object type references, coordinates (0x3fffff sentinel for unset)."}, + {"id": 25, "hex": "0x19", "class": "SpecialWeaponFireEventClass", "rva": "n/a", "scheduled": true, "category": "combat", "description": "Fire special weapon."}, + {"id": 28, "hex": "0x1C", "class": "BombingRunEventClass", "rva": "0x524560", "scheduled": true, "category": "combat", "description": "Call in a bombing run."}, + {"id": 40, "hex": "0x28", "class": "TacticalSpecialAbilityEventClass", "rva": "0x4292e0", "scheduled": true, "category": "combat", "description": "Tactical battle special ability. 0x20C+ byte struct with coordinate sentinels."}, + {"id": 54, "hex": "0x36", "class": "PlanetaryBombardEventClass", "rva": "0x52afc0", "scheduled": true, "category": "combat", "description": "Initiate orbital bombardment."}, + {"id": 57, "hex": "0x39", "class": "TacticalSpecialAbilityWithDummyTargetEventClass", "rva": "n/a", "scheduled": true, "category": "combat", "description": "Special ability with a dummy/placeholder target."}, + {"id": 38, "hex": "0x26", "class": "TacticalSuperWeaponEventClass", "rva": "n/a", "scheduled": true, "category": "combat", "description": "Activate a super weapon in tactical mode."} + ], + "production_economy_events": [ + {"id": 7, "hex": "0x07", "class": "ProductionEventClass", "rva": "0x523f50", "scheduled": true, "category": "economy", "description": "Queue/cancel unit or building production."}, + {"id": 8, "hex": "0x08", "class": "FleetManagementEventClass", "rva": "0x5af020", "scheduled": true, "category": "economy", "description": "Fleet assembly/management. Has 4 object ID fields (0x3fffff sentinel) and a signed offset at 0x14."}, + {"id": 9, "hex": "0x09", "class": "InvadeEventClass", "rva": "0x48fce0", "scheduled": true, "category": "galactic", "description": "Invade a planet. 0x200+ byte selection list with planet sentinel."}, + {"id": 12, "hex": "0x0C", "class": "ReinforceEventClass", "rva": "0x403ab0", "scheduled": true, "category": "economy", "description": "Bring in reinforcements. Uses 0x3fffff sentinel for target location."}, + {"id": 33, "hex": "0x21", "class": "TacticalBuildEventClass", "rva": "0x5d7740", "scheduled": true, "category": "economy", "description": "Build a structure in tactical mode."}, + {"id": 34, "hex": "0x22", "class": "TacticalSellEventClass", "rva": "0x689810", "scheduled": true, "category": "economy", "description": "Sell a tactical structure."}, + {"id": 42, "hex": "0x2A", "class": "DistributeMoneyEventClass", "rva": "0x498ca0", "scheduled": true, "category": "economy", "description": "Transfer credits to an ally."}, + {"id": 46, "hex": "0x2E", "class": "GalacticSellEventClass", "rva": "0x532690", "scheduled": true, "category": "economy", "description": "Sell unit/structure at galactic level. 0x3fffff object sentinel."}, + {"id": 45, "hex": "0x2D", "class": "RepairHardpointEventClass", "rva": "n/a", "scheduled": true, "category": "economy", "description": "Repair a hardpoint on a space station or capital ship."} + ], + "selection_ui_events": [ + {"id": 5, "hex": "0x05", "class": "SelectEventClass", "rva": "0x3ac9d0", "scheduled": true, "category": "selection", "description": "Unit selection. Has a 0x1FC-byte selection buffer."}, + {"id": 30, "hex": "0x1E", "class": "ControlGroupEventClass", "rva": "0x436060", "scheduled": true, "category": "selection", "description": "Assign/recall control group (Ctrl+1..9)."}, + {"id": 37, "hex": "0x25", "class": "SelectAllEventClass", "rva": "0x437f40", "scheduled": true, "category": "selection", "description": "Select all units."} + ], + "galactic_events": [ + {"id": 21, "hex": "0x15", "class": "EscortEventClass", "rva": "0x3b0010", "scheduled": true, "category": "galactic", "description": "Escort fleet action."}, + {"id": 22, "hex": "0x16", "class": "RetreatEventClass", "rva": "n/a", "scheduled": true, "category": "galactic", "description": "Retreat from battle."}, + {"id": 26, "hex": "0x1A", "class": "CinematicAnimationEventClass", "rva": "0x6899c0", "scheduled": true, "category": "galactic", "description": "Trigger cinematic animation."}, + {"id": 35, "hex": "0x23", "class": "AllyEventClass", "rva": "0x689ad0", "scheduled": true, "category": "diplomacy", "description": "Alliance/diplomacy action. Has player ID field (init -1)."}, + {"id": 49, "hex": "0x31", "class": "WithdrawlEventClass", "rva": "0x4aded0", "scheduled": true, "category": "galactic", "description": "Withdraw from engagement (note: engine typo 'Withdrawl')."}, + {"id": 52, "hex": "0x34", "class": "GarrisonEventClass", "rva": "0x440ad0", "scheduled": true, "category": "galactic", "description": "Garrison troops in a structure."}, + {"id": 48, "hex": "0x30", "class": "PlaceBeaconEventClass", "rva": "n/a", "scheduled": true, "category": "galactic", "description": "Place a marker/beacon on the map."}, + {"id": 53, "hex": "0x35", "class": "SetMarkerIDEventClass", "rva": "n/a", "scheduled": true, "category": "galactic", "description": "Set marker identifier."}, + {"id": 56, "hex": "0x38", "class": "SetGUIIndexEventClass", "rva": "n/a", "scheduled": true, "category": "ui", "description": "Set GUI tab index."}, + {"id": 58, "hex": "0x3A", "class": "SetAbilityAutofireEventClass", "rva": "n/a", "scheduled": true, "category": "galactic", "description": "Toggle ability autofire mode."}, + {"id": 43, "hex": "0x2B", "class": "SetUnitAbilityModeEventClass", "rva": "n/a", "scheduled": true, "category": "galactic", "description": "Set unit ability mode (stance/behavior)."} + ], + "setup_phase_events": [ + {"id": 31, "hex": "0x1F", "class": "SetupPhaseMoveEventClass", "rva": "0x5aea90", "scheduled": true, "category": "setup", "description": "Pre-battle setup phase unit placement."}, + {"id": 32, "hex": "0x20", "class": "SetupPhaseTriggerEndEventClass", "rva": "0x5aef30", "scheduled": true, "category": "setup", "description": "Signal end of setup phase (player ready)."} + ] + } + }, + + "synchronization_mechanism": { + "summary": "Lockstep with deterministic execution. All peers process the same events at the same frame. Three control events (IDs 0, 17, 18) form the sync protocol.", + "frame_sync_protocol": { + "description": "Each simulation frame follows this sequence: (1) Exchange FrameInfoEventClass (ID 0) with frame metadata; (2) Wait for FrameSyncEventClass (ID 17) from all peers — this is the lockstep barrier; (3) Execute all ScheduledEvents whose scheduled_frame matches the current frame; (4) Exchange PerformanceMetricsEventClass (ID 18) with timing/checksum data.", + "lockstep_barrier": "FrameSyncEventClass (ID 17). The simulation cannot advance until all peers have confirmed their events for this frame. This is the classic lockstep 'ready' signal.", + "delayed_event_queue": "GameModeClass maintains a DynamicVectorClass — events that arrived but whose scheduled_frame has not yet been reached. They are held in this queue and executed when the frame counter reaches their scheduled_frame value." + }, + "determinism_requirements": { + "description": "For lockstep to work, all peers must produce identical simulation results from identical inputs. This means the engine must be fully deterministic: same floating-point math, same RNG sequences, same execution order.", + "implications_for_modding": [ + "Any memory modification that changes gameplay state (HP, damage, position, unit counts, production queues) without going through the event system will cause a desync in multiplayer.", + "Single-player mods that directly write to GameObjectClass fields (e.g., HP at offset 0x5C) are safe in SP but will immediately desync in MP.", + "Visual-only modifications (camera position, UI elements, selection highlights) do NOT cause desyncs because they are not part of the deterministic simulation." + ] + }, + "sentinel_values": { + "0x3FFFFF": "Unset/null object reference sentinel. Used extensively in event constructors (FleetManagement, Reinforce, Invade, SpecialAbility, etc.) to indicate 'no target selected'. This is likely an index into a 22-bit object ID space.", + "0xFFFFFFFF": "Unscheduled frame (ScheduledEventClass.scheduled_frame default) or invalid/null reference.", + "0xFFFFFFFE": "Special sentinel in FleetManagementEventClass offset 0x14 — possibly 'auto-assign' or 'any available'." + } + }, + + "desync_detection": { + "summary": "Desync detection is inferred from PerformanceMetricsEventClass (ID 18) and the deterministic lockstep architecture. The engine likely checksums key simulation state each frame and compares across peers.", + "known_mechanisms": { + "performance_metrics_event": { + "event_id": 18, + "class": "PerformanceMetricsEventClass", + "rva": "0x4c1eb0", + "description": "Non-scheduled event (inherits EventClass directly) exchanged every frame. Despite the name 'PerformanceMetrics', in lockstep RTS engines this class typically carries: (1) frame timing data (for adaptive game speed), (2) state checksums (for desync detection). The fact that it is exchanged every frame and is NOT a ScheduledEvent confirms it is sync infrastructure, not gameplay." + }, + "frame_info_event": { + "event_id": 0, + "class": "FrameInfoEventClass", + "rva": "0x4c1b00", + "description": "Frame metadata. Likely carries the current frame number and possibly a hash/checksum of the game state." + } + }, + "what_would_be_checksummed": { + "description": "Based on the event system and Alamo engine structure, the following simulation state is most likely included in per-frame checksums:", + "high_confidence": [ + "Global game frame counter", + "RNG state (random number generator seed/state)", + "Total unit count per player", + "Total credits/income per player", + "Production queue state" + ], + "medium_confidence": [ + "Unit positions (at least checksum of all positions)", + "Unit HP values", + "Unit ownership (player IDs)", + "Active ability timers", + "Planet control state (galactic mode)" + ], + "low_confidence_but_possible": [ + "Individual unit state checksums", + "AI decision state", + "Pathfinding state" + ] + }, + "what_is_safe_to_modify_in_multiplayer": { + "safe": [ + "Camera position and zoom (not synchronized)", + "UI state (selection highlights, tooltips, HUD layout)", + "Sound/music settings", + "Visual effects (particle counts, texture quality)", + "Fog of war reveal (local rendering only — but may still be detected by FoW state checksumming)", + "ChatEventClass content (ID 16 — non-scheduled, social only)" + ], + "unsafe_will_desync": [ + "GameObjectClass.hp (offset 0x5C)", + "GameObjectClass.owner_player_id (offset 0x58)", + "GameObjectClass.object_id (offset 0x50)", + "Any production queue modification", + "Any unit spawn/destroy outside the event system", + "Any credit/resource value change", + "Any RNG state modification", + "Any movement/position override that bypasses ScheduledEventClass" + ] + } + }, + + "game_mode_system": { + "summary": "GameModeClass is the battle/map controller. LandModeClass extends it for ground battles. Both manage event processing and the delayed event queue.", + "game_mode_class": { + "class": "GameModeClass", + "constructor_rva": "0x35a5e0", + "description": "Large struct (~0x60 * 8 bytes). Manages the tactical/galactic game state, event queue (DelayedEventStruct vector), objectives (ObjectiveStruct vector), game speed (float at 0x10+4 = 0x3F800000 = 1.0f default), and player-specific state arrays.", + "key_fields_observed": [ + "Offset 0x10+4: game_speed float (default 1.0 = 0x3F800000)", + "Offset 0x28*8: DynamicVectorClass — delayed event queue", + "Offset 0x2B*8: DynamicVectorClass", + "Offset 0x37*8: DynamicVectorClass — possibly player scores or frame counters", + "Offset 0x3B*8: DynamicVectorClass> — per-player object lists" + ] + }, + "land_mode_class": { + "class": "LandModeClass", + "constructor_rva": "0x3b5210", + "inherits": ["GameModeClass"], + "description": "Ground battle mode. Extends GameModeClass with additional linked lists for ground-specific state and a HeroClashManagerClass at offset 0x3F0." + } + }, + + "key_addresses": { + "_note": "All addresses are absolute (image base 0x140000000). Subtract image base for RVA.", + "event_factory_registry": "DAT_140b36bc1 — global DynamicVectorClass where all event factories register at static init", + "max_event_slots": "DAT_140a16fb0 — global int used to size the EventQueueClass slot array and GameModeClass player arrays", + "frame_counter_source": "FUN_140294a70 with DAT_140a16fd0 — called in EventClass constructor to get current frame number", + "steam_peer_lobby_singleton": "SingletonInstance at static init 0x7e6450", + "steam_lobby_dialog_singleton": "SingletonInstance at static init 0x7e6590", + "steam_class_singleton": "SingletonInstance at static init 0x7e6580" + }, + + "open_questions": [ + "Exact checksum algorithm used for desync detection (could not locate a CRC/hash function directly referenced by PerformanceMetricsEventClass — needs runtime tracing)", + "Exact format of PerformanceMetricsEventClass payload (needs decompilation of its virtual Pack/Unpack methods, which are in the vtable)", + "Whether the engine supports 'late join' or only 'lobby start' (likely lobby-only given pure lockstep)", + "Maximum player count (the sentinel 0x3FFFFF = 22 bits suggests up to ~4M object IDs, but player count is likely 2-8 based on the 'max_event_slots' global)", + "Whether DebugEventClass (ID 11) is stripped in release builds or can be used to inject commands", + "The exact PacketTypeClass::tPacketMagicStruct format — likely a 4-byte magic + version for protocol handshake", + "Whether the game uses NAT punchthrough (likely via Steam relay) or requires port forwarding for Winsock mode" + ] +} diff --git a/re-findings/playerobject_complete.json b/re-findings/playerobject_complete.json new file mode 100644 index 000000000..ae221066a --- /dev/null +++ b/re-findings/playerobject_complete.json @@ -0,0 +1,641 @@ +{ + "struct_name": "PlayerClass", + "rtti_mangled_name": ".?AVPlayerClass@@", + "vtable_rva": "discovered_in_destructor", + "inherits": ["RefCountClass", "SignalGeneratorClass"], + "estimated_size": 1232, + "estimated_size_hex": "0x4D0", + "notes": [ + "PlayerClass is the actual RTTI class name (confirmed via ~PlayerClass destructor at 0x14027ED40).", + "Previously called 'PlayerObject' in the KB -- that was an informal name.", + "The struct inherits RefCountClass and SignalGeneratorClass (confirmed in destructor).", + "Accessed via PlayerListClass global (RVA 0xA16FD0) -> player_array[slot_index].", + "PlayerArray shortcut at RVA 0xA16FF0 points directly into the array.", + "PlayerWrapper (Lua binding) stores the PlayerClass pointer at wrapper+0x28.", + "AIPlayerClass at +0x360 is a separate object, NOT inlined into PlayerClass." + ], + "fields": [ + { + "offset": "0x00", + "name": "vtable_ptr", + "type": "pointer", + "size": 8, + "status": "CONFIRMED", + "evidence": "Standard vtable pointer. Destructor writes two vtable values during teardown." + }, + { + "offset": "0x08", + "name": "refcount_data", + "type": "struct_inline", + "size": 8, + "status": "DISCOVERED", + "evidence": "RefCountClass base class data. Destructor calls FID_conflict__Tidy_deallocate on offset_0x8." + }, + { + "offset": "0x10", + "name": "signal_generator_base", + "type": "struct_inline", + "size": 32, + "status": "DISCOVERED", + "evidence": "SignalGeneratorClass base class. Destructor calls SignalGeneratorClass::~SignalGeneratorClass." + }, + { + "offset": "0x37", + "name": "playable", + "type": "uint8", + "size": 1, + "status": "CONFIRMED", + "evidence": "KB + rvas.h PlayerObj::Playable = 0x37. Used to determine if player slot is active." + }, + { + "offset": "0x38", + "name": "padding_38", + "type": "unknown", + "size": 8, + "status": "DISCOVERED", + "evidence": "Gap between playable flag and DynamicVectorClass region starting at +0x40." + }, + { + "offset": "0x48", + "name": "slot_index", + "type": "int32", + "size": 4, + "status": "CONFIRMED", + "evidence": "KB + rvas.h PlayerObj::SlotIndex = 0x48. Matches owner_player_id on GameObjectClass+0x58." + }, + { + "offset": "0x4C", + "name": "player_id", + "type": "int32", + "size": 4, + "status": "CONFIRMED", + "evidence": "Accessed in AddCredits as *(param_1 + 0x4c), SetTechLevel as *(param_1 + 0x4c), PlayerList_FindByID returns matching player by this ID, Lua_Select_Object reads *(player + 0x4c). Used as the primary player identifier across the engine." + }, + { + "offset": "0x54", + "name": "team_id", + "type": "int32", + "size": 4, + "status": "DISCOVERED", + "evidence": "Getter at 0x140602EE0 reads *(player + 0x54) and returns as double. Context suggests team or secondary ID. The LuaValue getter wraps this int32." + }, + { + "offset": "0x58", + "name": "ai_player_type_id", + "type": "int32", + "size": 4, + "status": "DISCOVERED", + "evidence": "AIPlayerClass constructor stores second param at +0x58 of the AI object. Referenced in Get_Name wrapper (reads *(*(player + 0x360) + 0x58)) to get AI type for name lookup." + }, + { + "offset": "0x62", + "name": "local_player", + "type": "uint8", + "size": 1, + "status": "CONFIRMED", + "evidence": "KB + rvas.h PlayerObj::LocalPlayer = 0x62. Lua getter at 0x140603960 reads *(player + 0x62) as bool. Returns whether this is the local human player." + }, + { + "offset": "0x68", + "name": "faction_ref", + "type": "pointer", + "size": 8, + "status": "CONFIRMED", + "evidence": "KB + rvas.h PlayerObj::FactionName = 0x68. SetTechLevel compares factions via FUN_1403751a0(obj, *(player + 0x68)). Lua_Get_Faction_Name getter at 0x140602A00 reads *(player + 0x68) and extracts string. Change_Owner reads this. Contains FactionClass pointer; faction name string is at *(faction + 0x28) with MSVC SSO at faction+0x40 capacity." + }, + { + "offset": "0x70", + "name": "credits", + "type": "float32", + "size": 4, + "status": "CONFIRMED", + "evidence": "KB + rvas.h PlayerObj::Credits = 0x70. AddCredits (0x14027F370): reads/writes *(float*)(param_1 + 0x70). Lua_Get_Credits getter at 0x1406027F0 reads *(float*)(player + 0x70). Income multiplier applied for positive adds. Clamped to [0, max_credits]." + }, + { + "offset": "0x74", + "name": "max_credits", + "type": "float32", + "size": 4, + "status": "CONFIRMED", + "evidence": "KB + rvas.h PlayerObj::MaxCredits = 0x74. AddCredits: if *(float*)(param_1 + 0x74) >= 0, credits clamped to this value. If negative, no cap enforced." + }, + { + "offset": "0x84", + "name": "tech_level", + "type": "int32", + "size": 4, + "status": "CONFIRMED", + "evidence": "KB + rvas.h PlayerObj::TechLevel = 0x84. SetTechLevel (0x140288980): *(int*)(param_1 + 0x84) = clamped value. Lua_Get_Tech_Level at 0x140603040 reads *(int*)(player + 0x84). Auto-upgrade function at 0x140282190 increments this. Compared against objects' +0x89C tech requirement." + }, + { + "offset": "0x88", + "name": "max_tech_level", + "type": "int32", + "size": 4, + "status": "CONFIRMED", + "evidence": "KB + rvas.h PlayerObj::MaxTechLevel = 0x88. SetTechLevel: param_2 clamped to *(int*)(param_1 + 0x88). Auto-upgrade checks *(param_1 + 0x84) < *(param_1 + 0x88)." + }, + { + "offset": "0x8C", + "name": "dvec_int_array_0", + "type": "DynamicVectorClass", + "size": 24, + "status": "DISCOVERED", + "evidence": "Destructor runs a loop destroying 10 inlined DynamicVectorClass objects starting from offset_0xd0 region, iterating backwards in 24-byte strides. The first starts around 0x8C-0xD0 range." + }, + { + "offset": "0xE0", + "name": "historically_built_types", + "type": "DynamicVectorClass", + "size": 24, + "status": "CONFIRMED", + "evidence": "Destructor calls ~DynamicVectorClass at offset_0xe0." + }, + { + "offset": "0xF8", + "name": "buildable_types_1", + "type": "DynamicVectorClass", + "size": 24, + "status": "DISCOVERED", + "evidence": "Destructor calls ~DynamicVectorClass at offset_0xf8." + }, + { + "offset": "0x108", + "name": "is_human_controlled", + "type": "uint8", + "size": 1, + "status": "CONFIRMED", + "evidence": "Lua getter at 0x140603A40 reads *(byte*)(player + 0x108) as bool. Returns LuaValue. Maps to 'Is_Human' / 'IsHumanControlled' Lua binding." + }, + { + "offset": "0x110", + "name": "buildable_types_2", + "type": "DynamicVectorClass", + "size": 24, + "status": "DISCOVERED", + "evidence": "Destructor calls ~DynamicVectorClass at offset_0x110." + }, + { + "offset": "0x128", + "name": "buildable_types_3", + "type": "DynamicVectorClass", + "size": 24, + "status": "DISCOVERED", + "evidence": "Destructor calls ~DynamicVectorClass at offset_0x128." + }, + { + "offset": "0x140", + "name": "player_data_block_1", + "type": "struct_unknown", + "size": 64, + "status": "DISCOVERED", + "evidence": "Destructor calls cleanup function FUN_14027eb10 at field_0x140. Also calls FUN_140289530 at field_0x140." + }, + { + "offset": "0x180", + "name": "player_data_block_2", + "type": "struct_unknown", + "size": 64, + "status": "DISCOVERED", + "evidence": "Destructor calls cleanup function FUN_14027eb10 at field_0x180. Also calls FUN_140289530 at field_0x180." + }, + { + "offset": "0x1A8", + "name": "unlockable_types_dvec_header", + "type": "DynamicVectorClass_header", + "size": 8, + "status": "DISCOVERED", + "evidence": "FUN_140047aa0 called with (param_1 + 0x1a8) for vector resize in add_to_unlocked_list (0x14027F860)." + }, + { + "offset": "0x1B0", + "name": "unlocked_types_array", + "type": "pointer", + "size": 8, + "status": "CONFIRMED", + "evidence": "Unlock_Tech (0x14027F860), Lock_Tech (0x140286150), IsUnlocked check (0x140282580) all read *(ptr*)(player + 0x1B0) as the data pointer of a DynamicVectorClass. Contains the list of unlocked buildable types." + }, + { + "offset": "0x1B8", + "name": "unlocked_types_count", + "type": "int32", + "size": 4, + "status": "CONFIRMED", + "evidence": "IsUnlocked (0x140282580) reads *(int*)(player + 0x1B8) for iteration count. Unlock_Tech increments it. Lock_Tech decrements via vector remove." + }, + { + "offset": "0x1BC", + "name": "unlocked_types_capacity", + "type": "int32", + "size": 4, + "status": "CONFIRMED", + "evidence": "Unlock_Tech: capacity = *(int*)(param_1 + 0x1BC) * 2 >> 1 for growth check." + }, + { + "offset": "0x1C0", + "name": "locked_types_dvec", + "type": "DynamicVectorClass", + "size": 24, + "status": "CONFIRMED", + "evidence": "Destructor calls FUN_14027ea70 at offset_0x1c0. Lock_Tech_Add (0x14027F7C0) uses offset 0x1C0 header, 0x1C8 data ptr, 0x1D0 count, 0x1D4 capacity. IsLocked (0x140282550) reads 0x1C8/0x1D0." + }, + { + "offset": "0x1C8", + "name": "locked_types_array", + "type": "pointer", + "size": 8, + "status": "CONFIRMED", + "evidence": "IsLocked (0x140282550) reads *(ptr*)(player + 0x1C8). Unlock_Tech_Remove (0x140286100) reads same. Lock_Tech_Add writes to it." + }, + { + "offset": "0x1D0", + "name": "locked_types_count", + "type": "int32", + "size": 4, + "status": "CONFIRMED", + "evidence": "IsLocked reads *(int*)(player + 0x1D0) for loop bound. Lock_Tech_Add increments." + }, + { + "offset": "0x1D4", + "name": "locked_types_capacity", + "type": "int32", + "size": 4, + "status": "CONFIRMED", + "evidence": "Lock_Tech_Add: capacity = *(int*)(param_1 + 0x1D4) * 2 >> 1 for growth." + }, + { + "offset": "0x1E0", + "name": "type_list_4", + "type": "DynamicVectorClass", + "size": 24, + "status": "DISCOVERED", + "evidence": "Destructor calls ~DynamicVectorClass at offset_0x1e0." + }, + { + "offset": "0x1F8", + "name": "type_list_5", + "type": "DynamicVectorClass", + "size": 24, + "status": "DISCOVERED", + "evidence": "Destructor calls ~DynamicVectorClass at offset_0x1f8." + }, + { + "offset": "0x210", + "name": "type_list_6", + "type": "DynamicVectorClass", + "size": 24, + "status": "DISCOVERED", + "evidence": "Destructor calls ~DynamicVectorClass at offset_0x210." + }, + { + "offset": "0x228", + "name": "type_list_7", + "type": "DynamicVectorClass", + "size": 24, + "status": "DISCOVERED", + "evidence": "Destructor calls ~DynamicVectorClass at offset_0x228." + }, + { + "offset": "0x250", + "name": "type_list_8_vtable", + "type": "DynamicVectorClass", + "size": 24, + "status": "DISCOVERED", + "evidence": "Destructor sets offset_0x250 = DynamicVectorClass vtable, then frees offset_0x258 data." + }, + { + "offset": "0x258", + "name": "type_list_8_data", + "type": "pointer", + "size": 8, + "status": "DISCOVERED", + "evidence": "Destructor frees offset_0x258 (data ptr), resets offset_0x260 (count?) and offset_0x264 (capacity flags)." + }, + { + "offset": "0x268", + "name": "type_list_9_vtable", + "type": "DynamicVectorClass", + "size": 24, + "status": "DISCOVERED", + "evidence": "Destructor sets offset_0x268 = DynamicVectorClass vtable, then frees offset_0x270 data." + }, + { + "offset": "0x270", + "name": "type_list_9_data", + "type": "pointer", + "size": 8, + "status": "DISCOVERED", + "evidence": "Destructor frees offset_0x270 and resets offset_0x278 (count), offset_0x27c (capacity)." + }, + { + "offset": "0x280", + "name": "int_vector", + "type": "DynamicVectorClass", + "size": 24, + "status": "DISCOVERED", + "evidence": "Destructor calls ~DynamicVectorClass at offset_0x280." + }, + { + "offset": "0x298", + "name": "type_list_10", + "type": "DynamicVectorClass", + "size": 24, + "status": "DISCOVERED", + "evidence": "Destructor calls ~DynamicVectorClass at offset_0x298." + }, + { + "offset": "0x2B0", + "name": "sub_object_ptr", + "type": "pointer", + "size": 8, + "status": "DISCOVERED", + "evidence": "Destructor: calls virtual destructor on *(offset_0x2b0), then nulls it. A polymorphic sub-object." + }, + { + "offset": "0x2C0", + "name": "heap_buffer_1", + "type": "pointer", + "size": 8, + "status": "DISCOVERED", + "evidence": "Destructor calls thunk_FUN_1407864b8 (operator delete) on offset_0x2c0." + }, + { + "offset": "0x2C8", + "name": "sub_struct_2c8", + "type": "struct_unknown", + "size": 40, + "status": "DISCOVERED", + "evidence": "Destructor calls FUN_1404c5b80 at field_0x2c8. Likely a specialized container." + }, + { + "offset": "0x2F0", + "name": "large_sub_object", + "type": "pointer", + "size": 8, + "status": "DISCOVERED", + "evidence": "Destructor: if offset_0x2f0 != 0, calls FUN_140496830 then frees with size 0xB8. Contains MSVC SSO strings at sub+0x38, sub+0x58 (FileClass), sub+0xe0, sub+0x100. Likely a save/load or profile data structure." + }, + { + "offset": "0x318", + "name": "data_block_318", + "type": "struct_unknown", + "size": 24, + "status": "DISCOVERED", + "evidence": "Destructor calls FUN_14027ecc0 at offset_0x318." + }, + { + "offset": "0x360", + "name": "ai_player_ptr", + "type": "pointer (AIPlayerClass*)", + "size": 8, + "status": "CONFIRMED", + "evidence": "AddCredits: if *(ptr*)(player + 0x360) != 0, applies income multiplier from AI budget. SetTechLevel: propagates to AI via this pointer. Lua_Enable_As_Actor reads *(player + 0x360). Lua_Release_Credits_For_Tactical reads *(player + 0x360). Get_Name reads *(*(player + 0x360) + 0x58). Non-null for AI players, null for human players. Destructor calls FUN_140285dc0 at offset_0x360." + }, + { + "offset": "0x370", + "name": "diplomacy_table", + "type": "pointer (int32[])", + "size": 8, + "status": "CONFIRMED", + "evidence": "Is_Enemy (0x1402824F0): returns *(int*)(*(ptr*)(player + 0x370) + other_player_id * 4) == 1. Is_Ally (0x1402823E0): returns same[...] == 0. Make_Ally/Make_Enemy (0x140288800): writes *(int*)(*(ptr*)(player + 0x370) + target_id * 4) = 0 or 1. Array indexed by player_id, int32 per entry: 0=ally, 1=enemy." + }, + { + "offset": "0x378", + "name": "profile_data_ptr", + "type": "pointer", + "size": 8, + "status": "DISCOVERED", + "evidence": "Destructor: if offset_0x378 != 0, destroys MSVC SSO strings at sub+0x100, sub+0xe0, destroys FileClass at sub+0x58, destroys SSO at sub+0x38, frees with size 0x120. Large data structure -- likely player profile or save metadata." + }, + { + "offset": "0x380", + "name": "black_market_items", + "type": "DynamicVectorClass", + "size": 24, + "status": "CONFIRMED", + "evidence": "Destructor calls ~DynamicVectorClass at offset_0x380." + }, + { + "offset": "0x398", + "name": "difficulty_level", + "type": "int32", + "size": 4, + "status": "DISCOVERED", + "evidence": "Lua getter at 0x140602AE0 reads *(int*)(player + 0x398) and returns as double. Context in LuaValue property getter. Likely difficulty or a scoring metric." + }, + { + "offset": "0x3F8", + "name": "advisor_hints_base", + "type": "uint8[]", + "size": 16, + "status": "CONFIRMED", + "evidence": "Lua_Enable_Advisor_Hints (0x140601E80): writes *(char*)(player + 0x3F8 + game_mode_index) = bool_value. Indexed by game mode ID (0=galactic, 1=land, 2=space). Array of per-mode advisor hint enable flags." + }, + { + "offset": "0x448", + "name": "black_market_tutorial_flag", + "type": "uint8", + "size": 1, + "status": "CONFIRMED", + "evidence": "Lua_Set_Black_Market_Tutorial (0x140604300): *(char*)(player + 0x448) = bool. Directly written from Lua boolean parameter." + }, + { + "offset": "0x449", + "name": "sabotage_tutorial_flag", + "type": "uint8", + "size": 1, + "status": "CONFIRMED", + "evidence": "Lua_Set_Sabotage_Tutorial (0x1406043C0): *(char*)(player + 0x449) = bool. Directly written from Lua boolean parameter." + }, + { + "offset": "0x484", + "name": "space_station_level", + "type": "int32", + "size": 4, + "status": "DISCOVERED", + "evidence": "Lua getter at 0x140602690 reads *(int*)(player + 0x484) and returns as double. Last property registered in PlayerWrapper constructor (called with 'Get_Space_Station'). Likely Get_Space_Station_Level return value." + } + ], + "dynamic_vector_classes_in_struct": { + "summary": "The destructor reveals at least 15 DynamicVectorClass instances inlined into PlayerClass, plus 10 DynamicVectorClass instances in a loop. These store buildable types, locked/unlocked tech lists, historically built types, and black market items.", + "confirmed_dvec_offsets": [ + "0xE0 (HistoricallyBuiltObjectType)", + "0xF8 (GameObjectTypeClass const*)", + "0x110 (GameObjectTypeClass const*)", + "0x128 (GameObjectTypeClass const*)", + "0x1E0 (GameObjectTypeClass const*)", + "0x1F8 (GameObjectTypeClass const*)", + "0x210 (GameObjectTypeClass const*)", + "0x228 (GameObjectTypeClass const*)", + "0x280 (int)", + "0x298 (GameObjectTypeClass const*)", + "0x380 (BlackMarketItemClass const*)" + ] + }, + "lua_wrapper_method_map": { + "description": "Mapping from PlayerWrapper constructor registration order to Lua method name and engine function. Each entry has the function pointer stored in the wrapper object, which dispatches to the PlayerClass method.", + "methods": [ + {"lua_name": "Give_Money", "wrapper_rva": "0x603130", "engine_fn_rva": "0x27F370", "fields_accessed": ["0x70", "0x74", "0x360", "0x4C"]}, + {"lua_name": "Get_Faction_Name", "wrapper_rva": "0x602A00", "engine_fn_rva": "n/a (inline)", "fields_accessed": ["0x68"]}, + {"lua_name": "Get_Name (AI type name)", "wrapper_rva": "0x602060", "engine_fn_rva": "n/a (inline)", "fields_accessed": ["0x360"]}, + {"lua_name": "Get_Credits", "wrapper_rva": "0x6027F0", "engine_fn_rva": "n/a (inline)", "fields_accessed": ["0x70"]}, + {"lua_name": "Get_ID", "wrapper_rva": "0x602C40", "engine_fn_rva": "n/a (inline)", "fields_accessed": ["0x4C"]}, + {"lua_name": "Get_Tech_Level", "wrapper_rva": "0x603040", "engine_fn_rva": "n/a (inline)", "fields_accessed": ["0x84"]}, + {"lua_name": "Set_Tech_Level", "wrapper_rva": "0x604480", "engine_fn_rva": "0x288980", "fields_accessed": ["0x84", "0x88", "0x68", "0x4C", "0x1B0", "0x1B8", "0x1BC", "0xC1", "0xC2"]}, + {"lua_name": "Unlock_Tech", "wrapper_rva": "0x604540", "engine_fn_rva": "0x286100 + 0x27F860", "fields_accessed": ["0x1B0", "0x1B8", "0x1C8", "0x1D0"]}, + {"lua_name": "Lock_Tech", "wrapper_rva": "0x603B20", "engine_fn_rva": "0x286150 + 0x27F7C0", "fields_accessed": ["0x1B0", "0x1B8", "0x1C8", "0x1D0", "0x1D4"]}, + {"lua_name": "Is_Enemy", "wrapper_rva": "0x603760", "engine_fn_rva": "0x2824F0", "fields_accessed": ["0x370", "0x4C"]}, + {"lua_name": "Is_Ally", "wrapper_rva": "0x603560", "engine_fn_rva": "0x2823E0", "fields_accessed": ["0x370", "0x4C"]}, + {"lua_name": "Is_Human", "wrapper_rva": "0x603A40", "engine_fn_rva": "n/a (inline)", "fields_accessed": ["0x108"]}, + {"lua_name": "Is_Local_Player", "wrapper_rva": "0x603960", "engine_fn_rva": "n/a (inline)", "fields_accessed": ["0x62"]}, + {"lua_name": "Make_Ally", "wrapper_rva": "0x6046A0", "engine_fn_rva": "0x288800", "fields_accessed": ["0x370", "0x4C"]}, + {"lua_name": "Make_Enemy", "wrapper_rva": "0x604780", "engine_fn_rva": "0x288800", "fields_accessed": ["0x370", "0x4C"]}, + {"lua_name": "Enable_As_Actor", "wrapper_rva": "0x602640", "engine_fn_rva": "0x4B0250", "fields_accessed": ["0x360"]}, + {"lua_name": "Enable_Advisor_Hints", "wrapper_rva": "0x601E80", "engine_fn_rva": "n/a (inline)", "fields_accessed": ["0x3F8"]}, + {"lua_name": "Select_Object", "wrapper_rva": "0x603F60", "engine_fn_rva": "0x2BD2F0", "fields_accessed": ["0x4C"]}, + {"lua_name": "Retreat", "wrapper_rva": "0x603DE0", "engine_fn_rva": "0x340920", "fields_accessed": ["0x4C"]}, + {"lua_name": "Release_Credits_For_Tactical", "wrapper_rva": "0x603C70", "engine_fn_rva": "0x4B06D0", "fields_accessed": ["0x360"]}, + {"lua_name": "Disable_Bombing_Run", "wrapper_rva": "0x601AA0", "engine_fn_rva": "inline", "fields_accessed": ["0x4C"]}, + {"lua_name": "Disable_Orbital_Bombardment", "wrapper_rva": "0x601BE0", "engine_fn_rva": "inline", "fields_accessed": ["0x4C"]}, + {"lua_name": "Remove_Orbital_Bombardment", "wrapper_rva": "0x604860", "engine_fn_rva": "inline", "fields_accessed": ["0x4C"]}, + {"lua_name": "Set_Black_Market_Tutorial", "wrapper_rva": "0x604300", "engine_fn_rva": "n/a (inline)", "fields_accessed": ["0x448"]}, + {"lua_name": "Set_Sabotage_Tutorial", "wrapper_rva": "0x6043C0", "engine_fn_rva": "n/a (inline)", "fields_accessed": ["0x449"]}, + {"lua_name": "Get_Space_Station_Level", "wrapper_rva": "0x602690", "engine_fn_rva": "n/a (inline)", "fields_accessed": ["0x484"]}, + {"lua_name": "Get_Enemy_Player (build picker)", "wrapper_rva": "0x6028D0", "engine_fn_rva": "inline", "fields_accessed": ["0x360", "0x68"]}, + {"lua_name": "Get_Difficulty", "wrapper_rva": "0x602AE0", "engine_fn_rva": "n/a (inline)", "fields_accessed": ["0x398"]}, + {"lua_name": "Get_Team_ID", "wrapper_rva": "0x602EE0", "engine_fn_rva": "n/a (inline)", "fields_accessed": ["0x54"]}, + {"lua_name": "Produce_Object_Type (random tech picker)", "wrapper_rva": "0x6031F0", "engine_fn_rva": "0x27F860 + 0x282190", "fields_accessed": ["0x84", "0x88", "0x68", "0x1B0", "0x1B8", "0xC1", "0xC2"]}, + {"lua_name": "Get_Space_Station", "wrapper_rva": "0x6021C0", "engine_fn_rva": "inline", "fields_accessed": ["0x4C", "0x68"]} + ] + }, + "key_engine_functions": [ + { + "name": "AddCredits", + "rva": "0x27F370", + "signature": "float AddCredits(PlayerClass* player, float amount, char apply_to_ai)", + "fields_accessed": ["0x70", "0x74", "0x4C", "0x360"], + "notes": "If amount > 0 and AI player attached (+0x360 != 0), multiplies by income rate from AI budget at *(ai+0x20). Credits clamped to [0, max_credits] unless max_credits < 0." + }, + { + "name": "SetTechLevel", + "rva": "0x288980", + "signature": "int SetTechLevel(PlayerClass* player, int level, char propagate, char notify)", + "fields_accessed": ["0x84", "0x88", "0x4C", "0x68", "0x1B0", "0x1B8", "0x1BC", "0xC1", "0xC2"], + "notes": "Clamps to [0, max_tech_level]. Iterates game objects matching faction, checks obj+0x89C tech req vs new level, adds to unlocked list if obj+0xC1 and obj+0xC2 flags set." + }, + { + "name": "SetDiplomacy", + "rva": "0x288800", + "signature": "void SetDiplomacy(PlayerClass* player, int target_player_id, int relation)", + "fields_accessed": ["0x370"], + "notes": "Writes relation (0=ally, 1=enemy) into diplomacy_table[target_player_id]. Simple 1-line function." + }, + { + "name": "IsEnemy", + "rva": "0x2824F0", + "signature": "bool IsEnemy(PlayerClass* self, PlayerClass* other)", + "fields_accessed": ["0x370", "0x4C"], + "notes": "Returns diplomacy_table[other->player_id] == 1." + }, + { + "name": "IsAlly", + "rva": "0x2823E0", + "signature": "bool IsAlly(PlayerClass* self, PlayerClass* other)", + "fields_accessed": ["0x370", "0x4C"], + "notes": "Returns diplomacy_table[other->player_id] == 0." + }, + { + "name": "IsUnlocked", + "rva": "0x282580", + "signature": "bool IsUnlocked(PlayerClass* player, GameObjectTypeClass* type)", + "fields_accessed": ["0x1B0", "0x1B8"], + "notes": "Linear scan of unlocked_types_array for matching pointer." + }, + { + "name": "IsLocked", + "rva": "0x282550", + "signature": "bool IsLocked(PlayerClass* player, GameObjectTypeClass* type)", + "fields_accessed": ["0x1C8", "0x1D0"], + "notes": "Linear scan of locked_types_array for matching pointer." + }, + { + "name": "AddToUnlockedList", + "rva": "0x27F860", + "signature": "void AddToUnlockedList(PlayerClass* player, GameObjectTypeClass* type)", + "fields_accessed": ["0x1A8", "0x1B0", "0x1B8", "0x1BC"], + "notes": "Checks for duplicates, grows vector if needed, appends type pointer." + }, + { + "name": "AddToLockedList", + "rva": "0x27F7C0", + "signature": "void AddToLockedList(PlayerClass* player, GameObjectTypeClass* type)", + "fields_accessed": ["0x1C0", "0x1C8", "0x1D0", "0x1D4"], + "notes": "Same pattern as AddToUnlockedList but for locked types vector." + }, + { + "name": "AutoUpgradeTechLevel", + "rva": "0x282190", + "signature": "void AutoUpgradeTechLevel(PlayerClass* player)", + "fields_accessed": ["0x84", "0x88"], + "notes": "If tech_level < max_tech_level, calls SetTechLevel(player, tech_level + 1, 1)." + }, + { + "name": "PlayerList_FindByID", + "rva": "0x294BC0", + "signature": "PlayerClass* PlayerList_FindByID(PlayerListClass* list, int id)", + "fields_accessed": ["0x20", "0x28"], + "notes": "Bounds checks id against player_count, returns player_array[id]." + }, + { + "name": "GetLocalPlayerID", + "rva": "0x294A70", + "signature": "int GetLocalPlayerID(PlayerListClass* list)", + "fields_accessed": ["field_0x30 (local_player_index)"], + "notes": "Returns player_id (0x4C) of the player at list->local_player_index." + } + ], + "functions_analyzed": 35, + "xrefs_followed": 31, + "analysis_method": "Ghidra MCP decompilation of PlayerClass destructor, 30 PlayerWrapper Lua methods, and 12 engine helper functions", + "analysis_date": "2026-04-04", + "field_offset_summary": { + "description": "Quick-reference of all unique offsets with confident names", + "offsets": { + "0x00": "vtable_ptr (pointer)", + "0x08": "refcount_data (inherited)", + "0x37": "playable (byte, CONFIRMED)", + "0x48": "slot_index (int32, CONFIRMED)", + "0x4C": "player_id (int32, CONFIRMED)", + "0x54": "team_id (int32, DISCOVERED)", + "0x62": "local_player (byte, CONFIRMED)", + "0x68": "faction_ref (pointer, CONFIRMED)", + "0x70": "credits (float32, CONFIRMED)", + "0x74": "max_credits (float32, CONFIRMED)", + "0x84": "tech_level (int32, CONFIRMED)", + "0x88": "max_tech_level (int32, CONFIRMED)", + "0xE0": "historically_built_types (DynamicVectorClass)", + "0xF8": "buildable_types_1 (DynamicVectorClass)", + "0x108": "is_human_controlled (byte, CONFIRMED)", + "0x110": "buildable_types_2 (DynamicVectorClass)", + "0x128": "buildable_types_3 (DynamicVectorClass)", + "0x1B0": "unlocked_types_array (pointer, CONFIRMED)", + "0x1B8": "unlocked_types_count (int32, CONFIRMED)", + "0x1BC": "unlocked_types_capacity (int32, CONFIRMED)", + "0x1C8": "locked_types_array (pointer, CONFIRMED)", + "0x1D0": "locked_types_count (int32, CONFIRMED)", + "0x1D4": "locked_types_capacity (int32, CONFIRMED)", + "0x2B0": "sub_object_ptr (pointer)", + "0x2C0": "heap_buffer_1 (pointer)", + "0x2F0": "large_sub_object (pointer, size 0xB8)", + "0x360": "ai_player_ptr (pointer, CONFIRMED)", + "0x370": "diplomacy_table (pointer to int32[], CONFIRMED)", + "0x378": "profile_data_ptr (pointer, size 0x120)", + "0x380": "black_market_items (DynamicVectorClass)", + "0x398": "difficulty_level (int32, DISCOVERED)", + "0x3F8": "advisor_hints_base (byte[], indexed by game_mode)", + "0x448": "black_market_tutorial_flag (byte, CONFIRMED)", + "0x449": "sabotage_tutorial_flag (byte, CONFIRMED)", + "0x484": "space_station_level (int32, DISCOVERED)" + } + } +} diff --git a/re-findings/production_system.json b/re-findings/production_system.json new file mode 100644 index 000000000..5b26ef0b7 --- /dev/null +++ b/re-findings/production_system.json @@ -0,0 +1,1130 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "SWFOC Alamo Engine - Production/Build System Reverse Engineering", + "description": "Complete documentation of the production pipeline, build queue, unit cap system, cost deduction, and build time calculation in the Alamo engine (Star Wars: Empire at War - Forces of Corruption). Derived from Ghidra static analysis of StarWarsG.exe (x86_64).", + "_meta": { + "agent": "Agent 3H - Production/Build System RE", + "analysis_date": "2026-04-04", + "binary": "StarWarsG.exe", + "architecture": "x86_64", + "compiler": "MSVC", + "base_address_observed": "0x7FF6E3770000", + "tools": ["Ghidra 12.0.3 via MCP"], + "confidence_note": "All RVAs are relative to module base. Offsets verified via decompilation cross-references." + }, + + "overview": { + "summary": "The Alamo engine production system is event-driven. When a player requests unit production, a ProductionEventClass (event ID 7) is created and queued. The engine validates prerequisites (tech level, cost, unit cap, build slot availability), then creates an ObjectUnderConstructionClass entry in the production queue. Build time is computed as a multi-factor formula. Credits are deducted up-front (negative delta to PlayerObject+0x70). When the countdown expires, the unit is spawned and ownership transferred.", + "pipeline_stages": [ + "1. VALIDATION: CanProduce check (FUN_1402804d0) - 15+ prerequisite gates", + "2. COST CHECK: GetAdjustedCost (FUN_140400240/FUN_1403711c0) - calculates modified cost", + "3. UNIT CAP CHECK: CheckPopCap (FUN_1402ac320) - validates population capacity", + "4. QUEUE ENTRY CREATION: ObjectUnderConstructionClass allocation (0x38 bytes)", + "5. COST DEDUCTION: AddCredits (FUN_14027f370) with negative cost value", + "6. BUILD TIME CALCULATION: ComputeBuildTime (FUN_140400370) - 5-factor formula", + "7. COUNTDOWN: ScheduledEventClass timer decrements per frame", + "8. COMPLETION: ObjectUnderConstructionClass destructor spawns the unit" + ] + }, + + "classes": { + "ProductionEventClass": { + "description": "Network event for production commands. Extends ScheduledEventClass. Event type ID = 7. Created when a player clicks to produce a unit/building in the UI or via Lua _ProduceObject.", + "constructor_rva": "0x523F50", + "event_factory_rva": "0x01A980", + "event_type_id": 7, + "inherits": ["ScheduledEventClass", "EventClass"], + "fields": [ + { + "offset": "0x08", + "name": "event_type_id", + "type": "int32", + "value": 7, + "note": "Set in constructor: this->EventClass_data.offset_0x8 = 7" + } + ] + }, + + "TacticalBuildEventClass": { + "description": "Network event for tactical-mode construction (building structures in land/space battles). Event type ID = 0x21 (33). Extends ScheduledEventClass.", + "constructor_rva": "0x5D7740", + "event_factory_rva": "0x01AF70", + "event_type_id": 33, + "inherits": ["ScheduledEventClass"], + "fields": [ + { + "offset": "0x00", + "name": "object_bitmask", + "type": "int32", + "default": "0x3FFFFF", + "note": "Bitmask for object/slot reference" + }, + { + "offset": "0x04", + "name": "target_ref", + "type": "int64", + "default": "-1 (0xFFFFFFFFFFFFFFFF)" + } + ] + }, + + "GalacticSellEventClass": { + "description": "Network event for selling units/buildings on the galactic map. Event type ID = 0x2E (46).", + "constructor_rva": "0x532690", + "event_type_id": 46, + "inherits": ["ScheduledEventClass"], + "fields": [ + { + "offset": "0x00", + "name": "object_bitmask", + "type": "int32", + "default": "0x3FFFFF" + } + ] + }, + + "ObjectUnderConstructionClass": { + "description": "Represents a single item in the production queue. Allocated when production begins, destroyed when the unit/building finishes construction. Size = 0x38 bytes. Contains the type being built, build countdown, queue position, and the owner reference.", + "vtable_string": "vftable (set in constructor at 0x3FFF70)", + "allocation_size": "0x38", + "constructor_rva": "0x3FFF70", + "destructor_rva": "0x5242C0", + "completion_handler_rva": "0x42E890", + "serialization_rva": "0x497900", + "fields": [ + { + "offset": "0x00", + "name": "vtable_ptr", + "type": "pointer", + "width": 8, + "description": "Virtual function table pointer" + }, + { + "offset": "0x08", + "name": "producing_object_type_ptr", + "type": "pointer", + "width": 8, + "description": "Pointer to the GameObjectType being produced. Used to resolve the type name via +0xF8 for logging. Set from param_2 (the unit type). Serialized as field ID 6 in save/load." + }, + { + "offset": "0x10", + "name": "build_countdown", + "type": "float32", + "width": 4, + "description": "Remaining build time in seconds. Computed by ComputeBuildTime (FUN_140400370) and stored as float. Logged as 'countdown is %.2f seconds'. Decremented by the scheduler each frame. Serialized as field ID 7." + }, + { + "offset": "0x14", + "name": "prerequisite_list", + "type": "DynamicVectorClass", + "width": 8, + "description": "Inline DynamicVector for prerequisite tracking. Initialized via FUN_1404ad3a0. Serialized as field ID 0x15." + }, + { + "offset": "0x1C", + "name": "prerequisite_count", + "type": "int32", + "width": 4, + "description": "Number of prerequisites. Serialized as field ID 9." + }, + { + "offset": "0x20", + "name": "player_slot_id", + "type": "int32", + "width": 4, + "description": "Player slot that requested this production. Copied from param_4+0x4C (player's slot). Serialized as field ID 0x0B. Also stored separately at offset +0x2C." + }, + { + "offset": "0x24", + "name": "queue_flags", + "type": "int32", + "width": 4, + "description": "Production flags (e.g., queued vs immediate). Serialized as field ID 0x0B." + }, + { + "offset": "0x28", + "name": "build_cost", + "type": "float32", + "width": 4, + "description": "The computed cost at time of queue insertion. Used for refund on cancellation. Equals the result of GetAdjustedCost." + }, + { + "offset": "0x2C", + "name": "producing_object_id", + "type": "int32", + "width": 4, + "description": "Object ID of the production facility. Copied from param_3+0x50. Serialized as field ID 0x0C. Default 0x3FFFFF." + }, + { + "offset": "0x30", + "name": "completion_callback", + "type": "pointer", + "width": 8, + "description": "Callback or reference used at completion. Set to 0 in constructor." + } + ], + "log_message": "Object '%s' added to production queue at '%s' by player %d: countdown is %.2f seconds, once underway.", + "log_format_notes": "arg1 = type_name of unit being built (from producing_object_type_ptr+0xF8), arg2 = type_name of production facility (from param_3->GameObjectType+0xF8), arg3 = player_slot_id, arg4 = build_countdown" + }, + + "ProductionBehaviorClass": { + "description": "Behavior component attached to game objects that can produce units (space stations, factories, barracks, etc.). Inherits from BehaviorClass. Size = 0x40 bytes (from scalar deleting destructor). Manages the production queue for its parent object.", + "destructor_rva": "0x3FFF10", + "allocation_size": "0x40", + "inherits": ["BehaviorClass"], + "vtable_count": 3, + "note": "Destructor shows 3 vtable assignments before calling BehaviorClass::~BehaviorClass, indicating triple inheritance or 3 interface implementations." + }, + + "ProductionDataPackClass": { + "description": "Data pack for production state persistence. Contains the production queues (2 queues: galactic units and galactic structures/upgrades), organized as DynamicVectorClass. Discovered in the constructor at 0x559680 where it is initialized with ProductionDataPackClass::vftable.", + "constructor_rva": "0x559680", + "inherits": ["DynamicVectorClass"], + "structure": { + "queue_count": 2, + "queue_types": ["Galactic Units (queue 0)", "Galactic Structures/Upgrades (queue 1)"], + "ai_task_list": "MultiLinkedListClass per queue (0x58 bytes each)" + } + }, + + "TacticalBuildObjectsBehaviorClass": { + "description": "Behavior for tactical-mode building construction. Manages structures built during land/space battles (turrets, repair stations, etc.).", + "destructor_rva": "0x42D850", + "allocation_size": "0x40", + "inherits": ["BehaviorClass"] + }, + + "TacticalBuildObjectsDataPackClass": { + "description": "Data pack for tactical build state persistence. Tracks objects built during tactical battles.", + "constructor_rva": "0x561270", + "destructor_rva": "0x5612A0", + "fields": [ + { + "offset": "0x00", + "name": "build_count", + "type": "int32", + "default": 0 + }, + { + "offset": "0x08", + "name": "build_list_ptr", + "type": "pointer", + "default": 0 + }, + { + "offset": "0x10", + "name": "slot_index", + "type": "int32", + "default": "-1 (0xFFFFFFFF)" + }, + { + "offset": "0x14", + "name": "build_state", + "type": "int32", + "default": 0 + }, + { + "offset": "0x1C", + "name": "build_progress", + "type": "int32", + "default": 0 + } + ] + }, + + "TacticalUnderConstructionBehaviorClass": { + "description": "Behavior for objects currently under construction in tactical mode. Handles the building animation, partial construction, and completion logic.", + "destructor_rva": "0x5D6D60", + "data_pack_rva": "0x5617E0" + }, + + "AIPlanetBuildTaskClass": { + "description": "AI build task for planet-level production. Used by AI players to queue and prioritize production decisions. Size = 0x58 (from MultiLinkedListClass container).", + "constructor_rva": "0x6954B0", + "inherits": ["RefCountClass", "MultiLinkedListMember"], + "fields": [ + { + "offset": "0x00", + "name": "ref_count", + "type": "int32" + }, + { + "offset": "0x08", + "name": "build_target_ptr", + "type": "pointer", + "description": "What to build" + }, + { + "offset": "0x10", + "name": "priority", + "type": "int32" + }, + { + "offset": "0x18", + "name": "planet_ref", + "type": "pointer", + "description": "Where to build" + }, + { + "offset": "0x28", + "name": "magic_seed", + "type": "int64", + "default": "0x5D5E0B6B", + "description": "Debug/validation magic number" + }, + { + "offset": "0x30", + "name": "is_active", + "type": "uint16", + "default": 1 + }, + { + "offset": "0x3C", + "name": "failure_reason", + "type": "int32", + "default": "-1 (0xFFFFFFFF)" + } + ] + }, + + "ProduceForceBlockStatus": { + "description": "Lua blocking status for the Produce_Force AI command. When AI Lua scripts call Produce_Force(), this BlockingStatus object tracks whether the production is complete. Extends LuaMemberFunctionWrapper.", + "constructor_rva": "0x6C6500", + "inherits": ["LuaMemberFunctionWrapper"], + "fields": [ + { + "offset": "0x38", + "name": "target_type_ptr", + "type": "pointer", + "default": 0 + }, + { + "offset": "0x40", + "name": "produce_state", + "type": "int32", + "default": 0 + }, + { + "offset": "0x48", + "name": "completion_flag", + "type": "int32", + "default": 0 + } + ] + }, + + "ReduceProductionPriceAbilityClass": { + "description": "Special ability that reduces production cost. Applied by hero units or planet bonuses. Extends SpecialAbilityClass. Ability type ID = 3.", + "constructor_rva": "0x70A280", + "inherits": ["SpecialAbilityClass", "IXMLLoadableClass", "RefCountClass"], + "ability_type_id": 3, + "fields": [ + { + "offset": "0x00", + "name": "price_reduction_factor", + "type": "float32/pointer", + "description": "The fractional reduction applied to production cost. Feeds into the cost modifier chain." + }, + { + "offset": "0x08", + "name": "scope_filter", + "type": "pointer", + "description": "Filter for which unit types are affected by this reduction." + } + ] + }, + + "ReduceProductionTimeAbilityClass": { + "description": "Special ability that reduces production time. Applied by hero units or planet bonuses. Extends SpecialAbilityClass. Ability type ID = 3.", + "constructor_rva": "0x70B040", + "inherits": ["SpecialAbilityClass", "IXMLLoadableClass", "RefCountClass"], + "ability_type_id": 3, + "fields": [ + { + "offset": "0x00", + "name": "time_reduction_factor", + "type": "float32/pointer", + "description": "The fractional reduction applied to production time. Feeds into the build time modifier chain." + }, + { + "offset": "0x08", + "name": "scope_filter", + "type": "pointer", + "description": "Filter for which unit types are affected." + } + ] + }, + + "StarbaseUpgradeAbilityClass": { + "description": "Special ability for upgrading space stations. Ability type ID = 0x0C (12).", + "constructor_rva": "0x70EB50", + "inherits": ["SpecialAbilityClass"], + "ability_type_id": 12 + } + }, + + "functions": { + "CanProduce": { + "rva": "0x2804D0", + "signature": "bool CanProduce(PlayerObject* player, GameObjectType* type_to_build, GameObjectClass* production_facility, bool check_credits, ...out_params...)", + "description": "Master validation gate for production. Returns 1 if the object can be produced, 0 otherwise. Checks 15+ conditions in sequence. This is the most critical function in the production pipeline.", + "prerequisite_checks": [ + { + "order": 1, + "check": "type_to_build != NULL", + "failure_code": null, + "description": "Null type check" + }, + { + "order": 2, + "check": "production_facility != NULL", + "failure_code": null, + "description": "Null facility check" + }, + { + "order": 3, + "check": "production_facility[0x67] != 0xFF", + "failure_code": null, + "description": "Facility component query type check (has production component)" + }, + { + "order": 4, + "check": "type_to_build+0x21 != 1", + "failure_code": null, + "description": "Type is not disabled/locked" + }, + { + "order": 5, + "check": "FUN_1403751a0(type, player.faction_affiliation)", + "failure_code": null, + "description": "Faction affiliation check - type belongs to player's faction" + }, + { + "order": 6, + "check": "GetAdjustedCost(type, facility) >= 1", + "failure_code": null, + "description": "Cost must be positive (at least 1 credit)" + }, + { + "order": 7, + "check": "ComputeBuildTime(type, facility) >= 0.0", + "failure_code": null, + "description": "Build time must be non-negative" + }, + { + "order": 8, + "check": "!HasProperty(type, 0x5B, -1)", + "failure_code": null, + "description": "Type does not have 'unbuildable' property flag (0x5B)" + }, + { + "order": 9, + "check": "IsValidBuildLocation(type) OR HasGroundBuildFlag(type)", + "failure_code": null, + "description": "Build slot availability check" + }, + { + "order": 10, + "check": "player.credits >= cost (if check_credits && player.is_human && !free_build_mode)", + "failure_code": null, + "description": "Credit sufficiency check (only for human players when not in free-build mode). Reads player+0x70 (credits) and player+0x62 (is_human flag)" + }, + { + "order": 11, + "check": "player.tech_level(+0x84) >= type.required_tech_level(+0x89C) AND player.max_tech(+0x88) >= type.tech_level_req(+0x894)", + "failure_code": "error_code=1 (insufficient tech)", + "description": "Tech level prerequisite. Compares PlayerObject+0x84 against GameObjectType+0x89C (min tech level) and PlayerObject+0x88 against GameObjectType+0x894 (tech level requirement)." + }, + { + "order": 12, + "check": "FUN_1403F8AA0(parent, facility) >= type+0xF0C", + "failure_code": "error_code=2", + "description": "Build pad/slot count check. Verifies the planet/facility has enough build slots (GameObjectType+0xF0C)." + }, + { + "order": 13, + "check": "FUN_1403F8B30(parent, facility) >= type+0xF10", + "failure_code": "error_code=3", + "description": "Secondary slot requirement check (GameObjectType+0xF10)" + }, + { + "order": 14, + "check": "type category check (0x5F or 0x60) - space vs land unit cap", + "failure_code": null, + "description": "Checks unit cap per build category. For category 0x5F (space): validates against GetMaxSpaceUnits. For category 0x60 (land): validates against GetMaxLandUnits." + }, + { + "order": 15, + "check": "HasPrerequisites(type+0xF50, facility_type) if type+0xF60 > 0", + "failure_code": "error_code=5", + "description": "Build prerequisites - other structures/tech that must exist" + }, + { + "order": 16, + "check": "type+0x888 (build_limit_per_player) - check instance count", + "failure_code": null, + "description": "Per-player build limit. If type+0x888 > 0, counts existing instances across all planets." + }, + { + "order": 17, + "check": "type+0x880 (build_limit_global) - check total instances", + "failure_code": "error_code=6", + "description": "Global build limit. If type+0x880 > 0, counts instances across all allied players." + }, + { + "order": 18, + "check": "HasDuplicateCheck(type, facility_type)", + "failure_code": "error_code=8", + "description": "Prevents duplicate unique buildings" + } + ] + }, + + "ComputeBuildTime": { + "rva": "0x400370", + "signature": "float ComputeBuildTime(void* production_behavior, GameObjectType* type, GameObjectClass* facility)", + "description": "Computes the actual build time in seconds for producing a unit. Returns a multi-factor product of: base time, ability modifiers, tech level scaling, concurrent build penalty, and faction modifier. This is the core build time formula.", + "formula": "(base_time * price_ability_mod * tech_level_factor * faction_income_mod * planet_modifier) / concurrent_build_count", + "formula_details": { + "base_time": { + "source": "GameObjectType+0x890", + "type": "int32", + "description": "Base build time in engine units. Multiplied by a global speed scalar at DAT_140b15920 to get seconds. Read by FUN_140371230.", + "global_scalar_rva": "0xB15920" + }, + "price_ability_mod": { + "source": "ReduceProductionTimeAbilityClass via modifier system", + "description": "Computed by FUN_14055a010. Scans active abilities on the production facility and player. Uses a tree-based accumulator with GreaterThan and Plus functors. Returns (1.0 - max_reduction), clamped." + }, + "tech_level_factor": { + "source": "DAT_140b16dd4..DAT_140b16de4 (5 float globals)", + "description": "Scaling factor based on tech level difference. Computed as: station_level = facility.parent+0xB8+0x148; diff = station_level - type+0x89C. Maps diff values 1-5 to the 5 globals. Only applies if the type is a 'can_queue_from_starbase' type (category check passes).", + "globals": { + "tech_diff_1": "DAT_140b16dd4", + "tech_diff_2": "DAT_140b16dd8", + "tech_diff_3": "DAT_140b16ddc", + "tech_diff_4": "DAT_140b16de0", + "tech_diff_5": "DAT_140b16de4" + } + }, + "faction_income_mod": { + "source": "Player faction data -> faction+0x28", + "description": "Income modifier from faction. Read when player.is_human is true and not in free-build mode. Accessed via FUN_1404b0500(player+0x360)+0x28." + }, + "planet_modifier": { + "source": "Planet data array at planet+0x2FE8/0x2FE0", + "description": "Per-planet build time modifier. Indexed by game mode (space/land). Read by FUN_14033e410." + }, + "concurrent_build_count": { + "source": "Counted from production facility's container", + "description": "Number of concurrent build slots being used. Min 1. Each occupied slot at facility+0xB8+0x158/0x170 that shares the same GameObjectType is counted by FUN_1404b8170. Divides the total build time (more slots = faster per-unit, but total resources stay same)." + } + }, + "return": "float: build time in seconds. Used to initialize ObjectUnderConstructionClass+0x10." + }, + + "GetAdjustedCost": { + "rva": "0x400240", + "signature": "float GetAdjustedCost(void* production_behavior, GameObjectType* type, GameObjectClass* facility)", + "description": "Computes the adjusted credit cost for producing a unit. Applies ability modifiers (ReduceProductionPriceAbilityClass), faction modifiers, and planet-level cost adjustments.", + "formula": "base_cost * ability_price_mod * planet_cost_mod * tech_upgrade_mod", + "formula_details": { + "base_cost": { + "source": "GameObjectType+0x86C", + "type": "int32", + "description": "Base credit cost from XML data. Read by FUN_1403711C0 when no production behavior override is present." + }, + "ability_price_mod": { + "source": "ReduceProductionPriceAbilityClass via FUN_140559C10", + "description": "Price reduction from active abilities. Same tree-based accumulator as build time. Returns (1.0 - max_reduction)." + }, + "planet_cost_mod": { + "source": "Planet data array at planet+0x3000/0x2FF8", + "description": "Per-planet cost modifier. Read by FUN_14033E3E0. Indexed by game mode." + }, + "tech_upgrade_mod": { + "source": "Player faction upgrade bonus", + "description": "Only applies when type+0x894 (tech_level_req) != 0. Reads from faction via Plus::Plus_Constructor_or_Destructor." + } + }, + "return": "float: adjusted credit cost. Negative of this is passed to AddCredits." + }, + + "GetBaseCost": { + "rva": "0x3711C0", + "signature": "int GetBaseCost(GameObjectType* type, GameObjectClass* facility)", + "description": "Returns the base credit cost for a unit type. If the facility has a production behavior override (QueryInterface(6)), delegates to GetAdjustedCost. Otherwise returns type+0x86C * global_scalar.", + "base_cost_field": "GameObjectType+0x86C (int32)" + }, + + "AddCredits": { + "rva": "0x27F370", + "signature": "float AddCredits(PlayerObject* player, float amount, bool track_income)", + "description": "Adds (or subtracts) credits from a player. For production, called with negative amount (cost). Writes to player+0x70 (credits). Clamps to [0, max_credits]. If amount > 0 and player has income modifier at +0x360, multiplies by the modifier at [+0x360]->+0x20. Core function for all economic transactions.", + "key_fields": { + "credits": "PlayerObject+0x70 (float32)", + "max_credits": "PlayerObject+0x74 (float32, negative = no cap)", + "income_modifier_ptr": "PlayerObject+0x360 (pointer to modifier object)" + }, + "income_modifier_offset": "+0x20 from the modifier object (float multiplier for positive adds)" + }, + + "CheckPopCap": { + "rva": "0x2AC320", + "signature": "bool CheckPopCap(void* object_list_mgr, GameObjectType* type, uint player_slot, bool include_in_queue)", + "description": "Validates that the player has not exceeded the population/unit cap. Computes current population by iterating all owned objects and summing their population values (GameObjectType+0x2120). Compares against the max cap (GetMaxPopCap). Also counts units currently in production queues if include_in_queue=true.", + "algorithm": { + "step1": "Get current population count from GetCurrentPopulation (FUN_1402AC700)", + "step2": "Get max population cap from object list manager", + "step3": "Get population value of the requested unit from GetPopValue (FUN_140373500)", + "step4": "Return: pop_value <= (max_cap - current_pop)" + } + }, + + "GetCurrentPopulation": { + "rva": "0x2AC700", + "signature": "int GetCurrentPopulation(void* object_list_mgr, int player_slot)", + "description": "Counts the total population value of all units owned by the specified player. Iterates the player's object list, summing GameObjectType+0x2120 for each object. Also includes ally population if in cooperative mode (FUN_14028AFB0 alliance check).", + "population_value_field": "GameObjectType+0x2120 (int32)", + "base_cap_field": "FactionData+0x2EE8 (int32) - base population cap from faction definition" + }, + + "GetPopValue": { + "rva": "0x373500", + "signature": "int GetPopValue(GameObjectType* type, int game_mode)", + "description": "Returns the population value for a unit type based on game mode. Mode 2 (space) reads +0x2128, mode 1 (land) reads +0x212C, default reads +0x2124.", + "fields": { + "pop_value_default": "GameObjectType+0x2124 (int32)", + "pop_value_space": "GameObjectType+0x2128 (int32) - used when game_mode=2", + "pop_value_land": "GameObjectType+0x212C (int32) - used when game_mode=1" + } + }, + + "GetMaxSpaceUnits": { + "rva": "0x372740", + "signature": "int GetMaxSpaceUnits(GameObjectType* type, GameObjectClass* facility)", + "description": "Returns the maximum number of space units. Checks facility+0xB8+0x230 first (planet-specific override). Falls back to type+0x92C (default from XML).", + "override_field": "FacilityParent+0xB8+0x230", + "default_field": "GameObjectType+0x92C" + }, + + "GetMaxLandUnits": { + "rva": "0x372760", + "signature": "int GetMaxLandUnits(GameObjectType* type, GameObjectClass* facility)", + "description": "Returns the maximum number of land units. Checks facility+0xB8+0x234 first. Falls back to type+0x928.", + "override_field": "FacilityParent+0xB8+0x234", + "default_field": "GameObjectType+0x928" + }, + + "GetMaxStarbaseLevel": { + "rva": "0x372780", + "signature": "int GetMaxStarbaseLevel(GameObjectType* type, GameObjectClass* facility)", + "description": "Returns the maximum starbase upgrade level. Checks facility+0xB8+0x238 first. Falls back to type+0x668.", + "override_field": "FacilityParent+0xB8+0x238", + "default_field": "GameObjectType+0x668" + }, + + "IsFreeBuilding": { + "rva": "0x289050", + "signature": "bool IsFreeBuilding(PlayerObject* player)", + "description": "Returns true if building is free (AI or sandbox mode). Checks player+0x62 (is_human). If not human, returns the value of global DAT_140b15b20 (free_build_mode flag).", + "is_human_field": "PlayerObject+0x62", + "free_build_global": "DAT_140b15b20" + }, + + "IsValidBuildLocation": { + "rva": "0x282400", + "signature": "bool IsValidBuildLocation(GameObjectType* type)", + "description": "Checks if a type can be built at the current location. Validates build slot counts (type+0x86C > 0) and build pad requirements (type+0xF0C or type+0xF10). Also checks property flags for build categories (0x5C, 0x5F)." + }, + + "CountExistingInstances": { + "rva": "0x281C70", + "signature": "int CountExistingInstances(PlayerObject* player, GameObjectType* type, bool include_allies)", + "description": "Counts how many instances of the given type the player (and optionally allies) currently own. Used for build limit enforcement (type+0x888 per-player limit, type+0x880 global limit). Includes instances at player+0x198 (owned objects list) and recursively checks sub-types at type+0x958 and type+0x978." + }, + + "CountInProductionQueue": { + "rva": "0x281DF0", + "signature": "int CountInProductionQueue(PlayerObject* player, GameObjectType* type, bool include_allies)", + "description": "Counts how many of the given type are currently queued for production across all production facilities. Iterates the global object list and checks each facility's production queue. Also counts from player+0x2C8 (pending build list with count at +0x2D0)." + }, + + "CountQueuedByCategory": { + "rva": "0x281AC0", + "signature": "int CountQueuedByCategory(PlayerObject* player, GameObjectType* type, bool include_allies)", + "description": "Counts queued units matching a specific category. Uses player+0x3A0 for fast lookup if available, otherwise iterates the full object list via FUN_1402A9FF0." + }, + + "ProductionComplete": { + "rva": "0x42E890", + "signature": "void ProductionComplete(ObjectUnderConstructionClass* this, GameObjectClass* facility, GameObjectType* type)", + "description": "Handles completion of a production queue item. Resolves the spawn location, creates the actual unit via FUN_14029F810, transfers ownership, fires completion events, and cleans up the queue entry. Logs: '%s: Tactical construction of final %s is complete.' Reads construction position from facility+0x84 area.", + "key_operations": [ + "Resolve planet location via facility+0x150 -> +0x8 chain", + "Check galaxy map mode via DAT_140a16fd0", + "Create the actual game object via FUN_14029F810", + "Fire ProductionCompleteEvent (event type 0x22) via FUN_140220ED0", + "Transfer unit properties from template", + "Clean up ObjectUnderConstructionClass" + ] + }, + + "QueueInsert": { + "rva": "0x46FC0", + "signature": "void QueueInsert(DynamicVector* queue, ObjectUnderConstructionClass** entry)", + "description": "Inserts an ObjectUnderConstructionClass entry into the production queue DynamicVector." + } + }, + + "gameobjecttype_production_fields": { + "description": "Fields on GameObjectType (the type definition struct) that control production behavior. These are set from XML data definitions.", + "fields": [ + { + "offset": "0x86C", + "name": "build_cost", + "type": "int32", + "description": "Base credit cost to produce this unit. Read by GetBaseCost and the production validator." + }, + { + "offset": "0x87C", + "name": "build_limit_per_location", + "type": "int32", + "description": "Max instances of this type per build location (planet). -1 = unlimited." + }, + { + "offset": "0x880", + "name": "build_limit_global", + "type": "int32", + "description": "Max instances across all planets/players. 0 = cannot build. Positive = limit. Checked by CountExistingInstances + CountInProductionQueue." + }, + { + "offset": "0x888", + "name": "build_limit_per_player", + "type": "int32", + "description": "Max instances per player. 0 = cannot build. Positive = limit." + }, + { + "offset": "0x890", + "name": "build_time_base", + "type": "int32", + "description": "Base build time in engine units. Multiplied by global scalar (DAT_140b15920) to get seconds." + }, + { + "offset": "0x894", + "name": "tech_level_requirement", + "type": "int32", + "description": "Required tech level to build. If 0, no tech requirement. Compared against PlayerObject+0x88 (max_tech)." + }, + { + "offset": "0x89C", + "name": "min_tech_level", + "type": "int32", + "description": "Minimum tech level to appear in build menu. Compared against PlayerObject+0x84 (current tech level). Also used for tech-level-based build time scaling." + }, + { + "offset": "0x8A0", + "name": "station_level_requirement", + "type": "int32", + "description": "Required space station level. Compared against the current station level at facility parent." + }, + { + "offset": "0x928", + "name": "max_land_units_default", + "type": "int32", + "description": "Default max land unit count (fallback if no planet override)." + }, + { + "offset": "0x92C", + "name": "max_space_units_default", + "type": "int32", + "description": "Default max space unit count (fallback if no planet override)." + }, + { + "offset": "0xC1", + "name": "requires_special_build_check", + "type": "uint8", + "description": "If 1, triggers FUN_140282580 for a special prerequisite check." + }, + { + "offset": "0xC4", + "name": "is_multiqueue_type", + "type": "uint8", + "description": "If 1, this type supports concurrent multi-queue building (build time divided by active queues)." + }, + { + "offset": "0xF0C", + "name": "required_build_pads", + "type": "int32", + "description": "Number of build pads required on the planet. Compared against actual pad count." + }, + { + "offset": "0xF10", + "name": "required_build_slots", + "type": "int32", + "description": "Number of build slots required. Secondary slot check." + }, + { + "offset": "0xF20", + "name": "build_categories_ptr", + "type": "pointer", + "description": "Pointer to array of build category entries. Each entry is 0x10 bytes: [+0x00 ptr, +0x08 ref]. Used for category-based build slot checks." + }, + { + "offset": "0xF28", + "name": "build_categories_count", + "type": "int32", + "description": "Number of build category entries." + }, + { + "offset": "0xF50", + "name": "prerequisites_list", + "type": "DynamicVectorClass", + "description": "List of prerequisite type references that must exist before this can be built." + }, + { + "offset": "0xF60", + "name": "prerequisites_count", + "type": "int32", + "description": "Number of prerequisites. If 0, no prerequisite check." + }, + { + "offset": "0x2120", + "name": "pop_value", + "type": "int32", + "description": "Population cost (how many pop points this unit consumes). Added to current population when checking cap." + }, + { + "offset": "0x2124", + "name": "pop_value_default", + "type": "int32", + "description": "Default population value (used when game_mode is not space or land)." + }, + { + "offset": "0x2128", + "name": "pop_value_space", + "type": "int32", + "description": "Population value in space battles (game_mode=2)." + }, + { + "offset": "0x212C", + "name": "pop_value_land", + "type": "int32", + "description": "Population value in land battles (game_mode=1)." + } + ] + }, + + "player_object_production_fields": { + "description": "Fields on PlayerObject relevant to production.", + "fields": [ + { + "offset": "0x4C", + "name": "player_slot_id", + "type": "int32", + "description": "Player's slot index. Used throughout production for ownership." + }, + { + "offset": "0x62", + "name": "is_human_player", + "type": "uint8", + "description": "1 = human player (applies credit checks), 0 = AI (may skip credit checks)." + }, + { + "offset": "0x70", + "name": "credits", + "type": "float32", + "description": "Current credits. Deducted up-front when production starts." + }, + { + "offset": "0x74", + "name": "max_credits", + "type": "float32", + "description": "Maximum credit cap. Negative = unlimited." + }, + { + "offset": "0x84", + "name": "tech_level", + "type": "int32", + "description": "Current tech level. Must meet type+0x89C to build." + }, + { + "offset": "0x88", + "name": "max_tech_level", + "type": "int32", + "description": "Maximum tech level. Must meet type+0x894 to build." + }, + { + "offset": "0x198", + "name": "owned_objects_list_ptr", + "type": "pointer", + "description": "Pointer to the list of owned object-type/count pairs. Used by CountExistingInstances." + }, + { + "offset": "0x1A0", + "name": "owned_objects_list_count", + "type": "int32", + "description": "Count of entries in the owned objects list." + }, + { + "offset": "0x2C8", + "name": "pending_build_list_ptr", + "type": "pointer", + "description": "Pointer to list of pending build types (already queued)." + }, + { + "offset": "0x2D0", + "name": "pending_build_list_count", + "type": "int32", + "description": "Count of pending build entries." + }, + { + "offset": "0x360", + "name": "income_modifier_ptr", + "type": "pointer", + "description": "Pointer to income modifier object. If non-null, positive credit adds are multiplied by [this+0x20]." + }, + { + "offset": "0x370", + "name": "alliance_lookup_table_ptr", + "type": "pointer", + "description": "Pointer to int array indexed by player_slot. Value 0 = allied, non-zero = not allied. Used for shared unit cap counting." + }, + { + "offset": "0x3A0", + "name": "queued_production_tracker_ptr", + "type": "pointer", + "description": "Pointer to the production queue tracker. Used by CountQueuedByCategory for fast lookup." + } + ] + }, + + "production_queue_layout": { + "description": "The production queue is stored inside ProductionDataPackClass, which is accessed via the production facility's behavior component (QueryInterface(6)). Each facility has up to 2 queues.", + "queue_structure": { + "queue_0": { + "offset_from_datapack": "0x08", + "name": "galactic_unit_queue", + "type": "DynamicVectorClass", + "queue_type_code": "0x20", + "description": "Queue for galactic-mode units (ships, vehicles, infantry)" + }, + "queue_1": { + "offset_from_datapack": "0x18", + "name": "galactic_structure_queue", + "type": "DynamicVectorClass", + "queue_type_code": "0x08", + "description": "Queue for galactic-mode structures and upgrades" + } + }, + "queue_selection_logic": "If type+0xF0C != 0 OR HasProperty(type, 0x5F, -1): use queue_1 (structures, offset 0x18, type_code 0x08). Otherwise: use queue_0 (units, offset 0x30, type_code 0x20). Queue is selected in the ObjectUnderConstructionClass constructor at 0x3FFF70.", + "max_queue_depth": "Stored as global at DAT_140b26f78 (int). Checked when player is_human (player+0x62 != 0). AI players bypass queue depth limit." + }, + + "build_pipeline_pseudocode": { + "description": "Complete pseudocode for the production pipeline from UI click to unit spawn.", + "pseudocode": [ + "// === STAGE 1: UI/Lua triggers ProductionEventClass (event 7) ===", + "event = new ProductionEventClass(); // RVA 0x523F50", + "event.event_type = 7;", + "EventQueueClass::Enqueue(event); // network-synchronized", + "", + "// === STAGE 2: Event processed -> ObjectUnderConstructionClass creation ===", + "// FUN_1403FFF70 (ObjectUnderConstructionClass constructor/factory)", + "function CreateProductionEntry(player, facility, type_to_build, queue_flags):", + " // Validate all prerequisites", + " if (!CanProduce(player, type_to_build, facility, true)):", + " return false;", + "", + " // Get the production behavior from the facility", + " prod_datapack = facility->parent_object->offset_0xD0;", + "", + " // Select queue based on type category", + " if (type.required_build_pads > 0 || HasProperty(type, 0x5F)):", + " queue_offset = 0x18; // structure queue", + " queue_type = 0x08;", + " else:", + " queue_offset = 0x30; // unit queue", + " queue_type = 0x20;", + "", + " // Check queue depth for human players", + " if (player.is_human && prod_datapack[queue_offset].count >= MAX_QUEUE_DEPTH):", + " return false;", + "", + " // Check unit cap", + " if (!CheckPopCap(object_list, type_to_build, player.slot, true)):", + " return false;", + "", + " // Compute adjusted cost", + " cost = GetAdjustedCost(prod_behavior, type_to_build, facility);", + "", + " // Compute build time", + " build_time = ComputeBuildTime(prod_behavior, type_to_build, facility);", + "", + " // Create the queue entry (0x38 bytes)", + " entry = allocate(0x38);", + " entry.vtable = ObjectUnderConstructionClass::vftable;", + " entry.producing_object_type = type_to_build;", + " entry.build_countdown = build_time;", + " entry.player_slot_id = player.slot;", + " entry.producing_object_id = facility.object_id;", + " entry.build_cost = cost;", + "", + " // Deduct credits (if not free building)", + " if (!IsFreeBuilding(player)):", + " AddCredits(player, -cost, false); // XOR float sign bit via DAT_140800860", + "", + " // Insert into queue", + " QueueInsert(prod_datapack + queue_offset, &entry);", + "", + " // Fire network event for sync", + " FireEvent(facility + 0x38, EVENT_PRODUCTION_STARTED=2, entry);", + "", + " // Log", + " printf(\"Object '%s' added to production queue at '%s' by player %d: countdown is %.2f seconds\",", + " type_to_build.type_name, facility.type_name, player.slot, build_time);", + "", + " return true;", + "", + "// === STAGE 3: Build timer countdown (per frame) ===", + "// ScheduledEventClass timer system decrements entry.build_countdown each frame", + "// When countdown reaches 0, triggers completion", + "", + "// === STAGE 4: Production complete ===", + "// FUN_14042E890 (ObjectUnderConstructionClass::~ObjectUnderConstructionClass)", + "function OnProductionComplete(entry, facility, type):", + " planet = facility.parent->offset_0x150->offset_0x8;", + " spawn_location = ResolveSpawnLocation(facility);", + "", + " // Spawn the actual game object", + " new_object = SpawnObject(player, planet, type, spawn_location);", + "", + " // Transfer properties", + " TransferProperties(new_object, facility);", + "", + " // Fire completion event (0x22)", + " FireEvent(new_object + 0x38, EVENT_PRODUCTION_COMPLETE=0x22, new_object);", + "", + " // Log", + " printf(\"%s: Tactical construction of final '%s' is complete.\", facility_name, type_name);", + "", + " // Clean up queue entry", + " DeallocateEntry(entry);" + ] + }, + + "globals": { + "MAX_QUEUE_DEPTH": { + "rva": "0xB26F78", + "type": "int32", + "description": "Maximum number of items in a single production queue (for human players). AI bypasses this." + }, + "BUILD_TIME_SCALAR": { + "rva": "0xB15920", + "type": "float32", + "description": "Global multiplier applied to base build time (type+0x890) to convert engine units to seconds." + }, + "FREE_BUILD_MODE": { + "rva": "0xB15B20", + "type": "uint8", + "description": "Global flag: if set, AI players skip credit checks." + }, + "TECH_DIFF_SCALARS": { + "rva_base": "0xB16DD4", + "type": "float32[5]", + "description": "5 floats at 0xB16DD4..0xB16DE4. Build time multiplier based on tech level difference (station_level - required_tech). Index 0 = diff of 1, etc." + }, + "PLAYER_LIST_GLOBAL": { + "rva": "0xA16FD0", + "type": "pointer", + "description": "Global pointer to PlayerListClass. Used by all production functions to resolve player objects." + }, + "GAME_STATE_GLOBAL": { + "rva": "0xB15418", + "type": "pointer", + "description": "Global pointer to game state. Accessed for object list management and event dispatch." + } + }, + + "event_types": { + "description": "Network event type IDs relevant to production.", + "events": [ + {"id": 7, "name": "ProductionEventClass", "description": "Player requests production of a unit/building"}, + {"id": 33, "name": "TacticalBuildEventClass", "description": "Player requests tactical-mode construction"}, + {"id": 46, "name": "GalacticSellEventClass", "description": "Player sells a unit/building on galactic map"}, + {"id": 8, "name": "FleetManagementEventClass", "description": "Fleet management (related to production output)"}, + {"id": 12, "name": "ReinforceEventClass", "description": "Reinforcement spawn from reinforcement pool"}, + {"id": 42, "name": "DistributeMoneyEventClass", "description": "Credit distribution (trade routes, income)"}, + {"id": 9, "name": "InvadeEventClass", "description": "Planet invasion (triggers production facility capture)"} + ] + }, + + "modding_recipes": { + "instant_build": { + "description": "Set build countdown to 0 immediately after queue insertion.", + "method": "Write 0.0f to ObjectUnderConstructionClass+0x10 (build_countdown) right after it is set. OR patch the ComputeBuildTime function (RVA 0x400370) to always return 0.0f.", + "aob_note": "Locate the ObjectUnderConstructionClass+0x10 write in the constructor at 0x3FFF70." + }, + "free_build": { + "description": "Skip the credit deduction.", + "method": "Patch the AddCredits call in the production constructor (0x3FFF70 -> calls 0x27F370 with negative cost). NOP the call or patch the cost to 0. Alternatively, set DAT_140b15b20 (FREE_BUILD_MODE) to 1.", + "global_toggle_rva": "0xB15B20" + }, + "unlimited_pop_cap": { + "description": "Bypass population cap checks.", + "method": "Patch CheckPopCap (FUN_1402AC320) to always return true. OR set FactionData+0x2EE8 to a very high value." + }, + "unlock_all_tech": { + "description": "Skip tech level requirements.", + "method": "In CanProduce (0x2804D0), NOP the tech level comparison at the check: 'if player+0x84 != type+0x89C return 0' and 'if player+0x88 < type+0x894 return 0'." + } + }, + + "cross_references": { + "description": "Key cross-references between production system and other engine systems.", + "refs": [ + { + "from": "ProductionBehaviorClass", + "to": "BehaviorClass", + "relationship": "inherits", + "note": "All production facilities have this behavior component attached." + }, + { + "from": "ObjectUnderConstructionClass+0x08", + "to": "GameObjectType", + "relationship": "references", + "note": "Points to the type being produced. Type name at +0xF8." + }, + { + "from": "CanProduce", + "to": "PlayerObject+0x70 (credits)", + "relationship": "reads", + "note": "Credit check: (int)player.credits >= adjusted_cost" + }, + { + "from": "CanProduce", + "to": "PlayerObject+0x84 (tech_level)", + "relationship": "reads", + "note": "Tech level gate" + }, + { + "from": "CheckPopCap", + "to": "GameObjectType+0x2120 (pop_value)", + "relationship": "reads", + "note": "Accumulates population across all owned objects" + }, + { + "from": "ComputeBuildTime", + "to": "ReduceProductionTimeAbilityClass", + "relationship": "queries", + "note": "Scans active abilities for build time modifiers" + }, + { + "from": "GetAdjustedCost", + "to": "ReduceProductionPriceAbilityClass", + "relationship": "queries", + "note": "Scans active abilities for cost modifiers" + } + ] + } +} diff --git a/re-findings/save_editor_spec.md b/re-findings/save_editor_spec.md new file mode 100644 index 000000000..3a12e0522 --- /dev/null +++ b/re-findings/save_editor_spec.md @@ -0,0 +1,287 @@ +# SWFOC Save File Editor Specification + +## Status: FIRST PASS -- Binary Analysis Complete, No Sample File Validated + +This document describes what can be edited in Star Wars: Empire at War - Forces of Corruption +save files (.sav), based on reverse engineering of StarWarsG.exe (x86_64 Steam build) via +Ghidra. **No save editor exists in the modding community -- this is uncharted territory.** + +**Critical caveat:** No actual .sav sample file was available on disk during this analysis. +The chunk IDs and exact serialization order are derived from class structure and code flow, +not from hexdump validation. The first step before building an editor is obtaining and +hex-dumping an actual save file. + +--- + +## File Format Summary + +The save file uses the **Westwood/Petroglyph chunk format**, the same binary container used +for .alo, .ala, and other Alamo engine files. There is no file-level magic number or version +header -- the file begins directly with the first chunk. + +### Chunk Header (8 bytes) +``` +Offset Type Name +0x00 uint32 chunk_id -- identifies chunk type +0x04 uint32 chunk_size -- data size in bytes (bit 31 = has sub-chunks) +``` + +- If `chunk_size & 0x80000000`, the chunk contains nested sub-chunks, not raw data +- Actual data size = `chunk_size & 0x7FFFFFFF` +- Chunks nest to arbitrary depth (up to 256 levels) + +### Micro-Chunk Header (2 bytes) +``` +Offset Type Name +0x00 uint8 micro_chunk_id -- identifies field within parent chunk +0x01 uint8 micro_chunk_size -- data size (max 255 bytes) +``` + +Micro-chunks are leaf-level data containers inside a chunk. They hold individual fields +(an int, a float, a short string). They cannot nest. + +### String Encoding +- **In-file strings**: null-terminated ASCII/ANSI (strlen+1 bytes written including null) +- **Save names**: UTF-16LE wide strings (wchar_t, std::wstring with MSVC SSO) + +### Compression +zlib is statically linked (compress2 at RVA 0x7A1470, uncompress2 at RVA 0x7A1590). +Whether saves are zlib-wrapped or raw chunk trees is **TBD until a sample file is examined**. +If compressed, the signature bytes 0x78 0x9C or 0x78 0x01 will appear near the file start. + +### Checksum +No CRC or checksum calculation was found in the save path. The engine trusts the chunk +structure. This is good news for editing -- no integrity check to bypass. + +--- + +## Editable Fields + +### SAFE TO EDIT (high confidence) + +These are scalar values in micro-chunks. As long as the micro-chunk size byte is preserved +(the value fits in the same number of bytes), editing is safe. + +| Field | Type | Location | Notes | +|-------|------|----------|-------| +| Player credits | int32 | Player data chunk, micro-chunk | Per-player currency | +| Planet owner | int32 | Planet chunk, micro-chunk | Player index (0-based) | +| Tech level | int32 | Player data chunk | Per-player technology level | +| Unit HP | float32 | Per-unit persistent state | Current hitpoints | + +### MODERATELY SAFE (medium confidence) + +These require understanding the surrounding data but are still scalar edits. + +| Field | Type | Location | Notes | +|-------|------|----------|-------| +| Corruption level | int32/float32 | Planet chunk | Underworld corruption state | +| Ability cooldowns | float32 | AbilityCountdownDataPackClass | Timer values, set to 0 to reset | +| Superweapon timers | float32 | TacticalSuperWeaponDataPackClass | Cooldown remaining | +| Shield state | float32 | BaseShieldDataPackClass | Shield HP/recharge state | +| Visibility/fog | bitmask | PlanetaryDataPackClass | Per-player fog of war | + +### DANGEROUS -- DO NOT EDIT WITHOUT FULL PARSER + +| Field | Risk | Why | +|-------|------|-----| +| Save name (wstring) | HIGH | Changing length corrupts all parent chunk sizes | +| Object ID mappings | CRITICAL | SaveLoadClass::ObjectPointerPairClass -- pointer resolution table. Corrupting this breaks ALL object references on load | +| Adding/removing units | CRITICAL | Requires updating: PlanetaryDataPackClass vectors, ObjectPersistenceClass entries, player unit lists, linked list pointers | +| Faction names | HIGH | Referenced by multiple systems, length-prefixed in context | +| Chunk structure itself | CRITICAL | Moving, reordering, or resizing chunks without updating all parent sizes will corrupt the file | + +--- + +## Architecture for a Save Editor + +### Phase 1: Parser (read-only) + +Build a chunk tree parser first. This is straightforward: + +``` +function parse_chunk_tree(stream): + while stream.has_data(): + chunk_id = read_uint32(stream) + raw_size = read_uint32(stream) + has_children = (raw_size & 0x80000000) != 0 + data_size = raw_size & 0x7FFFFFFF + + if has_children: + children = parse_chunk_tree(stream.slice(data_size)) + yield Chunk(id=chunk_id, children=children) + else: + data = stream.read(data_size) + micro_chunks = parse_micro_chunks(data) + yield Chunk(id=chunk_id, data=data, micro_chunks=micro_chunks) + +function parse_micro_chunks(data): + offset = 0 + while offset < len(data): + mc_id = data[offset] + mc_size = data[offset + 1] + mc_data = data[offset + 2 : offset + 2 + mc_size] + yield MicroChunk(id=mc_id, data=mc_data) + offset += 2 + mc_size +``` + +### Phase 2: Chunk ID Catalog + +With a parsed tree, catalog chunk IDs by examining multiple save files: +1. Save at different campaign points +2. Diff the chunk trees to identify which chunks change +3. Map chunk IDs to game concepts (players, planets, units) +4. Build a lookup table: chunk_id -> human-readable name + +### Phase 3: Field Editor + +For scalar fields within micro-chunks: +1. Navigate to the target micro-chunk +2. Read the current value (interpret bytes as int32/float32/etc.) +3. Write the new value (same byte count) +4. No chunk size updates needed if the value size is unchanged + +For variable-length fields (strings): +1. Calculate the size delta +2. Update the micro-chunk size byte +3. Update ALL parent chunk sizes up to the root +4. Rewrite the file from the modified chunk tree + +### Phase 4: Structural Editor + +For adding/removing objects: +1. Full round-trip: parse entire file -> modify tree -> serialize entire file +2. Must update ObjectPointerPairClass mappings +3. Must update all DynamicVector counts and contents +4. This is the hardest part and requires understanding every chunk type + +--- + +## Key RVAs for Hooking/Debugging + +These addresses (relative to module base 0x140000000) are useful for runtime +debugging with x64dbg or Frida to capture actual save data. + +### Save Flow +| Function | RVA | Purpose | +|----------|-----|---------| +| SaveGameEventClass::Execute | 0x48FC00 | Entry point when save is triggered | +| Save dispatch function ptr | global at 0xB313D8 | Actual save execution | +| SaveGameEventClass::ctor | 0x48FA80 | Save event creation | + +### Chunk I/O +| Function | RVA | Purpose | +|----------|-----|---------| +| ChunkWriter::Open_Chunk | 0x21FE20 | Begin writing a chunk (logs chunk_id) | +| ChunkWriter::Close_Chunk | 0x21FEB0 | End chunk (patches size) | +| ChunkWriter::Open_Micro_Chunk | 0x21FFA0 | Begin micro-chunk (logs mc_id) | +| ChunkWriter::Close_Micro_Chunk | 0x220030 | End micro-chunk (patches size) | +| ChunkWriter::Write | 0x2200B0 | Write raw bytes | +| ChunkWriter::Write_CString | 0x220140 | Write null-terminated string | +| ChunkReader::Open_Chunk | 0x2204A0 | Read chunk header | +| ChunkReader::Close_Chunk | 0x220520 | Skip remaining data, pop level | + +### File I/O +| Function | RVA | Purpose | +|----------|-----|---------| +| FileClass::Open | 0x213600 | CreateFileA wrapper | +| FileClass::dtor | 0x2132F0 | CloseHandle | + +### Serialization Primitives +| Function | RVA | Purpose | +|----------|-----|---------| +| Write_Int | 0x2046F0 | Write integer to stream | +| Read_Int | 0x2043B0 | Read integer from stream | +| Write_String | 0x204FB0 | Write wstring to stream | +| Read_String | 0x204AD0 | Read wstring from stream | + +--- + +## Recommended Next Steps + +### 1. Obtain a Sample Save File +Play a galactic conquest game, save it, and locate the .sav file at: +`%APPDATA%\Petroglyph\Empire at War - Forces of Corruption\Save\` + +### 2. Hexdump and Validate Format +```bash +xxd -l 512 savefile.sav +``` +Look for: +- Does it start with a chunk header (8 bytes: id + size)? +- Or does it start with zlib magic (0x78 0x9C)? +- Are there recognizable strings (planet names, faction names)? + +### 3. Runtime Trace with Frida +Hook ChunkWriter::Open_Chunk to log all chunk_ids in write order: +```javascript +Interceptor.attach(base.add(0x21FE20), { + onEnter(args) { + console.log("Open_Chunk: id=" + args[1].toInt32().toString(16) + + " depth=" + Memory.readS32(args[0].add(0x10))); + } +}); +``` +This will produce the complete chunk ID catalog with nesting depth. + +### 4. Hook Open_Micro_Chunk Similarly +```javascript +Interceptor.attach(base.add(0x21FFA0), { + onEnter(args) { + console.log("Open_MicroChunk: id=" + args[1].toInt32().toString(16)); + } +}); +``` + +### 5. Build the Parser +With the chunk ID catalog from step 3, build a Python parser that reads +the chunk tree and displays it in human-readable form. This is the +foundation for all editing. + +--- + +## Known Persistent Data Structures + +These classes are serialized into the save file. Each has virtual serialize/deserialize +methods that emit micro-chunks. + +### Per-Planet (PlanetaryDataPackClass) +- Built structures (PersistentTacticalBuiltObjectStruct) +- Applied upgrades (PersistentUpgradeObjectStruct) +- Hyperspace lane connections (LineLinkStruct) +- Trade route state (TradeRouteLinkEntryClass) +- Ground unit garrison (DynamicVector of GameObjectClass*) +- Space unit garrison (DynamicVector of GameObjectClass*) +- Type references (DynamicVector of GameObjectTypeClass*) +- Fog of war (visibility modifiers per player) +- Planet name (SSO string) + +### Per-Object (ObjectPersistenceClass::tPersistentUnit) +- Object type reference +- DataPack state for each attached behavior/component +- Position, rotation, health state + +### Per-Player +- Credits, tech level, faction name +- AI state (AIDataPackClass) for computer players +- Diplomatic relations, team assignments +- Build queues, production state + +### Global +- Game mode (galactic conquest, campaign) +- Turn counter / game time +- Story flag state (campaign progress) +- Random seed state + +--- + +## Comparison with Other Alamo Engine Formats + +The .sav format shares the exact same chunk infrastructure as: +- **.alo** (3D model files) -- same ChunkReader/ChunkWriter +- **.ala** (animation files) -- same chunk headers +- **.meg** (archive files) -- different container but same engine + +Existing tools like Mike.nl's AloViewer or the Petroglyph modding tools parse +the chunk format for .alo files. Their chunk parsing code can be adapted for +.sav files -- only the chunk IDs and data interpretation differ. diff --git a/re-findings/save_format.json b/re-findings/save_format.json new file mode 100644 index 000000000..4ac24c691 --- /dev/null +++ b/re-findings/save_format.json @@ -0,0 +1,356 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "SWFOC Save File Format Specification", + "description": "Reverse-engineered save file format for Star Wars: Empire at War - Forces of Corruption (Alamo Engine, x86_64 Steam build). Derived from static analysis of StarWarsG.exe via Ghidra MCP.", + "_meta": { + "analysis_date": "2026-04-04", + "binary": "StarWarsG.exe (x86_64, MSVC)", + "base_address": "0x140000000", + "analyst": "Agent 3F (Claude)", + "confidence": "high for structure, medium for chunk IDs (no sample file available)", + "status": "FIRST_PASS - no .sav sample files found on disk to cross-validate" + }, + + "file_format": { + "extension": ".sav", + "default_directory": "%APPDATA%\\Petroglyph\\Empire at War - Forces of Corruption\\Save\\", + "encoding": "binary, little-endian", + "byte_order": "little-endian (x86)", + "overall_structure": "Hierarchical chunk tree (Westwood/Petroglyph chunk format). No top-level file magic or version header -- the file begins directly with the root chunk. zlib compression (compress2/uncompress2) is linked but usage context is TBD.", + "compression": { + "library": "zlib (statically linked)", + "functions": { + "compress2": "0x1407A1470", + "uncompress2": "0x1407A1590", + "deflateInit_": "0x1407A3080", + "deflateInit2_": "0x1407A2DF0", + "inflateInit_": "0x1407A54F0", + "compress_block": "0x1407A60A0" + }, + "notes": "zlib is present and may wrap the chunk payload or the entire file. Alamo engine .meg/.alo files use the same chunk format without compression. Save files may be uncompressed chunk trees or have a thin zlib wrapper. A sample file is needed to confirm." + }, + "checksum": { + "method": "none found", + "notes": "No CRC or checksum functions were identified in the save path. The engine does not appear to verify file integrity on load -- it relies on the chunk structure being well-formed." + } + }, + + "chunk_format": { + "description": "The Alamo engine uses a two-tier chunk system inherited from Westwood Studios W3D format. 'Chunks' are large containers that nest, 'micro-chunks' are leaf data units within a chunk.", + "chunk_header": { + "size": 8, + "layout": [ + {"offset": 0, "type": "uint32", "name": "chunk_id", "description": "Identifies the chunk type. Unique within context."}, + {"offset": 4, "type": "uint32", "name": "chunk_size", "description": "Size of the chunk data in bytes (excludes this 8-byte header). Bit 31 (0x80000000) is set if the chunk contains sub-chunks rather than raw data."} + ], + "sub_chunk_flag": "chunk_size & 0x80000000 != 0 means this chunk contains nested sub-chunks. data_size = chunk_size & 0x7FFFFFFF.", + "nesting": "Chunks nest to arbitrary depth. The ChunkWriter/ChunkReader support up to 256 nesting levels (0x800 byte stack / 8 bytes per level).", + "rvas": { + "ChunkWriterClass_ctor": "0x14021FCC0", + "ChunkWriterClass_dtor": "0x14021FDC0", + "ChunkWriterClass_Open_Chunk": "0x14021FE20", + "ChunkWriterClass_Close_Chunk": "0x14021FEB0", + "ChunkWriterClass_Write": "0x1402200B0", + "ChunkWriterClass_Write_CString": "0x140220140", + "ChunkReaderClass_ctor": "0x140220280", + "ChunkReaderClass_dtor": "0x140220370", + "ChunkReaderClass_Get_File_Size": "0x1402203F0", + "ChunkReaderClass_Tell": "0x140220460", + "ChunkReaderClass_Open_Chunk": "0x1402204A0", + "ChunkReaderClass_Close_Chunk": "0x140220520" + } + }, + "micro_chunk_header": { + "size": 2, + "layout": [ + {"offset": 0, "type": "uint8", "name": "micro_chunk_id", "description": "Identifies the micro-chunk type within its parent chunk."}, + {"offset": 1, "type": "uint8", "name": "micro_chunk_size", "description": "Size of the micro-chunk data in bytes (max 255). Excludes this 2-byte header."} + ], + "max_data_size": 255, + "notes": "Micro-chunks are leaf-level data containers. They cannot nest. The size byte is patched after all data is written (seek-back pattern).", + "rvas": { + "ChunkWriterClass_Open_Micro_Chunk": "0x14021FFA0", + "ChunkWriterClass_Close_Micro_Chunk": "0x140220030" + } + } + }, + + "string_encoding": { + "save_names": { + "type": "wchar_t (UTF-16LE)", + "notes": "Save game names are wide strings. The SaveGameStruct stores names as std::wstring (MSVC SSO, inline threshold 7 wchars / 14 bytes). The [AutoSave] sentinel is a 10-wchar wide string." + }, + "in_file_strings": { + "type": "null-terminated char (ASCII/ANSI)", + "notes": "ChunkWriterClass::Write_CString writes strlen+1 bytes (includes null terminator). Strings in save data (object type names, faction names, etc.) are single-byte null-terminated." + } + }, + + "io_layer": { + "FileClass": { + "description": "Core I/O wrapper around Win32 HANDLE (CreateFileA/CloseHandle). Both ChunkWriter and ChunkReader take a FileClass* (or RAMFileClass*) as their stream parameter.", + "handle_offset": "0x08 (HANDLE, initialized to INVALID_HANDLE_VALUE)", + "filename_offset": "0x18 (std::string, SSO threshold 15 chars)", + "open_mode_offset": "0x00 (int: -1=closed, 0=read, 1=write)", + "vtable_methods": { + "Read": "vtable[0x28] -- read(buf, size) -> bytes_read", + "Write": "vtable[0x30] -- write(buf, size) -> bytes_written", + "Seek": "vtable[0x38] -- seek(offset, whence) whence: 0=SET, 1=CUR, 2=END", + "Tell": "vtable[0x40] -- tell() -> position" + }, + "rvas": { + "FileClass_ctor_default": "0x140213010", + "FileClass_ctor_cstring": "0x1402130D0", + "FileClass_ctor_stdstring": "0x1402131E0", + "FileClass_dtor": "0x1402132F0", + "FileClass_Open": "0x140213600" + } + }, + "RAMFileClass": { + "description": "In-memory file implementation. Used for serialization buffers. Same vtable interface as FileClass.", + "rvas": { + "RAMFileClass_ctor1": "0x1402227E0", + "RAMFileClass_ctor2": "0x140222830", + "RAMFileClass_ctor3": "0x140222880", + "RAMFileClass_dtor": "0x1402228D0" + } + } + }, + + "save_load_system": { + "SaveLoadManagerClass": { + "description": "Singleton manager for save/load operations. Maintains a DynamicVector of SaveGameStruct entries representing available save slots.", + "SaveGameStruct": { + "stride": "0x70 (112 bytes per entry)", + "fields": [ + {"offset": "0x00", "type": "std::wstring", "name": "save_name", "description": "Wide string save name (SSO: ptr/inline at +0, length at +0x10, capacity at +0x18)"}, + {"offset": "0x10", "type": "uint64", "name": "name_length", "description": "wchar_t count of the save name"}, + {"offset": "0x18", "type": "uint64", "name": "name_capacity", "description": "Capacity for SSO string (inline threshold: 7 wchars)"} + ], + "sentinel": "[AutoSave] (10 wchars, L\"[AutoSave]\")" + }, + "rvas": { + "SaveGameStruct_Vector_ctor": "0x140056360", + "SaveGameStruct_Vector_dtor": "0x1400581C0" + } + }, + "SaveLoadClass": { + "description": "Core serialization class. Uses ObjectPointerPairClass to track object-to-ID mappings during save/load for pointer resolution.", + "ObjectPointerPairClass_Vector": { + "global_address": "0x140A13DB0", + "layout": { + "data_ptr": "offset 0x00", + "count": "offset 0x08", + "capacity_flags": "offset 0x0C (bit 31 = heap flag)" + } + }, + "rvas": { + "ObjectPointerPairClass_Vector_ctors": ["0x1407EC700", "0x1407EC770", "0x1407EC900", "0x1407ECA90"], + "ObjectPointerPairClass_Vector_dtor": "0x1401FF850" + } + }, + "SaveGameEventClass": { + "description": "Scheduled event that triggers a save. Event type ID: 36 (0x24). Created via EventFactoryClass.", + "size": "0x70 bytes", + "fields": [ + {"offset": "EventClass+0x00", "type": "int32", "name": "save_slot_index", "description": "Index into SaveGameStruct vector. 0xFFFFFFFF = new save."}, + {"offset": "EventClass+0x08", "type": "std::wstring", "name": "save_name", "description": "Display name of the save."} + ], + "event_type_id": 36, + "factory_singleton": "0x140B313E0", + "execute_handler_rva": "0x14048FC00", + "save_dispatch_fptr": "0x140B313D8 (function pointer to actual save execution)", + "serialize_rva": "0x14048FB90", + "deserialize_rva": "0x14048FB20", + "rvas": { + "ctor": "0x14048FA80", + "dtor": "0x14048FAD0", + "execute": "0x14048FC00" + } + }, + "ObjectPersistenceClass": { + "description": "Manages persistent object units across save/load. tPersistentUnit is the per-object save record.", + "tPersistentUnit": { + "notes": "Contains a DynamicVector and vtable. Each unit tracks an object's persistent state." + }, + "rvas": { + "tPersistentUnit_Vector_ctor": "0x1404F2D80", + "tPersistentUnit_Vector_iterate": "0x1404F2FF0", + "tPersistentUnit_Vector_dtor": "0x140046570" + } + } + }, + + "persistent_data_classes": { + "description": "These DataPack classes contain the per-object state that gets serialized. Each has serialize/deserialize virtual methods that write/read micro-chunks.", + "PlanetaryDataPackClass": { + "description": "Per-planet galactic map data. The largest and most important persistent structure for save editing.", + "dtor_rva": "0x1404B5FB0", + "sub_structures": { + "PersistentTacticalBuiltObjectStruct": { + "description": "Records of structures built on the planet surface in tactical mode.", + "vector_dtor_rva": "0x1404B5E60" + }, + "PersistentUpgradeObjectStruct": { + "description": "Records of upgrades applied to structures on this planet.", + "vector_dtor_rva": "0x1404B5ED0" + }, + "LineLinkStruct": { + "description": "Hyperspace lane connections between planets.", + "vector_dtor_rva": "0x1404B5DF0" + }, + "TradeRouteLinkEntryClass": { + "description": "Trade route connections and state.", + "vector_dtor_rva": "0x1404B5F40" + } + }, + "contains": [ + "DynamicVector", + "DynamicVector", + "DynamicVector", + "DynamicVector", + "DynamicVector (ground units)", + "DynamicVector (space units)", + "DynamicVector (type references)", + "DynamicVector*> (fog of war)", + "Linked list structures for unit tracking", + "SSO string for planet name" + ] + }, + "other_datapacks": { + "description": "Each of these DataPack classes has serialize/deserialize methods. They represent per-object component state in the save file.", + "list": [ + {"name": "AIDataPackClass", "notes": "AI player state (budgets, goals, perception)"}, + {"name": "AbilityCountdownDataPackClass", "notes": "Active ability cooldown timers"}, + {"name": "CombatantBehaviorClass", "notes": "Combat statistics and targeting"}, + {"name": "TacticalBuildObjectsDataPackClass", "notes": "Build queue and construction state"}, + {"name": "TacticalSuperWeaponDataPackClass", "notes": "Superweapon cooldown and targeting"}, + {"name": "LocomotorDataPackClass", "notes": "Unit movement state and pathfinding"}, + {"name": "ProjectileDataPackClass", "notes": "Active projectile state"}, + {"name": "GarrisonDataPackClass", "notes": "Garrisoned unit information"}, + {"name": "InfectionDataPackClass", "notes": "Corruption/infection spread state"}, + {"name": "DamageTrackingDataPackClass", "notes": "Damage history for combat bonuses"}, + {"name": "BaseShieldDataPackClass", "notes": "Shield generator state"}, + {"name": "DeathDataPackClass", "notes": "Death animation and cleanup state"}, + {"name": "GUIDataPackClass", "notes": "UI-related persistent state"}, + {"name": "SelectionDataPackClass", "notes": "Selection group persistence"}, + {"name": "HintDataPackClass", "notes": "Tutorial/hint completion state"}, + {"name": "TeamDataPackClass", "notes": "Team alliance and diplomacy state"} + ] + } + }, + + "serialization_primitives": { + "description": "Low-level read/write operations used by serialize/deserialize methods.", + "write_int": { + "rva": "0x1402046F0", + "signature": "Write_Int(stream, value, size_type)", + "size_type_7": "4 bytes (int32)" + }, + "read_int": { + "rva": "0x1402043B0", + "signature": "Read_Int(stream, size_type) -> value" + }, + "write_string": { + "rva": "0x140204FB0", + "signature": "Write_String(stream, std_wstring*)" + }, + "read_string": { + "rva": "0x140204AD0", + "signature": "Read_String(stream, std_wstring*) -> success" + } + }, + + "game_mode_classes": { + "GalacticModeClass": { + "description": "The galactic conquest game mode. Save/load is triggered from this context.", + "ctor_rva": "0x1404B1270", + "inherits": "GameModeClass", + "init_fields": { + "offset_0x338": "1 (default value)", + "offset_0x358": "0x0F", + "offset_0x368": "1", + "offset_0x36c": "0x3FFFFF (bitmask, likely faction/player visibility)" + } + }, + "GameModeClass": { + "ctor_rva": "0x14035A5E0", + "dtor_rva": "0x14035AD70" + } + }, + + "predicted_save_structure": { + "description": "Based on the class hierarchy and serialization system, the save file is predicted to have this top-level structure. Chunk IDs are UNKNOWN without a sample file -- these are placeholders based on common Alamo engine patterns.", + "tree": [ + { + "chunk": "ROOT (ID TBD)", + "contains_sub_chunks": true, + "children": [ + { + "chunk": "SAVE_HEADER (ID TBD)", + "data": ["save_name (wstring)", "save_timestamp", "mod_name", "version_info", "game_mode_id"] + }, + { + "chunk": "PLAYER_DATA (ID TBD)", + "contains_sub_chunks": true, + "repeats": "per player", + "children": [ + {"chunk": "PLAYER_INFO", "data": ["player_id", "faction_name (cstring)", "credits", "tech_level", "is_human", "team_id"]}, + {"chunk": "AI_STATE", "data": ["AIDataPackClass serialized state"]} + ] + }, + { + "chunk": "GALACTIC_MAP (ID TBD)", + "contains_sub_chunks": true, + "children": [ + { + "chunk": "PLANET (ID TBD)", + "repeats": "per planet", + "contains_sub_chunks": true, + "children": [ + {"chunk": "PLANET_INFO", "data": ["planet_name (cstring)", "owner_player_id", "corruption_level"]}, + {"chunk": "GROUND_UNITS", "data": ["PersistentTacticalBuiltObjectStruct array"]}, + {"chunk": "SPACE_UNITS", "data": ["unit type references, counts"]}, + {"chunk": "UPGRADES", "data": ["PersistentUpgradeObjectStruct array"]}, + {"chunk": "TRADE_ROUTES", "data": ["TradeRouteLinkEntryClass array"]}, + {"chunk": "VISIBILITY", "data": ["fog of war modifiers per player"]} + ] + } + ] + }, + { + "chunk": "OBJECT_PERSISTENCE (ID TBD)", + "contains_sub_chunks": true, + "data": ["ObjectPersistenceClass::tPersistentUnit array -- per-object save state"] + }, + { + "chunk": "STORY_STATE (ID TBD)", + "data": ["story flag completion, campaign progress"] + } + ] + } + ], + "confidence": "SPECULATIVE -- based on class hierarchy only. Actual chunk IDs and exact ordering require a sample .sav file hexdump." + }, + + "safely_editable_fields": { + "description": "Fields that can be edited with low risk if the chunk structure is preserved. All require maintaining correct chunk sizes.", + "high_confidence": [ + "credits (per player) -- int32, within a micro-chunk", + "planet_owner (per planet) -- int32, player index", + "tech_level (per player) -- int32" + ], + "medium_confidence": [ + "unit_hp (per unit) -- float32, within PersistentTacticalBuiltObjectStruct", + "corruption_level (per planet) -- likely int32 or float32", + "ability_cooldown_timers -- float32 values in AbilityCountdownDataPackClass", + "superweapon_cooldowns -- float32 values in TacticalSuperWeaponDataPackClass" + ], + "dangerous": [ + "save_name -- wstring, changing length will corrupt chunk sizes", + "object_id mappings -- SaveLoadClass::ObjectPointerPairClass references; corrupting these breaks all pointer resolution on load", + "Adding/removing units -- requires updating multiple linked structures (PlanetaryDataPackClass vectors, ObjectPersistenceClass, player unit lists)", + "faction_name changes -- referenced by multiple systems, length changes corrupt chunks" + ] + } +} diff --git a/re-findings/story_system.json b/re-findings/story_system.json new file mode 100644 index 000000000..66b10303e --- /dev/null +++ b/re-findings/story_system.json @@ -0,0 +1,473 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "SWFOC Alamo Engine — Story/Scripting System Reverse Engineering", + "description": "Complete documentation of the story flag system, reward dispatch, Fire_Story_Event mechanism, and event type hierarchy in SWFOC's Alamo engine. Derived from Ghidra decompilation of StarWarsG.exe (x86_64).", + "_meta": { + "analysis_date": "2026-04-04", + "module": "StarWarsG.exe", + "architecture": "x86_64", + "tools_used": ["Ghidra 12.0.3 via MCP", "RTTI class hierarchy recovery"], + "analyst": "Agent 3K (Story/Scripting RE)" + }, + + "story_architecture_overview": { + "status": "CONFIRMED", + "description": "The story system is built on a 3-layer hierarchy: StorySubPlotClass owns StoryEventClass instances, which inherit from SignalGeneratorClass. Events are stored in a CRC32-keyed hash table within each subplot. The system uses XML-driven story definitions parsed at load time, with events dispatched via a type-ID factory pattern.", + "class_hierarchy": { + "SignalGeneratorClass": "Base class providing signal/event notification infrastructure", + "StoryEventClass": "Base story event — inherits SignalGeneratorClass. Size >= 0x360 bytes. Contains 15+ MSVC SSO string fields for reward parameters, dialog text, etc.", + "StorySubPlotClass": "Container for events within a story plot. Inherits SignalGeneratorClass. Size >= 0x650 bytes. Owns a CRC32 hash table of events + 61 DynamicVectorClass slots (0x3D).", + "StoryEventWrapper": "Lua wrapper for StoryEventClass — exposed to script with 7 methods", + "StoryPlotWrapper": "Lua wrapper for StorySubPlotClass — exposed to script with 5 methods" + } + }, + + "story_flag_storage": { + "status": "CONFIRMED", + "mechanism": "CRC32 hash table within StorySubPlotClass", + "description": "Story flags/events are NOT stored as simple bool/int flags. Instead, each StorySubPlotClass contains a hash table at +0x20 (linked list head pointer) using CRC32(strupr(event_name)) XOR 0xDEADBEEF as the bucket key. Events are looked up case-insensitively by name. Each event node stores the CRC32 at +0x10 and a pointer to the StoryEventClass at +0x18.", + "hash_function": { + "rva": "0x215A30", + "name": "CRC32_Hash", + "description": "Standard CRC32 using a lookup table at DAT_140a14d20. Takes (byte* data, uint length, uint seed). Seed is inverted on entry and result.", + "status": "CONFIRMED" + }, + "event_lookup_function": { + "rva": "0x52EF90", + "name": "StorySubPlot_FindEventByName", + "description": "Looks up a StoryEventClass* by name string. Converts name to uppercase via _strupr, computes CRC32, XORs with 0xDEADBEEF, applies LCG (a=16807 c=0 m=2^31-1) for bucket index, walks linked list comparing CRC32 values.", + "bucket_formula": "bucket_index = LCG(CRC32(strupr(name)) ^ 0xDEADBEEF) & bucket_mask", + "lcg_constants": { + "a": 16807, + "m": 2147483647, + "note": "Lehmer/Park-Miller MINSTD generator used for hash distribution" + }, + "status": "CONFIRMED" + }, + "event_active_flag_offset": "0x4C in StoryEventClass (byte) — set to 0 when event is reset/inactive", + "subplot_hash_table_offset": "+0x20 (linked list root pointer within StorySubPlotClass)", + "subplot_bucket_array_offset": "+0x30 (bucket array pointer), +0x48 (bucket mask)" + }, + + "event_type_enum": { + "status": "CONFIRMED", + "description": "Event types are identified by integer IDs used in a switch/case factory at RVA 0x453310. The string-to-ID mapping is stored in a global std::map-like tree at DAT_140b30728, keyed by CRC32 of the uppercase type name string.", + "factory_function_rva": "0x453310", + "factory_function_name": "StoryEvent_Factory_Create", + "global_type_tree_address": "DAT_140b30728 (global pointer to std::map root node)", + "values": { + "0x01": {"class": "StoryEventEnterClass", "alloc_size": "0x3B0", "group": "enter/trigger", "notes": "Shared with cases 2,0x17,0x31,0x33"}, + "0x02": {"class": "StoryEventEnterClass", "alloc_size": "0x3B0", "group": "enter/trigger"}, + "0x03": {"class": "StoryEventSingleObjectNameClass", "alloc_size": "0x3B0", "group": "object-name", "notes": "Shared with cases 4,0x11,0x12,0x16,0x24,0x26,0x27,0x28,0x37"}, + "0x04": {"class": "StoryEventSingleObjectNameClass", "alloc_size": "0x3B0", "group": "object-name"}, + "0x05": {"class": "StoryEventConstructLevelClass", "alloc_size": "0x380", "group": "construct"}, + "0x06": {"class": "StoryEventDestroyClass", "alloc_size": "0x3C8", "group": "destroy", "notes": "Shared with case 8"}, + "0x07": {"class": "StoryEventDestroyBaseClass", "alloc_size": "0x388", "group": "destroy-base"}, + "0x08": {"class": "StoryEventDestroyClass", "alloc_size": "0x3C8", "group": "destroy"}, + "0x09": {"class": "StoryEventBeginEraClass", "alloc_size": "0x368", "group": "era", "notes": "Shared with case 0x0A"}, + "0x0A": {"class": "StoryEventBeginEraClass", "alloc_size": "0x368", "group": "era"}, + "0x0C": {"class": "StoryEventHeroMoveClass", "alloc_size": "0x390", "group": "hero-move", "notes": "Shared with case 0x0D"}, + "0x0D": {"class": "StoryEventHeroMoveClass", "alloc_size": "0x390", "group": "hero-move"}, + "0x0E": {"class": "StoryEventAccumulateClass", "alloc_size": "0x368", "group": "accumulate"}, + "0x0F": {"class": "StoryEventConquerCountClass", "alloc_size": "0x368", "group": "conquer-count"}, + "0x10": {"class": "StoryEventElapsedClass", "alloc_size": "0x370", "group": "elapsed-time"}, + "0x11": {"class": "StoryEventSingleObjectNameClass", "alloc_size": "0x3B0", "group": "object-name"}, + "0x12": {"class": "StoryEventSingleObjectNameClass", "alloc_size": "0x3B0", "group": "object-name"}, + "0x13": {"class": "StoryEventWinBattlesClass", "alloc_size": "0x3A0", "group": "win-battles", "notes": "Shared with case 0x14"}, + "0x14": {"class": "StoryEventWinBattlesClass", "alloc_size": "0x3A0", "group": "win-battles"}, + "0x15": {"class": "StoryEventRetreatClass", "alloc_size": "0x368", "group": "retreat"}, + "0x16": {"class": "StoryEventSingleObjectNameClass", "alloc_size": "0x3B0", "group": "object-name"}, + "0x17": {"class": "StoryEventEnterClass", "alloc_size": "0x3B0", "group": "enter/trigger"}, + "0x18": {"class": "StoryEventStartTacticalClass", "alloc_size": "0x398", "group": "tactical-start", "notes": "Shared with case 0x19"}, + "0x19": {"class": "StoryEventStartTacticalClass", "alloc_size": "0x398", "group": "tactical-start"}, + "0x1A": {"class": "StoryEventSelectPlanetClass", "alloc_size": "0x380", "group": "select-planet", "notes": "Shared with cases 0x1B, 0x1C"}, + "0x1B": {"class": "StoryEventSelectPlanetClass", "alloc_size": "0x380", "group": "select-planet"}, + "0x1C": {"class": "StoryEventSelectPlanetClass", "alloc_size": "0x380", "group": "select-planet"}, + "0x1D": {"class": "StoryEventStringClass", "alloc_size": "0x378", "group": "string-event", "notes": "Shared with cases 0x1E,0x21,0x32,0x38,0x39,0x3C"}, + "0x1E": {"class": "StoryEventStringClass", "alloc_size": "0x378", "group": "string-event", "notes": "Default elapsed timeout is 60.0 if value == -1.0"}, + "0x1F": {"class": "StoryEventFogRevealClass", "alloc_size": "0x390", "group": "fog-reveal", "notes": "Shared with case 0x20"}, + "0x20": {"class": "StoryEventFogRevealClass", "alloc_size": "0x390", "group": "fog-reveal"}, + "0x21": {"class": "StoryEventStringClass", "alloc_size": "0x378", "group": "string-event"}, + "0x22": {"class": "StoryEventClass (base)", "alloc_size": "0x360", "group": "generic", "notes": "Uses base StoryEventClass directly — no subclass"}, + "0x23": {"class": "StoryEventAINotificationClass", "alloc_size": "0x390", "group": "ai-notification"}, + "0x24": {"class": "StoryEventSingleObjectNameClass", "alloc_size": "0x3B0", "group": "object-name"}, + "0x25": {"class": "StoryEventCommandUnitClass", "alloc_size": "0x398", "group": "command-unit"}, + "0x26": {"class": "StoryEventSingleObjectNameClass", "alloc_size": "0x3B0", "group": "object-name"}, + "0x27": {"class": "StoryEventSingleObjectNameClass", "alloc_size": "0x3B0", "group": "object-name"}, + "0x28": {"class": "StoryEventSingleObjectNameClass", "alloc_size": "0x3B0", "group": "object-name"}, + "0x29": {"class": "StoryEventGuardUnitClass", "alloc_size": "0x390", "group": "guard-unit"}, + "0x2A": {"class": "StoryEventProximityClass", "alloc_size": "0x398", "group": "proximity"}, + "0x2B": {"class": "StoryEventDifficultyClass", "alloc_size": "0x368", "group": "difficulty"}, + "0x2C": {"class": "StoryEventFlagClass", "alloc_size": "0x380", "group": "flag-check", "notes": "This is the Check_Story_Flag / Set_Story_Flag event type"}, + "0x2D": {"class": "StoryEventLoadTacticalClass", "alloc_size": "0x398", "group": "load-tactical"}, + "0x2E": {"class": "StoryCheckDestroyedClass", "alloc_size": "0x378", "group": "check-destroyed"}, + "0x2F": {"class": "StoryEventVictoryClass", "alloc_size": "0x368", "group": "victory"}, + "0x30": {"class": "StoryEventMovieDoneClass", "alloc_size": "0x360", "group": "movie-done"}, + "0x31": {"class": "StoryEventEnterClass", "alloc_size": "0x3B0", "group": "enter/trigger"}, + "0x32": {"class": "StoryEventStringClass", "alloc_size": "0x378", "group": "string-event"}, + "0x33": {"class": "StoryEventEnterClass", "alloc_size": "0x3B0", "group": "enter/trigger"}, + "0x34": {"class": "StoryEventObjectiveTimeoutClass", "alloc_size": "0x390", "group": "objective-timeout"}, + "0x35": {"class": "StoryEventCaptureClass", "alloc_size": "0x380", "group": "capture"}, + "0x36": {"class": "StoryEventCorruptionLevelClass", "alloc_size": "0x380", "group": "corruption-level", "notes": "Shared with cases 0x3A, 0x3B"}, + "0x37": {"class": "StoryEventSingleObjectNameClass", "alloc_size": "0x3B0", "group": "object-name"}, + "0x38": {"class": "StoryEventStringClass", "alloc_size": "0x378", "group": "string-event"}, + "0x39": {"class": "StoryEventStringClass", "alloc_size": "0x378", "group": "string-event"}, + "0x3A": {"class": "StoryEventCorruptionLevelClass", "alloc_size": "0x380", "group": "corruption-level"}, + "0x3B": {"class": "StoryEventCorruptionLevelClass", "alloc_size": "0x380", "group": "corruption-level"}, + "0x3C": {"class": "StoryEventStringClass", "alloc_size": "0x378", "group": "string-event"} + }, + "total_event_types": 61, + "unique_concrete_classes": 28 + }, + + "reward_dispatch_system": { + "status": "CONFIRMED", + "description": "Rewards are NOT dispatched via a big switch/case. Instead, each StoryEventClass stores a reward_type as an integer at offset +0x3C (relative to event data start, i.e., after the base class header). The reward type is resolved from a string name via a global std::map tree at DAT_140b30728 using CRC32 lookup. The same tree is used for both event types and reward types.", + "reward_type_resolver_rva": "0x45C3F0", + "reward_type_resolver_name": "ResolveRewardTypeFromString", + "reward_type_resolver_description": "Takes a null-terminated string, computes CRC32, searches the global tree (red-black tree rooted at DAT_140b30728) for a matching node. Returns the integer value from node+0x20 (the enum value). Returns 0 if not found.", + "global_type_tree_rva": "0xB30728", + "reward_type_offset_in_event": "+0x3C from StoryEventClass data start", + "set_reward_type_lua_rva": "0x73EEF0", + "set_reward_type_lua_name": "StoryEventWrapper::Set_Reward_Type", + "set_reward_parameter_lua_rva": "0x73ECA0", + "set_reward_parameter_lua_name": "StoryEventWrapper::Set_Reward_Parameter", + "reward_parameter_storage": "StoryEventClass data offsets +0x50 through +0x190 — array of MSVC SSO strings at 0x20 stride, indexed by parameter number. Up to 14 reward parameters.", + "known_reward_type_strings": [ + "Note: The actual string->int mappings are populated at runtime from XML data definitions.", + "Common reward type names from modding community documentation include:", + "CYCLOPEAN_REVEAL_ALL, GIVE_MONEY, REMOVE_UNIT, SPAWN_HERO, SWITCH_SIDES,", + "TRIGGER_EVENT, SET_FLAG, DISABLE_UNIT, ENABLE_UNIT, LOCK_PLANET,", + "FLASH_PLANET, SET_TECH_LEVEL, DUAL_FLASH, DISABLE_BRANCH, ENABLE_BRANCH,", + "VICTORY, DEFEAT, SPEECH, TUTORIAL, STORY_TRIGGER, FORCE_RETREAT, etc.", + "These are NOT hardcoded in the binary — they are CRC32-hashed from XML at load time." + ] + }, + + "fire_story_event_mechanism": { + "status": "CONFIRMED", + "description": "Story events are NOT fired via a single 'Fire_Story_Event' function. Instead, the LuaScriptClass::GetEvent global function (type_id=0x3D=61) is registered as the 'GetEvent' Lua global, which wraps StoryPlotWrapper::Get_Event. The actual Lua Story_Event() and Story_Event_Trigger() are registered elsewhere in the game-mode-specific Lua binding setup (not in LuaScriptClass base). Events fire through the SignalGeneratorClass signal system.", + "lua_global_registration": { + "function_name": "GetEvent", + "registered_at_rva": "0x2567B0", + "description": "LuaScriptClass::RegisterGlobals registers 'GetEvent' as a global Lua function that wraps the story event lookup. The GetEvent object at +0xD8 in LuaScriptClass holds the 0x3D event type count.", + "status": "CONFIRMED" + }, + "story_event_wrapper_methods": { + "status": "CONFIRMED", + "wrapper_constructor_rva": "0x73DC80", + "methods_registered": [ + { + "name": "method_1 (inherited from base, at 0x40258770)", + "description": "Common base method — likely Get/Set name or similar" + }, + { + "name": "Add_Dialog_Text", + "rva": "0x73DF60", + "description": "Sets dialog text on the event. Accepts variable args (string, GameObjectWrapper, number). Writes to DynamicVector at event+0x2C0.", + "status": "CONFIRMED" + }, + { + "name": "Clear_Dialog_Text", + "rva": "0x73E4F0", + "description": "Clears the dialog text vector at event+0x2C0. Iterates and deallocates string entries at 0x20 stride.", + "status": "CONFIRMED" + }, + { + "name": "Set_Dialog", + "rva": "0x73E9A0", + "description": "Sets dialog identifier string. Writes to event data offset +0x230. Also calls FUN_14047d9c0 for dialog pre-caching.", + "status": "CONFIRMED" + }, + { + "name": "Set_Reward_Parameter", + "rva": "0x73ECA0", + "description": "Sets a reward parameter by index. Takes (number index, string/number value). Writes to event+0x50 + index*0x20. Validates exactly 1 parameter per index. Uses global static buffer at DAT_140b3b450.", + "status": "CONFIRMED" + }, + { + "name": "Set_Reward_Type", + "rva": "0x73EEF0", + "description": "Sets the reward type from a string name. Calls ResolveRewardTypeFromString (0x45C3F0) to convert the string to an enum int. Stores result at event+0x3C.", + "status": "CONFIRMED" + } + ] + }, + "story_plot_wrapper_methods": { + "status": "CONFIRMED", + "wrapper_constructor_rva": "0x724120", + "methods_registered": [ + { + "name": "method_1 (inherited, at 0x40258770)", + "description": "Common base method" + }, + { + "name": "Get_Event", + "rva": "0x724480", + "description": "Looks up an event by name string within this plot's event hash table. Calls StorySubPlot_FindEventByName (0x52EF90). Returns a StoryEventWrapper if found. Logs error if event not found.", + "status": "CONFIRMED" + }, + { + "name": "method_3 (at 0x7243a0, not resolved)", + "description": "Registered but function address did not resolve to a named function" + }, + { + "name": "method_4 (at 0x7247a0, not resolved)", + "description": "Registered but function address did not resolve to a named function" + }, + { + "name": "Reset", + "rva": "0x7246E0", + "description": "Resets all events in the subplot. Calls StorySubPlotClass::Reset (0x52FC10) which iterates the linked event list, sets event+0x4C to 0, and calls virtual method [0x20] twice on each event (re-initialization).", + "status": "CONFIRMED" + } + ] + } + }, + + "story_subplot_class": { + "status": "CONFIRMED", + "rtti_mangled": ".?AVStorySubPlotClass@@", + "constructor_rva": "0x52D400", + "destructor_rva": "0x52E220", + "inherits": ["SignalGeneratorClass"], + "size_minimum": "0x650", + "key_fields": { + "+0x08": "boolean: active flag + padding", + "+0x10": "linked list node allocation pointer", + "+0x14": "linked list count/capacity", + "+0x20": "Hash table linked list root (for event lookup by CRC32 name)", + "+0x30": "Hash table bucket array pointer", + "+0x48": "Hash table bucket mask (AND'd with LCG output)", + "+0x58..+0x5F8": "Array of 61 (0x3D) DynamicVectorClass slots (each 0x18 bytes = vtable + ptr + count/capacity)", + "+0x5F8": "Story mode pointer (unknown sub-object)", + "+0x600": "DynamicVectorClass vtable (embedded)", + "+0x608..+0x614": "DynamicVectorClass storage for final event list", + "+0x628": "Plot name string (MSVC SSO, size at +0x638, capacity at +0x640)", + "+0x644": "Boolean: is_active_story flag" + }, + "event_loading_process": { + "description": "StorySubPlotClass constructor loads events from XML. For each event: (1) read event name, (2) strupr + CRC32, (3) XOR 0xDEADBEEF + LCG for bucket, (4) check for CRC32 collision ('Event already exists!'), (5) parse event XML block via FUN_14045c5d0, (6) create concrete StoryEventClass via factory FUN_1404562a0, (7) insert into hash table, (8) set back-pointer event->subplot at +0x228, (9) assign sequential event index at +0x358. After all events loaded, calls FUN_140530180 to build the 61-slot type-indexed arrays and FUN_140452d70 to compute event dependencies.", + "status": "CONFIRMED" + } + }, + + "story_event_class": { + "status": "CONFIRMED", + "rtti_mangled": ".?AVStoryEventClass@@", + "constructor_rva": "0x4501D0", + "destructor_rva": "0x450600", + "inherits": ["SignalGeneratorClass"], + "size_minimum": "0x360", + "key_fields": { + "+0x00..+0x1F": "MSVC SSO string: event name (length at +0x10, capacity at +0x18, inline if cap < 16)", + "+0x20": "Event type ID (int32) — assigned from factory param_1", + "+0x38": "Event type variant (int32) — subclass-specific", + "+0x3C": "Reward type ID (int32) — set by Set_Reward_Type, resolved via ResolveRewardTypeFromString", + "+0x44": "Unknown int32 — copied from XML parse", + "+0x48": "Unknown int32 — copied from XML parse", + "+0x4C": "Active/triggered flag (byte) — 0 = inactive, set by Reset", + "+0x4D": "Boolean — related to reward parameter presence", + "+0x4E": "Unknown byte — from XML", + "+0x50..+0x190": "Reward parameter strings: array of MSVC SSO strings at 0x20 stride, up to 14 slots", + "+0x1F8..+0x228": "Additional string fields (15 SSO strings total in base class)", + "+0x228": "Back-pointer to owning StorySubPlotClass", + "+0x230": "Dialog ID string (MSVC SSO)", + "+0x250..+0x298": "Additional dialog/reward strings", + "+0x2A8..+0x2C0": "Dialog data array (unknown structure)", + "+0x2C0": "DynamicVector for dialog text entries", + "+0x2F8": "DynamicVector> vtable pointer (nested reward param lists)", + "+0x310": "DynamicVector> vtable pointer (event dependency lists)", + "+0x328": "DynamicVector vtable pointer (direct event refs)", + "+0x340..+0x358": "Dependency tracking: DynamicVectorClass (dependent event pointers, count at +0x350, capacity at +0x354)", + "+0x358": "Event index within subplot (int32)" + }, + "subclasses": [ + "StoryEventEnterClass", + "StoryEventSingleObjectNameClass", + "StoryEventConstructLevelClass", + "StoryEventDestroyClass", + "StoryEventDestroyBaseClass", + "StoryEventBeginEraClass", + "StoryEventHeroMoveClass", + "StoryEventAccumulateClass", + "StoryEventConquerCountClass", + "StoryEventElapsedClass", + "StoryEventWinBattlesClass", + "StoryEventRetreatClass", + "StoryEventStartTacticalClass", + "StoryEventSelectPlanetClass", + "StoryEventStringClass", + "StoryEventFogRevealClass", + "StoryEventAINotificationClass", + "StoryEventCommandUnitClass", + "StoryEventGuardUnitClass", + "StoryEventProximityClass", + "StoryEventDifficultyClass", + "StoryEventFlagClass", + "StoryEventLoadTacticalClass", + "StoryCheckDestroyedClass", + "StoryEventVictoryClass", + "StoryEventMovieDoneClass", + "StoryEventObjectiveTimeoutClass", + "StoryEventCaptureClass", + "StoryEventCorruptionLevelClass" + ] + }, + + "all_story_related_rvas": { + "status": "CONFIRMED", + "functions": { + "StoryEventClass::constructor": "0x4501D0", + "StoryEventClass::destructor": "0x450600", + "StoryEventCommandUnitClass::constructor": "0x4504E0", + "StoryEventHeroMoveClass::constructor": "0x450540", + "StoryEventSelectPlanetClass::constructor": "0x5DC4B0", + "StorySubPlotClass::destructor": "0x52E220", + "StorySubPlotClass::constructor_main": "0x52D400", + "StorySubPlot_FindEventByName": "0x52EF90", + "StorySubPlot_Reset": "0x52FC10", + "StorySubPlot_BuildTypeArrays": "0x530180", + "StorySubPlot_ComputeDependencies": "0x452D70", + "StoryEvent_Factory_Create": "0x453310", + "StoryEvent_ParseXMLBlock": "0x45C5D0", + "StoryEvent_BuildFromParsed": "0x4562A0", + "ResolveRewardTypeFromString": "0x45C3F0", + "CRC32_Hash": "0x215A30", + "StoryEventWrapper::LuaConstructor": "0x73DC80", + "StoryEventWrapper::Add_Dialog_Text": "0x73DF60", + "StoryEventWrapper::Clear_Dialog_Text": "0x73E4F0", + "StoryEventWrapper::Set_Dialog": "0x73E9A0", + "StoryEventWrapper::Set_Reward_Parameter": "0x73ECA0", + "StoryEventWrapper::Set_Reward_Type": "0x73EEF0", + "StoryPlotWrapper::LuaConstructor": "0x724120", + "StoryPlotWrapper::Get_Event": "0x724480", + "StoryPlotWrapper::Reset": "0x7246E0", + "LuaScriptClass::RegisterGlobals": "0x2567B0", + "LuaScriptClass::LuaConstructor": "0x242570", + "StoryLogWindow::constructor": "0x01AB40", + "DynamicVectorClass::constructor_1": "0x450590", + "DynamicVectorClass::constructor_2": "0x45E670", + "DynamicVectorClass::destructor": "0x47D6B0", + "LuaMemberFunctionWrapper::constructor": "0x73DC80", + "LuaMemberFunctionWrapper::constructor": "0x724120", + "FUN_14073E590_WrapEventAsLuaObject": "0x73E590", + "StoryEvent_DebugLog": "0x467540" + }, + "global_data": { + "DAT_140b30728_EventTypeTree": "0xB30728 — Global std::map root for event/reward type string-to-int lookup", + "DAT_140b30738_XMLNestingDepth": "0xB30738 — XML nesting depth counter for recursive parsing", + "DAT_140b3b450_RewardParamBuffer": "0xB3B450 — Static buffer for Set_Reward_Parameter temporary storage", + "DAT_140b3b458_RewardParamEnd": "0xB3B458 — End pointer for reward param buffer", + "DAT_140b3b468_DialogGuard": "0xB3B468 — Thread-safe init guard for dialog system", + "DAT_140a14d20_CRC32Table": "0xA14D20 — CRC32 lookup table (256 uint32 entries)", + "DAT_140a15738_ScriptIDCounter": "0xA15738 — Global incrementing script instance ID counter", + "DAT_140a157d0_StringTypeMarker": "0xA157D0 — Type discriminator marker for Lua string arguments", + "DAT_140a157c0_NumberTypeMarker": "0xA157C0 — Type discriminator marker for Lua number arguments", + "DAT_140a43b18_GameObjectTypeMarker": "0xA43B18 — Type discriminator marker for GameObjectWrapper arguments", + "DAT_140a44270_PlayerTypeMarker": "0xA44270 — Type discriminator marker for PlayerWrapper arguments" + }, + "vtables": { + "StorySubPlotClass::vftable": "referenced in constructor at 0x52D400", + "StoryEventClass::vftable": "referenced in constructor at 0x4501D0", + "StoryEventWrapper::vftable": "referenced in Lua wrapper constructor at 0x73DC80", + "StoryPlotWrapper::vftable": "referenced in Lua wrapper constructor at 0x724120", + "LuaScriptClass::vftable": "referenced in LuaScriptClass constructor" + } + }, + + "how_story_flags_are_checked_during_gameplay": { + "status": "CONFIRMED", + "description": "Story flags are checked through the StoryEventFlagClass (event type 0x2C). When the story system evaluates events each tick, it iterates through the 61 type-indexed DynamicVectorClass arrays in the StorySubPlotClass. For type 0x2C (flag events), it checks whether the named flag has been set. Flags are set via the Set_Story_Flag Lua function which fires a story event of type FLAG. The CheckStoryFlagClass (RTTI confirmed) is a separate class used in the condition evaluation chain. The actual flag storage is within the event hash table itself — a flag 'exists and is active' is determined by the event's active byte at +0x4C being non-zero.", + "flag_event_type_id": "0x2C", + "flag_class": "StoryEventFlagClass", + "check_class": "CheckStoryFlagClass", + "flow": [ + "1. Lua calls Set_Story_Flag('FLAG_NAME') or Check_Story_Flag('FLAG_NAME')", + "2. This triggers a story event of type FLAG (0x2C) or queries the event hash table", + "3. StorySubPlot_FindEventByName converts the name to uppercase, computes CRC32", + "4. CRC32 XOR 0xDEADBEEF, then LCG for bucket index", + "5. Walks the linked list in that bucket comparing CRC32 values", + "6. If found, reads the active flag at event+0x4C", + "7. For Set: sets event+0x4C to non-zero and triggers dependent events", + "8. For Check: returns whether event+0x4C is non-zero" + ] + }, + + "how_rewards_are_dispatched": { + "status": "CONFIRMED", + "description": "When a story event completes (all conditions met), the reward system reads the reward type ID from the event (+0x3C) and the reward parameters from the string array at +0x50. The reward type was parsed from XML at load time via ResolveRewardTypeFromString (0x45C3F0) which hashes the reward type name string and looks it up in the global type tree. Reward dispatch is handled through the event's virtual function table — each concrete event subclass can override the reward execution method. The reward parameters (up to 14 strings at 0x20 stride) provide the data needed for the reward (unit type names, credit amounts, planet names, etc.).", + "flow": [ + "1. Event conditions are met (checked via per-type virtual methods)", + "2. Event reads reward_type_id from +0x3C", + "3. Event reads reward parameters from +0x50..+0x190 (up to 14 SSO strings)", + "4. Reward execution dispatched via virtual function table on the event", + "5. Specific reward logic (spawn unit, give money, etc.) uses the string params" + ] + }, + + "lua_script_globals_registered": { + "status": "CONFIRMED", + "registration_function_rva": "0x2567B0", + "registered_globals": [ + {"name": "StringCompare", "class": "LuaStringCompare"}, + {"name": "_ScriptExit", "class": "LuaScriptExit"}, + {"name": "_ScriptMessage", "class": "LuaScriptMessage"}, + {"name": "_DebugBreak", "class": "LuaDebugBreak"}, + {"name": "_CustomScriptMessage", "class": "LuaCustomScriptMessage"}, + {"name": "ThreadValue", "type_id": "0x41"}, + {"name": "GlobalValue", "class": "GlobalValue"}, + {"name": "_MessagePopup", "class": "LuaMessagePopup"}, + {"name": "_OuputDebug", "class": "LuaDebugPrint", "note": "Typo in original: 'OuputDebug' not 'OutputDebug'"}, + {"name": "GetThreadID", "class": "LuaGetThreadID"}, + {"name": "DumpCallStack", "class": "DumpCallStack"}, + {"name": "Create_Thread", "class": "LuaCreateThread"}, + {"name": "Thread", "class": "LuaCreateThread"}, + {"name": "GetEvent", "class": "GetEvent", "event_type_count": 61, "note": "This is the story event accessor from Lua scripts"} + ], + "luascript_method": { + "name": "Debug_Should_Issue_Event_Alert", + "rva": "0x17D070", + "description": "Registered as a method on the LuaScriptClass Lua object" + } + }, + + "signal_system": { + "status": "CONFIRMED", + "description": "Both StoryEventClass and StorySubPlotClass inherit from SignalGeneratorClass, which provides a publish-subscribe notification system. Events signal their state changes (triggered, completed, reset) to listeners. The SignalDispatcherClass is a singleton that routes signals between generators and listeners. SignalListenerClass instances (e.g., in StorySubPlotClass) subscribe to events and react when they fire.", + "key_classes": { + "SignalGeneratorClass": {"constructor_rva": "0x240610"}, + "SignalListClass": {"constructor_rva": "0x2406C0"}, + "SignalDispatcherClass": {"constructors": ["0x2215B0", "0x2218C0"]}, + "SingletonInstance": {"rva": "0x7E7140"} + } + }, + + "debug_logging": { + "status": "CONFIRMED", + "log_function_rva": "0x467540", + "known_log_messages": [ + "Creating story %s", + "___________ Event %s, CRC %u", + "!!!!!! Event %s, CRC %u already exists!", + "STORY MODE ERROR! Unable to process event of type %s", + "Error: Can't create story %s", + "StoryPlotWrapper::Get_Event -- cannot find event %s in plot %s.", + "ERROR! Story event - Unable to find event %s in event %s, plot %s", + "ERROR! Compute_Dependants has come across an invalid dependant for %s in plot %s", + "Reward type %s", + "StoryEventWrapper::Set_Reward_Type -- invalid number of parameters. Expected 1, got %d.", + "StoryEventWrapper::Set_Reward_Type -- invalid type for parameter 1. Expected string.", + "StoryEventWrapper::Set_Dialog -- invalid number of parameters. Expected 1, got %d.", + "StoryEventWrapper::Set_Dialog -- invalid type for parameter 1. Expected string.", + "StoryEventWrapper::Set_Reward_Parameter -- invalid number of parameters. Expected 2, got %d.", + "StoryEventWrapper::Set_Reward_Parameter -- invalid type for parameter 1. Expected number.", + "StoryEventWrapper::Set_Reward_Parameter -- must set exactly one parameter per parameter index.", + "StoryEventWrapper::Add_Dialog_Text -- invalid number of parameters. Expected 2.", + "StoryEventWrapper::Add_Dialog_Text -- invalid type for parameter 1. Expected string.", + "StoryEventWrapper::Add_Dialog_Text -- invalid type for parameter %d.", + "StoryPlotWrapper::Get_Event -- invalid number of parameters. Expected 1, got %d.", + "StoryPlotWrapper::Get_Event -- invalid type for parameter 1. Expected string." + ] + } +} diff --git a/ruff.toml b/ruff.toml index eebc54071..66348dcf9 100644 --- a/ruff.toml +++ b/ruff.toml @@ -10,6 +10,12 @@ target-version = "py312" line-length = 100 +# Incorporated reverse-engineering editor sources (bridge/toolkit/overlay/Lua +# bridge/Ghidra scripts): vendored RE tooling reproduced verbatim for provenance, +# not first-party-maintained Python; out of scope for the lean lint lane +# (consistent with this repo treating Python as auxiliary tooling). +extend-exclude = ["bridge", "swfoc_lua_bridge", "swfoc_overlay", "swfoc_toolkit", "ghidra_scripts"] + [lint] # Default-aligned ruff rule set (pyflakes F + pycodestyle E/W + import-sort I). # Kept lean: this is the security/correctness lint lane, not a style overhaul of diff --git a/sonar-project.properties b/sonar-project.properties deleted file mode 100644 index 62e64d2bb..000000000 --- a/sonar-project.properties +++ /dev/null @@ -1,19 +0,0 @@ -sonar.projectKey=Prekzursil_SWFOC-Mod-Menu -sonar.organization=prekzursil - -sonar.sources=src/ -sonar.tests=tests/ -sonar.exclusions=**/obj/**,**/bin/**,native/build-*/**,1397421866(original mod)/**,3447786229(submod)/**,3661482670(cheat_mode_example)/** - -# C# coverage (OpenCover format from dotnet test + coverlet) -sonar.cs.opencover.reportsPaths=**/TestResults/coverage/opencover.opencover.xml - -# Branch configuration -sonar.branch.name=main - -# Quality gate thresholds -sonar.coverage.exclusions=tests/**,scripts/**,tools/**,native/** -sonar.cpd.exclusions=tests/** - -# Encoding -sonar.sourceEncoding=UTF-8 diff --git a/swfoc_lua_bridge/.claude/ralph-loop.local.md b/swfoc_lua_bridge/.claude/ralph-loop.local.md new file mode 100644 index 000000000..7a3b9e94b --- /dev/null +++ b/swfoc_lua_bridge/.claude/ralph-loop.local.md @@ -0,0 +1,10 @@ +--- +active: true +iteration: 26 +session_id: +max_iterations: 10000 +completion_promise: "SWFOC_RALPH_CHECKPOINT_REACHED" +started_at: "2026-04-23T05:39:26Z" +--- + +Continue the SWFOC trainer autonomous loop per the brief in dot-claude slash ralph-loop dot local dot md and the backlog at knowledge-base slash ralph_backlog_P0_P7_2026-04-23 dot md. Every iteration: read dot-remember slash now dot md and dot-remember slash ralph_loop_state dot md first diff --git a/swfoc_lua_bridge/SNAPSHOT_FORMAT.md b/swfoc_lua_bridge/SNAPSHOT_FORMAT.md new file mode 100644 index 000000000..7a43f2261 --- /dev/null +++ b/swfoc_lua_bridge/SNAPSHOT_FORMAT.md @@ -0,0 +1,412 @@ +# SWFOC Snapshot Format (v1 + v2) + +> **Version history** +> +> - **v1** — original format. Magic `SWFOCSNAPv1`. No explicit local-player +> slot in section 1; the reader infers it as the first player. +> - **v2** — added 2026-04-08. Magic `SWFOCSNAPv2`. Adds an explicit +> `local_slot: uint32` immediately after `player_count` in section 1. +> `UINT32_MAX` (`0xFFFFFFFF`) means "no local player". +> +> Both `swfoc_lua_bridge/replay_harness.cpp` and the synthetic generator +> `swfoc_lua_bridge/make_test_snapshot.py` accept either version. The +> writer in `lua_bridge.cpp::Lua_DumpState` emits v2 by default. Pass +> `--v1` to `make_test_snapshot.py` to emit a v1 snapshot for back-compat +> regression tests. Both readers cross-check the magic against the +> `format_version` field and reject mismatched headers. + +## Purpose + +The `.swfocsnap` file format captures a single point-in-time view of SWFOC game state from a running `StarWarsG.exe` so it can be replayed offline by editor tests and analysis tools. The capture side lives inside `powrprof.dll` (the SWFOC Lua bridge) as the Lua helper `SWFOC_DumpState(path)`. The replay side lives outside this document. + +A snapshot is **not** a full memory dump. It stores only the fields the replay harness needs to mock out player slots, Lua state counts, object-type populations, and whitelisted global bindings. Everything is little-endian, tightly packed, and fixed-width where possible so a C, C++, C#, Python, or Rust reader can parse it with `memcpy`-style reads. + +## Byte Order and Padding Rules + +- All multi-byte integers are **little-endian**. +- All floats are **IEEE 754 64-bit** (no 32-bit floats at the file format level; the capture widens player credits from `float` to `double`). +- All ASCII strings in fixed-width fields are **null-padded, not null-terminated** — a 64-byte faction field containing `"REBEL"` has bytes `52 45 42 45 4C 00 00 ... 00`. +- The header is aligned to 8 bytes. Sections are **not** padded between each other; each section is immediately followed by the next section header. + +## File Layout + +``` ++----------------------------+ offset 0 +| FILE HEADER | 68 bytes total ++----------------------------+ offset 0x44 +| SECTION 1 | variable ++----------------------------+ +| SECTION 2 | variable ++----------------------------+ +| ... | ++----------------------------+ +| SECTION N | variable ++----------------------------+ +| END MARKER | 12 bytes (section_id=0xFFFFFFFF, length=4, crc32) ++----------------------------+ end of file +``` + +## File Header (68 bytes) + +| Offset | Size | Type | Field | Description | +|--------|------|---------------------|----------------------|-----------------------------------------------------------------------------| +| 0x00 | 16 | char[16] | magic | ASCII `"SWFOCSNAPv1"` (legacy) or `"SWFOCSNAPv2"` (current) followed by 5 null bytes. v2 exact bytes: `53 57 46 4F 43 53 4E 41 50 76 32 00 00 00 00 00` | +| 0x10 | 4 | uint32 LE | format_version | `1` for legacy snapshots, `2` for current. Reader cross-checks against magic and rejects mismatch. | +| 0x14 | 8 | uint64 LE | capture_timestamp_ms | Unix epoch milliseconds at the instant the capture started | +| 0x1C | 32 | uint8[32] | engine_build_hash | SHA-256 digest of `StarWarsG.exe`, or all zeros if unavailable | +| 0x3C | 1 | uint8 | game_mode | `0=unknown`, `1=galactic`, `2=tactical_space`, `3=tactical_land`, `4=menu` | +| 0x3D | 7 | uint8[7] | reserved | Zero-filled padding; section 1 starts at absolute offset `0x44` | + +Total header size: **68 bytes**. The first section header begins at file offset `0x44`. The header is not 8-aligned at the end, but every fixed-size field inside it is naturally aligned for its type, and each section's payload does its own alignment internally (see Section 4 where the `uint64` raw value is explicitly 8-aligned via its `pad[7]` field). + +Magic is checked byte-for-byte. A reader that does not see exactly these 16 bytes must reject the file. + +## Section Framing + +Every section after the header is preceded by an 8-byte section header: + +| Offset | Size | Type | Field | Description | +|--------|------|------------|----------------|------------------------------------------------------------| +| 0x00 | 4 | uint32 LE | section_id | Section type (see table below) | +| 0x04 | 4 | uint32 LE | section_length | Number of payload bytes that follow, **not** including this header | + +Defined section IDs: + +| ID | Name | Description | +|-------------|------------------|-----------------------------------------------------------| +| `0x00000001`| player_array | Player slot snapshots | +| `0x00000002`| lua_state_registry| Count and pointer list of registered Lua states | +| `0x00000003`| object_catalog | Summary of named game object types and their populations | +| `0x00000004`| global_registry | Whitelisted Lua globals | +| `0x00000005`| metadata | Key/value metadata strings | +| `0xFFFFFFFF`| end | End-of-file marker; payload is a 4-byte CRC32 | + +Sections must appear in ascending ID order in a v1 snapshot. The end marker must be last. Readers must skip unknown section IDs by advancing `section_length` bytes. + +## Section 1 — `player_array` (ID 0x00000001) + +Payload (v2 — current): + +``` +uint32 player_count +uint32 local_slot // v2 only — UINT32_MAX = no local player +for i in 0..player_count: + uint32 slot // 0-based slot index + char faction[64] // null-padded ASCII + float64 credits // widened from engine float32 + int32 tech_level + char player_name[64] // null-padded ASCII +``` + +Payload (v1 — legacy, no `local_slot`): + +``` +uint32 player_count +for i in 0..player_count: + uint32 slot // 0-based slot index + char faction[64] // null-padded ASCII + float64 credits // widened from engine float32 + int32 tech_level + char player_name[64] // null-padded ASCII +``` + +Per-player record size: `4 + 64 + 8 + 4 + 64 = 144 bytes`. + +Section length: +- v1: `4 + 144 * player_count` +- v2: `4 + 4 + 144 * player_count` (extra `local_slot: uint32`) + +Notes: +- `faction` comes from `PlayerObj.FactionName` (engine offset `+0x68`, `char*`). +- `credits` is read from `PlayerObj.Credits` (engine offset `+0x70`, `float`) and widened to `double`. +- `tech_level` is read from `PlayerObj.TechLevel` (engine offset `+0x84`, `int32`). +- `player_name` is reserved for future display-name data. v1 capture writes an empty string (all zero bytes) because the engine does not expose a player display name the bridge can safely dereference. Readers must not assume this field is populated. +- The capture bounds-checks `player_count` to `[0, 8]`. If the engine reports more than 8 players, only the first 8 are written and the count is clamped. + +## Section 2 — `lua_state_registry` (ID 0x00000002) + +Payload: + +``` +uint32 state_count +for i in 0..state_count: + uint64 state_pointer +``` + +Section length: `4 + 8 * state_count`. + +Notes: +- `state_pointer` values are the raw `lua_State*` addresses captured inside the running game. They are useless for replay (they point into a foreign address space) but they tell downstream tooling how many Lua states the engine spawned and whether the count matches what we expect from the mod under test. +- The capture bounds-checks `state_count` to `[0, 1024]` to defend against corrupted vectors. + +## Section 3 — `object_catalog` (ID 0x00000003) + +Payload: + +``` +uint32 type_count +for i in 0..type_count: + char type_name[64] // null-padded ASCII + uint32 instance_count +``` + +Per-type record size: `64 + 4 = 68 bytes`. + +Section length: `4 + 68 * type_count`. + +Notes: +- This is a **summary**, not the full object dump. The replay harness uses it to size its mock object lists — e.g., "the replay needs to simulate 12 TIE_Fighter instances so filter queries return the right count". +- `type_name` matches the SWFOC `GameObject_Type` name strings (e.g. `Nebulon_B_Frigate`, `TIE_Fighter`). +- `instance_count` is the number of live instances of that type at capture time. Zero is valid and means the type is known but currently has no instances. +- v1 capture walks a fixed whitelist of well-known names. Future versions may add discovery; the format supports up to `UINT32_MAX` entries. + +## Section 4 — `global_registry` (ID 0x00000004) + +Payload: + +``` +uint32 global_count +for i in 0..global_count: + char name[64] // null-padded ASCII + uint8 lua_type // 0=nil, 1=boolean, 3=number, 4=string, 6=function, 7=userdata + uint8 pad[7] // zero padding so raw_value is 8-aligned + uint64 raw_value_or_ptr // number = IEEE 754 bit pattern, string = pointer, function = C closure addr +``` + +Per-global record size: `64 + 1 + 7 + 8 = 80 bytes`. + +Section length: `4 + 80 * global_count`. + +Allowed `lua_type` values follow the Lua 5.0.2 type tags from `lua_types.h`: + +| Value | Lua type | +|-------|--------------------| +| 0 | `LUA_TNIL` | +| 1 | `LUA_TBOOLEAN` | +| 3 | `LUA_TNUMBER` | +| 4 | `LUA_TSTRING` | +| 6 | `LUA_TFUNCTION` | +| 7 | `LUA_TUSERDATA` | + +Values 2 (`LUA_TLIGHTUSERDATA`), 5 (`LUA_TTABLE`), and 8 (`LUA_TTHREAD`) are reserved and not written by v1 capture. + +Whitelisted global names (the capture writes one record per name that resolves to any non-nil value, plus the SWFOC_* helpers, and one record per name that is nil so the replay can verify absence): + +- `Find_Player` +- `Find_Object_Type` +- `Find_All_Objects_Of_Type` +- `Spawn_Unit` +- `Story_Event` +- `Letter_Box_On` +- `Suspend_AI` +- `GameRandom` +- `Create_Position` +- `SWFOC_GetVersion` +- `SWFOC_GetLocalPlayer` +- `SWFOC_SetCredits` +- `SWFOC_GetCredits` +- `SWFOC_SetTechLevel` +- `SWFOC_UncapCredits` +- `SWFOC_HeroInstantRespawn` +- `SWFOC_ListFactions` +- `SWFOC_Log` +- `SWFOC_DoString` +- `SWFOC_DrainPipe` +- `SWFOC_StateInfo` +- `SWFOC_EventControl` +- `SWFOC_DumpState` + +For `LUA_TNUMBER` globals, `raw_value_or_ptr` holds the raw IEEE 754 bit pattern of the number (read via `memcpy` from a `double`). For `LUA_TSTRING` globals, it holds the captured string's engine pointer (useless for replay but diagnostic). For `LUA_TFUNCTION` globals, it holds the C function pointer if the binding is a C closure, otherwise zero. For `LUA_TNIL`, it is zero. + +## Section 5 — `metadata` (ID 0x00000005) + +Payload: + +``` +uint32 entry_count +for i in 0..entry_count: + uint16 key_length + char key[key_length] + uint16 value_length + char value[value_length] +``` + +Neither `key` nor `value` is null-terminated. There is no padding between entries. + +Section length: sum of the field sizes above. + +Required keys in v1 (capture must emit all of them, in any order): + +| Key | Example value | +|------------------------|------------------------------| +| `capture_method` | `powrprof_dll` | +| `mod_name` | `unknown` | +| `mod_version` | `unknown` | +| `swfoc_bridge_version` | `1.0` | + +Readers must tolerate additional unknown keys. + +## Section 11 — `selected_units` (ID 0x0000000B, added 2026-04-23) + +OPTIONAL. Emitted by `SWFOC_DumpState` only when the selection reader +resolves a non-empty selection (i.e. tactical battle with units selected). +Enables replay-side tests to observe what the user had selected at capture +time without re-walking the live selection pointer chain. + +Payload: + +``` +uint32 count +for i in 0..count: + uint64 obj_addr // absolute GameObject pointer captured live +``` + +Section length: `4 + 8 * count`. `count` is bounded to `64` by the writer +(mirrors `RVA::Selection::kMaxSelectionCount`). + +## Section 12 — `unit_detail` (ID 0x0000000C, added 2026-04-23) + +OPTIONAL. Emitted alongside section 11. One record per selected obj_addr; +carries the state reads Task 99 (hardpoint-behavior invulnerability) and +Task 100 (damage-path hunt) need for autonomous offline iteration. + +Payload: + +``` +uint32 unit_count +for i in 0..unit_count: + uint64 obj_addr + char type_name[64] // reserved (writer currently emits "") + int32 owner_slot // GameObj+0x58 + float32 hull // GameObj+0x5C + float32 max_hull // reserved; writer emits 0.0 (engine layout unstable) + uint8 invuln_flag // GameObj+0x3A7 (display flag, no gameplay effect) + uint8 prevent_death // GameObj+0x3A1 (bit 0x80 is the capture of interest) + uint8 reserved[6] // zero padding, 8-byte alignment buffer + uint32 hardpoint_count + uint32 hardpoint_index[hardpoint_count] // indices derived from GameObj.ComponentArray walk +``` + +Per-unit fixed portion: 92 bytes. `hardpoint_count` is bounded to 32 by +the writer. Behavior-object lists are NOT in this section — see section 13. + +## Section 13 — `behavior_attach` (ID 0x0000000D, added 2026-04-23) + +OPTIONAL. Flat list of (obj_addr, hp_index, behavior_name) triples that +the replay harness applies on top of section 12's unit records. Current +writer emits `entry_count=0` because the engine does not expose a +"list behaviors on hardpoint" read path yet — once Task 100's IDA +investigation uncovers it, the writer populates this section. + +Payload: + +``` +uint32 entry_count +for i in 0..entry_count: + uint64 obj_addr + uint32 hp_index + char behavior_name[32] // e.g. "INVULNERABLE" +``` + +Per-entry size: 44 bytes. Readers silently skip triples that reference +obj_addrs not present in section 12 (forward-compat: a capture that emits +behaviors for units outside the selection does not fail loading). + +## End Marker + +After the last content section, the file ends with a 12-byte record: + +| Offset | Size | Type | Field | Description | +|--------|------|------------|----------------|-----------------------------------------------------------| +| 0x00 | 4 | uint32 LE | section_id | Always `0xFFFFFFFF` | +| 0x04 | 4 | uint32 LE | section_length | Always `4` | +| 0x08 | 4 | uint32 LE | crc32 | CRC32 of every byte from offset 0 up to and including `section_length` (i.e., the 8 bytes `FF FF FF FF 04 00 00 00` are covered by the CRC; the 4-byte CRC itself is not) | + +CRC32 is computed with the standard polynomial `0xEDB88320`, initial value `0xFFFFFFFF`, final XOR `0xFFFFFFFF`, and byte-reflected input/output (the same variant used by zlib, PKZIP, and `crc32()` in Python's `zlib` module). + +## Example Hex Dump (Minimal Snapshot) + +The smallest meaningful v1 snapshot has the header, an empty `player_array`, an empty `lua_state_registry`, an empty `object_catalog`, an empty `global_registry`, a `metadata` section with only the four required keys, and the end marker. With `capture_timestamp_ms = 0x1234567890ABCDEF`, `engine_build_hash = all zeros`, `game_mode = 4` (menu), and the metadata set to the example values above, the file looks like this (offsets on the left, bytes in the middle, annotation on the right): + +``` +0000: 53 57 46 4F 43 53 4E 41 50 76 31 00 00 00 00 00 magic "SWFOCSNAPv1" + 5 nulls +0010: 01 00 00 00 format_version = 1 +0014: EF CD AB 90 78 56 34 12 capture_timestamp_ms = 0x1234567890ABCDEF +001C: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 engine_build_hash bytes 0..15 +002C: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 engine_build_hash bytes 16..31 +003C: 04 game_mode = 4 (menu) +003D: 00 00 00 00 00 00 00 reserved padding + +0044: 01 00 00 00 section 1 id = player_array +0048: 04 00 00 00 section 1 length = 4 +004C: 00 00 00 00 player_count = 0 + +0050: 02 00 00 00 section 2 id = lua_state_registry +0054: 04 00 00 00 section 2 length = 4 +0058: 00 00 00 00 state_count = 0 + +005C: 03 00 00 00 section 3 id = object_catalog +0060: 04 00 00 00 section 3 length = 4 +0064: 00 00 00 00 type_count = 0 + +0068: 04 00 00 00 section 4 id = global_registry +006C: 04 00 00 00 section 4 length = 4 +0070: 00 00 00 00 global_count = 0 + +0074: 05 00 00 00 section 5 id = metadata +0078: 58 00 00 00 section 5 length = 0x58 (= 88) +007C: 04 00 00 00 entry_count = 4 + +0080: 0E 00 key_length = 14 +0082: 63 61 70 74 75 72 65 5F 6D 65 74 68 6F 64 "capture_method" +0090: 0C 00 value_length = 12 +0092: 70 6F 77 72 70 72 6F 66 5F 64 6C 6C "powrprof_dll" + +009E: 08 00 key_length = 8 +00A0: 6D 6F 64 5F 6E 61 6D 65 "mod_name" +00A8: 07 00 value_length = 7 +00AA: 75 6E 6B 6E 6F 77 6E "unknown" + +00B1: 0B 00 key_length = 11 +00B3: 6D 6F 64 5F 76 65 72 73 69 6F 6E "mod_version" +00BE: 07 00 value_length = 7 +00C0: 75 6E 6B 6E 6F 77 6E "unknown" + +00C7: 14 00 key_length = 20 +00C9: 73 77 66 6F 63 5F 62 72 69 64 67 65 5F 76 65 72 + 73 69 6F 6E "swfoc_bridge_version" +00DD: 03 00 value_length = 3 +00DF: 31 2E 30 "1.0" + +00E2: FF FF FF FF end marker section_id +00E6: 04 00 00 00 end marker section_length = 4 +00EA: XX XX XX XX crc32 (LE) of bytes 0x00 .. 0x00E9 inclusive +``` + +Total file size: `0x00EE = 238 bytes` for a fully-empty snapshot with the required metadata. + +The actual CRC32 value depends on the concrete bytes produced by the capture, so it is shown as `XX XX XX XX` above. + +## Reader Checklist + +A compliant reader MUST: + +1. Verify the magic matches one of `SWFOCSNAPv1` or `SWFOCSNAPv2` exactly. +2. Verify `format_version` is `1` (legacy) or `2` (current). Reject the file if the magic and the version disagree (e.g. `SWFOCSNAPv1` header with `format_version=2`). +3. When `format_version >= 2`, read the additional `local_slot: uint32` field in section 1 immediately after `player_count`. Treat `UINT32_MAX` (`0xFFFFFFFF`) as "no local player". +4. When `format_version == 1`, derive the local slot from `players[0].slot` after the per-player loop completes (legacy convention). +5. Skip unknown section IDs by consuming `section_length` bytes. +6. Recompute the CRC32 over all bytes before the end-marker CRC field and compare. Reject the file on mismatch. +7. Treat all fixed-width strings as null-padded and stop interpreting at the first null byte. +8. Treat `capture_timestamp_ms = 0` as "unknown" but still accept the file. + +## Writer Guarantees + +The capture writer (`SWFOC_DumpState`) guarantees: + +1. The header is written exactly once at offset 0. +2. Sections appear in ascending ID order, no duplicates. +3. `player_count` is clamped to at most 8. +4. `state_count` is clamped to at most 1024. +5. The `capture_timestamp_ms` field is populated via `GetSystemTimeAsFileTime` converted to Unix milliseconds. +6. The end marker and CRC32 are written last. If the writer is interrupted before the end marker is emitted, the file is truncated and invalid — readers will detect this via the CRC mismatch and/or missing end marker. diff --git a/swfoc_lua_bridge/build.bat b/swfoc_lua_bridge/build.bat new file mode 100644 index 000000000..cea82c75a --- /dev/null +++ b/swfoc_lua_bridge/build.bat @@ -0,0 +1,69 @@ +@echo off +echo === SWFOC Lua Bridge Build === +echo. + +set GCC=x86_64-w64-mingw32-gcc +set GPP=x86_64-w64-mingw32-g++ +set CFLAGS=-O2 -DWIN32_LEAN_AND_MEAN +set CPPFLAGS=-O2 -std=c++17 -DWIN32_LEAN_AND_MEAN -I. -Iminhook/include + +echo [1/4] Compiling MinHook... +%GCC% -c %CFLAGS% -Iminhook/include -Iminhook/src minhook/src/hook.c -o hook.o +%GCC% -c %CFLAGS% minhook/src/buffer.c -o buffer.o +%GCC% -c %CFLAGS% minhook/src/trampoline.c -o trampoline.o +%GCC% -c %CFLAGS% minhook/src/hde/hde64.c -o hde64.o +if errorlevel 1 goto fail + +echo [2/4] Compiling proxy... +%GPP% -c %CPPFLAGS% proxy.cpp -o proxy.o +if errorlevel 1 goto fail + +echo [3/4] Compiling bridge... +%GPP% -c %CPPFLAGS% lua_bridge.cpp -o lua_bridge.o +%GPP% -c %CPPFLAGS% dllmain.cpp -o dllmain.o +if errorlevel 1 goto fail + +echo [4/4] Linking powrprof.dll... +%GPP% -shared -o powrprof.dll dllmain.o proxy.o lua_bridge.o hook.o buffer.o trampoline.o hde64.o exports.def -lkernel32 -luser32 -static -s +if errorlevel 1 goto fail + +echo. +echo === DLL BUILD SUCCESS === +dir powrprof.dll +echo. +echo Install: copy powrprof.dll to your game's corruption\ folder +echo. + +echo [5/6] Building test harness... +%GPP% -o bridge_test_harness.exe test_harness.cpp fake_lua.cpp fake_memory.cpp -std=c++17 -DTEST_MODE -DWIN32_LEAN_AND_MEAN -I. -static +if errorlevel 1 goto test_fail + +echo. +echo Running tests... +.\bridge_test_harness.exe +if errorlevel 1 goto test_fail + +echo [6/6] Building replay harness... +%GPP% -O2 -std=c++17 -static -o swfoc_replay.exe replay_harness.cpp fake_lua.cpp fake_memory.cpp -lws2_32 +if errorlevel 1 goto replay_fail + +echo. +echo === ALL BUILDS AND TESTS PASSED === +dir swfoc_replay.exe +goto end + +:replay_fail +echo. +echo === REPLAY HARNESS BUILD FAILED === +goto end + +:test_fail +echo. +echo === TEST HARNESS BUILD OR RUN FAILED === +goto end + +:fail +echo. +echo === DLL BUILD FAILED === + +:end diff --git a/swfoc_lua_bridge/dllmain.cpp b/swfoc_lua_bridge/dllmain.cpp new file mode 100644 index 000000000..88c627bb9 --- /dev/null +++ b/swfoc_lua_bridge/dllmain.cpp @@ -0,0 +1,49 @@ +// DLL entry point — initializes proxy forwarding + Lua bridge +// Includes crash handler to log exceptions before game terminates. + +#include +#include + +extern bool Proxy_Init(); +extern void Proxy_Shutdown(); +extern bool LuaBridge_Init(); +extern void LuaBridge_Shutdown(); + +static LONG WINAPI CrashHandler(EXCEPTION_POINTERS* ex) { + FILE* f = fopen("swfoc_bridge_crash.log", "a"); + if (f) { + DWORD code = ex->ExceptionRecord->ExceptionCode; + auto addr = (uintptr_t)ex->ExceptionRecord->ExceptionAddress; + auto base = (uintptr_t)GetModuleHandleA(nullptr); + auto rip = ex->ContextRecord->Rip; + auto rsp = ex->ContextRecord->Rsp; + auto rcx = ex->ContextRecord->Rcx; + auto rdx = ex->ContextRecord->Rdx; + auto r8 = ex->ContextRecord->R8; + + fprintf(f, "=== SWFOC BRIDGE CRASH ===\n"); + fprintf(f, "Exception: 0x%08lX at 0x%llX (RVA 0x%llX)\n", code, addr, addr - base); + fprintf(f, "RIP=0x%llX (RVA 0x%llX)\n", rip, rip - base); + fprintf(f, "RSP=0x%llX RCX=0x%llX RDX=0x%llX R8=0x%llX\n", rsp, rcx, rdx, r8); + fprintf(f, "Module base=0x%llX\n", base); + fprintf(f, "=============================\n\n"); + fclose(f); + } + return EXCEPTION_CONTINUE_SEARCH; // let the game's own handler run too +} + +BOOL APIENTRY DllMain(HMODULE hModule, DWORD reason, LPVOID reserved) { + switch (reason) { + case DLL_PROCESS_ATTACH: + DisableThreadLibraryCalls(hModule); + SetUnhandledExceptionFilter(CrashHandler); + if (!Proxy_Init()) return FALSE; + LuaBridge_Init(); + break; + case DLL_PROCESS_DETACH: + LuaBridge_Shutdown(); + Proxy_Shutdown(); + break; + } + return TRUE; +} diff --git a/swfoc_lua_bridge/exports.def b/swfoc_lua_bridge/exports.def new file mode 100644 index 000000000..6f501473e --- /dev/null +++ b/swfoc_lua_bridge/exports.def @@ -0,0 +1,8 @@ +LIBRARY powrprof +EXPORTS + CallNtPowerInformation=Proxy_CallNtPowerInformation + PowerReadACValueIndex=Proxy_PowerReadACValueIndex + PowerReadDCValueIndex=Proxy_PowerReadDCValueIndex + PowerGetActiveScheme=Proxy_PowerGetActiveScheme + PowerSettingRegisterNotification=Proxy_PowerSettingRegisterNotification + PowerSettingUnregisterNotification=Proxy_PowerSettingUnregisterNotification diff --git a/swfoc_lua_bridge/fake_lua.cpp b/swfoc_lua_bridge/fake_lua.cpp new file mode 100644 index 000000000..edd9a69ba --- /dev/null +++ b/swfoc_lua_bridge/fake_lua.cpp @@ -0,0 +1,285 @@ +// fake_lua.cpp -- Stub Lua 5.0.2 C API implementations for test harness. +// Every function logs its call to L->call_log and manipulates L->stack. + +#include "fake_lua.h" +#include +#include +#include + +// Lua type constants (must match lua_types.h / bridge code) +#define FL_TNIL 0 +#define FL_TBOOLEAN 1 +#define FL_TNUMBER 3 +#define FL_TSTRING 4 +#define FL_TTABLE 5 +#define FL_TFUNCTION 6 + +// GLOBALSINDEX pseudo-index +#define FL_GLOBALSINDEX (-10001) + +// ---- Internal helpers ---- + +// Resolve a Lua-style index (1-based positive, negative from top) to a +// zero-based vector index. Returns -1 if out of range. +static int resolve_index(FakeLuaState* L, int idx) { + if (idx > 0) return idx - 1; // 1-based -> 0-based + if (idx < 0 && idx != FL_GLOBALSINDEX) return (int)L->stack.size() + idx; + return -1; // GLOBALSINDEX or other pseudo-index +} + +static void log_call(FakeLuaState* L, const std::string& entry) { + L->call_log.push_back(entry); +} + +// ---- API stubs ---- + +int fake_gettop(FakeLuaState* L) { + log_call(L, "gettop"); + return (int)L->stack.size(); +} + +void fake_settop(FakeLuaState* L, int idx) { + log_call(L, "settop(" + std::to_string(idx) + ")"); + if (idx >= 0) { + L->stack.resize((size_t)idx); + } else { + // settop(-n-1) pops n elements: e.g. settop(-2) pops 1 + int newSize = (int)L->stack.size() + idx + 1; + if (newSize < 0) newSize = 0; + L->stack.resize((size_t)newSize); + } +} + +const char* fake_pushstring(FakeLuaState* L, const char* s) { + log_call(L, std::string("pushstring(\"") + (s ? s : "null") + "\")"); + StackEntry e; + e.type = FL_TSTRING; + e.strval = s ? s : ""; + L->stack.push_back(e); + // Return pointer to the string stored in the stack entry. + // This is stable until the entry is popped/overwritten. + return L->stack.back().strval.c_str(); +} + +void fake_pushnumber(FakeLuaState* L, double n) { + char buf[64]; + snprintf(buf, sizeof(buf), "pushnumber(%.6g)", n); + log_call(L, buf); + StackEntry e; + e.type = FL_TNUMBER; + e.numval = n; + L->stack.push_back(e); +} + +void fake_pushboolean(FakeLuaState* L, int b) { + log_call(L, std::string("pushboolean(") + std::to_string(b) + ")"); + StackEntry e; + e.type = FL_TBOOLEAN; + e.boolval = b; + L->stack.push_back(e); +} + +void fake_pushnil(FakeLuaState* L) { + log_call(L, "pushnil"); + StackEntry e; + e.type = FL_TNIL; + L->stack.push_back(e); +} + +void fake_pushcclosure(FakeLuaState* L, void* fn, int n) { + log_call(L, "pushcclosure"); + // Pop n upvalues from the stack (Lua 5.0 semantics) + for (int i = 0; i < n && !L->stack.empty(); i++) + L->stack.pop_back(); + StackEntry e; + e.type = FL_TFUNCTION; + e.funcptr = fn; + L->stack.push_back(e); +} + +void fake_newtable(FakeLuaState* L) { + log_call(L, "newtable"); + StackEntry e; + e.type = FL_TTABLE; + L->stack.push_back(e); +} + +void fake_settable(FakeLuaState* L, int idx) { + log_call(L, "settable(" + std::to_string(idx) + ")"); + // Stack: ... table ... key value (top = value, top-1 = key) + if (L->stack.size() < 2) return; + + StackEntry value = L->stack.back(); L->stack.pop_back(); + StackEntry key = L->stack.back(); L->stack.pop_back(); + + if (idx == FL_GLOBALSINDEX) { + // Set global + L->globals[key.strval] = value; + } + // For regular table indices we just pop -- we don't simulate real table storage + // beyond globals, which is sufficient for testing the bridge. +} + +void fake_gettable(FakeLuaState* L, int idx) { + log_call(L, "gettable(" + std::to_string(idx) + ")"); + if (L->stack.empty()) return; + + StackEntry key = L->stack.back(); L->stack.pop_back(); + + if (idx == FL_GLOBALSINDEX) { + auto it = L->globals.find(key.strval); + if (it != L->globals.end()) { + L->stack.push_back(it->second); + } else if (key.strval == "Find_Object_Type" && L->has_game_globals) { + StackEntry e; + e.type = FL_TFUNCTION; + e.strval = "Find_Object_Type"; + L->stack.push_back(e); + } else { + StackEntry e; + e.type = FL_TNIL; + L->stack.push_back(e); + } + } else { + // Non-global gettable: push nil + StackEntry e; + e.type = FL_TNIL; + L->stack.push_back(e); + } +} + +void fake_rawseti(FakeLuaState* L, int idx, int n) { + char buf[64]; + snprintf(buf, sizeof(buf), "rawseti(%d, %d)", idx, n); + log_call(L, buf); + // Pop the value from the top; the table at idx remains + if (!L->stack.empty()) { + L->stack.pop_back(); + } +} + +int fake_type(FakeLuaState* L, int idx) { + log_call(L, "type(" + std::to_string(idx) + ")"); + int ri = resolve_index(L, idx); + if (ri < 0 || ri >= (int)L->stack.size()) return FL_TNIL; + return L->stack[ri].type; +} + +double fake_tonumber(FakeLuaState* L, int idx) { + log_call(L, "tonumber(" + std::to_string(idx) + ")"); + int ri = resolve_index(L, idx); + if (ri < 0 || ri >= (int)L->stack.size()) return 0.0; + const auto& e = L->stack[ri]; + if (e.type == FL_TNUMBER) return e.numval; + if (e.type == FL_TSTRING) { + // Lua 5.0 coerces numeric strings + try { return std::stod(e.strval); } catch (...) { return 0.0; } + } + return 0.0; +} + +// We store returned c_str pointers in a static buffer so they survive the call. +// This is safe for single-threaded test usage. +static thread_local std::string s_tostring_buf; + +const char* fake_tostring(FakeLuaState* L, int idx) { + log_call(L, "tostring(" + std::to_string(idx) + ")"); + int ri = resolve_index(L, idx); + if (ri < 0 || ri >= (int)L->stack.size()) return nullptr; + const auto& e = L->stack[ri]; + if (e.type == FL_TSTRING) return e.strval.c_str(); + if (e.type == FL_TNUMBER) { + char buf[64]; + snprintf(buf, sizeof(buf), "%.14g", e.numval); + s_tostring_buf = buf; + return s_tostring_buf.c_str(); + } + return nullptr; +} + +int fake_pcall(FakeLuaState* L, int nargs, int nresults, int errfunc) { + char buf[128]; + snprintf(buf, sizeof(buf), "pcall(nargs=%d, nresults=%d)", nargs, nresults); + log_call(L, buf); + + if (L->pcall_error != 0) { + // Simulate error: pop the function + args, push error message + int toPop = 1 + nargs; + for (int i = 0; i < toPop && !L->stack.empty(); i++) + L->stack.pop_back(); + StackEntry e; + e.type = FL_TSTRING; + e.strval = L->pcall_error_msg.empty() ? "pcall error" : L->pcall_error_msg; + L->stack.push_back(e); + return L->pcall_error; + } + + // 2026-04-27: type-existence simulation. If the call shape is + // Find_Object_Type(typename) and the typename is in known_object_types, + // pop everything and push a fake userdata so the bridge code's + // "rty != LUA_TNIL" branch fires. Used by Lua_BatchTypeExists tests. + bool simulateTypeExists = false; + if (nargs == 1 && nresults == 1 && L->stack.size() >= 2) { + const auto& fn = L->stack[L->stack.size() - 2]; + const auto& arg = L->stack[L->stack.size() - 1]; + bool isFindObjType = + fn.type == FL_TFUNCTION && fn.strval == "Find_Object_Type"; + if (isFindObjType && arg.type == FL_TSTRING && + L->known_object_types.count(arg.strval) > 0) { + simulateTypeExists = true; + } + } + + // Simulate success: pop the function + args + int toPop = 1 + nargs; + for (int i = 0; i < toPop && !L->stack.empty(); i++) + L->stack.pop_back(); + + // Push nresults result values. For Find_Object_Type with a known + // typename we push a TUSERDATA-shaped entry; everything else gets nil. + for (int i = 0; i < nresults; i++) { + StackEntry e; + if (simulateTypeExists && i == 0) { + // LUA_TUSERDATA = 7 in Lua 5.0; the bridge code only checks + // "is non-nil", so the exact tag doesn't matter, but we use + // TFUNCTION (already defined) as a non-nil sentinel to keep + // dependencies on lua_types.h tag values out of fake_lua. + e.type = FL_TFUNCTION; + e.strval = ""; + } else { + e.type = FL_TNIL; + } + L->stack.push_back(e); + } + return 0; +} + +int fake_load(FakeLuaState* L, void* reader, void* data, const char* chunkname) { + log_call(L, std::string("load(\"") + (chunkname ? chunkname : "") + "\")"); + + if (L->load_error != 0) { + StackEntry e; + e.type = FL_TSTRING; + e.strval = L->load_error_msg.empty() ? "load error" : L->load_error_msg; + L->stack.push_back(e); + return L->load_error; + } + + // Success: push a fake compiled chunk (function) onto the stack + StackEntry e; + e.type = FL_TFUNCTION; + e.strval = chunkname ? chunkname : "chunk"; + L->stack.push_back(e); + return 0; +} + +void fake_reset(FakeLuaState* L) { + L->stack.clear(); + L->globals.clear(); + L->call_log.clear(); + L->has_game_globals = false; + L->pcall_error = 0; + L->pcall_error_msg.clear(); + L->load_error = 0; + L->load_error_msg.clear(); +} diff --git a/swfoc_lua_bridge/fake_lua.h b/swfoc_lua_bridge/fake_lua.h new file mode 100644 index 000000000..836eaeb4a --- /dev/null +++ b/swfoc_lua_bridge/fake_lua.h @@ -0,0 +1,67 @@ +#pragma once +// fake_lua.h -- Stub Lua 5.0.2 API for offline testing of the SWFOC bridge. +// Provides a FakeLuaState with stack simulation and call logging so that +// every bridge function can be exercised without the game process. + +#include +#include +#include +#include +#include +#include + +// Forward-declare so the bridge code's lua_CFunction typedef resolves. +struct FakeLuaState; + +// Stack entry: every push/pop manipulates these. +struct StackEntry { + int type; // LUA_TNIL=0, LUA_TBOOLEAN=1, LUA_TNUMBER=3, LUA_TSTRING=4, etc. + double numval; + std::string strval; + int boolval; + void* funcptr; // for TFUNCTION entries (closure pointer) + + StackEntry() : type(0), numval(0), strval(), boolval(0), funcptr(nullptr) {} +}; + +struct FakeLuaState { + std::vector stack; + std::map globals; // Global table (GLOBALSINDEX) + std::vector call_log; // Log of all API calls made + + bool has_game_globals = false; // If true, Find_Object_Type exists as a global function + int pcall_error = 0; // If non-zero, pcall returns this error code + std::string pcall_error_msg; // Error message for failed pcall + int load_error = 0; // If non-zero, lua_load returns this error + std::string load_error_msg; // Error message for failed load + + // 2026-04-27: type-existence simulation for Lua_BatchTypeExists tests. + // If non-empty, fake_pcall inspects the arg (top-1) of a Find_Object_Type + // call and pushes a fake userdata if the typename is in this set, nil + // otherwise. Lets the harness verify both the "exists" and "missing" + // paths without injecting a real engine pointer. + std::set known_object_types; +}; + +// ---- Stub function declarations ---- +// These match the signatures the bridge's function pointers expect. + +int fake_gettop(FakeLuaState* L); +void fake_settop(FakeLuaState* L, int idx); +const char* fake_pushstring(FakeLuaState* L, const char* s); +void fake_pushnumber(FakeLuaState* L, double n); +void fake_pushboolean(FakeLuaState* L, int b); +void fake_pushnil(FakeLuaState* L); +void fake_pushcclosure(FakeLuaState* L, void* fn, int n); +void fake_newtable(FakeLuaState* L); +void fake_settable(FakeLuaState* L, int idx); +void fake_gettable(FakeLuaState* L, int idx); +void fake_rawseti(FakeLuaState* L, int idx, int n); +int fake_type(FakeLuaState* L, int idx); +double fake_tonumber(FakeLuaState* L, int idx); +const char* fake_tostring(FakeLuaState* L, int idx); +int fake_pcall(FakeLuaState* L, int nargs, int nresults, int errfunc); +int fake_load(FakeLuaState* L, void* reader, void* data, const char* chunkname); + +// Helpers +void fake_reset(FakeLuaState* L); // Clear stack, globals, call_log diff --git a/swfoc_lua_bridge/fake_memory.cpp b/swfoc_lua_bridge/fake_memory.cpp new file mode 100644 index 000000000..959f1a5a7 --- /dev/null +++ b/swfoc_lua_bridge/fake_memory.cpp @@ -0,0 +1,131 @@ +// fake_memory.cpp -- Process memory mock implementations. +// Manages sparse byte regions keyed by address for offline testing. + +#include "fake_memory.h" +#include "rvas.h" +#include +#include + +FakeMemory g_fakeMem; + +// Page size for region alignment (4KB). +static constexpr size_t PAGE_SIZE = 0x1000; + +// Round addr down to page boundary. +static uintptr_t page_base(uintptr_t addr) { + return addr & ~(uintptr_t)(PAGE_SIZE - 1); +} + +void FakeMemory::ensure_region(uintptr_t addr, size_t len) { + uintptr_t base = page_base(addr); + size_t needed = (addr - base) + len; + // Round up to next page + size_t pages = (needed + PAGE_SIZE - 1) / PAGE_SIZE; + size_t total = pages * PAGE_SIZE; + + auto it = regions.find(base); + if (it == regions.end()) { + regions[base].resize(total, 0); + } else if (it->second.size() < total) { + it->second.resize(total, 0); + } +} + +void FakeMemory::write_bytes(uintptr_t addr, const void* data, size_t len) { + ensure_region(addr, len); + uintptr_t base = page_base(addr); + size_t offset = addr - base; + memcpy(regions[base].data() + offset, data, len); +} + +void FakeMemory::read_bytes(uintptr_t addr, void* out, size_t len) const { + uintptr_t base = page_base(addr); + auto it = regions.find(base); + if (it == regions.end()) { + memset(out, 0, len); + return; + } + size_t offset = addr - base; + if (offset + len <= it->second.size()) { + memcpy(out, it->second.data() + offset, len); + } else { + memset(out, 0, len); + } +} + +void FakeMemory::write_float(uintptr_t addr, float val) { + write_bytes(addr, &val, sizeof(float)); +} + +float FakeMemory::read_float(uintptr_t addr) const { + float val = 0; + read_bytes(addr, &val, sizeof(float)); + return val; +} + +void FakeMemory::write_int32(uintptr_t addr, int32_t val) { + write_bytes(addr, &val, sizeof(int32_t)); +} + +int32_t FakeMemory::read_int32(uintptr_t addr) const { + int32_t val = 0; + read_bytes(addr, &val, sizeof(int32_t)); + return val; +} + +void FakeMemory::write_uint8(uintptr_t addr, uint8_t val) { + write_bytes(addr, &val, sizeof(uint8_t)); +} + +uint8_t FakeMemory::read_uint8(uintptr_t addr) const { + uint8_t val = 0; + read_bytes(addr, &val, sizeof(uint8_t)); + return val; +} + +void FakeMemory::write_qword(uintptr_t addr, uint64_t val) { + write_bytes(addr, &val, sizeof(uint64_t)); +} + +uint64_t FakeMemory::read_qword(uintptr_t addr) const { + uint64_t val = 0; + read_bytes(addr, &val, sizeof(uint64_t)); + return val; +} + +void FakeMemory::write_string(uintptr_t addr, const char* s) { + size_t len = strlen(s) + 1; // include null terminator + write_bytes(addr, s, len); +} + +void FakeMemory::setup_player(uintptr_t addr, int slot, bool isLocal, + float credits, int tech, const char* faction) { + // Allocate enough for the biggest offset we use (TechLevel at 0x88 + 4 = 0x8C) + ensure_region(addr, 0x100); + + write_int32(addr + RVA::PlayerObj::SlotIndex, slot); + write_uint8(addr + RVA::PlayerObj::LocalPlayer, isLocal ? 1 : 0); + write_float(addr + RVA::PlayerObj::Credits, credits); + write_float(addr + RVA::PlayerObj::MaxCredits, 100000.0f); // default max + write_int32(addr + RVA::PlayerObj::TechLevel, tech); + write_int32(addr + RVA::PlayerObj::MaxTechLevel, 5); + + // Faction name is a pointer to a string. We store the string at addr+0xF0 + // and write the pointer at FactionName offset. + uintptr_t strAddr = addr + 0xF0; + write_string(strAddr, faction); + write_qword(addr + RVA::PlayerObj::FactionName, strAddr); +} + +void FakeMemory::setup_player_array(uintptr_t arrayAddr, uintptr_t countAddr, + const std::vector& players, int count) { + ensure_region(arrayAddr, count * 8); + for (int i = 0; i < count; i++) { + write_qword(arrayAddr + i * 8, players[i]); + } + write_int32(countAddr, count); +} + +void FakeMemory::clear() { + regions.clear(); +} diff --git a/swfoc_lua_bridge/fake_memory.h b/swfoc_lua_bridge/fake_memory.h new file mode 100644 index 000000000..c341b4f6a --- /dev/null +++ b/swfoc_lua_bridge/fake_memory.h @@ -0,0 +1,53 @@ +#pragma once +// fake_memory.h -- Process memory mock for offline bridge testing. +// Provides a simple address-to-byte-vector map so that the bridge's memory +// reads and writes (player objects, player array, globals) can be exercised +// without the game process. + +#include +#include +#include +#include +#include + +class FakeMemory { +public: + // Sparse memory: keyed by page-aligned base, value is byte vector. + // For simplicity we store flat regions at exact addresses. + std::map> regions; + + // Ensure a region of at least `len` bytes exists at `addr`. + void ensure_region(uintptr_t addr, size_t len); + + void write_bytes(uintptr_t addr, const void* data, size_t len); + void read_bytes(uintptr_t addr, void* out, size_t len) const; + + void write_float(uintptr_t addr, float val); + float read_float(uintptr_t addr) const; + + void write_int32(uintptr_t addr, int32_t val); + int32_t read_int32(uintptr_t addr) const; + + void write_uint8(uintptr_t addr, uint8_t val); + uint8_t read_uint8(uintptr_t addr) const; + + void write_qword(uintptr_t addr, uint64_t val); + uint64_t read_qword(uintptr_t addr) const; + + // Write a null-terminated C string starting at addr. + void write_string(uintptr_t addr, const char* s); + + // Set up a fake PlayerObject at the given address with specified fields. + void setup_player(uintptr_t addr, int slot, bool isLocal, + float credits, int tech, const char* faction); + + // Set up the PlayerArray global: write player pointers into an array + // at `arrayAddr`, and set PlayerCount at `countAddr`. + void setup_player_array(uintptr_t arrayAddr, uintptr_t countAddr, + const std::vector& players, int count); + + // Reset all memory regions. + void clear(); +}; + +extern FakeMemory g_fakeMem; diff --git a/swfoc_lua_bridge/lua_bridge.cpp b/swfoc_lua_bridge/lua_bridge.cpp new file mode 100644 index 000000000..1624abbdc --- /dev/null +++ b/swfoc_lua_bridge/lua_bridge.cpp @@ -0,0 +1,9306 @@ +// SWFOC Lua Bridge — hooks lua_open to inject custom Lua functions +// into the game's Lua 5.0.2 engine. All functions run in the game's +// own thread context (safe, no thread issues). + +#include +#include +#include +#include +#include +#include +#include +#include "minhook/include/MinHook.h" +#include "rvas.h" +#include "lua_types.h" +#include "shared_memory.h" + +static uintptr_t g_base = 0; +static lua_State* g_mainState = nullptr; +static FILE* g_log = nullptr; + +// Game state cache — only states with game globals (e.g. Find_Object_Type) +static CRITICAL_SECTION csGameStates; +static std::vector cached_game_states; + +// Track all states that received our SWFOC_* function registration (for pipe/shmem drain) +static std::vector registered_states; +static CRITICAL_SECTION csRegistered; + +// ====================================================================== +// Diagnostic counters — exposed via SWFOC_Diag* helpers for live-validation. +// All volatile + InterlockedIncrement so the PipeThreadProc writer and the +// Hook_luaD_call writer can race with the Lua reader on whatever state is +// currently draining commands. Zero-initialized at module load (BSS). +// ====================================================================== +static volatile LONG g_pipeReceivedCount = 0; // inc on every ReadFile success in PipeThreadProc +static volatile LONG g_pipeCompletedCount = 0; // inc on every successful Lua execute + reply +static volatile LONG g_pipeErrorCount = 0; // inc on every failure branch inside PipeThreadProc +static volatile LONGLONG g_luaDCallTickCounter = 0; // inc on every Hook_luaD_call invocation + +// Registered-helper name manifest — populated by RegisterAll once at first +// lua_open hook fire. Reader = Lua_DiagListRegisteredFunctions which pushes +// a stable c_str onto the Lua stack. The buffer is sized for the canonical +// 34-helper list with some slack for future additions. Bumped 2048 -> 4096 on +// 2026-04-10 alongside the PIPE_CMD_MAX bump so growth headroom never races +// the pipe-response limit. +static char g_registeredFunctionManifest[4096] = {0}; +static int g_registeredFunctionCount = 0; + +// ====================================================================== +// Lua C API function pointers (resolved from game binary) +// ====================================================================== + +static pfn_lua_open real_lua_open = nullptr; // original, pre-hook + +// luaD_call hook — used to drain pipe commands on the main thread +typedef void (*pfn_luaD_call)(lua_State* L, void* func, int nResults); +static pfn_luaD_call real_luaD_call = nullptr; + +// lua_close hook — removes states from game state cache on destruction +typedef void (*lua_close_t)(void* L); +static lua_close_t orig_lua_close = nullptr; +static pfn_lua_pushstring fn_pushstring = nullptr; +static pfn_lua_pushcclosure fn_pushcclosure = nullptr; +static pfn_lua_settop fn_settop = nullptr; +static pfn_lua_tonumber fn_tonumber = nullptr; +static pfn_lua_tostring fn_tostring = nullptr; +static pfn_lua_type fn_type = nullptr; +static pfn_lua_newtable fn_newtable = nullptr; +static pfn_lua_settable fn_settable = nullptr; +static pfn_lua_gettable fn_gettable = nullptr; +static pfn_lua_rawseti fn_rawseti = nullptr; +static pfn_lua_pushnumber fn_pushnumber = nullptr; +static pfn_lua_pushboolean fn_pushboolean = nullptr; +static pfn_lua_pushnil fn_pushnil = nullptr; +static pfn_lua_gettop fn_gettop = nullptr; +static pfn_lua_pcall fn_pcall = nullptr; +static pfn_lua_load fn_load = nullptr; + +// ====================================================================== +// Named pipe command queue (thread-safe) +// ====================================================================== + +// 2026-04-10: bumped from 4096 -> 16384 so the SWFOC_DiagListRegisteredFunctions +// manifest (and any future diagnostic payloads) can round-trip through the pipe +// without truncation. 16 KB is well under the 64 KB named-pipe protocol cap and +// command inputs are never close to that ceiling (longest Lua snippets are ~1 KB). +#define PIPE_CMD_MAX 16384 +#define PIPE_NAME "\\\\.\\pipe\\swfoc_bridge" + +static CRITICAL_SECTION g_pipeLock; +static char g_pipeCmd[PIPE_CMD_MAX]; +static bool g_pipeCmdPending = false; +// The response buffer must be at least as large as PIPE_CMD_MAX — this was +// the real truncation cause before the 2026-04-10 bump (old value was 512, +// which capped every reply regardless of what the pipe protocol supported). +static char g_pipeResult[PIPE_CMD_MAX]; +static bool g_pipeResultReady = false; +static HANDLE g_pipeThread = nullptr; +static volatile bool g_pipeShutdown = false; + +// ====================================================================== +// Shared memory command buffer (for CE which can't use pipes) +// ====================================================================== + +static HANDLE g_hCmdMap = nullptr; +static SharedCmdBuffer* g_cmdBuf = nullptr; +static uint32_t g_lastCmdSeq = 0; + +// Event buffer (for later waves) +static HANDLE g_hEvtMap = nullptr; +static SharedEvtBuffer* g_evtBuf = nullptr; + +// ====================================================================== +// Event ring buffer writer (lock-free SPSC: DLL is sole writer) +// ====================================================================== + +static void WriteEvent(uint16_t type, const void* payload, uint16_t payloadSize) { + if (!g_evtBuf) return; + if (!(g_evtBuf->flags.load(std::memory_order_acquire) & 1)) return; + + uint32_t totalSize = 4 + payloadSize; // 2 bytes type + 2 bytes size + payload + uint32_t wp = g_evtBuf->write_pos.load(std::memory_order_relaxed); + uint32_t ringSize = sizeof(g_evtBuf->ring); + + uint8_t header[4]; + memcpy(header, &type, 2); + memcpy(header + 2, &payloadSize, 2); + + for (uint32_t i = 0; i < 4; i++) + g_evtBuf->ring[(wp + i) % ringSize] = header[i]; + const uint8_t* src = static_cast(payload); + for (uint32_t i = 0; i < payloadSize; i++) + g_evtBuf->ring[(wp + 4 + i) % ringSize] = src[i]; + + g_evtBuf->write_pos.store((wp + totalSize) % ringSize, std::memory_order_release); + g_evtBuf->event_count.fetch_add(1, std::memory_order_relaxed); +} + +// ====================================================================== +// Event stream hooks — Take_Damage_Outer + DeathHandler +// ====================================================================== + +// Take_Damage_Outer (RVA 0x38A350, CONFIRMED-RE) +// Ghidra sig: char Take_Damage_Outer(GameObj* obj, int damageType, byte applyDamage, +// float* damageParams, int sourceInfo, uint flags) +typedef char (*pfn_TakeDamageOuter)(void* obj, int damageType, uint8_t applyDamage, + float* damageParams, int sourceInfo, unsigned int flags); +static pfn_TakeDamageOuter real_TakeDamageOuter = nullptr; + +// DeathHandler (RVA 0x39BDB0, CONFIRMED-RE) +// Ghidra sig: void DeathHandler(GameObj* obj, int deathCause, GameObj* killer, +// void* deathEvent, int deathAnim, int ownerTransfer) +typedef void (*pfn_DeathHandler)(void* obj, int deathCause, void* killer, + void* deathEvent, int deathAnim, int ownerTransfer); +static pfn_DeathHandler real_DeathHandler = nullptr; + +#pragma pack(push, 1) +struct EvtHPChange { + uint32_t unit_id; // GameObj+0x50 + float old_hp; // GameObj+0x5C (before damage) + float damage; // from damageParams[0] + int damage_type; // damageType arg +}; + +struct EvtUnitDied { + uint32_t unit_id; // GameObj+0x50 + int death_cause; // deathCause arg +}; +#pragma pack(pop) + +// 2026-04-28 (iter 96): forward-declare the global-damage-multiplier +// reader. Definition lives near the SWFOC_SetDamageMultiplier section +// further down (where the lock and storage are defined together). +// Returns the current multiplier, or 1.0f if the lock isn't initialised yet. +static float ReadGlobalDamageMultiplier(); + +// 2026-05-08 (iter 285): forward declaration + atomic definitions for +// Tier 3 HUD counter machinery used inside Hook_DeathHandler (line ~206). +// The Lua getters + units-alive walker live in the SetDamageMultiplier/ +// SetFireRate neighborhood (line ~6800+); only the atomics + forward decl +// of FindLocalPlayerSlot need to be visible at line 206. +static int FindLocalPlayerSlot(); +static std::atomic g_localPlayerKills{0}; +static std::atomic g_localPlayerDeaths{0}; + +static char Hook_TakeDamageOuter(void* obj, int damageType, uint8_t applyDamage, + float* damageParams, int sourceInfo, unsigned int flags) { + // 2026-04-28 (iter 96): SWFOC_SetDamageMultiplier global-only LIVE + // wiring. iter 95 architectural finding: Take_Damage_Outer is THE + // chokepoint for hull/shield damage and damageParams[0] is the + // damage float value (consistent with the inner sub_1403A9E30 + // prototype param 7 finding). Apply the global multiplier under the + // existing lock before forwarding. Per-slot semantics deferred — + // attacker context isn't available at this layer (sourceInfo arg is + // a tag, not an attacker GameObj*). See the iter 95 comment block + // for SetDamageMultiplier below for the full architectural reasoning. + if (damageParams) { + const float mult = ReadGlobalDamageMultiplier(); + if (mult != 1.0f && mult >= 0.0f) { + damageParams[0] = damageParams[0] * mult; + } + } + + if (g_evtBuf && (g_evtBuf->flags.load(std::memory_order_acquire) & 1)) { + EvtHPChange evt; + evt.unit_id = *reinterpret_cast(reinterpret_cast(obj) + RVA::GameObj::ObjectID); + evt.old_hp = *reinterpret_cast(reinterpret_cast(obj) + RVA::GameObj::HP); + evt.damage = damageParams ? damageParams[0] : 0.0f; // Records POST-scale value. + evt.damage_type = damageType; + WriteEvent(EVT_HP_CHANGE, &evt, sizeof(evt)); + } + return real_TakeDamageOuter(obj, damageType, applyDamage, damageParams, sourceInfo, flags); +} + +static void Hook_DeathHandler(void* obj, int deathCause, void* killer, + void* deathEvent, int deathAnim, int ownerTransfer) { + if (g_evtBuf && (g_evtBuf->flags.load(std::memory_order_acquire) & 1)) { + EvtUnitDied evt; + evt.unit_id = *reinterpret_cast(reinterpret_cast(obj) + RVA::GameObj::ObjectID); + evt.death_cause = deathCause; + WriteEvent(EVT_UNIT_DIED, &evt, sizeof(evt)); + } + // 2026-05-08 (iter 285): Tier 3 HUD counter increments. Compare killer + // and victim owner-slots against the local player; bump atomic counters + // when the local player is involved. Non-fatal on null/garbage pointers + // (defensive: deathCause may correspond to environmental kill where + // killer is null). FindLocalPlayerSlot() returns -1 in galactic-mode + // transitions; both branches gate on >=0 so transitions are no-ops. + const int localSlot = FindLocalPlayerSlot(); + if (localSlot >= 0) { + if (killer) { + const auto killerSlot = *reinterpret_cast( + reinterpret_cast(killer) + RVA::GameObj::OwnerPlayerID); + if (static_cast(killerSlot) == localSlot) { + g_localPlayerKills.fetch_add(1, std::memory_order_relaxed); + } + } + if (obj) { + const auto victimSlot = *reinterpret_cast( + reinterpret_cast(obj) + RVA::GameObj::OwnerPlayerID); + if (static_cast(victimSlot) == localSlot) { + g_localPlayerDeaths.fetch_add(1, std::memory_order_relaxed); + } + } + } + real_DeathHandler(obj, deathCause, killer, deathEvent, deathAnim, ownerTransfer); +} + +static void Log(const char* fmt, ...) { + if (!g_log) return; + va_list args; + va_start(args, fmt); + vfprintf(g_log, fmt, args); + va_end(args); + fflush(g_log); +} + +template +static T Resolve(uintptr_t rva) { + return reinterpret_cast(g_base + rva); +} + +// ====================================================================== +// CRC32 (polynomial 0xEDB88320, table-driven, zlib-compatible) +// Used by SWFOC_DumpState to seal snapshot files. SAFE from any thread — +// no Lua calls, no heap allocation, table is lazily initialized once. +// ====================================================================== + +static uint32_t g_crc32Table[256]; +static bool g_crc32TableReady = false; + +static void Crc32_BuildTable() { + for (uint32_t i = 0; i < 256; i++) { + uint32_t c = i; + for (int k = 0; k < 8; k++) { + c = (c & 1) ? (0xEDB88320u ^ (c >> 1)) : (c >> 1); + } + g_crc32Table[i] = c; + } + g_crc32TableReady = true; +} + +static uint32_t Crc32_Update(uint32_t crc, const void* data, size_t len) { + if (!g_crc32TableReady) Crc32_BuildTable(); + const uint8_t* p = static_cast(data); + crc = crc ^ 0xFFFFFFFFu; + for (size_t i = 0; i < len; i++) { + crc = g_crc32Table[(crc ^ p[i]) & 0xFFu] ^ (crc >> 8); + } + return crc ^ 0xFFFFFFFFu; +} + +// ====================================================================== +// Shared memory initialization +// ====================================================================== + +static bool InitSharedMemory() { + // Command buffer + g_hCmdMap = CreateFileMappingA(INVALID_HANDLE_VALUE, nullptr, + PAGE_READWRITE, 0, sizeof(SharedCmdBuffer), SHMEM_CMD_NAME); + if (!g_hCmdMap) { + Log("[SHM] CreateFileMapping CMD failed: %lu\n", GetLastError()); + return false; + } + g_cmdBuf = (SharedCmdBuffer*)MapViewOfFile(g_hCmdMap, + FILE_MAP_ALL_ACCESS, 0, 0, sizeof(SharedCmdBuffer)); + if (!g_cmdBuf) { + Log("[SHM] MapViewOfFile CMD failed: %lu\n", GetLastError()); + CloseHandle(g_hCmdMap); g_hCmdMap = nullptr; + return false; + } + memset(g_cmdBuf, 0, sizeof(SharedCmdBuffer)); + Log("[SHM] Command buffer created: %s (%u bytes)\n", SHMEM_CMD_NAME, (uint32_t)sizeof(SharedCmdBuffer)); + + // Event buffer (created now, populated later in Wave 1D) + g_hEvtMap = CreateFileMappingA(INVALID_HANDLE_VALUE, nullptr, + PAGE_READWRITE, 0, sizeof(SharedEvtBuffer), SHMEM_EVT_NAME); + if (g_hEvtMap) { + g_evtBuf = (SharedEvtBuffer*)MapViewOfFile(g_hEvtMap, + FILE_MAP_ALL_ACCESS, 0, 0, sizeof(SharedEvtBuffer)); + if (g_evtBuf) { + memset(g_evtBuf, 0, sizeof(SharedEvtBuffer)); + Log("[SHM] Event buffer created: %s (%u bytes)\n", SHMEM_EVT_NAME, (uint32_t)sizeof(SharedEvtBuffer)); + } + } + return true; +} + +// Helpers using now-confirmed RVAs +static void PushBool(lua_State* L, int b) { + fn_pushboolean(L, b); +} + +static void PushNil(lua_State* L) { + fn_pushnil(L); +} + +// ====================================================================== +// lua_load string reader + DoString +// ====================================================================== + +struct StringReaderData { + const char* str; + size_t len; + bool done; +}; + +static const char* StringReader(lua_State* L, void* ud, size_t* sz) { + (void)L; + StringReaderData* rd = static_cast(ud); + if (rd->done) { *sz = 0; return nullptr; } + rd->done = true; + *sz = rd->len; + return rd->str; +} + +// Load + execute a Lua string. Returns 0 on success, error code otherwise. +// On success, pushes 1 return value (or nil if script returns nothing). +// On failure, pushes error message string onto the stack. +// Caller must pop the top value in both cases. +static int DoString(lua_State* L, const char* code, const char* chunkname = "=pipe") { + if (!fn_load || !fn_pcall) return -1; + StringReaderData rd; + rd.str = code; + rd.len = strlen(code); + rd.done = false; + int loadErr = fn_load(L, StringReader, &rd, chunkname); + if (loadErr != 0) { + // error string is on top of stack + return loadErr; + } + // chunk is on top — call it with 0 args, 1 result (capture return value) + int callErr = fn_pcall(L, 0, 1, 0); + return callErr; // top of stack has return value (success) or error string (failure) +} + +// ====================================================================== +// Named pipe listener thread +// ====================================================================== + +static DWORD WINAPI PipeThreadProc(LPVOID) { + Log("[Pipe] Listener thread started, pipe=%s\n", PIPE_NAME); + + while (!g_pipeShutdown) { + HANDLE hPipe = CreateNamedPipeA( + PIPE_NAME, + PIPE_ACCESS_DUPLEX, + PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_WAIT, + 1, // max instances + 512, // out buffer + PIPE_CMD_MAX, // in buffer + 1000, // default timeout ms + nullptr); + + if (hPipe == INVALID_HANDLE_VALUE) { + Log("[Pipe] CreateNamedPipe failed: %lu\n", GetLastError()); + InterlockedIncrement(&g_pipeErrorCount); + Sleep(1000); + continue; + } + + // Wait for client — blocks until connection or error + BOOL connected = ConnectNamedPipe(hPipe, nullptr) + ? TRUE : (GetLastError() == ERROR_PIPE_CONNECTED); + + if (!connected || g_pipeShutdown) { + if (!g_pipeShutdown) InterlockedIncrement(&g_pipeErrorCount); + CloseHandle(hPipe); + continue; + } + + Log("[Pipe] Client connected\n"); + + // Read command in one blocking ReadFile call. + // The client sends the entire command + null terminator as a single write. + // ReadFile blocks until data arrives or the client disconnects. + char buf[PIPE_CMD_MAX]; + DWORD totalRead = 0; + BOOL readOk = ReadFile(hPipe, buf, PIPE_CMD_MAX - 1, &totalRead, nullptr); + if (!readOk || totalRead == 0) { + Log("[Pipe] Read failed or empty (client disconnected without sending)\n"); + InterlockedIncrement(&g_pipeErrorCount); + DisconnectNamedPipe(hPipe); + CloseHandle(hPipe); + continue; + } + // SWFOC_DiagPipeStats: count every successful ReadFile as "received". + InterlockedIncrement(&g_pipeReceivedCount); + // Find null terminator in the received data + buf[totalRead] = '\0'; // safety null + size_t cmdLen = strnlen(buf, totalRead); + + if (totalRead == 0) { + Log("[Pipe] Empty command, ignoring\n"); + InterlockedIncrement(&g_pipeErrorCount); + const char* resp = "ERR: empty command\n"; + DWORD written; + WriteFile(hPipe, resp, (DWORD)strlen(resp), &written, nullptr); + FlushFileBuffers(hPipe); + DisconnectNamedPipe(hPipe); + CloseHandle(hPipe); + continue; + } + + Log("[Pipe] Received %lu bytes: %.64s%s\n", totalRead, buf, totalRead > 64 ? "..." : ""); + + // Queue the command for main-thread execution via luaD_call hook. + // NEVER execute Lua from the pipe thread — race condition causes heap corruption. + EnterCriticalSection(&g_pipeLock); + if (g_pipeCmdPending) { + LeaveCriticalSection(&g_pipeLock); + InterlockedIncrement(&g_pipeErrorCount); + const char* resp = "ERR: queue full, try again\n"; + DWORD written; + WriteFile(hPipe, resp, (DWORD)strlen(resp), &written, nullptr); + FlushFileBuffers(hPipe); + DisconnectNamedPipe(hPipe); + CloseHandle(hPipe); + continue; + } + memcpy(g_pipeCmd, buf, totalRead + 1); + g_pipeCmdPending = true; + g_pipeResultReady = false; + LeaveCriticalSection(&g_pipeLock); + + // Wait for main thread (luaD_call hook) to execute and produce result + for (int wait = 0; wait < 10000; wait += 5) { + Sleep(5); + EnterCriticalSection(&g_pipeLock); + bool ready = g_pipeResultReady; + LeaveCriticalSection(&g_pipeLock); + if (ready) break; + } + + EnterCriticalSection(&g_pipeLock); + const char* resp; + bool timedOut = false; + if (g_pipeResultReady) { + resp = g_pipeResult; + } else { + resp = "ERR: timeout (10s) - game may be paused or in menu\n"; + g_pipeCmdPending = false; + timedOut = true; + } + DWORD written; + BOOL wrote = WriteFile(hPipe, resp, (DWORD)strlen(resp), &written, nullptr); + g_pipeResultReady = false; + LeaveCriticalSection(&g_pipeLock); + // SWFOC_DiagPipeStats: successful reply => completed, otherwise error. + if (timedOut || !wrote) { + InterlockedIncrement(&g_pipeErrorCount); + } else { + InterlockedIncrement(&g_pipeCompletedCount); + } + + FlushFileBuffers(hPipe); + DisconnectNamedPipe(hPipe); + CloseHandle(hPipe); + Log("[Pipe] Client disconnected\n"); + } + + Log("[Pipe] Listener thread exiting\n"); + return 0; +} + +// Drain one pending pipe command on the calling thread's lua_State. +// Returns true if a command was executed. +static bool DrainPipeCommand(lua_State* L) { + EnterCriticalSection(&g_pipeLock); + if (!g_pipeCmdPending) { + LeaveCriticalSection(&g_pipeLock); + return false; + } + // Copy command out under lock + char cmd[PIPE_CMD_MAX]; + memcpy(cmd, g_pipeCmd, PIPE_CMD_MAX); + LeaveCriticalSection(&g_pipeLock); + + Log("[Pipe] Executing: %.64s%s\n", cmd, strlen(cmd) > 64 ? "..." : ""); + int savedTop = fn_gettop(L); // Stack guard (Fix #3) + int err = DoString(L, cmd, "=pipe"); + + EnterCriticalSection(&g_pipeLock); + if (err == 0) { + // DoString now returns 1 value on success — capture it + const char* retVal = fn_tostring(L, -1); + if (retVal && retVal[0]) { + snprintf(g_pipeResult, sizeof(g_pipeResult), "%s\n", retVal); + } else { + strcpy(g_pipeResult, "OK\n"); + } + Log("[Pipe] Execution OK: %.64s\n", g_pipeResult); + } else { + const char* errMsg = fn_tostring(L, -1); + if (!errMsg) errMsg = "unknown error"; + snprintf(g_pipeResult, sizeof(g_pipeResult), "ERR: %s\n", errMsg); + Log("[Pipe] Execution error: %s\n", errMsg); + } + fn_settop(L, savedTop); // Restore stack regardless (Fix #3) + g_pipeResultReady = true; + g_pipeCmdPending = false; + LeaveCriticalSection(&g_pipeLock); + return true; +} + +// ====================================================================== +// Player helpers (read-only, safe) +// ====================================================================== + +static uintptr_t GetPlayerObj(int slot) { + auto pa = *reinterpret_cast(g_base + RVA::PlayerArray_Global); + if (!pa) return 0; + return *reinterpret_cast(pa + slot * 8); +} + +static int GetPlayerCount() { + return *reinterpret_cast(g_base + RVA::PlayerCount_Global); +} + +static int FindLocalPlayerSlot() { + int count = GetPlayerCount(); + for (int i = 0; i < count; i++) { + auto p = GetPlayerObj(i); + if (p && *reinterpret_cast(p + RVA::PlayerObj::LocalPlayer) == 1) + return i; + } + return -1; +} + +static const char* GetFactionName(int slot) { + auto p = GetPlayerObj(slot); + if (!p) return "?"; + auto namePtr = *reinterpret_cast(p + RVA::PlayerObj::FactionName); + return namePtr ? namePtr : "?"; +} + +// ====================================================================== +// Custom Lua functions +// ====================================================================== + +// SWFOC_GetVersion() -> deployment-observable version string. +// Bumped 2026-04-10 so live-game testers can distinguish a freshly deployed +// DLL from a stale one at a glance. The string is intentionally long and +// includes the build date + helper count so any mismatch is obvious. +#ifndef SWFOC_BRIDGE_VERSION +#define SWFOC_BRIDGE_VERSION "SWFOC Lua Bridge v1.0.2 (2026-05-20, 174 live helpers / 18 P2-pending, snapshot v2)" +#endif +static int Lua_GetVersion(lua_State* L) { + fn_pushstring(L, SWFOC_BRIDGE_VERSION); + return 1; +} + +// SWFOC_GetBuildInfo() -> "