diff --git a/mod/content/overtake_probe.gs.lua b/mod/content/overtake_probe.gs.lua index 1b9d2c5..b040d11 100644 --- a/mod/content/overtake_probe.gs.lua +++ b/mod/content/overtake_probe.gs.lua @@ -4,5 +4,8 @@ function data() updateScript = { fileName = "overtake_probe.script@update", }, + guiUpdateScript = { + fileName = "overtake_probe.script@guiUpdate", + }, } end diff --git a/mod/content/overtake_probe.script.tl b/mod/content/overtake_probe.script.tl index 455a655..1afc6c9 100644 --- a/mod/content/overtake_probe.script.tl +++ b/mod/content/overtake_probe.script.tl @@ -8,7 +8,7 @@ local TAG = "[overtake_probe] " local INTERVAL = 2.0 local MAX_GAP = 40.0 -local MAX_REBUILDS = 3 -- hand-built proposals were silently rejected; now via api.engine.util.proposal.replaceSegment +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 @@ -207,7 +207,94 @@ local function trace(state : GameScriptState) state:set(st) end -local ret : GameScript = { +-- 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 @@ -217,6 +304,9 @@ local ret : GameScript = { elapsed = 0.0 xpcall(dump, function(err : any) p("ERROR " .. tostring(err)) end, dt, state) end, + guiUpdate = function(_userParams : table, _state : GameScriptStateReadOnly, guiState : GameScriptState) + xpcall(guiStep, function(err : any) p("ERROR gui " .. tostring(err)) end, guiState) + end, } return ret