Files
tpf3-overtake/docs/research/vehicle-probe.md
T
2026-10-05 22:24:34 +02:00

3.8 KiB
Raw Blame History

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).