diff --git a/Content.Server/GameObjects/Components/MachineLinking/ISignalReceiver.cs b/Content.Server/GameObjects/Components/MachineLinking/ISignalReceiver.cs new file mode 100644 index 0000000000..78e032606f --- /dev/null +++ b/Content.Server/GameObjects/Components/MachineLinking/ISignalReceiver.cs @@ -0,0 +1,14 @@ +namespace Content.Server.GameObjects.Components.MachineLinking +{ + public interface ISignalReceiver + { + void TriggerSignal(SignalState state); + } + + public enum SignalState + { + On, + Off, + Toggle + } +} diff --git a/Content.Server/GameObjects/Components/MachineLinking/SignalButtonComponent.cs b/Content.Server/GameObjects/Components/MachineLinking/SignalButtonComponent.cs new file mode 100644 index 0000000000..4fc80c5902 --- /dev/null +++ b/Content.Server/GameObjects/Components/MachineLinking/SignalButtonComponent.cs @@ -0,0 +1,40 @@ +using Content.Shared.Interfaces; +using Content.Shared.Interfaces.GameObjects.Components; +using Robust.Shared.GameObjects; +using Robust.Shared.Interfaces.GameObjects; +using Robust.Shared.Localization; + +namespace Content.Server.GameObjects.Components.MachineLinking +{ + [RegisterComponent] + public class SignalButtonComponent : Component, IActivate, IInteractHand + { + public override string Name => "SignalButton"; + + public void Activate(ActivateEventArgs eventArgs) + { + TransmitSignal(eventArgs.User); + } + + public bool InteractHand(InteractHandEventArgs eventArgs) + { + TransmitSignal(eventArgs.User); + return true; + } + + private void TransmitSignal(IEntity user) + { + if (!Owner.TryGetComponent(out var transmitter)) + { + return; + } + + if (transmitter.TransmitSignal(user, SignalState.Toggle)) + { + // Since the button doesn't have an animation, I'm going to use a popup message + Owner.PopupMessage(user, Loc.GetString("Click.")); + } + } + + } +} diff --git a/Content.Server/GameObjects/Components/MachineLinking/SignalLinkerComponent.cs b/Content.Server/GameObjects/Components/MachineLinking/SignalLinkerComponent.cs new file mode 100644 index 0000000000..9a0acd7222 --- /dev/null +++ b/Content.Server/GameObjects/Components/MachineLinking/SignalLinkerComponent.cs @@ -0,0 +1,21 @@ +using Robust.Shared.GameObjects; +using Robust.Shared.ViewVariables; + +namespace Content.Server.GameObjects.Components.MachineLinking +{ + [RegisterComponent] + public class SignalLinkerComponent : Component + { + public override string Name => "SignalLinker"; + + [ViewVariables] + public SignalTransmitterComponent Link { get; set; } + + public override void Initialize() + { + base.Initialize(); + + Link = null; + } + } +} diff --git a/Content.Server/GameObjects/Components/MachineLinking/SignalReceiverComponent.cs b/Content.Server/GameObjects/Components/MachineLinking/SignalReceiverComponent.cs new file mode 100644 index 0000000000..c1e274862a --- /dev/null +++ b/Content.Server/GameObjects/Components/MachineLinking/SignalReceiverComponent.cs @@ -0,0 +1,118 @@ +using Content.Server.GameObjects.Components.Interactable; +using Content.Shared.GameObjects.Components.Interactable; +using Content.Shared.Interfaces; +using Content.Shared.Interfaces.GameObjects.Components; +using Robust.Shared.GameObjects; +using Robust.Shared.Interfaces.GameObjects; +using Robust.Shared.Interfaces.Map; +using Robust.Shared.IoC; +using Robust.Shared.Localization; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Content.Server.GameObjects.Components.MachineLinking +{ + [RegisterComponent] + public class SignalReceiverComponent : Component, IInteractUsing + { + [Dependency] private readonly IMapManager _mapManager = default!; + + public override string Name => "SignalReceiver"; + + private List _transmitters; + + public override void Initialize() + { + base.Initialize(); + + _transmitters = new List(); + } + + public void DistributeSignal(SignalState state) + { + foreach (var comp in Owner.GetAllComponents()) + { + comp.TriggerSignal(state); + } + } + + public void Subscribe(SignalTransmitterComponent transmitter) + { + if (_transmitters.Contains(transmitter)) + { + return; + } + + transmitter.Subscribe(this); + _transmitters.Add(transmitter); + } + + public void Unsubscribe(SignalTransmitterComponent transmitter) + { + transmitter.Unsubscribe(this); + _transmitters.Remove(transmitter); + } + + /// + /// Subscribes/Unsubscribes a transmitter to this component. Returns whether it was successful. + /// + /// + /// + /// + public bool Interact(IEntity user, SignalTransmitterComponent transmitter) + { + if (transmitter == null) + { + user.PopupMessage(user, Loc.GetString("Signal not set.")); + return false; + } + + if (_transmitters.Contains(transmitter)) + { + Unsubscribe(transmitter); + Owner.PopupMessage(user, Loc.GetString("Unlinked.")); + return true; + } + + if (transmitter.Range > 0 && !Owner.Transform.GridPosition.InRange(_mapManager, transmitter.Owner.Transform.GridPosition, transmitter.Range)) + { + Owner.PopupMessage(user, Loc.GetString("Out of range.")); + return false; + } + + Subscribe(transmitter); + Owner.PopupMessage(user, Loc.GetString("Linked!")); + return true; + } + + public async Task InteractUsing(InteractUsingEventArgs eventArgs) + { + if (!eventArgs.Using.TryGetComponent(out var tool)) + return false; + + if (tool.HasQuality(ToolQuality.Multitool) + && eventArgs.Using.TryGetComponent(out var linker)) + { + return Interact(eventArgs.User, linker.Link); + } + + return false; + } + + protected override void Shutdown() + { + base.Shutdown(); + + foreach (var transmitter in _transmitters) + { + if (transmitter.Deleted) + { + continue; + } + + transmitter.Unsubscribe(this); + } + _transmitters.Clear(); + } + } +} diff --git a/Content.Server/GameObjects/Components/MachineLinking/SignalSwitchComponent.cs b/Content.Server/GameObjects/Components/MachineLinking/SignalSwitchComponent.cs new file mode 100644 index 0000000000..6ad3734f3c --- /dev/null +++ b/Content.Server/GameObjects/Components/MachineLinking/SignalSwitchComponent.cs @@ -0,0 +1,87 @@ +using Content.Shared.GameObjects.EntitySystems; +using Content.Shared.GameObjects.Verbs; +using Content.Shared.Interfaces.GameObjects.Components; +using Robust.Server.GameObjects; +using Robust.Shared.GameObjects; +using Robust.Shared.Interfaces.GameObjects; +using Robust.Shared.Localization; +using Robust.Shared.Serialization; + +namespace Content.Server.GameObjects.Components.MachineLinking +{ + [RegisterComponent] + public class SignalSwitchComponent : Component, IInteractHand, IActivate + { + public override string Name => "SignalSwitch"; + + private bool _on; + + public override void Initialize() + { + base.Initialize(); + + UpdateSprite(); + } + + public override void ExposeData(ObjectSerializer serializer) + { + base.ExposeData(serializer); + + serializer.DataField(ref _on, "on", true); + } + + public void Activate(ActivateEventArgs eventArgs) + { + TransmitSignal(eventArgs.User); + } + + public bool InteractHand(InteractHandEventArgs eventArgs) + { + TransmitSignal(eventArgs.User); + return true; + } + + public void TransmitSignal(IEntity user) + { + _on = !_on; + + UpdateSprite(); + + if (!Owner.TryGetComponent(out var transmitter)) + { + return; + } + + transmitter.TransmitSignal(user, _on ? SignalState.On : SignalState.Off); + } + + private void UpdateSprite() + { + if (Owner.TryGetComponent(out var sprite)) + { + sprite.LayerSetState(0, _on ? "on" : "off"); + } + } + + [Verb] + private sealed class ToggleSwitchVerb : Verb + { + protected override void Activate(IEntity user, SignalSwitchComponent component) + { + component.TransmitSignal(user); + } + + protected override void GetData(IEntity user, SignalSwitchComponent component, VerbData data) + { + if (!ActionBlockerSystem.CanInteract(user)) + { + data.Visibility = VerbVisibility.Invisible; + return; + } + + data.Text = Loc.GetString("Toggle Switch"); + data.Visibility = VerbVisibility.Visible; + } + } + } +} diff --git a/Content.Server/GameObjects/Components/MachineLinking/SignalTransmitterComponent.cs b/Content.Server/GameObjects/Components/MachineLinking/SignalTransmitterComponent.cs new file mode 100644 index 0000000000..d2df05bc3d --- /dev/null +++ b/Content.Server/GameObjects/Components/MachineLinking/SignalTransmitterComponent.cs @@ -0,0 +1,166 @@ +using Content.Server.GameObjects.Components.Interactable; +using Content.Shared.GameObjects.Components.Interactable; +using Content.Shared.Interfaces; +using Content.Shared.Interfaces.GameObjects.Components; +using Robust.Shared.GameObjects; +using Robust.Shared.Interfaces.GameObjects; +using Robust.Shared.Interfaces.Map; +using Robust.Shared.IoC; +using Robust.Shared.Localization; +using Robust.Shared.Serialization; +using Robust.Shared.ViewVariables; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Content.Server.GameObjects.Components.MachineLinking +{ + [RegisterComponent] + public class SignalTransmitterComponent : Component, IInteractUsing + { + [Dependency] private readonly IMapManager _mapManager = default!; + [Dependency] private readonly IEntityManager _entityManager = default!; + + public override string Name => "SignalTransmitter"; + + private List _unresolvedReceivers; + private List _receivers; + [ViewVariables] private float _range; + + /// + /// 0 is unlimited range + /// + public float Range { get => _range; private set => _range = value; } + + public override void Initialize() + { + base.Initialize(); + + _receivers = new List(); + + if (_unresolvedReceivers != null) + { + foreach (var receiver in _unresolvedReceivers) + { + receiver.Subscribe(this); + } + _unresolvedReceivers = null; + } + } + + public override void ExposeData(ObjectSerializer serializer) + { + base.ExposeData(serializer); + + serializer.DataField(ref _range, "range", 10); + if (serializer.Reading) + { + if (!serializer.TryReadDataField("signalReceivers", out List entityUids)) + { + return; + } + + _unresolvedReceivers = new List(); + foreach (var entityUid in entityUids) + { + if (!_entityManager.TryGetEntity(entityUid, out var entity) + || !entity.TryGetComponent(out var receiver)) + { + continue; + } + + _unresolvedReceivers.Add(receiver); + } + } + else if (serializer.Writing) + { + var entityList = new List(); + foreach (var receiver in _receivers) + { + if (receiver.Deleted) + { + continue; + } + + entityList.Add(receiver.Owner.Uid); + } + + serializer.DataWriteFunction("signalReceivers", null, () => entityList); + } + } + + public bool TransmitSignal(IEntity user, SignalState state) + { + if (_receivers.Count == 0) + { + Owner.PopupMessage(user, Loc.GetString("No receivers connected.")); + return false; + } + + foreach (var receiver in _receivers) + { + if (Range > 0 && !Owner.Transform.GridPosition.InRange(_mapManager, receiver.Owner.Transform.GridPosition, Range)) + { + continue; + } + + receiver.DistributeSignal(state); + } + return true; + } + + public void Subscribe(SignalReceiverComponent receiver) + { + if (_receivers.Contains(receiver)) + { + return; + } + + _receivers.Add(receiver); + } + + public void Unsubscribe(SignalReceiverComponent receiver) + { + _receivers.Remove(receiver); + } + + public SignalTransmitterComponent GetSignal(IEntity user) + { + if (user != null) + { + Owner.PopupMessage(user, Loc.GetString("Signal fetched.")); + } + + return this; + } + + public async Task InteractUsing(InteractUsingEventArgs eventArgs) + { + if (!eventArgs.Using.TryGetComponent(out var tool)) + return false; + + if (tool.HasQuality(ToolQuality.Multitool) + && eventArgs.Using.TryGetComponent(out var linker)) + { + linker.Link = GetSignal(eventArgs.User); + } + + return false; + } + + protected override void Shutdown() + { + base.Shutdown(); + + foreach (var receiver in _receivers) + { + if (receiver.Deleted) + { + continue; + } + + receiver.Unsubscribe(this); + } + _receivers.Clear(); + } + } +} diff --git a/Content.Server/GameObjects/Components/Power/ApcNetComponents/PowerReceiverUsers/PoweredLightComponent.cs b/Content.Server/GameObjects/Components/Power/ApcNetComponents/PowerReceiverUsers/PoweredLightComponent.cs index 71befe71bc..0b8890964b 100644 --- a/Content.Server/GameObjects/Components/Power/ApcNetComponents/PowerReceiverUsers/PoweredLightComponent.cs +++ b/Content.Server/GameObjects/Components/Power/ApcNetComponents/PowerReceiverUsers/PoweredLightComponent.cs @@ -21,6 +21,8 @@ using Robust.Shared.IoC; using Robust.Shared.Localization; using Robust.Shared.Serialization; using Robust.Shared.ViewVariables; +using Content.Server.GameObjects.Components.MachineLinking; +using Content.Shared.Interfaces; namespace Content.Server.GameObjects.Components.Power.ApcNetComponents.PowerReceiverUsers { @@ -28,21 +30,17 @@ namespace Content.Server.GameObjects.Components.Power.ApcNetComponents.PowerRece /// Component that represents a wall light. It has a light bulb that can be replaced when broken. /// [RegisterComponent] - public class PoweredLightComponent : Component, IInteractHand, IInteractUsing, IMapInit + public class PoweredLightComponent : Component, IInteractHand, IInteractUsing, IMapInit, ISignalReceiver { - [Dependency] private IServerNotifyManager _notifyManager = default!; - public override string Name => "PoweredLight"; private static readonly TimeSpan _thunkDelay = TimeSpan.FromSeconds(2); - private TimeSpan _lastThunk; + [ViewVariables] private bool _on; private LightBulbType BulbType = LightBulbType.Tube; - [ViewVariables] private ContainerSlot _lightBulbContainer; - [ViewVariables] private LightBulbComponent LightBulb { @@ -78,6 +76,23 @@ namespace Content.Server.GameObjects.Components.Power.ApcNetComponents.PowerRece return InsertBulb(eventArgs.Using); } + public void TriggerSignal(SignalState state) + { + switch (state) + { + case SignalState.On: + _on = true; + break; + case SignalState.Off: + _on = false; + break; + case SignalState.Toggle: + _on = !_on; + break; + } + UpdateLight(); + } + public bool InteractHand(InteractHandEventArgs eventArgs) { if (!eventArgs.User.TryGetComponent(out IDamageableComponent damageableComponent)) @@ -103,7 +118,7 @@ namespace Content.Server.GameObjects.Components.Power.ApcNetComponents.PowerRece void Burn() { - _notifyManager.PopupMessage(Owner, eventArgs.User, Loc.GetString("You burn your hand!")); + Owner.PopupMessage(eventArgs.User, Loc.GetString("You burn your hand!")); damageableComponent.ChangeDamage(DamageType.Heat, 20, false, Owner); var audioSystem = EntitySystem.Get(); audioSystem.PlayFromEntity("/Audio/Effects/lightburn.ogg", Owner); @@ -158,6 +173,7 @@ namespace Content.Server.GameObjects.Components.Power.ApcNetComponents.PowerRece public override void ExposeData(ObjectSerializer serializer) { serializer.DataField(ref BulbType, "bulb", LightBulbType.Tube); + serializer.DataField(ref _on, "on", true); } /// @@ -189,9 +205,9 @@ namespace Content.Server.GameObjects.Components.Power.ApcNetComponents.PowerRece switch (LightBulb.State) { case LightBulbState.Normal: - powerReceiver.Load = LightBulb.PowerUse; - if (powerReceiver.Powered) + if (powerReceiver.Powered && _on) { + powerReceiver.Load = LightBulb.PowerUse; sprite.LayerSetState(0, "on"); light.Enabled = true; light.Color = LightBulb.Color; @@ -204,6 +220,7 @@ namespace Content.Server.GameObjects.Components.Power.ApcNetComponents.PowerRece } else { + powerReceiver.Load = 0; sprite.LayerSetState(0, "off"); light.Enabled = false; } diff --git a/Content.Server/GameObjects/EntitySystems/SignalLinkerSystem.cs b/Content.Server/GameObjects/EntitySystems/SignalLinkerSystem.cs new file mode 100644 index 0000000000..04e7fee928 --- /dev/null +++ b/Content.Server/GameObjects/EntitySystems/SignalLinkerSystem.cs @@ -0,0 +1,140 @@ +using Content.Server.GameObjects.Components.MachineLinking; +using Robust.Server.Interfaces.Console; +using Robust.Server.Interfaces.Player; +using Robust.Shared.GameObjects; +using Robust.Shared.GameObjects.Systems; +using Robust.Shared.Input; +using Robust.Shared.Input.Binding; +using Robust.Shared.Interfaces.GameObjects; +using Robust.Shared.IoC; +using Robust.Shared.Map; +using Robust.Shared.Network; +using Robust.Shared.Players; +using System.Collections.Generic; + +namespace Content.Server.GameObjects.EntitySystems +{ + public class SignalLinkerSystem : EntitySystem + { + private Dictionary _transmitters; + + public override void Initialize() + { + base.Initialize(); + + _transmitters = new Dictionary(); + } + + public void SignalLinkerKeybind(NetSessionId id, bool? enable) + { + if (enable == null) + { + enable = !_transmitters.ContainsKey(id); + } + + if (enable == true) + { + if (_transmitters.ContainsKey(id)) + { + return; + } + + if (_transmitters.Count == 0) + { + CommandBinds.Builder + .Bind(EngineKeyFunctions.Use, new PointerInputCmdHandler(HandleUse)) + .Register(); + } + + _transmitters.Add(id, null); + + } + else if (enable == false) + { + if (!_transmitters.ContainsKey(id)) + { + return; + } + + _transmitters.Remove(id); + if (_transmitters.Count == 0) + { + CommandBinds.Unregister(); + } + } + } + + private bool HandleUse(ICommonSession session, GridCoordinates coords, EntityUid uid) + { + if (!_transmitters.TryGetValue(session.SessionId, out var signalTransmitter)) + { + return false; + } + + if (!EntityManager.TryGetEntity(uid, out var entity)) + { + return false; + } + + if (entity == null) + { + return false; + } + + if (entity.TryGetComponent(out var signalReceiver)) + { + if (signalReceiver.Interact(session.AttachedEntity, signalTransmitter)) + { + return true; + } + } + + if (entity.TryGetComponent(out var transmitter)) + { + _transmitters[session.SessionId] = transmitter.GetSignal(session.AttachedEntity); + + return true; + } + + return false; + } + + } + + public class SignalLinkerCommand : IClientCommand + { + public string Command => "signallink"; + + public string Description => "Turns on signal linker mode. Click a transmitter to tune that signal and then click on each receiver to tune them to the transmitter signal."; + + public string Help => "signallink (on/off)"; + + public void Execute(IConsoleShell shell, IPlayerSession player, string[] args) + { + bool? enable = null; + if (args.Length > 0) + { + if (args[0] == "on") + enable = true; + else if (args[0] == "off") + enable = false; + else if (bool.TryParse(args[0], out var boolean)) + enable = boolean; + else if (int.TryParse(args[0], out var num)) + { + if (num == 1) + enable = true; + else if (num == 0) + enable = false; + } + } + + if (!IoCManager.Resolve().TryGetEntitySystem(out var system)) + { + return; + } + + system.SignalLinkerKeybind(player.SessionId, enable); + } + } +} diff --git a/Resources/Groups/groups.yml b/Resources/Groups/groups.yml index e7120fce27..85d071a009 100644 --- a/Resources/Groups/groups.yml +++ b/Resources/Groups/groups.yml @@ -101,6 +101,7 @@ - addaccent - readyall - factions + - signallink CanViewVar: true CanAdminPlace: true CanAdminMenu: true @@ -195,6 +196,7 @@ - addaccent - readyall - factions + - signallink CanViewVar: true CanAdminPlace: true CanScript: true diff --git a/Resources/Prototypes/Entities/Constructible/Walls/lighting.yml b/Resources/Prototypes/Entities/Constructible/Walls/lighting.yml index 06ee76322b..04c1f9ad2a 100644 --- a/Resources/Prototypes/Entities/Constructible/Walls/lighting.yml +++ b/Resources/Prototypes/Entities/Constructible/Walls/lighting.yml @@ -22,6 +22,7 @@ energy: 1.2 offset: "0.5, 0" color: "#DCDCC6" + - type: SignalReceiver placement: snap: - Wallmount diff --git a/Resources/Prototypes/Entities/Constructible/Walls/linking.yml b/Resources/Prototypes/Entities/Constructible/Walls/linking.yml new file mode 100644 index 0000000000..066e6745b1 --- /dev/null +++ b/Resources/Prototypes/Entities/Constructible/Walls/linking.yml @@ -0,0 +1,41 @@ +- type: entity + id: SignalSwitch + name: "signal switch" + components: + - type: Clickable + bounds: + all: -0.25,-0.375,0.25,0.375 + - type: InteractionOutline + - type: Collidable + - type: Sprite + sprite: Constructible/Linking/switch.rsi + state: on + - type: Icon + sprite: Constructible/Linking/switch.rsi + state: on + - type: SignalSwitch + - type: SignalTransmitter + placement: + snap: + - Wallmount + +- type: entity + id: SignalButton + name: "signal button" + components: + - type: Clickable + bounds: + all: -0.25,-0.375,0.25,0.375 + - type: InteractionOutline + - type: Collidable + - type: Sprite + sprite: Constructible/Linking/switch.rsi + state: dead + - type: Icon + sprite: Constructible/Linking/switch.rsi + state: dead + - type: SignalButton + - type: SignalTransmitter + placement: + snap: + - Wallmount diff --git a/Resources/Prototypes/Entities/Objects/Tools/tools.yml b/Resources/Prototypes/Entities/Objects/Tools/tools.yml index 51df8c6cac..bb8b312c91 100644 --- a/Resources/Prototypes/Entities/Objects/Tools/tools.yml +++ b/Resources/Prototypes/Entities/Objects/Tools/tools.yml @@ -185,6 +185,7 @@ - type: Tool qualities: - Multitool + - type: SignalLinker - type: entity name: jaws of life diff --git a/Resources/Textures/Constructible/Linking/switch.rsi/dead.png b/Resources/Textures/Constructible/Linking/switch.rsi/dead.png new file mode 100644 index 0000000000..7b6a4689e3 Binary files /dev/null and b/Resources/Textures/Constructible/Linking/switch.rsi/dead.png differ diff --git a/Resources/Textures/Constructible/Linking/switch.rsi/meta.json b/Resources/Textures/Constructible/Linking/switch.rsi/meta.json new file mode 100644 index 0000000000..52e2eb00d2 --- /dev/null +++ b/Resources/Textures/Constructible/Linking/switch.rsi/meta.json @@ -0,0 +1,44 @@ +{ + "version": 1, + "size": { + "x": 32, + "y": 32 + }, + "license": "CC-BY-SA-3.0", + "copyright": "Taken from https://github.com/tgstation/tgstation/commit/2f127f8b95220226cdd5be356723f3b849c53acf", + "states": [ + { + "name": "on", + "select": [], + "flags": {}, + "directions": 1, + "delays": [ + [ + 1 + ] + ] + }, + { + "name": "off", + "select": [], + "flags": {}, + "directions": 1, + "delays": [ + [ + 1 + ] + ] + }, + { + "name": "dead", + "select": [], + "flags": {}, + "directions": 1, + "delays": [ + [ + 1 + ] + ] + } + ] +} diff --git a/Resources/Textures/Constructible/Linking/switch.rsi/off.png b/Resources/Textures/Constructible/Linking/switch.rsi/off.png new file mode 100644 index 0000000000..41c50ed6c2 Binary files /dev/null and b/Resources/Textures/Constructible/Linking/switch.rsi/off.png differ diff --git a/Resources/Textures/Constructible/Linking/switch.rsi/on.png b/Resources/Textures/Constructible/Linking/switch.rsi/on.png new file mode 100644 index 0000000000..54f9cab603 Binary files /dev/null and b/Resources/Textures/Constructible/Linking/switch.rsi/on.png differ