Compare commits

..
6 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
3 changed files with 92 additions and 2 deletions

No files matched your search

+17
View File
@@ -91,3 +91,20 @@ The human confirmed in game what the log showed: rebuilding the segment under a
- So the error is not about callbacks or threads: **the exchange refuses a `MovePath`**, consistent with its doc comment (`EmissionEmitter` or `ModelInstanceList` only). - 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. - **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. - 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.
+32 -2
View File
@@ -26,10 +26,38 @@ local function turnaround(edge : EdgeId) : integer
return tn.turnaroundEdges[edge.index + 1] return tn.turnaroundEdges[edge.index + 1]
end end
-- 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 function dump(dt : number)
local player = api.engine.util.getPlayer() local player = api.engine.util.getPlayer()
local vehicles = api.engine.getEntitiesWithComponent(api.type.ComponentType.TRANSPORT_VEHICLE, { requireOwnedByPlayer = player }) 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)) p(string.format("tick=%d dt=%s playerVehicles=%d", ticks, tostring(dt), #vehicles))
local protoSegs : {integer : boolean} = {}
for _, v in ipairs(vehicles) do for _, v in ipairs(vehicles) do
local lv = api.engine.getComponent(v, api.type.ComponentType.LAND_VEHICLE) as Engine.Component.LandVehicle 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 mp = api.engine.getComponent(v, api.type.ComponentType.MOVE_PATH) as Engine.Component.MovePath
@@ -37,12 +65,14 @@ local function dump(dt : number)
if lv ~= nil and not lv.isTram and mp ~= nil then if lv ~= nil and not lv.isTram and mp ~= nil then
local edge, dir = edgeAt(mp) local edge, dir = edgeAt(mp)
p(string.format( 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", "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 edge.entity or -1, tostring(edge and edge.index), tostring(dir), tostring(edge and turnaround(edge)), 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), 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)) 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
end end
for seg, _ in pairs(protoSegs) do nodeLog(seg) end
end end
local ret : GameScript<table> = { local ret : GameScript<table> = {
@@ -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