Cargo shuttle changes (#14363)

This commit is contained in:
metalgearsloth
2023-03-23 16:10:49 +11:00
committed by GitHub
parent 7f4bb7fe8a
commit 569f30b721
26 changed files with 523 additions and 628 deletions

View File

@@ -1,12 +1,12 @@
using Content.Client.Cargo.UI;
using Content.Shared.Cargo.BUI;
using Content.Shared.Cargo.Events;
using JetBrains.Annotations;
using Robust.Client.GameObjects;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
namespace Content.Client.Cargo.BUI;
[UsedImplicitly]
public sealed class CargoShuttleConsoleBoundUserInterface : BoundUserInterface
{
private CargoShuttleMenu? _menu;
@@ -21,9 +21,7 @@ public sealed class CargoShuttleConsoleBoundUserInterface : BoundUserInterface
if (collection == null)
return;
_menu = new CargoShuttleMenu(collection.Resolve<IGameTiming>(), collection.Resolve<IPrototypeManager>(), collection.Resolve<IEntitySystemManager>().GetEntitySystem<SpriteSystem>());
_menu.ShuttleCallRequested += OnShuttleCall;
_menu.ShuttleRecallRequested += OnShuttleRecall;
_menu = new CargoShuttleMenu(collection.Resolve<IPrototypeManager>(), collection.Resolve<IEntitySystemManager>().GetEntitySystem<SpriteSystem>());
_menu.OnClose += Close;
_menu.OpenCentered();
@@ -38,24 +36,12 @@ public sealed class CargoShuttleConsoleBoundUserInterface : BoundUserInterface
}
}
private void OnShuttleRecall()
{
SendMessage(new CargoRecallShuttleMessage());
}
private void OnShuttleCall()
{
SendMessage(new CargoCallShuttleMessage());
}
protected override void UpdateState(BoundUserInterfaceState state)
{
base.UpdateState(state);
if (state is not CargoShuttleConsoleBoundUserInterfaceState cargoState) return;
_menu?.SetAccountName(cargoState.AccountName);
_menu?.SetShuttleName(cargoState.ShuttleName);
_menu?.SetShuttleETA(cargoState.ShuttleETA);
_menu?.SetOrders(cargoState.Orders);
_menu?.SetCanRecall(cargoState.CanRecall);
}
}

View File

@@ -22,12 +22,6 @@
<Label Name="ShuttleStatusLabel"
Text="{Loc 'cargo-console-menu-shuttle-status-away-text'}" />
</BoxContainer>
<Button Name="ShuttleCallButton"
Text="Call Shuttle"/>
<Button Name="ShuttleRecallButton"
Text="Recall Shuttle"
ToolTip="Needs to be out of range to recall."
Visible="False"/>
<Label Text="{Loc 'cargo-console-menu-orders-label'}" />
<PanelContainer VerticalExpand="True"
SizeFlagsStretchRatio="6">

View File

@@ -3,8 +3,6 @@ using Content.Shared.Cargo;
using Content.Shared.Cargo.Prototypes;
using Robust.Client.AutoGenerated;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
@@ -14,23 +12,14 @@ namespace Content.Client.Cargo.UI
[GenerateTypedNameReferences]
public sealed partial class CargoShuttleMenu : FancyWindow
{
private readonly IGameTiming _timing;
private readonly IPrototypeManager _protoManager;
private readonly SpriteSystem _spriteSystem;
public Action? ShuttleCallRequested;
public Action? ShuttleRecallRequested;
private TimeSpan? _shuttleEta;
public CargoShuttleMenu(IGameTiming timing, IPrototypeManager protoManager, SpriteSystem spriteSystem)
public CargoShuttleMenu(IPrototypeManager protoManager, SpriteSystem spriteSystem)
{
RobustXamlLoader.Load(this);
_timing = timing;
_protoManager = protoManager;
_spriteSystem = spriteSystem;
ShuttleCallButton.OnPressed += OnCallPressed;
ShuttleRecallButton.OnPressed += OnRecallPressed;
Title = Loc.GetString("cargo-shuttle-console-menu-title");
}
@@ -44,33 +33,6 @@ namespace Content.Client.Cargo.UI
ShuttleNameLabel.Text = name;
}
public void SetShuttleETA(TimeSpan? eta)
{
_shuttleEta = eta;
if (eta == null)
{
ShuttleCallButton.Visible = false;
ShuttleRecallButton.Visible = true;
}
else
{
ShuttleRecallButton.Visible = false;
ShuttleCallButton.Visible = true;
ShuttleCallButton.Disabled = true;
}
}
private void OnRecallPressed(BaseButton.ButtonEventArgs obj)
{
ShuttleRecallRequested?.Invoke();
}
private void OnCallPressed(BaseButton.ButtonEventArgs obj)
{
ShuttleCallRequested?.Invoke();
}
public void SetOrders(List<CargoOrderData> orders)
{
Orders.DisposeAllChildren();
@@ -102,27 +64,5 @@ namespace Content.Client.Cargo.UI
Orders.AddChild(row);
}
}
public void SetCanRecall(bool canRecall)
{
ShuttleRecallButton.Disabled = !canRecall;
}
protected override void Draw(DrawingHandleScreen handle)
{
base.Draw(handle);
var remaining = _shuttleEta - _timing.CurTime;
if (remaining == null || remaining <= TimeSpan.Zero)
{
ShuttleStatusLabel.Text = $"Available";
ShuttleCallButton.Disabled = false;
}
else
{
ShuttleStatusLabel.Text = $"Available in: {remaining.Value.TotalSeconds:0.0}";
}
}
}
}

View File

@@ -1,5 +1,6 @@
using Content.Shared.Shuttles.BUIStates;
using Content.Shared.Shuttles.Components;
using Content.Shared.Shuttles.Systems;
using Robust.Client.Graphics;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
@@ -33,14 +34,14 @@ public sealed class RadarControl : Control
private Angle? _rotation;
private float _radarMinRange = 64f;
private float _radarMaxRange = 256f;
public float RadarRange { get; private set; } = 256f;
private float _radarMinRange = SharedRadarConsoleSystem.DefaultMinRange;
private float _radarMaxRange = SharedRadarConsoleSystem.DefaultMaxRange;
public float RadarRange { get; private set; } = SharedRadarConsoleSystem.DefaultMinRange;
/// <summary>
/// We'll lerp between the radarrange and actual range
/// </summary>
private float _actualRadarRange = 256f;
private float _actualRadarRange = SharedRadarConsoleSystem.DefaultMinRange;
/// <summary>
/// Controls the maximum distance that IFF labels will display.
@@ -87,14 +88,11 @@ public sealed class RadarControl : Control
{
_radarMaxRange = ls.MaxRange;
if (_radarMaxRange < RadarRange)
{
_actualRadarRange = _radarMaxRange;
}
if (_radarMaxRange < _radarMinRange)
_radarMinRange = _radarMaxRange;
_actualRadarRange = Math.Clamp(_actualRadarRange, _radarMinRange, _radarMaxRange);
_docks.Clear();
foreach (var state in ls.Docks)

View File

@@ -19,7 +19,7 @@ namespace Content.Server.Alert.Click
if (entManager.TryGetComponent(player, out PilotComponent? pilotComponent) &&
pilotComponent.Console != null)
{
entManager.System<ShuttleConsoleSystem>().RemovePilot(pilotComponent);
entManager.System<ShuttleConsoleSystem>().RemovePilot(player, pilotComponent);
}
}
}

View File

@@ -263,20 +263,25 @@ namespace Content.Server.Cargo.Systems
private void UpdateOrders(StationCargoOrderDatabaseComponent component)
{
// Order added so all consoles need updating.
foreach (var comp in EntityQuery<CargoOrderConsoleComponent>(true))
var orderQuery = AllEntityQuery<CargoOrderConsoleComponent>();
while (orderQuery.MoveNext(out var uid, out var comp))
{
var station = _station.GetOwningStation(component.Owner);
if (station != component.Owner) continue;
var station = _station.GetOwningStation(uid);
if (station != component.Owner)
continue;
UpdateOrderState(comp, station);
}
foreach (var comp in EntityQuery<CargoShuttleConsoleComponent>(true))
var consoleQuery = AllEntityQuery<CargoShuttleConsoleComponent>();
while (consoleQuery.MoveNext(out var uid, out var comp))
{
var station = _station.GetOwningStation(component.Owner);
if (station != component.Owner) continue;
var station = _station.GetOwningStation(uid);
if (station != component.Owner)
continue;
UpdateShuttleState(comp, station);
UpdateShuttleState(uid, comp, station);
}
}

View File

@@ -17,20 +17,16 @@ using Content.Shared.Cargo.Prototypes;
using Content.Shared.CCVar;
using Content.Shared.Dataset;
using Content.Shared.GameTicking;
using Content.Shared.Mobs.Components;
using Content.Shared.Mobs.Systems;
using Content.Shared.Whitelist;
using Robust.Server.GameObjects;
using Robust.Server.Maps;
using Robust.Shared.Audio;
using Robust.Shared.Configuration;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Player;
using Robust.Shared.Random;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
using Robust.Shared.Prototypes;
using Content.Shared.Coordinates;
using Content.Shared.Mobs.Components;
using Robust.Shared.Map.Components;
namespace Content.Server.Cargo.Systems;
@@ -40,22 +36,19 @@ public sealed partial class CargoSystem
* Handles cargo shuttle mechanics, including cargo shuttle consoles.
*/
[Dependency] private readonly IComponentFactory _factory = default!;
[Dependency] private readonly IConfigurationManager _configManager = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly EntityLookupSystem _lookup = default!;
[Dependency] private readonly MapLoaderSystem _map = default!;
[Dependency] private readonly MobStateSystem _mobState = default!;
[Dependency] private readonly PricingSystem _pricing = default!;
[Dependency] private readonly ShuttleConsoleSystem _console = default!;
[Dependency] private readonly ShuttleSystem _shuttle = default!;
[Dependency] private readonly StackSystem _stack = default!;
public MapId? CargoMap { get; private set; }
private const float CallOffset = 50f;
private int _index;
/// <summary>
@@ -69,10 +62,11 @@ public sealed partial class CargoSystem
// Don't want to immediately call this as shuttles will get setup in the natural course of things.
_configManager.OnValueChanged(CCVars.CargoShuttles, SetCargoShuttleEnabled);
SubscribeLocalEvent<CargoShuttleComponent, MoveEvent>(OnCargoShuttleMove);
SubscribeLocalEvent<CargoShuttleComponent, FTLStartedEvent>(OnCargoFTLStarted);
SubscribeLocalEvent<CargoShuttleComponent, FTLCompletedEvent>(OnCargoFTLCompleted);
SubscribeLocalEvent<CargoShuttleComponent, FTLTagEvent>(OnCargoFTLTag);
SubscribeLocalEvent<CargoShuttleConsoleComponent, ComponentStartup>(OnCargoShuttleConsoleStartup);
SubscribeLocalEvent<CargoShuttleConsoleComponent, CargoCallShuttleMessage>(OnCargoShuttleCall);
SubscribeLocalEvent<CargoShuttleConsoleComponent, CargoRecallShuttleMessage>(RecallCargoShuttle);
SubscribeLocalEvent<CargoPalletConsoleComponent, CargoPalletSellMessage>(OnPalletSale);
SubscribeLocalEvent<CargoPalletConsoleComponent, CargoPalletAppraiseMessage>(OnPalletAppraise);
@@ -87,6 +81,16 @@ public sealed partial class CargoSystem
SubscribeLocalEvent<RoundRestartCleanupEvent>(OnRoundRestart);
}
private void OnCargoFTLTag(EntityUid uid, CargoShuttleComponent component, ref FTLTagEvent args)
{
if (args.Handled)
return;
// Just saves mappers forgetting.
args.Handled = true;
args.Tag = "DockCargo";
}
private void ShutdownShuttle()
{
_configManager.UnsubValueChanged(CCVars.CargoShuttles, SetCargoShuttleEnabled);
@@ -94,7 +98,9 @@ public sealed partial class CargoSystem
private void SetCargoShuttleEnabled(bool value)
{
if (_enabled == value) return;
if (_enabled == value)
return;
_enabled = value;
if (value)
@@ -116,7 +122,7 @@ public sealed partial class CargoSystem
private void OnCargoPilotConsoleOpen(EntityUid uid, CargoPilotConsoleComponent component, AfterActivatableUIOpenEvent args)
{
component.Entity = GetShuttleConsole(component);
component.Entity = GetShuttleConsole(uid);
}
private void OnCargoPilotConsoleClose(EntityUid uid, CargoPilotConsoleComponent component, BoundUIClosedEvent args)
@@ -126,30 +132,50 @@ public sealed partial class CargoSystem
private void OnCargoGetConsole(EntityUid uid, CargoPilotConsoleComponent component, ref ConsoleShuttleEvent args)
{
args.Console = GetShuttleConsole(component);
args.Console = GetShuttleConsole(uid);
}
private EntityUid? GetShuttleConsole(CargoPilotConsoleComponent component)
private EntityUid? GetShuttleConsole(EntityUid uid)
{
var stationUid = _station.GetOwningStation(component.Owner);
var stationUid = _station.GetOwningStation(uid);
if (!TryComp<StationCargoOrderDatabaseComponent>(stationUid, out var orderDatabase) ||
!TryComp<CargoShuttleComponent>(orderDatabase.Shuttle, out var shuttle)) return null;
!TryComp<CargoShuttleComponent>(orderDatabase.Shuttle, out var shuttle))
{
return null;
}
return GetShuttleConsole(shuttle);
return GetShuttleConsole(orderDatabase.Shuttle.Value, shuttle);
}
#endregion
#region Console
private void UpdateShuttleCargoConsoles(CargoShuttleComponent component)
private void UpdateCargoShuttleConsoles(CargoShuttleComponent component)
{
foreach (var console in EntityQuery<CargoShuttleConsoleComponent>(true))
// Update pilot consoles that are already open.
var pilotConsoleQuery = AllEntityQuery<CargoPilotConsoleComponent>();
while (pilotConsoleQuery.MoveNext(out var uid, out var console))
{
var stationUid = _station.GetOwningStation(console.Owner);
if (stationUid != component.Station) continue;
UpdateShuttleState(console, stationUid);
var stationUid = _station.GetOwningStation(uid);
if (stationUid == null || stationUid != component.Station)
continue;
console.Entity = GetShuttleConsole(stationUid.Value);
}
// Update order consoles.
var shuttleConsoleQuery = AllEntityQuery<CargoShuttleConsoleComponent>();
while (shuttleConsoleQuery.MoveNext(out var uid, out var console))
{
var stationUid = _station.GetOwningStation(uid);
if (stationUid != component.Station)
continue;
UpdateShuttleState(uid, console, stationUid);
}
}
@@ -198,10 +224,10 @@ public sealed partial class CargoSystem
private void OnCargoShuttleConsoleStartup(EntityUid uid, CargoShuttleConsoleComponent component, ComponentStartup args)
{
var station = _station.GetOwningStation(uid);
UpdateShuttleState(component, station);
UpdateShuttleState(uid, component, station);
}
private void UpdateShuttleState(CargoShuttleConsoleComponent component, EntityUid? station = null)
private void UpdateShuttleState(EntityUid uid, CargoShuttleConsoleComponent component, EntityUid? station = null)
{
TryComp<StationCargoOrderDatabaseComponent>(station, out var orderDatabase);
TryComp<CargoShuttleComponent>(orderDatabase?.Shuttle, out var shuttle);
@@ -209,12 +235,10 @@ public sealed partial class CargoSystem
var orders = GetProjectedOrders(orderDatabase, shuttle);
var shuttleName = orderDatabase?.Shuttle != null ? MetaData(orderDatabase.Shuttle.Value).EntityName : string.Empty;
_uiSystem.GetUiOrNull(component.Owner, CargoConsoleUiKey.Shuttle)?.SetState(
_uiSystem.GetUiOrNull(uid, CargoConsoleUiKey.Shuttle)?.SetState(
new CargoShuttleConsoleBoundUserInterfaceState(
station != null ? MetaData(station.Value).EntityName : Loc.GetString("cargo-shuttle-console-station-unknown"),
string.IsNullOrEmpty(shuttleName) ? Loc.GetString("cargo-shuttle-console-shuttle-not-found") : shuttleName,
_shuttle.CanFTL(shuttle?.Owner, out _),
shuttle?.NextCall,
orders));
}
@@ -222,32 +246,21 @@ public sealed partial class CargoSystem
#region Shuttle
public EntityUid? GetShuttleConsole(CargoShuttleComponent component)
public EntityUid? GetShuttleConsole(EntityUid uid, CargoShuttleComponent component)
{
foreach (var (comp, xform) in EntityQuery<ShuttleConsoleComponent, TransformComponent>(true))
var query = AllEntityQuery<ShuttleConsoleComponent, TransformComponent>();
while (query.MoveNext(out var cUid, out var comp, out var xform))
{
if (xform.ParentUid != component.Owner) continue;
return comp.Owner;
if (xform.ParentUid != uid)
continue;
return cUid;
}
return null;
}
private void OnCargoShuttleMove(EntityUid uid, CargoShuttleComponent component, ref MoveEvent args)
{
if (component.Station == null) return;
var oldCanRecall = component.CanRecall;
// Check if we can update the recall status.
var canRecall = _shuttle.CanFTL(uid, out _, args.Component);
if (oldCanRecall == canRecall) return;
component.CanRecall = canRecall;
_sawmill.Debug($"Updated CanRecall for {ToPrettyString(uid)}");
UpdateShuttleCargoConsoles(component);
}
/// <summary>
/// Returns the orders that can fit on the cargo shuttle.
/// </summary>
@@ -329,7 +342,10 @@ public sealed partial class CargoSystem
if (CargoMap == null ||
component.Shuttle != null ||
component.CargoShuttleProto == null) return;
component.CargoShuttleProto == null)
{
return;
}
var prototype = _protoMan.Index<CargoShuttlePrototype>(component.CargoShuttleProto);
var possibleNames = _protoMan.Index<DatasetPrototype>(prototype.NameDataset).Values;
@@ -348,11 +364,9 @@ public sealed partial class CargoSystem
xform.LocalPosition += 100 * _index;
var comp = EnsureComp<CargoShuttleComponent>(shuttleUid);
comp.Station = component.Owner;
comp.Coordinates = xform.Coordinates;
component.Shuttle = shuttleUid;
comp.NextCall = _timing.CurTime + TimeSpan.FromSeconds(comp.Cooldown);
UpdateShuttleCargoConsoles(comp);
UpdateCargoShuttleConsoles(comp);
_index++;
_sawmill.Info($"Added cargo shuttle to {ToPrettyString(shuttleUid)}");
}
@@ -400,80 +414,6 @@ public sealed partial class CargoSystem
}
}
private void SendToCargoMap(EntityUid uid, CargoShuttleComponent? component = null)
{
if (!Resolve(uid, ref component))
return;
component.NextCall = _timing.CurTime + TimeSpan.FromSeconds(component.Cooldown);
Transform(uid).Coordinates = component.Coordinates;
DebugTools.Assert(MetaData(uid).EntityPaused);
UpdateShuttleCargoConsoles(component);
_sawmill.Info($"Stashed cargo shuttle {ToPrettyString(uid)}");
}
/// <summary>
/// Retrieves a shuttle for delivery.
/// </summary>
public void CallShuttle(StationCargoOrderDatabaseComponent orderDatabase)
{
if (!TryComp<CargoShuttleComponent>(orderDatabase.Shuttle, out var shuttle))
return;
// Already called / not available
if (shuttle.NextCall == null || _timing.CurTime < shuttle.NextCall)
return;
if (!TryComp<StationDataComponent>(orderDatabase.Owner, out var stationData))
return;
var targetGrid = _station.GetLargestGrid(stationData);
// Nowhere to warp in to.
if (!TryComp<TransformComponent>(targetGrid, out var xform))
return;
shuttle.NextCall = null;
// Find a valid free area nearby to spawn in on
// TODO: Make this use hyperspace now.
var center = new Vector2();
var minRadius = 0f;
Box2? aabb = null;
var xformQuery = GetEntityQuery<TransformComponent>();
foreach (var grid in _mapManager.GetAllMapGrids(xform.MapID))
{
var worldAABB = xformQuery.GetComponent(grid.Owner).WorldMatrix.TransformBox(grid.LocalAABB);
aabb = aabb?.Union(worldAABB) ?? worldAABB;
}
if (aabb != null)
{
center = aabb.Value.Center;
minRadius = MathF.Max(aabb.Value.Width, aabb.Value.Height);
}
var offset = 0f;
if (TryComp<MapGridComponent>(orderDatabase.Shuttle, out var shuttleGrid))
{
var bounds = shuttleGrid.LocalAABB;
offset = MathF.Max(bounds.Width, bounds.Height) / 2f;
}
Transform(shuttle.Owner).Coordinates = new EntityCoordinates(xform.ParentUid,
center + _random.NextVector2(minRadius + offset, minRadius + CallOffset + offset));
DebugTools.Assert(!MetaData(shuttle.Owner).EntityPaused);
AddCargoContents(shuttle, orderDatabase);
UpdateOrders(orderDatabase);
UpdateShuttleCargoConsoles(shuttle);
_console.RefreshShuttleConsoles();
_sawmill.Info($"Retrieved cargo shuttle {ToPrettyString(shuttle.Owner)} from {ToPrettyString(orderDatabase.Owner)}");
}
private void AddCargoContents(CargoShuttleComponent shuttle, StationCargoOrderDatabaseComponent orderDatabase)
{
var xformQuery = GetEntityQuery<TransformComponent>();
@@ -510,74 +450,37 @@ public sealed partial class CargoSystem
UpdatePalletConsoleInterface(uid, component);
}
private void RecallCargoShuttle(EntityUid uid, CargoShuttleConsoleComponent component, CargoRecallShuttleMessage args)
private void OnCargoFTLStarted(EntityUid uid, CargoShuttleComponent component, ref FTLStartedEvent args)
{
var player = args.Session.AttachedEntity;
var xform = Transform(uid);
var stationUid = component.Station;
if (player == null) return;
var stationUid = _station.GetOwningStation(uid);
if (!TryComp<StationCargoOrderDatabaseComponent>(stationUid, out var orderDatabase) ||
!TryComp<StationBankAccountComponent>(stationUid, out var bank)) return;
if (!TryComp<CargoShuttleComponent>(orderDatabase.Shuttle, out var shuttle))
// Called
if (xform.MapID != CargoMap ||
!TryComp<StationCargoOrderDatabaseComponent>(stationUid, out var orderDatabase))
{
_popup.PopupEntity(Loc.GetString("cargo-no-shuttle"), args.Entity, args.Entity);
return;
}
if (!_shuttle.CanFTL(shuttle.Owner, out var reason))
{
_popup.PopupEntity(reason, args.Entity, args.Entity);
AddCargoContents(component, orderDatabase);
UpdateOrders(orderDatabase);
UpdateCargoShuttleConsoles(component);
}
private void OnCargoFTLCompleted(EntityUid uid, CargoShuttleComponent component, ref FTLCompletedEvent args)
{
var xform = Transform(uid);
// Recalled
if (xform.MapID != CargoMap)
return;
}
if (IsBlocked(shuttle))
var stationUid = component.Station;
if (TryComp<StationBankAccountComponent>(stationUid, out var bank))
{
_popup.PopupEntity(Loc.GetString("cargo-shuttle-console-organics"), player.Value, player.Value);
_audio.PlayPvs(_audio.GetSound(component.DenySound), uid);
return;
SellPallets(uid, out var amount);
bank.Balance += (int) amount;
}
SellPallets((EntityUid) orderDatabase.Shuttle, out double price);
bank.Balance += (int) price;
_console.RefreshShuttleConsoles();
SendToCargoMap(orderDatabase.Shuttle.Value);
}
/// <summary>
///
/// </summary>
/// <param name="component"></param>
private bool IsBlocked(CargoShuttleComponent component)
{
// TODO: Would be good to rate-limit this on the console.
var mobQuery = GetEntityQuery<MobStateComponent>();
var xformQuery = GetEntityQuery<TransformComponent>();
return FoundOrganics(component.Owner, mobQuery, xformQuery);
}
public bool FoundOrganics(EntityUid uid, EntityQuery<MobStateComponent> mobQuery, EntityQuery<TransformComponent> xformQuery)
{
var xform = xformQuery.GetComponent(uid);
var childEnumerator = xform.ChildEnumerator;
while (childEnumerator.MoveNext(out var child))
{
if ((mobQuery.TryGetComponent(child.Value, out var mobState) && !_mobState.IsDead(child.Value, mobState))
|| FoundOrganics(child.Value, mobQuery, xformQuery)) return true;
}
return false;
}
private void OnCargoShuttleCall(EntityUid uid, CargoShuttleConsoleComponent component, CargoCallShuttleMessage args)
{
var stationUid = _station.GetOwningStation(args.Entity);
if (!TryComp<StationCargoOrderDatabaseComponent>(stationUid, out var orderDatabase)) return;
CallShuttle(orderDatabase);
}
#endregion
@@ -600,13 +503,15 @@ public sealed partial class CargoSystem
CargoMap = null;
// Shuttle may not have been in the cargo dimension (e.g. on the station map) so need to delete.
foreach (var comp in EntityQuery<CargoShuttleComponent>())
var query = AllEntityQuery<CargoShuttleComponent>();
while (query.MoveNext(out var uid, out var comp))
{
if (TryComp<StationCargoOrderDatabaseComponent>(comp.Station, out var station))
{
station.Shuttle = null;
}
QueueDel(comp.Owner);
QueueDel(uid);
}
}
@@ -619,11 +524,23 @@ public sealed partial class CargoSystem
// It gets mapinit which is okay... buuutt we still want it paused to avoid power draining.
CargoMap = _mapManager.CreateMap();
_mapManager.SetMapPaused(CargoMap!.Value, true);
var mapUid = _mapManager.GetMapEntityId(CargoMap.Value);
var ftl = EnsureComp<FTLDestinationComponent>(_mapManager.GetMapEntityId(CargoMap.Value));
ftl.Whitelist = new EntityWhitelist()
{
Components = new[]
{
_factory.GetComponentName(typeof(CargoShuttleComponent))
}
};
MetaData(mapUid).EntityName = $"Trading post {_random.Next(1000):000}";
foreach (var comp in EntityQuery<StationCargoOrderDatabaseComponent>(true))
{
AddShuttle(comp);
}
_console.RefreshShuttleConsoles();
}
}

View File

@@ -1,7 +0,0 @@
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

@@ -1,6 +1,8 @@
using Content.Shared.Shuttles.Systems;
using Content.Shared.Tag;
using Robust.Shared.Audio;
using Robust.Shared.Map;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Server.Shuttles.Components;
@@ -37,6 +39,12 @@ public sealed class FTLComponent : Component
[ViewVariables(VVAccess.ReadWrite), DataField("dock")]
public bool Dock;
/// <summary>
/// If we're docking after FTL what is the prioritised dock tag (if applicable).
/// </summary>
[ViewVariables(VVAccess.ReadWrite), DataField("priorityTag", customTypeSerializer:typeof(PrototypeIdSerializer<TagPrototype>))]
public string? PriorityTag;
[ViewVariables(VVAccess.ReadWrite), DataField("soundTravel")]
public SoundSpecifier? TravelSound = new SoundPathSpecifier("/Audio/Effects/Shuttle/hyperspace_progress.ogg")
{

View File

@@ -0,0 +1,18 @@
using Content.Shared.Tag;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Server.Shuttles.Components;
/// <summary>
/// Given priority when considering where to dock.
/// </summary>
[RegisterComponent]
public sealed class PriorityDockComponent : Component
{
/// <summary>
/// Tag to match on the docking request, if this dock is to be prioritised.
/// </summary>
[ViewVariables(VVAccess.ReadWrite),
DataField("tag", customTypeSerializer: typeof(PrototypeIdSerializer<TagPrototype>))]
public string? Tag;
}

View File

@@ -1,5 +1,3 @@
using Content.Shared.Shuttles.Components;
namespace Content.Server.Shuttles.Components
{
[RegisterComponent]

View File

@@ -9,6 +9,8 @@ namespace Content.Server.Shuttles;
[AdminCommand(AdminFlags.Mapping)]
public sealed class DockCommand : IConsoleCommand
{
[Dependency] private readonly IEntityManager _entManager = default!;
public string Command => "dock";
public string Description => $"Attempts to dock 2 airlocks together. Doesn't check whether it is valid.";
public string Help => $"{Command} <airlock entityuid1> <airlock entityuid2>";
@@ -32,21 +34,19 @@ public sealed class DockCommand : IConsoleCommand
return;
}
var entManager = IoCManager.Resolve<IEntityManager>();
if (!entManager.TryGetComponent(airlock1, out DockingComponent? dock1))
if (!_entManager.TryGetComponent(airlock1, out DockingComponent? dock1))
{
shell.WriteError($"No docking component found on {airlock1}");
return;
}
if (!entManager.TryGetComponent(airlock2, out DockingComponent? dock2))
if (!_entManager.TryGetComponent(airlock2, out DockingComponent? dock2))
{
shell.WriteError($"No docking component found on {airlock2}");
return;
}
var dockSystem = EntitySystem.Get<DockingSystem>();
dockSystem.Dock(dock1, dock2);
var dockSystem = _entManager.System<DockingSystem>();
dockSystem.Dock(airlock1, dock1, airlock2, dock2);
}
}

View File

@@ -0,0 +1,7 @@
namespace Content.Server.Shuttles.Events;
/// <summary>
/// Raised when trying to get a priority tag for docking.
/// </summary>
[ByRefEvent]
public record struct FTLTagEvent(bool Handled, string? Tag);

View File

@@ -16,23 +16,25 @@ public sealed partial class DockingSystem
var dockingQuery = GetEntityQuery<DockingComponent>();
var xformQuery = GetEntityQuery<TransformComponent>();
var recentQuery = GetEntityQuery<RecentlyDockedComponent>();
var query = EntityQueryEnumerator<AutoDockComponent, PhysicsComponent>();
foreach (var (comp, body) in EntityQuery<AutoDockComponent, PhysicsComponent>())
while (query.MoveNext(out var dockUid, out var comp, out var body))
{
if (comp.Requesters.Count == 0 || !dockingQuery.TryGetComponent(comp.Owner, out var dock))
if (comp.Requesters.Count == 0 || !dockingQuery.TryGetComponent(dockUid, out var dock))
{
RemComp<AutoDockComponent>(comp.Owner);
RemComp<AutoDockComponent>(dockUid);
continue;
}
// Don't re-dock if we're already docked or recently were.
if (dock.Docked || recentQuery.HasComponent(comp.Owner)) continue;
if (dock.Docked || recentQuery.HasComponent(dockUid))
continue;
var dockable = GetDockable(body, xformQuery.GetComponent(comp.Owner));
var dockable = GetDockable(body, xformQuery.GetComponent(dockUid));
if (dockable == null) continue;
TryDock(dock, dockable);
TryDock(dockUid, dock, dockable.Owner, dockable);
}
// Work out recent docks that have gone past their designated threshold.

View File

@@ -144,12 +144,13 @@ namespace Content.Server.Shuttles.Systems
private void Cleanup(DockingComponent dockA)
{
_pathfinding.RemovePortal(dockA.PathfindHandle);
_jointSystem.RemoveJoint(dockA.DockJoint!);
if (dockA.DockJoint != null)
_jointSystem.RemoveJoint(dockA.DockJoint);
var dockBUid = dockA.DockedWith;
if (dockBUid == null ||
dockA.DockJoint == null ||
!TryComp(dockBUid, out DockingComponent? dockB))
{
DebugTools.Assert(false);
@@ -187,9 +188,9 @@ namespace Content.Server.Shuttles.Systems
GridBUid = gridBUid!.Value,
};
EntityManager.EventBus.RaiseLocalEvent(dockA.Owner, msg, false);
EntityManager.EventBus.RaiseLocalEvent(dockB.Owner, msg, false);
EntityManager.EventBus.RaiseEvent(EventSource.Local, msg);
RaiseLocalEvent(dockA.Owner, msg);
RaiseLocalEvent(dockB.Owner, msg);
RaiseLocalEvent(msg);
}
private void OnStartup(EntityUid uid, DockingComponent component, ComponentStartup args)
@@ -208,7 +209,7 @@ namespace Content.Server.Shuttles.Systems
var otherDock = EntityManager.GetComponent<DockingComponent>(component.DockedWith.Value);
DebugTools.Assert(otherDock.DockedWith != null);
Dock(component, otherDock);
Dock(uid, component, component.DockedWith.Value, otherDock);
DebugTools.Assert(component.Docked && otherDock.Docked);
}
}
@@ -234,7 +235,7 @@ namespace Content.Server.Shuttles.Systems
var other = Comp<DockingComponent>(component.DockedWith!.Value);
Undock(component);
Dock(component, other);
Dock(uid, component, component.DockedWith.Value, other);
_console.RefreshShuttleConsoles();
}
@@ -278,91 +279,96 @@ namespace Content.Server.Shuttles.Systems
/// <summary>
/// Docks 2 ports together and assumes it is valid.
/// </summary>
public void Dock(DockingComponent dockA, DockingComponent dockB)
public void Dock(EntityUid dockAUid, DockingComponent dockA, EntityUid dockBUid, DockingComponent dockB)
{
if (dockB.Owner.GetHashCode() < dockA.Owner.GetHashCode())
if (dockBUid.GetHashCode() < dockAUid.GetHashCode())
{
(dockA, dockB) = (dockB, dockA);
}
_sawmill.Debug($"Docking between {dockA.Owner} and {dockB.Owner}");
_sawmill.Debug($"Docking between {dockAUid} and {dockBUid}");
// https://gamedev.stackexchange.com/questions/98772/b2distancejoint-with-frequency-equal-to-0-vs-b2weldjoint
// We could also potentially use a prismatic joint? Depending if we want clamps that can extend or whatever
var dockAXform = EntityManager.GetComponent<TransformComponent>(dockA.Owner);
var dockBXform = EntityManager.GetComponent<TransformComponent>(dockB.Owner);
var dockAXform = EntityManager.GetComponent<TransformComponent>(dockAUid);
var dockBXform = EntityManager.GetComponent<TransformComponent>(dockBUid);
DebugTools.Assert(dockAXform.GridUid != null);
DebugTools.Assert(dockBXform.GridUid != null);
var gridA = dockAXform.GridUid!.Value;
var gridB = dockBXform.GridUid!.Value;
SharedJointSystem.LinearStiffness(
2f,
0.7f,
EntityManager.GetComponent<PhysicsComponent>(gridA).Mass,
EntityManager.GetComponent<PhysicsComponent>(gridB).Mass,
out var stiffness,
out var damping);
// These need playing around with
// Could also potentially have collideconnected false and stiffness 0 but it was a bit more suss???
WeldJoint joint;
// Pre-existing joint so use that.
if (dockA.DockJointId != null)
// May not be possible if map or the likes.
if (TryComp<PhysicsComponent>(gridA, out var gridPhysicsA) &&
TryComp<PhysicsComponent>(gridB, out var gridPhysicsB))
{
DebugTools.Assert(dockB.DockJointId == dockA.DockJointId);
joint = _jointSystem.GetOrCreateWeldJoint(gridA, gridB, dockA.DockJointId);
}
else
{
joint = _jointSystem.GetOrCreateWeldJoint(gridA, gridB, DockingJoint + dockA.Owner);
SharedJointSystem.LinearStiffness(
2f,
0.7f,
EntityManager.GetComponent<PhysicsComponent>(gridA).Mass,
EntityManager.GetComponent<PhysicsComponent>(gridB).Mass,
out var stiffness,
out var damping);
// These need playing around with
// Could also potentially have collideconnected false and stiffness 0 but it was a bit more suss???
WeldJoint joint;
// Pre-existing joint so use that.
if (dockA.DockJointId != null)
{
DebugTools.Assert(dockB.DockJointId == dockA.DockJointId);
joint = _jointSystem.GetOrCreateWeldJoint(gridA, gridB, dockA.DockJointId);
}
else
{
joint = _jointSystem.GetOrCreateWeldJoint(gridA, gridB, DockingJoint + dockAUid);
}
var gridAXform = EntityManager.GetComponent<TransformComponent>(gridA);
var gridBXform = EntityManager.GetComponent<TransformComponent>(gridB);
var anchorA = dockAXform.LocalPosition + dockAXform.LocalRotation.ToWorldVec() / 2f;
var anchorB = dockBXform.LocalPosition + dockBXform.LocalRotation.ToWorldVec() / 2f;
joint.LocalAnchorA = anchorA;
joint.LocalAnchorB = anchorB;
joint.ReferenceAngle = (float) (gridBXform.WorldRotation - gridAXform.WorldRotation);
joint.CollideConnected = true;
joint.Stiffness = stiffness;
joint.Damping = damping;
dockA.DockJoint = joint;
dockA.DockJointId = joint.ID;
dockB.DockJoint = joint;
dockB.DockJointId = joint.ID;
}
var gridAXform = EntityManager.GetComponent<TransformComponent>(gridA);
var gridBXform = EntityManager.GetComponent<TransformComponent>(gridB);
dockA.DockedWith = dockBUid;
dockB.DockedWith = dockAUid;
var anchorA = dockAXform.LocalPosition + dockAXform.LocalRotation.ToWorldVec() / 2f;
var anchorB = dockBXform.LocalPosition + dockBXform.LocalRotation.ToWorldVec() / 2f;
joint.LocalAnchorA = anchorA;
joint.LocalAnchorB = anchorB;
joint.ReferenceAngle = (float) (gridBXform.WorldRotation - gridAXform.WorldRotation);
joint.CollideConnected = true;
joint.Stiffness = stiffness;
joint.Damping = damping;
dockA.DockedWith = dockB.Owner;
dockB.DockedWith = dockA.Owner;
dockA.DockJoint = joint;
dockA.DockJointId = joint.ID;
dockB.DockJoint = joint;
dockB.DockJointId = joint.ID;
if (TryComp(dockA.Owner, out DoorComponent? doorA))
if (TryComp(dockAUid, out DoorComponent? doorA))
{
if (_doorSystem.TryOpen(doorA.Owner, doorA))
{
doorA.ChangeAirtight = false;
if (TryComp<AirlockComponent>(dockA.Owner, out var airlockA))
if (TryComp<AirlockComponent>(dockAUid, out var airlockA))
{
_airlocks.SetBoltsWithAudio(dockA.Owner, airlockA, true);
_airlocks.SetBoltsWithAudio(dockAUid, airlockA, true);
}
}
}
if (TryComp(dockB.Owner, out DoorComponent? doorB))
if (TryComp(dockBUid, out DoorComponent? doorB))
{
if (_doorSystem.TryOpen(doorB.Owner, doorB))
{
doorB.ChangeAirtight = false;
if (TryComp<AirlockComponent>(dockB.Owner, out var airlockB))
if (TryComp<AirlockComponent>(dockBUid, out var airlockB))
{
_airlocks.SetBoltsWithAudio(dockB.Owner, airlockB, true);
_airlocks.SetBoltsWithAudio(dockBUid, airlockB, true);
}
}
}
@@ -381,9 +387,9 @@ namespace Content.Server.Shuttles.Systems
GridBUid = gridB,
};
EntityManager.EventBus.RaiseLocalEvent(dockA.Owner, msg, false);
EntityManager.EventBus.RaiseLocalEvent(dockB.Owner, msg, false);
EntityManager.EventBus.RaiseEvent(EventSource.Local, msg);
RaiseLocalEvent(dockAUid, msg);
RaiseLocalEvent(dockBUid, msg);
RaiseLocalEvent(msg);
}
private bool CanDock(DockingComponent dockA, DockingComponent dockB)
@@ -433,11 +439,11 @@ namespace Content.Server.Shuttles.Systems
/// <summary>
/// Attempts to dock 2 ports together and will return early if it's not possible.
/// </summary>
private void TryDock(DockingComponent dockA, DockingComponent dockB)
private void TryDock(EntityUid dockAUid, DockingComponent dockA, EntityUid dockBUid, DockingComponent dockB)
{
if (!CanDock(dockA, dockB)) return;
Dock(dockA, dockB);
Dock(dockAUid, dockA, dockBUid, dockB);
}
public void Undock(DockingComponent dock)

View File

@@ -13,8 +13,8 @@ using Content.Shared.Shuttles.Systems;
using Content.Shared.Tag;
using Robust.Server.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.Map.Components;
using Robust.Shared.Physics.Components;
using Robust.Shared.Player;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
@@ -68,9 +68,10 @@ namespace Content.Server.Shuttles.Systems
return;
}
if (!dest.Enabled) return;
if (!dest.Enabled)
return;
EntityUid? entity = component.Owner;
EntityUid? entity = uid;
var getShuttleEv = new ConsoleShuttleEvent
{
@@ -80,13 +81,14 @@ namespace Content.Server.Shuttles.Systems
RaiseLocalEvent(entity.Value, ref getShuttleEv);
entity = getShuttleEv.Console;
if (entity == null || dest.Whitelist?.IsValid(entity.Value, EntityManager) == false)
if (!TryComp<TransformComponent>(entity, out var xform) ||
!TryComp<ShuttleComponent>(xform.GridUid, out var shuttle))
{
return;
}
if (!TryComp<TransformComponent>(entity, out var xform) ||
!TryComp<ShuttleComponent>(xform.GridUid, out var shuttle))
if (dest.Whitelist?.IsValid(entity.Value, EntityManager) == false &&
dest.Whitelist?.IsValid(xform.GridUid.Value, EntityManager) == false)
{
return;
}
@@ -97,13 +99,17 @@ namespace Content.Server.Shuttles.Systems
return;
}
if (!_shuttle.CanFTL(shuttle.Owner, out var reason))
if (!_shuttle.CanFTL(xform.GridUid, out var reason))
{
_popup.PopupCursor(reason, args.Session);
return;
}
_shuttle.FTLTravel(shuttle, args.Destination);
var dock = HasComp<MapComponent>(args.Destination) && HasComp<MapGridComponent>(args.Destination);
var tagEv = new FTLTagEvent();
RaiseLocalEvent(xform.GridUid.Value, ref tagEv);
_shuttle.FTLTravel(shuttle, args.Destination, dock: dock, priorityTag: tagEv.Tag);
}
private void OnDock(DockEvent ev)
@@ -128,10 +134,11 @@ namespace Content.Server.Shuttles.Systems
public void RefreshShuttleConsoles()
{
var docks = GetAllDocks();
var query = AllEntityQuery<ShuttleConsoleComponent>();
foreach (var comp in EntityQuery<ShuttleConsoleComponent>(true))
while (query.MoveNext(out var uid, out var comp))
{
UpdateState(comp, docks);
UpdateState(uid, comp, docks);
}
}
@@ -141,7 +148,10 @@ namespace Content.Server.Shuttles.Systems
private void OnConsoleUIClose(EntityUid uid, ShuttleConsoleComponent component, BoundUIClosedEvent args)
{
if ((ShuttleConsoleUiKey) args.UiKey != ShuttleConsoleUiKey.Key ||
args.Session.AttachedEntity is not {} user) return;
args.Session.AttachedEntity is not { } user)
{
return;
}
// In case they D/C should still clean them up.
foreach (var comp in EntityQuery<AutoDockComponent>(true))
@@ -160,12 +170,12 @@ namespace Content.Server.Shuttles.Systems
private void OnConsoleAnchorChange(EntityUid uid, ShuttleConsoleComponent component, ref AnchorStateChangedEvent args)
{
UpdateState(component);
UpdateState(uid, component);
}
private void OnConsolePowerChange(EntityUid uid, ShuttleConsoleComponent component, ref PowerChangedEvent args)
{
UpdateState(component);
UpdateState(uid, component);
}
private bool TryPilot(EntityUid user, EntityUid uid)
@@ -184,7 +194,7 @@ namespace Content.Server.Shuttles.Systems
if (console != null)
{
RemovePilot(pilotComponent);
RemovePilot(user, pilotComponent);
if (console == component)
{
@@ -208,16 +218,18 @@ namespace Content.Server.Shuttles.Systems
{
// TODO: NEED TO MAKE SURE THIS UPDATES ON ANCHORING CHANGES!
var result = new List<DockingInterfaceState>();
var query = AllEntityQuery<DockingComponent, TransformComponent>();
foreach (var (comp, xform) in EntityQuery<DockingComponent, TransformComponent>(true))
while (query.MoveNext(out var uid, out var comp, out var xform))
{
if (xform.ParentUid != xform.GridUid) continue;
if (xform.ParentUid != xform.GridUid)
continue;
var state = new DockingInterfaceState()
{
Coordinates = xform.Coordinates,
Angle = xform.LocalRotation,
Entity = comp.Owner,
Entity = uid,
Connected = comp.Docked,
Color = comp.RadarColor,
HighlightedColor = comp.HighlightedRadarColor,
@@ -228,9 +240,9 @@ namespace Content.Server.Shuttles.Systems
return result;
}
private void UpdateState(ShuttleConsoleComponent component, List<DockingInterfaceState>? docks = null)
private void UpdateState(EntityUid consoleUid, ShuttleConsoleComponent component, List<DockingInterfaceState>? docks = null)
{
EntityUid? entity = component.Owner;
EntityUid? entity = consoleUid;
var getShuttleEv = new ConsoleShuttleEvent
{
@@ -242,38 +254,44 @@ namespace Content.Server.Shuttles.Systems
TryComp<TransformComponent>(entity, out var consoleXform);
TryComp<RadarConsoleComponent>(entity, out var radar);
var range = radar?.MaxRange ?? 0f;
var range = radar?.MaxRange ?? SharedRadarConsoleSystem.DefaultMaxRange;
TryComp<ShuttleComponent>(consoleXform?.GridUid, out var shuttle);
var shuttleGridUid = consoleXform?.GridUid;
var destinations = new List<(EntityUid, string, bool)>();
var ftlState = FTLState.Available;
var ftlTime = TimeSpan.Zero;
if (TryComp<FTLComponent>(shuttle?.Owner, out var shuttleFtl))
if (TryComp<FTLComponent>(shuttleGridUid, out var shuttleFtl))
{
ftlState = shuttleFtl.State;
ftlTime = _timing.CurTime + TimeSpan.FromSeconds(shuttleFtl.Accumulator);
}
// Mass too large
if (entity != null && shuttle?.Owner != null && (!TryComp<PhysicsComponent>(shuttle?.Owner, out var shuttleBody) ||
shuttleBody.Mass < 1000f))
if (entity != null && shuttleGridUid != null &&
(!TryComp<PhysicsComponent>(shuttleGridUid, out var shuttleBody) || shuttleBody.Mass < 1000f))
{
var metaQuery = GetEntityQuery<MetaDataComponent>();
// Can't go anywhere when in FTL.
var locked = shuttleFtl != null || Paused(shuttle!.Owner);
var locked = shuttleFtl != null || Paused(shuttleGridUid.Value);
// Can't cache it because it may have a whitelist for the particular console.
// Include paused as we still want to show CentCom.
foreach (var comp in EntityQuery<FTLDestinationComponent>(true))
{
// Can't warp to itself or if it's not on the whitelist.
if (comp.Owner == shuttle?.Owner ||
comp.Whitelist?.IsValid(entity.Value) == false) continue;
var destQuery = AllEntityQuery<FTLDestinationComponent>();
var meta = metaQuery.GetComponent(comp.Owner);
while (destQuery.MoveNext(out var destUid, out var comp))
{
// Can't warp to itself or if it's not on the whitelist (console or shuttle).
if (destUid == shuttleGridUid ||
comp.Whitelist?.IsValid(entity.Value) == false &&
(shuttleGridUid == null || comp.Whitelist?.IsValid(shuttleGridUid.Value, EntityManager) == false))
{
continue;
}
var meta = metaQuery.GetComponent(destUid);
var name = meta.EntityName;
if (string.IsNullOrEmpty(name))
@@ -283,19 +301,19 @@ namespace Content.Server.Shuttles.Systems
comp.Enabled &&
(!TryComp<FTLComponent>(comp.Owner, out var ftl) || ftl.State == FTLState.Cooldown);
// Can't travel to same map.
if (canTravel && consoleXform?.MapUid == Transform(comp.Owner).MapUid)
// Can't travel to same map (yet)
if (canTravel && consoleXform?.MapUid == Transform(destUid).MapUid)
{
canTravel = false;
}
destinations.Add((comp.Owner, name, canTravel));
destinations.Add((destUid, name, canTravel));
}
}
docks ??= GetAllDocks();
_ui.GetUiOrNull(component.Owner, ShuttleConsoleUiKey.Key)
_ui.GetUiOrNull(consoleUid, ShuttleConsoleUiKey.Key)
?.SetState(new ShuttleConsoleBoundInterfaceState(
ftlState,
ftlTime,
@@ -311,12 +329,14 @@ namespace Content.Server.Shuttles.Systems
base.Update(frameTime);
var toRemove = new RemQueue<PilotComponent>();
var query = EntityQueryEnumerator<PilotComponent>();
foreach (var comp in EntityManager.EntityQuery<PilotComponent>())
while (query.MoveNext(out var uid, out var comp))
{
if (comp.Console == null) continue;
if (comp.Console == null)
continue;
if (!_blocker.CanInteract(comp.Owner, comp.Console.Owner))
if (!_blocker.CanInteract(uid, comp.Console.Owner))
{
toRemove.Add(comp);
}
@@ -324,7 +344,7 @@ namespace Content.Server.Shuttles.Systems
foreach (var comp in toRemove)
{
RemovePilot(comp);
RemovePilot(comp.Owner, comp);
}
}
@@ -341,15 +361,18 @@ namespace Content.Server.Shuttles.Systems
}
if (args.NewPosition.TryDistance(EntityManager, component.Position.Value, out var distance) &&
distance < PilotComponent.BreakDistance) return;
distance < PilotComponent.BreakDistance)
{
return;
}
RemovePilot(component);
RemovePilot(uid, component);
}
protected override void HandlePilotShutdown(EntityUid uid, PilotComponent component, ComponentShutdown args)
{
base.HandlePilotShutdown(uid, component, args);
RemovePilot(component);
RemovePilot(uid, component);
}
private void OnConsoleShutdown(EntityUid uid, ShuttleConsoleComponent component, ComponentShutdown args)
@@ -380,42 +403,43 @@ namespace Content.Server.Shuttles.Systems
Dirty(pilotComponent);
}
public void RemovePilot(PilotComponent pilotComponent)
public void RemovePilot(EntityUid pilotUid, PilotComponent pilotComponent)
{
var console = pilotComponent.Console;
if (console is not ShuttleConsoleComponent helmsman) return;
if (console is not ShuttleConsoleComponent helmsman)
return;
pilotComponent.Console = null;
pilotComponent.Position = null;
if (TryComp<SharedEyeComponent>(pilotComponent.Owner, out var eye))
if (TryComp<SharedEyeComponent>(pilotUid, out var eye))
{
eye.Zoom = new(1.0f, 1.0f);
}
if (!helmsman.SubscribedPilots.Remove(pilotComponent)) return;
_alertsSystem.ClearAlert(pilotComponent.Owner, AlertType.PilotingShuttle);
_alertsSystem.ClearAlert(pilotUid, AlertType.PilotingShuttle);
pilotComponent.Owner.PopupMessage(Loc.GetString("shuttle-pilot-end"));
if (pilotComponent.LifeStage < ComponentLifeStage.Stopping)
EntityManager.RemoveComponent<PilotComponent>(pilotComponent.Owner);
EntityManager.RemoveComponent<PilotComponent>(pilotUid);
}
public void RemovePilot(EntityUid entity)
{
if (!EntityManager.TryGetComponent(entity, out PilotComponent? pilotComponent)) return;
RemovePilot(pilotComponent);
RemovePilot(entity, pilotComponent);
}
public void ClearPilots(ShuttleConsoleComponent component)
{
while (component.SubscribedPilots.TryGetValue(0, out var pilot))
{
RemovePilot(pilot);
RemovePilot(pilot.Owner, pilot);
}
}
}

View File

@@ -152,7 +152,7 @@ public sealed partial class ShuttleSystem
else
{
FTLTravel(shuttle,
CentCom.Value, _consoleAccumulator, TransitTime, dock: true);
CentCom.Value, _consoleAccumulator, TransitTime, true);
}
}
}

View File

@@ -12,6 +12,7 @@ using Content.Shared.CCVar;
using Content.Shared.Database;
using Content.Shared.Shuttles.Events;
using Content.Shared.Tiles;
using Content.Shared.Tag;
using Robust.Server.GameObjects;
using Robust.Server.Maps;
using Robust.Server.Player;
@@ -109,126 +110,6 @@ public sealed partial class ShuttleSystem
});
}
/// <summary>
/// Checks whether the emergency shuttle can warp to the specified position.
/// </summary>
private bool ValidSpawn(MapGridComponent grid, Box2 area)
{
return !grid.GetLocalTilesIntersecting(area).Any();
}
private DockingConfig? GetDockingConfig(ShuttleComponent component, EntityUid targetGrid)
{
var gridDocks = GetDocks(targetGrid);
if (gridDocks.Count <= 0) return null;
var xformQuery = GetEntityQuery<TransformComponent>();
var targetGridGrid = Comp<MapGridComponent>(targetGrid);
var targetGridXform = xformQuery.GetComponent(targetGrid);
var targetGridAngle = targetGridXform.WorldRotation.Reduced();
var shuttleDocks = GetDocks(component.Owner);
var shuttleAABB = Comp<MapGridComponent>(component.Owner).LocalAABB;
var validDockConfigs = new List<DockingConfig>();
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,
targetGridAngle,
shuttleAABB,
targetGridGrid,
out var dockedAABB,
out var matty,
out var targetAngle)) continue;
// Can't just use the AABB as we want to get bounds as tight as possible.
var spawnPosition = new EntityCoordinates(targetGrid, matty.Transform(Vector2.Zero));
spawnPosition = new EntityCoordinates(targetGridXform.MapUid!.Value, spawnPosition.ToMapPos(EntityManager));
var dockedBounds = new Box2Rotated(shuttleAABB.Translated(spawnPosition.Position), targetGridAngle, spawnPosition.Position);
// Check if there's no intersecting grids (AKA oh god it's docking at cargo).
if (_mapManager.FindGridsIntersecting(targetGridXform.MapID,
dockedBounds).Any(o => o.Owner != targetGrid))
{
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),
targetGridAngle,
shuttleAABB, targetGridGrid,
out var otherDockedAABB,
out _,
out var otherTargetAngle) ||
!otherDockedAABB.Equals(dockedAABB) ||
!targetAngle.Equals(otherTargetAngle)) continue;
dockedPorts.Add((other, otherGrid));
}
}
validDockConfigs.Add(new DockingConfig()
{
Docks = dockedPorts,
Area = dockedAABB.Value,
Coordinates = spawnPosition,
Angle = targetAngle,
});
}
}
}
if (validDockConfigs.Count <= 0) return null;
// 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;
// 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>
@@ -305,6 +186,7 @@ public sealed partial class ShuttleSystem
TransformComponent gridDockXform,
Angle targetGridRotation,
Box2 shuttleAABB,
EntityUid gridUid,
MapGridComponent grid,
[NotNullWhen(true)] out Box2? shuttleDockedAABB,
out Matrix3 matty,
@@ -337,7 +219,8 @@ public sealed partial class ShuttleSystem
// Rounding moment
shuttleDockedAABB = shuttleDockedAABB.Value.Enlarged(-0.01f);
if (!ValidSpawn(grid, shuttleDockedAABB.Value)) return false;
if (!ValidSpawn(gridUid, grid, shuttleDockedAABB.Value))
return false;
gridRotation = targetGridRotation + gridDockAngle - shuttleDockAngle;
return true;

View File

@@ -1,4 +1,3 @@
using Content.Server.Doors.Components;
using Content.Server.Doors.Systems;
using Content.Server.Shuttles.Components;
using Content.Server.Station.Systems;
@@ -12,10 +11,12 @@ using Robust.Shared.Map;
using Robust.Shared.Player;
using Robust.Shared.Utility;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Content.Server.Shuttles.Events;
using Content.Shared.Buckle.Components;
using Content.Shared.Doors.Components;
using Content.Shared.Shuttles.Components;
using JetBrains.Annotations;
using Robust.Shared.Map.Components;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Components;
@@ -46,7 +47,7 @@ public sealed partial class ShuttleSystem
/// <summary>
/// Minimum mass a grid needs to be to block a shuttle recall.
/// </summary>
private const float ShuttleFTLMassThreshold = 300f;
public const float ShuttleFTLMassThreshold = 300f;
// I'm too lazy to make CVars.
@@ -69,6 +70,11 @@ public sealed partial class ShuttleSystem
/// </summary>
private const int FTLProximityIterations = 3;
/// <summary>
/// Minimum mass for an FTL destination
/// </summary>
public const float FTLDestinationMass = 500f;
private void InitializeFTL()
{
SubscribeLocalEvent<StationGridAddedEvent>(OnStationGridAdd);
@@ -76,7 +82,9 @@ public sealed partial class ShuttleSystem
private void OnStationGridAdd(StationGridAddedEvent ev)
{
if (TryComp<PhysicsComponent>(ev.GridId, out var body) && body.Mass > 500f)
if (HasComp<MapComponent>(ev.GridId) ||
TryComp<PhysicsComponent>(ev.GridId, out var body) &&
body.Mass > FTLDestinationMass)
{
AddFTLDestination(ev.GridId, true);
}
@@ -133,9 +141,11 @@ public sealed partial class ShuttleSystem
return destination;
}
[PublicAPI]
public void RemoveFTLDestination(EntityUid uid)
{
if (!RemComp<FTLDestinationComponent>(uid)) return;
if (!RemComp<FTLDestinationComponent>(uid))
return;
_console.RefreshShuttleConsoles();
}
@@ -145,7 +155,8 @@ public sealed partial class ShuttleSystem
public void FTLTravel(ShuttleComponent component,
EntityCoordinates coordinates,
float startupTime = DefaultStartupTime,
float hyperspaceTime = DefaultTravelTime)
float hyperspaceTime = DefaultTravelTime,
string? priorityTag = null)
{
if (!TrySetupFTL(component, out var hyperspace))
return;
@@ -155,6 +166,7 @@ public sealed partial class ShuttleSystem
hyperspace.Accumulator = hyperspace.StartupTime;
hyperspace.TargetCoordinates = coordinates;
hyperspace.Dock = false;
hyperspace.PriorityTag = priorityTag;
_console.RefreshShuttleConsoles();
}
@@ -165,7 +177,8 @@ public sealed partial class ShuttleSystem
EntityUid target,
float startupTime = DefaultStartupTime,
float hyperspaceTime = DefaultTravelTime,
bool dock = false)
bool dock = false,
string? priorityTag = null)
{
if (!TrySetupFTL(component, out var hyperspace))
return;
@@ -175,6 +188,7 @@ public sealed partial class ShuttleSystem
hyperspace.Accumulator = hyperspace.StartupTime;
hyperspace.TargetUid = target;
hyperspace.Dock = dock;
hyperspace.PriorityTag = priorityTag;
_console.RefreshShuttleConsoles();
}
@@ -206,18 +220,23 @@ public sealed partial class ShuttleSystem
SoundSystem.Play(_startupSound.GetSound(), Filter.Empty().AddInRange(Transform(uid).MapPosition, GetSoundRange(component.Owner)), _startupSound.Params);
// Make sure the map is setup before we leave to avoid pop-in (e.g. parallax).
SetupHyperspace();
var ev = new FTLStartedEvent();
RaiseLocalEvent(uid, ref ev);
return true;
}
private void UpdateHyperspace(float frameTime)
{
foreach (var comp in EntityQuery<FTLComponent>())
var query = EntityQueryEnumerator<FTLComponent>();
while (query.MoveNext(out var uid, out var comp))
{
comp.Accumulator -= frameTime;
if (comp.Accumulator > 0f) continue;
if (comp.Accumulator > 0f)
continue;
var uid = comp.Owner;
var xform = Transform(uid);
PhysicsComponent? body;
ShuttleComponent? shuttle;
@@ -280,13 +299,21 @@ public sealed partial class ShuttleSystem
SetDockBolts(uid, false);
SetDocks(uid, true);
if (TryComp(uid, out body))
{
_physics.SetLinearVelocity(uid, Vector2.Zero, body: body);
_physics.SetAngularVelocity(uid, 0f, body: body);
_physics.SetLinearDamping(body, ShuttleLinearDamping);
_physics.SetAngularDamping(body, ShuttleAngularDamping);
}
TryComp(uid, out shuttle);
MapId mapId;
if (comp.TargetUid != null && shuttle != null)
{
if (comp.Dock)
TryFTLDock(shuttle, comp.TargetUid.Value);
TryFTLDock(shuttle, comp.TargetUid.Value, comp.PriorityTag);
else
TryFTLProximity(shuttle, comp.TargetUid.Value);
@@ -439,7 +466,8 @@ public sealed partial class ShuttleSystem
/// <summary>
/// Tries to dock with the target grid, otherwise falls back to proximity.
/// </summary>
public bool TryFTLDock(ShuttleComponent component, EntityUid targetUid)
/// <param name="priorityTag">Priority docking tag to prefer, e.g. for emergency shuttle</param>
public bool TryFTLDock(ShuttleComponent component, EntityUid targetUid, string? priorityTag = null)
{
if (!TryComp<TransformComponent>(component.Owner, out var xform) ||
!TryComp<TransformComponent>(targetUid, out var targetXform) ||
@@ -449,7 +477,7 @@ public sealed partial class ShuttleSystem
return false;
}
var config = GetDockingConfig(component, targetUid);
var config = GetDockingConfig(component, targetUid, priorityTag);
if (config != null)
{
@@ -460,7 +488,7 @@ public sealed partial class ShuttleSystem
// Connect everything
foreach (var (dockA, dockB) in config.Docks)
{
_dockSystem.Dock(dockA, dockB);
_dockSystem.Dock(dockA.Owner, dockA, dockB.Owner, dockB);
}
return true;
@@ -476,7 +504,6 @@ public sealed partial class ShuttleSystem
public bool TryFTLProximity(ShuttleComponent component, EntityUid targetUid, TransformComponent? xform = null, TransformComponent? targetXform = null)
{
if (!Resolve(targetUid, ref targetXform) ||
targetXform.GridUid == null ||
targetXform.MapUid == null ||
!targetXform.MapUid.Value.IsValid() ||
!Resolve(component.Owner, ref xform))
@@ -502,9 +529,9 @@ public sealed partial class ShuttleSystem
var targetAABB = _transform.GetWorldMatrix(targetXform, xformQuery)
.TransformBox(targetLocalAABB).Enlarged(shuttleAABB.Size.Length);
var nearbyGrids = new HashSet<EntityUid>(1) { targetXform.GridUid.Value };
var nearbyGrids = new HashSet<EntityUid>();
var iteration = 0;
var lastCount = 1;
var lastCount = nearbyGrids.Count;
var mapId = targetXform.MapID;
while (iteration < FTLProximityIterations)
@@ -552,7 +579,7 @@ public sealed partial class ShuttleSystem
}
// TODO: This is pretty crude for multiple landings.
if (nearbyGrids.Count > 1 || !HasComp<MapComponent>(targetXform.GridUid.Value))
if (nearbyGrids.Count > 1 || !HasComp<MapComponent>(targetXform.GridUid))
{
var minRadius = (MathF.Max(targetAABB.Width, targetAABB.Height) + MathF.Max(shuttleAABB.Width, shuttleAABB.Height)) / 2f;
spawnPos = targetAABB.Center + _random.NextVector2(minRadius, minRadius + 64f);
@@ -570,7 +597,7 @@ public sealed partial class ShuttleSystem
xform.Coordinates = new EntityCoordinates(targetXform.MapUid.Value, spawnPos);
if (!HasComp<MapComponent>(targetXform.GridUid.Value))
if (!HasComp<MapComponent>(targetXform.GridUid))
{
_transform.SetLocalRotation(xform, _random.NextAngle());
}
@@ -581,4 +608,138 @@ public sealed partial class ShuttleSystem
return true;
}
/// <summary>
/// Checks whether the emergency shuttle can warp to the specified position.
/// </summary>
private bool ValidSpawn(EntityUid gridUid, MapGridComponent grid, Box2 area)
{
// If the target is a map then any tile is valid.
// TODO: We already need the entities-under check
if (HasComp<MapComponent>(gridUid))
return true;
return !grid.GetLocalTilesIntersecting(area).Any();
}
/// <summary>
/// Tries to get a valid docking configuration for the shuttle to the target grid.
/// </summary>
/// <param name="priorityTag">Priority docking tag to prefer, e.g. for emergency shuttle</param>
private DockingConfig? GetDockingConfig(ShuttleComponent component, EntityUid targetGrid, string? priorityTag = null)
{
var gridDocks = GetDocks(targetGrid);
if (gridDocks.Count <= 0)
return null;
var xformQuery = GetEntityQuery<TransformComponent>();
var targetGridGrid = Comp<MapGridComponent>(targetGrid);
var targetGridXform = xformQuery.GetComponent(targetGrid);
var targetGridAngle = targetGridXform.WorldRotation.Reduced();
var shuttleDocks = GetDocks(component.Owner);
var shuttleAABB = Comp<MapGridComponent>(component.Owner).LocalAABB;
var validDockConfigs = new List<DockingConfig>();
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,
targetGridAngle,
shuttleAABB,
targetGrid,
targetGridGrid,
out var dockedAABB,
out var matty,
out var targetAngle)) continue;
// Can't just use the AABB as we want to get bounds as tight as possible.
var spawnPosition = new EntityCoordinates(targetGrid, matty.Transform(Vector2.Zero));
spawnPosition = new EntityCoordinates(targetGridXform.MapUid!.Value, spawnPosition.ToMapPos(EntityManager));
var dockedBounds = new Box2Rotated(shuttleAABB.Translated(spawnPosition.Position), targetGridAngle, spawnPosition.Position);
// Check if there's no intersecting grids (AKA oh god it's docking at cargo).
if (_mapManager.FindGridsIntersecting(targetGridXform.MapID,
dockedBounds).Any(o => o.Owner != targetGrid))
{
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),
};
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),
targetGridAngle,
shuttleAABB,
targetGrid,
targetGridGrid,
out var otherDockedAABB,
out _,
out var otherTargetAngle) ||
!otherDockedAABB.Equals(dockedAABB) ||
!targetAngle.Equals(otherTargetAngle)) continue;
dockedPorts.Add((other, otherGrid));
}
}
validDockConfigs.Add(new DockingConfig()
{
Docks = dockedPorts,
Area = dockedAABB.Value,
Coordinates = spawnPosition,
Angle = targetAngle,
});
}
}
}
if (validDockConfigs.Count <= 0)
return null;
// Prioritise by priority docks, then by maximum connected ports, then by most similar angle.
validDockConfigs = validDockConfigs
.OrderByDescending(x => x.Docks.Any(docks =>
TryComp<PriorityDockComponent>(docks.DockB.Owner, out var priority) &&
priority.Tag?.Equals(priorityTag) == true))
.ThenByDescending(x => x.Docks.Count)
.ThenBy(x => Math.Abs(Angle.ShortestDistance(x.Angle.Reduced(), targetGridAngle).Theta)).ToList();
var location = validDockConfigs.First();
location.TargetGrid = targetGrid;
// TODO: Ideally do a hyperspace warpin, just have it run on like a 10 second timer.
return location;
}
}

View File

@@ -8,18 +8,6 @@ public sealed class CargoShuttleConsoleBoundUserInterfaceState : BoundUserInterf
public string AccountName;
public string ShuttleName;
// Unfortunately shuttles have essentially 3 states so can't just use a nullable var for it:
// 1. stowed
// 2. called but not recallable
// 3. called and recallable
// The reason we have 2 is so people don't spam the recall button in the UI.
public bool CanRecall;
/// <summary>
/// When the shuttle is expected to be usable.
/// </summary>
public TimeSpan? ShuttleETA;
/// <summary>
/// List of orders expected on the delivery.
/// </summary>
@@ -28,14 +16,10 @@ public sealed class CargoShuttleConsoleBoundUserInterfaceState : BoundUserInterf
public CargoShuttleConsoleBoundUserInterfaceState(
string accountName,
string shuttleName,
bool canRecall,
TimeSpan? shuttleETA,
List<CargoOrderData> orders)
{
AccountName = accountName;
ShuttleName = shuttleName;
CanRecall = canRecall;
ShuttleETA = shuttleETA;
Orders = orders;
}
}

View File

@@ -10,21 +10,6 @@ namespace Content.Shared.Cargo.Components;
[RegisterComponent, Access(typeof(SharedCargoSystem))]
public sealed class CargoShuttleComponent : Component
{
[ViewVariables(VVAccess.ReadWrite), DataField("nextCall")]
public TimeSpan? NextCall;
[ViewVariables(VVAccess.ReadWrite), DataField("cooldown")]
public float Cooldown = 30f;
[ViewVariables]
public bool CanRecall;
/// <summary>
/// The shuttle's assigned coordinates on the cargo map.
/// </summary>
[ViewVariables]
public EntityCoordinates Coordinates;
/// <summary>
/// The assigned station for this cargo shuttle.
/// </summary>

View File

@@ -1,12 +0,0 @@
using Robust.Shared.Serialization;
namespace Content.Shared.Cargo.Events;
/// <summary>
/// Raised on a cargo console requesting the cargo shuttle.
/// </summary>
[Serializable, NetSerializable]
public sealed class CargoCallShuttleMessage : BoundUserInterfaceMessage
{
}

View File

@@ -1,12 +0,0 @@
using Robust.Shared.Serialization;
namespace Content.Shared.Cargo.Events;
/// <summary>
/// Raised on a client request cargo shuttle recall
/// </summary>
[Serializable, NetSerializable]
public sealed class CargoRecallShuttleMessage : BoundUserInterfaceMessage
{
}

View File

@@ -6,6 +6,9 @@ namespace Content.Shared.Shuttles.Systems;
public abstract class SharedRadarConsoleSystem : EntitySystem
{
public const float DefaultMinRange = 64f;
public const float DefaultMaxRange = 256f;
public override void Initialize()
{
base.Initialize();

View File

@@ -727,7 +727,8 @@
id: AirlockExternalGlassShuttleEmergencyLocked
suffix: External, Emergency, Glass, Docking, Locked
components:
- type: EmergencyDock
- type: PriorityDock
tag: DockEmergency
- type: AccessReader
access: [["External"]]

View File

@@ -182,6 +182,12 @@
- type: Tag
id: DiscreteHealthAnalyzer #So construction recipes don't eat medical PDAs
- type: Tag
id: DockCargo
- type: Tag
id: DockEmergency
- type: Tag
id: Document