Use the original handling, walls and projection from the executable

Decoded from SKIDMARKS_CD32's machine code:

- Car physics: the per-frame engine/grip/steering update, gravity and
  jumps, the hill effect on the throttle, and body tilt. Replaces the
  reconstructed handling model.
- The meaning of every .set value (separate drag, grip and power for
  human and computer drivers; steering, gravity, tow hitch), and the
  six handling presets, offered by a new Handling menu option.
- Walls: the road edges are quadratic B-splines through the gates'
  ends. They match the painted road edges.
- The real projection, which lifts points by their height; the height
  map is indexed by world cells, not screen pixels.
- Tracks have one more gate than the stored count: the last gate was
  being dropped on every track.

Computer drivers now follow the road between the walls and steer like
the original's. Vehicle assets store the values as `handling`; older
files still load. Docs describe the decoded logic.
This commit is contained in:
2026-10-20 07:32:46 +00:00
parent ee895e0455
commit 6226c88d4e
44 changed files with 1283 additions and 791 deletions
+35 -8
View File
@@ -39,12 +39,12 @@ USAGE:
skids unpack FILE.adf|FILE.lha --out DIR extract every file from a disk image or archive
skids music [--data DIR] --out FILE.wav [--seconds S]
render the tune to a WAV file
skids render [--data DIR] --out FILE.png [--track N] [--seconds S] [--players 1|2] [--menu] [--caravans]
skids render [--data DIR] --out FILE.png [--track N] [--seconds S] [--players 1|2] [--menu] [--caravans] [--handling N]
simulate a race (or the menu) and save a frame
skids bench [--data DIR] [--seconds S] [--caravans]
skids bench [--data DIR] [--seconds S] [--caravans] [--handling N]
race computer cars on every track
skids trace [--data DIR] --out FILE.png [--track N] [--seconds S]
draw the computer cars' paths on a track
skids trace [--data DIR] --out FILE.png [--track N] [--seconds S] [--handling N]
draw the walls, gates and computer cars' paths
The game data is built into the executable, so no disks are needed. --data
adds to or overrides it with ADF images, LHA archives, or extracted track/,
@@ -52,6 +52,9 @@ vehicles/ and palettes/ folders, or an assets folder of edits. Without
--data, a folder like that in the current directory or next to the
executable is picked up automatically.
--handling picks the original's handling presets: 0 = each car's own
(CUSTOM), 1-6 = CLASSIC, GRUNTY, ZIPPY, RIPPY, SLIPPERY, PEDAL CAR.
CONTROLS:
Player 1: arrow keys (or W A S D in one-player games)
Player 2: W A S D
@@ -68,6 +71,7 @@ struct Args {
players: usize,
menu: bool,
caravans: bool,
handling: usize,
}
fn parse_args() -> Result<Args, String> {
@@ -82,6 +86,7 @@ fn parse_args() -> Result<Args, String> {
players: 1,
menu: false,
caravans: false,
handling: 0,
};
let mut it = std::env::args().skip(1);
while let Some(arg) = it.next() {
@@ -97,6 +102,12 @@ fn parse_args() -> Result<Args, String> {
"--mute" => a.mute = true,
"--menu" => a.menu = true,
"--caravans" => a.caravans = true,
"--handling" => {
a.handling = val("--handling")?.parse().map_err(|_| "bad --handling")?;
if a.handling > data::vehicle::PRESETS.len() {
return Err("--handling must be 0-6".into());
}
}
"info" | "extract" | "render" | "bench" | "trace" | "music" | "play" => a.cmd = arg,
"unpack" => {
a.cmd = arg;
@@ -370,6 +381,7 @@ fn render_shot(data: GameData, args: &Args) -> Result<(), String> {
App::new(data, audio)
};
app.settings.caravans = args.caravans;
app.settings.handling = args.handling;
if args.menu {
for _ in 0..(args.seconds / game::DT) as usize {
app.update(&Input::default());
@@ -399,7 +411,7 @@ fn render_shot(data: GameData, args: &Args) -> Result<(), String> {
.entrants
.iter_mut()
.filter(|e| matches!(e.driver, game::Driver::Human(_)))
.map(|e| e.ai.drive(&e.car, &race.track, e.next_gate, game::DT, true))
.map(|e| e.ai.drive(&e.car, &race.track, game::DT, true))
.collect();
race.step(&controls);
app::draw_race(&mut frame, &app.data, &race, &mut cams, players);
@@ -428,6 +440,7 @@ fn bench(data: GameData, args: &Args) -> Result<(), String> {
App::new(data, audio)
};
app.settings.caravans = args.caravans;
app.settings.handling = args.handling;
let frames = (args.seconds / game::DT) as usize;
out!(
"{:<10} {:>5} {:>8} {:>9} {:>8}",
@@ -537,6 +550,7 @@ fn trace(data: GameData, args: &Args) -> Result<(), String> {
App::new(data, audio)
};
app.settings.caravans = args.caravans;
app.settings.handling = args.handling;
// Same seed as `bench` uses for this track, so problems seen there can be traced.
let mut race = app.build_race(
args.track.saturating_sub(1),
@@ -547,18 +561,31 @@ fn trace(data: GameData, args: &Args) -> Result<(), String> {
args.track.max(1) as u32,
)?;
let mut img = race.track.rgb();
let plot = |img: &mut Vec<u32>, wx: f32, wy: f32, c: u32| {
let (x, y) = track::to_screen(wx, wy);
let plot = |img: &mut Vec<u32>, t: &track::Track, wx: f32, wy: f32, c: u32| {
let (x, y) = t.to_screen(wx, wy);
if track::on_screen(x, y, 0.0) {
img[y as usize * track::W + x as usize] = c;
}
};
// The walls: both road edges.
let edges = &race.track.edges;
for (k, g) in edges.iter().enumerate() {
let h = edges[(k + 1) % edges.len()];
for i in 0..8 {
let f = i as f32 / 8.0;
for (p, q) in [(g.a, h.a), (g.b, h.b)] {
let (x, y) = (p.0 + (q.0 - p.0) * f, p.1 + (q.1 - p.1) * f);
plot(&mut img, &race.track, x, y, 0xc0c0c0);
}
}
}
for (gi, g) in race.track.gates.iter().enumerate() {
let c = if gi == 0 { 0xffff00 } else { 0xff2020 };
for i in 0..=64 {
let k = i as f32 / 64.0;
plot(
&mut img,
&race.track,
g.a.0 + (g.b.0 - g.a.0) * k,
g.a.1 + (g.b.1 - g.a.1) * k,
c,
@@ -569,7 +596,7 @@ fn trace(data: GameData, args: &Args) -> Result<(), String> {
for _ in 0..(args.seconds / game::DT) as usize {
race.step(&[]);
for (i, e) in race.entrants.iter().enumerate() {
plot(&mut img, e.car.x, e.car.y, TRAIL[i % 4]);
plot(&mut img, &race.track, e.car.x, e.car.y, TRAIL[i % 4]);
}
}
let n = race.track.gates.len();