Files
tpf3-overtake/mod/content/overtake_probe.script.tl
T

154 lines
6.6 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.
-- 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 = 2.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
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
speed : number
dir : boolean
end
local record ProbeState
rebuilds : integer
watch : {integer} -- vehicles involved in the last rebuild, flagged in later dumps
end
-- 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
-- count BaseEdge lanes, not TransportNetwork edges (town roads split each lane into several edges).
-- Sidewalks carry modes 0/1 only; driving lanes carry 2+ (observed: [2,3,4]).
local driving = 0
for _, lc in ipairs(be.laneConfigs) do
for k, on in pairs(lc.transportModes) do
if on and (tonumber(tostring(k)) or 0) >= 2 then driving = driving + 1 break end
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<ProbeState>)
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 rebuilds=%d", ticks, tostring(dt), #vehicles, st.rebuilds))
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 seg = edge and edge.entity or -1
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 {}
laneSeg[key] = seg
table.insert(byLane[key], { v = v, pos = mp.dyn.pathPos.pos, speed = mp.dyn.speed, dir = dir })
end
end
end
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 (held so far in the logs)
local ahead = (a.dir and b.pos > a.pos) or (not a.dir and b.pos < a.pos)
-- leader must be moving, otherwise it's a queue at a stop or junction, not a slow vehicle
if i ~= j and ahead and math.abs(b.pos - a.pos) < MAX_GAP and b.speed > 1.0 then
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<ProbeState> = {
update = function(_userParams : table, state : GameScriptState<ProbeState>, 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, state)
end,
}
return ret