- C 66.9%
- Python 30%
- Shell 1.5%
- PowerShell 1.1%
- Makefile 0.5%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
Two paths and a hostname that were fine while this was local and are not once it is published. The OMT toolkit location is looked for in three places now -- OMT_TOOLKIT, a gitignored one-line tools/omt_path.txt for a fixed local checkout, then a sibling directory beside the repo -- instead of a hardcoded path on one backup drive. The game directory is the same: pass it on the command line as the README already says, or set MECHANIX_GAME, and the fallback is a plain `game` directory rather than somebody's home. Also drops the machine name from PROGRESS.md. It said which desktop the work was done on, which is nobody's business and no help to anyone reproducing it. Verified after: every tool parses, the census runs, and a full extraction of level 1 comes out identical. Co-Authored-By: Claude Opus 5 <[email protected]> |
||
| src | ||
| tools | ||
| traces | ||
| .gitattributes | ||
| .gitignore | ||
| CONTRIBUTING.md | ||
| HANDOFF.md | ||
| Makefile | ||
| PROGRESS.md | ||
| README.md | ||
| SPEC-GAPS.md | ||
mechanix-baseline — the post-spec baseline
This is a starting point, not a finished engine, and not a statement about what the game should be. Take it, build on it, change whatever needs changing.
Nothing here is a claim on the design. Where it says a wall is 80 degrees or a lap counts on a gate crossing, that is what the measurements and the owner's reading of the original came to on this pass — not a ruling. If it does not match the game you want to build, change it; the parts that are actually measured against captures are marked as such throughout, and everything else says openly that it is a choice.
A working implementation of the Hot Wheels Mechanix vehicle and race simulation:
a deterministic C99 core validated frame-by-frame against captures from the
original game, plus mxplay, which drives that same core in real time with the
game's own levels, cars, sounds and HUD.
It was written from the published specification in
mechanix-re — and the point of
writing it was to find out where that specification is silent, ambiguous or
wrong. There are thirty entries, and they are all written down.
Why this exists at all
Partly for the gaps list. Partly for a reason that has nothing to do with code: so that the mistakes in the specification are the spec author's to carry, and not yours.
The spec set was derived from the original's disassembly. Building an
implementation from it, driving that against fresh captures and writing down
every place it turned out to be wrong is work that has to touch the disassembly
side to be checked — and the whole point of the clean-room arrangement is that
the person building the engine does not. So it was done here instead. What
crosses the wall is this repository and SPEC-GAPS.md: measurements and prose,
findings and corrected numbers, and none of the analysis that produced them.
Read that as an invitation rather than an instruction. You did not write the spec, you are not answerable for its errors, and you are not bound by the readings this repository settled on.
And in the other direction: none of this runs without awefan's work. The OMT
parsers that decode every level, car, texture and sound here are his, out of
years of modding AWE Games titles, and his own Mechanix engine was the oracle
that settled four things in this extractor that would otherwise have been
guesses. CONTRIBUTING.md sets out exactly which parts are
whose, and which mistakes are not his.
Why "post-spec"
The specification was derived from the disassembly and never run. Implementing it and driving the result against real captures turned up a long list of things it does not say, says ambiguously, or says incorrectly — and a few things the earlier passes of this repository got wrong too, which are corrected in place rather than quietly edited.
The list is SPEC-GAPS.md, and it is the primary deliverable.
Thirty entries. A sample of what changes if you build straight from the spec:
| the spec says | what the game does |
|---|---|
| §4 covers throttle and brake | it says nothing about coasting, and a car with no key held decays at exactly 0.5 * speed per second (#6) |
§13.2 gives |v| from speed |
reverse is a different law entirely — |v| = |speed|, no 1.7 multiplier (#8) |
| §4 says turbo raises the cap "while held" | the key is not the gate. Held for 1148 frames, the cap moved for 0 of them; and while it is engaged, |v| = 1.9 * speed and the turn rate is ×1.5 (#3) |
| §13.4's yaw uses the steering angle | it uses the angle after this frame's integration, not before (#2) |
§16 says the path nodes are the vpointp markers |
they are not. 29 nodes against 20 markers, and no node lands on one (#21) |
| §16/§17 treat checkpoints as positions | they are quads you cross, two triangles each, on all seven levels (#25) |
| a face's material is a texture key | it is also what the face is — water, slowed ground, a sound trigger, a marker (#24) |
And the corrections this repository made to itself: every captured trace turns out to be from level 2, not level 1, which voids one earlier finding outright and re-keys another (#30).
What the spec got right is listed too, at the bottom of that file — including the parts that came out bit-exact, because a gaps list that only records failures misrepresents the document.
What you get
mxsim |
headless. Replays a trace, emits per-frame state, and tools/mxcheck.py reports per-law residuals against the capture. Steering is exact to the bit over 22,792 steps; position is exact to float32. |
mxplay |
the same core with a clock, a software rasteriser, all seven levels, nineteen cars with bolt-on parts, a garage menu, sound and the original's HUD art. |
mx_car.c, mx_race.c and mx_trig.c are shared between them, unchanged.
tools/crosscheck.sh builds both toolchains and compares all eight traces byte
for byte, so the physics on screen is the physics in the residual table. Anything
mxplay adds that the specification does not cover — water drag, wall sliding,
suspension, body lean, audio — is applied after mx_dynamics, in
mx_play.c, never inside the model. If you change the model, run
tools/crosscheck.sh; if it stops passing, the table stops describing the game.
Where it is honest about not knowing
Every chosen number says so where it is defined, and SPEC-GAPS.md says why it
could not be measured. The water drag, the wall scrub, the lean gain and the HUD
layout are reconstructions. The AI opponents are a stand-in — §19 says the
original's AI is its own routine and that synthetic key presses will diverge, and
synthetic key presses are exactly what these are. Two effects were measured and
deliberately not implemented — the cornering speed loss and the gradient term
— because their coefficients came out of a regression, and fitting a number that
makes one trace pass is the specific mistake this project has retracted before.
Provenance, and a boundary that has since moved
This started as dirty-side work: whoever wrote it has read the original's
disassembly, because the specification it was written from was derived from that
disassembly by the same project. mechanix-re's PROCESS/CLEAN-ROOM.md sets the
rule for that case — its own repository, no code copied into the spec, and the
independent implementer neither given it nor reading it.
The owner has since directed that the three strands merge: the specification
(mechanix-re), this baseline, and awefan's engine are to be combined and built
on. That is the owner's call to make, and it retires the "must not be given it"
half of the rule. What has not changed:
- no code from here is copied into
mechanix-re. The spec carries measurements and prose, andSPEC-GAPS.mdis the thing that crosses; - no game assets are committed here.
out/track1.mxtandout/car.mxmare derived from the game's own containers and are built from your copy, not shipped.
Where this repository has used awefan's published data, it has used it as an
oracle rather than a source: the tools/check_*.py scripts compare, and
nothing of his was copied in. CONTRIBUTING.md has the full
account — what is his, what those checks settled and with what numbers, and what
is emphatically not his.
Getting started
Nothing derived from the game is committed here — no meshes, no textures, no audio. The repository is code and measurements; the assets are built from your own copy of the game in one command, and stay on your machine.
You need: a copy of Hot Wheels Mechanix, Python 3 with Pillow and NumPy, the
OMT asset toolkit (awefan's -- these tools import it rather than reimplementing
its material resolution, which the naive lookup gets wrong for most meshes), and
a C99 compiler. mxplay is Win32 and builds with mingw; mxsim, the headless one,
builds with anything.
# 1. tell the tools where the OMT toolkit is (only if it is not in the default place)
export OMT_TOOLKIT=/path/to/omt_asset_toolkit
# 2. build the assets from YOUR game directory -- the one with level1.omt in it
sh tools/extract_all.sh /path/to/game # 7 levels, 7 skies, 19 cars, 13 parts
sh tools/extract_audio.sh /path/to/game # music per level, 102 effects
python3 tools/extract_hud.py /path/to/game out/hud.mxs # the HUD artwork
# 3. build and run
make play # -> mxplay.exe (i686-w64-mingw32-gcc)
./mxplay.exe
That drops you in the garage. Arrow keys move up and down the list and change the value on a row; Enter starts the race; Esc quits.
| row | what it does |
|---|---|
| LEVEL | all seven, by name — City, Tiki, Beach, Construction, Moon, Pluto, Asteroid |
| CAR | all nineteen out of Vehicles.omt |
| WHEELS | the four tyre sets. The car re-fits itself around them: the radius and the mounts are recomputed |
| ENGINE / WING / NITRO | the bolt-on parts, or none |
| RATING | 25–300. This one is real — SPEC 3 turns it into accel, cap and the turbo cap, and the line under the menu shows what you are about to get |
| LAPS | 1–12 |
| AUTOPILOT | let it drive itself, which is the quickest way to see a whole circuit |
In the race: arrow keys drive, X turbo, A toggles the autopilot, C
cycles the chase camera, R respawns, 1–7 jump straight to another level,
M music, S effects, W wireframe, B shows the faces the collision test
treats as walls, P pause, F1 hides the help, F2 back to the garage,
Esc quits.
If a level will not load, mxplay says which file is missing and which command
builds it. --race skips the garage and --level N --autopilot goes straight to
a self-driving lap, which is what the screenshots in this README were taken with.
What to look at first
If you are here for the reverse engineering rather than the driving,
SPEC-GAPS.md is the point of the repository and
HANDOFF.md is what is still open. The single most useful finding
for anyone building on the level data is #24: a face's material says what the
face is — not just which texture it wears — and tools/material_census.py
prints that vocabulary for every level.
Building by hand
C99, no dependencies beyond libm. make play is the one you want for the
game; the rest is the headless side.
make
On the machine the traces were captured on there is no native compiler, so the build goes through mingw in WSL and produces a 32-bit Windows binary — fitting, since the original is 32-bit x86:
make win # -> mxsim.exe, via i686-w64-mingw32-gcc
-ffp-contract=off is not optional. With contraction on, a compiler may fuse a multiply-add
and change the rounding, and §5's steering is checked for bit-exactness.
Run
# replay a trace's (elapsed, input) sequence, free-running from frame 0
mxsim replay traces/s8_lap.csv -o out/s8_closed.csv
# one-step predictions with each law fed only recorded inputs -- this is what a
# per-law residual means, and what the table below is measured in
mxsim replay traces/s8_lap.csv --law -o out/s8_law.csv
# the same, with the corrections this exercise measured switched OFF, so the
# difference they make is measurable rather than asserted
mxsim replay traces/s8_lap.csv --law --spec-literal -o out/s8_literal.csv
# SPEC 17's race accounting against a full six-lap race
mxsim race traces/race01.csv --laps 6 -o out/race01_check.csv
Validate
python tools/mxcheck.py traces/s8_lap.csv out/s8_law.csv # per-law residuals
python tools/mxcheck.py traces/s8_lap.csv --self-test # instrument check
python tools/sweep.py # every trace, one table
tools/mxcheck.py contains no model. It compares two CSVs — the frozen trace and mxsim's
output. A checker that restates the laws in Python is testing a second implementation of the
same belief, and this project has a recorded history of instruments that agreed with whatever
they were built alongside. --self-test compares a trace with itself and must produce exactly
zero everywhere; run it whenever the checker changes, because a clean pass on a negative is a
claim about the instrument until it has failed on a positive.
Residuals
python tools/sweep.py, one row per scenario. free is frames with no contact, not airborne,
not at the cap, no mid-frame parameter change, and speed above 200 (where §13.4 states its
measurements apply); straight is those of them with the steering centred, which is the
population §4's longitudinal law is a claim about.
| trace | frames | free | straight | steer max | pos med | speed med | |v| med | yaw med | speed med, turning |
|---|---|---|---|---|---|---|---|---|---|
| s1_accel | 2500 | 304 | 304 | 0 | 0 | 0.287 | 0.362 | 0 | — |
| s2_corner | 2500 | 1502 | 71 | 0 | 0 | 0.104 | 0.138 | 0 | 9.25 |
| s4_air | 3000 | 422 | 422 | 0 | 0 | 0.112 | 0.062 | 0 | — |
| s5_oob | 3000 | 636 | 636 | 0 | 0 | 0.339 | 0.339 | 0 | — |
| s6_brake | 2600 | 1944 | 1944 | 0 | 0 | 0.232 | 0.578 | 0 | — |
| s7_turbo | 2600 | 142 | 142 | 0 | 0 | 0 | 0.000064 | 0 | — |
| s8_lap | 4000 | 1359 | 470 | 0 | 0 | 0.837 | 4.32 | 0 | 12.25 |
| s9_coast | 2600 | 260 | 260 | 0 | 0 | 0.245 | 0.223 | 0 | — |
| trace01 | 1500 | 305 | 170 | 0 | 0 | 0.372 | 1.01 | 0 | 9.69 |
Against the targets this was to meet — the ones the specification's own laws already achieve:
| law | target | achieved |
|---|---|---|
| steering integration | exact (0.00000) | 0, all 22,792 steps of all eight traces |
| position advance | ≤ 0.001 units median | 0 — exact to float32 on free-running frames |
| speed update | ≤ 1.0 median, free-running | 0 – 0.84 |
| velocity magnitude | ≤ 1.0 median, free-running | 0 – 1.01, except s8_lap at 4.32 |
s8_lap's velocity residual is the one number above target. It is not a modelling error: the
same law applied to the trace's own recorded speed gives 3.12 on the same frames, so 3.12 of
the 4.32 is the specification's law disagreeing with the game, not this implementation
disagreeing with the law. s8_lap is the fastest trace (speed to 2228, five different
ratings, turbo engaged) and the disagreement grows with speed.
The turning column is the headline finding rather than a defect: §4's longitudinal law has
no steering term, and the original loses a great deal of speed while cornering. Ten to twelve
units of median error where a straight frame gives 0.1 is the size of what is missing
(SPEC-GAPS.md #7).
What the measured corrections buy
--spec-literal implements SPEC/VEHICLE-MODEL.md as written; the default adds the three
corrections this exercise measured (SPEC-GAPS.md #2, #3, #6, #8). The difference:
| correction | population | spec-literal | corrected |
|---|---|---|---|
| #2 yaw uses the post-integration steering angle | s8_lap, 1479 frames where the steering is ramping |
yaw median 0.0778° | 0 |
#3 |v| = 1.9 * speed while boosted |
s8_lap, free-running straight |
|v| median 5.92 | 4.32 |
#6 speed -= dt*0.5*speed when coasting |
s9_coast, free-running straight |
speed median 0.539 | 0.245 |
| #6 (same) | s6_brake |
speed median 0.348 | 0.232 |
#8 |v| = |speed| in reverse |
s6_brake, free-running straight |
|v| median 208.5 | 0.578 |
Correction #2 costs nothing while the steering sits on its ±45 clamp, which is most of a key-held corner — which is exactly why it is easy to miss.
SPEC-GAPS.md #11, the float32 rounding of intermediates, is not switchable: it is a
statement about arithmetic rather than about behaviour, and without it nothing else can be
judged, because it puts a one-ulp floor under every residual in the table.
Free-running replay
Fed only frame 0 and then the (elapsed, input) sequence, the model diverges — as it must,
with an unmodelled cornering loss, an unmodelled gradient term and no visibility of walls.
Frames until horizontal position error exceeds:
| trace | 1 unit | 10 units | 100 units |
|---|---|---|---|
| s1_accel | 233 (2.4 s) | 272 (2.9 s) | 477 (5.4 s) |
| s9_coast | 162 (2.1 s) | 203 (2.6 s) | 461 (5.3 s) |
| s8_lap | 33 (0.4 s) | 50 (0.6 s) | 91 (1.1 s) |
| s2_corner | 1 (0.2 s) | 4 (0.2 s) | 24 (0.5 s) |
s2_corner diverges immediately because it opens with the car pinned against a wall, which
the model cannot see. This table is reported because it is the honest answer to "does it drive
like the original", and the answer is: law by law yes, over a lap no, and SPEC-GAPS.md says
why.
Determinism across toolchains
sh tools/crosscheck.sh
builds with both toolchains and compares the output byte for byte. All eight traces come out
identical between a 32-bit x87 mingw build and a 64-bit SSE gcc build. That is the point of
storing state as float and computing intermediates in double with every product forced
through mx_f32(): without it, an x87 build keeps excess precision in registers and the two
diverge — which is exactly the failure §20 names as a risk to exact replay.
Race rules
mxsim race traces/race01.csv --laps 6, driving §17's accounting from the checkpoint
transitions observed in a complete six-lap race:
race rules over 5779 samples: lap 0 wrong, cp_next 1 wrong,
lap_time 149 off by >0.25s, best_lap 0 off by >0.25s
The one cp_next miss is the seeded first sample. The 149 lap-time samples are the crossings
themselves, where a 20 Hz log locates the line up to 50 ms late. Lap numbering ran 1→7 with
the finish on 7, exactly as §17 says.
mxplay — the same core, in real time
Build and controls are under Getting started above. What follows is what it is made of and what it is not.
The garage is the front end: nineteen cars, four tyre sets, three engines, three wings, three nitros, all seven levels, and a rating slider. The car re-fits itself around whatever is chosen — the wheel radius comes from the tyre mesh and the mounts are scaled from the measured Catapult track by the body's own bounding box, because a Shadow Jet is 108 units across where a Catapult is 71. Where an engine, wing or nitro mounts is not recovered from anything; nothing in the container says, so it is placed off the body's bounds and is a reconstruction.
The rating is the part with evidence behind it. §3 turns it into the whole
envelope — Acc = R*4.5 + 1800, accel = R + 600, cap = Acc*0.66 — verified
exact at all five ratings observed in one race, and the line under the menu shows
what you are about to get. What the game uses to set the rating from parts is
not recovered, and rather than invent a table the menu keeps them separate.
SPEC-GAPS.md #1 is the related open question: the rating changes during a
race.
All seven levels load. Every one has exactly one cstart and three ccheck
gates, so §17's four-checkpoint cycle is universal rather than the level-1 fact
§17 hedges it as. The music containers name their single track after the level —
urbanmix, tikimix, beachmix, constructmix, moonmix, plutomix,
asteroidmix — so level N wants musicN.
It links mx_car.c, mx_race.c and mx_trig.c unchanged — the same objects
the residual table above is measured against. tools/crosscheck.sh still passes,
so the physics on screen is the physics in the table. Everything mxplay adds sits
outside the model, applied after mx_dynamics rather than inside it:
src/mx_track.c |
level geometry, a uniform grid over xz, and the queries §20 says cannot be recovered from the specification: ground height, the blocked-axis mask §14's contact response needs, the water surface, and the gate crossings laps are counted from |
src/mx_play.c |
QueryPerformanceCounter for the continuous float seconds §8 insists on, a software rasteriser with a z-buffer and perspective-correct texturing, §6's chase camera, four-strut suspension, and a driver that follows a resampled racing line |
src/mx_sound.c |
a waveOut mixer, sixteen voices, fed from the frame loop so it can never stall the game. The game's 102 effects are all mono 11025 Hz, so there is no resampler. The engine note is nine loops indexed by revs — CarLoop100..CarLoop450 — not one loop pitched, and the higher ones are shorter because they are the same cycles recorded faster |
tools/extract_track.py |
reads your containers, and classifies each face by what its material says it is — see below |
The level says what its own surfaces are
The single most productive thing in this repository's second pass. A face's
material is not only a texture key: it is how the level declares what the face is.
tools/material_census.py lists every material on every face of all seven levels,
and five families fall out that a texture-only reading exports as ordinary solid,
drawn world — 21 sound trigger volumes, the water sheets, the slowed beds beneath
them, twenty-five unrecognised marker families, and per-level checkpoint boxes.
SPEC-GAPS.md #24 has the table and #25–#29 the consequences: laps counted by
crossing a gate quad, water you drive through, a wall defined as steeper than 80°,
a glancing contact that is not a full stop, and a driver that follows a line
instead of a scatter of points.
On level 2 with the autopilot that is worth two clean laps, a 56.5 s best and zero rescues in 115 seconds. Before it, the car left the track once a lap and no lap was ever counted.
What it is not yet
No menus and no vehicle select — the cars are whichever meshes the --car / AI
paths point at. The three opponents are a stand-in: §19 says the original's AI
is its own routine and that synthetic key presses will diverge, and synthetic key
presses are exactly what these are (SPEC-GAPS.md #18). Placement ranks by lap,
then position on the line — defensible and unverified, because §17 does not say
what the original ranks on (#19). The MPH figure is a
display mapping, not a measurement — nothing in the spec relates the internal
speed scalar to the original's dial and no trace records both, so the cap
simply reads 100.
The HUD draws the original's own artwork, and that artwork answered a question
along the way: HUD.omt holds no meshes at all, 65 canvases, and among them are
eight speedometer frames that differ only in one more arc segment being lit.
The original's speed gauge is not a needle over a dial. Where on the screen the
plates go is still a reconstruction — a canvas carries no position.
Six levels have no measured vertical offset and no node capture. Only level 2
has either. That matters far less for playing than it sounds — the offset shifts
geometry and markers together, so a level at the wrong offset is still
self-consistent — but the node capture matters a great deal: with the real
29-node set level 2 laps cleanly, and on the 20 vpointp markers the others
scrape scenery, because those are a different and coarser point set (#21). One
screen run per level fixes it; HANDOFF.md has the procedure.
Layout
src/mx.h types, constants, and the two-halves frame model
src/mx_car.c the vehicle model -- SPEC 4, 5, 13, 14
src/mx_trig.c OMT's 14-bit angles -- SPEC 10 -- and float32 rounding
src/mx_race.c race rules -- SPEC 17
src/mx_trace.c CSV reading, columns addressed by name
src/mx_main.c the headless CLI
src/mx_track.c level geometry and the ground / wall queries -- mxplay only
src/mx_play.c the real-time driver: window, rasteriser, camera, HUD
src/mx_sound.c a waveOut mixer -- mxplay only, and no part of the model
tools/mxcheck.py the checker
tools/sweep.py every trace, one markdown table
traces/ the frozen captures -- evidence, never regenerated in place
The traces
Captured from Mechanixsw.exe with an execute breakpoint at the game tick, recording the
elapsed the engine actually received, the game's own key array, and the car object. Breaking
every frame slows the game but does not invalidate the trace: the simulation is a pure
function of the (elapsed, input) sequence, so a perturbed-but-self-consistent record replays
exactly.
| trace | frames | what it is for |
|---|---|---|
s1_accel |
2500 | standing start to the cap, then a head-on wall for 1700 frames |
s2_corner |
2500 | sustained cornering both ways, 2374° of total heading change |
s4_air |
3000 | six forced launches and landings; 33% of frames airborne |
s5_oob |
3000 | five forced out-of-bounds events and respawns |
s6_brake |
2600 | brake through zero into reverse, to the reverse floor |
s7_turbo |
2600 | turbo key held with the boost never engaging (see SPEC-GAPS.md #3a) |
s8_lap |
4000 | a driven lap: five ratings, turbo engaged, a lap crossing, all four checkpoints |
s9_coast |
2600 | throttle released at speed — the only clean coasting data |
race01 |
5779 | a complete six-lap race sampled at 20 Hz, to the finish |
trace01 |
1500 | the specification's own published trace, for comparability |
Every one was checked for liveness before use — a trace of a car that did not move proves nothing, and this project has been caught by that repeatedly.