From dcc4b4afae66eefe32911f970f57e812a661464e Mon Sep 17 00:00:00 2001 From: ArthurErlich Date: Mon, 5 Oct 2026 22:37:57 +0200 Subject: [PATCH] chore(prototype): rebuild two-way segment under a held-back vehicle Replaces the timeToIgnore experiment (still in history) with the segment-rebuild probe for "Can rebuilding a road segment move a passer past its blocker?". Capped at 3 rebuilds per save via GameScriptState. Co-Authored-By: Claude Opus 5.5 --- mod/content/overtake_probe.script.tl | 124 +++++++++++++++++---------- 1 file changed, 81 insertions(+), 43 deletions(-) diff --git a/mod/content/overtake_probe.script.tl b/mod/content/overtake_probe.script.tl index 928c1fe..ce6f049 100644 --- a/mod/content/overtake_probe.script.tl +++ b/mod/content/overtake_probe.script.tl @@ -1,10 +1,14 @@ --- 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. +-- 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. +-- ponytail: module-local timer, not GameScriptState; fine for a probe (state resets between calls anyway). local TAG = "[overtake_probe] " local INTERVAL = 5.0 +local MAX_GAP = 40.0 +local MAX_REBUILDS = 3 -- per save; counted in GameScriptState because module locals don't persist local elapsed = 0.0 local ticks = 0 @@ -25,86 +29,120 @@ local function turnaround(edge : EdgeId) : integer 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) +local record ProbeState + rebuilds : integer + watch : {integer} -- vehicles involved in the last rebuild, flagged in later dumps end -local function dump(dt : number) +-- Two-way = exactly two driving lanes (modes other than the sidewalk ones). 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 + local tn = api.engine.getComponent(seg, api.type.ComponentType.TRANSPORT_NETWORK) as Engine.Component.TransportNetwork + local driving = 0 + for _, e in ipairs(tn.edges) do + if e.speedLimit > 0 then driving = driving + 1 end + end + if driving ~= 2 then return false, "driving lanes=" .. driving end + return true, string.format("template=%s length=%.1f", tostring(be.roadTemplate), be.distance) +end + +local function rebuild(seg : integer) + local be = api.engine.getComponent(seg, api.type.ComponentType.BASE_EDGE) as Engine.Component.BaseEdge + local bes = api.engine.getComponent(seg, api.type.ComponentType.BASE_EDGE_STREET) as Engine.Component.BaseEdgeStreet + local owned = api.engine.getComponent(seg, api.type.ComponentType.PLAYER_OWNED) as Engine.Component.PlayerOwned + + local sae = api.type.SegmentAndEntity.new() + sae.entity = -1 + sae.type = 0 + sae.comp = be + sae.streetEdge = bes + if owned ~= nil then sae.playerOwned = owned end + + local proposal = api.type.SimpleProposal.new() + proposal.streetProposal.edgesToRemove[1] = seg + proposal.streetProposal.edgesToAdd[1] = sae + -- engine-state scripts can't pass a callback; the next dump shows the effect + api.cmd.sendCommand(api.cmd.makeWorldBuildProposalCmd(proposal, nil, true, false)) +end + +local function dump(dt : number, state : GameScriptState) + local st = state:get() + st.rebuilds = st.rebuilds 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 vehicles = api.engine.getEntitiesWithComponent(api.type.ComponentType.TRANSPORT_VEHICLE, { requireOwnedByPlayer = player }) - p(string.format("tick=%d dt=%s playerVehicles=%d", ticks, tostring(dt), #vehicles)) + p(string.format("tick=%d dt=%s playerVehicles=%d rebuilds=%d", ticks, tostring(dt), #vehicles, st.rebuilds)) - local bySegment : {integer : {string}} = {} local byLane : {string : {Car}} = {} + local laneSeg : {string : integer} = {} 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 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) + 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)), + 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 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 }) + laneSeg[key] = seg + table.insert(byLane[key], { v = v, pos = mp.dyn.pathPos.pos, speed = mp.dyn.speed, dir = dir }) 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 + + if st.rebuilds >= MAX_REBUILDS 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; the log shows whether that holds + -- 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) if i ~= j and ahead and math.abs(b.pos - a.pos) < MAX_GAP then - tryIgnore(a, b) + local seg = laneSeg[key] + 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 } + 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 -local ret : GameScript = { - update = function(_userParams : table, _state : GameScriptState
, dt : number) : nil +local ret : GameScript = { + update = function(_userParams : table, state : GameScriptState, 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) + xpcall(dump, function(err : any) p("ERROR " .. tostring(err)) end, dt, state) end, }