feat: add ConstructSprite and ProcessSprite hook replacements
Browse filesReplaces ConstructSprite (0x4FAA30) and ProcessSprite (0x4FAB80) with
Rust implementations using parse_spr_header from openwa-core.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
crates/openwa-wormkit/src/replacements/mod.rs
CHANGED
|
@@ -15,6 +15,7 @@ mod replay_test;
|
|
| 15 |
mod scheme;
|
| 16 |
mod sound;
|
| 17 |
mod speech;
|
|
|
|
| 18 |
mod task;
|
| 19 |
mod team;
|
| 20 |
mod trace_desync;
|
|
@@ -52,6 +53,7 @@ pub fn install_all() -> Result<(), String> {
|
|
| 52 |
weapon::install()?;
|
| 53 |
team::install()?;
|
| 54 |
render::install()?;
|
|
|
|
| 55 |
sound::install()?;
|
| 56 |
speech::install()?;
|
| 57 |
ddgame_init::install()?;
|
|
|
|
| 15 |
mod scheme;
|
| 16 |
mod sound;
|
| 17 |
mod speech;
|
| 18 |
+
mod sprite;
|
| 19 |
mod task;
|
| 20 |
mod team;
|
| 21 |
mod trace_desync;
|
|
|
|
| 53 |
weapon::install()?;
|
| 54 |
team::install()?;
|
| 55 |
render::install()?;
|
| 56 |
+
sprite::install()?;
|
| 57 |
sound::install()?;
|
| 58 |
speech::install()?;
|
| 59 |
ddgame_init::install()?;
|
crates/openwa-wormkit/src/replacements/sprite.rs
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
//! Sprite loading hook replacements.
|
| 2 |
+
//!
|
| 3 |
+
//! Replaces ConstructSprite (0x4FAA30) and ProcessSprite (0x4FAB80) with
|
| 4 |
+
//! Rust implementations. Uses `parse_spr_header` from openwa-core for
|
| 5 |
+
//! format parsing.
|
| 6 |
+
|
| 7 |
+
use openwa_core::address::va;
|
| 8 |
+
use openwa_core::rebase::rb;
|
| 9 |
+
|
| 10 |
+
use crate::hook::{self, usercall_trampoline};
|
| 11 |
+
|
| 12 |
+
// ---------------------------------------------------------------------------
|
| 13 |
+
// ConstructSprite (0x4FAA30) — usercall(EAX=sprite, ECX=context), plain RET
|
| 14 |
+
// ---------------------------------------------------------------------------
|
| 15 |
+
|
| 16 |
+
usercall_trampoline!(fn trampoline_construct_sprite; impl_fn = construct_sprite_impl;
|
| 17 |
+
regs = [eax, ecx]);
|
| 18 |
+
|
| 19 |
+
unsafe extern "cdecl" fn construct_sprite_impl(sprite: u32, context: u32) {
|
| 20 |
+
let p = sprite as *mut u8;
|
| 21 |
+
|
| 22 |
+
// Zero the entire 0x70-byte struct first
|
| 23 |
+
core::ptr::write_bytes(p, 0, 0x70);
|
| 24 |
+
|
| 25 |
+
// Vtable
|
| 26 |
+
*(p as *mut u32) = rb(va::SPRITE_VTABLE);
|
| 27 |
+
// Context pointer (+0x04)
|
| 28 |
+
*(p.add(0x04) as *mut u32) = context;
|
| 29 |
+
// DisplayGfx vtable (+0x34)
|
| 30 |
+
*(p.add(0x34) as *mut u32) = rb(va::DISPLAYGFX_VTABLE);
|
| 31 |
+
// _unknown_38 = 1 (+0x38)
|
| 32 |
+
*(p.add(0x38) as *mut u32) = 1;
|
| 33 |
+
// _unknown_40 = 8 (+0x40)
|
| 34 |
+
*(p.add(0x40) as *mut u32) = 8;
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
// ---------------------------------------------------------------------------
|
| 38 |
+
// ProcessSprite (0x4FAB80)
|
| 39 |
+
// usercall(EAX=sprite, ECX=palette_ctx) + 1 stack(raw_data), RET 0x4
|
| 40 |
+
// ---------------------------------------------------------------------------
|
| 41 |
+
|
| 42 |
+
usercall_trampoline!(fn trampoline_process_sprite; impl_fn = process_sprite_impl;
|
| 43 |
+
regs = [eax, ecx]; stack_params = 1; ret_bytes = "0x4");
|
| 44 |
+
|
| 45 |
+
static mut PALETTE_MAP_COLOR_ADDR: u32 = 0;
|
| 46 |
+
|
| 47 |
+
/// Call WA's PaletteContext__MapColor to find the nearest display palette index.
|
| 48 |
+
#[cfg(target_arch = "x86")]
|
| 49 |
+
unsafe fn palette_map_color(palette_ctx: u32, rgb: u32) -> u8 {
|
| 50 |
+
let f: unsafe extern "thiscall" fn(u32, u32) -> u32 =
|
| 51 |
+
core::mem::transmute(PALETTE_MAP_COLOR_ADDR as usize);
|
| 52 |
+
f(palette_ctx, rgb) as u8
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
unsafe extern "cdecl" fn process_sprite_impl(
|
| 56 |
+
sprite: u32,
|
| 57 |
+
palette_ctx: u32,
|
| 58 |
+
raw_data: u32,
|
| 59 |
+
) -> u32 {
|
| 60 |
+
use openwa_core::render::spr::parse_spr_header;
|
| 61 |
+
|
| 62 |
+
let p = sprite as *mut u8;
|
| 63 |
+
let data_ptr = raw_data as *const u8;
|
| 64 |
+
|
| 65 |
+
// We need the data as a slice. Use data_size from header to determine length.
|
| 66 |
+
// data_size is at raw_data + 4.
|
| 67 |
+
let data_size = *(data_ptr.add(4) as *const u32);
|
| 68 |
+
// Total buffer: data_size covers from offset +4 onward, so total = data_size + 4.
|
| 69 |
+
let data_len = (data_size + 4) as usize;
|
| 70 |
+
let data = core::slice::from_raw_parts(data_ptr, data_len);
|
| 71 |
+
|
| 72 |
+
let hdr = match parse_spr_header(data) {
|
| 73 |
+
Ok(h) => h,
|
| 74 |
+
Err(e) => {
|
| 75 |
+
// This should never happen with valid WA game data
|
| 76 |
+
panic!("ProcessSprite: failed to parse .spr data: {}", e);
|
| 77 |
+
}
|
| 78 |
+
};
|
| 79 |
+
|
| 80 |
+
// --- Update global counter: data_size ---
|
| 81 |
+
let g_data_bytes = rb(va::G_SPRITE_DATA_BYTES) as *mut u32;
|
| 82 |
+
*g_data_bytes = (*g_data_bytes).wrapping_add(data_size);
|
| 83 |
+
|
| 84 |
+
// --- Store raw_frame_header_ptr (points to header_flags in raw buffer) ---
|
| 85 |
+
*(p.add(0x60) as *mut *const u8) = data_ptr.add(8);
|
| 86 |
+
|
| 87 |
+
// --- Store header_flags ---
|
| 88 |
+
*(p.add(0x14) as *mut u16) = hdr.header_flags;
|
| 89 |
+
|
| 90 |
+
// --- Build palette lookup table IN PLACE ---
|
| 91 |
+
// palette_data_ptr points to raw_data + 0x0A (the palette_entry_count field).
|
| 92 |
+
// WA overwrites this region with a 1-indexed palette index lookup table:
|
| 93 |
+
// [0] = 0 (transparent)
|
| 94 |
+
// [1..N] = PaletteContext__MapColor(rgb) for each entry
|
| 95 |
+
let palette_base = data_ptr.add(0x0A) as *mut u8;
|
| 96 |
+
*(p.add(0x68) as *mut *mut u8) = palette_base;
|
| 97 |
+
|
| 98 |
+
// Update global counter: palette bytes
|
| 99 |
+
let g_palette = rb(va::G_SPRITE_PALETTE_BYTES) as *mut u32;
|
| 100 |
+
*g_palette = (*g_palette).wrapping_add(hdr.palette_count as u32 * 3);
|
| 101 |
+
|
| 102 |
+
// Write transparent index
|
| 103 |
+
*palette_base = 0;
|
| 104 |
+
|
| 105 |
+
// Map each RGB entry to display palette index
|
| 106 |
+
let rgb_start = data_ptr.add(hdr.palette_offset);
|
| 107 |
+
for i in 0..hdr.palette_count as usize {
|
| 108 |
+
// Read 4 bytes (3 RGB + 1 from next entry, matching WA behavior)
|
| 109 |
+
let rgb_val = *(rgb_start.add(i * 3) as *const u32);
|
| 110 |
+
let mapped = palette_map_color(palette_ctx, rgb_val);
|
| 111 |
+
*palette_base.add(1 + i) = mapped;
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
// --- Secondary frame table (if header_flags & 0x4000) ---
|
| 115 |
+
let has_secondary = hdr.header_flags & 0x4000 != 0;
|
| 116 |
+
if has_secondary {
|
| 117 |
+
*(p.add(0x30) as *mut u16) = hdr.secondary_frame_count;
|
| 118 |
+
*(p.add(0x2C) as *mut *const u8) = data_ptr.add(hdr.secondary_frame_offset);
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
// --- Main frame header fields ---
|
| 122 |
+
// Copy unknown_08 + fps as a single u32 (matching WA's 4-byte copy)
|
| 123 |
+
let frame_header_ptr = if has_secondary {
|
| 124 |
+
// After secondary frames
|
| 125 |
+
data_ptr.add(hdr.secondary_frame_offset + hdr.secondary_frame_count as usize * 12)
|
| 126 |
+
} else {
|
| 127 |
+
data_ptr.add(hdr.palette_offset + hdr.palette_count as usize * 3)
|
| 128 |
+
};
|
| 129 |
+
|
| 130 |
+
// WA copies 4 bytes at once: *(u32*)(sprite+8) = *(u32*)(frame_header)
|
| 131 |
+
*(p.add(0x08) as *mut u32) = *(frame_header_ptr as *const u32);
|
| 132 |
+
*(p.add(0x10) as *mut u16) = hdr.flags;
|
| 133 |
+
*(p.add(0x0C) as *mut u16) = hdr.width;
|
| 134 |
+
*(p.add(0x0E) as *mut u16) = hdr.height;
|
| 135 |
+
*(p.add(0x12) as *mut u16) = hdr.frame_count;
|
| 136 |
+
*(p.add(0x16) as *mut u16) = hdr.max_frames;
|
| 137 |
+
|
| 138 |
+
// --- Scale fields ---
|
| 139 |
+
if hdr.is_scaled {
|
| 140 |
+
*(p.add(0x1C) as *mut u32) = hdr.scale_x;
|
| 141 |
+
*(p.add(0x20) as *mut u32) = hdr.scale_y;
|
| 142 |
+
*(p.add(0x24) as *mut u32) = 1; // is_scaled
|
| 143 |
+
} else {
|
| 144 |
+
*(p.add(0x24) as *mut u32) = 0;
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
// --- Frame metadata and bitmap pointers ---
|
| 148 |
+
let frame_meta_ptr = data_ptr.add(hdr.frame_meta_offset);
|
| 149 |
+
let bitmap_ptr = data_ptr.add(hdr.bitmap_offset);
|
| 150 |
+
*(p.add(0x28) as *mut *const u8) = frame_meta_ptr;
|
| 151 |
+
*(p.add(0x64) as *mut *const u8) = bitmap_ptr;
|
| 152 |
+
|
| 153 |
+
// --- Bitmap palette remapping (only when NO secondary frames) ---
|
| 154 |
+
if !has_secondary {
|
| 155 |
+
// Remap every bitmap byte: pixel = lookup_table[pixel]
|
| 156 |
+
let bitmap_start_relative = hdr.bitmap_offset;
|
| 157 |
+
let bitmap_byte_count_raw = (data_size as usize + 4).saturating_sub(bitmap_start_relative);
|
| 158 |
+
let dword_count = (bitmap_byte_count_raw + 3) / 4;
|
| 159 |
+
let remap_byte_count = dword_count * 4;
|
| 160 |
+
|
| 161 |
+
let bmp = bitmap_ptr as *mut u8;
|
| 162 |
+
for i in 0..remap_byte_count {
|
| 163 |
+
let idx = *bmp.add(i) as usize;
|
| 164 |
+
*bmp.add(i) = *palette_base.add(idx);
|
| 165 |
+
}
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
// --- Update global counters: pixel area and frame count ---
|
| 169 |
+
if hdr.frame_count > 0 {
|
| 170 |
+
let g_pixel_area = rb(va::G_SPRITE_PIXEL_AREA) as *mut u32;
|
| 171 |
+
let frames_ptr = frame_meta_ptr as *const [u8; 12];
|
| 172 |
+
for i in 0..hdr.frame_count as usize {
|
| 173 |
+
let frame = &*frames_ptr.add(i);
|
| 174 |
+
let start_x = i16::from_le_bytes([frame[4], frame[5]]);
|
| 175 |
+
let start_y = i16::from_le_bytes([frame[6], frame[7]]);
|
| 176 |
+
let end_x = i16::from_le_bytes([frame[8], frame[9]]);
|
| 177 |
+
let end_y = i16::from_le_bytes([frame[10], frame[11]]);
|
| 178 |
+
let area = (end_x as i32 - start_x as i32) * (end_y as i32 - start_y as i32);
|
| 179 |
+
*g_pixel_area = (*g_pixel_area).wrapping_add(area as u32);
|
| 180 |
+
}
|
| 181 |
+
}
|
| 182 |
+
|
| 183 |
+
let g_frame_count = rb(va::G_SPRITE_FRAME_COUNT) as *mut u32;
|
| 184 |
+
*g_frame_count = (*g_frame_count).wrapping_add(hdr.frame_count as u32);
|
| 185 |
+
|
| 186 |
+
1 // success
|
| 187 |
+
}
|
| 188 |
+
|
| 189 |
+
// ---------------------------------------------------------------------------
|
| 190 |
+
// Installation
|
| 191 |
+
// ---------------------------------------------------------------------------
|
| 192 |
+
|
| 193 |
+
pub fn install() -> Result<(), String> {
|
| 194 |
+
unsafe {
|
| 195 |
+
PALETTE_MAP_COLOR_ADDR = rb(va::PALETTE_CONTEXT_MAP_COLOR);
|
| 196 |
+
|
| 197 |
+
let _ = hook::install(
|
| 198 |
+
"ConstructSprite",
|
| 199 |
+
va::CONSTRUCT_SPRITE,
|
| 200 |
+
trampoline_construct_sprite as *const (),
|
| 201 |
+
)?;
|
| 202 |
+
|
| 203 |
+
let _ = hook::install(
|
| 204 |
+
"ProcessSprite",
|
| 205 |
+
va::PROCESS_SPRITE,
|
| 206 |
+
trampoline_process_sprite as *const (),
|
| 207 |
+
)?;
|
| 208 |
+
}
|
| 209 |
+
Ok(())
|
| 210 |
+
}
|