diff --git a/Content.Client/Entry/EntryPoint.cs b/Content.Client/Entry/EntryPoint.cs index 34747ef28f..00d93fa4c7 100644 --- a/Content.Client/Entry/EntryPoint.cs +++ b/Content.Client/Entry/EntryPoint.cs @@ -77,7 +77,6 @@ namespace Content.Client.Entry factory.RegisterClass(); factory.RegisterClass(); factory.RegisterClass(); - factory.RegisterClass(); factory.RegisterClass(); factory.RegisterClass(); factory.RegisterClass(); @@ -105,6 +104,7 @@ namespace Content.Client.Entry prototypes.RegisterIgnore("worldSpell"); prototypes.RegisterIgnore("entitySpell"); prototypes.RegisterIgnore("instantSpell"); + prototypes.RegisterIgnore("wireLayout"); ClientContentIoC.Register(); diff --git a/Content.Client/Entry/IgnoredComponents.cs b/Content.Client/Entry/IgnoredComponents.cs index 3de1604e03..d0220af785 100644 --- a/Content.Client/Entry/IgnoredComponents.cs +++ b/Content.Client/Entry/IgnoredComponents.cs @@ -344,7 +344,8 @@ namespace Content.Client.Entry "EnergySword", "DoorRemote", "InteractionPopup", - "HealthAnalyzer" + "HealthAnalyzer", + "Wires" }; } } diff --git a/Content.Client/Lathe/LatheSystem.cs b/Content.Client/Lathe/LatheSystem.cs index a62d820368..d9e029178f 100644 --- a/Content.Client/Lathe/LatheSystem.cs +++ b/Content.Client/Lathe/LatheSystem.cs @@ -13,12 +13,11 @@ namespace Content.Client.Lathe { if (TryComp(uid, out SpriteComponent? sprite)) { - if (args.Component.TryGetData(PowerDeviceVisuals.Powered, out bool powered) - && sprite.LayerMapTryGet(PowerDeviceVisualLayers.Powered, out var poweredLayer)) - sprite.LayerSetVisible(poweredLayer, powered); - if (args.Component.TryGetData(SharedWiresComponent.WiresVisuals.MaintenancePanelState, out bool panel) + if (args.Component.TryGetData(PowerDeviceVisuals.Powered, out bool powered)) + sprite.LayerSetVisible(PowerDeviceVisualLayers.Powered, powered); + if (args.Component.TryGetData(WiresVisuals.MaintenancePanelState, out bool panel) && sprite.LayerMapTryGet(WiresVisualizer.WiresVisualLayers.MaintenancePanel, out var panelLayer)) - sprite.LayerSetVisible(panelLayer, panel); + sprite.LayerSetVisible(WiresVisualizer.WiresVisualLayers.MaintenancePanel, panel); // Lathe specific stuff if (args.Component.TryGetData(LatheVisuals.IsRunning, out bool isRunning)) { diff --git a/Content.Client/Wires/UI/WiresBoundUserInterface.cs b/Content.Client/Wires/UI/WiresBoundUserInterface.cs index 46412ae339..61744fc232 100644 --- a/Content.Client/Wires/UI/WiresBoundUserInterface.cs +++ b/Content.Client/Wires/UI/WiresBoundUserInterface.cs @@ -1,6 +1,6 @@ -using Robust.Client.GameObjects; +using Content.Shared.Wires; +using Robust.Client.GameObjects; using Robust.Shared.GameObjects; -using static Content.Shared.Wires.SharedWiresComponent; namespace Content.Client.Wires.UI { diff --git a/Content.Client/Wires/UI/WiresMenu.cs b/Content.Client/Wires/UI/WiresMenu.cs index cb5f1646ff..08b84a6f90 100644 --- a/Content.Client/Wires/UI/WiresMenu.cs +++ b/Content.Client/Wires/UI/WiresMenu.cs @@ -14,7 +14,7 @@ using Robust.Shared.Input; using Robust.Shared.IoC; using Robust.Shared.Localization; using Robust.Shared.Maths; -using static Content.Shared.Wires.SharedWiresComponent; +using Robust.Shared.Random; using static Robust.Client.UserInterface.Controls.BoxContainer; namespace Content.Client.Wires.UI @@ -267,7 +267,16 @@ namespace Content.Client.Wires.UI _statusContainer.RemoveAllChildren(); - foreach (var status in state.Statuses) + var originalStatuses = new List(state.Statuses); // TODO: maybe not this way? + var shuffledStatuses = new List(); + for (var i = originalStatuses.Count; i > 0; i--) + { + var index = random.Next(originalStatuses.Count); + shuffledStatuses.Add(originalStatuses[index]); + originalStatuses.RemoveAt(index); + } + + foreach (var status in shuffledStatuses) { if (status.Value is StatusLightData statusLightData) { diff --git a/Content.Client/Wires/Visualizers/WiresVisualizer.cs b/Content.Client/Wires/Visualizers/WiresVisualizer.cs index dd8510ed8f..f306c2aecd 100644 --- a/Content.Client/Wires/Visualizers/WiresVisualizer.cs +++ b/Content.Client/Wires/Visualizers/WiresVisualizer.cs @@ -1,7 +1,7 @@ +using Content.Shared.Wires; using Robust.Client.GameObjects; using Robust.Shared.GameObjects; using Robust.Shared.IoC; -using static Content.Shared.Wires.SharedWiresComponent; namespace Content.Client.Wires.Visualizers { diff --git a/Content.Server/Access/AccessWireAction.cs b/Content.Server/Access/AccessWireAction.cs new file mode 100644 index 0000000000..713b4af550 --- /dev/null +++ b/Content.Server/Access/AccessWireAction.cs @@ -0,0 +1,95 @@ +using Content.Server.Wires; +using Content.Shared.Access; +using Content.Shared.Access.Components; +using Content.Shared.Wires; + +namespace Content.Server.Access; + +[DataDefinition] +public class AccessWireAction : BaseWireAction +{ + [DataField("color")] + private Color _statusColor = Color.Green; + + [DataField("name")] + private string _text = "ACC"; + + [DataField("pulseTimeout")] + private int _pulseTimeout = 30; + + public override StatusLightData? GetStatusLightData(Wire wire) + { + StatusLightState lightState = StatusLightState.Off; + if (IsPowered(wire.Owner) + && EntityManager.TryGetComponent(wire.Owner, out var access)) + { + if (access.Enabled) + { + lightState = StatusLightState.On; + } + } + + return new StatusLightData( + _statusColor, + lightState, + _text); + } + + public override object StatusKey { get; } = AccessWireActionKey.Status; + + public override bool Cut(EntityUid user, Wire wire) + { + if (EntityManager.TryGetComponent(wire.Owner, out var access)) + { + WiresSystem.TryCancelWireAction(wire.Owner, PulseTimeoutKey.Key); + access.Enabled = false; + } + + return true; + } + + public override bool Mend(EntityUid user, Wire wire) + { + if (EntityManager.TryGetComponent(wire.Owner, out var access)) + { + access.Enabled = true; + } + + return true; + } + + public override bool Pulse(EntityUid user, Wire wire) + { + if (EntityManager.TryGetComponent(wire.Owner, out var access)) + { + access.Enabled = false; + WiresSystem.StartWireAction(wire.Owner, _pulseTimeout, PulseTimeoutKey.Key, new TimedWireEvent(AwaitPulseCancel, wire)); + } + + return true; + } + + public override void Update(Wire wire) + { + if (!IsPowered(wire.Owner)) + { + WiresSystem.TryCancelWireAction(wire.Owner, PulseTimeoutKey.Key); + } + } + + private void AwaitPulseCancel(Wire wire) + { + if (!wire.IsCut) + { + if (EntityManager.TryGetComponent(wire.Owner, out var access)) + { + access.Enabled = true; + } + } + } + + private enum PulseTimeoutKey : byte + { + Key + } +} diff --git a/Content.Server/Arcade/Components/SpaceVillainArcadeComponent.cs b/Content.Server/Arcade/Components/SpaceVillainArcadeComponent.cs index a95aece504..703c44fe54 100644 --- a/Content.Server/Arcade/Components/SpaceVillainArcadeComponent.cs +++ b/Content.Server/Arcade/Components/SpaceVillainArcadeComponent.cs @@ -1,7 +1,7 @@ using Content.Server.Power.Components; using Content.Server.UserInterface; using Content.Server.VendingMachines; -using Content.Server.WireHacking; +using Content.Shared.ActionBlocker; using Content.Shared.Arcade; using Content.Shared.Interaction; using Content.Shared.Sound; @@ -16,7 +16,7 @@ using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototy namespace Content.Server.Arcade.Components { [RegisterComponent] - public sealed class SpaceVillainArcadeComponent : SharedSpaceVillainArcadeComponent, IWires + public sealed class SpaceVillainArcadeComponent : SharedSpaceVillainArcadeComponent { [Dependency] private readonly IRobustRandom _random = null!; @@ -24,9 +24,9 @@ namespace Content.Server.Arcade.Components private bool Powered => _entityManager.TryGetComponent(Owner, out var powerReceiverComponent) && powerReceiverComponent.Powered; [ViewVariables] private BoundUserInterface? UserInterface => Owner.GetUIOrNull(SpaceVillainArcadeUiKey.Key); - [ViewVariables] private bool _overflowFlag; - [ViewVariables] private bool _playerInvincibilityFlag; - [ViewVariables] private bool _enemyInvincibilityFlag; + [ViewVariables] public bool OverflowFlag; + [ViewVariables] public bool PlayerInvincibilityFlag; + [ViewVariables] public bool EnemyInvincibilityFlag; [ViewVariables] public SpaceVillainGame Game = null!; [DataField("newGameSound")] private SoundSpecifier _newGameSound = new SoundPathSpecifier("/Audio/Effects/Arcade/newgame.ogg"); @@ -62,7 +62,6 @@ namespace Content.Server.Arcade.Components "ToyPhazon", "ToyFireRipley", "ToyReticence", "ToyRipley", "ToySeraph", "ToyDurand", "ToySkeleton" }; - protected override void Initialize() { base.Initialize(); @@ -110,71 +109,6 @@ namespace Content.Server.Arcade.Components } } - public enum Wires - { - /// - /// Disables Max Health&Mana for both Enemy and Player. - /// - Overflow, - /// - /// Makes Player Invincible. - /// - PlayerInvincible, - /// - /// Makes Enemy Invincible. - /// - EnemyInvincible - } - - public void RegisterWires(WiresComponent.WiresBuilder builder) - { - builder.CreateWire(Wires.Overflow); - builder.CreateWire(Wires.PlayerInvincible); - builder.CreateWire(Wires.EnemyInvincible); - builder.CreateWire(4); - builder.CreateWire(5); - builder.CreateWire(6); - IndicatorUpdate(); - } - - public void WiresUpdate(WiresUpdateEventArgs args) - { - var wire = (Wires) args.Identifier; - var value = args.Action != SharedWiresComponent.WiresAction.Mend; - switch (wire) - { - case Wires.Overflow: - _overflowFlag = value; - break; - case Wires.PlayerInvincible: - _playerInvincibilityFlag = value; - break; - case Wires.EnemyInvincible: - _enemyInvincibilityFlag = value; - break; - } - - IndicatorUpdate(); - } - - public void IndicatorUpdate() - { - if (!_entityManager.TryGetComponent(Owner, out var wiresComponent)) return; - - wiresComponent.SetStatus(Indicators.HealthManager, - new SharedWiresComponent.StatusLightData(Color.Purple, - _playerInvincibilityFlag || _enemyInvincibilityFlag - ? SharedWiresComponent.StatusLightState.BlinkingSlow - : SharedWiresComponent.StatusLightState.On, - "MNGR")); - wiresComponent.SetStatus(Indicators.HealthLimiter, - new SharedWiresComponent.StatusLightData(Color.Red, - _overflowFlag - ? SharedWiresComponent.StatusLightState.BlinkingSlow - : SharedWiresComponent.StatusLightState.On, - "LIMT")); - } - /// /// Called when the user wins the game. /// @@ -247,7 +181,7 @@ namespace Content.Server.Arcade.Components /// private void ValidateVars() { - if (_owner._overflowFlag) return; + if (_owner.OverflowFlag) return; if (_playerHp > _playerHpMax) _playerHp = _playerHpMax; if (_playerMp > _playerMpMax) _playerMp = _playerMpMax; @@ -271,7 +205,7 @@ namespace Content.Server.Arcade.Components ("enemyName", _enemyName), ("attackAmount", attackAmount)); SoundSystem.Play(Filter.Pvs(_owner.Owner), _owner._playerAttackSound.GetSound(), _owner.Owner, AudioParams.Default.WithVolume(-4f)); - if (!_owner._enemyInvincibilityFlag) + if (!_owner.EnemyInvincibilityFlag) _enemyHp -= attackAmount; _turtleTracker -= _turtleTracker > 0 ? 1 : 0; break; @@ -282,7 +216,7 @@ namespace Content.Server.Arcade.Components ("magicPointAmount", pointAmount), ("healAmount", healAmount)); SoundSystem.Play(Filter.Pvs(_owner.Owner), _owner._playerHealSound.GetSound(), _owner.Owner, AudioParams.Default.WithVolume(-4f)); - if (!_owner._playerInvincibilityFlag) + if (!_owner.PlayerInvincibilityFlag) _playerMp -= pointAmount; _playerHp += healAmount; _turtleTracker++; @@ -380,7 +314,7 @@ namespace Content.Server.Arcade.Components _latestEnemyActionMessage = Loc.GetString("space-villain-game-enemy-throws-bomb-message", ("enemyName", _enemyName), ("damageReceived", boomAmount)); - if (_owner._playerInvincibilityFlag) return; + if (_owner.PlayerInvincibilityFlag) return; _playerHp -= boomAmount; _turtleTracker--; } @@ -390,7 +324,7 @@ namespace Content.Server.Arcade.Components _latestEnemyActionMessage = Loc.GetString("space-villain-game-enemy-steals-player-power-message", ("enemyName", _enemyName), ("stolenAmount", stealAmount)); - if (_owner._playerInvincibilityFlag) return; + if (_owner.PlayerInvincibilityFlag) return; _playerMp -= stealAmount; _enemyMp += stealAmount; } @@ -409,7 +343,7 @@ namespace Content.Server.Arcade.Components Loc.GetString("space-villain-game-enemy-attacks-message", ("enemyName", _enemyName), ("damageDealt", attackAmount)); - if (_owner._playerInvincibilityFlag) return; + if (_owner.PlayerInvincibilityFlag) return; _playerHp -= attackAmount; } } diff --git a/Content.Server/Arcade/WireActions/ArcadeInvincibilityWireActions.cs b/Content.Server/Arcade/WireActions/ArcadeInvincibilityWireActions.cs new file mode 100644 index 0000000000..7b783e5655 --- /dev/null +++ b/Content.Server/Arcade/WireActions/ArcadeInvincibilityWireActions.cs @@ -0,0 +1,74 @@ +using Content.Server.Arcade.Components; +using Content.Server.Wires; +using Content.Shared.Arcade; +using Content.Shared.Wires; + +namespace Content.Server.Arcade; + +[DataDefinition] +public sealed class ArcadePlayerInvincibleWireAction : BaseToggleWireAction +{ + private string _text = "MNGR"; + private Color _color = Color.Purple; + + public override object? StatusKey { get; } = SharedSpaceVillainArcadeComponent.Indicators.HealthManager; + + public override void ToggleValue(EntityUid owner, bool setting) + { + if (EntityManager.TryGetComponent(owner, out var arcade)) + { + arcade.PlayerInvincibilityFlag = !setting; + } + } + + public override bool GetValue(EntityUid owner) + { + return EntityManager.TryGetComponent(owner, out var arcade) + && !arcade.PlayerInvincibilityFlag; + } + + public override StatusLightData? GetStatusLightData(Wire wire) + { + var lightState = StatusLightState.Off; + + if (IsPowered(wire.Owner) && EntityManager.TryGetComponent(wire.Owner, out var arcade)) + { + lightState = arcade.PlayerInvincibilityFlag || arcade.EnemyInvincibilityFlag + ? StatusLightState.BlinkingSlow + : StatusLightState.On; + } + + return new StatusLightData( + _color, + lightState, + _text); + } +} + +[DataDefinition] +public sealed class ArcadeEnemyInvincibleWireAction : BaseToggleWireAction +{ + public override object? StatusKey { get; } = null; + + public override void ToggleValue(EntityUid owner, bool setting) + { + if (EntityManager.TryGetComponent(owner, out var arcade)) + { + arcade.PlayerInvincibilityFlag = !setting; + } + } + + public override bool GetValue(EntityUid owner) + { + return EntityManager.TryGetComponent(owner, out var arcade) + && !arcade.PlayerInvincibilityFlag; + } + + public override StatusLightData? GetStatusLightData(Wire wire) => null; +} + +public enum ArcadeInvincibilityWireActionKeys : short +{ + Player, + Enemy +} diff --git a/Content.Server/Arcade/WireActions/ArcadeOverflowWireAction.cs b/Content.Server/Arcade/WireActions/ArcadeOverflowWireAction.cs new file mode 100644 index 0000000000..0a42d67eb6 --- /dev/null +++ b/Content.Server/Arcade/WireActions/ArcadeOverflowWireAction.cs @@ -0,0 +1,46 @@ +using Content.Server.Arcade.Components; +using Content.Server.Wires; +using Content.Shared.Arcade; +using Content.Shared.Wires; + +namespace Content.Server.Arcade; + +[DataDefinition] +public sealed class ArcadeOverflowWireAction : BaseToggleWireAction +{ + private Color _color = Color.Red; + private string _text = "LMTR"; + + public override object? StatusKey { get; } = SharedSpaceVillainArcadeComponent.Indicators.HealthLimiter; + + public override void ToggleValue(EntityUid owner, bool setting) + { + if (EntityManager.TryGetComponent(owner, out var arcade)) + { + arcade.OverflowFlag = !setting; + } + } + + public override bool GetValue(EntityUid owner) + { + return EntityManager.TryGetComponent(owner, out var arcade) + && !arcade.OverflowFlag; + } + + public override StatusLightData? GetStatusLightData(Wire wire) + { + var lightState = StatusLightState.Off; + + if (IsPowered(wire.Owner) && EntityManager.HasComponent(wire.Owner)) + { + lightState = !GetValue(wire.Owner) + ? StatusLightState.BlinkingSlow + : StatusLightState.On; + } + + return new StatusLightData( + _color, + lightState, + _text); + } +} diff --git a/Content.Server/Atmos/Monitor/Components/AirAlarmComponent.cs b/Content.Server/Atmos/Monitor/Components/AirAlarmComponent.cs index e7a4ec9d03..cf649976c9 100644 --- a/Content.Server/Atmos/Monitor/Components/AirAlarmComponent.cs +++ b/Content.Server/Atmos/Monitor/Components/AirAlarmComponent.cs @@ -2,23 +2,17 @@ using System; using System.Threading; using System.Collections.Generic; using Content.Server.Atmos.Monitor.Systems; -using Content.Server.DeviceNetwork.Components; -using Content.Server.Power.Components; -using Content.Server.VendingMachines; // TODO: Move this out of vending machines??? -using Content.Server.WireHacking; using Content.Shared.Atmos.Monitor.Components; using Robust.Shared.GameObjects; using Robust.Shared.IoC; using Robust.Shared.Maths; using Robust.Shared.Network; using Robust.Shared.ViewVariables; -using static Content.Shared.Wires.SharedWiresComponent; -using static Content.Shared.Wires.SharedWiresComponent.WiresAction; namespace Content.Server.Atmos.Monitor.Components { [RegisterComponent] - public sealed class AirAlarmComponent : Component, IWires + public sealed class AirAlarmComponent : Component { [Dependency] private readonly IEntityManager _entMan = default!; @@ -33,188 +27,6 @@ namespace Content.Server.Atmos.Monitor.Components public HashSet ActivePlayers = new(); - public bool FullAccess = false; public bool CanSync = true; - - // <-- Wires --> - - private CancellationTokenSource _powerPulsedCancel = new(); - private int PowerPulsedTimeout = 30; - - private enum Wires - { - // Cutting this kills power. - // Pulsing it disrupts power. - Power, - // Cutting this allows full access. - // Pulsing this does nothing. - Access, - // Cutting/Remending this resets ONLY from panic mode. - // Pulsing this sets panic mode. - Panic, - // Cutting this clears sync'd devices, and makes - // the alarm unable to resync. - // Pulsing this resyncs all devices (ofc current - // implementation just auto-does this anyways) - DeviceSync, - // This does nothing. (placeholder for AI wire, - // if that ever gets implemented) - Dummy - } - - public void RegisterWires(WiresComponent.WiresBuilder builder) - { - foreach (var wire in Enum.GetValues()) - builder.CreateWire(wire); - - UpdateWires(); - } - - public void UpdateWires() - { - if (_airAlarmSystem == null) - _airAlarmSystem = EntitySystem.Get(); - - if (!_entMan.TryGetComponent(Owner, out var wires)) return; - - var pwrLightState = (PowerPulsed, PowerCut) switch { - (true, false) => StatusLightState.BlinkingFast, - (_, true) => StatusLightState.Off, - (_, _) => StatusLightState.On - }; - - var powerLight = new StatusLightData(Color.Yellow, pwrLightState, "POWR"); - - var accessLight = new StatusLightData( - Color.Green, - wires.IsWireCut(Wires.Access) ? StatusLightState.Off : StatusLightState.On, - "ACC" - ); - - var panicLight = new StatusLightData( - Color.Red, - CurrentMode == AirAlarmMode.Panic ? StatusLightState.On : StatusLightState.Off, - "PAN" - ); - - var syncLightState = StatusLightState.BlinkingSlow; - - if (_entMan.TryGetComponent(Owner, out var atmosMonitorComponent) && !atmosMonitorComponent.NetEnabled) - syncLightState = StatusLightState.Off; - else if (DeviceData.Count != 0) - syncLightState = StatusLightState.On; - - var syncLight = new StatusLightData(Color.Orange, syncLightState, "NET"); - - wires.SetStatus(AirAlarmWireStatus.Power, powerLight); - wires.SetStatus(AirAlarmWireStatus.Access, accessLight); - wires.SetStatus(AirAlarmWireStatus.Panic, panicLight); - wires.SetStatus(AirAlarmWireStatus.DeviceSync, syncLight); - } - - private bool _powerCut; - private bool PowerCut - { - get => _powerCut; - set - { - _powerCut = value; - SetPower(); - } - } - - private bool _powerPulsed; - private bool PowerPulsed - { - get => _powerPulsed && !_powerCut; - set - { - _powerPulsed = value; - SetPower(); - } - } - - private void SetPower() - { - if (_entMan.TryGetComponent(Owner, out var receiverComponent) - && _entMan.HasComponent(Owner)) - receiverComponent.PowerDisabled = PowerPulsed || PowerCut; - } - - public void WiresUpdate(WiresUpdateEventArgs args) - { - if (!_entMan.TryGetComponent(Owner, out var deviceNetworkComponent)) return; - - if (_airAlarmSystem == null) - _airAlarmSystem = EntitySystem.Get(); - - switch (args.Action) - { - case Pulse: - switch (args.Identifier) - { - case Wires.Power: - PowerPulsed = true; - _powerPulsedCancel.Cancel(); - _powerPulsedCancel = new CancellationTokenSource(); - Owner.SpawnTimer(TimeSpan.FromSeconds(PowerPulsedTimeout), - () => PowerPulsed = false, - _powerPulsedCancel.Token); - break; - case Wires.Panic: - if (CurrentMode != AirAlarmMode.Panic) - _airAlarmSystem.SetMode(Owner, deviceNetworkComponent.Address, AirAlarmMode.Panic, true, false); - break; - case Wires.DeviceSync: - _airAlarmSystem.SyncAllDevices(Owner); - break; - } - break; - case Mend: - switch (args.Identifier) - { - case Wires.Power: - _powerPulsedCancel.Cancel(); - PowerPulsed = false; - PowerCut = false; - break; - case Wires.Panic: - if (CurrentMode == AirAlarmMode.Panic) - _airAlarmSystem.SetMode(Owner, deviceNetworkComponent.Address, AirAlarmMode.Filtering, true, false); - break; - case Wires.Access: - FullAccess = false; - break; - case Wires.DeviceSync: - if (_entMan.TryGetComponent(Owner, out var atmosMonitorComponent)) - atmosMonitorComponent.NetEnabled = true; - - break; - } - break; - case Cut: - switch (args.Identifier) - { - case Wires.DeviceSync: - DeviceData.Clear(); - if (_entMan.TryGetComponent(Owner, out var atmosMonitorComponent)) - { - atmosMonitorComponent.NetworkAlarmStates.Clear(); - atmosMonitorComponent.NetEnabled = false; - } - - break; - case Wires.Power: - PowerCut = true; - break; - case Wires.Access: - FullAccess = true; - break; - } - break; - } - - UpdateWires(); - } } } diff --git a/Content.Server/Atmos/Monitor/Components/FireAlarmComponent.cs b/Content.Server/Atmos/Monitor/Components/FireAlarmComponent.cs index 1c2e541044..7f9c072e7d 100644 --- a/Content.Server/Atmos/Monitor/Components/FireAlarmComponent.cs +++ b/Content.Server/Atmos/Monitor/Components/FireAlarmComponent.cs @@ -1,165 +1,7 @@ -using System; -using System.Threading; -using Content.Server.Atmos.Monitor.Systems; -using Content.Server.Power.Components; -using Content.Server.VendingMachines; // TODO: Move this out of vending machines??? -using Content.Server.WireHacking; -using Content.Shared.Interaction; -using Content.Shared.Atmos.Monitor; -using Content.Shared.Atmos.Monitor.Components; -using Robust.Server.GameObjects; -using Robust.Shared.GameObjects; -using Robust.Shared.IoC; -using Robust.Shared.Log; -using Robust.Shared.Maths; -using static Content.Shared.Wires.SharedWiresComponent; -using static Content.Shared.Wires.SharedWiresComponent.WiresAction; - - namespace Content.Server.Atmos.Monitor.Components { [RegisterComponent] - public sealed class FireAlarmComponent : Component, IWires + public sealed class FireAlarmComponent : Component { - [Dependency] private readonly IEntityManager _entMan = default!; - - private AtmosMonitorSystem? _atmosMonitorSystem; - private CancellationTokenSource _powerPulsedCancel = new(); - private int PowerPulsedTimeout = 30; - - // Much more simpler than the air alarm wire set. - private enum Wires - { - // Cutting this kills power, - // pulsing it disrupts. - Power, - // Cutting this disables network - // connectivity, - // pulsing it sets off an alarm. - Alarm, - Dummy1, - Dummy2, - } - - private bool _powerCut; - private bool PowerCut - { - get => _powerCut; - set - { - _powerCut = value; - SetPower(); - } - } - - private bool _powerPulsed; - private bool PowerPulsed - { - get => _powerPulsed && !_powerCut; - set - { - _powerPulsed = value; - SetPower(); - } - } - - private void SetPower() - { - if (_entMan.TryGetComponent(Owner, out var receiverComponent) && _entMan.HasComponent(Owner)) - receiverComponent.PowerDisabled = PowerPulsed || PowerCut; - } - - - public void RegisterWires(WiresComponent.WiresBuilder builder) - { - builder.CreateWire(Wires.Power); - builder.CreateWire(Wires.Alarm); - builder.CreateWire(Wires.Dummy1); - builder.CreateWire(Wires.Dummy2); - - UpdateWires(); - } - - public void UpdateWires() - { - if (!_entMan.TryGetComponent(Owner, out var wiresComponent)) return; - - var powerLight = new StatusLightData(Color.Yellow, StatusLightState.On, "POWR"); - - if (PowerPulsed) - powerLight = new StatusLightData(Color.Yellow, StatusLightState.BlinkingFast, "POWR"); - else if (PowerCut) - powerLight = new StatusLightData(Color.Yellow, StatusLightState.Off, "POWR"); - - var syncLight = new StatusLightData(Color.Orange, StatusLightState.On, "NET"); - - if (_entMan.TryGetComponent(Owner, out var atmosMonitorComponent)) - if (!atmosMonitorComponent.NetEnabled) - syncLight = new StatusLightData(Color.Orange, StatusLightState.Off, "NET"); - else if (atmosMonitorComponent.HighestAlarmInNetwork == AtmosMonitorAlarmType.Danger) - syncLight = new StatusLightData(Color.Orange, StatusLightState.BlinkingFast, "NET"); - - wiresComponent.SetStatus(FireAlarmWireStatus.Power, powerLight); - wiresComponent.SetStatus(FireAlarmWireStatus.Alarm, syncLight); - } - - public void WiresUpdate(WiresUpdateEventArgs args) - { - if (_atmosMonitorSystem == null) - _atmosMonitorSystem = EntitySystem.Get(); - - switch (args.Action) - { - case Pulse: - switch (args.Identifier) - { - case Wires.Power: - PowerPulsed = true; - _powerPulsedCancel.Cancel(); - _powerPulsedCancel = new CancellationTokenSource(); - Owner.SpawnTimer(TimeSpan.FromSeconds(PowerPulsedTimeout), - () => PowerPulsed = false, - _powerPulsedCancel.Token); - break; - case Wires.Alarm: - if (_entMan.TryGetComponent(Owner, out var atmosMonitorComponent)) - _atmosMonitorSystem.Alert(Owner, AtmosMonitorAlarmType.Danger, monitor: atmosMonitorComponent); - break; - } - - break; - case Mend: - switch (args.Identifier) - { - case Wires.Power: - _powerPulsedCancel.Cancel(); - PowerPulsed = false; - PowerCut = false; - break; - case Wires.Alarm: - if (_entMan.TryGetComponent(Owner, out var atmosMonitorComponent)) - atmosMonitorComponent.NetEnabled = true; - break; - } - - break; - case Cut: - switch (args.Identifier) - { - case Wires.Power: - PowerCut = true; - break; - case Wires.Alarm: - if (_entMan.TryGetComponent(Owner, out var atmosMonitorComponent)) - atmosMonitorComponent.NetEnabled = false; - break; - - } - break; - - } - - UpdateWires(); - } } } diff --git a/Content.Server/Atmos/Monitor/Systems/AirAlarmSystem.cs b/Content.Server/Atmos/Monitor/Systems/AirAlarmSystem.cs index c49ddf573c..504582022a 100644 --- a/Content.Server/Atmos/Monitor/Systems/AirAlarmSystem.cs +++ b/Content.Server/Atmos/Monitor/Systems/AirAlarmSystem.cs @@ -7,7 +7,7 @@ using Content.Server.DeviceNetwork.Components; using Content.Server.DeviceNetwork.Systems; using Content.Server.Popups; using Content.Server.Power.Components; -using Content.Server.WireHacking; +using Content.Server.Wires; using Content.Shared.Access.Components; using Content.Shared.Access.Systems; using Content.Shared.Atmos; @@ -263,7 +263,7 @@ namespace Content.Server.Atmos.Monitor.Systems if (!EntityManager.TryGetComponent(uid, out AccessReaderComponent reader) || user == null) return false; - if (!_accessSystem.IsAllowed(reader, user.Value) && !component.FullAccess) + if (!_accessSystem.IsAllowed(reader, user.Value)) { _popup.PopupEntity(Loc.GetString("air-alarm-ui-access-denied"), user.Value, Filter.Entities(user.Value)); return false; @@ -401,7 +401,7 @@ namespace Content.Server.Atmos.Monitor.Systems // _airAlarmDataSystem.UpdateDeviceData(uid, args.SenderAddress, data); // _uiSystem.TrySendUiMessage(uid, SharedAirAlarmInterfaceKey.Key, new AirAlarmUpdateDeviceDataMessage(args.SenderAddress, data)); - if (HasComp(uid)) controller.UpdateWires(); + // if (HasComp(uid)) controller.UpdateWires(); if (!controller.DeviceData.TryAdd(args.SenderAddress, data)) controller.DeviceData[args.SenderAddress] = data; diff --git a/Content.Server/Atmos/Monitor/WireActions/AirAlarmPanicWire.cs b/Content.Server/Atmos/Monitor/WireActions/AirAlarmPanicWire.cs new file mode 100644 index 0000000000..6394e7216a --- /dev/null +++ b/Content.Server/Atmos/Monitor/WireActions/AirAlarmPanicWire.cs @@ -0,0 +1,75 @@ +using Content.Server.Atmos.Monitor.Components; +using Content.Server.Atmos.Monitor.Systems; +using Content.Server.DeviceNetwork.Components; +using Content.Server.Wires; +using Content.Shared.Atmos.Monitor.Components; +using Content.Shared.Wires; + +namespace Content.Server.Atmos.Monitor; + +[DataDefinition] +public sealed class AirAlarmPanicWire : BaseWireAction +{ + private string _text = "PANC"; + private Color _color = Color.Red; + + private AirAlarmSystem _airAlarmSystem = default!; + + public override object StatusKey { get; } = AirAlarmWireStatus.Panic; + + public override StatusLightData? GetStatusLightData(Wire wire) + { + var lightState = StatusLightState.Off; + if (IsPowered(wire.Owner) && EntityManager.TryGetComponent(wire.Owner, out var alarm)) + { + lightState = alarm.CurrentMode == AirAlarmMode.Panic + ? StatusLightState.On + : StatusLightState.Off; + } + + return new StatusLightData( + _color, + lightState, + _text); + } + + public override void Initialize() + { + base.Initialize(); + + _airAlarmSystem = EntitySystem.Get(); + } + + public override bool Cut(EntityUid user, Wire wire) + { + if (EntityManager.TryGetComponent(wire.Owner, out var devNet)) + { + _airAlarmSystem.SetMode(wire.Owner, devNet.Address, AirAlarmMode.Panic, true, false); + } + + return true; + } + + public override bool Mend(EntityUid user, Wire wire) + { + if (EntityManager.TryGetComponent(wire.Owner, out var devNet) + && EntityManager.TryGetComponent(wire.Owner, out var alarm) + && alarm.CurrentMode == AirAlarmMode.Panic) + { + _airAlarmSystem.SetMode(wire.Owner, devNet.Address, AirAlarmMode.Filtering, true, false, alarm); + } + + + return true; + } + + public override bool Pulse(EntityUid user, Wire wire) + { + if (EntityManager.TryGetComponent(wire.Owner, out var devNet)) + { + _airAlarmSystem.SetMode(wire.Owner, devNet.Address, AirAlarmMode.Panic, true, false); + } + + return true; + } +} diff --git a/Content.Server/Atmos/Monitor/WireActions/AtmosMonitorAlarmWire.cs b/Content.Server/Atmos/Monitor/WireActions/AtmosMonitorAlarmWire.cs new file mode 100644 index 0000000000..7fde988084 --- /dev/null +++ b/Content.Server/Atmos/Monitor/WireActions/AtmosMonitorAlarmWire.cs @@ -0,0 +1,77 @@ +using Content.Server.Atmos.Monitor.Components; +using Content.Server.Atmos.Monitor.Systems; +using Content.Server.Wires; +using Content.Shared.Atmos.Monitor; +using Content.Shared.Wires; + +namespace Content.Server.Atmos.Monitor; + +[DataDefinition] +public sealed class AtmosMonitorDeviceNetWire : BaseWireAction +{ + // whether or not this wire will send out an alarm upon + // being pulsed + [DataField("alarmOnPulse")] + private bool _alarmOnPulse = false; + + private string _text = "NETW"; + private Color _color = Color.Orange; + + private AtmosMonitorSystem _atmosMonitorSystem = default!; + + public override object StatusKey { get; } = AtmosMonitorAlarmWireActionKeys.Network; + + public override StatusLightData? GetStatusLightData(Wire wire) + { + var lightState = StatusLightState.Off; + + if (IsPowered(wire.Owner) && EntityManager.TryGetComponent(wire.Owner, out var monitor)) + { + lightState = monitor.HighestAlarmInNetwork == AtmosMonitorAlarmType.Danger + ? StatusLightState.BlinkingFast + : StatusLightState.On; + } + + return new StatusLightData( + _color, + lightState, + _text); + } + + public override void Initialize() + { + base.Initialize(); + + _atmosMonitorSystem = EntitySystem.Get(); + } + + public override bool Cut(EntityUid user, Wire wire) + { + if (EntityManager.TryGetComponent(wire.Owner, out var monitor)) + { + monitor.NetEnabled = false; + } + + return true; + } + + public override bool Mend(EntityUid user, Wire wire) + { + if (EntityManager.TryGetComponent(wire.Owner, out var monitor)) + { + monitor.NetEnabled = true; + } + + return true; + } + + public override bool Pulse(EntityUid user, Wire wire) + { + if (_alarmOnPulse) + { + _atmosMonitorSystem.Alert(wire.Owner, AtmosMonitorAlarmType.Danger); + } + + return true; + } +} diff --git a/Content.Server/Construction/Conditions/AllWiresCut.cs b/Content.Server/Construction/Conditions/AllWiresCut.cs index e49e7677cc..0c1aed6b83 100644 --- a/Content.Server/Construction/Conditions/AllWiresCut.cs +++ b/Content.Server/Construction/Conditions/AllWiresCut.cs @@ -1,5 +1,5 @@ using System.Collections.Generic; -using Content.Server.WireHacking; +using Content.Server.Wires; using Content.Shared.Construction; using Content.Shared.Examine; using JetBrains.Annotations; diff --git a/Content.Server/Construction/Conditions/WirePanel.cs b/Content.Server/Construction/Conditions/WirePanel.cs index 05fa1d5e23..dba0c79d10 100644 --- a/Content.Server/Construction/Conditions/WirePanel.cs +++ b/Content.Server/Construction/Conditions/WirePanel.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; using System.Threading.Tasks; -using Content.Server.WireHacking; +using Content.Server.Wires; using Content.Shared.Construction; using Content.Shared.Examine; using JetBrains.Annotations; diff --git a/Content.Server/Doors/Components/AirlockComponent.cs b/Content.Server/Doors/Components/AirlockComponent.cs index 4efbe31fe8..5b058f60ba 100644 --- a/Content.Server/Doors/Components/AirlockComponent.cs +++ b/Content.Server/Doors/Components/AirlockComponent.cs @@ -3,7 +3,7 @@ using System.Threading; using Content.Server.Doors.Systems; using Content.Server.Power.Components; using Content.Server.VendingMachines; -using Content.Server.WireHacking; +// using Content.Server.WireHacking; using Content.Shared.Doors; using Content.Shared.Doors.Components; using Content.Shared.Sound; @@ -14,8 +14,8 @@ using Robust.Shared.Maths; using Robust.Shared.Player; using Robust.Shared.Serialization.Manager.Attributes; using Robust.Shared.ViewVariables; -using static Content.Shared.Wires.SharedWiresComponent; -using static Content.Shared.Wires.SharedWiresComponent.WiresAction; +// using static Content.Shared.Wires.SharedWiresComponent; +// using static Content.Shared.Wires.SharedWiresComponent.WiresAction; namespace Content.Server.Doors.Components { @@ -24,7 +24,7 @@ namespace Content.Server.Doors.Components /// [RegisterComponent] [ComponentReference(typeof(SharedAirlockComponent))] - public sealed class AirlockComponent : SharedAirlockComponent, IWires + public sealed class AirlockComponent : SharedAirlockComponent { [Dependency] private readonly IEntityManager _entityManager = default!; @@ -65,8 +65,8 @@ namespace Content.Server.Doors.Components set { _powerWiresPulsed = value; - UpdateWiresStatus(); - UpdatePowerCutStatus(); + // UpdateWiresStatus(); + // UpdatePowerCutStatus(); } } @@ -83,16 +83,26 @@ namespace Content.Server.Doors.Components } } - private bool _boltLightsWirePulsed = true; + private bool _boltLightsEnabled = true; + + public bool BoltLightsEnabled + { + get => _boltLightsEnabled; + set + { + _boltLightsEnabled = value; + UpdateBoltLightStatus(); + } + } [ViewVariables(VVAccess.ReadWrite)] - private bool BoltLightsVisible + public bool BoltLightsVisible { - get => _boltLightsWirePulsed && BoltsDown && IsPowered() + get => _boltLightsEnabled && BoltsDown && IsPowered() && _entityManager.TryGetComponent(Owner, out var doorComponent) && doorComponent.State == DoorState.Closed; set { - _boltLightsWirePulsed = value; + _boltLightsEnabled = value; UpdateBoltLightStatus(); } } @@ -144,228 +154,6 @@ namespace Content.Server.Doors.Components } } - public void UpdateWiresStatus() - { - if (!_entityManager.TryGetComponent(Owner, out var wiresComponent)) return; - - var mainPowerCut = wiresComponent.IsWireCut(Wires.MainPower); - var backupPowerCut = wiresComponent.IsWireCut(Wires.BackupPower); - var statusLightState = PowerWiresPulsed ? StatusLightState.BlinkingFast : StatusLightState.On; - StatusLightData powerLight; - if (mainPowerCut && backupPowerCut) - { - powerLight = new StatusLightData(Color.DarkGoldenrod, StatusLightState.Off, "POWER"); - } - else if (mainPowerCut != backupPowerCut) - { - powerLight = new StatusLightData(Color.Gold, statusLightState, "POWER"); - } - else - { - powerLight = new StatusLightData(Color.Yellow, statusLightState, "POWER"); - } - - var boltStatus = - new StatusLightData(Color.Red, BoltsDown ? StatusLightState.On : StatusLightState.Off, "BOLT"); - var boltLightsStatus = new StatusLightData(Color.Lime, - _boltLightsWirePulsed ? StatusLightState.On : StatusLightState.Off, "BOLT LED"); - - var timingStatus = - new StatusLightData(Color.Orange, (AutoCloseDelayModifier <= 0) ? StatusLightState.Off : - !MathHelper.CloseToPercent(AutoCloseDelayModifier, 1.0f) ? StatusLightState.BlinkingSlow : - StatusLightState.On, - "TIME"); - - var safetyStatus = - new StatusLightData(Color.Red, Safety ? StatusLightState.On : StatusLightState.Off, "SAFETY"); - - wiresComponent.SetStatus(AirlockWireStatus.PowerIndicator, powerLight); - wiresComponent.SetStatus(AirlockWireStatus.BoltIndicator, boltStatus); - wiresComponent.SetStatus(AirlockWireStatus.BoltLightIndicator, boltLightsStatus); - wiresComponent.SetStatus(AirlockWireStatus.AIControlIndicator, new StatusLightData(Color.Purple, StatusLightState.BlinkingSlow, "AI CTRL")); - wiresComponent.SetStatus(AirlockWireStatus.TimingIndicator, timingStatus); - wiresComponent.SetStatus(AirlockWireStatus.SafetyIndicator, safetyStatus); - } - - private void UpdatePowerCutStatus() - { - if (!_entityManager.TryGetComponent(Owner, out var receiverComponent)) - { - return; - } - - if (PowerWiresPulsed) - { - receiverComponent.PowerDisabled = true; - return; - } - - if (!_entityManager.TryGetComponent(Owner, out var wiresComponent)) - { - return; - } - - receiverComponent.PowerDisabled = - wiresComponent.IsWireCut(Wires.MainPower) && - wiresComponent.IsWireCut(Wires.BackupPower); - } - - private enum Wires - { - /// - /// Pulsing turns off power for . - /// Cutting turns off power permanently if is also cut. - /// Mending restores power. - /// - MainPower, - - /// - BackupPower, - - /// - /// Pulsing causes for bolts to toggle (but only raise if power is on) - /// Cutting causes Bolts to drop - /// Mending does nothing - /// - Bolts, - - /// - /// Pulsing causes light to toggle - /// Cutting causes light to go out - /// Mending causes them to go on again - /// - BoltLight, - - // Placeholder for when AI is implemented - // aaaaany day now. - AIControl, - - /// - /// Pulsing causes door to close faster - /// Cutting disables door timer, causing door to stop closing automatically - /// Mending restores door timer - /// - Timing, - - /// - /// Pulsing toggles safety - /// Cutting disables safety - /// Mending enables safety - /// - Safety, - } - - public void RegisterWires(WiresComponent.WiresBuilder builder) - { - builder.CreateWire(Wires.MainPower); - builder.CreateWire(Wires.BackupPower); - builder.CreateWire(Wires.Bolts); - builder.CreateWire(Wires.BoltLight); - builder.CreateWire(Wires.Timing); - builder.CreateWire(Wires.Safety); - - UpdateWiresStatus(); - } - - public void WiresUpdate(WiresUpdateEventArgs args) - { - if (!_entityManager.TryGetComponent(Owner, out var doorComponent)) - { - return; - } - - if (args.Action == Pulse) - { - switch (args.Identifier) - { - case Wires.MainPower: - case Wires.BackupPower: - PowerWiresPulsed = true; - _powerWiresPulsedTimerCancel.Cancel(); - _powerWiresPulsedTimerCancel = new CancellationTokenSource(); - Owner.SpawnTimer(TimeSpan.FromSeconds(PowerWiresTimeout), - () => PowerWiresPulsed = false, - _powerWiresPulsedTimerCancel.Token); - break; - case Wires.Bolts: - if (!BoltsDown) - { - SetBoltsWithAudio(true); - } - else - { - if (IsPowered()) // only raise again if powered - { - SetBoltsWithAudio(false); - } - } - - break; - case Wires.BoltLight: - // we need to change the property here to set the appearance again - BoltLightsVisible = !_boltLightsWirePulsed; - break; - case Wires.Timing: - // This is permanent, until the wire gets cut & mended. - AutoCloseDelayModifier = 0.5f; - EntitySystem.Get().UpdateAutoClose(Owner, this); - break; - case Wires.Safety: - Safety = !Safety; - Dirty(); - break; - } - } - - else if (args.Action == Mend) - { - switch (args.Identifier) - { - case Wires.MainPower: - case Wires.BackupPower: - // mending power wires instantly restores power - _powerWiresPulsedTimerCancel?.Cancel(); - PowerWiresPulsed = false; - break; - case Wires.BoltLight: - BoltLightsVisible = true; - break; - case Wires.Timing: - AutoCloseDelayModifier = 1; - EntitySystem.Get().UpdateAutoClose(Owner, this); - break; - case Wires.Safety: - Safety = true; - Dirty(); - break; - } - } - - else if (args.Action == Cut) - { - switch (args.Identifier) - { - case Wires.Bolts: - SetBoltsWithAudio(true); - break; - case Wires.BoltLight: - BoltLightsVisible = false; - break; - case Wires.Timing: - AutoCloseDelayModifier = 0; // disable auto close - EntitySystem.Get().UpdateAutoClose(Owner, this); - break; - case Wires.Safety: - Safety = false; - Dirty(); - break; - } - } - - UpdateWiresStatus(); - UpdatePowerCutStatus(); - } - public void SetBoltsWithAudio(bool newBolts) { if (newBolts == BoltsDown) diff --git a/Content.Server/Doors/Systems/AirlockSystem.cs b/Content.Server/Doors/Systems/AirlockSystem.cs index 9dd46ceafb..7dbc15c0b7 100644 --- a/Content.Server/Doors/Systems/AirlockSystem.cs +++ b/Content.Server/Doors/Systems/AirlockSystem.cs @@ -1,6 +1,6 @@ using Content.Server.Doors.Components; using Content.Server.Power.Components; -using Content.Server.WireHacking; +using Content.Server.Wires; using Content.Shared.Doors; using Content.Shared.Doors.Components; using Content.Shared.Doors.Systems; @@ -15,6 +15,8 @@ namespace Content.Server.Doors.Systems { public sealed class AirlockSystem : SharedAirlockSystem { + [Dependency] private readonly WiresSystem _wiresSystem = default!; + public override void Initialize() { base.Initialize(); @@ -128,7 +130,7 @@ namespace Content.Server.Doors.Systems if (TryComp(uid, out var wiresComponent) && wiresComponent.IsPanelOpen && EntityManager.TryGetComponent(args.User, out ActorComponent? actor)) { - wiresComponent.OpenInterface(actor.PlayerSession); + _wiresSystem.OpenUserInterface(uid, actor.PlayerSession); args.Handled = true; } } diff --git a/Content.Server/Doors/WireActions/AirlockWireIdentifier.cs b/Content.Server/Doors/WireActions/AirlockWireIdentifier.cs new file mode 100644 index 0000000000..0769c33f3f --- /dev/null +++ b/Content.Server/Doors/WireActions/AirlockWireIdentifier.cs @@ -0,0 +1,9 @@ +namespace Content.Server.Doors; + +public enum AirlockWireIdentifier : byte +{ + Bolt, + BoltLight, + Timing, + Safety, +} diff --git a/Content.Server/Doors/WireActions/DoorBoltLightWireAction.cs b/Content.Server/Doors/WireActions/DoorBoltLightWireAction.cs new file mode 100644 index 0000000000..5b98842c95 --- /dev/null +++ b/Content.Server/Doors/WireActions/DoorBoltLightWireAction.cs @@ -0,0 +1,65 @@ +using Content.Server.Doors.Components; +using Content.Server.Wires; +using Content.Shared.Doors; +using Content.Shared.Doors.Components; +using Content.Shared.Wires; + +namespace Content.Server.Doors; + +[DataDefinition] +public class DoorBoltLightWireAction : BaseWireAction +{ + [DataField("color")] + private Color _statusColor = Color.Lime; + + [DataField("name")] + private string _text = "BLIT"; + + public override StatusLightData? GetStatusLightData(Wire wire) + { + StatusLightState lightState = StatusLightState.Off; + if (IsPowered(wire.Owner) && EntityManager.TryGetComponent(wire.Owner, out var door)) + { + lightState = door.BoltLightsEnabled + ? StatusLightState.On + : StatusLightState.Off; + } + + return new StatusLightData( + _statusColor, + lightState, + _text); + } + + public override object StatusKey { get; } = AirlockWireStatus.BoltLightIndicator; + + public override bool Cut(EntityUid user, Wire wire) + { + if (EntityManager.TryGetComponent(wire.Owner, out var door)) + { + door.BoltLightsVisible = false; + } + + return true; + } + + public override bool Mend(EntityUid user, Wire wire) + { + if (EntityManager.TryGetComponent(wire.Owner, out var door)) + { + door.BoltLightsVisible = true; + } + + return true; + } + + public override bool Pulse(EntityUid user, Wire wire) + { + if (EntityManager.TryGetComponent(wire.Owner, out var door)) + { + door.BoltLightsVisible = !door.BoltLightsEnabled; + } + + return true; + } +} diff --git a/Content.Server/Doors/WireActions/DoorBoltWireAction.cs b/Content.Server/Doors/WireActions/DoorBoltWireAction.cs new file mode 100644 index 0000000000..8c356c23f7 --- /dev/null +++ b/Content.Server/Doors/WireActions/DoorBoltWireAction.cs @@ -0,0 +1,66 @@ +using Content.Server.Doors.Components; +using Content.Server.Wires; +using Content.Shared.Doors; +using Content.Shared.Wires; + +namespace Content.Server.Doors; + +[DataDefinition] +public class DoorBoltWireAction : BaseWireAction +{ + [DataField("color")] + private Color _statusColor = Color.Red; + + [DataField("name")] + private string _text = "BOLT"; + + public override StatusLightData? GetStatusLightData(Wire wire) + { + StatusLightState lightState = StatusLightState.Off; + if (IsPowered(wire.Owner) + && EntityManager.TryGetComponent(wire.Owner, out var door)) + { + if (door.BoltsDown) + { + lightState = StatusLightState.On; + } + } + + return new StatusLightData( + _statusColor, + lightState, + _text); + } + + public override object StatusKey { get; } = AirlockWireStatus.BoltIndicator; + + public override bool Cut(EntityUid user, Wire wire) + { + if (EntityManager.TryGetComponent(wire.Owner, out var door)) + { + if (!door.BoltsDown) + { + door.SetBoltsWithAudio(true); + } + } + + return true; + } + + // does nothing + public override bool Mend(EntityUid user, Wire wire) + { + return true; + } + + public override bool Pulse(EntityUid user, Wire wire) + { + if (IsPowered(wire.Owner) + && EntityManager.TryGetComponent(wire.Owner, out var door)) + { + door.SetBoltsWithAudio(!door.BoltsDown); + } + + return true; + } +} diff --git a/Content.Server/Doors/WireActions/DoorSafetyWireAction.cs b/Content.Server/Doors/WireActions/DoorSafetyWireAction.cs new file mode 100644 index 0000000000..96d6aea6e3 --- /dev/null +++ b/Content.Server/Doors/WireActions/DoorSafetyWireAction.cs @@ -0,0 +1,94 @@ +using Content.Server.Doors.Components; +using Content.Server.Wires; +using Content.Shared.Doors; +using Content.Shared.Wires; + +namespace Content.Server.Doors; + +[DataDefinition] +public class DoorSafetyWireAction : BaseWireAction +{ + [DataField("color")] + private Color _statusColor = Color.Red; + + [DataField("name")] + private string _text = "SAFE"; + + [DataField("timeout")] + private int _timeout = 30; + + public override StatusLightData? GetStatusLightData(Wire wire) + { + var lightState = StatusLightState.Off; + if (IsPowered(wire.Owner) + && EntityManager.TryGetComponent(wire.Owner, out var door)) + { + lightState = door.Safety + ? StatusLightState.On + : StatusLightState.Off; + } + + return new StatusLightData( + _statusColor, + lightState, + _text); + } + + public override object StatusKey { get; } = AirlockWireStatus.SafetyIndicator; + + public override bool Cut(EntityUid user, Wire wire) + { + if (EntityManager.TryGetComponent(wire.Owner, out var door)) + { + WiresSystem.TryCancelWireAction(wire.Owner, PulseTimeoutKey.Key); + door.Safety = false; + } + + return true; + } + + public override bool Mend(EntityUid user, Wire wire) + { + if (EntityManager.TryGetComponent(wire.Owner, out var door)) + { + door.Safety = true; + } + + return true; + } + + public override bool Pulse(EntityUid user, Wire wire) + { + if (EntityManager.TryGetComponent(wire.Owner, out var door)) + { + door.Safety = false; + WiresSystem.StartWireAction(wire.Owner, _timeout, PulseTimeoutKey.Key, new TimedWireEvent(AwaitSafetyTimerFinish, wire)); + } + + return true; + } + + public override void Update(Wire wire) + { + if (!IsPowered(wire.Owner)) + { + WiresSystem.TryCancelWireAction(wire.Owner, PulseTimeoutKey.Key); + } + } + + private void AwaitSafetyTimerFinish(Wire wire) + { + if (!wire.IsCut) + { + if (EntityManager.TryGetComponent(wire.Owner, out var door)) + { + door.Safety = true; + } + } + } + + private enum PulseTimeoutKey : byte + { + Key + } +} diff --git a/Content.Server/Doors/WireActions/DoorTimingWireAction.cs b/Content.Server/Doors/WireActions/DoorTimingWireAction.cs new file mode 100644 index 0000000000..68beab4e83 --- /dev/null +++ b/Content.Server/Doors/WireActions/DoorTimingWireAction.cs @@ -0,0 +1,105 @@ +using Content.Server.Doors.Components; +using Content.Server.Wires; +using Content.Shared.Doors; +using Content.Shared.Wires; + +namespace Content.Server.Doors; + +[DataDefinition] +public class DoorTimingWireAction : BaseWireAction +{ + [DataField("color")] + private Color _statusColor = Color.Orange; + + [DataField("name")] + private string _text = "TIMR"; + + [DataField("timeout")] + private int _timeout = 30; + + public override StatusLightData? GetStatusLightData(Wire wire) + { + var lightState = StatusLightState.Off; + if (IsPowered(wire.Owner) + && EntityManager.TryGetComponent(wire.Owner, out var door)) + { + switch (door.AutoCloseDelayModifier) + { + case 0f: + lightState = StatusLightState.Off; + break; + case <= 0.5f: + lightState = StatusLightState.BlinkingSlow; + break; + default: + lightState = StatusLightState.On; + break; + } + } + + return new StatusLightData( + _statusColor, + lightState, + _text); + } + + public override object StatusKey { get; } = AirlockWireStatus.TimingIndicator; + + public override bool Cut(EntityUid user, Wire wire) + { + if (EntityManager.TryGetComponent(wire.Owner, out var door)) + { + WiresSystem.TryCancelWireAction(wire.Owner, PulseTimeoutKey.Key); + door.AutoCloseDelayModifier = 0f; + } + + return true; + } + + public override bool Mend(EntityUid user, Wire wire) + { + if (EntityManager.TryGetComponent(wire.Owner, out var door)) + { + door.AutoCloseDelayModifier = 1f; + } + + return true; + } + + public override bool Pulse(EntityUid user, Wire wire) + { + if (EntityManager.TryGetComponent(wire.Owner, out var door)) + { + door.AutoCloseDelayModifier = 0.5f; + WiresSystem.StartWireAction(wire.Owner, _timeout, PulseTimeoutKey.Key, new TimedWireEvent(AwaitTimingTimerFinish, wire)); + } + + + return true; + } + + public override void Update(Wire wire) + { + if (!IsPowered(wire.Owner)) + { + WiresSystem.TryCancelWireAction(wire.Owner, PulseTimeoutKey.Key); + } + } + + // timing timer??? ??? + private void AwaitTimingTimerFinish(Wire wire) + { + if (!wire.IsCut) + { + if (EntityManager.TryGetComponent(wire.Owner, out var door)) + { + door.AutoCloseDelayModifier = 1f; + } + } + } + + private enum PulseTimeoutKey : byte + { + Key + } +} diff --git a/Content.Server/ParticleAccelerator/Components/ParticleAcceleratorControlBoxComponent.cs b/Content.Server/ParticleAccelerator/Components/ParticleAcceleratorControlBoxComponent.cs index a8de028692..5234556df9 100644 --- a/Content.Server/ParticleAccelerator/Components/ParticleAcceleratorControlBoxComponent.cs +++ b/Content.Server/ParticleAccelerator/Components/ParticleAcceleratorControlBoxComponent.cs @@ -6,12 +6,15 @@ using Content.Server.Power.Components; using Content.Server.Power.EntitySystems; using Content.Server.UserInterface; using Content.Server.VendingMachines; -using Content.Server.WireHacking; +// using Content.Server.WireHacking; +using Content.Shared.ActionBlocker; +using Content.Shared.Interaction; using Content.Shared.Singularity.Components; using Robust.Server.GameObjects; using Robust.Shared.Map; using Robust.Shared.Utility; -using static Content.Shared.Wires.SharedWiresComponent; +using Robust.Shared.ViewVariables; +// using static Content.Shared.Wires.SharedWiresComponent; using Timer = Robust.Shared.Timing.Timer; namespace Content.Server.ParticleAccelerator.Components @@ -23,7 +26,7 @@ namespace Content.Server.ParticleAccelerator.Components /// Also contains primary logic for actual PA behavior, part scanning, etc... /// [RegisterComponent] - public sealed class ParticleAcceleratorControlBoxComponent : ParticleAcceleratorPartComponent, IWires + public sealed class ParticleAcceleratorControlBoxComponent : ParticleAcceleratorPartComponent { [Dependency] private readonly IEntityManager _entMan = default!; [Dependency] private readonly IMapManager _mapManager = default!; @@ -103,7 +106,7 @@ namespace Content.Server.ParticleAccelerator.Components { base.Startup(); - UpdateWireStatus(); + // UpdateWireStatus(); } // This is the power state for the PA control box itself. @@ -185,12 +188,14 @@ namespace Content.Server.ParticleAccelerator.Components UserInterface?.SetState(state); } + protected override void OnRemove() { UserInterface?.CloseAll(); base.OnRemove(); } + /* void IWires.RegisterWires(WiresComponent.WiresBuilder builder) { builder.CreateWire(ParticleAcceleratorControlBoxWires.Toggle); @@ -308,6 +313,7 @@ namespace Content.Server.ParticleAccelerator.Components wires.SetStatus(ParticleAcceleratorWireStatus.Limiter, limiterLight); wires.SetStatus(ParticleAcceleratorWireStatus.Strength, strengthLight); } + */ public void RescanParts() { diff --git a/Content.Server/Power/Components/ApcPowerReceiverComponent.cs b/Content.Server/Power/Components/ApcPowerReceiverComponent.cs index d072d793b5..f60e447278 100644 --- a/Content.Server/Power/Components/ApcPowerReceiverComponent.cs +++ b/Content.Server/Power/Components/ApcPowerReceiverComponent.cs @@ -45,7 +45,10 @@ namespace Content.Server.Power.Components /// [ViewVariables(VVAccess.ReadWrite)] [DataField("powerDisabled")] - public bool PowerDisabled { get => !NetworkLoad.Enabled; set => NetworkLoad.Enabled = !value; } + public bool PowerDisabled { + get => !NetworkLoad.Enabled; + set => NetworkLoad.Enabled = !value; + } public bool? PoweredLastUpdate; diff --git a/Content.Server/Power/EntitySystems/ActivatableUIRequiresPowerSystem.cs b/Content.Server/Power/EntitySystems/ActivatableUIRequiresPowerSystem.cs index 849d8a8c9e..003c419d9a 100644 --- a/Content.Server/Power/EntitySystems/ActivatableUIRequiresPowerSystem.cs +++ b/Content.Server/Power/EntitySystems/ActivatableUIRequiresPowerSystem.cs @@ -1,7 +1,7 @@ using Content.Shared.Popups; using Content.Server.Power.Components; using Content.Server.UserInterface; -using Content.Server.WireHacking; +using Content.Server.Wires; using JetBrains.Annotations; namespace Content.Server.Power.EntitySystems diff --git a/Content.Server/Power/EntitySystems/PowerMonitoringConsoleSystem.cs b/Content.Server/Power/EntitySystems/PowerMonitoringConsoleSystem.cs index 772107d29d..b635fdb696 100644 --- a/Content.Server/Power/EntitySystems/PowerMonitoringConsoleSystem.cs +++ b/Content.Server/Power/EntitySystems/PowerMonitoringConsoleSystem.cs @@ -5,7 +5,6 @@ using Content.Server.NodeContainer.Nodes; using Content.Server.Power.Components; using Content.Server.Power.NodeGroups; using Content.Server.UserInterface; -using Content.Server.WireHacking; using JetBrains.Annotations; using Robust.Server.GameObjects; @@ -99,4 +98,3 @@ internal sealed class PowerMonitoringConsoleSystem : EntitySystem return -x.Size.CompareTo(y.Size); } } - diff --git a/Content.Server/Power/PowerWireAction.cs b/Content.Server/Power/PowerWireAction.cs new file mode 100644 index 0000000000..810b1af877 --- /dev/null +++ b/Content.Server/Power/PowerWireAction.cs @@ -0,0 +1,266 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Content.Server.DoAfter; +using Content.Server.Electrocution; +using Content.Server.Power.Components; +using Content.Server.Wires; +using Content.Shared.Power; +using Content.Shared.Wires; +using Robust.Shared.GameObjects; +using Robust.Shared.Maths; +using Robust.Shared.Serialization; +using Robust.Shared.Serialization.Manager.Attributes; + +namespace Content.Server.Power; + +// Generic power wire action. Use on anything +// that requires power. +[DataDefinition] +public sealed class PowerWireAction : BaseWireAction +{ + [DataField("color")] + private Color _statusColor = Color.Red; + + [DataField("name")] + private string _text = "POWR"; + + [DataField("pulseTimeout")] + private int _pulseTimeout = 30; + + private ElectrocutionSystem _electrocutionSystem = default!; + + public override object StatusKey { get; } = PowerWireActionKey.Status; + + public override StatusLightData? GetStatusLightData(Wire wire) + { + StatusLightState lightState = StatusLightState.Off; + if (WiresSystem.TryGetData(wire.Owner, PowerWireActionInternalKeys.MainWire, out int main) + && main != wire.Id) + { + return null; + } + + if (IsPowered(wire.Owner)) + { + if (WiresSystem.TryGetData(wire.Owner, PowerWireActionKey.Pulsed, out bool pulsed) + && pulsed) + { + lightState = StatusLightState.BlinkingSlow; + } + else + { + lightState = (AllWiresCut(wire.Owner)) + ? StatusLightState.Off + : StatusLightState.On; + } + } + + return new StatusLightData( + _statusColor, + lightState, + _text); + } + + private bool AllWiresCut(EntityUid owner) + { + return WiresSystem.TryGetData(owner, PowerWireActionInternalKeys.CutWires, out int? cut) + && WiresSystem.TryGetData(owner, PowerWireActionInternalKeys.WireCount, out int? count) + && count == cut; + } + + // I feel like these two should be within ApcPowerReceiverComponent at this point. + // Getting it from a dictionary is significantly more expensive. + private void SetPower(EntityUid owner, bool pulsed) + { + if (!EntityManager.TryGetComponent(owner, out ApcPowerReceiverComponent? power)) + { + return; + } + + if (pulsed) + { + power.PowerDisabled = true; + return; + } + + if (WiresSystem.TryGetData(owner, PowerWireActionInternalKeys.CutWires, out int? cut) + && WiresSystem.TryGetData(owner, PowerWireActionInternalKeys.WireCount, out int? count)) + { + if (AllWiresCut(owner)) + { + power.PowerDisabled = true; + } + else + { + if (WiresSystem.TryGetData(owner, PowerWireActionKey.Pulsed, out bool isPulsed) + && isPulsed) + { + return; + } + + power.PowerDisabled = false; + } + } + } + + private void SetWireCuts(EntityUid owner, bool isCut) + { + if (WiresSystem.TryGetData(owner, PowerWireActionInternalKeys.CutWires, out int? cut)) + { + cut = isCut ? cut + 1 : cut - 1; + WiresSystem.SetData(owner, PowerWireActionInternalKeys.CutWires, cut); + } + } + + private void SetElectrified(EntityUid used, bool setting, ElectrifiedComponent? electrified = null) + { + if (electrified == null + && !EntityManager.TryGetComponent(used, out electrified)) + return; + + electrified.Enabled = setting; + } + + /// false if failed, true otherwise + private bool TrySetElectrocution(EntityUid user, Wire wire, bool timed = false) + { + if (EntityManager.TryGetComponent(wire.Owner, out var power) + && EntityManager.TryGetComponent(wire.Owner, out var electrified)) + { + // always set this to true + SetElectrified(wire.Owner, true, electrified); + + // if we were electrified, then return false + var electrifiedAttempt = _electrocutionSystem.TryDoElectrifiedAct(wire.Owner, user); + + // if this is timed, we set up a doAfter so that the + // electrocution continues - unless cancelled + // + // if the power is disabled however, just don't bother + if (timed && IsPowered(wire.Owner)) + { + WiresSystem.StartWireAction(wire.Owner, _pulseTimeout, PowerWireActionKey.ElectrifiedCancel, new TimedWireEvent(AwaitElectrifiedCancel, wire)); + } + else + { + SetElectrified(wire.Owner, false, electrified); + } + + return !electrifiedAttempt; + } + + return false; + } + + public override void Initialize() + { + base.Initialize(); + + _electrocutionSystem = EntitySystem.Get(); + } + + // This should add a wire into the entity's state, whether it be + // in WiresComponent or ApcPowerReceiverComponent. + public override bool AddWire(Wire wire, int count) + { + if (!WiresSystem.HasData(wire.Owner, PowerWireActionInternalKeys.CutWires)) + { + WiresSystem.SetData(wire.Owner, PowerWireActionInternalKeys.CutWires, 0); + } + + if (count == 1) + { + WiresSystem.SetData(wire.Owner, PowerWireActionInternalKeys.MainWire, wire.Id); + } + + WiresSystem.SetData(wire.Owner, PowerWireActionInternalKeys.WireCount, count); + + return true; + } + + public override bool Cut(EntityUid user, Wire wire) + { + if (!TrySetElectrocution(user, wire)) + return false; + + SetWireCuts(wire.Owner, true); + + SetPower(wire.Owner, false); + + return true; + } + + public override bool Mend(EntityUid user, Wire wire) + { + if (!TrySetElectrocution(user, wire)) + return false; + + // Mending any power wire restores shorts. + WiresSystem.TryCancelWireAction(wire.Owner, PowerWireActionKey.PulseCancel); + WiresSystem.TryCancelWireAction(wire.Owner, PowerWireActionKey.ElectrifiedCancel); + + SetWireCuts(wire.Owner, false); + + SetPower(wire.Owner, false); + + return true; + } + + public override bool Pulse(EntityUid user, Wire wire) + { + WiresSystem.TryCancelWireAction(wire.Owner, PowerWireActionKey.ElectrifiedCancel); + + if (!TrySetElectrocution(user, wire, true)) + return false; + + // disrupted power shouldn't re-disrupt + if (WiresSystem.TryGetData(wire.Owner, PowerWireActionKey.Pulsed, out bool pulsedKey) + && pulsedKey) + { + return false; + } + + WiresSystem.SetData(wire.Owner, PowerWireActionKey.Pulsed, true); + + WiresSystem.StartWireAction(wire.Owner, _pulseTimeout, PowerWireActionKey.PulseCancel, new TimedWireEvent(AwaitPulseCancel, wire)); + + SetPower(wire.Owner, true); + + // AwaitPulseCancel(wire.Owner, wire, _doAfterSystem.WaitDoAfter(doAfter)); + + return true; + } + + public override void Update(Wire wire) + { + if (!IsPowered(wire.Owner)) + { + if (!WiresSystem.TryGetData(wire.Owner, PowerWireActionKey.Pulsed, out bool pulsed) + || !pulsed) + { + WiresSystem.TryCancelWireAction(wire.Owner, PowerWireActionKey.ElectrifiedCancel); + WiresSystem.TryCancelWireAction(wire.Owner, PowerWireActionKey.PulseCancel); + } + } + } + + private void AwaitElectrifiedCancel(Wire wire) + { + WiresSystem.SetData(wire.Owner, PowerWireActionKey.Electrified, false); + SetElectrified(wire.Owner, false); + } + + private void AwaitPulseCancel(Wire wire) + { + WiresSystem.SetData(wire.Owner, PowerWireActionKey.Pulsed, false); + SetPower(wire.Owner, false); + } + + private enum PowerWireActionInternalKeys : byte + { + MainWire, + WireCount, + CutWires + } +} diff --git a/Content.Server/VendingMachines/VendingMachineComponent.cs b/Content.Server/VendingMachines/VendingMachineComponent.cs index 19b3745947..b52f9fbb1a 100644 --- a/Content.Server/VendingMachines/VendingMachineComponent.cs +++ b/Content.Server/VendingMachines/VendingMachineComponent.cs @@ -1,6 +1,6 @@ using System; using Content.Server.UserInterface; -using Content.Server.WireHacking; +using Content.Shared.Interaction; using Content.Shared.Sound; using Content.Shared.VendingMachines; using Robust.Server.GameObjects; @@ -8,13 +8,12 @@ using Robust.Shared.GameObjects; using Robust.Shared.Serialization.Manager.Attributes; using Robust.Shared.ViewVariables; using Content.Server.VendingMachines.systems; -using static Content.Shared.Wires.SharedWiresComponent; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; namespace Content.Server.VendingMachines { [RegisterComponent] - public sealed class VendingMachineComponent : SharedVendingMachineComponent, IWires + public sealed class VendingMachineComponent : SharedVendingMachineComponent { public bool Ejecting; public bool Emagged = false; @@ -37,45 +36,5 @@ namespace Content.Server.VendingMachines [ViewVariables] public BoundUserInterface? UserInterface => Owner.GetUIOrNull(VendingMachineUiKey.Key); public float NonLimitedEjectForce = 7.5f; public float NonLimitedEjectRange = 5f; - - public enum Wires - { - /// - /// Shoots a random item when pulsed. - /// - Limiter - } - public void RegisterWires(WiresComponent.WiresBuilder builder) - { - builder.CreateWire(Wires.Limiter); - } - - public void WiresUpdate(WiresUpdateEventArgs args) - { - var identifier = (Wires) args.Identifier; - if (identifier == Wires.Limiter && args.Action == WiresAction.Pulse) - { - EntitySystem.Get().EjectRandom(this.Owner, true, this); - } - } - } - - public sealed class WiresUpdateEventArgs : EventArgs - { - public readonly object Identifier; - public readonly WiresAction Action; - - public WiresUpdateEventArgs(object identifier, WiresAction action) - { - Identifier = identifier; - Action = action; - } - } - - public interface IWires - { - void RegisterWires(WiresComponent.WiresBuilder builder); - void WiresUpdate(WiresUpdateEventArgs args); } } - diff --git a/Content.Server/WireHacking/WireHackingSystem.cs b/Content.Server/WireHacking/WireHackingSystem.cs deleted file mode 100644 index ca175a11d0..0000000000 --- a/Content.Server/WireHacking/WireHackingSystem.cs +++ /dev/null @@ -1,266 +0,0 @@ -using Content.Server.Tools; -using Content.Server.VendingMachines; -using Content.Shared.Interaction; -using Content.Shared.Examine; -using Content.Shared.Tools.Components; -using Content.Shared.GameTicking; -using Robust.Server.GameObjects; -using Robust.Shared.Audio; -using Robust.Shared.Player; -using Robust.Shared.Random; -using static Content.Shared.Wires.SharedWiresComponent; - -namespace Content.Server.WireHacking -{ - public sealed class WireHackingSystem : EntitySystem - { - [Dependency] private readonly IRobustRandom _random = default!; - [Dependency] private readonly ToolSystem _tools = default!; - - [ViewVariables] private readonly Dictionary _layouts = - new(); - - public const float ScrewTime = 2.5f; - - public override void Initialize() - { - base.Initialize(); - - SubscribeLocalEvent(OnWiresStartup); - SubscribeLocalEvent(OnWiresMapInit); - SubscribeLocalEvent(OnWiresExamine); - SubscribeLocalEvent(OnInteractUsing); - - // Hacking DoAfters - SubscribeLocalEvent(OnWiresCut); - SubscribeLocalEvent(OnWiresMended); - SubscribeLocalEvent(OnWiresPulsed); - SubscribeLocalEvent(OnWiresCancelled); - - SubscribeLocalEvent(Reset); - } - - private void OnWiresCancelled(EntityUid uid, WiresComponent component, WiresComponent.WiresCancelledEvent args) - { - component.PendingDoAfters.Remove(args.Wire.Id); - } - - private void HackingInteract(WiresComponent component, WiresComponent.Wire wire) - { - component.PendingDoAfters.Remove(wire.Id); - } - - private void OnWiresCut(EntityUid uid, WiresComponent component, WiresComponent.WiresCutEvent args) - { - HackingInteract(component, args.Wire); - - // Re-validate - // Deletion for user + wires should already be handled by do-after and tool is checked once at end in active-hand anyway. - if (!component.CanWiresInteract(args.User, out var tool)) return; - - if (!tool.Qualities.Contains(component.CuttingQuality)) return; - - var wire = args.Wire; - - _tools.PlayToolSound(args.Tool.Owner, args.Tool); - wire.IsCut = true; - component.UpdateUserInterface(); - - wire.Owner.WiresUpdate(new WiresUpdateEventArgs(wire.Identifier, WiresAction.Cut)); - } - - private void OnWiresMended(EntityUid uid, WiresComponent component, WiresComponent.WiresMendedEvent args) - { - HackingInteract(component, args.Wire); - - if (!component.CanWiresInteract(args.User, out var tool)) return; - - if (!tool.Qualities.Contains(component.CuttingQuality)) return; - - var wire = args.Wire; - - _tools.PlayToolSound(args.Tool.Owner, args.Tool); - wire.IsCut = false; - component.UpdateUserInterface(); - - wire.Owner.WiresUpdate(new WiresUpdateEventArgs(wire.Identifier, WiresAction.Mend)); - } - - private void OnWiresPulsed(EntityUid uid, WiresComponent component, WiresComponent.WiresPulsedEvent args) - { - HackingInteract(component, args.Wire); - - if (args.Wire.IsCut || !component.CanWiresInteract(args.User, out var tool)) return; - - if (!tool.Qualities.Contains(component.PulsingQuality)) return; - - var wire = args.Wire; - SoundSystem.Play(Filter.Pvs(uid), component.PulseSound.GetSound(), uid); - wire.Owner.WiresUpdate(new WiresUpdateEventArgs(wire.Identifier, WiresAction.Pulse)); - } - - private void OnWiresExamine(EntityUid uid, WiresComponent component, ExaminedEvent args) - { - args.PushMarkup(Loc.GetString(component.IsPanelOpen - ? "wires-component-on-examine-panel-open" - : "wires-component-on-examine-panel-closed")); - } - - private async void OnInteractUsing(EntityUid uid, WiresComponent component, InteractUsingEvent args) - { - if (!TryComp(args.Used, out var tool)) - return; - - // opens the wires ui if using a tool with cutting or multitool quality on it - if (component.IsPanelOpen && - (tool.Qualities.Contains(component.CuttingQuality) || - tool.Qualities.Contains(component.PulsingQuality))) - { - if (TryComp(args.User, out ActorComponent? actor)) - { - component.OpenInterface(actor.PlayerSession); - return; - } - } - - // screws the panel open if the tool can do so - else if (await _tools.UseTool(tool.Owner, args.User, uid, - 0f, WireHackingSystem.ScrewTime, component.ScrewingQuality, toolComponent:tool)) - { - component.InvertPanel(); - if (component.IsPanelOpen) - { - SoundSystem.Play(Filter.Pvs(uid), component.ScrewdriverOpenSound.GetSound(), uid); - } - else - { - SoundSystem.Play(Filter.Pvs(uid), component.ScrewdriverCloseSound.GetSound(), uid); - } - } - } - - private void OnWiresStartup(EntityUid uid, WiresComponent component, ComponentStartup args) - { - WireLayout? layout = null; - if (component.LayoutId != null) - { - _layouts.TryGetValue(component.LayoutId, out layout); - } - - foreach (var wiresProvider in EntityManager.GetComponents(uid)) - { - var builder = new WiresComponent.WiresBuilder(component, wiresProvider, layout); - wiresProvider.RegisterWires(builder); - } - - if (layout != null) - { - component.WiresList.Sort((a, b) => - { - var pA = layout.Specifications[a.Identifier].Position; - var pB = layout.Specifications[b.Identifier].Position; - - return pA.CompareTo(pB); - }); - } - else - { - _random.Shuffle(component.WiresList); - - if (component.LayoutId != null) - { - var dict = new Dictionary(); - for (var i = 0; i < component.WiresList.Count; i++) - { - var d = component.WiresList[i]; - dict.Add(d.Identifier, new WireLayout.WireData(d.Letter, d.Color, i)); - } - - _layouts.Add(component.LayoutId, new WireLayout(dict)); - } - } - - var id = 0; - foreach (var wire in component.WiresList) - { - wire.Id = ++id; - } - - component.UpdateUserInterface(); - } - - private void Reset(RoundRestartCleanupEvent ev) - { - _layouts.Clear(); - } - - private void OnWiresMapInit(EntityUid uid, WiresComponent component, MapInitEvent args) - { - if (component.SerialNumber == null) - { - GenerateSerialNumber(component); - } - - if (component.WireSeed == 0) - { - component.WireSeed = _random.Next(1, int.MaxValue); - component.UpdateUserInterface(); - } - } - - private void GenerateSerialNumber(WiresComponent component) - { - Span data = stackalloc char[9]; - data[4] = '-'; - - if (_random.Prob(0.01f)) - { - for (var i = 0; i < 4; i++) - { - // Cyrillic Letters - data[i] = (char) _random.Next(0x0410, 0x0430); - } - } - else - { - for (var i = 0; i < 4; i++) - { - // Letters - data[i] = (char) _random.Next(0x41, 0x5B); - } - } - - for (var i = 5; i < 9; i++) - { - // Digits - data[i] = (char) _random.Next(0x30, 0x3A); - } - - component.SerialNumber = new string(data); - } - } - - public sealed class WireLayout - { - [ViewVariables] public IReadOnlyDictionary Specifications { get; } - - public WireLayout(IReadOnlyDictionary specifications) - { - Specifications = specifications; - } - - public sealed class WireData - { - public WireLetter Letter { get; } - public WireColor Color { get; } - public int Position { get; } - - public WireData(WireLetter letter, WireColor color, int position) - { - Letter = letter; - Color = color; - Position = position; - } - } - } -} diff --git a/Content.Server/WireHacking/WiresComponent.cs b/Content.Server/WireHacking/WiresComponent.cs deleted file mode 100644 index 2df4998cf8..0000000000 --- a/Content.Server/WireHacking/WiresComponent.cs +++ /dev/null @@ -1,509 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using Content.Server.DoAfter; -using Content.Server.Hands.Components; -using Content.Server.UserInterface; -using Content.Server.VendingMachines; -using Content.Shared.Interaction; -using Content.Shared.Popups; -using Content.Shared.Sound; -using Content.Shared.Tools; -using Content.Shared.Tools.Components; -using Content.Shared.Wires; -using Robust.Server.GameObjects; -using Robust.Server.Player; -using Robust.Shared.Random; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; - -namespace Content.Server.WireHacking -{ - [RegisterComponent] - public sealed class WiresComponent : SharedWiresComponent - { - [Dependency] private readonly IRobustRandom _random = default!; - [Dependency] private readonly IEntityManager _entities = default!; - private bool _isPanelOpen; - - [DataField("cuttingTime")] public float CuttingTime = 1f; - - [DataField("mendTime")] public float MendTime = 1f; - - [DataField("pulseTime")] public float PulseTime = 3f; - - [DataField("screwingQuality", customTypeSerializer:typeof(PrototypeIdSerializer))] - public string ScrewingQuality = "Screwing"; - - [DataField("cuttingQuality", customTypeSerializer:typeof(PrototypeIdSerializer))] - public string CuttingQuality = "Cutting"; - - [DataField("pulsingQuality", customTypeSerializer:typeof(PrototypeIdSerializer))] - public string PulsingQuality = "Pulsing"; - - /// - /// Make do_afters for hacking unique per wire so we can't spam a single wire. - /// - public HashSet PendingDoAfters = new(); - - /// - /// Opening the maintenance panel (typically with a screwdriver) changes this. - /// - [ViewVariables] - public bool IsPanelOpen - { - get => _isPanelOpen; - private set - { - if (_isPanelOpen == value) - { - return; - } - - _isPanelOpen = value; - - if (!_isPanelOpen) - UserInterface?.CloseAll(); - UpdateAppearance(); - } - } - - private bool _isPanelVisible = true; - - /// - /// Components can set this to prevent the maintenance panel overlay from showing even if it's open - /// - [ViewVariables] - public bool IsPanelVisible - { - get => _isPanelVisible; - set - { - if (_isPanelVisible == value) - { - return; - } - - _isPanelVisible = value; - UpdateAppearance(); - } - } - - [ViewVariables(VVAccess.ReadWrite)] - public string BoardName - { - get => _boardName; - set - { - _boardName = value; - UpdateUserInterface(); - } - } - - [ViewVariables(VVAccess.ReadWrite)] - public string? SerialNumber - { - get => _serialNumber; - set - { - _serialNumber = value; - UpdateUserInterface(); - } - } - - private void UpdateAppearance() - { - if (_entities.TryGetComponent(Owner, out AppearanceComponent? appearance)) - { - appearance.SetData(WiresVisuals.MaintenancePanelState, IsPanelOpen && IsPanelVisible); - } - } - - /// - /// Contains all registered wires. - /// - [ViewVariables] - public readonly List WiresList = new(); - - /// - /// Status messages are displayed at the bottom of the UI. - /// - [ViewVariables] - private readonly Dictionary _statuses = new(); - - /// - /// and . - /// - private readonly List _availableColors = - new((WireColor[]) Enum.GetValues(typeof(WireColor))); - - private readonly List _availableLetters = - new((WireLetter[]) Enum.GetValues(typeof(WireLetter))); - - [DataField("BoardName")] - private string _boardName = "Wires"; - - [DataField("SerialNumber")] - private string? _serialNumber; - - // Used to generate wire appearance randomization client side. - // We honestly don't care what it is or such but do care that it doesn't change between UI re-opens. - [ViewVariables] - [DataField("WireSeed")] - public int WireSeed; - [ViewVariables] - [DataField("LayoutId")] - public string? LayoutId = default; - - [DataField("pulseSound")] public SoundSpecifier PulseSound = new SoundPathSpecifier("/Audio/Effects/multitool_pulse.ogg"); - - [DataField("screwdriverOpenSound")] - public SoundSpecifier ScrewdriverOpenSound = new SoundPathSpecifier("/Audio/Machines/screwdriveropen.ogg"); - - [DataField("screwdriverCloseSound")] - public SoundSpecifier ScrewdriverCloseSound = new SoundPathSpecifier("/Audio/Machines/screwdriverclose.ogg"); - - [ViewVariables] private BoundUserInterface? UserInterface => Owner.GetUIOrNull(WiresUiKey.Key); - - protected override void Initialize() - { - base.Initialize(); - - if (_entities.TryGetComponent(Owner, out AppearanceComponent? appearance)) - { - appearance.SetData(WiresVisuals.MaintenancePanelState, IsPanelOpen); - } - - if (UserInterface != null) - { - UserInterface.OnReceiveMessage += UserInterfaceOnReceiveMessage; - } - } - - /// - /// Returns whether the wire associated with is cut. - /// - /// - public bool IsWireCut(object identifier) - { - var wire = WiresList.Find(x => x.Identifier.Equals(identifier)); - if (wire == null) throw new ArgumentException(); - return wire.IsCut; - } - - public sealed class Wire - { - /// - /// The component that registered the wire. - /// - public IWires Owner { get; } - - /// - /// Whether the wire is cut. - /// - public bool IsCut { get; set; } - - /// - /// Used in client-server communication to identify a wire without telling the client what the wire does. - /// - [ViewVariables] - public int Id { get; set; } - - /// - /// The color of the wire. - /// - [ViewVariables] - public WireColor Color { get; } - - /// - /// The greek letter shown below the wire. - /// - [ViewVariables] - public WireLetter Letter { get; } - - /// - /// Registered by components implementing IWires, used to identify which wire the client interacted with. - /// - [ViewVariables] - public object Identifier { get; } - - public Wire(IWires owner, bool isCut, WireColor color, WireLetter letter, object identifier) - { - Owner = owner; - IsCut = isCut; - Color = color; - Letter = letter; - Identifier = identifier; - } - } - - /// - /// Used by . - /// - public sealed class WiresBuilder - { - private readonly WiresComponent _wires; - private readonly IWires _owner; - private readonly WireLayout? _layout; - - public WiresBuilder(WiresComponent wires, IWires owner, WireLayout? layout) - { - _wires = wires; - _owner = owner; - _layout = layout; - } - - public void CreateWire(object identifier, (WireColor, WireLetter)? appearance = null, bool isCut = false) - { - WireLetter letter; - WireColor color; - if (!appearance.HasValue) - { - if (_layout != null && _layout.Specifications.TryGetValue(identifier, out var specification)) - { - color = specification.Color; - letter = specification.Letter; - _wires._availableColors.Remove(color); - _wires._availableLetters.Remove(letter); - } - else - { - (color, letter) = _wires.AssignAppearance(); - } - } - else - { - (color, letter) = appearance.Value; - _wires._availableColors.Remove(color); - _wires._availableLetters.Remove(letter); - } - - // TODO: ENSURE NO RANDOM OVERLAP. - _wires.WiresList.Add(new Wire(_owner, isCut, color, letter, identifier)); - } - } - - /// - /// Picks a color from and removes it from the list. - /// - /// The picked color. - private (WireColor, WireLetter) AssignAppearance() - { - var color = _availableColors.Count == 0 ? WireColor.Red : _random.PickAndTake(_availableColors); - var letter = _availableLetters.Count == 0 ? WireLetter.α : _random.PickAndTake(_availableLetters); - - return (color, letter); - } - - /// - /// Call this from other components to open the wires UI. - /// - public void OpenInterface(IPlayerSession session) - { - UserInterface?.Open(session); - } - - /// - /// Closes all wire UIs. - /// - public void CloseAll() - { - UserInterface?.CloseAll(); - } - - public bool CanWiresInteract(EntityUid user, [NotNullWhen(true)] out ToolComponent? tool) - { - tool = null; - - if (!_entities.TryGetComponent(user, out HandsComponent? handsComponent)) - { - Owner.PopupMessage(user, Loc.GetString("wires-component-ui-on-receive-message-no-hands")); - return false; - } - - if (!EntitySystem.Get().InRangeUnobstructed(user, Owner)) - { - Owner.PopupMessage(user, Loc.GetString("wires-component-ui-on-receive-message-cannot-reach")); - return false; - } - - if (handsComponent.ActiveHand?.HeldEntity is not { Valid: true } activeHandEntity || - !_entities.TryGetComponent(activeHandEntity, out tool)) - { - return false; - } - - return true; - } - - private void UserInterfaceOnReceiveMessage(ServerBoundUserInterfaceMessage serverMsg) - { - var message = serverMsg.Message; - switch (message) - { - case WiresActionMessage msg: - var wire = WiresList.Find(x => x.Id == msg.Id); - if (wire == null || - serverMsg.Session.AttachedEntity is not {} player || - PendingDoAfters.Contains(wire.Id)) - { - return; - } - - if (!CanWiresInteract(player, out var tool)) - return; - - var doAfterSystem = EntitySystem.Get(); - - switch (msg.Action) - { - case WiresAction.Cut: - if (!tool.Qualities.Contains(CuttingQuality)) - { - player.PopupMessageCursor(Loc.GetString("wires-component-ui-on-receive-message-need-wirecutters")); - return; - } - - doAfterSystem.DoAfter( - new DoAfterEventArgs(player, CuttingTime, target: Owner) - { - TargetFinishedEvent = new WiresCutEvent - { - Wire = wire, - Tool = tool, - User = player, - }, - TargetCancelledEvent = new WiresCancelledEvent() - { - Wire = wire, - }, - NeedHand = true, - }); - - PendingDoAfters.Add(wire.Id); - - break; - case WiresAction.Mend: - if (!tool.Qualities.Contains(CuttingQuality)) - { - player.PopupMessageCursor(Loc.GetString("wires-component-ui-on-receive-message-need-wirecutters")); - return; - } - - doAfterSystem.DoAfter( - new DoAfterEventArgs(player, MendTime, target: Owner) - { - TargetFinishedEvent = new WiresMendedEvent() - { - Wire = wire, - Tool = tool, - User = player, - }, - TargetCancelledEvent = new WiresCancelledEvent() - { - Wire = wire, - }, - NeedHand = true, - }); - - PendingDoAfters.Add(wire.Id); - - break; - case WiresAction.Pulse: - if (!tool.Qualities.Contains(PulsingQuality)) - { - player.PopupMessageCursor(Loc.GetString("wires-component-ui-on-receive-message-need-wirecutters")); - return; - } - - if (wire.IsCut) - { - player.PopupMessageCursor(Loc.GetString("wires-component-ui-on-receive-message-cannot-pulse-cut-wire")); - return; - } - - doAfterSystem.DoAfter( - new DoAfterEventArgs(player, PulseTime, target: Owner) - { - TargetFinishedEvent = new WiresPulsedEvent - { - Wire = wire, - Tool = tool, - User = player, - }, - TargetCancelledEvent = new WiresCancelledEvent() - { - Wire = wire, - }, - NeedHand = true, - }); - - PendingDoAfters.Add(wire.Id); - - break; - } - - break; - } - } - - public sealed class WiresCancelledEvent : EntityEventArgs - { - public Wire Wire { get; init; } = default!; - } - - public abstract class WiresEvent : EntityEventArgs - { - public EntityUid User { get; init; } = default!; - public Wire Wire { get; init; } = default!; - public ToolComponent Tool { get; init; } = default!; - } - - public sealed class WiresCutEvent : WiresEvent - { - } - - public sealed class WiresMendedEvent : WiresEvent - { - } - - public sealed class WiresPulsedEvent : WiresEvent - { - } - - internal void UpdateUserInterface() - { - var clientList = new List(); - foreach (var entry in WiresList) - { - clientList.Add(new ClientWire(entry.Id, entry.IsCut, entry.Color, - entry.Letter)); - } - - UserInterface?.SetState( - new WiresBoundUserInterfaceState( - clientList.ToArray(), - _statuses.Select(p => new StatusEntry(p.Key, p.Value)).ToArray(), - BoardName, - SerialNumber, - WireSeed)); - } - - public void SetStatus(object statusIdentifier, object status) - { - if (_statuses.TryGetValue(statusIdentifier, out var storedMessage)) - { - if (storedMessage == status) - { - return; - } - } - - _statuses[statusIdentifier] = status; - UpdateUserInterface(); - } - /// Just opens/closes the panel state in the right namespace - public void InvertPanel() - { - IsPanelOpen = !IsPanelOpen; - } - } -} diff --git a/Content.Server/Wires/BaseToggleWireAction.cs b/Content.Server/Wires/BaseToggleWireAction.cs new file mode 100644 index 0000000000..da83e170e8 --- /dev/null +++ b/Content.Server/Wires/BaseToggleWireAction.cs @@ -0,0 +1,76 @@ +namespace Content.Server.Wires; + +/// +/// Utility class meant to be implemented. This is to +/// toggle a value whenever a wire is cut, mended, +/// or pulsed. +/// +public abstract class BaseToggleWireAction : BaseWireAction +{ + /// + /// Toggles the value on the given entity. An implementor + /// is expected to handle the value toggle appropriately. + /// + public abstract void ToggleValue(EntityUid owner, bool setting); + /// + /// Gets the value on the given entity. An implementor + /// is expected to handle the value getter properly. + /// + public abstract bool GetValue(EntityUid owner); + /// + /// Timeout key for the wire, if it is pulsed. + /// If this is null, there will be no value revert + /// after a given delay, otherwise, the value will + /// be set to the opposite of what it currently is + /// (according to GetValue) + /// + public virtual object? TimeoutKey { get; } = null; + public virtual int Delay { get; } = 30; + + public override bool Cut(EntityUid user, Wire wire) + { + ToggleValue(wire.Owner, false); + + if (TimeoutKey != null) + { + WiresSystem.TryCancelWireAction(wire.Owner, TimeoutKey); + } + + return true; + } + + public override bool Mend(EntityUid user, Wire wire) + { + ToggleValue(wire.Owner, true); + + return true; + } + + public override bool Pulse(EntityUid user, Wire wire) + { + ToggleValue(wire.Owner, !GetValue(wire.Owner)); + + if (TimeoutKey != null) + { + WiresSystem.StartWireAction(wire.Owner, Delay, TimeoutKey, new TimedWireEvent(AwaitPulseCancel, wire)); + } + + return true; + } + + public override void Update(Wire wire) + { + if (TimeoutKey != null && !IsPowered(wire.Owner)) + { + WiresSystem.TryCancelWireAction(wire.Owner, TimeoutKey); + } + } + + private void AwaitPulseCancel(Wire wire) + { + if (!wire.IsCut) + { + ToggleValue(wire.Owner, !GetValue(wire.Owner)); + } + } +} diff --git a/Content.Server/Wires/BaseWireAction.cs b/Content.Server/Wires/BaseWireAction.cs new file mode 100644 index 0000000000..6df793bec7 --- /dev/null +++ b/Content.Server/Wires/BaseWireAction.cs @@ -0,0 +1,54 @@ +using Content.Server.Power.Components; +using Content.Shared.Wires; +using Robust.Shared.GameObjects; +using Robust.Shared.IoC; +using Robust.Shared.Serialization.Manager.Attributes; + +namespace Content.Server.Wires; + +/// +public abstract class BaseWireAction : IWireAction +{ + public IEntityManager EntityManager = default!; + public WiresSystem WiresSystem = default!; + + // not virtual so implementors are aware that they need a nullable here + public abstract object? StatusKey { get; } + + // ugly, but IoC doesn't work during deserialization + public virtual void Initialize() + { + EntityManager = IoCManager.Resolve(); + + WiresSystem = EntitySystem.Get(); + } + + public virtual bool AddWire(Wire wire, int count) => count == 1; + public abstract bool Cut(EntityUid user, Wire wire); + public abstract bool Mend(EntityUid user, Wire wire); + public abstract bool Pulse(EntityUid user, Wire wire); + public virtual void Update(Wire wire) + { + return; + } + public abstract StatusLightData? GetStatusLightData(Wire wire); + + // most things that use wires are powered by *something*, so + // + // this isn't required by any wire system methods though, so whatever inherits it here + // can use it + /// + /// Utility function to check if this given entity is powered. + /// + /// true if powered, false otherwise + public bool IsPowered(EntityUid uid) + { + if (!EntityManager.TryGetComponent(uid, out var power) + || power.PowerDisabled) // there's some kind of race condition here? + { + return false; + } + + return power.Powered; + } +} diff --git a/Content.Server/Wires/DummyWireAction.cs b/Content.Server/Wires/DummyWireAction.cs new file mode 100644 index 0000000000..4e771b54d3 --- /dev/null +++ b/Content.Server/Wires/DummyWireAction.cs @@ -0,0 +1,29 @@ +using Content.Shared.Wires; + +namespace Content.Server.Wires; + +// Exists so that dummy wires can be added. +// +// You *shouldn't* be adding these as raw +// wire actions, but it's here anyways as +// a serializable class for consistency. +// C'est la vie. +[DataDefinition] +public sealed class DummyWireAction : BaseWireAction +{ + public override object? StatusKey { get; } = null; + + public override StatusLightData? GetStatusLightData(Wire wire) => null; + public override bool AddWire(Wire wire, int count) => true; + public override bool Cut(EntityUid user, Wire wire) => true; + public override bool Mend(EntityUid user, Wire wire) => true; + public override bool Pulse(EntityUid user, Wire wire) => true; + + // doesn't matter if you get any information off of this, + // if you really want to mess with dummy wires, you should + // probably code your own implementation? + private enum DummyWireActionIdentifier + { + Key, + } +} diff --git a/Content.Server/Wires/IWireAction.cs b/Content.Server/Wires/IWireAction.cs new file mode 100644 index 0000000000..ee1d391053 --- /dev/null +++ b/Content.Server/Wires/IWireAction.cs @@ -0,0 +1,80 @@ +using Content.Shared.Wires; +using Robust.Shared.GameObjects; + +namespace Content.Server.Wires; + +/// +/// An interface used by WiresSystem to allow compositional wiresets. +/// This is expected to be flyweighted, do not store per-entity state +/// within an object/class that implements IWireAction. +/// +public interface IWireAction +{ + /// + /// This is to link the wire's status with + /// its corresponding UI key. If this is null, + /// GetStatusLightData MUST also return null, + /// otherwise nothing happens. + /// + public object? StatusKey { get; } + + /// + /// Called when the wire in the layout + /// is created for the first time. Ensures + /// that the referenced action has all + /// the correct system references (plus + /// other information if needed, + /// but wire actions should NOT be stateful!) + /// + public void Initialize(); + + /// + /// Called when a wire is finally processed + /// by WiresSystem upon wire layout + /// creation. Use this to set specific details + /// about the state of the entity in question. + /// + /// If this returns false, this will convert + /// the given wire into a 'dummy' wire instead. + /// + /// The wire in the entity's WiresComponent. + /// The current count of this instance of the wire type. + public bool AddWire(Wire wire, int count); + + /// + /// What happens when this wire is cut. + /// + /// The user attempting to interact with the wire. + /// The wire being interacted with. + /// true if successful, false otherwise. + public bool Cut(EntityUid user, Wire wire); + + /// + /// What happens when this wire is mended. + /// + /// The user attempting to interact with the wire. + /// The wire being interacted with. + /// true if successful, false otherwise. + public bool Mend(EntityUid user, Wire wire); + + /// + /// What happens when this wire is pulsed. + /// + /// The user attempting to interact with the wire. + /// The wire being interacted with. + /// true if successful, false otherwise. + public bool Pulse(EntityUid user, Wire wire); + + /// + /// Used when a wire's state on an entity needs to be updated. + /// Mostly for things related to entity events, e.g., power. + /// + public void Update(Wire wire); + + /// + /// Used for when WiresSystem requires the status light data + /// for display on the client. + /// + /// StatusLightData to display light data, null to have no status light. + public StatusLightData? GetStatusLightData(Wire wire); +} diff --git a/Content.Server/Wires/WireLayout.cs b/Content.Server/Wires/WireLayout.cs new file mode 100644 index 0000000000..900fced5b7 --- /dev/null +++ b/Content.Server/Wires/WireLayout.cs @@ -0,0 +1,38 @@ +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; + +namespace Content.Server.Wires; + +/// +/// WireLayout prototype. +/// +/// This is meant for ease of organizing wire sets on entities that use +/// wires. Once one of these is initialized, it should be stored in the +/// WiresSystem as a functional wire set. +/// +[Prototype("wireLayout")] +public sealed class WireLayoutPrototype : IPrototype, IInheritingPrototype +{ + [IdDataFieldAttribute] + public string ID { get; } = default!; + + [ParentDataField(typeof(AbstractPrototypeIdSerializer))] + public string? Parent { get; } = default!; + + [AbstractDataField] + public bool Abstract { get; } + + /// + /// How many wires in this layout will do + /// nothing (these are added upon layout + /// initialization) + /// + [DataField("dummyWires")] + public int DummyWires { get; } = default!; + + /// + /// All the valid IWireActions currently in this layout. + /// + [DataField("wires")] + public List? Wires { get; } +} diff --git a/Content.Server/Wires/WiresComponent.cs b/Content.Server/Wires/WiresComponent.cs new file mode 100644 index 0000000000..3059323592 --- /dev/null +++ b/Content.Server/Wires/WiresComponent.cs @@ -0,0 +1,90 @@ +using Content.Shared.Sound; + +namespace Content.Server.Wires; + +[RegisterComponent] +public sealed class WiresComponent : Component +{ + /// + /// Is the panel open for this entity's wires? + /// + [ViewVariables] + public bool IsPanelOpen { get; set; } + + /// + /// Should this entity's wires panel be visible at all? + /// + [ViewVariables] + public bool IsPanelVisible { get; set; } = true; + + /// + /// The name of this entity's internal board. + /// + [ViewVariables] + [DataField("BoardName")] + public string BoardName { get; set; } = "Wires"; + + /// + /// The layout ID of this entity's wires. + /// + [ViewVariables] + [DataField("LayoutId", required: true)] + public string LayoutId { get; set; } = default!; + + /// + /// The serial number of this board. Randomly generated upon start, + /// does not need to be set. + /// + [ViewVariables] + public string? SerialNumber { get; set; } + + /// + /// The seed that dictates the wires appearance, as well as + /// the status ordering on the UI client side. + /// + [ViewVariables] + public int WireSeed { get; set; } + + /// + /// The list of wires currently active on this entity. + /// + [ViewVariables] + public List WiresList { get; set; } = new(); + + /// + /// Queue of wires saved while the wire's DoAfter event occurs, to prevent too much spam. + /// + [ViewVariables] + public List WiresQueue { get; } = new(); + + /// + /// If this should follow the layout saved the first time the layout dictated by the + /// layout ID is generated, or if a new wire order should be generated every time. + /// + [ViewVariables] + [DataField("alwaysRandomize")] + public bool AlwaysRandomize { get; } + + /// + /// Per wire status, keyed by an object. + /// + [ViewVariables] + public Dictionary Statuses { get; } = new(); + + /// + /// The state data for the set of wires inside of this entity. + /// This is so that wire objects can be flyweighted between + /// entities without any issues. + /// + [ViewVariables] + public Dictionary StateData { get; } = new(); + + [DataField("pulseSound")] + public SoundSpecifier PulseSound = new SoundPathSpecifier("/Audio/Effects/multitool_pulse.ogg"); + + [DataField("screwdriverOpenSound")] + public SoundSpecifier ScrewdriverOpenSound = new SoundPathSpecifier("/Audio/Machines/screwdriveropen.ogg"); + + [DataField("screwdriverCloseSound")] + public SoundSpecifier ScrewdriverCloseSound = new SoundPathSpecifier("/Audio/Machines/screwdriverclose.ogg"); +} diff --git a/Content.Server/Wires/WiresSystem.cs b/Content.Server/Wires/WiresSystem.cs new file mode 100644 index 0000000000..f38dfa1e47 --- /dev/null +++ b/Content.Server/Wires/WiresSystem.cs @@ -0,0 +1,954 @@ +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Threading; +using Content.Server.DoAfter; +using Content.Server.Hands.Systems; +using Content.Server.Hands.Components; +using Content.Server.Power.Components; +using Content.Server.Tools; +using Content.Shared.Examine; +using Content.Shared.GameTicking; +using Content.Shared.Interaction; +using Content.Shared.Popups; +using Content.Shared.Tools.Components; +using Content.Shared.Wires; +using Robust.Server.GameObjects; +using Robust.Server.Player; +using Robust.Shared.Audio; +using Robust.Shared.Player; +using Robust.Shared.Prototypes; +using Robust.Shared.Random; + +namespace Content.Server.Wires; + +public sealed class WiresSystem : EntitySystem +{ + [Dependency] private readonly IRobustRandom _random = default!; + [Dependency] private readonly IPrototypeManager _protoMan = default!; + [Dependency] private readonly AudioSystem _audioSystem = default!; + [Dependency] private readonly UserInterfaceSystem _uiSystem = default!; + [Dependency] private readonly ToolSystem _toolSystem = default!; + [Dependency] private readonly SharedPopupSystem _popupSystem = default!; + [Dependency] private readonly SharedInteractionSystem _interactionSystem = default!; + [Dependency] private readonly HandsSystem _handsSystem = default!; + [Dependency] private readonly DoAfterSystem _doAfter = default!; + + // This is where all the wire layouts are stored. + [ViewVariables] private readonly Dictionary _layouts = new(); + + private const float ScrewTime = 2.5f; + private const float ToolTime = 1f; + + private static DummyWireAction _dummyWire = new DummyWireAction(); + + #region Initialization + public override void Initialize() + { + _dummyWire.Initialize(); + + SubscribeLocalEvent(Reset); + + // this is a broadcast event + SubscribeLocalEvent(OnToolFinished); + SubscribeLocalEvent(OnWiresStartup); + SubscribeLocalEvent(OnWiresActionMessage); + SubscribeLocalEvent(OnInteractUsing); + SubscribeLocalEvent(OnExamine); + SubscribeLocalEvent(OnMapInit); + SubscribeLocalEvent(OnTimedWire); + SubscribeLocalEvent(OnWiresPowered); + SubscribeLocalEvent(OnWireDoAfter); + SubscribeLocalEvent(OnWireDoAfterCancel); + } + + private void SetOrCreateWireLayout(EntityUid uid, WiresComponent? wires = null) + { + if (!Resolve(uid, ref wires)) + return; + + WireLayout? layout = null; + List? wireSet = null; + if (wires.LayoutId != null) + { + if (!wires.AlwaysRandomize) + { + TryGetLayout(wires.LayoutId, out layout); + } + + if (!_protoMan.TryIndex(wires.LayoutId, out WireLayoutPrototype? layoutPrototype)) + return; + + // does the prototype have a parent (and are the wires empty?) if so, we just create + // a new layout based on that + // + // TODO: Merge wire layouts... + if (!string.IsNullOrEmpty(layoutPrototype.Parent) && layoutPrototype.Wires == null) + { + var parent = layoutPrototype.Parent; + + if (!_protoMan.TryIndex(parent, out WireLayoutPrototype? parentPrototype)) + return; + + layoutPrototype = parentPrototype; + } + + if (layoutPrototype.Wires != null) + { + foreach (var wire in layoutPrototype.Wires) + { + wire.Initialize(); + } + + wireSet = CreateWireSet(uid, layout, layoutPrototype.Wires, layoutPrototype.DummyWires); + } + } + + if (wireSet == null || wireSet.Count == 0) + { + return; + } + + wires.WiresList.AddRange(wireSet); + + Dictionary types = new Dictionary(); + + if (layout != null) + { + for (var i = 0; i < wireSet.Count; i++) + { + wires.WiresList[layout.Specifications[i].Position] = wireSet[i]; + } + + var id = 0; + foreach (var wire in wires.WiresList) + { + var wireType = wire.Action.GetType(); + if (types.ContainsKey(wireType)) + { + types[wireType] += 1; + } + else + { + types.Add(wireType, 1); + } + + wire.Id = id; + id++; + + // don't care about the result, this should've + // been handled in layout creation + wire.Action.AddWire(wire, types[wireType]); + } + } + else + { + var enumeratedList = new List<(int, Wire)>(); + var data = new Dictionary(); + for (int i = 0; i < wireSet.Count; i++) + { + enumeratedList.Add((i, wireSet[i])); + } + _random.Shuffle(enumeratedList); + + for (var i = 0; i < enumeratedList.Count; i++) + { + (int id, Wire d) = enumeratedList[i]; + + var wireType = d.Action.GetType(); + if (types.ContainsKey(wireType)) + { + types[wireType] += 1; + } + else + { + types.Add(wireType, 1); + } + + d.Id = i; + + if (!d.Action.AddWire(d, types[wireType])) + { + d.Action = _dummyWire; + } + + data.Add(id, new WireLayout.WireData(d.Letter, d.Color, i)); + + wires.WiresList[i] = wireSet[id]; + } + + if (!wires.AlwaysRandomize && !string.IsNullOrEmpty(wires.LayoutId)) + { + AddLayout(wires.LayoutId, new WireLayout(data)); + } + } + } + + private List? CreateWireSet(EntityUid uid, WireLayout? layout, List wires, int dummyWires) + { + if (wires.Count == 0) + return null; + + List colors = + new((WireColor[]) Enum.GetValues(typeof(WireColor))); + + List letters = + new((WireLetter[]) Enum.GetValues(typeof(WireLetter))); + + + var wireSet = new List(); + for (var i = 0; i < wires.Count; i++) + { + wireSet.Add(CreateWire(uid, wires[i], i, layout, colors, letters)); + } + + for (var i = 1; i <= dummyWires; i++) + { + wireSet.Add(CreateWire(uid, _dummyWire, wires.Count + i, layout, colors, letters)); + } + + return wireSet; + } + + private Wire CreateWire(EntityUid uid, IWireAction action, int position, WireLayout? layout, List colors, List letters) + { + WireLetter letter; + WireColor color; + + if (layout != null + && layout.Specifications.TryGetValue(position, out var spec)) + { + color = spec.Color; + letter = spec.Letter; + colors.Remove(color); + letters.Remove(letter); + } + else + { + color = colors.Count == 0 ? WireColor.Red : _random.PickAndTake(colors); + letter = letters.Count == 0 ? WireLetter.α : _random.PickAndTake(letters); + } + + return new Wire( + uid, + false, + color, + letter, + action); + } + + private void OnWiresStartup(EntityUid uid, WiresComponent component, ComponentStartup args) + { + if (!String.IsNullOrEmpty(component.LayoutId)) + SetOrCreateWireLayout(uid, component); + + UpdateUserInterface(uid); + } + #endregion + + #region DoAfters + private void OnTimedWire(EntityUid uid, WiresComponent component, TimedWireEvent args) + { + args.Delegate(args.Wire); + UpdateUserInterface(uid); + } + + /// + /// Tries to cancel an active wire action via the given key that it's stored in. + /// + /// The key used to cancel the action. + public bool TryCancelWireAction(EntityUid owner, object key) + { + if (TryGetData(owner, key, out CancellationTokenSource? token)) + { + token.Cancel(); + return true; + } + + return false; + } + + /// + /// Starts a timed action for this entity. + /// + /// How long this takes to finish + /// The key used to cancel the action + /// The event that is sent out when the wire is finished + public void StartWireAction(EntityUid owner, float delay, object key, TimedWireEvent onFinish) + { + if (!HasComp(owner)) + { + return; + } + + if (!_activeWires.ContainsKey(owner)) + { + _activeWires.Add(owner, new()); + } + + CancellationTokenSource tokenSource = new(); + + // Starting an already started action will do nothing. + if (HasData(owner, key)) + { + return; + } + + SetData(owner, key, tokenSource); + + _activeWires[owner].Add(new ActiveWireAction + ( + key, + delay, + tokenSource.Token, + onFinish + )); + } + + private Dictionary> _activeWires = new(); + private List<(EntityUid, ActiveWireAction)> _finishedWires = new(); + + public override void Update(float frameTime) + { + foreach (var (owner, activeWires) in _activeWires) + { + foreach (var wire in activeWires) + { + if (wire.CancelToken.IsCancellationRequested) + { + RaiseLocalEvent(owner, wire.OnFinish); + _finishedWires.Add((owner, wire)); + } + else + { + wire.TimeLeft -= frameTime; + if (wire.TimeLeft <= 0) + { + RaiseLocalEvent(owner, wire.OnFinish); + _finishedWires.Add((owner, wire)); + } + } + } + } + + if (_finishedWires.Count != 0) + { + foreach (var (owner, wireAction) in _finishedWires) + { + // sure + _activeWires[owner].RemoveAll(action => action.CancelToken == wireAction.CancelToken); + + if (_activeWires[owner].Count == 0) + { + _activeWires.Remove(owner); + } + + RemoveData(owner, wireAction.Id); + } + + _finishedWires.Clear(); + } + } + + private class ActiveWireAction + { + /// + /// The wire action's ID. This is so that once the action is finished, + /// any related data can be removed from the state dictionary. + /// + public object Id; + + /// + /// How much time is left in this action before it finishes. + /// + public float TimeLeft; + + /// + /// The token used to cancel the action. + /// + public CancellationToken CancelToken; + + /// + /// The event called once the action finishes. + /// + public TimedWireEvent OnFinish; + + public ActiveWireAction(object identifier, float time, CancellationToken cancelToken, TimedWireEvent onFinish) + { + Id = identifier; + TimeLeft = time; + CancelToken = cancelToken; + OnFinish = onFinish; + } + } + + #endregion + + #region Event Handling + private void OnWiresPowered(EntityUid uid, WiresComponent component, PowerChangedEvent args) + { + UpdateUserInterface(uid); + foreach (var wire in component.WiresList) + { + wire.Action.Update(wire); + } + } + + private void OnWiresActionMessage(EntityUid uid, WiresComponent component, WiresActionMessage args) + { + if (args.Session.AttachedEntity == null) + { + return; + } + var player = (EntityUid) args.Session.AttachedEntity; + + if (!EntityManager.TryGetComponent(player, out HandsComponent? handsComponent)) + { + _popupSystem.PopupEntity(Loc.GetString("wires-component-ui-on-receive-message-no-hands"), uid, Filter.Entities(player)); + return; + } + + if (!_interactionSystem.InRangeUnobstructed(player, uid)) + { + _popupSystem.PopupEntity(Loc.GetString("wires-component-ui-on-receive-message-cannot-reach"), uid, Filter.Entities(player)); + return; + } + + var activeHand = handsComponent.ActiveHand; + + if (activeHand == null) + return; + + if (activeHand.HeldEntity == null) + return; + + var activeHandEntity = activeHand.HeldEntity.Value; + if (!EntityManager.TryGetComponent(activeHandEntity, out ToolComponent? tool)) + return; + + TryDoWireAction(uid, player, activeHandEntity, args.Id, args.Action, component, tool); + } + + private void OnWireDoAfter(EntityUid uid, WiresComponent component, OnWireDoAfterEvent args) + { + UpdateWires(args.Target, args.User, args.Tool, args.Id, args.Action, component); + } + + private void OnWireDoAfterCancel(EntityUid uid, WiresComponent component, OnWireDoAfterCancelEvent args) + { + component.WiresQueue.Remove(args.Id); + } + + private void OnInteractUsing(EntityUid uid, WiresComponent component, InteractUsingEvent args) + { + if (!EntityManager.TryGetComponent(args.Used, out ToolComponent? tool)) + return; + + if (component.IsPanelOpen && + _toolSystem.HasQuality(args.Used, "Cutting", tool) || + _toolSystem.HasQuality(args.Used, "Pulsing", tool)) + { + if (EntityManager.TryGetComponent(args.User, out ActorComponent? actor)) + { + _uiSystem.GetUiOrNull(uid, WiresUiKey.Key)?.Open(actor.PlayerSession); + args.Handled = true; + } + } + else if (_toolSystem.UseTool(args.Used, args.User, uid, 0f, ScrewTime, new string[]{ "Screwing" }, doAfterCompleteEvent:new WireToolFinishedEvent(uid), toolComponent:tool)) + { + args.Handled = true; + } + } + + private void OnToolFinished(WireToolFinishedEvent args) + { + if (!EntityManager.TryGetComponent(args.Target, out WiresComponent? component)) + return; + + component.IsPanelOpen = !component.IsPanelOpen; + UpdateAppearance(args.Target); + + if (component.IsPanelOpen) + { + SoundSystem.Play(Filter.Pvs(args.Target), component.ScrewdriverOpenSound.GetSound(), args.Target); + } + else + { + SoundSystem.Play(Filter.Pvs(args.Target), component.ScrewdriverCloseSound.GetSound(), args.Target); + _uiSystem.GetUiOrNull(args.Target, WiresUiKey.Key)?.CloseAll(); + } + } + + private void OnExamine(EntityUid uid, WiresComponent component, ExaminedEvent args) + { + args.PushMarkup(Loc.GetString(component.IsPanelOpen + ? "wires-component-on-examine-panel-open" + : "wires-component-on-examine-panel-closed")); + } + + private void OnMapInit(EntityUid uid, WiresComponent component, MapInitEvent args) + { + if (component.SerialNumber == null) + { + GenerateSerialNumber(uid, component); + } + + if (component.WireSeed == 0) + { + component.WireSeed = _random.Next(1, int.MaxValue); + UpdateUserInterface(uid); + } + } + #endregion + + #region Entity API + private void GenerateSerialNumber(EntityUid uid, WiresComponent? wires = null) + { + if (!Resolve(uid, ref wires)) + return; + + Span data = stackalloc char[9]; + data[4] = '-'; + + if (_random.Prob(0.01f)) + { + for (var i = 0; i < 4; i++) + { + // Cyrillic Letters + data[i] = (char) _random.Next(0x0410, 0x0430); + } + } + else + { + for (var i = 0; i < 4; i++) + { + // Letters + data[i] = (char) _random.Next(0x41, 0x5B); + } + } + + for (var i = 5; i < 9; i++) + { + // Digits + data[i] = (char) _random.Next(0x30, 0x3A); + } + + wires.SerialNumber = new string(data); + UpdateUserInterface(uid); + } + + private void UpdateAppearance(EntityUid uid, AppearanceComponent? appearance = null, WiresComponent? wires = null) + { + if (!Resolve(uid, ref appearance, ref wires)) + return; + + appearance.SetData(WiresVisuals.MaintenancePanelState, wires.IsPanelOpen && wires.IsPanelVisible); + } + + private void UpdateUserInterface(EntityUid uid, WiresComponent? wires = null, ServerUserInterfaceComponent? ui = null) + { + if (!Resolve(uid, ref wires, ref ui, false)) // logging this means that we get a bunch of errors + return; + + var clientList = new List(); + foreach (var entry in wires.WiresList) + { + clientList.Add(new ClientWire(entry.Id, entry.IsCut, entry.Color, + entry.Letter)); + + var statusData = entry.Action.GetStatusLightData(entry); + if (statusData != null && entry.Action.StatusKey != null) + { + wires.Statuses[entry.Action.StatusKey] = statusData; + } + } + + _uiSystem.GetUiOrNull(uid, WiresUiKey.Key)?.SetState( + new WiresBoundUserInterfaceState( + clientList.ToArray(), + wires.Statuses.Select(p => new StatusEntry(p.Key, p.Value)).ToArray(), + wires.BoardName, + wires.SerialNumber, + wires.WireSeed)); + } + + public void OpenUserInterface(EntityUid uid, IPlayerSession player) + { + _uiSystem.GetUiOrNull(uid, WiresUiKey.Key)?.Open(player); + } + + /// + /// Tries to get a wire on this entity by its integer id. + /// + /// The wire if found, otherwise null + public Wire? TryGetWire(EntityUid uid, int id, WiresComponent? wires = null) + { + if (!Resolve(uid, ref wires)) + return null; + + return id >= 0 && id < wires.WiresList.Count + ? wires.WiresList[id] + : null; + } + + /// + /// Tries to get all the wires on this entity by the wire action type. + /// + /// Enumerator of all wires in this entity according to the given type. + public IEnumerable TryGetWires(EntityUid uid, WiresComponent? wires = null) + { + if (!Resolve(uid, ref wires)) + yield break; + + foreach (var wire in wires.WiresList) + { + if (wire.GetType() == typeof(T)) + { + yield return wire; + } + } + } + + private void TryDoWireAction(EntityUid used, EntityUid user, EntityUid toolEntity, int id, WiresAction action, WiresComponent? wires = null, ToolComponent? tool = null) + { + if (!Resolve(used, ref wires) + || !Resolve(toolEntity, ref tool)) + return; + + if (wires.WiresQueue.Contains(id)) + return; + + var wire = TryGetWire(used, id, wires); + + if (wire == null) + return; + + switch (action) + { + case WiresAction.Cut: + if (!_toolSystem.HasQuality(toolEntity, "Cutting", tool)) + { + _popupSystem.PopupCursor(Loc.GetString("wires-component-ui-on-receive-message-need-wirecutters"), Filter.Entities(user)); + return; + } + + break; + case WiresAction.Mend: + if (!_toolSystem.HasQuality(toolEntity, "Cutting", tool)) + { + _popupSystem.PopupCursor(Loc.GetString("wires-component-ui-on-receive-message-need-wirecutters"), Filter.Entities(user)); + return; + } + + break; + case WiresAction.Pulse: + if (!_toolSystem.HasQuality(toolEntity, "Pulsing", tool)) + { + _popupSystem.PopupCursor(Loc.GetString("wires-component-ui-on-receive-message-need-multitool"), Filter.Entities(user)); + return; + } + + break; + } + + var args = new DoAfterEventArgs(user, ToolTime, default, used) + { + NeedHand = true, + BreakOnStun = true, + BreakOnDamage = true, + BreakOnUserMove = true, + TargetFinishedEvent = new OnWireDoAfterEvent + { + Target = used, + User = user, + Tool = toolEntity, + Action = action, + Id = id + }, + TargetCancelledEvent = new OnWireDoAfterCancelEvent + { + Id = id + } + }; + _doAfter.DoAfter(args); + + wires.WiresQueue.Add(id); + } + + + + private void UpdateWires(EntityUid used, EntityUid user, EntityUid toolEntity, int id, WiresAction action, WiresComponent? wires = null, ToolComponent? tool = null) + { + if (!Resolve(used, ref wires) + || !Resolve(toolEntity, ref tool)) + return; + + var wire = TryGetWire(used, id, wires); + + if (wire == null) + return; + + switch (action) + { + case WiresAction.Cut: + if (!_toolSystem.HasQuality(toolEntity, "Cutting", tool)) + { + _popupSystem.PopupCursor(Loc.GetString("wires-component-ui-on-receive-message-need-wirecutters"), Filter.Entities(user)); + return; + } + + _toolSystem.PlayToolSound(toolEntity, tool); + if (wire.Action.Cut(user, wire)) + { + wire.IsCut = true; + } + + UpdateUserInterface(used); + break; + case WiresAction.Mend: + if (!_toolSystem.HasQuality(toolEntity, "Cutting", tool)) + { + _popupSystem.PopupCursor(Loc.GetString("wires-component-ui-on-receive-message-need-wirecutters"), Filter.Entities(user)); + return; + } + + _toolSystem.PlayToolSound(toolEntity, tool); + if (wire.Action.Mend(user, wire)) + { + wire.IsCut = false; + } + + UpdateUserInterface(used); + break; + case WiresAction.Pulse: + if (!_toolSystem.HasQuality(toolEntity, "Pulsing", tool)) + { + _popupSystem.PopupCursor(Loc.GetString("wires-component-ui-on-receive-message-need-multitool"), Filter.Entities(user)); + return; + } + + if (wire.IsCut) + { + _popupSystem.PopupCursor(Loc.GetString("wires-component-ui-on-receive-message-cannot-pulse-cut-wire"), Filter.Entities(user)); + return; + } + + wire.Action.Pulse(user, wire); + + UpdateUserInterface(used); + SoundSystem.Play(Filter.Pvs(used), wires.PulseSound.GetSound(), used); + break; + } + + wires.WiresQueue.Remove(id); + } + + /// + /// Tries to get the stateful data stored in this entity's WiresComponent. + /// + /// The key that stores the data in the WiresComponent. + public bool TryGetData(EntityUid uid, object identifier, [NotNullWhen(true)] out T? data, WiresComponent? wires = null) + { + data = default(T); + if (!Resolve(uid, ref wires)) + return false; + + wires.StateData.TryGetValue(identifier, out var result); + + if (result is not T) + { + return false; + } + + data = (T) result; + + return true; + } + + /// + /// Sets data in the entity's WiresComponent state dictionary by key. + /// + /// The key that stores the data in the WiresComponent. + /// The data to store using the given identifier. + public void SetData(EntityUid uid, object identifier, object data, WiresComponent? wires = null) + { + if (!Resolve(uid, ref wires)) + return; + + if (wires.StateData.TryGetValue(identifier, out var storedMessage)) + { + if (storedMessage == data) + { + return; + } + } + + wires.StateData[identifier] = data; + UpdateUserInterface(uid, wires); + } + + /// + /// If this entity has data stored via this key in the WiresComponent it has + /// + public bool HasData(EntityUid uid, object identifier, WiresComponent? wires = null) + { + if (!Resolve(uid, ref wires)) + return false; + + return wires.StateData.ContainsKey(identifier); + } + + /// + /// Removes data from this entity stored in the given key from the entity's WiresComponent. + /// + /// The key that stores the data in the WiresComponent. + public void RemoveData(EntityUid uid, object identifier, WiresComponent? wires = null) + { + if (!Resolve(uid, ref wires)) + return; + + wires.StateData.Remove(identifier); + } + #endregion + + #region Layout Handling + private bool TryGetLayout(string id, [NotNullWhen(true)] out WireLayout? layout) + { + return _layouts.TryGetValue(id, out layout); + } + + private void AddLayout(string id, WireLayout layout) + { + _layouts.Add(id, layout); + } + + private void Reset(RoundRestartCleanupEvent args) + { + _layouts.Clear(); + } + #endregion + + #region Events + private sealed class WireToolFinishedEvent : EntityEventArgs + { + public EntityUid Target { get; } + + public WireToolFinishedEvent(EntityUid target) + { + Target = target; + } + } + + private sealed class OnWireDoAfterEvent : EntityEventArgs + { + public EntityUid User { get; set; } + public EntityUid Target { get; set; } + public EntityUid Tool { get; set; } + public WiresAction Action { get; set; } + public int Id { get; set; } + } + + private sealed class OnWireDoAfterCancelEvent : EntityEventArgs + { + public int Id { get; set; } + } + #endregion +} + +public sealed class Wire +{ + /// + /// The entity that registered the wire. + /// + public EntityUid Owner { get; } + + /// + /// Whether the wire is cut. + /// + public bool IsCut { get; set; } + + /// + /// Used in client-server communication to identify a wire without telling the client what the wire does. + /// + [ViewVariables] + public int Id { get; set; } + + /// + /// The color of the wire. + /// + [ViewVariables] + public WireColor Color { get; } + + /// + /// The greek letter shown below the wire. + /// + [ViewVariables] + public WireLetter Letter { get; } + + // The action that this wire performs upon activation. + public IWireAction Action { get; set; } + + public Wire(EntityUid owner, bool isCut, WireColor color, WireLetter letter, IWireAction action) + { + Owner = owner; + IsCut = isCut; + Color = color; + Letter = letter; + Action = action; + } +} + +// this is here so that when a DoAfter event is called, +// WiresSystem can call the action in question after the +// doafter is finished (either through cancellation +// or completion - this is implementation dependent) +public delegate void WireActionDelegate(Wire wire); + +// callbacks over the event bus, +// because async is banned +public sealed class TimedWireEvent : EntityEventArgs +{ + /// + /// The function to be called once + /// the timed event is complete. + /// + public WireActionDelegate Delegate { get; } + + /// + /// The wire tied to this timed wire event. + /// + public Wire Wire { get; } + + public TimedWireEvent(WireActionDelegate @delegate, Wire wire) + { + Delegate = @delegate; + Wire = wire; + } +} + +public sealed class WireLayout +{ + // why is this an ? + // List.Insert panics, + // and I needed a uniquer key for wires + // which allows me to have a unified identifier + [ViewVariables] public IReadOnlyDictionary Specifications { get; } + + public WireLayout(IReadOnlyDictionary specifications) + { + Specifications = specifications; + } + + public sealed class WireData + { + public WireLetter Letter { get; } + public WireColor Color { get; } + public int Position { get; } + + public WireData(WireLetter letter, WireColor color, int position) + { + Letter = letter; + Color = color; + Position = position; + } + } +} diff --git a/Content.Shared/Access/SharedAccessWire.cs b/Content.Shared/Access/SharedAccessWire.cs new file mode 100644 index 0000000000..d5d1b48c70 --- /dev/null +++ b/Content.Shared/Access/SharedAccessWire.cs @@ -0,0 +1,12 @@ +using Robust.Shared.Serialization; + +namespace Content.Shared.Access; + +[Serializable, NetSerializable] +public enum AccessWireActionKey : byte +{ + Key, + Status, + Pulsed, + PulseCancel +} diff --git a/Content.Shared/Atmos/Monitor/AtmosMonitorAlarmWireActionKeys.cs b/Content.Shared/Atmos/Monitor/AtmosMonitorAlarmWireActionKeys.cs new file mode 100644 index 0000000000..2f4fd74422 --- /dev/null +++ b/Content.Shared/Atmos/Monitor/AtmosMonitorAlarmWireActionKeys.cs @@ -0,0 +1,9 @@ +using Robust.Shared.Serialization; + +namespace Content.Shared.Atmos.Monitor; + +[Serializable, NetSerializable] +public enum AtmosMonitorAlarmWireActionKeys : byte +{ + Network, +} diff --git a/Content.Shared/Power/SharedPower.cs b/Content.Shared/Power/SharedPower.cs index 16f36ee765..2bed839a3d 100644 --- a/Content.Shared/Power/SharedPower.cs +++ b/Content.Shared/Power/SharedPower.cs @@ -10,4 +10,15 @@ namespace Content.Shared.Power Charging, Discharging, } + + [Serializable, NetSerializable] + public enum PowerWireActionKey : byte + { + Key, + Status, + Pulsed, + Electrified, + PulseCancel, + ElectrifiedCancel + } } diff --git a/Content.Shared/Wires/SharedWiresComponent.cs b/Content.Shared/Wires/SharedWiresComponent.cs index 72ba8d9d12..5daf6b4c78 100644 --- a/Content.Shared/Wires/SharedWiresComponent.cs +++ b/Content.Shared/Wires/SharedWiresComponent.cs @@ -5,177 +5,201 @@ using Robust.Shared.GameObjects; using Robust.Shared.Localization; using Robust.Shared.Maths; using Robust.Shared.Serialization; -using static Content.Shared.Wires.SharedWiresComponent; namespace Content.Shared.Wires { - [Virtual] - public class SharedWiresComponent : Component + [Serializable, NetSerializable] + public enum WiresVisuals : byte { - [Serializable, NetSerializable] - public enum WiresVisuals + MaintenancePanelState + } + + [Serializable, NetSerializable] + public enum WiresUiKey : byte + { + Key, + } + + [Serializable, NetSerializable] + public enum WiresAction : byte + { + Mend, + Cut, + Pulse, + } + + [Serializable, NetSerializable] + public enum StatusLightState : byte + { + Off, + On, + BlinkingFast, + BlinkingSlow + } + + [Serializable, NetSerializable] + public class WiresActionMessage : BoundUserInterfaceMessage + { + public readonly int Id; + public readonly WiresAction Action; + + public WiresActionMessage(int id, WiresAction action) { - MaintenancePanelState + Id = id; + Action = action; + } + } + + [SuppressMessage("ReSharper", "InconsistentNaming")] + [PublicAPI] + [Serializable, NetSerializable] + public enum WireLetter : byte + { + α, + β, + γ, + δ, + ε, + ζ, + η, + θ, + ι, + κ, + λ, + μ, + ν, + ξ, + ο, + π, + ρ, + σ, + τ, + υ, + φ, + χ, + ψ, + ω + } + + [PublicAPI] + [Serializable, NetSerializable] + public enum WireColor : byte + { + Red, + Blue, + Green, + Orange, + Brown, + Gold, + Gray, + Cyan, + Navy, + Purple, + Pink, + Fuchsia + } + + [Serializable, NetSerializable] + public struct StatusLightData + { + public StatusLightData(Color color, StatusLightState state, string text) + { + Color = color; + State = state; + Text = text; } - [Serializable, NetSerializable] - public enum WiresUiKey + public Color Color { get; } + public StatusLightState State { get; } + public string Text { get; } + + public override string ToString() { - Key, + return $"Color: {Color}, State: {State}, Text: {Text}"; + } + } + + [Serializable, NetSerializable] + public class WiresBoundUserInterfaceState : BoundUserInterfaceState + { + public string BoardName { get; } + public string? SerialNumber { get; } + public ClientWire[] WiresList { get; } + public StatusEntry[] Statuses { get; } + public int WireSeed { get; } + + public WiresBoundUserInterfaceState(ClientWire[] wiresList, StatusEntry[] statuses, string boardName, string? serialNumber, int wireSeed) + { + BoardName = boardName; + SerialNumber = serialNumber; + WireSeed = wireSeed; + WiresList = wiresList; + Statuses = statuses; + } + } + + [Serializable, NetSerializable] + public struct StatusEntry + { + /// + /// The key of this status, according to the status dictionary + /// server side. + /// + public readonly object Key; + + /// + /// The value of this status, according to the status dictionary + /// server side.. + /// + public readonly object Value; + + public StatusEntry(object key, object value) + { + Key = key; + Value = value; } - [Serializable, NetSerializable] - public enum WiresAction + public override string ToString() { - Mend, - Cut, - Pulse, + return $"{Key}, {Value}"; } + } - [Serializable, NetSerializable] - public enum StatusLightState + + /// + /// ClientWire, sent by the server so that the client knows + /// what wires there are on an entity. + /// + [Serializable, NetSerializable] + public class ClientWire + { + /// + /// ID of this wire, which corresponds to + /// the ID server side. + /// + public int Id; + + /// + /// Whether this wire is cut or not. + /// + public bool IsCut; + + /// + /// Current color of the wire. + /// + public WireColor Color; + + /// + /// Current letter of the wire. + /// + public WireLetter Letter; + + public ClientWire(int id, bool isCut, WireColor color, WireLetter letter) { - Off, - On, - BlinkingFast, - BlinkingSlow - } - - [SuppressMessage("ReSharper", "InconsistentNaming")] - [PublicAPI] - [Serializable, NetSerializable] - public enum WireLetter : byte - { - α, - β, - γ, - δ, - ε, - ζ, - η, - θ, - ι, - κ, - λ, - μ, - ν, - ξ, - ο, - π, - ρ, - σ, - τ, - υ, - φ, - χ, - ψ, - ω - } - - [PublicAPI] - [Serializable, NetSerializable] - public enum WireColor : byte - { - Red, - Blue, - Green, - Orange, - Brown, - Gold, - Gray, - Cyan, - Navy, - Purple, - Pink, - Fuchsia - } - - [Serializable, NetSerializable] - public struct StatusLightData - { - public StatusLightData(Color color, StatusLightState state, string text) - { - Color = color; - State = state; - Text = text; - } - - public Color Color { get; } - public StatusLightState State { get; } - public string Text { get; } - - public override string ToString() - { - return $"Color: {Color}, State: {State}, Text: {Text}"; - } - } - - [Serializable, NetSerializable] - public sealed class WiresBoundUserInterfaceState : BoundUserInterfaceState - { - public string BoardName { get; } - public string? SerialNumber { get; } - public ClientWire[] WiresList { get; } - public StatusEntry[] Statuses { get; } - public int WireSeed { get; } - - public WiresBoundUserInterfaceState(ClientWire[] wiresList, StatusEntry[] statuses, string boardName, string? serialNumber, int wireSeed) - { - BoardName = boardName; - SerialNumber = serialNumber; - WireSeed = wireSeed; - WiresList = wiresList; - Statuses = statuses; - } - } - - [Serializable, NetSerializable] - public struct StatusEntry - { - public readonly object Key; - public readonly object Value; - - public StatusEntry(object key, object value) - { - Key = key; - Value = value; - } - - public override string ToString() - { - return $"{Key}, {Value}"; - } - } - - - [Serializable, NetSerializable] - public sealed class ClientWire - { - public int Id; - public bool IsCut; - public WireColor Color; - public WireLetter Letter; - - public ClientWire(int id, bool isCut, WireColor color, WireLetter letter) - { - Id = id; - IsCut = isCut; - Letter = letter; - Color = color; - } - } - - [Serializable, NetSerializable] - public sealed class WiresActionMessage : BoundUserInterfaceMessage - { - public readonly int Id; - public readonly WiresAction Action; - - public WiresActionMessage(int id, WiresAction action) - { - Id = id; - Action = action; - } + Id = id; + IsCut = isCut; + Letter = letter; + Color = color; } } diff --git a/Content.Tests/Shared/WireHackingTest.cs b/Content.Tests/Shared/WireHackingTest.cs index 9e5c140799..a889f90595 100644 --- a/Content.Tests/Shared/WireHackingTest.cs +++ b/Content.Tests/Shared/WireHackingTest.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using Content.Shared.Wires; using NUnit.Framework; using Robust.UnitTesting; -using static Content.Shared.Wires.SharedWiresComponent; namespace Content.Tests.Shared { diff --git a/Resources/Locale/en-US/wires/components/wires-component.ftl b/Resources/Locale/en-US/wires/components/wires-component.ftl index 8fafd02718..204c2e3523 100644 --- a/Resources/Locale/en-US/wires/components/wires-component.ftl +++ b/Resources/Locale/en-US/wires/components/wires-component.ftl @@ -1,6 +1,7 @@ wires-component-ui-on-receive-message-no-hands = You have no hands. wires-component-ui-on-receive-message-cannot-reach = You can't reach there! wires-component-ui-on-receive-message-need-wirecutters = You need to hold a wirecutter in your hand! +wires-component-ui-on-receive-message-need-multitool = You need to hold a multitool in your hand! wires-component-ui-on-receive-message-cannot-pulse-cut-wire = You can't pulse a wire that's been cut! wires-component-on-examine-panel-open = The [color=lightgray]maintenance panel[/color] is [color=darkgreen]open[/color]. wires-component-on-examine-panel-closed = The [color=lightgray]maintenance panel[/color] is [color=darkgreen]closed[/color]. diff --git a/Resources/Prototypes/Entities/Structures/Doors/Airlocks/base_structureairlocks.yml b/Resources/Prototypes/Entities/Structures/Doors/Airlocks/base_structureairlocks.yml index 13333fc897..9cb2d6d29f 100644 --- a/Resources/Prototypes/Entities/Structures/Doors/Airlocks/base_structureairlocks.yml +++ b/Resources/Prototypes/Entities/Structures/Doors/Airlocks/base_structureairlocks.yml @@ -56,6 +56,9 @@ - type: ApcPowerReceiver powerLoad: 20 - type: ExtensionCableReceiver + - type: Electrified + enabled: false + usesApcPower: true - type: Wires BoardName: "Airlock Control" LayoutId: Airlock diff --git a/Resources/Prototypes/Entities/Structures/Machines/Computers/arcades.yml b/Resources/Prototypes/Entities/Structures/Machines/Computers/arcades.yml index 978cb63b07..d82e1143ee 100644 --- a/Resources/Prototypes/Entities/Structures/Machines/Computers/arcades.yml +++ b/Resources/Prototypes/Entities/Structures/Machines/Computers/arcades.yml @@ -40,6 +40,7 @@ components: - type: SpaceVillainArcade - type: Wires + LayoutId: Arcade BoardName: "Arcade" - type: ActivatableUI key: enum.SpaceVillainArcadeUiKey.Key @@ -63,6 +64,9 @@ - type: ActivatableUI key: enum.BlockGameUiKey.Key - type: ActivatableUIRequiresPower + - type: Wires + LayoutId: Arcade + BoardName: "Arcade" - type: UserInterface interfaces: - key: enum.BlockGameUiKey.Key diff --git a/Resources/Prototypes/Entities/Structures/Machines/vending_machines.yml b/Resources/Prototypes/Entities/Structures/Machines/vending_machines.yml index f85dabe93a..5f6dcc2dd3 100644 --- a/Resources/Prototypes/Entities/Structures/Machines/vending_machines.yml +++ b/Resources/Prototypes/Entities/Structures/Machines/vending_machines.yml @@ -52,6 +52,10 @@ BoardName: "Vending Machine" LayoutId: Vending - type: Anchorable + - type: DoAfter + - type: Electrified + enabled: false + usesApcPower: true - type: PointLight enabled: false castShadows: false diff --git a/Resources/Prototypes/Entities/Structures/Wallmounts/air_alarm.yml b/Resources/Prototypes/Entities/Structures/Wallmounts/air_alarm.yml index fb03b84016..4d3afeab7b 100644 --- a/Resources/Prototypes/Entities/Structures/Wallmounts/air_alarm.yml +++ b/Resources/Prototypes/Entities/Structures/Wallmounts/air_alarm.yml @@ -5,6 +5,9 @@ components: - type: WallMount - type: ApcPowerReceiver + - type: Electrified + enabled: false + usesApcPower: true - type: ExtensionCableReceiver - type: DeviceNetwork deviceNetId: Apc diff --git a/Resources/Prototypes/Entities/Structures/Wallmounts/fire_alarm.yml b/Resources/Prototypes/Entities/Structures/Wallmounts/fire_alarm.yml index d325fdca39..ef4b4e7ad5 100644 --- a/Resources/Prototypes/Entities/Structures/Wallmounts/fire_alarm.yml +++ b/Resources/Prototypes/Entities/Structures/Wallmounts/fire_alarm.yml @@ -5,6 +5,9 @@ components: - type: WallMount - type: ApcPowerReceiver + - type: Electrified + enabled: false + usesApcPower: true - type: ExtensionCableReceiver - type: DeviceNetwork deviceNetId: Apc diff --git a/Resources/Prototypes/Wires/layouts.yml b/Resources/Prototypes/Wires/layouts.yml new file mode 100644 index 0000000000..61413a2e6e --- /dev/null +++ b/Resources/Prototypes/Wires/layouts.yml @@ -0,0 +1,52 @@ +- type: wireLayout + id: Airlock + wires: + - !type:PowerWireAction + - !type:PowerWireAction + pulseTimeout: 15 + - !type:DoorBoltWireAction + - !type:DoorBoltLightWireAction + - !type:DoorTimingWireAction + - !type:DoorSafetyWireAction + +- type: wireLayout + parent: Airlock + id: AirlockSecurity + +- type: wireLayout + parent: Airlock + id: AirlockCommand + +- type: wireLayout + parent: Airlock + id: AirlockArmory + +- type: wireLayout + id: Vending + dummyWires: 2 + wires: + - !type:PowerWireAction + - !type:AccessWireAction + +- type: wireLayout + id: AirAlarm + wires: + - !type:PowerWireAction + - !type:AccessWireAction + - !type:AirAlarmPanicWire + - !type:AtmosMonitorDeviceNetWire + +- type: wireLayout + id: FireAlarm + wires: + - !type:PowerWireAction + - !type:AtmosMonitorDeviceNetWire + alarmOnPulse: true + +- type: wireLayout + id: Arcade + wires: + - !type:PowerWireAction + - !type:ArcadeOverflowWireAction + - !type:ArcadePlayerInvincibleWireAction + - !type:ArcadeEnemyInvincibleWireAction