Decoded from SKIDMARKS_CD32: - Computer drivers: the path of nodes laid 2 game units apart along the middle of the road, with side points 1 unit in from each wall. Drivers aim four nodes past where they'll be in four frames, or cut to the other side two nodes on at 70% throttle when running wide. Replaces the pure-pursuit AI; laps are quicker on every track. - Collisions: six points on the player's car (corners and sides, from .set words 6 and 7, now known to be half-length and half-width) are tested against the other car's box. Hits push the cars apart with a strength that grows with their speed difference, and spin the car. As in the original, only human players' cars collide; cars at different heights don't. - Towing: the hitch is at the car's back, the caravan sits 0.656 units behind it, and past 90 degrees both car and caravan bend back. The reconstructed collision weight (`mass`) is gone; old asset files with it still load.
19 KiB
Super Skidmarks data formats
Notes from reverse engineering the AGA/CD32 release of Super Skidmarks (Acid Software, 1995), worked out from all six disks and from the main executable's machine code. See Gaps for what's still unknown.
All multi-byte values are big-endian unless noted.
Disks
Each disk is a standard 880 KB double-density ADF (1760 blocks of 512
bytes). Disk 1 is FFS (DOS\1); disks 2–6 are OFS (DOS\0). The [HD] in
the file names refers to the hard-drive-installable release.
| Disk | Archive | Game files |
|---|---|---|
| 1 | smcd32a.lha |
Executables, sfx/, gfx/, the tune, vehicles/*.set, Camaro.aga, caravan.aga |
| 2 | smcd32b.lha |
vehicles/cow.aga, track/TRACK1 |
| 3 | smcd32c.lha |
track/TRACK2, vehicles/TRUCK.aga |
| 4 | smcd32d.lha |
track/TRACK3, vehicles/PORSCHE.aga |
| 5 | smcd32e.lha |
track/track4, vehicles/vw.aga |
| 6 | smcd32f.lha |
vehicles/F1.aga, Midget.aga, mini.aga, Police.aga |
Disk 1 also carries an Installer script that installs from all six disks, and a few AmigaDOS commands.
Each archive stands on its own; the set isn't one archive split across
disks. They use level-0 headers under a top-level super_skidmarks_cd³²\
directory.
- Compression: disks 2–6 store their members (
-lh0-), which are themselves CrunchMania-packed. Disk 1 mostly uses-lh5-(LZSS with static Huffman blocks, decoded bysrc/formats/lzh.rs). - Integrity: every member is checked against its header CRC-16. One
file,
c/iconx(an AmigaDOS utility the game doesn't use), fails its CRC in this dump. The referencelhatool reports the same failure.
CrunchMania
0 4 id: "CrM!" (LZ + fixed codes) or "CrM2" (LZ + Huffman)
lower-case "Crm" = delta-encoded samples (running byte sum)
4 2 unused
6 4 unpacked size
10 4 packed size (excludes this 14-byte header)
14 … packed stream
The stream is read backwards from 14 + packed - 6, a byte at a time,
LSB first. The last six bytes seed the bit buffer: a 32-bit word shifted
right by 16 - shift with 16 + shift valid bits, where shift is the
final 16-bit word. Output is also written from the end towards the start.
src/formats/crm.rs implements both variants. Every packed file on the
disks decodes exactly: the output buffer fills completely and the input is
consumed to its first byte.
Tracks (track/TRACKn)
0x00 7 × u32 end offset of sections 0..6
0x1C … section 0: thumbnails and palette for the whole file (20622 bytes, raw)
… sections 1..6: one track each, CrunchMania-packed
So the four files hold 24 tracks.
The last 142 bytes of section 0 hold:
| Offset | Size | Contents |
|---|---|---|
| 0 | 2 | File number (1–4) |
| 2 | 96 | The palette shared by the file's six tracks: 16 colours × R, G, B, each a u16 from 0 to 15 (Amiga 12-bit colour) |
| 98 | 12 | Six u16, one per track, meaning unknown. The values are 0–5, some with bit 6 (0x40) set, plus one 0x3F. |
| 110 | … | The name of the set of tracks, NUL-terminated |
The first 20480 bytes of section 0 are the track-selector thumbnails:
- Picture: one 640 × 64, 16-colour picture in the file's palette, stored as four bitplanes one after another (80 bytes per row, 5120 bytes per plane).
- Layout: the six tracks' thumbnails, 123 × 32 each, sit in a 3 × 2 grid at its top left, in track order: top row tracks 1–3, bottom row 4–6.
- Display: the picture is Amiga hi-res, whose pixels are half as wide as low-res ones. So a thumbnail shows at 123 × 64 next to the low-res track pictures, the same shape as the track.
Each track section unpacks to 281240 (0x44A98) bytes:
| Offset | Size | Contents |
|---|---|---|
0x0000 |
8192 | Height map: 64 rows (world y) × 64 u16 (world x). Each cell is one unit of the game's world; the ground there is value / 64 pixels high. |
0x2000 |
8192 | Unused, zero on every track. |
0x4000 |
8 | u16 index of the last gate (so the track has one more gate than this), u16 start x, u16 start y, u16 flags. |
0x4008 |
70 × 8 | Gate slots: x1 y1 x2 y2, world coordinates × 32 (× 512 in the game's own units). Slots after the last gate repeat the list from the start. |
0x4238 |
2144 | Unknown. 88 bytes of mostly doubled bytes, then what looks like leftover picture data. |
0x4A98 |
4 × 65536 | Four non-interleaved 1024 × 512 bitplanes, giving 16 colours. |
The executable replaces the start point and flags of two tracks with fixed values: (844, 50, flags 1) for file 1's track 2 and (187, 119, flags 0) for file 2's track 4. It isn't clear whether those track numbers count from 0 or 1.
Track geometry
Gates are line segments across the road and run in race order; gate 0 is the start/finish line. They're defined on an isometric ground plane rotated 45° to the picture.
The rest of this section uses this port's world units, which are 1/16 of the game's own unit (the 16.16 fixed-point values the executable works in). So a height-map cell is 16 × 16 units.
The executable projects a point z pixels above the ground plane like
this:
screen x = wx - wy + 512
screen y = (wx + wy) / 2 - 156 - z
The car sprite position uses 182 in place of 156, putting the sprite's top
left 26 pixels higher. With z set to the ground height, 98% of points
sampled along the gates land on road pixels across all 24 tracks.
Walls. The gates are also the control points of the road edges:
- The curves: routine
$24670evaluates a uniform quadratic B-spline through the gates' first ends for one edge, and their second ends for the other. At parameteri + fthat is(1-f)²/2 · G[i] + (1/2 + f - f²) · G[i+1] + f²/2 · G[i+2]. - Zones: at set-up the game cuts the road into zones, two per gate,
between consecutive cross-sections of the two curves. Each zone's four
sides are tested against the car (routine
$25b2c). - Collisions: crossing a side edge is a wall hit; crossing the front or back moves the car on to the next or previous zone.
- Fit: points 5% in from either edge are on road pixels 95% of the time, and points 15% outside only 25% of the time. So the curves follow the painted road edge.
The stored start point is on the road for 14 of the 24 tracks read as screen coordinates, and 10 read as world coordinates. The game seems to use it for the starting camera position. This port forms the grid up behind gate 0.
Road and scenery
Colours 8–15 are the driving surface on every track, so the fourth bitplane is effectively a road mask. The game's physics don't read the picture; the walls keep cars on the road.
Vehicles
Sprite banks (vehicles/*.aga)
CrunchMania-packed, 409600 bytes unpacked: 25 tilt blocks × 32 headings × 512-byte frames.
A frame is a 64 × 32 AGA hardware sprite. Each line holds 8 bytes of plane 0, then 8 bytes of plane 1, giving 3 colours plus transparent:
| Index | Use |
|---|---|
| 0 | transparent |
| 1 | tyres, shadow, dark |
| 2 | trim / accent |
| 3 | bodywork |
- Heading: frame 12 faces screen-right, and frames advance clockwise in 11.25° steps. In world terms, frame = 12 + (angle + 45°) / 11.25°.
- Tilt: block = roll × 5 + pitch, each in 0..4, with 2 = level. Block 12 is the car on flat ground.
The caravan (caravan.aga) uses the same layout. It's a trailer with a tow
bar, not a car.
Police.aga reads as all zeros with one third-party ADF tool, but it's
intact when read directly from the disk image.
Settings (vehicles/*.set)
- Header: the car's name and its driver's name, each followed by a newline.
- Settings: 16
u16values (see below). The caravan's values aren't driving values; two of them are −4. - Menu picture: 512 bytes. A 64 × 32 picture in two bitplanes, plane 0 (256 bytes) then plane 1.
Routine $332a copies the settings into each car, as 16.16 fixed point
with these shifts. Word k is the kth u16, counting from 0:
| Word | Car field | Value | Meaning |
|---|---|---|---|
| 0 | $bc |
w / 64 | drag (computer drivers) |
| 1 | $e2 |
w / 512 | grip (computer drivers) |
| 2 | $e6 |
w / 16 | steering, 64ths of a turn per frame |
| 3 | $c0 |
−w / 64 | stored but never read |
| 4 | $106 |
w / 8 | gravity, pixels per frame² |
| 5 | $b8 |
w / 512 | power (computer drivers) |
| 6 | (collisions, towing) | w / 32 | half the car's length, in game units; the tow hitch is at the back |
| 7 | (collisions) | w / 32 | half the car's width |
| 8 | $bc |
w / 64 | drag (human drivers) |
| 9 | $e2 |
w / 512 | grip (human drivers) |
| 10 | $b8 |
w / 512 | power (human drivers) |
Words 11–15 aren't read. Game logic shows how drag,
grip and power work together. In short, top speed is
power × drag / (1 − drag) game units per frame. For the Camaro that is
271 of this port's units a second for human drivers and 186 for computer
ones.
Handling presets
The race menu's handling option is CUSTOM (each car's own settings, as
above) or one of six presets that replace them for every car. The table
is at $21dc6, 8 bytes each. Bytes 0–2 are for human drivers, 3–5 for
computer drivers, and byte 6 is steering for both:
| Preset | Human | Computer | Steering |
|---|---|---|---|
| CLASSIC | 98 26 24 | 98 24 24 | 44 |
| GRUNTY | 92 40 30 | 108 33 25 | 60 |
| ZIPPY | 102 61 47 | 115 59 45 | 64 |
| RIPPY | 100 80 60 | 110 50 60 | 72 |
| SLIPPERY | 25 127 101 | 29 45 122 | 68 |
| PEDAL CAR | 117 23 17 | 104 17 10 | 30 |
For a preset (a, t, g):
- drag = a/128 × 0.98 + 0.01
- p = t/16 × (1/drag − 1), and power = p/8
- grip = g/256 / p / drag / 8
- steering = byte 6 / 32, gravity = 0.5
- The car's size (words 6 and 7) still comes from its own settings.
So top speed is simply t/128 game units per frame.
Sounds (sfx/*.iff, gfx/*.iff)
Uncompressed IFF 8SVX, 8-bit signed, mostly at Amiga rates (8363 Hz and
others). Looping sounds use the repeat part of VHDR. gfx/ holds a few
extra and alternative sounds (crash1, crash2, engine, motor,
screech); gfx/crash4.iff isn't IFF.
Pictures (gfx/*.iff)
IFF ILBM with ByteRun1 compression. skidtoon.iff is the 608 × 180,
16-colour title picture; chat2, mouse and sign are small odds and
ends.
ball.spr is a bank of 64 saved hardware sprites: a spinning ball with
the Acid logo. Each 544-byte frame is:
- Header (16 bytes):
u32saved memory pointer,u16lines (33),u16strips (4),u16flags (0x8020),u16bytes per strip (132), 4 unused bytes. - Strips: four 16-pixel sprites, each line two bitplane words.
Flag bit 15 marks attached sprites. The strips pair up into two 16-colour columns, so a frame is 32 × 33 pixels. The bank carries no colours.
Music (sfx/tune.crn)
A CrunchMania-packed OctaMED MMD1 module: 4 tracks, 21 plain 8-bit
sample instruments, 70 blocks played in order. The main song is positions
0–65, ending with a jump back to the start. Positions 66–69 hold separate
short pieces that end with "stop".
The song uses these commands:
1,2(slide up, slide down)3(portamento)9(speed)B(jump)C(volume, decimal)D(volume slide)F00(break),F1(play twice),F2(delay),FD(set pitch),FE(stop)
src/music.rs plays these. Its loudness tracks OpenMPT's rendering of the
module second by second through the whole song (correlation 0.996 over the
first minute), and it loops at the same point.
Game logic
SKIDMARKS_CD³² is one code hunk of 222,032 bytes, compiled from Blitz
Basic 2. Addresses below are offsets into it. All arithmetic is 16.16
fixed point (multiply at $2ceda, divide at $2e122).
Car record
Each car is a 266-byte ($10a) record:
| Offset | Field |
|---|---|
$0, $4, $8 |
x, y, height z |
$c, $10, $14 |
velocity x, y, vertical |
$18, $1c |
this frame's velocity change |
$58, $5c |
heading (64 per turn) and turn rate |
$60, $64 |
heading vector, from sine/cosine tables of 64 entries |
$68 |
computer steering, −1, 0 or 1 |
$6c, $6e |
pitch and roll notch, 0–4 |
$76 |
controller: 0–7 for joysticks and keyboards, 8 for computer |
$78 |
sprite frame: roll × 160 + pitch × 32 + heading / 2 |
$7a |
airborne |
$7e |
state: negative while racing, 1 while warping |
$a6 |
engine speed |
$aa |
forward speed |
$b8, $bc, $e2, $e6, $106 |
handling (above) |
$86 |
active (negative) |
$d2, $d6 |
links to the car towing this one, and the one it tows |
$ea |
zone (half-gate) progress |
One frame
The race loop runs at 50 Hz. Each car gets steering ($1c618), drawing
($1cafa), and moving ($1982c) in turn. While drawing, a car sprite
is skipped if the beam is already past a given line, so a frame always
fits in one field. Then $247ac integrates every car.
- Steering:
- Input is −1, 0 or 1, from the joystick or the computer driver.
- turn = (turn + input × steering) / 2. Airborne cars can't steer, and their turn rate is limited to ±1.
- Height:
- vertical −= gravity.
- The car looks two frames ahead: if
z + 2 × verticalis still above the ground atpos + 2 × velocity, it's airborne. - Otherwise vertical = (ground there − z) / 2.
- Pitch and roll each step one notch a frame towards
floor(rise + 2.5), clamped to 0–4. The rise is the ground height change over 0.2 units along world y (pitch) or world −x (roll). - heading += turn.
- Engine and grip, skipped while airborne:
- throttle is −1, 0 or 1. Half the vertical speed is taken off it going forwards (added going backwards), so hills slow cars down and speed them up.
- target = (engine speed + throttle × power) × drag
- change = grip × (heading vector × target − velocity)
- engine speed += (target − engine speed) / 4
- Walls:
- The zone test runs on
pos + 4 × velocity + 10 × change. - On a wall the velocity points back off it. The car is held at the contact point and loses 1.5× its velocity into the wall, so it bounces back at half speed.
- Crossing a zone's front or back moves
$eaon, and counts the laps.
- The zone test runs on
- Integrate: velocity += change, position += velocity, z += vertical.
A warping car (state 1) instead flies straight at a point on the track: its velocity is the distance to it / 16.
Note that the heading vector points to the car's rear: pushing the joystick forward gives a throttle of −1. This document flips that round and talks about forward throttle as positive.
Computer drivers
Computer cars use words 0, 1 and 5 for handling rather than 8, 9 and 10.
The path. At set-up ($1ea82 onwards) the game walks the middle of
the road, the average of the two edge curves. It drops a node every 2
game units, adjusting each step by bisection until the gap squared is
between 3.8 and 4.2. Each 36-byte node (array -$7e68(a5)) holds:
- the centre point
- the unit vector across the road
- a point 1 unit in from each wall
- its position along the spline
A second table maps each zone to its first node.
Each frame:
- The driver predicts its position four frames ahead,
pos + 4 × velocity. It finds the first node that point hasn't passed: only nodes within −2 to +8 zones of the car count, and a node is passed once the point is beyond the line across the road there. - Its default target is the centre of the node 4 on, at full throttle.
- If the predicted point is beyond the line joining one side's inset
points at that node and the one before, it's running wide. It aims
at the other side's point 2 nodes on instead, at 0.7 throttle. The
constants are Motorola fast floating point values set at
$14886: 1.0 (negated), and that times$b333. - Steering is the sign of the cross product of its heading with the direction from where it is now to the target, as if holding a joystick hard over.
Collisions between cars
Routine $1b324 tests only the human players' cars. Each is tested
against every other active car and caravan except the caravan it tows.
So computer cars pass through each other and through caravans.
For a pair (A = the player's car, B = the other):
- Proximity (
$260ba): skip unless |dx| ≤ 2 and |dy| ≤ 2 game units and |dz| < 17 pixels. So cars on a bridge don't hit cars under it. - Strength: k = 64 × |velocity A − velocity B|² + 20, in game units per frame.
- Points: six points on A, rotated with A, come from a table built
for every heading out of words 6 and 7: the four corners, then one on
each side at 0.58 of the half-width. Each is tested against B's box,
which is the same for every vehicle: ±0.375 across and ±0.75 along
B's heading (
$25f3c). - Push: for each point inside the box, B's velocity gains the point's offset from A's centre × k / 256 and A's loses the same. The side points use 2.5 k.
- Spin: each corner inside the box also turns A by k / 16 (64ths of a turn) on its turn rate, and twice that on its heading. The sign is the one that swings that corner away from B.
- Sound: any hit plays the crash sound for both cars.
Towing
A caravan is a car record linked from its car ($d6), and it's dragged
in the per-car loop at $1cf64:
- Hitch: at the back of the car, half its length (word 6) behind the centre. The game uses the car's position after this frame's move.
- Swing: the caravan turns to point at the hitch, and is placed 0.656 game units behind it, by setting its velocity to the gap.
- Jack-knife: if the caravan ends up more than 90° from the car, both are set to 45° either side of the angle halfway between them. So the caravan steers the car too.
- Warps: when the car warps, its caravan is parked off the map until it lands.
Executables
SKIDMARKS_CD³²(237 KB): a single-hunk compiled Blitz Basic 2 program. It includes IFF ILBM/8SVX/ANIM loaders and a tracker replay routine with note-period tables.skidmarks2(90 KB): a three-hunk program whose code is packed.
Gaps
- Game logic still to decode:
- the warp rules (when a car is warped back to the track)
- the race rules and scoring
- Unknown data:
- the six per-track words after each track file's palette
- the 2144 bytes after the gate slots in each track section
- the colours of
ball.spr, probably set by the executable
The engine's own editable formats, which skids extract converts all of
the above into, are described in assets/README.md.