55 lines
6.4 KiB
Markdown
55 lines
6.4 KiB
Markdown
# In-game vehicle probe
|
||
|
||
Ticket "Probe vehicle data and native passing in game" (part of map "Map: TpF3 overtaking mod"). Run 2026-10-05 on game build 40408, with the throwaway probe `mod/content/overtake_probe.{gs.lua,script.tl}` on branch `prototype/vehicle-probe`. Log excerpt: `probe-logs/2026-10-05-vehicle-probe.txt`.
|
||
|
||
## How the probe works
|
||
|
||
- A mod registers a per-tick game script by shipping `content/<name>.gs.lua` (`data()` returns `updateScript = { fileName = "<name>.script@update" }`), the same way the base game's `vehicle/vehicle_modifier.gs.lua` does. No `mod.json` entry is needed. Adding it to an existing save logs `Creating entity for newly added GameScript haylan_tf3_overtake::/overtake_probe.gs`.
|
||
- `update` runs every 0.2 s of game time (`dt = 0.2`).
|
||
- **Module-level Lua variables don't persist between calls**: the script runs in several or fresh Lua states. Persistent state has to go through `GameScriptState`.
|
||
- **No hot reload**: an edited `.tl` takes effect only after the save is reloaded.
|
||
- `print` goes to `<Steam>/userdata/<id>/3493540/local/crash_dump/stdout.txt`.
|
||
|
||
## API facts (observed, overriding tealdef where they differ)
|
||
|
||
- Player vehicles: `api.engine.getEntitiesWithComponent(TRANSPORT_VEHICLE, { requireOwnedByPlayer = api.engine.util.getPlayer() })`. Road vehicles also carry `LAND_VEHICLE` and `MOVE_PATH`.
|
||
- `MovePath.path.edges` is a **1-based Lua table of `EdgeIdDir` userdata** (`.edgeId`, `.dir`), **not** `{EdgeId, boolean}` as the tealdef says. The current edge is `edges[dyn.pathPos.edgeIndex + 1]`.
|
||
- `EdgeId.entity` is the **street segment** (it carries `BASE_EDGE` and `TRANSPORT_NETWORK`). `EdgeId.index` is the **lane** within that segment's `TransportNetwork.edges`.
|
||
- `TransportNetwork.turnaroundEdges[index + 1]` is the opposite lane. `TransportNetwork.edges[i]` has `transportModes`, `speedLimit` and `curSpeed`.
|
||
- `BaseEdge.laneConfigs` lists the lanes, with `forward`, `offset`, `width` and `transportModes`. Mode keys print as integers: sidewalks `[0,1]`, driving lanes `[2,3,4]`.
|
||
- `landVehicleMoveSystem.getVehicleInfo` only returns `availPower`, `traction`, `tractiveEffort` and `vehicleAvailPower`.
|
||
- `MovePath.blocked` stayed `0` for every vehicle the whole time, including vehicles clearly queued behind a slower one. It doesn't signal "held up by the vehicle ahead".
|
||
- `MovePath.dyn.timeToIgnore` is negative and counts down for almost every vehicle. It was briefly positive (+9.4) once, right after a stop. Its meaning is unknown.
|
||
|
||
## Observed traffic behaviour
|
||
|
||
| Road | Lanes per direction | What happened |
|
||
|---|---|---|
|
||
| `country_new_medium` (segments 6527 → 29697) | 2 (lanes 3 and 4 forward) | 29251 (power 243) followed 29852 (power 160) in lane 4 over 6 segments, 24–30 m behind. It accelerated to 17.7 m/s and then dropped to 15.1 m/s to match the vehicle ahead. Lane 3 was free and stayed unused. |
|
||
| `country_new_medium` (segment 28894) | 2 | The two vehicles were in different lanes (3 and 4), but their order never changed. This looks like lane choice for the next turn, not passing. |
|
||
| `country_new_small` (segments 26513 → 28566) | 1 (lane 2 forward, lane 1 opposite) | 29251 was held at exactly 16.67 m/s (29851's top speed) behind 29851, below the road's 22.2 m/s limit, for 6+ segments. |
|
||
|
||
## Verdict
|
||
|
||
- The engine does **not** pass natively, on either multilane or two-way roads. Bus lanes weren't tested.
|
||
- The read side is sufficient: a script can tell, per vehicle, which segment, lane and direction it's in, where it is, how fast it's going, and what the opposite lane is. So both the "held back" condition and an oncoming-traffic check can be computed. "Held back" must be derived (same segment and lane, small gap, speed below the vehicle's capability), because `blocked` doesn't report it.
|
||
- **No documented command can make a vehicle ignore the one ahead** (see `overtake-feasibility.md`).
|
||
|
||
## Searching for a way to make a vehicle ignore the one ahead
|
||
|
||
- **Script commands:** none of the commands in `api.cmd` moves a vehicle, changes its lane, or turns off the queueing. The full list was checked.
|
||
- **Model metadata:** `landVehicle` (engines, topSpeed, weights) and `extent` have no following or blocking parameters.
|
||
- **`Transformator.LandVehicleScriptingApi` / `TransfOutput.setUserTransf`:** a per-model animation script. It can offset the model on screen, which could make a vehicle look like it changes lanes, but it doesn't touch the simulation.
|
||
- **Executable strings:** `LandVehicleMoveSystem.cpp` contains `GetNextEdgeTrafficPenaltyAndAlternatives`, `existingPathLaneSwitch` and `switchIndices`. So the engine itself switches lanes at segment boundaries based on a traffic penalty, which matches the lane split seen on segment 28894. None of it is exposed to scripts.
|
||
- **`api.cmd.debug.makeComponentExchangeCmd(entity, MovePath)`** (save loaded with "Debug mode is active"): building the command with a `MovePath` **succeeds**. `api.cmd.sendCommand(cmd, callback)` fails with `Error decoding argument #3`, because engine-state scripts can't pass a callback (base scripts never do). The version without a callback was committed but **never ran in game**. Whether the swap is applied, and what a positive `timeToIgnore` does, are both **untested**.
|
||
|
||
## Decision
|
||
|
||
Overtaking on two-way roads (one lane per direction) is dropped (decided by the user, 2026-10-05). The effort continues on multilane roads only: a held-back vehicle should switch to a lane the slower vehicle isn't in.
|
||
|
||
## Segment rebuild (ticket "Can rebuilding a road segment move a passer past its blocker?")
|
||
|
||
- The script sent 3 `makeWorldBuildProposalCmd` proposals (remove segment + re-add a copy of its `BaseEdge`/`BaseEdgeStreet`, `ignoreErrors = true`) for segments 29701 and 29699 (`country_new_medium`). **None took effect**: both segment ids stayed in use by vehicles until the end of the log, and 29699 was "rebuilt" twice. The game logged no error, and there's no callback in engine state. The copied `BaseEdge.distance` read `0.0`, so the copy is probably incomplete. A next attempt would need `api.engine.util.proposal.makeProposalData` to see the proposal's errors.
|
||
- The human's manual segment deletion left no trace in the probe data: every vehicle stayed `tvState=1` (en route), with only normal path trimming.
|
||
- Related find: `api.cmd.makeVehicleSendToDepotCmd(vehicle, sellOnArrival, jumpToDepotEntity)` "teleports" a vehicle to a depot. The engine can relocate vehicles instantly, but only to depots.
|