#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Content.Server.GameObjects.Components.Interactable;
using Content.Server.GameObjects.Components.VendingMachines;
using Content.Server.GameObjects.EntitySystems;
using Content.Server.Interfaces;
using Content.Server.Interfaces.GameObjects.Components.Items;
using Content.Shared.GameObjects.Components;
using Content.Shared.GameObjects.Components.Interactable;
using Content.Shared.GameObjects.EntitySystems;
using Content.Shared.Interfaces.GameObjects.Components;
using JetBrains.Annotations;
using Robust.Server.GameObjects;
using Robust.Server.GameObjects.Components.UserInterface;
using Robust.Server.GameObjects.EntitySystems;
using Robust.Server.Interfaces.GameObjects;
using Robust.Server.Interfaces.Player;
using Robust.Shared.GameObjects;
using Robust.Shared.GameObjects.Systems;
using Robust.Shared.Interfaces.Random;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Random;
using Robust.Shared.Serialization;
using Robust.Shared.Utility;
using Robust.Shared.ViewVariables;
namespace Content.Server.GameObjects.Components
{
[RegisterComponent]
public class WiresComponent : SharedWiresComponent, IInteractUsing, IExamine, IMapInit
{
#pragma warning disable 649
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly IServerNotifyManager _notifyManager = default!;
#pragma warning restore 649
private AudioSystem _audioSystem = default!;
private AppearanceComponent _appearance = default!;
private BoundUserInterface _userInterface = default!;
private bool _isPanelOpen;
///
/// Opening the maintenance panel (typically with a screwdriver) changes this.
///
[ViewVariables]
public bool IsPanelOpen
{
get => _isPanelOpen;
private set
{
if (_isPanelOpen == value)
{
return;
}
_isPanelOpen = value;
UpdateAppearance();
}
}
private bool _isPanelVisible = true;
///
/// Components can set this to prevent the maintenance panel overlay from showing even if it's open
///
[ViewVariables]
public bool IsPanelVisible
{
get => _isPanelVisible;
set
{
if (_isPanelVisible == value)
{
return;
}
_isPanelVisible = value;
UpdateAppearance();
}
}
[ViewVariables(VVAccess.ReadWrite)]
public string BoardName
{
get => _boardName;
set
{
_boardName = value;
UpdateUserInterface();
}
}
[ViewVariables(VVAccess.ReadWrite)]
public string? SerialNumber
{
get => _serialNumber;
set
{
_serialNumber = value;
UpdateUserInterface();
}
}
private void UpdateAppearance()
{
_appearance.SetData(WiresVisuals.MaintenancePanelState, IsPanelOpen && IsPanelVisible);
}
///
/// Contains all registered wires.
///
public readonly List WiresList = new List();
///
/// Status messages are displayed at the bottom of the UI.
///
private readonly Dictionary _statuses = new Dictionary();
///
/// and .
///
private readonly List _availableColors =
new List((WireColor[]) Enum.GetValues(typeof(WireColor)));
private readonly List _availableLetters =
new List((WireLetter[]) Enum.GetValues(typeof(WireLetter)));
private string _boardName = default!;
private string? _serialNumber;
// Used to generate wire appearance randomization client side.
// We honestly don't care what it is or such but do care that it doesn't change between UI re-opens.
[ViewVariables]
private int _wireSeed;
[ViewVariables]
private string? _layoutId;
public override void Initialize()
{
base.Initialize();
_audioSystem = EntitySystem.Get();
_appearance = Owner.GetComponent();
_appearance.SetData(WiresVisuals.MaintenancePanelState, IsPanelOpen);
_userInterface = Owner.GetComponent()
.GetBoundUserInterface(WiresUiKey.Key);
_userInterface.OnReceiveMessage += UserInterfaceOnReceiveMessage;
}
private void GenerateSerialNumber()
{
var random = IoCManager.Resolve();
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);
}
SerialNumber = new string(data);
}
protected override void Startup()
{
base.Startup();
WireLayout? layout = null;
var hackingSystem = EntitySystem.Get();
if (_layoutId != null)
{
hackingSystem.TryGetLayout(_layoutId, out layout);
}
foreach (var wiresProvider in Owner.GetAllComponents())
{
var builder = new WiresBuilder(this, wiresProvider, layout);
wiresProvider.RegisterWires(builder);
}
if (layout != null)
{
WiresList.Sort((a, b) =>
{
var pA = layout.Specifications[a.Identifier].Position;
var pB = layout.Specifications[b.Identifier].Position;
return pA.CompareTo(pB);
});
}
else
{
IoCManager.Resolve().Shuffle(WiresList);
if (_layoutId != null)
{
var dict = new Dictionary();
for (var i = 0; i < WiresList.Count; i++)
{
var d = WiresList[i];
dict.Add(d.Identifier, new WireLayout.WireData(d.Letter, d.Color, i));
}
hackingSystem.AddLayout(_layoutId, new WireLayout(dict));
}
}
var id = 0;
foreach (var wire in WiresList)
{
wire.Id = ++id;
}
UpdateUserInterface();
}
///
/// Returns whether the wire associated with is cut.
///
///
public bool IsWireCut(object identifier)
{
var wire = WiresList.Find(x => x.Identifier.Equals(identifier));
if (wire == null) throw new ArgumentException();
return wire.IsCut;
}
public class Wire
{
///
/// The component that registered the wire.
///
public IWires Owner { get; }
///
/// Whether the wire is cut.
///
public bool IsCut { get; set; }
///
/// Used in client-server communication to identify a wire without telling the client what the wire does.
///
[ViewVariables]
public int Id { get; set; }
///
/// The color of the wire.
///
[ViewVariables]
public WireColor Color { get; }
///
/// The greek letter shown below the wire.
///
[ViewVariables]
public WireLetter Letter { get; }
///
/// Registered by components implementing IWires, used to identify which wire the client interacted with.
///
[ViewVariables]
public object Identifier { get; }
public Wire(IWires owner, bool isCut, WireColor color, WireLetter letter, object identifier)
{
Owner = owner;
IsCut = isCut;
Color = color;
Letter = letter;
Identifier = identifier;
}
}
///
/// Used by .
///
public class WiresBuilder
{
[NotNull] private readonly WiresComponent _wires;
[NotNull] private readonly IWires _owner;
private readonly WireLayout? _layout;
public WiresBuilder(WiresComponent wires, IWires owner, WireLayout? layout)
{
_wires = wires;
_owner = owner;
_layout = layout;
}
public void CreateWire(object identifier, (WireColor, WireLetter)? appearance = null, bool isCut = false)
{
WireLetter letter;
WireColor color;
if (!appearance.HasValue)
{
if (_layout != null && _layout.Specifications.TryGetValue(identifier, out var specification))
{
color = specification.Color;
letter = specification.Letter;
_wires._availableColors.Remove(color);
_wires._availableLetters.Remove(letter);
}
else
{
(color, letter) = _wires.AssignAppearance();
}
}
else
{
(color, letter) = appearance.Value;
_wires._availableColors.Remove(color);
_wires._availableLetters.Remove(letter);
}
// TODO: ENSURE NO RANDOM OVERLAP.
_wires.WiresList.Add(new Wire(_owner, isCut, color, letter, identifier));
}
}
///
/// Picks a color from and removes it from the list.
///
/// The picked color.
private (WireColor, WireLetter) AssignAppearance()
{
var color = _availableColors.Count == 0 ? WireColor.Red : _random.PickAndTake(_availableColors);
var letter = _availableLetters.Count == 0 ? WireLetter.α : _random.PickAndTake(_availableLetters);
return (color, letter);
}
///
/// Call this from other components to open the wires UI.
///
public void OpenInterface(IPlayerSession session)
{
_userInterface.Open(session);
}
private void UserInterfaceOnReceiveMessage(ServerBoundUserInterfaceMessage serverMsg)
{
var message = serverMsg.Message;
switch (message)
{
case WiresActionMessage msg:
var wire = WiresList.Find(x => x.Id == msg.Id);
var player = serverMsg.Session.AttachedEntity;
if (wire == null || player == null)
{
return;
}
if (!player.TryGetComponent(out IHandsComponent handsComponent))
{
_notifyManager.PopupMessage(Owner.Transform.GridPosition, player,
Loc.GetString("You have no hands."));
return;
}
if (!EntitySystem.Get().InRangeUnobstructed(player.Transform.MapPosition, Owner.Transform.MapPosition, ignoredEnt: Owner))
{
_notifyManager.PopupMessage(Owner.Transform.GridPosition, player,
Loc.GetString("You can't reach there!"));
return;
}
var activeHandEntity = handsComponent.GetActiveHand?.Owner;
ToolComponent? tool = null;
activeHandEntity?.TryGetComponent(out tool);
switch (msg.Action)
{
case WiresAction.Cut:
if (tool == null || !tool.HasQuality(ToolQuality.Cutting))
{
_notifyManager.PopupMessageCursor(player,
Loc.GetString("You need to hold a wirecutter in your hand!"));
return;
}
tool.PlayUseSound();
wire.IsCut = true;
UpdateUserInterface();
break;
case WiresAction.Mend:
if (tool == null || !tool.HasQuality(ToolQuality.Cutting))
{
_notifyManager.PopupMessageCursor(player,
Loc.GetString("You need to hold a wirecutter in your hand!"));
return;
}
tool.PlayUseSound();
wire.IsCut = false;
UpdateUserInterface();
break;
case WiresAction.Pulse:
if (tool == null || !tool.HasQuality(ToolQuality.Multitool))
{
_notifyManager.PopupMessageCursor(player,
Loc.GetString("You need to hold a multitool in your hand!"));
return;
}
if (wire.IsCut)
{
_notifyManager.PopupMessageCursor(player,
Loc.GetString("You can't pulse a wire that's been cut!"));
return;
}
_audioSystem.PlayFromEntity("/Audio/Effects/multitool_pulse.ogg", Owner);
break;
}
wire.Owner.WiresUpdate(new WiresUpdateEventArgs(wire.Identifier, msg.Action));
break;
}
}
private void UpdateUserInterface()
{
var clientList = new List();
foreach (var entry in WiresList)
{
clientList.Add(new ClientWire(entry.Id, entry.IsCut, entry.Color,
entry.Letter));
}
_userInterface.SetState(
new WiresBoundUserInterfaceState(
clientList.ToArray(),
_statuses.Select(p => new StatusEntry(p.Key, p.Value)).ToArray(),
BoardName,
SerialNumber,
_wireSeed));
}
public override void ExposeData(ObjectSerializer serializer)
{
base.ExposeData(serializer);
serializer.DataField(ref _boardName, "BoardName", "Wires");
serializer.DataField(ref _serialNumber, "SerialNumber", null);
serializer.DataField(ref _wireSeed, "WireSeed", 0);
serializer.DataField(ref _layoutId, "LayoutId", null);
}
async Task IInteractUsing.InteractUsing(InteractUsingEventArgs eventArgs)
{
if (!eventArgs.Using.TryGetComponent(out var tool))
return false;
if (!await tool.UseTool(eventArgs.User, Owner, 0.5f, ToolQuality.Screwing))
return false;
IsPanelOpen = !IsPanelOpen;
EntitySystem.Get()
.PlayFromEntity(IsPanelOpen ? "/Audio/Machines/screwdriveropen.ogg" : "/Audio/Machines/screwdriverclose.ogg",
Owner);
return true;
}
void IExamine.Examine(FormattedMessage message, bool inDetailsRange)
{
var loc = IoCManager.Resolve();
message.AddMarkup(loc.GetString(IsPanelOpen
? "The [color=lightgray]maintenance panel[/color] is [color=darkgreen]open[/color]."
: "The [color=lightgray]maintenance panel[/color] is [color=darkred]closed[/color]."));
}
public void SetStatus(object statusIdentifier, object status)
{
if (_statuses.TryGetValue(statusIdentifier, out var storedMessage))
{
if (storedMessage == status)
{
return;
}
}
_statuses[statusIdentifier] = status;
UpdateUserInterface();
}
void IMapInit.MapInit()
{
if (SerialNumber == null)
{
GenerateSerialNumber();
}
if (_wireSeed == 0)
{
_wireSeed = IoCManager.Resolve().Next(1, int.MaxValue);
UpdateUserInterface();
}
}
}
}