-- PROTOTYPE (ticket "Can a script swap the positions of a follower and its leader?"): throwaway, -- lives only on branch prototype/vehicle-probe. -- 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 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 local record Car v : integer pos : number pos01 : number speed : number dir : boolean 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)) 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) p(string.format( "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)) end end end -- EXPERIMENT (GUI thread, where sendCommand callbacks work): flip the positions of a follower and its leader by -- exchanging their MovePath components (route + position) with api.cmd.debug.makeComponentExchangeCmd. local MAX_FLIPS = 2 local FLIP_SCAN_FRAMES = 60 -- scan for a pair about once per second local FLIP_TRACE_FRAMES = 600 -- then log both vehicles every 10 frames for ~10 s local record GuiState frames : integer flips : integer traceLeft : integer a : integer b : integer end local function movePathOf(v : integer) : Engine.Component.MovePath return api.engine.getComponent(v, api.type.ComponentType.MOVE_PATH) as Engine.Component.MovePath end local function flipLine(v : integer) : string local mp = movePathOf(v) if mp == nil then return "veh=" .. v .. " no MovePath" end local edge, _dir = edgeAt(mp) return string.format("veh=%d seg=%s lane=%s pos=%.1f v=%.2f", v, tostring(edge and edge.entity), tostring(edge and edge.index), mp.dyn.pathPos.pos, mp.dyn.speed) end local function sendFlip(gs : GuiState, follower : integer, leader : integer) local mpF, mpL = movePathOf(follower), movePathOf(leader) p("FLIP_TEST before " .. flipLine(follower) .. " | " .. flipLine(leader)) local function done(label : string) : function(any, boolean) return function(res : any, success : boolean) p(string.format("FLIP_TEST result %s success=%s res=%s", label, tostring(success), tostring(res))) end end api.cmd.sendCommand(api.cmd.debug.makeComponentExchangeCmd(follower, mpL as Engine.Component), done("follower<-leader")) api.cmd.sendCommand(api.cmd.debug.makeComponentExchangeCmd(leader, mpF as Engine.Component), done("leader<-follower")) gs.flips = gs.flips + 1 gs.traceLeft = FLIP_TRACE_FRAMES gs.a, gs.b = follower, leader end -- same pair rule as the engine-side probe: same segment + lane, follower < MAX_GAP behind a moving leader local function findPair() : integer, integer local player = api.engine.util.getPlayer() local byLane : {string : {Car}} = {} for _, v in ipairs(api.engine.getEntitiesWithComponent(api.type.ComponentType.TRANSPORT_VEHICLE, { requireOwnedByPlayer = player })) do local lv = api.engine.getComponent(v, api.type.ComponentType.LAND_VEHICLE) as Engine.Component.LandVehicle local mp = movePathOf(v) if lv ~= nil and not lv.isTram and mp ~= nil then local edge, dir = edgeAt(mp) if edge ~= nil then local key = edge.entity .. ":" .. edge.index byLane[key] = byLane[key] or {} 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 for _, cars in pairs(byLane) do for i = 1, #cars do for j = 1, #cars do local a, b = cars[i], cars[j] 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 and b.speed > 1.0 then return a.v, b.v end end end end return nil, nil end local function guiStep(gsState : GameScriptState) local gs = gsState:get() gs.frames = (gs.frames or 0) + 1 gs.flips = gs.flips or 0 gs.traceLeft = gs.traceLeft or 0 if gs.traceLeft > 0 then if gs.traceLeft % 10 == 0 then p(string.format("FLIP_TRACE f=%d %s | %s", FLIP_TRACE_FRAMES - gs.traceLeft, flipLine(gs.a), flipLine(gs.b))) end gs.traceLeft = gs.traceLeft - 1 elseif gs.flips < MAX_FLIPS and gs.frames % FLIP_SCAN_FRAMES == 0 then local follower, leader = findPair() if follower ~= nil then sendFlip(gs, follower, leader) end end gsState:set(gs) end local ret : GameScriptWithGui = { 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) end, guiUpdate = function(_userParams : table, _state : GameScriptStateReadOnly
, guiState : GameScriptState) xpcall(guiStep, function(err : any) p("ERROR gui " .. tostring(err)) end, guiState) end, } return ret