Replace raw u32 sprite_flags with typed SpriteOp (packed sprite index +
transform flags) and SpriteFlags (bitflags for the 13 known blit flags).
Follows the same pattern as SoundId/KnownSoundId in the audio system.
- SpriteId enum renamed to KnownSpriteId (non-exhaustive catalog)
- SpriteOp: #[repr(transparent)] newtype with index()/flags() accessors
- SpriteFlags: bitflags! type replacing raw 0x0080_0000-style masks
- Updated through the full pipeline: DrawSpriteCmd, RenderMessage,
vtable signature, dispatcher, and blit_sprite implementation
- Added bitflags 2 as direct dep (already transitive via windows crate)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
refactor(cloud): rename pos_y/vel_y to anim_phase/phase_speed, use RenderMessageβ
CTaskCloud.pos_y was never a position β it's a per-frame animation phase
counter passed as the palette arg to blit_sprite. Rename to anim_phase
(and vel_y to phase_speed) to match actual semantics. Migrate the cloud
producer from DrawSpriteCmd to RenderMessage::Sprite as the first typed
render queue user.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
fix(cloud): use field 0x44 as render Y, not field 0x34β
CTaskCloud::HandleMessage RenderScene was sending `pos_y` (field 0x34)
as the sprite Y, which the constructor masks to `(pos_x + render_y) &
0xFFFF` β always 0 β pinning every cloud to the very top of the level.
The original at 0x548527..0x54852f loads `[ESI+0x44]` into EAX (the
usercall Y register for RQ_DrawSpriteLocal) and pushes `[ESI+0x34]` as
the trailing stack arg, which lands in the queue command's `palette`
slot and acts as a per-cloud animation/palette index incremented each
frame by `vel_y`.
Rename the misnamed `y_seed: u32` to `render_y: Fixed`, swap the two
values in the DrawSpriteCmd write, and correct the field docs.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
refactor: fix DDGameWrapper size to 0x6F10 and remove WABoxβ
DDGameWrapper had an off-by-16 size assertion (0x6F00 vs actual allocation
0x6F10); the 0x20-byte tail is unzeroed unknown padding. Fixed struct field
and size assertion.
WABox never had a Drop implementation so it was purely a leak-by-design
wrapper with no safety value. Remove it entirely and replace all uses:
- WABox::alloc(...).leak() β wa_malloc_struct_zeroed::<T>()
- WABox::from_value(val).leak() β wa_malloc_struct::<T>() + ptr::write(p, val)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
refactor: eliminate DDDisplay, make DisplayBase generic, rename DDDisplayWrapperβ
- Make DisplayBase<V: Vtable> generic over vtable type (like CTask<V>, BitGrid<V>)
- Eliminate DDDisplay wrapper type β DisplayGfx is now the direct this type
for all display vtable methods, removing constant as_gfx() casts
- DisplayGfx.base is now DisplayBase<*const DisplayVtable> (38-slot vtable)
- bind_DisplayVtable!(DisplayGfx, base.vtable) uses nested field path
- Rename DDDisplayVtable β DisplayVtable, dd_display.rs β display_vtable.rs
- Rename DDDisplayWrapper β RenderContext (manages framebuffer + dispatches
to renderer backends β the DD prefix was misleading since WA supports
multiple graphics backends), display_wrapper.rs β render_context.rs
- Update all address constants (DDISPLAY_* β DISPLAY_GFX_*, etc.)
- Rename 51 functions in Ghidra to match
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
refactor: rename TeamArenaState to TeamArena, use raw pointers for soundnessβ
Move BitGrid from task::bit_grid to display::bitgrid as its canonical
home. Delete the old module. Rename fields to match known semantics:
_unused_04 β external_buffer, _unused_1c/20 β clip_left/clip_top,
width_dup/height_dup β clip_right/clip_bottom.
Convert bit_grid_init to BitGrid::init with *mut BitGrid parameter.
Replace magic vtable address with va::BIT_GRID_VTABLE constant.
Remove redundant double memset. Remove dead TaskStateMachine class.
Rename DDGame::display_gfx β display_bitgrid (it's a BitGrid, not
DisplayGfx). Rename Sprite::display_gfx β bitgrid_vtable. Clean up
tsm variable names and stale re-exports throughout.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Port the cloud spawning factory (0x552960) that creates a CTaskFilter
and spawns CTaskCloud children with deterministic LCG randomization.
- Add rng.rs with shared wa_lcg() used by advance_rng, advance_effect_rng,
and cloud spawning (eliminates duplicated LCG constants)
- Port create_weather_filter in task/team.rs with naked asm bridge for
CTaskFilter constructor (usercall ECX=0x1B)
- Add Subscribe (slot 8) and SubscribeAll (slot 7) to CTaskFilterVTable
- Add CTASK_TEAM_CREATE_WEATHER_FILTER and CTASK_FILTER_SUBSCRIBE addresses
- Hook via MinHook in wormkit cloud.rs
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
refactor: fix CTaskCloud vtable (12β7 slots), add constructor and addressesβ
The vtable was incorrectly sized at 12 slots β verified as 7 by gap to
CTaskCPU vtable (0x669D54 - 0x669D38 = 0x1C). CTaskCloud does not
override ProcessFrame; all logic runs in HandleMessage via FrameFinish.
- Fix vtable size 12β7, remove phantom process_frame slot 7
- Add write_replay_state (slot 0, 0x548430) to vtable definition
- Add ReadReplayState (0x548370) and WriteReplayState address constants
- Rename _unknown_44 β y_seed (EDI from constructor, seeds initial pos_y)
- Document _unknown_54 as unused padding (never touched by any code path)
- Add CloudType enum and CTaskCloud::init() Rust constructor
- Export new addresses and CloudType
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
refactor: replace raw pointer arithmetic with typed struct accessβ
- weapon_fire.rs: use DDGame.level_width/level_height instead of
.add(0x77C0/0x77C4); use new CTaskWorm accessors instead of
.add(0x34/0x48) on raw worm pointer
- sound_ops.rs: use CGameTask.pos_y instead of .add(0x88)
- sound_ops.rs + active_sound.rs: use SoundEmitter.local_ref_count
instead of .add(8) on raw emitter pointer
- game_task.rs: split SoundEmitter._unknown_04 to expose ref count
field at offset 0x08
- worm.rs: add fire_subtype_1/set_fire_subtype_1_raw/set_action_field_raw
accessors; rewrite state() to use subclass_data slice indexing
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
feat: port streaming sound chain to Rust, unify SoundId/SoundSlotβ
Fully port play_worm_sound/play_worm_sound_2 β the streaming sound
playback chain no longer calls into WA.exe via naked asm bridges.
New Rust implementations (sound_ops.rs):
- distance_3d_attenuation: stereo panning with i64 fixed-point math
- compute_distance_params: reads listener pos + attenuation from DDGame
- record_active_sound: ring buffer insert into ActiveSoundTable
- dispatch_local_sound: 3D audio + play_sound_pooled + record
- load_and_play_streaming / load_and_play_streaming_positional
Removed: naked asm bridges (call_load_and_play_streaming,
call_load_and_play_streaming_positional, LOAD_AND_PLAY_STREAMING_ADDR).
Unify SoundId and SoundSlot into a single SoundId type β they were
the same packed u32 format (low 16 = slot index, bit 16 = loop,
bit 17 = raw volume). Flag accessors moved to SoundId.
Other changes:
- Add Fixed Div/Div<i32> operators
- Name DDGame+0x77B8 (level_width_sound), GameInfo+0xF38C
(sound_attenuation), DDGame listener_pos() accessor
Co-Authored-Byβ¦
refactor: extract game logic from wormkit/replacements to openwa-coreβ
The Rust port of fire_armageddon (0x51EA60) was complete but never
wired in due to a desync caused by a subtle buffer offset error.
Root cause: the original function writes message fields at buffer
offsets 0x04/0x08/0x0C/0x10, then passes (buffer + 4) as the data
pointer to HandleMessage. This happens because PUSH ESI (argument to
SharedData__Lookup) is cleaned by the callee's RET 4, shifting the
ESP-relative LEA by 4 bytes. The Rust port was passing buffer + 0,
putting all fields 4 bytes too high.
Also:
- Remove dead call_fire_usercall bridge (no longer needed)
- Add CTaskWorm::is_in_state() helper
- Clean up weapon_release.rs: hexβdecimal constants, use is_in_state
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Port FUN_00515020 as play_worm_sound_2 in sound.rs β manages the secondary
sound handle at CTaskWorm+0x3B4. Includes special teleport positioning case
(worm Y < -9999 AND sound 0x36 β play at weapon target via FUN_00546BB0).
Hook installed at 0x515020 so all 23 WA callers go through Rust.
Also:
- fire_girder: replaced raw PlaySoundGlobal transmute with sound::queue_sound
- fire_teleport: replaced call_worm_play_sound bridge with play_worm_sound_2
- Fixed flags passthrough in streaming sound bridges (was hardcoded to 3)
- Added sound_handle_2 field to CTaskWorm at +0x3B4
- Removed dead call_worm_play_sound bridge
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
fix: correct SharedData entity type and rename all SpecialFireSubtype/WormStateβ
SpecialFireSubtype::NapalmStrike (subtype 13) is actually used by
multiple weapons (Napalm Strike, Surrender, and others) β it's a
generic "send team message 0x2B" handler, not Napalm-specific.
Renamed to StrikeFire to match the WA function name (StrikeFire at
0x51E2C0). Also renamed TeamMessage::NapalmStrike to StrikeFire and
on_napalm_strike to on_strike_fire.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
fix: eliminate noalias UB in vtable dispatch, add typed TeamMessageβ
The bind_! macro generated &mut self methods for vtable calls, giving
LLVM noalias guarantees that WA bridge calls violated. This caused
codegen-sensitive desyncs where unrelated code changes would shift
optimization decisions and expose the UB.
Fix: auto-generate _raw associated functions (taking raw pointers) for
every bind_! vtable method. Migrate all call sites in weapon dispatch,
weapon release, sound, cloud, and filter handlers to use _raw variants
and CTask::ddgame_raw() instead of the Task trait methods.
Also adds TeamMessage enum with NapalmStrike variant as the first typed
message, and CTaskTeam::on_napalm_strike handler (not yet wired up due
to a separate binary layout sensitivity issue under investigation).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
refactor: use Weapon enum for weapon IDs and classificationβ
Change CTaskWorm.selected_weapon from u32 to Weapon enum. Update
is_super_weapon, is_weapon_category_a, is_weapon_category_b to take
Weapon and match on named variants instead of hex magic numbers.
Replace raw 0x22/0x24 check with Weapon::Girder/GirderPack match.
Use SpecialFireSubtype variants in type-4 sound dispatch.
Idiomatic range check: Weapon::JetPack..=Weapon::CrateShower.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
refactor: add weapon fire type enums, use Fixed for aim/scale paramsβ
Add FireType, FireMethod, and SpecialFireSubtype enums to weapon.rs
with TryFrom<i32>. All weapon dispatch matches in weapon.rs and
weapon_release.rs now use typed enum variants instead of raw integers.
Rename fire_subtype_34 β special_subtype, fire_subtype_38 β fire_method
in WeaponEntry for clarity.
Use Fixed type for aim direction params and landscape_scale field.
Add Mul<i32> for Fixed (integer scaling without shift). Remove
fixed_mul_shrd β superseded by Fixed's Mul impl. Spawn offset
calculation now reads as: aim_dir_x * scale * 0x18.
Rename param_1/param_2 β spawn_x/spawn_y, param_3/param_4 β
aim_dir_x/aim_dir_y based on caller analysis (FUN_0051cb60 computes
adjusted worm position + aim direction from angle).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Full Rust reimplementation of the weapon release orchestration function
that handles ammo sync, spawn offset calculation, network timing, weapon
stat counters, sound/visual effects, and delegates to FireWeapon.
New module weapon_release.rs with:
- usercall trampoline (EAX=worm, 4 stack params, RET 0x10)
- Pure Rust: sync check, spawn offsets, delay, network timing,
SharedData HandleMessage (0x49), 4 weapon stat counters,
3 weapon category classifiers (IsSuperWeapon, CategoryA/B)
- Rust-to-Rust PlaySoundLocal via new sound::play_sound_local() helper
- Bridges for PlayWormSound/StopWormSound/SpawnEffect (addr-as-param)
- Direct call to fire_weapon_impl (made pub(crate))
- DDGame::advance_effect_rng() for secondary RNG at +0x45F0
Also adds 16 named fields to CTaskWorm (landscape_scale, shot_data,
fire_sync_frames, difficulty_level, weapon_params, sound_handle, etc.),
fills all WormState enum gaps (0x69-0x6B, 0x6F, 0x76, 0x79-0x7A,
0x84-0x85, 0x8A), and removes from_raw_unchecked antβ¦
feat: port Air Strike, Indian Nuclear Test, and Jet Pack to Rustβ
Live memory debugging (debug CLI + Skunk weapon) confirmed:
- +0x144 poison_source_mask: alliance bitmask preventing double-poison
- +0x148 poison_damage: per-turn HP loss (set by PoisonWorm msg 0x51,
consumed by ApplyPoison msg 0x3E)
- +0x178/+0x17C display_health_raw/target_health_raw: animated HP
interpolation for health bar display
- +0x312 health_text: ASCII string for floating HP number ("100", "88")
- +0x31B poison_text: ASCII string for green poison number ("5")
WormEntry +0x90/+0x94 and +0x70 were initially suspected as poison
fields but not reliably correlated β reverted to unknown.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
fix: correct CTask vtable layout β 7 slots, not 8β
CTask base has exactly 7 virtual methods (confirmed by wkJellyWorm vtNum=7
and Ghidra decompilation of each slot):
Slot 0: WriteReplayState Slot 1: Free (scalar deleting dtor)
Slot 2: HandleMessage Slot 3-4: stubs (returns 0)
Slot 5: ProcessChildren Slot 6: ProcessFrame
Previous code had size=8 with ProcessFrame misplaced at slot 7. Slot 7
doesn't exist in CTask base β it's the first class-specific extension
slot for derived types (e.g., OnContactEntity for CTaskWorm,
process_frame for CTaskCloud).
Also update CTaskWormVTable doc comment with full 20-slot layout table
showing which slots are CTask base, CGameTask, and CTaskWorm-specific.
Ghidra renames: CTask__vt7_ProcessFrame β CTask__vt7_INVALID_NotInBaseVtable,
CTask__vt5 β ProcessChildren, CTask__vt6 β ProcessFrame,
CTaskWorm_ProcessFrame_Maybe β CTaskWorm__PerFrameUpdate_Maybe (not vtable slot).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
feat: add WormState enum β typed worm state machine valuesβ
Add repr(u8) WormState enum with all known states from CTaskWorm::HandleMessage
(0x510B40) decompilation and weapon fire dispatch analysis:
- Idle/Active states (0x64-0x68)
- Weapon fire states (0x6C-0x75: Blowtorch, Bat, Kamikaze, etc.)
- Aiming/charging states (0x77-0x7E)
- Dying/dead states (0x7F-0x8B)
Update CTaskWormVTable::set_state to take WormState parameter.
Replace all hex literals in weapon.rs with enum variants.
Source: Ghidra decompilation of HandleMessage switch cases +
SetState call sites across weapon fire dispatch.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
fix: broadcast_message now works β use read_volatile for children stateβ
The pure Rust CTask::HandleMessage port (broadcast_message) was failing
because LLVM cached children_watermark and children_data reads across
virtual dispatch calls. Child handlers can modify the parent's children
array (add/remove/realloc), so these fields must be re-read each
iteration.
Fix: use read_volatile via core::ptr::addr_of! for both fields.
This matches the original WA code which re-reads [EDI+0x10] and
[EDI+0x14] from memory each iteration.
CTaskCloud and CTaskFilter handlers now use broadcast_message directly
instead of calling the original WA CTask::HandleMessage at 0x562F30.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
feat: add CTaskVtable, broadcast_message, and retype children_dataβ
Add base CTaskVtable (8 slots, 0x669F8C) via #[vtable] macro with
write_replay_state, free, handle_message, and process_frame.
Add Task::broadcast_message() β pure Rust port of CTask::HandleMessage
(0x562F30) that iterates the sparse children array and dispatches via
typed CTaskVtable. Currently unused (callers use original WA function
pending investigation of a subtle iteration bug).
Retype CTask.children_data from *mut u8 to *mut *mut CTask for type
safety.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
feat: add Vtable marker trait for CTask<V> type safetyβ
Add unsafe trait Vtable as a bound on CTask<V> and CGameTask<V>,
preventing arbitrary types from being used as vtable parameters.
- *const c_void implements Vtable (the default/untyped case)
- #[vtable] proc macro auto-generates `impl Vtable for *const MyVTable`
- CTask<V: Vtable> and CGameTask<V: Vtable> enforce the bound
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
feat: add Task trait for ergonomic CTask base accessβ
Add unsafe trait Task with default methods:
- task() / task_mut() β reference to CTask base
- as_task_ptr() / as_task_ptr_mut() β raw pointers
- ddgame() β direct DDGame pointer access
Implemented for all CTask subclasses (CTaskWorm, CTaskTeam,
CTaskMissile, CTaskArrow, CTaskMine, etc.) via unsafe impls.
Replace all .base.base chains in weapon.rs with trait methods:
- (*worm).base.base.ddgame β (*worm).ddgame()
- &(*worm).base.base as *const _ as *const CTask β (*worm).as_task_ptr()
- SharedDataTable::from_task(task as *const _ as ...) β from_task((*worm).as_task_ptr())
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Make CTask generic over its vtable pointer type:
CTask<V: 'static = *const c_void>
This allows subclasses to specify their typed vtable:
CTaskTeam { base: CTask<*const CTaskTeamVTable>, ... }
Changes:
FieldRegistry derive macro (openwa-derive):
- Handle generic type params by substituting defaults in size_of
expressions. offset_of uses the bare struct name (Rust applies
defaults). Builds a generic_defaults map from TypeParam::default.
bind macro (openwa-derive):
- Accept dotted field paths ($($vtable_field:ident).+) instead of
single idents, enabling bind_VTable!(Struct, base.vtable) for
inherited vtables.
CTask (openwa-core):
- Add generic parameter V with default *const c_void. All existing
code using bare CTask continues to work unchanged.
CTaskTeam (openwa-core):
- Use CTask<*const CTaskTeamVTable> as base type
- Replace manual vtable() and handle_message() with bind macro
- bind_CTaskTeamVTable!(CTaskTeam, base.vtable) generates typed
method wrappers automatiβ¦
feat: add CTaskTeamVTable and typed handle_message dispatchβ
Define CTaskTeamVTable (12 slots, vtable at 0x669EE4) via #[vtable] macro
with typed signatures for handle_message (slot 2), write_replay_state,
free, get_entity_data, and process_frame.
Add CTaskTeam::vtable() accessor and CTaskTeam::handle_message() method
that replaces raw vtable pointer arithmetic in weapon fire handlers.
Extract lookup_team_task() helper for SharedData CTaskTeam lookup.
Note: bind_CTaskTeamVTable! not used because CTask.vtable is *mut u8
(FieldRegistry derive doesn't support generic structs, so CTask<V>
would require manual impls across the entire task hierarchy).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
feat: port weapon firing pipeline β CreateWeaponProjectile, ProjectileFire, CreateArrowβ
Port the core weapon firing functions from WA.exe to Rust:
- CreateWeaponProjectile (0x51E0F0): full replacement β allocates CTaskMissile
(0x40C bytes), SharedData lookup for parent CTaskTurnGame, pool capacity
check, calls original constructor. All projectile weapons flow through this.
- ProjectileFire (0x51DFB0): full replacement β advances game RNG, computes
random spread angle via Fixed16.16 trig table interpolation, applies 2D
rotation matrix to template velocity, calls ProjectileFire_Single per shot.
Used by Uzi, Handgun, Minigun.
- CreateArrow (0x51ED90): full replacement β allocates CTaskArrow (0x168 bytes),
same pool/SharedData pattern as CWP. Used by Shotgun and Longbow (which fire
physically simulated arrow entities, NOT hitscan).
Also adds:
- WeaponSpawnData struct (11 DWORDs) replacing raw [u32; 11] in CTaskMissile
- CTaskArrow struct (0x168 bytes) with vtable at 0x664198
- CTaskMissile size corrected from 0x41C to 0x40C (matching actual allocation)
- Pasβ¦
feat: upgrade debug tools with registry, typed reflection, and symbolic addressesβ
Wire the address registry, field registries, typed vtables, and live
object tracker into all debug tools:
- Watchpoint stack traces now show symbolicated function names via
format_va() instead of raw hex addresses
- Validation auto-discovers vtables from the registry and validates
all named slots from #[vtable(...)] structs
- New ValueKind enum + FieldFormatter trait for typed field formatting:
scalars as decimal, Fixed as float, pointers resolved via registry,
CString as quoted strings. Extensible via inventory::submit!
- Snapshot fallback uses FieldRegistry for entity types without
hand-coded Snapshot impls (CTaskTeam, CTaskMine, etc.)
- New debug CLI commands: inspect (typed struct read), objects (list
tracked live objects)
- Symbolic address expressions: ddgame, ddgame+field_name,
ddgame->field_name work in all CLI commands
- New protocol messages: Inspect, ListObjects, ResolveAlias, ResolveField
- #[field(kind = "CString")] attribute for string fields in FieldRegistβ¦
feat: add #[vtable(...)] attribute macro for typed vtable definitionsβ
Move all CTask* class blocks from address.rs into their respective
task/ module files. address.rs re-exports via pub use so all existing
va::* call sites are unchanged.
Moved to task/base.rs: CTask, CTaskLand, CTaskDirt, CTaskSpriteAnim,
CTaskCPU, CTaskSeaBubble, + 10 ctor-only entities
Moved to task/game_task.rs: CGameTask
Moved to task/worm.rs: CTaskWorm
Moved to task/turn_game.rs: CTaskTurnGame
Moved to task/missile.rs: CTaskMissile
Moved to task/mine_oil_drum.rs: CTaskMine, CTaskOilDrum
Moved to task/supply_crate.rs: CTaskCrate
Moved to task/cloud.rs: CTaskCloud
Moved to task/filter.rs: CTaskFilter
Moved to task/fire.rs: CTaskFire
Moved to task/team.rs: CTaskTeam
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
feat: add #[derive(FieldRegistry)] to all task entity structsβ
Apply FieldRegistry to every #[repr(C)] struct under task/:
CTaskMine, CTaskOilDrum, CTaskCrate, CTaskCloud, CTaskFilter,
CTaskFire, CTaskTeam, CTaskTurnGame, TurnGameCtx, BitGrid,
BungeeTrailTask, WeaponAimTask.
All entity types now have auto-generated field maps available in the
global registry, so the debug UI's raw memory view labels every known
field automatically.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
feat: replace hardcoded debug UI field labels with registry lookupsβ
Remove CGAMETASK_FIELD_LABELS and all per-entity extra_labels arrays
from the debug UI inspector. Field names are now resolved via
registry::field_at_inherited() which walks the CTask inheritance chain
(entity β CGameTask β CTask).
Add #[derive(FieldRegistry)] to CGameTask and CTaskMissile. Add
field_at_inherited() helper for inheritance-aware offset β name lookup.
Entity types without Rust struct definitions (CTaskMine, CTaskOilDrum,
CTaskCrate) will show base class fields only until their structs are
defined β but any new struct fields are automatically picked up.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
feat: add #[derive(FieldRegistry)] proc macro for auto-generated field mapsβ
New openwa-derive crate with FieldRegistry proc macro that generates
HasFieldRegistry impls for #[repr(C)] structs. Uses offset_of!() and
size_of() for compiler-verified offsets. Fields prefixed _unknown/_pad
are automatically excluded.
Applied to 5 key structs: DDGame, DDGameWrapper, GameSession, CTask,
CTaskWorm. Each now has a field_registry() -> &'static StructFields
method for runtime offset-to-name lookups.
This lays groundwork for typed vtable derive macros using the same
proc macro infrastructure.
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: port Napalm Strike and Select Worm to pure Rustβ
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>
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>
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>