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]>
503 lines
20 KiB
Python
503 lines
20 KiB
Python
import asyncio
|
|
import json
|
|
import os
|
|
|
|
import monocypher
|
|
import pytest
|
|
from openttd import OpenTTDAdminClient
|
|
from openttd.protocol import (
|
|
GS_BRIDGE_VERSION,
|
|
AdminUpdateFrequency,
|
|
AdminUpdateType,
|
|
PacketAdminType,
|
|
)
|
|
|
|
|
|
class FakeNetworkError(OSError):
|
|
"""Stand-in for a socket-level failure, so it matches the client's narrowed handlers."""
|
|
|
|
def decode_gamescript_payload(packet):
|
|
"""Decode the JSON payload of an AdminGamescript packet (2-byte length + 1-byte type + string)."""
|
|
return json.loads(packet[3:].split(b"\x00")[0])
|
|
|
|
class MockTransport:
|
|
def __init__(self):
|
|
self._closing = False
|
|
def is_closing(self):
|
|
return self._closing
|
|
def close(self):
|
|
self._closing = True
|
|
def write(self, data):
|
|
return len(data)
|
|
|
|
class MockProtocol:
|
|
def __init__(self):
|
|
self.sent = []
|
|
async def send_packet(self, data):
|
|
self.sent.append(data)
|
|
return len(data)
|
|
|
|
def test_admin_packet_types():
|
|
assert PacketAdminType.AdminJoin == 0
|
|
assert PacketAdminType.ServerWelcome == 104
|
|
assert PacketAdminType.ServerAuthRequest == 128
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_admin_list_vehicles():
|
|
client = OpenTTDAdminClient("127.0.0.1", port=3977, admin_name="TestAdmin")
|
|
proto = MockProtocol()
|
|
client._protocol = proto
|
|
client._transport = MockTransport()
|
|
|
|
await client.list_vehicles()
|
|
await client.list_vehicles(company_id=2)
|
|
|
|
assert len(proto.sent) == 2
|
|
assert decode_gamescript_payload(proto.sent[0]) == {"command": "list_vehicles"}
|
|
assert decode_gamescript_payload(proto.sent[1]) == {"command": "list_vehicles", "company_id": 2}
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_admin_list_stations():
|
|
client = OpenTTDAdminClient("127.0.0.1", port=3977, admin_name="TestAdmin")
|
|
proto = MockProtocol()
|
|
client._protocol = proto
|
|
client._transport = MockTransport()
|
|
|
|
await client.list_stations()
|
|
await client.list_stations(company_id=2)
|
|
|
|
assert len(proto.sent) == 2
|
|
assert decode_gamescript_payload(proto.sent[0]) == {"command": "list_stations"}
|
|
assert decode_gamescript_payload(proto.sent[1]) == {"command": "list_stations", "company_id": 2}
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_admin_client_connect_and_actions(monkeypatch):
|
|
client = OpenTTDAdminClient("127.0.0.1", port=3977, admin_name="TestAdmin")
|
|
assert client.host == "127.0.0.1"
|
|
assert client.port == 3977
|
|
assert client.admin_name == "TestAdmin"
|
|
|
|
# 1. Connect (secure=False)
|
|
proto = MockProtocol()
|
|
transport = MockTransport()
|
|
async def mock_connect(*args, **kwargs):
|
|
return transport, proto
|
|
|
|
monkeypatch.setattr(asyncio.get_running_loop(), "create_connection", mock_connect)
|
|
await client.connect(admin_password="asd", secure=False)
|
|
assert len(proto.sent) == 1
|
|
|
|
# 2. Connect (secure=True)
|
|
proto.sent.clear()
|
|
await client.connect(admin_password="asd", secure=True)
|
|
assert len(proto.sent) == 1
|
|
|
|
# 3. Connect Exception
|
|
async def mock_fail(*args, **kwargs):
|
|
raise FakeNetworkError("Connection Failed")
|
|
monkeypatch.setattr(asyncio.get_running_loop(), "create_connection", mock_fail)
|
|
with pytest.raises(Exception, match="Connection Failed"):
|
|
await client.connect()
|
|
|
|
# Reset connection mock for further tests
|
|
client._protocol = proto
|
|
client._transport = transport
|
|
|
|
# 4. Actions
|
|
proto.sent.clear()
|
|
await client.send_rcon("help")
|
|
await client.send_chat("hello")
|
|
await client.update_frequency(AdminUpdateType.Chat, AdminUpdateFrequency.Automatic)
|
|
await client.poll(AdminUpdateType.ClientInfo)
|
|
await client.poll_clients(1)
|
|
await client.poll_companies(2)
|
|
await client.poll_economy(3)
|
|
await client.poll_stats(4)
|
|
await client.send_gamescript({"cmd": "test"})
|
|
assert len(proto.sent) == 9
|
|
|
|
# 5. Protocol callbacks
|
|
client.connected(None)
|
|
|
|
# X25519 authentication handshake callback
|
|
server_pub = monocypher.x25519_public_key(monocypher.generate_key())
|
|
nonce = os.urandom(24)
|
|
await client.receive_ServerAuthRequest(None, 1, server_pub + nonce)
|
|
assert client._session_key_send is not None
|
|
|
|
await client.receive_ServerEnableEncryption(None, os.urandom(24))
|
|
assert client.encryption_enabled is True
|
|
|
|
await client.receive_ServerProtocol(None, 3, {})
|
|
await client.receive_ServerWelcome(None, server_name="JGRPP")
|
|
assert client.joined.is_set()
|
|
|
|
# Chat callback with and without handler
|
|
chat_events = []
|
|
client.on_chat = lambda **k: chat_events.append(k)
|
|
await client.receive_ServerChat(None, client_id=1, message="hello")
|
|
assert chat_events == [{"client_id": 1, "message": "hello"}]
|
|
client.on_chat = None
|
|
await client.receive_ServerChat(None, client_id=1, message="hello")
|
|
|
|
# Console callback with and without handler
|
|
console_events = []
|
|
client.on_console = lambda **k: console_events.append(k)
|
|
await client.receive_ServerConsole(None, origin="server", text="welcome")
|
|
assert console_events == [{"origin": "server", "text": "welcome"}]
|
|
client.on_console = None
|
|
await client.receive_ServerConsole(None, origin="server", text="welcome")
|
|
|
|
# Other callbacks
|
|
await client.receive_ServerRcon(None, text="res")
|
|
await client.receive_ServerRconEnd(None, command="help")
|
|
await client.receive_ServerClientJoin(None, client_id=1)
|
|
await client.receive_ServerClientInfo(None, client_id=1, name="user", network_address="127.0.0.1")
|
|
await client.receive_ServerClientUpdate(None, client_id=1, name="user2")
|
|
await client.receive_ServerClientQuit(None, client_id=1)
|
|
await client.receive_ServerClientError(None, client_id=1, error_code=3)
|
|
await client.receive_ServerCompanyNew(None, company_id=1)
|
|
await client.receive_ServerCompanyInfo(None, company_id=1, name="company")
|
|
await client.receive_ServerCompanyUpdate(None, company_id=1)
|
|
await client.receive_ServerCompanyRemove(None, company_id=1)
|
|
await client.receive_ServerCompanyEconomy(None, company_id=1, money=1000, loan=100)
|
|
await client.receive_ServerCompanyStats(None, company_id=1, vehicles={}, stations={})
|
|
|
|
# Gamescript callback with and without handler
|
|
gs_events = []
|
|
client.on_gamescript = lambda data: gs_events.append(data)
|
|
await client.receive_ServerGamescript(None, data={"a": 1})
|
|
assert gs_events == [{"a": 1}]
|
|
client.on_gamescript = None
|
|
await client.receive_ServerGamescript(None, data={"a": 1})
|
|
|
|
await client.receive_ServerDate(None)
|
|
|
|
# Error callbacks that shut down the client
|
|
client.shutdown_event.clear()
|
|
await client.receive_ServerError(None, 5)
|
|
assert client.shutdown_event.is_set()
|
|
|
|
client.shutdown_event.clear()
|
|
await client.receive_ServerFull(None)
|
|
assert client.shutdown_event.is_set()
|
|
|
|
client.shutdown_event.clear()
|
|
await client.receive_ServerBanned(None)
|
|
assert client.shutdown_event.is_set()
|
|
|
|
client.shutdown_event.clear()
|
|
await client.receive_ServerShutdown(None)
|
|
assert client.shutdown_event.is_set()
|
|
|
|
await client.receive_ServerNewGame(None)
|
|
await client.receive_ServerPong(None, payload=123)
|
|
|
|
# 6. Disconnect
|
|
client.shutdown_event.clear()
|
|
client.disconnect(None)
|
|
assert client.shutdown_event.is_set()
|
|
|
|
# 7. Quit
|
|
client._transport = MockTransport()
|
|
await client.quit()
|
|
assert client.shutdown_event.is_set()
|
|
|
|
# 8. Quit Exception
|
|
class BadProtocol:
|
|
async def send_packet(self, data):
|
|
raise FakeNetworkError("Fail")
|
|
client._transport = MockTransport()
|
|
client._protocol = BadProtocol()
|
|
await client.quit()
|
|
assert client.shutdown_event.is_set()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_admin_get_timetable_request_and_response():
|
|
client = OpenTTDAdminClient("127.0.0.1", port=3977, admin_name="TestAdmin")
|
|
proto = MockProtocol()
|
|
client._protocol = proto
|
|
client._transport = MockTransport()
|
|
|
|
task = asyncio.ensure_future(client.get_timetable(5))
|
|
await asyncio.sleep(0) # let the task send the request
|
|
|
|
# First use auto-subscribes to Gamescript updates, then sends the query.
|
|
assert len(proto.sent) == 2
|
|
assert proto.sent[0][2] == PacketAdminType.AdminUpdateFrequency
|
|
assert decode_gamescript_payload(proto.sent[1]) == {
|
|
"command": "get_timetable", "vehicle_id": 5, "request_id": 1,
|
|
}
|
|
|
|
response = {"command": "get_timetable", "vehicle_id": 5, "request_id": 1,
|
|
"lateness": 0, "start_tick": 0, "current_order_time": 3,
|
|
"total_duration": -1, "orders": []}
|
|
await client.receive_ServerGamescript(None, data=response)
|
|
assert await task == response
|
|
assert client._gs_futures == {}
|
|
|
|
# Second call must not re-subscribe and must use a fresh request id.
|
|
task = asyncio.ensure_future(client.get_timetable(9))
|
|
await asyncio.sleep(0)
|
|
assert len(proto.sent) == 3
|
|
assert decode_gamescript_payload(proto.sent[2])["request_id"] == 2
|
|
await client.receive_ServerGamescript(None, data={"request_id": 2, "orders": []})
|
|
assert (await task)["orders"] == []
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_admin_get_timetable_timeout():
|
|
client = OpenTTDAdminClient("127.0.0.1", port=3977, admin_name="TestAdmin")
|
|
client._protocol = MockProtocol()
|
|
client._transport = MockTransport()
|
|
|
|
with pytest.raises(asyncio.TimeoutError):
|
|
await client.get_timetable(5, timeout=0.05)
|
|
assert client._gs_futures == {}
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_admin_get_timetable_error_response():
|
|
client = OpenTTDAdminClient("127.0.0.1", port=3977, admin_name="TestAdmin")
|
|
client._protocol = MockProtocol()
|
|
client._transport = MockTransport()
|
|
|
|
task = asyncio.ensure_future(client.get_timetable(65535))
|
|
await asyncio.sleep(0)
|
|
await client.receive_ServerGamescript(
|
|
None, data={"command": "get_timetable", "vehicle_id": 65535,
|
|
"request_id": 1, "error": "invalid_vehicle"})
|
|
with pytest.raises(ValueError, match="invalid_vehicle"):
|
|
await task
|
|
assert client._gs_futures == {}
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_admin_get_timetable_disconnect_fails_pending():
|
|
client = OpenTTDAdminClient("127.0.0.1", port=3977, admin_name="TestAdmin")
|
|
client._protocol = MockProtocol()
|
|
client._transport = MockTransport()
|
|
|
|
task = asyncio.ensure_future(client.get_timetable(5))
|
|
await asyncio.sleep(0)
|
|
client.disconnect(None)
|
|
with pytest.raises(ConnectionError):
|
|
await task
|
|
assert client._gs_futures == {}
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_admin_get_station_request_and_response():
|
|
client = OpenTTDAdminClient("127.0.0.1", port=3977, admin_name="TestAdmin")
|
|
proto = MockProtocol()
|
|
client._protocol = proto
|
|
client._transport = MockTransport()
|
|
|
|
task = asyncio.ensure_future(client.get_station(3))
|
|
await asyncio.sleep(0) # let the task send the request
|
|
|
|
# First use auto-subscribes to Gamescript updates, then sends the query.
|
|
assert len(proto.sent) == 2
|
|
assert proto.sent[0][2] == PacketAdminType.AdminUpdateFrequency
|
|
assert decode_gamescript_payload(proto.sent[1]) == {
|
|
"command": "get_station", "station_id": 3, "request_id": 1,
|
|
}
|
|
|
|
response = {"command": "get_station", "station_id": 3, "request_id": 1,
|
|
"name": "Test Central", "location": 12345, "owner": 0,
|
|
"cargo": [{"cargo_id": 0, "waiting": 42, "planned": 17, "rating": 71}]}
|
|
await client.receive_ServerGamescript(None, data=response)
|
|
assert await task == response
|
|
assert client._gs_futures == {}
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_admin_get_station_error_response():
|
|
client = OpenTTDAdminClient("127.0.0.1", port=3977, admin_name="TestAdmin")
|
|
client._protocol = MockProtocol()
|
|
client._transport = MockTransport()
|
|
|
|
task = asyncio.ensure_future(client.get_station(65535))
|
|
await asyncio.sleep(0)
|
|
await client.receive_ServerGamescript(
|
|
None, data={"command": "get_station", "station_id": 65535,
|
|
"request_id": 1, "error": "invalid_station"})
|
|
with pytest.raises(ValueError, match="invalid_station"):
|
|
await task
|
|
assert client._gs_futures == {}
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_admin_get_station_timeout():
|
|
client = OpenTTDAdminClient("127.0.0.1", port=3977, admin_name="TestAdmin")
|
|
client._protocol = MockProtocol()
|
|
client._transport = MockTransport()
|
|
|
|
with pytest.raises(asyncio.TimeoutError):
|
|
await client.get_station(3, timeout=0.05)
|
|
assert client._gs_futures == {}
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_admin_get_station_cargo_request_and_response():
|
|
client = OpenTTDAdminClient("127.0.0.1", port=3977, admin_name="TestAdmin")
|
|
proto = MockProtocol()
|
|
client._protocol = proto
|
|
client._transport = MockTransport()
|
|
|
|
task = asyncio.ensure_future(client.get_station_cargo(3, 0))
|
|
await asyncio.sleep(0) # let the task send the request
|
|
|
|
# First use auto-subscribes to Gamescript updates, then sends the query.
|
|
assert len(proto.sent) == 2
|
|
assert proto.sent[0][2] == PacketAdminType.AdminUpdateFrequency
|
|
assert decode_gamescript_payload(proto.sent[1]) == {
|
|
"command": "get_station_cargo", "station_id": 3, "cargo_id": 0, "request_id": 1,
|
|
}
|
|
|
|
response = {"command": "get_station_cargo", "station_id": 3, "cargo_id": 0, "request_id": 1,
|
|
"waiting": 60, "planned": 40,
|
|
"waiting_by_from": [{"station": 5, "amount": 25}, {"station": 6, "amount": 35}],
|
|
"planned_by_from": [{"station": 5, "amount": 40}],
|
|
"waiting_by_via": [{"station": 7, "amount": 60}],
|
|
"planned_by_via": [{"station": 7, "amount": 40}]}
|
|
await client.receive_ServerGamescript(None, data=response)
|
|
assert await task == response
|
|
assert client._gs_futures == {}
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_admin_get_station_cargo_with_filters_encoding():
|
|
client = OpenTTDAdminClient("127.0.0.1", port=3977, admin_name="TestAdmin")
|
|
proto = MockProtocol()
|
|
client._protocol = proto
|
|
client._transport = MockTransport()
|
|
client._gs_subscribed = True # skip the auto-subscribe so only the query is sent
|
|
|
|
task = asyncio.ensure_future(client.get_station_cargo(3, 0, from_station=6, via_station=7))
|
|
await asyncio.sleep(0)
|
|
|
|
assert len(proto.sent) == 1
|
|
assert decode_gamescript_payload(proto.sent[0]) == {
|
|
"command": "get_station_cargo", "station_id": 3, "cargo_id": 0,
|
|
"from_station": 6, "via_station": 7, "request_id": 1,
|
|
}
|
|
await client.receive_ServerGamescript(
|
|
None, data={"request_id": 1, "waiting": 12, "planned": 8})
|
|
assert (await task)["waiting"] == 12
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_admin_get_station_cargo_error_response():
|
|
client = OpenTTDAdminClient("127.0.0.1", port=3977, admin_name="TestAdmin")
|
|
client._protocol = MockProtocol()
|
|
client._transport = MockTransport()
|
|
|
|
task = asyncio.ensure_future(client.get_station_cargo(3, 999))
|
|
await asyncio.sleep(0)
|
|
await client.receive_ServerGamescript(
|
|
None, data={"command": "get_station_cargo", "station_id": 3, "cargo_id": 999,
|
|
"request_id": 1, "error": "invalid_cargo"})
|
|
with pytest.raises(ValueError, match="invalid_cargo"):
|
|
await task
|
|
assert client._gs_futures == {}
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_admin_get_dispatch_request_and_response():
|
|
client = OpenTTDAdminClient("127.0.0.1", port=3977, admin_name="TestAdmin")
|
|
proto = MockProtocol()
|
|
client._protocol = proto
|
|
client._transport = MockTransport()
|
|
|
|
task = asyncio.ensure_future(client.get_dispatch(7))
|
|
await asyncio.sleep(0) # let the task send the request
|
|
|
|
# First use auto-subscribes to Gamescript updates, then sends the query.
|
|
assert len(proto.sent) == 2
|
|
assert proto.sent[0][2] == PacketAdminType.AdminUpdateFrequency
|
|
assert decode_gamescript_payload(proto.sent[1]) == {
|
|
"command": "get_dispatch", "vehicle_id": 7, "request_id": 1,
|
|
}
|
|
|
|
response = {"command": "get_dispatch", "vehicle_id": 7, "request_id": 1,
|
|
"enabled": 1,
|
|
"schedules": [{"index": 0, "duration": 3000, "start_tick": 0, "delay": 0,
|
|
"reuse_slots": 0,
|
|
"slots": [{"offset": 500, "flags": 0}, {"offset": 1500, "flags": 0}]}]}
|
|
await client.receive_ServerGamescript(None, data=response)
|
|
assert await task == response
|
|
assert client._gs_futures == {}
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_admin_get_dispatch_error_response():
|
|
client = OpenTTDAdminClient("127.0.0.1", port=3977, admin_name="TestAdmin")
|
|
client._protocol = MockProtocol()
|
|
client._transport = MockTransport()
|
|
|
|
task = asyncio.ensure_future(client.get_dispatch(65535))
|
|
await asyncio.sleep(0)
|
|
await client.receive_ServerGamescript(
|
|
None, data={"command": "get_dispatch", "vehicle_id": 65535,
|
|
"request_id": 1, "error": "invalid_vehicle"})
|
|
with pytest.raises(ValueError, match="invalid_vehicle"):
|
|
await task
|
|
assert client._gs_futures == {}
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_admin_get_bridge_version_request_and_response():
|
|
client = OpenTTDAdminClient("127.0.0.1", port=3977, admin_name="TestAdmin")
|
|
proto = MockProtocol()
|
|
client._protocol = proto
|
|
client._transport = MockTransport()
|
|
|
|
task = asyncio.ensure_future(client.get_bridge_version())
|
|
await asyncio.sleep(0) # let the task send the request
|
|
|
|
# First use auto-subscribes to Gamescript updates, then sends the query.
|
|
assert len(proto.sent) == 2
|
|
assert proto.sent[0][2] == PacketAdminType.AdminUpdateFrequency
|
|
assert decode_gamescript_payload(proto.sent[1]) == {
|
|
"command": "get_version", "request_id": 1,
|
|
}
|
|
|
|
response = {"command": "get_version", "request_id": 1, "version": GS_BRIDGE_VERSION,
|
|
"commands": ["get_timetable", "get_version"],
|
|
"events": ["vehicle_arrive", "vehicle_depart"]}
|
|
await client.receive_ServerGamescript(None, data=response)
|
|
assert await task == response
|
|
assert client._gs_futures == {}
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_admin_get_bridge_version_rejects_older_bridge():
|
|
"""A bridge that answers but is too old fails by name, not by timeout."""
|
|
client = OpenTTDAdminClient("127.0.0.1", port=3977, admin_name="TestAdmin")
|
|
client._protocol = MockProtocol()
|
|
client._transport = MockTransport()
|
|
|
|
task = asyncio.ensure_future(client.get_bridge_version(minimum=GS_BRIDGE_VERSION + 1))
|
|
await asyncio.sleep(0)
|
|
await client.receive_ServerGamescript(
|
|
None, data={"command": "get_version", "request_id": 1, "version": GS_BRIDGE_VERSION})
|
|
with pytest.raises(ValueError, match=f"version {GS_BRIDGE_VERSION}"):
|
|
await task
|
|
assert client._gs_futures == {}
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_admin_get_bridge_version_minimum_zero_accepts_anything():
|
|
"""minimum=0 reads the version without requiring anything, even if the reply omits it."""
|
|
client = OpenTTDAdminClient("127.0.0.1", port=3977, admin_name="TestAdmin")
|
|
client._protocol = MockProtocol()
|
|
client._transport = MockTransport()
|
|
|
|
task = asyncio.ensure_future(client.get_bridge_version(minimum=0))
|
|
await asyncio.sleep(0)
|
|
await client.receive_ServerGamescript(
|
|
None, data={"command": "get_version", "request_id": 1, "commands": []})
|
|
assert await task == {"command": "get_version", "request_id": 1, "commands": []}
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_admin_gamescript_passthrough_unmatched():
|
|
client = OpenTTDAdminClient("127.0.0.1", port=3977, admin_name="TestAdmin")
|
|
client._protocol = MockProtocol()
|
|
client._transport = MockTransport()
|
|
|
|
gs_events = []
|
|
client.on_gamescript = lambda data: gs_events.append(data)
|
|
|
|
# No request_id, unknown request_id, and non-dict payloads all pass through.
|
|
await client.receive_ServerGamescript(None, data={"vehicles": []})
|
|
await client.receive_ServerGamescript(None, data={"request_id": 999, "orders": []})
|
|
await client.receive_ServerGamescript(None, data="plain string")
|
|
assert gs_events == [{"vehicles": []}, {"request_id": 999, "orders": []}, "plain string"]
|