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

@@ -24,7 +24,8 @@ namespace Content.Client.Computer
{
base.InitializeEntity(entity);
var sprite = IoCManager.Resolve<IEntityManager>().GetComponent<ISpriteComponent>(entity);
if (!IoCManager.Resolve<IEntityManager>().TryGetComponent<SpriteComponent>(entity, out var sprite)) return;
sprite.LayerSetState(Layers.Screen, ScreenState);
if (!string.IsNullOrEmpty(KeyboardState))
@@ -38,7 +39,7 @@ namespace Content.Client.Computer
{
base.OnChangeData(component);
var sprite = IoCManager.Resolve<IEntityManager>().GetComponent<ISpriteComponent>(component.Owner);
if (!IoCManager.Resolve<IEntityManager>().TryGetComponent<SpriteComponent>(component.Owner, out var sprite)) return;
if (!component.TryGetData(ComputerVisuals.Powered, out bool powered))
{

View File

@@ -0,0 +1,13 @@
using Content.Client.Computer;
using Content.Client.Shuttles.UI;
using Content.Shared.Shuttles.BUIStates;
using JetBrains.Annotations;
using Robust.Client.GameObjects;
namespace Content.Client.Shuttles.BUI;
[UsedImplicitly]
public sealed class EmergencyConsoleBoundUserInterface : ComputerBoundUserInterface<EmergencyConsoleWindow, EmergencyConsoleBoundUserInterfaceState>
{
public EmergencyConsoleBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey) {}
}

View File

@@ -19,6 +19,15 @@ public sealed class RadarConsoleBoundUserInterface : BoundUserInterface
_window?.OpenCentered();
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (disposing)
{
_window?.Dispose();
}
}
protected override void UpdateState(BoundUserInterfaceState state)
{
base.UpdateState(state);

View File

@@ -0,0 +1,17 @@
using Content.Client.Shuttles.Systems;
using Robust.Shared.Console;
namespace Content.Client.Shuttles.Commands;
public sealed class ShowEmergencyShuttleCommand : IConsoleCommand
{
public string Command => "showemergencyshuttle";
public string Description => "Shows the expected position of the emergency shuttle";
public string Help => $"{Command}";
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
var tstalker = IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<ShuttleSystem>();
tstalker.EnableShuttlePosition ^= true;
shell.WriteLine($"Set emergency shuttle debug to {tstalker.EnableShuttlePosition}");
}
}

View File

@@ -0,0 +1,80 @@
using Content.Shared.Shuttles.Events;
using Content.Shared.Shuttles.Systems;
using Robust.Client.Graphics;
using Robust.Shared.Enums;
namespace Content.Client.Shuttles.Systems;
public sealed partial class ShuttleSystem : SharedShuttleSystem
{
/// <summary>
/// Should we show the expected emergency shuttle position.
/// </summary>
public bool EnableShuttlePosition
{
get => _enableShuttlePosition;
set
{
if (_enableShuttlePosition == value) return;
_enableShuttlePosition = value;
var overlayManager = IoCManager.Resolve<IOverlayManager>();
if (_enableShuttlePosition)
{
_overlay = new EmergencyShuttleOverlay(EntityManager);
overlayManager.AddOverlay(_overlay);
RaiseNetworkEvent(new EmergencyShuttleRequestPositionMessage());
}
else
{
overlayManager.RemoveOverlay(_overlay!);
_overlay = null;
}
}
}
private bool _enableShuttlePosition;
private EmergencyShuttleOverlay? _overlay;
public override void Initialize()
{
base.Initialize();
SubscribeNetworkEvent<EmergencyShuttlePositionMessage>(OnShuttlePosMessage);
}
private void OnShuttlePosMessage(EmergencyShuttlePositionMessage ev)
{
if (_overlay == null) return;
_overlay.StationUid = ev.StationUid;
_overlay.Position = ev.Position;
}
}
/// <summary>
/// Shows the expected position of the emergency shuttle. Nothing more.
/// </summary>
public sealed class EmergencyShuttleOverlay : Overlay
{
private IEntityManager _entManager;
public override OverlaySpace Space => OverlaySpace.WorldSpace;
public EntityUid? StationUid;
public Box2? Position;
public EmergencyShuttleOverlay(IEntityManager entManager)
{
_entManager = entManager;
}
protected override void Draw(in OverlayDrawArgs args)
{
if (Position == null || !_entManager.TryGetComponent<TransformComponent>(StationUid, out var xform)) return;
args.WorldHandle.SetTransform(xform.WorldMatrix);
args.WorldHandle.DrawRect(Position.Value, Color.Red.WithAlpha(100));
args.WorldHandle.SetTransform(Matrix3.Identity);
}
}

View File

@@ -0,0 +1,41 @@
<userInterface:FancyWindow xmlns="https://spacestation14.io"
xmlns:userInterface="clr-namespace:Content.Client.UserInterface"
Title="Emergency Shuttle Console"
MinSize="400 400">
<BoxContainer Orientation="Vertical"
Margin="5">
<Label Name="Countdown"
HorizontalAlignment="Center"
Text="00:00"/>
<BoxContainer Name="EngineStatusContainer"
HorizontalAlignment="Center">
<Label HorizontalAlignment="Center"
Text="{Loc 'emergency-shuttle-ui-engines'}"/>
<Label Name="EngineStatus"
HorizontalAlignment="Center"
FontColorOverride="#FFA500"
Text="{Loc 'emergency-shuttle-ui-idle'}"/>
</BoxContainer>
<BoxContainer Name="LaunchContainer">
<Label Text="{Loc 'emergency-shuttle-ui-early-authorize'}"/>
<Button Name="RepealAllButton" Text="{Loc 'emergency-shuttle-ui-repeal-all'}"/>
</BoxContainer>
<!-- Spacer -->
<BoxContainer Name="AuthorizeContainer">
<Button Name="AuthorizeButton"
Text="{Loc 'emergency-shuttle-ui-authorize'}"/>
<Button Name="RepealButton" Text="{Loc 'emergency-shuttle-ui-repeal'}"/>
</BoxContainer>
<BoxContainer Name="AuthorizationsTextContainer">
<Label Text="{Loc 'emergency-shuttle-ui-authorizations'}"/>
<Label Name="AuthorizationCount"
Text="{Loc 'emergency-shuttle-ui-remaining'}"
HorizontalAlignment="Right"
Align="Right"
HorizontalExpand="True"/>
</BoxContainer>
<!-- Spacer -->
<BoxContainer Name="AuthorizationsContainer" Orientation="Vertical">
</BoxContainer>
</BoxContainer>
</userInterface:FancyWindow>

View File

@@ -0,0 +1,93 @@
using Content.Client.Computer;
using Content.Client.UserInterface;
using Content.Shared.Shuttles.BUIStates;
using Content.Shared.Shuttles.Events;
using Robust.Client.AutoGenerated;
using Robust.Client.Graphics;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Timing;
namespace Content.Client.Shuttles.UI;
[GenerateTypedNameReferences]
public sealed partial class EmergencyConsoleWindow : FancyWindow,
IComputerWindow<EmergencyConsoleBoundUserInterfaceState>
{
private readonly IGameTiming _timing;
private TimeSpan? _earlyLaunchTime;
public EmergencyConsoleWindow()
{
_timing = IoCManager.Resolve<IGameTiming>();
RobustXamlLoader.Load(this);
}
public void SetupComputerWindow(ComputerBoundUserInterfaceBase cb)
{
RepealAllButton.OnPressed += args =>
{
OnRepealAllPressed(cb, args);
};
AuthorizeButton.OnPressed += args =>
{
OnAuthorizePressed(cb, args);
};
RepealButton.OnPressed += args =>
{
OnRepealPressed(cb, args);
};
}
private void OnRepealAllPressed(ComputerBoundUserInterfaceBase cb, BaseButton.ButtonEventArgs obj)
{
cb.SendMessage(new EmergencyShuttleRepealAllMessage());
}
private void OnRepealPressed(ComputerBoundUserInterfaceBase cb, BaseButton.ButtonEventArgs obj)
{
cb.SendMessage(new EmergencyShuttleRepealMessage());
}
private void OnAuthorizePressed(ComputerBoundUserInterfaceBase cb, BaseButton.ButtonEventArgs obj)
{
cb.SendMessage(new EmergencyShuttleAuthorizeMessage());
}
public void UpdateState(EmergencyConsoleBoundUserInterfaceState scc)
{
// TODO: Loc and cvar for this.
_earlyLaunchTime = scc.EarlyLaunchTime;
AuthorizationsContainer.DisposeAllChildren();
var remainingAuths = scc.AuthorizationsRequired - scc.Authorizations.Count;
AuthorizationCount.Text = Loc.GetString("emergency-shuttle-ui-remaining", ("remaining", remainingAuths));
foreach (var auth in scc.Authorizations)
{
AuthorizationsContainer.AddChild(new Label
{
Text = auth,
FontColorOverride = Color.Lime,
});
}
}
protected override void Draw(DrawingHandleScreen handle)
{
base.Draw(handle);
if (_earlyLaunchTime == null)
{
Countdown.Text = "00:10";
}
else
{
var remaining = _earlyLaunchTime.Value - _timing.CurTime;
if (remaining < TimeSpan.Zero)
remaining = TimeSpan.Zero;
Countdown.Text = $"{remaining.Minutes:00}:{remaining.Seconds:00}";
}
}
}

View File

@@ -45,7 +45,7 @@ public sealed class RadarControl : Control
/// </summary>
private Dictionary<EntityUid, Control> _iffControls = new();
private Dictionary<EntityUid, Dictionary<EntityUid, DockingInterfaceState>> _docks = new();
private Dictionary<EntityUid, List<DockingInterfaceState>> _docks = new();
public bool ShowIFF { get; set; } = true;
public bool ShowDocks { get; set; } = true;
@@ -88,7 +88,7 @@ public sealed class RadarControl : Control
{
var coordinates = state.Coordinates;
var grid = _docks.GetOrNew(coordinates.EntityId);
grid.Add(state.Entity, state);
grid.Add(state);
}
}
@@ -288,8 +288,9 @@ public sealed class RadarControl : Control
if (_docks.TryGetValue(uid, out var docks))
{
foreach (var (ent, state) in docks)
foreach (var state in docks)
{
var ent = state.Entity;
var position = state.Coordinates.Position;
var uiPosition = matrix.Transform(position);

View File

@@ -103,7 +103,7 @@ namespace Content.IntegrationTests.Tests
async Task WaitForEvent()
{
var timeout = Task.Delay(TimeSpan.FromSeconds(10));
var timeout = Task.Delay(TimeSpan.FromSeconds(60));
var currentCount = Thread.VolatileRead(ref eventCount);
while (currentCount == Thread.VolatileRead(ref eventCount) && !timeout.IsCompleted)
{

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");
}

View File

@@ -69,6 +69,7 @@ public enum LogType
Trigger = 65,
Anchor = 66,
Unanchor = 67,
EmergencyShuttle = 68,
// haha so funny
Emag = 69,
}

View File

@@ -901,6 +901,28 @@ namespace Content.Shared.CCVar
CVarDef.Create("shuttle.idle_angular_damping", 100f, CVar.SERVERONLY);
/*
* Emergency
*/
/// <summary>
/// How long the emergency shuttle remains docked with the station, in seconds.
/// </summary>
public static readonly CVarDef<float> EmergencyShuttleDockTime =
CVarDef.Create("shuttle.emergency_dock_time", 180f, CVar.SERVERONLY);
/// <summary>
/// How long after the console is authorized for the shuttle to early launch.
/// </summary>
public static readonly CVarDef<float> EmergencyShuttleAuthorizeTime =
CVarDef.Create("shuttle.emergency_authorize_time", 10f, CVar.SERVERONLY);
/// <summary>
/// How long after the console is authorized for the shuttle to early launch.
/// </summary>
public static readonly CVarDef<float> EmergencyShuttleTransitTime =
CVarDef.Create("shuttle.emergency_transit_time", 120f, CVar.SERVERONLY);
/*
* VIEWPORT
*/

View File

@@ -33,10 +33,10 @@ namespace Content.Shared.Movement.Components
[Dependency] private readonly IConfigurationManager _configurationManager = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly IEntityManager _entityManager = default!;
private GameTick _lastInputTick;
private ushort _lastInputSubTick;
private Vector2 _curTickWalkMovement;
private Vector2 _curTickSprintMovement;
public GameTick _lastInputTick;
public ushort _lastInputSubTick;
public Vector2 CurTickWalkMovement;
public Vector2 CurTickSprintMovement;
private MoveButtons _heldMoveButtons = MoveButtons.None;
@@ -89,8 +89,8 @@ namespace Content.Shared.Movement.Components
}
else
{
walk = _curTickWalkMovement;
sprint = _curTickSprintMovement;
walk = CurTickWalkMovement;
sprint = CurTickSprintMovement;
remainingFraction = (ushort.MaxValue - _lastInputSubTick) / (float) ushort.MaxValue;
}
@@ -120,7 +120,6 @@ namespace Content.Shared.Movement.Components
protected override void Initialize()
{
base.Initialize();
Owner.EnsureComponentWarn<PhysicsComponent>();
LastGridAngle = _entityManager.GetComponent<TransformComponent>(Owner).Parent?.WorldRotation ?? new Angle(0);
}
@@ -154,8 +153,8 @@ namespace Content.Shared.Movement.Components
if (_gameTiming.CurTick > _lastInputTick)
{
_curTickWalkMovement = Vector2.Zero;
_curTickSprintMovement = Vector2.Zero;
CurTickWalkMovement = Vector2.Zero;
CurTickSprintMovement = Vector2.Zero;
_lastInputTick = _gameTiming.CurTick;
_lastInputSubTick = 0;
}
@@ -164,7 +163,7 @@ namespace Content.Shared.Movement.Components
{
var fraction = (subTick - _lastInputSubTick) / (float) ushort.MaxValue;
ref var lastMoveAmount = ref Sprinting ? ref _curTickSprintMovement : ref _curTickWalkMovement;
ref var lastMoveAmount = ref Sprinting ? ref CurTickSprintMovement : ref CurTickWalkMovement;
lastMoveAmount += DirVecForButtons(_heldMoveButtons) * fraction;

View File

@@ -59,15 +59,6 @@ namespace Content.Shared.Movement.Components
}
}
protected override void Initialize()
{
base.Initialize();
if (!IoCManager.Resolve<IEntityManager>().HasComponent<IMoverComponent>(Owner))
{
Owner.EnsureComponentWarn<SharedPlayerInputMoverComponent>();
}
}
public override ComponentState GetComponentState()
{
return new PlayerMobMoverComponentState(_grabRange, _pushStrength);

View File

@@ -0,0 +1,16 @@
using Robust.Shared.Serialization;
namespace Content.Shared.Shuttles.BUIStates;
[Serializable, NetSerializable]
public sealed class EmergencyConsoleBoundUserInterfaceState : BoundUserInterfaceState
{
/// <summary>
/// null if we're not early launching.
/// </summary>
public TimeSpan? EarlyLaunchTime;
public List<string> Authorizations = new();
public int AuthorizationsRequired;
public TimeSpan? TimeToLaunch;
}

View File

@@ -0,0 +1,9 @@
using Robust.Shared.Serialization;
namespace Content.Shared.Shuttles.Events;
[Serializable, NetSerializable]
public sealed class EmergencyShuttleAuthorizeMessage : BoundUserInterfaceMessage
{
}

View File

@@ -0,0 +1,13 @@
using Robust.Shared.Serialization;
namespace Content.Shared.Shuttles.Events;
/// <summary>
/// For debugging the expected emergency shuttle position.
/// </summary>
[Serializable, NetSerializable]
public sealed class EmergencyShuttlePositionMessage : EntityEventArgs
{
public EntityUid? StationUid;
public Box2? Position;
}

View File

@@ -0,0 +1,9 @@
using Robust.Shared.Serialization;
namespace Content.Shared.Shuttles.Events;
[Serializable, NetSerializable]
public sealed class EmergencyShuttleRepealAllMessage : BoundUserInterfaceMessage
{
}

View File

@@ -0,0 +1,9 @@
using Robust.Shared.Serialization;
namespace Content.Shared.Shuttles.Events;
[Serializable, NetSerializable]
public sealed class EmergencyShuttleRepealMessage : BoundUserInterfaceMessage
{
}

View File

@@ -0,0 +1,12 @@
using Robust.Shared.Serialization;
namespace Content.Shared.Shuttles.Events;
/// <summary>
/// Raised on the client to request the expected position of the emergency shuttle for debugging.
/// </summary>
[Serializable, NetSerializable]
public sealed class EmergencyShuttleRequestPositionMessage : EntityEventArgs
{
}

View File

@@ -0,0 +1,14 @@
using Robust.Shared.Serialization;
namespace Content.Shared.Shuttles.Systems;
public abstract partial class SharedShuttleSystem
{
}
[Serializable, NetSerializable]
public enum EmergencyConsoleUiKey : byte
{
Key,
}

View File

@@ -0,0 +1,6 @@
namespace Content.Shared.Shuttles.Systems;
public abstract partial class SharedShuttleSystem : EntitySystem
{
}

View File

@@ -4,3 +4,5 @@ meteors.ogg taken from https://github.com/tgstation/tgstation/blob/95731342b9716
outbreak7.ogg taken from /tg/station at commit https://github.com/tgstation/tgstation/commit/40d89d11ea4a5cb81d61dc1018b46f4e7d32c62a used under CC-BY-SA-3.0
welcome.ogg taken from /tg/station at commit https://github.com/tgstation/tgstation/commit/40d89d11ea4a5cb81d61dc1018b46f4e7d32c62a used under CC-BY-SA-3.0
announce.ogg taken from /tg/station https://github.com/tgstation/tgstation/commit/40d89d11ea4a5cb81d61dc1018b46f4e7d32c62a cut from the beginning of "shuttlerecalled.ogg" used under CC-BY-SA-3.0
shuttle_dock.ogg taken from /tg/station at https://github.com/tgstation/tgstation/blob/b327cb667c1ca5d1aa14e6368e66cbfa7b343b9c/sound/ai/default/shuttledock.ogg used under CC-BY-SA-3.0

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,5 @@
The following sounds are taken from TGstation github (licensed under CC by 3.0):
hyperspace_begin.ogg taken from https://github.com/tgstation/tgstation/tree/71a79f1f75902d2ccab27cbffb971b12b7ab042d/sound/runtime/hyperspace
hyperspace_end.ogg taken from https://github.com/tgstation/tgstation/tree/71a79f1f75902d2ccab27cbffb971b12b7ab042d/sound/runtime/hyperspace

View File

@@ -1,5 +1,5 @@
## RoundEndSystem
round-end-system-shuttle-called-announcement = An emergency shuttle has been sent. ETA: {$minutes} minutes.
round-end-system-shuttle-called-announcement = An emergency shuttle has been sent. ETA: {$time} {$units}.
round-end-system-shuttle-recalled-announcement = The emergency shuttle has been recalled.
round-end-system-round-restart-eta-announcement = Restarting the round in {$minutes} minutes...

View File

@@ -0,0 +1,33 @@
# Commands
## Delay shuttle round end
emergency-shuttle-command-round-desc = Stops the timer that ends the round when the emergency shuttle exits hyperspace.
emergency-shuttle-command-round-yes = Round delayed.
emergency-shuttle-command-round-no = Unable to delay round end.
## Dock emergency shuttle
emergency-shuttle-command-dock-desc = Calls the emergency shuttle and docks it to the station... if it can.
## Launch emergency shuttle
emergency-shuttle-command-launch-desc = Early launches the emergency shuttle if possible.
# Emergency shuttle
emergency-shuttle-left = The Emergency Shuttle has left the station. Estimate {$transitTime} seconds until the shuttle clears the area.
emergency-shuttle-launch-time = The emergency shuttle will launch in {$consoleAccumulator} seconds.
emergency-shuttle-docked = The Emergency Shuttle has docked with the station. It will leave in {$time} seconds.
emergency-shuttle-good-luck = The Emergency Shuttle is unable to find a station. Good luck.
emergency-shuttle-nearby = The Emergency Shuttle is unable to find a valid docking port. It has warped in nearby.
# Emergency shuttle console popup / announcement
emergency-shuttle-console-auth-left = {$remaining} authorizations needed until shuttle is launched early.
emergency-shuttle-console-auth-revoked = Early launch authorization revoked, {$remaining} authorizations needed.
emergency-shuttle-console-denied = Access denied
# UI
emergency-shuttle-ui-engines = ENGINES:
emergency-shuttle-ui-idle = Idle
emergency-shuttle-ui-repeal-all = Repeal All
emergency-shuttle-ui-early-authorize = Early Launch Authorization
emergency-shuttle-ui-authorize = AUTHORIZE
emergency-shuttle-ui-repeal = REPEAL
emergency-shuttle-ui-authorizations = Authorizations
emergency-shuttle-ui-remaining = Remaining: {$remaining}

File diff suppressed because it is too large Load Diff

View File

@@ -14,3 +14,9 @@
- Command
- Captain
- HeadOfPersonnel
- type: accessLevel
id: EmergencyShuttleLaunch
- type: accessLevel
id: EmergencyShuttleRepealAll

View File

@@ -5,6 +5,7 @@
green:
announcement: alert-level-green-announcement
color: Green
shuttleTime: 1200
blue:
announcement: alert-level-blue-announcement
sound: /Audio/Misc/notice1.ogg
@@ -13,10 +14,12 @@
announcement: alert-level-violet-announcement
sound: /Audio/Misc/notice1.ogg
color: Violet
shuttleTime: 1200
yellow:
announcement: alert-level-yellow-announcement
sound: /Audio/Misc/notice1.ogg
color: Yellow
shuttleTime: 1200
red:
announcement: alert-level-red-announcement
sound: /Audio/Misc/notice1.ogg
@@ -33,9 +36,11 @@
sound: /Audio/Misc/delta.ogg
disableSelection: true
color: DarkRed
shuttleTime: 1200
epsilon:
announcement: alert-level-epsilon-announcement
selectable: false
sound: /Audio/Misc/epsilon.ogg
disableSelection: true
color: DarkViolet
shuttleTime: 1200

View File

@@ -137,6 +137,8 @@
types:
Poison: 2
Piercing: 1
- type: PlayerInputMover
- type: PlayerMobMover
- type: UtilityAI
behaviorSets:
- UnarmedAttackHostiles
@@ -652,6 +654,8 @@
damage:
types:
Blunt: 10
- type: PlayerInputMover
- type: PlayerMobMover
- type: UtilityAI
behaviorSets:
- UnarmedAttackHostiles
@@ -1130,6 +1134,8 @@
- type: MovementSpeedModifier
baseWalkSpeed : 7
baseSprintSpeed : 7
- type: PlayerInputMover
- type: PlayerMobMover
- type: UtilityAI
behaviorSets:
- UnarmedAttackHostiles
@@ -1296,6 +1302,8 @@
- type: AiFactionTag
factions:
- Xeno
- type: PlayerInputMover
- type: PlayerMobMover
- type: UtilityAI
behaviorSets:
- Idle

View File

@@ -4,6 +4,8 @@
parent: SimpleSpaceMobBase
description: It looks friendly. Why don't you give it a hug?
components:
- type: PlayerInputMover
- type: PlayerMobMover
- type: UtilityAI
behaviorSets:
- Idle

View File

@@ -4,6 +4,8 @@
parent: SimpleSpaceMobBase
description: It's a space carp.
components:
- type: PlayerInputMover
- type: PlayerMobMover
- type: UtilityAI
behaviorSets:
- Idle

View File

@@ -6,6 +6,8 @@
description: A miserable pile of secrets.
suffix: AI
components:
- type: PlayerInputMover
- type: PlayerMobMover
- type: UtilityAI
startingGear: PassengerGear
behaviorSets:

View File

@@ -4,6 +4,8 @@
id: MobCivilian
description: A miserable pile of secrets.
components:
- type: PlayerInputMover
- type: PlayerMobMover
- type: UtilityAI
behaviorSets:
- Clothing
@@ -22,6 +24,8 @@
id: MobSpirate
description: Yarr!
components:
- type: PlayerInputMover
- type: PlayerMobMover
- type: UtilityAI
behaviorSets:
- Clothing

View File

@@ -7,6 +7,8 @@
- type: Tag
tags:
- FootstepSound
- type: PlayerInputMover
- type: PlayerMobMover
- type: UtilityAI
behaviorSets:
- UnarmedAttackHostiles

View File

@@ -82,6 +82,8 @@
types:
Piercing: 5
Slash: 5
- type: PlayerInputMover
- type: PlayerMobMover
- type: UtilityAI
behaviorSets:
- UnarmedAttackHostiles

View File

@@ -8,6 +8,8 @@
- type: MovementSpeedModifier
baseWalkSpeed : 3.75
baseSprintSpeed : 3.75
- type: PlayerInputMover
- type: PlayerMobMover
- type: UtilityAI
behaviorSets:
- Idle
@@ -125,6 +127,8 @@
- type: MovementSpeedModifier
baseWalkSpeed : 3.5
baseSprintSpeed : 3.5
- type: PlayerInputMover
- type: PlayerMobMover
- type: UtilityAI
behaviorSets:
- Idle

View File

@@ -12,6 +12,8 @@
Flammable: [Touch]
Extinguish: [Touch]
Acidic: [Touch, Ingestion]
- type: PlayerInputMover
- type: PlayerMobMover
- type: UtilityAI
behaviorSets:
# - Clothing
@@ -159,6 +161,8 @@
parent: SimpleSpaceMobBase
suffix: AI
components:
- type: PlayerInputMover
- type: PlayerMobMover
- type: UtilityAI
behaviorSets:
- Idle

View File

@@ -4,6 +4,8 @@
parent: SimpleSpaceMobBase
description: It's a space tick, watch out for its nasty bite. Centcomm reports that 90 percent of cargo leg amputations are due to space tick bites.
components:
- type: PlayerInputMover
- type: PlayerMobMover
- type: UtilityAI
behaviorSets:
- Idle

View File

@@ -7,6 +7,8 @@
components:
- type: CombatMode
canDisarm: true
- type: PlayerInputMover
- type: PlayerMobMover
- type: UtilityAI
behaviorSets:
- Idle
@@ -340,6 +342,8 @@
attributes:
proper: true
gender: male
- type: PlayerInputMover
- type: PlayerMobMover
- type: UtilityAI
behaviorSets:
- Idle

View File

@@ -42,6 +42,8 @@
types:
Piercing: 8
Slash: 7
- type: PlayerInputMover
- type: PlayerMobMover
- type: UtilityAI
behaviorSets:
- Idle

View File

@@ -650,6 +650,15 @@
- type: AccessReader
access: [["External"]]
- type: entity
parent: AirlockGlassShuttle
id: AirlockExternalGlassShuttleEmergencyLocked
suffix: External, Emergency, Glass, Docking, Locked
components:
- type: EmergencyDock
- type: AccessReader
access: [["External"]]
#HighSecDoors
- type: entity
parent: HighSecDoor

View File

@@ -3,7 +3,7 @@
id: ArcadeBase
description: An arcade cabinet.
name: arcade
parent: ComputerBase
parent: BaseComputer
components:
- type: ApcPowerReceiver
powerLoad: 350

View File

@@ -1,7 +1,7 @@
- type: entity
abstract: true
parent: ComputerFrame
id: ComputerBase
id: BaseComputer
name: computer
placement:
mode: SnapgridCenter

View File

@@ -1,5 +1,5 @@
- type: entity
parent: ComputerBase
parent: BaseComputer
id: ComputerAlert
name: alerts computer
description: Used to access the station's automated alert system.
@@ -13,8 +13,33 @@
board: AlertsComputerCircuitboard
- type: entity
parent: ComputerBase
id: ComputerShuttleBase
parent: BaseComputer
id: ComputerEmergencyShuttle
name: emergency shuttle console
description: Handles authorization to early launch the shuttle.
components:
- type: AccessReader
access:
- [ EmergencyShuttleLaunch ]
- type: EmergencyShuttleConsole
- type: ActivatableUI
key: enum.EmergencyConsoleUiKey.Key
- type: ActivatableUIRequiresPower
- type: UserInterface
interfaces:
- key: enum.EmergencyConsoleUiKey.Key
type: EmergencyConsoleBoundUserInterface
- type: ExtensionCableReceiver
- type: PointLight
radius: 1.5
energy: 1.6
color: "#43ccb5"
- type: Rotatable
rotateWhileAnchored: true
- type: entity
parent: BaseComputer
id: BaseComputerShuttle
name: shuttle console
description: Used to pilot a shuttle.
abstract: true
@@ -37,7 +62,7 @@
rotateWhileAnchored: true
- type: entity
parent: ComputerShuttleBase
parent: BaseComputerShuttle
id: ComputerShuttle
name: shuttle console
description: Used to pilot a shuttle.
@@ -51,7 +76,7 @@
board: ShuttleConsoleCircuitboard
- type: entity
parent: ComputerShuttleBase
parent: BaseComputerShuttle
id: ComputerShuttleSyndie
name: syndicate shuttle console
description: Used to pilot a syndicate shuttle.
@@ -69,7 +94,7 @@
color: "#c94242"
- type: entity
parent: ComputerShuttleBase
parent: BaseComputerShuttle
id: ComputerShuttleCargo
name: cargo shuttle console
description: Used to pilot the cargo shuttle.
@@ -88,7 +113,7 @@
color: "#c94242"
- type: entity
parent: ComputerBase
parent: BaseComputer
id: ComputerPowerMonitoring
name: power monitoring computer
description: It monitors power levels across the station.
@@ -119,7 +144,7 @@
type: PowerMonitoringConsoleBoundUserInterface
- type: entity
parent: ComputerBase
parent: BaseComputer
id: ComputerMedicalRecords
name: medical records computer
description: This can be used to check medical records.
@@ -137,7 +162,7 @@
board: MedicalRecordsComputerCircuitboard
- type: entity
parent: ComputerBase
parent: BaseComputer
id: ComputerCriminalRecords
name: criminal records computer
description: This can be used to check criminal records.
@@ -155,7 +180,7 @@
board: CriminalRecordsComputerCircuitboard
- type: entity
parent: ComputerBase
parent: BaseComputer
id: ComputerCrewMonitoring
name: crew monitoring console
description: Used to monitor active health sensors built into most of the crew's uniforms.
@@ -186,7 +211,7 @@
range: 500
- type: entity
parent: ComputerBase
parent: BaseComputer
id: ComputerResearchAndDevelopment
name: R&D computer
description: A computer used to interface with R&D tools.
@@ -220,7 +245,7 @@
color: "#b53ca1"
- type: entity
parent: ComputerBase
parent: BaseComputer
id: ComputerId
name: ID card computer
description: Terminal for programming Nanotrasen employee ID cards to access parts of the station.
@@ -269,7 +294,7 @@
- EmagImmune
- type: entity
parent: ComputerBase
parent: BaseComputer
id: computerBodyScanner
name: body scanner computer
description: A body scanner.
@@ -297,7 +322,7 @@
color: "#1f8c28"
- type: entity
parent: ComputerBase
parent: BaseComputer
id: ComputerComms
name: communications computer
description: This can be used for various important functions. Still under development.
@@ -351,7 +376,7 @@
color: "#f71713"
- type: entity
parent: ComputerBase
parent: BaseComputer
id: ComputerSolarControl
name: solar control computer
description: A controller for solar panel arrays.
@@ -381,7 +406,7 @@
- type: entity
parent: ComputerBase
parent: BaseComputer
id: ComputerRadar
name: mass scanner computer
description: A computer for detecting nearby bodies, displaying them by position and mass.
@@ -408,7 +433,7 @@
- type: entity
id: ComputerCargoShuttle
parent: ComputerBase
parent: BaseComputer
name: cargo shuttle computer
description: Used to order the shuttle.
components:
@@ -436,7 +461,7 @@
- type: entity
id: ComputerCargoOrders
parent: ComputerBase
parent: BaseComputer
name: cargo request computer
description: Used to order supplies and approve requests.
components:
@@ -463,7 +488,7 @@
access: [["Cargo"]]
- type: entity
parent: ComputerBase
parent: BaseComputer
id: ComputerSurveillanceCameraMonitor
name: camera monitor
description: A surveillance camera monitor. You're watching them. Maybe.
@@ -492,7 +517,7 @@
type: SurveillanceCameraMonitorBoundUserInterface
- type: entity
parent: ComputerBase
parent: BaseComputer
id: ComputerSurveillanceWirelessCameraMonitor
name: wireless camera monitor
description: A wireless surveillance camera monitor. You're watching them. Maybe.

View File

@@ -16,6 +16,7 @@
- nodevisfilter
- showspread
- showambient
- showemergencyshuttle
- Flags: MAPPING
Commands: