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:
@@ -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());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user