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