Added arrow_sprites/arrow_gfxdirs null/ptr pattern and raw hex dump
to snapshot output. Multi-frame comparison (frames 1, 1350, 1351)
confirms:
- Arrow arrays are IDENTICAL between constructors (no diff)
- Only 2 DDGame field diffs exist, both static from construction:
1. landscape_property: misclassified pointer (heap addr as u32)
2. gfx_color_table: [1..9] vs [0xFFFFFFFF x 9]
- All gameplay state identical: entities, positions, health, ammo, RNG
- Diffs are the same at frames 1, 1350, and 1351 (no drift over time)
Root cause remains: headless LoadSprites fallback in create_ddgame
doesn't correctly populate GfxDir colour data.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
debug: narrow desync to GfxDir colour loading via snapshot diffβ
Snapshot diff at frame 1350 shows only 2 differences between original
and Rust constructors:
1. landscape_property (DDGame+0x468) β different values
2. gfx_color_table (DDGame+0x730C) β [1,2,3,4,5,6,8,7,9] vs [0xFF x 9]
Both trace to the headless LoadSprites fallback in create_ddgame.
Patching only the color table doesn't fix the desync β landscape_property
(computed from PCLandscape vtable getter) is also affected.
Root cause: wa_load_sprites in the gfx_mode==0 path doesn't correctly
populate GfxDir colour data, which affects both the colour table AND
landscape property computation downstream.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
New `snapshot` command captures canonicalized game state for diffing:
- DDGame: frame counter, RNG, game speed, camera, team arena, ammo
- Teams: name, alliance, worm count, per-worm state/hp/name
- Entities: BFS walk of task tree, rich dump for CTaskWorm/CTaskMissile,
raw hex with pointer canonicalization for unknown entity types
Snapshot trait implemented for DDGame, WormEntry, CTaskWorm, CTaskMissile.
Unknown regions dumped as raw hex with [ptr---] replacing heap pointers.
Usage: openwa-debug snapshot > /tmp/state.txt
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
feat: add Snapshot trait for canonicalized game state dumpsβ
Defines the core trait in openwa-core::snapshot with helpers:
- write_indent, fmt_ptr, fmt_ptr32 for formatting
- write_raw_region for hex-dumping unknown struct regions
with automatic pointer canonicalization
Trait impls for DDGame, WormEntry, CTaskWorm, CTaskMissile
and the capture/CLI infrastructure are next.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
debug: add OPENWA_WATCH_FRAME for runtime hardware watchpointsβ
Arms DR0 on DDGame+0x50 at the specified frame via the existing VEH
watchpoint infrastructure. Set OPENWA_WATCH_FRAME=0 to arm from the
first frame.
Key finding: DDGame+0x50 is a sprite pointer at frame 1350, NOT a
small integer. The earlier raw-dump comparison was flawed (cross-
process pointer values compared without ASLR normalization). The real
desync is in sub-object content, not in DDGame field values.
Also adds shotgun replay (PASSES with both constructors, confirming
the desync is longbow-specific, not arrow-handler-specific).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
docs: add frame debugging commands to debug-cli skillβ
New debug CLI commands:
suspend β pause game at next frame boundary
resume β unpause game
step [N] β advance N frames then pause
frame β query current frame and pause state
break <N> β set one-shot frame breakpoint
Architecture: game thread cooperatively blocks on a Windows
manual-reset event in the TurnManager_ProcessFrame hook. Debug
server (separate TCP thread) controls the event.
Set OPENWA_BREAK_FRAME=1350 env var to auto-pause at a frame,
enabling headless replay debugging at exact desync points.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
debug: add OPENWA_USE_ORIG_CTOR env var to switch DDGame constructorsβ
Set OPENWA_USE_ORIG_CTOR=1 to use the original WA DDGame constructor
instead of our Rust replacement. Useful for A/B testing: longbow replay
passes with original, fails with ours, confirming the desync is in
create_ddgame.
The color table (DDGame+0x730C) difference is rendering-only and likely
not the cause of the simulation desync. The real issue is somewhere
else in the ~500-line create_ddgame function.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
fix: add missing LoadSprites fallback in DDGame constructor for gfx_mode=0β
The original DDGame constructor calls GfxHandler__LoadSprites (0x570B50)
when gfx_mode=0 (headless) or when GfxResource__Create fails. Our Rust
replacement skipped this call entirely, leaving DDGame+0x730C (color
table) unpopulated. This caused checksum mismatches in the longbow
replay.
Added the missing call with correct parameters (primary_gfx_dir as 4th
param for gfx_mode=0, 0 for resource creation failure). The color table
is still not fully matching vanilla (0xFFFFFFFF vs 1-9) β further
investigation needed in LoadSprites or CTaskGameState initialization.
Also: cleaned up bisect debug code, removed unused call_original
bridge functions.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Recorded from unmodded WA 3.8.1. Exercises Longbow (subtype_38=4)
and Dynamite. Expected log from vanilla game serves as ground truth.
Note: currently shows checksum mismatches at frames 1350/2186/2249/2775
due to a pre-existing bug in one of the other hooks (proven by
disabling FireWeapon hook entirely β mismatches persist). Needs
bisection to identify the offending hook.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
FireWeapon subtype_38=4 handler (0x51ED90) is thiscall (ECX=worm),
not stdcall. The wrong convention passed garbage in ECX, causing a
NULL dereference at DDGame+0x72A4 when firing Longbow or Shotgun.
Confirmed via crash analysis: EAX=0 at MOV ECX,[EAX+0x72A4] in
FireWeapon__Shotgun, and Ghidra disassembly showing MOV ESI,ECX
as the first instruction (thiscall pattern).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
feat: map WeaponEntry and WeaponFireParams fields from live memoryβ
Documents all commands, address syntax, pointer chain semantics,
key address chains (DDGame via g_GameSession->0xA0->0x488), and
common patterns for struct inspection and data dumping.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
feat: add absolute addresses, offsets, and pointer chains to debug CLIβ
Replace inline pointer classification logic in dump_region with calls to
the shared classify_pointer function. Remove duplicated IsBadReadPtr
extern and can_read helper (now live in openwa_core::mem).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
feat: add openwa-debug-cli binary for debug server accessβ
Add SharedDataTable::lookup() β pure Rust equivalent of FUN_004FDF90.
Looks up an entity by (key_esi, key_edi) in the 256-bucket hash table.
Port two weapon fire handlers using the new lookup:
- Napalm Strike (subtype 13): lookup CTaskTeam, send message 0x2B
- Select Worm (subtype 21): lookup CTaskTeam, send message 0x5D
with buf = [8, team_index]
Both share the fire_send_team_message() helper which handles the
SharedData lookup + HandleMessage vtable dispatch pattern.
Verified: headless PASS (byte-identical).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
TCP server on 127.0.0.1:19840 for live memory inspection. Spawns when
OPENWA_DEBUG_SERVER=1. Handles ping, help, and read (Ghidra VA) requests
with automatic ASLR rebasing and pointer classification.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
feat: add mem module with pointer classification to openwa-coreβ
Add DATA_END constant and mem module with can_read, classify_pointer,
and classify_region functions extracted from the validation dump logic.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
feat: add openwa-debug-proto crate with wire protocol typesβ
Add from_ptr(*mut TeamArenaState) alongside the existing from_raw(u32)
used by trampolines. fire_skip_go now constructs TeamArenaRef directly
from the typed DDGame.team_arena field pointer β no u32 cast.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
fix: use *mut u8 in TeamArenaRef, add team_header_mutβ
TeamArenaRef.base was *const u8 but team_header_mut() cast it to
*mut, which is unsound. Change base to *mut u8 so all mutable
access goes through legitimate mutable pointers.
blocks() now returns *mut TeamBlock. team_header_mut() no longer
needs a const-to-mut cast.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
refactor: name TeamHeader.turn_action_flags, clean up fire_skip_goβ
Identify TeamHeader._unknown_7c as turn_action_flags (u32 bitfield).
Skip Go toggles a bit here to mark the team's turn as skipped.
In game_version > 0x1C the bit toggles; in older versions it only sets.
fire_skip_go now accesses the field via TeamArenaRef::team_header()
instead of raw byte slicing into team_blocks_region.
Verified: headless PASS (byte-identical).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add a menu bar to the debug UI toolbar with a Cheats dropdown.
"Unlock all weapons" sets ammo to -1 (unlimited) and delays to 0
for all 6 alliances across all 71 weapons, using the new typed
WeaponSlots/TeamWeaponSlots structs.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace the opaque _unknown_4620 blob with a proper team_arena field
at DDGame+0x4628. The previously separate init_field_64d8 and
init_field_72a4 were actually team_arena.team_count and
team_arena.weapon_slots[754] respectively β now accessed correctly.
fire_skip_go uses ddgame.team_arena._teams_region directly instead
of raw pointer arithmetic through offsets::TEAM_ARENA_STATE.
Verified: headless PASS (byte-identical).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
feat: port Skip Go to pure Rust, type fire_weapon_special paramsβ
Replace Skip Go (0x51E8C0) bridge with pure Rust implementation β
reads team arena flags via typed DDGame/GameInfo/CTaskWorm fields,
toggles bit based on weapon entry's fire_params and game_version.
Type fire_weapon_special params as *mut CTaskWorm and *const WeaponEntry.
Use (*worm).state() accessor for Teleport check instead of raw +0x44.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
fix: correct EAX register values in FireWeapon type-4 dispatchβ
Audit of all 24 type-4 special weapon cases against original disasm
revealed three distinct EAX patterns:
- EAX=worm (explicit MOV EAX,ESI): AirStrike, Mail/Mine, Freeze, Surrender
- EAX=entry (unchanged from switch): Drill, Prod, Napalm, SkipGo,
SelectWorm, JetPack, LowGrav
- EAX=*(worm+0x44) (state field): Teleport check
Previously all cases passed EAX=worm, which was wrong for entry-based
and state-based handlers. Fixed by passing entry pointer or worm state
as appropriate per case.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
refactor: propagate WeaponFireParams type through FireWeapon bridgesβ
Update bridge function signatures to accept *const WeaponFireParams
instead of u32 for the fire params pointer. GrenadeMortar and Girder
pass &fire_subtype_34/38 (different offset into WeaponEntry) β cast
at call site.
Export WeaponFireParams from game::weapon module.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
refactor: extract WeaponFireParams sub-struct from WeaponEntryβ
The region at WeaponEntry+0x3C onward is passed as a pointer to all
fire sub-functions. Extract it as WeaponFireParams (0x194 bytes),
embedded in WeaponEntry. Internal layout still unknown but the struct
boundary is now explicit.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
refactor: eliminate pointer arithmetic from FireWeapon implβ
- Rename WeaponEntry.fire_complete β fire_params_start (u32), since
its address is passed to sub-functions as a params base pointer
- Add CTaskWorm::fire_complete() / set_fire_complete() for the
completion flag at CGameTask.subclass_data[12] (worm+0x3C)
- Rename params β fire_params, worm_u32 β w for clarity
- All field reads now go through typed struct access, no raw offsets
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
refactor: type FireWeapon params as *const WeaponEntry and *mut CTaskWormβ
Replace raw u32 parameters with typed pointers:
- entry: *const WeaponEntry (EAX = active weapon table entry)
- worm: *mut CTaskWorm (stack param = worm task)
Read fire_type/subtypes via WeaponEntry fields instead of raw offsets.
Read selected_weapon via CTaskWorm field.
Clarify that completion flag (worm+0x3C) writes to CGameTask.subclass_data,
while params pointer (entry+0x3C) points into WeaponEntry.fire_complete β
same relative offset on different objects.
Verified: headless PASS, headful 133+3 / 0 FAIL.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
feat: identify weapon_ctx as WeaponEntry pointer, add fire fieldsβ
Runtime investigation confirmed CTaskWorm+0x36C points to the active
WeaponEntry in the WeaponTable (entry index matches selected_weapon).
Add newly discovered WeaponEntry fields:
- fire_type (+0x30): weapon dispatch type (1-4)
- fire_subtype_34 (+0x34): subtype for types 3/4
- fire_subtype_38 (+0x38): subtype for types 1/2
- fire_complete (+0x3C): completion flag / params base
Type CTaskWorm.active_weapon_entry as *mut WeaponEntry.
Remove incorrect CTaskWorm weapon accessors (they read from the worm's
subclass_data instead of the WeaponEntry).
Verified: headless PASS (byte-identical).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
refactor: rename CGameTask._unknown_30 to subclass_data, fix CTaskWorm weapon_ctxβ
Rename the polymorphic 84-byte region at CGameTask+0x30 to
subclass_data with documentation of per-subclass usage.
CTaskWorm+0x36C (weapon_ctx) is NOT a self-pointer β runtime logging
confirmed weapon_ctx != worm (differ by ~0x20000). It points to a
separate weapon context object with fire state at +0x30..+0x3C.
Renamed from weapon_self_ptr to weapon_ctx: *mut u8.
Removed incorrect CTaskWorm weapon fire accessors that read from
subclass_data (wrong object). Weapon state must be read from the
weapon_ctx pointer, as done in the working FireWeapon impl.
Verified: headless PASS (byte-identical), headful 133+3 / 0 FAIL.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
refactor: rename CGameTask._unknown_30 to subclass_data, add CTaskWorm weapon accessorsβ
Rename the polymorphic 84-byte region at CGameTask+0x30 to
subclass_data with documentation of per-subclass usage (worm weapon
state, filter subscriptions, missile params, team vtable, etc.).
Add CTaskWorm weapon fire accessor methods:
- weapon_fire_type(), weapon_fire_subtype_34/38()
- weapon_fire_complete() / weapon_fire_complete_mut()
- weapon_params_ptr(), weapon_params_34_ptr(), weapon_params_38_ptr()
- weapon_self_ptr field at +0x36C
Make task::worm and task::game_task modules public.
Update FireWeapon impl to use worm_eax (first cdecl param = EAX) for
field reads β must match original register source for determinism.
Verified: headless PASS (byte-identical), headful 133+3 / 0 FAIL.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
refactor: name CTaskWorm weapon_self_ptr field, make game_task pubβ
Add weapon_self_ptr (*mut CTaskWorm) at CTaskWorm+0x36C β the
self-pointer read by WeaponRelease (MOV EAX, [EDI+0x36C]) before
calling FireWeapon. Make game_task module public for typed access.
Note: weapon fire state fields at CGameTask+0x30..+0x3C are NOT added
to CGameTask because the _unknown_30 region is polymorphic β each
subclass (worm, missile, filter, team) uses it differently.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
feat: replace FireWeapon dispatch with Rust implementationβ
Full Rust replacement for FireWeapon (0x51EE60) β the weapon firing
dispatcher that routes 4 weapon types Γ subtypes to 20+ handlers.
Key discovery: EAX at entry = *(CTaskWorm+0x36C) = worm self-pointer,
so weapon_ctx == worm for all dispatch paths. All sub-functions use
EAX=ESI=EDI=worm as their implicit register context.
Implementation:
- Naked trampoline captures EAX/ECX/stack, saves callee-saved regs
- Type 1: PlacedExplosive, Projectile, CreateWeaponProjectile, Shotgun
- Type 2: RopeType1, CreateWeaponProjectile, RopeType3
- Type 3: GrenadeMortar
- Type 4: 24 special weapon subtypes (vtable calls + usercall bridges)
- All bridges save/restore ESI/EDI/EBX and set ESI=EDI=worm
Verified: headless PASS (byte-identical), headful 133+3 / 0 FAIL.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
fix: read weapon ID from CTaskWorm in FireWeapon loggerβ
The stack param to FireWeapon is the CTaskWorm pointer (pushed by
WeaponRelease as EDI=EAX from its usercall convention). Read
selected_weapon at CTaskWorm+0x170 for accurate weapon names.
Previous version read from wrong offset in weapon context, producing
garbage (Skunk, SheepStrike, None). Now correctly shows:
Bazooka(27), Uzi(10), Grenade(9), Teleport(4), etc.
Also reveals dispatch table correction: Bazooka uses sub38=3
(CreateWeaponProjectile), not sub38=2 (FireWeapon__Projectile).
Verified: headless PASS, headful 133+3 / 0 FAIL.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
feat: add FireWeapon passthrough hook with weapon type loggingβ
Hook FireWeapon (0x51EE60) as naked passthrough: save regs β call
logger β restore β jmp original. Logs weapon type (1-4) and subtypes
for each weapon fired during gameplay.
Verified: headless PASS (byte-identical), headful 133+3 / 0 FAIL.
58 weapon fires logged in test replay (mostly type=1 homing/bazooka).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
docs: update weapon-system.md with SubtractAmmo investigation resultsβ
Verified all 5 call sites for SubtractAmmo (0x522680):
- CTaskWorm: Teleport (0x28)
- CTaskTeam: selected weapon (dynamic)
- FUN_0055F8C0: DamageX2 (0x43), CrateSpy (0x44)
Hook is correct β never triggered because test replay doesn't use
those weapons. No rename needed.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
fix: correct WeaponTable layout β no header, add entry fieldsβ
InitWeaponTable analysis revealed the weapon table is a flat array of
71 Γ 0x1D0 = 0x80B0 bytes with NO 16-byte header. The previous struct
had a phantom _header that made address math accidentally correct but
mislabeled the field being checked.
WeaponEntry now has proper fields:
- panel_state (+0x08, init 0xFFFFFFFF)
- defined (+0x10, checked by CheckWeaponAvail)
- availability (+0x24, init 0xFFFFFFFF, then 0/1 per weapon)
- enabled (+0x28, init 1)
check_weapon_avail now reads `entry.defined` instead of misnamed
`entry.name1`. Updated weapon-system.md to match.
Verified: headless PASS (byte-identical), headful 133+3 PASS / 0 FAIL.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add WeaponEntry (0x1D0 bytes) and WeaponTable (header + 71 entries)
structs from wkJellyWorm RE data. DDGame.weapon_table is now typed
*mut WeaponTable, enabling array indexing instead of pointer math.
Use Weapon enum names in check_weapon_avail match arms instead of hex
literals. Fix hex-to-decimal conversion errors (0x37=55=NuclearTest,
not 0x37=37=NinjaRope). Rename fields to match actual weapons:
- weapon_36_disabled β donkey_disabled
- weapon_42_mode β invisibility_mode
- weapon_avail_threshold β double_turn_time_threshold
- weapon_index_offset β aquasheep_is_supersheep
- weapon_restriction_active β supersheep_restricted
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
refactor: type check_weapon_avail, add CoordList struct, name DDGame/GameInfo fieldsβ
Add three new typed structs replacing raw pointer arithmetic:
- TeamIndexMap (0x64 bytes): entries[i16;16] + count + gap + terminator.
Three instances at DDGame+0x7650 replace 6 fragmented byte arrays.
- RenderEntry (0x14 bytes): active flag + unknown data.
14 entries at DDGame+0x73B0 replace opaque _unknown_73b0 blob.
- NetBridge (0x2C bytes): ddgame back-pointer + net_config bytes.
Created for online games only, replaces ddgame_secondary raw pointer.
ddgame_init_render_indices now takes *mut DDGame and uses pure field
access. DDGameWrapper.ddgame_secondary renamed to net_bridge with
typed *mut NetBridge pointer.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
feat: add /type-struct skill for struct identification and typingβ
Formalizes the process of turning raw pointer arithmetic into typed
Rust structs: identify β map fields β create struct β update types β
replace pointer math β propagate β verify.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>