From bfe5a22d1d1cb6062653d8970dd4df30e64197f6 Mon Sep 17 00:00:00 2001 From: ArthurErlich Date: Mon, 5 Oct 2026 23:23:53 +0200 Subject: [PATCH] chore(prototype): remove segment rebuild and dead engine-side experiments Rebuild confirmed ineffective on single-lane roads (log + in game). Engine side is logging only again; the GUI-thread position flip is the one experiment left. Co-Authored-By: Claude Opus 5.5 --- docs/research/vehicle-probe.md | 4 + mod/content/overtake_probe.script.tl | 176 ++------------------------- 2 files changed, 16 insertions(+), 164 deletions(-) diff --git a/docs/research/vehicle-probe.md b/docs/research/vehicle-probe.md index a793923..6b481a4 100644 --- a/docs/research/vehicle-probe.md +++ b/docs/research/vehicle-probe.md @@ -80,3 +80,7 @@ Scanned: the 9 mods in `C:\Users\Public\mod.io\10640\mods\` (e.g. `devers_1`, `m - 3 `replaceSegment` rebuilds on `country_new_small` (one lane per direction), segments 30213, 29776 and 30215, follower 25.8 m behind leader. **No effect**: the next snapshot had the same gap (22.6/48.4 → 49.3/75.1), both vehicles stayed in lane 2, same speeds, same order for 10+ snapshots. Rebuilding does not move vehicles past each other on single-lane roads. - How the multilane lane change happens is still uncaught: snapshots every 2 s only show before (both lane 4) and after (the leader in lane 3 two segments later). The probe now traces the two vehicles on every update for ~30 s after a rebuild (`TRACE` lines), and rebuilds only multilane segments. + +## Segment rebuild dropped (2026-10-05) + +The human confirmed in game what the log showed: rebuilding the segment under a held-back vehicle does not get it past the blocker on a single-lane road. The rebuild code is removed from the probe (it's still in git history, last in commit 5d3a254). The lane split after rebuilds on multilane roads (see above) is left as an observation for the multilane ticket, not pursued here. diff --git a/mod/content/overtake_probe.script.tl b/mod/content/overtake_probe.script.tl index 1afc6c9..fbfdcff 100644 --- a/mod/content/overtake_probe.script.tl +++ b/mod/content/overtake_probe.script.tl @@ -1,18 +1,14 @@ -- PROTOTYPE (tickets "Probe vehicle data and native passing in game", "Can rebuilding a road segment move a passer -- past its blocker?"): throwaway, lives only on branch prototype/vehicle-probe. --- Every INTERVAL it dumps every player-owned road vehicle and groups them by street segment. --- EXPERIMENT: when a player vehicle is within MAX_GAP behind another in the same lane of a two-way segment, --- rebuild that segment (remove + re-add an identical BaseEdge) and log where both vehicles end up. +-- Engine thread: every INTERVAL it logs every player-owned road vehicle (segment, lane, position, speed). +-- GUI thread: EXPERIMENT flipping the positions of a follower and its leader (see below). +-- Earlier experiments (timeToIgnore, engine-side jump, set-line, segment rebuild) are in git history and +-- docs/research/vehicle-probe.md; none moved a vehicle past another on a single-lane road. -- ponytail: module-local timer, not GameScriptState; fine for a probe (state resets between calls anyway). local TAG = "[overtake_probe] " local INTERVAL = 2.0 local MAX_GAP = 40.0 -local MAX_REBUILDS = 0 -- off while testing the GUI-thread position flip (replaceSegment works, see vehicle-probe.md) -local MAX_SETLINES = 0 -- set-line disabled: lanes ahead get re-planned by the engine anyway, no forced lane change -local MAX_JUMPS = 0 -- dead: sendCommand(makeComponentExchangeCmd(...)) fails in engine state even without a callback -local JUMP_AHEAD = 15.0 -local WATCH_TICKS = 150 -- ~30 s of game time at dt=0.2: log watched vehicles every update after a rebuild local elapsed = 0.0 local ticks = 0 @@ -41,170 +37,23 @@ local record Car dir : boolean end -local record ProbeState - rebuilds : integer - setlines : integer - jumps : integer - watch : {integer} -- vehicles involved in the last rebuild, flagged in later dumps - watchLeft : integer -- updates left for per-tick logging of the watched vehicles -end - --- No edge objects (stops, signals), --- because re-adding them is out of scope for this probe. -local function rebuildable(seg : integer) : boolean, string - local be = api.engine.getComponent(seg, api.type.ComponentType.BASE_EDGE) as Engine.Component.BaseEdge - if be == nil then return false, "no BaseEdge" end - if #be.objects > 0 then return false, "has edge objects" end - -- count BaseEdge lanes, not TransportNetwork edges (town roads split each lane into several edges). - -- Sidewalks carry modes 0/1 only; driving lanes carry 2+ (observed: [2,3,4]). - local driving = 0 - for _, lc in ipairs(be.laneConfigs) do - for k, on in pairs(lc.transportModes) do - if on and (tonumber(tostring(k)) or 0) >= 2 then driving = driving + 1 break end - end - end - -- one lane per direction: 3 rebuilds there changed nothing (same gap, same lane), so trace multilane only - if driving < 4 then return false, "driving lanes=" .. driving .. " (single-lane rebuilds showed no effect)" end - return true, string.format("drivingLanes=%d template=%s length=%.1f", driving, tostring(be.roadTemplate), be.distance) -end - -local function rebuild(seg : integer) - -- same call the official campaign (mission 03 init.script.tl) uses to refresh a road segment in place - local proposal = api.engine.util.proposal.replaceSegment(seg) - api.cmd.sendCommand(api.cmd.makeWorldBuildProposalCmd(proposal, nil, true, true)) -end - --- lanes of the next few path edges, to see whether a re-path changes the lane choice -local function lanesAhead(mp : Engine.Component.MovePath) : string - local out : {string} = {} - local edges = mp.path.edges as {any} - for k = mp.dyn.pathPos.edgeIndex + 1, math.min(#edges, mp.dyn.pathPos.edgeIndex + 4) do - local e = edges[k] as UtilPathfinding.EdgeIdDir - table.insert(out, e.edgeId.entity .. ":" .. e.edgeId.index) - end - return table.concat(out, " ") -end - -local function setLine(v : integer) - local tv = api.engine.getComponent(v, api.type.ComponentType.TRANSPORT_VEHICLE) as Engine.Component.TransportVehicle - p(string.format("SETLINE_TEST send veh=%d line=%s stopIndex=%s", v, tostring(tv.line), tostring(tv.stopIndex))) - api.cmd.sendCommand(api.cmd.makeVehicleSetLineCmd(v, tv.line, tv.stopIndex)) -end - --- returns false when the target doesn't fit on the current edge (we don't jump across edges) -local function jumpPast(follower : Car, leader : Car) : boolean, string - if leader.pos01 <= 0 then return false, "leader pos01=0" end - local len = leader.pos / leader.pos01 - local target = leader.pos + JUMP_AHEAD - if target >= len then return false, string.format("target %.1f beyond edge length %.1f", target, len) end - local mp = api.engine.getComponent(follower.v, api.type.ComponentType.MOVE_PATH) as Engine.Component.MovePath - mp.dyn.pathPos.pos = target - mp.dyn.pathPos.pos01 = target / len - api.cmd.sendCommand(api.cmd.debug.makeComponentExchangeCmd(follower.v, mp as Engine.Component)) - return true, string.format("pos %.1f -> %.1f (leader %.1f, edge length %.1f)", follower.pos, target, leader.pos, len) -end - -local function dump(dt : number, state : GameScriptState) - local st = state:get() - st.rebuilds = st.rebuilds or 0 - st.setlines = st.setlines or 0 - st.jumps = st.jumps or 0 - st.watch = st.watch or {} - local watched : {integer : boolean} = {} - for _, w in ipairs(st.watch) do watched[w] = true 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 rebuilds=%d setlines=%d jumps=%d", ticks, tostring(dt), #vehicles, st.rebuilds, st.setlines, st.jumps)) - - local byLane : {string : {Car}} = {} - local laneSeg : {string : integer} = {} + p(string.format("tick=%d dt=%s playerVehicles=%d", ticks, tostring(dt), #vehicles)) 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 local tv = api.engine.getComponent(v, api.type.ComponentType.TRANSPORT_VEHICLE) as Engine.Component.TransportVehicle if lv ~= nil and not lv.isTram and mp ~= nil then local edge, dir = edgeAt(mp) - local seg = edge and edge.entity or -1 p(string.format( - "%sveh=%d seg=%d lane=%s dir=%s turnaround=%s edgeIndex=%d/%d pos=%.1f speed=%.2f state=%s tvState=%s line=%s standing=%.1f", - watched[v] and "WATCH " or "", v, seg, tostring(edge and edge.index), tostring(dir), tostring(edge and turnaround(edge)), + "veh=%d seg=%d lane=%s dir=%s turnaround=%s edgeIndex=%d/%d pos=%.1f speed=%.2f state=%s tvState=%s line=%s standing=%.1f", + v, edge and edge.entity or -1, tostring(edge and edge.index), tostring(dir), tostring(edge and turnaround(edge)), mp.dyn.pathPos.edgeIndex, #(mp.path.edges as {any}), mp.dyn.pathPos.pos, mp.dyn.speed, tostring(mp.state), tostring(tv and tv.state), tostring(tv and tv.line), mp.dyn.timeStanding)) - if watched[v] then p("WATCH lanesAhead veh=" .. v .. " " .. lanesAhead(mp)) end - if edge ~= nil then - local key = seg .. ":" .. edge.index - byLane[key] = byLane[key] or {} - laneSeg[key] = seg - table.insert(byLane[key], { v = v, pos = mp.dyn.pathPos.pos, pos01 = mp.dyn.pathPos.pos01, speed = mp.dyn.speed, dir = dir }) - end end end - - if st.rebuilds >= MAX_REBUILDS and st.setlines >= MAX_SETLINES and st.jumps >= MAX_JUMPS then return end - for key, 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 (held so far in the logs) - local ahead = (a.dir and b.pos > a.pos) or (not a.dir and b.pos < a.pos) - -- leader must be moving, otherwise it's a queue at a stop or junction, not a slow vehicle - if i ~= j and ahead and math.abs(b.pos - a.pos) < MAX_GAP and b.speed > 1.0 then - local seg = laneSeg[key] - if st.jumps < MAX_JUMPS then - local ok, why = jumpPast(a, b) - p(string.format("JUMP_TEST seg=%d follower=%d leader=%d sent=%s %s", seg, a.v, b.v, tostring(ok), why)) - if ok then - st.jumps = st.jumps + 1 - st.watch = { a.v, b.v } - state:set(st) - return -- one experiment per dump - end - end - if st.setlines < MAX_SETLINES then - local amp = api.engine.getComponent(a.v, api.type.ComponentType.MOVE_PATH) as Engine.Component.MovePath - p(string.format("SETLINE_TEST candidate seg=%d follower=%d (v=%.2f) leader=%d (v=%.2f) lanesAhead before: %s", - seg, a.v, a.speed, b.v, b.speed, lanesAhead(amp))) - setLine(a.v) - st.setlines = st.setlines + 1 - st.watch = { a.v, b.v } - state:set(st) - return -- one experiment per dump - end - if st.rebuilds >= MAX_REBUILDS then return end - local ok, why = rebuildable(seg) - p(string.format("REBUILD_TEST candidate seg=%d follower=%d (pos=%.1f v=%.2f) leader=%d (pos=%.1f v=%.2f) ok=%s %s", - seg, a.v, a.pos, a.speed, b.v, b.pos, b.speed, tostring(ok), why)) - if ok then - rebuild(seg) - st.rebuilds = st.rebuilds + 1 - st.watch = { a.v, b.v } - st.watchLeft = WATCH_TICKS - state:set(st) - p(string.format("REBUILD_TEST sent #%d seg=%d", st.rebuilds, seg)) - return -- one rebuild per dump - end - end - end - end - end -end - --- per-update trace of the watched vehicles, to catch the moment a lane changes -local function trace(state : GameScriptState) - local st = state:get() - if (st.watchLeft or 0) <= 0 or st.watch == nil then return end - for _, v in ipairs(st.watch) do - local mp = api.engine.getComponent(v, api.type.ComponentType.MOVE_PATH) as Engine.Component.MovePath - if mp ~= nil then - local edge, _dir = edgeAt(mp) - p(string.format("TRACE t=%d veh=%d seg=%s lane=%s pos=%.1f v=%.2f ahead=%s", - WATCH_TICKS - st.watchLeft, v, tostring(edge and edge.entity), tostring(edge and edge.index), - mp.dyn.pathPos.pos, mp.dyn.speed, lanesAhead(mp))) - end - end - st.watchLeft = st.watchLeft - 1 - state:set(st) end -- EXPERIMENT (GUI thread, where sendCommand callbacks work): flip the positions of a follower and its leader by @@ -294,17 +143,16 @@ local function guiStep(gsState : GameScriptState) gsState:set(gs) end -local ret : GameScriptWithGui = { - update = function(_userParams : table, state : GameScriptState, dt : number) : nil +local ret : GameScriptWithGui = { + update = function(_userParams : table, _state : GameScriptState, dt : number) : nil ticks = ticks + 1 elapsed = elapsed + dt - if dt > 0 then xpcall(trace, function(err : any) p("ERROR trace " .. tostring(err)) end, state) end 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, state) + xpcall(dump, function(err : any) p("ERROR " .. tostring(err)) end, dt) end, - guiUpdate = function(_userParams : table, _state : GameScriptStateReadOnly, guiState : GameScriptState) + guiUpdate = function(_userParams : table, _state : GameScriptStateReadOnly
, guiState : GameScriptState) xpcall(guiStep, function(err : any) p("ERROR gui " .. tostring(err)) end, guiState) end, }