# Player control bindings and vehicle steering

This note records what the installed retail executable establishes about PC
player controls. It separates the binding names exposed by the game from the
runtime vehicle-control path; the default key and axis assignments are not
decoded yet.

## Input configuration names

The executable embeds three PC configuration filenames:

| Configuration | Evidence |
|---|---|
| `PC_Keyboard_Config.fng` | Retail executable string table |
| `PC_JoyPad_Config.fng` | Retail executable string table |
| `PC_Wheel_Config.fng` | Retail executable string table |

Its UI property names include `PCCOThrottle`, `PCCOSteerLeft`,
`PCCOSteerRight`, `PCCOEBrake`, `PCCOShiftUp`, `PCCOShiftDown`, `PCCONOS`,
and `PCCOCamera`. Keyboard, joypad, and wheel variants each have `Primary` and
`Secondary` binding properties. This proves the game supports separately
configured throttle, left/right steering, e-brake, shifting, and nitrous
actions for those device classes. These strings do not contain the configured
defaults or tell us whether analog axes are normalized or filtered.

The `.fng` files are named by the executable but were not present as loose files
in the extracted game data. I also decoded the installed `FRONTEND/FrontB.lzc`
JDLZ stream read-only (compressed SHA-256
`813e8f966ca9af9a5374454193b3a1e57460ff9374a8c3cc7822bfbcd017418e`, decoded
size 5,969,360 bytes). It contains frontend `.fng` resources, but no exact
`PC_Keyboard_Config.fng`, `PC_JoyPad_Config.fng`, `PC_Wheel_Config.fng`, or
`PCCOSteer*` property strings. A byte search of the extracted game tree plus
this decoded bundle finds those configuration/property strings only in
`SPEED2.EXE`; `Support/config.xml` contains EA account/registration metadata,
not input values. The earlier read-only XP profile inventory likewise found
no `.fng`, `PC_*`, or control/profile files. Therefore the executable proves
the setting names and callbacks exist, but the installed static resources do
not supply their default bindings or profile values.

The UI property names alone cannot recover binding values. Those values are
likely supplied by generated/profile data at runtime, but that storage/source
is not present in the extracted installation and has not been proven from the
executable.

### Binding property callbacks

Generated property callbacks in the executable reference each `PCCO*` name
directly and pass it to the shared helper at `0x006d7120` with a property
object pointer. The callback sequence at `0x00774c10..0x00774e1a` covers the
camera, nitrous, e-brake, shift-down, shift-up, throttle, steering-right, and
steering-left label/primary/secondary properties. This confirms that the
binding names participate in the runtime property system rather than existing
only as unused UI strings. The stubs contain property keys, not default key
codes or device values, so the configured bindings still need to be recovered
from the `.fng` resources or profile data.

### JoyEvent symbol and identifier table

The executable's `.data` table at `0x007fafa0` stores pairs of a 32-bit value
and a pointer to its `JOY_EVENT_*` name. The driving action family appears as:

| ID | Event name |
|---:|---|
| `0x8f` | `JOY_EVENT_STEER` |
| `0x90` | `JOY_EVENT_STEER_ANALOG` |
| `0x91` | `JOY_EVENT_THROTTLE` |
| `0x92` | `JOY_EVENT_THROTTLE_ANALOG` |
| `0x93` | `JOY_EVENT_THROTTLE_ANALOG_ALTERNATE` |
| `0x94` | `JOY_EVENT_BRAKE` |
| `0x95` | `JOY_EVENT_BRAKE_ANALOG` |
| `0x96` | `JOY_EVENT_BRAKE_ANALOG_ALTERNATE` |
| `0x97` | `JOY_EVENT_REVERSE` |
| `0x98` | `JOY_EVENT_REVERSE_ANALOG` |
| `0x99` | `JOY_EVENT_EBRAKE` |
| `0x9a` | `JOY_EVENT_EBRAKE_ANALOG` |
| `0x9b` | `JOY_EVENT_SHIFTUP` |
| `0x9c` | `JOY_EVENT_SHIFTDOWN` |
| `0x9d` | `JOY_EVENT_SHIFTUP_ALTERNATE` |
| `0x9e` | `JOY_EVENT_SHIFTDOWN_ALTERNATE` |
| `0x9f` | `JOY_EVENT_DISENGAGE_CLUTCH` |
| `0xa0` | `JOY_EVENT_DISENGAGE_CLUTCH_ANALOG` |
| `0xa1` | `JOY_EVENT_RESET_CAR` |
| `0xa2` | `JOY_EVENT_HONK_HORN` |
| `0xa3` | `JOY_EVENT_NITRO_BOOST` |
| `0xa4` | `JOY_EVENT_NITRO_BOOST_ALTERNATE` |

These pairs provide a static symbol-to-number mapping, including distinct
digital, analog, and alternate event names. A direct code reference to this
table base has not been found, so it is not yet proof that runtime dispatch
uses these numbers. The table also does not prove that each `PCCO*`
configuration property resolves to a particular entry, reveal the default key
or axis assigned to it, or show where a dispatched event becomes a vehicle
command.

### DirectInput device discovery

The executable imports `DirectInput8Create` from `dinput8.dll` through IAT slot
`0x00783024`. Initialization routine `0x005d0070` creates the DirectInput 8
interface, then invokes its vtable slot `+0x10` to enumerate attached device
class `4` (game controllers), with callback `0x005cef00` and flags `1`.
`0x005cfa10` repeats that enumeration periodically; it increments a counter
and re-enumerates when the prior count is divisible by 32.

The enumeration callback checks the product GUID at device-instance `+0x14`
against two hard-coded IDs. If neither matches, it compares the product name
at `+0x12c` against `Nostromo n50 Speedpad` and `Nostromo n40 Gamepad`. Those
two devices are accepted directly; other devices continue through helper
`0x005cd1f0` for registration/filtering. This establishes real joystick/game
controller discovery and special handling for those Nostromo models. It does
not establish how the discovered axes or buttons map to the named game actions.

Registration provides more detail about the device layer. It compares each new
device GUID with the two existing slots at `0x0086ffb4` and keeps at most two
controller devices. For a new device it creates the DirectInput device,
installs data format pointer `0x007bfde0`, sets a cooperative level using the
window handle at `0x00870990` and flag `5`, then enumerates its objects with
callback `0x005c8660`. That callback checks object flags and, on one path,
sets a numeric range with endpoints `-10000` and `10000`. A separate helper
`0x005bfb60` classifies three known object GUIDs through a small table and
otherwise returns the object's type byte. Registration uses the resulting
codes to set per-slot flags; their semantic names are not established.

### DirectInput state format

The format object at `0x007bfde0` is a 24-byte `DIDATAFORMAT`: `dwObjSize` is
16, `dwDataSize` is `0x110` (272), `dwNumObjs` is 164, and its object-format
array begins at `0x006d66c0`. The first six descriptors write 32-bit values at
offsets `0, 4, 8, 12, 16, 20`, followed by two at offsets `24, 28`; this is
consistent with DirectInput's joystick extended-state layout. The executable
calls `GetDeviceState` with this same `0x110` size. Together, the size and 164
object descriptors identify the state as `DIJOYSTATE2`, whose layout is six
axes, two sliders, four POV values, 128 button bytes, and velocity,
acceleration, and force values for those axes/sliders. The structure and
member offsets are documented by [Microsoft's DIJOYSTATE2 reference](https://learn.microsoft.com/en-us/previous-versions/windows/desktop/ee416628%28v%3Dvs.85%29).

The descriptor array confirms the full layout directly: entries `0..7` are
the six position axes and two position sliders at offsets `0..28`; `8..11`
are the four POV values at `32..44`; `12..139` are 128 button bytes at
`48..175`; `140..147` are six velocity axes and two velocity sliders at
`176..204`; `148..155` are the acceleration values at `208..236`; and
`156..163` are the force values at `240..268`. All multibyte fields are four
bytes; buttons are one byte each. The same slider offsets `24/28` recur in the
velocity, acceleration, and force groups, matching DirectInput's documented
structure. This verifies both the state type and the location of each raw
control field, independent of the official structure documentation.

The per-frame updater `0x005cfa10` polls the two device slots through
`0x005c8700`; each successful read obtains `0x110` bytes. It copies the two
current device-state blocks (`0x220` bytes total) into its caller's snapshot
area. Device registration and state collection are therefore concrete, and
the raw value layout is no longer unknown. The configured indices that feed
`JOY_EVENT_STEER`, `JOY_EVENT_THROTTLE`, and the other driving actions have not
been recovered from the binding records or config/profile data.

The runtime update (`0x005cfa10`) refreshes device enumeration every 32 update
passes, then services each populated slot through `0x005c8700`. That routine
invokes the standard IDirectInputDevice8 vtable slots `+0x64` (`Poll`),
`+0x1c` (`Acquire`), `+0x58` (`SendForceFeedbackCommand`), and `+0x24`
(`GetDeviceState`) on different branches. It also copies a per-slot
`0x110`-byte block into the caller's update object and clears associated arrays
when a later call returns `0x8007001e`. This establishes the device polling,
acquisition, force-feedback, and state-query lifecycle. The call site still
does not show how returned control values map to steering, throttle, or brake;
that needs tracing the consumers of the copied state block and the control
binding data.

### Binding-record sampling

The executable's static data contains nine records with handler pointer
`0x005795c0` at record offset `+0x1c`. This handler consumes a binding record
and supplied input-state arrays. The record's bytes `+0x12..+0x15` select a
bit/index, an optional second bit, an axis-side mode, and a source slot; the
exact schema names are not established. It uses a per-source record at a
`0x2c` stride and runs a sample through `0x00576790`, a clamped piecewise
linear curve sampler. The float result is rounded to an integer by
`0x0075bed4` before the axis-side conversion.

For axis-side mode `1`, values below `127` become `2*(127-value)` and all
others become `0`; mode `2` maps values above `128` to `2*(value-128)` and
otherwise returns `0`. A result of `254` is represented as `255`. Thus the
handler can turn the two halves of a centered axis into separate one-sided
byte channels with a neutral value of zero. This is compatible with the
driver callbacks' byte inputs, but the record table has not yet been connected
to a specific `JOY_EVENT_*` by an executed dispatch call, and the actual
keyboard/gamepad/wheel defaults are still unknown. Do not treat the formulas
as proof of the default steering axis or polarity.

## Vehicle steering evidence

### `CarController` reflected interface

The serializer at `0x00404820` registers the executable name `CarController`
and delegates to the serializer of the object at `this+0x08`. A vtable whose
serializer slot `+0x08` points to `0x00404820` is present at `0x007843f0`.
Its entries at `+0x38`, `+0x3c`, and `+0x40` point to
`0x00404890`, `0x004048d0`, and `0x004048f0`, respectively. This ties the
following methods to the reflected controller class rather than grouping them
only by nearby addresses.

Constructor `0x0040f630` installs that vtable, stores its actor argument at
`this+0x04`, clears `this+0x0c`, and initializes a 16-dword flag area beginning
at `this+0x14`. The class is a shared base rather than a player-only type:
`AICarController` constructor `0x00420500` calls `0x0040f630` with the same
actor before installing its derived vtable `0x007852b0`, and its serializer at
`0x00428260` delegates to `0x00404820`. This gives a confirmed inheritance
edge into the AI vehicle controller path. It prevents using `CarController`'s
name alone as proof of a player-input implementation.
The route controller reaches this base too: `RealController` constructor
`0x004266d0` calls `0x00418470`, which calls `0x0040f630` before installing
the `RealController` vtable `0x00785230`. Its serializer `0x00418580` also
delegates to `0x00404820`. Both identified subclasses are therefore on AI or
route-control paths, while the player-input subclass/caller remains unknown.

- `0x00404890` reads the attached actor from controller `+0x04` and branches
  on actor dword `+0x28`. Values `1` and `3` return immediately. Other values
  branch on actor dword `+0x18` and the method's second argument, then call
  `0x005f46a0(actor, 3)` or `0x005f46a0(actor, 1)`. The helper's semantic
  effect is not assigned here.
- `0x004048d0` accepts a dword and, when an actor is attached, writes it to
  `actor+0x468`.
- `0x004048f0` writes float `1.0` to `actor+0x468` when an actor is attached.

These methods establish an actor-facing `CarController` interface and a
specific vehicle state field it can update. They do not yet connect a
`PCCOSteer*` or `JOY_EVENT_STEER*` binding to these methods; `@PlayerSteering`
still has no confirmed code reference. Although `actor+0x468` now has traced
writers and consumers, its gameplay label and the helper's numeric modes
remain unknown. Physics routine `0x004038d0` reads `actor+0x468`, divides an input by
it, and uses the quotient as a lookup input to a response table associated
with the object pointed to by `actor+0x10`. A separate update path at
`0x00417678..0x0041773e` computes and clamps a float before storing it to the
same actor field. The exact downstream use is clearer from the helper trace:
the controller accessor at `0x005b5380` returns a lookup table at
`actor+0x20+0x10+0x55c`; `0x005764d0` samples that table using the divided
input. It clamps below/above the table range to the first/last value and
linearly interpolates between adjacent values. The dynamic update path floors
its stored scale at `1.0`; the base `CarController` setter can also write a
supplied value directly, while its default setter writes `1.0`.

```c
float sample_vehicle_response(Actor *actor, float input) {
    float scale = actor->response_scale_468;
    void *component = *(void **)((byte *)actor + 0x10);
    void *curve_owner = (byte *)component + 0x10;
    Curve *curve = ensure_curve_table(curve_owner); // accessor returns owner + 0x55c
    return sample_clamped_linear_curve(curve, input / scale);
}
```

This establishes an input-scale and response-curve stage in the vehicle path.
The input's gameplay label and the curve's tuning data are not mapped yet, so
it is not identified as steering angle, steering torque, or throttle response.

The executable contains the names `CarController`, `RealController`, and
`@PlayerSteering`, plus the action label `Steer player in drag race`. The
`RealController` vtable links that class to route following. The drag-race
label belongs to a route action whose callback filters route candidates; it
does not identify the raw player input handler. The `@PlayerSteering` name is
now connected to a concrete `RealDriver` member. Serializer `0x00407034`
passes the object at `RealDriver+0x220` to serializer `0x00405d50`, which
registers the type name `@PlayerSteering`. The ownership is independently
confirmed by constructor `0x00425fc0`, which installs vtable `0x00784fc8`
and initializes `+0x220` with constructor `0x00405c60`; that vtable's
serializer slot points to `0x00406df0`, which registers the class name
`RealDriver`. The nearby `@Before ThePlayerSteering` string still does not
prove inheritance or input-callback behavior: it is passed with `RealDriver`
to the no-op helper `0x00402350`.

### `@PlayerSteering` object registration

The `RealDriver` serializer at `0x00407034` establishes the `+0x220` member
relationship. Nearby code at `0x0040701c..0x00407028` passes the strings
`RealDriver` and `@Before ThePlayerSteering` to `0x00402350`. That helper is a
bare `ret 8` in this build, so the strings are present at the call site but
this call itself establishes no inheritance behavior or live input callback.

Serializer `0x00405d50` registers these `@PlayerSteering` fields using the
following helpers:

| Offset | Observed registration | Constructor initialization |
|---:|---|---|
| `+0x10` | `0x00571640` with `(1, 1, 0)` | byte zero |
| `+0x14` | nested serialization through `0x0057cbe0` | object initialized using global settings at `0x783ad4/0x783ad8` |
| `+0x48` | nested serialization through `0x0057cbe0` | object initialized using global settings at `0x783ad4/0x783adc` |
| `+0x7c` | `0x00571710` | 16-bit zero |
| `+0x80`, `+0x84` | `0x00571770` | `45.0`, `0` |

Constructor `0x00405c60` stores its incoming owner argument at `+0x00`,
initializes `+0x04` to zero and `+0x08/+0x0c` to `1.0/7.0`. The fields at
`+0x80/+0x84` are registered through the helper used for four-byte values;
their runtime units and purpose are not identified.
The nested objects' type names and the meaning of the `+0x10` registration
arguments remain unresolved.

This is useful structural evidence that the `@PlayerSteering` structure is
embedded in `RealDriver` and has two nested control/configuration objects
plus scalar state. The later runtime trace connects `JOY_EVENT_STEER` and
`JOY_EVENT_STEER_ANALOG` values to this object's update, but the configured
`PC_*_Config.fng` binding and raw DirectInput sample feeding those events are
not decoded, and the steering response equation is only partially recovered.
Do not reuse this object as the AI `APsychoDriver` or `RealController`
controller merely because their code also updates a car.

### `@PlayerSteering` runtime calculation path

The runtime link is direct. `RealDriver` update routine `0x0041e2c0` calls
wrapper `0x0041dfc0` with `this = RealDriver+0x220`; the constructor and
serializer identify that address as the `@PlayerSteering` object. It passes
three values derived from the RealDriver update (`+0x2ac`, its timestep, and
either zero or `+0x2c8`). The wrapper reads/writes the registered scalar
fields `+0x14`, `+0x7c`, and `+0x84`. Its calculation selection is:

- if global `0x81cb8c` is nonzero, call `0x00406a40`;
- otherwise, if helper `0x005c1000` returns true, call `0x004061c0`;
- otherwise, call `0x00416bb0` (the ordinary path).

`RealDriver` constructor `0x00425fc0` initializes `+0x2ac` to `-1`; setter
`0x00410420` later stores a supplied integer there, and getter `0x00410460`
returns it. Both are in the `RealDriver` vtable (`+0x10` setter, `+0x0c`
getter). The update first skips its driving/control path while the value
remains `-1`, so this index is explicitly assigned during setup; the virtual
setter's caller has not yet been identified.

The `0x005c1000` condition is specifically a controller-device check. Its
index argument is `RealDriver+0x2ac`; the helper loads that slot's device
pointer from `0x0086ffb4 + 4*index` and requires the corresponding active
byte at `0x0086e5bc + index` to be nonzero. So `+0x2ac` functions as the
controller slot index, and the steering response switches between
`0x004061c0` and `0x00416bb0` based on whether that slot has an active
enumerated device. This is a proven device-presence/active-state branch, but
it still does not show which axis or button value feeds either calculation.

The `+0x7c` word is a packed full-turn angular state: helpers multiply it by
`360/65536` degrees per unit (`0.0054931640625`) and wrap values at 180 degrees
by subtracting 360. The update code converts radians back through the matching
57.29578 degrees-per-radian constant before quantizing to this word. This
establishes the numeric format and wrap behavior, although the code path alone
does not prove whether it represents wheel angle, desired yaw, or another
steering-related angle.

In the ordinary calculation `0x00405dc0` receives the embedded steering object
as `this`. It follows the owner pointer at `this+0`, then reads an input object
pointer at `owner+0x30`; that pointed-to object supplies floats at `+0x20c`,
`+0x210`, and `+0x214`. The routine uses them in its curve and response
calculation, along with a value at `owner+0x42c`, and stores its
result at steering state `+0x80`. The alternate calculation `0x004061c0` also
reads the same three slots through the owner's `+0x30`. Constructor
`0x00425fc0` passes its owner argument to the steering constructor and stores
that same argument at `RealDriver+0x2a8`. The lifecycle trace in the
longitudinal handoff section shows the PhysicsMover receives this same owner's
`+0x30` pointer as its command source. Therefore the player steering path and
PhysicsMover read the same three command slots from the same object.

### Driving events into the steering request

`RealDriver` registration routine `0x0041e060` subscribes its controller slot
(`+0x2ac`) to `JOY_EVENT_STEER` (`0x8f`) and
`JOY_EVENT_STEER_ANALOG` (`0x90`) with callback `0x00407750`. It registers
throttle (`0x91..0x93`), brake (`0x94..0x96`), reverse (`0x97..0x98`), and
e-brake (`0x99..0x9a`) events through separate callbacks. Thus the event IDs
are connected to live per-RealDriver subscriptions, not just names in a static
table.

The steering callback stores the incoming byte in `RealDriver+0x2c4` for
event `0x8f`, or `+0x2c5` for `0x90`, while resetting the other slot to
`0x80`. It selects the non-`0x80` byte, converts it as
`(value - 128) / 255`, and stores the resulting float at `RealDriver+0x2c8`;
`0xff` is treated specially and stores `-1.0`. In the update routine
`0x0041e2c0`, that `+0x2c8` value (or zero when the `+0x2fb` gate is set) is
passed into the `@PlayerSteering` wrapper `0x0041dfc0`. This closes the
input-event-to-steering-request edge in the player control path. The mapping
from a configured keyboard/pad/wheel binding to event value bytes is still
unknown, as are the response curves after the request reaches
`@PlayerSteering`.

The adjacent callbacks expose the start of the throttle/brake channels too.
Throttle callback `0x00416de0` handles IDs `0x91` (digital), `0x92` (analog),
and `0x93` (alternate analog). It keeps three byte samples at `+0x2d0..+0x2d2`,
selects the digital sample when nonzero or otherwise the larger analog sample,
and writes the selected value scaled by `1/255` to `+0x2d4`. A transmission
mode check can run an additional curve helper before that write. Brake callback
`0x00407eb0` handles `0x94..0x96`, stores three byte samples at `+0x2d8..+0x2da`,
uses the digital sample when nonzero or otherwise the larger analog sample,
and scales the result by `1/255` into `+0x2dc`. Both callbacks force their
output to zero while `+0x2fb` is set. The following RealDriver update uses
these accumulated channels in the vehicle-control path; the lower-level
mapping from the normalized values to engine and brake forces still needs to
be traced.

Reverse is a distinct registered input path, not just the brake callback's
negative-speed branch. `JOY_EVENT_REVERSE` (`0x97`) and
`JOY_EVENT_REVERSE_ANALOG` (`0x98`) both call `0x004077d0`. The digital sample
is stored at `RealDriver+0x2e0`; the analog sample is stored at `+0x2e1` (with
an additional device/mode-dependent conversion on that path). The callback
selects the nonzero digital sample, or otherwise the analog sample, and scales
the selected byte by `1/255` into `+0x2e4`. The runtime vehicle response method
at `0x005b53a0` reads a float at its own `+0x2e4` alongside other control-like
fields. This establishes a consumer with a matching offset, but not yet that
the method is called on the live `RealDriver` object. Its direct callers
resolve resource/property records and write the returned scalar through
`0x004ff550`; no direct call from the `RealDriver` tick `0x0041e2c0` appears in
the disassembly. The method's meaning and its exact runtime binding therefore
remain open. The browser currently maps S directly to brake and permits its
fixed deceleration to carry speed negative; that behavior cannot be equated to
the retail reverse path without the live player-object link and a runtime
brake-only/reverse-only trace.

### Reverse/brake response helper follow-up

The helper at `0x005b53a0` returns a float. It calls `0x0059a350` on the same
object, combines that result with values read at `+0x2d0`, `+0x2dc`, and `+0x2e4`,
then clamps the result between globals `0x00788150` (`0.03`) and
`0x00784250` (`1.0`). Its visible arithmetic is:

```text
curve_part = (sample_59a350 - g_86b3d8) / (g_86b3dc - g_86b3d8) * 0.82
blend_part = (float_at_2dc * float_at_2d0 - g_86b3f0)
             / (g_86b3f4 - g_86b3f0) * 0.10
reverse_part = int_convert(float_at_2e4 + 0.5) * (1/3) * 0.08
result = clamp(curve_part + blend_part + reverse_part, 0.03, 1.0)
```

`0x0075bed4` performs the integer conversion used by `reverse_part`. The four
`g_86b3*` operands lie in the executable's zero-filled virtual data region;
their runtime values are not stored in the file and cannot be recovered from
the static image alone. The helper reads all three object fields as 32-bit
floats. This is a warning against assuming its receiver is the `RealDriver`:
`RealDriver+0x2d0..+0x2d2` are three raw sample bytes, whereas this helper reads
a float starting at `+0x2d0`. The direct caller at `0x00496160`, for example, gets a
record through `0x00511e00`, passes its nested `+0x10` object to this helper,
and sends the result to a response-parameter setter. Other direct callers are
`0x0049631d`, `0x004a2380`, `0x004c1c4c`, `0x005103d8`, `0x0054ffb0`,
`0x0054ffc0`, `0x0055006e`, and `0x0055007e`. These references do not show a
call from the player tick or identify a writer that copies live
`RealDriver+0x2e4` into the helper's object. The same offsets alone are not
enough to identify the object as `RealDriver`.

The next useful capture is therefore a single free-roam trace that records the
`RealDriver` address, its reverse/brake/throttle fields, and the `this` pointer
at `0x005b53a0`; if the helper is hit, record its return and the receiver of
the following `0x004ff550` call. Compare a brake-only input with reverse-only
input at rest and while moving. The VM activation dialog currently prevents
that capture. Until the object identity and call path are proven, this helper
does not justify changing browser braking or reverse behavior.

### Longitudinal command handoff

RealDriver tick routine `0x0041e2c0` invokes `0x00407a30` before the steering
wrapper. That routine combines the normalized event values with timestep,
vehicle speed, and state-dependent gates, then uses three setters as command
slots: `0x005eb910` writes `RealDriver+0x20c`, `0x005eb900` writes `+0x210`,
and `0x005eb8f0` writes `+0x214`. Their call dataflow ties `+0x210` to the
e-brake accumulator `+0x2ec`, `+0x214` to brake `+0x2dc`, and the `+0x20c`
output to the throttle path based on `+0x2d4`. The routine then copies
`RealDriver+0x20c` to the object pointer stored at `RealDriver+0x2a8`, offset
`+0x438`.

The pointer's role is clearer than its concrete class. Constructor
`0x00425fc0` stores its incoming argument at `RealDriver+0x2a8`. The
`RealController` constructor `0x004266d0` creates the embedded driver at
`RealController+8`, passing its own incoming argument to that constructor;
the base constructor also stores the argument at `RealController+4`. In the
controller setup path at `0x005eba30`, mode 1 creates this `RealController`
with the host/controller object as that argument. Immediately after creating
the controller, the setup writes `[controller+8]` to `owner+0x30`. So in this
mode, `owner+0x30` points directly at the embedded `RealDriver`, not a
separate command block.

Lifecycle routine `0x005f6d50` can create this mode-1 controller on an owner
and then create that owner's selected component through `0x005f46a0`; component
mode 1 is a `PhysicsMover`. The setup also stores the same embedded-driver
pointer in the selected component wrapper at `+0x04`, and the mover constructor
takes that pointer as its command source. The `RealDriver`, its embedded
steering object, and `PhysicsMover` therefore share one live object: the driver
stores the host at `+0x2a8`, steering stores it at `+0x00`, while both
steering and physics follow host `+0x30` to the driver itself.

This closes the per-frame command handoff. The longitudinal update writes
throttle, e-brake, and brake to `RealDriver+0x20c/+0x210/+0x214`; those are
exactly the three fields read through `owner+0x30` by steering and physics,
because that pointer aliases the embedded driver. Its additional throttle
copy to `owner+0x438` is a separate host field and is not needed for the
steering/mover command path. The fields' exact actuator units remain unknown,
so do not label them torque or force without further evidence.

### Longitudinal update branch trace (`0x00407a30`)

The assembly gives more detail about how the normalized event channels become
the three `RealDriver` outputs. The function accepts a timestep, reads the host
through `RealDriver+0x2a8`, and starts with the host value at `+0x42c`. If the
global mode byte at `0x89e7e2` is set, or either mode word at `0x890100` and
`0x890110` is nonzero, it calculates
`speedMetric = host[+0x42c] / (1 + 4 * abs(host[+0x118]))`. The exact
interpretation of this transformed value is unknown.

The first phase decrements the driver's `+0x2bc` accumulator by the timestep.
If the decremented timer is strictly positive and `abs(speedMetric) > 8.94`,
it sets the local throttle candidate to zero and passes e-brake `1.0` to
`0x005eb900` (`RealDriver+0x210`). Otherwise it resets `+0x2bc` to zero,
retains the current throttle sample (`+0x2d4`), and passes the e-brake sample
(`+0x2ec`). Thus the override is the positive-timer/high-speed branch; it is
not the fallback when either comparison fails.

Next, the routine updates state byte `+0x2f0`. It sets the byte when the
normalized brake input at `+0x2dc` is strictly above `0.6` and throttle at
`+0x2d4` is at least `0.5`. If that condition fails, it clears the byte only
when `speedMetric > -2.0`; at or below `-2.0`, the previous byte is retained.
This state then selects how the local throttle/brake values are routed. Let
`throttleCandidate` be zero in the timer override branch above and the input
sample otherwise. The branch-local values passed onward are:

```text
if state_2f0 == 0 or speedMetric == 0.5:
    first = throttleCandidate
    second = brake
    auxiliary = 0
else:
    auxiliary = min(2 * brake, 1.0)
    if speedMetric < -30.0:
        first = throttleCandidate
        second = 0
    else:
        first = 0
        second = throttleCandidate
```

`second` is passed through `0x005eb8f0` into `RealDriver+0x214`, while
`first` continues into the `+0x20c` shaping path. The state byte's gameplay
label is not established.

The `+0x20c` path starts from the first local value above. Under its mode gate,
it passes the candidate through curve helper `0x00577310` using curve data at
`RealDriver+0x2cc`, then samples the returned curve with `0x005764d0`. The
candidate is rate-limited relative to the previous `+0x20c`: the ordinary
bounds are `previous - 1.5*dt` and `previous + 1.0*dt`, from globals
`0x783b3c` and `0x783b38`. Some mode/state branches use fixed bounds or skip
the curve path. The final candidate is written by `0x005eb910`; a later global
settings gate can overwrite `+0x20c` with `0` and `+0x214` with `0.5`. Finally,
the routine copies the resulting `+0x20c` to `host+0x438`.

The compared constants are decoded from `.rdata`:

| Address | Value | Use in this callback |
|---:|---:|---|
| `0x783b34` | `4.0` | speed-metric transform multiplier |
| `0x784250` | `1.0` | transform offset and unit ceiling |
| `0x784568` | `8.94` | absolute speed-metric threshold in the timer gate |
| `0x784564` | `0.6` | brake threshold for state `+0x2f0` |
| `0x784260` | `0.5` | throttle threshold and speed-metric comparison |
| `0x783b30` | `-2.0` | state-clear threshold |
| `0x783b2c` | `-30.0` | state-dependent local-pair branch |
| `0x783b38` | `1.0` | positive output slew bound per `dt` |
| `0x783b3c` | `1.5` | negative output slew bound per `dt` |

These values make the input thresholds and rate bounds directly reproducible;
the host fields, auxiliary state, and actuator units remain unnamed.

A separate component also writes a float at `+0x438`, but its owner has not
been linked to the controller host. Its update entry `0x005b72d0` is in the
vtable at `0x007a25e0`; constructor `0x005b5cc0` stores the string `PhysMov`
at component `+0x0c` and an owner-derived pointer at `+0x58` (constructor
arg4+`0x40`). Factory `0x005f46a0` allocates this `0x130`-byte component in
one dispatch case, passes its own `this` pointer as constructor arg4, and
stores the returned component at factory-owner `+0x34`. One caller,
`0x004020d0`, invokes the factory with `ECX=[caller+4]`; neither that caller
relationship nor the factory's `+0x40` derivation proves pointer identity
with `RealDriver+0x2a8`. The update calls
`0x005b6300` on that component. In one state branch, `0x005b6300` adds the
global float at `0x865154` to owner `+0x438`, compares the accumulated value
with a global threshold, and resets it to zero in other branches. The same
branch reads four associated objects through component
`+0x38/+0x3c/+0x40/+0x44` and tests each object's float at `+0x118` before
accumulating. This looks like a timer/state path, not an actuator read. Treat
`+0x438` as class-scoped rather than inferring meaning from the offset alone.

Another direct read of numeric offset `+0x438` occurs in a different object:
the routine at `0x005a6d00` reads float fields from subobject offsets `+0x420`
through `+0x44c` (definition-record offsets `+0x530` through `+0x55c`)
of a definition record, including `+0x438`, multiplies them by a scalar, and
accumulates them into a separate output block. Its caller `0x005ac4f0`
receives a record subobject at `+0x110` from `0x005b4dc0`. That caller uses an
index at its own `+0` to select a `0x890`-byte record from the global table at
`0x008a1ccc`. The loader at `0x00636be0` sets this table base while loading
chunk `0x34600`, which is the 46-record traffic vehicle definition table
exported from `GLOBALB.BUN`. Therefore this read is of a definition field,
not a read of the actor field written through `RealDriver+0x2a8`. See
[`traffic-vehicle-definitions.md`](traffic-vehicle-definitions.md) for the
recovered data and the unknown field band. The direct actuator consumer of
the RealDriver host's `+0x438` remains unidentified.

An executable-wide search for the numeric offset also finds unrelated
objects. Constructor `0x0044bea0` installs vtable `0x00787de8` and stores a
pointer at its own `+0x438`; accessor `0x0044c240` returns that pointer, and
other routines dereference it. This is a different class and field type from
the float written through `RealDriver+0x2a8` at `0x00407e93`. Treat
offset-only xrefs as candidates; they do not establish that two accesses
refer to the same runtime object or value.

The result is passed through `0x00406460`, and the wrapper stores the result
at `+0x84` and returns the updated `+0x7c` word. `0x00416bb0` takes the same
object layout, reads its owner pointer at `+0`, and calls `0x00405dc0`
followed by `0x004060e0`. Constructor `0x00405c60` stores its incoming owner
argument in this slot. `RealDriver` constructor `0x00425fc0` passes the same
owner argument to the embedded steering object and stores it at
`RealDriver+0x2a8`.

`0x00405dc0` is the steering calculation core. It reads vehicle speed through
the owner pointer (`owner+0x42c`), follows the pointer at `owner+0x30` and
reads that object's fields `+0x20c/+0x210/+0x214`, and samples vehicle state.
It updates steering state
including `this+0x10`, `this+0x80`, and `this+0x7c`; `0x004060e0` also updates
`this+0x80/+0x84`. The call sites use timestep-like and scalar arguments, and
the code has speed/state-dependent thresholds and clamps. These operations
are enough to establish a real steering response path, but the three owner
values and the threshold/window constants are not yet semantically decoded.
The event-value input is now linked; the Windows key, pad axis, or wheel
binding and DirectInput sample that supply those event values are not yet
identified.

### Drag-race steering action (`0x0040cab0`)

Constructor `0x0040cab0` creates action kind `3` and installs vtable
`0x00784830`. Its display-name method at `0x0040cad0` returns
`Steer player in drag race`. The vtable callback at `0x0040cae0` calls
`0x005dcbe0(candidate, 2)`.

The helper reads the dword at candidate `+0x0c`, masks its low nibble, and
returns true only when that nibble is `2` or `8`. It does not read keyboard,
gamepad, wheel, throttle, or steering-axis state. This callback therefore
classifies a route candidate for the drag-race action. It is an action/route
selection step, not evidence of the player's actual steering implementation.

### `RealController` object trace

The reflection serializer at `0x00418580` registers the exact class name
`RealController`. It serializes a set of fields from a controller object, then
delegates inherited state to `0x00404820`. The observed field addresses are
object-relative:

| Offset | Serialization operation | What is established |
|---:|---|---|
| `+0x54`, `+0x58`, `+0x5c` | four-byte field helper `0x00571770` | three serialized 32-bit slots; semantic types unknown |
| `+0x3d8`, `+0x3dc` | four-byte field helper `0x00571770` | serialized 32-bit fields; the update path uses both as floats |
| `+0x3e0` | helper `0x005d9530` with manager `0x0088a320` | manager-backed route index/cache; `0xffffffff` is an absent sentinel in readers |

The class vtable at `0x00785230` points to this serializer at slot `+0x08` and
to update routine `0x0042caa0` at slot `+0x0c`. Constructor `0x004266d0`
installs that vtable and initializes the same extended object layout. The
update routine processes route state, calls the route helpers at
`0x0041d0e0`/`0x00418620`, resets and refills lookahead data, and uses actor
fields including speed-related `+0x42c` before handing off to another control
routine. This is evidence that `RealController` is part of route following;
it does not establish it as the player's raw input handler. See the
[`traffic-lane-action.md`](traffic-lane-action.md) trace for its update sketch.

The path from steering/throttle/brake event IDs into the embedded `RealDriver`
is partially recovered, and its command slots are now linked to the
`PhysicsMover` in controller mode 1. The configured device binding and the
downstream translation from mover commands into drivetrain and wheel forces
remain open. `RealController` is the containing controller class; the AI
route-following controller is a separate path and should not be conflated with
the player's event-driven `RealDriver`.

Separate AI driver analysis in [traffic-lane-action.md](traffic-lane-action.md)
establishes a neighboring, non-player control path: the AI route follower
builds a forward 3D lookahead queue, chooses a target, computes heading error,
and updates a rate-limited heading. Helper `0x00405990` limits one heading
update to `360 degrees × dt`; helper `0x00405940` wraps a radian difference.
Those findings describe the AI path and must not be assumed to be the player's
steering response.

## Remaining reverse-engineering work

- Find the packed/default values for `PCCO*Primary` and `PCCO*Secondary` in the
  PC configuration resources or user profile data.
- Recover the actuator units and follow the shared driver command values from
  the `PhysicsMover` gate into drivetrain and wheel forces.
- Recover how keyboard, gamepad, and wheel bindings produce the event bytes,
  including dead zones and device-specific filtering.
- Trace the player car's steering angle and tire/vehicle dynamics separately
  from the AI heading controller.

Until those steps are complete, a WebGL recreation can use the named action
set as a sensible control surface, but its response curves would be an
implementation choice rather than a recovered game mechanic.

### Installer ISO control-data check (2026-10-16)

The supplied ISO contains `Setup.exe`, `data2.cab`, `data3.cab`, `data4.cab`,
and DirectX files. The setup executable's strings explicitly include
“This setup is password protected.” The three `data*.cab` files extracted
from the ISO are opaque to the available archive reader and do not expose
plain filenames or control strings when searched as raw bytes. They were not
decompressed in this check, so the raw scan does **not** establish whether
the CAB members contain PC binding resources. The decoded installed-resource
search and the XP profile inventory above remain the evidence for absent
default binding files. No keyboard mapping or handling constant was recovered
from the installer image.

### WebGL driving reconstruction boundary

The viewer uses the recovered `0.0044`-second maximum substep, the ordinary
RealDriver throttle slew limits (`+1.0/s`, `-1.5/s`), and a 16-bit full-turn
angle grid (`2π/65536`) for its steering angle and vehicle heading. The angle
grid follows the packed steering word and packed physics heading observed in
the executable. The viewer's digital-key-to-angle mapping, `0.55`-radian
steering ceiling, speed/yaw response, fixed acceleration and braking values,
and handbrake behavior are still approximations: the retail steering curves
and tire/force equations have not been decoded. The 16-bit conversion improves
representation fidelity; it does not claim to reproduce retail steering
response or the tire model.

### Follow-up disassembly: response recovered, motion handoff still open

The matched `SPEED2.EXE` has more usable steering data than the event names
alone. Ordinary `RealDriver` steering selects a signed input profile in
`0x00416bb0` (mode 1 or 4), then `0x00405dc0` combines the profile output with
speed, the three driver command slots, a speed-dependent table, and a
steering-state table. `0x004060e0` applies a steering-change-rate table and
stateful smoothing. `0x0041dfc0` combines those results with a runtime scalar
from the owner's nested `+0x270` field, applies a ±45° bound to an intermediate,
and writes the packed result at `RealDriver+0x1e0`. The relevant static knots
and branch conditions are tabulated in
[`webgl-driving-gap.md`](webgl-driving-gap.md).

These constants do not give a safe replacement for the viewer's heading
equation. The profile endpoints used by the current digital keys are `-1/+1`
in both recovered modes, and the later factors depend on runtime fields and a
stateful object. The generic controller-mode-1 lifecycle does establish a
conditional handoff: actor `+0x30` points to the embedded `RealDriver`, and
the mode-1 `PhysicsMover` stores that pointer at mover `+0x04`. Its finalizer
therefore reads the same `RealDriver+0x1e0` word written by the driver tick and
uses it as the heading target. Static evidence does not establish that the
free-roam player selects this controller/component path. It also does not
link the driver's output to `TwoWheelAckermanSteering+0x90`. No numeric yaw,
tire-force, or acceleration change is justified from the static tables alone;
the browser's direct heading update remains explicitly provisional.

There is a concrete scope trap in the apparent handoff: `0x004220a0` is the
`DragDriver` update and its `RealDriver` at wrapper `+0x470` is nested in an
AI driver path (see [`traffic-lane-action.md`](traffic-lane-action.md)).
Its output at wrapper `+0x650` cannot establish the free-roam player's active
controller or prove that the player's `RealDriver+0x1e0` is the mover's heading
target. The generic controller factory has several modes; without the live
player actor's selected mode and pointer chain, the shared class layout is not
enough to transfer this AI result to the player.

The minimum useful live trace first records the primary free-roam player
actor's controller and component selection at `0x005f6d50`, `0x005eba30`, and
`0x005f46a0`, including the actor's `+0x30` pointer and the objects' vtables.
This determines whether the conditional mode-1 handoff applies to the player
and separates it from the known `DragDriver`/AI wrapper. For the selected
player driver and its mover, log the `0x0041dfc0`/`0x00416bb0` steering path and
`0x005a5540` finalizer together: input bytes/request, selected mode and profile
output, owner speed and
`+0x270` scalar, steering `+0x20/+0x54/+0x7c/+0x84` state and the smoothing
object/state at `+0x48`, results from `0x00405dc0`/`0x004060e0`,
`RealDriver+0x1e0`, and the live pointer path to
mover owner `+0x1e0` and Ackermann `+0x90`. Also record mover-state transform
`+0x20`, vectors `+0x30/+0x50/+0xd0`, accumulators `+0x100..+0x128`, and heading
`+0x3ce` before/after finalization. Repeat a left/right sweep while stationary,
rolling slowly, and at a steady moderate speed. This is enough to identify the
active configuration, smoothing, and command-to-motion handoff without
guessing the meaning of unlabelled physics values; a single screenshot or
final steering angle cannot resolve those stateful paths.

### Ready-to-run XP capture

`tools/capture_nfsu2_player_steering.gdb` is a GDB script for the local QEMU
stub. Launch NFSU2 and enter free roam first, then start GDB from the repository
root. The executable must be loaded so the fixed-address breakpoints can be
installed:

```sh
gdb -q -x tools/capture_nfsu2_player_steering.gdb
```

It connects to `127.0.0.1:1234`, compares mode-refresh owners against the
primary player actor resolved from `0x008900b4` at `0x005f6d50`, and records
controller factory modes at `0x005eba30`, component candidates at
`0x005f46a0`, nonzero steering updates at `0x0041dfc0`, and matching mover
finalizers at `0x005a5540`. The output is written to
`/tmp/nfsu2-player-steering-runtime.log`. After attaching, do a short centered stationary-to-rolling
left/right sweep, repeat at moderate speed, and briefly press throttle and
brake. The script automatically continues through its breakpoints; stop the
capture with Ctrl-C and detach GDB when finished. The steering and mover
records include the pointer and field values listed above, allowing the
driver-to-owner/Ackermann handoff to be checked in the same log. It also logs
normalized throttle, brake, and e-brake inputs, their three command outputs,
the raw `RealDriver+0x2f4` value, and connected `DriveTrain+0x44/+0x14` state
when those inputs change. These remain raw fields; the shift-state and mode
labels are not established by disassembly. It also logs reverse bytes
`RealDriver+0x2e0/+0x2e1`, normalized reverse `+0x2e4`, brake/throttle state
`+0x2f0`, and the candidate transformed host speed metric with the mode gates.

QEMU's GDB stub accepts one debugger session at a time. If another capture
script (currently `tools/capture_nfsu2_traffic.gdb`) is attached, detach that
GDB session before starting this one; do not launch two remote GDB sessions
against port `1234`.

This script does not bypass Windows activation or logon. The current runtime
copy still stops before the desktop, so a user must first reach the desktop
through the normal Windows path. Attaching before launching the game is needed
to observe the factory calls; attaching after entering free roam misses that
initial controller selection. On a fresh 2026-10-15 check, the VM was paused by
the existing debugger; resuming it with QEMU HMP `cont` allowed input again.
Tab/Enter briefly removed the activation dialog to a blank Welcome screen, but
the activation prompt returned after several seconds; Ctrl+Alt+Del returned to
the same prompt. No Windows desktop or gameplay state was reached.
