feat: add /type-struct skill for struct identification and typing
Browse filesFormalizes 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>
.claude/skills/type-struct/SKILL.md
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
name: type-struct
|
| 3 |
+
description: Identify a class/struct from raw pointer usage, create a typed Rust struct, and propagate type information to eliminate pointer arithmetic. Works with Ghidra for reverse engineering context.
|
| 4 |
+
---
|
| 5 |
+
|
| 6 |
+
# Type a Struct
|
| 7 |
+
|
| 8 |
+
Turn raw pointer arithmetic (`base.add(0x1234)`, `wb(0x878095 + idx * 0xD7B, ...)`) into typed Rust struct field access.
|
| 9 |
+
|
| 10 |
+
## Process
|
| 11 |
+
|
| 12 |
+
### 1. Identify the struct
|
| 13 |
+
|
| 14 |
+
Find a pattern: repeated stride arithmetic, consistent base+offset reads/writes, or a Ghidra-visible vtable.
|
| 15 |
+
|
| 16 |
+
- Grep for the stride/offset pattern across the codebase to find all access sites
|
| 17 |
+
- Note the allocation size (from `wa_malloc`, `wa_malloc_zeroed`, or CRT malloc calls)
|
| 18 |
+
- Check Ghidra for vtable assignments, constructor calls, and inheritance (xrefs to the constructor show all callers and usage contexts)
|
| 19 |
+
- Determine: is this a new struct, or does it belong in an existing one (e.g., fields in DDGame, GameInfo)?
|
| 20 |
+
|
| 21 |
+
### 2. Map the field layout
|
| 22 |
+
|
| 23 |
+
For each offset accessed in the code, record:
|
| 24 |
+
|
| 25 |
+
| Offset | Size | Type | Name | How used |
|
| 26 |
+
|--------|------|------|------|----------|
|
| 27 |
+
|
| 28 |
+
Compute offsets relative to the struct base. Verify with:
|
| 29 |
+
- Allocation size = total struct size
|
| 30 |
+
- `RET imm16` on the constructor confirms param count
|
| 31 |
+
- Ghidra decompilation of the constructor shows field initialization order
|
| 32 |
+
- Cross-reference with thirdparty RE sources (wkJellyWorm, WormKit) if available
|
| 33 |
+
|
| 34 |
+
### 3. Create the Rust struct
|
| 35 |
+
|
| 36 |
+
- Place in the appropriate module (`engine/`, `frontend/`, `task/`, `audio/`, etc.)
|
| 37 |
+
- Use `#[repr(C)]` for ABI compatibility
|
| 38 |
+
- Add `const _: () = assert!(core::mem::size_of::<T>() == SIZE);` to verify total size
|
| 39 |
+
- Name known fields descriptively; use `_unknown_XXXX: [u8; N]` for gaps
|
| 40 |
+
- If the struct has a vtable, create a companion `FooVtable` struct with typed function pointers for known slots and `usize` for unknown slots
|
| 41 |
+
- Register the module in the parent `mod.rs` and re-export if appropriate
|
| 42 |
+
|
| 43 |
+
### 4. Update variable and argument types
|
| 44 |
+
|
| 45 |
+
- Replace `u32` / `*mut u8` parameters with `*mut NewStruct` in function signatures
|
| 46 |
+
- This is ABI-compatible on i686 (all 32-bit pointers) so no calling convention changes needed
|
| 47 |
+
- Update both Rust functions AND FFI `extern` function pointer types (stdcall, thiscall, cdecl)
|
| 48 |
+
- For naked asm bridges, parameter types can be changed without affecting the assembly
|
| 49 |
+
|
| 50 |
+
### 5. Replace pointer arithmetic with field access
|
| 51 |
+
|
| 52 |
+
- `*(base.add(0x1234) as *mut u32) = val` β `(*ptr).field_name = val`
|
| 53 |
+
- `wb(CONST + idx * STRIDE, val)` β `array[idx].field = val`
|
| 54 |
+
- For vtable calls: `*(vtable as *const u32).add(N)` β `(*(*ptr).vtable).method`
|
| 55 |
+
- Use `vcall!` macro for one-liner vtable dispatch where appropriate
|
| 56 |
+
|
| 57 |
+
### 6. Propagate types
|
| 58 |
+
|
| 59 |
+
- Update callers to pass typed pointers instead of `u32`
|
| 60 |
+
- Remove now-unnecessary `_OFF` address constants from `address.rs` (offsets are now struct fields)
|
| 61 |
+
- Rename address constants if the struct name changed (e.g., `MAP_CLASS_*` β `MAP_VIEW_*`)
|
| 62 |
+
- Update Ghidra labels to match the new names
|
| 63 |
+
|
| 64 |
+
### 7. Verify
|
| 65 |
+
|
| 66 |
+
- `cargo build --release -p openwa-wormkit` β must compile clean, zero warnings
|
| 67 |
+
- Run `/replay-test` (headful + headless) to verify runtime correctness
|
| 68 |
+
- Check that `sizeof` assertion passes (struct size matches allocation size)
|
| 69 |
+
|
| 70 |
+
## Naming conventions
|
| 71 |
+
|
| 72 |
+
- Struct names: `PascalCase`, matching WA class names where known (e.g., `DDGame`, `CTaskWorm`, `MapView`)
|
| 73 |
+
- Unknown fields: `_unknown_XXXX` where XXXX is the hex offset
|
| 74 |
+
- Vtable structs: `FooVtable` companion to `Foo`
|
| 75 |
+
- Address constants: `STRUCT_NAME_FUNCTION` (e.g., `MAP_VIEW_CONSTRUCTOR`)
|
| 76 |
+
- Module placement: `engine/` for core game objects, `frontend/` for UI/CWnd-derived, `task/` for CTask hierarchy, `audio/` for sound, `display/` for graphics
|
| 77 |
+
|
| 78 |
+
## When NOT to create a struct
|
| 79 |
+
|
| 80 |
+
- If offsets use inconsistent strides (e.g., player data with mixed 0x78/0x3C/0x1E strides) β these are interleaved arrays, not a single struct
|
| 81 |
+
- If the "struct" is actually a function's stack frame (local variables)
|
| 82 |
+
- If only 1-2 fields are known and the struct is huge β use raw offsets with named constants until more fields are discovered
|