//! Track files (`track/TRACKn`). //! //! 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. //! //! Each track section (281240 bytes once decrunched): //! 0x0000 height map, 64x64 BE16 8.8 fixed point, one cell per 16x8 pixels //! 0x2000 8192 bytes, unused (always zero on the retail disks) //! 0x4000 BE16 gate count, start x, start y (screen pixels), flags //! 0x4008 gates: x1 y1 x2 y2 BE16, world coordinates in 1/32 units //! 0x4A98 four 1024x512 bitplanes (65536 bytes each), giving 16 colours //! //! The world is an isometric ground plane rotated 45 degrees to the picture: //! screen x = wx - wy + 512, screen y = (wx + wy) * 31/64 - 252. The constants //! were fitted so that 96% of gate samples over all 24 tracks land on road //! pixels (against 40-70% for a plain 2:1 squash). The stored start point //! only lands on the road on 14 of the 24 tracks, so the grid is placed from //! the gates instead. use super::image; use super::palette::{self, Rgb}; use super::unpack; use crate::formats::{be16, be32}; pub const W: usize = 1024; pub const H: usize = 512; pub const HM_W: usize = 64; pub const HM_H: usize = 64; pub const CELL_W: f32 = (W / HM_W) as f32; pub const CELL_H: f32 = (H / HM_H) as f32; const SECTION_SIZE: usize = 0x44a98; const GATES_AT: usize = 0x4000; const PLANES_AT: usize = 0x4a98; const PLANE_SIZE: usize = W * H / 8; const SECTIONS: usize = 7; const ISO_X0: f32 = 512.0; const ISO_Y_SCALE: f32 = 31.0 / 64.0; const ISO_Y0: f32 = -252.0; pub fn to_screen(wx: f32, wy: f32) -> (f32, f32) { (wx - wy + ISO_X0, (wx + wy) * ISO_Y_SCALE + ISO_Y0) } 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 struct TrackFile { pub name: String, pub number: u32, /// Packed track sections (index 0 = first playable track). pub sections: Vec>, } #[derive(Clone, Copy, Debug)] pub struct Gate { pub a: (f32, f32), pub b: (f32, f32), } impl Gate { pub fn mid(&self) -> (f32, f32) { ((self.a.0 + self.b.0) * 0.5, (self.a.1 + self.b.1) * 0.5) } } pub struct Track { pub name: String, /// Palette indices, W x H. pub pixels: Vec, pub palette: [Rgb; 16], /// Height samples in 8.8 fixed point. pub height: Vec, /// Gates in world coordinates. pub gates: Vec, /// Start position and flags as stored. Kept for reference: the start /// point does not line up with the road reliably, and the flags' meaning /// is unknown. #[allow(dead_code)] pub start: (f32, f32), #[allow(dead_code)] pub flags: u16, } impl TrackFile { pub fn parse(name: &str, d: &[u8]) -> Result { if d.len() < SECTIONS * 4 { return Err("file too short".into()); } let ends: Vec = (0..SECTIONS).map(|i| be32(d, i * 4) as usize).collect(); let mut start = SECTIONS * 4; let mut sections = Vec::new(); for (i, &end) in ends.iter().enumerate() { if end < start || end > d.len() { return Err(format!("section {i} has a bad offset")); } if i > 0 { sections.push(d[start..end].to_vec()); } start = end; } let number = name .trim_start_matches(|c: char| !c.is_ascii_digit()) .parse() .unwrap_or(u32::MAX); Ok(TrackFile { name: name.to_string(), number, sections, }) } pub fn track_name(&self, section: usize) -> String { format!("{}-{}", self.name.to_uppercase(), section + 1) } pub fn decode(&self, section: usize) -> Result { let raw = unpack(&self.sections[section]).map_err(|e| e.to_string())?; Track::from_section(self.track_name(section), &raw) } } impl Track { pub fn from_section(name: String, d: &[u8]) -> Result { if d.len() < SECTION_SIZE { return Err(format!( "track section is {} bytes, expected {SECTION_SIZE}", d.len() )); } let height = (0..HM_W * HM_H).map(|i| be16(d, i * 2)).collect(); let count = be16(d, GATES_AT) as usize; let start = (be16(d, GATES_AT + 2) as f32, be16(d, GATES_AT + 4) as f32); let flags = be16(d, GATES_AT + 6); if count == 0 || GATES_AT + 8 + count * 8 > PLANES_AT { return Err(format!("implausible gate count {count}")); } let gates = (0..count) .map(|i| { let o = GATES_AT + 8 + i * 8; let w = |k: usize| be16(d, o + k * 2) as f32 / 32.0; Gate { a: (w(0), w(1)), b: (w(2), w(3)), } }) .collect(); let planes = &d[PLANES_AT..PLANES_AT + 4 * PLANE_SIZE]; let mut pixels = vec![0u8; W * H]; for (p, plane) in planes.chunks_exact(PLANE_SIZE).enumerate() { for (byte_i, &byte) in plane.iter().enumerate() { if byte == 0 { continue; } let base = byte_i * 8; for bit in 0..8 { if byte & (0x80 >> bit) != 0 { pixels[base + bit] |= 1 << p; } } } } Ok(Track { name, pixels, palette: palette::TRACK_DEFAULT, height, gates, start, flags, }) } /// Ground height (in height units) at a screen position, bilinear. pub fn height_at(&self, sx: f32, sy: f32) -> f32 { let fx = (sx / CELL_W - 0.5).clamp(0.0, (HM_W - 1) as f32); let fy = (sy / CELL_H - 0.5).clamp(0.0, (HM_H - 1) as f32); let (x0, y0) = (fx as usize, fy as usize); let (x1, y1) = ((x0 + 1).min(HM_W - 1), (y0 + 1).min(HM_H - 1)); let (tx, ty) = (fx - x0 as f32, fy - y0 as f32); let h = |x: usize, y: usize| self.height[y * HM_W + x] as f32 / 256.0; let top = h(x0, y0) * (1.0 - tx) + h(x1, y0) * tx; let bot = h(x0, y1) * (1.0 - tx) + h(x1, y1) * tx; top * (1.0 - ty) + bot * ty } /// Height at a world position. pub fn height_world(&self, wx: f32, wy: f32) -> f32 { let (sx, sy) = to_screen(wx, wy); self.height_at(sx, sy) } pub fn pixel(&self, sx: f32, sy: f32) -> u8 { let x = (sx as isize).clamp(0, W as isize - 1) as usize; let y = (sy as isize).clamp(0, H as isize - 1) as usize; self.pixels[y * W + x] } /// The upper half of the palette is used for the driving surface: the /// fourth bitplane traces the road on every retail track. pub fn on_road(&self, sx: f32, sy: f32) -> bool { self.pixel(sx, sy) & 8 != 0 } pub fn on_road_world(&self, wx: f32, wy: f32) -> bool { let (sx, sy) = to_screen(wx, wy); on_screen(sx, sy, 0.0) && self.on_road(sx, sy) } /// Loads an editable asset: `track.png` (16-colour indexed; its palette /// is the track palette) and `track.txt` (gates and height map). pub fn from_assets(name: &str, png: &[u8], txt: &str) -> Result { let img = image::read_indexed(png, &palette::TRACK_DEFAULT)?; if (img.w, img.h) != (W, H) { return Err(format!( "track.png is {}x{}, expected {W}x{H}", img.w, img.h )); } if let Some(bad) = img.pixels.iter().find(|&&p| p > 15) { return Err(format!( "track.png uses palette index {bad}; tracks may only use 0-15" )); } let mut pal = palette::TRACK_DEFAULT; if let Some(own) = &img.palette { for (dst, src) in pal.iter_mut().zip(own) { *dst = *src; } } let mut gates = Vec::new(); let mut start = (0.0, 0.0); let mut flags = 0; let mut height = Vec::with_capacity(HM_W * HM_H); let mut in_height = false; for (n, line) in txt.lines().enumerate() { let err = |m: &str| format!("track.txt line {}: {m}", n + 1); let line = line.split('#').next().unwrap_or("").trim(); if line.is_empty() { continue; } let mut words = line.split_whitespace(); let first = words.next().unwrap_or(""); let nums = |w: std::str::SplitWhitespace| -> Result, String> { w.map(|x| { x.parse::() .map_err(|_| err(&format!("`{x}` is not a number"))) }) .collect() }; if in_height { for x in line.split_whitespace() { height.push( x.parse::() .map_err(|_| err(&format!("bad height `{x}`")))?, ); } continue; } match first { "gate" => { let v = nums(words)?; if v.len() != 4 { return Err(err("gate needs x1 y1 x2 y2")); } gates.push(Gate { a: (v[0], v[1]), b: (v[2], v[3]), }); } "start" => { let v = nums(words)?; if v.len() != 2 { return Err(err("start needs x y")); } start = (v[0], v[1]); } "flags" => { flags = words .next() .and_then(|x| x.parse().ok()) .ok_or_else(|| err("bad flags"))? } "height" => in_height = true, other => return Err(err(&format!("unknown entry `{other}`"))), } } if gates.len() < 2 { return Err("track.txt needs at least two gates".into()); } if height.len() != HM_W * HM_H { return Err(format!( "track.txt height map has {} values, expected {}", height.len(), HM_W * HM_H )); } Ok(Track { name: name.to_string(), pixels: img.pixels, palette: pal, height, gates, start, flags, }) } pub fn to_png(&self) -> Result, String> { image::write_indexed(W, H, &self.palette, false, &self.pixels) } pub fn to_text(&self) -> String { let mut t = format!( "# Skids track {}. Lines starting with # are comments.\n\ #\n\ # The picture is track.png: 1024x512, 16 colours. Its palette is the\n\ # track's palette. Colours 8-15 are road (full grip), 0-7 are scenery.\n\ #\n\ # gate x1 y1 x2 y2: lap gates in race order, in world units. Gate 0 is\n\ # the start/finish line; the grid forms up behind it. World to picture:\n\ # picture x = wx - wy + 512, picture y = (wx + wy) * 31/64 - 252\n", self.name ); for g in &self.gates { t.push_str(&format!("gate {} {} {} {}\n", g.a.0, g.a.1, g.b.0, g.b.1)); } t.push_str(&format!( "\n# As stored in the original data; not used by the game.\nstart {} {}\nflags {}\n\n\ # height: 64 rows of 64 values, ground height x 256. Each value covers\n\ # 16x8 pixels of the picture.\nheight\n", self.start.0, self.start.1, self.flags )); for row in self.height.chunks(HM_W) { let line: Vec = row.iter().map(|v| v.to_string()).collect(); t.push_str(&line.join(" ")); t.push('\n'); } t } pub fn rgb(&self) -> Vec { let pal: Vec = self.palette.iter().map(|c| c.to_u32()).collect(); self.pixels.iter().map(|&i| pal[i as usize]).collect() } }