Files
tbd-station-14/Content.Server/Shuttles/Systems/ArrivalsSystem.cs
Hannah Giovanna Dawson 3b3163c4f4 SS-28662 Add cvars to support forcing people to departures and making those at departures invincible (#28765)
* SS-28662 Add cvar to force spawn everyone at departures

This cvar means everyone must spawn at departures. This
could be handy for an admin event? But mostly it's so the
tutorial departures terminal can be seen by all newbies on
gateway servers.

* Small fix to ArrivalsSystem flow

* Remove incorrect todo

* Add godmode arrivals cvar
2024-06-14 04:15:42 -07:00

592 lines
22 KiB
C#

using System.Linq;
using System.Numerics;
using Content.Server.Administration;
using Content.Server.DeviceNetwork.Components;
using Content.Server.DeviceNetwork.Systems;
using Content.Server.GameTicking;
using Content.Server.GameTicking.Events;
using Content.Server.Parallax;
using Content.Server.Screens.Components;
using Content.Server.Shuttles.Components;
using Content.Server.Shuttles.Events;
using Content.Server.Spawners.Components;
using Content.Server.Station.Components;
using Content.Server.Station.Events;
using Content.Server.Station.Systems;
using Content.Shared.Administration;
using Content.Shared.CCVar;
using Content.Shared.Damage.Components;
using Content.Shared.DeviceNetwork;
using Content.Shared.Mobs.Components;
using Content.Shared.Movement.Components;
using Content.Shared.Parallax.Biomes;
using Content.Shared.Salvage;
using Content.Shared.Shuttles.Components;
using Content.Shared.Tiles;
using Robust.Server.GameObjects;
using Robust.Shared.Collections;
using Robust.Shared.Configuration;
using Robust.Shared.Console;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Timing;
using TimedDespawnComponent = Robust.Shared.Spawners.TimedDespawnComponent;
namespace Content.Server.Shuttles.Systems;
/// <summary>
/// If enabled spawns players on a separate arrivals station before they can transfer to the main station.
/// </summary>
public sealed class ArrivalsSystem : EntitySystem
{
[Dependency] private readonly IConfigurationManager _cfgManager = default!;
[Dependency] private readonly IConsoleHost _console = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly IPrototypeManager _protoManager = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly BiomeSystem _biomes = default!;
[Dependency] private readonly GameTicker _ticker = default!;
[Dependency] private readonly MapLoaderSystem _loader = default!;
[Dependency] private readonly DeviceNetworkSystem _deviceNetworkSystem = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly ShuttleSystem _shuttles = default!;
[Dependency] private readonly StationSpawningSystem _stationSpawning = default!;
[Dependency] private readonly StationSystem _station = default!;
private EntityQuery<PendingClockInComponent> _pendingQuery;
private EntityQuery<ArrivalsBlacklistComponent> _blacklistQuery;
private EntityQuery<MobStateComponent> _mobQuery;
/// <summary>
/// If enabled then spawns players on an alternate map so they can take a shuttle to the station.
/// </summary>
public bool Enabled { get; private set; }
/// <summary>
/// Flags if all players must arrive via the Arrivals system, or if they can spawn in other ways.
/// </summary>
public bool Forced { get; private set; }
/// <summary>
/// Flags if all players spawning at the departure terminal have godmode until they leave the terminal.
/// </summary>
public bool ArrivalsGodmode { get; private set; }
/// <summary>
/// The first arrival is a little early, to save everyone 10s
/// </summary>
private const float RoundStartFTLDuration = 10f;
private readonly List<ProtoId<BiomeTemplatePrototype>> _arrivalsBiomeOptions = new()
{
"Grasslands",
"LowDesert",
"Snow",
};
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<StationArrivalsComponent, StationPostInitEvent>(OnStationPostInit);
SubscribeLocalEvent<ArrivalsShuttleComponent, ComponentStartup>(OnShuttleStartup);
SubscribeLocalEvent<ArrivalsShuttleComponent, FTLTagEvent>(OnShuttleTag);
SubscribeLocalEvent<RoundStartingEvent>(OnRoundStarting);
SubscribeLocalEvent<ArrivalsShuttleComponent, FTLStartedEvent>(OnArrivalsFTL);
SubscribeLocalEvent<ArrivalsShuttleComponent, FTLCompletedEvent>(OnArrivalsDocked);
_pendingQuery = GetEntityQuery<PendingClockInComponent>();
_blacklistQuery = GetEntityQuery<ArrivalsBlacklistComponent>();
_mobQuery = GetEntityQuery<MobStateComponent>();
// Don't invoke immediately as it will get set in the natural course of things.
Enabled = _cfgManager.GetCVar(CCVars.ArrivalsShuttles);
Forced = _cfgManager.GetCVar(CCVars.ForceArrivals);
ArrivalsGodmode = _cfgManager.GetCVar(CCVars.GodmodeArrivals);
_cfgManager.OnValueChanged(CCVars.ArrivalsShuttles, SetArrivals);
_cfgManager.OnValueChanged(CCVars.ForceArrivals, b => Forced = b);
_cfgManager.OnValueChanged(CCVars.GodmodeArrivals, b => ArrivalsGodmode = b);
// Command so admins can set these for funsies
_console.RegisterCommand("arrivals", ArrivalsCommand, ArrivalsCompletion);
}
private void OnShuttleTag(EntityUid uid, ArrivalsShuttleComponent component, ref FTLTagEvent args)
{
if (args.Handled)
return;
// Just saves mappers forgetting. (v2 boogaloo)
args.Handled = true;
args.Tag = "DockArrivals";
}
private CompletionResult ArrivalsCompletion(IConsoleShell shell, string[] args)
{
if (args.Length != 1)
return CompletionResult.Empty;
return new CompletionResult(new CompletionOption[]
{
// Enables and disable are separate comms in case you don't want to accidentally toggle it, compared to
// returns which doesn't have an immediate effect
new("enable", Loc.GetString("cmd-arrivals-enable-hint")),
new("disable", Loc.GetString("cmd-arrivals-disable-hint")),
new("returns", Loc.GetString("cmd-arrivals-returns-hint")),
new ("force", Loc.GetString("cmd-arrivals-force-hint"))
}, "Option");
}
[AdminCommand(AdminFlags.Fun)]
private void ArrivalsCommand(IConsoleShell shell, string argstr, string[] args)
{
if (args.Length != 1)
{
shell.WriteError(Loc.GetString("cmd-arrivals-invalid"));
return;
}
switch (args[0])
{
case "enable":
_cfgManager.SetCVar(CCVars.ArrivalsShuttles, true);
break;
case "disable":
_cfgManager.SetCVar(CCVars.ArrivalsShuttles, false);
break;
case "returns":
var existing = _cfgManager.GetCVar(CCVars.ArrivalsReturns);
_cfgManager.SetCVar(CCVars.ArrivalsReturns, !existing);
shell.WriteLine(Loc.GetString("cmd-arrivals-returns", ("value", !existing)));
break;
case "force":
var query = AllEntityQuery<PendingClockInComponent, TransformComponent>();
var spawnPoints = EntityQuery<SpawnPointComponent, TransformComponent>().ToList();
TryGetArrivals(out var arrivalsUid);
while (query.MoveNext(out var uid, out _, out var pendingXform))
{
_random.Shuffle(spawnPoints);
foreach (var (point, xform) in spawnPoints)
{
if (point.SpawnType != SpawnPointType.LateJoin || xform.GridUid == arrivalsUid)
continue;
_transform.SetCoordinates(uid, pendingXform, xform.Coordinates);
break;
}
RemCompDeferred<AutoOrientComponent>(uid);
RemCompDeferred<PendingClockInComponent>(uid);
shell.WriteLine(Loc.GetString("cmd-arrivals-forced", ("uid", ToPrettyString(uid))));
}
break;
default:
shell.WriteError(Loc.GetString($"cmd-arrivals-invalid"));
break;
}
}
/// <summary>
/// First sends shuttle timer data, then kicks people off the shuttle if it isn't leaving the arrivals terminal
/// </summary>
private void OnArrivalsFTL(EntityUid shuttleUid, ArrivalsShuttleComponent component, ref FTLStartedEvent args)
{
if (!TryGetArrivals(out EntityUid arrivals))
return;
if (TryComp<DeviceNetworkComponent>(shuttleUid, out var netComp))
{
TryComp<FTLComponent>(shuttleUid, out var ftlComp);
var ftlTime = TimeSpan.FromSeconds(ftlComp?.TravelTime ?? _shuttles.DefaultTravelTime);
var payload = new NetworkPayload
{
[ShuttleTimerMasks.ShuttleMap] = shuttleUid,
[ShuttleTimerMasks.ShuttleTime] = ftlTime
};
// unfortunate levels of spaghetti due to roundstart arrivals ftl behavior
EntityUid? sourceMap;
var arrivalsDelay = _cfgManager.GetCVar(CCVars.ArrivalsCooldown);
if (component.FirstRun)
{
var station = _station.GetLargestGrid(Comp<StationDataComponent>(component.Station));
sourceMap = station == null ? null : Transform(station.Value)?.MapUid;
arrivalsDelay += RoundStartFTLDuration;
component.FirstRun = false;
payload.Add(ShuttleTimerMasks.DestMap, Transform(args.TargetCoordinates.EntityId).MapUid);
payload.Add(ShuttleTimerMasks.DestTime, ftlTime);
}
else
sourceMap = args.FromMapUid;
payload.Add(ShuttleTimerMasks.SourceMap, sourceMap);
payload.Add(ShuttleTimerMasks.SourceTime, ftlTime + TimeSpan.FromSeconds(arrivalsDelay));
_deviceNetworkSystem.QueuePacket(shuttleUid, null, payload, netComp.TransmitFrequency);
}
// Don't do anything here when leaving arrivals.
var arrivalsMapUid = Transform(arrivals).MapUid;
if (args.FromMapUid == arrivalsMapUid)
return;
// Any mob then yeet them off the shuttle.
if (!_cfgManager.GetCVar(CCVars.ArrivalsReturns) && args.FromMapUid != null)
DumpChildren(shuttleUid, ref args);
var pendingQuery = AllEntityQuery<PendingClockInComponent, TransformComponent>();
// We're heading from the station back to arrivals (if leaving arrivals, would have returned above).
// Process everyone who holds a PendingClockInComponent
// Note, due to way DumpChildren works, anyone who doesn't have a PendingClockInComponent gets left in space
// and will not warp. This is intended behavior.
while (pendingQuery.MoveNext(out var pUid, out _, out var xform))
{
if (xform.GridUid == shuttleUid)
{
// Warp all players who are still on this shuttle to a spawn point. This doesn't let them return to
// arrivals. It also ensures noobs, slow players or AFK players safely leave the shuttle.
TryTeleportToMapSpawn(pUid, component.Station, xform);
}
// Players who have remained at arrivals keep their warp coupon (PendingClockInComponent) for now.
if (xform.MapUid == arrivalsMapUid)
continue;
// The player has successfully left arrivals and is also not on the shuttle. Remove their warp coupon.
RemCompDeferred<PendingClockInComponent>(pUid);
RemCompDeferred<AutoOrientComponent>(pUid);
if (ArrivalsGodmode)
RemCompDeferred<GodmodeComponent>(pUid);
}
}
private void OnArrivalsDocked(EntityUid uid, ArrivalsShuttleComponent component, ref FTLCompletedEvent args)
{
var dockTime = component.NextTransfer - _timing.CurTime + TimeSpan.FromSeconds(_shuttles.DefaultStartupTime);
if (TryComp<DeviceNetworkComponent>(uid, out var netComp))
{
var payload = new NetworkPayload
{
[ShuttleTimerMasks.ShuttleMap] = uid,
[ShuttleTimerMasks.ShuttleTime] = dockTime,
[ShuttleTimerMasks.SourceMap] = args.MapUid,
[ShuttleTimerMasks.SourceTime] = dockTime,
[ShuttleTimerMasks.Docked] = true
};
_deviceNetworkSystem.QueuePacket(uid, null, payload, netComp.TransmitFrequency);
}
}
private void DumpChildren(EntityUid uid, ref FTLStartedEvent args)
{
var toDump = new List<Entity<TransformComponent>>();
DumpChildren(uid, ref args, toDump);
foreach (var (ent, xform) in toDump)
{
var rotation = xform.LocalRotation;
_transform.SetCoordinates(ent, new EntityCoordinates(args.FromMapUid!.Value, Vector2.Transform(xform.LocalPosition, args.FTLFrom)));
_transform.SetWorldRotation(ent, args.FromRotation + rotation);
}
}
private void DumpChildren(EntityUid uid, ref FTLStartedEvent args, List<Entity<TransformComponent>> toDump)
{
if (_pendingQuery.HasComponent(uid))
return;
var xform = Transform(uid);
if (_mobQuery.HasComponent(uid) || _blacklistQuery.HasComponent(uid))
{
toDump.Add((uid, xform));
return;
}
var children = xform.ChildEnumerator;
while (children.MoveNext(out var child))
{
DumpChildren(child, ref args, toDump);
}
}
public void HandlePlayerSpawning(PlayerSpawningEvent ev)
{
if (ev.SpawnResult != null)
return;
// Only works on latejoin even if enabled.
if (!Enabled || !Forced && _ticker.RunLevel != GameRunLevel.InRound)
return;
if (!HasComp<StationArrivalsComponent>(ev.Station))
return;
TryGetArrivals(out var arrivals);
if (!TryComp(arrivals, out TransformComponent? arrivalsXform))
return;
var mapId = arrivalsXform.MapID;
var points = EntityQueryEnumerator<SpawnPointComponent, TransformComponent>();
var possiblePositions = new List<EntityCoordinates>();
while (points.MoveNext(out var uid, out var spawnPoint, out var xform))
{
if (spawnPoint.SpawnType != SpawnPointType.LateJoin || xform.MapID != mapId)
continue;
possiblePositions.Add(xform.Coordinates);
}
if (possiblePositions.Count <= 0)
return;
var spawnLoc = _random.Pick(possiblePositions);
ev.SpawnResult = _stationSpawning.SpawnPlayerMob(
spawnLoc,
ev.Job,
ev.HumanoidCharacterProfile,
ev.Station);
EnsureComp<PendingClockInComponent>(ev.SpawnResult.Value);
EnsureComp<AutoOrientComponent>(ev.SpawnResult.Value);
// If you're forced to spawn, you're invincible until you leave wherever you were forced to spawn.
if (ArrivalsGodmode)
EnsureComp<GodmodeComponent>(ev.SpawnResult.Value);
}
private bool TryTeleportToMapSpawn(EntityUid player, EntityUid stationId, TransformComponent? transform = null)
{
if (!Resolve(player, ref transform))
return false;
var points = EntityQueryEnumerator<SpawnPointComponent, TransformComponent>();
var possiblePositions = new ValueList<EntityCoordinates>(32);
// Find a spawnpoint on the same map as the player is already docked with now.
while (points.MoveNext(out var uid, out var spawnPoint, out var xform))
{
if (spawnPoint.SpawnType == SpawnPointType.LateJoin &&
_station.GetOwningStation(uid, xform) == stationId)
{
// Add to list of possible spawn locations
possiblePositions.Add(xform.Coordinates);
}
}
if (possiblePositions.Count > 0)
{
// Move the player to a random late-join spawnpoint.
_transform.SetCoordinates(player, transform, _random.Pick(possiblePositions));
return true;
}
return false;
}
private void OnShuttleStartup(EntityUid uid, ArrivalsShuttleComponent component, ComponentStartup args)
{
EnsureComp<PreventPilotComponent>(uid);
}
private bool TryGetArrivals(out EntityUid uid)
{
var arrivalsQuery = EntityQueryEnumerator<ArrivalsSourceComponent>();
while (arrivalsQuery.MoveNext(out uid, out _))
{
return true;
}
return false;
}
public TimeSpan? NextShuttleArrival()
{
var query = EntityQueryEnumerator<ArrivalsShuttleComponent>();
var time = TimeSpan.MaxValue;
while (query.MoveNext(out var uid, out var comp))
{
if (comp.NextArrivalsTime < time)
time = comp.NextArrivalsTime;
}
var duration = _timing.CurTime;
return (time < duration) ? null : time - duration;
}
public override void Update(float frameTime)
{
base.Update(frameTime);
var query = EntityQueryEnumerator<ArrivalsShuttleComponent, ShuttleComponent, TransformComponent>();
var curTime = _timing.CurTime;
TryGetArrivals(out var arrivals);
if (TryComp(arrivals, out TransformComponent? arrivalsXform))
{
while (query.MoveNext(out var uid, out var comp, out var shuttle, out var xform))
{
if (comp.NextTransfer > curTime || !TryComp<StationDataComponent>(comp.Station, out var data))
continue;
var tripTime = _shuttles.DefaultTravelTime + _shuttles.DefaultStartupTime;
// Go back to arrivals source
if (xform.MapUid != arrivalsXform.MapUid)
{
if (arrivals.IsValid())
_shuttles.FTLToDock(uid, shuttle, arrivals);
comp.NextArrivalsTime = _timing.CurTime + TimeSpan.FromSeconds(tripTime);
}
// Go to station
else
{
var targetGrid = _station.GetLargestGrid(data);
if (targetGrid != null)
_shuttles.FTLToDock(uid, shuttle, targetGrid.Value);
// The ArrivalsCooldown includes the trip there, so we only need to add the time taken for
// the trip back.
comp.NextArrivalsTime = _timing.CurTime + TimeSpan.FromSeconds(
_cfgManager.GetCVar(CCVars.ArrivalsCooldown) + tripTime);
}
comp.NextTransfer += TimeSpan.FromSeconds(_cfgManager.GetCVar(CCVars.ArrivalsCooldown));
}
}
}
private void OnRoundStarting(RoundStartingEvent ev)
{
// Setup arrivals station
if (!Enabled)
return;
SetupArrivalsStation();
}
private void SetupArrivalsStation()
{
var mapId = _mapManager.CreateMap();
var mapUid = _mapManager.GetMapEntityId(mapId);
_mapManager.AddUninitializedMap(mapId);
if (!_loader.TryLoad(mapId, _cfgManager.GetCVar(CCVars.ArrivalsMap), out var uids))
{
return;
}
foreach (var id in uids)
{
EnsureComp<ArrivalsSourceComponent>(id);
EnsureComp<ProtectedGridComponent>(id);
EnsureComp<PreventPilotComponent>(id);
}
// Setup planet arrivals if relevant
if (_cfgManager.GetCVar(CCVars.ArrivalsPlanet))
{
var template = _random.Pick(_arrivalsBiomeOptions);
_biomes.EnsurePlanet(mapUid, _protoManager.Index(template));
var restricted = new RestrictedRangeComponent
{
Range = 32f
};
AddComp(mapUid, restricted);
}
_mapManager.DoMapInitialize(mapId);
// Handle roundstart stations.
var query = AllEntityQuery<StationArrivalsComponent>();
while (query.MoveNext(out var uid, out var comp))
{
SetupShuttle(uid, comp);
}
}
private void SetArrivals(bool obj)
{
Enabled = obj;
if (Enabled)
{
SetupArrivalsStation();
var query = AllEntityQuery<StationArrivalsComponent>();
while (query.MoveNext(out var sUid, out var comp))
{
SetupShuttle(sUid, comp);
}
}
else
{
var sourceQuery = AllEntityQuery<ArrivalsSourceComponent>();
while (sourceQuery.MoveNext(out var uid, out _))
{
QueueDel(uid);
}
var shuttleQuery = AllEntityQuery<ArrivalsShuttleComponent>();
while (shuttleQuery.MoveNext(out var uid, out _))
{
QueueDel(uid);
}
}
}
private void OnStationPostInit(EntityUid uid, StationArrivalsComponent component, ref StationPostInitEvent args)
{
if (!Enabled)
return;
// If it's a latespawn station then this will fail but that's okey
SetupShuttle(uid, component);
}
private void SetupShuttle(EntityUid uid, StationArrivalsComponent component)
{
if (!Deleted(component.Shuttle))
return;
// Spawn arrivals on a dummy map then dock it to the source.
var dummyMap = _mapManager.CreateMap();
if (TryGetArrivals(out var arrivals) &&
_loader.TryLoad(dummyMap, component.ShuttlePath.ToString(), out var shuttleUids))
{
component.Shuttle = shuttleUids[0];
var shuttleComp = Comp<ShuttleComponent>(component.Shuttle);
var arrivalsComp = EnsureComp<ArrivalsShuttleComponent>(component.Shuttle);
arrivalsComp.Station = uid;
EnsureComp<ProtectedGridComponent>(uid);
_shuttles.FTLToDock(component.Shuttle, shuttleComp, arrivals, hyperspaceTime: RoundStartFTLDuration);
arrivalsComp.NextTransfer = _timing.CurTime + TimeSpan.FromSeconds(_cfgManager.GetCVar(CCVars.ArrivalsCooldown));
}
// Don't start the arrivals shuttle immediately docked so power has a time to stabilise?
var timer = AddComp<TimedDespawnComponent>(_mapManager.GetMapEntityId(dummyMap));
timer.Lifetime = 15f;
}
}