using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Numerics;
using Content.Server._NF.GameTicking.Events;
using Content.Server._NF.PublicTransit.Components;
using Content.Server._NF.PublicTransit.Prototypes;
using Content.Server._NF.SectorServices;
using Content.Server._NF.Station.Systems;
using Content.Server.Chat.Systems;
using Content.Server.DeviceNetwork.Systems;
using Content.Server.GameTicking;
using Content.Server.Maps;
using Content.Server.Screens.Components;
using Content.Server.Shuttles.Components;
using Content.Server.Shuttles.Events;
using Content.Server.Shuttles.Systems;
using Content.Server.Station.Components;
using Content.Server.Station.Systems;
using Content.Shared._NF.CCVar;
using Content.Shared._NF.PublicTransit;
using Content.Shared._NF.PublicTransit.Components;
using Content.Shared.DeviceNetwork;
using Content.Shared.DeviceNetwork.Components;
using Content.Shared.Examine;
using Content.Shared.Random.Helpers;
using Content.Shared.Shuttles.Components;
using Robust.Server.GameObjects;
using Robust.Shared.Configuration;
using Robust.Shared.EntitySerialization.Systems;
using Robust.Shared.Map.Components;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Spawners;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Content.Server._NF.PublicTransit;
///
/// If enabled, spawns a public trasnport grid as definied by cvar, to act as an automatic transit shuttle between designated grids
///
public sealed class PublicTransitSystem : EntitySystem
{
[Dependency] private readonly IConfigurationManager _cfgManager = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly GameTicker _ticker = default!;
[Dependency] private readonly MapSystem _map = default!;
[Dependency] private readonly MapLoaderSystem _loader = default!;
[Dependency] private readonly ShuttleSystem _shuttles = default!;
[Dependency] private readonly ChatSystem _chat = default!;
[Dependency] private readonly MetaDataSystem _meta = default!;
[Dependency] private readonly StationRenameWarpsSystems _renameWarps = default!;
[Dependency] private readonly IPrototypeManager _proto = default!;
[Dependency] private readonly StationSystem _station = default!;
[Dependency] private readonly AppearanceSystem _appearance = default!;
[Dependency] private readonly DeviceNetworkSystem _deviceNetwork = default!;
[Dependency] private readonly SectorServiceSystem _sectorService = default!;
[Dependency] private readonly IRobustRandom _random = default!;
///
/// If enabled then spawns the bus and sets up the bus line.
///
public bool Enabled { get; private set; }
private TimeSpan _hyperspaceTimePerRoute = TimeSpan.FromSeconds(10);
private const float ShuttleSpawnBuffer = 4f;
private const ushort TransitShuttleScreenFrequency = 10000;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent(OnStationStartup);
SubscribeLocalEvent(OnStationRemove);
SubscribeLocalEvent(OnShuttleArrival);
SubscribeLocalEvent(OnShuttleTag);
SubscribeLocalEvent(OnPublicTransitVisualsInit);
SubscribeLocalEvent(OnScheduleExamined);
SubscribeLocalEvent(OnStationsGenerated);
Enabled = _cfgManager.GetCVar(NFCCVars.PublicTransit);
_cfgManager.OnValueChanged(NFCCVars.PublicTransit, SetTransit);
}
public override void Shutdown()
{
base.Shutdown();
_cfgManager.UnsubValueChanged(NFCCVars.PublicTransit, SetTransit);
}
///
/// Hardcoded snippit to intercept FTL events. It catches the transit shuttle and ensures its looking for the "DockTransit" priority dock.
///
private void OnShuttleTag(Entity ent, ref FTLTagEvent args)
{
if (args.Handled)
return;
// Just saves mappers forgetting, or ensuring that a non-standard grid forced to be a bus will prioritize the "DockTransit" tagged docks
args.Tag = ent.Comp.DockTag;
args.Handled = true;
}
private void OnPublicTransitVisualsInit(Entity ent, ref MapInitEvent args)
{
TrySetGridVisuals(ent);
}
private bool TrySetGridVisuals(Entity ent)
{
if (!TryComp(ent, out TransformComponent? xform))
return false;
PublicTransitRoutePrototype? transitRoute;
// Exception: if this is a route-dedicated bus schedule, just get its route's livery colour
if (TryComp(ent, out BusScheduleComponent? comp)
&& comp.RouteId != null
&& _proto.TryIndex(comp.RouteId, out transitRoute))
{
_appearance.SetData(ent, PublicTransitVisuals.Livery, transitRoute.LiveryColor);
return true;
}
// Otherwise, check the grid we're on.
if (TryComp(xform.GridUid, out TransitShuttleComponent? transitShuttle)
&& _proto.TryIndex(transitShuttle.RouteId, out transitRoute))
{
_appearance.SetData(ent, PublicTransitVisuals.Livery, transitRoute.LiveryColor);
return true;
}
else if (TryComp(xform.GridUid, out StationTransitComponent? stationTransit)
&& stationTransit.Routes.Count > 0
&& _proto.TryIndex(stationTransit.Routes.First().Key, out transitRoute))
{
_appearance.SetData(ent, PublicTransitVisuals.Livery, transitRoute.LiveryColor);
return true;
}
return false;
}
private void OnScheduleExamined(Entity ent, ref ExaminedEvent args)
{
if (!args.IsInDetailsRange)
return;
using (args.PushGroup(nameof(BusScheduleComponent)))
{
if (!TryComp(ent, out TransformComponent? xform)
|| xform.GridUid == null)
{
args.PushMarkup(Loc.GetString("bus-schedule-no-bus"));
return;
}
if (TryComp(xform.GridUid, out var transitShuttle))
{
// This is a bus, it only serves one route - even if the schedule is for a different route, give our info.
PrintBusSchedule(transitShuttle.RouteId, (xform.GridUid.Value, transitShuttle), ref args);
}
else if (TryComp(xform.GridUid, out var stationTransit))
{
// Get the route associated with this grid.
if (stationTransit.Routes.Count <= 0)
{
args.PushMarkup(Loc.GetString("bus-schedule-no-bus"));
return;
}
var route = ent.Comp.RouteId;
if (route == null)
{
route = stationTransit.Routes.First().Key;
}
else if (!stationTransit.Routes.ContainsKey(route.Value))
{
args.PushMarkup(Loc.GetString("bus-schedule-no-buses-on-route"));
return;
}
PrintStationSchedule(route.Value, xform.GridUid.Value, ref args);
}
else
{
// If any of the above have failed, or if no case was met, this thing isn't a bus and doesn't have bus service.
args.PushMarkup(Loc.GetString("bus-schedule-no-bus"));
return;
}
}
}
private void PrintBusSchedule(ProtoId route, Entity grid, ref ExaminedEvent args)
{
if (!TryComp(_sectorService.GetServiceEntity(), out var sectorPublicTransit)
|| !sectorPublicTransit.Routes.TryGetValue(route, out var routeData)
|| !routeData.StopIndicesByGrid.TryGetValue(grid.Comp.CurrentGrid, out var destInfo))
{
args.PushMarkup(Loc.GetString("bus-schedule-no-stops-on-route"));
return;
}
FormattedMessage message = new();
message.AddMarkupPermissive(Loc.GetString("bus-schedule-arrival-header"));
var arrivalTime = grid.Comp.NextTransfer - _timing.CurTime + routeData.Prototype.TravelTime;
// On the way to the next grid
int maxIndex = routeData.GridStops.Count;
if (HasComp(grid))
{
var nextStopArrival = arrivalTime - routeData.Prototype.WaitTime - routeData.Prototype.TravelTime;
message.PushNewline();
if (nextStopArrival.TotalSeconds >= 1)
message.AddMarkupPermissive(Loc.GetString("bus-schedule-arrival", ("station", Name(grid.Comp.CurrentGrid)), ("time", nextStopArrival.ToString(@"hh\:mm\:ss"))));
else
message.AddMarkupPermissive(Loc.GetString("bus-schedule-arrival-now", ("station", Name(grid.Comp.CurrentGrid))));
maxIndex -= 1; // Don't double count the furthest index.
}
for (int i = 1; i <= maxIndex; i++)
{
var stopUid = routeData.GridStops.GetValueAtIndex((destInfo.stopIndex + i) % routeData.GridStops.Count);
message.PushNewline();
if (arrivalTime.TotalSeconds >= 1)
message.AddMarkupPermissive(Loc.GetString("bus-schedule-arrival", ("station", Name(stopUid)), ("time", arrivalTime.ToString(@"hh\:mm\:ss"))));
else
message.AddMarkupPermissive(Loc.GetString("bus-schedule-arrival-now", ("station", Name(stopUid))));
arrivalTime += routeData.Prototype.TravelTime + routeData.Prototype.WaitTime;
}
args.PushMessage(message);
}
private void PrintStationSchedule(ProtoId route, EntityUid grid, ref ExaminedEvent args)
{
// Get stop index on requested route
if (!TryComp(_sectorService.GetServiceEntity(), out var sectorPublicTransit)
|| !sectorPublicTransit.Routes.TryGetValue(route, out var routeData)
|| !routeData.StopIndicesByGrid.TryGetValue(grid, out var destInfo))
{
args.PushMarkup(Loc.GetString("bus-schedule-no-buses-on-route"));
return;
}
Entity? nextBusMaybe = null;
var stopDistance = int.MaxValue;
var numStops = routeData.StopIndicesByGrid.Count;
// Get the buses associated with this route, find the closest one before this stop.
var busQuery = EntityQueryEnumerator();
while (busQuery.MoveNext(out var busUid, out var busComp))
{
if (busComp.RouteId != route)
continue;
// Compare the grid the bus is at (or going to) with our grid's info.
if (!routeData.StopIndicesByGrid.TryGetValue(busComp.CurrentGrid, out var busInfo))
continue;
// Find distance (ensure positive modulo)
var distance = (destInfo.stopIndex - busInfo.stopIndex + numStops) % numStops;
if (distance < stopDistance)
{
stopDistance = distance;
nextBusMaybe = (busUid, busComp);
}
}
if (nextBusMaybe is not { } nextBus)
{
args.PushMarkup(Loc.GetString("bus-schedule-no-buses-on-route"));
return;
}
// Calculate the departure time from this stop and the arrival time at the next stops.
var departureTime = nextBus.Comp.NextTransfer + stopDistance * (routeData.Prototype.TravelTime + routeData.Prototype.WaitTime) - _timing.CurTime;
FormattedMessage message = new();
if (departureTime.TotalSeconds >= 1)
message.AddMarkupPermissive(Loc.GetString("bus-schedule-next-departure", ("bus", Name(nextBus)), ("time", departureTime.ToString(@"hh\:mm\:ss"))));
else
message.AddMarkupPermissive(Loc.GetString("bus-schedule-next-departure-now", ("bus", Name(nextBus))));
message.PushNewline();
message.AddMarkupPermissive(Loc.GetString("bus-schedule-arrival-header"));
var arrivalTime = departureTime + routeData.Prototype.TravelTime;
for (int i = 1; i <= routeData.GridStops.Count - 1; i++) // Don't double count our station.
{
var stopUid = routeData.GridStops.GetValueAtIndex((destInfo.stopIndex + i) % routeData.GridStops.Count);
message.PushNewline();
if (arrivalTime.TotalSeconds >= 1)
message.AddMarkupPermissive(Loc.GetString("bus-schedule-arrival", ("station", Name(stopUid)), ("time", arrivalTime.ToString(@"hh\:mm\:ss"))));
else
message.AddMarkupPermissive(Loc.GetString("bus-schedule-arrival-now", ("station", Name(stopUid))));
arrivalTime += routeData.Prototype.TravelTime + routeData.Prototype.WaitTime;
}
args.PushMessage(message);
}
private void OnStationsGenerated(StationsGeneratedEvent args)
{
if (!TryComp(_sectorService.GetServiceEntity(), out var sectorPublicTransit))
return;
if (Enabled && !sectorPublicTransit.RoutesCreated)
SetupPublicTransit(sectorPublicTransit);
sectorPublicTransit.StationsGenerated = true;
}
///
/// Checks to make sure the grid is on the appropriate playfield, i.e., not in mapping space being worked on.
/// If so, adds the grid to the list of bus stops, but only if its not already there
///
private void OnStationStartup(Entity ent, ref ComponentStartup args)
{
if (Transform(ent).MapID != _ticker.DefaultMap) //best solution i could find because of componentinit/mapinit race conditions
return;
if (!TryComp(_sectorService.GetServiceEntity(), out var sectorPublicTransit))
return;
// Add each present route
foreach (var (routeId, routeIndex) in ent.Comp.Routes)
{
if (!sectorPublicTransit.Routes.TryGetValue(routeId, out var route))
{
if (!_proto.TryIndex(routeId, out var routeProto))
continue;
route = new PublicTransitRoute(routeProto);
sectorPublicTransit.Routes.Add(routeId, route);
}
// Already added (running from startup, reasonable)
if (route.GridStops.ContainsValue(ent))
continue;
// Handle duplicate values (e.g. three trade stations)
int actualRouteIndex = routeIndex;
while (!route.GridStops.TryAdd(actualRouteIndex, ent))
actualRouteIndex++;
CalculateGridIndices(route);
}
// TODO: add bus if needed, adjust departure times
}
private void CalculateGridIndices(PublicTransitRoute route)
{
// Recalculate grid indices
route.StopIndicesByGrid.Clear();
var index = 0;
foreach (var stop in route.GridStops)
{
route.StopIndicesByGrid[stop.Value] = (stop.Key, index++);
}
}
///
/// When a bus stop gets deleted in-game, we need to remove it from the list of bus stops, or else we get FTL problems
///
private void OnStationRemove(Entity ent, ref ComponentRemove args)
{
if (!TryComp(_sectorService.GetServiceEntity(), out var sectorPublicTransit))
return;
foreach (var route in sectorPublicTransit.Routes.Values)
{
var index = route.GridStops.IndexOfValue(ent);
if (index != -1)
route.GridStops.RemoveAt(index);
CalculateGridIndices(route);
}
// TODO: could add logic to rebalance the buses here.
}
private void OnShuttleArrival(Entity ent, ref FTLCompletedEvent args)
{
if (!TryComp(_sectorService.GetServiceEntity(), out var sectorPublicTransit))
return;
var consoleQuery = EntityQueryEnumerator();
while (consoleQuery.MoveNext(out var consoleUid, out _, out var xform))
{
if (xform.GridUid != ent)
continue;
// Find route details.
if (!sectorPublicTransit.Routes.TryGetValue(ent.Comp.RouteId, out var route))
continue;
// Note: the next grid is not cached in case stations are added or removed.
if (!TryGetNextStop(route, ent.Comp.CurrentGrid, out var nextGrid))
continue;
if (!TryComp(nextGrid, out MetaDataComponent? metadata))
continue;
_chat.TrySendInGameICMessage(consoleUid, Loc.GetString("public-transit-arrival",
("destination", metadata.EntityName), ("waittime", route.Prototype.WaitTime)),
InGameICChatType.Speak, ChatTransmitRange.HideChat, hideLog: true, checkRadioPrefix: false,
ignoreActionBlocker: true);
}
}
///
/// Here is our bus stop list handler. Theres probably a better way...
/// First, sets our output to null just in case
/// then, makes sure that our counter/index isnt out of range (reaching the end of the list will force you back to the beginning, like a loop)
/// Then, it checks to make sure that there even is anything in the list
/// and if so, we return the next station, and then increment our counter for the next time its ran
///
private bool TryGetNextStop(PublicTransitRoute route, EntityUid currentGrid, [NotNullWhen(true)] out EntityUid? nextGrid)
{
nextGrid = null;
if (route.GridStops.Count <= 0)
return false;
// If not in array, move to first item (-1 to 0). If in array, move to next item (if last, revert to first).
var currentIndex = route.GridStops.IndexOfValue(currentGrid);
nextGrid = route.GridStops.GetValueAtIndex((currentIndex + 1) % route.GridStops.Count);
return true;
}
///
/// We check the current time every tick, and if its not yet time, we just ignore.
/// If the timer is ready, we send the shuttle on an FTL journey to the destination it has saved
/// then we check our bus list, and if it returns true with the next station, we cache it on the component and reset the timer
/// if it returns false or gives a bad grid, we are just going to FTL back to where we are and try again until theres a proper destination
/// This could cause unintended behavior, if a destination is deleted while it's next in the cache, the shuttle is going to be stuck in FTL space
/// However the timer is going to force it to FTL to the next bus stop
/// If it happens that all bus stops are deleted and we never get a valid stop again, we are going to be stuck FTL'ing forever in ftl space
/// but at that point, theres nowhere to return to anyway
///
public override void Update(float frameTime)
{
base.Update(frameTime);
if (!TryComp(_sectorService.GetServiceEntity(), out var sectorPublicTransit))
return;
var curTime = _timing.CurTime;
// Update periodically, no need to have the buses on time to the millisecond.
if (sectorPublicTransit.NextUpdate > curTime)
return;
sectorPublicTransit.NextUpdate = curTime + sectorPublicTransit.UpdatePeriod;
var query = EntityQueryEnumerator();
while (query.MoveNext(out var uid, out var comp, out var shuttle))
{
if (comp.NextTransfer > curTime)
continue;
if (!sectorPublicTransit.Routes.TryGetValue(comp.RouteId, out var route))
continue;
// Regardless of whether we have a station to go to, don't rerun the same conditions frequently.
comp.NextTransfer = curTime + route.Prototype.TravelTime + route.Prototype.WaitTime;
if (!TryGetNextStop(route, comp.CurrentGrid, out var nextGrid))
continue; // NOTE: this bus is dead, should we despawn it?
// FTL to next station if it exists. Do this before the print.
var hyperspaceTime = MathF.Max(0.0f, (float)route.Prototype.TravelTime.TotalSeconds - _shuttles.DefaultStartupTime);
_shuttles.FTLToDock(uid, shuttle, nextGrid.Value, startupTime: _shuttles.DefaultStartupTime, hyperspaceTime: hyperspaceTime, priorityTag: comp.DockTag); // TODO: Unhard code the priorityTag as it should be added from the system.
comp.CurrentGrid = nextGrid.Value;
if (!TryComp(nextGrid, out MetaDataComponent? metadata))
continue;
var consoleQuery = EntityQueryEnumerator();
while (consoleQuery.MoveNext(out var consoleUid, out _, out var xform))
{
if (xform.GridUid != uid)
continue;
_chat.TrySendInGameICMessage(consoleUid, Loc.GetString("public-transit-departure",
("destination", metadata.EntityName), ("flytime", route.Prototype.TravelTime)),
InGameICChatType.Speak, ChatTransmitRange.HideChat, hideLog: true, checkRadioPrefix: false,
ignoreActionBlocker: true);
}
}
}
///
/// Here is handling a simple CVAR change to enable/disable the system
/// if the cvar is changed to enabled, we setup the transit system
/// if its changed to disabled, we delete any bus grids that exist
/// along with anyone/thing riding the bus
/// you've been warned
///
private void SetTransit(bool obj)
{
Enabled = obj;
if (!Enabled)
{
var shuttleQuery = AllEntityQuery();
while (shuttleQuery.MoveNext(out var uid, out _))
{
QueueDel(uid);
}
if (TryComp(_sectorService.GetServiceEntity(), out var publicTransit))
publicTransit.RoutesCreated = false;
}
else if (TryComp(_sectorService.GetServiceEntity(), out var publicTransit)
&& !publicTransit.RoutesCreated
&& publicTransit.StationsGenerated)
{
SetupPublicTransit(publicTransit);
}
}
///
/// Here is where we handle setting up the transit system, including sanity checks.
/// This is called multiple times, from a few different sources, to ensure that if the system is activated dynamically
/// it will still function as intended
///
///
/// Bus scheduling may be clumped if disabled and reenabled with enough stops to require additional buses.
///
private void SetupPublicTransit(SectorPublicTransitComponent comp)
{
Dictionary, List> busesByRoute = new();
// Count the existing buses.
var query = EntityQueryEnumerator();
while (query.MoveNext(out var ent, out var transit))
{
if (!busesByRoute.ContainsKey(transit.RouteId))
busesByRoute[transit.RouteId] = new();
busesByRoute[transit.RouteId].Add(ent);
}
// Set up bus depot
// NOTE: this only works with one depot at the moment.
foreach (var route in comp.Routes.Values)
{
route.GridStops.Remove(0);
}
var busDepotEnumerator = EntityQueryEnumerator();
while (busDepotEnumerator.MoveNext(out var depotStation, out _))
{
if (!TryComp(depotStation, out var stationData))
continue;
// Assuming the largest grid is the depot.
var depotGrid = _station.GetLargestGrid(stationData);
if (depotGrid == null)
continue;
var transit = EnsureComp(depotGrid.Value);
transit.Routes.Clear();
foreach (var route in comp.Routes.Values)
{
if (route.GridStops.Count <= 0)
continue;
route.GridStops.Add(0, depotGrid.Value);
transit.Routes[route.Prototype.ID] = 0;
// Route changed, recalculate grid indices
CalculateGridIndices(route);
}
}
var shuttleOffset = 500.0f;
var dummyMapUid = _map.CreateMap(out var dummyMap);
var initialHyperspaceTime = _hyperspaceTimePerRoute;
// For each route: find out the number of buses we need on it, then add more buses until we get to that count.
// Leave the excess buses for now.
foreach (var route in comp.Routes.Values)
{
var numBuses = 0;
if (busesByRoute.TryGetValue(route.Prototype.ID, out var routeBuses))
numBuses = routeBuses.Count;
var neededBuses = 1;
if (route.Prototype.StationsPerBus > 0)
neededBuses += route.GridStops.Count / route.Prototype.StationsPerBus;
if (numBuses >= neededBuses)
continue;
var routeHopTime = route.Prototype.WaitTime + route.Prototype.TravelTime;
while (numBuses < neededBuses)
{
var busProto = _random.Pick(route.Prototype.BusVessels);
if (!_proto.TryIndex(busProto, out var busVessel))
continue;
// Spawn the bus onto a dummy map
if (!_loader.TryLoadGrid(dummyMap, busVessel.ShuttlePath, out var shuttleMaybe, offset: new Vector2(shuttleOffset, 1f))
|| shuttleMaybe is not { } shuttleEnt
|| !TryComp(shuttleEnt, out var mapGrid)
|| !TryComp(shuttleEnt, out var shuttleComp))
{
break;
}
shuttleOffset += mapGrid.LocalAABB.Width + ShuttleSpawnBuffer;
// Here we are making sure that the shuttle has the TransitShuttle comp onto it, in case of dynamically changing the bus grid.
var transitComp = EnsureComp(shuttleEnt.Owner);
transitComp.RouteId = route.Prototype.ID;
transitComp.DockTag = route.Prototype.DockTag;
var busSuffix = (char)('A' + numBuses);
transitComp.ScreenText = Loc.GetString("public-transit-shuttle-screen-text", ("number", route.Prototype.RouteNumber), ("suffix", busSuffix));
EnsureComp(shuttleEnt.Owner);
var shuttleName = Loc.GetString("public-transit-shuttle-name", ("number", route.Prototype.RouteNumber), ("suffix", busSuffix));
// Set both the bus grid and station name, adjust warp points
_meta.SetEntityName(shuttleEnt.Owner, shuttleName);
if (_proto.TryIndex(busVessel.ID, out var stationProto))
{
var shuttleStation = _station.InitializeNewStation(stationProto.Stations[busVessel.ID], [shuttleEnt], shuttleName);
}
_renameWarps.SyncWarpPointsToGrid(shuttleEnt);
// Space each bus out in the schedule (in the next station if fractional time remaining, with that time added to the delay before leaving)
var relativePosition = (float)(numBuses * route.GridStops.Count) / neededBuses;
var relativeIndex = MathF.Ceiling(relativePosition);
var extraTime = (relativeIndex - relativePosition) * routeHopTime;
// We set up a default in case the second time we call it fails for some reason
var nextGrid = route.GridStops.GetValueAtIndex((int)relativeIndex);
_shuttles.FTLToDock(shuttleEnt, shuttleComp, nextGrid, hyperspaceTime: (float)initialHyperspaceTime.TotalSeconds, priorityTag: transitComp.DockTag);
transitComp.CurrentGrid = nextGrid;
transitComp.NextTransfer = _timing.CurTime + route.Prototype.WaitTime + extraTime + initialHyperspaceTime;
// Set up the screen text on the bus
var netComp = EnsureComp(shuttleEnt);
_deviceNetwork.SetTransmitFrequency(shuttleEnt, TransitShuttleScreenFrequency, netComp);
var payload = new NetworkPayload
{
[ScreenMasks.Text] = transitComp.ScreenText,
[ScreenMasks.LocalGrid] = shuttleEnt.Owner,
};
_deviceNetwork.QueuePacket(shuttleEnt, null, payload, TransitShuttleScreenFrequency, device: netComp);
numBuses++;
}
// Space out routes so they don't all FTL at once.
initialHyperspaceTime += _hyperspaceTimePerRoute;
}
// the FTL sequence takes a few seconds to warm up and send the grid, so we give the temp dummy map
// some buffer time before calling a self-delete
var timer = AddComp(dummyMapUid);
timer.Lifetime = (float)initialHyperspaceTime.TotalSeconds + 10f;
comp.RoutesCreated = true;
// Set up livery for route-based visuals
var visualsEnumerator = EntityQueryEnumerator();
while (visualsEnumerator.MoveNext(out var visualUid, out var visualComp))
{
TrySetGridVisuals((visualUid, visualComp));
}
}
}