diff --git a/Content.Server/_DV/MetalDetector/MetalDetectorSystem.cs b/Content.Server/_DV/MetalDetector/MetalDetectorSystem.cs
new file mode 100644
index 0000000000..fa57d51be7
--- /dev/null
+++ b/Content.Server/_DV/MetalDetector/MetalDetectorSystem.cs
@@ -0,0 +1,158 @@
+using System.Linq;
+using Content.Server.DeviceLinking.Systems;
+using Content.Server.Power.Components;
+using Content.Shared._DV.MetalDetector;
+using Content.Shared.Access.Components;
+using Content.Shared.Access.Systems;
+using Content.Shared.Contraband;
+using Content.Shared.Emag.Components;
+using Content.Shared.Emag.Systems;
+using Content.Shared.Implants.Components;
+using Content.Shared.Inventory;
+using Content.Shared.Item.ItemToggle.Components;
+using Content.Shared.StepTrigger.Systems;
+using Content.Shared.Storage;
+using Content.Shared.Toggleable;
+using Robust.Shared.Containers;
+using Robust.Shared.Random;
+using Robust.Shared.Timing;
+
+namespace Content.Server._DV.MetalDetector;
+
+///
+/// Systems related to the Metal Detector and how it functions.
+///
+public sealed class MetalDetectorSystem : EntitySystem
+{
+ [Dependency] private readonly InventorySystem _inventorySystem = default!;
+ [Dependency] private readonly SharedIdCardSystem _idSystem = default!;
+ [Dependency] private readonly SharedContainerSystem _containerSystem = default!;
+ [Dependency] private readonly SharedAppearanceSystem _appearanceSystem = default!;
+ [Dependency] private readonly EmagSystem _emag = default!;
+ [Dependency] private readonly DeviceLinkSystem _deviceLink = default!;
+ [Dependency] private readonly IGameTiming _timing = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(HandleStepOnTriggered);
+ SubscribeLocalEvent(HandleStepOffTriggered);
+ SubscribeLocalEvent(HandleStepTriggerAttempt);
+ SubscribeLocalEvent(OnEmagged);
+ }
+
+ public override void Update(float frameTime)
+ {
+ base.Update(frameTime);
+
+ using var query = EntityQueryEnumerator();
+ while (query.MoveNext(out var ent, out var comp))
+ {
+ if (!comp.IsSirenRunning || !TryComp(ent, out var toggle) || !toggle.Activated)
+ continue;
+
+ var powered = TryComp(ent, out var receiver) && receiver.Powered;
+
+ TryComp(ent, out var appComp);
+
+ if (comp.EndOfSirenSound <= _timing.CurTime || !powered)
+ {
+ comp.IsSirenRunning = false;
+ var toggledEvent = new ItemToggledEvent(false, false, ent);
+ toggle.Activated = false;
+ _appearanceSystem.SetData(ent, MetalDetectorVisuals.MetalDetectorActivated, false);
+ _appearanceSystem.SetData(ent, ToggleableVisuals.Enabled, false, appComp);
+ RaiseLocalEvent(ent, ref toggledEvent);
+ }
+ }
+ }
+
+ private void HandleStepOnTriggered(Entity ent, ref StepTriggeredOnEvent args)
+ {
+ if (ent.Comp.IsSirenRunning)
+ {
+ ent.Comp.EndOfSirenSound = _timing.CurTime + ent.Comp.SirenRunTime;
+ return;
+ }
+
+ var random = new Random();
+ if (TryComp(ent, out var toggleComponent) && (CheckForContraband(args.Tripper)
+ || random.NextFloat(0.0f, 100.0f) < ent.Comp.FalsePositiveChance
+ || HasComp(ent)))
+ {
+ toggleComponent.Activated = true;
+ TryComp(ent, out var appComp);
+ _appearanceSystem.SetData(ent, MetalDetectorVisuals.MetalDetectorActivated, true);
+ _appearanceSystem.SetData(ent, ToggleableVisuals.Enabled, true, appComp);
+ var toggledEvent = new ItemToggledEvent(false, true, ent);
+ ent.Comp.EndOfSirenSound = _timing.CurTime + ent.Comp.SirenRunTime;
+ RaiseLocalEvent(ent, ref toggledEvent);
+
+ _deviceLink.InvokePort(ent, ent.Comp.TriggerPort);
+ }
+ }
+
+ private void HandleStepOffTriggered(Entity ent, ref StepTriggeredOffEvent args)
+ {
+ // Start timer to deactivate the siren
+ ent.Comp.IsSirenRunning = TryComp(ent, out var toggleComponent) && toggleComponent.Activated;
+ }
+
+ private void HandleStepTriggerAttempt(Entity ent, ref StepTriggerAttemptEvent args)
+ {
+ args.Continue = TryComp(ent, out var receiver) && receiver.Powered;
+ }
+
+ private void OnEmagged(Entity metalDetectorComponent, ref GotEmaggedEvent args)
+ {
+ if (!_emag.CompareFlag(args.Type, EmagType.Interaction))
+ return;
+
+ if (_emag.CheckFlag(metalDetectorComponent.Owner, EmagType.Interaction))
+ return;
+
+ args.Handled = true;
+ }
+
+ private bool CheckForContraband(EntityUid characterUid)
+ {
+ // In it's just a contraband item
+ if (HasComp(characterUid))
+ return true;
+
+ if (_containerSystem.TryGetContainer(characterUid, ImplanterComponent.ImplantSlotId, out var implants))
+ {
+ var storageImplanter = implants.ContainedEntities.ToList().Find(HasComp);
+
+ if (TryComp(storageImplanter, out var storage))
+ {
+ foreach (var stored in storage.Container.ContainedEntities)
+ {
+ if (IsEntityContraband(characterUid, stored))
+ return true;
+ }
+ }
+ }
+
+ foreach (var item in _inventorySystem.GetHandOrInventoryEntities(characterUid))
+ {
+ if (IsEntityContraband(characterUid, item))
+ return true;
+ }
+
+ return false;
+ }
+
+ private bool IsEntityContraband(EntityUid characterUid, EntityUid item)
+ {
+ if (!TryComp(item, out var contrabandComp))
+ return false;
+
+ if (!_idSystem.TryFindIdCard(characterUid, out var idCard))
+ return true;
+
+ return !idCard.Comp.JobDepartments.Intersect(contrabandComp.AllowedDepartments).Any();
+ }
+
+}
diff --git a/Content.Shared/_DV/MetalDetector/MetalDetectorComponent.cs b/Content.Shared/_DV/MetalDetector/MetalDetectorComponent.cs
new file mode 100644
index 0000000000..ae4b1be89d
--- /dev/null
+++ b/Content.Shared/_DV/MetalDetector/MetalDetectorComponent.cs
@@ -0,0 +1,54 @@
+using Content.Shared.DeviceLinking;
+using Robust.Shared.Audio;
+using Robust.Shared.Serialization;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
+
+namespace Content.Shared._DV.MetalDetector;
+
+///
+/// Responsible for holding some data of whom has passed by and which item is the most dangerous.
+///
+[RegisterComponent]
+public sealed partial class MetalDetectorComponent : Component
+{
+ [DataField]
+ public TimeSpan SirenRunTime = TimeSpan.FromSeconds(10);
+
+ ///
+ /// Value to determine the chance that the Metal Detector simply fires and gives a false positive. from 0 to 100.
+ ///
+ [DataField]
+ public float FalsePositiveChance = 5.0f;
+
+ ///
+ /// Timespan which is set runtime to determine when the Siren should stop running
+ ///
+ [DataField]
+ public TimeSpan EndOfSirenSound = TimeSpan.FromSeconds(0);
+
+ public bool IsSirenRunning = false;
+
+ ///
+ /// Siren Sound which is played when the Metal Detector fires.
+ ///
+ [DataField]
+ public SoundSpecifier? SirenSound;
+
+ ///
+ /// The port that gets signaled when the the metal detector fires
+ ///
+ [DataField(customTypeSerializer: typeof(PrototypeIdSerializer))]
+ public string TriggerPort = "Trigger";
+}
+
+[Serializable, NetSerializable]
+public enum MetalDetectorVisuals : byte
+{
+ MetalDetectorActivated,
+}
+
+[Serializable, NetSerializable]
+public enum MetalDetectorVisualLayers : byte
+{
+ MetalDetectorLayer,
+}
diff --git a/Resources/Prototypes/Entities/Structures/Machines/lathe.yml b/Resources/Prototypes/Entities/Structures/Machines/lathe.yml
index 11731005db..d3f9983e6a 100644
--- a/Resources/Prototypes/Entities/Structures/Machines/lathe.yml
+++ b/Resources/Prototypes/Entities/Structures/Machines/lathe.yml
@@ -450,6 +450,7 @@
- SecurityAmmoStatic
- SecurityWeaponsStatic
# Being DeltaV Additions
+ - SecurityMachines
- ReportingStatic
- SecurityRubberAmmoStatic
- SecurityShotgunDrumsStatic
@@ -519,7 +520,7 @@
unlitRunningState: unlit-building # DeltaV - added animation sprite layers
staticPacks:
- SecurityAmmoStatic
- # Start DeltaV Additions
+ # Start DeltaV Additions
- SecurityShotgunDrumsStatic
- AmmoFabPracticeStatic # Removes practice guns
- SecurityRubberAmmoStatic
diff --git a/Resources/Prototypes/_DV/Entities/Objects/Devices/CircuitBoards/Machine/production.yml b/Resources/Prototypes/_DV/Entities/Objects/Devices/CircuitBoards/Machine/production.yml
index d9cdbe928d..4ab3422a94 100644
--- a/Resources/Prototypes/_DV/Entities/Objects/Devices/CircuitBoards/Machine/production.yml
+++ b/Resources/Prototypes/_DV/Entities/Objects/Devices/CircuitBoards/Machine/production.yml
@@ -101,3 +101,19 @@
stackRequirements:
Manipulator: 4
Cable: 5
+
+# Metal Detector
+- type: entity
+ id: MetalDetectorMachineCircuitBoard
+ parent: BaseMachineCircuitboard
+ name: metal detector machine board
+ description: Makes beeps and boops when evil passes by
+ components:
+ - type: Sprite
+ state: security
+ - type: MachineBoard
+ prototype: MetalDetector
+ stackRequirements:
+ Manipulator: 4
+ Glass: 5
+ Cable: 5
diff --git a/Resources/Prototypes/_DV/Entities/Structures/Machines/metal_detector.yml b/Resources/Prototypes/_DV/Entities/Structures/Machines/metal_detector.yml
new file mode 100644
index 0000000000..307f22ad6b
--- /dev/null
+++ b/Resources/Prototypes/_DV/Entities/Structures/Machines/metal_detector.yml
@@ -0,0 +1,112 @@
+- type: entity
+ parent: ConstructibleMachine
+ id: MetalDetector
+ name: Metal Detector
+ description: Beeps when metal and contraband is detected by a passing person.
+ placement:
+ mode: SnapgridCenter
+ components:
+ - type: Sprite
+ sprite: _DV/Structures/Machines/metal_detector.rsi
+ layers:
+ - state: metal_detector_off
+ map: [ "enum.MetalDetectorVisualLayers.MetalDetectorLayer" ]
+ - type: GenericVisualizer
+ visuals:
+ enum.MetalDetectorVisuals.MetalDetectorActivated:
+ enum.MetalDetectorVisualLayers.MetalDetectorLayer:
+ True: { state: metal_detector_on }
+ False: { state: metal_detector_off }
+ - type: ApcPowerReceiver
+ powerLoad: 1000
+ - type: Clickable
+ - type: InteractionOutline
+ - type: Pullable
+ - type: Machine
+ board: MetalDetectorMachineCircuitBoard
+ - type: Transform
+ anchored: true
+ noRot: true
+ - type: ExtensionCableReceiver
+ - type: LightningTarget
+ priority: 1
+ - type: DeviceNetwork
+ deviceNetId: Wireless
+ - type: WirelessNetworkConnection
+ range: 200
+ - type: DeviceLinkSource
+ ports:
+ - Trigger
+ - type: WiresPanel
+ - type: Wires
+ alwaysRandomize: true
+ boardName: wires-board-name-apc
+ layoutId: APC
+ - type: WiresVisuals
+ - type: Destructible
+ thresholds:
+ - trigger:
+ !type:DamageTrigger
+ damage: 200
+ behaviors:
+ - !type:DoActsBehavior
+ acts: [ "Destruction" ]
+ - trigger:
+ !type:DamageTrigger
+ damage: 100
+ behaviors:
+ - !type:DoActsBehavior
+ acts: ["Destruction"]
+ - !type:PlaySoundBehavior
+ sound:
+ collection: MetalBreak
+ - type: LockedWiresPanel
+ - type: LockedAnchorable
+ - type: Damageable
+ damageContainer: Inorganic
+ damageModifierSet: Metallic
+ - type: Anchorable
+ - type: Physics
+ bodyType: Static
+ - type: ToggleableVisuals
+ spriteLayer: null
+ - type: ItemToggle
+ predictable: false # issues between ToggleCellDraw and ItemToggleActiveSound
+ onActivate: false
+ - type: ItemTogglePointLight
+ - type: PointLight
+ enabled: false
+ radius: 2.5
+ energy: 3
+ color: red
+ netsync: false
+ mask: /Textures/Effects/LightMasks/double_cone.png
+ - type: RotatingLight
+ speed: 360
+ - type: ItemToggleActiveSound
+ activeSound:
+ path: /Audio/Effects/Vehicle/policesiren.ogg
+ params:
+ volume: -3
+ - type: Appearance
+ - type: Fixtures
+ fixtures:
+ shape:
+ shape:
+ !type:PhysShapeAabb
+ bounds: "-0.5,-0.5,0.5,0.5"
+ mask:
+ - TabletopMachineMask
+ layer:
+ - TabletopMachineLayer
+ overlap:
+ shape:
+ !type:PhysShapeAabb
+ bounds: "-0.5,-0.5,0.5,0.5"
+ hard: false
+ layer:
+ - Impassable
+ - type: MetalDetector
+ - type: StepTrigger
+ requiredTriggeredSpeed: 0
+ stepOn: true
diff --git a/Resources/Prototypes/_DV/Recipes/Lathes/Packs/security.yml b/Resources/Prototypes/_DV/Recipes/Lathes/Packs/security.yml
index 3c1fe93ac8..efcdb750fe 100644
--- a/Resources/Prototypes/_DV/Recipes/Lathes/Packs/security.yml
+++ b/Resources/Prototypes/_DV/Recipes/Lathes/Packs/security.yml
@@ -64,6 +64,11 @@
recipes:
- Truncheon
+- type: latheRecipePack
+ id: SecurityMachines
+ recipes:
+ - MetalDetectorMachineCircuitBoard
+
## Dynamic
- type: latheRecipePack
diff --git a/Resources/Prototypes/_DV/Recipes/Lathes/machine_boards.yml b/Resources/Prototypes/_DV/Recipes/Lathes/machine_boards.yml
index 343a21e83a..fcffa83fc1 100644
--- a/Resources/Prototypes/_DV/Recipes/Lathes/machine_boards.yml
+++ b/Resources/Prototypes/_DV/Recipes/Lathes/machine_boards.yml
@@ -3,3 +3,9 @@
parent: [ BaseCircuitboardRecipe, BaseEngineeringMachineRecipeCategory ]
id: KitchenFreezerMachineCircuitBoard
result: KitchenFreezerMachineCircuitBoard
+
+# Security
+- type: latheRecipe
+ parent: [ BaseCircuitboardRecipe, BaseSecurityMachineRecipeCategory ]
+ id: MetalDetectorMachineCircuitBoard
+ result: MetalDetectorMachineCircuitBoard
diff --git a/Resources/Textures/_DV/Structures/Machines/metal_detector.rsi/meta.json b/Resources/Textures/_DV/Structures/Machines/metal_detector.rsi/meta.json
new file mode 100644
index 0000000000..b8d1637ac3
--- /dev/null
+++ b/Resources/Textures/_DV/Structures/Machines/metal_detector.rsi/meta.json
@@ -0,0 +1,28 @@
+{
+ "version": 1,
+ "license": "CC-BY-SA-4.0",
+ "copyright": "From coryduck on the Delta-V Server Discord",
+ "size": {
+ "x": 32,
+ "y": 32
+ },
+ "states": [
+ {
+ "name": "metal_detector_off"
+ },
+ {
+ "name": "metal_detector_on",
+ "delays": [
+ [
+ 0.1,
+ 0.1,
+ 0.1,
+ 0.1,
+ 0.1,
+ 0.1,
+ 0.1
+ ]
+ ]
+ }
+ ]
+}
diff --git a/Resources/Textures/_DV/Structures/Machines/metal_detector.rsi/metal_detector_off.png b/Resources/Textures/_DV/Structures/Machines/metal_detector.rsi/metal_detector_off.png
new file mode 100644
index 0000000000..19b0a70c29
Binary files /dev/null and b/Resources/Textures/_DV/Structures/Machines/metal_detector.rsi/metal_detector_off.png differ
diff --git a/Resources/Textures/_DV/Structures/Machines/metal_detector.rsi/metal_detector_on.png b/Resources/Textures/_DV/Structures/Machines/metal_detector.rsi/metal_detector_on.png
new file mode 100644
index 0000000000..bf29c3ec69
Binary files /dev/null and b/Resources/Textures/_DV/Structures/Machines/metal_detector.rsi/metal_detector_on.png differ