diff --git a/Content.Client/GameObjects/Components/Body/BodyComponent.cs b/Content.Client/GameObjects/Components/Body/BodyComponent.cs index fb36486055..524513b372 100644 --- a/Content.Client/GameObjects/Components/Body/BodyComponent.cs +++ b/Content.Client/GameObjects/Components/Body/BodyComponent.cs @@ -1,4 +1,4 @@ -#nullable enable +#nullable enable using Content.Client.GameObjects.Components.Disposal; using Content.Client.GameObjects.Components.MedicalScanner; using Content.Shared.GameObjects.Components.Body; @@ -14,7 +14,8 @@ namespace Content.Client.GameObjects.Components.Body public bool CanDrop(CanDropEventArgs eventArgs) { if (eventArgs.Target.HasComponent() || - eventArgs.Target.HasComponent()) + eventArgs.Target.HasComponent() || + eventArgs.Target.HasComponent()) { return true; } diff --git a/Content.Client/GameObjects/Components/Configuration/ConfigurationBoundUserInterface.cs b/Content.Client/GameObjects/Components/Configuration/ConfigurationBoundUserInterface.cs new file mode 100644 index 0000000000..d173f061ed --- /dev/null +++ b/Content.Client/GameObjects/Components/Configuration/ConfigurationBoundUserInterface.cs @@ -0,0 +1,55 @@ +using Robust.Client.GameObjects.Components.UserInterface; +using Robust.Shared.GameObjects.Components.UserInterface; +using System.Collections.Generic; +using System.Text.RegularExpressions; +using static Content.Shared.GameObjects.Components.SharedConfigurationComponent; + +namespace Content.Client.GameObjects.Components.Wires +{ + public class ConfigurationBoundUserInterface : BoundUserInterface + { + public Regex Validation { get; internal set; } + + public ConfigurationBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey) + { + } + + private ConfigurationMenu _menu; + + protected override void Open() + { + base.Open(); + _menu = new ConfigurationMenu(this); + + _menu.OnClose += Close; + _menu.OpenCentered(); + } + + protected override void UpdateState(BoundUserInterfaceState state) + { + base.UpdateState(state); + _menu.Populate(state as ConfigurationBoundUserInterfaceState); + } + + protected override void ReceiveMessage(BoundUserInterfaceMessage message) + { + base.ReceiveMessage(message); + if (message is ValidationUpdateMessage msg) + { + Validation = new Regex(msg.ValidationString, RegexOptions.Compiled); + } + } + + public void SendConfiguration(Dictionary config) + { + SendMessage(new ConfigurationUpdatedMessage(config)); + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + + _menu.Close(); + } + } +} diff --git a/Content.Client/GameObjects/Components/Configuration/ConfigurationMenu.cs b/Content.Client/GameObjects/Components/Configuration/ConfigurationMenu.cs new file mode 100644 index 0000000000..d236ae0202 --- /dev/null +++ b/Content.Client/GameObjects/Components/Configuration/ConfigurationMenu.cs @@ -0,0 +1,176 @@ +using System.Collections.Generic; +using Namotion.Reflection; +using Robust.Client.UserInterface; +using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.CustomControls; +using Robust.Shared.Localization; +using Robust.Shared.Maths; +using static Content.Shared.GameObjects.Components.SharedConfigurationComponent; +using static Robust.Client.UserInterface.Controls.BaseButton; + +namespace Content.Client.GameObjects.Components.Wires +{ + public class ConfigurationMenu : SS14Window + { + public ConfigurationBoundUserInterface Owner { get; } + + private readonly VBoxContainer _baseContainer; + private readonly VBoxContainer _column; + private readonly HBoxContainer _row; + + private readonly List<(string name, LineEdit input)> _inputs; + + protected override Vector2? CustomSize => (300, 250); + + public ConfigurationMenu(ConfigurationBoundUserInterface owner) + { + Owner = owner; + + _inputs = new List<(string name, LineEdit input)>(); + + Title = Loc.GetString("Device Configuration"); + + var margin = new MarginContainer + { + MarginBottomOverride = 8, + MarginLeftOverride = 8, + MarginRightOverride = 8, + MarginTopOverride = 8 + }; + + _baseContainer = new VBoxContainer + { + SizeFlagsVertical = SizeFlags.FillExpand, + SizeFlagsHorizontal = SizeFlags.FillExpand + }; + + _column = new VBoxContainer + { + SeparationOverride = 16, + SizeFlagsVertical = SizeFlags.Fill + }; + + _row = new HBoxContainer + { + SeparationOverride = 16, + SizeFlagsHorizontal = SizeFlags.FillExpand + }; + + var buttonRow = new HBoxContainer + { + SizeFlagsHorizontal = SizeFlags.FillExpand + }; + + var spacer1 = new HBoxContainer + { + SizeFlagsHorizontal = SizeFlags.Expand + }; + + var spacer2 = new HBoxContainer() + { + SizeFlagsHorizontal = SizeFlags.Expand + }; + + var confirmButton = new Button + { + Text = Loc.GetString("Confirm"), + SizeFlagsHorizontal = SizeFlags.ShrinkCenter, + SizeFlagsVertical = SizeFlags.ShrinkCenter + }; + + confirmButton.OnButtonUp += OnConfirm; + buttonRow.AddChild(spacer1); + buttonRow.AddChild(confirmButton); + buttonRow.AddChild(spacer2); + + var outerColumn = new ScrollContainer + { + SizeFlagsVertical = SizeFlags.FillExpand, + SizeFlagsHorizontal = SizeFlags.FillExpand, + ModulateSelfOverride = Color.FromHex("#202025") + }; + + margin.AddChild(_column); + outerColumn.AddChild(margin); + _baseContainer.AddChild(outerColumn); + _baseContainer.AddChild(buttonRow); + Contents.AddChild(_baseContainer); + } + + public void Populate(ConfigurationBoundUserInterfaceState state) + { + _column.Children.Clear(); + _inputs.Clear(); + + foreach (var field in state.Config) + { + var margin = new MarginContainer + { + MarginRightOverride = 8 + }; + + var label = new Label + { + Name = field.Key, + Text = field.Key + ":", + SizeFlagsVertical = SizeFlags.ShrinkCenter, + SizeFlagsHorizontal = SizeFlags.FillExpand, + SizeFlagsStretchRatio = .2f, + CustomMinimumSize = new Vector2(60, 0) + }; + + var input = new LineEdit + { + Name = field.Key + "-input", + Text = field.Value, + IsValid = Validate, + SizeFlagsHorizontal = SizeFlags.FillExpand, + SizeFlagsStretchRatio = .8f + }; + + _inputs.Add((field.Key, input)); + + var row = new HBoxContainer(); + CopyProperties(_row, row); + + margin.AddChild(label); + row.AddChild(margin); + row.AddChild(input); + _column.AddChild(row); + } + } + + private void OnConfirm(ButtonEventArgs args) + { + var config = GenerateDictionary(_inputs, "Text"); + + Owner.SendConfiguration(config); + Close(); + } + + private bool Validate(string value) + { + return Owner.Validation == null || Owner.Validation.IsMatch(value); + } + + private Dictionary GenerateDictionary(List<(string name, TInput input)> inputs, string propertyName) where TInput : Control + { + var dictionary = new Dictionary(); + foreach (var input in inputs) + { + var value = input.input.TryGetPropertyValue(propertyName); + dictionary.Add(input.name, value); + } + + return dictionary; + } + + private static void CopyProperties(T from, T to) where T : Control + { + foreach (var property in from.AllAttachedProperties) + { + to.SetValue(property.Key, property.Value); + } + } + } +} diff --git a/Content.Client/GameObjects/Components/Disposal/DisposalMailingUnitBoundUserInterface.cs b/Content.Client/GameObjects/Components/Disposal/DisposalMailingUnitBoundUserInterface.cs new file mode 100644 index 0000000000..164ba9f6f5 --- /dev/null +++ b/Content.Client/GameObjects/Components/Disposal/DisposalMailingUnitBoundUserInterface.cs @@ -0,0 +1,73 @@ +#nullable enable +using JetBrains.Annotations; +using Robust.Client.GameObjects.Components.UserInterface; +using Robust.Client.UserInterface.Controls; +using Robust.Shared.GameObjects.Components.UserInterface; +using static Content.Shared.GameObjects.Components.Disposal.SharedDisposalMailingUnitComponent; + +namespace Content.Client.GameObjects.Components.Disposal +{ + /// + /// Initializes a and updates it when new server messages are received. + /// + [UsedImplicitly] + public class DisposalMailingUnitBoundUserInterface : BoundUserInterface + { + private DisposalMailingUnitWindow? _window; + + public DisposalMailingUnitBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey) + { + } + + private void ButtonPressed(UiButton button) + { + SendMessage(new UiButtonPressedMessage(button)); + } + + protected override void Open() + { + base.Open(); + + _window = new DisposalMailingUnitWindow(); + + _window.OpenCentered(); + _window.OnClose += Close; + + _window.Eject.OnPressed += _ => ButtonPressed(UiButton.Eject); + _window.Engage.OnPressed += _ => ButtonPressed(UiButton.Engage); + _window.Power.OnPressed += _ => ButtonPressed(UiButton.Power); + _window.TargetListContainer.OnItemSelected += TargetSelected; + + } + + + protected override void UpdateState(BoundUserInterfaceState state) + { + base.UpdateState(state); + + if (!(state is DisposalMailingUnitBoundUserInterfaceState cast)) + { + return; + } + + _window?.UpdateState(cast); + } + + private void TargetSelected(ItemList.ItemListSelectedEventArgs item) + { + SendMessage(new UiTargetUpdateMessage(_window?.TargetList[item.ItemIndex])); + //(ノ°Д°)ノ︵ ┻━┻ + if (_window != null) _window.Engage.Disabled = false; + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + + if (disposing) + { + _window?.Dispose(); + } + } + } +} diff --git a/Content.Client/GameObjects/Components/Disposal/DisposalMailingUnitComponent.cs b/Content.Client/GameObjects/Components/Disposal/DisposalMailingUnitComponent.cs new file mode 100644 index 0000000000..fd661f6afa --- /dev/null +++ b/Content.Client/GameObjects/Components/Disposal/DisposalMailingUnitComponent.cs @@ -0,0 +1,11 @@ +using Content.Shared.GameObjects.Components.Disposal; +using Robust.Shared.GameObjects; + +namespace Content.Client.GameObjects.Components.Disposal +{ + [RegisterComponent] + [ComponentReference(typeof(SharedDisposalMailingUnitComponent))] + public class DisposalMailingUnitComponent : SharedDisposalMailingUnitComponent + { + } +} diff --git a/Content.Client/GameObjects/Components/Disposal/DisposalMailingUnitWindow.cs b/Content.Client/GameObjects/Components/Disposal/DisposalMailingUnitWindow.cs new file mode 100644 index 0000000000..1849a33722 --- /dev/null +++ b/Content.Client/GameObjects/Components/Disposal/DisposalMailingUnitWindow.cs @@ -0,0 +1,285 @@ +using Content.Shared.GameObjects.Components.Disposal; +using Robust.Client.Graphics.Drawing; +using Robust.Client.UserInterface; +using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.CustomControls; +using Robust.Shared.Localization; +using Robust.Shared.Maths; +using System.Collections.Generic; +using static Content.Shared.GameObjects.Components.Disposal.SharedDisposalMailingUnitComponent; + +namespace Content.Client.GameObjects.Components.Disposal +{ + /// + /// Client-side UI used to control a + /// + public class DisposalMailingUnitWindow : SS14Window + { + private readonly Label _unitState; + private readonly ProgressBar _pressureBar; + private readonly Label _pressurePercentage; + public readonly Button Engage; + public readonly Button Eject; + public readonly Button Power; + + public readonly ItemList TargetListContainer; + public List TargetList; + private readonly Label _tagLabel; + + protected override Vector2? CustomSize => (460, 220); + + public DisposalMailingUnitWindow() + { + TargetList = new List(); + Contents.AddChild(new HBoxContainer + { + Children = + { + new MarginContainer + { + MarginLeftOverride = 8, + MarginRightOverride = 8, + SizeFlagsHorizontal = SizeFlags.FillExpand, + Children = + { + new VBoxContainer + { + SizeFlagsHorizontal = SizeFlags.FillExpand, + Children = + { + new HBoxContainer + { + Children = + { + new Label {Text = Loc.GetString("State: ")}, + new Control {CustomMinimumSize = (4, 0)}, + (_unitState = new Label {Text = Loc.GetString("Ready")}) + } + }, + new Control {CustomMinimumSize = (0, 10)}, + new HBoxContainer + { + SizeFlagsHorizontal = SizeFlags.FillExpand, + Children = + { + new Label {Text = Loc.GetString("Pressure:")}, + new Control {CustomMinimumSize = (4, 0)}, + (_pressureBar = new ProgressBar + { + CustomMinimumSize = (100, 20), + SizeFlagsHorizontal = SizeFlags.FillExpand, + MinValue = 0, + MaxValue = 1, + Page = 0, + Value = 0.5f, + Children = + { + (_pressurePercentage = new Label()) + } + }) + } + }, + new Control {CustomMinimumSize = (0, 10)}, + new HBoxContainer + { + SizeFlagsHorizontal = SizeFlags.FillExpand, + Children = + { + new Label {Text = Loc.GetString("Handle:")}, + new Control { + CustomMinimumSize = (4, 0), + SizeFlagsHorizontal = SizeFlags.FillExpand + }, + (Engage = new Button + { + CustomMinimumSize = (16, 0), + Text = Loc.GetString("Engage"), + ToggleMode = true, + Disabled = true + }) + } + }, + new Control {CustomMinimumSize = (0, 10)}, + new HBoxContainer + { + SizeFlagsHorizontal = SizeFlags.FillExpand, + Children = + { + new Label {Text = Loc.GetString("Eject:")}, + new Control { + CustomMinimumSize = (4, 0), + SizeFlagsHorizontal = SizeFlags.FillExpand + }, + (Eject = new Button { + CustomMinimumSize = (16, 0), + Text = Loc.GetString("Eject Contents"), + //SizeFlagsHorizontal = SizeFlags.ShrinkEnd + }) + } + }, + new Control {CustomMinimumSize = (0, 10)}, + new HBoxContainer + { + Children = + { + (Power = new CheckButton {Text = Loc.GetString("Power")}), + } + } + } + }, + } + }, + new MarginContainer + { + MarginLeftOverride = 12, + MarginRightOverride = 8, + SizeFlagsHorizontal = SizeFlags.FillExpand, + Children = + { + new VBoxContainer + { + SizeFlagsHorizontal = SizeFlags.Fill, + Children = + { + new HBoxContainer + { + Children = + { + new Label + { + Text = Loc.GetString("Select a destination:") + } + } + }, + new Control { CustomMinimumSize = new Vector2(0, 8) }, + new HBoxContainer + { + SizeFlagsVertical = SizeFlags.FillExpand, + Children = + { + (TargetListContainer = new ItemList + { + SelectMode = ItemList.ItemListSelectMode.Single, + SizeFlagsHorizontal = SizeFlags.FillExpand, + SizeFlagsVertical = SizeFlags.FillExpand + }) + } + }, + new PanelContainer + { + PanelOverride = new StyleBoxFlat + { + BackgroundColor = Color.FromHex("#ACBDBA") + }, + SizeFlagsHorizontal = SizeFlags.FillExpand, + CustomMinimumSize = new Vector2(0, 1), + }, + new HBoxContainer + { + Children = + { + new VBoxContainer + { + Children = + { + new MarginContainer + { + MarginLeftOverride = 4, + Children = + { + new HBoxContainer + { + SizeFlagsHorizontal = SizeFlags.FillExpand, + Children = + { + new Label + { + Text = Loc.GetString("This unit:") + }, + new Control + { + CustomMinimumSize = new Vector2(4, 0) + }, + (_tagLabel = new Label + { + Text = "-", + SizeFlagsVertical = SizeFlags.ShrinkEnd + }) + } + } + } + }, + } + } + } + } + } + } + } + } + } + }); + } + + private void UpdatePressureBar(float pressure) + { + _pressureBar.Value = pressure; + + var normalized = pressure / _pressureBar.MaxValue; + + const float leftHue = 0.0f; // Red + const float middleHue = 0.066f; // Orange + const float rightHue = 0.33f; // Green + const float saturation = 1.0f; // Uniform saturation + const float value = 0.8f; // Uniform value / brightness + const float alpha = 1.0f; // Uniform alpha + + // These should add up to 1.0 or your transition won't be smooth + const float leftSideSize = 0.5f; // Fraction of _chargeBar lerped from leftHue to middleHue + const float rightSideSize = 0.5f; // Fraction of _chargeBar lerped from middleHue to rightHue + + float finalHue; + if (normalized <= leftSideSize) + { + normalized /= leftSideSize; // Adjust range to 0.0 to 1.0 + finalHue = MathHelper.Lerp(leftHue, middleHue, normalized); + } + else + { + normalized = (normalized - leftSideSize) / rightSideSize; // Adjust range to 0.0 to 1.0. + finalHue = MathHelper.Lerp(middleHue, rightHue, normalized); + } + + // Check if null first to avoid repeatedly creating this. + _pressureBar.ForegroundStyleBoxOverride ??= new StyleBoxFlat(); + + var foregroundStyleBoxOverride = (StyleBoxFlat) _pressureBar.ForegroundStyleBoxOverride; + foregroundStyleBoxOverride.BackgroundColor = + Color.FromHsv(new Vector4(finalHue, saturation, value, alpha)); + + var percentage = pressure / _pressureBar.MaxValue * 100; + _pressurePercentage.Text = $" {percentage:0}%"; + } + + public void UpdateState(DisposalMailingUnitBoundUserInterfaceState state) + { + Title = state.UnitName; + _unitState.Text = state.UnitState; + UpdatePressureBar(state.Pressure); + Power.Pressed = state.Powered; + Engage.Pressed = state.Engaged; + PopulateTargetList(state.Tags); + _tagLabel.Text = state.Tag; + TargetList = state.Tags; + } + + private void PopulateTargetList(List tags) + { + TargetListContainer.Clear(); + foreach (var target in tags) + { + TargetListContainer.AddItem(target); + } + } + } +} diff --git a/Content.Client/GameObjects/EntitySystems/DoAfter/DoAfterSystem.cs b/Content.Client/GameObjects/EntitySystems/DoAfter/DoAfterSystem.cs index 42583cb010..7dfb8225d0 100644 --- a/Content.Client/GameObjects/EntitySystems/DoAfter/DoAfterSystem.cs +++ b/Content.Client/GameObjects/EntitySystems/DoAfter/DoAfterSystem.cs @@ -1,4 +1,4 @@ -#nullable enable +#nullable enable using System.Collections.Generic; using System.Linq; using Content.Client.GameObjects.Components; @@ -124,9 +124,7 @@ namespace Content.Client.GameObjects.EntitySystems.DoAfter if (doAfter.BreakOnTargetMove) { - var targetEntity = _entityManager.GetEntity(doAfter.TargetUid); - - if (targetEntity.Transform.Coordinates != doAfter.TargetGrid) + if (_entityManager.TryGetEntity(doAfter.TargetUid, out var targetEntity) && targetEntity.Transform.Coordinates != doAfter.TargetGrid) { comp.Cancel(id, currentTime); continue; diff --git a/Content.Client/IgnoredComponents.cs b/Content.Client/IgnoredComponents.cs index cabc82c641..c906233b6c 100644 --- a/Content.Client/IgnoredComponents.cs +++ b/Content.Client/IgnoredComponents.cs @@ -182,6 +182,7 @@ "GasCanisterPort", "Lung", "Cleanable", + "Configuration", "Brain", "PlantHolder", "SeedExtractor", diff --git a/Content.Server/DeviceNetwork/DeviceNetwork.cs b/Content.Server/DeviceNetwork/DeviceNetwork.cs new file mode 100644 index 0000000000..414cbe9cb8 --- /dev/null +++ b/Content.Server/DeviceNetwork/DeviceNetwork.cs @@ -0,0 +1,198 @@ +using Content.Server.Interfaces; +using Robust.Shared.Interfaces.Random; +using Robust.Shared.IoC; +using System.Collections.Generic; + +namespace Content.Server.GameObjects.EntitySystems.DeviceNetwork +{ + public delegate void OnReceiveNetMessage(int frequency, string sender, IReadOnlyDictionary payload, Metadata metadata, bool broadcast); + + public class DeviceNetwork : IDeviceNetwork + { + private const int PACKAGES_PER_TICK = 30; + + private readonly IRobustRandom _random = IoCManager.Resolve(); + private readonly Dictionary> _devices = new Dictionary>(); + private readonly Queue _packages = new Queue(); + + /// + public DeviceNetworkConnection Register(int netId, int frequency, OnReceiveNetMessage messageHandler, bool receiveAll = false) + { + var address = GenerateValidAddress(netId, frequency); + + var device = new NetworkDevice + { + Address = address, + Frequency = frequency, + ReceiveAll = receiveAll, + ReceiveNetMessage = messageHandler + }; + + AddDevice(netId, device); + + return new DeviceNetworkConnection(this, netId, address, frequency); + } + + public DeviceNetworkConnection Register(int netId, OnReceiveNetMessage messageHandler, bool receiveAll = false) + { + return Register(netId, 0, messageHandler, receiveAll); + } + + public void Update() + { + var i = PACKAGES_PER_TICK; + while (_packages.Count > 0 && i > 0) + { + i--; + + var package = _packages.Dequeue(); + + if (package.Broadcast) + { + BroadcastPackage(package); + continue; + } + + SendPackage(package); + } + } + + public bool EnqueuePackage(int netId, int frequency, string address, IReadOnlyDictionary data, string sender, Metadata metadata, bool broadcast = false) + { + if (!_devices.ContainsKey(netId)) + return false; + + var package = new NetworkPackage() + { + NetId = netId, + Frequency = frequency, + Address = address, + Broadcast = broadcast, + Data = data, + Sender = sender, + Metadata = metadata + }; + + _packages.Enqueue(package); + return true; + } + + public void RemoveDevice(int netId, int frequency, string address) + { + var device = DeviceWithAddress(netId, frequency, address); + _devices[netId].Remove(device); + } + + public void SetDeviceReceiveAll(int netId, int frequency, string address, bool receiveAll) + { + var device = DeviceWithAddress(netId, frequency, address); + device.ReceiveAll = receiveAll; + } + + public bool GetDeviceReceiveAll(int netId, int frequency, string address) + { + var device = DeviceWithAddress(netId, frequency, address); + return device.ReceiveAll; + } + + private string GenerateValidAddress(int netId, int frequency) + { + var unique = false; + var devices = DevicesForFrequency(netId, frequency); + var address = ""; + + while (!unique) + { + address = _random.Next().ToString("x"); + unique = !devices.Exists(device => device.Address == address); + } + + return address; + } + + private void AddDevice(int netId, NetworkDevice networkDevice) + { + if(!_devices.ContainsKey(netId)) + _devices[netId] = new List(); + + _devices[netId].Add(networkDevice); + } + + private List DevicesForFrequency(int netId, int frequency) + { + if (!_devices.ContainsKey(netId)) + return new List(); + + var result = _devices[netId].FindAll(device => device.Frequency == frequency); + + return result; + } + + private NetworkDevice DeviceWithAddress(int netId, int frequency, string address) + { + var devices = DevicesForFrequency(netId, frequency); + + var device = devices.Find(device => device.Address == address); + + return device; + } + + private List DevicesWithReceiveAll(int netId, int frequency) + { + if (!_devices.ContainsKey(netId)) + return new List(); + + var result = _devices[netId].FindAll(device => device.Frequency == frequency && device.ReceiveAll); + + return result; + } + + private void BroadcastPackage(NetworkPackage package) + { + var devices = DevicesForFrequency(package.NetId, package.Frequency); + SendToDevices(devices, package, true); + } + + private void SendPackage(NetworkPackage package) + { + var devices = DevicesWithReceiveAll(package.NetId, package.Frequency); + var device = DeviceWithAddress(package.NetId, package.Frequency, package.Address); + + devices.Add(device); + + SendToDevices(devices, package, false); + } + + private void SendToDevices(List devices, NetworkPackage package, bool broadcast) + { + for (var index = 0; index < devices.Count; index++) + { + var device = devices[index]; + if (device.Address == package.Sender) + continue; + + device.ReceiveNetMessage(package.Frequency, package.Sender, package.Data, package.Metadata, broadcast); + } + } + + internal class NetworkDevice + { + public int Frequency; + public string Address; + public OnReceiveNetMessage ReceiveNetMessage; + public bool ReceiveAll; + } + + internal class NetworkPackage + { + public int NetId; + public int Frequency; + public string Address; + public bool Broadcast; + public IReadOnlyDictionary Data { get; set; } + public Metadata Metadata; + public string Sender; + + } + } +} diff --git a/Content.Server/DeviceNetwork/DeviceNetworkConnection.cs b/Content.Server/DeviceNetwork/DeviceNetworkConnection.cs new file mode 100644 index 0000000000..204f00edf6 --- /dev/null +++ b/Content.Server/DeviceNetwork/DeviceNetworkConnection.cs @@ -0,0 +1,72 @@ +using Content.Server.Interfaces; +using Robust.Shared.ViewVariables; +using System.Collections.Generic; + +namespace Content.Server.GameObjects.EntitySystems.DeviceNetwork +{ + public class DeviceNetworkConnection : IDeviceNetworkConnection + { + private readonly DeviceNetwork _network; + [ViewVariables] + private readonly int _netId; + + [ViewVariables] + public bool Open { get; internal set; } + [ViewVariables] + public string Address { get; internal set; } + [ViewVariables] + public int Frequency { get; internal set; } + + [ViewVariables] + public bool RecieveAll + { + get => _network.GetDeviceReceiveAll(_netId, Frequency, Address); + set => _network.SetDeviceReceiveAll(_netId, Frequency, Address, value); + } + + public DeviceNetworkConnection(DeviceNetwork network, int netId, string address, int frequency) + { + _network = network; + _netId = netId; + Open = true; + Address = address; + Frequency = frequency; + } + + public bool Send(int frequency, string address, IReadOnlyDictionary payload, Metadata metadata) + { + return Open && _network.EnqueuePackage(_netId, frequency, address, payload, Address, metadata); + } + + public bool Send(int frequency, string address, Dictionary payload) + { + return Send(frequency, address, payload); + } + + public bool Send(string address, Dictionary payload) + { + return Send(0, address, payload); + } + + public bool Broadcast(int frequency, IReadOnlyDictionary payload, Metadata metadata) + { + return Open && _network.EnqueuePackage(_netId, frequency, "", payload, Address, metadata, true); + } + + public bool Broadcast(int frequency, Dictionary payload) + { + return Broadcast(frequency, payload); + } + + public bool Broadcast(Dictionary payload) + { + return Broadcast(0, payload); + } + + public void Close() + { + _network.RemoveDevice(_netId, Frequency, Address); + Open = false; + } + } +} diff --git a/Content.Server/DeviceNetwork/Metadata.cs b/Content.Server/DeviceNetwork/Metadata.cs new file mode 100644 index 0000000000..1066676523 --- /dev/null +++ b/Content.Server/DeviceNetwork/Metadata.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace Content.Server.GameObjects.EntitySystems.DeviceNetwork +{ + public class Metadata : Dictionary + { + public bool TryParseMetadata(string key, [NotNullWhen(true)] out T data) + { + if(TryGetValue(key, out var value) && value is T typedValue) + { + data = typedValue; + return true; + } + + data = default; + return false; + } + } +} diff --git a/Content.Server/DeviceNetwork/NetworkConnections/BaseNetworkConnection.cs b/Content.Server/DeviceNetwork/NetworkConnections/BaseNetworkConnection.cs new file mode 100644 index 0000000000..9706bb36b9 --- /dev/null +++ b/Content.Server/DeviceNetwork/NetworkConnections/BaseNetworkConnection.cs @@ -0,0 +1,70 @@ +using Content.Server.Interfaces; +using Robust.Shared.IoC; +using Robust.Shared.ViewVariables; +using System.Collections.Generic; + +namespace Content.Server.GameObjects.EntitySystems.DeviceNetwork +{ + public abstract class BaseNetworkConnection : IDeviceNetworkConnection + { + protected readonly DeviceNetworkConnection Connection; + + protected OnReceiveNetMessage MessageHandler; + + [ViewVariables] + public bool Open => Connection.Open; + [ViewVariables] + public string Address => Connection.Address; + [ViewVariables] + public int Frequency => Connection.Frequency; + + protected BaseNetworkConnection(int netId, int frequency, OnReceiveNetMessage onReceive, bool receiveAll) + { + var network = IoCManager.Resolve(); + Connection = network.Register(netId, frequency, OnReceiveNetMessage, receiveAll); + MessageHandler = onReceive; + + } + + public bool Send(int frequency, string address, Dictionary payload) + { + var data = ManipulatePayload(payload); + var metadata = GetMetadata(); + return Connection.Send(frequency, address, data, metadata); + } + + public bool Send(string address, Dictionary payload) + { + return Send(0, address, payload); + } + + public bool Broadcast(int frequency, Dictionary payload) + { + var data = ManipulatePayload(payload); + var metadata = GetMetadata(); + return Connection.Broadcast(frequency, data, metadata); + } + + public bool Broadcast(Dictionary payload) + { + return Broadcast(0, payload); + } + + public void Close() + { + Connection.Close(); + } + + private void OnReceiveNetMessage(int frequency, string sender, IReadOnlyDictionary payload, Metadata metadata, bool broadcast) + { + if (CanReceive(frequency, sender, payload, metadata, broadcast)) + { + MessageHandler(frequency, sender, payload, metadata, broadcast); + } + } + + protected abstract bool CanReceive(int frequency, string sender, IReadOnlyDictionary payload, Metadata metadata, bool broadcast); + protected abstract Dictionary ManipulatePayload(Dictionary payload); + protected abstract Metadata GetMetadata(); + } +} diff --git a/Content.Server/DeviceNetwork/NetworkConnections/WiredNetworkConnection.cs b/Content.Server/DeviceNetwork/NetworkConnections/WiredNetworkConnection.cs new file mode 100644 index 0000000000..d11e230059 --- /dev/null +++ b/Content.Server/DeviceNetwork/NetworkConnections/WiredNetworkConnection.cs @@ -0,0 +1,84 @@ +using Content.Server.GameObjects.Components.NodeContainer; +using Content.Server.GameObjects.Components.NodeContainer.NodeGroups; +using Content.Server.GameObjects.Components.Power.ApcNetComponents; +using Robust.Shared.Interfaces.GameObjects; +using System.Collections.Generic; + +namespace Content.Server.GameObjects.EntitySystems.DeviceNetwork +{ + public class WiredNetworkConnection : BaseNetworkConnection + { + public const string WIRENET = "powernet"; + + private readonly IEntity _owner; + + public WiredNetworkConnection(OnReceiveNetMessage onReceive, bool receiveAll, IEntity owner) : base(NetworkUtils.WIRED, 0, onReceive, receiveAll) + { + _owner = owner; + } + + protected override bool CanReceive(int frequency, string sender, IReadOnlyDictionary payload, Metadata metadata, bool broadcast) + { + if (_owner.Deleted) + { + Connection.Close(); + return false; + } + + if (_owner.TryGetComponent(out var powerReceiver) + && TryGetWireNet(powerReceiver, out var ownNet) + && metadata.TryParseMetadata(WIRENET, out var senderNet)) + { + return ownNet.Equals(senderNet); + } + + return false; + } + + protected override Metadata GetMetadata() + { + if (_owner.Deleted) + { + Connection.Close(); + return new Metadata(); + } + + if (_owner.TryGetComponent(out var powerReceiver) + && TryGetWireNet(powerReceiver, out var net)) + { + var metadata = new Metadata + { + {WIRENET, net } + }; + + return metadata; + } + + return new Metadata(); + } + + protected override Dictionary ManipulatePayload(Dictionary payload) + { + return payload; + } + + private bool TryGetWireNet(PowerReceiverComponent powerReceiver, out INodeGroup net) + { + if (powerReceiver.Provider is PowerProviderComponent && powerReceiver.Provider.ProviderOwner.TryGetComponent(out var nodeContainer)) + { + var nodes = nodeContainer.Nodes; + for (var index = 0; index < nodes.Count; index++) + { + if (nodes[index].NodeGroupID == NodeGroupID.WireNet) + { + net = nodes[index].NodeGroup; + return true; + } + } + + } + net = default; + return false; + } + } +} diff --git a/Content.Server/DeviceNetwork/NetworkConnections/WirelessNetworkConnection.cs b/Content.Server/DeviceNetwork/NetworkConnections/WirelessNetworkConnection.cs new file mode 100644 index 0000000000..5763ef3d3b --- /dev/null +++ b/Content.Server/DeviceNetwork/NetworkConnections/WirelessNetworkConnection.cs @@ -0,0 +1,63 @@ +using Robust.Shared.Interfaces.GameObjects; +using Robust.Shared.Maths; +using System; +using System.Collections.Generic; + +namespace Content.Server.GameObjects.EntitySystems.DeviceNetwork +{ + public class WirelessNetworkConnection : BaseNetworkConnection + { + public const string WIRELESS_POSITION = "position"; + + private readonly IEntity _owner; + + private float _range; + public float Range { get => _range; set => _range = Math.Abs(value); } + + public WirelessNetworkConnection(int frequency, OnReceiveNetMessage onReceive, bool receiveAll, IEntity owner, float range) : base(NetworkUtils.WIRELESS, frequency, onReceive, receiveAll) + { + _owner = owner; + Range = range; + } + + protected override bool CanReceive(int frequency, string sender, IReadOnlyDictionary payload, Metadata metadata, bool broadcast) + { + if (_owner.Deleted) + { + Connection.Close(); + return false; + } + + if (metadata.TryParseMetadata(WIRELESS_POSITION, out var position)) + { + var ownPosition = _owner.Transform.WorldPosition; + var distance = (ownPosition - position).Length; + return distance <= Range; + } + //Only receive packages with the same frequency + return frequency == Frequency; + } + + protected override Metadata GetMetadata() + { + if (_owner.Deleted) + { + Connection.Close(); + return new Metadata(); + } + + var position = _owner.Transform.WorldPosition; + var metadata = new Metadata + { + {WIRELESS_POSITION, position} + }; + + return metadata; + } + + protected override Dictionary ManipulatePayload(Dictionary payload) + { + return payload; + } + } +} diff --git a/Content.Server/DeviceNetwork/NetworkUtils.cs b/Content.Server/DeviceNetwork/NetworkUtils.cs new file mode 100644 index 0000000000..349d1a6c4d --- /dev/null +++ b/Content.Server/DeviceNetwork/NetworkUtils.cs @@ -0,0 +1,38 @@ +using Content.Server.Interfaces; +using System; +using System.Collections.Generic; +using System.Text; + +namespace Content.Server.GameObjects.EntitySystems.DeviceNetwork +{ + /// + /// A collection of utilities to help with using device networks + /// + public static class NetworkUtils + { + public const int PRIVATE = 0; + public const int WIRED = 1; + public const int WIRELESS = 2; + + public const string COMMAND = "command"; + public const string MESSAGE = "message"; + public const string PING = "ping"; + + /// + /// Handles responding to pings. + /// + public static void PingResponse(T connection, string sender, IReadOnlyDictionary payload, string message = "") where T : IDeviceNetworkConnection + { + if (payload.TryGetValue(COMMAND, out var command) && command == PING) + { + var response = new Dictionary + { + {COMMAND, "ping_response"}, + {MESSAGE, message} + }; + + connection.Send(connection.Frequency, sender, response); + } + } + } +} diff --git a/Content.Server/GameObjects/Components/ConfigurationComponent.cs b/Content.Server/GameObjects/Components/ConfigurationComponent.cs new file mode 100644 index 0000000000..77049e6260 --- /dev/null +++ b/Content.Server/GameObjects/Components/ConfigurationComponent.cs @@ -0,0 +1,118 @@ +using Content.Server.GameObjects.Components.Interactable; +using Content.Server.Utility; +using Content.Shared.GameObjects.Components; +using Content.Shared.GameObjects.Components.Interactable; +using Content.Shared.Interfaces.GameObjects.Components; +using Robust.Server.GameObjects.Components.UserInterface; +using Robust.Server.Interfaces.GameObjects; +using Robust.Shared.GameObjects; +using Robust.Shared.Serialization; +using Robust.Shared.ViewVariables; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading.Tasks; + +namespace Content.Server.GameObjects.Components +{ + [RegisterComponent] + [ComponentReference(typeof(SharedConfigurationComponent))] + public class ConfigurationComponent : SharedConfigurationComponent, IInteractUsing + { + [ViewVariables] private BoundUserInterface UserInterface => Owner.GetUIOrNull(ConfigurationUiKey.Key); + + [ViewVariables] + private readonly Dictionary _config = new Dictionary(); + + private Regex _validation; + + public event Action> OnConfigUpdate; + + public override void Initialize() + { + base.Initialize(); + + if (UserInterface != null) + { + UserInterface.OnReceiveMessage += UserInterfaceOnReceiveMessage; + } + } + + public override void ExposeData(ObjectSerializer serializer) + { + base.ExposeData(serializer); + + serializer.DataReadWriteFunction("keys", new List(), + (list) => FillConfiguration(list, _config, ""), + () => _config.Keys.ToList()); + + serializer.DataReadFunction("vailidation", "^[a-zA-Z0-9 ]*$", value => _validation = new Regex("^[a-zA-Z0-9 ]*$", RegexOptions.Compiled)); + } + + public string GetConfig(string name) + { + return _config.GetValueOrDefault(name); + } + + protected override void Startup() + { + base.Startup(); + UpdateUserInterface(); + } + + async Task IInteractUsing.InteractUsing(InteractUsingEventArgs eventArgs) + { + if (UserInterface == null || !eventArgs.User.TryGetComponent(out IActorComponent actor)) + return false; + + if (!eventArgs.Using.TryGetComponent(out var tool)) + return false; + + if (!await tool.UseTool(eventArgs.User, Owner, 0.2f, ToolQuality.Multitool)) + return false; + + UpdateUserInterface(); + UserInterface.Open(actor.playerSession); + UserInterface.SendMessage(new ValidationUpdateMessage(_validation.ToString()), actor.playerSession); + return true; + } + + private void UserInterfaceOnReceiveMessage(ServerBoundUserInterfaceMessage serverMsg) + { + var message = serverMsg.Message; + var config = new Dictionary(_config); + + if (message is ConfigurationUpdatedMessage msg) + { + foreach (var key in config.Keys) + { + var value = msg.Config.GetValueOrDefault(key); + + if (_validation != null && !_validation.IsMatch(value) && value != "") + continue; + + _config[key] = value; + } + + OnConfigUpdate(_config); + } + } + + private void UpdateUserInterface() + { + if (UserInterface == null) + return; + + UserInterface.SetState(new ConfigurationBoundUserInterfaceState(_config)); + } + + private static void FillConfiguration(List list, Dictionary configuration, T value){ + for (var index = 0; index < list.Count; index++) + { + configuration.Add(list[index], value); + } + } + } +} diff --git a/Content.Server/GameObjects/Components/Disposal/DisposalEntryComponent.cs b/Content.Server/GameObjects/Components/Disposal/DisposalEntryComponent.cs index 5dacccd52d..4128234253 100644 --- a/Content.Server/GameObjects/Components/Disposal/DisposalEntryComponent.cs +++ b/Content.Server/GameObjects/Components/Disposal/DisposalEntryComponent.cs @@ -33,7 +33,7 @@ namespace Content.Server.GameObjects.Components.Disposal return TryInsert(holderComponent); } - private bool TryInsert(DisposalHolderComponent holder) + public bool TryInsert(DisposalHolderComponent holder) { if (!Contents.Insert(holder.Owner)) { diff --git a/Content.Server/GameObjects/Components/Disposal/DisposalMailingUnitComponent.cs b/Content.Server/GameObjects/Components/Disposal/DisposalMailingUnitComponent.cs new file mode 100644 index 0000000000..67cac05d66 --- /dev/null +++ b/Content.Server/GameObjects/Components/Disposal/DisposalMailingUnitComponent.cs @@ -0,0 +1,840 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +using Content.Server.GameObjects.Components.GUI; +using Content.Server.GameObjects.Components.Items.Storage; +using Content.Server.GameObjects.Components.Power.ApcNetComponents; +using Content.Server.GameObjects.EntitySystems.DeviceNetwork; +using Content.Server.GameObjects.EntitySystems.DoAfter; +using Content.Server.Interfaces; +using Content.Server.Interfaces.GameObjects.Components.Items; +using Content.Server.Utility; +using Content.Shared.GameObjects.Components.Body; +using Content.Shared.GameObjects.Components.Disposal; +using Content.Shared.GameObjects.EntitySystems; +using Content.Shared.GameObjects.Verbs; +using Content.Shared.Interfaces; +using Content.Shared.Interfaces.GameObjects.Components; +using Robust.Server.GameObjects; +using Robust.Server.GameObjects.Components.Container; +using Robust.Server.GameObjects.Components.UserInterface; +using Robust.Server.GameObjects.EntitySystems; +using Robust.Server.Interfaces.GameObjects; +using Robust.Shared.Audio; +using Robust.Shared.Containers; +using Robust.Shared.GameObjects; +using Robust.Shared.GameObjects.Components; +using Robust.Shared.GameObjects.Components.Transform; +using Robust.Shared.GameObjects.Systems; +using Robust.Shared.Interfaces.GameObjects; +using Robust.Shared.Interfaces.Timing; +using Robust.Shared.IoC; +using Robust.Shared.Localization; +using Robust.Shared.Log; +using Robust.Shared.Serialization; +using Robust.Shared.ViewVariables; +using Timer = Robust.Shared.Timers.Timer; + +namespace Content.Server.GameObjects.Components.Disposal +{ + [RegisterComponent] + [ComponentReference(typeof(SharedDisposalMailingUnitComponent))] + [ComponentReference(typeof(IActivate))] + [ComponentReference(typeof(IInteractUsing))] + public class DisposalMailingUnitComponent : SharedDisposalMailingUnitComponent, IInteractHand, IActivate, IInteractUsing, IDragDropOn + { + [Dependency] private readonly IGameTiming _gameTiming = default!; + + private const string HolderPrototypeId = "DisposalHolder"; + + /// + /// The delay for an entity trying to move out of this unit. + /// + private static readonly TimeSpan ExitAttemptDelay = TimeSpan.FromSeconds(0.5); + + /// + /// Last time that an entity tried to exit this disposal unit. + /// + [ViewVariables] + private TimeSpan _lastExitAttempt; + + public static readonly Regex TagRegex = new Regex("^[a-zA-Z0-9, ]*$", RegexOptions.Compiled); + + /// + /// The current pressure of this disposal unit. + /// Prevents it from flushing if it is not equal to or bigger than 1. + /// + [ViewVariables] + private float _pressure; + + private bool _engaged; + + [ViewVariables(VVAccess.ReadWrite)] + private TimeSpan _automaticEngageTime; + + [ViewVariables(VVAccess.ReadWrite)] + private TimeSpan _flushDelay; + + [ViewVariables(VVAccess.ReadWrite)] + private float _entryDelay; + + /// + /// Token used to cancel the automatic engage of a disposal unit + /// after an entity enters it. + /// + private CancellationTokenSource? _automaticEngageToken; + + /// + /// Container of entities inside this disposal unit. + /// + [ViewVariables] + private Container _container = default!; + + [ViewVariables] + private WiredNetworkConnection? _connection; + + [ViewVariables] public IReadOnlyList ContainedEntities => _container.ContainedEntities; + + [ViewVariables] + private readonly List _targetList = new List(); + + [ViewVariables] + private string _target = ""; + + [ViewVariables(VVAccess.ReadWrite)] + private string _tag = ""; + + [ViewVariables] + public bool Powered => + !Owner.TryGetComponent(out PowerReceiverComponent? receiver) || + receiver.Powered; + + [ViewVariables] + private PressureState State => _pressure >= 1 ? PressureState.Ready : PressureState.Pressurizing; + + [ViewVariables(VVAccess.ReadWrite)] + private bool Engaged + { + get => _engaged; + set + { + var oldEngaged = _engaged; + _engaged = value; + + if (oldEngaged == value) + { + return; + } + + UpdateVisualState(); + } + } + + [ViewVariables] private BoundUserInterface? UserInterface => Owner.GetUIOrNull(DisposalMailingUnitUiKey.Key); + + private DisposalMailingUnitBoundUserInterfaceState? _lastUiState; + + /// + /// Store the translated state. + /// + private (PressureState State, string Localized) _locState; + + public bool CanInsert(IEntity entity) + { + if (!Anchored) + { + return false; + } + + if (!entity.TryGetComponent(out IPhysicsComponent? physics) || + !physics.CanCollide) + { + return false; + } + + if (!entity.HasComponent() && + !entity.HasComponent()) + { + return false; + } + return _container.CanInsert(entity); + } + + private void TryQueueEngage() + { + if (!Powered && ContainedEntities.Count == 0) + { + return; + } + + _automaticEngageToken = new CancellationTokenSource(); + + Timer.Spawn(_automaticEngageTime, () => + { + if (!TryFlush()) + { + TryQueueEngage(); + } + }, _automaticEngageToken.Token); + } + + private void AfterInsert(IEntity entity) + { + TryQueueEngage(); + + if (entity.TryGetComponent(out IActorComponent? actor)) + { + UserInterface?.Close(actor.playerSession); + } + + UpdateVisualState(); + } + + public async Task TryInsert(IEntity entity, IEntity? user = default) + { + if (!CanInsert(entity)) + return false; + + if (user != null && _entryDelay > 0f) + { + var doAfterSystem = EntitySystem.Get(); + + var doAfterArgs = new DoAfterEventArgs(user, _entryDelay, default, Owner) + { + BreakOnDamage = true, + BreakOnStun = true, + BreakOnTargetMove = true, + BreakOnUserMove = true, + NeedHand = false, + }; + + var result = await doAfterSystem.DoAfter(doAfterArgs); + + if (result == DoAfterStatus.Cancelled) + return false; + + } + + if (!_container.Insert(entity)) + return false; + + AfterInsert(entity); + + return true; + } + + private bool TryDrop(IEntity user, IEntity entity) + { + if (!user.TryGetComponent(out HandsComponent? hands)) + { + return false; + } + + if (!CanInsert(entity) || !hands.Drop(entity, _container)) + { + return false; + } + + AfterInsert(entity); + + return true; + } + + private void Remove(IEntity entity) + { + _container.Remove(entity); + + if (ContainedEntities.Count == 0) + { + _automaticEngageToken?.Cancel(); + _automaticEngageToken = null; + } + + UpdateVisualState(); + } + + private bool CanFlush() + { + return _pressure >= 1 && Powered && Anchored; + } + + private void ToggleEngage() + { + Engaged ^= true; + + if (Engaged && CanFlush()) + { + Timer.Spawn(_flushDelay, () => TryFlush()); + } + } + + public bool TryFlush() + { + if (!CanFlush()) + { + return false; + } + + var snapGrid = Owner.GetComponent(); + var entry = snapGrid + .GetLocal() + .FirstOrDefault(entity => entity.HasComponent()); + + if (entry == null) + { + return false; + } + + var entryComponent = entry.GetComponent(); + var entities = _container.ContainedEntities.ToList(); + foreach (var entity in _container.ContainedEntities.ToList()) + { + _container.Remove(entity); + } + + var holder = CreateTaggedHolder(entities, _target); + + entryComponent.TryInsert(holder); + + _automaticEngageToken?.Cancel(); + _automaticEngageToken = null; + + _pressure = 0; + + Engaged = false; + + UpdateVisualState(true); + UpdateInterface(); + + if (_connection != null) + { + var data = new Dictionary + { + { NetworkUtils.COMMAND, NET_CMD_SENT }, + { NET_SRC, _tag }, + { NET_TARGET, _target } + }; + + _connection.Broadcast(_connection.Frequency, data); + } + + return true; + } + + private DisposalHolderComponent CreateTaggedHolder(IReadOnlyCollection entities, string tag) + { + var holder = Owner.EntityManager.SpawnEntity(HolderPrototypeId, Owner.Transform.MapPosition); + var holderComponent = holder.GetComponent(); + + holderComponent.Tags.Add(tag); + holderComponent.Tags.Add(TAGS_MAIL); + + foreach (var entity in entities) + { + holderComponent.TryInsert(entity); + } + + return holderComponent; + } + + private void UpdateTargetList() + { + _targetList.Clear(); + var payload = new Dictionary + { + { NetworkUtils.COMMAND, NET_CMD_REQUEST } + }; + + _connection?.Broadcast(_connection.Frequency, payload); + } + + private void TryEjectContents() + { + foreach (var entity in _container.ContainedEntities.ToArray()) + { + Remove(entity); + } + } + + private void TogglePower() + { + if (!Owner.TryGetComponent(out PowerReceiverComponent? receiver)) + { + return; + } + + receiver.PowerDisabled = !receiver.PowerDisabled; + UpdateInterface(); + } + + private DisposalMailingUnitBoundUserInterfaceState GetInterfaceState() + { + string stateString; + + if (_locState.State != State) + { + stateString = Loc.GetString($"{State}"); + _locState = (State, stateString); + } + else + { + stateString = _locState.Localized; + } + + return new DisposalMailingUnitBoundUserInterfaceState(Owner.Name, stateString, _pressure, Powered, Engaged, _tag, _targetList, _target); + } + + private void UpdateInterface(bool checkEqual = true) + { + var state = GetInterfaceState(); + + if (checkEqual && _lastUiState != null && _lastUiState.Equals(state)) + { + return; + } + + _lastUiState = state; + UserInterface?.SetState((DisposalMailingUnitBoundUserInterfaceState) state.Clone()); + } + + private bool PlayerCanUse(IEntity? player) + { + if (player == null) + { + return false; + } + + if (!ActionBlockerSystem.CanInteract(player) || + !ActionBlockerSystem.CanUse(player)) + { + return false; + } + + return true; + } + + private void OnUiReceiveMessage(ServerBoundUserInterfaceMessage obj) + { + if (obj.Session.AttachedEntity == null) + { + return; + } + + if (!PlayerCanUse(obj.Session.AttachedEntity)) + { + return; + } + + if (obj.Message is UiButtonPressedMessage buttonMessage) + { + switch (buttonMessage.Button) + { + case UiButton.Eject: + TryEjectContents(); + break; + case UiButton.Engage: + ToggleEngage(); + break; + case UiButton.Power: + TogglePower(); + EntitySystem.Get().PlayFromEntity("/Audio/Machines/machine_switch.ogg", Owner, AudioParams.Default.WithVolume(-2f)); + + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + + if (obj.Message is UiTargetUpdateMessage tagMessage && TagRegex.IsMatch(tagMessage.Target)) + { + _target = tagMessage.Target; + } + } + + private void OnConfigUpdate(Dictionary config) + { + if (config.TryGetValue("Tag", out var tag)) + _tag = tag; + } + + private void UpdateVisualState() + { + UpdateVisualState(false); + } + + private void UpdateVisualState(bool flush) + { + if (!Owner.TryGetComponent(out AppearanceComponent? appearance)) + { + return; + } + + + if (!Anchored) + { + appearance.SetData(Visuals.VisualState, VisualState.UnAnchored); + appearance.SetData(Visuals.Handle, HandleState.Normal); + appearance.SetData(Visuals.Light, LightState.Off); + return; + } + else if (_pressure < 1) + { + appearance.SetData(Visuals.VisualState, VisualState.Charging); + } + else + { + appearance.SetData(Visuals.VisualState, VisualState.Anchored); + } + + appearance.SetData(Visuals.Handle, Engaged + ? HandleState.Engaged + : HandleState.Normal); + + if (!Powered) + { + appearance.SetData(Visuals.Light, LightState.Off); + return; + } + + if (flush) + { + appearance.SetData(Visuals.VisualState, VisualState.Flushing); + appearance.SetData(Visuals.Light, LightState.Off); + return; + } + + if (ContainedEntities.Count > 0) + { + appearance.SetData(Visuals.Light, LightState.Full); + return; + } + + appearance.SetData(Visuals.Light, _pressure < 1 + ? LightState.Charging + : LightState.Ready); + } + + public override void Update(float frameTime) + { + base.Update(frameTime); + if (!Powered) + { + return; + } + + var oldPressure = _pressure; + + _pressure = _pressure + frameTime > 1 + ? 1 + : _pressure + 0.05f * frameTime; + + if (oldPressure < 1 && _pressure >= 1) + { + UpdateVisualState(); + + if (Engaged) + { + TryFlush(); + } + } + + UpdateInterface(); + } + + private void PowerStateChanged(object? sender, PowerStateEventArgs args) + { + if (!args.Powered) + { + _automaticEngageToken?.Cancel(); + _automaticEngageToken = null; + } + + UpdateVisualState(); + + if (Engaged && !TryFlush()) + { + TryQueueEngage(); + } + } + + public override void ExposeData(ObjectSerializer serializer) + { + base.ExposeData(serializer); + + serializer.DataReadWriteFunction( + "pressure", + 1.0f, + pressure => _pressure = pressure, + () => _pressure); + + serializer.DataReadWriteFunction( + "automaticEngageTime", + 30, + seconds => _automaticEngageTime = TimeSpan.FromSeconds(seconds), + () => (int) _automaticEngageTime.TotalSeconds); + + serializer.DataReadWriteFunction( + "flushDelay", + 3, + seconds => _flushDelay = TimeSpan.FromSeconds(seconds), + () => (int) _flushDelay.TotalSeconds); + + serializer.DataReadWriteFunction( + "entryDelay", + 0.5f, + seconds => _entryDelay = seconds, + () => (int) _entryDelay); + + serializer.DataField(ref _tag, "Tag", ""); + } + + public override void Initialize() + { + base.Initialize(); + + _container = ContainerManagerComponent.Ensure(Name, Owner); + + if (UserInterface != null) + { + UserInterface.OnReceiveMessage += OnUiReceiveMessage; + } + + var network = IoCManager.Resolve(); + _connection = new WiredNetworkConnection(OnReceiveNetMessage, false, Owner); + + if (Owner.TryGetComponent(out var configuration)) + configuration.OnConfigUpdate += OnConfigUpdate; + + UpdateInterface(); + } + + protected override void Startup() + { + base.Startup(); + + if(!Owner.HasComponent()) + { + Logger.WarningS("VitalComponentMissing", $"Disposal unit {Owner.Uid} is missing an anchorable component"); + } + + if (Owner.TryGetComponent(out IPhysicsComponent? physics)) + { + physics.AnchoredChanged += UpdateVisualState; + } + + if (Owner.TryGetComponent(out PowerReceiverComponent? receiver)) + { + receiver.OnPowerStateChanged += PowerStateChanged; + } + + UpdateTargetList(); + UpdateVisualState(); + } + + public override void OnRemove() + { + if (Owner.TryGetComponent(out IPhysicsComponent? physics)) + { + physics.AnchoredChanged -= UpdateVisualState; + } + + if (Owner.TryGetComponent(out PowerReceiverComponent? receiver)) + { + receiver.OnPowerStateChanged -= PowerStateChanged; + } + + if (_container != null) + { + foreach (var entity in _container.ContainedEntities.ToArray()) + { + _container.ForceRemove(entity); + } + } + + UserInterface?.CloseAll(); + + _automaticEngageToken?.Cancel(); + _automaticEngageToken = null; + + _container = null!; + + _connection!.Close(); + + base.OnRemove(); + } + + public override void HandleMessage(ComponentMessage message, IComponent? component) + { + base.HandleMessage(message, component); + + switch (message) + { + case RelayMovementEntityMessage msg: + if (!msg.Entity.TryGetComponent(out HandsComponent? hands) || + hands.Count == 0 || + _gameTiming.CurTime < _lastExitAttempt + ExitAttemptDelay) + { + break; + } + + _lastExitAttempt = _gameTiming.CurTime; + Remove(msg.Entity); + break; + } + } + + private void OnReceiveNetMessage(int frequency, string sender, IReadOnlyDictionary payload, object _, bool broadcast) + { + if (payload.TryGetValue(NetworkUtils.COMMAND, out var command) && Powered) + { + if (command == NET_CMD_RESPONSE && payload.TryGetValue(NET_TAG, out var tag)) + { + _targetList.Add(tag); + UpdateInterface(false); + } + + if (command == NET_CMD_REQUEST) + { + if (_tag == "" || !Powered) + return; + + var data = new Dictionary + { + {NetworkUtils.COMMAND, NET_CMD_RESPONSE}, + {NET_TAG, _tag} + }; + + _connection?.Send(frequency, sender, data); + } + } + } + + private bool IsValidInteraction(ITargetedInteractEventArgs eventArgs) + { + if (!ActionBlockerSystem.CanInteract(eventArgs.User)) + { + Owner.PopupMessage(eventArgs.User, Loc.GetString("You can't do that!")); + return false; + } + + if (ContainerHelpers.IsInContainer(eventArgs.User)) + { + Owner.PopupMessage(eventArgs.User, Loc.GetString("You can't reach there!")); + return false; + } + // This popup message doesn't appear on clicks, even when code was seperate. Unsure why. + + if (!eventArgs.User.HasComponent()) + { + Owner.PopupMessage(eventArgs.User, Loc.GetString("You have no hands!")); + return false; + } + + return true; + } + + + bool IInteractHand.InteractHand(InteractHandEventArgs eventArgs) + { + if (!eventArgs.User.TryGetComponent(out IActorComponent? actor)) + { + return false; + } + + // Duplicated code here, not sure how else to get actor inside to make UserInterface happy. + + if (IsValidInteraction(eventArgs)) + { + UpdateTargetList(); + UpdateInterface(false); + UserInterface?.Open(actor.playerSession); + return true; + } + + return false; + } + + void IActivate.Activate(ActivateEventArgs eventArgs) + { + if (!eventArgs.User.TryGetComponent(out IActorComponent? actor)) + { + return; + } + + if (IsValidInteraction(eventArgs)) + { + UserInterface?.Open(actor.playerSession); + } + + return; + } + + + async Task IInteractUsing.InteractUsing(InteractUsingEventArgs eventArgs) + { + return TryDrop(eventArgs.User, eventArgs.Using); + } + + bool IDragDropOn.CanDragDropOn(DragDropEventArgs eventArgs) + { + return CanInsert(eventArgs.Dragged); + } + + bool IDragDropOn.DragDropOn(DragDropEventArgs eventArgs) + { + _ = TryInsert(eventArgs.Dragged, eventArgs.User); + return true; + } + + [Verb] + private sealed class SelfInsertVerb : Verb + { + protected override void GetData(IEntity user, DisposalMailingUnitComponent component, VerbData data) + { + data.Visibility = VerbVisibility.Invisible; + + if (!ActionBlockerSystem.CanInteract(user) || + component.ContainedEntities.Contains(user)) + { + return; + } + + data.Visibility = VerbVisibility.Visible; + data.Text = Loc.GetString("Jump inside"); + } + + protected override void Activate(IEntity user, DisposalMailingUnitComponent component) + { + _ = component.TryInsert(user, user); + } + } + + [Verb] + private sealed class FlushVerb : Verb + { + protected override void GetData(IEntity user, DisposalMailingUnitComponent component, VerbData data) + { + data.Visibility = VerbVisibility.Invisible; + + if (!ActionBlockerSystem.CanInteract(user) || + component.ContainedEntities.Contains(user)) + { + return; + } + + data.Visibility = VerbVisibility.Visible; + data.Text = Loc.GetString("Flush"); + } + + protected override void Activate(IEntity user, DisposalMailingUnitComponent component) + { + component.Engaged = true; + component.TryFlush(); + } + } + } +} diff --git a/Content.Server/GameObjects/Components/NodeContainer/NodeGroups/INodeGroup.cs b/Content.Server/GameObjects/Components/NodeContainer/NodeGroups/INodeGroup.cs index e57ce1db5d..c193922a7b 100644 --- a/Content.Server/GameObjects/Components/NodeContainer/NodeGroups/INodeGroup.cs +++ b/Content.Server/GameObjects/Components/NodeContainer/NodeGroups/INodeGroup.cs @@ -25,7 +25,7 @@ namespace Content.Server.GameObjects.Components.NodeContainer.NodeGroups void RemakeGroup(); } - [NodeGroup(NodeGroupID.Default)] + [NodeGroup(NodeGroupID.Default, NodeGroupID.WireNet)] public class BaseNodeGroup : INodeGroup { [ViewVariables] diff --git a/Content.Server/GameObjects/Components/NodeContainer/NodeGroups/NodeGroupFactory.cs b/Content.Server/GameObjects/Components/NodeContainer/NodeGroups/NodeGroupFactory.cs index 81e21dae26..26559fbd23 100644 --- a/Content.Server/GameObjects/Components/NodeContainer/NodeGroups/NodeGroupFactory.cs +++ b/Content.Server/GameObjects/Components/NodeContainer/NodeGroups/NodeGroupFactory.cs @@ -64,5 +64,6 @@ Apc, AMEngine, Pipe, + WireNet } } diff --git a/Content.Server/GameObjects/Components/Power/ApcNetComponents/PowerProviderComponent.cs b/Content.Server/GameObjects/Components/Power/ApcNetComponents/PowerProviderComponent.cs index 4b53fcec71..f462b3f3a0 100644 --- a/Content.Server/GameObjects/Components/Power/ApcNetComponents/PowerProviderComponent.cs +++ b/Content.Server/GameObjects/Components/Power/ApcNetComponents/PowerProviderComponent.cs @@ -2,10 +2,8 @@ using System.Collections.Generic; using System.Linq; using Content.Server.GameObjects.Components.NodeContainer.NodeGroups; -using Robust.Server.Interfaces.GameObjects; using Robust.Shared.GameObjects; -using Robust.Shared.Interfaces.Map; -using Robust.Shared.IoC; +using Robust.Shared.Interfaces.GameObjects; using Robust.Shared.Serialization; using Robust.Shared.ViewVariables; @@ -19,6 +17,8 @@ namespace Content.Server.GameObjects.Components.Power.ApcNetComponents void AddReceiver(PowerReceiverComponent receiver); void RemoveReceiver(PowerReceiverComponent receiver); + + public IEntity ProviderOwner { get; } } [RegisterComponent] @@ -26,6 +26,8 @@ namespace Content.Server.GameObjects.Components.Power.ApcNetComponents { public override string Name => "PowerProvider"; + public IEntity ProviderOwner => Owner; + /// /// The max distance this can transmit power to s from. /// @@ -126,6 +128,7 @@ namespace Content.Server.GameObjects.Components.Power.ApcNetComponents { public void AddReceiver(PowerReceiverComponent receiver) { } public void RemoveReceiver(PowerReceiverComponent receiver) { } + public IEntity ProviderOwner => default; } } } diff --git a/Content.Server/GameObjects/Components/Power/ApcNetComponents/PowerReceiverComponent.cs b/Content.Server/GameObjects/Components/Power/ApcNetComponents/PowerReceiverComponent.cs index a6e924e4f0..c84c416a04 100644 --- a/Content.Server/GameObjects/Components/Power/ApcNetComponents/PowerReceiverComponent.cs +++ b/Content.Server/GameObjects/Components/Power/ApcNetComponents/PowerReceiverComponent.cs @@ -8,7 +8,6 @@ using Robust.Server.Interfaces.GameObjects; using Robust.Shared.GameObjects; using Robust.Shared.GameObjects.ComponentDependencies; using Robust.Shared.GameObjects.Components; -using Robust.Shared.Interfaces.Map; using Robust.Shared.IoC; using Robust.Shared.Localization; using Robust.Shared.Serialization; @@ -234,7 +233,7 @@ namespace Content.Server.GameObjects.Components.Power.ApcNetComponents public void Examine(FormattedMessage message, bool inDetailsRange) { - message.AddMarkup(Loc.GetString("It appears to be {0}.", this.Powered ? "[color=darkgreen]powered[/color]" : "[color=darkred]un-powered[/color]")); + message.AddMarkup(Loc.GetString("It appears to be {0}.", Powered ? "[color=darkgreen]powered[/color]" : "[color=darkred]un-powered[/color]")); } } diff --git a/Content.Server/GameObjects/EntitySystems/DeviceNetworkSystem.cs b/Content.Server/GameObjects/EntitySystems/DeviceNetworkSystem.cs new file mode 100644 index 0000000000..f5b99a5988 --- /dev/null +++ b/Content.Server/GameObjects/EntitySystems/DeviceNetworkSystem.cs @@ -0,0 +1,28 @@ +using Content.Server.Interfaces; +using Robust.Shared.GameObjects.Systems; +using Robust.Shared.IoC; + +namespace Content.Server.GameObjects.EntitySystems.DeviceNetwork +{ + public class DeviceNetworkSystem : EntitySystem + { + private IDeviceNetwork _network; + + public override void Initialize() + { + base.Initialize(); + + _network = IoCManager.Resolve(); + } + + public override void Update(float frameTime) + { + base.Update(frameTime); + + if (_network == null) + return; + //(ノ°Д°)ノ︵ ┻━┻ + _network.Update(); + } + } +} diff --git a/Content.Server/Interfaces/IDeviceNetwork.cs b/Content.Server/Interfaces/IDeviceNetwork.cs new file mode 100644 index 0000000000..242abd51ec --- /dev/null +++ b/Content.Server/Interfaces/IDeviceNetwork.cs @@ -0,0 +1,25 @@ +using System; +using Content.Server.GameObjects.EntitySystems.DeviceNetwork; + +namespace Content.Server.Interfaces +{ + /// + /// Package based device network allowing devices to communicate with eachother + /// + public interface IDeviceNetwork + { + /// + /// Registers a device with the device network + /// + /// The id of the network to register with + /// The frequency the device receives packages on. Wired networks use frequency 0 + /// The delegate that gets called when the device receives a message + /// If the device should receive all packages on its frequency or only ones addressed to itself + /// + public DeviceNetworkConnection Register(int netId, int frequency, OnReceiveNetMessage messageHandler, bool receiveAll = false); + /// + public DeviceNetworkConnection Register(int netId, OnReceiveNetMessage messageHandler, bool receiveAll = false); + + public void Update(); + } +} diff --git a/Content.Server/Interfaces/IDeviceNetworkConnection.cs b/Content.Server/Interfaces/IDeviceNetworkConnection.cs new file mode 100644 index 0000000000..854acf78db --- /dev/null +++ b/Content.Server/Interfaces/IDeviceNetworkConnection.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Content.Server.Interfaces +{ + public interface IDeviceNetworkConnection + { + public int Frequency { get; } + /// + /// Sends a package to a specific device + /// + /// The frequency the package should be send on + /// The target devices address + /// + /// + public bool Send(int frequency, string address, Dictionary payload); + /// + public bool Send(string address, Dictionary payload); + /// + /// Sends a package to all devices + /// + /// The frequency the package should be send on + /// + /// + public bool Broadcast(int frequency, Dictionary payload); + /// + public bool Broadcast(Dictionary payload); + } +} diff --git a/Content.Server/ServerContentIoC.cs b/Content.Server/ServerContentIoC.cs index dadb213851..f9c86be44d 100644 --- a/Content.Server/ServerContentIoC.cs +++ b/Content.Server/ServerContentIoC.cs @@ -6,6 +6,7 @@ using Content.Server.Database; using Content.Server.GameObjects.Components.Mobs.Speech; using Content.Server.GameObjects.Components.NodeContainer.NodeGroups; using Content.Server.GameObjects.Components.Power.PowerNetComponents; +using Content.Server.GameObjects.EntitySystems.DeviceNetwork; using Content.Server.GameTicking; using Content.Server.Interfaces; using Content.Server.Interfaces.Chat; @@ -44,6 +45,7 @@ namespace Content.Server IoCManager.Register(); IoCManager.Register(); IoCManager.Register(); + IoCManager.Register(); } } } diff --git a/Content.Shared/GameObjects/Components/Disposal/SharedDisposalMailingUnitComponent.cs b/Content.Shared/GameObjects/Components/Disposal/SharedDisposalMailingUnitComponent.cs new file mode 100644 index 0000000000..5c9def4322 --- /dev/null +++ b/Content.Shared/GameObjects/Components/Disposal/SharedDisposalMailingUnitComponent.cs @@ -0,0 +1,109 @@ +using System; +using System.Collections.Generic; +using Robust.Shared.GameObjects.Components; +using Robust.Shared.GameObjects.Components.UserInterface; +using Robust.Shared.Serialization; +using Robust.Shared.Utility; + +namespace Content.Shared.GameObjects.Components.Disposal +{ + public abstract class SharedDisposalMailingUnitComponent : SharedDisposalUnitComponent, ICollideSpecial + { + public override string Name => "DisposalMailingUnit"; + + public const string TAGS_MAIL = "mail"; + + public const string NET_TAG = "tag"; + public const string NET_SRC = "src"; + public const string NET_TARGET = "target"; + public const string NET_CMD_SENT = "mail_sent"; + public const string NET_CMD_REQUEST = "get_mailer_tag"; + public const string NET_CMD_RESPONSE = "mailer_tag"; + + [Serializable, NetSerializable] + public new enum UiButton + { + Eject, + Engage, + Power + } + + [Serializable, NetSerializable] + public class DisposalMailingUnitBoundUserInterfaceState : BoundUserInterfaceState, IEquatable, ICloneable + { + public readonly string UnitName; + public readonly string UnitState; + public readonly float Pressure; + public readonly bool Powered; + public readonly bool Engaged; + public readonly string Tag; + public readonly List Tags; + public readonly string Target; + + public DisposalMailingUnitBoundUserInterfaceState(string unitName, string unitState, float pressure, bool powered, + bool engaged, string tag, List tags, string target) + { + UnitName = unitName; + UnitState = unitState; + Pressure = pressure; + Powered = powered; + Engaged = engaged; + Tag = tag; + Tags = tags; + Target = target; + } + + public object Clone() + { + return new DisposalMailingUnitBoundUserInterfaceState(UnitName, UnitState, Pressure, Powered, Engaged, Tag, (List)Tags.Clone(), Target); + } + + public bool Equals(DisposalMailingUnitBoundUserInterfaceState other) + { + if (other is null) return false; + if (ReferenceEquals(this, other)) return true; + return UnitName == other.UnitName && + UnitState == other.UnitState && + Powered == other.Powered && + Engaged == other.Engaged && + Pressure.Equals(other.Pressure) && + Tag == other.Tag && + Target == other.Target; + } + } + + /// + /// Message data sent from client to server when a mailing unit ui button is pressed. + /// + [Serializable, NetSerializable] + public new class UiButtonPressedMessage : BoundUserInterfaceMessage + { + public readonly UiButton Button; + + public UiButtonPressedMessage(UiButton button) + { + Button = button; + } + } + + /// + /// Message data sent from client to server when the mailing units target is updated. + /// + [Serializable, NetSerializable] + public class UiTargetUpdateMessage : BoundUserInterfaceMessage + { + public readonly string Target; + + public UiTargetUpdateMessage(string target) + { + Target = target; + } + } + + [Serializable, NetSerializable] + public enum DisposalMailingUnitUiKey + { + Key + } + } +} diff --git a/Content.Shared/GameObjects/Components/SharedConfigurationComponent.cs b/Content.Shared/GameObjects/Components/SharedConfigurationComponent.cs new file mode 100644 index 0000000000..413507d3b3 --- /dev/null +++ b/Content.Shared/GameObjects/Components/SharedConfigurationComponent.cs @@ -0,0 +1,55 @@ +using Robust.Shared.GameObjects; +using Robust.Shared.GameObjects.Components.UserInterface; +using Robust.Shared.Serialization; +using System; +using System.Collections.Generic; + +namespace Content.Shared.GameObjects.Components +{ + public class SharedConfigurationComponent : Component + { + public override string Name => "Configuration"; + + [Serializable, NetSerializable] + public class ConfigurationBoundUserInterfaceState : BoundUserInterfaceState + { + public readonly Dictionary Config; + + public ConfigurationBoundUserInterfaceState(Dictionary config) + { + Config = config; + } + } + + /// + /// Message data sent from client to server when the device configuration is updated. + /// + [Serializable, NetSerializable] + public class ConfigurationUpdatedMessage : BoundUserInterfaceMessage + { + public readonly Dictionary Config; + + public ConfigurationUpdatedMessage(Dictionary config) + { + Config = config; + } + } + + [Serializable, NetSerializable] + public class ValidationUpdateMessage : BoundUserInterfaceMessage + { + public readonly string ValidationString; + + public ValidationUpdateMessage(string validationString) + { + ValidationString = validationString; + } + } + + [Serializable, NetSerializable] + public enum ConfigurationUiKey + { + Key + } + } +} diff --git a/Content.Shared/GameObjects/EntitySystems/SharedDisposalUnitSystem.cs b/Content.Shared/GameObjects/EntitySystems/SharedDisposalUnitSystem.cs index c019620ef0..d957a9f203 100644 --- a/Content.Shared/GameObjects/EntitySystems/SharedDisposalUnitSystem.cs +++ b/Content.Shared/GameObjects/EntitySystems/SharedDisposalUnitSystem.cs @@ -13,6 +13,11 @@ namespace Content.Shared.GameObjects.EntitySystems { comp.Update(frameTime); } + + foreach (var comp in ComponentManager.EntityQuery()) + { + comp.Update(frameTime); + } } } } diff --git a/Resources/Prototypes/Entities/Constructible/Power/wires.yml b/Resources/Prototypes/Entities/Constructible/Power/wires.yml index 6f4bb120aa..bfbce84ce5 100644 --- a/Resources/Prototypes/Entities/Constructible/Power/wires.yml +++ b/Resources/Prototypes/Entities/Constructible/Power/wires.yml @@ -41,6 +41,8 @@ nodes: - !type:AdjacentNode nodeGroupID: HVPower + - !type:AdjacentNode + nodeGroupID: WireNet - type: Wire wireDroppedOnCutPrototype: HVWireStack1 wireType: HighVoltage @@ -71,6 +73,8 @@ nodes: - !type:AdjacentNode nodeGroupID: MVPower + - !type:AdjacentNode + nodeGroupID: WireNet - type: Wire wireDroppedOnCutPrototype: MVWireStack1 wireType: MediumVoltage @@ -101,6 +105,8 @@ nodes: - !type:AdjacentNode nodeGroupID: Apc + - !type:AdjacentNode + nodeGroupID: WireNet - type: PowerProvider voltage: Apc - type: Wire diff --git a/Resources/Prototypes/Entities/Constructible/Specific/disposal.yml b/Resources/Prototypes/Entities/Constructible/Specific/disposal.yml index 5391b72a0b..375ebb16ee 100644 --- a/Resources/Prototypes/Entities/Constructible/Specific/disposal.yml +++ b/Resources/Prototypes/Entities/Constructible/Specific/disposal.yml @@ -330,3 +330,74 @@ - !type:PhysShapeAabb bounds: "-0.5,-0.5,0.25,0.25" layer: [ Underplating ] + +- type: entity + id: DisposalMailingUnit + name: disposal mailing unit + description: A pneumatic waste disposal unit + placement: + mode: SnapgridCenter + snap: + - Disposal + components: + - type: Sprite + netsync: false + sprite: Constructible/Power/disposal.rsi + layers: + - state: condisposal + map: ["enum.DisposalUnitVisualLayers.Base"] + - state: dispover-handle + map: ["enum.DisposalUnitVisualLayers.Handle"] + - state: dispover-ready + map: ["enum.DisposalUnitVisualLayers.Light"] + + - type: PowerReceiver + - type: Configuration + keys: + - Tag + - type: DisposalMailingUnit + flushTime: 2 + - type: Clickable + - type: InteractionOutline + - type: Physics + anchored: true + shapes: + - !type:PhysShapeAabb + bounds: "-0.35,-0.3,0.35,0.3" + mask: + - Impassable + - MobImpassable + - VaultImpassable + - SmallImpassable + layer: + - Opaque + - Impassable + - MobImpassable + - VaultImpassable + - SmallImpassable + - type: SnapGrid + offset: Center + - type: Anchorable + - type: Destructible + thresholdvalue: 100 + resistances: metallicResistances + - type: Appearance + visuals: + - type: DisposalUnitVisualizer + state_unanchored: condisposal + state_anchored: disposal + state_charging: disposal-charging + overlay_charging: dispover-charge + overlay_ready: dispover-ready + overlay_full: dispover-full + overlay_engaged: dispover-handle + state_flush: disposal-flush + flush_sound: /Audio/Machines/disposalflush.ogg + flush_time: 2 + - type: UserInterface + interfaces: + - key: enum.DisposalMailingUnitUiKey.Key + type: DisposalMailingUnitBoundUserInterface + - key: enum.ConfigurationUiKey.Key + type: ConfigurationBoundUserInterface + - type: Pullable