Evac shuttle (#8931)

Co-authored-by: metalgearsloth <metalgearsloth@gmail.com>
This commit is contained in:
metalgearsloth
2022-06-26 15:20:45 +10:00
committed by GitHub
parent f647c8a658
commit 521ed99766
73 changed files with 5336 additions and 188 deletions

View File

@@ -401,14 +401,10 @@ namespace Content.Server.Shuttles.Systems
Dock(dockA, dockB);
}
private void Undock(DockingComponent dock)
public void Undock(DockingComponent dock)
{
if (dock.DockedWith == null)
{
DebugTools.Assert(false);
_sawmill.Error($"Tried to undock {(dock).Owner} but not docked with anything?");
return;
}
if (TryComp(dock.Owner, out DoorComponent? doorA))
{

View File

@@ -15,12 +15,14 @@ using Content.Shared.Shuttles.Systems;
using Content.Shared.Tag;
using Robust.Server.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.Map;
using Robust.Shared.Utility;
namespace Content.Server.Shuttles.Systems
{
public sealed class ShuttleConsoleSystem : SharedShuttleConsoleSystem
{
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly ActionBlockerSystem _blocker = default!;
[Dependency] private readonly AlertsSystem _alertsSystem = default!;
[Dependency] private readonly TagSystem _tags = default!;
@@ -86,7 +88,7 @@ namespace Content.Server.Shuttles.Systems
private void OnConsoleUIOpenAttempt(EntityUid uid, ShuttleConsoleComponent component, ActivatableUIOpenAttemptEvent args)
{
if (!TryPilot(args.User, uid))
if (!component.CanPilot || !TryPilot(args.User, uid))
args.Cancel();
}
@@ -233,6 +235,7 @@ namespace Content.Server.Shuttles.Systems
var range = radar?.MaxRange ?? 0f;
TryComp<ShuttleComponent>(consoleXform?.GridUid, out var shuttle);
component.CanPilot = shuttle is { CanPilot: true };
var mode = shuttle?.Mode ?? ShuttleMode.Cruise;
docks ??= GetAllDocks();

View File

@@ -0,0 +1,263 @@
using System.Threading;
using Content.Server.Popups;
using Content.Server.RoundEnd;
using Content.Server.Shuttles.Components;
using Content.Server.Shuttles.Events;
using Content.Server.Station.Components;
using Content.Shared.Access.Systems;
using Content.Shared.CCVar;
using Content.Shared.Database;
using Content.Shared.Shuttles.BUIStates;
using Content.Shared.Shuttles.Events;
using Content.Shared.Shuttles.Systems;
using Robust.Shared.Audio;
using Robust.Shared.Map;
using Robust.Shared.Player;
using Robust.Shared.Timing;
using Timer = Robust.Shared.Timing.Timer;
namespace Content.Server.Shuttles.Systems;
public sealed partial class ShuttleSystem
{
/*
* Handles the emergency shuttle's console and early launching.
*/
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly AccessReaderSystem _reader = default!;
[Dependency] private readonly PopupSystem _popup = default!;
[Dependency] private readonly RoundEndSystem _roundEnd = default!;
/// <summary>
/// Has the emergency shuttle arrived?
/// </summary>
public bool EmergencyShuttleArrived { get; private set; }
public bool EarlyLaunchAuthorized { get; private set; }
/// <summary>
/// How much time remaining until the shuttle consoles for emergency shuttles are unlocked?
/// </summary>
private float _consoleAccumulator;
/// <summary>
/// How long after the transit is over to end the round.
/// </summary>
private readonly TimeSpan _bufferTime = TimeSpan.FromSeconds(3);
/// <summary>
/// <see cref="CCVars.EmergencyShuttleTransitTime"/>
/// </summary>
private float _transitTime;
/// <summary>
/// <see cref="CCVars.EmergencyShuttleAuthorizeTime"/>
/// </summary>
private float _authorizeTime;
private CancellationTokenSource? _roundEndCancelToken;
private const string EmergencyRepealAllAccess = "EmergencyShuttleRepealAll";
/// <summary>
/// Have the emergency shuttles been authorised to launch at Centcomm?
/// </summary>
private bool _launchedShuttles;
private void InitializeEmergencyConsole()
{
_configManager.OnValueChanged(CCVars.EmergencyShuttleTransitTime, SetTransitTime, true);
_configManager.OnValueChanged(CCVars.EmergencyShuttleAuthorizeTime, SetAuthorizeTime, true);
SubscribeLocalEvent<EmergencyShuttleConsoleComponent, ComponentStartup>(OnEmergencyStartup);
SubscribeLocalEvent<EmergencyShuttleConsoleComponent, EmergencyShuttleAuthorizeMessage>(OnEmergencyAuthorize);
SubscribeLocalEvent<EmergencyShuttleConsoleComponent, EmergencyShuttleRepealMessage>(OnEmergencyRepeal);
SubscribeLocalEvent<EmergencyShuttleConsoleComponent, EmergencyShuttleRepealAllMessage>(OnEmergencyRepealAll);
}
private void SetAuthorizeTime(float obj)
{
_authorizeTime = obj;
}
private void SetTransitTime(float obj)
{
_transitTime = obj;
}
private void ShutdownEmergencyConsole()
{
_configManager.UnsubValueChanged(CCVars.EmergencyShuttleAuthorizeTime, SetAuthorizeTime);
_configManager.UnsubValueChanged(CCVars.EmergencyShuttleTransitTime, SetTransitTime);
}
private void OnEmergencyStartup(EntityUid uid, EmergencyShuttleConsoleComponent component, ComponentStartup args)
{
UpdateConsoleState(uid, component);
}
private void UpdateEmergencyConsole(float frameTime)
{
if (_consoleAccumulator <= 0f) return;
_consoleAccumulator -= frameTime;
if (!_launchedShuttles && _consoleAccumulator <= DefaultStartupTime)
{
_launchedShuttles = true;
if (_centcommMap != null)
{
foreach (var comp in EntityQuery<StationDataComponent>(true))
{
if (!TryComp<ShuttleComponent>(comp.EmergencyShuttle, out var shuttle)) continue;
// TODO: Add support so Hyperspace will just dock it to Centcomm.
Hyperspace(shuttle,
new EntityCoordinates(
_mapManager.GetMapEntityId(_centcommMap.Value),
Vector2.One * 1000f), _consoleAccumulator, _transitTime);
}
}
}
if (_consoleAccumulator <= 0f)
{
_launchedShuttles = true;
_chatSystem.DispatchGlobalStationAnnouncement(Loc.GetString("emergency-shuttle-left", ("transitTime", $"{_transitTime:0}")));
_roundEndCancelToken = new CancellationTokenSource();
Timer.Spawn((int) (_transitTime * 1000) + _bufferTime.Milliseconds, () => _roundEnd.EndRound(), _roundEndCancelToken.Token);
}
}
private void OnEmergencyRepealAll(EntityUid uid, EmergencyShuttleConsoleComponent component, EmergencyShuttleRepealAllMessage args)
{
var player = args.Session.AttachedEntity;
if (player == null) return;
if (!_reader.FindAccessTags(player.Value).Contains(EmergencyRepealAllAccess))
{
_popup.PopupCursor(Loc.GetString("emergency-shuttle-console-denied"), Filter.Entities(player.Value));
return;
}
if (component.AuthorizedEntities.Count == 0) return;
_logger.Add(LogType.EmergencyShuttle, LogImpact.High, $"Emergency shuttle early launch REPEAL ALL by {args.Session:user}");
component.AuthorizedEntities.Clear();
UpdateAllEmergencyConsoles();
}
private void OnEmergencyRepeal(EntityUid uid, EmergencyShuttleConsoleComponent component, EmergencyShuttleRepealMessage args)
{
var player = args.Session.AttachedEntity;
if (player == null) return;
if (!_reader.IsAllowed(player.Value, uid))
{
_popup.PopupCursor("Access denied", Filter.Entities(player.Value));
return;
}
// TODO: This is fucking bad
if (!component.AuthorizedEntities.Remove(MetaData(player.Value).EntityName)) return;
_logger.Add(LogType.EmergencyShuttle, LogImpact.High, $"Emergency shuttle early launch REPEAL by {args.Session:user}");
var remaining = component.AuthorizationsRequired - component.AuthorizedEntities.Count;
_chatSystem.DispatchGlobalStationAnnouncement(Loc.GetString("emergency-shuttle-console-auth-revoked", ("remaining", remaining)));
CheckForLaunch(component);
UpdateAllEmergencyConsoles();
}
private void OnEmergencyAuthorize(EntityUid uid, EmergencyShuttleConsoleComponent component, EmergencyShuttleAuthorizeMessage args)
{
var player = args.Session.AttachedEntity;
if (player == null) return;
if (!_reader.IsAllowed(player.Value, uid))
{
_popup.PopupCursor(Loc.GetString("emergency-shuttle-console-denied"), Filter.Entities(player.Value));
return;
}
// TODO: This is fucking bad
if (!component.AuthorizedEntities.Add(MetaData(player.Value).EntityName)) return;
_logger.Add(LogType.EmergencyShuttle, LogImpact.High, $"Emergency shuttle early launch AUTH by {args.Session:user}");
var remaining = component.AuthorizationsRequired - component.AuthorizedEntities.Count;
if (remaining > 0)
_chatSystem.DispatchGlobalStationAnnouncement(Loc.GetString("emergency-shuttle-console-auth-left", ("remaining", remaining)), playDefaultSound: false);
SoundSystem.Play("/Audio/Misc/notice1.ogg", Filter.Broadcast());
CheckForLaunch(component);
UpdateAllEmergencyConsoles();
}
private void CleanupEmergencyConsole()
{
_roundEndCancelToken = null;
_launchedShuttles = false;
_consoleAccumulator = 0f;
EarlyLaunchAuthorized = false;
EmergencyShuttleArrived = false;
}
private void UpdateAllEmergencyConsoles()
{
foreach (var comp in EntityQuery<EmergencyShuttleConsoleComponent>(true))
{
UpdateConsoleState(comp.Owner, comp);
}
}
private void UpdateConsoleState(EntityUid uid, EmergencyShuttleConsoleComponent component)
{
var auths = new List<string>();
foreach (var auth in component.AuthorizedEntities)
{
auths.Add(auth);
}
_uiSystem.GetUiOrNull(uid, EmergencyConsoleUiKey.Key)?.SetState(new EmergencyConsoleBoundUserInterfaceState()
{
EarlyLaunchTime = EarlyLaunchAuthorized ? _timing.CurTime + TimeSpan.FromSeconds(_consoleAccumulator) : null,
Authorizations = auths,
AuthorizationsRequired = component.AuthorizationsRequired,
});
}
private void CheckForLaunch(EmergencyShuttleConsoleComponent component)
{
if (component.AuthorizedEntities.Count < component.AuthorizationsRequired || EarlyLaunchAuthorized)
return;
EarlyLaunch();
}
/// <summary>
/// Attempts to early launch the emergency shuttle if not already done.
/// </summary>
public void EarlyLaunch()
{
if (EarlyLaunchAuthorized || !EmergencyShuttleArrived) return;
_logger.Add(LogType.EmergencyShuttle, LogImpact.Extreme, $"Emergency shuttle launch authorized");
_consoleAccumulator = MathF.Max(1f, MathF.Min(_consoleAccumulator, _authorizeTime));
EarlyLaunchAuthorized = true;
RaiseLocalEvent(new EmergencyShuttleAuthorizedEvent());
_chatSystem.DispatchGlobalStationAnnouncement(Loc.GetString("emergency-shuttle-launch-time", ("consoleAccumulator", $"{_consoleAccumulator:0}")), playDefaultSound: false);
UpdateAllEmergencyConsoles();
}
public bool DelayEmergencyRoundEnd()
{
if (_roundEndCancelToken == null) return false;
_roundEndCancelToken = null;
_roundEndCancelToken?.Cancel();
return true;
}
}

View File

@@ -0,0 +1,465 @@
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Content.Server.Administration.Logs;
using Content.Server.Administration.Managers;
using Content.Server.Chat.Systems;
using Content.Server.Communications;
using Content.Server.GameTicking.Events;
using Content.Server.Shuttles.Components;
using Content.Server.Station.Components;
using Content.Server.Station.Systems;
using Content.Shared.CCVar;
using Content.Shared.Database;
using Content.Shared.Shuttles.Events;
using Robust.Server.Maps;
using Robust.Server.Player;
using Robust.Shared.Audio;
using Robust.Shared.Configuration;
using Robust.Shared.Map;
using Robust.Shared.Player;
using Robust.Shared.Random;
namespace Content.Server.Shuttles.Systems;
public sealed partial class ShuttleSystem
{
/*
* Handles the escape shuttle + Centcomm.
*/
[Dependency] private readonly IAdminLogManager _logger = default!;
[Dependency] private readonly IAdminManager _admin = default!;
[Dependency] private readonly IConfigurationManager _configManager = default!;
[Dependency] private readonly IMapLoader _loader = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly ChatSystem _chatSystem = default!;
[Dependency] private readonly CommunicationsConsoleSystem _commsConsole = default!;
[Dependency] private readonly DockingSystem _dockSystem = default!;
[Dependency] private readonly StationSystem _station = default!;
private MapId? _centcommMap;
/// <summary>
/// Used for multiple shuttle spawn offsets.
/// </summary>
private float _shuttleIndex;
private const float ShuttleSpawnBuffer = 1f;
private void InitializeEscape()
{
SubscribeLocalEvent<RoundStartingEvent>(OnRoundStart);
SubscribeLocalEvent<StationDataComponent, ComponentStartup>(OnStationStartup);
SubscribeNetworkEvent<EmergencyShuttleRequestPositionMessage>(OnShuttleRequestPosition);
}
/// <summary>
/// If the client is requesting debug info on where an emergency shuttle would dock.
/// </summary>
private void OnShuttleRequestPosition(EmergencyShuttleRequestPositionMessage msg, EntitySessionEventArgs args)
{
if (!_admin.IsAdmin((IPlayerSession) args.SenderSession)) return;
var player = args.SenderSession.AttachedEntity;
if (player == null ||
!TryComp<StationDataComponent>(_station.GetOwningStation(player.Value), out var stationData)) return;
var config = GetDockingConfig(stationData);
if (config != null)
{
RaiseNetworkEvent(new EmergencyShuttlePositionMessage()
{
StationUid = config.TargetGrid,
Position = config.Area,
});
}
}
/// <summary>
/// Checks whether the emergency shuttle can warp to the specified position.
/// </summary>
private bool ValidSpawn(IMapGridComponent grid, Box2 area)
{
return !grid.Grid.GetLocalTilesIntersecting(area).Any();
}
/// <summary>
/// Tries to find the most valid docking config for the station.
/// </summary>
private DockingConfig? GetDockingConfig(StationDataComponent dataComponent)
{
// Find the largest grid associated with the station, then try all combinations of docks on it with
// all of them on the shuttle and try to find the most appropriate.
if (dataComponent.EmergencyShuttle == null) return null;
var targetGrid = GetLargestGrid(dataComponent);
if (targetGrid == null) return null;
var gridDocks = GetDocks(targetGrid.Value);
if (gridDocks.Count <= 0) return null;
var xformQuery = GetEntityQuery<TransformComponent>();
var targetGridGrid = Comp<IMapGridComponent>(targetGrid.Value);
var targetGridXform = xformQuery.GetComponent(targetGrid.Value);
var targetGridRotation = targetGridXform.WorldRotation.ToVec();
var shuttleDocks = GetDocks(dataComponent.EmergencyShuttle.Value);
var shuttleAABB = Comp<IMapGridComponent>(dataComponent.EmergencyShuttle.Value).Grid.LocalAABB;
var validDockConfigs = new List<DockingConfig>();
if (TryComp<ShuttleComponent>(dataComponent.EmergencyShuttle, out var shuttle))
{
SetPilotable(shuttle, false);
}
if (shuttleDocks.Count > 0)
{
// We'll try all combinations of shuttle docks and see which one is most suitable
foreach (var shuttleDock in shuttleDocks)
{
var shuttleDockXform = xformQuery.GetComponent(shuttleDock.Owner);
foreach (var gridDock in gridDocks)
{
var gridXform = xformQuery.GetComponent(gridDock.Owner);
if (!CanDock(
shuttleDock, shuttleDockXform,
gridDock, gridXform,
targetGridRotation,
shuttleAABB,
targetGridGrid,
out var dockedAABB,
out var matty,
out var targetAngle)) continue;
// Alright well the spawn is valid now to check how many we can connect
// Get the matrix for each shuttle dock and test it against the grid docks to see
// if the connected position / direction matches.
var dockedPorts = new List<(DockingComponent DockA, DockingComponent DockB)>()
{
(shuttleDock, gridDock),
};
// TODO: Check shuttle orientation as the tiebreaker.
foreach (var other in shuttleDocks)
{
if (other == shuttleDock) continue;
foreach (var otherGrid in gridDocks)
{
if (otherGrid == gridDock) continue;
if (!CanDock(
other,
xformQuery.GetComponent(other.Owner),
otherGrid,
xformQuery.GetComponent(otherGrid.Owner),
targetGridRotation,
shuttleAABB, targetGridGrid,
out var otherDockedAABB,
out _,
out var otherTargetAngle) ||
!otherDockedAABB.Equals(dockedAABB) ||
!targetAngle.Equals(otherTargetAngle)) continue;
dockedPorts.Add((other, otherGrid));
}
}
var spawnPosition = new EntityCoordinates(targetGrid.Value, matty.Transform(Vector2.Zero));
spawnPosition = new EntityCoordinates(targetGridXform.MapUid!.Value, spawnPosition.ToMapPos(EntityManager));
var spawnRotation = shuttleDockXform.LocalRotation +
gridXform.LocalRotation +
targetGridXform.LocalRotation;
validDockConfigs.Add(new DockingConfig()
{
Docks = dockedPorts,
Area = dockedAABB.Value,
Coordinates = spawnPosition,
Angle = spawnRotation,
});
}
}
}
if (validDockConfigs.Count <= 0) return null;
var targetGridAngle = targetGridXform.WorldRotation.Reduced();
// Prioritise by priority docks, then by maximum connected ports, then by most similar angle.
validDockConfigs = validDockConfigs
.OrderByDescending(x => x.Docks.Any(docks => HasComp<EmergencyDockComponent>(docks.DockB.Owner)))
.ThenByDescending(x => x.Docks.Count)
.ThenBy(x => Math.Abs(Angle.ShortestDistance(x.Angle.Reduced(), targetGridAngle).Theta)).ToList();
var location = validDockConfigs.First();
location.TargetGrid = targetGrid.Value;
// TODO: Ideally do a hyperspace warpin, just have it run on like a 10 second timer.
return location;
}
/// <summary>
/// Calls the emergency shuttle for the station.
/// </summary>
/// <param name="stationUid"></param>
/// <param name="dryRun">Should we show the debug data and not actually call it.</param>
public void CallEmergencyShuttle(EntityUid? stationUid)
{
if (!TryComp<StationDataComponent>(stationUid, out var stationData) ||
!TryComp<TransformComponent>(stationData.EmergencyShuttle, out var xform)) return;
var config = GetDockingConfig(stationData);
if (config != null)
{
// Set position
xform.Coordinates = config.Coordinates;
xform.WorldRotation = config.Angle;
// Connect everything
foreach (var (dockA, dockB) in config.Docks)
{
_dockSystem.Dock(dockA, dockB);
}
_logger.Add(LogType.EmergencyShuttle, LogImpact.High, $"Emergency shuttle {ToPrettyString(stationUid.Value)} docked with stations");
_chatSystem.DispatchStationAnnouncement(stationUid.Value, Loc.GetString("emergency-shuttle-docked", ("time", $"{_consoleAccumulator:0}")), playDefaultSound: false);
// TODO: Need filter extensions or something don't blame me.
SoundSystem.Play("/Audio/Announcements/shuttle_dock.ogg", Filter.Broadcast());
}
else
{
var shuttleAABB = Comp<IMapGridComponent>(stationData.EmergencyShuttle.Value).Grid.WorldAABB;
Box2? aabb = null;
// Spawn nearby.
foreach (var gridUid in stationData.Grids)
{
var grid = Comp<IMapGridComponent>(gridUid).Grid;
var gridAABB = grid.WorldAABB;
aabb = aabb?.Union(gridAABB) ?? gridAABB;
}
// UHH GOOD LUCK
if (aabb == null)
{
_logger.Add(LogType.EmergencyShuttle, LogImpact.High, $"Emergency shuttle {ToPrettyString(stationUid.Value)} unable to dock with station {ToPrettyString(stationUid.Value)}");
_chatSystem.DispatchStationAnnouncement(stationUid.Value, Loc.GetString("emergency-shuttle-good-luck"), playDefaultSound: false);
// TODO: Need filter extensions or something don't blame me.
SoundSystem.Play("/Audio/Misc/notice1.ogg", Filter.Broadcast());
return;
}
var minRadius = MathF.Max(aabb.Value.Width, aabb.Value.Height) + MathF.Max(shuttleAABB.Width, shuttleAABB.Height);
var spawnPos = aabb.Value.Center + _random.NextVector2(minRadius, minRadius + 10f);
if (TryComp<PhysicsComponent>(stationData.EmergencyShuttle, out var shuttleBody))
{
shuttleBody.LinearVelocity = Vector2.Zero;
shuttleBody.AngularVelocity = 0f;
}
xform.WorldPosition = spawnPos;
xform.WorldRotation = _random.NextAngle();
_logger.Add(LogType.EmergencyShuttle, LogImpact.High, $"Emergency shuttle {ToPrettyString(stationUid.Value)} unable to find a valid docking port for {ToPrettyString(stationUid.Value)}");
_chatSystem.DispatchStationAnnouncement(stationUid.Value, Loc.GetString("emergency-shuttle-nearby"), playDefaultSound: false);
// TODO: Need filter extensions or something don't blame me.
SoundSystem.Play("/Audio/Misc/notice1.ogg", Filter.Broadcast());
}
}
/// <summary>
/// Checks if 2 docks can be connected by moving the shuttle directly onto docks.
/// </summary>
private bool CanDock(
DockingComponent shuttleDock,
TransformComponent shuttleXform,
DockingComponent gridDock,
TransformComponent gridXform,
Vector2 targetGridRotation,
Box2 shuttleAABB,
IMapGridComponent grid,
[NotNullWhen(true)] out Box2? shuttleDockedAABB,
out Matrix3 matty,
out Vector2 gridRotation)
{
gridRotation = Vector2.Zero;
matty = Matrix3.Identity;
shuttleDockedAABB = null;
if (shuttleDock.Docked ||
gridDock.Docked ||
!shuttleXform.Anchored ||
!gridXform.Anchored)
{
return false;
}
// First, get the station dock's position relative to the shuttle, this is where we rotate it around
var stationDockPos = shuttleXform.LocalPosition +
shuttleXform.LocalRotation.RotateVec(new Vector2(0f, -1f));
var stationDockMatrix = Matrix3.CreateInverseTransform(stationDockPos, -shuttleXform.LocalRotation);
var gridXformMatrix = Matrix3.CreateTransform(gridXform.LocalPosition, gridXform.LocalRotation);
Matrix3.Multiply(in stationDockMatrix, in gridXformMatrix, out matty);
shuttleDockedAABB = matty.TransformBox(shuttleAABB);
if (!ValidSpawn(grid, shuttleDockedAABB.Value)) return false;
gridRotation = matty.Transform(targetGridRotation);
return true;
}
private void OnStationStartup(EntityUid uid, StationDataComponent component, ComponentStartup args)
{
AddEmergencyShuttle(component);
}
private void OnRoundStart(RoundStartingEvent ev)
{
Setup();
}
/// <summary>
/// Spawns the emergency shuttle for each station and starts the countdown until controls unlock.
/// </summary>
public void CallEmergencyShuttle()
{
if (EmergencyShuttleArrived) return;
_consoleAccumulator = _configManager.GetCVar(CCVars.EmergencyShuttleDockTime);
EmergencyShuttleArrived = true;
if (_centcommMap != null)
_mapManager.SetMapPaused(_centcommMap.Value, false);
foreach (var comp in EntityQuery<StationDataComponent>(true))
{
CallEmergencyShuttle(comp.Owner);
}
_commsConsole.UpdateCommsConsoleInterface();
}
/// <summary>
/// Gets the largest member grid from a station.
/// </summary>
private EntityUid? GetLargestGrid(StationDataComponent component)
{
EntityUid? largestGrid = null;
Box2 largestBounds = new Box2();
foreach (var gridUid in component.Grids)
{
if (!TryComp<IMapGridComponent>(gridUid, out var grid)) continue;
if (grid.Grid.LocalAABB.Size.LengthSquared < largestBounds.Size.LengthSquared) continue;
largestBounds = grid.Grid.LocalAABB;
largestGrid = gridUid;
}
return largestGrid;
}
private List<DockingComponent> GetDocks(EntityUid uid)
{
var result = new List<DockingComponent>();
foreach (var (dock, xform) in EntityQuery<DockingComponent, TransformComponent>(true))
{
if (xform.ParentUid != uid || !dock.Enabled) continue;
result.Add(dock);
}
return result;
}
private void Setup()
{
if (_centcommMap != null && _mapManager.MapExists(_centcommMap.Value)) return;
_centcommMap = _mapManager.CreateMap();
_mapManager.SetMapPaused(_centcommMap.Value, true);
// Load Centcomm, when we get it!
// var (_, centcomm) = _loader.LoadBlueprint(_centcommMap.Value, "/Maps/Salvage/saltern.yml", new MapLoadOptions());
// _centcomm = centcomm;
foreach (var comp in EntityQuery<StationDataComponent>(true))
{
AddEmergencyShuttle(comp);
}
}
private void AddEmergencyShuttle(StationDataComponent component)
{
if (_centcommMap == null || component.EmergencyShuttle != null) return;
// Load escape shuttle
var (_, shuttle) = _loader.LoadBlueprint(_centcommMap.Value, component.EmergencyShuttlePath.ToString(), new MapLoadOptions()
{
// Should be far enough... right? I'm too lazy to bounds check centcomm rn.
Offset = new Vector2(500f + _shuttleIndex, 0f)
});
if (shuttle == null)
{
_sawmill.Error($"Unable to spawn emergency shuttle {component.EmergencyShuttlePath} for {ToPrettyString(component.Owner)}");
return;
}
_shuttleIndex += _mapManager.GetGrid(shuttle.Value).LocalAABB.Width + ShuttleSpawnBuffer;
component.EmergencyShuttle = shuttle;
}
private void CleanupEmergencyShuttle()
{
_shuttleIndex = 0f;
if (_centcommMap == null || !_mapManager.MapExists(_centcommMap.Value))
{
_centcommMap = null;
return;
}
_mapManager.DeleteMap(_centcommMap.Value);
}
/// <summary>
/// Stores the data for a valid docking configuration for the emergency shuttle
/// </summary>
private sealed class DockingConfig
{
/// <summary>
/// The pairs of docks that can connect.
/// </summary>
public List<(DockingComponent DockA, DockingComponent DockB)> Docks = new();
/// <summary>
/// Area relative to the target grid the emergency shuttle will spawn in on.
/// </summary>
public Box2 Area;
/// <summary>
/// Target grid for docking.
/// </summary>
public EntityUid TargetGrid;
public EntityCoordinates Coordinates;
public Angle Angle;
}
}

View File

@@ -0,0 +1,210 @@
using Content.Server.Buckle.Components;
using Content.Server.Doors.Components;
using Content.Server.Shuttles.Components;
using Content.Server.Stunnable;
using Content.Shared.Sound;
using Content.Shared.StatusEffect;
using Robust.Shared.Audio;
using Robust.Shared.Collections;
using Robust.Shared.Map;
using Robust.Shared.Player;
using Robust.Shared.Utility;
namespace Content.Server.Shuttles.Systems;
public sealed partial class ShuttleSystem
{
/*
* This is a way to move a shuttle from one location to another, via an intermediate map for fanciness.
*/
[Dependency] private readonly StunSystem _stuns = default!;
private MapId? _hyperSpaceMap;
private const float DefaultStartupTime = 5.5f;
private const float DefaultTravelTime = 30f;
// I'm too lazy to make CVars.
private readonly SoundSpecifier _startupSound = new SoundPathSpecifier("/Audio/Effects/Shuttle/hyperspace_begin.ogg");
// private SoundSpecifier _travelSound = new SoundPathSpecifier();
private readonly SoundSpecifier _arrivalSound = new SoundPathSpecifier("/Audio/Effects/Shuttle/hyperspace_end.ogg");
private readonly TimeSpan _hyperspaceKnockdownTime = TimeSpan.FromSeconds(5);
/// Left-side of the station we're allowed to use
private float _index;
/// <summary>
/// Space between grids within hyperspace.
/// </summary>
private const float Buffer = 5f;
/// <summary>
/// Moves a shuttle from its current position to the target one. Goes through the hyperspace map while the timer is running.
/// </summary>
public void Hyperspace(ShuttleComponent component,
EntityCoordinates coordinates,
float startupTime = DefaultStartupTime,
float hyperspaceTime = DefaultTravelTime)
{
if (HasComp<HyperspaceComponent>(component.Owner))
{
_sawmill.Warning($"Tried queuing {ToPrettyString(component.Owner)} which already has HyperspaceComponent?");
return;
}
SetDocks(component.Owner, false);
var hyperspace = AddComp<HyperspaceComponent>(component.Owner);
hyperspace.StartupTime = startupTime;
hyperspace.TravelTime = hyperspaceTime;
hyperspace.Accumulator = hyperspace.StartupTime;
hyperspace.TargetCoordinates = coordinates;
// TODO: Need BroadcastGrid to not be bad.
SoundSystem.Play(_startupSound.GetSound(), Filter.Pvs(component.Owner, GetSoundRange(component.Owner), entityManager: EntityManager), _startupSound.Params);
}
private void UpdateHyperspace(float frameTime)
{
foreach (var comp in EntityQuery<HyperspaceComponent>())
{
comp.Accumulator -= frameTime;
if (comp.Accumulator > 0f) continue;
var xform = Transform(comp.Owner);
PhysicsComponent? body;
switch (comp.State)
{
// Startup time has elapsed and in hyperspace.
case HyperspaceState.Starting:
DoTheDinosaur(xform);
comp.State = HyperspaceState.Travelling;
SetupHyperspace();
var width = Comp<IMapGridComponent>(comp.Owner).Grid.LocalAABB.Width;
xform.Coordinates = new EntityCoordinates(_mapManager.GetMapEntityId(_hyperSpaceMap!.Value), new Vector2(_index + width / 2f, 0f));
xform.LocalRotation = Angle.Zero;
_index += width + Buffer;
comp.Accumulator += comp.TravelTime;
if (TryComp(comp.Owner, out body))
{
body.LinearVelocity = new Vector2(0f, 100f);
body.AngularVelocity = 0f;
body.LinearDamping = 0f;
body.AngularDamping = 0f;
}
SetDockBolts(comp.Owner, true);
break;
// Arrive.
case HyperspaceState.Travelling:
DoTheDinosaur(xform);
SetDocks(comp.Owner, true);
SetDockBolts(comp.Owner, false);
if (TryComp(comp.Owner, out body))
{
body.LinearVelocity = Vector2.Zero;
body.AngularVelocity = 0f;
body.LinearDamping = ShuttleIdleLinearDamping;
body.AngularDamping = ShuttleIdleAngularDamping;
}
xform.Coordinates = comp.TargetCoordinates;
SoundSystem.Play(_arrivalSound.GetSound(),
Filter.Pvs(comp.Owner, GetSoundRange(comp.Owner), entityManager: EntityManager));
RemComp<HyperspaceComponent>(comp.Owner);
break;
default:
throw new ArgumentOutOfRangeException();
}
}
}
private void SetDocks(EntityUid uid, bool enabled)
{
foreach (var (dock, xform) in EntityQuery<DockingComponent, TransformComponent>(true))
{
if (xform.ParentUid != uid || dock.Enabled == enabled) continue;
_dockSystem.Undock(dock);
dock.Enabled = enabled;
}
}
private void SetDockBolts(EntityUid uid, bool enabled)
{
foreach (var (dock, door, xform) in EntityQuery<DockingComponent, AirlockComponent, TransformComponent>(true))
{
if (xform.ParentUid != uid ||
dock.Enabled == enabled) continue;
door.SetBoltsWithAudio(enabled);
}
}
private float GetSoundRange(EntityUid uid)
{
if (!_mapManager.TryGetGrid(uid, out var grid)) return 4f;
return MathF.Max(grid.LocalAABB.Width, grid.LocalAABB.Height) + 12.5f;
}
private void SetupHyperspace()
{
if (_hyperSpaceMap != null) return;
_hyperSpaceMap = _mapManager.CreateMap();
_sawmill.Info($"Setup hyperspace map at {_hyperSpaceMap.Value}");
DebugTools.Assert(!_mapManager.IsMapPaused(_hyperSpaceMap.Value));
}
private void CleanupHyperspace()
{
_index = 0f;
if (_hyperSpaceMap == null || !_mapManager.MapExists(_hyperSpaceMap.Value))
{
_hyperSpaceMap = null;
return;
}
_mapManager.DeleteMap(_hyperSpaceMap.Value);
_hyperSpaceMap = null;
}
/// <summary>
/// Puts everyone unbuckled on the floor, paralyzed.
/// </summary>
private void DoTheDinosaur(TransformComponent xform)
{
var buckleQuery = GetEntityQuery<BuckleComponent>();
var statusQuery = GetEntityQuery<StatusEffectsComponent>();
// Get enumeration exceptions from people dropping things if we just paralyze as we go
var toKnock = new ValueList<EntityUid>();
KnockOverKids(xform, buckleQuery, statusQuery, ref toKnock);
foreach (var child in toKnock)
{
if (!statusQuery.TryGetComponent(child, out var status)) continue;
_stuns.TryParalyze(child, _hyperspaceKnockdownTime, true, status);
}
}
private void KnockOverKids(TransformComponent xform, EntityQuery<BuckleComponent> buckleQuery, EntityQuery<StatusEffectsComponent> statusQuery, ref ValueList<EntityUid> toKnock)
{
// Not recursive because probably not necessary? If we need it to be that's why this method is separate.
var childEnumerator = xform.ChildEnumerator;
while (childEnumerator.MoveNext(out var child))
{
if (!buckleQuery.TryGetComponent(child.Value, out var buckle) || buckle.Buckled) continue;
toKnock.Add(child.Value);
}
}
}

View File

@@ -1,15 +1,24 @@
using Content.Server.RoundEnd;
using Content.Server.Shuttles.Components;
using Content.Shared.CCVar;
using Content.Shared.GameTicking;
using Content.Shared.Shuttles.Components;
using JetBrains.Annotations;
using Robust.Server.GameObjects;
using Robust.Shared.Configuration;
using Robust.Shared.Map;
using Robust.Shared.Physics;
namespace Content.Server.Shuttles.Systems
{
[UsedImplicitly]
public sealed class ShuttleSystem : EntitySystem
public sealed partial class ShuttleSystem : EntitySystem
{
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly FixtureSystem _fixtures = default!;
[Dependency] private readonly UserInterfaceSystem _uiSystem = default!;
private ISawmill _sawmill = default!;
public const float TileMassMultiplier = 0.5f;
@@ -25,10 +34,17 @@ namespace Content.Server.Shuttles.Systems
public override void Initialize()
{
base.Initialize();
_sawmill = Logger.GetSawmill("shuttles");
InitializeEmergencyConsole();
InitializeEscape();
SubscribeLocalEvent<ShuttleComponent, ComponentAdd>(OnShuttleAdd);
SubscribeLocalEvent<ShuttleComponent, ComponentStartup>(OnShuttleStartup);
SubscribeLocalEvent<ShuttleComponent, ComponentShutdown>(OnShuttleShutdown);
SubscribeLocalEvent<RoundRestartCleanupEvent>(OnRoundRestart);
SubscribeLocalEvent<GridInitializeEvent>(OnGridInit);
SubscribeLocalEvent<GridFixtureChangeEvent>(OnGridFixtureChange);
@@ -41,6 +57,20 @@ namespace Content.Server.Shuttles.Systems
configManager.OnValueChanged(CCVars.ShuttleMaxAngularMomentum, SetShuttleMaxAngularMomentum, true);
}
public override void Update(float frameTime)
{
base.Update(frameTime);
UpdateEmergencyConsole(frameTime);
UpdateHyperspace(frameTime);
}
private void OnRoundRestart(RoundRestartCleanupEvent ev)
{
CleanupEmergencyConsole();
CleanupEmergencyShuttle();
CleanupHyperspace();
}
private void SetShuttleMaxLinearSpeed(float value) => ShuttleMaxLinearSpeed = value;
private void SetShuttleMaxAngularSpeed(float value) => ShuttleMaxAngularSpeed = value;
private void SetShuttleMaxAngularAcc(float value) => ShuttleMaxAngularAcc = value;
@@ -51,12 +81,12 @@ namespace Content.Server.Shuttles.Systems
public override void Shutdown()
{
base.Shutdown();
var configManager = IoCManager.Resolve<IConfigurationManager>();
configManager.UnsubValueChanged(CCVars.ShuttleMaxLinearSpeed, SetShuttleMaxLinearSpeed);
configManager.UnsubValueChanged(CCVars.ShuttleMaxAngularSpeed, SetShuttleMaxAngularSpeed);
configManager.UnsubValueChanged(CCVars.ShuttleIdleLinearDamping, SetShuttleIdleLinearDamping);
configManager.UnsubValueChanged(CCVars.ShuttleIdleAngularDamping, SetShuttleIdleAngularDamping);
configManager.UnsubValueChanged(CCVars.ShuttleMaxAngularMomentum, SetShuttleMaxAngularMomentum);
ShutdownEmergencyConsole();
_configManager.UnsubValueChanged(CCVars.ShuttleMaxLinearSpeed, SetShuttleMaxLinearSpeed);
_configManager.UnsubValueChanged(CCVars.ShuttleMaxAngularSpeed, SetShuttleMaxAngularSpeed);
_configManager.UnsubValueChanged(CCVars.ShuttleIdleLinearDamping, SetShuttleIdleLinearDamping);
_configManager.UnsubValueChanged(CCVars.ShuttleIdleAngularDamping, SetShuttleIdleAngularDamping);
_configManager.UnsubValueChanged(CCVars.ShuttleMaxAngularMomentum, SetShuttleMaxAngularMomentum);
}
private void OnShuttleAdd(EntityUid uid, ShuttleComponent component, ComponentAdd args)
@@ -107,6 +137,24 @@ namespace Content.Server.Shuttles.Systems
}
}
/// <summary>
/// Enables or disables a shuttle's piloting controls.
/// </summary>
public void SetPilotable(ShuttleComponent component, bool value)
{
if (component.CanPilot == value) return;
component.CanPilot = value;
foreach (var comp in EntityQuery<ShuttleConsoleComponent>(true))
{
comp.CanPilot = value;
// I'm gonna pray if the UI is force closed and we block UI opens that BUI handles it.
if (!value)
_uiSystem.GetUiOrNull(comp.Owner, ShuttleConsoleUiKey.Key)?.CloseAll();
}
}
public void Toggle(ShuttleComponent component)
{
if (!EntityManager.TryGetComponent(component.Owner, out PhysicsComponent? physicsComponent)) return;