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]>
630 lines
21 KiB
Python
630 lines
21 KiB
Python
import struct
|
|
from enum import IntEnum, StrEnum
|
|
|
|
import monocypher
|
|
from openttd_protocol.wire.exceptions import SocketClosed
|
|
from openttd_protocol.wire.read import read_string, read_uint8, read_uint16, read_uint32
|
|
from openttd_protocol.wire.tcp import TCPProtocol
|
|
|
|
|
|
def write_varuint(buffer, value):
|
|
"""Encode a non-negative integer using OpenTTD's UTF-8-like varuint scheme."""
|
|
if value < 0:
|
|
raise ValueError("write_varuint requires a non-negative value")
|
|
thresholds = [1 << 7, 1 << 14, 1 << 21, 1 << 28, 1 << 35, 1 << 42, 1 << 49, 1 << 56]
|
|
for extra, limit in enumerate(thresholds):
|
|
if value < limit:
|
|
header_ones = (0xFF << (8 - extra)) & 0xFF
|
|
header = header_ones | (value >> (extra * 8))
|
|
buffer.append(header)
|
|
for i in range(extra - 1, -1, -1):
|
|
buffer.append((value >> (i * 8)) & 0xFF)
|
|
return
|
|
buffer.append(0xFF)
|
|
for i in range(7, -1, -1):
|
|
buffer.append((value >> (i * 8)) & 0xFF)
|
|
|
|
def read_varuint(data):
|
|
"""Decode a varuint written by write_varuint. Returns (value, rest)."""
|
|
header = data[0]
|
|
mask = 0x80
|
|
extra = 0
|
|
while header & mask:
|
|
extra += 1
|
|
mask >>= 1
|
|
value = header & (0x7F >> extra)
|
|
rest = data[1:]
|
|
for i in range(extra):
|
|
value = (value << 8) | rest[i]
|
|
return value, rest[extra:]
|
|
|
|
def write_varuint_signed(buffer, value):
|
|
"""Encode a signed integer using zigzag + write_varuint."""
|
|
zigzag = (value << 1) ^ (-1 if value < 0 else 0)
|
|
write_varuint(buffer, zigzag)
|
|
|
|
def read_varuint_signed(data):
|
|
"""Decode a signed varuint written by write_varuint_signed. Returns (value, rest)."""
|
|
zigzag, rest = read_varuint(data)
|
|
value = (zigzag >> 1) ^ -(zigzag & 1)
|
|
return value, rest
|
|
|
|
class GameCommand(IntEnum):
|
|
DeleteOrder = 51
|
|
InsertOrder = 52
|
|
ChangeTimetable = 174
|
|
SetVehicleOnTime = 176
|
|
AutofillTimetable = 177
|
|
SetTimetableStart = 180
|
|
# Scheduled dispatch (JGRPP)
|
|
SchDispatch = 205
|
|
SchDispatchAdd = 206
|
|
SchDispatchRemove = 207
|
|
SchDispatchSetDuration = 208
|
|
SchDispatchSetStartDate = 209
|
|
SchDispatchClear = 213
|
|
SchDispatchAddNewSchedule = 214
|
|
SchDispatchRemoveSchedule = 215
|
|
|
|
# Sentinel VehicleOrderID meaning "append to the end of the order list" for InsertOrder.
|
|
INVALID_VEH_ORDER_ID = 0xFFFF
|
|
|
|
class OrderType(IntEnum):
|
|
"""OrderType occupies bits 0-3 of an order's `type` byte (bits 6-7 hold OrderNonStopFlags)."""
|
|
GotoStation = 1
|
|
GotoDepot = 2
|
|
GotoWaypoint = 6
|
|
|
|
class OrderNonStopFlags(IntEnum):
|
|
"""Packed into bits 6-7 of an order's `type` byte."""
|
|
StopEverywhere = 0
|
|
NoStopAtIntermediate = 1
|
|
NoStopAtDestination = 2
|
|
NoStopAtAny = 3
|
|
|
|
class OrderStopLocation(IntEnum):
|
|
"""Packed into bits 4-5 of an order's `type` byte. Near-end/middle/through are train-only;
|
|
FarEnd is the only value the server accepts for every vehicle type, so it is the safe default."""
|
|
PlatformNearEnd = 0
|
|
PlatformMiddle = 1
|
|
PlatformFarEnd = 2
|
|
PlatformThrough = 3
|
|
|
|
class ModifyTimetableFlags(IntEnum):
|
|
WaitTime = 0
|
|
TravelTime = 1
|
|
TravelSpeed = 2
|
|
SetWaitFixed = 3
|
|
SetTravelFixed = 4
|
|
SetLeaveType = 5
|
|
AssignSchedule = 6
|
|
|
|
class ModifyTimetableCtrlFlag(IntEnum):
|
|
ClearField = 1 << 0
|
|
|
|
class PacketGameType(IntEnum):
|
|
ServerFull = 0
|
|
ServerBanned = 1
|
|
ClientJoin = 2
|
|
ServerError = 3
|
|
ClientUnused = 4
|
|
ServerUnused = 5
|
|
ServerGameInfo = 6
|
|
ClientGameInfo = 7
|
|
ServerNewGame = 8
|
|
ServerShutdown = 9
|
|
ServerGameInfoExtended = 10
|
|
ServerAuthenticationRequest = 11
|
|
ClientAuthenticationResponse = 12
|
|
ServerEnableEncryption = 13
|
|
ClientIdentify = 14
|
|
ServerCheckNewGRFs = 15
|
|
ClientNewGRFsChecked = 16
|
|
ServerNeedCompanyPassword = 17
|
|
ClientCompanyPassword = 18
|
|
ClientSettingsPassword = 19
|
|
ServerSettingsAccess = 20
|
|
ServerWelcome = 21
|
|
ServerClientInfo = 22
|
|
ClientGetMap = 23
|
|
ServerWaitForMap = 24
|
|
ServerMapBegin = 25
|
|
ServerMapSize = 26
|
|
ServerMapData = 27
|
|
ServerMapDone = 28
|
|
ClientMapOk = 29
|
|
ServerClientJoined = 30
|
|
ServerFrame = 31
|
|
ClientAck = 32
|
|
ServerSync = 33
|
|
ClientCommand = 34
|
|
ServerCommand = 35
|
|
ClientChat = 36
|
|
ServerChat = 37
|
|
ServerExternalChat = 38
|
|
ClientQuit = 47
|
|
ServerCompanyUpdate = 45
|
|
PACKET_END = 100
|
|
|
|
class PacketAdminType(IntEnum):
|
|
AdminJoin = 0
|
|
AdminQuit = 1
|
|
AdminUpdateFrequency = 2
|
|
AdminPoll = 3
|
|
AdminChat = 4
|
|
AdminRcon = 5
|
|
AdminGamescript = 6
|
|
AdminPing = 7
|
|
AdminExternalChat = 8
|
|
AdminJoinSecure = 9
|
|
AdminAuthResponse = 10
|
|
|
|
ServerFull = 100
|
|
ServerBanned = 101
|
|
ServerError = 102
|
|
ServerProtocol = 103
|
|
ServerWelcome = 104
|
|
ServerNewGame = 105
|
|
ServerShutdown = 106
|
|
|
|
ServerDate = 107
|
|
ServerClientJoin = 108
|
|
ServerClientInfo = 109
|
|
ServerClientUpdate = 110
|
|
ServerClientQuit = 111
|
|
ServerClientError = 112
|
|
ServerCompanyNew = 113
|
|
ServerCompanyInfo = 114
|
|
ServerCompanyUpdate = 115
|
|
ServerCompanyRemove = 116
|
|
ServerCompanyEconomy = 117
|
|
ServerCompanyStats = 118
|
|
ServerChat = 119
|
|
ServerRcon = 120
|
|
ServerConsole = 121
|
|
ServerCmdNames = 122
|
|
ServerCmdLoggingOld = 123
|
|
ServerGamescript = 124
|
|
ServerRconEnd = 125
|
|
ServerPong = 126
|
|
ServerCmdLogging = 127
|
|
ServerAuthRequest = 128
|
|
ServerEnableEncryption = 129
|
|
|
|
PACKET_END = 130
|
|
|
|
class AdminUpdateFrequency(IntEnum):
|
|
Poll = 1 << 0
|
|
Daily = 1 << 1
|
|
Weekly = 1 << 2
|
|
Monthly = 1 << 3
|
|
Quarterly = 1 << 4
|
|
Annually = 1 << 5
|
|
Automatic = 1 << 6
|
|
|
|
class AdminUpdateType(IntEnum):
|
|
Date = 0
|
|
ClientInfo = 1
|
|
CompanyInfo = 2
|
|
CompanyEconomy = 3
|
|
CompanyStats = 4
|
|
Chat = 5
|
|
Console = 6
|
|
CmdNames = 7
|
|
CmdLogging = 8
|
|
Gamescript = 9
|
|
End = 10
|
|
|
|
class NetworkAuthenticationMethod(IntEnum):
|
|
X25519_KeyExchangeOnly = 0
|
|
X25519_PAKE = 1
|
|
X25519_AuthorizedKey = 2
|
|
|
|
# Version of the AdminBridge GameScript's JSON protocol this client is written against. The
|
|
# bridge reports its own version via get_version (OpenTTDAdminClient.get_bridge_version()), and
|
|
# an older one will not understand everything sent here. The bridge source lives in
|
|
# gamescript/AdminBridge/; tests/test_gamescript.py keeps this in step with the version declared
|
|
# there, so bumping one without the other fails in CI rather than at runtime.
|
|
GS_BRIDGE_VERSION = 4
|
|
|
|
class GameEventType(StrEnum):
|
|
"""Event kinds the AdminBridge GameScript can push over the Admin Network.
|
|
|
|
These are the values of the "event" field of each event dict, and what
|
|
OpenTTDAdminClient.subscribe_events() and wait_for_event() take. They are plain strings,
|
|
so a bare "vehicle_arrive" works everywhere a member does.
|
|
|
|
The first three are synthesised by the GameScript sampling game state on an interval,
|
|
because the engine raises no event for them; the rest are engine events forwarded as they
|
|
happen. VehicleLost, VehicleWaitingInDepot and VehicleUnprofitable have deliberately no
|
|
entry here: the engine only ever raises those for AI companies, never for a GameScript.
|
|
"""
|
|
|
|
# Polled: derived by diffing successive samples of the game state.
|
|
VehicleArrive = "vehicle_arrive"
|
|
VehicleDepart = "vehicle_depart"
|
|
CargoWaiting = "cargo_waiting"
|
|
# Forwarded straight from the engine's own GameScript events.
|
|
VehicleCrashed = "vehicle_crashed"
|
|
StationFirstVehicle = "station_first_vehicle"
|
|
IndustryOpen = "industry_open"
|
|
IndustryClose = "industry_close"
|
|
TownFounded = "town_founded"
|
|
CompanyNew = "company_new"
|
|
CompanyInTrouble = "company_in_trouble"
|
|
CompanyBankrupt = "company_bankrupt"
|
|
SubsidyOffer = "subsidy_offer"
|
|
SubsidyOfferExpired = "subsidy_offer_expired"
|
|
SubsidyAwarded = "subsidy_awarded"
|
|
SubsidyExpired = "subsidy_expired"
|
|
# Emitted by the bridge itself, never subscribed to: one poll produced more events than
|
|
# fit in the per-poll cap and `count` of them were discarded.
|
|
EventsDropped = "events_dropped"
|
|
|
|
class OpenTTDProtocol(TCPProtocol):
|
|
"""Low-level OpenTTD TCP protocol handler with encryption support."""
|
|
PacketType = PacketGameType
|
|
PACKET_END = PacketGameType.PACKET_END
|
|
|
|
def __init__(self, handler):
|
|
super().__init__(handler)
|
|
self.handler = handler
|
|
|
|
def receive_packet(self, source, data):
|
|
try:
|
|
if self.handler.encryption_enabled:
|
|
if not self.handler._recv_aead:
|
|
self.handler._recv_aead = monocypher.IncrementalAuthenticatedEncryption(self.handler._session_key_recv, self.handler._encryption_nonce)
|
|
_, rest = read_uint16(data)
|
|
payload = self.handler._recv_aead.unlock(bytes(rest[:16]), bytes(rest[16:]))
|
|
if payload is None:
|
|
raise SocketClosed("Decryption failed")
|
|
data = memoryview(struct.pack("<H", len(payload) + 2) + payload)
|
|
|
|
return super().receive_packet(source, data)
|
|
except Exception: # noqa: BLE001 - untrusted wire data: any decode failure must degrade to a no-op packet rather than kill the connection
|
|
return PacketGameType.ServerUnused, {}
|
|
|
|
async def send_packet(self, data):
|
|
if self.handler.encryption_enabled:
|
|
if not self.handler._send_aead:
|
|
self.handler._send_aead = monocypher.IncrementalAuthenticatedEncryption(self.handler._session_key_send, self.handler._encryption_nonce)
|
|
_, payload = read_uint16(memoryview(data))
|
|
mac, ciphertext = self.handler._send_aead.lock(payload.tobytes())
|
|
data = struct.pack("<H", 18 + len(ciphertext)) + mac + ciphertext
|
|
|
|
await self._can_write.wait()
|
|
if self.transport.is_closing():
|
|
raise SocketClosed
|
|
self.transport.write(data)
|
|
return len(data)
|
|
|
|
# --- Static Parsers ---
|
|
@staticmethod
|
|
def receive_ServerGameInfo(source, data):
|
|
from openttd_protocol.protocol.game import GameProtocol
|
|
return GameProtocol.receive_PACKET_SERVER_GAME_INFO(source, data)
|
|
@staticmethod
|
|
def receive_ServerError(source, data):
|
|
ec, _ = read_uint8(data)
|
|
return {"error_code": ec}
|
|
@staticmethod
|
|
def receive_ServerAuthenticationRequest(source, data):
|
|
at, rest = read_uint8(data)
|
|
return {"auth_type": at, "data": rest}
|
|
@staticmethod
|
|
def receive_ServerEnableEncryption(source, data): return {"data": data}
|
|
@staticmethod
|
|
def receive_ServerCheckNewGRFs(source, data): return {}
|
|
@staticmethod
|
|
def receive_ServerUnused(source, data): return {}
|
|
@staticmethod
|
|
def receive_ServerWelcome(source, data):
|
|
cid, _ = read_uint32(data)
|
|
return {"client_id": cid}
|
|
@staticmethod
|
|
def receive_ServerNeedCompanyPassword(source, data):
|
|
seed, data = read_uint32(data)
|
|
sid, _ = read_string(data)
|
|
return {"seed": seed, "server_id": sid}
|
|
@staticmethod
|
|
def receive_ServerFrame(source, data):
|
|
f, data = read_uint32(data)
|
|
_, data = read_uint32(data)
|
|
token = 0
|
|
if len(data) > 0:
|
|
if len(data) >= 13:
|
|
data = data[12:]
|
|
token, _ = read_uint8(data)
|
|
return {"frame": f, "token": token}
|
|
@staticmethod
|
|
def receive_ServerChat(source, data):
|
|
_, data = read_uint8(data)
|
|
cid, data = read_uint32(data)
|
|
_, data = read_uint8(data)
|
|
msg, _ = read_string(data)
|
|
return {"client_id": cid, "message": msg}
|
|
@staticmethod
|
|
def receive_ServerCompanyUpdate(source, data):
|
|
mask, _ = read_uint16(data)
|
|
return {"passworded_mask": mask}
|
|
@staticmethod
|
|
def receive_ServerMapDone(source, data): return {}
|
|
@staticmethod
|
|
def receive_ServerClientInfo(source, data): return {}
|
|
@staticmethod
|
|
def receive_ServerSync(source, data): return {}
|
|
@staticmethod
|
|
def receive_ServerClientJoined(source, data): return {}
|
|
@staticmethod
|
|
def receive_ServerMapBegin(source, data): return {}
|
|
@staticmethod
|
|
def receive_ServerMapSize(source, data): return {"size": 0}
|
|
@staticmethod
|
|
def receive_ServerMapData(source, data): return {"data": data}
|
|
@staticmethod
|
|
def receive_ServerConfigurationUpdate(source, data): return {}
|
|
@staticmethod
|
|
def receive_ServerExternalChat(source, data): return {}
|
|
@staticmethod
|
|
def receive_ServerCommand(source, data):
|
|
company, data = read_uint8(data)
|
|
cmd, data = read_uint16(data)
|
|
error_msg, data = read_uint16(data)
|
|
tile, data = read_uint32(data)
|
|
payload_len, data = read_uint16(data)
|
|
payload = data[:payload_len]
|
|
data = data[payload_len:]
|
|
callback, data = read_uint8(data)
|
|
callback_param = 0
|
|
if callback != 0:
|
|
callback_param, data = read_uint32(data)
|
|
frame, data = read_uint32(data)
|
|
my_cmd, _ = read_uint8(data)
|
|
return {
|
|
"company": company, "cmd": cmd, "error_msg": error_msg, "tile": tile,
|
|
"payload": payload, "callback": callback, "callback_param": callback_param,
|
|
"frame": frame, "my_cmd": bool(my_cmd)
|
|
}
|
|
@staticmethod
|
|
def receive_ServerFull(source, data): return {}
|
|
@staticmethod
|
|
def receive_ServerBanned(source, data): return {}
|
|
@staticmethod
|
|
def receive_ClientAck(source, data):
|
|
f, data = read_uint32(data)
|
|
t, _ = read_uint8(data)
|
|
return {"frame": f, "token": t}
|
|
@staticmethod
|
|
def receive_ClientIdentify(source, data): return {}
|
|
|
|
class OpenTTDAdminProtocol(OpenTTDProtocol):
|
|
"""Low-level OpenTTD Admin TCP protocol handler."""
|
|
PacketType = PacketAdminType
|
|
PACKET_END = PacketAdminType.PACKET_END
|
|
|
|
@staticmethod
|
|
def receive_ServerProtocol(source, data):
|
|
v, data = read_uint8(data)
|
|
updates = {}
|
|
while True:
|
|
has_more, data = read_uint8(data)
|
|
if not has_more:
|
|
break
|
|
ut, data = read_uint16(data)
|
|
freqs, data = read_uint16(data)
|
|
updates[ut] = freqs
|
|
return {"version": v, "updates": updates}
|
|
|
|
@staticmethod
|
|
def receive_ServerWelcome(source, data):
|
|
name, data = read_string(data)
|
|
ver, data = read_string(data)
|
|
dedi, data = read_uint8(data)
|
|
map_name, data = read_string(data)
|
|
seed, data = read_uint32(data)
|
|
land, data = read_uint8(data)
|
|
date, data = read_uint32(data)
|
|
width, data = read_uint16(data)
|
|
height, _ = read_uint16(data)
|
|
return {
|
|
"server_name": name, "openttd_version": ver, "dedicated": bool(dedi),
|
|
"map_name": map_name, "generation_seed": seed, "landscape": land,
|
|
"start_date": date, "map_width": width, "map_height": height
|
|
}
|
|
|
|
@staticmethod
|
|
def receive_ServerDate(source, data):
|
|
d, _ = read_uint32(data)
|
|
return {"date": d}
|
|
|
|
@staticmethod
|
|
def receive_ServerChat(source, data):
|
|
action, data = read_uint8(data)
|
|
dest_type, data = read_uint8(data)
|
|
cid, data = read_uint32(data)
|
|
msg, data = read_string(data)
|
|
money = 0
|
|
if len(data) >= 8:
|
|
import struct
|
|
money = struct.unpack("<Q", data[:8])[0]
|
|
return {"action": action, "dest_type": dest_type, "client_id": cid, "message": msg, "money": money}
|
|
|
|
@staticmethod
|
|
def receive_ServerConsole(source, data):
|
|
origin, data = read_string(data)
|
|
text, _ = read_string(data)
|
|
return {"origin": origin, "text": text}
|
|
|
|
@staticmethod
|
|
def receive_ServerRcon(source, data):
|
|
color, data = read_uint16(data)
|
|
text, _ = read_string(data)
|
|
return {"color": color, "text": text}
|
|
|
|
@staticmethod
|
|
def receive_ServerRconEnd(source, data):
|
|
cmd, _ = read_string(data)
|
|
return {"command": cmd}
|
|
|
|
@staticmethod
|
|
def receive_ServerAuthRequest(source, data):
|
|
at, rest = read_uint8(data)
|
|
return {"auth_type": at, "data": rest}
|
|
|
|
@staticmethod
|
|
def receive_ServerEnableEncryption(source, data): return {"data": data}
|
|
@staticmethod
|
|
def receive_ServerError(source, data):
|
|
ec, _ = read_uint8(data)
|
|
return {"error_code": ec}
|
|
@staticmethod
|
|
def receive_ServerFull(source, data): return {}
|
|
@staticmethod
|
|
def receive_ServerBanned(source, data): return {}
|
|
@staticmethod
|
|
def receive_ServerShutdown(source, data): return {}
|
|
@staticmethod
|
|
def receive_ServerNewGame(source, data): return {}
|
|
@staticmethod
|
|
def receive_ServerClientJoin(source, data):
|
|
cid, _ = read_uint32(data)
|
|
return {"client_id": cid}
|
|
|
|
@staticmethod
|
|
def receive_ServerClientInfo(source, data):
|
|
cid, data = read_uint32(data)
|
|
addr, data = read_string(data)
|
|
name, data = read_string(data)
|
|
lang, data = read_uint8(data)
|
|
date, data = read_uint32(data)
|
|
playas, _ = read_uint8(data)
|
|
return {
|
|
"client_id": cid, "network_address": addr, "name": name,
|
|
"language": lang, "join_date": date, "play_as": playas
|
|
}
|
|
|
|
@staticmethod
|
|
def receive_ServerClientUpdate(source, data):
|
|
cid, data = read_uint32(data)
|
|
name, data = read_string(data)
|
|
playas, _ = read_uint8(data)
|
|
return {"client_id": cid, "name": name, "play_as": playas}
|
|
|
|
@staticmethod
|
|
def receive_ServerClientQuit(source, data):
|
|
cid, _ = read_uint32(data)
|
|
return {"client_id": cid}
|
|
|
|
@staticmethod
|
|
def receive_ServerClientError(source, data):
|
|
cid, data = read_uint32(data)
|
|
error, _ = read_uint8(data)
|
|
return {"client_id": cid, "error_code": error}
|
|
|
|
@staticmethod
|
|
def receive_ServerCompanyNew(source, data):
|
|
cid, _ = read_uint8(data)
|
|
return {"company_id": cid}
|
|
|
|
@staticmethod
|
|
def receive_ServerCompanyInfo(source, data):
|
|
cid, data = read_uint8(data)
|
|
name, data = read_string(data)
|
|
manager, data = read_string(data)
|
|
color, data = read_uint8(data)
|
|
protected, data = read_uint8(data)
|
|
year, data = read_uint32(data)
|
|
is_ai, _ = read_uint8(data)
|
|
return {
|
|
"company_id": cid, "name": name, "manager_name": manager,
|
|
"color": color, "password_protected": bool(protected),
|
|
"inaugurated_year": year, "is_ai": bool(is_ai)
|
|
}
|
|
|
|
@staticmethod
|
|
def receive_ServerCompanyUpdate(source, data):
|
|
cid, data = read_uint8(data)
|
|
name, data = read_string(data)
|
|
manager, data = read_string(data)
|
|
color, data = read_uint8(data)
|
|
protected, data = read_uint8(data)
|
|
bankrupt, data = read_uint8(data)
|
|
s1, data = read_uint8(data)
|
|
s2, data = read_uint8(data)
|
|
s3, data = read_uint8(data)
|
|
s4, _ = read_uint8(data)
|
|
return {
|
|
"company_id": cid, "name": name, "manager_name": manager,
|
|
"color": color, "password_protected": bool(protected),
|
|
"quarters_of_bankruptcy": bankrupt, "share_owners": [s1, s2, s3, s4]
|
|
}
|
|
|
|
@staticmethod
|
|
def receive_ServerCompanyRemove(source, data):
|
|
cid, data = read_uint8(data)
|
|
reason, _ = read_uint8(data)
|
|
return {"company_id": cid, "reason": reason}
|
|
|
|
@staticmethod
|
|
def receive_ServerCompanyEconomy(source, data):
|
|
import struct
|
|
cid, data = read_uint8(data)
|
|
money = struct.unpack("<Q", data[:8])[0]
|
|
data = data[8:]
|
|
loan = struct.unpack("<Q", data[:8])[0]
|
|
data = data[8:]
|
|
income = struct.unpack("<q", data[:8])[0]
|
|
data = data[8:]
|
|
delivered, data = read_uint16(data)
|
|
val_lq = struct.unpack("<Q", data[:8])[0]
|
|
data = data[8:]
|
|
perf_lq, data = read_uint16(data)
|
|
del_lq, data = read_uint16(data)
|
|
val_pq = struct.unpack("<Q", data[:8])[0]
|
|
data = data[8:]
|
|
perf_pq, data = read_uint16(data)
|
|
del_pq, _ = read_uint16(data)
|
|
return {
|
|
"company_id": cid, "money": money, "loan": loan, "income": income,
|
|
"delivered_cargo": delivered, "value_last_quarter": val_lq,
|
|
"performance_last_quarter": perf_lq, "delivered_cargo_last_quarter": del_lq,
|
|
"value_previous_quarter": val_pq, "performance_previous_quarter": perf_pq,
|
|
"delivered_cargo_previous_quarter": del_pq
|
|
}
|
|
|
|
@staticmethod
|
|
def receive_ServerCompanyStats(source, data):
|
|
cid, data = read_uint8(data)
|
|
trains, data = read_uint16(data)
|
|
lorries, data = read_uint16(data)
|
|
buses, data = read_uint16(data)
|
|
planes, data = read_uint16(data)
|
|
ships, data = read_uint16(data)
|
|
t_stations, data = read_uint16(data)
|
|
l_stations, data = read_uint16(data)
|
|
b_stops, data = read_uint16(data)
|
|
airports, data = read_uint16(data)
|
|
harbours, _ = read_uint16(data)
|
|
return {
|
|
"company_id": cid,
|
|
"vehicles": {"trains": trains, "lorries": lorries, "buses": buses, "planes": planes, "ships": ships},
|
|
"stations": {"train": t_stations, "lorry": l_stations, "bus": b_stops, "airport": airports, "harbour": harbours}
|
|
}
|
|
|
|
@staticmethod
|
|
def receive_ServerGamescript(source, data):
|
|
json_str, _ = read_string(data)
|
|
import json
|
|
try:
|
|
return {"data": json.loads(json_str)}
|
|
except json.JSONDecodeError:
|
|
return {"raw_data": json_str}
|
|
|
|
@staticmethod
|
|
def receive_ServerPong(source, data):
|
|
payload, _ = read_uint32(data)
|
|
return {"payload": payload}
|
|
|
|
|