using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Threading; using Content.Server.Construction; using Content.Server.Construction.Components; using Content.Server.Power.Components; using Content.Shared.DoAfter; using Content.Shared.GameTicking; using Content.Shared.Hands.Components; using Content.Shared.Interaction; using Content.Shared.Popups; using Content.Shared.Power; using Content.Shared.Tools.Components; using Content.Shared.Wires; using Robust.Server.GameObjects; using Robust.Shared.Player; using Robust.Shared.Prototypes; using Robust.Shared.Random; namespace Content.Server.Wires; public sealed class WiresSystem : SharedWiresSystem { [Dependency] private readonly IPrototypeManager _protoMan = default!; [Dependency] private readonly SharedDoAfterSystem _doAfter = default!; [Dependency] private readonly SharedPopupSystem _popupSystem = default!; [Dependency] private readonly SharedInteractionSystem _interactionSystem = default!; [Dependency] private readonly UserInterfaceSystem _uiSystem = default!; [Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly ConstructionSystem _construction = default!; // This is where all the wire layouts are stored. [ViewVariables] private readonly Dictionary _layouts = new(); private float _toolTime = 0f; #region Initialization public override void Initialize() { base.Initialize(); SubscribeLocalEvent(Reset); // this is a broadcast event SubscribeLocalEvent(OnPanelChanged); SubscribeLocalEvent(OnWiresActionMessage); SubscribeLocalEvent(OnInteractUsing); SubscribeLocalEvent(OnMapInit); SubscribeLocalEvent(OnTimedWire); SubscribeLocalEvent(OnWiresPowered); SubscribeLocalEvent(OnDoAfter); SubscribeLocalEvent(SetWiresPanelSecurity); } private void SetOrCreateWireLayout(EntityUid uid, WiresComponent? wires = null) { if (!Resolve(uid, ref wires)) return; WireLayout? layout = null; List? wireSet = null; if (!wires.AlwaysRandomize) { TryGetLayout(wires.LayoutId, out layout); } List wireActions = new(); var dummyWires = 0; if (!_protoMan.TryIndex(wires.LayoutId, out WireLayoutPrototype? layoutPrototype)) { return; } dummyWires += layoutPrototype.DummyWires; if (layoutPrototype.Wires != null) { wireActions.AddRange(layoutPrototype.Wires); } // does the prototype have a parent (and are the wires empty?) if so, we just create // a new layout based on that foreach (var parentLayout in _protoMan.EnumerateParents(wires.LayoutId)) { if (parentLayout.Wires != null) { wireActions.AddRange(parentLayout.Wires); } dummyWires += parentLayout.DummyWires; } if (wireActions.Count > 0) { foreach (var wire in wireActions) { wire.Initialize(); } wireSet = CreateWireSet(uid, layout, wireActions, dummyWires); } if (wireSet == null || wireSet.Count == 0) { return; } wires.WiresList.AddRange(wireSet); var types = new Dictionary(); if (layout != null) { for (var i = 0; i < wireSet.Count; i++) { wires.WiresList[layout.Specifications[i].Position] = wireSet[i]; } var id = 0; foreach (var wire in wires.WiresList) { wire.Id = id++; if (wire.Action == null) continue; var wireType = wire.Action.GetType(); if (types.ContainsKey(wireType)) { types[wireType] += 1; } else { types.Add(wireType, 1); } // don't care about the result, this should've // been handled in layout creation wire.Action.AddWire(wire, types[wireType]); } } else { var enumeratedList = new List<(int, Wire)>(); var data = new Dictionary(); for (int i = 0; i < wireSet.Count; i++) { enumeratedList.Add((i, wireSet[i])); } _random.Shuffle(enumeratedList); for (var i = 0; i < enumeratedList.Count; i++) { (int id, Wire d) = enumeratedList[i]; d.Id = i; if (d.Action != null) { var actionType = d.Action.GetType(); if (!types.TryAdd(actionType, 1)) types[actionType] += 1; if (!d.Action.AddWire(d, types[actionType])) d.Action = null; } data.Add(id, new WireLayout.WireData(d.Letter, d.Color, i)); wires.WiresList[i] = wireSet[id]; } if (!wires.AlwaysRandomize && !string.IsNullOrEmpty(wires.LayoutId)) { AddLayout(wires.LayoutId, new WireLayout(data)); } } } private List? CreateWireSet(EntityUid uid, WireLayout? layout, List wires, int dummyWires) { if (wires.Count == 0) return null; List colors = new((WireColor[])Enum.GetValues(typeof(WireColor))); List letters = new((WireLetter[])Enum.GetValues(typeof(WireLetter))); var wireSet = new List(); for (var i = 0; i < wires.Count; i++) { wireSet.Add(CreateWire(uid, wires[i], i, layout, colors, letters)); } for (var i = 1; i <= dummyWires; i++) { wireSet.Add(CreateWire(uid, null, wires.Count + i, layout, colors, letters)); } return wireSet; } private Wire CreateWire(EntityUid uid, IWireAction? action, int position, WireLayout? layout, List colors, List letters) { WireLetter letter; WireColor color; if (layout != null && layout.Specifications.TryGetValue(position, out var spec)) { color = spec.Color; letter = spec.Letter; colors.Remove(color); letters.Remove(letter); } else { color = colors.Count == 0 ? WireColor.Red : _random.PickAndTake(colors); letter = letters.Count == 0 ? WireLetter.α : _random.PickAndTake(letters); } return new Wire( uid, false, color, letter, position, action); } #endregion #region DoAfters private void OnTimedWire(EntityUid uid, WiresComponent component, TimedWireEvent args) { args.Delegate(args.Wire); UpdateUserInterface(uid); } /// /// Tries to cancel an active wire action via the given key that it's stored in. /// /// The key used to cancel the action. public bool TryCancelWireAction(EntityUid owner, object key) { if (TryGetData(owner, key, out var token)) { token.Cancel(); return true; } return false; } /// /// Starts a timed action for this entity. /// /// How long this takes to finish /// The key used to cancel the action /// The event that is sent out when the wire is finished public void StartWireAction(EntityUid owner, float delay, object key, TimedWireEvent onFinish) { if (!HasComp(owner)) { return; } if (!_activeWires.ContainsKey(owner)) { _activeWires.Add(owner, new()); } CancellationTokenSource tokenSource = new(); // Starting an already started action will do nothing. if (HasData(owner, key)) { return; } SetData(owner, key, tokenSource); _activeWires[owner].Add(new ActiveWireAction ( key, delay, tokenSource.Token, onFinish )); } private Dictionary> _activeWires = new(); private List<(EntityUid, ActiveWireAction)> _finishedWires = new(); public override void Update(float frameTime) { foreach (var (owner, activeWires) in _activeWires) { if (!HasComp(owner)) _activeWires.Remove(owner); foreach (var wire in activeWires) { if (wire.CancelToken.IsCancellationRequested) { RaiseLocalEvent(owner, wire.OnFinish, true); _finishedWires.Add((owner, wire)); } else { wire.TimeLeft -= frameTime; if (wire.TimeLeft <= 0) { RaiseLocalEvent(owner, wire.OnFinish, true); _finishedWires.Add((owner, wire)); } } } } if (_finishedWires.Count != 0) { foreach (var (owner, wireAction) in _finishedWires) { if (!_activeWires.TryGetValue(owner, out var activeWire)) { continue; } activeWire.RemoveAll(action => action.CancelToken == wireAction.CancelToken); if (activeWire.Count == 0) { _activeWires.Remove(owner); } RemoveData(owner, wireAction.Id); } _finishedWires.Clear(); } } private sealed class ActiveWireAction { /// /// The wire action's ID. This is so that once the action is finished, /// any related data can be removed from the state dictionary. /// public object Id; /// /// How much time is left in this action before it finishes. /// public float TimeLeft; /// /// The token used to cancel the action. /// public CancellationToken CancelToken; /// /// The event called once the action finishes. /// public TimedWireEvent OnFinish; public ActiveWireAction(object identifier, float time, CancellationToken cancelToken, TimedWireEvent onFinish) { Id = identifier; TimeLeft = time; CancelToken = cancelToken; OnFinish = onFinish; } } #endregion #region Event Handling private void OnWiresPowered(EntityUid uid, WiresComponent component, ref PowerChangedEvent args) { UpdateUserInterface(uid); foreach (var wire in component.WiresList) { wire.Action?.Update(wire); } } private void OnWiresActionMessage(EntityUid uid, WiresComponent component, WiresActionMessage args) { var player = args.Actor; if (!EntityManager.TryGetComponent(player, out HandsComponent? handsComponent)) { _popupSystem.PopupEntity(Loc.GetString("wires-component-ui-on-receive-message-no-hands"), uid, player); return; } if (!_interactionSystem.InRangeUnobstructed(player, uid)) { _popupSystem.PopupEntity(Loc.GetString("wires-component-ui-on-receive-message-cannot-reach"), uid, player); return; } var activeHand = handsComponent.ActiveHand; if (activeHand == null) return; if (activeHand.HeldEntity == null) return; var activeHandEntity = activeHand.HeldEntity.Value; if (!EntityManager.TryGetComponent(activeHandEntity, out ToolComponent? tool)) return; TryDoWireAction(uid, player, activeHandEntity, args.Id, args.Action, component, tool); } private void OnDoAfter(EntityUid uid, WiresComponent component, WireDoAfterEvent args) { if (args.Cancelled) { component.WiresQueue.Remove(args.Id); return; } if (args.Handled || args.Args.Target == null || args.Args.Used == null) return; UpdateWires(args.Args.Target.Value, args.Args.User, args.Args.Used.Value, args.Id, args.Action, component); args.Handled = true; } private void OnInteractUsing(EntityUid uid, WiresComponent component, InteractUsingEvent args) { if (args.Handled) return; if (!TryComp(args.Used, out var tool)) return; if (!IsPanelOpen(uid)) return; if (Tool.HasQuality(args.Used, "Cutting", tool) || Tool.HasQuality(args.Used, "Pulsing", tool)) { if (TryComp(args.User, out ActorComponent? actor)) { _uiSystem.OpenUi(uid, WiresUiKey.Key, actor.PlayerSession); args.Handled = true; } } } private void OnPanelChanged(Entity ent, ref PanelChangedEvent args) { if (args.Open) return; _uiSystem.CloseUi(ent.Owner, WiresUiKey.Key); } private void OnMapInit(EntityUid uid, WiresComponent component, MapInitEvent args) { if (!string.IsNullOrEmpty(component.LayoutId)) SetOrCreateWireLayout(uid, component); if (component.SerialNumber == null) GenerateSerialNumber(uid, component); if (component.WireSeed == 0) component.WireSeed = _random.Next(1, int.MaxValue); // Update the construction graph to make sure that it starts on the node specified by WiresPanelSecurityComponent if (TryComp(uid, out var wiresPanelSecurity) && !string.IsNullOrEmpty(wiresPanelSecurity.SecurityLevel) && TryComp(uid, out var construction)) { _construction.ChangeNode(uid, null, wiresPanelSecurity.SecurityLevel, true, construction); } UpdateUserInterface(uid); } #endregion #region Entity API private void GenerateSerialNumber(EntityUid uid, WiresComponent? wires = null) { if (!Resolve(uid, ref wires)) return; Span data = stackalloc char[9]; data[4] = '-'; if (_random.Prob(0.01f)) { for (var i = 0; i < 4; i++) { // Cyrillic Letters data[i] = (char)_random.Next(0x0410, 0x0430); } } else { for (var i = 0; i < 4; i++) { // Letters data[i] = (char)_random.Next(0x41, 0x5B); } } for (var i = 5; i < 9; i++) { // Digits data[i] = (char)_random.Next(0x30, 0x3A); } wires.SerialNumber = new string(data); UpdateUserInterface(uid); } private void UpdateUserInterface(EntityUid uid, WiresComponent? wires = null, UserInterfaceComponent? ui = null) { if (!Resolve(uid, ref wires, ref ui, false)) // logging this means that we get a bunch of errors return; var clientList = new List(); foreach (var entry in wires.WiresList) { clientList.Add(new ClientWire(entry.Id, entry.IsCut, entry.Color, entry.Letter)); var statusData = entry.Action?.GetStatusLightData(entry); if (statusData != null && entry.Action?.StatusKey != null) { wires.Statuses[entry.Action.StatusKey] = (entry.OriginalPosition, statusData); } } var statuses = new List<(int position, object key, object value)>(); foreach (var (key, value) in wires.Statuses) { var valueCast = ((int position, StatusLightData? value))value; statuses.Add((valueCast.position, key, valueCast.value!)); } statuses.Sort((a, b) => a.position.CompareTo(b.position)); _uiSystem.SetUiState((uid, ui), WiresUiKey.Key, new WiresBoundUserInterfaceState( clientList.ToArray(), statuses.Select(p => new StatusEntry(p.key, p.value)).ToArray(), Loc.GetString(wires.BoardName), wires.SerialNumber, wires.WireSeed)); } public void OpenUserInterface(EntityUid uid, ICommonSession player) { _uiSystem.OpenUi(uid, WiresUiKey.Key, player); } /// /// Tries to get a wire on this entity by its integer id. /// /// The wire if found, otherwise null public Wire? TryGetWire(EntityUid uid, int id, WiresComponent? wires = null) { if (!Resolve(uid, ref wires)) return null; return id >= 0 && id < wires.WiresList.Count ? wires.WiresList[id] : null; } /// /// Tries to get all the wires on this entity by the wire action type. /// /// Enumerator of all wires in this entity according to the given type. public IEnumerable TryGetWires(EntityUid uid, WiresComponent? wires = null) { if (!Resolve(uid, ref wires)) yield break; foreach (var wire in wires.WiresList) { if (wire.GetType() == typeof(T)) { yield return wire; } } } public void SetWiresPanelSecurity(EntityUid uid, WiresPanelSecurityComponent component, WiresPanelSecurityEvent args) { component.Examine = args.Examine; component.WiresAccessible = args.WiresAccessible; Dirty(uid, component); if (!args.WiresAccessible) { _uiSystem.CloseUi(uid, WiresUiKey.Key); } } private void TryDoWireAction(EntityUid target, EntityUid user, EntityUid toolEntity, int id, WiresAction action, WiresComponent? wires = null, ToolComponent? tool = null) { if (!Resolve(target, ref wires) || !Resolve(toolEntity, ref tool)) return; if (wires.WiresQueue.Contains(id)) return; var wire = TryGetWire(target, id, wires); if (wire == null) return; switch (action) { case WiresAction.Cut: if (!Tool.HasQuality(toolEntity, "Cutting", tool)) { _popupSystem.PopupCursor(Loc.GetString("wires-component-ui-on-receive-message-need-wirecutters"), user); return; } if (wire.IsCut) { _popupSystem.PopupCursor(Loc.GetString("wires-component-ui-on-receive-message-cannot-cut-cut-wire"), user); return; } break; case WiresAction.Mend: if (!Tool.HasQuality(toolEntity, "Cutting", tool)) { _popupSystem.PopupCursor(Loc.GetString("wires-component-ui-on-receive-message-need-wirecutters"), user); return; } if (!wire.IsCut) { _popupSystem.PopupCursor(Loc.GetString("wires-component-ui-on-receive-message-cannot-mend-uncut-wire"), user); return; } break; case WiresAction.Pulse: if (!Tool.HasQuality(toolEntity, "Pulsing", tool)) { _popupSystem.PopupCursor(Loc.GetString("wires-component-ui-on-receive-message-need-multitool"), user); return; } if (wire.IsCut) { _popupSystem.PopupCursor(Loc.GetString("wires-component-ui-on-receive-message-cannot-pulse-cut-wire"), user); return; } break; } wires.WiresQueue.Add(id); if (_toolTime > 0f) { var args = new DoAfterArgs(EntityManager, user, _toolTime, new WireDoAfterEvent(action, id), target, target: target, used: toolEntity) { NeedHand = true, BreakOnDamage = true, BreakOnMove = true }; _doAfter.TryStartDoAfter(args); } else { UpdateWires(target, user, toolEntity, id, action, wires); } } private void UpdateWires(EntityUid used, EntityUid user, EntityUid toolEntity, int id, WiresAction action, WiresComponent? wires = null, ToolComponent? tool = null) { if (!Resolve(used, ref wires)) return; if (!wires.WiresQueue.Contains(id)) return; if (!Resolve(toolEntity, ref tool)) { wires.WiresQueue.Remove(id); return; } var wire = TryGetWire(used, id, wires); if (wire == null) { wires.WiresQueue.Remove(id); return; } switch (action) { case WiresAction.Cut: if (!Tool.HasQuality(toolEntity, "Cutting", tool)) { _popupSystem.PopupCursor(Loc.GetString("wires-component-ui-on-receive-message-need-wirecutters"), user); break; } if (wire.IsCut) { _popupSystem.PopupCursor(Loc.GetString("wires-component-ui-on-receive-message-cannot-cut-cut-wire"), user); break; } Tool.PlayToolSound(toolEntity, tool, null); if (wire.Action == null || wire.Action.Cut(user, wire)) { wire.IsCut = true; } UpdateUserInterface(used); break; case WiresAction.Mend: if (!Tool.HasQuality(toolEntity, "Cutting", tool)) { _popupSystem.PopupCursor(Loc.GetString("wires-component-ui-on-receive-message-need-wirecutters"), user); break; } if (!wire.IsCut) { _popupSystem.PopupCursor(Loc.GetString("wires-component-ui-on-receive-message-cannot-mend-uncut-wire"), user); break; } Tool.PlayToolSound(toolEntity, tool, null); if (wire.Action == null || wire.Action.Mend(user, wire)) { wire.IsCut = false; } UpdateUserInterface(used); break; case WiresAction.Pulse: if (!Tool.HasQuality(toolEntity, "Pulsing", tool)) { _popupSystem.PopupCursor(Loc.GetString("wires-component-ui-on-receive-message-need-multitool"), user); break; } if (wire.IsCut) { _popupSystem.PopupCursor(Loc.GetString("wires-component-ui-on-receive-message-cannot-pulse-cut-wire"), user); break; } wire.Action?.Pulse(user, wire); UpdateUserInterface(used); Audio.PlayPvs(wires.PulseSound, used); break; } wire.Action?.Update(wire); wires.WiresQueue.Remove(id); } /// /// Tries to get the stateful data stored in this entity's WiresComponent. /// /// The key that stores the data in the WiresComponent. public bool TryGetData(EntityUid uid, object identifier, [NotNullWhen(true)] out T? data, WiresComponent? wires = null) { data = default(T); if (!Resolve(uid, ref wires)) return false; wires.StateData.TryGetValue(identifier, out var result); if (result is not T) { return false; } data = (T)result; return true; } /// /// Sets data in the entity's WiresComponent state dictionary by key. /// /// The key that stores the data in the WiresComponent. /// The data to store using the given identifier. public void SetData(EntityUid uid, object identifier, object data, WiresComponent? wires = null) { if (!Resolve(uid, ref wires)) return; if (wires.StateData.TryGetValue(identifier, out var storedMessage)) { if (storedMessage == data) { return; } } wires.StateData[identifier] = data; UpdateUserInterface(uid, wires); } /// /// If this entity has data stored via this key in the WiresComponent it has /// public bool HasData(EntityUid uid, object identifier, WiresComponent? wires = null) { if (!Resolve(uid, ref wires)) return false; return wires.StateData.ContainsKey(identifier); } /// /// Removes data from this entity stored in the given key from the entity's WiresComponent. /// /// The key that stores the data in the WiresComponent. public void RemoveData(EntityUid uid, object identifier, WiresComponent? wires = null) { if (!Resolve(uid, ref wires)) return; wires.StateData.Remove(identifier); } #endregion #region Layout Handling private bool TryGetLayout(string id, [NotNullWhen(true)] out WireLayout? layout) { return _layouts.TryGetValue(id, out layout); } private void AddLayout(string id, WireLayout layout) { _layouts.Add(id, layout); } private void Reset(RoundRestartCleanupEvent args) { _layouts.Clear(); } #endregion } public sealed class Wire { /// /// The entity that registered the wire. /// public EntityUid Owner { get; } /// /// Whether the wire is cut. /// public bool IsCut { get; set; } /// /// Used in client-server communication to identify a wire without telling the client what the wire does. /// [ViewVariables] public int Id { get; set; } /// /// The original position of this wire in the prototype. /// [ViewVariables] public int OriginalPosition { get; set; } /// /// The color of the wire. /// [ViewVariables] public WireColor Color { get; } /// /// The greek letter shown below the wire. /// [ViewVariables] public WireLetter Letter { get; } /// /// The action that this wire performs when mended, cut or puled. This also determines the status lights that this wire adds. /// public IWireAction? Action { get; set; } public Wire(EntityUid owner, bool isCut, WireColor color, WireLetter letter, int position, IWireAction? action) { Owner = owner; IsCut = isCut; Color = color; OriginalPosition = position; Letter = letter; Action = action; } } // this is here so that when a DoAfter event is called, // WiresSystem can call the action in question after the // doafter is finished (either through cancellation // or completion - this is implementation dependent) public delegate void WireActionDelegate(Wire wire); // callbacks over the event bus, // because async is banned public sealed class TimedWireEvent : EntityEventArgs { /// /// The function to be called once /// the timed event is complete. /// public WireActionDelegate Delegate { get; } /// /// The wire tied to this timed wire event. /// public Wire Wire { get; } public TimedWireEvent(WireActionDelegate @delegate, Wire wire) { Delegate = @delegate; Wire = wire; } } public sealed class WireLayout { // why is this an ? // List.Insert panics, // and I needed a uniquer key for wires // which allows me to have a unified identifier [ViewVariables] public IReadOnlyDictionary Specifications { get; } public WireLayout(IReadOnlyDictionary specifications) { Specifications = specifications; } public sealed class WireData { public WireLetter Letter { get; } public WireColor Color { get; } public int Position { get; } public WireData(WireLetter letter, WireColor color, int position) { Letter = letter; Color = color; Position = position; } } }