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
@@ -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/<id>.png` and `<id>.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)
|
||||
|
||||
|
||||
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 856 B |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 1013 B |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 941 B |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
@@ -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).
|
||||
|
||||
@@ -233,8 +233,22 @@ impl App {
|
||||
fn thumb(&mut self, i: usize) -> Option<&Vec<u32>> {
|
||||
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<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)
|
||||
}
|
||||
|
||||
@@ -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<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 {
|
||||
if let Some(name) = stem(key, "gfx", ".png") {
|
||||
match image::read_indexed(data, &palette::TRACK_DEFAULT) {
|
||||
|
||||
@@ -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<Vec<u8>>,
|
||||
/// Packed track sections (index 0 = first playable track).
|
||||
pub sections: Vec<Vec<u8>>,
|
||||
}
|
||||
@@ -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<Vec<u8>>,
|
||||
/// Palette indices, W x H.
|
||||
pub pixels: Vec<u8>,
|
||||
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<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.
|
||||
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<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> {
|
||||
image::write_indexed(W, H, &self.palette, false, &self.pixels)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
/// sample editors keep it.
|
||||
pub fn sound_to_wav(s: &Sound) -> Vec<u8> {
|
||||
@@ -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.
|
||||
|
||||
@@ -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<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"));
|
||||
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),
|
||||
|
||||