refactor: rename CGameTask._unknown_30 to subclass_data, add CTaskWorm weapon accessors
Browse filesRename 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>
|
@@ -13,8 +13,15 @@ use crate::fixed::Fixed;
|
|
| 13 |
pub struct CGameTask {
|
| 14 |
/// 0x00-0x2F: Base CTask fields
|
| 15 |
pub base: CTask,
|
| 16 |
-
/// 0x30-0x83:
|
| 17 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
/// 0x84: X position in fixed-point
|
| 19 |
pub pos_x: Fixed,
|
| 20 |
/// 0x88: Y position in fixed-point
|
|
|
|
| 13 |
pub struct CGameTask {
|
| 14 |
/// 0x00-0x2F: Base CTask fields
|
| 15 |
pub base: CTask,
|
| 16 |
+
/// 0x30-0x83: Subclass-specific data (84 bytes). Each CGameTask derivative
|
| 17 |
+
/// uses this region differently:
|
| 18 |
+
/// - CTaskWorm: weapon fire state (+0x30 type, +0x34/+0x38 subtypes, +0x3C flag)
|
| 19 |
+
/// - CTaskFilter: boolean message subscription table (+0x30..+0x93)
|
| 20 |
+
/// - CTaskMissile: spawn/physics parameters
|
| 21 |
+
/// - CTaskTeam: secondary vtable pointer (+0x30)
|
| 22 |
+
/// - CTaskCloud: parallax scroll depth (+0x30)
|
| 23 |
+
/// Access via subclass accessor methods, not directly.
|
| 24 |
+
pub subclass_data: [u8; 0x54],
|
| 25 |
/// 0x84: X position in fixed-point
|
| 26 |
pub pos_x: Fixed,
|
| 27 |
/// 0x88: Y position in fixed-point
|
|
@@ -10,7 +10,7 @@ mod overlays;
|
|
| 10 |
mod supply_crate;
|
| 11 |
mod team;
|
| 12 |
mod turn_game;
|
| 13 |
-
mod worm;
|
| 14 |
|
| 15 |
pub use base::{CTask, CTaskBfsIter, SharedDataIter, SharedDataNode, SharedDataTable};
|
| 16 |
pub use cloud::CTaskCloud;
|
|
|
|
| 10 |
mod supply_crate;
|
| 11 |
mod team;
|
| 12 |
mod turn_game;
|
| 13 |
+
pub mod worm;
|
| 14 |
|
| 15 |
pub use base::{CTask, CTaskBfsIter, SharedDataIter, SharedDataNode, SharedDataTable};
|
| 16 |
pub use cloud::CTaskCloud;
|
|
@@ -75,7 +75,7 @@ const _: () = assert!(core::mem::size_of::<CTaskWormVTable>() == 20 * 4);
|
|
| 75 |
/// Vtable at 0x6644C8. Class type byte: 0x12.
|
| 76 |
///
|
| 77 |
/// # Important fields in the CGameTask base
|
| 78 |
-
/// The worm state field lives at **offset +0x44** (inside `base.
|
| 79 |
/// Use [`CTaskWorm::state`] to read it without pointer arithmetic.
|
| 80 |
///
|
| 81 |
/// Source: Ghidra decompilation of 0x50BFB0, vtable analysis of 0x6644C8,
|
|
@@ -175,16 +175,59 @@ pub struct CTaskWorm {
|
|
| 175 |
const _: () = assert!(core::mem::size_of::<CTaskWorm>() == 0x3FC);
|
| 176 |
|
| 177 |
impl CTaskWorm {
|
| 178 |
-
/// Returns the worm's current state code (lives at offset +0x44, inside
|
| 179 |
-
///
|
| 180 |
///
|
| 181 |
/// Known states: `0x65`=idle, `0x67`=active turn, `0x7F`=drowning,
|
| 182 |
/// `0x80`=hurt, `0x81`/`0x86`=dead, `0x87`=dead variant, `0x8B`=unknown.
|
| 183 |
pub fn state(&self) -> u32 {
|
| 184 |
-
// SAFETY: offset 0x44 is within CGameTask._unknown_40 (0x40..0x84).
|
| 185 |
unsafe { *((self as *const CTaskWorm as *const u8).add(0x44) as *const u32) }
|
| 186 |
}
|
| 187 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 188 |
/// Returns a reference to the vtable.
|
| 189 |
///
|
| 190 |
/// # Safety
|
|
|
|
| 75 |
/// Vtable at 0x6644C8. Class type byte: 0x12.
|
| 76 |
///
|
| 77 |
/// # Important fields in the CGameTask base
|
| 78 |
+
/// The worm state field lives at **offset +0x44** (inside `base.subclass_data`).
|
| 79 |
/// Use [`CTaskWorm::state`] to read it without pointer arithmetic.
|
| 80 |
///
|
| 81 |
/// Source: Ghidra decompilation of 0x50BFB0, vtable analysis of 0x6644C8,
|
|
|
|
| 175 |
const _: () = assert!(core::mem::size_of::<CTaskWorm>() == 0x3FC);
|
| 176 |
|
| 177 |
impl CTaskWorm {
|
| 178 |
+
/// Returns the worm's current state code (lives at offset +0x44, inside
|
| 179 |
+
/// `base.subclass_data`).
|
| 180 |
///
|
| 181 |
/// Known states: `0x65`=idle, `0x67`=active turn, `0x7F`=drowning,
|
| 182 |
/// `0x80`=hurt, `0x81`/`0x86`=dead, `0x87`=dead variant, `0x8B`=unknown.
|
| 183 |
pub fn state(&self) -> u32 {
|
|
|
|
| 184 |
unsafe { *((self as *const CTaskWorm as *const u8).add(0x44) as *const u32) }
|
| 185 |
}
|
| 186 |
|
| 187 |
+
// ββ Weapon fire state accessors (offsets +0x30..+0x3C in subclass_data) ββ
|
| 188 |
+
|
| 189 |
+
/// Weapon fire type (1=projectile, 2=rope, 3=grenade, 4=special).
|
| 190 |
+
/// Offset +0x30 in CGameTask.subclass_data.
|
| 191 |
+
pub fn weapon_fire_type(&self) -> i32 {
|
| 192 |
+
unsafe { *(self.base.subclass_data.as_ptr().add(0) as *const i32) }
|
| 193 |
+
}
|
| 194 |
+
|
| 195 |
+
/// Weapon fire subtype for types 3 and 4. Offset +0x34.
|
| 196 |
+
pub fn weapon_fire_subtype_34(&self) -> i32 {
|
| 197 |
+
unsafe { *(self.base.subclass_data.as_ptr().add(4) as *const i32) }
|
| 198 |
+
}
|
| 199 |
+
|
| 200 |
+
/// Weapon fire subtype for types 1 and 2. Offset +0x38.
|
| 201 |
+
pub fn weapon_fire_subtype_38(&self) -> i32 {
|
| 202 |
+
unsafe { *(self.base.subclass_data.as_ptr().add(8) as *const i32) }
|
| 203 |
+
}
|
| 204 |
+
|
| 205 |
+
/// Weapon fire completion flag. Offset +0x3C.
|
| 206 |
+
/// Set to 0 before dispatch, 1 after.
|
| 207 |
+
pub fn weapon_fire_complete(&self) -> i32 {
|
| 208 |
+
unsafe { *(self.base.subclass_data.as_ptr().add(12) as *const i32) }
|
| 209 |
+
}
|
| 210 |
+
|
| 211 |
+
/// Mutable pointer to weapon_fire_complete for setting the flag.
|
| 212 |
+
pub fn weapon_fire_complete_mut(&mut self) -> &mut i32 {
|
| 213 |
+
unsafe { &mut *(self.base.subclass_data.as_mut_ptr().add(12) as *mut i32) }
|
| 214 |
+
}
|
| 215 |
+
|
| 216 |
+
/// Address of weapon_fire_complete field (passed as params base to fire handlers).
|
| 217 |
+
pub fn weapon_params_ptr(&self) -> u32 {
|
| 218 |
+
self as *const _ as u32 + 0x3C
|
| 219 |
+
}
|
| 220 |
+
|
| 221 |
+
/// Address of weapon_fire_subtype_34 field (params for GrenadeMortar).
|
| 222 |
+
pub fn weapon_params_34_ptr(&self) -> u32 {
|
| 223 |
+
self as *const _ as u32 + 0x34
|
| 224 |
+
}
|
| 225 |
+
|
| 226 |
+
/// Address of weapon_fire_subtype_38 field (params for type-4 specials).
|
| 227 |
+
pub fn weapon_params_38_ptr(&self) -> u32 {
|
| 228 |
+
self as *const _ as u32 + 0x38
|
| 229 |
+
}
|
| 230 |
+
|
| 231 |
/// Returns a reference to the vtable.
|
| 232 |
///
|
| 233 |
/// # Safety
|
|
@@ -179,28 +179,38 @@ unsafe extern "C" fn trampoline_fire_weapon() {
|
|
| 179 |
}
|
| 180 |
|
| 181 |
/// Rust implementation of FireWeapon dispatch.
|
| 182 |
-
|
| 183 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 184 |
let weapon_type = *(ctx.add(0x30) as *const i32);
|
| 185 |
let subtype_34 = *(ctx.add(0x34) as *const i32);
|
| 186 |
let subtype_38 = *(ctx.add(0x38) as *const i32);
|
| 187 |
-
let params =
|
| 188 |
-
let worm_ptr = worm as *mut u8;
|
| 189 |
|
| 190 |
// Log weapon fire
|
| 191 |
-
let weapon_id = *(
|
| 192 |
let weapon_name = Weapon::try_from(weapon_id)
|
| 193 |
-
.map(|
|
| 194 |
.unwrap_or_else(|id| format!("Unknown({})", id));
|
| 195 |
let _ = log_line(&format!(
|
| 196 |
"[Weapon] FireWeapon: {} (id={}) type={} sub34={} sub38={}",
|
| 197 |
weapon_name, weapon_id, weapon_type, subtype_34, subtype_38
|
| 198 |
));
|
| 199 |
|
| 200 |
-
//
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
use openwa_core::rebase::rb;
|
| 204 |
|
| 205 |
match weapon_type {
|
| 206 |
1 => match subtype_38 {
|
|
@@ -216,18 +226,13 @@ unsafe extern "cdecl" fn fire_weapon_impl(weapon_ctx: u32, local_struct: u32, wo
|
|
| 216 |
3 => call_fire_stdcall3(worm, params, local_struct, rb(0x51E240)), // RopeType3
|
| 217 |
_ => {}
|
| 218 |
},
|
| 219 |
-
3 =>
|
| 220 |
-
|
| 221 |
-
call_fire_stdcall3(worm, params_34, local_struct, rb(0x51E2C0)); // GrenadeMortar
|
| 222 |
-
}
|
| 223 |
-
4 => {
|
| 224 |
-
let params_38 = weapon_ctx.wrapping_add(0x38);
|
| 225 |
-
fire_weapon_special(subtype_34, params_38, worm, local_struct);
|
| 226 |
-
}
|
| 227 |
_ => {}
|
| 228 |
}
|
| 229 |
|
| 230 |
-
|
|
|
|
| 231 |
}
|
| 232 |
|
| 233 |
// ββ Sub-function bridges ββββββββββββββββββββββββββββββββββββ
|
|
|
|
| 179 |
}
|
| 180 |
|
| 181 |
/// Rust implementation of FireWeapon dispatch.
|
| 182 |
+
/// EAX = worm self-pointer (== stack param), ECX = local_struct from WeaponRelease.
|
| 183 |
+
unsafe extern "cdecl" fn fire_weapon_impl(worm_eax: u32, local_struct: u32, worm: u32) {
|
| 184 |
+
use openwa_core::rebase::rb;
|
| 185 |
+
|
| 186 |
+
// EAX (worm_eax) = *(CTaskWorm+0x36C), stack param (worm) = CTaskWorm*.
|
| 187 |
+
// Check if they're actually the same β they might not be!
|
| 188 |
+
if worm_eax != worm {
|
| 189 |
+
let _ = log_line(&format!(
|
| 190 |
+
"[Weapon] WARNING: EAX={:#x} != worm={:#x} (delta={})",
|
| 191 |
+
worm_eax, worm, worm_eax.wrapping_sub(worm) as i32
|
| 192 |
+
));
|
| 193 |
+
}
|
| 194 |
+
|
| 195 |
+
// Use worm_eax (original EAX) for field reads β this is what the original code uses
|
| 196 |
+
let ctx = worm_eax as *const u8;
|
| 197 |
let weapon_type = *(ctx.add(0x30) as *const i32);
|
| 198 |
let subtype_34 = *(ctx.add(0x34) as *const i32);
|
| 199 |
let subtype_38 = *(ctx.add(0x38) as *const i32);
|
| 200 |
+
let params = worm_eax.wrapping_add(0x3C);
|
|
|
|
| 201 |
|
| 202 |
// Log weapon fire
|
| 203 |
+
let weapon_id = *((worm as *const u8).add(0x170) as *const u32);
|
| 204 |
let weapon_name = Weapon::try_from(weapon_id)
|
| 205 |
+
.map(|wp| format!("{:?}", wp))
|
| 206 |
.unwrap_or_else(|id| format!("Unknown({})", id));
|
| 207 |
let _ = log_line(&format!(
|
| 208 |
"[Weapon] FireWeapon: {} (id={}) type={} sub34={} sub38={}",
|
| 209 |
weapon_name, weapon_id, weapon_type, subtype_34, subtype_38
|
| 210 |
));
|
| 211 |
|
| 212 |
+
// Clear completion flag before dispatch
|
| 213 |
+
core::ptr::write_volatile((worm + 0x3C) as *mut i32, 0);
|
|
|
|
|
|
|
| 214 |
|
| 215 |
match weapon_type {
|
| 216 |
1 => match subtype_38 {
|
|
|
|
| 226 |
3 => call_fire_stdcall3(worm, params, local_struct, rb(0x51E240)), // RopeType3
|
| 227 |
_ => {}
|
| 228 |
},
|
| 229 |
+
3 => call_fire_stdcall3(worm, worm_eax.wrapping_add(0x34), local_struct, rb(0x51E2C0)),
|
| 230 |
+
4 => fire_weapon_special(subtype_34, worm_eax.wrapping_add(0x38), worm, local_struct),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 231 |
_ => {}
|
| 232 |
}
|
| 233 |
|
| 234 |
+
// Set completion flag after dispatch
|
| 235 |
+
core::ptr::write_volatile((worm + 0x3C) as *mut i32, 1);
|
| 236 |
}
|
| 237 |
|
| 238 |
// ββ Sub-function bridges ββββββββββββββββββββββββββββββββββββ
|
|
@@ -1529,7 +1529,7 @@ unsafe fn dump_missile_raw(ptr: u32) {
|
|
| 1529 |
let base = ptr as *const u32;
|
| 1530 |
let sections: &[(usize, usize, &str)] = &[
|
| 1531 |
(0x000, 0x030, "CTask base"),
|
| 1532 |
-
(0x030, 0x084, "CGameTask
|
| 1533 |
(0x084, 0x098, "CGameTask pos/speed (0x84β0x97)"),
|
| 1534 |
(0x098, 0x0FC, "CGameTask _unknown_98"),
|
| 1535 |
(0x0FC, 0x130, "CTaskMissile 0xFCβ0x12F"),
|
|
|
|
| 1529 |
let base = ptr as *const u32;
|
| 1530 |
let sections: &[(usize, usize, &str)] = &[
|
| 1531 |
(0x000, 0x030, "CTask base"),
|
| 1532 |
+
(0x030, 0x084, "CGameTask subclass_data"),
|
| 1533 |
(0x084, 0x098, "CGameTask pos/speed (0x84β0x97)"),
|
| 1534 |
(0x098, 0x0FC, "CGameTask _unknown_98"),
|
| 1535 |
(0x0FC, 0x130, "CTaskMissile 0xFCβ0x12F"),
|