diff --git a/game/reconstructed/mechrecon.hpp b/game/reconstructed/mechrecon.hpp index e0a9563..30d4c33 100644 --- a/game/reconstructed/mechrecon.hpp +++ b/game/reconstructed/mechrecon.hpp @@ -46,18 +46,34 @@ struct Recon //===========================================================================// // Memory shim -- the decomp's "Memory::Allocate / Memory::Free" map onto the -// engine's per-object MemoryBlock allocators; for the offline reconstruction a -// plain heap allocation is behaviour-equivalent. +// engine's per-object MemoryBlock allocators. +// +// ZERO-FILLED ON PURPOSE (#67 root cause, 2026-07-27). "A plain heap +// allocation is behaviour-equivalent" turned out FALSE: the 1995 binary +// leaves fields uninitialized that its ctors never store (proven: the Torso +// ctor @004b6b0c stores neither targetTwist @0x218 nor twistAtUpdate @0x21C), +// and on the POD that was latent -- MemoryBlock arenas carve fresh OS-zeroed +// pages, one pool per type, low churn, so first allocations read as zero +// (the engine's own DEBUG_NEW_ON NaN-fill shows the developers knew the +// hazard). Our ::operator new recycles dirty heap: replicant torsos spawned +// with garbage twist targets, clamped to the limit -- rendered "twisted full +// right" to everyone (playtest night 4, three reporters). Zero-filling +// reproduces the pod's EFFECTIVE allocation semantics for the whole class of +// never-stored fields at once; the ctors stay byte-faithful to the binary. //===========================================================================// namespace Memory { - inline void *Allocate(size_t bytes) { return ::operator new(bytes); } - inline void *AllocateArray(size_t bytes){ return ::operator new(bytes); } + inline void *Allocate(size_t bytes) + { void *p = ::operator new(bytes); ::memset(p, 0, bytes); return p; } + inline void *AllocateArray(size_t bytes) + { void *p = ::operator new(bytes); ::memset(p, 0, bytes); return p; } inline void Free(void *where) { ::operator delete(where); } } -inline void *Alloc(size_t bytes) { return ::operator new(bytes); } -inline void *AllocateArray(size_t bytes){ return ::operator new(bytes); } +inline void *Alloc(size_t bytes) + { void *p = ::operator new(bytes); ::memset(p, 0, bytes); return p; } +inline void *AllocateArray(size_t bytes) + { void *p = ::operator new(bytes); ::memset(p, 0, bytes); return p; } //===========================================================================// // Ref-count / name helpers (decomp artifacts). diff --git a/scratchpad/rig_watch.ps1 b/scratchpad/rig_watch.ps1 new file mode 100644 index 0000000..c38c64f --- /dev/null +++ b/scratchpad/rig_watch.ps1 @@ -0,0 +1,49 @@ +# Watch-rig: pod B runs UNDER cdb with the torso write-watch script. +$repo = 'C:\git\bt411' +$content = Join-Path $repo 'content' +$scratch = 'C:\Users\EPILECTRIK\AppData\Local\Temp\claude\C--git-bt411\89ab96e9-6cf0-4555-939d-05bf3708745a\scratchpad' +$exe = Join-Path $repo 'build\Release\btl4.exe' +$cdb = 'C:\Program Files (x86)\Windows Kits\10\Debuggers\x86\cdb.exe' +if (-not (Test-Path $cdb)) { $cdb = 'C:\Program Files (x86)\Windows Kits\10\Debuggers\x64\cdb.exe' } +$relaylog = Join-Path $scratch 'watch_relay.log' +$pidfile = Join-Path $scratch 'watch_pids.txt' + +if (@(Get-NetTCPConnection -State Listen -LocalPort 1520 -ErrorAction SilentlyContinue).Count -gt 0) { + "ABORT: port 1520 in use"; exit 1 +} +Remove-Item $relaylog, (Join-Path $scratch 'cdb_torso.log') -ErrorAction SilentlyContinue +$pids = @() + +$relay = Start-Process -FilePath 'python' ` + -ArgumentList '-u', (Join-Path $repo 'tools\btconsole.py'), '--relay', '1520', + (Join-Path $content '_TWIST.EGG'), '--bind', '127.0.0.1', '--manual-launch' ` + -WorkingDirectory $content -PassThru ` + -RedirectStandardOutput $relaylog -RedirectStandardError (Join-Path $scratch 'watch.err') +$pids += $relay.Id +Start-Sleep -Seconds 3 + +$env:BT_START_INSIDE = '1' +$env:BT_DEV_GAUGES = '1' +$env:BT_RELAY = '127.0.0.1:1520' +$env:BT_MATCHLOG = '0' +$env:BT_LOG_APPEND = '1' +$env:BT_TORSO_LOG = '1' + +# pod A: normal (the master we ignore) +$env:BT_LOG = 'w_a.log'; $env:BT_SELF = '127.0.0.1:1502' +$a = Start-Process -FilePath $exe -ArgumentList '-egg','_TWIST.EGG','-net','1502' ` + -WorkingDirectory $content -PassThru +$pids += $a.Id +Start-Sleep -Seconds 2 + +# pod B: UNDER CDB with the write-watch (its replicant torso is the victim) +$env:BT_LOG = 'w_b.log'; $env:BT_SELF = '127.0.0.1:1602' +$b = Start-Process -FilePath $cdb ` + -ArgumentList '-logo', (Join-Path $scratch 'cdb_torso.log'), + '-cf', (Join-Path $scratch 'torso_watch.cdb'), + '-o', $exe, '-egg', '_TWIST.EGG', '-net', '1602' ` + -WorkingDirectory $content -PassThru +$pids += $b.Id + +Set-Content -Path $pidfile -Value $pids +"relay=$($relay.Id) podA=$($a.Id) cdb=$($b.Id)" diff --git a/scratchpad/torso_watch.cdb b/scratchpad/torso_watch.cdb new file mode 100644 index 0000000..f2d01f5 --- /dev/null +++ b/scratchpad/torso_watch.cdb @@ -0,0 +1,8 @@ +* torso_watch.cdb -- arm a write-watch on twistAtUpdate (+0x21C) of every +* Torso the moment it is constructed; print a stack per write and continue. +* The poisoned field's writer will appear as the first non-Torso stack. +.symfix+ C:\git\bt411\build\Release +.sympath+ C:\git\bt411\build\Release +bu btl4!Torso::Torso ".echo ==TORSO-CTOR==; r @ecx; ba w4 @ecx+0x21c \"kb 10; gc\"; gc" +sxn av +g