paavohtl Claude Sonnet 4.6 commited on
Commit
0f02b41
·
1 Parent(s): 95a1173

feat: add ConstructSprite and ProcessSprite hook replacements

Browse files

Replaces 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
+ }