Extract the track thumbnails and the ball sprite bank

- Track files: decode the selector thumbnails (a 640x64 16-colour
  picture, four sequential bitplanes, six 123x32 tiles in a 3x2 grid) into
  tracks/<name>/thumb.png, and show them in the menu at double height
  (they are Amiga hi-res)
- gfx/ball.spr: decode the 64-frame attached-sprite bank (spinning ball
  with the Acid logo) into gfx/ball.png; its colours are not in the file
- Command-line output no longer panics when piped into e.g. head
This commit is contained in:
2026-10-19 20:43:34 +00:00
parent 9d82d99c15
commit c36572f8dc
32 changed files with 262 additions and 36 deletions
+14 -2
View File
@@ -47,7 +47,14 @@ Plain text; lines starting with `#` are comments.
- **`start`, `flags`:** values from the original data that the game - **`start`, `flags`:** values from the original data that the game
doesn't use. doesn't use.
To add a track, make a new folder with both files. Tracks are listed in ### `thumb.png` (optional)
The track's 123 × 32 thumbnail from the original track-select screen, in
the track's palette. It's drawn at double height in the menu, because the
original's pixels are half-width. Without one, the menu shows a shrunken
`track.png`.
To add a track, make a new folder with `track.png` and `track.txt`. Tracks are listed in
name order. name order.
## Vehicles: `vehicles/<id>.png` and `<id>.txt` ## Vehicles: `vehicles/<id>.png` and `<id>.txt`
@@ -125,7 +132,12 @@ files, the first in name order plays.
## Pictures: `gfx/*.png` ## Pictures: `gfx/*.png`
Indexed PNGs. `skidtoon.png` is the title picture at the top of the menu, Indexed PNGs. `skidtoon.png` is the title picture at the top of the menu,
shown at half size. The others aren't used. shown at half size. The others aren't used:
- `ball.png`: 64 frames of a spinning ball with the Acid logo, 16 frames
across, each 32 × 33. Its real colours aren't known, so it has a grey
palette.
- `chat2.png`, `mouse.png`, `sign.png`: small odds and ends.
## Palettes: `palettes/*.pal` (optional) ## Palettes: `palettes/*.pal` (optional)
Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 856 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1013 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 941 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

+25 -6
View File
@@ -58,7 +58,7 @@ consumed to its first byte.
``` ```
0x00 7 × u32 end offset of sections 0..6 0x00 7 × u32 end offset of sections 0..6
0x1C … section 0: header for the whole file (20622 bytes, raw) 0x1C … section 0: thumbnails and palette for the whole file (20622 bytes, raw)
… sections 1..6: one track each, CrunchMania-packed … sections 1..6: one track each, CrunchMania-packed
``` ```
@@ -73,8 +73,16 @@ The last 142 bytes of section 0 hold:
| 98 | 12 | Unknown | | 98 | 12 | Unknown |
| 110 | … | The name of the set of tracks, NUL-terminated | | 110 | … | The name of the set of tracks, NUL-terminated |
The rest of section 0 is the track-selector thumbnails, which aren't The first 20480 bytes of section 0 are the track-selector thumbnails:
decoded. - **Picture:** one 640 × 64, 16-colour picture in the file's palette,
stored as four bitplanes one after another (80 bytes per row, 5120 bytes
per plane).
- **Layout:** the six tracks' thumbnails, 123 × 32 each, sit in a 3 × 2
grid at its top left, in track order: top row tracks 1–3, bottom row
4–6.
- **Display:** the picture is Amiga hi-res, whose pixels are half as wide
as low-res ones. So a thumbnail shows at 123 × 64 next to the low-res
track pictures, the same shape as the track.
Each track section unpacks to 281240 (`0x44A98`) bytes: Each track section unpacks to 281240 (`0x44A98`) bytes:
@@ -166,7 +174,18 @@ extra and alternative sounds (`crash1`, `crash2`, `engine`, `motor`,
IFF ILBM with ByteRun1 compression. `skidtoon.iff` is the 608 × 180, IFF ILBM with ByteRun1 compression. `skidtoon.iff` is the 608 × 180,
16-colour title picture; `chat2`, `mouse` and `sign` are small odds and 16-colour title picture; `chat2`, `mouse` and `sign` are small odds and
ends. `ball.spr` (34816 bytes) is in another format and isn't decoded. ends.
`ball.spr` is a bank of 64 saved hardware sprites: a spinning ball with
the Acid logo. Each 544-byte frame is:
- **Header** (16 bytes): `u32` saved memory pointer, `u16` lines (33),
`u16` strips (4), `u16` flags (`0x8020`), `u16` bytes per strip (132),
4 unused bytes.
- **Strips:** four 16-pixel sprites, each line two bitplane words.
Flag bit 15 marks attached sprites. The strips pair up into two 16-colour
columns, so a frame is 32 × 33 pixels. The bank carries no colours.
## Music (`sfx/tune.crn`) ## Music (`sfx/tune.crn`)
@@ -202,8 +221,8 @@ first minute), and it loops at the same point.
decoded; this port's versions are reconstructions. Decoding the `.set` decoded; this port's versions are reconstructions. Decoding the `.set`
values would be the first step towards the original handling. values would be the first step towards the original handling.
- **Unknown data:** the 12 bytes after each track file's palette, the bit - **Unknown data:** the 12 bytes after each track file's palette, the bit
pattern after the gates in each track section, the thumbnails, and pattern after the gates in each track section, and the colours of
`ball.spr`. `ball.spr` (probably set by the executable).
The engine's own editable formats, which `skids extract` converts all of The engine's own editable formats, which `skids extract` converts all of
the above into, are described in [assets/README.md](../assets/README.md). the above into, are described in [assets/README.md](../assets/README.md).
+16 -2
View File
@@ -233,8 +233,22 @@ impl App {
fn thumb(&mut self, i: usize) -> Option<&Vec<u32>> { fn thumb(&mut self, i: usize) -> Option<&Vec<u32>> {
if !self.thumbs.contains_key(&i) { if !self.thumbs.contains_key(&i) {
let t = self.data.load_track(i).ok()?; let t = self.data.load_track(i).ok()?;
self.thumbs let img = match &t.thumbnail {
.insert(i, render::downscale(&t.rgb(), THUMB_W, THUMB_H)); // The original thumbnail: Amiga hi-res pixels, so each row is
// shown twice to keep its shape.
Some(px) => {
let pal: Vec<u32> = t.palette.iter().map(|c| c.to_u32()).collect();
let mut out = vec![0x101010u32; THUMB_W * THUMB_H];
for y in 0..THUMB_H {
for x in 0..track::THUMB_W.min(THUMB_W) {
out[y * THUMB_W + x] = pal[px[(y / 2) * track::THUMB_W + x] as usize];
}
}
out
}
None => render::downscale(&t.rgb(), THUMB_W, THUMB_H),
};
self.thumbs.insert(i, img);
} }
self.thumbs.get(&i) self.thumbs.get(&i)
} }
+18 -2
View File
@@ -299,8 +299,14 @@ impl GameData {
let mut t = match &entry.source { let mut t = match &entry.source {
TrackSource::Asset { png, txt } => { TrackSource::Asset { png, txt } => {
let txt = String::from_utf8_lossy(self.files.get(txt).unwrap_or_default()); let txt = String::from_utf8_lossy(self.files.get(txt).unwrap_or_default());
Track::from_assets(&entry.name, self.files.get(png).unwrap_or_default(), &txt) let mut t =
.map_err(|e| format!("{}: {e}", entry.name))? Track::from_assets(&entry.name, self.files.get(png).unwrap_or_default(), &txt)
.map_err(|e| format!("{}: {e}", entry.name))?;
let thumb = png.replace("/track.png", "/thumb.png");
if let Some(d) = self.files.get(&thumb) {
t.set_thumb_png(d).map_err(|e| format!("{thumb}: {e}"))?;
}
t
} }
TrackSource::Packed { file, section } => self.track_files[*file].decode(*section)?, TrackSource::Packed { file, section } => self.track_files[*file].decode(*section)?,
}; };
@@ -383,6 +389,16 @@ fn load_media(files: &FileSet, warnings: &mut Vec<String>) -> Media {
} }
} }
} }
for (key, data) in &files.files {
if let Some(name) = stem(key, "gfx", ".spr") {
match iff::read_sprite_bank(data) {
Ok(p) => {
pictures.insert(name, p);
}
Err(e) => warnings.push(format!("{key}: {e}")),
}
}
}
for (key, data) in &files.files { for (key, data) in &files.files {
if let Some(name) = stem(key, "gfx", ".png") { if let Some(name) = stem(key, "gfx", ".png") {
match image::read_indexed(data, &palette::TRACK_DEFAULT) { match image::read_indexed(data, &palette::TRACK_DEFAULT) {
+64 -2
View File
@@ -3,6 +3,10 @@
//! File layout: //! File layout:
//! 0x00 7 x BE32 end offsets. Section 0 runs from 0x1c to the first offset //! 0x00 7 x BE32 end offsets. Section 0 runs from 0x1c to the first offset
//! and holds the selector thumbnails; sections 1..6 are tracks. //! and holds the selector thumbnails; sections 1..6 are tracks.
//! Section 0 starts with the selector thumbnails: a 640x64 16-colour
//! picture, four bitplanes one after another (80 bytes per row), with
//! the six tracks' 123x32 thumbnails in a 3x2 grid at its top left.
//! Its pixels are Amiga hi-res, so each shows at 123x64 on screen.
//! The last 142 bytes of section 0 are: BE16 file number, the 16 //! The last 142 bytes of section 0 are: BE16 file number, the 16
//! colour palette shared by all six tracks as 48 BE16 values (R, G, B //! colour palette shared by all six tracks as 48 BE16 values (R, G, B
//! per colour, 0-15 each), 12 bytes of unknown settings, then the //! per colour, 0-15 each), 12 bytes of unknown settings, then the
@@ -50,11 +54,16 @@ pub fn on_screen(sx: f32, sy: f32, margin: f32) -> bool {
sx >= margin && sy >= margin && sx < W as f32 - margin && sy < H as f32 - margin sx >= margin && sy >= margin && sx < W as f32 - margin && sy < H as f32 - margin
} }
pub const THUMB_W: usize = 123;
pub const THUMB_H: usize = 32;
pub struct TrackFile { pub struct TrackFile {
pub name: String, pub name: String,
pub number: u32, pub number: u32,
pub palette: [Rgb; 16], pub palette: [Rgb; 16],
pub set_name: String, pub set_name: String,
/// The six selector thumbnails, THUMB_W x THUMB_H palette indices each.
pub thumbnails: Vec<Vec<u8>>,
/// Packed track sections (index 0 = first playable track). /// Packed track sections (index 0 = first playable track).
pub sections: Vec<Vec<u8>>, pub sections: Vec<Vec<u8>>,
} }
@@ -75,6 +84,8 @@ pub struct Track {
pub name: String, pub name: String,
/// The set of six tracks this one belongs to. /// The set of six tracks this one belongs to.
pub set_name: String, pub set_name: String,
/// The original selector thumbnail, THUMB_W x THUMB_H palette indices.
pub thumbnail: Option<Vec<u8>>,
/// Palette indices, W x H. /// Palette indices, W x H.
pub pixels: Vec<u8>, pub pixels: Vec<u8>,
pub palette: [Rgb; 16], pub palette: [Rgb; 16],
@@ -112,13 +123,16 @@ impl TrackFile {
.trim_start_matches(|c: char| !c.is_ascii_digit()) .trim_start_matches(|c: char| !c.is_ascii_digit())
.parse() .parse()
.unwrap_or(u32::MAX); .unwrap_or(u32::MAX);
let (palette, set_name) = file_header(&d[SECTIONS * 4..ends[0]]) let header = &d[SECTIONS * 4..ends[0]];
.unwrap_or((palette::TRACK_DEFAULT, String::new())); let thumbnails = thumbnails(header);
let (palette, set_name) =
file_header(header).unwrap_or((palette::TRACK_DEFAULT, String::new()));
Ok(TrackFile { Ok(TrackFile {
name: name.to_string(), name: name.to_string(),
number, number,
palette, palette,
set_name, set_name,
thumbnails,
sections, sections,
}) })
} }
@@ -132,10 +146,38 @@ impl TrackFile {
let mut t = Track::from_section(self.track_name(section), &raw)?; let mut t = Track::from_section(self.track_name(section), &raw)?;
t.palette = self.palette; t.palette = self.palette;
t.set_name = self.set_name.clone(); t.set_name = self.set_name.clone();
t.thumbnail = self.thumbnails.get(section).cloned();
Ok(t) Ok(t)
} }
} }
/// The six thumbnails from the start of section 0.
fn thumbnails(s: &[u8]) -> Vec<Vec<u8>> {
const ROW: usize = 80;
const ROWS: usize = 64;
let plane = ROW * ROWS;
if s.len() < plane * 4 {
return Vec::new();
}
let px = |x: usize, y: usize| -> u8 {
(0..4)
.map(|p| ((s[p * plane + y * ROW + x / 8] >> (7 - x % 8)) & 1) << p)
.sum()
};
(0..6)
.map(|i| {
let (ox, oy) = ((i % 3) * THUMB_W, (i / 3) * THUMB_H);
let mut t = Vec::with_capacity(THUMB_W * THUMB_H);
for y in 0..THUMB_H {
for x in 0..THUMB_W {
t.push(px(ox + x, oy + y));
}
}
t
})
.collect()
}
/// Palette and set name from the tail of section 0. /// Palette and set name from the tail of section 0.
fn file_header(s: &[u8]) -> Option<([Rgb; 16], String)> { fn file_header(s: &[u8]) -> Option<([Rgb; 16], String)> {
let tail = s.get(s.len().checked_sub(142)?..)?; let tail = s.get(s.len().checked_sub(142)?..)?;
@@ -197,6 +239,7 @@ impl Track {
pixels, pixels,
palette: palette::TRACK_DEFAULT, palette: palette::TRACK_DEFAULT,
set_name: String::new(), set_name: String::new(),
thumbnail: None,
height, height,
gates, gates,
start, start,
@@ -336,6 +379,7 @@ impl Track {
pixels: img.pixels, pixels: img.pixels,
palette: pal, palette: pal,
set_name, set_name,
thumbnail: None,
height, height,
gates, gates,
start, start,
@@ -343,6 +387,24 @@ impl Track {
}) })
} }
/// The thumbnail as an indexed PNG in the track's palette.
pub fn thumb_png(&self) -> Option<Result<Vec<u8>, String>> {
self.thumbnail
.as_ref()
.map(|t| image::write_indexed(THUMB_W, THUMB_H, &self.palette, false, t))
}
pub fn set_thumb_png(&mut self, png: &[u8]) -> Result<(), String> {
let img = image::read_indexed(png, &self.palette)?;
if (img.w, img.h) != (THUMB_W, THUMB_H) || img.pixels.iter().any(|&p| p > 15) {
return Err(format!(
"thumb.png must be {THUMB_W}x{THUMB_H} using colours 0-15"
));
}
self.thumbnail = Some(img.pixels);
Ok(())
}
pub fn to_png(&self) -> Result<Vec<u8>, String> { pub fn to_png(&self) -> Result<Vec<u8>, String> {
image::write_indexed(W, H, &self.palette, false, &self.pixels) image::write_indexed(W, H, &self.palette, false, &self.pixels)
} }
+85
View File
@@ -170,6 +170,74 @@ pub fn read_ilbm(d: &[u8]) -> Result<Picture, String> {
}) })
} }
/// Reads a bank of hardware sprites saved by the game (`gfx/ball.spr`) as
/// one picture sheet, 16 frames across.
///
/// Each frame is a 16-byte header (BE32 saved pointer, BE16 lines, BE16
/// strip count, BE16 flags, BE16 bytes per strip, 4 unused) followed by the
/// strips: 16-pixel sprites with two bitplane words per line. With flag bit
/// 15 set, strips pair up as attached sprites (16 colours, 16 pixels per
/// pair); otherwise each strip is a 4-colour, 16-pixel column. The bank
/// carries no colours, so the sheet gets a grey ramp.
pub fn read_sprite_bank(d: &[u8]) -> Result<Picture, String> {
if d.len() < 16 {
return Err("too short for a sprite bank".into());
}
let lines = be16(d, 4) as usize;
let strips = be16(d, 6) as usize;
let attached = be16(d, 8) & 0x8000 != 0 && strips.is_multiple_of(2);
let per_strip = be16(d, 10) as usize;
if lines == 0 || strips == 0 || per_strip != lines * 4 {
return Err("not a sprite bank".into());
}
let record = 16 + strips * per_strip;
let frames = d.len() / record;
let fw = if attached {
strips / 2 * 16
} else {
strips * 16
};
let (cols, rows) = (frames.min(16), frames.div_ceil(16));
let (w, h) = (cols * fw, rows * lines);
let mut pixels = vec![0u8; w * h];
for f in 0..frames {
let r = &d[f * record..(f + 1) * record];
if be16(r, 4) as usize != lines || be16(r, 6) as usize != strips {
return Err(format!("frame {f} has a different size"));
}
let strip_px = |s: usize, x: usize, y: usize| -> u8 {
let o = 16 + s * per_strip + y * 4;
let bit = 0x8000 >> x;
((be16(r, o) & bit != 0) as u8) | (((be16(r, o + 2) & bit != 0) as u8) << 1)
};
let (ox, oy) = ((f % 16) * fw, (f / 16) * lines);
for y in 0..lines {
for x in 0..fw {
let v = if attached {
let pair = x / 16;
strip_px(pair * 2, x % 16, y) | (strip_px(pair * 2 + 1, x % 16, y) << 2)
} else {
strip_px(x / 16, x % 16, y)
};
pixels[(oy + y) * w + ox + x] = v;
}
}
}
let n = if attached { 16 } else { 4 };
let palette = (0..n)
.map(|i| {
let v = (i * 255 / (n - 1)) as u8;
(v, v, v)
})
.collect();
Ok(Picture {
w,
h,
pixels,
palette,
})
}
/// Writes 8-bit unsigned mono WAV, with the loop start in a `smpl` chunk so /// Writes 8-bit unsigned mono WAV, with the loop start in a `smpl` chunk so
/// sample editors keep it. /// sample editors keep it.
pub fn sound_to_wav(s: &Sound) -> Vec<u8> { pub fn sound_to_wav(s: &Sound) -> Vec<u8> {
@@ -277,6 +345,23 @@ mod tests {
assert_eq!(back, s); assert_eq!(back, s);
} }
#[test]
fn attached_sprite_bank() {
// One frame: 1 line, 2 attached strips; pixel 0 uses all four planes.
let mut d = vec![0u8; 16 + 2 * 4];
d[5] = 1;
d[7] = 2;
d[8] = 0x80;
d[11] = 4;
for o in [16, 18, 20, 22] {
d[o] = 0x80;
}
let p = read_sprite_bank(&d).unwrap();
assert_eq!((p.w, p.h), (16, 1));
assert_eq!(p.pixels[0], 15);
assert_eq!(p.pixels[1], 0);
}
#[test] #[test]
fn byterun1_picture() { fn byterun1_picture() {
// 16x1, 1 plane: run of 2 x 0xff. // 16x1, 1 plane: run of 2 x 0xff.
+40 -22
View File
@@ -3,6 +3,17 @@
//! The engine reads the original Amiga data (ADF images, the LHA archives on //! The engine reads the original Amiga data (ADF images, the LHA archives on
//! them, or extracted files) at run time; no game assets are bundled. //! them, or extracted files) at run time; no game assets are bundled.
/// Like `println!`, but a closed pipe (e.g. `skids info | head`) ends the
/// program quietly instead of panicking.
macro_rules! out {
($($arg:tt)*) => {{
use std::io::Write;
if writeln!(std::io::stdout(), $($arg)*).is_err() {
std::process::exit(0);
}
}};
}
mod app; mod app;
mod audio; mod audio;
mod data; mod data;
@@ -202,24 +213,24 @@ fn main() -> ExitCode {
fn info(data: &GameData, dir: Option<&Path>) { fn info(data: &GameData, dir: Option<&Path>) {
let built_in = data::embedded::files().len(); let built_in = data::embedded::files().len();
match (built_in, dir) { match (built_in, dir) {
(0, Some(d)) => println!("Data: {}", d.display()), (0, Some(d)) => out!("Data: {}", d.display()),
(n, Some(d)) => println!("Data: built in ({n} files), plus {}", d.display()), (n, Some(d)) => out!("Data: built in ({n} files), plus {}", d.display()),
(n, None) => println!("Data: built in ({n} files)"), (n, None) => out!("Data: built in ({n} files)"),
} }
println!("Tracks:"); out!("Tracks:");
for (i, entry) in data.tracks.iter().enumerate() { for (i, entry) in data.tracks.iter().enumerate() {
let from = match entry.source { let from = match entry.source {
data::TrackSource::Asset { .. } => "asset", data::TrackSource::Asset { .. } => "asset",
data::TrackSource::Packed { .. } => "original data", data::TrackSource::Packed { .. } => "original data",
}; };
match data.load_track(i) { match data.load_track(i) {
Ok(t) => println!(" {:<10} {:>2} gates ({from})", t.name, t.gates.len()), Ok(t) => out!(" {:<10} {:>2} gates ({from})", t.name, t.gates.len()),
Err(e) => println!(" {:<10} ERROR {e}", entry.name), Err(e) => out!(" {:<10} ERROR {e}", entry.name),
} }
} }
println!("Vehicles:"); out!("Vehicles:");
for v in &data.vehicles { for v in &data.vehicles {
println!(" {:<10} ({})", v.name, v.id); out!(" {:<10} ({})", v.name, v.id);
} }
} }
@@ -263,7 +274,7 @@ fn unpack(input: &Path, out: Option<&Path>) -> Result<(), String> {
let dest = out.join(safe); let dest = out.join(safe);
std::fs::create_dir_all(dest.parent().unwrap()).map_err(|e| e.to_string())?; std::fs::create_dir_all(dest.parent().unwrap()).map_err(|e| e.to_string())?;
std::fs::write(&dest, &data).map_err(|e| format!("{}: {e}", dest.display()))?; std::fs::write(&dest, &data).map_err(|e| format!("{}: {e}", dest.display()))?;
println!("{:>8} {}", data.len(), path); out!("{:>8} {}", data.len(), path);
} }
if failed > 0 { if failed > 0 {
return Err(format!("{failed} files could not be extracted")); return Err(format!("{failed} files could not be extracted"));
@@ -299,7 +310,10 @@ fn extract(data: &GameData, out: Option<&Path>) -> Result<(), String> {
} }
write(dir.join("track.png"), &png)?; write(dir.join("track.png"), &png)?;
write(dir.join("track.txt"), txt.as_bytes())?; write(dir.join("track.txt"), txt.as_bytes())?;
println!("wrote {}", dir.display()); if let Some(thumb) = t.thumb_png() {
write(dir.join("thumb.png"), &thumb?)?;
}
out!("wrote {}", dir.display());
} }
for v in &data.vehicles { for v in &data.vehicles {
let (png, txt) = (v.sheet_png()?, v.settings_text()); let (png, txt) = (v.sheet_png()?, v.settings_text());
@@ -313,7 +327,7 @@ fn extract(data: &GameData, out: Option<&Path>) -> Result<(), String> {
if let Some(menu) = v.menu_png() { if let Some(menu) = v.menu_png() {
write(dir.join(format!("{}-menu.png", v.id)), &menu?)?; write(dir.join(format!("{}-menu.png", v.id)), &menu?)?;
} }
println!("wrote {}", dir.join(&v.id).display()); out!("wrote {}", dir.join(&v.id).display());
} }
for (name, s) in &data.sounds { for (name, s) in &data.sounds {
let wav = formats::iff::sound_to_wav(s); let wav = formats::iff::sound_to_wav(s);
@@ -323,13 +337,13 @@ fn extract(data: &GameData, out: Option<&Path>) -> Result<(), String> {
} }
let path = out.join("sounds").join(format!("{name}.wav")); let path = out.join("sounds").join(format!("{name}.wav"));
write(path.clone(), &wav)?; write(path.clone(), &wav)?;
println!("wrote {}", path.display()); out!("wrote {}", path.display());
} }
if let Some((name, med)) = &data.music { if let Some((name, med)) = &data.music {
formats::med::parse(med).map_err(|e| format!("music {name}: {e}"))?; formats::med::parse(med).map_err(|e| format!("music {name}: {e}"))?;
let path = out.join("music").join(format!("{name}.med")); let path = out.join("music").join(format!("{name}.med"));
write(path.clone(), med)?; write(path.clone(), med)?;
println!("wrote {}", path.display()); out!("wrote {}", path.display());
} }
for (name, p) in &data.pictures { for (name, p) in &data.pictures {
let pal: Vec<data::palette::Rgb> = p let pal: Vec<data::palette::Rgb> = p
@@ -343,7 +357,7 @@ fn extract(data: &GameData, out: Option<&Path>) -> Result<(), String> {
} }
let path = out.join("gfx").join(format!("{name}.png")); let path = out.join("gfx").join(format!("{name}.png"));
write(path.clone(), &png)?; write(path.clone(), &png)?;
println!("wrote {}", path.display()); out!("wrote {}", path.display());
} }
Ok(()) Ok(())
} }
@@ -394,7 +408,7 @@ fn render_shot(data: GameData, args: &Args) -> Result<(), String> {
let n = race.track.gates.len(); let n = race.track.gates.len();
for &i in &race.standings() { for &i in &race.standings() {
let e = &race.entrants[i]; let e = &race.entrants[i];
println!( out!(
"{:<10} laps {} gates {} best {:?}", "{:<10} laps {} gates {} best {:?}",
e.name, e.name,
e.laps_done(n), e.laps_done(n),
@@ -402,7 +416,7 @@ fn render_shot(data: GameData, args: &Args) -> Result<(), String> {
e.best_lap e.best_lap
); );
} }
println!("wrote {}", out.display()); out!("wrote {}", out.display());
Ok(()) Ok(())
} }
@@ -415,9 +429,13 @@ fn bench(data: GameData, args: &Args) -> Result<(), String> {
}; };
app.settings.caravans = args.caravans; app.settings.caravans = args.caravans;
let frames = (args.seconds / game::DT) as usize; let frames = (args.seconds / game::DT) as usize;
println!( out!(
"{:<10} {:>5} {:>8} {:>9} {:>8}", "{:<10} {:>5} {:>8} {:>9} {:>8}",
"TRACK", "GATES", "LAPS", "BEST LAP", "ON ROAD" "TRACK",
"GATES",
"LAPS",
"BEST LAP",
"ON ROAD"
); );
for ti in 0..app.data.tracks.len() { for ti in 0..app.data.tracks.len() {
let mut race = app.build_race(ti, 0, [0, 0], 4, 99, ti as u32 + 1)?; let mut race = app.build_race(ti, 0, [0, 0], 4, 99, ti as u32 + 1)?;
@@ -445,7 +463,7 @@ fn bench(data: GameData, args: &Args) -> Result<(), String> {
} else { } else {
app::fmt_time(best) app::fmt_time(best)
}; };
println!( out!(
"{:<10} {:>5} {:>8} {:>9} {:>7.0}%", "{:<10} {:>5} {:>8} {:>9} {:>7.0}%",
race.track.name, race.track.name,
n, n,
@@ -494,7 +512,7 @@ fn render_music(data: &GameData, args: &Args) -> Result<(), String> {
let n = (wav.len() - 8) as u32; let n = (wav.len() - 8) as u32;
wav[4..8].copy_from_slice(&n.to_le_bytes()); wav[4..8].copy_from_slice(&n.to_le_bytes());
std::fs::write(&out, wav).map_err(|e| e.to_string())?; std::fs::write(&out, wav).map_err(|e| e.to_string())?;
println!( out!(
"wrote {} ({:.0} s, peak {:.2}, position {})", "wrote {} ({:.0} s, peak {:.2}, position {})",
out.display(), out.display(),
args.seconds, args.seconds,
@@ -502,7 +520,7 @@ fn render_music(data: &GameData, args: &Args) -> Result<(), String> {
player.position() player.position()
); );
if let Some(t) = wrapped_at { if let Some(t) = wrapped_at {
println!( out!(
"the song loops back to the start after {}:{:04.1}", "the song loops back to the start after {}:{:04.1}",
(t / 60.0) as u32, (t / 60.0) as u32,
t % 60.0 t % 60.0
@@ -556,7 +574,7 @@ fn trace(data: GameData, args: &Args) -> Result<(), String> {
} }
let n = race.track.gates.len(); let n = race.track.gates.len();
for e in &race.entrants { for e in &race.entrants {
println!( out!(
"{:<8} laps {} next gate {}", "{:<8} laps {} next gate {}",
e.name, e.name,
e.laps_done(n), e.laps_done(n),