diff --git a/assets/README.md b/assets/README.md index b6c46ae..c195555 100644 --- a/assets/README.md +++ b/assets/README.md @@ -47,7 +47,14 @@ Plain text; lines starting with `#` are comments. - **`start`, `flags`:** values from the original data that the game 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. ## Vehicles: `vehicles/.png` and `.txt` @@ -125,7 +132,12 @@ files, the first in name order plays. ## Pictures: `gfx/*.png` 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) diff --git a/assets/gfx/ball.png b/assets/gfx/ball.png new file mode 100644 index 0000000..af73698 Binary files /dev/null and b/assets/gfx/ball.png differ diff --git a/assets/tracks/track1-1/thumb.png b/assets/tracks/track1-1/thumb.png new file mode 100644 index 0000000..113c77e Binary files /dev/null and b/assets/tracks/track1-1/thumb.png differ diff --git a/assets/tracks/track1-2/thumb.png b/assets/tracks/track1-2/thumb.png new file mode 100644 index 0000000..b1c8289 Binary files /dev/null and b/assets/tracks/track1-2/thumb.png differ diff --git a/assets/tracks/track1-3/thumb.png b/assets/tracks/track1-3/thumb.png new file mode 100644 index 0000000..a95fc6b Binary files /dev/null and b/assets/tracks/track1-3/thumb.png differ diff --git a/assets/tracks/track1-4/thumb.png b/assets/tracks/track1-4/thumb.png new file mode 100644 index 0000000..3feb7c2 Binary files /dev/null and b/assets/tracks/track1-4/thumb.png differ diff --git a/assets/tracks/track1-5/thumb.png b/assets/tracks/track1-5/thumb.png new file mode 100644 index 0000000..dd4ab57 Binary files /dev/null and b/assets/tracks/track1-5/thumb.png differ diff --git a/assets/tracks/track1-6/thumb.png b/assets/tracks/track1-6/thumb.png new file mode 100644 index 0000000..58fea65 Binary files /dev/null and b/assets/tracks/track1-6/thumb.png differ diff --git a/assets/tracks/track2-1/thumb.png b/assets/tracks/track2-1/thumb.png new file mode 100644 index 0000000..682df1c Binary files /dev/null and b/assets/tracks/track2-1/thumb.png differ diff --git a/assets/tracks/track2-2/thumb.png b/assets/tracks/track2-2/thumb.png new file mode 100644 index 0000000..dd41a8f Binary files /dev/null and b/assets/tracks/track2-2/thumb.png differ diff --git a/assets/tracks/track2-3/thumb.png b/assets/tracks/track2-3/thumb.png new file mode 100644 index 0000000..f037ccb Binary files /dev/null and b/assets/tracks/track2-3/thumb.png differ diff --git a/assets/tracks/track2-4/thumb.png b/assets/tracks/track2-4/thumb.png new file mode 100644 index 0000000..6a4cf3a Binary files /dev/null and b/assets/tracks/track2-4/thumb.png differ diff --git a/assets/tracks/track2-5/thumb.png b/assets/tracks/track2-5/thumb.png new file mode 100644 index 0000000..cd17654 Binary files /dev/null and b/assets/tracks/track2-5/thumb.png differ diff --git a/assets/tracks/track2-6/thumb.png b/assets/tracks/track2-6/thumb.png new file mode 100644 index 0000000..e490b5d Binary files /dev/null and b/assets/tracks/track2-6/thumb.png differ diff --git a/assets/tracks/track3-1/thumb.png b/assets/tracks/track3-1/thumb.png new file mode 100644 index 0000000..0c1ceec Binary files /dev/null and b/assets/tracks/track3-1/thumb.png differ diff --git a/assets/tracks/track3-2/thumb.png b/assets/tracks/track3-2/thumb.png new file mode 100644 index 0000000..4617418 Binary files /dev/null and b/assets/tracks/track3-2/thumb.png differ diff --git a/assets/tracks/track3-3/thumb.png b/assets/tracks/track3-3/thumb.png new file mode 100644 index 0000000..748dbaa Binary files /dev/null and b/assets/tracks/track3-3/thumb.png differ diff --git a/assets/tracks/track3-4/thumb.png b/assets/tracks/track3-4/thumb.png new file mode 100644 index 0000000..2675dff Binary files /dev/null and b/assets/tracks/track3-4/thumb.png differ diff --git a/assets/tracks/track3-5/thumb.png b/assets/tracks/track3-5/thumb.png new file mode 100644 index 0000000..33511ef Binary files /dev/null and b/assets/tracks/track3-5/thumb.png differ diff --git a/assets/tracks/track3-6/thumb.png b/assets/tracks/track3-6/thumb.png new file mode 100644 index 0000000..7caaab1 Binary files /dev/null and b/assets/tracks/track3-6/thumb.png differ diff --git a/assets/tracks/track4-1/thumb.png b/assets/tracks/track4-1/thumb.png new file mode 100644 index 0000000..7cc3f70 Binary files /dev/null and b/assets/tracks/track4-1/thumb.png differ diff --git a/assets/tracks/track4-2/thumb.png b/assets/tracks/track4-2/thumb.png new file mode 100644 index 0000000..e2d15bc Binary files /dev/null and b/assets/tracks/track4-2/thumb.png differ diff --git a/assets/tracks/track4-3/thumb.png b/assets/tracks/track4-3/thumb.png new file mode 100644 index 0000000..16e2dd4 Binary files /dev/null and b/assets/tracks/track4-3/thumb.png differ diff --git a/assets/tracks/track4-4/thumb.png b/assets/tracks/track4-4/thumb.png new file mode 100644 index 0000000..f04711b Binary files /dev/null and b/assets/tracks/track4-4/thumb.png differ diff --git a/assets/tracks/track4-5/thumb.png b/assets/tracks/track4-5/thumb.png new file mode 100644 index 0000000..9cbb425 Binary files /dev/null and b/assets/tracks/track4-5/thumb.png differ diff --git a/assets/tracks/track4-6/thumb.png b/assets/tracks/track4-6/thumb.png new file mode 100644 index 0000000..916ec25 Binary files /dev/null and b/assets/tracks/track4-6/thumb.png differ diff --git a/docs/FORMATS.md b/docs/FORMATS.md index b7f2bf4..5b91597 100644 --- a/docs/FORMATS.md +++ b/docs/FORMATS.md @@ -58,7 +58,7 @@ consumed to its first byte. ``` 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 ``` @@ -73,8 +73,16 @@ The last 142 bytes of section 0 hold: | 98 | 12 | Unknown | | 110 | … | The name of the set of tracks, NUL-terminated | -The rest of section 0 is the track-selector thumbnails, which aren't -decoded. +The first 20480 bytes of section 0 are the track-selector thumbnails: +- **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: @@ -166,7 +174,18 @@ extra and alternative sounds (`crash1`, `crash2`, `engine`, `motor`, IFF ILBM with ByteRun1 compression. `skidtoon.iff` is the 608 × 180, 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`) @@ -202,8 +221,8 @@ first minute), and it loops at the same point. decoded; this port's versions are reconstructions. Decoding the `.set` values would be the first step towards the original handling. - **Unknown data:** the 12 bytes after each track file's palette, the bit - pattern after the gates in each track section, the thumbnails, and - `ball.spr`. + pattern after the gates in each track section, and the colours of + `ball.spr` (probably set by the executable). The engine's own editable formats, which `skids extract` converts all of the above into, are described in [assets/README.md](../assets/README.md). diff --git a/src/app.rs b/src/app.rs index bd00eb2..32b8bf4 100644 --- a/src/app.rs +++ b/src/app.rs @@ -233,8 +233,22 @@ impl App { fn thumb(&mut self, i: usize) -> Option<&Vec> { if !self.thumbs.contains_key(&i) { let t = self.data.load_track(i).ok()?; - self.thumbs - .insert(i, render::downscale(&t.rgb(), THUMB_W, THUMB_H)); + let img = match &t.thumbnail { + // The original thumbnail: Amiga hi-res pixels, so each row is + // shown twice to keep its shape. + Some(px) => { + let pal: Vec = 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) } diff --git a/src/data/mod.rs b/src/data/mod.rs index 5eb1be0..70a12b1 100644 --- a/src/data/mod.rs +++ b/src/data/mod.rs @@ -299,8 +299,14 @@ impl GameData { let mut t = match &entry.source { TrackSource::Asset { png, txt } => { 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) - .map_err(|e| format!("{}: {e}", entry.name))? + let mut t = + 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)?, }; @@ -383,6 +389,16 @@ fn load_media(files: &FileSet, warnings: &mut Vec) -> 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 { if let Some(name) = stem(key, "gfx", ".png") { match image::read_indexed(data, &palette::TRACK_DEFAULT) { diff --git a/src/data/track.rs b/src/data/track.rs index daf1a4e..f6733d9 100644 --- a/src/data/track.rs +++ b/src/data/track.rs @@ -3,6 +3,10 @@ //! File layout: //! 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. +//! 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 //! 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 @@ -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 } +pub const THUMB_W: usize = 123; +pub const THUMB_H: usize = 32; + pub struct TrackFile { pub name: String, pub number: u32, pub palette: [Rgb; 16], pub set_name: String, + /// The six selector thumbnails, THUMB_W x THUMB_H palette indices each. + pub thumbnails: Vec>, /// Packed track sections (index 0 = first playable track). pub sections: Vec>, } @@ -75,6 +84,8 @@ pub struct Track { pub name: String, /// The set of six tracks this one belongs to. pub set_name: String, + /// The original selector thumbnail, THUMB_W x THUMB_H palette indices. + pub thumbnail: Option>, /// Palette indices, W x H. pub pixels: Vec, pub palette: [Rgb; 16], @@ -112,13 +123,16 @@ impl TrackFile { .trim_start_matches(|c: char| !c.is_ascii_digit()) .parse() .unwrap_or(u32::MAX); - let (palette, set_name) = file_header(&d[SECTIONS * 4..ends[0]]) - .unwrap_or((palette::TRACK_DEFAULT, String::new())); + let header = &d[SECTIONS * 4..ends[0]]; + let thumbnails = thumbnails(header); + let (palette, set_name) = + file_header(header).unwrap_or((palette::TRACK_DEFAULT, String::new())); Ok(TrackFile { name: name.to_string(), number, palette, set_name, + thumbnails, sections, }) } @@ -132,10 +146,38 @@ impl TrackFile { let mut t = Track::from_section(self.track_name(section), &raw)?; t.palette = self.palette; t.set_name = self.set_name.clone(); + t.thumbnail = self.thumbnails.get(section).cloned(); Ok(t) } } +/// The six thumbnails from the start of section 0. +fn thumbnails(s: &[u8]) -> Vec> { + 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. fn file_header(s: &[u8]) -> Option<([Rgb; 16], String)> { let tail = s.get(s.len().checked_sub(142)?..)?; @@ -197,6 +239,7 @@ impl Track { pixels, palette: palette::TRACK_DEFAULT, set_name: String::new(), + thumbnail: None, height, gates, start, @@ -336,6 +379,7 @@ impl Track { pixels: img.pixels, palette: pal, set_name, + thumbnail: None, height, gates, start, @@ -343,6 +387,24 @@ impl Track { }) } + /// The thumbnail as an indexed PNG in the track's palette. + pub fn thumb_png(&self) -> Option, 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, String> { image::write_indexed(W, H, &self.palette, false, &self.pixels) } diff --git a/src/formats/iff.rs b/src/formats/iff.rs index 1c5cbe3..46e93c9 100644 --- a/src/formats/iff.rs +++ b/src/formats/iff.rs @@ -170,6 +170,74 @@ pub fn read_ilbm(d: &[u8]) -> Result { }) } +/// 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 { + 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 /// sample editors keep it. pub fn sound_to_wav(s: &Sound) -> Vec { @@ -277,6 +345,23 @@ mod tests { 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] fn byterun1_picture() { // 16x1, 1 plane: run of 2 x 0xff. diff --git a/src/main.rs b/src/main.rs index a8f1b61..60e234b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,6 +3,17 @@ //! 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. +/// 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 audio; mod data; @@ -202,24 +213,24 @@ fn main() -> ExitCode { fn info(data: &GameData, dir: Option<&Path>) { let built_in = data::embedded::files().len(); match (built_in, dir) { - (0, Some(d)) => println!("Data: {}", d.display()), - (n, Some(d)) => println!("Data: built in ({n} files), plus {}", d.display()), - (n, None) => println!("Data: built in ({n} files)"), + (0, Some(d)) => out!("Data: {}", d.display()), + (n, Some(d)) => out!("Data: built in ({n} files), plus {}", d.display()), + (n, None) => out!("Data: built in ({n} files)"), } - println!("Tracks:"); + out!("Tracks:"); for (i, entry) in data.tracks.iter().enumerate() { let from = match entry.source { data::TrackSource::Asset { .. } => "asset", data::TrackSource::Packed { .. } => "original data", }; match data.load_track(i) { - Ok(t) => println!(" {:<10} {:>2} gates ({from})", t.name, t.gates.len()), - Err(e) => println!(" {:<10} ERROR {e}", entry.name), + Ok(t) => out!(" {:<10} {:>2} gates ({from})", t.name, t.gates.len()), + Err(e) => out!(" {:<10} ERROR {e}", entry.name), } } - println!("Vehicles:"); + out!("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); 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()))?; - println!("{:>8} {}", data.len(), path); + out!("{:>8} {}", data.len(), path); } if failed > 0 { 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.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 { 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() { 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 { 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")); write(path.clone(), &wav)?; - println!("wrote {}", path.display()); + out!("wrote {}", path.display()); } if let Some((name, med)) = &data.music { formats::med::parse(med).map_err(|e| format!("music {name}: {e}"))?; let path = out.join("music").join(format!("{name}.med")); write(path.clone(), med)?; - println!("wrote {}", path.display()); + out!("wrote {}", path.display()); } for (name, p) in &data.pictures { let pal: Vec = p @@ -343,7 +357,7 @@ fn extract(data: &GameData, out: Option<&Path>) -> Result<(), String> { } let path = out.join("gfx").join(format!("{name}.png")); write(path.clone(), &png)?; - println!("wrote {}", path.display()); + out!("wrote {}", path.display()); } Ok(()) } @@ -394,7 +408,7 @@ fn render_shot(data: GameData, args: &Args) -> Result<(), String> { let n = race.track.gates.len(); for &i in &race.standings() { let e = &race.entrants[i]; - println!( + out!( "{:<10} laps {} gates {} best {:?}", e.name, e.laps_done(n), @@ -402,7 +416,7 @@ fn render_shot(data: GameData, args: &Args) -> Result<(), String> { e.best_lap ); } - println!("wrote {}", out.display()); + out!("wrote {}", out.display()); Ok(()) } @@ -415,9 +429,13 @@ fn bench(data: GameData, args: &Args) -> Result<(), String> { }; app.settings.caravans = args.caravans; let frames = (args.seconds / game::DT) as usize; - println!( + out!( "{:<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() { 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 { app::fmt_time(best) }; - println!( + out!( "{:<10} {:>5} {:>8} {:>9} {:>7.0}%", race.track.name, n, @@ -494,7 +512,7 @@ fn render_music(data: &GameData, args: &Args) -> Result<(), String> { let n = (wav.len() - 8) as u32; wav[4..8].copy_from_slice(&n.to_le_bytes()); std::fs::write(&out, wav).map_err(|e| e.to_string())?; - println!( + out!( "wrote {} ({:.0} s, peak {:.2}, position {})", out.display(), args.seconds, @@ -502,7 +520,7 @@ fn render_music(data: &GameData, args: &Args) -> Result<(), String> { player.position() ); if let Some(t) = wrapped_at { - println!( + out!( "the song loops back to the start after {}:{:04.1}", (t / 60.0) as u32, t % 60.0 @@ -556,7 +574,7 @@ fn trace(data: GameData, args: &Args) -> Result<(), String> { } let n = race.track.gates.len(); for e in &race.entrants { - println!( + out!( "{:<8} laps {} next gate {}", e.name, e.laps_done(n),