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>
161 lines
6.8 KiB
Plaintext
161 lines
6.8 KiB
Plaintext
-- 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.
|
|
-- 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<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<table, GuiState> = {
|
|
update = function(_userParams : table, _state : GameScriptState<table>, 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<table>, guiState : GameScriptState<GuiState>)
|
|
xpcall(guiStep, function(err : any) p("ERROR gui " .. tostring(err)) end, guiState)
|
|
end,
|
|
}
|
|
|
|
return ret
|