kovagoadiandClaude f7ca395a4f
Continuous Integration / lint-and-security (pull_request) Failing after 19s
Continuous Integration / tests-and-coverage (pull_request) Successful in 24s
Put the AdminBridge GameScript under version control
The last commit noted in passing that the server-side half of the admin
GameScript channel "is not in this repo -- docker/config is gitignored --
so it has to be updated separately for any of this to work." That was true
of all nine features the README documents: list_vehicles, list_stations,
list_cargo, get_timetable, get_station, get_station_cargo, get_dispatch and
the event stream all answer from 705 lines of Squirrel that no clone could
reproduce, no reviewer could see, and CI never touched.

The bridge now lives in gamescript/AdminBridge/ with its own README, the
same arrangement docker/patches/ uses for the local JGRPP patches, and
docker-compose.yml bind-mounts it read-only over the container's
game/AdminBridge. docker/config stays ignored -- it also holds savegames,
downloaded content and generated config -- so the copy under it is now
shadowed and can be deleted. main.nut is byte-identical to what was running,
apart from the version work below.

Adds a version handshake, because the channel gives no way to tell a stale
bridge from a hung one: a bridge that does not recognise a command drops it
silently, so a client ahead of the server sees nothing but timeouts. The
bridge now answers get_version with its protocol version plus its command
and event catalogues, and get_bridge_version() raises when that is below
GS_BRIDGE_VERSION. It is opt-in rather than checked on connect: GameScripts
do not tick while the game is paused, so an automatic check would refuse to
connect to a paused server. A bridge older than 4 predates get_version
itself and can only fail by timing out, so the E2E test catches that and
reports it by name instead.

tests/test_gamescript.py gives CI a foothold on the GameScript without a
Squirrel toolchain: it parses the .nut files and pins the protocol version
across info.nut, main.nut and protocol.py, the event catalogue against
GameEventType, and the command table against the commands client.py sends.
The version has to be declared three times because a GameScript cannot read
its own info.nut at runtime -- GSController.GetVersion() returns the OpenTTD
version, not the script's.

info.nut also gains MinVersionToLoad() { return 1; }. The engine defaults it
to GetVersion(), so without it this bump would orphan every savegame pinned
to version 3: the scanner finds no compatible script and falls back with a
warning. The bridge keeps no savegame state, so any version can take over.

HandleCommand now dispatches through the same table get_version reports,
rather than an if/else chain, so the catalogue a client feature-detects
against cannot drift from what is implemented.

Co-Authored-By: Claude <[email protected]>
2026-08-31 19:06:33 +02:00
2026-06-05 23:18:09 +02:00
2026-06-05 23:23:55 +02:00

OpenTTD Python Client

A high-performance, Object-Oriented Python client for OpenTTD servers, specifically optimized for JGR Patch Pack (JGRPP). This client handles the modern secure handshake, including X25519 PAKE authentication and AEAD stream encryption.

🚀 Features

  • Secure Authentication: Full implementation of X25519 PAKE (Password-Authenticated Key Exchange).
  • Stream Encryption: Automatic XChaCha20-Poly1305 authenticated encryption for all game traffic.
  • Modular Design: Separates low-level binary protocol handling from high-level game logic.
  • State Management: Handles the full join sequence including Map download and synchronization.
  • Comprehensive Testing: Robustly tested with unit, logic, and E2E tests (including 100% coverage for unit/logic tests).
  • Vehicle Listing: Query vehicle data via the Admin GameScript channel with list_vehicles().
  • Vehicle Timetables: Read and modify a vehicle's timetable (change_timetable(), autofill_timetable(), set_timetable_start(), set_vehicle_on_time(), get_vehicle_timetable()) via real game-protocol commands.
  • Order Editing: Add and remove a vehicle's orders (add_order() inserts a "go to station" stop, remove_order() deletes one) via real game-protocol commands.
  • Scheduled Dispatch (JGRPP): Edit a vehicle's dispatch schedules and departure slots over the game port (add_dispatch_schedule(), add_dispatch_slot(), set_dispatch_duration(), set_scheduled_dispatch(), and more), and read them back authoritatively with OpenTTDAdminClient.get_dispatch() (via a patched GameScript API + the AdminBridge GS).
  • Authoritative Timetable Reads: OpenTTDAdminClient.get_timetable() fetches the real, current timetable of any vehicle from the running game (via a patched GameScript API + the AdminBridge GS) — no company join needed, works for timetables set before connecting.
  • Station Listing: Enumerate stations via the Admin GameScript channel with list_stations().
  • Station Cargo Snapshots: OpenTTDAdminClient.get_station() returns a station's live per-cargo state from the running game — both the real-time amount waiting and the planned flow through the cargodist link graph — over the AdminBridge GS (stock GameScript API, no server patch needed).
  • Cargo Flow Breakdown: OpenTTDAdminClient.get_station_cargo() breaks one cargo type down by source station and next hop (routing destination) for both waiting (real-time) and planned amounts, with optional from_station/via_station filters.
  • Version-checked Server Bridge: the server-side AdminBridge GameScript that answers all of the above lives in gamescript/AdminBridge/ and is mounted into the Docker server automatically. get_bridge_version() verifies at startup that the running bridge is new enough, so an outdated one fails by name instead of hanging every query.
  • Game Events: react to the game instead of polling it — subscribe_events() streams events over the AdminBridge GS as they happen: a vehicle reaching or leaving a stop (vehicle_arrive/vehicle_depart, with dwell time and cargo aboard), a station's waiting cargo changing (cargo_waiting), plus crashes, industries opening/closing, towns, companies and subsidies. Consume them with await wait_for_event() or an on_event callback; filter by kind, company, vehicle, station or cargo.

🛠 Setup

Prerequisites

  • Python 3.11+
  • A running OpenTTD server (preferably JGRPP)

Installation

  1. Create and activate a virtual environment:
    python3 -m venv venv
    source venv/bin/activate  # Linux/macOS
    
  2. Install dependencies:
    pip install openttd-protocol pymonocypher
    

📖 Usage

Running the default client

The main.py script is configured to join the local server and the company "Én transport".

python3 main.py [Username] [CompanyID]

Example:

python3 main.py MyBot 0

Module Integration

You can use the openttd package in your own projects:

from openttd import OpenTTDClient

client = OpenTTDClient(host="127.0.0.1", username="BotName")
await client.connect(server_password="asd")
await client.join_company(company_id=0, company_password="asd123")

await client.joined.wait()
# Your logic here...

📂 Project Structure

  • main.py: Main entry point and usage example.
  • lib/openttd/: Core package containing the protocol and client logic.
  • gamescript/AdminBridge/: The server-side GameScript the admin client's GameScript channel talks to.
  • docker/: Dedicated JGRPP server (Dockerfile, compose, and the local server patches in docker/patches/).
  • docs/: Extensive documentation on architecture, protocol, and contributing.
  • tests/: Comprehensive test suite (Logic, Protocol, E2E).

🧪 Testing

We maintain high test coverage. To run normal (non-E2E) tests:

pytest -m "not e2e"

For detailed instructions on E2E testing and coverage reports, see the Testing Guide.

📜 Documentation

S
Description
No description provided
Readme
429 KiB
Languages
Python 86.6%
Squirrel 12.7%
Dockerfile 0.7%