Compare commits

..
8 Commits
Author SHA1 Message Date
haylanandClaude Bot 0e76dc453a docs(research): record blocking-bypass search and multilane decision
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 22:32:32 +02:00
haylanandClaude Bot b53ed477f3 fix(prototype): send component exchange without callback
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 22:29:28 +02:00
haylanandClaude Bot f9bc0e79fc chore(prototype): try timeToIgnore via debug component exchange
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 22:26:39 +02:00
haylanandClaude Bot 58fe0d2986 docs(research): record in-game vehicle probe findings
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 22:24:34 +02:00
haylanandClaude Bot b6daeaa328 chore(prototype): log lane layout of shared street segments
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 22:17:52 +02:00
haylanandClaude Bot aa5dfa655f fix(prototype): read path edges as EdgeIdDir
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 22:16:17 +02:00
haylan eaf6553cae docs: add mod.io terms research note
Capture the mod.io ToU/AUP clauses that bind a published mod (TpF3 has no
Steam Workshop; mod.io is the official in-game mod browser). Uploads grant
mod.io + the game admin a perpetual irrevocable license; game-side rules
override mod.io's per ToU §1.7. Corrects the map's out-of-scope line.
2026-10-05 22:12:48 +02:00
haylanandClaude Bot c936946eb4 chore(prototype): add in-game vehicle probe game script
Throwaway probe for "Probe vehicle data and native passing in game":
logs every player-owned road vehicle's MovePath/LandVehicle/move-info
state every 5s and groups vehicles by street segment to expose lanes
and oncoming traffic.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 22:06:26 +02:00
5 changed files with 1727 additions and 0 deletions

No files matched your search

+26
View File
@@ -0,0 +1,26 @@
# mod.io Terms: what applies to a published mod
Ad-hoc research 2026-10-05 (no ticket — reference material for the map).
Fetched from the no-chrome legal routes: `https://mod.io/legal/terms?no_links=true` (Terms of Use) and `https://mod.io/legal/aup?no_links=true` (Acceptable Use Policy). `mod.io/terms` itself is a JS app; the same text is also available via `GET /agreements/types/:id/current` (1 = Terms of Use, 3 = Game Terms, 4 = API Access Terms) with an API key — not needed for offline reading.
## Why this matters
TpF3 has **no Steam Workshop**: the official in-game mod browser is mod.io (see `docs/reference/wiki/modding_general_publishing.md`). The map's old "Steam Workshop publishing" out-of-scope line has been corrected to mod.io. This note is the mod.io side of that story; the game-side rules (free, non-commercial, no offensive content, monetization) are already captured in the publishing wiki doc.
## Key clauses (Terms of Use, by number)
- **§1.5** The Acceptable Use Policy is incorporated into the ToU; breaching the AUP is breaching the ToU.
- **§1.7 Priority order** (first wins on conflict): Game Terms > game's AUP > mod.io AUP > ToU clauses 15/16 (EU/AU consumers) > Monetization Terms > ToU. So **Urban Games' own rules override mod.io's**.
- **§1.9** mod.io may change the ToU; 30 days after publication the new terms bind you.
- **§6.2 UGC license** — by uploading a mod you grant mod.io **and the Relevant Game Admin (Urban Games)** a transferable, sublicensable, royalty-free, **irrevocable, non-exclusive, perpetual worldwide** license to use, reproduce, modify, make derivative works of, and distribute the mod "for the sole purpose of providing the Services". Optional *Enhanced License Rights* (sale/resale) only if separately opted in via the UGC Control Panel, and those are revocable.
- **§6.3 warranty** — you warrant the UGC is yours or you have the right to distribute it, and does not violate others' rights. This is what makes the wiki's "no content you don't have copyright or permission to distribute" enforceable.
- **§8 Copyright** — DMCA-style process; mod.io (or the Game Admin) may delete/disable content alleged to infringe.
- **§11 Termination** — mod.io may terminate/suspend the account immediately for material or repeated breach; and the Game Admin may **restrict access to the game's mod services at any time, without notice**.
- **AUP** — standard prohibitions: nothing illegal, no spam/malware, no harassment/defamation, no probing or unauthorised access to the service, no scraping of data you aren't meant to get.
## Implications for this mod
- Free and non-commercial is the game-side rule anyway; nothing new here.
- Uploading = a perpetual irrevocable license to mod.io + Urban Games. Fine for a code-only behavioural mod; just know it before publishing anything with third-party assets (see also the base-game-file reuse rules in the publishing wiki doc).
- The mod can be pulled or access restricted at Urban Games' discretion at any time — no recourse beyond the ToU's notice process.
- Nothing in the ToU/AUP touches the technical side of the mod (script API, staging area, etc.); it only governs distribution and conduct.
File diff suppressed because it is too large. Load diff
+48
View File
@@ -0,0 +1,48 @@
# 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.
+8
View File
@@ -0,0 +1,8 @@
-- PROTOTYPE (ticket "Probe vehicle data and native passing in game"): throwaway, lives only on branch prototype/vehicle-probe.
function data()
return {
updateScript = {
fileName = "overtake_probe.script@update",
},
}
end
+111
View File
@@ -0,0 +1,111 @@
-- PROTOTYPE (ticket "Probe vehicle data and native passing in game"): throwaway, lives only on branch prototype/vehicle-probe.
-- Every INTERVAL it dumps every player-owned road vehicle, then groups them by street segment
-- so lanes (edge index) and oncoming traffic (opposite direction / turnaround edge) are visible in stdout.txt.
-- ponytail: module-local timer, not GameScriptState; fine for a probe, resets on reload.
local TAG = "[overtake_probe] "
local INTERVAL = 5.0
local elapsed = 0.0
local ticks = 0
local function p(s : string)
print(TAG .. s)
end
local function edgeAt(mp : Engine.Component.MovePath) : EdgeId, boolean
-- path.edges is 1-based, elements are EdgeIdDir userdata (tealdef says {EdgeId, boolean})
local e = (mp.path.edges as {any})[mp.dyn.pathPos.edgeIndex + 1] as UtilPathfinding.EdgeIdDir
if e == nil then return nil, nil end
return e.edgeId, e.dir
end
local function turnaround(edge : EdgeId) : integer
local tn = api.engine.getComponent(edge.entity, api.type.ComponentType.TRANSPORT_NETWORK) as Engine.Component.TransportNetwork
if tn == nil or tn.turnaroundEdges == nil then return nil end
return tn.turnaroundEdges[edge.index + 1]
end
-- EXPERIMENT: does api.cmd.debug.makeComponentExchangeCmd accept a MovePath, and does a positive
-- dyn.timeToIgnore make the follower ignore the vehicle ahead? Hypothesis only; nothing documents either.
local IGNORE_SECONDS = 10.0
local MAX_GAP = 40.0
local record Car
v : integer
pos : number
speed : number
dir : boolean
mp : Engine.Component.MovePath
end
local function tryIgnore(follower : Car, leader : Car)
local mp = follower.mp
mp.dyn.timeToIgnore = IGNORE_SECONDS
p(string.format("IGNORE_TEST send veh=%d leader=%d gap=%.1f v=%.2f leaderV=%.2f", follower.v, leader.v, math.abs(leader.pos - follower.pos), follower.speed, leader.speed))
local cmd = api.cmd.debug.makeComponentExchangeCmd(follower.v, mp as Engine.Component)
-- engine-state scripts can't pass a callback ("Error decoding argument #3"); the next dump shows the effect
api.cmd.sendCommand(cmd)
end
local function dump(dt : number)
local player = api.engine.util.getPlayer()
local vehicles = api.engine.getEntitiesWithComponent(api.type.ComponentType.TRANSPORT_VEHICLE, { requireOwnedByPlayer = player })
p(string.format("tick=%d dt=%s playerVehicles=%d", ticks, tostring(dt), #vehicles))
local bySegment : {integer : {string}} = {}
local byLane : {string : {Car}} = {}
for _, v in ipairs(vehicles) do
local lv = api.engine.getComponent(v, api.type.ComponentType.LAND_VEHICLE) as Engine.Component.LandVehicle
local mp = api.engine.getComponent(v, api.type.ComponentType.MOVE_PATH) as Engine.Component.MovePath
if lv ~= nil and not lv.isTram and mp ~= nil then
local edge, dir = edgeAt(mp)
local info = api.engine.system.landVehicleMoveSystem.getVehicleInfo(v)
local seg = edge and edge.entity or -1
local line = string.format(
"veh=%d seg=%d lane=%s dir=%s turnaround=%s pos=%.1f speed=%.2f accel=%.2f blocked=%s state=%s standing=%.1f timeToIgnore=%.2f timeUntilAccel=%.2f occ=%s..%s brake=%.1f power=%s traction=%s",
v, seg, tostring(edge and edge.index), tostring(dir), tostring(edge and turnaround(edge)),
mp.dyn.pathPos.pos, mp.dyn.speed, mp.dyn.accel, tostring(mp.blocked), tostring(mp.state),
mp.dyn.timeStanding, mp.dyn.timeToIgnore, mp.dyn.timeUntilAccel,
tostring(mp.occupiedBackIndex), tostring(mp.occupiedFrontIndex), lv.dyn.brakingTimer,
tostring(info and info.availPower), tostring(info and info.traction))
p(line)
if edge ~= nil then
local key = seg .. ":" .. edge.index
byLane[key] = byLane[key] or {}
table.insert(byLane[key], { v = v, pos = mp.dyn.pathPos.pos, speed = mp.dyn.speed, dir = dir, mp = mp })
end
bySegment[seg] = bySegment[seg] or {}
table.insert(bySegment[seg], string.format("%d(lane=%s,dir=%s,v=%.1f,blk=%s)", v, tostring(edge and edge.index), tostring(dir), mp.dyn.speed, tostring(mp.blocked)))
end
end
for seg, list in pairs(bySegment) do
if #list > 1 then
p(string.format("SHARED seg=%d: %s", seg, table.concat(list, " ")))
end
end
for _, cars in pairs(byLane) do
for i = 1, #cars do
for j = 1, #cars do
local a, b = cars[i], cars[j]
-- ponytail: assumes pos grows in driving direction when dir=true; the log shows whether that holds
local ahead = (a.dir and b.pos > a.pos) or (not a.dir and b.pos < a.pos)
if i ~= j and ahead and math.abs(b.pos - a.pos) < MAX_GAP then
tryIgnore(a, b)
end
end
end
end
end
local ret : GameScript<table> = {
update = function(_userParams : table, _state : GameScriptState<table>, dt : number) : nil
ticks = ticks + 1
elapsed = elapsed + dt
if ticks == 1 then p("update running, first dt=" .. tostring(dt)) end
if elapsed < INTERVAL then return end
elapsed = 0.0
xpcall(dump, function(err : any) p("ERROR " .. tostring(err)) end, dt)
end,
}
return ret