chore(prototype): flip follower/leader positions via MovePath exchange on GUI thread

Rebuild switched off for this run. Callbacks log success per exchange.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
haylanandClaude Bot committed 2026-10-05 23:22:58 +02:00
1 parent 90829ee9b9
commit 5d3a254e55
2 files changed
+95 -2

No files matched your search

+3
View File
@@ -4,5 +4,8 @@ function data()
updateScript = {
fileName = "overtake_probe.script@update",
},
guiUpdateScript = {
fileName = "overtake_probe.script@guiUpdate",
},
}
end
+92 -2
View File
@@ -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<ProbeState>)
state:set(st)
end
local ret : GameScript<ProbeState> = {
-- 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<GuiState>)
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<ProbeState, GuiState> = {
update = function(_userParams : table, state : GameScriptState<ProbeState>, dt : number) : nil
ticks = ticks + 1
elapsed = elapsed + dt
@@ -217,6 +304,9 @@ local ret : GameScript<ProbeState> = {
elapsed = 0.0
xpcall(dump, function(err : any) p("ERROR " .. tostring(err)) end, dt, state)
end,
guiUpdate = function(_userParams : table, _state : GameScriptStateReadOnly<ProbeState>, guiState : GameScriptState<GuiState>)
xpcall(guiStep, function(err : any) p("ERROR gui " .. tostring(err)) end, guiState)
end,
}
return ret