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 <noreply@anthropic.com>
This commit is contained in:
haylanandClaude Bot committed 2026-10-05 23:23:53 +02:00
1 parent 5d3a254e55
commit bfe5a22d1d
2 files changed
+16 -164

No files matched your search

+4
View File
@@ -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. - 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. - 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.
+12 -164
View File
@@ -1,18 +1,14 @@
-- PROTOTYPE (tickets "Probe vehicle data and native passing in game", "Can rebuilding a road segment move a passer -- 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. -- 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. -- Engine thread: every INTERVAL it logs every player-owned road vehicle (segment, lane, position, speed).
-- EXPERIMENT: when a player vehicle is within MAX_GAP behind another in the same lane of a two-way segment, -- GUI thread: EXPERIMENT flipping the positions of a follower and its leader (see below).
-- rebuild that segment (remove + re-add an identical BaseEdge) and log where both vehicles end up. -- 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). -- ponytail: module-local timer, not GameScriptState; fine for a probe (state resets between calls anyway).
local TAG = "[overtake_probe] " local TAG = "[overtake_probe] "
local INTERVAL = 2.0 local INTERVAL = 2.0
local MAX_GAP = 40.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 elapsed = 0.0
local ticks = 0 local ticks = 0
@@ -41,170 +37,23 @@ local record Car
dir : boolean dir : boolean
end end
local record ProbeState local function dump(dt : number)
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<ProbeState>)
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 player = api.engine.util.getPlayer() local player = api.engine.util.getPlayer()
local vehicles = api.engine.getEntitiesWithComponent(api.type.ComponentType.TRANSPORT_VEHICLE, { requireOwnedByPlayer = player }) 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)) p(string.format("tick=%d dt=%s playerVehicles=%d", ticks, tostring(dt), #vehicles))
local byLane : {string : {Car}} = {}
local laneSeg : {string : integer} = {}
for _, v in ipairs(vehicles) do for _, v in ipairs(vehicles) do
local lv = api.engine.getComponent(v, api.type.ComponentType.LAND_VEHICLE) as Engine.Component.LandVehicle 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 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 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 if lv ~= nil and not lv.isTram and mp ~= nil then
local edge, dir = edgeAt(mp) local edge, dir = edgeAt(mp)
local seg = edge and edge.entity or -1
p(string.format( 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", "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",
watched[v] and "WATCH " or "", v, seg, tostring(edge and edge.index), tostring(dir), tostring(edge and turnaround(edge)), 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), 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)) 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
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<ProbeState>)
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 end
-- EXPERIMENT (GUI thread, where sendCommand callbacks work): flip the positions of a follower and its leader by -- 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<GuiState>)
gsState:set(gs) gsState:set(gs)
end end
local ret : GameScriptWithGui<ProbeState, GuiState> = { local ret : GameScriptWithGui<table, GuiState> = {
update = function(_userParams : table, state : GameScriptState<ProbeState>, dt : number) : nil update = function(_userParams : table, _state : GameScriptState<table>, dt : number) : nil
ticks = ticks + 1 ticks = ticks + 1
elapsed = elapsed + dt 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 ticks == 1 then p("update running, first dt=" .. tostring(dt)) end
if elapsed < INTERVAL then return end if elapsed < INTERVAL then return end
elapsed = 0.0 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, end,
guiUpdate = function(_userParams : table, _state : GameScriptStateReadOnly<ProbeState>, guiState : GameScriptState<GuiState>) guiUpdate = function(_userParams : table, _state : GameScriptStateReadOnly<table>, guiState : GameScriptState<GuiState>)
xpcall(guiStep, function(err : any) p("ERROR gui " .. tostring(err)) end, guiState) xpcall(guiStep, function(err : any) p("ERROR gui " .. tostring(err)) end, guiState)
end, end,
} }