diff --git a/Content.Server/Station/Systems/StationJobsSystem.cs b/Content.Server/Station/Systems/StationJobsSystem.cs
index 0790ffdf8fb..f7b866beb63 100644
--- a/Content.Server/Station/Systems/StationJobsSystem.cs
+++ b/Content.Server/Station/Systems/StationJobsSystem.cs
@@ -14,6 +14,7 @@
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
+using Content.Server._DVA.Station.Events; // DeltaV - AutomaticSpareIdSystem
namespace Content.Server.Station.Systems;
@@ -111,6 +112,12 @@ public bool TryAssignJob(EntityUid station, string jobPrototypeId, NetUserId net
stationJobs.PlayerJobs.TryAdd(netUserId, new());
stationJobs.PlayerJobs[netUserId].Add(jobPrototypeId);
+
+ // DeltaV START - AutomaticSpareIdSystem: Raise an event when a player takes a job
+ var jobAddedEvent = new PlayerJobAddedEvent(netUserId, jobPrototypeId);
+ RaiseLocalEvent(station, ref jobAddedEvent, false);
+ // DeltaV END
+
return true;
}
@@ -207,7 +214,17 @@ public bool TryRemovePlayerJobs(EntityUid station,
if (!Resolve(station, ref jobsComponent, false))
return false;
- return jobsComponent.PlayerJobs.Remove(userId);
+ // DeltaV START - AutomaticSpareIdSystem: Raise an event when a player loses jobs
+ if (jobsComponent.PlayerJobs.Remove(userId, out var jobs))
+ {
+ var jobsRemovedEvent = new PlayerJobsRemovedEvent(userId, jobs);
+ RaiseLocalEvent(station, ref jobsRemovedEvent, false);
+ return true;
+ }
+ return false;
+
+ // return jobsComponent.PlayerJobs.Remove(userId);
+ // DeltaV END
}
///
diff --git a/Content.Server/_DVA/Cabinet/DVSpareIDSafeComponent.cs b/Content.Server/_DVA/Cabinet/DVSpareIDSafeComponent.cs
new file mode 100644
index 00000000000..b6e6dc04405
--- /dev/null
+++ b/Content.Server/_DVA/Cabinet/DVSpareIDSafeComponent.cs
@@ -0,0 +1,7 @@
+namespace Content.Server._DVA.Cabinet;
+
+///
+/// Component that marks an entity as a spare ID safe. Is interacted with by DVAutomaticSpareIdSystem to unlock the safe when there is no captain present.
+///
+[RegisterComponent]
+public sealed partial class DVSpareIDSafeComponent : Component;
\ No newline at end of file
diff --git a/Content.Server/_DVA/Station/Components/DVAutomaticSpareIdComponent.cs b/Content.Server/_DVA/Station/Components/DVAutomaticSpareIdComponent.cs
new file mode 100644
index 00000000000..849babfd07c
--- /dev/null
+++ b/Content.Server/_DVA/Station/Components/DVAutomaticSpareIdComponent.cs
@@ -0,0 +1,95 @@
+using Content.Server._DVA.Station.Systems;
+using Content.Shared.Access;
+using Content.Shared.Roles;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
+
+namespace Content.Server._DVA.Station.Components;
+
+public enum AutomaticSpareIdState
+{
+ RoundStart,
+ Alerted,
+ AwaitingUnlock,
+ Unlocked,
+ CaptainPresent,
+ WarOps
+}
+
+[RegisterComponent, Access(typeof(DVAutomaticSpareIdSystem)), AutoGenerateComponentPause]
+public sealed partial class DVAutomaticSpareIdComponent : Component
+{
+ ///
+ /// The current state of the automatic spare ID system
+ ///
+ [DataField]
+ public AutomaticSpareIdState State = AutomaticSpareIdState.RoundStart;
+
+ ///
+ /// Timeout before an action is taken if the state doesn't change
+ ///
+ [DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), AutoPausedField]
+ public TimeSpan? Timeout;
+
+ ///
+ /// The job considered as Captain for the automatic spare ID system
+ ///
+ [DataField]
+ public ProtoId CaptainJob = "Captain";
+
+ ///
+ /// The access that the spare ID safe will be extended to have if it is automatically unlocked
+ /// if there is no captain
+ ///
+ [DataField]
+ public ProtoId GrantAccessToCommand = "Command";
+
+ ///
+ /// The access that the spare ID safe will be extended to have if it is automatically unlocked
+ /// if there is a captain
+ ///
+ [DataField]
+ public ProtoId GrantAccessToCaptain = "Captain";
+
+ ///
+ /// Message for when a Captain joins after the system has alerted about their absence
+ ///
+ [DataField]
+ public LocId CaptainPresentAfterAlertsMessage = "captain-arrived-revoke-aco-announcement";
+
+ ///
+ /// Message for when the system alerts but isn't going to automatically unlock
+ ///
+ [DataField]
+ public LocId AlertedMessage = "no-captain-request-aco-vote-announcement";
+
+ ///
+ /// Message for when the system alerts and will automatically unlock
+ ///
+ [DataField]
+ public LocId AwaitingUnlockMessage = "no-captain-request-aco-vote-with-aa-announcement";
+
+ ///
+ /// Message for when the system alerts automatically unlock
+ ///
+ [DataField]
+ public LocId UnlockedMessage = "no-captain-aa-unlocked-announcement";
+
+ ///
+ /// The amount of time in which that the spare ID will unlock after nuclear operatives declare war.
+ ///
+ [DataField]
+ public TimeSpan WarOpsUnlockDelay = TimeSpan.FromSeconds(15);
+
+ ///
+ /// Message that will be displayed to the station when there is no captain and war ops is declared.
+ ///
+ [DataField]
+ public LocId WarOpsUnlockedMessageACO = "spare-id-warops-no-captain";
+
+ ///
+ /// Message that will be displayed to the station when there is a captain and war ops is declared.
+ ///
+ [DataField]
+ public LocId WarOpsUnlockedMessageCaptain = "spare-id-warops-captain";
+}
diff --git a/Content.Server/_DVA/Station/Events/PlayerJobAddedEvent.cs b/Content.Server/_DVA/Station/Events/PlayerJobAddedEvent.cs
new file mode 100644
index 00000000000..56b3786fdd3
--- /dev/null
+++ b/Content.Server/_DVA/Station/Events/PlayerJobAddedEvent.cs
@@ -0,0 +1,9 @@
+using Robust.Shared.Network;
+
+namespace Content.Server._DVA.Station.Events;
+
+///
+/// Event is raised when a player takes a job.
+///
+[ByRefEvent]
+public record struct PlayerJobAddedEvent(NetUserId Player, string JobPrototypeId);
\ No newline at end of file
diff --git a/Content.Server/_DVA/Station/Events/PlayerJobsRemovedEvent.cs b/Content.Server/_DVA/Station/Events/PlayerJobsRemovedEvent.cs
new file mode 100644
index 00000000000..c858d5b083d
--- /dev/null
+++ b/Content.Server/_DVA/Station/Events/PlayerJobsRemovedEvent.cs
@@ -0,0 +1,11 @@
+using Robust.Shared.Network;
+using Robust.Shared.Prototypes;
+using Content.Shared.Roles;
+
+namespace Content.Server._DVA.Station.Events;
+
+///
+/// Event is raised when a player loses jobs.
+///
+[ByRefEvent]
+public record struct PlayerJobsRemovedEvent(NetUserId Player, List> PlayerJobs);
\ No newline at end of file
diff --git a/Content.Server/_DVA/Station/Systems/DVAutomaticSpareIdSystem.Commands.cs b/Content.Server/_DVA/Station/Systems/DVAutomaticSpareIdSystem.Commands.cs
new file mode 100644
index 00000000000..6356d0dc10d
--- /dev/null
+++ b/Content.Server/_DVA/Station/Systems/DVAutomaticSpareIdSystem.Commands.cs
@@ -0,0 +1,41 @@
+using System.Diagnostics;
+using Content.Server._DVA.Station.Components;
+using Content.Server._DVA.Station.Systems;
+using Content.Server.Administration;
+using Content.Shared.Administration;
+using Robust.Shared.Toolshed;
+using Robust.Shared.Toolshed.Errors;
+using Robust.Shared.Utility;
+
+namespace Robust.Shared._DVA.Station.Systems;
+
+[ToolshedCommand(Name = "spareid"), AdminCommand(AdminFlags.Spawn)]
+public sealed partial class DVAutomaticSpareIdSystemCommand : ToolshedCommand
+{
+ [Dependency] private IEntityManager _entityManager = default!;
+ private DVAutomaticSpareIdSystem? _automaticSpareIdSystem;
+
+ [CommandImplementation("unlock")]
+ public void Unlock(IInvocationContext ctx, [PipedArgument] EntityUid stationUid, bool doAnnouncement)
+ {
+ _automaticSpareIdSystem ??= GetSys();
+ if (!_entityManager.TryGetComponent(stationUid, out var spareId))
+ {
+ ctx.ReportError(new AutomaticSpareIdSystemMissing());
+ return;
+ }
+ _automaticSpareIdSystem.ForceUnlock((stationUid, spareId), null, doAnnouncement ? "command-spareid-unlock-announcement" : null);
+ }
+}
+
+public record struct AutomaticSpareIdSystemMissing : IConError
+{
+ public FormattedMessage DescribeInner()
+ {
+ return FormattedMessage.FromMarkupOrThrow("This command doesn't function if there is no automatic spare ID system. Common usage: stations:get | spareid:unlock true");
+ }
+
+ public string? Expression { get; set; }
+ public Vector2i? IssueSpan { get; set; }
+ public StackTrace? Trace { get; set; }
+}
diff --git a/Content.Server/_DVA/Station/Systems/DVAutomaticSpareIdSystem.cs b/Content.Server/_DVA/Station/Systems/DVAutomaticSpareIdSystem.cs
new file mode 100644
index 00000000000..e2eebb22b28
--- /dev/null
+++ b/Content.Server/_DVA/Station/Systems/DVAutomaticSpareIdSystem.cs
@@ -0,0 +1,239 @@
+using System.Linq;
+using Content.Server._DVA.Cabinet;
+using Content.Server._DVA.Station.Components;
+using Content.Server._DVA.Station.Events;
+using Content.Server.Chat.Systems;
+using Content.Server.NukeOps;
+using Content.Server.Station.Components;
+using Content.Shared._DVA.CCVars;
+using Content.Shared.Access.Components;
+using Content.Shared.Access;
+using Content.Shared.NukeOps;
+using Robust.Shared.Configuration;
+using Robust.Shared.Timing;
+using Robust.Shared.Utility;
+using Robust.Shared.Prototypes;
+
+namespace Content.Server._DVA.Station.Systems;
+
+public sealed partial class DVAutomaticSpareIdSystem : EntitySystem
+{
+ [Dependency] private ChatSystem _chat = default!;
+ [Dependency] private IConfigurationManager _cfg = default!;
+ [Dependency] private IGameTiming _timing = default!;
+
+ private bool _autoUnlock;
+ private TimeSpan _alertDelay;
+ private TimeSpan _unlockDelay;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnMapInit);
+ SubscribeLocalEvent(OnPlayerJobAdded);
+ SubscribeLocalEvent(OnPlayerJobsRemoved);
+ SubscribeLocalEvent(OnWarDeclared);
+
+ Subs.CVar(_cfg, DCCVars.SpareIdAutoUnlock, a => _autoUnlock = a, true);
+ Subs.CVar(_cfg, DCCVars.SpareIdAlertDelay, a => _alertDelay = a, true);
+ Subs.CVar(_cfg, DCCVars.SpareIdUnlockDelay, a => _unlockDelay = a, true);
+ }
+
+ public override void Update(float frameTime)
+ {
+ base.Update(frameTime);
+
+ var query = EntityQueryEnumerator();
+ while (query.MoveNext(out var station, out var spareId))
+ {
+ if (spareId.Timeout is { } timeout && _timing.CurTime > timeout)
+ {
+ Timeout((station, spareId));
+ }
+ }
+ }
+
+ private void Timeout(Entity ent)
+ {
+ if (ent.Comp.State is AutomaticSpareIdState.RoundStart)
+ {
+ if (HasCaptain(ent))
+ RoundStartCaptain(ent);
+ else
+ RoundStartNoCaptain(ent);
+ }
+ else if (ent.Comp.State is AutomaticSpareIdState.AwaitingUnlock)
+ {
+ MoveToUnlocked(ent);
+ }
+ else if (ent.Comp.State is AutomaticSpareIdState.WarOps)
+ {
+ // Default to these, then check if there is a captain
+ var message = ent.Comp.WarOpsUnlockedMessageACO;
+ var accessGranted = ent.Comp.GrantAccessToCommand;
+ if (HasCaptain(ent))
+ {
+ message = ent.Comp.WarOpsUnlockedMessageCaptain;
+ accessGranted = ent.Comp.GrantAccessToCaptain;
+ }
+ ent.Comp.State = AutomaticSpareIdState.AwaitingUnlock;
+ MoveToUnlocked(ent, accessGranted, message);
+ }
+ else
+ {
+ DebugTools.Assert($"Spare ID state timed out with unexpected state {ent.Comp.State}");
+ }
+ }
+
+ private void RoundStartNoCaptain(Entity ent)
+ {
+ if (_autoUnlock)
+ MoveToAwaitingUnlock(ent);
+ else
+ MoveToAlerted(ent);
+ }
+
+ private static void RoundStartCaptain(Entity ent)
+ {
+ ent.Comp.State = AutomaticSpareIdState.CaptainPresent;
+ ent.Comp.Timeout = null;
+ }
+
+ private void OnMapInit(Entity ent, ref MapInitEvent args)
+ {
+ ent.Comp.Timeout = _timing.CurTime + _alertDelay;
+ }
+
+ private void OnPlayerJobAdded(Entity ent, ref PlayerJobAddedEvent args)
+ {
+ if (args.JobPrototypeId == ent.Comp.CaptainJob)
+ MoveToCaptainPresent(ent);
+ }
+
+ private void OnPlayerJobsRemoved(Entity ent, ref PlayerJobsRemovedEvent args)
+ {
+ if (!args.PlayerJobs.Contains(ent.Comp.CaptainJob) || HasCaptain(ent))
+ return;
+
+ MoveToAlerted(ent);
+ }
+
+ private void OnWarDeclared(ref WarDeclaredEvent args)
+ {
+ if (args.Status == WarConditionStatus.YesWar)
+ {
+ foreach (var spareId in EntityQuery())
+ {
+ spareId.Timeout = _timing.CurTime + spareId.WarOpsUnlockDelay;
+ spareId.State = AutomaticSpareIdState.WarOps;
+ }
+ }
+ }
+
+ private bool HasCaptain(Entity ent)
+ {
+ if (!TryComp(ent, out var stationJobs))
+ return false;
+
+ return stationJobs.PlayerJobs.Any(playerJobs => playerJobs.Value.Contains(ent.Comp.CaptainJob));
+ }
+
+ private void MoveToAwaitingUnlock(Entity ent)
+ {
+ DebugTools.Assert(ent.Comp.State is AutomaticSpareIdState.RoundStart, $"Spare ID state has unexpected state {ent.Comp.State} on awaiting to unlock");
+
+ ent.Comp.State = AutomaticSpareIdState.AwaitingUnlock;
+ ent.Comp.Timeout = _timing.CurTime + _unlockDelay;
+
+ _chat.DispatchStationAnnouncement(ent, Loc.GetString(ent.Comp.AwaitingUnlockMessage, ("minutes", _unlockDelay.TotalMinutes)), colorOverride: Color.Gold);
+ }
+
+ ///
+ /// Unlocks all spare ID cabinets, giving access to a certain access prototype and displays a message to the station.
+ ///
+ /// The station entity that has the .
+ /// The access to give to the spare ID cabinet. If not specified or null, will default to ent.Comp.GrantAccessToCommand
+ /// The message to display to the station upon unlocking the spare ID. If not specified or null, will default to ent.Comp.UnlockedMessage
+ private void MoveToUnlocked(Entity ent, ProtoId? newSpareIdAccess = null, LocId? unlockMessageLocId = null)
+ {
+ DebugTools.Assert(ent.Comp.State is AutomaticSpareIdState.AwaitingUnlock, $"Spare ID state has unexpected state {ent.Comp.State} on unlocking");
+
+ ent.Comp.State = AutomaticSpareIdState.Unlocked;
+ ent.Comp.Timeout = null;
+
+ UnlockCabinet(ent, newSpareIdAccess);
+
+ if (!unlockMessageLocId.HasValue)
+ unlockMessageLocId = ent.Comp.UnlockedMessage; // Default message if nothing is specified
+
+ _chat.DispatchStationAnnouncement(ent, Loc.GetString(unlockMessageLocId), colorOverride: Color.Red);
+ }
+
+ private void MoveToCaptainPresent(Entity ent)
+ {
+ if (!(ent.Comp.State is AutomaticSpareIdState.Alerted or AutomaticSpareIdState.AwaitingUnlock or AutomaticSpareIdState.Unlocked))
+ {
+ return;
+ }
+
+ ent.Comp.State = AutomaticSpareIdState.CaptainPresent;
+ ent.Comp.Timeout = null;
+
+ _chat.DispatchStationAnnouncement(ent, Loc.GetString(ent.Comp.CaptainPresentAfterAlertsMessage), colorOverride: Color.Gold);
+ }
+
+ private void MoveToAlerted(Entity ent)
+ {
+ DebugTools.Assert(ent.Comp.State is AutomaticSpareIdState.RoundStart or AutomaticSpareIdState.CaptainPresent, $"Spare ID state has unexpected state {ent.Comp.State} on moving to alerted");
+
+ ent.Comp.State = AutomaticSpareIdState.Alerted;
+ ent.Comp.Timeout = null;
+
+ _chat.DispatchStationAnnouncement(ent, Loc.GetString(ent.Comp.AlertedMessage), colorOverride: Color.Gold);
+ }
+
+ ///
+ /// Unlocks all spare ID cabinets, giving access to a certain access prototype.
+ ///
+ /// The station entity that has the .
+ /// The access to give to the spare ID cabinet. If not specified or null, will default to ent.Comp.GrantAccessToCommand
+ private void UnlockCabinet(Entity ent, ProtoId? newSpareIdAccess = null)
+ {
+ var query = EntityQueryEnumerator();
+ while (query.MoveNext(out var uid, out _, out var accessReader))
+ {
+ var accesses = accessReader.AccessLists;
+ if (accesses.Count <= 0)
+ continue;
+
+ if (!newSpareIdAccess.HasValue)
+ newSpareIdAccess = ent.Comp.GrantAccessToCommand; // Default to command if no access is specified
+
+ accesses.Add([newSpareIdAccess.Value]);
+ Dirty(uid, accessReader);
+ RaiseLocalEvent(uid, new AccessReaderConfigurationChangedEvent());
+ }
+ }
+
+ ///
+ /// Force unlocks the spare ID cabinets and optionally displays an announcement.
+ ///
+ /// The station entity that has the .
+ /// The access to give to the spare ID cabinet. If not specified or null, will default to ent.Comp.GrantAccessToCommand
+ /// The message to display to the station upon unlocking the spare ID. If not specified or null, no announcement will be made
+ public void ForceUnlock(Entity ent, ProtoId? newSpareIdAccess = null, LocId? unlockMessageLocId = null)
+ {
+ UnlockCabinet(ent, newSpareIdAccess);
+
+ // Make sure the state is set to the expected state given the current game state. Prevents errouneous announcements.
+ if (HasCaptain(ent))
+ ent.Comp.State = AutomaticSpareIdState.CaptainPresent;
+ else
+ ent.Comp.State = AutomaticSpareIdState.Unlocked;
+ ent.Comp.Timeout = null;
+
+ if (unlockMessageLocId.HasValue)
+ _chat.DispatchStationAnnouncement(ent, Loc.GetString(unlockMessageLocId), colorOverride: Color.Red);
+ }
+}
diff --git a/Content.Shared/_DVA/CCVars/DCCVars.cs b/Content.Shared/_DVA/CCVars/DCCVars.cs
index ffdef39e659..d5d0774e7a3 100644
--- a/Content.Shared/_DVA/CCVars/DCCVars.cs
+++ b/Content.Shared/_DVA/CCVars/DCCVars.cs
@@ -9,6 +9,32 @@ namespace Content.Shared._DVA.CCVars;
// ReSharper disable once InconsistentNaming - Shush you
public sealed partial class DCCVars
{
+ /*
+ * Auto ACO
+ */
+
+ ///
+ /// How long after the announcement before the spare ID is unlocked
+ ///
+ public static readonly CVarDef SpareIdUnlockDelay =
+ CVarDef.Create("game.spare_id.unlock_delay", TimeSpan.FromMinutes(5), CVar.SERVERONLY | CVar.ARCHIVE);
+
+ ///
+ /// How long to wait before checking for a captain after roundstart
+ ///
+ public static readonly CVarDef SpareIdAlertDelay =
+ CVarDef.Create("game.spare_id.alert_delay", TimeSpan.FromMinutes(15), CVar.SERVERONLY | CVar.ARCHIVE);
+
+ ///
+ /// Determines if the automatic spare ID process should automatically unlock the cabinet
+ ///
+ public static readonly CVarDef SpareIdAutoUnlock =
+ CVarDef.Create("game.spare_id.auto_unlock", true, CVar.SERVERONLY | CVar.ARCHIVE);
+
+ /*
+ * Misc.
+ */
+
///
/// The total time a player has to be SSD to be considered cryoable (stage 3).
/// Default is 20 minutes. Value should be bigger than .
diff --git a/Resources/Locale/en-US/_DVA/job/captain-state.ftl b/Resources/Locale/en-US/_DVA/job/captain-state.ftl
new file mode 100644
index 00000000000..1e1091f370d
--- /dev/null
+++ b/Resources/Locale/en-US/_DVA/job/captain-state.ftl
@@ -0,0 +1,12 @@
+# Announcements related to captain presence and ACO state
+
+captain-arrived-revoke-aco-announcement = The Acting Commanding Officer's position is revoked due to the arrival of a NanoTrasen-appointed captain. All personnel are to return to the standard Chain of Command.
+no-captain-request-aco-vote-with-aa-announcement = Station records indicate that no captain is currently present. Command personnel are requested to nominate an Acting Commanding Officer and report the results to Central Command in accordance with Standard Operating Procedure. The spare captain ID cabinet will be unlocked in {$minutes} minutes to ensure continued operational efficiency.
+no-captain-request-aco-vote-announcement = Station records indicate that no captain is currently present. Command personnel are requested to nominate an Acting Commanding Officer and report the results to Central Command in accordance with Standard Operating Procedure.
+no-captain-aa-unlocked-announcement = Command access authority has been granted to the Spare ID cabinet for use by the Acting Commanding Officer. Unauthorized possession of the spare captain ID is punishable under Grand Felony Offense [307]: Grand Larceny.
+
+spare-id-warops-no-captain = Due to the current circumstances, command has been granted to the Spare ID cabinet for use by the Acting Commanding Officer. Ensure the spare ID remains secure. Unauthorized possession of the spare captain ID is punishable under Grand Felony Offense [307]: Grand Larceny.
+spare-id-warops-captain = Due to the current circumstances, access has been granted to the Spare ID cabinet for use by the Captain. Ensure the spare ID remains secure. Unauthorized possession of the spare captain ID is punishable under Grand Felony Offense [307]: Grand Larceny.
+
+command-description-spareid-unlock = Force unlocks the spare ID cabinet, optionally making an announcement.
+command-spareid-unlock-announcement = Command access authority has been manually granted to the Spare ID cabinet by Central Command. Unauthorized possession of the spare captain ID is punishable under Grand Felony Offense [307]: Grand Larceny.
\ No newline at end of file
diff --git a/Resources/Prototypes/Catalog/Fills/Lockers/heads.yml b/Resources/Prototypes/Catalog/Fills/Lockers/heads.yml
index 46915bad263..c3cd90970c2 100644
--- a/Resources/Prototypes/Catalog/Fills/Lockers/heads.yml
+++ b/Resources/Prototypes/Catalog/Fills/Lockers/heads.yml
@@ -30,7 +30,7 @@
id: LockerFillCaptainNoLaser
table: !type:AllSelector
children:
- - id: CaptainIDCard
+ #- id: CaptainIDCard # DeltaV - Replaced by the spare ID system
- id: CigarGoldCase
prob: 0.25
- id: ClothingBeltSheathFilled
diff --git a/Resources/Prototypes/Entities/Stations/nanotrasen.yml b/Resources/Prototypes/Entities/Stations/nanotrasen.yml
index 03155c8197d..d2da2afa21f 100644
--- a/Resources/Prototypes/Entities/Stations/nanotrasen.yml
+++ b/Resources/Prototypes/Entities/Stations/nanotrasen.yml
@@ -26,6 +26,9 @@
- BaseStationAllEventsEligible
- BaseStationNanotrasen
- BaseStationDeliveries
+ # Begin DeltaV - Station additions
+ - BaseStationAutomaticSpareId
+ # End DeltaV - Station additions
categories: [ HideSpawnMenu ]
components:
- type: Transform
diff --git a/Resources/Prototypes/_DVA/Entities/Stations/base.yml b/Resources/Prototypes/_DVA/Entities/Stations/base.yml
new file mode 100644
index 00000000000..380013e932b
--- /dev/null
+++ b/Resources/Prototypes/_DVA/Entities/Stations/base.yml
@@ -0,0 +1,5 @@
+- type: entity
+ id: BaseStationAutomaticSpareId
+ abstract: true
+ components:
+ - type: DVAutomaticSpareId
\ No newline at end of file
diff --git a/Resources/Prototypes/_DVA/Entities/Structures/Wallmounts/spare_id_cabinet.yml b/Resources/Prototypes/_DVA/Entities/Structures/Wallmounts/spare_id_cabinet.yml
new file mode 100644
index 00000000000..4ba512998da
--- /dev/null
+++ b/Resources/Prototypes/_DVA/Entities/Structures/Wallmounts/spare_id_cabinet.yml
@@ -0,0 +1,41 @@
+- type: entity
+ parent: BaseWallmountCabinetGlass
+ id: SpareIdCabinet
+ name: spare id cabinet
+ description: There is a small label that reads "For authorized personnel only".
+ placement:
+ mode: SnapgridCenter
+ components:
+ - type: Sprite
+ sprite: _DVA/Structures/Wallmounts/idcard_cabinet.rsi
+ layers:
+ - state: cabinet
+ - state: card
+ map: ["enum.ItemCabinetVisuals.Layer"]
+ visible: true
+ - state: glass
+ map: ["enum.OpenableVisuals.Layer"]
+ - state: locked
+ shader: unshaded
+ map: ["enum.LockVisualLayers.Lock"]
+ - type: ItemSlots
+ slots:
+ ItemCabinet:
+ ejectOnInteract: true
+ whitelist:
+ components:
+ - IdCard
+ - type: Lock
+ - type: AccessReader
+ access: [["CentralCommand"]]
+ - type: DVSpareIDSafe
+
+- type: entity
+ parent: SpareIdCabinet
+ id: SpareIdCabinetFilled
+ suffix: Filled
+ components:
+ - type: ContainerFill
+ containers:
+ ItemCabinet:
+ - CaptainIDCard
\ No newline at end of file
diff --git a/Resources/Textures/_DVA/Structures/Wallmounts/idcard_cabinet.rsi/cabinet-empty-closed.png b/Resources/Textures/_DVA/Structures/Wallmounts/idcard_cabinet.rsi/cabinet-empty-closed.png
new file mode 100644
index 00000000000..e196f37c3f4
Binary files /dev/null and b/Resources/Textures/_DVA/Structures/Wallmounts/idcard_cabinet.rsi/cabinet-empty-closed.png differ
diff --git a/Resources/Textures/_DVA/Structures/Wallmounts/idcard_cabinet.rsi/cabinet-empty-open.png b/Resources/Textures/_DVA/Structures/Wallmounts/idcard_cabinet.rsi/cabinet-empty-open.png
new file mode 100644
index 00000000000..e7a20d2b61b
Binary files /dev/null and b/Resources/Textures/_DVA/Structures/Wallmounts/idcard_cabinet.rsi/cabinet-empty-open.png differ
diff --git a/Resources/Textures/_DVA/Structures/Wallmounts/idcard_cabinet.rsi/cabinet-filled-closed.png b/Resources/Textures/_DVA/Structures/Wallmounts/idcard_cabinet.rsi/cabinet-filled-closed.png
new file mode 100644
index 00000000000..746ca5ea79e
Binary files /dev/null and b/Resources/Textures/_DVA/Structures/Wallmounts/idcard_cabinet.rsi/cabinet-filled-closed.png differ
diff --git a/Resources/Textures/_DVA/Structures/Wallmounts/idcard_cabinet.rsi/cabinet-filled-open.png b/Resources/Textures/_DVA/Structures/Wallmounts/idcard_cabinet.rsi/cabinet-filled-open.png
new file mode 100644
index 00000000000..fd7c9bcbd2b
Binary files /dev/null and b/Resources/Textures/_DVA/Structures/Wallmounts/idcard_cabinet.rsi/cabinet-filled-open.png differ
diff --git a/Resources/Textures/_DVA/Structures/Wallmounts/idcard_cabinet.rsi/cabinet.png b/Resources/Textures/_DVA/Structures/Wallmounts/idcard_cabinet.rsi/cabinet.png
new file mode 100644
index 00000000000..0ba17d40ab6
Binary files /dev/null and b/Resources/Textures/_DVA/Structures/Wallmounts/idcard_cabinet.rsi/cabinet.png differ
diff --git a/Resources/Textures/_DVA/Structures/Wallmounts/idcard_cabinet.rsi/card.png b/Resources/Textures/_DVA/Structures/Wallmounts/idcard_cabinet.rsi/card.png
new file mode 100644
index 00000000000..c53c3d27637
Binary files /dev/null and b/Resources/Textures/_DVA/Structures/Wallmounts/idcard_cabinet.rsi/card.png differ
diff --git a/Resources/Textures/_DVA/Structures/Wallmounts/idcard_cabinet.rsi/glass-1.png b/Resources/Textures/_DVA/Structures/Wallmounts/idcard_cabinet.rsi/glass-1.png
new file mode 100644
index 00000000000..450de1004bb
Binary files /dev/null and b/Resources/Textures/_DVA/Structures/Wallmounts/idcard_cabinet.rsi/glass-1.png differ
diff --git a/Resources/Textures/_DVA/Structures/Wallmounts/idcard_cabinet.rsi/glass-2.png b/Resources/Textures/_DVA/Structures/Wallmounts/idcard_cabinet.rsi/glass-2.png
new file mode 100644
index 00000000000..7f6f593d25e
Binary files /dev/null and b/Resources/Textures/_DVA/Structures/Wallmounts/idcard_cabinet.rsi/glass-2.png differ
diff --git a/Resources/Textures/_DVA/Structures/Wallmounts/idcard_cabinet.rsi/glass-3.png b/Resources/Textures/_DVA/Structures/Wallmounts/idcard_cabinet.rsi/glass-3.png
new file mode 100644
index 00000000000..2f80165912d
Binary files /dev/null and b/Resources/Textures/_DVA/Structures/Wallmounts/idcard_cabinet.rsi/glass-3.png differ
diff --git a/Resources/Textures/_DVA/Structures/Wallmounts/idcard_cabinet.rsi/glass-4.png b/Resources/Textures/_DVA/Structures/Wallmounts/idcard_cabinet.rsi/glass-4.png
new file mode 100644
index 00000000000..0f3d0d02202
Binary files /dev/null and b/Resources/Textures/_DVA/Structures/Wallmounts/idcard_cabinet.rsi/glass-4.png differ
diff --git a/Resources/Textures/_DVA/Structures/Wallmounts/idcard_cabinet.rsi/glass-up.png b/Resources/Textures/_DVA/Structures/Wallmounts/idcard_cabinet.rsi/glass-up.png
new file mode 100644
index 00000000000..14d0125aef0
Binary files /dev/null and b/Resources/Textures/_DVA/Structures/Wallmounts/idcard_cabinet.rsi/glass-up.png differ
diff --git a/Resources/Textures/_DVA/Structures/Wallmounts/idcard_cabinet.rsi/glass.png b/Resources/Textures/_DVA/Structures/Wallmounts/idcard_cabinet.rsi/glass.png
new file mode 100644
index 00000000000..afed9b66d7c
Binary files /dev/null and b/Resources/Textures/_DVA/Structures/Wallmounts/idcard_cabinet.rsi/glass.png differ
diff --git a/Resources/Textures/_DVA/Structures/Wallmounts/idcard_cabinet.rsi/locked.png b/Resources/Textures/_DVA/Structures/Wallmounts/idcard_cabinet.rsi/locked.png
new file mode 100644
index 00000000000..f24a3ab0470
Binary files /dev/null and b/Resources/Textures/_DVA/Structures/Wallmounts/idcard_cabinet.rsi/locked.png differ
diff --git a/Resources/Textures/_DVA/Structures/Wallmounts/idcard_cabinet.rsi/meta.json b/Resources/Textures/_DVA/Structures/Wallmounts/idcard_cabinet.rsi/meta.json
new file mode 100644
index 00000000000..920cf6679cb
--- /dev/null
+++ b/Resources/Textures/_DVA/Structures/Wallmounts/idcard_cabinet.rsi/meta.json
@@ -0,0 +1,53 @@
+{
+ "version": 1,
+ "license": "CC0-1.0",
+ "copyright": "Original work by TJohnson.",
+ "size": {
+ "x": 32,
+ "y": 32
+ },
+ "states": [
+ {
+ "name": "cabinet-empty-closed"
+ },
+ {
+ "name": "cabinet-empty-open"
+ },
+ {
+ "name": "cabinet-filled-open"
+ },
+ {
+ "name": "cabinet-filled-closed"
+ },
+ {
+ "name": "cabinet"
+ },
+ {
+ "name": "card"
+ },
+ {
+ "name": "glass"
+ },
+ {
+ "name": "glass-1"
+ },
+ {
+ "name": "glass-2"
+ },
+ {
+ "name": "glass-3"
+ },
+ {
+ "name": "glass-4"
+ },
+ {
+ "name": "glass-up"
+ },
+ {
+ "name": "unlocked"
+ },
+ {
+ "name": "locked"
+ }
+ ]
+}
diff --git a/Resources/Textures/_DVA/Structures/Wallmounts/idcard_cabinet.rsi/unlocked.png b/Resources/Textures/_DVA/Structures/Wallmounts/idcard_cabinet.rsi/unlocked.png
new file mode 100644
index 00000000000..22d718bb96f
Binary files /dev/null and b/Resources/Textures/_DVA/Structures/Wallmounts/idcard_cabinet.rsi/unlocked.png differ