Add the contents of disk 1

- LHA -lh5/-lh6/-lh7 decoding with CRC checks (disk 1 is mostly lh5)
- Real track palettes and set names, read from each track file's header
- Camaro, plus the caravan as a towable trailer (Caravans option)
- Drivers' names, menu pictures and the original .set values per car
- Original sound effects (IFF 8SVX) through a new sample mixer: engine
  pitched with revs, tyre screech, crashes, backfire, horn, start beeps
- The original tune (OctaMED MMD1) through a new player; loudness tracks
  OpenMPT's render through the whole song. Music option in the menu
- Title picture (IFF ILBM) at the top of the menu
- skids unpack and skids music commands
- Gate crossings now judged on each car's whole movement per frame,
  including collision knocks; stalled computer cars are put back on the
  line after 8 seconds

The executable's game logic (handling, AI) is still not decoded.
This commit is contained in:
2026-10-19 20:24:33 +00:00
committed by Claude
parent f27bd2e993
commit 9d82d99c15
106 changed files with 2706 additions and 287 deletions
+296
View File
@@ -0,0 +1,296 @@
//! IFF readers: 8SVX sounds and ILBM pictures.
use super::{be16, be32};
/// An 8-bit signed sample, as stored in an 8SVX `BODY`.
#[derive(Clone, Debug, PartialEq)]
pub struct Sound {
pub rate: u32,
pub data: Vec<i8>,
/// Loop start in samples, if the sound repeats.
pub loop_start: Option<usize>,
pub volume: f32,
}
#[derive(Clone, Debug)]
pub struct Picture {
pub w: usize,
pub h: usize,
/// Palette indices, w * h.
pub pixels: Vec<u8>,
/// 8-bit RGB triples.
pub palette: Vec<(u8, u8, u8)>,
}
/// (chunk id, chunk body) pairs.
type Chunks<'a> = Vec<(&'a [u8], &'a [u8])>;
fn chunks(d: &[u8]) -> Option<(&[u8], Chunks<'_>)> {
if d.len() < 12 || &d[0..4] != b"FORM" {
return None;
}
let end = (be32(d, 4) as usize + 8).min(d.len());
let form = &d[8..12];
let mut out = Vec::new();
let mut p = 12;
while p + 8 <= end {
let id = &d[p..p + 4];
let n = be32(d, p + 4) as usize;
let body = d.get(p + 8..p + 8 + n)?;
out.push((id, body));
p += 8 + n + (n & 1);
}
Some((form, out))
}
pub fn is_8svx(d: &[u8]) -> bool {
d.len() >= 12 && &d[0..4] == b"FORM" && &d[8..12] == b"8SVX"
}
pub fn read_8svx(d: &[u8]) -> Result<Sound, String> {
let (form, chunks) = chunks(d).ok_or("not an IFF file")?;
if form != b"8SVX" {
return Err("not an 8SVX sound".into());
}
let vhdr = chunks
.iter()
.find(|c| c.0 == b"VHDR")
.ok_or("no VHDR chunk")?
.1;
let body = chunks
.iter()
.find(|c| c.0 == b"BODY")
.ok_or("no BODY chunk")?
.1;
if vhdr.len() < 20 {
return Err("short VHDR".into());
}
let one_shot = be32(vhdr, 0) as usize;
let repeat = be32(vhdr, 4) as usize;
let rate = be16(vhdr, 12) as u32;
if vhdr[15] != 0 {
return Err("compressed 8SVX is not supported".into());
}
let volume = (be32(vhdr, 16) as f32 / 65536.0).clamp(0.0, 1.0);
// Only the first octave is used; multi-octave sounds store more after it.
let len = if one_shot + repeat > 0 {
(one_shot + repeat).min(body.len())
} else {
body.len()
};
let data = body[..len].iter().map(|&b| b as i8).collect();
let loop_start = (repeat > 2).then_some(one_shot);
Ok(Sound {
rate: rate.max(1000),
data,
loop_start,
volume,
})
}
pub fn read_ilbm(d: &[u8]) -> Result<Picture, String> {
let (form, chunks) = chunks(d).ok_or("not an IFF file")?;
if form != b"ILBM" {
return Err("not an ILBM picture".into());
}
let bmhd = chunks
.iter()
.find(|c| c.0 == b"BMHD")
.ok_or("no BMHD chunk")?
.1;
let body = chunks
.iter()
.find(|c| c.0 == b"BODY")
.ok_or("no BODY chunk")?
.1;
let w = be16(bmhd, 0) as usize;
let h = be16(bmhd, 2) as usize;
let planes = bmhd[8] as usize;
let masking = bmhd[9];
let compression = bmhd[10];
if planes == 0 || planes > 8 || w == 0 || h == 0 {
return Err("unsupported bitmap layout".into());
}
let palette = chunks
.iter()
.find(|c| c.0 == b"CMAP")
.map(|c| c.1.chunks_exact(3).map(|p| (p[0], p[1], p[2])).collect())
.unwrap_or_else(|| {
(0..1u16 << planes)
.map(|i| {
let v = (i * 255 / ((1 << planes) - 1)) as u8;
(v, v, v)
})
.collect()
});
let row_bytes = w.div_ceil(16) * 2;
let rows_per_line = planes + (masking == 1) as usize;
let mut raw = Vec::with_capacity(row_bytes * rows_per_line * h);
match compression {
0 => raw.extend_from_slice(body),
1 => {
// ByteRun1
let mut i = 0;
while i < body.len() && raw.len() < row_bytes * rows_per_line * h {
let n = body[i] as i8;
i += 1;
if n >= 0 {
let k = n as usize + 1;
raw.extend_from_slice(body.get(i..i + k).ok_or("ByteRun1 data ended early")?);
i += k;
} else if n != -128 {
let b = *body.get(i).ok_or("ByteRun1 data ended early")?;
raw.extend(std::iter::repeat_n(b, (1 - n as isize) as usize));
i += 1;
}
}
}
_ => return Err(format!("unknown ILBM compression {compression}")),
}
if raw.len() < row_bytes * rows_per_line * h {
return Err("BODY is shorter than the picture".into());
}
let mut pixels = vec![0u8; w * h];
for y in 0..h {
for p in 0..planes {
let row = &raw[(y * rows_per_line + p) * row_bytes..][..row_bytes];
for x in 0..w {
if row[x >> 3] & (0x80 >> (x & 7)) != 0 {
pixels[y * w + x] |= 1 << p;
}
}
}
}
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> {
let data: Vec<u8> = s.data.iter().map(|&v| (v as i16 + 128) as u8).collect();
let mut chunks: Vec<(&[u8; 4], Vec<u8>)> = Vec::new();
let mut fmt = Vec::new();
fmt.extend_from_slice(&1u16.to_le_bytes());
fmt.extend_from_slice(&1u16.to_le_bytes());
fmt.extend_from_slice(&s.rate.to_le_bytes());
fmt.extend_from_slice(&s.rate.to_le_bytes());
fmt.extend_from_slice(&1u16.to_le_bytes());
fmt.extend_from_slice(&8u16.to_le_bytes());
chunks.push((b"fmt ", fmt));
chunks.push((b"data", data));
if let Some(start) = s.loop_start {
let mut smpl = vec![0u8; 36];
smpl[28..32].copy_from_slice(&1u32.to_le_bytes());
let mut lp = vec![0u8; 24];
lp[8..12].copy_from_slice(&(start as u32).to_le_bytes());
lp[12..16].copy_from_slice(&(s.data.len() as u32 - 1).to_le_bytes());
smpl.extend(lp);
chunks.push((b"smpl", smpl));
}
let mut out = b"RIFF\0\0\0\0WAVE".to_vec();
for (id, body) in chunks {
out.extend_from_slice(id);
out.extend_from_slice(&(body.len() as u32).to_le_bytes());
out.extend_from_slice(&body);
if body.len() & 1 == 1 {
out.push(0);
}
}
let n = (out.len() - 8) as u32;
out[4..8].copy_from_slice(&n.to_le_bytes());
out
}
/// Reads mono or stereo 8/16-bit PCM WAV (stereo is mixed down).
pub fn wav_to_sound(d: &[u8]) -> Result<Sound, String> {
if d.len() < 12 || &d[0..4] != b"RIFF" || &d[8..12] != b"WAVE" {
return Err("not a WAV file".into());
}
let le16 = |o: usize| u16::from_le_bytes([d[o], d[o + 1]]);
let le32 = |o: usize| u32::from_le_bytes([d[o], d[o + 1], d[o + 2], d[o + 3]]);
let (mut fmt, mut data, mut loop_start) = (None, None, None);
let mut p = 12;
while p + 8 <= d.len() {
let n = le32(p + 4) as usize;
let body = p + 8..(p + 8 + n).min(d.len());
match &d[p..p + 4] {
b"fmt " if n >= 16 => {
fmt = Some((le16(p + 8), le16(p + 10), le32(p + 12), le16(p + 22)))
}
b"data" => data = Some(body),
b"smpl" if n >= 60 && le32(p + 8 + 28) > 0 => {
loop_start = Some(le32(p + 8 + 36 + 8) as usize)
}
_ => {}
}
p += 8 + n + (n & 1);
}
let (format, channels, rate, bits) = fmt.ok_or("no fmt chunk")?;
let body = &d[data.ok_or("no data chunk")?];
if format != 1 || channels == 0 || !(bits == 8 || bits == 16) {
return Err("only 8- or 16-bit PCM WAV is supported".into());
}
let step = channels as usize * (bits as usize / 8);
let sample = |frame: &[u8]| -> i32 {
let mut acc = 0i32;
for c in 0..channels as usize {
acc += if bits == 8 {
(frame[c] as i32 - 128) << 8
} else {
i16::from_le_bytes([frame[2 * c], frame[2 * c + 1]]) as i32
};
}
acc / channels as i32
};
let data: Vec<i8> = body
.chunks_exact(step)
.map(|f| (sample(f) >> 8) as i8)
.collect();
let loop_start = loop_start.filter(|&s| s + 2 < data.len());
Ok(Sound {
rate: rate.max(1000),
data,
loop_start,
volume: 1.0,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn wav_round_trip() {
let s = Sound {
rate: 8363,
data: (-50..50).map(|v| v as i8).collect(),
loop_start: Some(20),
volume: 1.0,
};
let back = wav_to_sound(&sound_to_wav(&s)).unwrap();
assert_eq!(back, s);
}
#[test]
fn byterun1_picture() {
// 16x1, 1 plane: run of 2 x 0xff.
let mut d = b"FORM\0\0\0\0ILBMBMHD\0\0\0\x14".to_vec();
let mut bmhd = vec![0u8; 20];
bmhd[1] = 16;
bmhd[3] = 1;
bmhd[8] = 1;
bmhd[10] = 1;
d.extend(bmhd);
d.extend(b"BODY\0\0\0\x02\xff\xff");
let n = (d.len() - 8) as u32;
d[4..8].copy_from_slice(&n.to_be_bytes());
let p = read_ilbm(&d).unwrap();
assert_eq!(p.pixels, vec![1; 16]);
}
}