Disposal mailing (#2194)

* Implement device networking

* Implement device configuration menu

* Fix device network

* Implement disposal mailing unit

* Implement base network connection
Implement wired and wireless network connection
Implement device network metadata

* Fix dereference null error

* Fix wired network null checks

* Change BaseNetworks enum to NetworkUtils class
Add PingResponse function to NetworkUtils
Change device network file structure

* Add doc comments

* Apply suggestions from code review

Co-authored-by: DrSmugleaf <DrSmugleaf@users.noreply.github.com>

* Add tag validation to disposal mailing unit

* Add tag validation to the mailing unit component

* Address reviews
Change WiredNetwork can connect check
Change device networking string literals to constants

* Address reviews
Revert changes to PowerProvider and PowerReceiver
Add new NodeGroup
WELP

* Fix recursive access to Owner property

* Integrate suggested changes

* Fix TryGetWireNet acting on NullPowerProvider
Fix network connections not checking if their owner has been deleted

* Close device network connection when the owning entity got deleted
Fix mailing unit not closing the device network connection on remove

* Remove GetWireNet from NullPowerProvider

Co-authored-by: Julian Giebel <j.giebel@netrocks.info>
Co-authored-by: DrSmugleaf <DrSmugleaf@users.noreply.github.com>
This commit is contained in:
Julian Giebel
2020-10-30 01:16:26 +01:00
committed by GitHub
parent 1b3cbd4d3a
commit 45b610f933
31 changed files with 2450 additions and 13 deletions

View File

@@ -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<DisposalUnitComponent>() ||
eventArgs.Target.HasComponent<MedicalScannerComponent>())
eventArgs.Target.HasComponent<MedicalScannerComponent>() ||
eventArgs.Target.HasComponent<DisposalMailingUnitComponent>())
{
return true;
}

View File

@@ -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<string, string> config)
{
SendMessage(new ConfigurationUpdatedMessage(config));
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
_menu.Close();
}
}
}

View File

@@ -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<string, LineEdit>(_inputs, "Text");
Owner.SendConfiguration(config);
Close();
}
private bool Validate(string value)
{
return Owner.Validation == null || Owner.Validation.IsMatch(value);
}
private Dictionary<string, TConfig> GenerateDictionary<TConfig, TInput>(List<(string name, TInput input)> inputs, string propertyName) where TInput : Control
{
var dictionary = new Dictionary<string, TConfig>();
foreach (var input in inputs)
{
var value = input.input.TryGetPropertyValue<TConfig>(propertyName);
dictionary.Add(input.name, value);
}
return dictionary;
}
private static void CopyProperties<T>(T from, T to) where T : Control
{
foreach (var property in from.AllAttachedProperties)
{
to.SetValue(property.Key, property.Value);
}
}
}
}

View File

@@ -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
{
/// <summary>
/// Initializes a <see cref="DisposalMailingUnitWindow"/> and updates it when new server messages are received.
/// </summary>
[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();
}
}
}
}

View File

@@ -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
{
}
}

View File

@@ -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
{
/// <summary>
/// Client-side UI used to control a <see cref="SharedDisposalMailingUnitComponent"/>
/// </summary>
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<string> TargetList;
private readonly Label _tagLabel;
protected override Vector2? CustomSize => (460, 220);
public DisposalMailingUnitWindow()
{
TargetList = new List<string>();
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<string> tags)
{
TargetListContainer.Clear();
foreach (var target in tags)
{
TargetListContainer.AddItem(target);
}
}
}
}

View File

@@ -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;

View File

@@ -182,6 +182,7 @@
"GasCanisterPort",
"Lung",
"Cleanable",
"Configuration",
"Brain",
"PlantHolder",
"SeedExtractor",

View File

@@ -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<string, string> payload, Metadata metadata, bool broadcast);
public class DeviceNetwork : IDeviceNetwork
{
private const int PACKAGES_PER_TICK = 30;
private readonly IRobustRandom _random = IoCManager.Resolve<IRobustRandom>();
private readonly Dictionary<int, List<NetworkDevice>> _devices = new Dictionary<int, List<NetworkDevice>>();
private readonly Queue<NetworkPackage> _packages = new Queue<NetworkPackage>();
/// <inheritdoc/>
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<string, string> 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<NetworkDevice>();
_devices[netId].Add(networkDevice);
}
private List<NetworkDevice> DevicesForFrequency(int netId, int frequency)
{
if (!_devices.ContainsKey(netId))
return new List<NetworkDevice>();
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<NetworkDevice> DevicesWithReceiveAll(int netId, int frequency)
{
if (!_devices.ContainsKey(netId))
return new List<NetworkDevice>();
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<NetworkDevice> 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<string, string> Data { get; set; }
public Metadata Metadata;
public string Sender;
}
}
}

View File

@@ -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<string, string> payload, Metadata metadata)
{
return Open && _network.EnqueuePackage(_netId, frequency, address, payload, Address, metadata);
}
public bool Send(int frequency, string address, Dictionary<string, string> payload)
{
return Send(frequency, address, payload);
}
public bool Send(string address, Dictionary<string, string> payload)
{
return Send(0, address, payload);
}
public bool Broadcast(int frequency, IReadOnlyDictionary<string, string> payload, Metadata metadata)
{
return Open && _network.EnqueuePackage(_netId, frequency, "", payload, Address, metadata, true);
}
public bool Broadcast(int frequency, Dictionary<string, string> payload)
{
return Broadcast(frequency, payload);
}
public bool Broadcast(Dictionary<string, string> payload)
{
return Broadcast(0, payload);
}
public void Close()
{
_network.RemoveDevice(_netId, Frequency, Address);
Open = false;
}
}
}

View File

@@ -0,0 +1,20 @@
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
namespace Content.Server.GameObjects.EntitySystems.DeviceNetwork
{
public class Metadata : Dictionary<string, object>
{
public bool TryParseMetadata<T>(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;
}
}
}

View File

@@ -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<IDeviceNetwork>();
Connection = network.Register(netId, frequency, OnReceiveNetMessage, receiveAll);
MessageHandler = onReceive;
}
public bool Send(int frequency, string address, Dictionary<string, string> payload)
{
var data = ManipulatePayload(payload);
var metadata = GetMetadata();
return Connection.Send(frequency, address, data, metadata);
}
public bool Send(string address, Dictionary<string, string> payload)
{
return Send(0, address, payload);
}
public bool Broadcast(int frequency, Dictionary<string, string> payload)
{
var data = ManipulatePayload(payload);
var metadata = GetMetadata();
return Connection.Broadcast(frequency, data, metadata);
}
public bool Broadcast(Dictionary<string, string> payload)
{
return Broadcast(0, payload);
}
public void Close()
{
Connection.Close();
}
private void OnReceiveNetMessage(int frequency, string sender, IReadOnlyDictionary<string, string> 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<string, string> payload, Metadata metadata, bool broadcast);
protected abstract Dictionary<string, string> ManipulatePayload(Dictionary<string, string> payload);
protected abstract Metadata GetMetadata();
}
}

View File

@@ -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<string, string> payload, Metadata metadata, bool broadcast)
{
if (_owner.Deleted)
{
Connection.Close();
return false;
}
if (_owner.TryGetComponent<PowerReceiverComponent>(out var powerReceiver)
&& TryGetWireNet(powerReceiver, out var ownNet)
&& metadata.TryParseMetadata<INodeGroup>(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<PowerReceiverComponent>(out var powerReceiver)
&& TryGetWireNet(powerReceiver, out var net))
{
var metadata = new Metadata
{
{WIRENET, net }
};
return metadata;
}
return new Metadata();
}
protected override Dictionary<string, string> ManipulatePayload(Dictionary<string, string> payload)
{
return payload;
}
private bool TryGetWireNet(PowerReceiverComponent powerReceiver, out INodeGroup net)
{
if (powerReceiver.Provider is PowerProviderComponent && powerReceiver.Provider.ProviderOwner.TryGetComponent<NodeContainerComponent>(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;
}
}
}

View File

@@ -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<string, string> payload, Metadata metadata, bool broadcast)
{
if (_owner.Deleted)
{
Connection.Close();
return false;
}
if (metadata.TryParseMetadata<Vector2>(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<string, string> ManipulatePayload(Dictionary<string, string> payload)
{
return payload;
}
}
}

View File

@@ -0,0 +1,38 @@
using Content.Server.Interfaces;
using System;
using System.Collections.Generic;
using System.Text;
namespace Content.Server.GameObjects.EntitySystems.DeviceNetwork
{
/// <summary>
/// A collection of utilities to help with using device networks
/// </summary>
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";
/// <summary>
/// Handles responding to pings.
/// </summary>
public static void PingResponse<T>(T connection, string sender, IReadOnlyDictionary<string, string> payload, string message = "") where T : IDeviceNetworkConnection
{
if (payload.TryGetValue(COMMAND, out var command) && command == PING)
{
var response = new Dictionary<string, string>
{
{COMMAND, "ping_response"},
{MESSAGE, message}
};
connection.Send(connection.Frequency, sender, response);
}
}
}
}

View File

@@ -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<string, string> _config = new Dictionary<string, string>();
private Regex _validation;
public event Action<Dictionary<string, string>> 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<string>(),
(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<bool> IInteractUsing.InteractUsing(InteractUsingEventArgs eventArgs)
{
if (UserInterface == null || !eventArgs.User.TryGetComponent(out IActorComponent actor))
return false;
if (!eventArgs.Using.TryGetComponent<ToolComponent>(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<string, string>(_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<T>(List<string> list, Dictionary<string, T> configuration, T value){
for (var index = 0; index < list.Count; index++)
{
configuration.Add(list[index], value);
}
}
}
}

View File

@@ -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))
{

View File

@@ -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";
/// <summary>
/// The delay for an entity trying to move out of this unit.
/// </summary>
private static readonly TimeSpan ExitAttemptDelay = TimeSpan.FromSeconds(0.5);
/// <summary>
/// Last time that an entity tried to exit this disposal unit.
/// </summary>
[ViewVariables]
private TimeSpan _lastExitAttempt;
public static readonly Regex TagRegex = new Regex("^[a-zA-Z0-9, ]*$", RegexOptions.Compiled);
/// <summary>
/// The current pressure of this disposal unit.
/// Prevents it from flushing if it is not equal to or bigger than 1.
/// </summary>
[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;
/// <summary>
/// Token used to cancel the automatic engage of a disposal unit
/// after an entity enters it.
/// </summary>
private CancellationTokenSource? _automaticEngageToken;
/// <summary>
/// Container of entities inside this disposal unit.
/// </summary>
[ViewVariables]
private Container _container = default!;
[ViewVariables]
private WiredNetworkConnection? _connection;
[ViewVariables] public IReadOnlyList<IEntity> ContainedEntities => _container.ContainedEntities;
[ViewVariables]
private readonly List<string> _targetList = new List<string>();
[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;
/// <summary>
/// Store the translated state.
/// </summary>
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<ItemComponent>() &&
!entity.HasComponent<IBody>())
{
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<bool> TryInsert(IEntity entity, IEntity? user = default)
{
if (!CanInsert(entity))
return false;
if (user != null && _entryDelay > 0f)
{
var doAfterSystem = EntitySystem.Get<DoAfterSystem>();
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<SnapGridComponent>();
var entry = snapGrid
.GetLocal()
.FirstOrDefault(entity => entity.HasComponent<DisposalEntryComponent>());
if (entry == null)
{
return false;
}
var entryComponent = entry.GetComponent<DisposalEntryComponent>();
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<string, string>
{
{ NetworkUtils.COMMAND, NET_CMD_SENT },
{ NET_SRC, _tag },
{ NET_TARGET, _target }
};
_connection.Broadcast(_connection.Frequency, data);
}
return true;
}
private DisposalHolderComponent CreateTaggedHolder(IReadOnlyCollection<IEntity> entities, string tag)
{
var holder = Owner.EntityManager.SpawnEntity(HolderPrototypeId, Owner.Transform.MapPosition);
var holderComponent = holder.GetComponent<DisposalHolderComponent>();
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<string, string>
{
{ 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<AudioSystem>().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<string, string> 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<Container>(Name, Owner);
if (UserInterface != null)
{
UserInterface.OnReceiveMessage += OnUiReceiveMessage;
}
var network = IoCManager.Resolve<IDeviceNetwork>();
_connection = new WiredNetworkConnection(OnReceiveNetMessage, false, Owner);
if (Owner.TryGetComponent<ConfigurationComponent>(out var configuration))
configuration.OnConfigUpdate += OnConfigUpdate;
UpdateInterface();
}
protected override void Startup()
{
base.Startup();
if(!Owner.HasComponent<AnchorableComponent>())
{
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<string, string> 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<string, string>
{
{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<IHandsComponent>())
{
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<bool> 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<DisposalMailingUnitComponent>
{
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<DisposalMailingUnitComponent>
{
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();
}
}
}
}

View File

@@ -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]

View File

@@ -64,5 +64,6 @@
Apc,
AMEngine,
Pipe,
WireNet
}
}

View File

@@ -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;
/// <summary>
/// The max distance this can transmit power to <see cref="PowerReceiverComponent"/>s from.
/// </summary>
@@ -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;
}
}
}

View File

@@ -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]"));
}
}

View File

@@ -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<IDeviceNetwork>();
}
public override void Update(float frameTime)
{
base.Update(frameTime);
if (_network == null)
return;
//(ノ°Д°)ノ︵ ┻━┻
_network.Update();
}
}
}

View File

@@ -0,0 +1,25 @@
using System;
using Content.Server.GameObjects.EntitySystems.DeviceNetwork;
namespace Content.Server.Interfaces
{
/// <summary>
/// Package based device network allowing devices to communicate with eachother
/// </summary>
public interface IDeviceNetwork
{
/// <summary>
/// Registers a device with the device network
/// </summary>
/// <param name="netId"><see cref="NetworkUtils"/>The id of the network to register with</param>
/// <param name="frequency">The frequency the device receives packages on. Wired networks use frequency 0</param>
/// <param name="messageHandler">The delegate that gets called when the device receives a message</param>
/// <param name="receiveAll">If the device should receive all packages on its frequency or only ones addressed to itself</param>
/// <returns></returns>
public DeviceNetworkConnection Register(int netId, int frequency, OnReceiveNetMessage messageHandler, bool receiveAll = false);
/// <see cref="Register(int, int, OnReceiveNetMessage, bool)"/>
public DeviceNetworkConnection Register(int netId, OnReceiveNetMessage messageHandler, bool receiveAll = false);
public void Update();
}
}

View File

@@ -0,0 +1,30 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace Content.Server.Interfaces
{
public interface IDeviceNetworkConnection
{
public int Frequency { get; }
/// <summary>
/// Sends a package to a specific device
/// </summary>
/// <param name="frequency">The frequency the package should be send on</param>
/// <param name="address">The target devices address</param>
/// <param name="payload"></param>
/// <returns></returns>
public bool Send(int frequency, string address, Dictionary<string, string> payload);
/// <see cref="Send(int, string, Dictionary{string, string})"/>
public bool Send(string address, Dictionary<string, string> payload);
/// <summary>
/// Sends a package to all devices
/// </summary>
/// <param name="frequency">The frequency the package should be send on</param>
/// <param name="payload"></param>
/// <returns></returns>
public bool Broadcast(int frequency, Dictionary<string, string> payload);
/// <see cref="Broadcast(int, Dictionary{string, string})"/>
public bool Broadcast(Dictionary<string, string> payload);
}
}

View File

@@ -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<ConsiderationsManager, ConsiderationsManager>();
IoCManager.Register<IAccentManager, AccentManager>();
IoCManager.Register<IConnectionManager, ConnectionManager>();
IoCManager.Register<IDeviceNetwork, DeviceNetwork>();
}
}
}

View File

@@ -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<DisposalMailingUnitBoundUserInterfaceState>, 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<string> Tags;
public readonly string Target;
public DisposalMailingUnitBoundUserInterfaceState(string unitName, string unitState, float pressure, bool powered,
bool engaged, string tag, List<string> 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<string>)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;
}
}
/// <summary>
/// Message data sent from client to server when a mailing unit ui button is pressed.
/// </summary>
[Serializable, NetSerializable]
public new class UiButtonPressedMessage : BoundUserInterfaceMessage
{
public readonly UiButton Button;
public UiButtonPressedMessage(UiButton button)
{
Button = button;
}
}
/// <summary>
/// Message data sent from client to server when the mailing units target is updated.
/// </summary>
[Serializable, NetSerializable]
public class UiTargetUpdateMessage : BoundUserInterfaceMessage
{
public readonly string Target;
public UiTargetUpdateMessage(string target)
{
Target = target;
}
}
[Serializable, NetSerializable]
public enum DisposalMailingUnitUiKey
{
Key
}
}
}

View File

@@ -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<string, string> Config;
public ConfigurationBoundUserInterfaceState(Dictionary<string, string> config)
{
Config = config;
}
}
/// <summary>
/// Message data sent from client to server when the device configuration is updated.
/// </summary>
[Serializable, NetSerializable]
public class ConfigurationUpdatedMessage : BoundUserInterfaceMessage
{
public readonly Dictionary<string, string> Config;
public ConfigurationUpdatedMessage(Dictionary<string, string> 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
}
}
}

View File

@@ -13,6 +13,11 @@ namespace Content.Shared.GameObjects.EntitySystems
{
comp.Update(frameTime);
}
foreach (var comp in ComponentManager.EntityQuery<SharedDisposalMailingUnitComponent>())
{
comp.Update(frameTime);
}
}
}
}

View File

@@ -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

View File

@@ -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