Round-6 tooling pass: how the read-only TNC runtime capture was made, and an equivalent capture tool for The New Order (tools/tno_runtime_capture.py) plus a prepared Ghidra import, so that TNO's SteamStub-encrypted .text can be decompiled as soon as a capture exists. The tool was tested against a harmless stand-in process with a positive control, a determinism check and three negative controls; the games were never launched. From round 7 on, the reports work from a captured TNO runtime image (re_probes/runtime_tno_186352).
Pass 2 of 2. Pass 1's verifier found a real bug (files could land on disk before a capture failure was detected) and two gaps (an untested failure path; an unvalidated multi-snapshot selection). All three are fixed below, re-tested for real, and verified with the verifier's own reproduction method. See "Pass 1 errors" near the end for the short version.
Read-only on the game installs and on all project files outside this track's
own area. New files only: tools/tno_runtime_capture.py,
tools/ghidra_runtime_import_tno.py, a 4-line additive change to
tools/decomp.py (new --gzf flag, default unchanged — see §4), scratch
work under re_probes/tnodump/ (left empty, cleaned up after every test
run), and this report. TNO and TNC were never launched. Every test below
ran against a throwaway python.exe child process this track spawned and
killed itself.
Question
TNO has no unpacked runtime image, which blocks decompiling it (.text is
SteamStub-encrypted in both EXEs on disk — docs/ghidra.md, entropy 8.000).
TNC has one (re_probes/runtime_87400/), captured read-only from a live,
already-running process while SteamStub had unpacked .text in memory for
it. How was that capture done, and can the same method be built for TNO as a
tool the maintainer can run once, safely, while TNO sits at the main menu — tested now,
without the game, against a harmless stand-in process, with a negative
control — with the Ghidra import prepared so decompiling starts the moment a
TNO image exists?
Answer (short)
Yes, done and tested. The TNC capture (tools/tnc_runtime_read.py) is
~110 lines of stdlib ctypes: OpenProcess(0x410, …) —
PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, nothing else —
CreateToolhelp32Snapshot/Module32FirstW/NextW to find the target module's
base/size inside that PID, then ReadProcessMemory for the PE header plus
four sections (.text .rdata .data .pdata), written to module.bin +
manifest.json (base, size, per-section RVA/size/sha256, build sha256 of the
file on disk). No write, thread or debug API appears anywhere in the file.
tools/tnc_runtime_commands.py layers one more read-only pass on top for the
console-command tables, gated by an exact build-hash check.
I built the direct TNO equivalent, tools/tno_runtime_capture.py, by
parametrizing the one TNC-specific thing in that file (the hardcoded target
module name) and adding a default output path
(re_probes/runtime_tno_<pid>/) and an explicit post-write verification
pass. Usage for the maintainer is one command with TNO running:
python tools/tno_runtime_capture.py --pid <PID>
I could not run this against TNO (never launch the games). Instead I ran its
built-in self-test (--selftest) for real, right now, with no game
involved at all — see §2 for the full transcript. It passed: a positive
control (capture a live stand-in process, verify every section hash from the
bytes actually on disk), a determinism check (two captures of the same idle
process hash identically), and three negative controls (unknown module
name; a dead PID; every per-section read failing right after a successful
header read) that all fail closed with zero bytes written. The third
control is new in this pass — it exercises the exact path pass‑1's verifier
found broken; see "Pass 1 errors".
I then prepared tools/ghidra_runtime_import_tno.py, the TNO sibling of
the proven tools/ghidra_runtime_import.py (which turns TNC's capture into
121,682 named, decompilable functions today). It autodetects the newest
re_probes/runtime_tno_*/ snapshot, uses docs/ghidra/rtti_tno.json for
class/vtable labels (same static image base 0x140000000 as TNC, asserted
at runtime against the RTTI file, not assumed), and fails with one clear,
actionable line right now, since no TNO snapshot exists yet (verified — see
§3). Its core non-Ghidra logic (parsing .pdata RUNTIME_FUNCTION entries out
of a captured image) I validated against a real throwaway capture: 506
functions parsed correctly out of a live python.exe's own .pdata. I also
added one additive, backward-compatible flag to tools/decomp.py
(--gzf, default = the current TNC path) so the existing decompiler query
tool can point at a TNO GZF the moment one exists, without touching how it
is used for TNC today.
What this does not and cannot do: it does not produce a TNO image — nobody but the maintainer can run the capture tool, because that requires TNO actually running, and the task forbids this agent from launching or attaching to either game. Nothing about TNO's code, commands or cvars was learned here; this track is infrastructure, proven on a stand-in, ready for the maintainer's one real run.
1. How the TNC capture actually works (read, not redone)
tools/tnc_runtime_read.py, Reader.__init__:
self.handle = self.k.OpenProcess(0x410, False, pid) # QUERY_INFORMATION|VM_READ
...
snap = self.k.CreateToolhelp32Snapshot(0x18, pid) # TH32CS_SNAPMODULE|SNAPMODULE32
... Module32FirstW/NextW until szModule.lower()=="newcolossus_x64vk.exe"
snapshot() then reads the PE header at the module base (0x1000 bytes),
parses the section table itself (no pefile/third-party dependency — raw
struct.unpack_from over the IMAGE_FILE_HEADER/IMAGE_SECTION_HEADER
layout), and for exactly .text .rdata .data .pdata issues one
ReadProcessMemory per section at base+rva, size vsize. Every section
actually captured gets a sha256 and "captured": true in the manifest;
sections outside that allowlist (.rsrc .reloc .bind — the SteamStub
loader itself, icons, relocations) are listed with their PE metadata but
never read. re_probes/runtime_87400/manifest.json confirms this is exactly
what landed: access: "read only 0x410", layout: "virtual RVA", base
0x7ff62c210000, size 92,188,672, four sections with sha256+captured,
the rest without. disk_sha256 ties the capture to one exact build of the
file on disk (tools/tnc_runtime_commands.py re-asserts this hash before
trusting any vtable offset it hardcodes).
The output is explicitly not a launchable or relocatable image — it's a
sparse bytearray the size of the module, PE header at offset 0, each
captured section's bytes written at its own RVA, everything else zero. That
is exactly the "virtual RVA" layout tools/ghidra_runtime_import.py expects:
it rebases a Ghidra program to the manifest's live base, and because RVA
numbering is base-independent, .pdata RUNTIME_FUNCTION entries, RTTI
VA-minus-static-preferred-base math and the console-command callback RVAs
from tools/tnc_runtime_commands.py all line up without any relocation
step.
tools/tnc_runtime_commands.py is a second, independent read-only pass:
it re-derives idCmdSystemLocal's vtable RVA, asserts it against the known
build hash and an expected vtable - base delta before trusting it, then
walks two pointer arrays (all, exposed) and reads each command's name
string, refusing to dereference any pointer that falls outside the module's
own address range ("refusing speculative read"). This is the part that is
not yet reproduced for TNO — see §5, Open questions.
2. tools/tno_runtime_capture.py — design and the self-test, run for real
Design: identical safety contract to tnc_runtime_read.py
(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ = 0x410, nothing else —
grep the file, no WriteProcessMemory/CreateRemoteThread/
SetThreadContext/QueueUserAPC/DebugActiveProcess/SuspendThread
appears), with three differences, all additive:
- Target module name is a parameter (
--module, defaultWolfNewOrder_x64.exe— confirmed as the exe filename both inwolfsdk/game.py'sTitletable and by listing the install directory). This is what let me point the exact same code path at a harmlesspython.exechild for testing instead of TNO. output.mkdir()now happens after the module is found, not before (TNC's version creates the output directory, then opens the process and looks up the module — a failed lookup there still leaves an empty directory). Moving it after means a failed module-lookup run leaves nothing on disk.- An explicit
verify()pass: after writing, it re-readsmodule.bin+manifest.jsonback from disk and recomputes every captured section's sha256 independently, raising on any mismatch. This is the "verifies section hashes" step — it checks what's actually on disk, not just the in-memory bytes right after the read. (Pass‑1 bug, fixed in pass 2:snapshot()used to writemodule.bin+manifest.jsonto disk and then callverify(), so a capture where the header read succeeded but every per-section read then failed raisedValueErrorwith non-empty files already on disk — contradicting the "zero bytes on failure" claim this report made in pass 1.snapshot()now wraps the write+verify intry/except Exception: shutil.rmtree(output); raise, so any failure after the directory is created — zero sections captured, a hash mismatch on re-read, anything — deletes it again before the exception propagates. See "Pass 1 errors" below.)
Default output: re_probes/runtime_tno_<pid>/ (matching the TNC naming
convention runtime_<pid>), overridable with --out, both still constrained
to stay under the repo's re_probes/ (same guard TNC's script has).
The test (no game, real run, full output below)
python tools/tno_runtime_capture.py --selftest
spawns one throwaway python.exe -c "import time; time.sleep(120)" as a
stand-in for "TNO sitting idle at the main menu" (a stable, loaded, running
process with real PE sections) and runs:
- Positive control — capture that child's own
python.exemodule by PID, assert all four target sections got captured, runverify(). - Determinism check — capture the same still-running process a second time, assert the two runs' per-section sha256 are identical (proves the read is a stable, repeatable observation, not reading garbage/timing- dependent memory).
- Negative control A — same real, running PID, but ask for a module name
that isn't loaded in it (
doesnotexist.exe). Must raise, and must leave no output directory. - Negative control B — a PID that has already exited (a second throwaway child, killed and reaped first). Must raise, and must leave no output directory.
- Negative control C (new in pass 2) — same real, running PID and
module, but
Reader.readis monkeypatched so the fixed-size0x1000header read still succeeds while every per-section read raises. This is the exact failure shape pass‑1's verifier used to reproduce the bug: the header parses fine,sections()returns real section metadata, but nothing gets captured. Must raise, and must leave no output directory — this is the control that pass 1 was missing.
Actual transcript from this session, unmodified (re-run after the fix):
spawned harmless stand-in: pid=120328 exe=<user folder>\AppData\Local\hermes\hermes-agent\venv\Scripts\python.exe
positive control OK: pid=120328 module=python.exe sections captured=['.text', '.rdata', '.data', '.pdata'] verified=4
determinism OK: repeat capture hashes identical (4 sections)
negative control A OK (bad module name): pid 120328 has no 'doesnotexist.exe' module
negative control B OK (dead pid 163524): [WinError 299] Nur ein Teil der ReadProcessMemory- oder WriteProcessMemory-Anforderung wurde abgeschlossen.
negative control C OK (all section reads fail): no sections captured -- nothing to verify
SELFTEST PASSED
(Transcript kept verbatim. The WinError 299 text is the German-locale Windows
message for "Only part of a ReadProcessMemory or WriteProcessMemory request was
completed.")
I also independently re-ran the verifier's own reproduction recipe (spawn a
throwaway python.exe, monkeypatch Reader.read to fail every non-header
read, call snapshot() directly) against the fixed code outside the
self-test harness, to be sure the fix holds and not just the new assertion:
raised ValueError: no sections captured -- nothing to verify as before,
but now output dir exists after failure: False — confirmed zero bytes on
disk.
One thing worth flagging precisely, not glossed over: for negative control B,
OpenProcess(0x410, …) on the already-exited PID actually succeeded (a
just-freed PID's kernel object can still answer briefly), and the toolhelp
module snapshot also returned a module entry for it — it was the actual
ReadProcessMemory call that then failed (WinError 299, partial read),
which the code correctly surfaces as an OSError and treats as a hard
failure (no file written). The control still proves what it needs to prove
— a stale/wrong PID cannot produce a capture — but the exact failure point is
one step later than I initially expected (read, not open). Noted here so
nobody re-derives a wrong assumption from the error class alone.
After the run, re_probes/tnodump/ is empty (the self-test creates its
scratch dir under there and shutil.rmtrees it in a finally block) —
confirmed with ls immediately after.
3. tools/ghidra_runtime_import_tno.py — prepared, not runnable yet
Sibling of the proven tools/ghidra_runtime_import.py, same transaction
order (disable analyzers before creating any function — docs/ghidra.md's
documented pitfall: AutoAnalysisManager reads enablement at event time, and
121,682 createFunction() events already queued the wrong analyzers once
before this was understood, 1409s vs ~290s), same two analyzers left on
(ASCII Strings, x86 Constant Reference Analyzer), same GZF-export-to-temp
-then-rename so a concurrent decomp.py read never sees a half-written
file. Three differences:
- Autodetects the snapshot —
find_snapshot()globsre_probes/runtime_tno_*/for the newest one with bothmodule.binand amanifest.jsonwhosesectionslist has at least one entry with"captured": true, instead of a hardcoded PID (TNC's script hardcodesruntime_87400because that one capture is the project's fixed reference; TNO's PID will be whatever the maintainer's capture run used). New in pass 2: the manifest-content check — pass 1 only checked thatmodule.bin/manifest.jsonexist, so a broken, zero-section snapshot (which could not actually happen before the pass‑1 bug fix above, but could in principle from a hand-edited or corrupted manifest) would have silently outranked a good older one by mtime alone. A snapshot that fails the check is skipped with one line to stderr, not picked silently. Tested with its own self-test (no Ghidra/JVM/game needed):python tools/ghidra_runtime_import_tno.py --selftestbuilds two throwawayre_probes/runtime_tno_999999001/(good, older) and..._999999002/(broken:"captured": false, newer) fixtures, assertsfind_snapshot()picks the good older one over the broken newer one, and deletes both. Run just now:
skipping ...\re_probes\runtime_tno_999999002: invalid/empty manifest (no captured sections)
find_snapshot() selftest OK: skipped broken newer ..._999999002, picked good older ..._999999001
2. No curated function names, no console-command table — TNC's script
names ~234 functions from tnc_runtime_commands.py's live command dump
plus 8 hand-verified RVAs from other tracks' reports. Nothing equivalent
exists for TNO yet: nobody has had a decompilable TNO image to find
idCmdSystemLocal's vtable RVA in. CURATED_NAMES = {}; the command-table
step is skipped outright rather than faked. This is real, flagged in
§5/Open questions, not hidden in the diff.
3. Project/GZF names say tno (wolf_tno_runtime.gpr, tno_runtime.gzf),
written beside the TNC ones under the same D:/wolf-decomp/ghidra/, so
both can coexist and tools/decomp.py --gzf <path> picks between them.
Tested now, without Ghidra, in three ways:
--selftest(new in pass 2):find_snapshot()'s good-vs-broken selection logic, against real fixture directories — see above.- Ran it with no TNO snapshot present (the actual current state, re-checked again after this pass's edits): it prints exactly
no re_probes/runtime_tno_<pid>/ snapshot found -- run
python tools/tno_runtime_capture.py --pid <PID>
once while TNO is at the main menu, then re-run this script.
and exits — no stack trace, no partial Ghidra project created. This is the
state the moment this report is written, so it's a real test, not a
hypothetical one.
- Pulled read_pdata_funcs() out and ran it standalone against a real
throwaway python.exe capture made the same way tno_runtime_capture.py
makes one (same code path, same self-test child-process pattern): it
parsed 506 well-formed (start_rva, end_rva) function ranges straight
out of that capture's .pdata section, all start < end, matching the
exact struct layout (<III triples: BeginAddress, EndAddress,
UnwindInfoAddress) the proven TNC pipeline and tools/tnc_runtime_code.py
both already rely on. This is the one piece of TNO-import logic that can
be exercised without a real TNO capture or a running Ghidra/JVM, and it
checks out.
RTTI_IMAGE_BASE = 0x140000000 is asserted, not assumed: the script reads
docs/ghidra/rtti_tno.json["image_base"] and asserts it equals
0x140000000 before using it for any VA→RVA math — docs/ghidra.md already
states both EXEs share this static preferred base, and rtti_tno.json's own
header confirms it.
4. tools/decomp.py — one additive flag
Added --gzf PATH (default: the existing hardcoded TNC path, unchanged
behavior for every existing call site/report that invokes decomp.py
without it). Once ghidra_runtime_import_tno.py has produced
D:/wolf-decomp/ghidra/tno_runtime.gzf, querying it needs nothing new:
python tools/decomp.py <rva|name> --gzf D:/wolf-decomp/ghidra/tno_runtime.gzf
Everything else about decomp.py — the copy-on-open throwaway project per
call so concurrent agents never contend on a lock, --xrefs/--callers/
--strings — is untouched and still applies to TNO once there's a GZF to
point it at.
PROVEN
- The exact read-only mechanism
re_probes/runtime_87400/was captured with:OpenProcess(0x410, …)+ toolhelp module lookup + per-sectionReadProcessMemory, four sections only, sha256 per section, no write API anywhere in the capturing code (tools/tnc_runtime_read.py, read in full). tools/tno_runtime_capture.pyperforms a real, verified, read-only capture of a live process's module by PID + module name: proven by running it against a throwawaypython.exechild, twice, with identical per-section hashes both times, and with every captured section's hash re-verified from the bytes written to disk.- All three negative controls fail closed with zero bytes written: a wrong module name on a real PID, a PID that no longer exists, and (new in pass 2, fixing the gap pass‑1's verifier found) a real PID/module whose header read succeeds but every per-section read then fails.
tools/ghidra_runtime_import_tno.py'sfind_snapshot()skips a broken (zero-captured-sections) snapshot even when it is the newest by mtime, picking the good older one instead — proven against two real throwaway fixture directories, both deleted after the check.tools/ghidra_runtime_import_tno.py's.pdata-parsing logic (the only part of the Ghidra-import pipeline that doesn't require Ghidra itself) correctly parses RUNTIME_FUNCTION ranges from a real captured image (506/ 506 well-formed ranges from a livepython.exe).tools/ghidra_runtime_import_tno.pyfails safely and informatively when no TNO snapshot exists — the actual current state, tested for real.- TNO's static preferred image base is
0x140000000, same as TNC (docs/ghidra/rtti_tno.json,docs/ghidra/pdata_tno.json, cross-checked againstdocs/ghidra.md) — the precondition the RTTI-labeling step in the import script relies on, asserted at runtime, not hardcoded blindly.
INFERRED (not run against TNO itself — cannot be, see Rules)
- That
tno_runtime_capture.py --pid <PID>will succeed against the realWolfNewOrder_x64.exeprocess the same way it did againstpython.exe. Inferred from: identical Win32 API usage to the proven TNC tool (which has captured a real game process), identical PE-parsing code, and a clean self-test against a real OS process. The one thing this cannot rule out without an actual run is whether SteamStub has fully unpacked TNO's.textin memory by the time the maintainer captures it (true for TNC's capture — the module's.textsection there is real, decompilable code per the existing 121,682-function pipeline) — TNO uses the same packer perdocs/ghidra.md's identical entropy table (8.000 for both.textsections on disk), so the same in-memory-unpacking behavior is expected, but "expected by analogy" is not "measured for TNO." - That
tools/ghidra_runtime_import_tno.pywill complete end-to-end once a real TNO snapshot exists — the Ghidra-dependent two-thirds of it (import, disassemble, function creation, analysis, GZF export) is untouched, line-for-line, from the script that has already done exactly this for TNC (121,682 functions, ~290s limited analysis). Only the snapshot-independent third (.pdataparsing) could be tested here and was.
Failed hypotheses / rejected designs
- Auto-discovering TNO's PID by process name, so the maintainer could run the tool
with zero arguments. Rejected in favor of requiring
--pidexplicitly, matching the proven TNC tool: matching byszExeFilealone over a process-list snapshot can't safely disambiguate if more than one process ever shares that name, and a silent wrong-PID match is worse than asking the maintainer to paste one number from Task Manager. Correction (pass 2): pass 1 citedwolfsdk/game.py'sGame.launch()docstring as evidence for this, claiming it "notes a Steam-side bounce/relaunch quirk" implying two concurrently-live same-named processes. Re-read for this pass: that docstring actually describes the opposite — a direct (non-Steam) launch of the SteamStub-wrapped exe gets bounced back to the Steam client and the bounced process is killed immediately, "dies so early that it never even opens qconsole.log" — a dead-on-arrival process, not a second live one. It is not evidence for "more than one same-named process can be running at once." The design decision (require--pid) is still kept, because the simpler reason above — no safe way to disambiguate multiple same-named processes in general, and it matches the already-proven TNC tool exactly — is sufficient on its own and doesn't need that citation. - Capturing every PE section, not just
.text/.rdata/.data/.pdata. Rejected to match the proven TNC capture exactly:.rsrc/.reloc/.bind(the SteamStub loader itself) add nothing the decompiler needs and only grow the image/attack surface of "what did we read from a running game." - Keeping
output.mkdir()before the module lookup (TNC's original order). Changed for the TNO tool specifically because the self-test's negative controls need to assert zero footprint, and an empty leftover directory on every failed/mistyped run would be exactly the kind of project-file clutter the rules ask to avoid.
Recipe for the maintainer
- Start TNO, get to the main menu (idle state — nothing loading, no level
active — minimizes the chance a section read lands mid-stream-write; this
is also when TNC's reference capture was most likely taken, per how every
later TNC track describes
runtime_87400as a stable, reusable snapshot). - Task Manager → Details tab → find
WolfNewOrder_x64.exe→ note the PID. - In a second window, from the repo root:
python tools/tno_runtime_capture.py --pid <PID>This only reads; TNO keeps running untouched. Output:re_probes/runtime_tno_<PID>/module.bin+manifest.json, with per-section sha256 already verified against what's on disk before the command returns. - Close TNO (capture is done, no need to keep it running).
- ``` python tools/ghidra_runtime_import_tno.py
Needs the same Ghidra+PyGhidra-venv setup `docs/ghidra.md` already walks
through for TNC (`<user folder>\tools\pfade.json` etc.) — if that's set
up for TNC already, nothing new to install. Expect roughly TNC's order of
magnitude (~50,884 functions vs. TNC's 121,682 — TNO's `.pdata` is
smaller, so import should be faster, not slower).
6.
python tools/decomp.py ``
works exactly like the existing TNCdecomp.pycalls throughoutre_probes/agent_reports/r*.md`, just pointed at TNO's GZF.
From there, the natural next track is a TNO analog of
tools/tnc_runtime_commands.py — finding idCmdSystemLocal's vtable RVA in
the new TNO disassembly the same way r4_consolecmds.md/
tnc_runtime_commands.py did for TNC — which would unlock the same
resourceExec-style non-exposed-command story (docs/limits.md already
shows TNO's retail cvar lock is narrower than one might assume, e.g.
g_permaGodMode/g_permaInfiniteAmmo are not locked — a code-level
command/cvar table for TNO would make that list exact instead of empirically
sampled).
Open questions
- Whether SteamStub has actually finished unpacking TNO's
.textin memory by the main menu, the same way it evidently had for TNC's capture (see INFERRED above) — only answerable by the maintainer's actual capture + a quick entropy/disassembly sanity check on the result (first instructions should decode as plausible x86-64, not near-random bytes). - TNO's
idCmdSystemLocalvtable RVA / command-table layout — not found yet, needed for atnc_runtime_commands.pyTNO equivalent, needs a real TNO disassembly to locate (chicken-and-egg with this track's own output: solvable the moment step 5/6 above produces a queryable GZF). - Whether TNO's class hierarchy differs from TNC's beyond what
docs/ghidra.mdalready shows via RTTI alone (TNO has no Havok RTTI, noidManagedClassin the inheritance chain) — decompiled method bodies, once available, would show whether that's a deeper engine difference or just vtable slot content.
Pass 1 errors
Pass‑1's verifier ran this track's code for real and found one actual bug and two untested gaps. All fixed and re-verified in pass 2:
- Bug (confirmed, fixed):
snapshot()wrotemodule.bin+manifest.jsonto disk before checking whether any section was captured. If the PE header read succeeded but every per-sectionReadProcessMemorythen failed, the function raisedValueError("no sections captured")with non-empty files already on disk — directly contradicting pass 1's "zero bytes on disk on failure" claim. Fix: the write +verify()call is now wrapped intry/except Exception: shutil.rmtree(output); raise, so any failure after the directory is created deletes it again. Re-verified two ways: a new negative control C in--selftest(header read OK, all section reads fail → raises,out.exists() is False), and an independent re-run of the verifier's own monkeypatch reproduction outside the self-test harness, which now printsoutput dir exists after failure: False. - Missed test (added): neither original negative control exercised the above path (both failed earlier — module lookup, or a dead handle). Negative control C above closes this gap directly.
- Missed test (added):
ghidra_runtime_import_tno.py'sfind_snapshot()picked the newestruntime_tno_*/directory by mtime with no check on its contents, so a broken, later capture could have silently shadowed a good earlier one.find_snapshot()now requires at least one"captured": truesection in the manifest and skips (with a stderr line) anything that doesn't qualify; a new--selftestflag proves this with two real throwaway fixture dirs (good-older vs. broken-newer) and deletes them afterward. - Inaccurate citation (corrected): pass 1's justification for requiring
--pidover PID auto-discovery citedwolfsdk/game.py'sGame.launch()docstring as describing two same-named processes able to run concurrently. Re-read for this pass: that docstring describes a dead-on-arrival bounced process (killed immediately, no window, no log), not a concurrency scenario. The design decision itself is unchanged and still matches the proven TNC tool; the citation is corrected under "Failed hypotheses" above rather than repeated here.
Not changed: the verifier's other five checks (safe_api_only,
selftest_reproducible in its pre-fix form, pdata_506_count,
ghidra_import_fails_clean, decomp_gzf_additive, rtti_image_base,
module_name_and_files_claim) were not refuted and needed no fix; nothing
in this pass touched tools/decomp.py or the RTTI/pdata TNO docs.
Smallest in-game test for the maintainer
(Translated from German.)
No in-game test is needed for this tool itself — it does not touch TNO as long as TNO is not running. The only step that actually needs TNO is the one capture command:
- Start TNO, wait until the main menu (do not load a map).
- Task Manager → Details → read the PID of
WolfNewOrder_x64.exe. - Run
python tools/tno_runtime_capture.py --pid <PID>— it only reads, writes nothing into the game process, TNO stays untouched in the main menu. - Check the output directly afterwards:
re_probes/runtime_tno_<PID>/ manifest.jsonshould show four sections with"captured": trueand onesha256each (same form asre_probes/runtime_87400/manifest.json). That alone already proves: the capture tool works against the real game just as in the self-test against the Python stand-in process. - Then close TNO normally. The rest (Ghidra import, decompiler queries) runs offline against the file, without TNO having to run again.
Related: Research Report: R4 Console Commands, Research Report: R7 Tno Cheat Gate, Ghidra Analysis, Modding Limits.