diff --git a/Content.Client/IgnoredComponents.cs b/Content.Client/IgnoredComponents.cs
index 5db13ba33a..1dfd6423f4 100644
--- a/Content.Client/IgnoredComponents.cs
+++ b/Content.Client/IgnoredComponents.cs
@@ -233,6 +233,7 @@ namespace Content.Client
"FoamSolutionAreaEffect",
"GasFilter",
"Recyclable",
+ "LightReplacer",
"SecretStash",
"Toilet",
"ClusterFlash",
diff --git a/Content.Server/GameObjects/Components/Janitorial/LightReplacerComponent.cs b/Content.Server/GameObjects/Components/Janitorial/LightReplacerComponent.cs
new file mode 100644
index 0000000000..7ec4df5dd6
--- /dev/null
+++ b/Content.Server/GameObjects/Components/Janitorial/LightReplacerComponent.cs
@@ -0,0 +1,167 @@
+#nullable enable
+using Content.Server.GameObjects.Components.Items.Storage;
+using Content.Server.GameObjects.Components.Power.ApcNetComponents.PowerReceiverUsers;
+using Content.Shared.GameObjects;
+using Content.Shared.GameObjects.EntitySystems.ActionBlocker;
+using Content.Shared.Interfaces;
+using Content.Shared.Interfaces.GameObjects.Components;
+using Robust.Server.GameObjects;
+using Robust.Shared.Audio;
+using Robust.Shared.Containers;
+using Robust.Shared.GameObjects;
+using Robust.Shared.Localization;
+using Robust.Shared.Player;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization.Manager.Attributes;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
+using Robust.Shared.ViewVariables;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace Content.Server.GameObjects.Components.Janitorial
+{
+ ///
+ /// Device that allows user to quikly change bulbs in
+ /// Can be reloaded by new light tubes or light bulbs
+ ///
+ [RegisterComponent]
+ public class LightReplacerComponent : Component
+ {
+ public override string Name => "LightReplacer";
+ public override uint? NetID => ContentNetIDs.LIGHT_REPLACER;
+
+ [DataField("sound")] private string _sound = "/Audio/Weapons/click.ogg";
+
+ // bulbs that were inside light replacer when it spawned
+ [DataField("contents")] private List _contents = new();
+ // bulbs that were inserted inside light replacer
+ [ViewVariables] private IContainer _insertedBulbs = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+ _insertedBulbs = ContainerHelpers.EnsureContainer(Owner, "light_replacer_storage");
+ }
+
+ public bool TryReplaceBulb(PoweredLightComponent fixture, IEntity? user = null)
+ {
+ // check if light bulb is broken or missing
+ if (fixture.LightBulb != null && fixture.LightBulb.State == LightBulbState.Normal) return false;
+
+ // try get first inserted bulb of the same type as targeted light fixtutre
+ var bulb = _insertedBulbs.ContainedEntities.FirstOrDefault(
+ (e) => e.GetComponentOrNull()?.Type == fixture.BulbType);
+
+ // found bulb in inserted storage
+ if (bulb != null)
+ {
+ // try to remove it
+ var hasRemoved = _insertedBulbs.Remove(bulb);
+ if (!hasRemoved)
+ return false;
+ }
+ // try to create new instance of bulb from LightReplacerEntity
+ else
+ {
+ var bulbEnt = _contents.FirstOrDefault((e) => e.Type == fixture.BulbType && e.Amount > 0);
+
+ // found right bulb, let's spawn it
+ if (bulbEnt != null)
+ {
+ bulb = Owner.EntityManager.SpawnEntity(bulbEnt.PrototypeName, Owner.Transform.Coordinates);
+ bulbEnt.Amount--;
+ }
+ // not found any light bulbs
+ else
+ {
+ if (user != null)
+ {
+ var msg = Loc.GetString("comp-light-replacer-missing-light", ("light-replacer", Owner));
+ user.PopupMessage(msg);
+ }
+ return false;
+ }
+ }
+
+ // insert it into fixture
+ var wasReplaced = fixture.ReplaceBulb(bulb);
+ if (wasReplaced)
+ {
+ EntitySystem.Get().Play(Filter.Broadcast(), _sound,
+ Owner, AudioParams.Default.WithVolume(-4f));
+ }
+
+
+ return wasReplaced;
+ }
+
+ public bool TryInsertBulb(LightBulbComponent bulb, IEntity? user = null, bool showTooltip = false)
+ {
+ // only normal lights can be inserted inside light replacer
+ if (bulb.State != LightBulbState.Normal)
+ {
+ if (showTooltip && user != null)
+ {
+ var msg = Loc.GetString("comp-light-replacer-insert-broken-light");
+ user.PopupMessage(msg);
+ }
+
+ return false;
+ }
+
+ // try insert light and show message
+ var hasInsert = _insertedBulbs.Insert(bulb.Owner);
+ if (hasInsert && showTooltip && user != null)
+ {
+ var msg = Loc.GetString("comp-light-replacer-insert-light",
+ ("light-replacer", Owner), ("bulb", bulb.Owner));
+ user.PopupMessage(msg);
+ }
+
+
+ return hasInsert;
+ }
+
+ public bool TryInsertBulb(ServerStorageComponent storage, IEntity? user = null)
+ {
+ if (storage.StoredEntities == null)
+ return false;
+
+ var insertedBulbs = 0;
+ var storagedEnts = storage.StoredEntities.ToArray();
+ foreach (var ent in storagedEnts)
+ {
+ if (ent.TryGetComponent(out LightBulbComponent? bulb))
+ {
+ if (TryInsertBulb(bulb))
+ insertedBulbs++;
+ }
+ }
+
+ // show some message if success
+ if (insertedBulbs > 0 && user != null)
+ {
+ var msg = Loc.GetString("comp-light-replacer-refill-from-storage", ("light-replacer", Owner));
+ user.PopupMessage(msg);
+ }
+
+ return insertedBulbs > 0;
+ }
+
+ [Serializable]
+ [DataDefinition]
+ public class LightReplacerEntity
+ {
+ [DataField("name", customTypeSerializer: typeof(PrototypeIdSerializer))]
+ public string? PrototypeName;
+
+ [DataField("amount")]
+ public int Amount;
+
+ [DataField("type")]
+ public LightBulbType Type;
+ }
+ }
+}
diff --git a/Content.Server/GameObjects/Components/Power/ApcNetComponents/PowerReceiverUsers/PoweredLightComponent.cs b/Content.Server/GameObjects/Components/Power/ApcNetComponents/PowerReceiverUsers/PoweredLightComponent.cs
index 6b75a9333e..ad78398cf9 100644
--- a/Content.Server/GameObjects/Components/Power/ApcNetComponents/PowerReceiverUsers/PoweredLightComponent.cs
+++ b/Content.Server/GameObjects/Components/Power/ApcNetComponents/PowerReceiverUsers/PoweredLightComponent.cs
@@ -62,12 +62,13 @@ namespace Content.Server.GameObjects.Components.Power.ApcNetComponents.PowerRece
[ViewVariables] [DataField("ignoreGhostsBoo")]
private bool _ignoreGhostsBoo;
- [DataField("bulb")]
- private LightBulbType BulbType = LightBulbType.Tube;
+ [DataField("bulb")] private LightBulbType _bulbType = LightBulbType.Tube;
+ public LightBulbType BulbType => _bulbType;
+
[ViewVariables] private ContainerSlot _lightBulbContainer = default!;
[ViewVariables]
- private LightBulbComponent? LightBulb
+ public LightBulbComponent? LightBulb
{
get
{
@@ -126,6 +127,15 @@ namespace Content.Server.GameObjects.Components.Power.ApcNetComponents.PowerRece
}
}
+ ///
+ /// Try to replace current bulb with a new one
+ ///
+ public bool ReplaceBulb(IEntity bulb)
+ {
+ EjectBulb();
+ return InsertBulb(bulb);
+ }
+
///
/// Inserts the bulb if possible.
///
@@ -134,7 +144,7 @@ namespace Content.Server.GameObjects.Components.Power.ApcNetComponents.PowerRece
{
if (LightBulb != null) return false;
if (!bulb.TryGetComponent(out LightBulbComponent? lightBulb)) return false;
- if (lightBulb.Type != BulbType) return false;
+ if (lightBulb.Type != _bulbType) return false;
var inserted = _lightBulbContainer.Insert(bulb);
@@ -149,7 +159,7 @@ namespace Content.Server.GameObjects.Components.Power.ApcNetComponents.PowerRece
///
/// Ejects the bulb to a mob's hand if possible.
///
- private void EjectBulb(IEntity user)
+ private void EjectBulb(IEntity? user = null)
{
if (LightBulb == null) return;
@@ -160,9 +170,17 @@ namespace Content.Server.GameObjects.Components.Power.ApcNetComponents.PowerRece
if (!_lightBulbContainer.Remove(bulb.Owner)) return;
- if (!user.TryGetComponent(out HandsComponent? hands)
- || !hands.PutInHand(bulb.Owner.GetComponent()))
- bulb.Owner.Transform.Coordinates = user.Transform.Coordinates;
+ if (user != null)
+ {
+ if (!user.TryGetComponent(out HandsComponent? hands)
+ || !hands.PutInHand(bulb.Owner.GetComponent()))
+ bulb.Owner.Transform.Coordinates = user.Transform.Coordinates;
+ }
+ else
+ {
+ bulb.Owner.Transform.Coordinates = Owner.Transform.Coordinates;
+ }
+
}
///
@@ -259,7 +277,7 @@ namespace Content.Server.GameObjects.Components.Power.ApcNetComponents.PowerRece
{
if (_hasLampOnSpawn)
{
- var prototype = BulbType switch
+ var prototype = _bulbType switch
{
LightBulbType.Bulb => "LightBulb",
LightBulbType.Tube => "LightTube",
diff --git a/Content.Server/GameObjects/EntitySystems/Janitorial/LightReplacerSystem.cs b/Content.Server/GameObjects/EntitySystems/Janitorial/LightReplacerSystem.cs
new file mode 100644
index 0000000000..4bc32cb12d
--- /dev/null
+++ b/Content.Server/GameObjects/EntitySystems/Janitorial/LightReplacerSystem.cs
@@ -0,0 +1,66 @@
+#nullable enable
+using System;
+using Content.Server.GameObjects.Components.Items.Storage;
+using Content.Server.GameObjects.Components.Janitorial;
+using Content.Server.GameObjects.Components.Power.ApcNetComponents.PowerReceiverUsers;
+using Content.Shared.GameObjects.EntitySystems.ActionBlocker;
+using Content.Shared.Interfaces.GameObjects.Components;
+using JetBrains.Annotations;
+using Robust.Shared.GameObjects;
+
+namespace Content.Server.GameObjects.EntitySystems.Janitorial
+{
+ [UsedImplicitly]
+ public class LightReplacerSystem : EntitySystem
+ {
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(HandleInteract);
+ SubscribeLocalEvent(HandleAfterInteract);
+ }
+
+ public override void Shutdown()
+ {
+ base.Shutdown();
+
+ UnsubscribeLocalEvent(HandleInteract);
+ UnsubscribeLocalEvent(HandleAfterInteract);
+ }
+
+ private void HandleAfterInteract(EntityUid uid, LightReplacerComponent component, AfterInteractMessage eventArgs)
+ {
+ // standard interaction checks
+ if (!ActionBlockerSystem.CanUse(eventArgs.User)) return;
+ if (!eventArgs.CanReach) return;
+
+ // behaviour will depends on target type
+ if (eventArgs.Attacked != null)
+ {
+ // replace broken light in fixture?
+ if (eventArgs.Attacked.TryGetComponent(out PoweredLightComponent? fixture))
+ component.TryReplaceBulb(fixture, eventArgs.User);
+ // add new bulb to light replacer container?
+ else if (eventArgs.Attacked.TryGetComponent(out LightBulbComponent? bulb))
+ component.TryInsertBulb(bulb, eventArgs.User, true);
+ }
+ }
+
+ private void HandleInteract(EntityUid uid, LightReplacerComponent component, InteractUsingMessage eventArgs)
+ {
+ // standard interaction checks
+ if (!ActionBlockerSystem.CanInteract(eventArgs.User)) return;
+
+ if (eventArgs.ItemInHand != null)
+ {
+ // want to insert a new light bulb?
+ if (eventArgs.ItemInHand.TryGetComponent(out LightBulbComponent? bulb))
+ component.TryInsertBulb(bulb, eventArgs.User, true);
+ // add bulbs from storage?
+ else if (eventArgs.ItemInHand.TryGetComponent(out ServerStorageComponent? storage))
+ component.TryInsertBulb(storage, eventArgs.User);
+ }
+ }
+ }
+}
diff --git a/Content.Shared/GameObjects/ContentNetIDs.cs b/Content.Shared/GameObjects/ContentNetIDs.cs
index d7f12ed5f2..6b4168f17a 100644
--- a/Content.Shared/GameObjects/ContentNetIDs.cs
+++ b/Content.Shared/GameObjects/ContentNetIDs.cs
@@ -97,6 +97,7 @@ namespace Content.Shared.GameObjects
public const uint DOOR = 1087;
public const uint SPAWN_AFTER_INTERACT = 1088;
public const uint DISASSEMBLE_ON_ACTIVATE = 1089;
+ public const uint LIGHT_REPLACER = 1090;
// Net IDs for integration tests.
public const uint PREDICTION_TEST = 10001;
diff --git a/Resources/Locale/en-US/components/light-replacer-component.ftl b/Resources/Locale/en-US/components/light-replacer-component.ftl
new file mode 100644
index 0000000000..f183ac8f7e
--- /dev/null
+++ b/Resources/Locale/en-US/components/light-replacer-component.ftl
@@ -0,0 +1,14 @@
+
+### Interaction Messages
+
+# Shown when player tries to replace light, but there is no lighs left
+comp-light-replacer-missing-light = No lights left in {$light-replacer}.
+
+# Shown when player inserts light bulb inside light replacer
+comp-light-replacer-insert-light = You insert {$bulb} into {$light-replacer}.
+
+# Shown when player tries to insert in light replacer brolen light bulb
+comp-light-replacer-insert-broken-light = You can't insert broken lights!
+
+# Shown when player refill light from light box
+comp-light-replacer-refill-from-storage = You refill {$light-replacer}.
\ No newline at end of file
diff --git a/Resources/Prototypes/Catalog/Fills/Lockers/service.yml b/Resources/Prototypes/Catalog/Fills/Lockers/service.yml
index 00790d63d3..c65688972b 100644
--- a/Resources/Prototypes/Catalog/Fills/Lockers/service.yml
+++ b/Resources/Prototypes/Catalog/Fills/Lockers/service.yml
@@ -47,6 +47,10 @@
amount: 3
- name: TrashBag
amount: 2
+ - name: LightReplacer
+ amount: 1
+ - name: BoxLightMixed
+ amount: 1
#- type: entity
# id: LockerLegalFilled
diff --git a/Resources/Prototypes/Entities/Objects/Specific/janitor.yml b/Resources/Prototypes/Entities/Objects/Specific/janitor.yml
index 5afe3a58fc..3870deaadf 100644
--- a/Resources/Prototypes/Entities/Objects/Specific/janitor.yml
+++ b/Resources/Prototypes/Entities/Objects/Specific/janitor.yml
@@ -266,3 +266,24 @@
quickInsert: true
areaInsert: true
storageSoundCollection: trashBagRustle
+
+- type: entity
+ parent: BaseItem
+ name: light replacer
+ id: LightReplacer
+ description: An item which uses magnets to easily replace broken lights.
+ components:
+ - type: Sprite
+ sprite: Objects/Specific/Janitorial/light_replacer.rsi
+ state: icon
+ - type: Item
+ sprite: Objects/Specific/Janitorial/light_replacer.rsi
+ - type: LightReplacer
+ contents:
+ - name: LightTube
+ amount: 8
+ type: Tube
+ - name: LightBulb
+ amount: 5
+ type: Bulb
+
diff --git a/Resources/Prototypes/Recipes/Lathes/janitorial.yml b/Resources/Prototypes/Recipes/Lathes/janitorial.yml
index dd15452c58..ec40593cc0 100644
--- a/Resources/Prototypes/Recipes/Lathes/janitorial.yml
+++ b/Resources/Prototypes/Recipes/Lathes/janitorial.yml
@@ -46,3 +46,12 @@
completetime: 300
materials:
plastic: 100
+
+- type: latheRecipe
+ id: LightReplacer
+ icon: Objects/Specific/Janitorial/light_replacer.rsi
+ result: LightReplacer
+ completetime: 600
+ materials:
+ steel: 100
+ glass: 1000
diff --git a/Resources/Textures/Objects/Specific/Janitorial/light_replacer.rsi/emagged.png b/Resources/Textures/Objects/Specific/Janitorial/light_replacer.rsi/emagged.png
new file mode 100644
index 0000000000..5b0d4b79b7
Binary files /dev/null and b/Resources/Textures/Objects/Specific/Janitorial/light_replacer.rsi/emagged.png differ
diff --git a/Resources/Textures/Objects/Specific/Janitorial/light_replacer.rsi/icon.png b/Resources/Textures/Objects/Specific/Janitorial/light_replacer.rsi/icon.png
new file mode 100644
index 0000000000..6524ae80e5
Binary files /dev/null and b/Resources/Textures/Objects/Specific/Janitorial/light_replacer.rsi/icon.png differ
diff --git a/Resources/Textures/Objects/Specific/Janitorial/light_replacer.rsi/inhand-left.png b/Resources/Textures/Objects/Specific/Janitorial/light_replacer.rsi/inhand-left.png
new file mode 100644
index 0000000000..301ecdaefb
Binary files /dev/null and b/Resources/Textures/Objects/Specific/Janitorial/light_replacer.rsi/inhand-left.png differ
diff --git a/Resources/Textures/Objects/Specific/Janitorial/light_replacer.rsi/inhand-right.png b/Resources/Textures/Objects/Specific/Janitorial/light_replacer.rsi/inhand-right.png
new file mode 100644
index 0000000000..d5e242d336
Binary files /dev/null and b/Resources/Textures/Objects/Specific/Janitorial/light_replacer.rsi/inhand-right.png differ
diff --git a/Resources/Textures/Objects/Specific/Janitorial/light_replacer.rsi/meta.json b/Resources/Textures/Objects/Specific/Janitorial/light_replacer.rsi/meta.json
new file mode 100644
index 0000000000..ea81c49910
--- /dev/null
+++ b/Resources/Textures/Objects/Specific/Janitorial/light_replacer.rsi/meta.json
@@ -0,0 +1,25 @@
+{
+ "version": 1,
+ "size": {
+ "x": 32,
+ "y": 32
+ },
+ "license": "CC-BY-SA-3.0",
+ "copyright": "Taken from https://github.com/discordia-space/CEV-Eris/commit/d76180f48949870dd57c7274d494175b3b3515ba",
+ "states": [
+ {
+ "name": "icon"
+ },
+ {
+ "name": "emagged"
+ },
+ {
+ "name": "inhand-right",
+ "directions": 4
+ },
+ {
+ "name": "inhand-left",
+ "directions": 4
+ }
+ ]
+}
\ No newline at end of file