From aa73314dc6ed61ed96081b4ea43d3e8edd272d53 Mon Sep 17 00:00:00 2001 From: ultradyper Date: Sun, 16 Aug 2026 14:00:59 +0000 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20=D0=98=D0=98=20=D0=BF=D0=BE=D0=BB?= =?UTF-8?q?=D1=83=D1=87=D0=B8=D0=BB=20=D0=B4=D0=BE=D1=81=D1=82=D1=83=D0=BF?= =?UTF-8?q?=20=D0=BA=20=D0=9D=D0=B0=D0=BD=D0=BE=D0=9C=D0=B0=D0=BA=D1=81?= =?UTF-8?q?=D1=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../StationAiNanoChatBoundUserInterface.cs | 48 ++++ .../ADT/NanoChat/StationAiNanoChatWindow.cs | 26 ++ .../Cartridges/NanoChatCartridgeSystem.cs | 61 +++- .../NanoChat/StationAiNanoChatComponent.cs | 27 ++ .../ADT/NanoChat/StationAiNanoChatSystem.cs | 267 ++++++++++++++++++ .../NanoChat/SharedStationAiNanoChatSystem.cs | 38 +++ .../Locale/ru-RU/ADT/silicons/station-ai.ftl | 3 + .../Prototypes/ADT/Actions/station_ai.yml | 11 + .../Entities/Mobs/Player/silicon.yml | 4 + 9 files changed, 475 insertions(+), 10 deletions(-) create mode 100644 Content.Client/ADT/NanoChat/StationAiNanoChatBoundUserInterface.cs create mode 100644 Content.Client/ADT/NanoChat/StationAiNanoChatWindow.cs create mode 100644 Content.Server/ADT/NanoChat/StationAiNanoChatComponent.cs create mode 100644 Content.Server/ADT/NanoChat/StationAiNanoChatSystem.cs create mode 100644 Content.Shared/ADT/NanoChat/SharedStationAiNanoChatSystem.cs diff --git a/Content.Client/ADT/NanoChat/StationAiNanoChatBoundUserInterface.cs b/Content.Client/ADT/NanoChat/StationAiNanoChatBoundUserInterface.cs new file mode 100644 index 00000000000..5500a2483c0 --- /dev/null +++ b/Content.Client/ADT/NanoChat/StationAiNanoChatBoundUserInterface.cs @@ -0,0 +1,48 @@ +using Content.Client.ADT.CartridgeLoader.Cartridges; +using Content.Shared.ADT.CartridgeLoader.Cartridges; +using Content.Shared.ADT.NanoChat; +using JetBrains.Annotations; +using Robust.Client.UserInterface; + +namespace Content.Client.ADT.NanoChat; + +[UsedImplicitly] +public sealed class StationAiNanoChatBoundUserInterface(EntityUid owner, Enum uiKey) : BoundUserInterface(owner, uiKey) +{ + private StationAiNanoChatWindow? _window; + + protected override void Open() + { + base.Open(); + + _window = this.CreateWindow(); + _window.Fragment.OnMessageSent += OnMessageSent; + } + + private void OnMessageSent(NanoChatUiMessageType type, uint? number, string? content, string? job) + { + SendMessage(new StationAiNanoChatUiMessage(type, number, content, job)); + } + + protected override void UpdateState(BoundUserInterfaceState state) + { + base.UpdateState(state); + + if (state is not NanoChatUiState nanoChatState || _window == null) + return; + + _window.UpdateState(nanoChatState); + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + + if (_window == null) + return; + + _window.Fragment.OnMessageSent -= OnMessageSent; + _window.Dispose(); + _window = null; + } +} diff --git a/Content.Client/ADT/NanoChat/StationAiNanoChatWindow.cs b/Content.Client/ADT/NanoChat/StationAiNanoChatWindow.cs new file mode 100644 index 00000000000..bbe60c8a2df --- /dev/null +++ b/Content.Client/ADT/NanoChat/StationAiNanoChatWindow.cs @@ -0,0 +1,26 @@ +using System.Numerics; +using Content.Client.ADT.CartridgeLoader.Cartridges; +using Content.Shared.ADT.CartridgeLoader.Cartridges; +using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.CustomControls; + +namespace Content.Client.ADT.NanoChat; + +public sealed class StationAiNanoChatWindow : DefaultWindow +{ + public readonly NanoChatUiFragment Fragment; + + public StationAiNanoChatWindow() + { + Title = Loc.GetString("station-ai-nanochat-window-title"); + MinSize = new Vector2(600, 400); + + Fragment = new NanoChatUiFragment(); + Contents.AddChild(Fragment); + } + + public void UpdateState(NanoChatUiState state) + { + Fragment.UpdateState(state); + } +} diff --git a/Content.Server/ADT/CartridgeLoader/Cartridges/NanoChatCartridgeSystem.cs b/Content.Server/ADT/CartridgeLoader/Cartridges/NanoChatCartridgeSystem.cs index aeb21361a06..ff00bef3079 100644 --- a/Content.Server/ADT/CartridgeLoader/Cartridges/NanoChatCartridgeSystem.cs +++ b/Content.Server/ADT/CartridgeLoader/Cartridges/NanoChatCartridgeSystem.cs @@ -1,4 +1,5 @@ using System.Linq; +using Content.Server.ADT.NanoChat; using Content.Server.Administration.Logs; using Content.Server.CartridgeLoader; using Content.Server.Power.Components; @@ -10,6 +11,7 @@ using Content.Shared.ADT.CartridgeLoader.Cartridges; using Content.Shared.ADT.NanoChat; using Content.Shared.PDA; +using Content.Shared.Radio; using Content.Shared.Radio.Components; using Robust.Shared.Prototypes; using Robust.Shared.Timing; @@ -259,7 +261,7 @@ private void HandleSendMessage(Entity cartridge, ); // Attempt delivery - var (deliveryFailed, recipients) = AttemptMessageDelivery(cartridge, msg.RecipientNumber.Value); + var (deliveryFailed, recipients) = AttemptMessageDeliveryInternal(cartridge, msg.RecipientNumber.Value, cartridge.Comp.RadioChannel); // Update delivery status message = message with { DeliveryFailed = deliveryFailed }; @@ -300,17 +302,19 @@ private bool EnsureRecipientExists(Entity card, uint reci } /// - /// Attempts to deliver a message to recipients. + /// Attempts to deliver a message to recipients, including cards held by a station AI. /// - /// The sending cartridge entity + /// The sending entity (cartridge or station AI) /// The recipient's number + /// The radio channel used for delivery /// Tuple containing delivery status and recipients if found. - private (bool failed, List> recipient) AttemptMessageDelivery( - Entity sender, - uint recipientNumber) + public (bool failed, List> recipient) AttemptMessageDeliveryInternal( + EntityUid sender, + uint recipientNumber, + ProtoId channelId) { // First verify we can send from this device - var channel = _prototype.Index(sender.Comp.RadioChannel); + var channel = _prototype.Index(channelId); var sendAttemptEvent = new RadioSendAttemptEvent(channel, sender); RaiseLocalEvent(ref sendAttemptEvent); if (sendAttemptEvent.Cancelled) @@ -331,11 +335,14 @@ private bool EnsureRecipientExists(Entity card, uint reci if (foundRecipients.Count == 0) return (true, foundRecipients); + var senderStation = _station.GetOwningStation(sender); + // Now check if any of these cards can receive var deliverableRecipients = new List>(); foreach (var recipient in foundRecipients) { // Find any cartridges that have this card + var foundCartridge = false; var cartridgeQuery = EntityQueryEnumerator(); while (cartridgeQuery.MoveNext(out var receiverUid, out var receiverCart, out _)) { @@ -344,7 +351,6 @@ private bool EnsureRecipientExists(Entity card, uint reci // Check if devices are on same station/map var recipientStation = _station.GetOwningStation(receiverUid); - var senderStation = _station.GetOwningStation(sender); // Both entities must be on a station if (recipientStation == null || senderStation == null) @@ -366,8 +372,38 @@ private bool EnsureRecipientExists(Entity card, uint reci // Found valid cartridge that can receive deliverableRecipients.Add(recipient); + foundCartridge = true; break; // Only need one valid cartridge per card } + + if (foundCartridge) + continue; + + // Cards held by a station AI (e.g. in a core) can receive without a cartridge + if (!HasComp(recipient.Owner)) + continue; + + var aiRecipientStation = _station.GetOwningStation(recipient.Owner); + + // Both entities must be on a station + if (aiRecipientStation == null || senderStation == null) + continue; + + // Must be on same map/station unless long range allowed + if (!channel.LongRange && aiRecipientStation != senderStation) + continue; + + // Needs telecomms + if (!HasActiveServer(senderStation.Value) || !HasActiveServer(aiRecipientStation.Value)) + continue; + + // Check if recipient can receive + var aiReceiveAttemptEv = new RadioReceiveAttemptEvent(channel, sender, recipient.Owner); + RaiseLocalEvent(ref aiReceiveAttemptEv); + if (aiReceiveAttemptEv.Cancelled) + continue; + + deliverableRecipients.Add(recipient); } return (deliverableRecipients.Count == 0, deliverableRecipients); @@ -397,7 +433,7 @@ private bool HasActiveServer(EntityUid station) /// The sender's card entity /// The recipient's card entity /// The to deliver - private void DeliverMessageToRecipient(Entity sender, + public void DeliverMessageToRecipient(Entity sender, Entity recipient, NanoChatMessage message) { @@ -487,7 +523,7 @@ private void UpdateUIForAllCards() /// /// Gets the for a given NanoChat number. /// - private NanoChatRecipient? GetCardInfo(uint number) + public NanoChatRecipient? GetCardInfo(uint number) { // Find card with this number to get its info var query = EntityQueryEnumerator(); @@ -504,6 +540,11 @@ private void UpdateUIForAllCards() jobTitle = idCard.LocalizedJobTitle; name = idCard.FullName ?? name; } + else if (HasComp(uid)) + { + name = Name(uid); + jobTitle = Loc.GetString("job-name-station-ai"); + } return new NanoChatRecipient(number, name, jobTitle); } diff --git a/Content.Server/ADT/NanoChat/StationAiNanoChatComponent.cs b/Content.Server/ADT/NanoChat/StationAiNanoChatComponent.cs new file mode 100644 index 00000000000..5900a87cc5c --- /dev/null +++ b/Content.Server/ADT/NanoChat/StationAiNanoChatComponent.cs @@ -0,0 +1,27 @@ +using Content.Shared.Radio; +using Robust.Shared.Prototypes; + +namespace Content.Server.ADT.NanoChat; + +/// +/// Added to a station AI while it is held in a core (via the AiHeld prototype). +/// Marks its NanoChat card as deliverable without a PDA cartridge and grants the NanoChat action. +/// +[RegisterComponent] +public sealed partial class StationAiNanoChatComponent : Component +{ + /// + /// The action that opens the NanoChat UI. + /// + [DataField("NanoChat")] + public EntProtoId Action = "ActionStationAiNanoChat"; + + [DataField, AutoNetworkedField] + public EntityUid? ActionEntity; + + /// + /// The required to send or receive messages. + /// + [DataField] + public ProtoId RadioChannel = "Common"; +} diff --git a/Content.Server/ADT/NanoChat/StationAiNanoChatSystem.cs b/Content.Server/ADT/NanoChat/StationAiNanoChatSystem.cs new file mode 100644 index 00000000000..06c88ab469f --- /dev/null +++ b/Content.Server/ADT/NanoChat/StationAiNanoChatSystem.cs @@ -0,0 +1,267 @@ +using System.Linq; +using Content.Server.ADT.CartridgeLoader.Cartridges; +using Content.Server.Administration.Logs; +using Content.Server.Actions; +using Content.Server.Station.Systems; +using Content.Shared.Access.Components; +using Content.Shared.ADT.CartridgeLoader.Cartridges; +using Content.Shared.ADT.NanoChat; +using Content.Shared.Database; +using Content.Shared.Radio; +using Content.Shared.Radio.Components; +using Robust.Server.GameObjects; +using Robust.Shared.Player; +using Robust.Shared.Timing; +using Robust.Shared.Utility; + +namespace Content.Server.ADT.NanoChat; + +public sealed class StationAiNanoChatSystem : EntitySystem +{ + [Dependency] private readonly UserInterfaceSystem _uiSystem = default!; + [Dependency] private readonly ActionsSystem _action = default!; + [Dependency] private readonly StationSystem _station = default!; + [Dependency] private readonly SharedNanoChatSystem _nanoChat = default!; + [Dependency] private readonly NanoChatCartridgeSystem _nanoChatCartridge = default!; + [Dependency] private readonly IAdminLogManager _adminLogger = default!; + [Dependency] private readonly IGameTiming _timing = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnMapInit); + SubscribeLocalEvent(OnShutdown); + SubscribeLocalEvent(OnAction); + SubscribeLocalEvent(OnUiOpened); + SubscribeLocalEvent(OnCardMessageReceived); + + Subs.BuiEvents(StationAiNanoChatUiKey.Key, subs => + { + subs.Event(OnMessage); + }); + } + + private void OnUiOpened(Entity ent, ref BoundUIOpenedEvent args) + { + if (!StationAiNanoChatUiKey.Key.Equals(args.UiKey)) + return; + + UpdateUi(ent); + } + + private void OnCardMessageReceived(Entity ent, ref NanoChatMessageReceivedEvent args) + { + if (!HasComp(ent.Owner)) + return; + + UpdateUi((ent.Owner, Comp(ent.Owner))); + } + + private void OnMapInit(EntityUid uid, StationAiNanoChatComponent component, MapInitEvent args) + { + _action.AddAction(uid, ref component.ActionEntity, component.Action); + } + + private void OnShutdown(EntityUid uid, StationAiNanoChatComponent component, ComponentShutdown args) + { + _action.RemoveAction(uid, component.ActionEntity); + } + + private void OnAction(EntityUid uid, StationAiNanoChatComponent comp, StationAiNanoChatActionEvent args) + { + if (args.Handled) + return; + + if (!TryComp(uid, out var actor)) + return; + + _uiSystem.TryOpenUi(uid, StationAiNanoChatUiKey.Key, actor.Owner); + args.Handled = true; + } + + private void OnMessage(Entity ent, ref StationAiNanoChatUiMessage msg) + { + if (!TryComp(ent.Owner, out var cardComp)) + return; + + var card = new Entity(ent.Owner, cardComp); + + switch (msg.Type) + { + case NanoChatUiMessageType.NewChat: + HandleNewChat(card, msg); + break; + case NanoChatUiMessageType.SelectChat: + HandleSelectChat(card, msg); + break; + case NanoChatUiMessageType.CloseChat: + HandleCloseChat(card); + break; + case NanoChatUiMessageType.ToggleMute: + HandleToggleMute(card); + break; + case NanoChatUiMessageType.DeleteChat: + HandleDeleteChat(card, msg); + break; + case NanoChatUiMessageType.SendMessage: + HandleSendMessage(ent, card, msg); + break; + case NanoChatUiMessageType.ToggleListNumber: + HandleToggleListNumber(card); + break; + } + + UpdateUi(ent); + } + + private void HandleNewChat(Entity card, StationAiNanoChatUiMessage msg) + { + if (msg.RecipientNumber == null || msg.Content == null || msg.RecipientNumber == card.Comp.Number) + return; + + var name = msg.Content.Trim(); + var jobTitle = string.IsNullOrWhiteSpace(msg.RecipientJob) ? null : msg.RecipientJob.Trim(); + + _nanoChat.SetRecipient((card, card.Comp), msg.RecipientNumber.Value, new NanoChatRecipient(msg.RecipientNumber.Value, name, jobTitle)); + + _adminLogger.Add(LogType.Action, + LogImpact.Low, + $"{ToPrettyString(msg.Actor):user} created new NanoChat conversation with #{msg.RecipientNumber:D4} ({name})"); + + var recipientEv = new NanoChatRecipientUpdatedEvent(card); + RaiseLocalEvent(ref recipientEv); + } + + private void HandleSelectChat(Entity card, StationAiNanoChatUiMessage msg) + { + if (msg.RecipientNumber == null) + return; + + _nanoChat.SetCurrentChat((card, card.Comp), msg.RecipientNumber); + + if (_nanoChat.GetRecipient((card, card.Comp), msg.RecipientNumber.Value) is { } recipient) + { + _nanoChat.SetRecipient((card, card.Comp), + msg.RecipientNumber.Value, + recipient with { HasUnread = false }); + } + } + + private void HandleCloseChat(Entity card) + { + _nanoChat.SetCurrentChat((card, card.Comp), null); + } + + private void HandleDeleteChat(Entity card, StationAiNanoChatUiMessage msg) + { + if (msg.RecipientNumber == null) + return; + + var deleted = _nanoChat.TryDeleteChat((card, card.Comp), msg.RecipientNumber.Value, true); + + if (!deleted) + return; + + _adminLogger.Add(LogType.Action, + LogImpact.Low, + $"{ToPrettyString(msg.Actor):user} deleted NanoChat conversation with #{msg.RecipientNumber:D4}"); + } + + private void HandleToggleMute(Entity card) + { + _nanoChat.SetNotificationsMuted((card, card.Comp), !_nanoChat.GetNotificationsMuted((card, card.Comp))); + } + + private void HandleToggleListNumber(Entity card) + { + _nanoChat.SetListNumber((card, card.Comp), !_nanoChat.GetListNumber((card, card.Comp))); + } + + private void HandleSendMessage(Entity ent, + Entity card, + StationAiNanoChatUiMessage msg) + { + if (msg.RecipientNumber == null || msg.Content == null || card.Comp.Number == null) + return; + + if (!_nanoChat.EnsureRecipientExists((card, card.Comp), msg.RecipientNumber.Value, + _nanoChatCartridge.GetCardInfo(msg.RecipientNumber.Value))) + return; + + var content = msg.Content; + if (!string.IsNullOrWhiteSpace(content)) + { + content = FormattedMessage.EscapeText(content.Trim()); + if (content.Length > NanoChatMessage.MaxContentLength) + content = content[..NanoChatMessage.MaxContentLength]; + } + + var message = new NanoChatMessage( + _timing.CurTime, + content, + (uint)card.Comp.Number + ); + + var (deliveryFailed, recipients) = _nanoChatCartridge.AttemptMessageDeliveryInternal( + ent.Owner, msg.RecipientNumber.Value, ent.Comp.RadioChannel); + + message = message with { DeliveryFailed = deliveryFailed }; + + _nanoChat.AddMessage((card, card.Comp), msg.RecipientNumber.Value, message); + + var recipientsText = recipients.Count > 0 + ? string.Join(", ", recipients.Select(r => ToPrettyString(r))) + : $"#{msg.RecipientNumber:D4}"; + + _adminLogger.Add(LogType.Chat, + LogImpact.Low, + $"{ToPrettyString(card):user} sent NanoChat message to {recipientsText}: {content}{(deliveryFailed ? " [DELIVERY FAILED]" : "")}"); + + var msgEv = new NanoChatMessageReceivedEvent(card); + RaiseLocalEvent(ref msgEv); + + if (deliveryFailed) + return; + + foreach (var recipient in recipients) + { + _nanoChatCartridge.DeliverMessageToRecipient(card, recipient, message); + } + } + + private void UpdateUi(Entity ent) + { + if (!TryComp(ent.Owner, out var card)) + return; + + List? contacts = null; + if (_station.GetOwningStation(ent.Owner) is { } station) + { + contacts = new List(); + + var query = AllEntityQuery(); + while (query.MoveNext(out var entityId, out var nanoChatCard, out var idCardComponent)) + { + if (nanoChatCard.ListNumber && nanoChatCard.Number is uint nanoChatNumber && + idCardComponent.FullName is string fullName && + _station.GetOwningStation(entityId) == station) + { + contacts.Add(new NanoChatRecipient(nanoChatNumber, fullName)); + } + } + contacts.Sort((contactA, contactB) => string.CompareOrdinal(contactA.Name, contactB.Name)); + } + + var state = new NanoChatUiState(card.Recipients, + card.Messages, + contacts, + card.CurrentChat, + card.Number ?? 0, + card.MaxRecipients, + card.NotificationsMuted, + card.ListNumber); + + _uiSystem.SetUiState(ent.Owner, StationAiNanoChatUiKey.Key, state); + } +} diff --git a/Content.Shared/ADT/NanoChat/SharedStationAiNanoChatSystem.cs b/Content.Shared/ADT/NanoChat/SharedStationAiNanoChatSystem.cs new file mode 100644 index 00000000000..027da130fd7 --- /dev/null +++ b/Content.Shared/ADT/NanoChat/SharedStationAiNanoChatSystem.cs @@ -0,0 +1,38 @@ +using Content.Shared.Actions; +using Content.Shared.ADT.CartridgeLoader.Cartridges; +using Robust.Shared.Serialization; + +namespace Content.Shared.ADT.NanoChat; + +[Serializable, NetSerializable] +public enum StationAiNanoChatUiKey : byte +{ + Key +} + +public sealed partial class StationAiNanoChatActionEvent : InstantActionEvent +{ +} + +[Serializable, NetSerializable] +public sealed class StationAiNanoChatUiMessage : BoundUserInterfaceMessage +{ + public readonly NanoChatUiMessageType Type; + + public readonly uint? RecipientNumber; + + public readonly string? Content; + + public readonly string? RecipientJob; + + public StationAiNanoChatUiMessage(NanoChatUiMessageType type, + uint? recipientNumber = null, + string? content = null, + string? recipientJob = null) + { + Type = type; + RecipientNumber = recipientNumber; + Content = content; + RecipientJob = recipientJob; + } +} diff --git a/Resources/Locale/ru-RU/ADT/silicons/station-ai.ftl b/Resources/Locale/ru-RU/ADT/silicons/station-ai.ftl index e4cacbe7830..5fa3f0b2f13 100644 --- a/Resources/Locale/ru-RU/ADT/silicons/station-ai.ftl +++ b/Resources/Locale/ru-RU/ADT/silicons/station-ai.ftl @@ -21,3 +21,6 @@ ent-ActionStationAiAtmosphericAlerts = Интерфейс атмосферной .desc = Просмотр интерфейса атмосферной сигнализации. ent-ActionStationAiInfo = Просмотреть информацию .desc = Просмотрите общую информацию о станции. +ent-ActionStationAiNanoChat = НаноМакс + .desc = Открыть мессенджер НаноМакс. +station-ai-nanochat-window-title = НаноМакс diff --git a/Resources/Prototypes/ADT/Actions/station_ai.yml b/Resources/Prototypes/ADT/Actions/station_ai.yml index 68d453b7736..1cff3776cbf 100644 --- a/Resources/Prototypes/ADT/Actions/station_ai.yml +++ b/Resources/Prototypes/ADT/Actions/station_ai.yml @@ -101,3 +101,14 @@ priority: -9 - type: InstantAction event: !type:ToggleRemoteDevicesScreenEvent + +- type: entity + id: ActionStationAiNanoChat + components: + - type: Action + icon: {sprite: ADT/Misc/program_icons.rsi, state: nanochat} + iconOn: {sprite: ADT/Misc/program_icons.rsi, state: nanochat} + keywords: [ "AI", "console", "interface" ] + priority: -4 + - type: InstantAction + event: !type:StationAiNanoChatActionEvent diff --git a/Resources/Prototypes/Entities/Mobs/Player/silicon.yml b/Resources/Prototypes/Entities/Mobs/Player/silicon.yml index c42673dd349..12c7613e577 100644 --- a/Resources/Prototypes/Entities/Mobs/Player/silicon.yml +++ b/Resources/Prototypes/Entities/Mobs/Player/silicon.yml @@ -76,6 +76,8 @@ type: AtmosAlertsComputerBoundUserInterface enum.StationAiInfoUiKey.Key: type: StationAiInfoBoundUserInterface + enum.StationAiNanoChatUiKey.Key: + type: StationAiNanoChatBoundUserInterface # ADT End - type: IntrinsicUI uis: @@ -94,6 +96,7 @@ toggleAction: ADTActionOpenRemoteDevicesMenu - type: AtmosAlertsComputer - type: StationAiInfo + - type: StationAiNanoChat - type: CrewManifestViewer ownerKey: enum.StationAiInfoUiKey.Key # ADT End @@ -538,6 +541,7 @@ - type: CollectiveMindRank rankName: collective-mind-sai-rank - type: StationAiBrain + - type: NanoChatCard # ADT-Tweak-End - type: StartingMindRole mindRole: "MindRoleSiliconBrain" From f1b1d010b7022a86d5748845ea5396123f233676 Mon Sep 17 00:00:00 2001 From: ultradyper Date: Sun, 16 Aug 2026 17:36:32 +0000 Subject: [PATCH 2/2] =?UTF-8?q?=D0=9F=D0=BE=D0=BC=D1=8B=D0=BB=20=D1=81=20?= =?UTF-8?q?=D0=BC=D1=8B=D0=BB=D0=BE=D0=BC=20=D0=BA=D0=BE=D0=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../StationAiNanoChatBoundUserInterface.cs | 14 +- .../Cartridges/NanoChatCartridgeSystem.cs | 121 ++++++++++++--- .../NanoChat/StationAiNanoChatComponent.cs | 4 +- .../ADT/NanoChat/StationAiNanoChatSystem.cs | 139 +----------------- 4 files changed, 117 insertions(+), 161 deletions(-) diff --git a/Content.Client/ADT/NanoChat/StationAiNanoChatBoundUserInterface.cs b/Content.Client/ADT/NanoChat/StationAiNanoChatBoundUserInterface.cs index 5500a2483c0..ea1254efeb2 100644 --- a/Content.Client/ADT/NanoChat/StationAiNanoChatBoundUserInterface.cs +++ b/Content.Client/ADT/NanoChat/StationAiNanoChatBoundUserInterface.cs @@ -36,13 +36,13 @@ protected override void UpdateState(BoundUserInterfaceState state) protected override void Dispose(bool disposing) { - base.Dispose(disposing); - - if (_window == null) - return; + if (_window != null) + { + _window.Fragment.OnMessageSent -= OnMessageSent; + _window.Dispose(); + _window = null; + } - _window.Fragment.OnMessageSent -= OnMessageSent; - _window.Dispose(); - _window = null; + base.Dispose(disposing); } } diff --git a/Content.Server/ADT/CartridgeLoader/Cartridges/NanoChatCartridgeSystem.cs b/Content.Server/ADT/CartridgeLoader/Cartridges/NanoChatCartridgeSystem.cs index ff00bef3079..8f852945117 100644 --- a/Content.Server/ADT/CartridgeLoader/Cartridges/NanoChatCartridgeSystem.cs +++ b/Content.Server/ADT/CartridgeLoader/Cartridges/NanoChatCartridgeSystem.cs @@ -13,6 +13,7 @@ using Content.Shared.PDA; using Content.Shared.Radio; using Content.Shared.Radio.Components; +using JetBrains.Annotations; using Robust.Shared.Prototypes; using Robust.Shared.Timing; using Robust.Shared.Utility; @@ -137,32 +138,41 @@ private bool GetCardEntity( /// private void HandleNewChat(Entity card, NanoChatUiMessageEvent msg) { - if (msg.RecipientNumber == null || msg.Content == null || msg.RecipientNumber == card.Comp.Number) + NewChat(card, msg.RecipientNumber, msg.Content, msg.RecipientJob, msg.Actor); + } + + /// + /// Creates a new chat conversation on a card. + /// + [PublicAPI] + public void NewChat(Entity card, uint? recipientNumber, string? content, string? recipientJob, EntityUid actor) + { + if (recipientNumber == null || content == null || recipientNumber == card.Comp.Number) return; - var name = msg.Content; + var name = content; if (!string.IsNullOrWhiteSpace(name)) { name = name.Trim(); } - var jobTitle = msg.RecipientJob; + var jobTitle = recipientJob; if (!string.IsNullOrWhiteSpace(jobTitle)) { jobTitle = jobTitle.Trim(); } // Add new recipient - var recipient = new NanoChatRecipient(msg.RecipientNumber.Value, + var recipient = new NanoChatRecipient(recipientNumber.Value, name, jobTitle); // Initialize or update recipient - _nanoChat.SetRecipient((card, card.Comp), msg.RecipientNumber.Value, recipient); + _nanoChat.SetRecipient((card, card.Comp), recipientNumber.Value, recipient); _adminLogger.Add(LogType.Action, LogImpact.Low, - $"{ToPrettyString(msg.Actor):user} created new NanoChat conversation with #{msg.RecipientNumber:D4} ({name})"); + $"{ToPrettyString(actor):user} created new NanoChat conversation with #{recipientNumber:D4} ({name})"); var recipientEv = new NanoChatRecipientUpdatedEvent(card); RaiseLocalEvent(ref recipientEv); @@ -174,16 +184,25 @@ private void HandleNewChat(Entity card, NanoChatUiMessage /// private void HandleSelectChat(Entity card, NanoChatUiMessageEvent msg) { - if (msg.RecipientNumber == null) + SelectChat(card, msg.RecipientNumber); + } + + /// + /// Selects a chat conversation on a card. + /// + [PublicAPI] + public void SelectChat(Entity card, uint? recipientNumber) + { + if (recipientNumber == null) return; - _nanoChat.SetCurrentChat((card, card.Comp), msg.RecipientNumber); + _nanoChat.SetCurrentChat((card, card.Comp), recipientNumber); // Clear unread flag when selecting chat - if (_nanoChat.GetRecipient((card, card.Comp), msg.RecipientNumber.Value) is { } recipient) + if (_nanoChat.GetRecipient((card, card.Comp), recipientNumber.Value) is { } recipient) { _nanoChat.SetRecipient((card, card.Comp), - msg.RecipientNumber.Value, + recipientNumber.Value, recipient with { HasUnread = false }); } } @@ -192,6 +211,15 @@ private void HandleSelectChat(Entity card, NanoChatUiMess /// Handles closing the current chat conversation. /// private void HandleCloseChat(Entity card) + { + CloseChat(card); + } + + /// + /// Closes the current chat conversation on a card. + /// + [PublicAPI] + public void CloseChat(Entity card) { _nanoChat.SetCurrentChat((card, card.Comp), null); } @@ -201,18 +229,27 @@ private void HandleCloseChat(Entity card) /// private void HandleDeleteChat(Entity card, NanoChatUiMessageEvent msg) { - if (msg.RecipientNumber == null || card.Comp.Number == null) + DeleteChat(card, msg.RecipientNumber, msg.Actor); + } + + /// + /// Deletes a chat conversation on a card. + /// + [PublicAPI] + public void DeleteChat(Entity card, uint? recipientNumber, EntityUid actor) + { + if (recipientNumber == null || card.Comp.Number == null) return; // Delete chat but keep the messages - var deleted = _nanoChat.TryDeleteChat((card, card.Comp), msg.RecipientNumber.Value, true); + var deleted = _nanoChat.TryDeleteChat((card, card.Comp), recipientNumber.Value, true); if (!deleted) return; _adminLogger.Add(LogType.Action, LogImpact.Low, - $"{ToPrettyString(msg.Actor):user} deleted NanoChat conversation with #{msg.RecipientNumber:D4}"); + $"{ToPrettyString(actor):user} deleted NanoChat conversation with #{recipientNumber:D4}"); UpdateUIForCard(card); } @@ -221,12 +258,30 @@ private void HandleDeleteChat(Entity card, NanoChatUiMess /// Handles toggling notification mute state. /// private void HandleToggleMute(Entity card) + { + ToggleMute(card); + } + + /// + /// Toggles notification mute state on a card. + /// + [PublicAPI] + public void ToggleMute(Entity card) { _nanoChat.SetNotificationsMuted((card, card.Comp), !_nanoChat.GetNotificationsMuted((card, card.Comp))); UpdateUIForCard(card); } private void HandleToggleListNumber(Entity card) + { + ToggleListNumber(card); + } + + /// + /// Toggles card number visibility in the station directory. + /// + [PublicAPI] + public void ToggleListNumber(Entity card) { _nanoChat.SetListNumber((card, card.Comp), !_nanoChat.GetListNumber((card, card.Comp))); UpdateUIForAllCards(); @@ -239,13 +294,25 @@ private void HandleSendMessage(Entity cartridge, Entity card, NanoChatUiMessageEvent msg) { - if (msg.RecipientNumber == null || msg.Content == null || card.Comp.Number == null) + SendMessage(cartridge, card, msg.RecipientNumber, msg.Content, cartridge.Comp.RadioChannel); + } + + /// + /// Sends a NanoChat message from a card, attempting delivery through the radio channel. + /// + [PublicAPI] + public void SendMessage(EntityUid sender, + Entity card, + uint? recipientNumber, + string? content, + ProtoId channelId) + { + if (recipientNumber == null || content == null || card.Comp.Number == null) return; - if (!EnsureRecipientExists(card, msg.RecipientNumber.Value)) + if (!EnsureRecipientExists(card, recipientNumber.Value)) return; - var content = msg.Content; if (!string.IsNullOrWhiteSpace(content)) { content = FormattedMessage.EscapeText(content.Trim()); @@ -261,18 +328,18 @@ private void HandleSendMessage(Entity cartridge, ); // Attempt delivery - var (deliveryFailed, recipients) = AttemptMessageDeliveryInternal(cartridge, msg.RecipientNumber.Value, cartridge.Comp.RadioChannel); + var (deliveryFailed, recipients) = AttemptMessageDeliveryInternal(sender, recipientNumber.Value, channelId); // Update delivery status message = message with { DeliveryFailed = deliveryFailed }; // Store message in sender's outbox under recipient's number - _nanoChat.AddMessage((card, card.Comp), msg.RecipientNumber.Value, message); + _nanoChat.AddMessage((card, card.Comp), recipientNumber.Value, message); // Log message attempt var recipientsText = recipients.Count > 0 ? string.Join(", ", recipients.Select(r => ToPrettyString(r))) - : $"#{msg.RecipientNumber:D4}"; + : $"#{recipientNumber:D4}"; _adminLogger.Add(LogType.Chat, LogImpact.Low, @@ -308,6 +375,7 @@ private bool EnsureRecipientExists(Entity card, uint reci /// The recipient's number /// The radio channel used for delivery /// Tuple containing delivery status and recipients if found. + [PublicAPI] public (bool failed, List> recipient) AttemptMessageDeliveryInternal( EntityUid sender, uint recipientNumber, @@ -433,6 +501,7 @@ private bool HasActiveServer(EntityUid station) /// The sender's card entity /// The recipient's card entity /// The to deliver + [PublicAPI] public void DeliverMessageToRecipient(Entity sender, Entity recipient, NanoChatMessage message) @@ -509,7 +578,8 @@ private void UpdateUIForCard(EntityUid cardUid) /// /// Updates the UI for all PDAs containing a NanoChat cartridge. /// - private void UpdateUIForAllCards() + [PublicAPI] + public void UpdateUIForAllCards() { // Find any PDA containing this card and update its UI var query = EntityQueryEnumerator(); @@ -523,6 +593,7 @@ private void UpdateUIForAllCards() /// /// Gets the for a given NanoChat number. /// + [PublicAPI] public NanoChatRecipient? GetCardInfo(uint number) { // Find card with this number to get its info @@ -585,6 +656,16 @@ private void UpdateUI(Entity ent, EntityUid loader) contacts.Add(new NanoChatRecipient(nanoChatNumber, fullName)); } } + + // Cards held by a station AI are visible in the directory as well + var aiQuery = AllEntityQuery(); + while (aiQuery.MoveNext(out var entityId, out var nanoChatCard, out _)) + { + if (nanoChatCard.ListNumber && nanoChatCard.Number is uint nanoChatNumber && _station.GetOwningStation(entityId) == station) + { + contacts.Add(new NanoChatRecipient(nanoChatNumber, Name(entityId))); + } + } contacts.Sort((contactA, contactB) => string.CompareOrdinal(contactA.Name, contactB.Name)); } else diff --git a/Content.Server/ADT/NanoChat/StationAiNanoChatComponent.cs b/Content.Server/ADT/NanoChat/StationAiNanoChatComponent.cs index 5900a87cc5c..211bd570842 100644 --- a/Content.Server/ADT/NanoChat/StationAiNanoChatComponent.cs +++ b/Content.Server/ADT/NanoChat/StationAiNanoChatComponent.cs @@ -13,10 +13,10 @@ public sealed partial class StationAiNanoChatComponent : Component /// /// The action that opens the NanoChat UI. /// - [DataField("NanoChat")] + [DataField] public EntProtoId Action = "ActionStationAiNanoChat"; - [DataField, AutoNetworkedField] + [DataField] public EntityUid? ActionEntity; /// diff --git a/Content.Server/ADT/NanoChat/StationAiNanoChatSystem.cs b/Content.Server/ADT/NanoChat/StationAiNanoChatSystem.cs index 06c88ab469f..3bd8ee6f71a 100644 --- a/Content.Server/ADT/NanoChat/StationAiNanoChatSystem.cs +++ b/Content.Server/ADT/NanoChat/StationAiNanoChatSystem.cs @@ -1,18 +1,11 @@ -using System.Linq; using Content.Server.ADT.CartridgeLoader.Cartridges; -using Content.Server.Administration.Logs; using Content.Server.Actions; using Content.Server.Station.Systems; using Content.Shared.Access.Components; using Content.Shared.ADT.CartridgeLoader.Cartridges; using Content.Shared.ADT.NanoChat; -using Content.Shared.Database; -using Content.Shared.Radio; -using Content.Shared.Radio.Components; using Robust.Server.GameObjects; using Robust.Shared.Player; -using Robust.Shared.Timing; -using Robust.Shared.Utility; namespace Content.Server.ADT.NanoChat; @@ -21,10 +14,7 @@ public sealed class StationAiNanoChatSystem : EntitySystem [Dependency] private readonly UserInterfaceSystem _uiSystem = default!; [Dependency] private readonly ActionsSystem _action = default!; [Dependency] private readonly StationSystem _station = default!; - [Dependency] private readonly SharedNanoChatSystem _nanoChat = default!; [Dependency] private readonly NanoChatCartridgeSystem _nanoChatCartridge = default!; - [Dependency] private readonly IAdminLogManager _adminLogger = default!; - [Dependency] private readonly IGameTiming _timing = default!; public override void Initialize() { @@ -90,146 +80,31 @@ private void OnMessage(Entity ent, ref StationAiNano switch (msg.Type) { case NanoChatUiMessageType.NewChat: - HandleNewChat(card, msg); + _nanoChatCartridge.NewChat(card, msg.RecipientNumber, msg.Content, msg.RecipientJob, msg.Actor); break; case NanoChatUiMessageType.SelectChat: - HandleSelectChat(card, msg); + _nanoChatCartridge.SelectChat(card, msg.RecipientNumber); break; case NanoChatUiMessageType.CloseChat: - HandleCloseChat(card); + _nanoChatCartridge.CloseChat(card); break; case NanoChatUiMessageType.ToggleMute: - HandleToggleMute(card); + _nanoChatCartridge.ToggleMute(card); break; case NanoChatUiMessageType.DeleteChat: - HandleDeleteChat(card, msg); + _nanoChatCartridge.DeleteChat(card, msg.RecipientNumber, msg.Actor); break; case NanoChatUiMessageType.SendMessage: - HandleSendMessage(ent, card, msg); + _nanoChatCartridge.SendMessage(ent.Owner, card, msg.RecipientNumber, msg.Content, ent.Comp.RadioChannel); break; case NanoChatUiMessageType.ToggleListNumber: - HandleToggleListNumber(card); + _nanoChatCartridge.ToggleListNumber(card); break; } UpdateUi(ent); } - private void HandleNewChat(Entity card, StationAiNanoChatUiMessage msg) - { - if (msg.RecipientNumber == null || msg.Content == null || msg.RecipientNumber == card.Comp.Number) - return; - - var name = msg.Content.Trim(); - var jobTitle = string.IsNullOrWhiteSpace(msg.RecipientJob) ? null : msg.RecipientJob.Trim(); - - _nanoChat.SetRecipient((card, card.Comp), msg.RecipientNumber.Value, new NanoChatRecipient(msg.RecipientNumber.Value, name, jobTitle)); - - _adminLogger.Add(LogType.Action, - LogImpact.Low, - $"{ToPrettyString(msg.Actor):user} created new NanoChat conversation with #{msg.RecipientNumber:D4} ({name})"); - - var recipientEv = new NanoChatRecipientUpdatedEvent(card); - RaiseLocalEvent(ref recipientEv); - } - - private void HandleSelectChat(Entity card, StationAiNanoChatUiMessage msg) - { - if (msg.RecipientNumber == null) - return; - - _nanoChat.SetCurrentChat((card, card.Comp), msg.RecipientNumber); - - if (_nanoChat.GetRecipient((card, card.Comp), msg.RecipientNumber.Value) is { } recipient) - { - _nanoChat.SetRecipient((card, card.Comp), - msg.RecipientNumber.Value, - recipient with { HasUnread = false }); - } - } - - private void HandleCloseChat(Entity card) - { - _nanoChat.SetCurrentChat((card, card.Comp), null); - } - - private void HandleDeleteChat(Entity card, StationAiNanoChatUiMessage msg) - { - if (msg.RecipientNumber == null) - return; - - var deleted = _nanoChat.TryDeleteChat((card, card.Comp), msg.RecipientNumber.Value, true); - - if (!deleted) - return; - - _adminLogger.Add(LogType.Action, - LogImpact.Low, - $"{ToPrettyString(msg.Actor):user} deleted NanoChat conversation with #{msg.RecipientNumber:D4}"); - } - - private void HandleToggleMute(Entity card) - { - _nanoChat.SetNotificationsMuted((card, card.Comp), !_nanoChat.GetNotificationsMuted((card, card.Comp))); - } - - private void HandleToggleListNumber(Entity card) - { - _nanoChat.SetListNumber((card, card.Comp), !_nanoChat.GetListNumber((card, card.Comp))); - } - - private void HandleSendMessage(Entity ent, - Entity card, - StationAiNanoChatUiMessage msg) - { - if (msg.RecipientNumber == null || msg.Content == null || card.Comp.Number == null) - return; - - if (!_nanoChat.EnsureRecipientExists((card, card.Comp), msg.RecipientNumber.Value, - _nanoChatCartridge.GetCardInfo(msg.RecipientNumber.Value))) - return; - - var content = msg.Content; - if (!string.IsNullOrWhiteSpace(content)) - { - content = FormattedMessage.EscapeText(content.Trim()); - if (content.Length > NanoChatMessage.MaxContentLength) - content = content[..NanoChatMessage.MaxContentLength]; - } - - var message = new NanoChatMessage( - _timing.CurTime, - content, - (uint)card.Comp.Number - ); - - var (deliveryFailed, recipients) = _nanoChatCartridge.AttemptMessageDeliveryInternal( - ent.Owner, msg.RecipientNumber.Value, ent.Comp.RadioChannel); - - message = message with { DeliveryFailed = deliveryFailed }; - - _nanoChat.AddMessage((card, card.Comp), msg.RecipientNumber.Value, message); - - var recipientsText = recipients.Count > 0 - ? string.Join(", ", recipients.Select(r => ToPrettyString(r))) - : $"#{msg.RecipientNumber:D4}"; - - _adminLogger.Add(LogType.Chat, - LogImpact.Low, - $"{ToPrettyString(card):user} sent NanoChat message to {recipientsText}: {content}{(deliveryFailed ? " [DELIVERY FAILED]" : "")}"); - - var msgEv = new NanoChatMessageReceivedEvent(card); - RaiseLocalEvent(ref msgEv); - - if (deliveryFailed) - return; - - foreach (var recipient in recipients) - { - _nanoChatCartridge.DeliverMessageToRecipient(card, recipient, message); - } - } - private void UpdateUi(Entity ent) { if (!TryComp(ent.Owner, out var card))