Add scheduled dispatch support (edit + authoritative view)
Editing (game port, OpenTTDClient): a core of JGRPP's scheduled dispatch DoCommands — set_scheduled_dispatch (enable/disable), add/remove schedule, add/remove/clear slots, and set duration/start date. Adds the command IDs to protocol.py. Viewing (admin, OpenTTDAdminClient.get_dispatch): the GameScript API has no dispatch support, so a new server patch (docker/patches/0002-*) adds read-only GSOrder.GetScheduledDispatch* / IsScheduledDispatchEnabled getters, an AdminBridge GameScript get_dispatch handler exposes them, and get_dispatch() returns the live schedules and slots (mirrors get_timetable). Note: set_dispatch_start_date values are normalised by the engine relative to current game time, so they read back offset from the requested value. Includes unit + e2e tests, a demo in main.py, and protocol/timetable docs. The AdminBridge GameScript and the patched OpenTTD-patches clone live outside this repo; the 0002 patch file is the durable source for the latter. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,7 @@ import pytest
|
||||
from openttd import OpenTTDClient
|
||||
from openttd.protocol import (
|
||||
OpenTTDProtocol, GameCommand, ModifyTimetableFlags, ModifyTimetableCtrlFlag,
|
||||
OrderType, OrderNonStopFlags, OrderStopLocation, INVALID_VEH_ORDER_ID,
|
||||
write_varuint, read_varuint, write_varuint_signed, read_varuint_signed
|
||||
)
|
||||
from openttd_protocol.wire.read import read_uint8, read_uint16
|
||||
@@ -180,6 +181,155 @@ async def test_client_set_vehicle_on_time_sends_expected_payload():
|
||||
assert (vehicle_id, apply_to_group) == (7, 1)
|
||||
|
||||
|
||||
# --- OpenTTDClient order add/remove commands ---
|
||||
|
||||
def _decode_insert_order_payload(payload):
|
||||
vehicle_id, rest = read_varuint(payload)
|
||||
sel_ord, rest = read_uint16(rest)
|
||||
order_type, rest = read_uint8(rest)
|
||||
order_flags, rest = read_uint16(rest)
|
||||
station, _ = read_uint16(rest)
|
||||
return vehicle_id, sel_ord, order_type, order_flags, station
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_client_add_order_appends_by_default():
|
||||
client = new_client()
|
||||
await client.add_order(7, 6)
|
||||
assert len(client._protocol.sent) == 1
|
||||
parsed = decode_sent_command(client._protocol.sent[0])
|
||||
assert parsed["cmd"] == GameCommand.InsertOrder
|
||||
assert parsed["company"] == 0
|
||||
vehicle_id, sel_ord, order_type, order_flags, station = _decode_insert_order_payload(parsed["payload"])
|
||||
assert vehicle_id == 7
|
||||
assert sel_ord == INVALID_VEH_ORDER_ID # append to the end
|
||||
# OT_GOTO_STATION in bits 0-3, far-end stop location in bits 4-5, stop-everywhere non-stop in 6-7
|
||||
assert order_type == (OrderType.GotoStation | (OrderStopLocation.PlatformFarEnd << 4))
|
||||
assert order_flags == 0
|
||||
assert station == 6
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_client_add_order_insert_position_and_nonstop():
|
||||
client = new_client()
|
||||
await client.add_order(7, 6, before_position=1, non_stop=OrderNonStopFlags.NoStopAtIntermediate)
|
||||
parsed = decode_sent_command(client._protocol.sent[0])
|
||||
vehicle_id, sel_ord, order_type, order_flags, station = _decode_insert_order_payload(parsed["payload"])
|
||||
assert sel_ord == 1 # insert before order position 1
|
||||
assert order_type == (OrderType.GotoStation
|
||||
| (OrderStopLocation.PlatformFarEnd << 4)
|
||||
| (OrderNonStopFlags.NoStopAtIntermediate << 6))
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_client_remove_order_sends_expected_payload():
|
||||
client = new_client()
|
||||
await client.remove_order(7, 2)
|
||||
parsed = decode_sent_command(client._protocol.sent[0])
|
||||
assert parsed["cmd"] == GameCommand.DeleteOrder
|
||||
vehicle_id, rest = read_varuint(parsed["payload"])
|
||||
order_position, _ = read_uint16(rest)
|
||||
assert (vehicle_id, order_position) == (7, 2)
|
||||
|
||||
|
||||
# --- OpenTTDClient scheduled dispatch edit commands ---
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_client_set_scheduled_dispatch_payload():
|
||||
client = new_client()
|
||||
await client.set_scheduled_dispatch(7, True)
|
||||
parsed = decode_sent_command(client._protocol.sent[0])
|
||||
assert parsed["cmd"] == GameCommand.SchDispatch
|
||||
vehicle_id, rest = read_varuint(parsed["payload"])
|
||||
enabled, _ = read_uint8(rest)
|
||||
assert (vehicle_id, enabled) == (7, 1)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_client_add_dispatch_schedule_payload():
|
||||
client = new_client()
|
||||
await client.add_dispatch_schedule(7, -1234, 3000)
|
||||
parsed = decode_sent_command(client._protocol.sent[0])
|
||||
assert parsed["cmd"] == GameCommand.SchDispatchAddNewSchedule
|
||||
vehicle_id, rest = read_varuint(parsed["payload"])
|
||||
start_tick, rest = read_varuint_signed(rest)
|
||||
duration, _ = read_varuint(rest)
|
||||
assert (vehicle_id, start_tick, duration) == (7, -1234, 3000)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_client_remove_dispatch_schedule_payload():
|
||||
client = new_client()
|
||||
await client.remove_dispatch_schedule(7, 2)
|
||||
parsed = decode_sent_command(client._protocol.sent[0])
|
||||
assert parsed["cmd"] == GameCommand.SchDispatchRemoveSchedule
|
||||
vehicle_id, rest = read_varuint(parsed["payload"])
|
||||
schedule_index, _ = read_varuint(rest)
|
||||
assert (vehicle_id, schedule_index) == (7, 2)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_client_add_dispatch_slot_payload_defaults_and_extras():
|
||||
client = new_client()
|
||||
# Defaults: single slot, no interval/extra/flags/route.
|
||||
await client.add_dispatch_slot(7, 1, 500)
|
||||
# Bulk: three extra slots spaced 250 ticks apart, with flags and route id.
|
||||
await client.add_dispatch_slot(7, 1, 500, interval=250, extra_slots=3, slot_flags=5, route_id=2)
|
||||
|
||||
def decode(payload):
|
||||
vehicle_id, rest = read_varuint(payload)
|
||||
schedule_index, rest = read_varuint(rest)
|
||||
offset, rest = read_varuint(rest)
|
||||
interval, rest = read_varuint(rest)
|
||||
extra_slots, rest = read_varuint(rest)
|
||||
slot_flags, rest = read_uint16(rest)
|
||||
route_id, _ = read_uint8(rest)
|
||||
return (vehicle_id, schedule_index, offset, interval, extra_slots, slot_flags, route_id)
|
||||
|
||||
p0 = decode_sent_command(client._protocol.sent[0])
|
||||
p1 = decode_sent_command(client._protocol.sent[1])
|
||||
assert p0["cmd"] == GameCommand.SchDispatchAdd
|
||||
assert decode(p0["payload"]) == (7, 1, 500, 0, 0, 0, 0)
|
||||
assert decode(p1["payload"]) == (7, 1, 500, 250, 3, 5, 2)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_client_remove_dispatch_slot_payload():
|
||||
client = new_client()
|
||||
await client.remove_dispatch_slot(7, 1, 500)
|
||||
parsed = decode_sent_command(client._protocol.sent[0])
|
||||
assert parsed["cmd"] == GameCommand.SchDispatchRemove
|
||||
vehicle_id, rest = read_varuint(parsed["payload"])
|
||||
schedule_index, rest = read_varuint(rest)
|
||||
offset, _ = read_varuint(rest)
|
||||
assert (vehicle_id, schedule_index, offset) == (7, 1, 500)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_client_clear_dispatch_schedule_payload():
|
||||
client = new_client()
|
||||
await client.clear_dispatch_schedule(7, 1)
|
||||
parsed = decode_sent_command(client._protocol.sent[0])
|
||||
assert parsed["cmd"] == GameCommand.SchDispatchClear
|
||||
vehicle_id, rest = read_varuint(parsed["payload"])
|
||||
schedule_index, _ = read_varuint(rest)
|
||||
assert (vehicle_id, schedule_index) == (7, 1)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_client_set_dispatch_duration_payload():
|
||||
client = new_client()
|
||||
await client.set_dispatch_duration(7, 1, 4000)
|
||||
parsed = decode_sent_command(client._protocol.sent[0])
|
||||
assert parsed["cmd"] == GameCommand.SchDispatchSetDuration
|
||||
vehicle_id, rest = read_varuint(parsed["payload"])
|
||||
schedule_index, rest = read_varuint(rest)
|
||||
duration, _ = read_varuint(rest)
|
||||
assert (vehicle_id, schedule_index, duration) == (7, 1, 4000)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_client_set_dispatch_start_date_payload():
|
||||
client = new_client()
|
||||
await client.set_dispatch_start_date(7, 1, 1_000_000)
|
||||
parsed = decode_sent_command(client._protocol.sent[0])
|
||||
assert parsed["cmd"] == GameCommand.SchDispatchSetStartDate
|
||||
vehicle_id, rest = read_varuint(parsed["payload"])
|
||||
schedule_index, rest = read_varuint(rest)
|
||||
start_tick, _ = read_varuint_signed(rest)
|
||||
assert (vehicle_id, schedule_index, start_tick) == (7, 1, 1_000_000)
|
||||
|
||||
|
||||
# --- OpenTTDClient.receive_ServerCommand dispatch ---
|
||||
|
||||
async def feed_command(client, cmd, payload):
|
||||
|
||||
Reference in New Issue
Block a user