Compare commits

..
Author SHA1 Message Date
haylanandClaude Bot 1afd1f2e61 chore(prototype): log lane connections at prototype segment nodes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 00:14:12 +02:00
haylanandClaude Bot 6ed16fb1ab docs(research): third street run; lane connections and the crossing tool
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 00:09:40 +02:00
haylanandClaude Bot 3f14583ab4 chore(prototype): passing lanes at 20 m/s to make the normal lane preferred
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 00:03:10 +02:00
haylanandClaude Bot e807744bf7 chore(prototype): keep street variant B only; record second run
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 23:59:14 +02:00
haylanandClaude Bot 0424e66c6f chore(prototype): passing lanes BUS/TRUCK only; probe logs road template
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 23:47:05 +02:00
haylanandClaude Bot f7254a7709 chore(prototype): street types with overlaid passing lanes (variants A/B)
Copy of country_new_small with a 0.5 m passing lane per direction at the
centre line, offset +-2.5 into the oncoming lane. A and B use opposite
signs because the unit/sign of LaneConfig.offset is unknown.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 23:35:17 +02:00
haylanandClaude Bot 9623abb868 chore(prototype): remove GUI position flip; MovePath exchange refused on both threads
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 23:27:39 +02:00
4 changed files with 101 additions and 108 deletions

No files matched your search

+24
View File
@@ -84,3 +84,27 @@ Scanned: the 9 mods in `C:\Users\Public\mod.io\10640\mods\` (e.g. `devers_1`, `m
## Segment rebuild dropped (2026-10-05)
The human confirmed in game what the log showed: rebuilding the segment under a held-back vehicle does not get it past the blocker on a single-lane road. The rebuild code is removed from the probe (it's still in git history, last in commit 5d3a254). The lane split after rebuilds on multilane roads (see above) is left as an observation for the multilane ticket, not pursued here.
## Position flip via `MovePath` exchange on the GUI thread (run 2026-10-05 ~21:23 UTC)
- On the GUI thread (`guiUpdate`, with a callback), `api.cmd.sendCommand(api.cmd.debug.makeComponentExchangeCmd(follower, leaderMovePath), cb)` fails with the **same** `Error decoding argument #3: The lua object can't be cast to desired type`, and the callback is never reached. Debug mode was active.
- So the error is not about callbacks or threads: **the exchange refuses a `MovePath`**, consistent with its doc comment (`EmissionEmitter` or `ModelInstanceList` only).
- **Conclusion: no script API can write a road vehicle's position or route.** Moving a vehicle past another (jump, flip, teleport) is impossible for line vehicles. The only exception is `jumpToDepotEntity`, which can only target a depot.
- The probe retried every frame (5951 errors) because the error came before the counter was incremented. The experiment is removed, and the probe is logging-only again.
## Overlaid passing lanes (ticket "Can a street type with overlaid passing lanes fake overtaking on single-lane roads?")
- Prototype: `mod/content/streets/overtake_proto_{a,b}.street_template.lua`, copies of `country_new_small` with a 0.5 m passing lane per direction at the centre line, offset ±2.5 into the oncoming lane (A and B use opposite signs). A mod street template sits anywhere under `content/` and refers to base resources with `::/…`.
- First in-game run (human, 2026-10-05): the template loads, the lanes are visibly offset as intended, and vehicles can drive on the passing lanes. **Town (non-player) cars sometimes choose the passing lane.** **A faster bus behind a slower vehicle does not switch to the empty lane**: the switching logic is missing.
- Changes for the next run: passing lanes allow only `BUS`/`TRUCK` (keeps private `CAR` town traffic out), and the probe logs the road template per vehicle (`road=`), since lane indices 2/3 also mean ordinary lanes on `country_new_medium`.
- Hypothesis for the switch: the engine only re-chooses lanes at segment boundaries (`GetNextEdgeTrafficPenaltyAndAlternatives`), so short segments give a blocked bus more chances to pick the passing lane. Fallback trigger: `replaceSegment` ahead of the pair (made the leader re-plan lanes on multilane roads).
- Second run (2026-10-05 ~21:50 UTC, variant B, passing lanes BUS/TRUCK only). The human judged **variant B correct** (backward passing lane offset -2.5, forward +2.5), so variant A was removed. On the prototype road, lane 4 is the normal forward lane and lane 3 the forward passing lane.
- **An overtake happened**: fast 29251 (lane 4, 22.22 m/s) got past slow 29851 over segments 29707 → 30211 (side by side) → 30215 → 29708 → 29042 and stayed ahead. But the *slow* vehicle was in the passing lane (lane 3), and the passer went past in the normal lane.
- The engine treats both lanes as equal: some vehicles cruise in the passing lane almost the whole way (29796: 28/28 samples in lane 3, 27775: 29/29). Others mix (29251: 33 in lane 4, 14 in lane 3).
- Vehicles slowed to 9–10 m/s and briefly stopped on segment 27290 (presumably the end of the prototype stretch): a merge from two lanes into one.
- Visually the lane change is abrupt (human: "the lanes need a smoother transition"): the switch happens at the node, which has almost zero length on a straight, and the jump is about 5–6 m sideways.
- Next ideas: a lower speed limit on the passing lane (prefer the normal lane unless it's blocked), a smaller offset or transition segments for a smoother change.
- Third run setup: passing lanes at 20 m/s (normal lanes 22.22 m/s), everything else unchanged. Question: does a lower limit keep vehicles in the normal lane and send them to the passing lane only when the normal lane is blocked?
- Third run (20 m/s passing lanes): **no change.** Slow buses still sat in the left passing lane (29849 and 29851: 29/29 samples in lane 3); fast 29251 split 8/11. The engine doesn't pick lanes by speed limit (the slow buses top out at 16.67 < 20 anyway). Observed pattern: a vehicle gets a lane when it enters the stretch and keeps it; it doesn't move over when blocked; passes happen only when fast and slow happen to be in different lanes.
- Lane connections are editable: `BaseNodeConfig.laneConnections` (`LaneConnection{segment0, lane0, segment1, lane1, withRoad, withTram}`, lanes zero-based left-to-right seen from the node) plus `userModifiedLaneConnections`, scriptable via `SimpleStreetProposal.nodeConfigsToAdd`. The game also has a native **crossing tool** ("allows to change the road and tram lanes at crossings", `UI::LaneModifier` in the executable). Next test (option a, by hand): at the stretch's entry junction, connect the normal road only to the right lane, to see whether entry control works and whether a blocked fast bus moves left inside the stretch.
- Fourth run (after the human's crossing-tool edits): **entry control not effective.** Slow buses 29849, 29851 and 29852 still entered prototype stretches (segments 21304, 29012) in the left lane 3 and stayed there (12–14/12–14 samples). 29850, 29848 and the fast 29251 entered in lane 4. No vehicle switched lanes inside a stretch. It's unknown whether or where the edits were saved, so the probe now logs the lane connections (`NODE … userModified= conns=`) at both nodes of every occupied prototype segment.
-3
View File
@@ -4,8 +4,5 @@ function data()
updateScript = {
fileName = "overtake_probe.script@update",
},
guiUpdateScript = {
fileName = "overtake_probe.script@guiUpdate",
},
}
end
+34 -105
View File
@@ -1,14 +1,11 @@
-- 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
-- PROTOTYPE: throwaway, lives only on branch prototype/vehicle-probe.
-- Every INTERVAL it logs every player-owned road vehicle (segment, lane, position, speed).
-- All experiments (timeToIgnore, jump, set-line, segment rebuild, GUI position flip) 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
@@ -29,18 +26,38 @@ local function turnaround(edge : EdgeId) : integer
return tn.turnaroundEdges[edge.index + 1]
end
local record Car
v : integer
pos : number
pos01 : number
speed : number
dir : boolean
-- short street template name of a segment, so passing-lane use on the prototype road can be told apart
local function roadOf(seg : integer) : string
local be = api.engine.getComponent(seg, api.type.ComponentType.BASE_EDGE) as Engine.Component.BaseEdge
if be == nil then return "?" end
return (tostring(be.roadTemplate):match("([^/]+)%.street_template$")) or tostring(be.roadTemplate)
end
-- lane connections at both nodes of a segment, to check whether crossing-tool edits exist where vehicles enter
local function nodeLog(seg : integer)
local be = api.engine.getComponent(seg, api.type.ComponentType.BASE_EDGE) as Engine.Component.BaseEdge
if be == nil then return end
for _, node in ipairs({ be.node0, be.node1 }) do
local cfg = api.engine.getComponent(node, api.type.ComponentType.BASE_NODE_CONFIG) as Engine.Component.BaseNodeConfig
if cfg == nil then
p(string.format("NODE seg=%d node=%d no BaseNodeConfig (default connections)", seg, node))
else
local conns : {string} = {}
for _, lc in ipairs(cfg.laneConnections) do
if lc.segment0 == seg or lc.segment1 == seg then
table.insert(conns, string.format("%d:%d->%d:%d", lc.segment0, lc.lane0, lc.segment1, lc.lane1))
end
end
p(string.format("NODE seg=%d node=%d userModified=%s conns=%s", seg, node, tostring(cfg.userModifiedLaneConnections), table.concat(conns, " ")))
end
end
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))
local protoSegs : {integer : boolean} = {}
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
@@ -48,102 +65,17 @@ local function dump(dt : number)
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)),
"veh=%d road=%s 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 roadOf(edge.entity) or "-", 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))
if edge ~= nil and roadOf(edge.entity) == "overtake_proto_b" then protoSegs[edge.entity] = true end
end
end
for seg, _ in pairs(protoSegs) do nodeLog(seg) 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> = {
local ret : GameScript<table> = {
update = function(_userParams : table, _state : GameScriptState<table>, dt : number) : nil
ticks = ticks + 1
elapsed = elapsed + dt
@@ -152,9 +84,6 @@ local ret : GameScriptWithGui<table, GuiState> = {
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
@@ -0,0 +1,43 @@
-- PROTOTYPE (ticket "Can a street type with overlaid passing lanes fake overtaking on single-lane roads?"):
-- throwaway, lives only on branch prototype/vehicle-probe.
-- Looks like country_new_small (same style, one visible lane per direction), but each direction gets a narrow
-- extra passing lane at the centre line, pushed by `offset` over the oncoming lane. Passing lanes run at 20 m/s
-- (normal lanes 22.22) to test whether a lower limit keeps vehicles in the normal lane unless it's blocked.
-- Offsets B -2.5 / F +2.5 put the passing lanes over the oncoming lanes (variant B; the opposite signs, variant A,
-- were wrong and are removed).
function data()
return {
availability = { yearFrom = 0, yearTo = 0 },
cost = 180,
country = true,
defaultWithCrosswalk = false,
description = {
description = "PROTOTYPE overtaking road, variant B: country small road with overlaid passing lanes (offset B -2.5 / F +2.5).",
icon = "::/infrastructure/street/country/country_new_small.tga",
name = "Overtake proto B (passing lanes B -2.5 / F +2.5)",
previewIcon = "::/infrastructure/street/country/country_new_small_preview.tga",
},
emissions = { noise = 0, pollution = 0, pollutionRadius = 0, radius = 0 },
fillGroundTex = "::/infrastructure/street/shared/street_fill.gtex",
laneConfigs = {
{ forward = false, height = 0, offset = -1, speed = 22.22, transportModes = { "PERSON", "CARGO" }, width = 2 },
{ forward = false, height = 0, offset = -0.5, speed = 22.22, transportModes = { "CAR", "BUS", "TRUCK" }, width = 5 },
-- backward passing lane, meant to sit over the forward driving lane (BUS/TRUCK only: keeps town cars out)
{ forward = false, height = 0, offset = -2.5, speed = 20, transportModes = { "BUS", "TRUCK" }, width = 0.5 },
-- forward passing lane, meant to sit over the backward (oncoming) driving lane (BUS/TRUCK only)
{ forward = true, height = 0, offset = 2.5, speed = 20, transportModes = { "BUS", "TRUCK" }, width = 0.5 },
{ forward = true, height = 0, offset = 0.5, speed = 22.22, transportModes = { "CAR", "BUS", "TRUCK" }, width = 5 },
{ forward = true, height = 0, offset = 1, speed = 22.22, transportModes = { "PERSON", "CARGO" }, width = 2 },
},
maintenanceCost = 30,
menuCategory = { categories = { { category = "roads_country", filterCategories = { }, order = 442 } } },
modifiers = { maxSpeedModifier = 0, noiseModifier = 0, pollutionModifier = 0 },
pedestrianWalkPenalty = true,
priority = 200,
roadType = "STREET",
sidewalkFillGroundTex = "::/infrastructure/shared/none.gtex",
simBuildable = false, -- towns must not build the prototype
streetStyle = "::/infrastructure/street/country/country_new_small.street",
builderAudioRes = "::/gui/construction/sound/buildoze_street.builder_audio",
}
end