Add Rust revival engine for Super Skidmarks

Cross-platform (Windows/macOS/Linux) engine that plays the original
Amiga data straight from the ADF images:

- ADF (OFS/FFS), LHA and CrunchMania (CrM!/CrM2) readers
- Track decoding: 24 tracks of 1024x512 16-colour planar art, height
  maps and lap gates on an isometric ground plane
- Vehicle decoding: 8 cars, 32 headings x 25 tilt frames of 64x32
  AGA sprites
- Reconstructed handling, AI, race rules, menus and synthesised sound
  (disk 1 with the executable, palettes and audio is not in the set)
- info/extract/bench/trace/render tools, format notes, CI builds
This commit is contained in:
2026-10-19 18:31:17 +00:00
parent f8ba2ecf4d
commit f88ba23282
22 changed files with 5656 additions and 0 deletions
+132
View File
@@ -0,0 +1,132 @@
//! Colour palettes.
//!
//! The track and vehicle files carry no colour tables; the retail game sets
//! them from its main executable, which lives on disk 1. Until that disk is
//! available we use a reconstructed palette. Indices on the retail tracks are
//! ordered dark-to-light, with 0-7 used for scenery and 8-15 for the driving
//! surface, so two ramps give a faithful-looking result.
//!
//! Real palettes can be dropped in as text files (16 lines of `RRGGBB`,
//! `#` comments allowed) next to the game data:
//! palettes/track1-3.pal one track
//! palettes/track1.pal all six tracks in TRACK1
//! palettes/default.pal every track
use super::FileSet;
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Rgb(pub u8, pub u8, pub u8);
impl Rgb {
pub const fn to_u32(self) -> u32 {
((self.0 as u32) << 16) | ((self.1 as u32) << 8) | self.2 as u32
}
pub fn scale(self, f: f32) -> Rgb {
let s = |c: u8| ((c as f32 * f).round().clamp(0.0, 255.0)) as u8;
Rgb(s(self.0), s(self.1), s(self.2))
}
pub fn mix(self, o: Rgb, t: f32) -> Rgb {
let m = |a: u8, b: u8| (a as f32 + (b as f32 - a as f32) * t).round() as u8;
Rgb(m(self.0, o.0), m(self.1, o.1), m(self.2, o.2))
}
}
pub const TRACK_DEFAULT: [Rgb; 16] = [
// scenery: deep shadow through sunlit grass
Rgb(0x0c, 0x1a, 0x0e),
Rgb(0x18, 0x2c, 0x14),
Rgb(0x24, 0x40, 0x1a),
Rgb(0x34, 0x56, 0x20),
Rgb(0x48, 0x6c, 0x28),
Rgb(0x60, 0x84, 0x34),
Rgb(0x84, 0x9c, 0x4c),
Rgb(0xb0, 0xb8, 0x7c),
// driving surface: tarmac, kerbs and markings
Rgb(0x3a, 0x36, 0x34),
Rgb(0x50, 0x4c, 0x48),
Rgb(0x66, 0x62, 0x5c),
Rgb(0x7c, 0x78, 0x70),
Rgb(0x94, 0x90, 0x86),
Rgb(0xae, 0xaa, 0x9e),
Rgb(0xcc, 0xc8, 0xbc),
Rgb(0xf0, 0xee, 0xe4),
];
/// Per-player body colours for the three-colour vehicle sprites
/// (index 1 = tyres and shadow, 2 = trim, 3 = bodywork).
pub const TEAM_COLOURS: [Rgb; 8] = [
Rgb(0xe0, 0x30, 0x28),
Rgb(0x30, 0x60, 0xe8),
Rgb(0xf0, 0xc8, 0x20),
Rgb(0x30, 0xb0, 0x48),
Rgb(0xe0, 0x60, 0xd0),
Rgb(0x20, 0xc0, 0xc8),
Rgb(0xf0, 0x80, 0x20),
Rgb(0xe8, 0xe8, 0xe8),
];
pub fn car_palette(team: usize) -> [u32; 4] {
let c = TEAM_COLOURS[team % TEAM_COLOURS.len()];
[
0,
Rgb(0x10, 0x10, 0x14).to_u32(),
c.scale(0.7).to_u32(),
c.mix(Rgb(0xff, 0xff, 0xff), 0.35).to_u32(),
]
}
pub fn parse(text: &str) -> Option<[Rgb; 16]> {
let mut out = [Rgb(0, 0, 0); 16];
let mut n = 0;
for line in text.lines() {
let line = line
.split('#')
.next()
.unwrap_or("")
.trim()
.trim_start_matches("0x");
if line.is_empty() {
continue;
}
if n == 16 || line.len() != 6 {
return None;
}
let v = u32::from_str_radix(line, 16).ok()?;
out[n] = Rgb((v >> 16) as u8, (v >> 8) as u8, v as u8);
n += 1;
}
(n == 16).then_some(out)
}
pub fn override_for(files: &FileSet, track_file: &str, section: usize) -> Option<[Rgb; 16]> {
let base = track_file.to_lowercase();
[
format!("palettes/{base}-{}.pal", section + 1),
format!("palettes/{base}.pal"),
"palettes/default.pal".into(),
]
.iter()
.find_map(|k| {
files
.get(k)
.and_then(|d| parse(&String::from_utf8_lossy(d)))
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_palette_file() {
let mut text = String::from("# test\n");
for i in 0..16 {
text.push_str(&format!("{:02x}{:02x}{:02x}\n", i, i * 2, i * 3));
}
let p = parse(&text).unwrap();
assert_eq!(p[15], Rgb(15, 30, 45));
assert!(parse("ffffff\n").is_none());
}
}