using Content.Server.Administration; using Content.Server.Administration.Managers; using Content.Server.Chat.Managers; using Content.Server.DeviceNetwork.Systems; using Content.Server.Popups; using Content.Server.Power.Components; using Content.Server.Tools; using Content.Shared.Administration.Logs; using Content.Shared.Containers.ItemSlots; using Content.Shared.Database; using Content.Shared.DeviceNetwork; using Content.Shared.DeviceNetwork.Components; using Content.Shared.DeviceNetwork.Events; using Content.Shared.Emag.Systems; using Content.Shared.Fax; using Content.Shared.Fax.Components; using Content.Shared.Fax.Systems; using Content.Shared.Interaction; using Content.Shared.Labels.Components; using Content.Shared.Labels.EntitySystems; using Content.Shared.Mobs.Components; using Content.Shared.NameModifier.Components; using Content.Shared.Paper; using Content.Shared.Power; using Content.Shared.Tools; using Content.Shared.UserInterface; using Robust.Server.GameObjects; using Robust.Shared.Audio; using Robust.Shared.Audio.Systems; using Robust.Shared.Containers; using Robust.Shared.Player; using Robust.Shared.Prototypes; 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 LabelSystem _labelSystem = 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!; [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!; [Dependency] private readonly MetaDataSystem _metaData = default!; [Dependency] private readonly FaxecuteSystem _faxecute = default!; [Dependency] private readonly EmagSystem _emag = default!; private static readonly ProtoId ScrewingQuality = "Screwing"; private const string PaperSlotId = "Paper"; public override void Initialize() { base.Initialize(); // Hooks SubscribeLocalEvent(OnComponentInit); SubscribeLocalEvent(OnMapInit); SubscribeLocalEvent(OnComponentRemove); SubscribeLocalEvent(OnItemSlotChanged); SubscribeLocalEvent(OnItemSlotChanged); SubscribeLocalEvent(OnPowerChanged); SubscribeLocalEvent(OnPacketReceived); // Interaction SubscribeLocalEvent(OnInteractUsing); SubscribeLocalEvent(OnEmagged); // UI SubscribeLocalEvent(OnToggleInterface); SubscribeLocalEvent(OnFileButtonPressed); SubscribeLocalEvent(OnCopyButtonPressed); SubscribeLocalEvent(OnSendButtonPressed); SubscribeLocalEvent(OnRefreshButtonPressed); SubscribeLocalEvent(OnDestinationSelected); } public override void Update(float frameTime) { base.Update(frameTime); var query = EntityQueryEnumerator(); while (query.MoveNext(out var uid, out var fax, out var receiver)) { if (!receiver.Powered) continue; ProcessPrintingAnimation(uid, frameTime, fax); ProcessInsertingAnimation(uid, frameTime, fax); ProcessSendingTimeout(uid, frameTime, fax); } } private void ProcessPrintingAnimation(EntityUid uid, float frameTime, FaxMachineComponent comp) { if (comp.PrintingTimeRemaining > 0) { comp.PrintingTimeRemaining -= frameTime; UpdateAppearance(uid, comp); var isAnimationEnd = comp.PrintingTimeRemaining <= 0; if (isAnimationEnd) { SpawnPaperFromQueue(uid, comp); UpdateUserInterface(uid, comp); } return; } if (comp.PrintingQueue.Count > 0) { comp.PrintingTimeRemaining = comp.PrintingTime; _audioSystem.PlayPvs(comp.PrintSound, uid); } } private void ProcessInsertingAnimation(EntityUid uid, float frameTime, FaxMachineComponent comp) { if (comp.InsertingTimeRemaining <= 0) return; comp.InsertingTimeRemaining -= frameTime; UpdateAppearance(uid, comp); var isAnimationEnd = comp.InsertingTimeRemaining <= 0; if (isAnimationEnd) { _itemSlotsSystem.SetLock(uid, comp.PaperSlot, false); UpdateUserInterface(uid, comp); } } private void ProcessSendingTimeout(EntityUid uid, float frameTime, FaxMachineComponent comp) { if (comp.SendTimeoutRemaining > 0) { comp.SendTimeoutRemaining -= frameTime; if (comp.SendTimeoutRemaining <= 0) UpdateUserInterface(uid, comp); } } private void OnComponentInit(EntityUid uid, FaxMachineComponent component, ComponentInit args) { _itemSlotsSystem.AddItemSlot(uid, 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(uid, 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(args.User, out var actor) || !_toolSystem.HasQuality(args.Used, ScrewingQuality)) // 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); return; } if (component.KnownFaxes.ContainsValue(newName) && !_emag.CheckFlag(uid, EmagType.Interaction)) // Allow existing names if emagged for fun { _popupSystem.PopupEntity(Loc.GetString("fax-machine-popup-name-exist"), uid); return; } _adminLogger.Add(LogType.Action, LogImpact.Low, $"{ToPrettyString(args.User):user} renamed {ToPrettyString(uid):tool} from \"{component.FaxName}\" to \"{newName}\""); component.FaxName = newName; _popupSystem.PopupEntity(Loc.GetString("fax-machine-popup-name-set"), uid); UpdateUserInterface(uid, component); }); args.Handled = true; } private void OnEmagged(EntityUid uid, FaxMachineComponent component, ref GotEmaggedEvent args) { if (!_emag.CompareFlag(args.Type, EmagType.Interaction)) return; if (_emag.CheckFlag(uid, EmagType.Interaction)) return; args.Handled = true; } private void OnPacketReceived(EntityUid uid, FaxMachineComponent component, DeviceNetworkPacketEvent args) { if (!HasComp(uid) || string.IsNullOrEmpty(args.SenderAddress)) return; if (args.Data.TryGetValue(DeviceNetworkConstants.Command, out string? command)) { switch (command) { case FaxConstants.FaxPingCommand: var isForSyndie = _emag.CheckFlag(uid, EmagType.Interaction) && 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; args.Data.TryGetValue(FaxConstants.FaxPaperLabelData, out string? label); args.Data.TryGetValue(FaxConstants.FaxPaperStampStateData, out string? stampState); args.Data.TryGetValue(FaxConstants.FaxPaperStampedByData, out List? stampedBy); args.Data.TryGetValue(FaxConstants.FaxPaperPrototypeData, out string? prototypeId); args.Data.TryGetValue(FaxConstants.FaxPaperLockedData, out bool? locked); var printout = new FaxPrintout(content, name, label, prototypeId, stampState, stampedBy, locked ?? false); Receive(uid, printout, args.SenderAddress); break; } } } private void OnToggleInterface(EntityUid uid, FaxMachineComponent component, AfterActivatableUIOpenEvent args) { UpdateUserInterface(uid, component); } private void OnFileButtonPressed(EntityUid uid, FaxMachineComponent component, FaxFileMessage args) { args.Label = args.Label?[..Math.Min(args.Label.Length, FaxFileMessageValidation.MaxLabelSize)]; args.Content = args.Content[..Math.Min(args.Content.Length, FaxFileMessageValidation.MaxContentSize)]; PrintFile(uid, component, args); } private void OnCopyButtonPressed(EntityUid uid, FaxMachineComponent component, FaxCopyMessage args) { if (HasComp(component.PaperSlot.Item)) _faxecute.Faxecute(uid, component); // when button pressed it will hurt the mob. else Copy(uid, component, args); } private void OnSendButtonPressed(EntityUid uid, FaxMachineComponent component, FaxSendMessage args) { if (HasComp(component.PaperSlot.Item)) _faxecute.Faxecute(uid, component); // when button pressed it will hurt the mob. else Send(uid, component, args); } 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 (TryComp(component.PaperSlot.Item, out var faxable)) component.InsertingState = faxable.InsertingState; if (component.InsertingTimeRemaining > 0) { _appearanceSystem.SetData(uid, FaxMachineVisuals.VisualState, FaxMachineVisualState.Inserting); Dirty(uid, component); } 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 canCopy = isPaperInserted && component.SendTimeoutRemaining <= 0 && component.InsertingTimeRemaining <= 0; var state = new FaxUiState(component.FaxName, component.KnownFaxes, canSend, canCopy, isPaperInserted, component.DestinationFaxAddress); _userInterface.SetUiState(uid, FaxUiKey.Key, state); } /// /// Set fax destination address not checking if he knows it exists /// public void SetDestination(EntityUid uid, string destAddress, FaxMachineComponent? component = null) { if (!Resolve(uid, ref component)) return; component.DestinationFaxAddress = destAddress; UpdateUserInterface(uid, component); } /// /// 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 /// 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 (_emag.CheckFlag(uid, EmagType.Interaction)) payload.Add(FaxConstants.FaxSyndicateData, true); _deviceNetworkSystem.QueuePacket(uid, null, payload); } /// /// Makes fax print from a file from the computer. A timeout is set after copying, /// which is shared by the send button. /// public void PrintFile(EntityUid uid, FaxMachineComponent component, FaxFileMessage args) { var prototype = args.OfficePaper ? component.PrintOfficePaperId : component.PrintPaperId; var name = Loc.GetString("fax-machine-printed-paper-name"); var printout = new FaxPrintout(args.Content, name, args.Label, prototype); component.PrintingQueue.Enqueue(printout); component.SendTimeoutRemaining += component.SendTimeout; UpdateUserInterface(uid, component); // Unfortunately, since a paper entity does not yet exist, we have to emulate what LabelSystem will do. var nameWithLabel = (args.Label is { } label) ? $"{name} ({label})" : name; _adminLogger.Add(LogType.Action, LogImpact.Low, $"{ToPrettyString(args.Actor):actor} " + $"added print job to \"{component.FaxName}\" {ToPrettyString(uid):tool} " + $"of {nameWithLabel}: {args.Content}"); } /// /// Copies the paper in the fax. A timeout is set after copying, /// which is shared by the send button. /// public void Copy(EntityUid uid, FaxMachineComponent? component, FaxCopyMessage args) { if (!Resolve(uid, ref component)) return; if (component.SendTimeoutRemaining > 0) return; var sendEntity = component.PaperSlot.Item; if (sendEntity == null) return; if (!TryComp(sendEntity, out MetaDataComponent? metadata) || !TryComp(sendEntity, out var paper)) return; TryComp(sendEntity, out var labelComponent); TryComp(sendEntity, out var nameMod); // TODO: See comment in 'Send()' about not being able to copy whole entities var printout = new FaxPrintout(paper.Content, nameMod?.BaseName ?? metadata.EntityName, labelComponent?.CurrentLabel, metadata.EntityPrototype?.ID ?? component.PrintPaperId, paper.StampState, paper.StampedBy, paper.EditingDisabled); component.PrintingQueue.Enqueue(printout); component.SendTimeoutRemaining += component.SendTimeout; // Don't play component.SendSound - it clashes with the printing sound, which // will start immediately. UpdateUserInterface(uid, component); _adminLogger.Add(LogType.Action, LogImpact.Low, $"{ToPrettyString(args.Actor):actor} " + $"added copy job to \"{component.FaxName}\" {ToPrettyString(uid):tool} " + $"of {ToPrettyString(sendEntity):subject}: {printout.Content}"); } /// /// Sends message to addressee if paper is set and a known fax is selected /// A timeout is set after sending, which is shared by the copy button. /// public void Send(EntityUid uid, FaxMachineComponent? component, FaxSendMessage args) { if (!Resolve(uid, ref component)) return; if (component.SendTimeoutRemaining > 0) 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(sendEntity, out MetaDataComponent? metadata) || !TryComp(sendEntity, out var paper)) return; TryComp(sendEntity, out var nameMod); TryComp(sendEntity, out var labelComponent); var payload = new NetworkPayload() { { DeviceNetworkConstants.Command, FaxConstants.FaxPrintCommand }, { FaxConstants.FaxPaperNameData, nameMod?.BaseName ?? metadata.EntityName }, { FaxConstants.FaxPaperLabelData, labelComponent?.CurrentLabel }, { FaxConstants.FaxPaperContentData, paper.Content }, { FaxConstants.FaxPaperLockedData, paper.EditingDisabled }, }; if (metadata.EntityPrototype != null) { // TODO: Ideally, we could just make a copy of the whole entity when it's // faxed, in order to preserve visuals, etc.. This functionality isn't // available yet, so we'll pass along the originating prototypeId and fall // back to component.PrintPaperId in SpawnPaperFromQueue if we can't find one here. payload[FaxConstants.FaxPaperPrototypeData] = metadata.EntityPrototype.ID; } if (paper.StampState != null) { payload[FaxConstants.FaxPaperStampStateData] = paper.StampState; payload[FaxConstants.FaxPaperStampedByData] = paper.StampedBy; } _deviceNetworkSystem.QueuePacket(uid, component.DestinationFaxAddress, payload); _adminLogger.Add(LogType.Action, LogImpact.Low, $"{ToPrettyString(args.Actor):actor} " + $"sent fax from \"{component.FaxName}\" {ToPrettyString(uid):tool} " + $"to \"{faxName}\" ({component.DestinationFaxAddress}) " + $"of {ToPrettyString(sendEntity):subject}: {paper.Content}"); component.SendTimeoutRemaining += component.SendTimeout; _audioSystem.PlayPvs(component.SendSound, uid); UpdateUserInterface(uid, component); } /// /// Accepts a new message and adds it to the queue to print /// If has parameter "notifyAdmins" also output a special message to admin chat. /// public void Receive(EntityUid uid, FaxPrintout printout, string? fromAddress = null, FaxMachineComponent? component = null) { if (!Resolve(uid, ref component)) return; var faxName = Loc.GetString("fax-machine-popup-source-unknown"); if (fromAddress != null && component.KnownFaxes.TryGetValue(fromAddress, out var fax)) // If message received from unknown fax address faxName = fax; _popupSystem.PopupEntity(Loc.GetString("fax-machine-popup-received", ("from", faxName)), 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 entityToSpawn = printout.PrototypeId.Length == 0 ? component.PrintPaperId.ToString() : printout.PrototypeId; var printed = EntityManager.SpawnEntity(entityToSpawn, Transform(uid).Coordinates); if (TryComp(printed, out var paper)) { _paperSystem.SetContent((printed, paper), printout.Content); // Apply stamps if (printout.StampState != null) { foreach (var stamp in printout.StampedBy) { _paperSystem.TryStamp((printed, paper), stamp, printout.StampState); } } paper.EditingDisabled = printout.Locked; } _metaData.SetEntityName(printed, printout.Name); if (printout.Label is { } label) { _labelSystem.Label(printed, label); } _adminLogger.Add(LogType.Action, LogImpact.Low, $"\"{component.FaxName}\" {ToPrettyString(uid):tool} printed {ToPrettyString(printed):subject}: {printout.Content}"); } 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, AudioParams.Default.WithVolume(-8f)); } }