Compare commits
27
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9623abb868 | ||
|
|
0465a93e04 | ||
|
|
bfe5a22d1d | ||
|
|
5d3a254e55 | ||
|
|
90829ee9b9 | ||
|
|
d4fab92fd4 | ||
|
|
949462566e | ||
|
|
39f2c63667 | ||
|
|
e77fabb772 | ||
|
|
ea23aa1dc5 | ||
|
|
69b94552c2 | ||
|
|
2a08067a6d | ||
|
|
dcc4b4afae | ||
|
|
0e76dc453a | ||
|
|
b53ed477f3 | ||
|
|
f9bc0e79fc | ||
|
|
58fe0d2986 | ||
|
|
b6daeaa328 | ||
|
|
aa5dfa655f | ||
|
|
eaf6553cae | ||
|
|
c936946eb4 | ||
|
|
9edc9c065a | ||
|
|
0ef87ebab7 | ||
|
|
86a48e75c9 | ||
|
|
94ea772fa8 | ||
|
|
035adb5762 | ||
|
|
8e145a2870 |
No files matched your search
@@ -0,0 +1,2 @@
|
|||||||
|
tlconfig.lua
|
||||||
|
.claude/settings.local.json
|
||||||
Vendored
+5
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"recommendations": [
|
||||||
|
"pdesaulniers.vscode-teal"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
## Agent skills
|
||||||
|
|
||||||
|
### Issue tracker
|
||||||
|
|
||||||
|
Issues live in Gitea (`git.arthurerlich.de`), driven with the `tea` CLI. See `docs/agents/issue-tracker.md`.
|
||||||
|
|
||||||
|
### Triage labels
|
||||||
|
|
||||||
|
Default five-role vocabulary (`needs-triage`, `needs-info`, `ready-for-agent`, `ready-for-human`, `wontfix`). See `docs/agents/triage-labels.md`.
|
||||||
|
|
||||||
|
### Domain docs
|
||||||
|
|
||||||
|
Single-context: one `CONTEXT.md` + `docs/adr/` at the repo root. See `docs/agents/domain.md`.
|
||||||
|
|
||||||
|
## Modding
|
||||||
|
|
||||||
|
- The mod is `mod/` (junctioned into the game's `staging_area` as `haylan_tf3_overtake`). Scripts are Teal (`.tl`, typed Lua); `mod.json` goes at the root of `mod/`.
|
||||||
|
- Type-check with `tl check mod/<file>.tl` (run from the repo root; it reads `tlconfig.lua`). `tl` is type-check only; whether the game compiles `.tl` itself is undocumented.
|
||||||
|
- `tlconfig.lua` and `.claude/settings.local.json` are machine-local and gitignored. Copy `tlconfig.template.lua` to `tlconfig.lua` and replace `<tf3_install_folder>` with the game's install path (here `D:/SteamLibrary/steamapps/common/Transport Fever 3/`).
|
||||||
|
- Authoritative API types: `<install>/api/tealdef` and `<install>/base/tealdef`. Read them, and the offline docs in `docs/reference/`, before guessing an API. Research notes are in `docs/research/`; vocabulary in `CONTEXT.md`.
|
||||||
+27
@@ -0,0 +1,27 @@
|
|||||||
|
# TpF3 Overtake
|
||||||
|
|
||||||
|
A Transport Fever 3 script-mod that lets road vehicles pass slower vehicles instead of queueing behind them.
|
||||||
|
|
||||||
|
## Language
|
||||||
|
|
||||||
|
**Overtaking**:
|
||||||
|
A faster vehicle getting past a slower one ahead of it on the same road, as seen by the player. It may be a visual illusion (the passer drives through the blocker) rather than a real lane change.
|
||||||
|
_Avoid_: Passing, bypass
|
||||||
|
|
||||||
|
**Blocker**:
|
||||||
|
The slower vehicle ahead that the passer would otherwise queue behind.
|
||||||
|
_Avoid_: Obstacle, lead vehicle
|
||||||
|
|
||||||
|
**Passer**:
|
||||||
|
The faster vehicle that overtakes the blocker.
|
||||||
|
_Avoid_: Overtaker
|
||||||
|
|
||||||
|
**Player-owned vehicle**:
|
||||||
|
A vehicle belonging to one of the player's lines. Only these are in scope for overtaking.
|
||||||
|
_Avoid_: AI vehicle, ambient traffic
|
||||||
|
|
||||||
|
**Same-direction multilane road**:
|
||||||
|
A road with two or more lanes in the passer's direction, where overtaking needs no oncoming-traffic check.
|
||||||
|
|
||||||
|
**Two-way road**:
|
||||||
|
A road with one lane per direction, where overtaking is only safe if no oncoming vehicle is in the way.
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
require "api_def"
|
||||||
|
require "content_def"
|
||||||
|
|
||||||
|
-- list your mods here, if you provide your own d.tl definitions
|
||||||
|
-- require "example_mod_1"
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
# Domain Docs
|
||||||
|
|
||||||
|
How the engineering skills should consume this repo's domain documentation when exploring the codebase.
|
||||||
|
|
||||||
|
## Before exploring, read these
|
||||||
|
|
||||||
|
- **`CONTEXT.md`** at the repo root, or
|
||||||
|
- **`CONTEXT-MAP.md`** at the repo root if it exists — it points at one `CONTEXT.md` per context. Read each one relevant to the topic.
|
||||||
|
- **`docs/adr/`** — read ADRs that touch the area you're about to work in. In multi-context repos, also check `src/<context>/docs/adr/` for context-scoped decisions.
|
||||||
|
|
||||||
|
If any of these files don't exist, **proceed silently**. Don't flag their absence; don't suggest creating them upfront. The `/domain-modeling` skill (reached via `/grill-with-docs` and `/improve-codebase-architecture`) creates them lazily when terms or decisions actually get resolved.
|
||||||
|
|
||||||
|
## File structure
|
||||||
|
|
||||||
|
Single-context repo (most repos):
|
||||||
|
|
||||||
|
```
|
||||||
|
/
|
||||||
|
├── CONTEXT.md
|
||||||
|
├── docs/adr/
|
||||||
|
│ ├── 0001-event-sourced-orders.md
|
||||||
|
│ └── 0002-postgres-for-write-model.md
|
||||||
|
└── src/
|
||||||
|
```
|
||||||
|
|
||||||
|
Multi-context repo (presence of `CONTEXT-MAP.md` at the root):
|
||||||
|
|
||||||
|
```
|
||||||
|
/
|
||||||
|
├── CONTEXT-MAP.md
|
||||||
|
├── docs/adr/ ← system-wide decisions
|
||||||
|
└── src/
|
||||||
|
├── ordering/
|
||||||
|
│ ├── CONTEXT.md
|
||||||
|
│ └── docs/adr/ ← context-specific decisions
|
||||||
|
└── billing/
|
||||||
|
├── CONTEXT.md
|
||||||
|
└── docs/adr/
|
||||||
|
```
|
||||||
|
|
||||||
|
## Use the glossary's vocabulary
|
||||||
|
|
||||||
|
When your output names a domain concept (in an issue title, a refactor proposal, a hypothesis, a test name), use the term as defined in `CONTEXT.md`. Don't drift to synonyms the glossary explicitly avoids.
|
||||||
|
|
||||||
|
If the concept you need isn't in the glossary yet, that's a signal — either you're inventing language the project doesn't use (reconsider) or there's a real gap (note it for `/domain-modeling`).
|
||||||
|
|
||||||
|
## Flag ADR conflicts
|
||||||
|
|
||||||
|
If your output contradicts an existing ADR, surface it explicitly rather than silently overriding:
|
||||||
|
|
||||||
|
> _Contradicts ADR-0007 (event-sourced orders) — but worth reopening because…_
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
# Issue tracker: Gitea
|
||||||
|
|
||||||
|
Issues and specs for this repo live as Gitea issues on `git.arthurerlich.de` (repo `haylan/tpf3-overtake`). Use the `tea` CLI for all operations (see the `gitea-tea` skill).
|
||||||
|
|
||||||
|
## Conventions
|
||||||
|
|
||||||
|
- **Create an issue**: `tea issues create --title "..." --description "..."`. Use a heredoc for multi-line bodies.
|
||||||
|
- **Read an issue**: `tea issues <number> --comments`
|
||||||
|
- **List issues**: `tea issues list --state open --fields index,title,body,labels` with `--labels` filters as needed.
|
||||||
|
- **Comment on an issue**: `tea comment <number> "..."`
|
||||||
|
- **Apply / remove labels**: `tea issues edit <number> --add-labels "..."` / `--remove-labels "..."`
|
||||||
|
- **Close**: `tea issues close <number>` (comment first with `tea comment`)
|
||||||
|
|
||||||
|
Infer the repo from `git remote -v` — `tea` does this inside a clone.
|
||||||
|
|
||||||
|
## Pull requests as a triage surface
|
||||||
|
|
||||||
|
**PRs as a request surface: no.**
|
||||||
|
|
||||||
|
## When a skill says "publish to the issue tracker"
|
||||||
|
|
||||||
|
Create a Gitea issue.
|
||||||
|
|
||||||
|
## When a skill says "fetch the relevant ticket"
|
||||||
|
|
||||||
|
Run `tea issues <number> --comments`.
|
||||||
|
|
||||||
|
## Wayfinding operations
|
||||||
|
|
||||||
|
Used by `/wayfinder`. The **map** is a single issue labelled `wayfinder:map`; **child** tickets are issues labelled `wayfinder:<type>` (`research`/`prototype`/`grilling`/`task`) with `Part of #<map>` at the top of the body and listed in the map's task list.
|
||||||
|
|
||||||
|
- **Blocking**: a `Blocked by: #<n>, #<n>` line at the top of the child body. Unblocked when every blocker is closed.
|
||||||
|
- **Frontier query**: open children of the map, minus any with an open blocker or an assignee; first in map order wins.
|
||||||
|
- **Claim**: assign the issue to yourself (`tea issues edit <n> --add-assignees <user>`).
|
||||||
|
- **Resolve**: comment the answer, close the issue, then append a pointer to the map's Decisions-so-far.
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
# Triage Labels
|
||||||
|
|
||||||
|
The skills speak in terms of five canonical triage roles. This file maps those roles to the actual label strings used in this repo's issue tracker.
|
||||||
|
|
||||||
|
| Label in mattpocock/skills | Label in our tracker | Meaning |
|
||||||
|
| -------------------------- | -------------------- | ---------------------------------------- |
|
||||||
|
| `needs-triage` | `needs-triage` | Maintainer needs to evaluate this issue |
|
||||||
|
| `needs-info` | `needs-info` | Waiting on reporter for more information |
|
||||||
|
| `ready-for-agent` | `ready-for-agent` | Fully specified, ready for an AFK agent |
|
||||||
|
| `ready-for-human` | `ready-for-human` | Requires human implementation |
|
||||||
|
| `wontfix` | `wontfix` | Will not be actioned |
|
||||||
|
|
||||||
|
When a skill mentions a role (e.g. "apply the AFK-ready triage label"), use the corresponding label string from this table.
|
||||||
|
|
||||||
|
Edit the right-hand column to match whatever vocabulary you actually use.
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
# mod.io Terms: what applies to a published mod
|
||||||
|
|
||||||
|
Ad-hoc research 2026-10-05 (no ticket — reference material for the map).
|
||||||
|
Fetched from the no-chrome legal routes: `https://mod.io/legal/terms?no_links=true` (Terms of Use) and `https://mod.io/legal/aup?no_links=true` (Acceptable Use Policy). `mod.io/terms` itself is a JS app; the same text is also available via `GET /agreements/types/:id/current` (1 = Terms of Use, 3 = Game Terms, 4 = API Access Terms) with an API key — not needed for offline reading.
|
||||||
|
|
||||||
|
## Why this matters
|
||||||
|
|
||||||
|
TpF3 has **no Steam Workshop**: the official in-game mod browser is mod.io (see `docs/reference/wiki/modding_general_publishing.md`). The map's old "Steam Workshop publishing" out-of-scope line has been corrected to mod.io. This note is the mod.io side of that story; the game-side rules (free, non-commercial, no offensive content, monetization) are already captured in the publishing wiki doc.
|
||||||
|
|
||||||
|
## Key clauses (Terms of Use, by number)
|
||||||
|
|
||||||
|
- **§1.5** The Acceptable Use Policy is incorporated into the ToU; breaching the AUP is breaching the ToU.
|
||||||
|
- **§1.7 Priority order** (first wins on conflict): Game Terms > game's AUP > mod.io AUP > ToU clauses 15/16 (EU/AU consumers) > Monetization Terms > ToU. So **Urban Games' own rules override mod.io's**.
|
||||||
|
- **§1.9** mod.io may change the ToU; 30 days after publication the new terms bind you.
|
||||||
|
- **§6.2 UGC license** — by uploading a mod you grant mod.io **and the Relevant Game Admin (Urban Games)** a transferable, sublicensable, royalty-free, **irrevocable, non-exclusive, perpetual worldwide** license to use, reproduce, modify, make derivative works of, and distribute the mod "for the sole purpose of providing the Services". Optional *Enhanced License Rights* (sale/resale) only if separately opted in via the UGC Control Panel, and those are revocable.
|
||||||
|
- **§6.3 warranty** — you warrant the UGC is yours or you have the right to distribute it, and does not violate others' rights. This is what makes the wiki's "no content you don't have copyright or permission to distribute" enforceable.
|
||||||
|
- **§8 Copyright** — DMCA-style process; mod.io (or the Game Admin) may delete/disable content alleged to infringe.
|
||||||
|
- **§11 Termination** — mod.io may terminate/suspend the account immediately for material or repeated breach; and the Game Admin may **restrict access to the game's mod services at any time, without notice**.
|
||||||
|
- **AUP** — standard prohibitions: nothing illegal, no spam/malware, no harassment/defamation, no probing or unauthorised access to the service, no scraping of data you aren't meant to get.
|
||||||
|
|
||||||
|
## Implications for this mod
|
||||||
|
|
||||||
|
- Free and non-commercial is the game-side rule anyway; nothing new here.
|
||||||
|
- Uploading = a perpetual irrevocable license to mod.io + Urban Games. Fine for a code-only behavioural mod; just know it before publishing anything with third-party assets (see also the base-game-file reuse rules in the publishing wiki doc).
|
||||||
|
- The mod can be pulled or access restricted at Urban Games' discretion at any time — no recourse beyond the ToU's notice process.
|
||||||
|
- Nothing in the ToU/AUP touches the technical side of the mod (script API, staging area, etc.); it only governs distribution and conduct.
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
# Can TpF3 scripting make cars overtake?
|
||||||
|
|
||||||
|
Issue #2 (part of map #1). Researched 2026-10-04.
|
||||||
|
|
||||||
|
**Verdict: not feasible with the documented API.** Scripts can observe vehicles and scale a vehicle's top speed, but nothing documented lets a script change a road vehicle's lane, path position or per-tick movement. Overtaking would need engine support that the docs do not show.
|
||||||
|
|
||||||
|
## Sources
|
||||||
|
|
||||||
|
- Wiki, modifiers/filters: https://wiki.transportfever3.com/doku.php?id=modding:scripting:modifiersfilters
|
||||||
|
- Wiki, base config: https://wiki.transportfever3.com/doku.php?id=modding:scripting:baseconfig
|
||||||
|
- Script-doc index: https://wiki.transportfever3.com/script-doc/ (pages `api/engine.html`, `api/engine/system.html`, `api/cmd.html`, `content/scripts/gamescript.html`)
|
||||||
|
- Local example mod: `.examples/ug_tf3_example_config_change` (uses `preRunFn` to set `baseConfig.trainAccelerationFactor`)
|
||||||
|
|
||||||
|
## Findings
|
||||||
|
|
||||||
|
1. **The "modifiers/filters" page is about resource loading, not runtime behaviour.** `addModifier(category, fn)` edits resource data (`fileName`, `data`) as it loads, and `addFileFilter` toggles resource visibility. Vehicle-related categories are only `loadModule`, `loadMultipleUnit`, plus the `model/vehicle` and `model/car` filters. They cannot touch live vehicle movement. The page says it "is not yet adapted for Transport Fever 3", so it may be outdated.
|
||||||
|
2. **BaseConfig has no road-vehicle or traffic-behaviour fields.** The vehicle-related ones are `trainAccelerationFactor`, `trainBrakeDeceleration`, `simulateCargoWeight`, `loadspeedPenaltyForTrainOverlength` and `maxVehicleCloneAmount`. All are global and train-oriented. This runs once in `preRunFn`.
|
||||||
|
3. **Game script phases.** `gamescript.html` defines `update`, `postUpdate` and `handleEvent`, which run on the engine state with mutable script state, and `guiUpdate` and `guiHandleEvent` for the GUI. "Mutable" here means the script's own state, not engine entities. `api.engine` is documented as "for reading the game engine state". So per-tick script code can read the world but only change it through `api.cmd` commands.
|
||||||
|
4. **Read access to vehicles exists but is thin.** `api.engine.system.landVehicleMoveSystem` exposes `getVehicleInfo(vehicleEntity)`, whose `LandVehicleMoveInfo` return type is not expanded in the docs. It also has `getBlockedTrains`, `isTrainWaitingForFreePath` and `forEach` (train oriented). Components include `Engine.Component.MovePath` (`path`, `dyn.pathPos`, `dyn.speed`, `dyn.accel`, `blocked`, `occupiedFrontIndex`, `occupiedBackIndex`) and `Engine.Component.LandVehicle` (`isTram`, `dyn.brakingTimer`, `turnLeft`, `turnRight`). `BaseEdge` carries `laneConfig` and `BaseNodeConfig.laneConnections`.
|
||||||
|
5. **Write access is limited to commands.** The relevant `api.cmd` entries are `makeVehicleSetModifiersCmd(vehicle, TransportVehicle.Modifiers)`, `makeVehicleSetStoppedByUserCmd`, `makeVehicleSendToDepotCmd`, `makeVehicleReverseCmd`, `makeVehicleSetLineCmd` and `makeVehicleTryToDepartCmd`. There is no command to set a `MovePath`, speed, lane or position on a land vehicle. `makeComponentExchangeCmd` is marked debug and its doc says it accepts only `EmissionEmitter` or `ModelInstanceList`. `makeCustomVehicleCreateOrUpdateCmd` takes a `Carrier` and an optional `movePathAircraft`, so it is aimed at aircraft or custom vehicles rather than road traffic.
|
||||||
|
6. **No overtaking concept appears anywhere.** A text search of `api.engine`, `api.engine.system` and `api.cmd` found no "overtake" or "passing" terms.
|
||||||
|
|
||||||
|
## Candidate mechanisms
|
||||||
|
|
||||||
|
| Mechanism | What it gives | Overtaking? |
|
||||||
|
|---|---|---|
|
||||||
|
| `makeVehicleSetModifiersCmd` with `topSpeedScale` | Per-vehicle top speed scaling | No. A faster car still queues behind a slower one. |
|
||||||
|
| `BaseConfig` in `preRunFn` | Global acceleration or braking (trains only) | No |
|
||||||
|
| Lane config (`BaseEdge.laneConfig`, `laneConnections`) via construction or street definitions | Static lane layout, for example an extra lane built into the road | Only as static road design. Whether the engine's car AI uses a spare lane to pass is undocumented. |
|
||||||
|
| `landVehicleMoveSystem` / `MovePath` reads | Observe speed, position and blockage per tick | Detection only |
|
||||||
|
| Custom entities / custom vehicles (`makeCustomEntity*`, `makeCustomVehicleCreateOrUpdateCmd`) | Script-driven fake visuals | Theoretically a script could despawn a real car and animate a fake one past the blocker. This is a heavy hack with unproven viability for road vehicles. |
|
||||||
|
|
||||||
|
## Open questions (not answered by the docs)
|
||||||
|
|
||||||
|
- The fields of `LandVehicleMoveInfo`.
|
||||||
|
- Whether the engine already supports passing on multi-lane or bus-lane roads, outside of script control.
|
||||||
|
- Whether anything outside the published script-doc (native mod interfaces) exists. Testing in-game would be the next step.
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,93 @@
|
|||||||
|
# In-game vehicle probe
|
||||||
|
|
||||||
|
Ticket "Probe vehicle data and native passing in game" (part of map "Map: TpF3 overtaking mod"). Run 2026-10-05 on game build 40408, with the throwaway probe `mod/content/overtake_probe.{gs.lua,script.tl}` on branch `prototype/vehicle-probe`. Log excerpt: `probe-logs/2026-10-05-vehicle-probe.txt`.
|
||||||
|
|
||||||
|
## How the probe works
|
||||||
|
|
||||||
|
- A mod registers a per-tick game script by shipping `content/<name>.gs.lua` (`data()` returns `updateScript = { fileName = "<name>.script@update" }`), the same way the base game's `vehicle/vehicle_modifier.gs.lua` does. No `mod.json` entry is needed. Adding it to an existing save logs `Creating entity for newly added GameScript haylan_tf3_overtake::/overtake_probe.gs`.
|
||||||
|
- `update` runs every 0.2 s of game time (`dt = 0.2`).
|
||||||
|
- **Module-level Lua variables don't persist between calls**: the script runs in several or fresh Lua states. Persistent state has to go through `GameScriptState`.
|
||||||
|
- **No hot reload**: an edited `.tl` takes effect only after the save is reloaded.
|
||||||
|
- `print` goes to `<Steam>/userdata/<id>/3493540/local/crash_dump/stdout.txt`.
|
||||||
|
|
||||||
|
## API facts (observed, overriding tealdef where they differ)
|
||||||
|
|
||||||
|
- Player vehicles: `api.engine.getEntitiesWithComponent(TRANSPORT_VEHICLE, { requireOwnedByPlayer = api.engine.util.getPlayer() })`. Road vehicles also carry `LAND_VEHICLE` and `MOVE_PATH`.
|
||||||
|
- `MovePath.path.edges` is a **1-based Lua table of `EdgeIdDir` userdata** (`.edgeId`, `.dir`), **not** `{EdgeId, boolean}` as the tealdef says. The current edge is `edges[dyn.pathPos.edgeIndex + 1]`.
|
||||||
|
- `EdgeId.entity` is the **street segment** (it carries `BASE_EDGE` and `TRANSPORT_NETWORK`). `EdgeId.index` is the **lane** within that segment's `TransportNetwork.edges`.
|
||||||
|
- `TransportNetwork.turnaroundEdges[index + 1]` is the opposite lane. `TransportNetwork.edges[i]` has `transportModes`, `speedLimit` and `curSpeed`.
|
||||||
|
- `BaseEdge.laneConfigs` lists the lanes, with `forward`, `offset`, `width` and `transportModes`. Mode keys print as integers: sidewalks `[0,1]`, driving lanes `[2,3,4]`.
|
||||||
|
- `landVehicleMoveSystem.getVehicleInfo` only returns `availPower`, `traction`, `tractiveEffort` and `vehicleAvailPower`.
|
||||||
|
- `MovePath.blocked` stayed `0` for every vehicle the whole time, including vehicles clearly queued behind a slower one. It doesn't signal "held up by the vehicle ahead".
|
||||||
|
- `MovePath.dyn.timeToIgnore` is negative and counts down for almost every vehicle. It was briefly positive (+9.4) once, right after a stop. Its meaning is unknown.
|
||||||
|
|
||||||
|
## Observed traffic behaviour
|
||||||
|
|
||||||
|
| Road | Lanes per direction | What happened |
|
||||||
|
|---|---|---|
|
||||||
|
| `country_new_medium` (segments 6527 → 29697) | 2 (lanes 3 and 4 forward) | 29251 (power 243) followed 29852 (power 160) in lane 4 over 6 segments, 24–30 m behind. It accelerated to 17.7 m/s and then dropped to 15.1 m/s to match the vehicle ahead. Lane 3 was free and stayed unused. |
|
||||||
|
| `country_new_medium` (segment 28894) | 2 | The two vehicles were in different lanes (3 and 4), but their order never changed. This looks like lane choice for the next turn, not passing. |
|
||||||
|
| `country_new_small` (segments 26513 → 28566) | 1 (lane 2 forward, lane 1 opposite) | 29251 was held at exactly 16.67 m/s (29851's top speed) behind 29851, below the road's 22.2 m/s limit, for 6+ segments. |
|
||||||
|
|
||||||
|
## Verdict
|
||||||
|
|
||||||
|
- The engine does **not** pass natively, on either multilane or two-way roads. Bus lanes weren't tested.
|
||||||
|
- The read side is sufficient: a script can tell, per vehicle, which segment, lane and direction it's in, where it is, how fast it's going, and what the opposite lane is. So both the "held back" condition and an oncoming-traffic check can be computed. "Held back" must be derived (same segment and lane, small gap, speed below the vehicle's capability), because `blocked` doesn't report it.
|
||||||
|
- **No documented command can make a vehicle ignore the one ahead** (see `overtake-feasibility.md`).
|
||||||
|
|
||||||
|
## Searching for a way to make a vehicle ignore the one ahead
|
||||||
|
|
||||||
|
- **Script commands:** none of the commands in `api.cmd` moves a vehicle, changes its lane, or turns off the queueing. The full list was checked.
|
||||||
|
- **Model metadata:** `landVehicle` (engines, topSpeed, weights) and `extent` have no following or blocking parameters.
|
||||||
|
- **`Transformator.LandVehicleScriptingApi` / `TransfOutput.setUserTransf`:** a per-model animation script. It can offset the model on screen, which could make a vehicle look like it changes lanes, but it doesn't touch the simulation.
|
||||||
|
- **Executable strings:** `LandVehicleMoveSystem.cpp` contains `GetNextEdgeTrafficPenaltyAndAlternatives`, `existingPathLaneSwitch` and `switchIndices`. So the engine itself switches lanes at segment boundaries based on a traffic penalty, which matches the lane split seen on segment 28894. None of it is exposed to scripts.
|
||||||
|
- **`api.cmd.debug.makeComponentExchangeCmd(entity, MovePath)`** (save loaded with "Debug mode is active"): building the command with a `MovePath` **succeeds**. `api.cmd.sendCommand(cmd, callback)` fails with `Error decoding argument #3`, because engine-state scripts can't pass a callback (base scripts never do). The version without a callback was committed but **never ran in game**. Whether the swap is applied, and what a positive `timeToIgnore` does, are both **untested**.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
Overtaking on two-way roads (one lane per direction) is dropped (decided by the user, 2026-10-05). The effort continues on multilane roads only: a held-back vehicle should switch to a lane the slower vehicle isn't in.
|
||||||
|
|
||||||
|
## Segment rebuild (ticket "Can rebuilding a road segment move a passer past its blocker?")
|
||||||
|
|
||||||
|
- The script sent 3 `makeWorldBuildProposalCmd` proposals (remove segment + re-add a copy of its `BaseEdge`/`BaseEdgeStreet`, `ignoreErrors = true`) for segments 29701 and 29699 (`country_new_medium`). **None took effect**: both segment ids stayed in use by vehicles until the end of the log, and 29699 was "rebuilt" twice. The game logged no error, and there's no callback in engine state. The copied `BaseEdge.distance` read `0.0`, so the copy is probably incomplete. A next attempt would need `api.engine.util.proposal.makeProposalData` to see the proposal's errors.
|
||||||
|
- The human's manual segment deletion left no trace in the probe data: every vehicle stayed `tvState=1` (en route), with only normal path trimming.
|
||||||
|
- Related find: `api.cmd.makeVehicleSendToDepotCmd(vehicle, sellOnArrival, jumpToDepotEntity)` "teleports" a vehicle to a depot. The engine can relocate vehicles instantly, but only to depots.
|
||||||
|
|
||||||
|
## Lessons from downloaded mods and the game folder (2026-10-05)
|
||||||
|
|
||||||
|
Scanned: the 9 mods in `C:\Users\Public\mod.io\10640\mods\` (e.g. `devers_1`, `move_it_probe_overpass`, `zhenya_auto_assign_terminals`, `celmi_timetables`, `burntnuggie_line_priority_1`), and every `.lua`/`.tl` in the install's `base`, `mods/release` and `dlcs`, including inside the zips.
|
||||||
|
|
||||||
|
- **Callbacks only work on the GUI thread.** Mods send commands from a `guiUpdateScript`/`guiHandleEventScript` (declared in the same `.gs.lua`) and get `function(res, success, resultEntities)` back. Errors are readable via `res.resultProposalData.errorState.messages`. From `update` (engine thread), callbacks fail with `Error decoding argument #3`, which is what we hit.
|
||||||
|
- **Rebuilding a segment:** `api.engine.util.proposal.replaceSegment(entity, streetTemplate?)` returns a ready `Proposal`. The official campaign (mission 03 `init.script.tl`) sends it from engine state with `makeWorldBuildProposalCmd(proposal, nil, true, true)`. `devers_1` uses it the same way. It keeps lane links, edge objects (signals) and electrification. `api.engine.util.proposal.makeProposalData(proposal)` validates a proposal (`.costs`, `.errorState`) without sending it.
|
||||||
|
- **Teleport:** only `makeVehicleSendToDepotCmd(vehicle, sellOnArrival, jumpToDepotEntity)`. Mission 05 uses it for a ship, then `makeVehicleSetLineCmd(vehicle, newLine, 0)` and a restart. No coordinate or lane-position teleport exists for line vehicles. `makeCustomVehicleCreateOrUpdateCmd` is aircraft-only ("carrier currently must be Air").
|
||||||
|
- `api.engine.util.transport.calcPosition(edge.geometry, t)` gives a world position along an edge geometry.
|
||||||
|
- `burntnuggie_line_priority_1` sets line **reservation** priority via `makeLineUpdateCmd`. That's path reservation for rail, not road traffic.
|
||||||
|
- `makeVehicleSetLineCmd` to the vehicle's own line: the lanes planned ahead change, but the engine re-plans them on its own all the time (the leader's changed too, without a command). It doesn't force a lane change, and with `stopIndex=0` it re-targets the line's first stop.
|
||||||
|
|
||||||
|
## Segment rebuild via `replaceSegment` (run 2026-10-05 ~20:55 UTC)
|
||||||
|
|
||||||
|
- `api.engine.util.proposal.replaceSegment(seg)` plus `makeWorldBuildProposalCmd(p, nil, true, true)` from engine state **works**: segments 29702, 21304 and 29012 never appeared again after their rebuild (they got new ids).
|
||||||
|
- **After all 3 rebuilds, the follower and leader ended up in different lanes** (`country_new_medium`, 2 lanes per direction). Each time the *leader* moved from lane 4 to lane 3, either right away or one segment later, and the follower stayed in lane 4.
|
||||||
|
- #1 (seg 29702): follower 29251 at 50.4 m behind leader 29852 at 76.7 m. Afterwards, on seg 29699, 29251 is at 29.1 m (lane 4) and 29852 at 20.2 m (lane 3). 29251 drives 18.6–19.6 m/s, above its earlier 16.67, and stays a segment ahead. **The follower got past.**
|
||||||
|
- #2 (seg 21304): 29851 (leader) moved to lane 3 on seg 27833. The lanes split, but the order was unchanged.
|
||||||
|
- #3 (seg 29012): 29251 (leader) moved to lane 3 on seg 29322.
|
||||||
|
- Without rebuilds, the same pair stayed together in lane 4 for 6 segments (first probe). The engine also switches lanes by itself at segment ends sometimes, so 3/3 is suggestive, not proof.
|
||||||
|
- Likely cause (human's hypothesis): the vehicles' `MovePath` refers to lanes of the removed segment, so the engine must re-attach them to the new segment and re-plan, and a vehicle can land in a different lane.
|
||||||
|
- **Debug `MovePath` exchange is unusable from engine state**: `api.cmd.sendCommand(api.cmd.debug.makeComponentExchangeCmd(v, movePath))` fails with `Error decoding argument #3` even **without** a callback. So the earlier error wasn't caused by the callback. The only untried route is the GUI thread.
|
||||||
|
- Not yet tested: a rebuild on a two-way (one lane per direction) road.
|
||||||
|
|
||||||
|
## Rebuild on a two-way road (run 2026-10-05 ~21:12 UTC)
|
||||||
|
|
||||||
|
- 3 `replaceSegment` rebuilds on `country_new_small` (one lane per direction), segments 30213, 29776 and 30215, follower 25.8 m behind leader. **No effect**: the next snapshot had the same gap (22.6/48.4 → 49.3/75.1), both vehicles stayed in lane 2, same speeds, same order for 10+ snapshots. Rebuilding does not move vehicles past each other on single-lane roads.
|
||||||
|
- How the multilane lane change happens is still uncaught: snapshots every 2 s only show before (both lane 4) and after (the leader in lane 3 two segments later). The probe now traces the two vehicles on every update for ~30 s after a rebuild (`TRACE` lines), and rebuilds only multilane segments.
|
||||||
|
|
||||||
|
## 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.
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "haylan",
|
||||||
|
"role": "CREATOR"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Lets road vehicles overtake slower vehicles (work in progress).",
|
||||||
|
"summary": "Vehicle overtaking (WIP)",
|
||||||
|
"name": "Overtake",
|
||||||
|
"tags": [
|
||||||
|
"Script Mod",
|
||||||
|
"Car",
|
||||||
|
"Road"
|
||||||
|
],
|
||||||
|
"url": "https://git.arthurerlich.de/haylan/tpf3-overtake"
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
local mod = {}
|
||||||
|
|
||||||
|
mod.preRunFn = function(_configDict : {{string, string}}, _allModParams : {string : {string : integer}}, _baseConfig : BaseConfig)
|
||||||
|
print("[haylan_tf3_overtake] hello from preRunFn")
|
||||||
|
end
|
||||||
|
|
||||||
|
mod.postRunFn = function(_configDict : {{string, string}}, _allModParams : {string : {string : integer}})
|
||||||
|
print("[haylan_tf3_overtake] hello from postRunFn")
|
||||||
|
end
|
||||||
|
|
||||||
|
return mod
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
-- PROTOTYPE (ticket "Probe vehicle data and native passing in game"): throwaway, lives only on branch prototype/vehicle-probe.
|
||||||
|
function data()
|
||||||
|
return {
|
||||||
|
updateScript = {
|
||||||
|
fileName = "overtake_probe.script@update",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
end
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
-- 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 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 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
|
||||||
|
|
||||||
|
local ret : GameScript<table> = {
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"modId": "haylan_tf3_overtake",
|
||||||
|
"revision": 1,
|
||||||
|
"severityAdd": "None",
|
||||||
|
"severityRemove": "Warning",
|
||||||
|
"cosmetic": false,
|
||||||
|
"preRunScript": {
|
||||||
|
"fileName": "haylan_tf3_overtake::mod.script@preRunFn"
|
||||||
|
},
|
||||||
|
"postRunScript": {
|
||||||
|
"fileName": "haylan_tf3_overtake::mod.script@postRunFn"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
return {
|
||||||
|
include_dir = {
|
||||||
|
-- ensure to set proper paths for the definitions for the base game
|
||||||
|
"<tf3_install_folder>api/tealdef",
|
||||||
|
"<tf3_install_folder>base/tealdef",
|
||||||
|
|
||||||
|
-- list your mods here, if you provide your own d.tl definitions
|
||||||
|
-- "example_mod_1",
|
||||||
|
},
|
||||||
|
global_env_def = "all_def"
|
||||||
|
}
|
||||||
Reference in new issue
Block a user