class AdminBridge extends GSController { /* --- Event subscription state (see HandleSubscribeEvents) --- */ event_kinds = null; // table of subscribed kind -> true; null when not subscribed event_order = null; // canonical kind ordering, so subscribe replies are stable event_catalog = null; // table of every supported kind -> true, for validation interval = 10; // ticks between state polls sleep_ticks = 10; // ticks slept per main-loop iteration last_poll = 0; // tick of the last state poll seeded = false; // false until the first poll has recorded a state baseline company_id = null; // optional owner filter for the polled vehicle/station events watch_vehicles = null; // explicit vehicle ids to poll; null = every vehicle watch_stations = null; // explicit station ids to poll; null = every station cargo_ids = null; // cargo ids to inspect; set at subscribe time min_cargo_delta = 1; // smallest waiting-amount change worth a cargo_waiting event include_cargo = true; // attach each vehicle's load to arrive/depart events vehicle_at = null; // vehicle id -> { station = id or -1, since = tick } cargo_prev = null; // station id -> table of cargo id -> last seen waiting amount /* Cap on how many events one poll may emit. A first poll over a busy map can otherwise * produce thousands at once; past the cap the rest are dropped and reported as such. */ MAX_EVENTS_PER_POLL = 200; /* Events per admin packet. Kept well under the packet size limit so a batch always fits. */ EVENT_BATCH_SIZE = 24; /* Version of the JSON protocol spoken over the admin port, reported by get_version. * A GameScript cannot read its own info.nut at runtime (GSController.GetVersion() returns * the *OpenTTD* version), so this duplicates info.nut's GetVersion() and the two are pinned * together — along with the client's openttd.protocol.GS_BRIDGE_VERSION — by * tests/test_gamescript.py. Bump all three whenever the protocol changes. */ BRIDGE_VERSION = 4; /* Every command the bridge answers: name -> handler method plus the request fields that * handler needs. HandleCommand dispatches through this and get_version reports it, so the * catalogue a client feature-detects against cannot drift from what is implemented. */ COMMANDS = { get_version = { handler = "HandleGetVersion", requires = [] }, list_vehicles = { handler = "HandleListVehicles", requires = [] }, list_stations = { handler = "HandleListStations", requires = [] }, list_cargo = { handler = "HandleListCargo", requires = [] }, get_timetable = { handler = "HandleGetTimetable", requires = ["vehicle_id"] }, get_station = { handler = "HandleGetStation", requires = ["station_id"] }, get_station_cargo = { handler = "HandleGetStationCargo", requires = ["station_id", "cargo_id"] }, get_dispatch = { handler = "HandleGetDispatch", requires = ["vehicle_id"] }, subscribe_events = { handler = "HandleSubscribeEvents", requires = [] }, unsubscribe_events = { handler = "HandleUnsubscribeEvents", requires = [] }, }; function InitEvents() { this.ResetEventState(); this.event_order = [ /* Synthesised by polling game state (PollState). */ "vehicle_arrive", "vehicle_depart", "cargo_waiting", /* Native GameScript events, forwarded as they arrive (HandleNativeEvent). */ "vehicle_crashed", "station_first_vehicle", "industry_open", "industry_close", "town_founded", "company_new", "company_in_trouble", "company_bankrupt", "subsidy_offer", "subsidy_offer_expired", "subsidy_awarded", "subsidy_expired" ]; this.event_catalog = {}; foreach (kind in this.event_order) this.event_catalog[kind] <- true; } function Start() { GSLog.Info("AdminBridge started."); this.InitEvents(); while (true) { this.Sleep(this.sleep_ticks); local pending = []; local event = GSEventController.GetNextEvent(); while (event != null) { if (event.GetEventType() == GSEvent.ET_ADMIN_PORT) { local admin_event = GSEventAdminPort.Convert(event); local request = admin_event.GetObject(); if (request != null && "command" in request) this.HandleCommand(request); } else if (this.event_kinds != null) { this.HandleNativeEvent(event, pending); } event = GSEventController.GetNextEvent(); } if (this.event_kinds != null) this.PollState(pending); this.SendEventBatch(pending); } } function HandleCommand(request) { if (!(request.command in this.COMMANDS)) return; local spec = this.COMMANDS[request.command]; /* A command missing the fields its handler needs is dropped rather than answered: * there is nothing to answer *about*, and no request_id is guaranteed either. */ foreach (field in spec.requires) { if (!(field in request)) return; } this[spec.handler](request); } /* Version handshake. A client expecting a newer bridge than the server runs would otherwise * just time out on the first command this bridge does not know; asking here turns that into * an answer. The two catalogues let a client feature-detect, which survives a bridge that * gained commands out of version order better than comparing numbers does. */ function HandleGetVersion(request) { local commands = []; foreach (name, _ in this.COMMANDS) commands.append(name); commands.sort(); // table iteration order is arbitrary; keep the reply stable local events = []; foreach (kind in this.event_order) events.append(kind); local reply = { command = "get_version", version = this.BRIDGE_VERSION, commands = commands, events = events }; if ("request_id" in request) reply.request_id <- request.request_id; GSAdmin.Send(reply); } /* Cargo id -> label ("PASS", "COAL", ...) so callers can name what a vehicle carries. * Labels come from the NewGRF cargo table, so ids are not stable across games. */ function HandleListCargo(request) { local cargo = []; foreach (c, _ in GSCargoList()) { local label = ""; try { label = GSCargo.GetCargoLabel(c); } catch (e) { label = ""; } local freight = 0; try { freight = GSCargo.IsFreight(c) ? 1 : 0; } catch (e) { freight = 0; } cargo.append({ cargo_id = c, label = label, freight = freight }); } local reply = { command = "list_cargo", cargo = cargo }; if ("request_id" in request) reply.request_id <- request.request_id; GSAdmin.Send(reply); } function HandleListVehicles(request) { local vehicle_list = GSVehicleList(); local vehicles = []; foreach (v, _ in vehicle_list) { if (!GSVehicle.IsValidVehicle(v)) continue; local next_stop = "None"; if (GSOrder.GetOrderCount(v) > 0) { local dest_tile = GSOrder.GetOrderDestination(v, GSOrder.ORDER_CURRENT); local station_id = GSStation.GetStationID(dest_tile); if (GSStation.IsValidStation(station_id)) next_stop = GSStation.GetName(station_id); } /* Which cargoes this vehicle can actually carry. A vehicle (an articulated * train especially) can have capacity for several cargo types, so report * every one with a non-zero capacity rather than a single "cargo type". */ local cargo = []; foreach (c, _ in GSCargoList()) { local cap = GSVehicle.GetCapacity(v, c); if (cap > 0) cargo.append({ cargo_id = c, capacity = cap }); } vehicles.append({ id = v, age = GSVehicle.GetAge(v), max_age = GSVehicle.GetMaxAge(v), next_stop = next_stop, type = GSVehicle.GetVehicleType(v), owner = GSVehicle.GetOwner(v), order_count = GSOrder.GetOrderCount(v), cargo = cargo }); } local reply = { vehicles = vehicles }; if ("request_id" in request) reply.request_id <- request.request_id; GSAdmin.Send(reply); } function HandleGetTimetable(request) { local v = request.vehicle_id; local reply = { command = "get_timetable", vehicle_id = v }; if ("request_id" in request) reply.request_id <- request.request_id; if (!GSVehicle.IsValidVehicle(v)) { reply.error <- "invalid_vehicle"; GSAdmin.Send(reply); return; } reply.lateness <- GSOrder.GetTimetableLateness(v); reply.start_tick <- GSOrder.GetTimetableStartTick(v); reply.current_order_time <- GSOrder.GetCurrentOrderTime(v); reply.total_duration <- GSOrder.GetTimetableTotalDuration(v); local orders = []; local count = GSOrder.GetOrderCount(v); for (local i = 0; i < count; i++) { /* Resolve the order's destination station so callers can build a station * graph (vertices = stations, edges = segments). Non-station orders * (depots, waypoints, conditional) have no station: report -1. */ local station_id = -1; if (GSOrder.IsValidVehicleOrder(v, i) && GSOrder.IsGotoStationOrder(v, i)) { local sid = GSStation.GetStationID(GSOrder.GetOrderDestination(v, i)); if (GSStation.IsValidStation(sid)) station_id = sid; } orders.append({ position = i, station_id = station_id, wait_time = GSOrder.GetTimetableWaitTime(v, i), travel_time = GSOrder.GetTimetableTravelTime(v, i), wait_timetabled = GSOrder.IsWaitTimetabled(v, i) ? 1 : 0, travel_timetabled = GSOrder.IsTravelTimetabled(v, i) ? 1 : 0, wait_fixed = GSOrder.IsWaitFixed(v, i) ? 1 : 0, travel_fixed = GSOrder.IsTravelFixed(v, i) ? 1 : 0, leave_type = GSOrder.GetLeaveType(v, i), max_speed = GSOrder.GetTimetableMaxSpeed(v, i) }); } reply.orders <- orders; if (!GSAdmin.Send(reply)) { /* Response exceeded the admin packet size limit; send a small error * instead so the waiting client fails fast rather than timing out. */ local fallback = { command = "get_timetable", vehicle_id = v, error = "response_too_large" }; if ("request_id" in request) fallback.request_id <- request.request_id; GSAdmin.Send(fallback); } } function HandleListStations(request) { /* GameScripts run as a deity, so GSStationList lists every company's stations; * an optional company_id filters to a single owner (the deity list ignores it). */ local filter_owner = ("company_id" in request) ? request.company_id : null; local station_list = GSStationList(GSStation.STATION_ANY); local stations = []; foreach (s, _ in station_list) { if (!GSStation.IsValidStation(s)) continue; local owner = GSStation.GetOwner(s); if (filter_owner != null && owner != filter_owner) continue; stations.append({ id = s, name = GSStation.GetName(s), location = GSStation.GetLocation(s), owner = owner }); } local reply = { command = "list_stations", stations = stations }; if ("request_id" in request) reply.request_id <- request.request_id; GSAdmin.Send(reply); } function HandleGetStation(request) { local sid = request.station_id; local reply = { command = "get_station", station_id = sid }; if ("request_id" in request) reply.request_id <- request.request_id; if (!GSStation.IsValidStation(sid)) { reply.error <- "invalid_station"; GSAdmin.Send(reply); return; } reply.name <- GSStation.GetName(sid); reply.location <- GSStation.GetLocation(sid); reply.owner <- GSStation.GetOwner(sid); local cargo = []; local cargo_list = GSCargoList(); foreach (c, _ in cargo_list) { local waiting = GSStation.GetCargoWaiting(sid, c); local planned = GSStation.GetCargoPlanned(sid, c); local has_rating = GSStation.HasCargoRating(sid, c); /* Skip cargo the station has never handled to keep the reply compact. */ if (waiting <= 0 && planned <= 0 && !has_rating) continue; cargo.append({ cargo_id = c, waiting = waiting, // real-time: units currently waiting planned = planned, // planned: cargodist link-graph flow rating = has_rating ? GSStation.GetCargoRating(sid, c) : null }); } reply.cargo <- cargo; if (!GSAdmin.Send(reply)) { /* Response exceeded the admin packet size limit; send a small error * instead so the waiting client fails fast rather than timing out. */ local fallback = { command = "get_station", station_id = sid, error = "response_too_large" }; if ("request_id" in request) fallback.request_id <- request.request_id; GSAdmin.Send(fallback); } } /* Convert a GSList of station_id -> amount into an array of {station, amount}, * dropping zero entries to keep the reply compact. */ function CargoListToPairs(list) { local pairs = []; foreach (station, _ in list) { local amount = list.GetValue(station); if (amount <= 0) continue; pairs.append({ station = station, amount = amount }); } return pairs; } function HandleGetStationCargo(request) { local sid = request.station_id; local cargo = request.cargo_id; local reply = { command = "get_station_cargo", station_id = sid, cargo_id = cargo }; if ("request_id" in request) reply.request_id <- request.request_id; if (!GSStation.IsValidStation(sid)) { reply.error <- "invalid_station"; GSAdmin.Send(reply); return; } if (!GSCargo.IsValidCargo(cargo)) { reply.error <- "invalid_cargo"; GSAdmin.Send(reply); return; } /* Optional source (from) and next-hop (via) filters. STATION_INVALID is a legal value * (deleted source / "via any"); any other non-station value is rejected. */ local from = null; local via = null; if ("from_station" in request) { from = request.from_station; if (from != GSStation.STATION_INVALID && !GSStation.IsValidStation(from)) { reply.error <- "invalid_from_station"; GSAdmin.Send(reply); return; } reply.from_station <- from; } if ("via_station" in request) { via = request.via_station; if (via != GSStation.STATION_INVALID && !GSStation.IsValidStation(via)) { reply.error <- "invalid_via_station"; GSAdmin.Send(reply); return; } reply.via_station <- via; } /* Totals, honouring whichever filters were supplied. */ if (from != null && via != null) { reply.waiting <- GSStation.GetCargoWaitingFromVia(sid, from, via, cargo); reply.planned <- GSStation.GetCargoPlannedFromVia(sid, from, via, cargo); } else if (from != null) { reply.waiting <- GSStation.GetCargoWaitingFrom(sid, from, cargo); reply.planned <- GSStation.GetCargoPlannedFrom(sid, from, cargo); } else if (via != null) { reply.waiting <- GSStation.GetCargoWaitingVia(sid, via, cargo); reply.planned <- GSStation.GetCargoPlannedVia(sid, via, cargo); } else { reply.waiting <- GSStation.GetCargoWaiting(sid, cargo); reply.planned <- GSStation.GetCargoPlanned(sid, cargo); } /* Breakdown grouped by source station (restricted to the via filter if given). */ local w_by_from = (via != null) ? GSStationList_CargoWaitingViaByFrom(sid, cargo, via) : GSStationList_CargoWaitingByFrom(sid, cargo); local p_by_from = (via != null) ? GSStationList_CargoPlannedViaByFrom(sid, cargo, via) : GSStationList_CargoPlannedByFrom(sid, cargo); reply.waiting_by_from <- this.CargoListToPairs(w_by_from); reply.planned_by_from <- this.CargoListToPairs(p_by_from); /* Breakdown grouped by next hop (restricted to the from filter if given). */ local w_by_via = (from != null) ? GSStationList_CargoWaitingFromByVia(sid, cargo, from) : GSStationList_CargoWaitingByVia(sid, cargo); local p_by_via = (from != null) ? GSStationList_CargoPlannedFromByVia(sid, cargo, from) : GSStationList_CargoPlannedByVia(sid, cargo); reply.waiting_by_via <- this.CargoListToPairs(w_by_via); reply.planned_by_via <- this.CargoListToPairs(p_by_via); if (!GSAdmin.Send(reply)) { /* Response exceeded the admin packet size limit; send a small error * instead so the waiting client fails fast rather than timing out. */ local fallback = { command = "get_station_cargo", station_id = sid, cargo_id = cargo, error = "response_too_large" }; if ("request_id" in request) fallback.request_id <- request.request_id; GSAdmin.Send(fallback); } } function HandleGetDispatch(request) { local v = request.vehicle_id; local reply = { command = "get_dispatch", vehicle_id = v }; if ("request_id" in request) reply.request_id <- request.request_id; local count = GSOrder.GetScheduledDispatchScheduleCount(v); if (count < 0) { reply.error <- "invalid_vehicle"; GSAdmin.Send(reply); return; } reply.enabled <- GSOrder.IsScheduledDispatchEnabled(v); local schedules = []; for (local s = 0; s < count; s++) { local slots = []; local slot_count = GSOrder.GetScheduledDispatchSlotCount(v, s); for (local k = 0; k < slot_count; k++) { slots.append({ offset = GSOrder.GetScheduledDispatchSlotOffset(v, s, k), flags = GSOrder.GetScheduledDispatchSlotFlags(v, s, k) }); } schedules.append({ index = s, duration = GSOrder.GetScheduledDispatchDuration(v, s), start_tick = GSOrder.GetScheduledDispatchStartTick(v, s), delay = GSOrder.GetScheduledDispatchDelay(v, s), reuse_slots = GSOrder.GetScheduledDispatchReuseSlots(v, s), slots = slots }); } reply.schedules <- schedules; if (!GSAdmin.Send(reply)) { /* Response exceeded the admin packet size limit; send a small error * instead so the waiting client fails fast rather than timing out. */ local fallback = { command = "get_dispatch", vehicle_id = v, error = "response_too_large" }; if ("request_id" in request) fallback.request_id <- request.request_id; GSAdmin.Send(fallback); } } /* --- Events --- * * The engine has no GameScript event for "a vehicle reached a stop" or "cargo arrived", * so those are synthesised here: every `interval` ticks the bridge samples the watched * vehicles and stations and emits an event for each change against the previous sample. * The handful of events the engine *does* raise for a deity script (crashes, industries, * companies, ...) are forwarded straight through. Everything is pushed to the admin port * unsolicited, batched as { command = "events", events = [...] }. */ function ResetEventState() { this.event_kinds = null; this.interval = 10; this.sleep_ticks = 10; this.last_poll = 0; this.seeded = false; this.company_id = null; this.watch_vehicles = null; this.watch_stations = null; this.cargo_ids = null; this.min_cargo_delta = 1; this.include_cargo = true; this.vehicle_at = {}; this.cargo_prev = {}; } /* Copy a request field that must be an array of integers, or null when absent. */ function ReadIdList(request, key) { if (!(key in request) || request[key] == null) return null; local out = []; foreach (id in request[key]) out.append(id); return out; } function HandleSubscribeEvents(request) { local reply = { command = "subscribe_events" }; if ("request_id" in request) reply.request_id <- request.request_id; /* Validate everything before touching the live subscription, so a rejected request * leaves whatever was subscribed before running untouched. */ local kinds = {}; if ("events" in request && request.events != null) { foreach (kind in request.events) { if (!(kind in this.event_catalog)) { reply.error <- "unknown_event"; reply.event <- kind; GSAdmin.Send(reply); return; } kinds[kind] <- true; } } else { foreach (kind in this.event_order) kinds[kind] <- true; } local new_interval = ("interval" in request) ? request.interval : 10; if (typeof new_interval != "integer" || new_interval < 1) { reply.error <- "invalid_interval"; GSAdmin.Send(reply); return; } local new_min_delta = ("min_cargo_delta" in request) ? request.min_cargo_delta : 1; if (typeof new_min_delta != "integer" || new_min_delta < 1) { reply.error <- "invalid_min_cargo_delta"; GSAdmin.Send(reply); return; } /* Cargo ids are validated up front because the amount getters below take them as a * precondition; station and vehicle ids are not, since they can vanish mid-subscription * anyway and are re-checked on every poll. */ local new_cargo = this.ReadIdList(request, "cargo"); if (new_cargo != null) { foreach (c in new_cargo) { if (!GSCargo.IsValidCargo(c)) { reply.error <- "invalid_cargo"; reply.cargo_id <- c; GSAdmin.Send(reply); return; } } } else { new_cargo = []; foreach (c, _ in GSCargoList()) new_cargo.append(c); } this.ResetEventState(); this.event_kinds = kinds; this.interval = new_interval; this.sleep_ticks = new_interval < 10 ? new_interval : 10; this.min_cargo_delta = new_min_delta; this.cargo_ids = new_cargo; this.watch_vehicles = this.ReadIdList(request, "vehicles"); this.watch_stations = this.ReadIdList(request, "stations"); if ("company_id" in request && request.company_id != null) this.company_id = request.company_id; if ("include_cargo" in request) this.include_cargo = request.include_cargo ? true : false; local accepted = []; foreach (kind in this.event_order) { if (kind in this.event_kinds) accepted.append(kind); } reply.events <- accepted; reply.interval <- this.interval; GSAdmin.Send(reply); } function HandleUnsubscribeEvents(request) { this.ResetEventState(); local reply = { command = "unsubscribe_events", events = [] }; if ("request_id" in request) reply.request_id <- request.request_id; GSAdmin.Send(reply); } function Wants(kind) { return this.event_kinds != null && (kind in this.event_kinds); } /* Forward the events the engine raises for a deity GameScript. Vehicle "lost", "waiting in * depot" and "unprofitable" are deliberately absent: the engine only ever raises those for * AI companies, so a GameScript can never observe them. */ function HandleNativeEvent(event, out) { local type = event.GetEventType(); local tick = this.GetTick(); if (type == GSEvent.ET_VEHICLE_CRASHED) { if (!this.Wants("vehicle_crashed")) return; local e = GSEventVehicleCrashed.Convert(event); out.append({ event = "vehicle_crashed", tick = tick, vehicle_id = e.GetVehicleID(), tile = e.GetCrashSite(), reason = e.GetCrashReason(), victims = e.GetVictims(), owner = e.GetVehicleOwner() }); } else if (type == GSEvent.ET_STATION_FIRST_VEHICLE) { if (!this.Wants("station_first_vehicle")) return; local e = GSEventStationFirstVehicle.Convert(event); out.append({ event = "station_first_vehicle", tick = tick, station_id = e.GetStationID(), vehicle_id = e.GetVehicleID() }); } else if (type == GSEvent.ET_INDUSTRY_OPEN) { if (!this.Wants("industry_open")) return; out.append({ event = "industry_open", tick = tick, industry_id = GSEventIndustryOpen.Convert(event).GetIndustryID() }); } else if (type == GSEvent.ET_INDUSTRY_CLOSE) { if (!this.Wants("industry_close")) return; out.append({ event = "industry_close", tick = tick, industry_id = GSEventIndustryClose.Convert(event).GetIndustryID() }); } else if (type == GSEvent.ET_TOWN_FOUNDED) { if (!this.Wants("town_founded")) return; out.append({ event = "town_founded", tick = tick, town_id = GSEventTownFounded.Convert(event).GetTownID() }); } else if (type == GSEvent.ET_COMPANY_NEW) { if (!this.Wants("company_new")) return; out.append({ event = "company_new", tick = tick, company_id = GSEventCompanyNew.Convert(event).GetCompanyID() }); } else if (type == GSEvent.ET_COMPANY_IN_TROUBLE) { if (!this.Wants("company_in_trouble")) return; out.append({ event = "company_in_trouble", tick = tick, company_id = GSEventCompanyInTrouble.Convert(event).GetCompanyID() }); } else if (type == GSEvent.ET_COMPANY_BANKRUPT) { if (!this.Wants("company_bankrupt")) return; out.append({ event = "company_bankrupt", tick = tick, company_id = GSEventCompanyBankrupt.Convert(event).GetCompanyID() }); } else if (type == GSEvent.ET_SUBSIDY_OFFER) { if (!this.Wants("subsidy_offer")) return; out.append({ event = "subsidy_offer", tick = tick, subsidy_id = GSEventSubsidyOffer.Convert(event).GetSubsidyID() }); } else if (type == GSEvent.ET_SUBSIDY_OFFER_EXPIRED) { if (!this.Wants("subsidy_offer_expired")) return; out.append({ event = "subsidy_offer_expired", tick = tick, subsidy_id = GSEventSubsidyOfferExpired.Convert(event).GetSubsidyID() }); } else if (type == GSEvent.ET_SUBSIDY_AWARDED) { if (!this.Wants("subsidy_awarded")) return; out.append({ event = "subsidy_awarded", tick = tick, subsidy_id = GSEventSubsidyAwarded.Convert(event).GetSubsidyID() }); } else if (type == GSEvent.ET_SUBSIDY_EXPIRED) { if (!this.Wants("subsidy_expired")) return; out.append({ event = "subsidy_expired", tick = tick, subsidy_id = GSEventSubsidyExpired.Convert(event).GetSubsidyID() }); } } function PollState(out) { local now = this.GetTick(); if (this.seeded && now - this.last_poll < this.interval) return; this.last_poll = now; this.PollVehicles(now, out); this.PollCargo(now, out); /* The first poll only records where everything already is: a vehicle that was sitting * at a station when the subscription started did not just arrive. */ this.seeded = true; } function WatchedVehicles() { if (this.watch_vehicles != null) return this.watch_vehicles; local out = []; foreach (v, _ in GSVehicleList()) out.append(v); return out; } function WatchedStations() { if (this.watch_stations != null) return this.watch_stations; local out = []; foreach (s, _ in GSStationList(GSStation.STATION_ANY)) out.append(s); return out; } /* Which station a vehicle is stopped at, or -1 when it is not loading at one. Note that a * vehicle stopped by hand or broken down at a platform reports its own state instead, so it * reads here as having left the station. */ function VehicleStation(v) { if (GSVehicle.GetState(v) != GSVehicle.VS_AT_STATION) return -1; local sid = GSStation.GetStationID(GSVehicle.GetLocation(v)); return GSStation.IsValidStation(sid) ? sid : -1; } function PollVehicles(now, out) { local want_arrive = this.Wants("vehicle_arrive"); local want_depart = this.Wants("vehicle_depart"); if (!want_arrive && !want_depart) return; local live = {}; foreach (v in this.WatchedVehicles()) { if (!GSVehicle.IsValidVehicle(v)) continue; if (this.company_id != null && GSVehicle.GetOwner(v) != this.company_id) continue; live[v] <- true; local at = this.VehicleStation(v); local known = (v in this.vehicle_at) ? this.vehicle_at[v] : null; local was = (known == null) ? -1 : known.station; if (was == at) continue; if (this.seeded) { /* A vehicle that moves from one station straight to another in a single * sampling window yields both a depart and an arrive, in that order. */ if (was != -1 && want_depart) out.append(this.VehicleEvent("vehicle_depart", now, v, was, now - known.since)); if (at != -1 && want_arrive) out.append(this.VehicleEvent("vehicle_arrive", now, v, at, 0)); } this.vehicle_at[v] <- { station = at, since = now }; } /* Forget vehicles that were sold or fell out of the filter, so the table cannot grow * without bound over a long subscription. */ local stale = []; foreach (v, _ in this.vehicle_at) { if (!(v in live)) stale.append(v); } foreach (v in stale) delete this.vehicle_at[v]; } function VehicleEvent(kind, tick, v, sid, dwell) { local ev = { event = kind, tick = tick, vehicle_id = v, station_id = sid, owner = GSVehicle.GetOwner(v), vehicle_type = GSVehicle.GetVehicleType(v), order_position = GSOrder.ResolveOrderPosition(v, GSOrder.ORDER_CURRENT) }; /* How long the vehicle had been loading, in ticks. For a vehicle that was already at a * station when the subscription started this counts from the first poll, not from the * real arrival. */ if (kind == "vehicle_depart") ev.dwell <- dwell; if (this.include_cargo) ev.cargo <- this.VehicleCargo(v); return ev; } function VehicleCargo(v) { local out = []; foreach (c in this.cargo_ids) { local load = GSVehicle.GetCargoLoad(v, c); if (load > 0) out.append({ cargo_id = c, load = load }); } return out; } function PollCargo(now, out) { if (!this.Wants("cargo_waiting")) return; local live = {}; foreach (sid in this.WatchedStations()) { if (!GSStation.IsValidStation(sid)) continue; if (this.company_id != null && GSStation.GetOwner(sid) != this.company_id) continue; live[sid] <- true; if (!(sid in this.cargo_prev)) this.cargo_prev[sid] <- {}; local prev = this.cargo_prev[sid]; foreach (c in this.cargo_ids) { local waiting = GSStation.GetCargoWaiting(sid, c); local before = (c in prev) ? prev[c] : 0; if (waiting == before) continue; prev[c] <- waiting; if (!this.seeded) continue; local delta = waiting - before; local magnitude = delta < 0 ? -delta : delta; if (magnitude < this.min_cargo_delta) continue; out.append({ event = "cargo_waiting", tick = now, station_id = sid, cargo_id = c, waiting = waiting, delta = delta }); } } local stale = []; foreach (sid, _ in this.cargo_prev) { if (!(sid in live)) stale.append(sid); } foreach (sid in stale) delete this.cargo_prev[sid]; } function SendEventBatch(events) { if (events.len() == 0) return; local dropped = 0; if (events.len() > this.MAX_EVENTS_PER_POLL) { dropped = events.len() - this.MAX_EVENTS_PER_POLL; events = events.slice(0, this.MAX_EVENTS_PER_POLL); } local sent = 0; while (sent < events.len()) { local end = sent + this.EVENT_BATCH_SIZE; if (end > events.len()) end = events.len(); GSAdmin.Send({ command = "events", events = events.slice(sent, end) }); sent = end; } /* Tell the client its view has a hole in it rather than letting it silently miss * transitions it is counting on. */ if (dropped > 0) { GSAdmin.Send({ command = "events", events = [{ event = "events_dropped", tick = this.GetTick(), count = dropped }] }); } } }