# Ambient traffic manager reconstruction

This is a readable reconstruction of the ambient traffic population manager in
the installed `SPEED2.EXE`. It combines the candidate refresh, density lookup,
handle creation, and per-frame retirement paths. Names such as `candidate`,
`slot`, and `quota` are descriptive aliases for fields used at the listed
offsets; they are not recovered source names.

## Runtime data

| Structure | Evidence-backed layout |
|---|---|
| Vehicle definition | 46 rows, stride `0x890`; class at `+0x844`, six signed target bytes at `+0x854..+0x859`, variation count at `+0xd9`, variation bytes at `+0x880` |
| Manager definition slot | linked from manager `+0x900`; definition index `+0x10`, selected variation `+0x17`, resource handle `+0x8f8`, age timestamp `+0x900`, matched candidate count `+0x90c`, retire flag `+0x914`, assigned candidate count `+0x910` |
| Candidate record | array rooted at `0x0089e7a0`, count `0x0089e814`, record stride `0x940`; nested definition index `+0x10`, variation key `+0x07`, assigned definition index `+0x908` |
| Spatial density area | type-16 polygon records; traffic mode is read from area field `+0x3c` |

The runtime candidate storage is admitted by `0x00601600` up to 22 entries.
Candidate records are not the live vehicle objects: density refresh assigns
definitions to candidates, then creates one streaming handle per manager slot
with a positive assigned count. Streamed objects and their resource paths are
covered in [traffic-resource-streaming.md](traffic-resource-streaming.md).

## Density column selection

The normal traffic mode is column zero. When the traffic mode global is 6,
the caller looks up the player's XY position in the type-16 spatial area list
using lookup object `0x0088f298` and selector 16. The lookup first checks each
area's 2D bounds, then runs point-in-polygon over its signed vertex count and
vertex array. A matching area supplies its `+0x3c` field as the definition
quota column. A miss leaves the initialized column at zero. This is a spatial
selection; the column is not randomly chosen.

## Candidate assignment refresh (`0x00412be0`)

The caller resets each manager slot's `+0x910` count, then scans the candidate
array. Only candidates whose nested definition has class value 2 at `+0x844`
are assigned by this path. For each eligible candidate, the manager selects
an under-quota slot, writes its definition index to candidate `+0x908`, and
increments the slot's `+0x910` count.

```text
refresh_assignments(manager, densityColumn):
    for slot in manager.definitionSlots:
        slot.assignedCount = 0                         // +0x910

    underQuota = [slot for slot in manager.definitionSlots
                  if definition(slot.index).quota[densityColumn] > 0]
    distinctSlotsUsed = 0

    for candidate in candidateArray[0 .. min(count, 22)):
        seed = definition(candidate.nestedDefinitionIndex) // candidate +0x10
        if seed.class != 2:
            continue

        if distinctSlotsUsed < 7:
            slot = pseudoRandomChoice(underQuota)
            candidate.assignedDefinitionIndex = slot.index // candidate +0x908
            if slot.assignedCount == 0:
                distinctSlotsUsed += 1
            slot.assignedCount += 1

            target = definition(slot.index).quota[densityColumn]
            if slot.assignedCount == target:
                remove slot from underQuota
        else:
            slot = first used slot with assignedCount < quota
            if no such slot:
                slot = first used slot
            candidate.assignedDefinitionIndex = slot.index

            # Observed fallback quirk: the counter increment is always made
            # on the first manager-list entry, not on `slot`.
            firstManagerSlot.assignedCount += 1

    for slot in manager.definitionSlots:
        if slot.assignedCount > 0:
            create_stream_handle(slot)
```

### Retail PRNG (`0x0043c1c0`)

The PRNG helper `0x0043c1c0` returns the global state modulo the bound and
then advances that state. Its exact 32-bit state transition is recoverable
from `0x0043c1c0` and the matching seed-mix helper `0x0043e6b0`:

```text
nextRandomBounded(bound):
    old = state[0x007ea624]
    result = unsigned(old) % bound

    x = old XOR 0x1d872b41
    y = x XOR (x >> 5)
    state[0x007ea624] = y XOR (y << 27) XOR x  // wrap to 32 bits
    return result
```

The modulo is computed from the old state before the recurrence writes the
next state. All shifts and XORs operate on 32-bit values. This is not a linear
congruential update. Global-state setup at `0x0057ecb0` has two observed seed
sources:

```text
if globalFlag[0x008650d0] != 0:
    seed = 0x0057e3b0(0x20, 0x0a)
else:
    seed = 0x0043bdf0() + 0x12345678
    if globalFlag[0x008650d4] != 0:
        0x0057d5b0(configObject, seed, 0x20, 0x0a)  // return ignored

state[0x007ea624] = mixSeed(seed)  // 0x0043e6b0
```

The flags' product meaning is not recovered. In the normal path, `0x0043bdf0`
calls imported `QueryPerformanceCounter` through IAT slot `0x0078311c`, shifts
the returned 64-bit counter right by the runtime count at `0x008284e4`, and
returns the low 32 bits. Helper `0x0043bd90` prepares that shift count from
`QueryPerformanceFrequency` at IAT slot `0x00783118`; it scales the timer period
against the `60,000` comparison constant and counts the doublings needed to
reach that threshold:

```text
periodMs = 1000.0 / QueryPerformanceFrequency()
scaled = periodMs * 2147483648.0
shift = 0
while scaled < 60000.0:
    scaled *= 2.0
    shift += 1
```

The `0x0057d5b0` callback's effect is unknown, but it does not replace the
local `seed` before the `0x0043e6b0` call. Thus the normal seed is
counter-derived and the alternate path is runtime-supplied; neither produces a
fixed initial state from the executable alone.

Helper `0x0043c140` applies the same bounded selection and recurrence to a
caller-supplied state pointer. The manager wrapper `0x005ea4e0` passes
`0x00890080+0xf8`, while traffic assignment and variation selection call
`0x0043c1c0`, which uses global state `0x007ea624`. These are separate state
locations using the same recurrence. Their initial values are linked: collection
constructor `0x005ff490` calls `0x0043c1c0(0x7fffffff)` and stores that result
at `collection+0xf8`. This consumes one value from the global stream to seed
the collection-local stream; after that, the two state words advance
independently.
The spawn caller passes `configuredCap - activeTotal` as its bound, then scans
the manager list for the inactive entry at that ordinal; it does not use the
inactive-entry count as the bound.

The global-state generator chooses among currently under-quota definition
slots; it does not select candidate rows or density columns. If the local
seven-slot threshold has been reached, the fallback walks the already-used
slots in manager-list order.

There is a counter inconsistency in that fallback. The assigned definition
index is still written from the selected slot, but the increment at
`0x00412d6f` targets the first manager-list entry. This means `+0x910` can
stop representing the number of candidates assigned to each definition after
the fallback begins. Keep this behavior if matching the observed executable.

The follow-up `0x00412db0` creates one handle for each slot with positive
`+0x910`; it does not create one handle per candidate. It refreshes the slot
timestamps at `+0x900/+0x904`, clears `+0x914`, calls `0x0063bc70` with the
definition index, stores the returned handle at `+0x8f8`, and increments the
manager active total.

## Handle creation and aged retirement (`0x00412950`)

The per-frame policy chooses the current density column, compares the age of
each populated slot against six seconds, marks over-quota slots for retirement,
releases eligible handles, and may start one new handle:

```text
tick_population(manager, dt):
    mode = currentDensityColumn()                    // usually 0

    for slot in manager.definitionSlots:
        if slot.timestamp is valid:
            age = (globalTimer - slot.timestamp) * 0.00025
        else:
            age = largeSentinel

        target = definition(slot.index).quota[mode]
        if age > 6.0:
            target = max(target, 4)
            if slot.matchedCandidateCount > target:
                slot.retireWhenClear = true

        if definition(slot.index).quota[5] != 0:
            slot.retireWhenClear = false

    for slot in manager.definitionSlots:
        if age(slot) > 6.0 and slot.retireWhenClear
           and slot.referenceCount == 0:
            release(slot.handle)
            clear(slot.handle, slot.timestamp, slot.retireWhenClear)
            manager.activeTotal -= 1

    if manager.activeTotal < min(7, manager.configuredCap):
        inactive = slots whose timestamp is 0 or 0x7fffffff
        # Uses configuredCap-activeTotal as the bound, then treats the result
        # as an ordinal while walking the inactive entries.
        ordinal = randomBelow(manager.configuredCap - manager.activeTotal)
        slot = inactive[ordinal] if ordinal < inactive.length else none
        if slot is none:
            return
        if definition(slot.index).quota[mode] > 0:
            slot.variation = chooseVariation(definition(slot.index))
            slot.handle = createStreamingHandle(slot.index) // 0x0063bc70
            slot.timestamp = globalTimer
            slot.matchedCandidateCount = 0
            slot.retireWhenClear = false
            manager.activeTotal += 1
```

The age conversion and comparison are direct: timer ticks are multiplied by
`0.00025` and the branch is strict `age > 6.0`. Both the retirement decision
and release pass use this strict threshold. The global active ceiling is
seven, reduced by the configured cap at manager `+0x908`. The spawn block
attempts at most one handle per call. Its random bound is specifically
`configuredCap - activeTotal`; the result is used as an ordinal while scanning
inactive entries. When configured cap exceeds seven, this bound can exceed the
remaining global seven-car allowance, so the selection can cover a longer
prefix of the inactive list. If its selected slot has zero quota in the
current column, the routine does not retry another slot during that invocation.

The variation byte is selected through `0x0061bc20` from the definition's
variation table when its count byte at `+0xd9` enables choices. It is stored
at slot `+0x17` and later compared with candidate descriptor byte `+0x07`.
The model-specific meaning of the selected byte is not named by this trace.

## Extracted type-2 quota profiles

The installed table has 15 class-2 definitions. Quota vectors correspond to
columns zero through five:

| Quota vector | Definitions |
|---|---|
| `[5,5,5,5,2,0]` | TAXI, 4DR_SEDAN02, TAXI02, COUPE, PICKUP, 4DR_SEDAN, SUV, MINIVAN, HATCHBACK, HATCHBACK02 |
| `[3,3,3,5,1,0]` | PANELVAN |
| `[3,3,3,3,1,0]` | PARCELVAN |
| `[0,0,0,0,0,0]` | AMBULANCE, FIRETRUCK, BUS |

These are assignment targets, not visible car counts. The separate active
handle ceiling of seven limits the actual streamed set. See
[traffic-vehicle-definitions.md](traffic-vehicle-definitions.md) for all 46
records and the variation bytes.

## Candidate producers and limits

The shared candidate array is bounded at 22 records. `0x004fe9f0` is one
producer: it can create up to 16 kind-3 descriptors from a selector-dependent
count, seeded with type-2 definition index 31 (TAXI). The later density
assignment can replace that candidate's assigned definition at `+0x908`.
Other producers add descriptors through `0x00601600`; their kind values and
owner counters are preserved in the runtime records. The selector-dependent
count and all source record bytes are documented in
[keyed-mode-records.md](keyed-mode-records.md).

Candidate matching, resource reconciliation, and the live object's pose and
frame callbacks are separate steps. This manager model recovers which models
can be requested and when a handle is started or retired; it does not reproduce
route choice, collision avoidance, intersection response, wheel placement, or
render submission. Those are documented separately in the traffic action,
route, vehicle-physics, and resource-streaming notes.

## Evidence map

- `0x00412950`: per-frame age, quota-floor, retirement, seven-car cap, and
  single-slot spawn attempt.
- `0x00412be0`: candidate-to-definition assignment and seven-distinct-slot
  fallback.
- `0x0043c1c0`: bounded integer selection and its exact 32-bit state
  recurrence; `0x0043e6b0` applies the same recurrence to an external seed.
- `0x0043c140` and `0x005ea4e0`: pointer-based bounded stream used by manager
  spawn selection; constructor `0x005ff490` seeds it from one global-stream draw.
- `0x0057ecb0`, `0x0043bdf0`, and `0x0043bd90`: global RNG seed selection and
  QueryPerformanceCounter/QueryPerformanceFrequency fallback.
- `0x00412db0`: one streaming handle per positively counted slot.
- `0x0057f93c..0x0057f9a0`: spatial mode-6 density-column refresh caller.
- `0x005d9bf0`: type-16 bounds and point-in-polygon area lookup.
- `0x00601600`: candidate admission and 22-record capacity.
- `0x004fe9f0`: selector-driven kind-3 candidate producer.

Expanded instruction-level evidence and disassembly line references are in
the [traffic manager sections of README.md](README.md#traffic-manager-per-frame-policy-0x412950).
