Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1afd1f2e61 | ||
|
|
6ed16fb1ab | ||
|
|
3f14583ab4 | ||
|
|
e807744bf7 | ||
|
|
0424e66c6f | ||
|
|
f7254a7709 | ||
|
|
9623abb868 |
No files matched your search
@@ -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.
|
||||
@@ -4,8 +4,5 @@ function data()
|
||||
updateScript = {
|
||||
fileName = "overtake_probe.script@update",
|
||||
},
|
||||
guiUpdateScript = {
|
||||
fileName = "overtake_probe.script@guiUpdate",
|
||||
},
|
||||
}
|
||||
end
|
||||
@@ -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
|
||||
Reference in new issue
Block a user