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

@@ -8,14 +8,11 @@ using Robust.Shared.Prototypes;
namespace Content.Server.AI.Components
{
[RegisterComponent]
[ComponentReference(typeof(IMobMoverComponent))]
[Virtual]
public class AiControllerComponent : Component, IMobMoverComponent, IMoverComponent
public class AiControllerComponent : Component
{
[DataField("logic")] private float _visionRadius = 8.0f;
public bool CanMove { get; set; } = true;
// TODO: Need to ECS a lot more of the AI first before we can ECS this
/// <summary>
/// Whether the AI is actively iterated.
@@ -46,59 +43,6 @@ namespace Content.Server.AI.Components
set => _visionRadius = value;
}
/// <inheritdoc />
protected override void Initialize()
{
base.Initialize();
// This component requires a physics component.
Owner.EnsureComponent<PhysicsComponent>();
}
/// <summary>
/// Movement speed (m/s) that the entity walks, after modifiers
/// </summary>
[ViewVariables]
public float CurrentWalkSpeed
{
get
{
if (IoCManager.Resolve<IEntityManager>().TryGetComponent(Owner, out MovementSpeedModifierComponent? component))
{
return component.CurrentWalkSpeed;
}
return MovementSpeedModifierComponent.DefaultBaseWalkSpeed;
}
}
/// <summary>
/// Movement speed (m/s) that the entity walks, after modifiers
/// </summary>
[ViewVariables]
public float CurrentSprintSpeed
{
get
{
if (IoCManager.Resolve<IEntityManager>().TryGetComponent(Owner, out MovementSpeedModifierComponent? component))
{
return component.CurrentSprintSpeed;
}
return MovementSpeedModifierComponent.DefaultBaseSprintSpeed;
}
}
public Angle LastGridAngle { get => Angle.Zero; set {} }
/// <inheritdoc />
[ViewVariables(VVAccess.ReadWrite)]
public float PushStrength { get; set; } = IMobMoverComponent.PushStrengthDefault;
/// <inheritdoc />
[ViewVariables(VVAccess.ReadWrite)]
public float GrabRange { get; set; } = IMobMoverComponent.GrabRangeDefault;
/// <summary>
/// Is the entity Sprinting (running)?
/// </summary>
@@ -111,17 +55,6 @@ namespace Content.Server.AI.Components
[ViewVariables]
public Vector2 VelocityDir { get; set; }
(Vector2 walking, Vector2 sprinting) IMoverComponent.VelocityDir =>
Sprinting ? (Vector2.Zero, VelocityDir) : (VelocityDir, Vector2.Zero);
public EntityCoordinates LastPosition { get; set; }
[ViewVariables(VVAccess.ReadWrite)]
public float StepSoundDistance { get; set; }
public void SetVelocityDirection(Direction direction, ushort subTick, bool enabled) { }
public void SetSprinting(ushort subTick, bool walking) { }
public virtual void Update(float frameTime) {}
}
}

View File

@@ -8,8 +8,10 @@ using Content.Server.CPUJob.JobQueues;
using Content.Shared.Access.Systems;
using Content.Shared.Doors.Components;
using Content.Shared.Interaction;
using Content.Shared.Movement.Components;
using Robust.Shared.Map;
using Robust.Shared.Physics;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Content.Server.AI.Steering
@@ -17,6 +19,7 @@ namespace Content.Server.AI.Steering
public sealed class AiSteeringSystem : EntitySystem
{
// http://www.red3d.com/cwr/papers/1999/gdc99steer.html for a steering overview
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly PathfindingSystem _pathfindingSystem = default!;
[Dependency] private readonly AccessReaderSystem _accessReader = default!;
@@ -120,9 +123,9 @@ namespace Content.Server.AI.Steering
/// <exception cref="InvalidOperationException"></exception>
public void Unregister(EntityUid entity)
{
if (EntityManager.TryGetComponent(entity, out AiControllerComponent? controller))
if (EntityManager.TryGetComponent(entity, out SharedPlayerInputMoverComponent? controller))
{
controller.VelocityDir = Vector2.Zero;
controller.CurTickSprintMovement = Vector2.Zero;
}
if (_pathfindingRequests.TryGetValue(entity, out var request))
@@ -228,6 +231,13 @@ namespace Content.Server.AI.Steering
_listIndex = (_listIndex + 1) % _agentLists.Count;
}
private void SetDirection(SharedPlayerInputMoverComponent component, Vector2 value)
{
component.CurTickSprintMovement = value;
component._lastInputTick = _timing.CurTick;
component._lastInputSubTick = ushort.MaxValue;
}
/// <summary>
/// Go through each steerer and combine their vectors
/// </summary>
@@ -240,7 +250,7 @@ namespace Content.Server.AI.Steering
{
// Main optimisation to be done below is the redundant calls and adding more variables
if (Deleted(entity) ||
!EntityManager.TryGetComponent(entity, out AiControllerComponent? controller) ||
!EntityManager.TryGetComponent(entity, out SharedPlayerInputMoverComponent? controller) ||
!controller.CanMove ||
!TryComp(entity, out TransformComponent? xform) ||
xform.GridUid == null)
@@ -252,13 +262,13 @@ namespace Content.Server.AI.Steering
if (entitySteering != null && (!EntityManager.EntityExists(entitySteering.Target) ? EntityLifeStage.Deleted : EntityManager.GetComponent<MetaDataComponent>(entitySteering.Target).EntityLifeStage) >= EntityLifeStage.Deleted)
{
controller.VelocityDir = Vector2.Zero;
controller.CurTickSprintMovement = Vector2.Zero;
return SteeringStatus.NoPath;
}
if (_mapManager.IsGridPaused(xform.GridUid.Value))
{
controller.VelocityDir = Vector2.Zero;
SetDirection(controller, Vector2.Zero);
return SteeringStatus.Pending;
}
@@ -266,7 +276,7 @@ namespace Content.Server.AI.Steering
// Check if we can even arrive -> Currently only samegrid movement supported
if (xform.GridUid != steeringRequest.TargetGrid.GetGridUid(EntityManager))
{
controller.VelocityDir = Vector2.Zero;
SetDirection(controller, Vector2.Zero);
return SteeringStatus.NoPath;
}
@@ -280,7 +290,7 @@ namespace Content.Server.AI.Steering
_interactionSystem.InRangeUnobstructed(entity, steeringRequest.TargetMap, steeringRequest.ArrivalDistance, popup: true))
{
// TODO: Need cruder LOS checks for ranged weaps
controller.VelocityDir = Vector2.Zero;
SetDirection(controller, Vector2.Zero);
return SteeringStatus.Arrived;
}
@@ -291,7 +301,7 @@ namespace Content.Server.AI.Steering
// If we're really close don't swiggity swoogity back and forth and just wait for the interaction check maybe?
if (steeringRequest.TimeUntilInteractionCheck > 0.0f && targetDistance <= 0.1f)
{
controller.VelocityDir = Vector2.Zero;
SetDirection(controller, Vector2.Zero);
return SteeringStatus.Moving;
}
@@ -305,7 +315,7 @@ namespace Content.Server.AI.Steering
break;
// Currently nothing should be cancelling these except external factors
case TaskCanceledException _:
controller.VelocityDir = Vector2.Zero;
SetDirection(controller, Vector2.Zero);
return SteeringStatus.NoPath;
default:
throw pathRequest.Job.Exception;
@@ -314,7 +324,7 @@ namespace Content.Server.AI.Steering
var path = _pathfindingRequests[entity].Job.Result;
if (path == null || path.Count == 0)
{
controller.VelocityDir = Vector2.Zero;
SetDirection(controller, Vector2.Zero);
return SteeringStatus.NoPath;
}
@@ -335,7 +345,7 @@ namespace Content.Server.AI.Steering
// If the route's empty we could be close and may not need a re-path so we won't check if it is
if (!_paths.ContainsKey(entity) && !_pathfindingRequests.ContainsKey(entity) && targetDistance > 1.5f)
{
controller.VelocityDir = Vector2.Zero;
SetDirection(controller, Vector2.Zero);
RequestPath(entity, steeringRequest);
return SteeringStatus.Pending;
}
@@ -365,14 +375,14 @@ namespace Content.Server.AI.Steering
var nextGrid = NextGrid(entity, steeringRequest);
if (!nextGrid.HasValue)
{
controller.VelocityDir = Vector2.Zero;
SetDirection(controller, Vector2.Zero);
return SteeringStatus.NoPath;
}
// Validate that we can even get to the next grid (could probably just check if we can use nextTile if we're not near the target grid)
if (!_pathfindingSystem.CanTraverse(entity, nextGrid.Value))
{
controller.VelocityDir = Vector2.Zero;
SetDirection(controller, Vector2.Zero);
return SteeringStatus.NoPath;
}
@@ -392,7 +402,7 @@ namespace Content.Server.AI.Steering
// Move towards it
DebugTools.Assert(movementVector != new Vector2(float.NaN, float.NaN));
controller.VelocityDir = movementVector.Normalized;
SetDirection(controller, movementVector.Normalized);
return SteeringStatus.Moving;
}

View File

@@ -16,7 +16,7 @@ namespace Content.Server.AI.Utility.AiLogic
// TODO: Need to split out the IMover stuff for NPC to a generic one that can be used for hoomans as well.
[RegisterComponent]
[ComponentProtoName("UtilityAI")]
[ComponentReference(typeof(AiControllerComponent)), ComponentReference(typeof(IMoverComponent))]
[ComponentReference(typeof(AiControllerComponent))]
public sealed class UtilityAi : AiControllerComponent
{
// TODO: Look at having ParallelOperators (probably no more than that as then you'd have a full-blown BT)

View File

@@ -57,5 +57,10 @@ public sealed class AlertLevelDetail
/// The color that this alert level will show in-game in chat.
/// </summary>
[DataField("color")] public Color Color { get; } = Color.White;
/// <summary>
/// How long it takes for the shuttle to arrive when called.
/// </summary>
[DataField("shuttleTime")] public TimeSpan ShuttleTime { get; } = TimeSpan.FromMinutes(5);
}

View File

@@ -5,6 +5,7 @@ using Content.Server.Chat;
using Content.Server.Chat.Systems;
using Content.Server.Popups;
using Content.Server.RoundEnd;
using Content.Server.Shuttles.Systems;
using Content.Server.Station.Systems;
using Content.Shared.Access.Components;
using Content.Shared.Access.Systems;
@@ -15,13 +16,14 @@ namespace Content.Server.Communications
{
public sealed class CommunicationsConsoleSystem : EntitySystem
{
[Dependency] private readonly RoundEndSystem _roundEndSystem = default!;
[Dependency] private readonly AlertLevelSystem _alertLevelSystem = default!;
[Dependency] private readonly StationSystem _stationSystem = default!;
[Dependency] private readonly IdCardSystem _idCardSystem = default!;
[Dependency] private readonly AccessReaderSystem _accessReaderSystem = default!;
[Dependency] private readonly PopupSystem _popupSystem = default!;
[Dependency] private readonly AlertLevelSystem _alertLevelSystem = default!;
[Dependency] private readonly ChatSystem _chatSystem = default!;
[Dependency] private readonly IdCardSystem _idCardSystem = default!;
[Dependency] private readonly PopupSystem _popupSystem = default!;
[Dependency] private readonly RoundEndSystem _roundEndSystem = default!;
[Dependency] private readonly ShuttleSystem _shuttle = default!;
[Dependency] private readonly StationSystem _stationSystem = default!;
private const int MaxMessageLength = 256;
@@ -29,7 +31,7 @@ namespace Content.Server.Communications
{
// All events that refresh the BUI
SubscribeLocalEvent<AlertLevelChangedEvent>(OnAlertLevelChanged);
SubscribeLocalEvent<CommunicationsConsoleComponent, ComponentInit>((_, comp, _) => UpdateBoundUserInterface(comp));
SubscribeLocalEvent<CommunicationsConsoleComponent, ComponentInit>((_, comp, _) => UpdateCommsConsoleInterface(comp));
SubscribeLocalEvent<RoundEndSystemChangedEvent>(_ => OnGenericBroadcastEvent());
SubscribeLocalEvent<AlertLevelDelayFinishedEvent>(_ => OnGenericBroadcastEvent());
@@ -48,7 +50,7 @@ namespace Content.Server.Communications
if (comp.AlreadyRefreshed) continue;
if (comp.AnnouncementCooldownRemaining <= 0f)
{
UpdateBoundUserInterface(comp);
UpdateCommsConsoleInterface(comp);
comp.AlreadyRefreshed = true;
continue;
}
@@ -65,7 +67,7 @@ namespace Content.Server.Communications
{
foreach (var comp in EntityQuery<CommunicationsConsoleComponent>())
{
UpdateBoundUserInterface(comp);
UpdateCommsConsoleInterface(comp);
}
}
@@ -75,17 +77,32 @@ namespace Content.Server.Communications
/// <param name="args">Alert level changed event arguments</param>
private void OnAlertLevelChanged(AlertLevelChangedEvent args)
{
foreach (var comp in EntityQuery<CommunicationsConsoleComponent>())
foreach (var comp in EntityQuery<CommunicationsConsoleComponent>(true))
{
var entStation = _stationSystem.GetOwningStation(comp.Owner);
if (args.Station == entStation)
{
UpdateBoundUserInterface(comp);
UpdateCommsConsoleInterface(comp);
}
}
}
private void UpdateBoundUserInterface(CommunicationsConsoleComponent comp)
/// <summary>
/// Updates the UI for all comms consoles.
/// </summary>
public void UpdateCommsConsoleInterface()
{
foreach (var comp in EntityQuery<CommunicationsConsoleComponent>())
{
UpdateCommsConsoleInterface(comp);
}
}
/// <summary>
/// Updates the UI for a particular comms console.
/// </summary>
/// <param name="comp"></param>
public void UpdateCommsConsoleInterface(CommunicationsConsoleComponent comp)
{
var uid = comp.Owner;
@@ -144,6 +161,8 @@ namespace Content.Server.Communications
private bool CanCall(CommunicationsConsoleComponent comp)
{
if (_shuttle.EmergencyShuttleArrived) return false;
return comp.CanCallShuttle && _roundEndSystem.CanCall();
}
@@ -189,7 +208,7 @@ namespace Content.Server.Communications
comp.AnnouncementCooldownRemaining = comp.DelayBetweenAnnouncements;
comp.AlreadyRefreshed = false;
UpdateBoundUserInterface(comp);
UpdateCommsConsoleInterface(comp);
// allow admemes with vv
Loc.TryGetString(comp.AnnouncementDisplayName, out var title);
@@ -206,7 +225,7 @@ namespace Content.Server.Communications
private void OnCallShuttleMessage(EntityUid uid, CommunicationsConsoleComponent comp, CommunicationsConsoleCallEmergencyShuttleMessage message)
{
if (!comp.CanCallShuttle) return;
if (!CanCall(comp)) return;
if (message.Session.AttachedEntity is not {Valid: true} mob) return;
if (!CanUse(mob, uid))
{
@@ -218,7 +237,7 @@ namespace Content.Server.Communications
private void OnRecallShuttleMessage(EntityUid uid, CommunicationsConsoleComponent comp, CommunicationsConsoleRecallEmergencyShuttleMessage message)
{
if (!comp.CanCallShuttle) return;
if (!CanCall(comp)) return;
if (message.Session.AttachedEntity is not {Valid: true} mob) return;
if (!CanUse(mob, uid))
{

View File

@@ -154,7 +154,7 @@ namespace Content.Server.Doors.Components
BoltsDown = newBolts;
SoundSystem.Play(newBolts ? BoltDownSound.GetSound() : BoltUpSound.GetSound(), Filter.Broadcast(), Owner);
SoundSystem.Play(newBolts ? BoltDownSound.GetSound() : BoltUpSound.GetSound(), Filter.Pvs(Owner), Owner);
}
}
}

View File

@@ -78,9 +78,10 @@ namespace Content.Server.Physics.Controllers
_excludedMobs.Add(mover.Owner);
var gridId = xform.GridUid;
// This tries to see if the grid is a shuttle
// This tries to see if the grid is a shuttle and if the console should work.
if (!_mapManager.TryGetGrid(gridId, out var grid) ||
!EntityManager.TryGetComponent(grid.GridEntityId, out ShuttleComponent? shuttleComponent)) continue;
!EntityManager.TryGetComponent(grid.GridEntityId, out ShuttleComponent? shuttleComponent) ||
!shuttleComponent.Enabled) continue;
if (!newPilots.TryGetValue(shuttleComponent, out var pilots))
{

View File

@@ -1,29 +1,42 @@
using System.Threading;
using Content.Server.Administration.Logs;
using Content.Server.AlertLevel;
using Content.Server.Chat;
using Content.Server.Chat.Managers;
using Content.Server.Chat.Systems;
using Content.Server.GameTicking;
using Content.Server.Shuttles.Systems;
using Content.Server.Station.Systems;
using Content.Shared.Database;
using Content.Shared.GameTicking;
using Robust.Shared.Audio;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
using Timer = Robust.Shared.Timing.Timer;
namespace Content.Server.RoundEnd
{
/// <summary>
/// Handles ending rounds normally and also via requesting it (e.g. via comms console)
/// If you request a round end then an escape shuttle will be used.
/// </summary>
public sealed class RoundEndSystem : EntitySystem
{
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
[Dependency] private readonly IChatManager _chatManager = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly IPrototypeManager _protoManager = default!;
[Dependency] private readonly ChatSystem _chatSystem = default!;
[Dependency] private readonly GameTicker _gameTicker = default!;
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
[Dependency] private readonly ShuttleSystem _shuttle = default!;
[Dependency] private readonly StationSystem _stationSystem = default!;
public TimeSpan DefaultCooldownDuration { get; set; } = TimeSpan.FromSeconds(30);
/// <summary>
/// Countdown to use where there is no station alert countdown to be found.
/// </summary>
public TimeSpan DefaultCountdownDuration { get; set; } = TimeSpan.FromMinutes(4);
public TimeSpan DefaultRestartRoundDuration { get; set; } = TimeSpan.FromMinutes(1);
@@ -62,7 +75,20 @@ namespace Content.Server.RoundEnd
public void RequestRoundEnd(EntityUid? requester = null, bool checkCooldown = true)
{
RequestRoundEnd(DefaultCountdownDuration, requester, checkCooldown);
var duration = DefaultCountdownDuration;
if (requester != null)
{
var stationUid = _stationSystem.GetOwningStation(requester.Value);
if (TryComp<AlertLevelComponent>(stationUid, out var alertLevel))
{
duration = _protoManager
.Index<AlertLevelPrototype>(AlertLevelSystem.DefaultAlertLevelSet)
.Levels[alertLevel.CurrentLevel].ShuttleTime;
}
}
RequestRoundEnd(duration, requester, checkCooldown);
}
public void RequestRoundEnd(TimeSpan countdownTime, EntityUid? requester = null, bool checkCooldown = true)
@@ -83,12 +109,32 @@ namespace Content.Server.RoundEnd
_adminLogger.Add(LogType.ShuttleCalled, LogImpact.High, $"Shuttle called");
}
_chatSystem.DispatchGlobalStationAnnouncement(Loc.GetString("round-end-system-shuttle-called-announcement",("minutes", countdownTime.Minutes)), Loc.GetString("Station"), false, Color.Gold);
// I originally had these set up here but somehow time gets passed as 0 to Loc so IDEK.
int time;
string units;
if (countdownTime.TotalSeconds < 60)
{
time = countdownTime.Seconds;
units = "seconds";
}
else
{
time = countdownTime.Minutes;
units = "minutes";
}
_chatSystem.DispatchGlobalStationAnnouncement(Loc.GetString("round-end-system-shuttle-called-announcement",
("time", time),
("units", units)),
Loc.GetString("Station"),
false,
Color.Gold);
SoundSystem.Play("/Audio/Announcements/shuttlecalled.ogg", Filter.Broadcast());
ExpectedCountdownEnd = _gameTiming.CurTime + countdownTime;
Timer.Spawn(countdownTime, EndRound, _countdownTokenSource.Token);
Timer.Spawn(countdownTime, _shuttle.CallEmergencyShuttle, _countdownTokenSource.Token);
ActivateCooldown();
RaiseLocalEvent(RoundEndSystemChangedEvent.Default);

View File

@@ -0,0 +1,29 @@
using Content.Server.Administration;
using Content.Server.Shuttles.Systems;
using Content.Shared.Administration;
using Robust.Shared.Console;
namespace Content.Server.Shuttles.Commands;
/// <summary>
/// Delays the round from ending via the shuttle call. Can still be ended via other means.
/// </summary>
[AdminCommand(AdminFlags.Fun)]
public sealed class DelayRoundEndCommand : IConsoleCommand
{
public string Command => "delayroundend";
public string Description => Loc.GetString("emergency-shuttle-command-round-desc");
public string Help => $"{Command}";
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
var system = IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<ShuttleSystem>();
if (system.DelayEmergencyRoundEnd())
{
shell.WriteLine(Loc.GetString("emergency-shuttle-command-round-yes"));
}
else
{
shell.WriteLine(Loc.GetString("emergency-shuttle-command-round-no"));
}
}
}

View File

@@ -0,0 +1,22 @@
using Content.Server.Administration;
using Content.Server.Shuttles.Systems;
using Content.Shared.Administration;
using Robust.Shared.Console;
namespace Content.Server.Shuttles.Commands;
/// <summary>
/// Calls in the emergency shuttle.
/// </summary>
[AdminCommand(AdminFlags.Fun)]
public sealed class DockEmergencyShuttleCommand : IConsoleCommand
{
public string Command => "dockemergencyshuttle";
public string Description => Loc.GetString("emergency-shuttle-command-dock-desc");
public string Help => $"{Command}";
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
var system = IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<ShuttleSystem>();
system.CallEmergencyShuttle();
}
}

View File

@@ -0,0 +1,22 @@
using Content.Server.Administration;
using Content.Server.Shuttles.Systems;
using Content.Shared.Administration;
using Robust.Shared.Console;
namespace Content.Server.Shuttles.Commands;
/// <summary>
/// Early launches in the emergency shuttle.
/// </summary>
[AdminCommand(AdminFlags.Fun)]
public sealed class LaunchEmergencyShuttleCommand : IConsoleCommand
{
public string Command => "launchemergencyshuttle";
public string Description => Loc.GetString("emergency-shuttle-command-launch-desc");
public string Help => $"{Command}";
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
var system = IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<ShuttleSystem>();
system.EarlyLaunch();
}
}

View File

@@ -0,0 +1,7 @@
namespace Content.Server.Shuttles.Components;
/// <summary>
/// Given priority when considering where to dock an emergency shuttle.
/// </summary>
[RegisterComponent]
public sealed class EmergencyDockComponent : Component {}

View File

@@ -0,0 +1,16 @@
namespace Content.Server.Shuttles.Components;
[RegisterComponent]
public sealed class EmergencyShuttleConsoleComponent : Component
{
// TODO: Okay doing it by string is kinda suss but also ID card tracking doesn't seem to be robust enough
/// <summary>
/// ID cards that have been used to authorize an early launch.
/// </summary>
[ViewVariables(VVAccess.ReadWrite), DataField("authorized")]
public HashSet<string> AuthorizedEntities = new();
[ViewVariables(VVAccess.ReadWrite), DataField("authorizationsRequired")]
public int AuthorizationsRequired = 3;
}

View File

@@ -0,0 +1,31 @@
using Robust.Shared.Map;
namespace Content.Server.Shuttles.Components;
/// <summary>
/// Added to a component when it is queued or is travelling through hyperspace
/// </summary>
[RegisterComponent]
public sealed class HyperspaceComponent : Component
{
[ViewVariables]
public HyperspaceState State = HyperspaceState.Starting;
[ViewVariables(VVAccess.ReadWrite)]
public float StartupTime = 0f;
[ViewVariables(VVAccess.ReadWrite)]
public float TravelTime = 0f;
[ViewVariables(VVAccess.ReadWrite)]
public float Accumulator = 0f;
[ViewVariables(VVAccess.ReadWrite), DataField("targetCoordinates")]
public EntityCoordinates TargetCoordinates;
}
public enum HyperspaceState : byte
{
Starting,
Travelling,
}

View File

@@ -5,6 +5,12 @@ namespace Content.Server.Shuttles.Components
[RegisterComponent]
public sealed class ShuttleComponent : Component
{
/// <summary>
/// Should controls be enabled or disabled on this shuttle.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public bool CanPilot = true;
[ViewVariables]
public bool Enabled = true;

View File

@@ -5,6 +5,12 @@ namespace Content.Server.Shuttles.Components
[RegisterComponent]
public sealed class ShuttleConsoleComponent : SharedShuttleConsoleComponent
{
/// <summary>
/// Set by shuttlesystem if the grid should no longer be pilotable.
/// </summary>
[ViewVariables]
public bool CanPilot = true;
[ViewVariables]
public readonly List<PilotComponent> SubscribedPilots = new();

View File

@@ -0,0 +1,8 @@
using Content.Server.Shuttles.Components;
namespace Content.Server.Shuttles.Events;
/// <summary>
/// Raised when <see cref="EmergencyShuttleConsoleComponent"/> has authorized successfully.
/// </summary>
public sealed class EmergencyShuttleAuthorizedEvent {}

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;

View File

@@ -1,9 +1,11 @@
using Content.Server.Station.Systems;
using Content.Server.Shuttles.Systems;
using Content.Server.Station.Systems;
using Robust.Shared.Utility;
namespace Content.Server.Station.Components;
/// <summary>
/// Stores core information about a station, namely it's config and associated grids.
/// Stores core information about a station, namely its config and associated grids.
/// All station entities will have this component.
/// </summary>
[RegisterComponent, Access(typeof(StationSystem))]
@@ -23,4 +25,16 @@ public sealed class StationDataComponent : Component
/// </remarks>
[DataField("grids")]
public readonly HashSet<EntityUid> Grids = new();
/// <summary>
/// The emergency shuttle assigned to this station.
/// </summary>
[ViewVariables, Access(typeof(ShuttleSystem), Friend = AccessPermissions.ReadWrite)]
public EntityUid? EmergencyShuttle;
/// <summary>
/// Emergency shuttle map path for this station.
/// </summary>
[ViewVariables(VVAccess.ReadWrite), Access(typeof(ShuttleSystem), Friend = AccessPermissions.ReadExecute)]
public ResourcePath EmergencyShuttlePath = new("/Maps/Shuttles/emergency_shuttle.yml");
}