Fax Machine (#11704)

This commit is contained in:
Morb
2022-12-11 21:06:11 +03:00
committed by GitHub
parent 2fe900da57
commit dbba104eab
26 changed files with 1133 additions and 35 deletions

View File

@@ -0,0 +1,58 @@
using Content.Shared.Fax;
using Robust.Client.GameObjects;
namespace Content.Client.Fax.UI;
public sealed class FaxBoundUi : BoundUserInterface
{
private FaxWindow? _window;
public FaxBoundUi(ClientUserInterfaceComponent owner, Enum uiKey) : base(owner, uiKey)
{
}
protected override void Open()
{
base.Open();
_window = new FaxWindow();
_window.OpenCentered();
_window.OnClose += Close;
_window.SendButtonPressed += OnSendButtonPressed;
_window.RefreshButtonPressed += OnRefreshButtonPressed;
_window.PeerSelected += OnPeerSelected;
}
private void OnSendButtonPressed()
{
SendMessage(new FaxSendMessage());
}
private void OnRefreshButtonPressed()
{
SendMessage(new FaxRefreshMessage());
}
private void OnPeerSelected(string address)
{
SendMessage(new FaxDestinationMessage(address));
}
protected override void UpdateState(BoundUserInterfaceState state)
{
base.UpdateState(state);
if (_window == null || state is not FaxUiState cast)
return;
_window.UpdateState(cast);
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (disposing)
_window?.Dispose();
}
}

View File

@@ -0,0 +1,32 @@
<DefaultWindow xmlns="https://spacestation14.io"
xmlns:viewport="clr-namespace:Content.Client.Viewport"
Title="{Loc 'fax-machine-ui-window'}"
MinWidth="250">
<BoxContainer Orientation="Vertical" VerticalExpand="True">
<BoxContainer Orientation="Horizontal" HorizontalExpand="True">
<Label Text="{Loc 'fax-machine-ui-paper'}" />
<Control MinWidth="4" />
<Label Name="PaperStatusLabel" />
</BoxContainer>
<Control HorizontalExpand="True" MinHeight="20" />
<BoxContainer Orientation="Horizontal" HorizontalExpand="True">
<Label Text="{Loc 'fax-machine-ui-from'}" />
<Control MinWidth="4" />
<Label Name="FromLabel" />
</BoxContainer>
<BoxContainer Orientation="Horizontal" HorizontalExpand="True">
<Label Text="{Loc 'fax-machine-ui-to'}" />
<Control MinWidth="4" />
<OptionButton Name="PeerSelector" HorizontalExpand="True" />
</BoxContainer>
<Control HorizontalExpand="True" MinHeight="20" />
<BoxContainer Orientation="Horizontal" HorizontalExpand="True">
<Button Name="SendButton"
Text="{Loc 'fax-machine-ui-send-button'}"
HorizontalExpand="True"
Disabled="True" />
<Button Name="RefreshButton"
Text="{Loc 'fax-machine-ui-refresh-button'}" />
</BoxContainer>
</BoxContainer>
</DefaultWindow>

View File

@@ -0,0 +1,81 @@
using System.Linq;
using Content.Shared.Fax;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
namespace Content.Client.Fax.UI;
[GenerateTypedNameReferences]
public sealed partial class FaxWindow : DefaultWindow
{
public event Action? SendButtonPressed;
public event Action? RefreshButtonPressed;
public event Action<string>? PeerSelected;
public FaxWindow()
{
RobustXamlLoader.Load(this);
SendButton.OnPressed += _ => SendButtonPressed?.Invoke();
RefreshButton.OnPressed += _ => RefreshButtonPressed?.Invoke();
PeerSelector.OnItemSelected += args =>
PeerSelected?.Invoke((string) args.Button.GetItemMetadata(args.Id)!);
}
public void UpdateState(FaxUiState state)
{
SendButton.Disabled = !state.CanSend;
FromLabel.Text = state.DeviceName;
if (state.IsPaperInserted)
{
PaperStatusLabel.FontColorOverride = Color.Green;
PaperStatusLabel.Text = Loc.GetString("fax-machine-ui-paper-inserted");
}
else
{
PaperStatusLabel.FontColorOverride = Color.Red;
PaperStatusLabel.Text = Loc.GetString("fax-machine-ui-paper-not-inserted");
}
if (state.AvailablePeers.Count == 0)
{
PeerSelector.AddItem(Loc.GetString("fax-machine-ui-no-peers"));
PeerSelector.Disabled = true;
}
if (PeerSelector.Disabled && state.AvailablePeers.Count != 0)
{
PeerSelector.Clear();
PeerSelector.Disabled = false;
}
// always must be selected destination
if (string.IsNullOrEmpty(state.DestinationAddress) && state.AvailablePeers.Count != 0)
{
PeerSelected?.Invoke(state.AvailablePeers.First().Key);
return;
}
if (state.AvailablePeers.Count != 0)
{
PeerSelector.Clear();
foreach (var (address, name) in state.AvailablePeers)
{
var id = AddPeerSelect(name, address);
if (address == state.DestinationAddress)
PeerSelector.Select(id);
}
}
}
private int AddPeerSelect(string name, string address)
{
PeerSelector.AddItem(name);
PeerSelector.SetItemMetadata(PeerSelector.ItemCount - 1, address);
return PeerSelector.ItemCount - 1;
}
}

View File

@@ -0,0 +1,28 @@
namespace Content.Server.Fax;
public static class FaxConstants
{
// Commands
/**
* Used to get other faxes connected to current network
*/
public const string FaxPingCommand = "fax_ping";
/**
* Used as response to ping command
*/
public const string FaxPongCommand = "fax_pong";
/**
* Used when fax sending data to destination fax
*/
public const string FaxPrintCommand = "fax_print";
// Data
public const string FaxNameData = "fax_data_name";
public const string FaxPaperNameData = "fax_data_title";
public const string FaxPaperContentData = "fax_data_content";
public const string FaxSyndicateData = "fax_data_i_am_syndicate";
}

View File

@@ -0,0 +1,142 @@
using Content.Shared.Containers.ItemSlots;
using Robust.Shared.Audio;
namespace Content.Server.Fax;
[RegisterComponent]
public sealed class FaxMachineComponent : Component
{
/// <summary>
/// Name with which the fax will be visible to others on the network
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField("name")]
public string FaxName { get; set; } = "Unknown";
/// <summary>
/// Device address of fax in network to which data will be send
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField("destinationAddress")]
public string? DestinationFaxAddress { get; set; }
/// <summary>
/// Contains the item to be sent, assumes it's paper...
/// </summary>
[DataField("paperSlot", required: true)]
public ItemSlot PaperSlot = new();
/// <summary>
/// Is fax machine should respond to pings in network
/// This will make it visible to others on the network
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField("responsePings")]
public bool ResponsePings { get; set; } = true;
/// <summary>
/// Should admins be notified on message receive
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField("notifyAdmins")]
public bool NotifyAdmins { get; set; } = false;
/// <summary>
/// Should that fax receive nuke codes send by admins. Probably should be captain fax only
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField("receiveNukeCodes")]
public bool ReceiveNukeCodes { get; set; } = false;
/// <summary>
/// Is fax was emaaged
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField("emagged")]
public bool Emagged { get; set; } = false;
/// <summary>
/// Sound to play when fax has been emagged
/// </summary>
[DataField("emagSound")]
public SoundSpecifier EmagSound = new SoundCollectionSpecifier("sparks");
/// <summary>
/// Sound to play when fax printing new message
/// </summary>
[DataField("printSound")]
public SoundSpecifier PrintSound = new SoundPathSpecifier("/Audio/Machines/printer.ogg");
/// <summary>
/// Sound to play when fax successfully send message
/// </summary>
[DataField("sendSound")]
public SoundSpecifier SendSound = new SoundPathSpecifier("/Audio/Machines/high_tech_confirm.ogg");
/// <summary>
/// Known faxes in network by address with fax names
/// </summary>
[ViewVariables]
public Dictionary<string, string> KnownFaxes { get; } = new();
/// <summary>
/// Print queue of the incoming message
/// </summary>
[ViewVariables]
[DataField("printingQueue")]
public Queue<FaxPrintout> PrintingQueue { get; } = new();
/// <summary>
/// Message sending timeout
/// </summary>
[ViewVariables]
[DataField("sendTimeoutRemaining")]
public float SendTimeoutRemaining;
/// <summary>
/// Message sending timeout
/// </summary>
[ViewVariables]
[DataField("sendTimeout")]
public float SendTimeout = 5f;
/// <summary>
/// Remaining time of inserting animation
/// </summary>
[DataField("insertingTimeRemaining")]
public float InsertingTimeRemaining;
/// <summary>
/// How long the inserting animation will play
/// </summary>
[ViewVariables]
public float InsertionTime = 1.88f; // 0.02 off for correct animation
/// <summary>
/// Remaining time of printing animation
/// </summary>
[DataField("printingTimeRemaining")]
public float PrintingTimeRemaining;
/// <summary>
/// How long the printing animation will play
/// </summary>
[ViewVariables]
public float PrintingTime = 2.3f;
}
[DataDefinition]
public sealed class FaxPrintout
{
[DataField("name")]
public string Name { get; }
[DataField("content")]
public string Content { get; }
public FaxPrintout(string content, string name)
{
Content = content;
Name = name;
}
}

View File

@@ -0,0 +1,442 @@
using Content.Server.Administration;
using Content.Server.Administration.Managers;
using Content.Server.Chat.Managers;
using Content.Server.DeviceNetwork;
using Content.Server.DeviceNetwork.Components;
using Content.Server.DeviceNetwork.Systems;
using Content.Server.Paper;
using Content.Server.Popups;
using Content.Server.Power.Components;
using Content.Server.Power.EntitySystems;
using Content.Server.Tools;
using Content.Server.UserInterface;
using Content.Shared.Containers.ItemSlots;
using Content.Shared.Emag.Systems;
using Content.Shared.Fax;
using Content.Shared.Interaction;
using Robust.Server.GameObjects;
using Robust.Shared.Containers;
using Robust.Shared.Player;
namespace Content.Server.Fax;
public sealed class FaxSystem : EntitySystem
{
[Dependency] private readonly IChatManager _chat = default!;
[Dependency] private readonly IAdminManager _adminManager = default!;
[Dependency] private readonly ItemSlotsSystem _itemSlotsSystem = default!;
[Dependency] private readonly SharedAppearanceSystem _appearanceSystem = default!;
[Dependency] private readonly PopupSystem _popupSystem = default!;
[Dependency] private readonly DeviceNetworkSystem _deviceNetworkSystem = default!;
[Dependency] private readonly PaperSystem _paperSystem = default!;
[Dependency] private readonly SharedAudioSystem _audioSystem = default!;
[Dependency] private readonly ToolSystem _toolSystem = default!;
[Dependency] private readonly QuickDialogSystem _quickDialog = default!;
[Dependency] private readonly UserInterfaceSystem _userInterface = default!;
public const string PaperSlotId = "Paper";
public override void Initialize()
{
base.Initialize();
// Hooks
SubscribeLocalEvent<FaxMachineComponent, ComponentInit>(OnComponentInit);
SubscribeLocalEvent<FaxMachineComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<FaxMachineComponent, ComponentRemove>(OnComponentRemove);
SubscribeLocalEvent<FaxMachineComponent, EntInsertedIntoContainerMessage>(OnItemSlotChanged);
SubscribeLocalEvent<FaxMachineComponent, EntRemovedFromContainerMessage>(OnItemSlotChanged);
SubscribeLocalEvent<FaxMachineComponent, PowerChangedEvent>(OnPowerChanged);
SubscribeLocalEvent<FaxMachineComponent, DeviceNetworkPacketEvent>(OnPacketReceived);
// Interaction
SubscribeLocalEvent<FaxMachineComponent, InteractUsingEvent>(OnInteractUsing);
SubscribeLocalEvent<FaxMachineComponent, GotEmaggedEvent>(OnEmagged);
// UI
SubscribeLocalEvent<FaxMachineComponent, AfterActivatableUIOpenEvent>(OnToggleInterface);
SubscribeLocalEvent<FaxMachineComponent, FaxSendMessage>(OnSendButtonPressed);
SubscribeLocalEvent<FaxMachineComponent, FaxRefreshMessage>(OnRefreshButtonPressed);
SubscribeLocalEvent<FaxMachineComponent, FaxDestinationMessage>(OnDestinationSelected);
}
public override void Update(float frameTime)
{
base.Update(frameTime);
foreach (var (comp, receiver) in EntityQuery<FaxMachineComponent, ApcPowerReceiverComponent>())
{
if (!receiver.Powered)
continue;
ProcessPrintingAnimation(frameTime, comp);
ProcessInsertingAnimation(frameTime, comp);
ProcessSendingTimeout(frameTime, comp);
}
}
private void ProcessPrintingAnimation(float frameTime, FaxMachineComponent comp)
{
if (comp.PrintingTimeRemaining > 0)
{
comp.PrintingTimeRemaining -= frameTime;
UpdateAppearance(comp.Owner, comp);
var isAnimationEnd = comp.PrintingTimeRemaining <= 0;
if (isAnimationEnd)
{
SpawnPaperFromQueue(comp.Owner, comp);
UpdateUserInterface(comp.Owner, comp);
}
return;
}
if (comp.PrintingQueue.Count > 0)
{
comp.PrintingTimeRemaining = comp.PrintingTime;
_audioSystem.PlayPvs(comp.PrintSound, comp.Owner);
}
}
private void ProcessInsertingAnimation(float frameTime, FaxMachineComponent comp)
{
if (comp.InsertingTimeRemaining <= 0)
return;
comp.InsertingTimeRemaining -= frameTime;
UpdateAppearance(comp.Owner, comp);
var isAnimationEnd = comp.InsertingTimeRemaining <= 0;
if (isAnimationEnd)
{
_itemSlotsSystem.SetLock(comp.Owner, comp.PaperSlot, false);
UpdateUserInterface(comp.Owner, comp);
}
}
private void ProcessSendingTimeout(float frameTime, FaxMachineComponent comp)
{
if (comp.SendTimeoutRemaining > 0)
{
comp.SendTimeoutRemaining -= frameTime;
if (comp.SendTimeoutRemaining <= 0)
UpdateUserInterface(comp.Owner, comp);
}
}
private void OnComponentInit(EntityUid uid, FaxMachineComponent component, ComponentInit args)
{
_itemSlotsSystem.AddItemSlot(uid, FaxSystem.PaperSlotId, component.PaperSlot);
UpdateAppearance(uid, component);
}
private void OnComponentRemove(EntityUid uid, FaxMachineComponent component, ComponentRemove args)
{
_itemSlotsSystem.RemoveItemSlot(uid, component.PaperSlot);
}
private void OnMapInit(EntityUid uid, FaxMachineComponent component, MapInitEvent args)
{
// Load all faxes on map in cache each other to prevent taking same name by user created fax
Refresh(uid, component);
}
private void OnItemSlotChanged(EntityUid uid, FaxMachineComponent component, ContainerModifiedMessage args)
{
if (!component.Initialized)
return;
if (args.Container.ID != component.PaperSlot.ID)
return;
var isPaperInserted = component.PaperSlot.Item.HasValue;
if (isPaperInserted)
{
component.InsertingTimeRemaining = component.InsertionTime;
_itemSlotsSystem.SetLock(uid, component.PaperSlot, true);
}
UpdateUserInterface(uid, component);
}
private void OnPowerChanged(EntityUid uid, FaxMachineComponent component, ref PowerChangedEvent args)
{
var isInsertInterrupted = !args.Powered && component.InsertingTimeRemaining > 0;
if (isInsertInterrupted)
{
component.InsertingTimeRemaining = 0f; // Reset animation
// Drop from slot because animation did not play completely
_itemSlotsSystem.SetLock(uid, component.PaperSlot, false);
_itemSlotsSystem.TryEject(uid, component.PaperSlot, null, out var _, true);
}
var isPrintInterrupted = !args.Powered && component.PrintingTimeRemaining > 0;
if (isPrintInterrupted)
{
component.PrintingTimeRemaining = 0f; // Reset animation
}
if (isInsertInterrupted || isPrintInterrupted)
UpdateAppearance(component.Owner, component);
_itemSlotsSystem.SetLock(uid, component.PaperSlot, !args.Powered); // Lock slot when power is off
}
private void OnInteractUsing(EntityUid uid, FaxMachineComponent component, InteractUsingEvent args)
{
if (args.Handled ||
!TryComp<ActorComponent>(args.User, out var actor) ||
!_toolSystem.HasQuality(args.Used, "Screwing")) // Screwing because Pulsing already used by device linking
return;
_quickDialog.OpenDialog(actor.PlayerSession,
Loc.GetString("fax-machine-dialog-rename"),
Loc.GetString("fax-machine-dialog-field-name"),
(string newName) =>
{
if (component.FaxName == newName)
return;
if (newName.Length > 20)
{
_popupSystem.PopupEntity(Loc.GetString("fax-machine-popup-name-long"), uid, Filter.Pvs(uid));
return;
}
if (component.KnownFaxes.ContainsValue(newName) && !component.Emagged) // Allow exist names if emagged for fun
{
_popupSystem.PopupEntity(Loc.GetString("fax-machine-popup-name-exist"), uid, Filter.Pvs(uid));
return;
}
component.FaxName = newName;
_popupSystem.PopupEntity(Loc.GetString("fax-machine-popup-name-set"), uid, Filter.Pvs(uid));
UpdateUserInterface(uid, component);
});
args.Handled = true;
}
private void OnEmagged(EntityUid uid, FaxMachineComponent component, GotEmaggedEvent args)
{
if (component.Emagged)
return;
_audioSystem.PlayPvs(component.EmagSound, uid);
component.Emagged = true;
args.Handled = true;
}
private void OnPacketReceived(EntityUid uid, FaxMachineComponent component, DeviceNetworkPacketEvent args)
{
if (!HasComp<DeviceNetworkComponent>(uid) || string.IsNullOrEmpty(args.SenderAddress))
return;
if (args.Data.TryGetValue(DeviceNetworkConstants.Command, out string? command))
{
switch (command)
{
case FaxConstants.FaxPingCommand:
var isForSyndie = component.Emagged &&
args.Data.ContainsKey(FaxConstants.FaxSyndicateData);
if (!isForSyndie && !component.ResponsePings)
return;
var payload = new NetworkPayload()
{
{ DeviceNetworkConstants.Command, FaxConstants.FaxPongCommand },
{ FaxConstants.FaxNameData, component.FaxName }
};
_deviceNetworkSystem.QueuePacket(uid, args.SenderAddress, payload);
break;
case FaxConstants.FaxPongCommand:
if (!args.Data.TryGetValue(FaxConstants.FaxNameData, out string? faxName))
return;
component.KnownFaxes[args.SenderAddress] = faxName;
UpdateUserInterface(uid, component);
break;
case FaxConstants.FaxPrintCommand:
if (!args.Data.TryGetValue(FaxConstants.FaxPaperNameData, out string? name) ||
!args.Data.TryGetValue(FaxConstants.FaxPaperContentData, out string? content))
return;
var printout = new FaxPrintout(content, name);
Receive(uid, printout, args.SenderAddress);
break;
}
}
}
private void OnToggleInterface(EntityUid uid, FaxMachineComponent component, AfterActivatableUIOpenEvent args)
{
UpdateUserInterface(uid, component);
}
private void OnSendButtonPressed(EntityUid uid, FaxMachineComponent component, FaxSendMessage args)
{
Send(uid, component);
}
private void OnRefreshButtonPressed(EntityUid uid, FaxMachineComponent component, FaxRefreshMessage args)
{
Refresh(uid, component);
}
private void OnDestinationSelected(EntityUid uid, FaxMachineComponent component, FaxDestinationMessage args)
{
SetDestination(uid, args.Address, component);
}
private void UpdateAppearance(EntityUid uid, FaxMachineComponent? component = null)
{
if (!Resolve(uid, ref component))
return;
if (component.InsertingTimeRemaining > 0)
_appearanceSystem.SetData(uid, FaxMachineVisuals.VisualState, FaxMachineVisualState.Inserting);
else if (component.PrintingTimeRemaining > 0)
_appearanceSystem.SetData(uid, FaxMachineVisuals.VisualState, FaxMachineVisualState.Printing);
else
_appearanceSystem.SetData(uid, FaxMachineVisuals.VisualState, FaxMachineVisualState.Normal);
}
private void UpdateUserInterface(EntityUid uid, FaxMachineComponent? component = null)
{
if (!Resolve(uid, ref component))
return;
var isPaperInserted = component.PaperSlot.Item != null;
var canSend = isPaperInserted &&
component.DestinationFaxAddress != null &&
component.SendTimeoutRemaining <= 0 &&
component.InsertingTimeRemaining <= 0;
var state = new FaxUiState(component.FaxName, component.KnownFaxes, canSend, isPaperInserted, component.DestinationFaxAddress);
_userInterface.TrySetUiState(uid, FaxUiKey.Key, state);
}
/// <summary>
/// Set fax destination address not checking if he knows it exists
/// </summary>
public void SetDestination(EntityUid uid, string destAddress, FaxMachineComponent? component = null)
{
if (!Resolve(uid, ref component))
return;
component.DestinationFaxAddress = destAddress;
UpdateUserInterface(uid, component);
}
/// <summary>
/// Clears current known fax info and make network scan ping
/// Adds special data to payload if it was emagged to identify itself as a Syndicate
/// </summary>
public void Refresh(EntityUid uid, FaxMachineComponent? component = null)
{
if (!Resolve(uid, ref component))
return;
component.DestinationFaxAddress = null;
component.KnownFaxes.Clear();
var payload = new NetworkPayload()
{
{ DeviceNetworkConstants.Command, FaxConstants.FaxPingCommand }
};
if (component.Emagged)
payload.Add(FaxConstants.FaxSyndicateData, true);
_deviceNetworkSystem.QueuePacket(uid, null, payload);
}
/// <summary>
/// Sends message to addressee if paper is set and a known fax is selected
/// A timeout is set after sending
/// </summary>
public void Send(EntityUid uid, FaxMachineComponent? component = null)
{
if (!Resolve(uid, ref component))
return;
var sendEntity = component.PaperSlot.Item;
if (sendEntity == null)
return;
if (component.DestinationFaxAddress == null)
return;
if (!component.KnownFaxes.TryGetValue(component.DestinationFaxAddress, out var faxName))
return;
if (!TryComp<MetaDataComponent>(sendEntity, out var metadata) ||
!TryComp<PaperComponent>(sendEntity, out var paper))
return;
var payload = new NetworkPayload()
{
{ DeviceNetworkConstants.Command, FaxConstants.FaxPrintCommand },
{ FaxConstants.FaxPaperNameData, metadata.EntityName },
{ FaxConstants.FaxPaperContentData, paper.Content },
};
_deviceNetworkSystem.QueuePacket(uid, component.DestinationFaxAddress, payload);
component.SendTimeoutRemaining += component.SendTimeout;
_audioSystem.PlayPvs(component.SendSound, uid);
UpdateUserInterface(uid, component);
}
/// <summary>
/// Accepts a new message and adds it to the queue to print
/// If has parameter "notifyAdmins" also output a special message to admin chat.
/// </summary>
public void Receive(EntityUid uid, FaxPrintout printout, string? fromAddress, FaxMachineComponent? component = null)
{
if (!Resolve(uid, ref component))
return;
var faxName = Loc.GetString("fax-machine-popup-source-unknown");
if (fromAddress != null && component.KnownFaxes.ContainsKey(fromAddress)) // If message received from unknown for fax address
faxName = component.KnownFaxes[fromAddress];
_popupSystem.PopupEntity(Loc.GetString("fax-machine-popup-received", ("from", faxName)), uid, Filter.Pvs(uid));
_appearanceSystem.SetData(uid, FaxMachineVisuals.VisualState, FaxMachineVisualState.Printing);
if (component.NotifyAdmins)
NotifyAdmins(faxName);
component.PrintingQueue.Enqueue(printout);
}
private void SpawnPaperFromQueue(EntityUid uid, FaxMachineComponent? component = null)
{
if (!Resolve(uid, ref component) || component.PrintingQueue.Count == 0)
return;
var printout = component.PrintingQueue.Dequeue();
var printed = EntityManager.SpawnEntity("Paper", Transform(uid).Coordinates);
if (TryComp<PaperComponent>(printed, out var paper))
_paperSystem.SetContent(printed, printout.Content);
if (TryComp<MetaDataComponent>(printed, out var metadata))
metadata.EntityName = printout.Name;
}
private void NotifyAdmins(string faxName)
{
_chat.SendAdminAnnouncement(Loc.GetString("fax-machine-chat-notify", ("fax", faxName)));
_audioSystem.PlayGlobal("/Audio/Machines/high_tech_confirm.ogg", Filter.Empty().AddPlayers(_adminManager.ActiveAdmins), false);
}
}

View File

@@ -1,5 +1,7 @@
using System.Diagnostics.CodeAnalysis;
using Content.Server.Chat.Systems; using Content.Server.Chat.Systems;
using Content.Server.Communications; using Content.Server.Communications;
using Content.Server.Fax;
using Content.Server.Paper; using Content.Server.Paper;
using Content.Server.Station.Components; using Content.Server.Station.Components;
using Content.Server.Station.Systems; using Content.Server.Station.Systems;
@@ -10,8 +12,8 @@ namespace Content.Server.Nuke
{ {
[Dependency] private readonly ChatSystem _chatSystem = default!; [Dependency] private readonly ChatSystem _chatSystem = default!;
[Dependency] private readonly StationSystem _station = default!; [Dependency] private readonly StationSystem _station = default!;
[Dependency] private readonly PaperSystem _paper = default!;
private const string NukePaperPrototype = "NukeCodePaper"; [Dependency] private readonly FaxSystem _faxSystem = default!;
public override void Initialize() public override void Initialize()
{ {
@@ -25,34 +27,18 @@ namespace Content.Server.Nuke
SetupPaper(uid); SetupPaper(uid);
} }
private void SetupPaper(EntityUid uid, EntityUid? station = null, PaperComponent? paper = null) private void SetupPaper(EntityUid uid, EntityUid? station = null)
{ {
if (!Resolve(uid, ref paper)) if (TryGetRelativeNukeCode(uid, out var paperContent, station))
{ {
return; _paper.SetContent(uid, paperContent);
}
var owningStation = station ?? _station.GetOwningStation(uid);
var transform = Transform(uid);
// Find the first nuke that matches the paper's location.
foreach (var nuke in EntityQuery<NukeComponent>())
{
if (owningStation == null && nuke.OriginMapGrid != (transform.MapID, transform.GridUid)
|| nuke.OriginStation != owningStation)
{
continue;
}
paper.Content += $"{MetaData(nuke.Owner).EntityName} - {nuke.Code}";
break;
} }
} }
/// <summary> /// <summary>
/// Send a nuclear code to all communication consoles /// Send a nuclear code to all faxes on that station which are authorized to receive nuke codes.
/// </summary> /// </summary>
/// <returns>True if at least one console received codes</returns> /// <returns>True if at least one fax received codes</returns>
public bool SendNukeCodes(EntityUid station) public bool SendNukeCodes(EntityUid station)
{ {
if (!HasComp<StationDataComponent>(station)) if (!HasComp<StationDataComponent>(station))
@@ -60,20 +46,17 @@ namespace Content.Server.Nuke
return false; return false;
} }
// todo: this should probably be handled by fax system
var wasSent = false; var wasSent = false;
var consoles = EntityQuery<CommunicationsConsoleComponent, TransformComponent>(); var faxes = EntityManager.EntityQuery<FaxMachineComponent>();
foreach (var (console, transform) in consoles) foreach (var fax in faxes)
{ {
var owningStation = _station.GetOwningStation(console.Owner); if (!fax.ReceiveNukeCodes || !TryGetRelativeNukeCode(fax.Owner, out var paperContent, station))
if (owningStation == null || owningStation != station)
{ {
continue; continue;
} }
var consolePos = transform.MapPosition; var printout = new FaxPrintout(paperContent, Loc.GetString("nuke-codes-fax-paper-name"));
var uid = Spawn(NukePaperPrototype, consolePos); _faxSystem.Receive(fax.Owner, printout, null, fax);
SetupPaper(uid, station);
wasSent = true; wasSent = true;
} }
@@ -86,5 +69,35 @@ namespace Content.Server.Nuke
return wasSent; return wasSent;
} }
private bool TryGetRelativeNukeCode(
EntityUid uid,
[NotNullWhen(true)] out string? nukeCode,
EntityUid? station = null,
TransformComponent? transform = null)
{
nukeCode = null;
if (!Resolve(uid, ref transform))
{
return false;
}
var owningStation = station ?? _station.GetOwningStation(uid);
// Find the first nuke that matches the passed location.
foreach (var nuke in EntityQuery<NukeComponent>())
{
if (owningStation == null && nuke.OriginMapGrid != (transform.MapID, transform.GridUid)
|| nuke.OriginStation != owningStation)
{
continue;
}
nukeCode = Loc.GetString("nuke-codes-message", ("name", MetaData(nuke.Owner).EntityName), ("code", nuke.Code));
return true;
}
return false;
}
} }
} }

View File

@@ -0,0 +1,17 @@
using Robust.Shared.Serialization;
namespace Content.Shared.Fax;
[Serializable, NetSerializable]
public enum FaxMachineVisuals : byte
{
VisualState,
}
[Serializable, NetSerializable]
public enum FaxMachineVisualState : byte
{
Normal,
Inserting,
Printing
}

View File

@@ -0,0 +1,53 @@
using Robust.Shared.Serialization;
namespace Content.Shared.Fax;
[Serializable, NetSerializable]
public enum FaxUiKey : byte
{
Key
}
[Serializable, NetSerializable]
public sealed class FaxUiState : BoundUserInterfaceState
{
public string DeviceName { get; }
public Dictionary<string, string> AvailablePeers { get; }
public string? DestinationAddress { get; }
public bool IsPaperInserted { get; }
public bool CanSend { get; }
public FaxUiState(string deviceName,
Dictionary<string, string> peers,
bool canSend,
bool isPaperInserted,
string? destAddress)
{
DeviceName = deviceName;
AvailablePeers = peers;
IsPaperInserted = isPaperInserted;
CanSend = canSend;
DestinationAddress = destAddress;
}
}
[Serializable, NetSerializable]
public sealed class FaxSendMessage : BoundUserInterfaceMessage
{
}
[Serializable, NetSerializable]
public sealed class FaxRefreshMessage : BoundUserInterfaceMessage
{
}
[Serializable, NetSerializable]
public sealed class FaxDestinationMessage : BoundUserInterfaceMessage
{
public string Address { get; }
public FaxDestinationMessage(string address)
{
Address = address;
}
}

Binary file not shown.

View File

@@ -20,4 +20,12 @@ machine_vend_hot_drink.ogg original from https://freesound.org/people/waxsocks/s
scan_loop.ogg from https://freesound.org/people/steaq/sounds/509249/ CC-0 by steaq scan_loop.ogg from https://freesound.org/people/steaq/sounds/509249/ CC-0 by steaq
scan_finish.ogg from https://freesound.org/people/pan14/sounds/263133/ CC-0 by pan14 scan_finish.ogg from https://freesound.org/people/pan14/sounds/263133/ CC-0 by pan14
printer.ogg from https://github.com/tgstation/tgstation/blob/31c5aaf4b885a50c9d5a0777e2647af64e7193bf/sound/machines/printer.ogg
high_tech_confirm.ogg from https://github.com/tgstation/tgstation/blob/14aa5d2d8efca6370778d730e36ffcf258b9352e/sound/machines/high_tech_confirm.ogg
tray_eject.ogg from https://github.com/tgstation/tgstation/blob/3eeba3899f22638595333c63b7b7433001f91bb2/sound/machines/eject.ogg
scanning.ogg from https://freesound.org/people/SamuelGremaud/sounds/455375/ and edited

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -4,6 +4,7 @@ device-frequency-prototype-name-suit-sensors = Suit Sensors
device-frequency-prototype-name-lights = Smart Lights device-frequency-prototype-name-lights = Smart Lights
device-frequency-prototype-name-mailing-units = Mailing Units device-frequency-prototype-name-mailing-units = Mailing Units
device-frequency-prototype-name-pdas = PDAs device-frequency-prototype-name-pdas = PDAs
device-frequency-prototype-name-fax = Fax
## camera frequencies ## camera frequencies
device-frequency-prototype-name-surveillance-camera-test = Subnet Test device-frequency-prototype-name-surveillance-camera-test = Subnet Test

View File

@@ -0,0 +1,20 @@
fax-machine-popup-source-unknown = unknown
fax-machine-popup-received = Received correspondence from { $from }.
fax-machine-popup-name-long = Fax name is too long
fax-machine-popup-name-exist = Fax with same name already exist in network
fax-machine-popup-name-set = Fax name has been updated
fax-machine-dialog-rename = Rename
fax-machine-dialog-field-name = Name
fax-machine-ui-window = Fax Machine
fax-machine-ui-send-button = Send
fax-machine-ui-refresh-button = Refresh
fax-machine-ui-no-peers = No Peers
fax-machine-ui-to = To:
fax-machine-ui-from = From:
fax-machine-ui-paper = Paper:
fax-machine-ui-paper-inserted = Paper in tray
fax-machine-ui-paper-not-inserted = No paper
fax-machine-chat-notify = Received new fax message on "{$fax}" fax

View File

@@ -32,3 +32,8 @@ nuke-label-nanotrasen = NT-{$serial}
# do you even need this one? It's more funnier to say that # do you even need this one? It's more funnier to say that
# the Syndicate stole a NT nuke # the Syndicate stole a NT nuke
nuke-label-syndicate = SYN-{$serial} nuke-label-syndicate = SYN-{$serial}
# Codes
nuke-codes-message = [color=red]TOP SECRET![/color]
Nuclear device activation code: {$name} - {$code}
nuke-codes-fax-paper-name = nuclear authentication codes

View File

@@ -25,6 +25,7 @@ verb-categories-instrument-style = Instrument Style
verb-categories-set-sensor = Sensor verb-categories-set-sensor = Sensor
verb-categories-timer = Set Delay verb-categories-timer = Set Delay
verb-categories-lever = Lever verb-categories-lever = Lever
verb-categories-fax = Set Destination
verb-common-toggle-light = Toggle light verb-common-toggle-light = Toggle light
verb-common-close = Close verb-common-close = Close

View File

@@ -74,3 +74,8 @@
id: PDA id: PDA
name: device-frequency-prototype-name-pdas name: device-frequency-prototype-name-pdas
frequency: 2202 frequency: 2202
- type: deviceFrequency
id: Fax
name: device-frequency-prototype-name-fax
frequency: 2640

View File

@@ -57,9 +57,6 @@
components: components:
- type: NukeCodePaper - type: NukeCodePaper
- type: Paper - type: Paper
content: |
[color=red]TOP SECRET![/color]
Nuclear device activation code:
- type: entity - type: entity
name: pen name: pen

View File

@@ -0,0 +1,106 @@
- type: entity
parent: BaseMachinePowered
id: FaxMachineBase
name: long range fax machine
description: Bluespace technologies on the application of bureaucracy.
components:
- type: Sprite
sprite: Structures/Machines/fax_machine.rsi
netsync: false
layers:
- state: icon
map: ["base"]
- type: Icon
sprite: Structures/Machines/fax_machine.rsi
state: icon
- type: Appearance
- type: Physics
bodyType: Static
- type: Fixtures
fixtures:
- shape:
!type:PhysShapeAabb
bounds: "-0.25,-0.25,0.25,0.25"
density: 25
mask:
- MachineMask
layer:
- MachineLayer
- type: ActivatableUI
key: enum.FaxUiKey.Key
- type: ActivatableUIRequiresPower
- type: UserInterface
interfaces:
- key: enum.FaxUiKey.Key
type: FaxBoundUi
- type: ApcPowerReceiver
powerLoad: 250
- type: FaxMachine
paperSlot:
insertSound: /Audio/Machines/scanning.ogg
ejectSound: /Audio/Machines/tray_eject.ogg
whitelist:
components:
- Paper
- type: GenericVisualizer
visuals:
enum.PowerDeviceVisuals.Powered:
base:
True: { state: idle }
False: { state: icon }
enum.FaxMachineVisuals.VisualState:
base:
Inserting: { state: inserting }
Printing: { state: printing }
- type: ItemSlots
- type: ContainerContainer
containers:
Paper: !type:ContainerSlot
- type: DeviceNetworkRequiresPower
- type: DeviceNetwork
deviceNetId: Wireless
receiveFrequencyId: Fax
transmitFrequencyId: Fax
# Special
- type: entity
parent: FaxMachineBase
id: FaxMachineCentcom
name: centcom long range fax machine
suffix: Centcom
components:
- type: Sprite
layers:
- state: icon
map: [ "base" ]
color: "#bfe3ff"
- type: FaxMachine
name: "Central Command"
notifyAdmins: true
- type: entity
parent: FaxMachineBase
id: FaxMachineSyndie
name: syndicate long range fax machine
suffix: Syndicate
components:
- type: Sprite
layers:
- state: icon
map: [ "base" ]
color: "#a3a3a3"
- type: FaxMachine
name: "ERR*?*%!"
responsePings: false
emagged: true
notifyAdmins: true
- type: entity
parent: FaxMachineBase
id: FaxMachineCaptain
name: captain long range fax machine
suffix: Centcom
components:
- type: FaxMachine
name: "Captain's Office"
receiveNukeCodes: true

Binary file not shown.

After

Width:  |  Height:  |  Size: 350 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 909 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -0,0 +1,89 @@
{
"version": 1,
"license": "CC-BY-SA-3.0",
"copyright": "Taken from vgstation at commit https://github.com/vgstation-coders/vgstation13/commit/695aafae161eebebdea00d4a5d624ec154d06be2",
"size": {
"x": 32,
"y": 32
},
"states": [
{
"name": "icon"
},
{
"name": "idle",
"delays": [
[
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1
]
]
},
{
"name": "inserting",
"delays": [
[
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1
]
]
},
{
"name": "printing",
"delays": [
[
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1
]
]
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB