paavohtl Claude Sonnet 4.6 commited on
Commit
ca71a43
·
1 Parent(s): f6c761a

feat: add .spr header parser with SprHeader and parse_spr_header()

Browse files
crates/openwa-core/src/render/mod.rs CHANGED
@@ -1,10 +1,12 @@
1
  pub mod gfx_dir;
2
  pub mod landscape;
3
  pub mod queue;
 
4
  pub mod sprite;
5
  pub mod turn_order;
6
 
7
  pub use landscape::{DirtyRect, PCLandscape};
 
8
  pub use queue::{
9
  DrawBitmapGlobalCmd, DrawLineStripHeader, DrawPixelCmd, DrawPolygonHeader, DrawRectCmd,
10
  DrawScaledCmd, DrawSpriteCmd, DrawSpriteOffsetCmd, DrawTextboxLocalCmd, RenderQueue,
 
1
  pub mod gfx_dir;
2
  pub mod landscape;
3
  pub mod queue;
4
+ pub mod spr;
5
  pub mod sprite;
6
  pub mod turn_order;
7
 
8
  pub use landscape::{DirtyRect, PCLandscape};
9
+ pub use spr::{SprError, SprHeader};
10
  pub use queue::{
11
  DrawBitmapGlobalCmd, DrawLineStripHeader, DrawPixelCmd, DrawPolygonHeader, DrawRectCmd,
12
  DrawScaledCmd, DrawSpriteCmd, DrawSpriteOffsetCmd, DrawTextboxLocalCmd, RenderQueue,
crates/openwa-core/src/render/spr.rs ADDED
@@ -0,0 +1,288 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ //! `.spr` sprite format parser.
2
+ //!
3
+ //! WA's sprite format stores paletted animated sprites with per-frame metadata.
4
+ //! This module provides both a clean Rust parser (`ParsedSprite`) for tooling
5
+ //! and shared metadata extraction (`parse_spr_header`) used by WormKit hook
6
+ //! replacements.
7
+
8
+ /// Errors from `.spr` parsing.
9
+ #[derive(Debug)]
10
+ pub enum SprError {
11
+ /// Data too short to contain required fields.
12
+ TooShort {
13
+ expected: usize,
14
+ actual: usize,
15
+ },
16
+ /// Internal structure is inconsistent.
17
+ InvalidData(&'static str),
18
+ }
19
+
20
+ impl core::fmt::Display for SprError {
21
+ fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
22
+ match self {
23
+ SprError::TooShort { expected, actual } => {
24
+ write!(f, "spr data too short: need {} bytes, got {}", expected, actual)
25
+ }
26
+ SprError::InvalidData(msg) => write!(f, "invalid spr data: {}", msg),
27
+ }
28
+ }
29
+ }
30
+
31
+ /// Parsed metadata and byte offsets into raw `.spr` data.
32
+ ///
33
+ /// Produced by `parse_spr_header`. Contains all scalar fields and the byte
34
+ /// offsets needed to locate palette, frame, and bitmap data within the raw
35
+ /// buffer. No heap allocation — just arithmetic on the input slice.
36
+ #[derive(Debug, Clone)]
37
+ pub struct SprHeader {
38
+ /// Total payload size from `.spr` header (+0x04). Used for global counter.
39
+ pub data_size: u32,
40
+ /// Header flags from `.spr` (+0x08).
41
+ pub header_flags: u16,
42
+ /// Number of RGB palette entries.
43
+ pub palette_count: u16,
44
+ /// Unknown field stored at sprite+0x08.
45
+ pub unknown_08: u16,
46
+ /// Animation speed (frames per second).
47
+ pub fps: u16,
48
+ /// Sprite flags.
49
+ pub flags: u16,
50
+ /// Sprite width in pixels.
51
+ pub width: u16,
52
+ /// Sprite height in pixels.
53
+ pub height: u16,
54
+ /// Frame count (1 if scaled).
55
+ pub frame_count: u16,
56
+ /// Max frames (same as frame_count after processing).
57
+ pub max_frames: u16,
58
+ /// Scale X: `((raw >> 8 & 0x7F) << 16) >> 5`, or 0 if not scaled.
59
+ pub scale_x: u32,
60
+ /// Scale Y: `((raw & 0x7F) << 16) >> 5`, or 0 if not scaled.
61
+ pub scale_y: u32,
62
+ /// True if the frame_count word encoded scaling instead of a count.
63
+ pub is_scaled: bool,
64
+ /// Number of secondary frames (0 if header_flags & 0x4000 is clear).
65
+ pub secondary_frame_count: u16,
66
+ /// Byte offset of RGB palette data (3 bytes per entry).
67
+ pub palette_offset: usize,
68
+ /// Byte offset of secondary frame table (0 if none).
69
+ pub secondary_frame_offset: usize,
70
+ /// Byte offset of main SpriteFrame metadata array.
71
+ pub frame_meta_offset: usize,
72
+ /// Byte offset of bitmap pixel data.
73
+ pub bitmap_offset: usize,
74
+ /// Size of bitmap pixel data in bytes.
75
+ pub bitmap_size: usize,
76
+ }
77
+
78
+ /// Align `offset` up to the next 4-byte boundary.
79
+ fn align4(offset: usize) -> usize {
80
+ (offset + 3) & !3
81
+ }
82
+
83
+ /// Parse `.spr` header and compute data region offsets.
84
+ ///
85
+ /// This is a pure function with no WA dependencies. Both `ParsedSprite::parse`
86
+ /// and the WormKit ProcessSprite hook build on this.
87
+ pub fn parse_spr_header(data: &[u8]) -> Result<SprHeader, SprError> {
88
+ // Minimum: 4 (unused) + 4 (data_size) + 2 (header_flags) + 2 (palette_count) = 12
89
+ if data.len() < 12 {
90
+ return Err(SprError::TooShort { expected: 12, actual: data.len() });
91
+ }
92
+
93
+ let data_size = u32::from_le_bytes([data[4], data[5], data[6], data[7]]);
94
+ let header_flags = u16::from_le_bytes([data[8], data[9]]);
95
+ let palette_count = u16::from_le_bytes([data[10], data[11]]);
96
+
97
+ let palette_offset = 12; // 0x0C
98
+ let palette_end = palette_offset + palette_count as usize * 3;
99
+
100
+ if data.len() < palette_end {
101
+ return Err(SprError::TooShort { expected: palette_end, actual: data.len() });
102
+ }
103
+
104
+ let mut cursor = palette_end;
105
+
106
+ // Secondary frame table (if header_flags & 0x4000)
107
+ let mut secondary_frame_count: u16 = 0;
108
+ let mut secondary_frame_offset: usize = 0;
109
+
110
+ if header_flags & 0x4000 != 0 {
111
+ if data.len() < cursor + 2 {
112
+ return Err(SprError::TooShort { expected: cursor + 2, actual: data.len() });
113
+ }
114
+ secondary_frame_count = u16::from_le_bytes([data[cursor], data[cursor + 1]]);
115
+ cursor = align4(cursor + 2);
116
+ secondary_frame_offset = cursor;
117
+ cursor += secondary_frame_count as usize * 12;
118
+ }
119
+
120
+ // Main frame header (12 bytes)
121
+ if data.len() < cursor + 12 {
122
+ return Err(SprError::TooShort { expected: cursor + 12, actual: data.len() });
123
+ }
124
+
125
+ let unknown_08 = u16::from_le_bytes([data[cursor], data[cursor + 1]]);
126
+ let fps = u16::from_le_bytes([data[cursor + 2], data[cursor + 3]]);
127
+ let flags = u16::from_le_bytes([data[cursor + 4], data[cursor + 5]]);
128
+ let width = u16::from_le_bytes([data[cursor + 6], data[cursor + 7]]);
129
+ let height = u16::from_le_bytes([data[cursor + 8], data[cursor + 9]]);
130
+ let frame_count_raw = u16::from_le_bytes([data[cursor + 10], data[cursor + 11]]);
131
+
132
+ cursor += 12;
133
+
134
+ // Scale processing
135
+ let (frame_count, max_frames, scale_x, scale_y, is_scaled) =
136
+ if (frame_count_raw as i16) < 0 {
137
+ let sx = (((frame_count_raw >> 8) & 0x7F) as u32) << 16 >> 5;
138
+ let sy = ((frame_count_raw & 0x7F) as u32) << 16 >> 5;
139
+ (1u16, 1u16, sx, sy, true)
140
+ } else {
141
+ (frame_count_raw, frame_count_raw, 0u32, 0u32, false)
142
+ };
143
+
144
+ // Frame metadata (aligned to 4 bytes)
145
+ let frame_meta_offset = align4(cursor);
146
+ let bitmap_offset = frame_meta_offset + frame_count as usize * 12;
147
+
148
+ let bitmap_size = if data.len() > bitmap_offset {
149
+ data.len() - bitmap_offset
150
+ } else {
151
+ 0
152
+ };
153
+
154
+ Ok(SprHeader {
155
+ data_size,
156
+ header_flags,
157
+ palette_count,
158
+ unknown_08,
159
+ fps,
160
+ flags,
161
+ width,
162
+ height,
163
+ frame_count,
164
+ max_frames,
165
+ scale_x,
166
+ scale_y,
167
+ is_scaled,
168
+ secondary_frame_count,
169
+ palette_offset,
170
+ secondary_frame_offset,
171
+ frame_meta_offset,
172
+ bitmap_offset,
173
+ bitmap_size,
174
+ })
175
+ }
176
+
177
+ #[cfg(test)]
178
+ mod tests {
179
+ use super::*;
180
+
181
+ /// Build a minimal single-frame .spr with given dimensions and palette.
182
+ fn make_spr(width: u16, height: u16, palette_count: u16, frame_count: u16) -> Vec<u8> {
183
+ let mut data = Vec::new();
184
+
185
+ // +0x00: unused magic
186
+ data.extend_from_slice(&0u32.to_le_bytes());
187
+ // +0x04: data_size (we'll fix up later)
188
+ let data_size_pos = data.len();
189
+ data.extend_from_slice(&0u32.to_le_bytes());
190
+ // +0x08: header_flags
191
+ data.extend_from_slice(&0u16.to_le_bytes());
192
+ // +0x0A: palette_entry_count
193
+ data.extend_from_slice(&palette_count.to_le_bytes());
194
+ // +0x0C: palette RGB entries (all zeros)
195
+ for _ in 0..palette_count {
196
+ data.extend_from_slice(&[0u8; 3]);
197
+ }
198
+
199
+ // Main frame header (12 bytes)
200
+ data.extend_from_slice(&0u16.to_le_bytes()); // unknown_08
201
+ data.extend_from_slice(&10u16.to_le_bytes()); // fps = 10
202
+ data.extend_from_slice(&0u16.to_le_bytes()); // flags
203
+ data.extend_from_slice(&width.to_le_bytes());
204
+ data.extend_from_slice(&height.to_le_bytes());
205
+ data.extend_from_slice(&frame_count.to_le_bytes());
206
+
207
+ // Align to 4 bytes
208
+ while data.len() % 4 != 0 {
209
+ data.push(0);
210
+ }
211
+
212
+ // Frame metadata (12 bytes per frame)
213
+ for i in 0..frame_count {
214
+ let offset = i as u32 * width as u32 * height as u32;
215
+ data.extend_from_slice(&offset.to_le_bytes()); // bitmap_offset
216
+ data.extend_from_slice(&0u16.to_le_bytes()); // start_x
217
+ data.extend_from_slice(&0u16.to_le_bytes()); // start_y
218
+ data.extend_from_slice(&width.to_le_bytes()); // end_x
219
+ data.extend_from_slice(&height.to_le_bytes()); // end_y
220
+ }
221
+
222
+ // Bitmap data (width * height * frame_count bytes)
223
+ let bitmap_bytes = width as usize * height as usize * frame_count as usize;
224
+ data.resize(data.len() + bitmap_bytes, 0);
225
+
226
+ // Fix up data_size (everything from +4 onward)
227
+ let total = (data.len() - 4) as u32;
228
+ data[data_size_pos..data_size_pos + 4].copy_from_slice(&total.to_le_bytes());
229
+
230
+ data
231
+ }
232
+
233
+ #[test]
234
+ fn parse_single_frame() {
235
+ let data = make_spr(32, 16, 4, 1);
236
+ let hdr = parse_spr_header(&data).unwrap();
237
+
238
+ assert_eq!(hdr.width, 32);
239
+ assert_eq!(hdr.height, 16);
240
+ assert_eq!(hdr.fps, 10);
241
+ assert_eq!(hdr.palette_count, 4);
242
+ assert_eq!(hdr.frame_count, 1);
243
+ assert!(!hdr.is_scaled);
244
+ assert_eq!(hdr.scale_x, 0);
245
+ assert_eq!(hdr.scale_y, 0);
246
+ assert_eq!(hdr.secondary_frame_count, 0);
247
+ assert_eq!(hdr.palette_offset, 12);
248
+ assert!(hdr.frame_meta_offset > 0);
249
+ assert!(hdr.bitmap_offset > hdr.frame_meta_offset);
250
+ }
251
+
252
+ #[test]
253
+ fn parse_multi_frame() {
254
+ let data = make_spr(8, 8, 2, 5);
255
+ let hdr = parse_spr_header(&data).unwrap();
256
+
257
+ assert_eq!(hdr.frame_count, 5);
258
+ assert_eq!(hdr.max_frames, 5);
259
+ // 5 frames * 12 bytes each = 60 bytes of frame metadata
260
+ assert_eq!(hdr.bitmap_offset - hdr.frame_meta_offset, 60);
261
+ }
262
+
263
+ #[test]
264
+ fn parse_scaled_sprite() {
265
+ // frame_count_raw = 0xA050: bit15 set, scale_x_raw = 0x20, scale_y_raw = 0x50
266
+ let mut data = make_spr(32, 32, 0, 1);
267
+ // Overwrite frame_count_raw in the main frame header
268
+ // palette_count = 0, so palette_end = 12
269
+ // main frame header starts at 12, frame_count is at offset 10 within it = byte 22
270
+ data[22] = 0x50; // low byte
271
+ data[23] = 0xA0; // high byte (bit 15 set)
272
+
273
+ let hdr = parse_spr_header(&data).unwrap();
274
+
275
+ assert!(hdr.is_scaled);
276
+ assert_eq!(hdr.frame_count, 1);
277
+ assert_eq!(hdr.max_frames, 1);
278
+ // scale_x = (0x20 << 16) >> 5 = 0x200000 >> 5 = 0x10000
279
+ assert_eq!(hdr.scale_x, (0x20u32 << 16) >> 5);
280
+ // scale_y = (0x50 << 16) >> 5 = 0x500000 >> 5 = 0x28000
281
+ assert_eq!(hdr.scale_y, (0x50u32 << 16) >> 5);
282
+ }
283
+
284
+ #[test]
285
+ fn parse_too_short() {
286
+ assert!(parse_spr_header(&[0; 4]).is_err());
287
+ }
288
+ }