Skip to content

The car controllers ​

How WINPOD.EXE drives each car before the physics moves it: the computer's controller and the player's controls. Traced for Un-POD (docs/PORT.md, "Opponents" and "Player input") and ported in crates/pod-ai. Addresses are POD 2.8's WINPOD.EXE (target/pod, the reference: AGENTS.md). The controllers were first traced in the first edition; its addresses, and the races recorded from it, are history in docs/versions.md. Each finding is observed (read in the code), measured (run under Unicorn, or read from the running game in run/vm), inferred or unknown.

Where the controller sits ​

Each driver (0x6345d8 + n × 0xb0, docs/systems/race.md) holds two functions: the physics step at +8 (FUN_0041b66c for every car) and the controller at +0xc. The physics step calls the controller first (call [edx+0xc] at 0x41b6f3), so each physics step runs the controller once (observed). The end of race runs it twice per step (FUN_00473240).

The controller is chosen from the racer's entry kind (0x6339c4, copied to driver +0x38) when FUN_004980a4 puts the cars on the grid (observed):

Kind (+0x38)Set up byController (+0xc)
1, a humanFUN_00422728FUN_00415f9c, the player's controls
1, with DAT_00633348 = 2 and DAT_004fde5e = 0FUN_00422698the computer's (inferred: the demo)
2, a computer carFUN_00422698FUN_004221d8
2 with bit 4 (a network car hosted elsewhere)FUN_00422698FUN_00422408, which follows the network's path state (FUN_00483c98, FUN_00483b08)
8FUN_004b291c(unknown)

So driver +0x38's value 2 marks a computer car and value 4 a network car (observed in FUN_00422698, which picks FUN_00422408 on flag 4; in the first edition measured in a running race: docs/versions.md).

When a human car crosses the end of a path ("Fin Course", FUN_00455bb8), FUN_00422788 gives it the computer's controller with lane 0: after the finish the computer drives the player's car (observed).

The step time. The physics step scales the frame time for each computer car before it calls the controller: the catch-up FUN_00422be8 (pod-physics' step::catch_up). The controller's dt (the rule draw's clock, the reset timer) is that car's scaled time, so the caller hands pod_ai::Race::dt the scaled value per car (measured: the POD step logs, whose draw clock matches only with it).

What the controller writes ​

The physics step reads (observed in both controllers; how the physics uses them is crates/pod-physics's subject):

FieldWhat
driver +0x34the input word: steering in the high half, buttons + 8 in the low half
car +0x58, +0x208the front wheels' angle, 16.16 radians, within ±car+0x484
car +0x130, +0x2e0minus the angle times car+0x494 (the rear wheels, inferred), or 0
car +0x44athe gear, set to 8 (reverse) while a computer car waits to be put back

The buttons: 1 accelerate, 0x10 brake, 4 left, 8 right, 0x20 gear up, 0x40 gear down (observed: FUN_0046fd18 maps the key map's slots 0, 1, 2, 3, 7, 8 to them, and FUN_004729e4 reads those slots from Ubi.ini as KeyboardAccelerate (default 200, the up arrow), KeyBoardBrake (0xd0, down), KeyboardLeft (0xcb), KeyboardRight (0xcd), KeyboardGearUp and KeyboardGearDown (defaults by language, from the tables at 0x58d509)). The start settles the cars with +0x34 = 0x10 (brake, no bias; docs/systems/race.md).

The computer: FUN_004221d8 ​

One step (observed; measured: see "Evidence"):

  1. Speed: the dot product FUN_004d41a0 of the velocity (+0x4c8) and the forward vector (+0x4a4).
  2. The cars around (FUN_0046dc04): every other car is classified by FUN_0046da4c against the driver's frame (position +0x498, forward, sideways +0x4b0). Nothing beyond 25 units in x or y or 5 in z. Then flags: 0x20 touching (closer than 1/64 unit, or, for any car but the target car at +0x64, within 12 units ahead or behind and 7 to the side); 0x10 just ahead in the lane (12 to 20 units ahead, within 7 to the side); 1 ahead (the forward share of the direction above 0.5); within 17 units, 2 or 4 to a side (sideways share above ±0.342, 20°); within 12 units, 8 behind (forward share below −0.94). The list goes to 0x58d770 as (car, flags) pairs; the flags of all the cars are or'ed together.
  3. The lane (FUN_00422028): the path tracker moves on (FUN_004abec4, pod-race). The driver steers at the point after its line point (+0x28). Within 8 units (|dx| + |dy|) of it the driver passes it, and the lane state's count (+0x78 bits 8 to 15) drops by one, clearing the state at 1. After such a pass, if the tracker's link starts at a fork and the car is more than 50 units from the tracker's point, the tracker jumps to the nearest point of the fork's other links (FUN_00421f58). Further than 8 units the driver also passes the point when it is nearer the next point than the segment is long less 8 and the next point is no nearer than the last one.
  4. The behaviour (FUN_0045f2a0, below): a throttle word and, often, a new target point.
  5. Steering (FUN_0046d990): the target's tangent, sideways over forward in 16.16, against the largest angle max (+0x484) in 16 steps of max / 16. The index runs 0 to 31: 0 beyond max, 31 beyond −max, else the number of steps the tangent takes to pass max. A target less than 1/64 unit ahead gives 31 on the − side, else 0. The angle is (index − 15) × max >> 4, negated when car +0xa4 is negative (inferred: running backwards).
  6. Throttle: t is the behaviour's word, plus 2 unless the index is 0 or 31. On a lane of type 2 the high half of the state is a speed limit: above it t is 0. Then t = 1 above speed 30 (0x1e0000) coasts; t below 1 brakes (0x10) above speed 20, or on a lane of type 5 when the behaviour asked to slow; anything else accelerates (1).
  7. A reset: while +0x7c is negative it counts up by the step time with the gear at 8. When it reaches 0 the gear goes to 0, FUN_004aa8a0 places the tracker from the chassis point (+0x360; pod-race's Paths::place, its search wider on Beltane), the line is the nearest point of a special lane within 40 units (FUN_004561d4) or else FUN_00455ed0's (the tracker's link's lanes within 40 units, then the special lanes, then the node whose first lane passes nearest and its nearest lane point by |dx| + |dy|), and the state clears. The car accelerates.
  8. The angle goes to the four angle fields, and +0x34 = angle << 16 + throttle + 8.

Note the input word's high half: the step first stores the steering index there, then the angle itself (observed: shl ebx, 0x10 of the angle at 0x422300). The human controller stores an index (below).

Behaviours and the opponents' rules ​

A driver runs one behaviour (+0x68) each step (observed, FUN_0045f2a0):

  • on a lane of type 5 it keeps its behaviour;
  • during the countdown it goes back to Steady with the state 0xc803;
  • on a lane of type 3 any other behaviour gives way to Settle;
  • otherwise, when the flags masked with 0xcf differ from the situation the behaviour was chosen for (+0x6c), it picks a new one (FUN_0045f1f8).

The draw. FUN_0045f1f8 adds the step's time to DAT_00581e88 and takes its signed remainder by 100 as a byte. For each of the driver's rules (+0x70 count, +0x74 table, 8 bytes: the behaviour's function, the situation byte, the chance byte) whose situation is the masked flags, the rule is taken when the draw is below its chance, else the draw drops by the chance. The loop does not stop: a later rule can take the same draw again, and the last one to take it wins. With none, Settle. This is the opponents' only chance: no rand() call is reachable from the controller (observed: the call closure of FUN_004221d8, and docs/systems/race.md, "Random numbers"). It depends on the frame times, so a replay needs them.

The rules are read from text by FUN_0045f404 (observed): a count (%ld), then per rule three numbers (%ld%ld%ld): the rule type, the situation, the chance in percent. Its error messages name the block "Attaques_Disponibles" and the file "Fich_Conc"; which file that is, is unknown (docs/formats/tracks.md). The opponent's lane (+0x44) and a second word (+0x48; unknown: no controller reads it) come with the opponent (FUN_0046774c). FUN_0045f3dc stores each rule, and FUN_0045f36c turns its type into a function (observed, the jump table at 0x45f32c):

TypeBehaviourWhat it does
(default)Steady, FUN_0045f1a0steer at the next line point; a touching car: avoid (FUN_0045f11c); a car just ahead: draft beside it (FUN_0045ef84)
Settle, FUN_0045f1c4as Steady, then back to Steady, the target car cleared, the situation the current flags
1Overtake, FUN_0045e8d8pass the car ahead: aim beside it by 3 × the car's length (same heading) or 4 × its width, on the side free of cars
2, 3CutIn4, CutIn2, FUN_0045ea70, FUN_0045eb48aim 16 units ahead of the car beside and 8 to its side
4, 9, 14Steady
5, 6Shadow4, Shadow2, FUN_0045ec20, FUN_0045ecd0beside a car facing the same way: move the target one sideways vector away
7Shadow8, FUN_0045ed80a car behind facing the same way: move the target towards its side
8, 12, 13Ram1, Ram4, Ram2, FUN_0045e7dc, FUN_0045e830, FUN_0045e884throttle word 2, aim at the car ahead or beside
10Push, FUN_0045e7c8throttle word 2
11Brake, FUN_0045e7d0throttle word −2
15, 16Idle, FUN_0045e7d8nothing

A behaviour moves the target only when the new point lies within 33° of the lane's direction (FUN_0045e6e0: the dot product of the two normalised directions above 0xd6d8); "same heading" is the same test on the two cars' forward vectors. The car a behaviour acts on is the target car (+0x64) if it carries the flag, else the last car in the list that does.

Difficulty. The difficulty (DAT_00633334: 1, 2 or 4) picks the track's race paths and opponents. The lanes, their speed limits (type 2) and the opponents' rules differ per difficulty; no speed factor per difficulty is in the controller (observed). On Azthec, POD's EASY opponents carry no rules at all, so they only follow their lanes and draft; the MEDIUM ones carry 4 to 10 each (measured in the running game: driver +0x70).

Lanes and "Troncon" ​

The lanes are the race sections (read_race_sections, FUN_00466c20): per path node a list of entries, 12 bytes at run time: the lane number | the point count << 16, the first point, and flags (0x80 off limits; the low four bits a type; bits 8 to 15 a count of points before the state clears; for type 2 a speed limit in the high half). FUN_004560ac lists the entries of type 4 to 6 at DAT_0054325c.

At a gate with "Troncon" (FUN_00455c64) a computer car (driver flag 2) takes the lane FUN_004559dc chooses at the gate's node. Of the lanes not off limits and not of type 4 to 6 (type 1 only when the car's speed by FUN_00423ac0 is above the high half of its flags) it is the lane whose number is nearest the driver's lane (+0x44), within 100. On a tie it is the later lane whose number is below the driver's. With none, it is the first lane of the node's first link out. The driver takes the lane's first point as its line, the flags as its state (+0x78) and the next point as its target. The tracker starts on the link carrying the lane (FUN_00455aac) and runs (observed; measured). The human branch is pod-race's.

The player: FUN_00415f9c ​

  1. Buttons from the driver's device (+0x40, records of 0x34 bytes at 0x621184). A device whose +0x2f is below 4 is read by FUN_0046fd18, else by FUN_0046fe80. Kind 0 (+0x2c) is the keyboard: DirectInput's GetDeviceState (256 bytes, vtable +0x24 of the device at DAT_00621b50) against the key map at 0x621938 from Ubi.ini. Kinds 1 to 3 are joysticks. The x axis against two thresholds (+0x18, +0x1c) gives 4 and 8. The y axis (+0x20, +0x24) and the buttons give words from a map of 36 words (0x4fdf88 and on, one for the first joystick, DAT_004fdf80, and one for the others, per kind). FUN_0046fe80 reads +0x2f buttons through the table at 0x4ea8a0 (observed).
  2. Steering by the kind: 2 the axis linearly (FUN_00415a8c: max / 2|r| × (v²/|r| + |v|), v the axis from its centre +0x10 and r the travel to +0x8, both × 256); 3 the axis to the fourth power (FUN_00415b70: max × v⁴ / r⁴); else the keyboard (FUN_00415c68). The keyboard turns the wheels at the car's steering speed (+0x488) per second while 8 or 4 is held, from the centre when they pointed the other way. With neither held they return at the return speed (+0x48c) times the steering speed. A counter-steer then overrides the player when the car slides against the steering: wheel 3's vector (+0x294) across the car above 0.73 and longer than 8, its sideways share above 0.09, the body turning into the slide (+0x4fc · +0x4bc) or the latch at +0x490 bit 2 set. The wheels go to half the slide share, at most the largest angle (observed; measured).
  3. The input word: high half ((angle << 16) / max + 0xffff) >> 12, at least 0; low half the buttons + 8.

The gearbox is not in the controllers: no controller writes the gear except the computer's reset (observed). The automatic gearbox belongs to the physics step (FUN_00418bd4, docs/formats/cars.md); the gear buttons 0x20 and 0x40 reach it through +0x34 (observed as inputs; how the physics uses them is pod-physics').

The fixed-point helpers ​

FUN_004c5e88 builds three tables at start-up (observed; measured: every word, see "Evidence"): square roots (0x70c704, entries 0x2000 to 0x7fff hold √j × 65536 truncated), reciprocals (0x6e4704, entry k holds 2^32 / 256k) and squares (0x704704, (256k)² >> 16). The pointers to them are DAT_004ed434, DAT_004ed42c and DAT_004ed430. FUN_004d3e90 is a square root through the table. FUN_00423ac0 and FUN_00423c64 take a vector's length and normalise it with the squares and reciprocals. FUN_004d43ad takes a length with the x87 and the root table, and FUN_004d4606 scales a vector, both rounding as the dot product does (docs/systems/race.md).

Evidence ​

WhatHow it is known
The computer's controller FUN_004221d8 and all its calleesMeasured: measure/controllers.py runs POD's WINPOD.EXE under Unicorn on 2,000 whole random race states near POD's Beltane paths (eight cars, rules of all 16 types, lane states, resets) and on five races of eight computer cars over 3,000 steps each, the cars moved by a stand-in for the physics, with 894 "Troncon" lane choices; crates/pod-ai/tests/measured.rs matches every neighbour list, line, tracker, behaviour, situation, target, state, the draw clock, the input word and the car fields
Steering FUN_0046d990, lengths FUN_004d43ad, FUN_00423c64, lanes FUN_004559dc, FUN_00455ed0Measured: 2,000 random cases each (500 for FUN_00455ed0), all match
The player's FUN_00415f9cMeasured: 2,000 cases over the keyboard, the three joystick kinds and both readers, all match (the input word and every car field)
The tables of FUN_004c5e88Measured: all 90,112 words between 0x6d4704 and 0x72c704
Against POD's running gameMeasured: two races on Azthec read from POD in the emulated PC with run/vm/ctlsnap.py (300 snapshots each, run/vm/pod.sh on display :4), run under Unicorn by measure/controllers_recorded.py and checked by tests/measured.rs. EASY: 2,100 controller steps, 284 lane passes, 30 rule draws (no rules to take). MEDIUM (POD_DIFFICULTY=Normal): 2,100 steps, 241 lane passes, 21 draws, 3 of which took a rule. Every step matches, the draw clock DAT_00581e88 included
Against POD's own resultsMeasured: the race-loop session's POD step logs (derived/physics/pod-beltane-easy, pod-beltane-medium; pod-physics' run/vm/steplog.s) hold each car and driver as the game left them when the controller returned, every step. measure/controllers_steplog.py turns them into cases with the step time the controller saw and the opponents' rules from the memory snapshot; the test carries the draw clock from car to car. EASY: 11,431 controller calls; MEDIUM: 7,763, with 94 behaviour changes (overtake, ram, brake, shadow). Every call matches the game's own result with no emulator in between, and the port's draw clock reaches the game's at all 2,740 step boundaries

The FPU's precision. The port's x87 helpers round each product and sum to 53 bits, as under the control word 0x027f, and the measurements run under 0x027f. The first edition races with 0x027f in the emulated PC (measured in pod-physics' step log; Watcom's start-up leaves 0x037f, and what changes it is unknown), and there 53 bits gave the game's value where 64 did not (docs/versions.md). POD races with 0x027f too (measured: every step of the POD step logs records it). Unicorn itself starts with 0, single precision (docs/knowhow/unicorn-fpu-starts-single-precision.md).

Not ported ​

  • FUN_00422408, the network computer car; FUN_004b291c (kind 8).
  • "Decision PIT" (FUN_004570ec), which sends a damaged car into the pit lane.
  • DirectInput itself and Ubi.ini's key names: the caller hands in the key map, the keyboard state and the joysticks as the original read them.

Open ​

  • What +0x48 of a computer driver does (1 to 3 in the opponent records).
  • What the high bits of a lane's flags mean beyond type 2's speed limit and the point count, and which lanes of Beltane carry types 1, 3 and 5.
  • The key map's slots 4 to 6 (read by FUN_004729e4, not by the controller).
  • A headless live championship using the Rust port reaches Galleria and then indexes lane point 1824 in a list of 1824 points (crates/pod-ai/src/computer.rs, follow). Measured by python3 run/e2e/unpod.py --out derived/e2e/worktree-merge-finaltest, after rebuilding with cargo build --release -p unpod; a standalone Galleria race reproduces the panic. The track has a final point outside every declared lane range, but why the original avoids this boundary is unknown. Do not wrap or clamp the line until the track transition and original run are compared at the same point.

Prose CC0 1.0, code Apache 2.0. POD: Planet of Death is © Ubi Soft; none of its files are on this site.