From 8e8938fe7e18f8a003f3e7ad034984bd7abce561 Mon Sep 17 00:00:00 2001
From: Kill_Me_I_Noobs <118206719+Vonsant@users.noreply.github.com>
Date: Thu, 12 Dec 2024 17:33:02 +0300
Subject: [PATCH 1/2] =?UTF-8?q?Revert=20"Revert=20"[PORT]=20=D0=A1=D0=B8?=
=?UTF-8?q?=D1=81=D1=82=D0=B5=D0=BC=D0=B0=20=D0=BE=D0=B1=D0=BC=D0=B5=D0=BD?=
=?UTF-8?q?=D0=B0=20=D1=81=D0=BE=D0=BE=D0=B1=D1=89=D0=B5=D0=BD=D0=B8=D1=8F?=
=?UTF-8?q?=D0=BC=D0=B8=20=D0=B2=20=D0=9A=D0=9F=D0=9A=20(#153)"=20(#154)"?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
This reverts commit b72d32203b7681df37fbec2a051f0e8128104277.
---
.../UI/AgentIDCardBoundUserInterface.cs | 9 +
.../Access/UI/AgentIDCardWindow.xaml | 4 +
.../Access/UI/AgentIDCardWindow.xaml.cs | 37 ++
.../CartridgeLoader/Cartridges/LogProbeUi.cs | 2 +-
.../Cartridges/LogProbeUiFragment.xaml | 26 +-
.../Cartridges/LogProbeUiFragment.xaml.cs | 109 +++-
.../Cartridges/NanoChatEntry.xaml | 48 ++
.../Cartridges/NanoChatEntry.xaml.cs | 39 ++
.../Cartridges/NanoChatLogEntry.xaml | 21 +
.../Cartridges/NanoChatLogEntry.xaml.cs | 17 +
.../Cartridges/NanoChatMessageBubble.xaml | 55 ++
.../Cartridges/NanoChatMessageBubble.xaml.cs | 62 +++
.../CartridgeLoader/Cartridges/NanoChatUi.cs | 43 ++
.../Cartridges/NanoChatUiFragment.xaml | 167 ++++++
.../Cartridges/NanoChatUiFragment.xaml.cs | 254 +++++++++
.../Cartridges/NewChatPopup.xaml | 52 ++
.../Cartridges/NewChatPopup.xaml.cs | 87 +++
.../_CorvaxNext/NanoChat/NanoChatSystem.cs | 5 +
.../Access/Systems/AgentIDCardSystem.cs | 53 +-
.../Cartridges/LogProbeCartridgeComponent.cs | 7 +
.../Cartridges/LogProbeCartridgeSystem.cs | 16 +-
.../LogProbeCartridgeSystem.NanoChat.cs | 82 +++
.../Cartridges/NanoChatCartridgeComponent.cs | 26 +
.../Cartridges/NanoChatCartridgeSystem.cs | 514 ++++++++++++++++++
.../_CorvaxNext/NanoChat/NanoChatSystem.cs | 130 +++++
.../Access/SharedAgentIDCardSystem.cs | 16 +-
.../Cartridges/LogProbeUiState.cs | 11 +-
.../Cartridges/NanoChatUiMessageEvent.cs | 166 ++++++
.../Cartridges/NanoChatUiState.cs | 30 +
.../NanoChat/NanoChatCardComponent.cs | 52 ++
.../NanoChat/SharedNanoChatSystem.cs | 273 ++++++++++
.../components/agent-id-card-component.ftl | 1 +
.../_corvaxnext/cartridge-loader/nanochat.ftl | 36 ++
.../components/nanochat-card-component.ftl | 5 +
.../components/agent-id-card-component.ftl | 1 +
.../_corvaxnext/cartridge-loader/nanochat.ftl | 36 ++
.../components/nanochat-card-component.ftl | 5 +
.../entities/objects/devices/cartriges.ftl | 4 +-
.../Entities/Objects/Devices/pda.yml | 11 +
.../Objects/Misc/identification_cards.yml | 3 +
.../Entities/Objects/Devices/cartridges.yml | 23 +-
.../_CorvaxNext/name_identifier_groups.yml | 4 +
.../Interface/VerbIcons/ATTRIBUTION.txt | 2 +
.../_CorvaxNext/Interface/VerbIcons/bell.svg | 5 +
.../Interface/VerbIcons/bell.svg.png | Bin 0 -> 720 bytes
.../Interface/VerbIcons/bell_muted.png | Bin 0 -> 355 bytes
.../Misc/program_icons.rsi/meta.json | 14 +
.../Misc/program_icons.rsi/nanochat.png | Bin 0 -> 725 bytes
.../Devices/cartridge.rsi/cart-chat.png | Bin 0 -> 419 bytes
.../Objects/Devices/cartridge.rsi/meta.json | 5 +-
50 files changed, 2552 insertions(+), 16 deletions(-)
create mode 100644 Content.Client/_CorvaxNext/CartridgeLoader/Cartridges/NanoChatEntry.xaml
create mode 100644 Content.Client/_CorvaxNext/CartridgeLoader/Cartridges/NanoChatEntry.xaml.cs
create mode 100644 Content.Client/_CorvaxNext/CartridgeLoader/Cartridges/NanoChatLogEntry.xaml
create mode 100644 Content.Client/_CorvaxNext/CartridgeLoader/Cartridges/NanoChatLogEntry.xaml.cs
create mode 100644 Content.Client/_CorvaxNext/CartridgeLoader/Cartridges/NanoChatMessageBubble.xaml
create mode 100644 Content.Client/_CorvaxNext/CartridgeLoader/Cartridges/NanoChatMessageBubble.xaml.cs
create mode 100644 Content.Client/_CorvaxNext/CartridgeLoader/Cartridges/NanoChatUi.cs
create mode 100644 Content.Client/_CorvaxNext/CartridgeLoader/Cartridges/NanoChatUiFragment.xaml
create mode 100644 Content.Client/_CorvaxNext/CartridgeLoader/Cartridges/NanoChatUiFragment.xaml.cs
create mode 100644 Content.Client/_CorvaxNext/CartridgeLoader/Cartridges/NewChatPopup.xaml
create mode 100644 Content.Client/_CorvaxNext/CartridgeLoader/Cartridges/NewChatPopup.xaml.cs
create mode 100644 Content.Client/_CorvaxNext/NanoChat/NanoChatSystem.cs
create mode 100644 Content.Server/_CorvaxNext/CartridgeLoader/Cartridges/LogProbeCartridgeSystem.NanoChat.cs
create mode 100644 Content.Server/_CorvaxNext/CartridgeLoader/Cartridges/NanoChatCartridgeComponent.cs
create mode 100644 Content.Server/_CorvaxNext/CartridgeLoader/Cartridges/NanoChatCartridgeSystem.cs
create mode 100644 Content.Server/_CorvaxNext/NanoChat/NanoChatSystem.cs
create mode 100644 Content.Shared/_CorvaxNext/CartridgeLoader/Cartridges/NanoChatUiMessageEvent.cs
create mode 100644 Content.Shared/_CorvaxNext/CartridgeLoader/Cartridges/NanoChatUiState.cs
create mode 100644 Content.Shared/_CorvaxNext/NanoChat/NanoChatCardComponent.cs
create mode 100644 Content.Shared/_CorvaxNext/NanoChat/SharedNanoChatSystem.cs
create mode 100644 Resources/Locale/en-US/_corvaxnext/access/components/agent-id-card-component.ftl
create mode 100644 Resources/Locale/en-US/_corvaxnext/cartridge-loader/nanochat.ftl
create mode 100644 Resources/Locale/en-US/_corvaxnext/nanochat/components/nanochat-card-component.ftl
create mode 100644 Resources/Locale/ru-RU/_corvaxnext/access/components/agent-id-card-component.ftl
create mode 100644 Resources/Locale/ru-RU/_corvaxnext/cartridge-loader/nanochat.ftl
create mode 100644 Resources/Locale/ru-RU/_corvaxnext/nanochat/components/nanochat-card-component.ftl
create mode 100644 Resources/Prototypes/_CorvaxNext/name_identifier_groups.yml
create mode 100644 Resources/Textures/_CorvaxNext/Interface/VerbIcons/ATTRIBUTION.txt
create mode 100644 Resources/Textures/_CorvaxNext/Interface/VerbIcons/bell.svg
create mode 100644 Resources/Textures/_CorvaxNext/Interface/VerbIcons/bell.svg.png
create mode 100644 Resources/Textures/_CorvaxNext/Interface/VerbIcons/bell_muted.png
create mode 100644 Resources/Textures/_CorvaxNext/Misc/program_icons.rsi/meta.json
create mode 100644 Resources/Textures/_CorvaxNext/Misc/program_icons.rsi/nanochat.png
create mode 100644 Resources/Textures/_CorvaxNext/Objects/Devices/cartridge.rsi/cart-chat.png
diff --git a/Content.Client/Access/UI/AgentIDCardBoundUserInterface.cs b/Content.Client/Access/UI/AgentIDCardBoundUserInterface.cs
index 050756fcd14..24e9bc7a01d 100644
--- a/Content.Client/Access/UI/AgentIDCardBoundUserInterface.cs
+++ b/Content.Client/Access/UI/AgentIDCardBoundUserInterface.cs
@@ -26,8 +26,16 @@ protected override void Open()
_window.OnNameChanged += OnNameChanged;
_window.OnJobChanged += OnJobChanged;
_window.OnJobIconChanged += OnJobIconChanged;
+ _window.OnNumberChanged += OnNumberChanged; // Corvax-Next-PDAChat
}
+ // Corvax-Next-PDAChat-Start
+ private void OnNumberChanged(uint newNumber)
+ {
+ SendMessage(new AgentIDCardNumberChangedMessage(newNumber));
+ }
+ // Corvax-Next-PDAChat-End
+
private void OnNameChanged(string newName)
{
SendMessage(new AgentIDCardNameChangedMessage(newName));
@@ -56,6 +64,7 @@ protected override void UpdateState(BoundUserInterfaceState state)
_window.SetCurrentName(cast.CurrentName);
_window.SetCurrentJob(cast.CurrentJob);
_window.SetAllowedIcons(cast.CurrentJobIconId);
+ _window.SetCurrentNumber(cast.CurrentNumber); // Corvax-Next-PDAChat
}
}
}
diff --git a/Content.Client/Access/UI/AgentIDCardWindow.xaml b/Content.Client/Access/UI/AgentIDCardWindow.xaml
index 7d091e4e165..a61ed2a5ae2 100644
--- a/Content.Client/Access/UI/AgentIDCardWindow.xaml
+++ b/Content.Client/Access/UI/AgentIDCardWindow.xaml
@@ -6,6 +6,10 @@
+
+
+
+
diff --git a/Content.Client/Access/UI/AgentIDCardWindow.xaml.cs b/Content.Client/Access/UI/AgentIDCardWindow.xaml.cs
index 320bb88a67e..14df0ff7ef8 100644
--- a/Content.Client/Access/UI/AgentIDCardWindow.xaml.cs
+++ b/Content.Client/Access/UI/AgentIDCardWindow.xaml.cs
@@ -20,9 +20,13 @@ public sealed partial class AgentIDCardWindow : DefaultWindow
private readonly SpriteSystem _spriteSystem;
private const int JobIconColumnCount = 10;
+
+ private const int MaxNumberLength = 4; // Corvax-Next-PDAChat - Same as NewChatPopup
public event Action? OnNameChanged;
public event Action? OnJobChanged;
+
+ public event Action? OnNumberChanged; // Corvax-Next-PDAChat - Add event for number changes
public event Action>? OnJobIconChanged;
@@ -35,9 +39,42 @@ public AgentIDCardWindow()
NameLineEdit.OnTextEntered += e => OnNameChanged?.Invoke(e.Text);
NameLineEdit.OnFocusExit += e => OnNameChanged?.Invoke(e.Text);
+ // Corvax-Next-PDAChat-Start
JobLineEdit.OnTextEntered += e => OnJobChanged?.Invoke(e.Text);
JobLineEdit.OnFocusExit += e => OnJobChanged?.Invoke(e.Text);
+
+ // Corvax-Next-PDAChat - Add handlers for number changes
+ NumberLineEdit.OnTextEntered += OnNumberEntered;
+ NumberLineEdit.OnFocusExit += OnNumberEntered;
+
+ // Corvax-Next-PDAChat - Filter to only allow digits
+ NumberLineEdit.OnTextChanged += args =>
+ {
+ if (args.Text.Length > MaxNumberLength)
+ {
+ NumberLineEdit.Text = args.Text[..MaxNumberLength];
+ }
+
+ // Filter to digits only
+ var newText = string.Concat(args.Text.Where(char.IsDigit));
+ if (newText != args.Text)
+ NumberLineEdit.Text = newText;
+ };
+ }
+
+ // Corvax-Next-PDAChat - Add number validation and event
+ private void OnNumberEntered(LineEdit.LineEditEventArgs args)
+ {
+ if (uint.TryParse(args.Text, out var number) && number > 0)
+ OnNumberChanged?.Invoke(number);
+ }
+
+ // Corvax-Next-PDAChat - Add setter for current number
+ public void SetCurrentNumber(uint? number)
+ {
+ NumberLineEdit.Text = number?.ToString("D4") ?? "";
}
+ // Corvax-Next-PDAChat-End
public void SetAllowedIcons(string currentJobIconId)
{
diff --git a/Content.Client/CartridgeLoader/Cartridges/LogProbeUi.cs b/Content.Client/CartridgeLoader/Cartridges/LogProbeUi.cs
index d28d3228c94..7ece0abcbd9 100644
--- a/Content.Client/CartridgeLoader/Cartridges/LogProbeUi.cs
+++ b/Content.Client/CartridgeLoader/Cartridges/LogProbeUi.cs
@@ -23,6 +23,6 @@ public override void UpdateState(BoundUserInterfaceState state)
if (state is not LogProbeUiState logProbeUiState)
return;
- _fragment?.UpdateState(logProbeUiState.PulledLogs);
+ _fragment?.UpdateState(logProbeUiState); // Corvax-Next-PDAChat - just take the state
}
}
diff --git a/Content.Client/CartridgeLoader/Cartridges/LogProbeUiFragment.xaml b/Content.Client/CartridgeLoader/Cartridges/LogProbeUiFragment.xaml
index d12fb55cdce..7c571e08f9b 100644
--- a/Content.Client/CartridgeLoader/Cartridges/LogProbeUiFragment.xaml
+++ b/Content.Client/CartridgeLoader/Cartridges/LogProbeUiFragment.xaml
@@ -9,10 +9,28 @@
BorderColor="#5a5a5a"
BorderThickness="0 0 0 1"/>
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/CartridgeLoader/Cartridges/LogProbeUiFragment.xaml.cs b/Content.Client/CartridgeLoader/Cartridges/LogProbeUiFragment.xaml.cs
index b22e0bc1964..a16c92a066c 100644
--- a/Content.Client/CartridgeLoader/Cartridges/LogProbeUiFragment.xaml.cs
+++ b/Content.Client/CartridgeLoader/Cartridges/LogProbeUiFragment.xaml.cs
@@ -1,4 +1,7 @@
-using Content.Shared.CartridgeLoader.Cartridges;
+using System.Linq;
+using Content.Client._CorvaxNext.CartridgeLoader.Cartridges;
+using Content.Shared.CartridgeLoader.Cartridges;
+using Content.Shared._CorvaxNext.CartridgeLoader.Cartridges;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
@@ -13,10 +16,112 @@ public LogProbeUiFragment()
RobustXamlLoader.Load(this);
}
- public void UpdateState(List logs)
+ // Corvax-Next-PDAChat-Start - Update to handle both types of data
+ public void UpdateState(LogProbeUiState state)
{
ProbedDeviceContainer.RemoveAllChildren();
+ if (state.NanoChatData != null)
+ {
+ SetupNanoChatView(state.NanoChatData.Value);
+ DisplayNanoChatData(state.NanoChatData.Value);
+ }
+ else
+ {
+ SetupAccessLogView();
+ if (state.PulledLogs.Count > 0)
+ DisplayAccessLogs(state.PulledLogs);
+ }
+ }
+
+ private void SetupNanoChatView(NanoChatData data)
+ {
+ TitleLabel.Text = Loc.GetString("log-probe-header-nanochat");
+ ContentLabel.Text = Loc.GetString("log-probe-label-message");
+
+ // Show card info if available
+ var cardInfo = new List();
+ if (data.CardNumber != null)
+ cardInfo.Add(Loc.GetString("log-probe-card-number", ("number", $"#{data.CardNumber:D4}")));
+
+ // Add recipient count
+ cardInfo.Add(Loc.GetString("log-probe-recipients", ("count", data.Recipients.Count)));
+
+ CardNumberLabel.Text = string.Join(" | ", cardInfo);
+ CardNumberLabel.Visible = true;
+ }
+
+ private void SetupAccessLogView()
+ {
+ TitleLabel.Text = Loc.GetString("log-probe-header-access");
+ ContentLabel.Text = Loc.GetString("log-probe-label-accessor");
+ CardNumberLabel.Visible = false;
+ }
+
+ private void DisplayNanoChatData(NanoChatData data)
+ {
+ // First add a recipient list entry
+ var recipientsList = Loc.GetString("log-probe-recipient-list") + "\n" + string.Join("\n",
+ data.Recipients.Values
+ .OrderBy(r => r.Name)
+ .Select(r => $" {r.Name}" +
+ (string.IsNullOrEmpty(r.JobTitle) ? "" : $" ({r.JobTitle})") +
+ $" | #{r.Number:D4}"));
+
+ var recipientsEntry = new LogProbeUiEntry(0, "---", recipientsList);
+ ProbedDeviceContainer.AddChild(recipientsEntry);
+
+ var count = 1;
+ foreach (var (partnerId, messages) in data.Messages)
+ {
+ // Show only successfully delivered incoming messages
+ var incomingMessages = messages
+ .Where(msg => msg.SenderId == partnerId && !msg.DeliveryFailed)
+ .OrderByDescending(msg => msg.Timestamp);
+
+ foreach (var msg in incomingMessages)
+ {
+ var messageText = Loc.GetString("log-probe-message-format",
+ ("sender", $"#{msg.SenderId:D4}"),
+ ("recipient", $"#{data.CardNumber:D4}"),
+ ("content", msg.Content));
+
+ var entry = new NanoChatLogEntry(
+ count,
+ TimeSpan.FromSeconds(Math.Truncate(msg.Timestamp.TotalSeconds)).ToString(),
+ messageText);
+
+ ProbedDeviceContainer.AddChild(entry);
+ count++;
+ }
+
+ // Show only successfully delivered outgoing messages
+ var outgoingMessages = messages
+ .Where(msg => msg.SenderId == data.CardNumber && !msg.DeliveryFailed)
+ .OrderByDescending(msg => msg.Timestamp);
+
+ foreach (var msg in outgoingMessages)
+ {
+ var messageText = Loc.GetString("log-probe-message-format",
+ ("sender", $"#{msg.SenderId:D4}"),
+ ("recipient", $"#{partnerId:D4}"),
+ ("content", msg.Content));
+
+ var entry = new NanoChatLogEntry(
+ count,
+ TimeSpan.FromSeconds(Math.Truncate(msg.Timestamp.TotalSeconds)).ToString(),
+ messageText);
+
+ ProbedDeviceContainer.AddChild(entry);
+ count++;
+ }
+ }
+ }
+ // Corvax-Next-PDAChat-End
+
+ // Corvax-Next-PDAChat - Handle this in a separate method
+ private void DisplayAccessLogs(List logs)
+ {
//Reverse the list so the oldest entries appear at the bottom
logs.Reverse();
diff --git a/Content.Client/_CorvaxNext/CartridgeLoader/Cartridges/NanoChatEntry.xaml b/Content.Client/_CorvaxNext/CartridgeLoader/Cartridges/NanoChatEntry.xaml
new file mode 100644
index 00000000000..0b136133624
--- /dev/null
+++ b/Content.Client/_CorvaxNext/CartridgeLoader/Cartridges/NanoChatEntry.xaml
@@ -0,0 +1,48 @@
+
+
+
diff --git a/Content.Client/_CorvaxNext/CartridgeLoader/Cartridges/NanoChatEntry.xaml.cs b/Content.Client/_CorvaxNext/CartridgeLoader/Cartridges/NanoChatEntry.xaml.cs
new file mode 100644
index 00000000000..2a2b665d929
--- /dev/null
+++ b/Content.Client/_CorvaxNext/CartridgeLoader/Cartridges/NanoChatEntry.xaml.cs
@@ -0,0 +1,39 @@
+using Content.Shared._CorvaxNext.CartridgeLoader.Cartridges;
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.XAML;
+
+namespace Content.Client._CorvaxNext.CartridgeLoader.Cartridges;
+
+[GenerateTypedNameReferences]
+public sealed partial class NanoChatEntry : BoxContainer
+{
+ public event Action? OnPressed;
+ private uint _number;
+ private Action? _pressHandler;
+
+ public NanoChatEntry()
+ {
+ RobustXamlLoader.Load(this);
+ }
+
+ public void SetRecipient(NanoChatRecipient recipient, uint number, bool isSelected)
+ {
+ // Remove old handler if it exists
+ if (_pressHandler != null)
+ ChatButton.OnPressed -= _pressHandler;
+
+ _number = number;
+
+ // Create and store new handler
+ _pressHandler = _ => OnPressed?.Invoke(_number);
+ ChatButton.OnPressed += _pressHandler;
+
+ NameLabel.Text = recipient.Name;
+ JobLabel.Text = recipient.JobTitle ?? "";
+ JobLabel.Visible = !string.IsNullOrEmpty(recipient.JobTitle);
+ UnreadIndicator.Visible = recipient.HasUnread;
+
+ ChatButton.ModulateSelfOverride = isSelected ? NanoChatMessageBubble.OwnMessageColor : null;
+ }
+}
diff --git a/Content.Client/_CorvaxNext/CartridgeLoader/Cartridges/NanoChatLogEntry.xaml b/Content.Client/_CorvaxNext/CartridgeLoader/Cartridges/NanoChatLogEntry.xaml
new file mode 100644
index 00000000000..c87478d6301
--- /dev/null
+++ b/Content.Client/_CorvaxNext/CartridgeLoader/Cartridges/NanoChatLogEntry.xaml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
diff --git a/Content.Client/_CorvaxNext/CartridgeLoader/Cartridges/NanoChatLogEntry.xaml.cs b/Content.Client/_CorvaxNext/CartridgeLoader/Cartridges/NanoChatLogEntry.xaml.cs
new file mode 100644
index 00000000000..48e1eeaa05b
--- /dev/null
+++ b/Content.Client/_CorvaxNext/CartridgeLoader/Cartridges/NanoChatLogEntry.xaml.cs
@@ -0,0 +1,17 @@
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.XAML;
+
+namespace Content.Client._CorvaxNext.CartridgeLoader.Cartridges;
+
+[GenerateTypedNameReferences]
+public sealed partial class NanoChatLogEntry : BoxContainer
+{
+ public NanoChatLogEntry(int number, string time, string message)
+ {
+ RobustXamlLoader.Load(this);
+ NumberLabel.Text = number.ToString();
+ TimeLabel.Text = time;
+ MessageLabel.Text = message;
+ }
+}
diff --git a/Content.Client/_CorvaxNext/CartridgeLoader/Cartridges/NanoChatMessageBubble.xaml b/Content.Client/_CorvaxNext/CartridgeLoader/Cartridges/NanoChatMessageBubble.xaml
new file mode 100644
index 00000000000..7f4126213f9
--- /dev/null
+++ b/Content.Client/_CorvaxNext/CartridgeLoader/Cartridges/NanoChatMessageBubble.xaml
@@ -0,0 +1,55 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/_CorvaxNext/CartridgeLoader/Cartridges/NanoChatMessageBubble.xaml.cs b/Content.Client/_CorvaxNext/CartridgeLoader/Cartridges/NanoChatMessageBubble.xaml.cs
new file mode 100644
index 00000000000..b903d153afa
--- /dev/null
+++ b/Content.Client/_CorvaxNext/CartridgeLoader/Cartridges/NanoChatMessageBubble.xaml.cs
@@ -0,0 +1,62 @@
+using Content.Shared._CorvaxNext.CartridgeLoader.Cartridges;
+using Robust.Client.AutoGenerated;
+using Robust.Client.Graphics;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.XAML;
+
+namespace Content.Client._CorvaxNext.CartridgeLoader.Cartridges;
+
+[GenerateTypedNameReferences]
+public sealed partial class NanoChatMessageBubble : BoxContainer
+{
+ public static readonly Color OwnMessageColor = Color.FromHex("#173717d9"); // Dark green
+ public static readonly Color OtherMessageColor = Color.FromHex("#252525d9"); // Dark gray
+ public static readonly Color BorderColor = Color.FromHex("#40404066"); // Subtle border
+ public static readonly Color TextColor = Color.FromHex("#dcdcdc"); // Slightly softened white
+ public static readonly Color ErrorColor = Color.FromHex("#cc3333"); // Red
+
+ public NanoChatMessageBubble()
+ {
+ RobustXamlLoader.Load(this);
+ }
+
+ public void SetMessage(NanoChatMessage message, bool isOwnMessage)
+ {
+ if (MessagePanel.PanelOverride is not StyleBoxFlat)
+ return;
+
+ // Configure message appearance
+ var style = (StyleBoxFlat)MessagePanel.PanelOverride;
+ style.BackgroundColor = isOwnMessage ? OwnMessageColor : OtherMessageColor;
+ style.BorderColor = BorderColor;
+
+ // Set message content
+ MessageText.Text = message.Content;
+ MessageText.Modulate = TextColor;
+
+ // Show delivery failed text if needed (only for own messages)
+ DeliveryFailedLabel.Visible = isOwnMessage && message.DeliveryFailed;
+ if (DeliveryFailedLabel.Visible)
+ DeliveryFailedLabel.Modulate = ErrorColor;
+
+ // For own messages: FlexSpace -> MessagePanel -> RightSpacer
+ // For other messages: LeftSpacer -> MessagePanel -> FlexSpace
+ MessageContainer.RemoveAllChildren();
+
+ // fuuuuuck
+ MessageBox.Parent?.RemoveChild(MessageBox);
+
+ if (isOwnMessage)
+ {
+ MessageContainer.AddChild(FlexSpace);
+ MessageContainer.AddChild(MessageBox);
+ MessageContainer.AddChild(RightSpacer);
+ }
+ else
+ {
+ MessageContainer.AddChild(LeftSpacer);
+ MessageContainer.AddChild(MessageBox);
+ MessageContainer.AddChild(FlexSpace);
+ }
+ }
+}
diff --git a/Content.Client/_CorvaxNext/CartridgeLoader/Cartridges/NanoChatUi.cs b/Content.Client/_CorvaxNext/CartridgeLoader/Cartridges/NanoChatUi.cs
new file mode 100644
index 00000000000..1573132297e
--- /dev/null
+++ b/Content.Client/_CorvaxNext/CartridgeLoader/Cartridges/NanoChatUi.cs
@@ -0,0 +1,43 @@
+using Content.Client.UserInterface.Fragments;
+using Content.Shared.CartridgeLoader;
+using Content.Shared._CorvaxNext.CartridgeLoader.Cartridges;
+using Robust.Client.UserInterface;
+
+namespace Content.Client._CorvaxNext.CartridgeLoader.Cartridges;
+
+public sealed partial class NanoChatUi : UIFragment
+{
+ private NanoChatUiFragment? _fragment;
+
+ public override Control GetUIFragmentRoot()
+ {
+ return _fragment!;
+ }
+
+ public override void Setup(BoundUserInterface userInterface, EntityUid? fragmentOwner)
+ {
+ _fragment = new NanoChatUiFragment();
+
+ _fragment.OnMessageSent += (type, number, content, job) =>
+ {
+ SendNanoChatUiMessage(type, number, content, job, userInterface);
+ };
+ }
+
+ public override void UpdateState(BoundUserInterfaceState state)
+ {
+ if (state is NanoChatUiState cast)
+ _fragment?.UpdateState(cast);
+ }
+
+ private static void SendNanoChatUiMessage(NanoChatUiMessageType type,
+ uint? number,
+ string? content,
+ string? job,
+ BoundUserInterface userInterface)
+ {
+ var nanoChatMessage = new NanoChatUiMessageEvent(type, number, content, job);
+ var message = new CartridgeUiMessage(nanoChatMessage);
+ userInterface.SendMessage(message);
+ }
+}
diff --git a/Content.Client/_CorvaxNext/CartridgeLoader/Cartridges/NanoChatUiFragment.xaml b/Content.Client/_CorvaxNext/CartridgeLoader/Cartridges/NanoChatUiFragment.xaml
new file mode 100644
index 00000000000..d0e54d2e58a
--- /dev/null
+++ b/Content.Client/_CorvaxNext/CartridgeLoader/Cartridges/NanoChatUiFragment.xaml
@@ -0,0 +1,167 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/_CorvaxNext/CartridgeLoader/Cartridges/NanoChatUiFragment.xaml.cs b/Content.Client/_CorvaxNext/CartridgeLoader/Cartridges/NanoChatUiFragment.xaml.cs
new file mode 100644
index 00000000000..045c00cba79
--- /dev/null
+++ b/Content.Client/_CorvaxNext/CartridgeLoader/Cartridges/NanoChatUiFragment.xaml.cs
@@ -0,0 +1,254 @@
+using System.Linq;
+using System.Numerics;
+using Content.Shared._CorvaxNext.CartridgeLoader.Cartridges;
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.XAML;
+using Robust.Client.UserInterface;
+using Robust.Shared.Timing;
+
+namespace Content.Client._CorvaxNext.CartridgeLoader.Cartridges;
+
+[GenerateTypedNameReferences]
+public sealed partial class NanoChatUiFragment : BoxContainer
+{
+ [Dependency] private readonly IGameTiming _timing = default!;
+
+ private const int MaxMessageLength = 256;
+
+ private readonly NewChatPopup _newChatPopup;
+ private uint? _currentChat;
+ private uint? _pendingChat;
+ private uint _ownNumber;
+ private bool _notificationsMuted;
+ private Dictionary _recipients = new();
+ private Dictionary> _messages = new();
+
+ public event Action? OnMessageSent;
+
+ public NanoChatUiFragment()
+ {
+ IoCManager.InjectDependencies(this);
+ RobustXamlLoader.Load(this);
+
+ _newChatPopup = new NewChatPopup();
+ SetupEventHandlers();
+ }
+
+ private void SetupEventHandlers()
+ {
+ _newChatPopup.OnChatCreated += (number, name, job) =>
+ {
+ OnMessageSent?.Invoke(NanoChatUiMessageType.NewChat, number, name, job);
+ };
+
+ NewChatButton.OnPressed += _ =>
+ {
+ _newChatPopup.ClearInputs();
+ _newChatPopup.OpenCentered();
+ };
+
+ MuteButton.OnPressed += _ =>
+ {
+ _notificationsMuted = !_notificationsMuted;
+ UpdateMuteButton();
+ OnMessageSent?.Invoke(NanoChatUiMessageType.ToggleMute, null, null, null);
+ };
+
+ MessageInput.OnTextChanged += args =>
+ {
+ var length = args.Text.Length;
+ var isValid = !string.IsNullOrWhiteSpace(args.Text) &&
+ length <= MaxMessageLength &&
+ (_currentChat != null || _pendingChat != null);
+
+ SendButton.Disabled = !isValid;
+
+ // Show character count when over limit
+ CharacterCount.Visible = length > MaxMessageLength;
+ if (length > MaxMessageLength)
+ {
+ CharacterCount.Text = Loc.GetString("nano-chat-message-too-long",
+ ("current", length),
+ ("max", MaxMessageLength));
+ CharacterCount.StyleClasses.Add("LabelDanger");
+ }
+ };
+
+ SendButton.OnPressed += _ => SendMessage();
+ DeleteChatButton.OnPressed += _ => DeleteCurrentChat();
+ }
+
+ private void SendMessage()
+ {
+ var activeChat = _pendingChat ?? _currentChat;
+ if (activeChat == null || string.IsNullOrWhiteSpace(MessageInput.Text))
+ return;
+
+ var messageContent = MessageInput.Text;
+
+ // Add predicted message
+ var predictedMessage = new NanoChatMessage(
+ _timing.CurTime,
+ messageContent,
+ _ownNumber
+ );
+
+ if (!_messages.TryGetValue(activeChat.Value, out var value))
+ {
+ value = new List();
+ _messages[activeChat.Value] = value;
+ }
+
+ value.Add(predictedMessage);
+
+ // Update UI with predicted message
+ UpdateMessages(_messages);
+
+ // Send message event
+ OnMessageSent?.Invoke(NanoChatUiMessageType.SendMessage, activeChat, messageContent, null);
+
+ // Clear input
+ MessageInput.Text = string.Empty;
+ SendButton.Disabled = true;
+ }
+
+ private void SelectChat(uint number)
+ {
+ // Don't reselect the same chat
+ if (_currentChat == number && _pendingChat == null)
+ return;
+
+ _pendingChat = number;
+
+ // Predict marking messages as read
+ if (_recipients.TryGetValue(number, out var recipient))
+ {
+ recipient.HasUnread = false;
+ _recipients[number] = recipient;
+ UpdateChatList(_recipients);
+ }
+
+ OnMessageSent?.Invoke(NanoChatUiMessageType.SelectChat, number, null, null);
+ UpdateCurrentChat();
+ }
+
+ private void DeleteCurrentChat()
+ {
+ var activeChat = _pendingChat ?? _currentChat;
+ if (activeChat == null)
+ return;
+
+ OnMessageSent?.Invoke(NanoChatUiMessageType.DeleteChat, activeChat, null, null);
+ }
+
+ private void UpdateChatList(Dictionary recipients)
+ {
+ ChatList.RemoveAllChildren();
+ _recipients = recipients;
+
+ NoChatsLabel.Visible = recipients.Count == 0;
+ if (NoChatsLabel.Parent != ChatList)
+ {
+ NoChatsLabel.Parent?.RemoveChild(NoChatsLabel);
+ ChatList.AddChild(NoChatsLabel);
+ }
+
+ foreach (var (number, recipient) in recipients.OrderBy(r => r.Value.Name))
+ {
+ var entry = new NanoChatEntry();
+ // For pending chat selection, always show it as selected even if unconfirmed
+ var isSelected = (_pendingChat == number) || (_pendingChat == null && _currentChat == number);
+ entry.SetRecipient(recipient, number, isSelected);
+ entry.OnPressed += SelectChat;
+ ChatList.AddChild(entry);
+ }
+ }
+
+ private void UpdateCurrentChat()
+ {
+ var activeChat = _pendingChat ?? _currentChat;
+ var hasActiveChat = activeChat != null;
+
+ // Update UI state
+ MessagesScroll.Visible = hasActiveChat;
+ CurrentChatName.Visible = !hasActiveChat;
+ MessageInputContainer.Visible = hasActiveChat;
+ DeleteChatButton.Visible = hasActiveChat;
+ DeleteChatButton.Disabled = !hasActiveChat;
+
+ if (activeChat != null && _recipients.TryGetValue(activeChat.Value, out var recipient))
+ {
+ CurrentChatName.Text = recipient.Name + (string.IsNullOrEmpty(recipient.JobTitle) ? "" : $" ({recipient.JobTitle})");
+ }
+ else
+ {
+ CurrentChatName.Text = Loc.GetString("nano-chat-select-chat");
+ }
+ }
+
+ private void UpdateMessages(Dictionary> messages)
+ {
+ _messages = messages;
+ MessageList.RemoveAllChildren();
+
+ var activeChat = _pendingChat ?? _currentChat;
+ if (activeChat == null || !messages.TryGetValue(activeChat.Value, out var chatMessages))
+ return;
+
+ foreach (var message in chatMessages)
+ {
+ var messageBubble = new NanoChatMessageBubble();
+ messageBubble.SetMessage(message, message.SenderId == _ownNumber);
+ MessageList.AddChild(messageBubble);
+
+ // Add spacing between messages
+ MessageList.AddChild(new Control { MinSize = new Vector2(0, 4) });
+ }
+
+ MessageList.InvalidateMeasure();
+ MessagesScroll.InvalidateMeasure();
+
+ // Scroll to bottom after messages are added
+ if (MessageList.Parent is ScrollContainer scroll)
+ scroll.SetScrollValue(new Vector2(0, float.MaxValue));
+ }
+
+ private void UpdateMuteButton()
+ {
+ if (BellMutedIcon != null)
+ BellMutedIcon.Visible = _notificationsMuted;
+ }
+
+ public void UpdateState(NanoChatUiState state)
+ {
+ _ownNumber = state.OwnNumber;
+ _notificationsMuted = state.NotificationsMuted;
+ OwnNumberLabel.Text = $"#{state.OwnNumber:D4}";
+ UpdateMuteButton();
+
+ // Update new chat button state based on recipient limit
+ var atLimit = state.Recipients.Count >= state.MaxRecipients;
+ NewChatButton.Disabled = atLimit;
+ NewChatButton.ToolTip = atLimit
+ ? Loc.GetString("nano-chat-max-recipients")
+ : Loc.GetString("nano-chat-new-chat");
+
+ // First handle pending chat resolution if we have one
+ if (_pendingChat != null)
+ {
+ if (_pendingChat == state.CurrentChat)
+ _currentChat = _pendingChat; // Server confirmed our selection
+
+ _pendingChat = null; // Clear pending either way
+ }
+
+ // No pending chat or it was just cleared, update current directly
+ if (_pendingChat == null)
+ _currentChat = state.CurrentChat;
+
+ UpdateCurrentChat();
+ UpdateChatList(state.Recipients);
+ UpdateMessages(state.Messages);
+ }
+}
diff --git a/Content.Client/_CorvaxNext/CartridgeLoader/Cartridges/NewChatPopup.xaml b/Content.Client/_CorvaxNext/CartridgeLoader/Cartridges/NewChatPopup.xaml
new file mode 100644
index 00000000000..20095c4fce9
--- /dev/null
+++ b/Content.Client/_CorvaxNext/CartridgeLoader/Cartridges/NewChatPopup.xaml
@@ -0,0 +1,52 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/_CorvaxNext/CartridgeLoader/Cartridges/NewChatPopup.xaml.cs b/Content.Client/_CorvaxNext/CartridgeLoader/Cartridges/NewChatPopup.xaml.cs
new file mode 100644
index 00000000000..0f416f41566
--- /dev/null
+++ b/Content.Client/_CorvaxNext/CartridgeLoader/Cartridges/NewChatPopup.xaml.cs
@@ -0,0 +1,87 @@
+using System.Linq;
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface.CustomControls;
+using Robust.Client.UserInterface.XAML;
+
+namespace Content.Client._CorvaxNext.CartridgeLoader.Cartridges;
+
+[GenerateTypedNameReferences]
+public sealed partial class NewChatPopup : DefaultWindow
+{
+ private const int MaxInputLength = 16;
+ private const int MaxNumberLength = 4; // i hardcoded it to be 4 so suffer
+
+ public event Action? OnChatCreated;
+
+ public NewChatPopup()
+ {
+ RobustXamlLoader.Load(this);
+
+ // margins trolling
+ ContentsContainer.Margin = new Thickness(3);
+
+ // Button handlers
+ CancelButton.OnPressed += _ => Close();
+ CreateButton.OnPressed += _ => CreateChat();
+
+ // Input validation
+ NumberInput.OnTextChanged += _ => ValidateInputs();
+ NameInput.OnTextChanged += _ => ValidateInputs();
+
+ // Input validation
+ NumberInput.OnTextChanged += args =>
+ {
+ if (args.Text.Length > MaxNumberLength)
+ NumberInput.Text = args.Text[..MaxNumberLength];
+
+ // Filter to digits only
+ var newText = string.Concat(NumberInput.Text.Where(char.IsDigit));
+ if (newText != NumberInput.Text)
+ NumberInput.Text = newText;
+
+ ValidateInputs();
+ };
+
+ NameInput.OnTextChanged += args =>
+ {
+ if (args.Text.Length > MaxInputLength)
+ NameInput.Text = args.Text[..MaxInputLength];
+ ValidateInputs();
+ };
+
+ JobInput.OnTextChanged += args =>
+ {
+ if (args.Text.Length > MaxInputLength)
+ JobInput.Text = args.Text[..MaxInputLength];
+ };
+ }
+
+ private void ValidateInputs()
+ {
+ var isValid = !string.IsNullOrWhiteSpace(NumberInput.Text) &&
+ !string.IsNullOrWhiteSpace(NameInput.Text) &&
+ uint.TryParse(NumberInput.Text, out _);
+
+ CreateButton.Disabled = !isValid;
+ }
+
+ private void CreateChat()
+ {
+ if (!uint.TryParse(NumberInput.Text, out var number))
+ return;
+
+ var name = NameInput.Text.Trim();
+ var job = string.IsNullOrWhiteSpace(JobInput.Text) ? null : JobInput.Text.Trim();
+
+ OnChatCreated?.Invoke(number, name, job);
+ Close();
+ }
+
+ public void ClearInputs()
+ {
+ NumberInput.Text = string.Empty;
+ NameInput.Text = string.Empty;
+ JobInput.Text = string.Empty;
+ ValidateInputs();
+ }
+}
diff --git a/Content.Client/_CorvaxNext/NanoChat/NanoChatSystem.cs b/Content.Client/_CorvaxNext/NanoChat/NanoChatSystem.cs
new file mode 100644
index 00000000000..62f8680eaac
--- /dev/null
+++ b/Content.Client/_CorvaxNext/NanoChat/NanoChatSystem.cs
@@ -0,0 +1,5 @@
+using Content.Shared._CorvaxNext.NanoChat;
+
+namespace Content.Client._CorvaxNext.NanoChat;
+
+public sealed class NanoChatSystem : SharedNanoChatSystem;
diff --git a/Content.Server/Access/Systems/AgentIDCardSystem.cs b/Content.Server/Access/Systems/AgentIDCardSystem.cs
index a38aefce935..07090402359 100644
--- a/Content.Server/Access/Systems/AgentIDCardSystem.cs
+++ b/Content.Server/Access/Systems/AgentIDCardSystem.cs
@@ -9,6 +9,7 @@
using Robust.Shared.Prototypes;
using Content.Shared.Roles;
using System.Diagnostics.CodeAnalysis;
+using Content.Shared._CorvaxNext.NanoChat;
namespace Content.Server.Access.Systems
{
@@ -18,6 +19,7 @@ public sealed class AgentIDCardSystem : SharedAgentIdCardSystem
[Dependency] private readonly IdCardSystem _cardSystem = default!;
[Dependency] private readonly UserInterfaceSystem _uiSystem = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
+ [Dependency] private readonly SharedNanoChatSystem _nanoChat = default!; // Corvax-Next-PDAChat
public override void Initialize()
{
@@ -28,6 +30,17 @@ public override void Initialize()
SubscribeLocalEvent(OnNameChanged);
SubscribeLocalEvent(OnJobChanged);
SubscribeLocalEvent(OnJobIconChanged);
+ SubscribeLocalEvent(OnNumberChanged); // Corvax-Next-PDAChat
+ }
+
+ // Corvax-Next-PDAChat - Add number change handler
+ private void OnNumberChanged(Entity ent, ref AgentIDCardNumberChangedMessage args)
+ {
+ if (!TryComp(ent, out var comp))
+ return;
+
+ _nanoChat.SetNumber((ent, comp), args.Number);
+ Dirty(ent, comp);
}
private void OnAfterInteract(EntityUid uid, AgentIDCardComponent component, AfterInteractEvent args)
@@ -41,6 +54,34 @@ private void OnAfterInteract(EntityUid uid, AgentIDCardComponent component, Afte
var beforeLength = access.Tags.Count;
access.Tags.UnionWith(targetAccess.Tags);
var addedLength = access.Tags.Count - beforeLength;
+
+ // Corvax-Next-PDAChat-Start - Copy NanoChat data if available
+ if (TryComp(args.Target, out var targetNanoChat) &&
+ TryComp(uid, out var agentNanoChat))
+ {
+ // First clear existing data
+ _nanoChat.Clear((uid, agentNanoChat));
+
+ // Copy the number
+ if (_nanoChat.GetNumber((args.Target.Value, targetNanoChat)) is { } number)
+ _nanoChat.SetNumber((uid, agentNanoChat), number);
+
+ // Copy all recipients and their messages
+ foreach (var (recipientNumber, recipient) in _nanoChat.GetRecipients((args.Target.Value, targetNanoChat)))
+ {
+ _nanoChat.SetRecipient((uid, agentNanoChat), recipientNumber, recipient);
+
+ if (_nanoChat.GetMessagesForRecipient((args.Target.Value, targetNanoChat), recipientNumber) is not
+ { } messages)
+ continue;
+
+ foreach (var message in messages)
+ {
+ _nanoChat.AddMessage((uid, agentNanoChat), recipientNumber, message);
+ }
+ }
+ }
+ // Corvax-Next-PDAChat-End
if (addedLength == 0)
{
@@ -67,7 +108,17 @@ private void AfterUIOpen(EntityUid uid, AgentIDCardComponent component, AfterAct
if (!TryComp(uid, out var idCard))
return;
- var state = new AgentIDCardBoundUserInterfaceState(idCard.FullName ?? "", idCard.LocalizedJobTitle ?? "", idCard.JobIcon);
+ // Corvax-Next-PDAChat-Start - Get current number if it exists
+ uint? currentNumber = null;
+ if (TryComp(uid, out var comp))
+ currentNumber = comp.Number;
+
+ var state = new AgentIDCardBoundUserInterfaceState(
+ idCard.FullName ?? "",
+ idCard.LocalizedJobTitle ?? "",
+ idCard.JobIcon,
+ currentNumber); // Corvax-Next-PDAChat-End - Pass current number
+
_uiSystem.SetUiState(uid, AgentIDCardUiKey.Key, state);
}
diff --git a/Content.Server/CartridgeLoader/Cartridges/LogProbeCartridgeComponent.cs b/Content.Server/CartridgeLoader/Cartridges/LogProbeCartridgeComponent.cs
index cfa92dd67f7..e8e54c32485 100644
--- a/Content.Server/CartridgeLoader/Cartridges/LogProbeCartridgeComponent.cs
+++ b/Content.Server/CartridgeLoader/Cartridges/LogProbeCartridgeComponent.cs
@@ -1,4 +1,5 @@
using Content.Shared.CartridgeLoader.Cartridges;
+using Content.Shared._CorvaxNext.CartridgeLoader.Cartridges;
using Robust.Shared.Audio;
namespace Content.Server.CartridgeLoader.Cartridges;
@@ -18,4 +19,10 @@ public sealed partial class LogProbeCartridgeComponent : Component
///
[DataField, ViewVariables(VVAccess.ReadWrite)]
public SoundSpecifier SoundScan = new SoundPathSpecifier("/Audio/Machines/scan_finish.ogg");
+
+ ///
+ /// Corvax-Next-PDAChat: The last scanned NanoChat data, if any
+ ///
+ [DataField]
+ public NanoChatData? ScannedNanoChatData;
}
diff --git a/Content.Server/CartridgeLoader/Cartridges/LogProbeCartridgeSystem.cs b/Content.Server/CartridgeLoader/Cartridges/LogProbeCartridgeSystem.cs
index f5ccea95900..307c8f05fa5 100644
--- a/Content.Server/CartridgeLoader/Cartridges/LogProbeCartridgeSystem.cs
+++ b/Content.Server/CartridgeLoader/Cartridges/LogProbeCartridgeSystem.cs
@@ -3,12 +3,13 @@
using Content.Shared.CartridgeLoader;
using Content.Shared.CartridgeLoader.Cartridges;
using Content.Shared.Popups;
+using Content.Shared._CorvaxNext.NanoChat;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Random;
namespace Content.Server.CartridgeLoader.Cartridges;
-public sealed class LogProbeCartridgeSystem : EntitySystem
+public sealed partial class LogProbeCartridgeSystem : EntitySystem // Corvax-Next-PDAChat - Made partial
{
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly CartridgeLoaderSystem? _cartridgeLoaderSystem = default!;
@@ -18,6 +19,7 @@ public sealed class LogProbeCartridgeSystem : EntitySystem
public override void Initialize()
{
base.Initialize();
+ InitializeNanoChat(); // Corvax-Next-PDAChat
SubscribeLocalEvent(OnUiReady);
SubscribeLocalEvent(AfterInteract);
}
@@ -33,6 +35,15 @@ private void AfterInteract(Entity ent, ref Cartridge
if (args.InteractEvent.Handled || !args.InteractEvent.CanReach || args.InteractEvent.Target is not { } target)
return;
+ // Corvax-Next-PDAChat-Start - Add NanoChat card scanning
+ if (TryComp(target, out var nanoChatCard))
+ {
+ ScanNanoChatCard(ent, args, target, nanoChatCard);
+ args.InteractEvent.Handled = true;
+ return;
+ }
+ // Corvax-Next-PDAChat-End
+
if (!TryComp(target, out AccessReaderComponent? accessReaderComponent))
return;
@@ -41,6 +52,7 @@ private void AfterInteract(Entity ent, ref Cartridge
_popupSystem.PopupCursor(Loc.GetString("log-probe-scan", ("device", target)), args.InteractEvent.User);
ent.Comp.PulledAccessLogs.Clear();
+ ent.Comp.ScannedNanoChatData = null; // Corvax-Next-PDAChat - Clear any previous NanoChat data
foreach (var accessRecord in accessReaderComponent.AccessLog)
{
@@ -65,7 +77,7 @@ private void OnUiReady(Entity ent, ref CartridgeUiRe
private void UpdateUiState(Entity ent, EntityUid loaderUid)
{
- var state = new LogProbeUiState(ent.Comp.PulledAccessLogs);
+ var state = new LogProbeUiState(ent.Comp.PulledAccessLogs, ent.Comp.ScannedNanoChatData); // Corvax-Next-PDAChat - NanoChat support
_cartridgeLoaderSystem?.UpdateCartridgeUiState(loaderUid, state);
}
}
diff --git a/Content.Server/_CorvaxNext/CartridgeLoader/Cartridges/LogProbeCartridgeSystem.NanoChat.cs b/Content.Server/_CorvaxNext/CartridgeLoader/Cartridges/LogProbeCartridgeSystem.NanoChat.cs
new file mode 100644
index 00000000000..fdcc9d19b0d
--- /dev/null
+++ b/Content.Server/_CorvaxNext/CartridgeLoader/Cartridges/LogProbeCartridgeSystem.NanoChat.cs
@@ -0,0 +1,82 @@
+using Content.Shared.Audio;
+using Content.Shared.CartridgeLoader;
+using Content.Shared._CorvaxNext.CartridgeLoader.Cartridges;
+using Content.Shared._CorvaxNext.NanoChat;
+
+namespace Content.Server.CartridgeLoader.Cartridges;
+
+public sealed partial class LogProbeCartridgeSystem
+{
+ private void InitializeNanoChat()
+ {
+ SubscribeLocalEvent(OnRecipientUpdated);
+ SubscribeLocalEvent(OnMessageReceived);
+ }
+
+ private void OnRecipientUpdated(ref NanoChatRecipientUpdatedEvent args)
+ {
+ var query = EntityQueryEnumerator();
+ while (query.MoveNext(out var uid, out var probe, out var cartridge))
+ {
+ if (probe.ScannedNanoChatData == null || GetEntity(probe.ScannedNanoChatData.Value.Card) != args.CardUid)
+ continue;
+
+ if (!TryComp(args.CardUid, out var card))
+ continue;
+
+ probe.ScannedNanoChatData = new NanoChatData(
+ new Dictionary(card.Recipients),
+ probe.ScannedNanoChatData.Value.Messages,
+ card.Number,
+ GetNetEntity(args.CardUid));
+
+ if (cartridge.LoaderUid != null)
+ UpdateUiState((uid, probe), cartridge.LoaderUid.Value);
+ }
+ }
+
+ private void OnMessageReceived(ref NanoChatMessageReceivedEvent args)
+ {
+ var query = EntityQueryEnumerator();
+ while (query.MoveNext(out var uid, out var probe, out var cartridge))
+ {
+ if (probe.ScannedNanoChatData == null || GetEntity(probe.ScannedNanoChatData.Value.Card) != args.CardUid)
+ continue;
+
+ if (!TryComp(args.CardUid, out var card))
+ continue;
+
+ probe.ScannedNanoChatData = new NanoChatData(
+ probe.ScannedNanoChatData.Value.Recipients,
+ new Dictionary>(card.Messages),
+ card.Number,
+ GetNetEntity(args.CardUid));
+
+ if (cartridge.LoaderUid != null)
+ UpdateUiState((uid, probe), cartridge.LoaderUid.Value);
+ }
+ }
+
+ private void ScanNanoChatCard(Entity ent,
+ CartridgeAfterInteractEvent args,
+ EntityUid target,
+ NanoChatCardComponent card)
+ {
+ _audioSystem.PlayEntity(ent.Comp.SoundScan,
+ args.InteractEvent.User,
+ target,
+ AudioHelpers.WithVariation(0.25f, _random));
+ _popupSystem.PopupCursor(Loc.GetString("log-probe-scan-nanochat", ("card", target)), args.InteractEvent.User);
+
+ ent.Comp.PulledAccessLogs.Clear();
+
+ ent.Comp.ScannedNanoChatData = new NanoChatData(
+ new Dictionary(card.Recipients),
+ new Dictionary>(card.Messages),
+ card.Number,
+ GetNetEntity(target)
+ );
+
+ UpdateUiState(ent, args.Loader);
+ }
+}
diff --git a/Content.Server/_CorvaxNext/CartridgeLoader/Cartridges/NanoChatCartridgeComponent.cs b/Content.Server/_CorvaxNext/CartridgeLoader/Cartridges/NanoChatCartridgeComponent.cs
new file mode 100644
index 00000000000..0f4ff971c5f
--- /dev/null
+++ b/Content.Server/_CorvaxNext/CartridgeLoader/Cartridges/NanoChatCartridgeComponent.cs
@@ -0,0 +1,26 @@
+using Content.Shared.Radio;
+using Robust.Shared.Prototypes;
+
+namespace Content.Server._CorvaxNext.CartridgeLoader.Cartridges;
+
+[RegisterComponent, Access(typeof(NanoChatCartridgeSystem))]
+public sealed partial class NanoChatCartridgeComponent : Component
+{
+ ///
+ /// Station entity to keep track of.
+ ///
+ [DataField]
+ public EntityUid? Station;
+
+ ///
+ /// The NanoChat card to keep track of.
+ ///
+ [DataField]
+ public EntityUid? Card;
+
+ ///
+ /// The required to send or receive messages.
+ ///
+ [DataField]
+ public ProtoId RadioChannel = "Common";
+}
diff --git a/Content.Server/_CorvaxNext/CartridgeLoader/Cartridges/NanoChatCartridgeSystem.cs b/Content.Server/_CorvaxNext/CartridgeLoader/Cartridges/NanoChatCartridgeSystem.cs
new file mode 100644
index 00000000000..77c3e697d53
--- /dev/null
+++ b/Content.Server/_CorvaxNext/CartridgeLoader/Cartridges/NanoChatCartridgeSystem.cs
@@ -0,0 +1,514 @@
+using System.Linq;
+using Content.Server.Administration.Logs;
+using Content.Server.CartridgeLoader;
+using Content.Server.Power.Components;
+using Content.Server.Radio;
+using Content.Server.Radio.Components;
+using Content.Server.Station.Systems;
+using Content.Shared.Access.Components;
+using Content.Shared.CartridgeLoader;
+using Content.Shared.Database;
+using Content.Shared._CorvaxNext.CartridgeLoader.Cartridges;
+using Content.Shared._CorvaxNext.NanoChat;
+using Content.Shared.PDA;
+using Content.Shared.Radio.Components;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Timing;
+
+namespace Content.Server._CorvaxNext.CartridgeLoader.Cartridges;
+
+public sealed class NanoChatCartridgeSystem : EntitySystem
+{
+ [Dependency] private readonly CartridgeLoaderSystem _cartridge = default!;
+ [Dependency] private readonly IAdminLogManager _adminLogger = default!;
+ [Dependency] private readonly IGameTiming _timing = default!;
+ [Dependency] private readonly IPrototypeManager _prototype = default!;
+ [Dependency] private readonly SharedNanoChatSystem _nanoChat = default!;
+ [Dependency] private readonly StationSystem _station = default!;
+
+ // Messages in notifications get cut off after this point
+ // no point in storing it on the comp
+ private const int NotificationMaxLength = 64;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnUiReady);
+ SubscribeLocalEvent(OnMessage);
+ }
+
+ public override void Update(float frameTime)
+ {
+ base.Update(frameTime);
+
+ // Update card references for any cartridges that need it
+ var query = EntityQueryEnumerator();
+ while (query.MoveNext(out var uid, out var nanoChat, out var cartridge))
+ {
+ if (cartridge.LoaderUid == null)
+ continue;
+
+ // Check if we need to update our card reference
+ if (!TryComp(cartridge.LoaderUid, out var pda))
+ continue;
+
+ var newCard = pda.ContainedId;
+ var currentCard = nanoChat.Card;
+
+ // If the cards match, nothing to do
+ if (newCard == currentCard)
+ continue;
+
+ // Update card reference
+ nanoChat.Card = newCard;
+
+ // Update UI state since card reference changed
+ UpdateUI((uid, nanoChat), cartridge.LoaderUid.Value);
+ }
+ }
+
+ ///
+ /// Handles incoming UI messages from the NanoChat cartridge.
+ ///
+ private void OnMessage(Entity ent, ref CartridgeMessageEvent args)
+ {
+ if (args is not NanoChatUiMessageEvent msg)
+ return;
+
+ if (!GetCardEntity(GetEntity(args.LoaderUid), out var card))
+ return;
+
+ 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;
+ }
+
+ UpdateUI(ent, GetEntity(args.LoaderUid));
+ }
+
+ ///
+ /// Gets the ID card entity associated with a PDA.
+ ///
+ /// The PDA entity ID
+ /// Output parameter containing the found card entity and component
+ /// True if a valid NanoChat card was found
+ private bool GetCardEntity(
+ EntityUid loaderUid,
+ out Entity card)
+ {
+ card = default;
+
+ // Get the PDA and check if it has an ID card
+ if (!TryComp(loaderUid, out var pda) ||
+ pda.ContainedId == null ||
+ !TryComp(pda.ContainedId, out var idCard))
+ return false;
+
+ card = (pda.ContainedId.Value, idCard);
+ return true;
+ }
+
+ ///
+ /// Handles creation of a new chat conversation.
+ ///
+ private void HandleNewChat(Entity card, NanoChatUiMessageEvent msg)
+ {
+ if (msg.RecipientNumber == null || msg.Content == null || msg.RecipientNumber == card.Comp.Number)
+ return;
+
+ // Add new recipient
+ var recipient = new NanoChatRecipient(msg.RecipientNumber.Value,
+ msg.Content,
+ msg.RecipientJob);
+
+ // Initialize or update recipient
+ _nanoChat.SetRecipient((card, card.Comp), msg.RecipientNumber.Value, recipient);
+
+ _adminLogger.Add(LogType.Action,
+ LogImpact.Low,
+ $"{ToPrettyString(msg.Actor):user} created new NanoChat conversation with #{msg.RecipientNumber:D4} ({msg.Content})");
+
+ var recipientEv = new NanoChatRecipientUpdatedEvent(card);
+ RaiseLocalEvent(ref recipientEv);
+ UpdateUIForCard(card);
+ }
+
+ ///
+ /// Handles selecting a chat conversation.
+ ///
+ private void HandleSelectChat(Entity card, NanoChatUiMessageEvent msg)
+ {
+ if (msg.RecipientNumber == null)
+ return;
+
+ _nanoChat.SetCurrentChat((card, card.Comp), msg.RecipientNumber);
+
+ // Clear unread flag when selecting chat
+ if (_nanoChat.GetRecipient((card, card.Comp), msg.RecipientNumber.Value) is { } recipient)
+ {
+ _nanoChat.SetRecipient((card, card.Comp),
+ msg.RecipientNumber.Value,
+ recipient with { HasUnread = false });
+ }
+ }
+
+ ///
+ /// Handles closing the current chat conversation.
+ ///
+ private void HandleCloseChat(Entity card)
+ {
+ _nanoChat.SetCurrentChat((card, card.Comp), null);
+ }
+
+ ///
+ /// Handles deletion of a chat conversation.
+ ///
+ private void HandleDeleteChat(Entity card, NanoChatUiMessageEvent msg)
+ {
+ if (msg.RecipientNumber == null || card.Comp.Number == null)
+ return;
+
+ // Delete chat but keep the messages
+ 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}");
+
+ UpdateUIForCard(card);
+ }
+
+ ///
+ /// Handles toggling notification mute state.
+ ///
+ private void HandleToggleMute(Entity card)
+ {
+ _nanoChat.SetNotificationsMuted((card, card.Comp), !_nanoChat.GetNotificationsMuted((card, card.Comp)));
+ UpdateUIForCard(card);
+ }
+
+ ///
+ /// Handles sending a new message in a chat conversation.
+ ///
+ private void HandleSendMessage(Entity cartridge,
+ Entity card,
+ NanoChatUiMessageEvent msg)
+ {
+ if (msg.RecipientNumber == null || msg.Content == null || card.Comp.Number == null)
+ return;
+
+ if (!EnsureRecipientExists(card, msg.RecipientNumber.Value))
+ return;
+
+ // Create and store message for sender
+ var message = new NanoChatMessage(
+ _timing.CurTime,
+ msg.Content,
+ (uint)card.Comp.Number
+ );
+
+ // Attempt delivery
+ var (deliveryFailed, recipients) = AttemptMessageDelivery(cartridge, msg.RecipientNumber.Value);
+
+ // 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);
+
+ // Log message attempt
+ 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}: {msg.Content}{(deliveryFailed ? " [DELIVERY FAILED]" : "")}");
+
+ var msgEv = new NanoChatMessageReceivedEvent(card);
+ RaiseLocalEvent(ref msgEv);
+
+ if (deliveryFailed)
+ return;
+
+ foreach (var recipient in recipients)
+ {
+ DeliverMessageToRecipient(card, recipient, message);
+ }
+ }
+
+ ///
+ /// Ensures a recipient exists in the sender's contacts.
+ ///
+ /// The card to check contacts for
+ /// The recipient's number to check
+ /// True if the recipient exists or was created successfully
+ private bool EnsureRecipientExists(Entity card, uint recipientNumber)
+ {
+ return _nanoChat.EnsureRecipientExists((card, card.Comp), recipientNumber, GetCardInfo(recipientNumber));
+ }
+
+ ///
+ /// Attempts to deliver a message to recipients.
+ ///
+ /// The sending cartridge entity
+ /// The recipient's number
+ /// Tuple containing delivery status and recipients if found.
+ private (bool failed, List> recipient) AttemptMessageDelivery(
+ Entity sender,
+ uint recipientNumber)
+ {
+ // First verify we can send from this device
+ var channel = _prototype.Index(sender.Comp.RadioChannel);
+ var sendAttemptEvent = new RadioSendAttemptEvent(channel, sender);
+ RaiseLocalEvent(ref sendAttemptEvent);
+ if (sendAttemptEvent.Cancelled)
+ return (true, new List>());
+
+ var foundRecipients = new List>();
+
+ // Find all cards with matching number
+ var cardQuery = EntityQueryEnumerator();
+ while (cardQuery.MoveNext(out var cardUid, out var card))
+ {
+ if (card.Number != recipientNumber)
+ continue;
+
+ foundRecipients.Add((cardUid, card));
+ }
+
+ if (foundRecipients.Count == 0)
+ return (true, foundRecipients);
+
+ // 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 cartridgeQuery = EntityQueryEnumerator();
+ while (cartridgeQuery.MoveNext(out var receiverUid, out var receiverCart, out _))
+ {
+ if (receiverCart.Card != recipient.Owner)
+ continue;
+
+ // 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)
+ continue;
+
+ // Must be on same map/station unless long range allowed
+ if (!channel.LongRange && recipientStation != senderStation)
+ continue;
+
+ // Needs telecomms
+ if (!HasActiveServer(senderStation.Value) || !HasActiveServer(recipientStation.Value))
+ continue;
+
+ // Check if recipient can receive
+ var receiveAttemptEv = new RadioReceiveAttemptEvent(channel, sender, receiverUid);
+ RaiseLocalEvent(ref receiveAttemptEv);
+ if (receiveAttemptEv.Cancelled)
+ continue;
+
+ // Found valid cartridge that can receive
+ deliverableRecipients.Add(recipient);
+ break; // Only need one valid cartridge per card
+ }
+ }
+
+ return (deliverableRecipients.Count == 0, deliverableRecipients);
+ }
+
+ ///
+ /// Checks if there are any active telecomms servers on the given station
+ ///
+ private bool HasActiveServer(EntityUid station)
+ {
+ // I have no idea why this isn't public in the RadioSystem
+ var query =
+ EntityQueryEnumerator();
+
+ while (query.MoveNext(out var uid, out _, out _, out var power))
+ {
+ if (_station.GetOwningStation(uid) == station && power.Powered)
+ return true;
+ }
+
+ return false;
+ }
+
+ ///
+ /// Delivers a message to the recipient and handles associated notifications.
+ ///
+ /// The sender's card entity
+ /// The recipient's card entity
+ /// The to deliver
+ private void DeliverMessageToRecipient(Entity sender,
+ Entity recipient,
+ NanoChatMessage message)
+ {
+ var senderNumber = sender.Comp.Number;
+ if (senderNumber == null)
+ return;
+
+ // Always try to get and add sender info to recipient's contacts
+ if (!EnsureRecipientExists(recipient, senderNumber.Value))
+ return;
+
+ _nanoChat.AddMessage((recipient, recipient.Comp), senderNumber.Value, message with { DeliveryFailed = false });
+
+
+ if (_nanoChat.GetCurrentChat((recipient, recipient.Comp)) != senderNumber)
+ HandleUnreadNotification(recipient, message);
+
+ var msgEv = new NanoChatMessageReceivedEvent(recipient);
+ RaiseLocalEvent(ref msgEv);
+ UpdateUIForCard(recipient);
+ }
+
+ ///
+ /// Handles unread message notifications and updates unread status.
+ ///
+ private void HandleUnreadNotification(Entity recipient, NanoChatMessage message)
+ {
+ // Get sender name from contacts or fall back to number
+ var recipients = _nanoChat.GetRecipients((recipient, recipient.Comp));
+ var senderName = recipients.TryGetValue(message.SenderId, out var existingRecipient)
+ ? existingRecipient.Name
+ : $"#{message.SenderId:D4}";
+
+ if (!recipient.Comp.Recipients[message.SenderId].HasUnread && !recipient.Comp.NotificationsMuted)
+ {
+ var pdaQuery = EntityQueryEnumerator();
+ while (pdaQuery.MoveNext(out var pdaUid, out var pdaComp))
+ {
+ if (pdaComp.ContainedId != recipient)
+ continue;
+
+ _cartridge.SendNotification(pdaUid,
+ Loc.GetString("nano-chat-new-message-title", ("sender", senderName)),
+ Loc.GetString("nano-chat-new-message-body", ("message", TruncateMessage(message.Content))));
+ break;
+ }
+ }
+
+ // Update unread status
+ _nanoChat.SetRecipient((recipient, recipient.Comp),
+ message.SenderId,
+ existingRecipient with { HasUnread = true });
+ }
+
+ ///
+ /// Updates the UI for any PDAs containing the specified card.
+ ///
+ private void UpdateUIForCard(EntityUid cardUid)
+ {
+ // Find any PDA containing this card and update its UI
+ var query = EntityQueryEnumerator();
+ while (query.MoveNext(out var uid, out var comp, out var cartridge))
+ {
+ if (comp.Card != cardUid || cartridge.LoaderUid == null)
+ continue;
+
+ UpdateUI((uid, comp), cartridge.LoaderUid.Value);
+ }
+ }
+
+ ///
+ /// Gets the for a given NanoChat number.
+ ///
+ private NanoChatRecipient? GetCardInfo(uint number)
+ {
+ // Find card with this number to get its info
+ var query = EntityQueryEnumerator();
+ while (query.MoveNext(out var uid, out var card))
+ {
+ if (card.Number != number)
+ continue;
+
+ // Try to get job title from ID card if possible
+ string? jobTitle = null;
+ var name = "Unknown";
+ if (TryComp(uid, out var idCard))
+ {
+ jobTitle = idCard.LocalizedJobTitle;
+ name = idCard.FullName ?? name;
+ }
+
+ return new NanoChatRecipient(number, name, jobTitle);
+ }
+
+ return null;
+ }
+
+ ///
+ /// Truncates a message to the notification maximum length.
+ ///
+ private static string TruncateMessage(string message)
+ {
+ return message.Length <= NotificationMaxLength
+ ? message
+ : message[..(NotificationMaxLength - 4)] + " [...]";
+ }
+
+ private void OnUiReady(Entity ent, ref CartridgeUiReadyEvent args)
+ {
+ _cartridge.RegisterBackgroundProgram(args.Loader, ent);
+ UpdateUI(ent, args.Loader);
+ }
+
+ private void UpdateUI(Entity ent, EntityUid loader)
+ {
+ if (_station.GetOwningStation(loader) is { } station)
+ ent.Comp.Station = station;
+
+ var recipients = new Dictionary();
+ var messages = new Dictionary>();
+ uint? currentChat = null;
+ uint ownNumber = 0;
+ var maxRecipients = 50;
+ var notificationsMuted = false;
+
+ if (ent.Comp.Card != null && TryComp(ent.Comp.Card, out var card))
+ {
+ recipients = card.Recipients;
+ messages = card.Messages;
+ currentChat = card.CurrentChat;
+ ownNumber = card.Number ?? 0;
+ maxRecipients = card.MaxRecipients;
+ notificationsMuted = card.NotificationsMuted;
+ }
+
+ var state = new NanoChatUiState(recipients,
+ messages,
+ currentChat,
+ ownNumber,
+ maxRecipients,
+ notificationsMuted);
+ _cartridge.UpdateCartridgeUiState(loader, state);
+ }
+}
diff --git a/Content.Server/_CorvaxNext/NanoChat/NanoChatSystem.cs b/Content.Server/_CorvaxNext/NanoChat/NanoChatSystem.cs
new file mode 100644
index 00000000000..4775abbf392
--- /dev/null
+++ b/Content.Server/_CorvaxNext/NanoChat/NanoChatSystem.cs
@@ -0,0 +1,130 @@
+using System.Linq;
+using Content.Server.Access.Systems;
+using Content.Server.Administration.Logs;
+using Content.Server.Kitchen.Components;
+using Content.Server.NameIdentifier;
+using Content.Shared.Database;
+using Content.Shared._CorvaxNext.CartridgeLoader.Cartridges;
+using Content.Shared._CorvaxNext.NanoChat;
+using Content.Shared.NameIdentifier;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Random;
+
+namespace Content.Server._CorvaxNext.NanoChat;
+
+///
+/// Handles NanoChat features that are specific to the server but not related to the cartridge itself.
+///
+public sealed class NanoChatSystem : SharedNanoChatSystem
+{
+ [Dependency] private readonly IAdminLogManager _adminLogger = default!;
+ [Dependency] private readonly IRobustRandom _random = default!;
+ [Dependency] private readonly NameIdentifierSystem _name = default!;
+
+ private readonly ProtoId _nameIdentifierGroup = "NanoChat";
+
+ public override void Initialize()
+ {
+ base.Initialize();
+ SubscribeLocalEvent(OnCardInit);
+ SubscribeLocalEvent(OnMicrowaved, after: [typeof(IdCardSystem)]);
+ }
+
+ private void OnMicrowaved(Entity ent, ref BeingMicrowavedEvent args)
+ {
+ // Skip if the entity was deleted (e.g., by ID card system burning it)
+ if (Deleted(ent))
+ return;
+
+ if (!TryComp(args.Microwave, out var micro) || micro.Broken)
+ return;
+
+ var randomPick = _random.NextFloat();
+
+ // Super lucky - erase all messages (10% chance)
+ if (randomPick <= 0.10f)
+ {
+ ent.Comp.Messages.Clear();
+ // TODO: these shouldn't be shown at the same time as the popups from IdCardSystem
+ // _popup.PopupEntity(Loc.GetString("nanochat-card-microwave-erased", ("card", ent)),
+ // ent,
+ // PopupType.Medium);
+
+ _adminLogger.Add(LogType.Action,
+ LogImpact.Medium,
+ $"{ToPrettyString(args.Microwave)} erased all messages on {ToPrettyString(ent)}");
+ }
+ else
+ {
+ // Scramble random messages for random recipients
+ ScrambleMessages(ent);
+ // _popup.PopupEntity(Loc.GetString("nanochat-card-microwave-scrambled", ("card", ent)),
+ // ent,
+ // PopupType.Medium);
+
+ _adminLogger.Add(LogType.Action,
+ LogImpact.Medium,
+ $"{ToPrettyString(args.Microwave)} scrambled messages on {ToPrettyString(ent)}");
+ }
+
+ Dirty(ent);
+ }
+
+ private void ScrambleMessages(NanoChatCardComponent component)
+ {
+ foreach (var (recipientNumber, messages) in component.Messages)
+ {
+ for (var i = 0; i < messages.Count; i++)
+ {
+ // 50% chance to scramble each message
+ if (!_random.Prob(0.5f))
+ continue;
+
+ var message = messages[i];
+ message.Content = ScrambleText(message.Content);
+ messages[i] = message;
+ }
+
+ // 25% chance to reassign the conversation to a random recipient
+ if (_random.Prob(0.25f) && component.Recipients.Count > 0)
+ {
+ var newRecipient = _random.Pick(component.Recipients.Keys.ToList());
+ if (newRecipient == recipientNumber)
+ continue;
+
+ if (!component.Messages.ContainsKey(newRecipient))
+ component.Messages[newRecipient] = new List();
+
+ component.Messages[newRecipient].AddRange(messages);
+ component.Messages[recipientNumber].Clear();
+ }
+ }
+ }
+
+ private string ScrambleText(string text)
+ {
+ var chars = text.ToCharArray();
+ var n = chars.Length;
+
+ // Fisher-Yates shuffle of characters
+ while (n > 1)
+ {
+ n--;
+ var k = _random.Next(n + 1);
+ (chars[k], chars[n]) = (chars[n], chars[k]);
+ }
+
+ return new string(chars);
+ }
+
+ private void OnCardInit(Entity ent, ref MapInitEvent args)
+ {
+ if (ent.Comp.Number != null)
+ return;
+
+ // Assign a random number
+ _name.GenerateUniqueName(ent, _nameIdentifierGroup, out var number);
+ ent.Comp.Number = (uint)number;
+ Dirty(ent);
+ }
+}
diff --git a/Content.Shared/Access/SharedAgentIDCardSystem.cs b/Content.Shared/Access/SharedAgentIDCardSystem.cs
index aefd413de8b..12c40982ef4 100644
--- a/Content.Shared/Access/SharedAgentIDCardSystem.cs
+++ b/Content.Shared/Access/SharedAgentIDCardSystem.cs
@@ -28,12 +28,26 @@ public sealed class AgentIDCardBoundUserInterfaceState : BoundUserInterfaceState
public string CurrentName { get; }
public string CurrentJob { get; }
public string CurrentJobIconId { get; }
+ public uint? CurrentNumber { get; } // Corvax-Next-PDAChat
- public AgentIDCardBoundUserInterfaceState(string currentName, string currentJob, string currentJobIconId)
+ public AgentIDCardBoundUserInterfaceState(string currentName, string currentJob, string currentJobIconId, uint? currentNumber = null) // Corvax-Next-PDAChat - Added currentNumber
{
CurrentName = currentName;
CurrentJob = currentJob;
CurrentJobIconId = currentJobIconId;
+ CurrentNumber = currentNumber; // Corvax-Next-PDAChat
+ }
+ }
+
+ // Corvax-Next-PDAChat - Add number change message
+ [Serializable, NetSerializable]
+ public sealed class AgentIDCardNumberChangedMessage : BoundUserInterfaceMessage
+ {
+ public uint Number { get; }
+
+ public AgentIDCardNumberChangedMessage(uint number)
+ {
+ Number = number;
}
}
diff --git a/Content.Shared/CartridgeLoader/Cartridges/LogProbeUiState.cs b/Content.Shared/CartridgeLoader/Cartridges/LogProbeUiState.cs
index 9dc507b7e51..fbe9ee3fc4b 100644
--- a/Content.Shared/CartridgeLoader/Cartridges/LogProbeUiState.cs
+++ b/Content.Shared/CartridgeLoader/Cartridges/LogProbeUiState.cs
@@ -1,4 +1,5 @@
-using Robust.Shared.Serialization;
+using Content.Shared._CorvaxNext.CartridgeLoader.Cartridges;
+using Robust.Shared.Serialization;
namespace Content.Shared.CartridgeLoader.Cartridges;
@@ -10,9 +11,15 @@ public sealed class LogProbeUiState : BoundUserInterfaceState
///
public List PulledLogs;
- public LogProbeUiState(List pulledLogs)
+ ///
+ /// Corvax-Next-PDAChat: The NanoChat data if a card was scanned, null otherwise
+ ///
+ public NanoChatData? NanoChatData { get; }
+
+ public LogProbeUiState(List pulledLogs, NanoChatData? nanoChatData = null) // Corvax-Next-PDAChat - NanoChat support
{
PulledLogs = pulledLogs;
+ NanoChatData = nanoChatData; // Corvax-Next-PDAChat
}
}
diff --git a/Content.Shared/_CorvaxNext/CartridgeLoader/Cartridges/NanoChatUiMessageEvent.cs b/Content.Shared/_CorvaxNext/CartridgeLoader/Cartridges/NanoChatUiMessageEvent.cs
new file mode 100644
index 00000000000..c426ca63150
--- /dev/null
+++ b/Content.Shared/_CorvaxNext/CartridgeLoader/Cartridges/NanoChatUiMessageEvent.cs
@@ -0,0 +1,166 @@
+using Content.Shared.CartridgeLoader;
+using Robust.Shared.Serialization;
+
+namespace Content.Shared._CorvaxNext.CartridgeLoader.Cartridges;
+
+[Serializable, NetSerializable]
+public sealed class NanoChatUiMessageEvent : CartridgeMessageEvent
+{
+ ///
+ /// The type of UI message being sent.
+ ///
+ public readonly NanoChatUiMessageType Type;
+
+ ///
+ /// The recipient's NanoChat number, if applicable.
+ ///
+ public readonly uint? RecipientNumber;
+
+ ///
+ /// The content of the message or name for new chats.
+ ///
+ public readonly string? Content;
+
+ ///
+ /// The recipient's job title when creating a new chat.
+ ///
+ public readonly string? RecipientJob;
+
+ ///
+ /// Creates a new NanoChat UI message event.
+ ///
+ /// The type of message being sent
+ /// Optional recipient number for the message
+ /// Optional content of the message
+ /// Optional job title for new chat creation
+ public NanoChatUiMessageEvent(NanoChatUiMessageType type,
+ uint? recipientNumber = null,
+ string? content = null,
+ string? recipientJob = null)
+ {
+ Type = type;
+ RecipientNumber = recipientNumber;
+ Content = content;
+ RecipientJob = recipientJob;
+ }
+}
+
+[Serializable, NetSerializable]
+public enum NanoChatUiMessageType : byte
+{
+ NewChat,
+ SelectChat,
+ CloseChat,
+ SendMessage,
+ DeleteChat,
+ ToggleMute,
+}
+
+// putting this here because i can
+[Serializable, NetSerializable, DataRecord]
+public struct NanoChatRecipient
+{
+ ///
+ /// The recipient's unique NanoChat number.
+ ///
+ public uint Number;
+
+ ///
+ /// The recipient's display name, typically from their ID card.
+ ///
+ public string Name;
+
+ ///
+ /// The recipient's job title, if available.
+ ///
+ public string? JobTitle;
+
+ ///
+ /// Whether this recipient has unread messages.
+ ///
+ public bool HasUnread;
+
+ ///
+ /// Creates a new NanoChat recipient.
+ ///
+ /// The recipient's NanoChat number
+ /// The recipient's display name
+ /// Optional job title for the recipient
+ /// Whether there are unread messages from this recipient
+ public NanoChatRecipient(uint number, string name, string? jobTitle = null, bool hasUnread = false)
+ {
+ Number = number;
+ Name = name;
+ JobTitle = jobTitle;
+ HasUnread = hasUnread;
+ }
+}
+
+[Serializable, NetSerializable, DataRecord]
+public struct NanoChatMessage
+{
+ ///
+ /// When the message was sent.
+ ///
+ public TimeSpan Timestamp;
+
+ ///
+ /// The content of the message.
+ ///
+ public string Content;
+
+ ///
+ /// The NanoChat number of the sender.
+ ///
+ public uint SenderId;
+
+ ///
+ /// Whether the message failed to deliver to the recipient.
+ /// This can happen if the recipient is out of range or if there's no active telecomms server.
+ ///
+ public bool DeliveryFailed;
+
+ ///
+ /// Creates a new NanoChat message.
+ ///
+ /// When the message was sent
+ /// The content of the message
+ /// The sender's NanoChat number
+ /// Whether delivery to the recipient failed
+ public NanoChatMessage(TimeSpan timestamp, string content, uint senderId, bool deliveryFailed = false)
+ {
+ Timestamp = timestamp;
+ Content = content;
+ SenderId = senderId;
+ DeliveryFailed = deliveryFailed;
+ }
+}
+
+///
+/// NanoChat log data struct
+///
+/// Used by the LogProbe
+[Serializable, NetSerializable, DataRecord]
+public readonly struct NanoChatData(
+ Dictionary recipients,
+ Dictionary> messages,
+ uint? cardNumber,
+ NetEntity card)
+{
+ public Dictionary Recipients { get; } = recipients;
+ public Dictionary> Messages { get; } = messages;
+ public uint? CardNumber { get; } = cardNumber;
+ public NetEntity Card { get; } = card;
+}
+
+///
+/// Raised on the NanoChat card whenever a recipient gets added
+///
+[ByRefEvent]
+public readonly record struct NanoChatRecipientUpdatedEvent(EntityUid CardUid);
+
+///
+/// Raised on the NanoChat card whenever it receives or tries sending a messsage
+///
+[ByRefEvent]
+public readonly record struct NanoChatMessageReceivedEvent(EntityUid CardUid);
diff --git a/Content.Shared/_CorvaxNext/CartridgeLoader/Cartridges/NanoChatUiState.cs b/Content.Shared/_CorvaxNext/CartridgeLoader/Cartridges/NanoChatUiState.cs
new file mode 100644
index 00000000000..bed210682ae
--- /dev/null
+++ b/Content.Shared/_CorvaxNext/CartridgeLoader/Cartridges/NanoChatUiState.cs
@@ -0,0 +1,30 @@
+using Robust.Shared.Serialization;
+
+namespace Content.Shared._CorvaxNext.CartridgeLoader.Cartridges;
+
+[Serializable, NetSerializable]
+public sealed class NanoChatUiState : BoundUserInterfaceState
+{
+ public readonly Dictionary Recipients = new();
+ public readonly Dictionary> Messages = new();
+ public readonly uint? CurrentChat;
+ public readonly uint OwnNumber;
+ public readonly int MaxRecipients;
+ public readonly bool NotificationsMuted;
+
+ public NanoChatUiState(
+ Dictionary recipients,
+ Dictionary> messages,
+ uint? currentChat,
+ uint ownNumber,
+ int maxRecipients,
+ bool notificationsMuted)
+ {
+ Recipients = recipients;
+ Messages = messages;
+ CurrentChat = currentChat;
+ OwnNumber = ownNumber;
+ MaxRecipients = maxRecipients;
+ NotificationsMuted = notificationsMuted;
+ }
+}
diff --git a/Content.Shared/_CorvaxNext/NanoChat/NanoChatCardComponent.cs b/Content.Shared/_CorvaxNext/NanoChat/NanoChatCardComponent.cs
new file mode 100644
index 00000000000..7830b902a09
--- /dev/null
+++ b/Content.Shared/_CorvaxNext/NanoChat/NanoChatCardComponent.cs
@@ -0,0 +1,52 @@
+using Content.Shared._CorvaxNext.CartridgeLoader.Cartridges;
+using Robust.Shared.GameStates;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
+
+namespace Content.Shared._CorvaxNext.NanoChat;
+
+[RegisterComponent, NetworkedComponent, Access(typeof(SharedNanoChatSystem))]
+[AutoGenerateComponentPause, AutoGenerateComponentState]
+public sealed partial class NanoChatCardComponent : Component
+{
+ ///
+ /// The number assigned to this card.
+ ///
+ [DataField, AutoNetworkedField]
+ public uint? Number;
+
+ ///
+ /// All chat recipients stored on this card.
+ ///
+ [DataField]
+ public Dictionary Recipients = new();
+
+ ///
+ /// All messages stored on this card, keyed by recipient number.
+ ///
+ [DataField]
+ public Dictionary> Messages = new();
+
+ ///
+ /// The currently selected chat recipient number.
+ ///
+ [DataField]
+ public uint? CurrentChat;
+
+ ///
+ /// The maximum amount of recipients this card supports.
+ ///
+ [DataField]
+ public int MaxRecipients = 50;
+
+ ///
+ /// Last time a message was sent, for rate limiting.
+ ///
+ [DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), AutoPausedField]
+ public TimeSpan LastMessageTime; // TODO: actually use this, compare against actor and not the card
+
+ ///
+ /// Whether to send notifications.
+ ///
+ [DataField]
+ public bool NotificationsMuted;
+}
diff --git a/Content.Shared/_CorvaxNext/NanoChat/SharedNanoChatSystem.cs b/Content.Shared/_CorvaxNext/NanoChat/SharedNanoChatSystem.cs
new file mode 100644
index 00000000000..f40b84cbb92
--- /dev/null
+++ b/Content.Shared/_CorvaxNext/NanoChat/SharedNanoChatSystem.cs
@@ -0,0 +1,273 @@
+using Content.Shared._CorvaxNext.CartridgeLoader.Cartridges;
+using Content.Shared.Examine;
+using Robust.Shared.Timing;
+
+namespace Content.Shared._CorvaxNext.NanoChat;
+
+///
+/// Base system for NanoChat functionality shared between client and server.
+///
+public abstract class SharedNanoChatSystem : EntitySystem
+{
+ [Dependency] private readonly IGameTiming _timing = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+ SubscribeLocalEvent(OnExamined);
+ }
+
+ private void OnExamined(Entity ent, ref ExaminedEvent args)
+ {
+ if (!args.IsInDetailsRange)
+ return;
+
+ if (ent.Comp.Number == null)
+ {
+ args.PushMarkup(Loc.GetString("nanochat-card-examine-no-number"));
+ return;
+ }
+
+ args.PushMarkup(Loc.GetString("nanochat-card-examine-number", ("number", $"{ent.Comp.Number:D4}")));
+ }
+
+ #region Public API Methods
+
+ ///
+ /// Gets the NanoChat number for a card.
+ ///
+ public uint? GetNumber(Entity card)
+ {
+ if (!Resolve(card, ref card.Comp))
+ return null;
+
+ return card.Comp.Number;
+ }
+
+ ///
+ /// Sets the NanoChat number for a card.
+ ///
+ public void SetNumber(Entity card, uint number)
+ {
+ if (!Resolve(card, ref card.Comp))
+ return;
+
+ card.Comp.Number = number;
+ Dirty(card);
+ }
+
+ ///
+ /// Gets the recipients dictionary from a card.
+ ///
+ public IReadOnlyDictionary GetRecipients(Entity card)
+ {
+ if (!Resolve(card, ref card.Comp))
+ return new Dictionary();
+
+ return card.Comp.Recipients;
+ }
+
+ ///
+ /// Gets the messages dictionary from a card.
+ ///
+ public IReadOnlyDictionary> GetMessages(Entity card)
+ {
+ if (!Resolve(card, ref card.Comp))
+ return new Dictionary>();
+
+ return card.Comp.Messages;
+ }
+
+ ///
+ /// Sets a specific recipient in the card.
+ ///
+ public void SetRecipient(Entity card, uint number, NanoChatRecipient recipient)
+ {
+ if (!Resolve(card, ref card.Comp))
+ return;
+
+ card.Comp.Recipients[number] = recipient;
+ Dirty(card);
+ }
+
+ ///
+ /// Gets a specific recipient from the card.
+ ///
+ public NanoChatRecipient? GetRecipient(Entity card, uint number)
+ {
+ if (!Resolve(card, ref card.Comp) || !card.Comp.Recipients.TryGetValue(number, out var recipient))
+ return null;
+
+ return recipient;
+ }
+
+ ///
+ /// Gets all messages for a specific recipient.
+ ///
+ public List? GetMessagesForRecipient(Entity card, uint recipientNumber)
+ {
+ if (!Resolve(card, ref card.Comp) || !card.Comp.Messages.TryGetValue(recipientNumber, out var messages))
+ return null;
+
+ return new List(messages);
+ }
+
+ ///
+ /// Adds a message to a recipient's conversation.
+ ///
+ public void AddMessage(Entity card, uint recipientNumber, NanoChatMessage message)
+ {
+ if (!Resolve(card, ref card.Comp))
+ return;
+
+ if (!card.Comp.Messages.TryGetValue(recipientNumber, out var messages))
+ {
+ messages = new List();
+ card.Comp.Messages[recipientNumber] = messages;
+ }
+
+ messages.Add(message);
+ card.Comp.LastMessageTime = _timing.CurTime;
+ Dirty(card);
+ }
+
+ ///
+ /// Gets the currently selected chat recipient.
+ ///
+ public uint? GetCurrentChat(Entity card)
+ {
+ if (!Resolve(card, ref card.Comp))
+ return null;
+
+ return card.Comp.CurrentChat;
+ }
+
+ ///
+ /// Sets the currently selected chat recipient.
+ ///
+ public void SetCurrentChat(Entity card, uint? recipient)
+ {
+ if (!Resolve(card, ref card.Comp))
+ return;
+
+ card.Comp.CurrentChat = recipient;
+ Dirty(card);
+ }
+
+ ///
+ /// Gets whether notifications are muted.
+ ///
+ public bool GetNotificationsMuted(Entity card)
+ {
+ if (!Resolve(card, ref card.Comp))
+ return false;
+
+ return card.Comp.NotificationsMuted;
+ }
+
+ ///
+ /// Sets whether notifications are muted.
+ ///
+ public void SetNotificationsMuted(Entity card, bool muted)
+ {
+ if (!Resolve(card, ref card.Comp))
+ return;
+
+ card.Comp.NotificationsMuted = muted;
+ Dirty(card);
+ }
+
+ ///
+ /// Gets the time of the last message.
+ ///
+ public TimeSpan? GetLastMessageTime(Entity card)
+ {
+ if (!Resolve(card, ref card.Comp))
+ return null;
+
+ return card.Comp.LastMessageTime;
+ }
+
+ ///
+ /// Gets if there are unread messages from a recipient.
+ ///
+ public bool HasUnreadMessages(Entity card, uint recipientNumber)
+ {
+ if (!Resolve(card, ref card.Comp) || !card.Comp.Recipients.TryGetValue(recipientNumber, out var recipient))
+ return false;
+
+ return recipient.HasUnread;
+ }
+
+ ///
+ /// Clears all messages and recipients from the card.
+ ///
+ public void Clear(Entity card)
+ {
+ if (!Resolve(card, ref card.Comp))
+ return;
+
+ card.Comp.Messages.Clear();
+ card.Comp.Recipients.Clear();
+ card.Comp.CurrentChat = null;
+ Dirty(card);
+ }
+
+ ///
+ /// Deletes a chat conversation with a recipient from the card.
+ /// Optionally keeps message history while removing from active chats.
+ ///
+ /// True if the chat was deleted successfully
+ public bool TryDeleteChat(Entity card, uint recipientNumber, bool keepMessages = false)
+ {
+ if (!Resolve(card, ref card.Comp))
+ return false;
+
+ // Remove from recipients list
+ var removed = card.Comp.Recipients.Remove(recipientNumber);
+
+ // Clear messages if requested
+ if (!keepMessages)
+ card.Comp.Messages.Remove(recipientNumber);
+
+ // Clear current chat if we just deleted it
+ if (card.Comp.CurrentChat == recipientNumber)
+ card.Comp.CurrentChat = null;
+
+ if (removed)
+ Dirty(card);
+
+ return removed;
+ }
+
+ ///
+ /// Ensures a recipient exists in the card's contacts and message lists.
+ /// If the recipient doesn't exist, they will be added with the provided info.
+ ///
+ /// True if the recipient was added or already existed
+ public bool EnsureRecipientExists(Entity card,
+ uint recipientNumber,
+ NanoChatRecipient? recipientInfo = null)
+ {
+ if (!Resolve(card, ref card.Comp))
+ return false;
+
+ if (!card.Comp.Recipients.ContainsKey(recipientNumber))
+ {
+ // Only add if we have recipient info
+ if (recipientInfo == null)
+ return false;
+
+ card.Comp.Recipients[recipientNumber] = recipientInfo.Value;
+ }
+
+ // Ensure message list exists for this recipient
+ if (!card.Comp.Messages.ContainsKey(recipientNumber))
+ card.Comp.Messages[recipientNumber] = new List();
+
+ Dirty(card);
+ return true;
+ }
+
+ #endregion
+}
diff --git a/Resources/Locale/en-US/_corvaxnext/access/components/agent-id-card-component.ftl b/Resources/Locale/en-US/_corvaxnext/access/components/agent-id-card-component.ftl
new file mode 100644
index 00000000000..00b6312fbde
--- /dev/null
+++ b/Resources/Locale/en-US/_corvaxnext/access/components/agent-id-card-component.ftl
@@ -0,0 +1 @@
+agent-id-card-current-number = NanoChat Number
diff --git a/Resources/Locale/en-US/_corvaxnext/cartridge-loader/nanochat.ftl b/Resources/Locale/en-US/_corvaxnext/cartridge-loader/nanochat.ftl
new file mode 100644
index 00000000000..60e50447a07
--- /dev/null
+++ b/Resources/Locale/en-US/_corvaxnext/cartridge-loader/nanochat.ftl
@@ -0,0 +1,36 @@
+# General
+nano-chat-program-name = NanoChat
+nano-chat-title = NanoChat
+nano-chat-new-chat = New Chat
+nano-chat-contacts = CONTACTS
+nano-chat-no-chats = No active chats
+nano-chat-select-chat = Select a chat to begin
+nano-chat-message-placeholder = Type a message...
+nano-chat-send = Send
+nano-chat-delete = Delete
+nano-chat-loading = Loading...
+nano-chat-message-too-long = Message too long ({$current}/{$max} characters)
+nano-chat-max-recipients = Maximum number of chats reached
+nano-chat-new-message-title = Message from {$sender}
+nano-chat-new-message-body = {$message}
+nano-chat-toggle-mute = Mute notifications
+nano-chat-delivery-failed = Failed to deliver
+# Create chat popup
+nano-chat-new-title = Add a new chat
+nano-chat-number-label = Number
+nano-chat-name-label = Name
+nano-chat-job-label = Job title
+nano-chat-number-placeholder = Enter a number
+nano-chat-name-placeholder = Enter a name
+nano-chat-job-placeholder = Enter a job title (optional)
+nano-chat-cancel = Cancel
+nano-chat-create = Create
+# LogProbe additions
+log-probe-scan-nanochat = Scanned {$card}'s NanoChat logs
+log-probe-header-access = Access Log Scanner
+log-probe-header-nanochat = NanoChat Log Scanner
+log-probe-label-message = Message
+log-probe-card-number = Card: {$number}
+log-probe-recipients = {$count} Recipients
+log-probe-recipient-list = Known Recipients:
+log-probe-message-format = {$sender} → {$recipient}: {$content}
diff --git a/Resources/Locale/en-US/_corvaxnext/nanochat/components/nanochat-card-component.ftl b/Resources/Locale/en-US/_corvaxnext/nanochat/components/nanochat-card-component.ftl
new file mode 100644
index 00000000000..ace8a44cb70
--- /dev/null
+++ b/Resources/Locale/en-US/_corvaxnext/nanochat/components/nanochat-card-component.ftl
@@ -0,0 +1,5 @@
+nanochat-card-examine-no-number = The NanoChat card has not been assigned a number yet.
+nanochat-card-examine-number = The NanoChat card displays #{$number}.
+# Microwave interactions
+nanochat-card-microwave-erased = The {$card} emits a soft beep as all its message history vanishes into the ether!
+nanochat-card-microwave-scrambled = The {$card} crackles as its messages become scrambled!
diff --git a/Resources/Locale/ru-RU/_corvaxnext/access/components/agent-id-card-component.ftl b/Resources/Locale/ru-RU/_corvaxnext/access/components/agent-id-card-component.ftl
new file mode 100644
index 00000000000..406a5e13360
--- /dev/null
+++ b/Resources/Locale/ru-RU/_corvaxnext/access/components/agent-id-card-component.ftl
@@ -0,0 +1 @@
+agent-id-card-current-number = Номер NanoChat
diff --git a/Resources/Locale/ru-RU/_corvaxnext/cartridge-loader/nanochat.ftl b/Resources/Locale/ru-RU/_corvaxnext/cartridge-loader/nanochat.ftl
new file mode 100644
index 00000000000..e7ba5f7a8a3
--- /dev/null
+++ b/Resources/Locale/ru-RU/_corvaxnext/cartridge-loader/nanochat.ftl
@@ -0,0 +1,36 @@
+# General
+nano-chat-program-name = NanoChat
+nano-chat-title = NanoChat
+nano-chat-new-chat = Новый Чат
+nano-chat-contacts = КОНТАКТЫ
+nano-chat-no-chats = Нет активных чатов
+nano-chat-select-chat = Выберите чат
+nano-chat-message-placeholder = Введите сообщение...
+nano-chat-send = Отправить
+nano-chat-delete = Удалить
+nano-chat-loading = Загрузка...
+nano-chat-message-too-long = Сообщение содержит ({$current}/{$max} символов)
+nano-chat-max-recipients = Достигнут лимит чатов
+nano-chat-new-message-title = Сообщение от {$sender}
+nano-chat-new-message-body = {$message}
+nano-chat-toggle-mute = Отключить оповещения
+nano-chat-delivery-failed = Ошибка отправления
+# Create chat popup
+nano-chat-new-title = Создать новый чат
+nano-chat-number-label = Номер
+nano-chat-name-label = Имя
+nano-chat-job-label = Должность
+nano-chat-number-placeholder = Введите номер
+nano-chat-name-placeholder = Введите имя
+nano-chat-job-placeholder = Введите должность (необязательно)
+nano-chat-cancel = Отменить
+nano-chat-create = Создать
+# LogProbe additions
+log-probe-scan-nanochat = Логи NanoChat {$card} просканированы
+log-probe-header-access = Сканер Логов Доступа
+log-probe-header-nanochat = Сканер Логов NanoChat
+log-probe-label-message = Сообщение
+log-probe-card-number = Номер: {$number}
+log-probe-recipients = {$count} Получателей
+log-probe-recipient-list = Известные Получатели:
+log-probe-message-format = {$sender} → {$recipient}: {$content}
diff --git a/Resources/Locale/ru-RU/_corvaxnext/nanochat/components/nanochat-card-component.ftl b/Resources/Locale/ru-RU/_corvaxnext/nanochat/components/nanochat-card-component.ftl
new file mode 100644
index 00000000000..3fe4d907dc2
--- /dev/null
+++ b/Resources/Locale/ru-RU/_corvaxnext/nanochat/components/nanochat-card-component.ftl
@@ -0,0 +1,5 @@
+nanochat-card-examine-no-number = Карточке NanoChat ещё не присвоен номер.
+nanochat-card-examine-number = На карточке NanoChat отображается номер #{$number}.
+# Microwave interactions
+nanochat-card-microwave-erased = {$card} издаёт тихий писк, и вся история сообщений растворяется в пустоте!
+nanochat-card-microwave-scrambled = {$card} потрескивает, пока все сообщения превращаются в кашу!
diff --git a/Resources/Locale/ru-RU/ss14-ru/prototypes/_corvaxnext/entities/objects/devices/cartriges.ftl b/Resources/Locale/ru-RU/ss14-ru/prototypes/_corvaxnext/entities/objects/devices/cartriges.ftl
index 427dadde5e1..8949b842190 100644
--- a/Resources/Locale/ru-RU/ss14-ru/prototypes/_corvaxnext/entities/objects/devices/cartriges.ftl
+++ b/Resources/Locale/ru-RU/ss14-ru/prototypes/_corvaxnext/entities/objects/devices/cartriges.ftl
@@ -1,2 +1,4 @@
ent-SecWatchCartridge = картридж "око сб"
- .desc = Картридж, отслеживающий статус разыскиваемых службой безопасности лиц.
\ No newline at end of file
+ .desc = Картридж, отслеживающий статус разыскиваемых службой безопасности лиц.
+ent-NanoChatCartridge = картридж NanoChat
+ .desc = Для просто чилловых парней, которые любят початиться.
diff --git a/Resources/Prototypes/Entities/Objects/Devices/pda.yml b/Resources/Prototypes/Entities/Objects/Devices/pda.yml
index 8b13d2598bb..10e834d7cd1 100644
--- a/Resources/Prototypes/Entities/Objects/Devices/pda.yml
+++ b/Resources/Prototypes/Entities/Objects/Devices/pda.yml
@@ -77,6 +77,7 @@
- NotekeeperCartridge
- NewsReaderCartridge
- SecWatchCartridge # Corvax-Next-SecWatch
+ - NanoChatCartridge # Corvax-Next-PDAChat
cartridgeSlot:
priority: -1
name: device-pda-slot-component-slot-name-cartridge
@@ -126,6 +127,7 @@
- NotekeeperCartridge
- NewsReaderCartridge
- WantedListCartridge
+ - NanoChatCartridge # Corvax-Next-PDAChat
- type: entity
parent: BasePDA
@@ -140,6 +142,7 @@
- NewsReaderCartridge
- MedTekCartridge
- SecWatchCartridge # Corvax-Next-SecWatch
+ - NanoChatCartridge # Corvax-Next-PDAChat
- type: entity
parent: BasePDA
@@ -411,6 +414,7 @@
- NewsReaderCartridge
- AstroNavCartridge
- SecWatchCartridge # Corvax-Next-SecWatch
+ - NanoChatCartridge # Corvax-Next-PDAChat
- type: entity
parent: BasePDA
@@ -694,6 +698,7 @@
- NewsReaderCartridge
- WantedListCartridge
- LogProbeCartridge
+ - NanoChatCartridge # Corvax-Next-PDAChat
- type: entity
parent: BaseSecurityPDA
@@ -752,6 +757,7 @@
- WantedListCartridge
- LogProbeCartridge
- AstroNavCartridge
+ - NanoChatCartridge # Corvax-Next-PDAChat
- type: entity
parent: CentcomPDA
@@ -777,6 +783,7 @@
- MedTekCartridge
- AstroNavCartridge
- SecWatchCartridge # Corvax-Next-SecWatch
+ - NanoChatCartridge # Corvax-Next-PDAChat
- type: entity
parent: CentcomPDA
@@ -895,6 +902,7 @@
- WantedListCartridge
- LogProbeCartridge
- AstroNavCartridge
+ - NanoChatCartridge # Corvax-Next-PDAChat
- type: entity
parent: ERTLeaderPDA
@@ -1043,6 +1051,7 @@
- NewsReaderCartridge
- WantedListCartridge
- LogProbeCartridge
+ - NanoChatCartridge # Corvax-Next-PDAChat
- type: entity
parent: BaseMedicalPDA
@@ -1066,6 +1075,7 @@
- NewsReaderCartridge
- WantedListCartridge
- MedTekCartridge
+ - NanoChatCartridge # Corvax-Next-PDAChat
- type: entity
parent: ClownPDA
@@ -1182,3 +1192,4 @@
preinstalled:
- NotekeeperCartridge
- MedTekCartridge
+ - NanoChatCartridge # Corvax-Next-PDAChat
diff --git a/Resources/Prototypes/Entities/Objects/Misc/identification_cards.yml b/Resources/Prototypes/Entities/Objects/Misc/identification_cards.yml
index ebc8de5ed90..e43b8010b46 100644
--- a/Resources/Prototypes/Entities/Objects/Misc/identification_cards.yml
+++ b/Resources/Prototypes/Entities/Objects/Misc/identification_cards.yml
@@ -24,6 +24,7 @@
- WhitelistChameleon
- type: StealTarget
stealGroup: IDCard
+ - type: NanoChatCard # Corvax-Next-PDAChat
#IDs with layers
@@ -828,3 +829,5 @@
- NuclearOperative
- SyndicateAgent
- Wizard
+ - type: NanoChatCard # Corvax-Next-PDAChat
+ notificationsMuted: true
diff --git a/Resources/Prototypes/_CorvaxNext/Entities/Objects/Devices/cartridges.yml b/Resources/Prototypes/_CorvaxNext/Entities/Objects/Devices/cartridges.yml
index b271eb6378f..9bc1b9323f1 100644
--- a/Resources/Prototypes/_CorvaxNext/Entities/Objects/Devices/cartridges.yml
+++ b/Resources/Prototypes/_CorvaxNext/Entities/Objects/Devices/cartridges.yml
@@ -17,4 +17,25 @@
icon:
sprite: Objects/Weapons/Melee/stunbaton.rsi
state: stunbaton_on
- - type: SecWatchCartridge
\ No newline at end of file
+ - type: SecWatchCartridge
+
+- type: entity
+ parent: BaseItem
+ id: NanoChatCartridge
+ name: NanoChat cartridge
+ description: Lets you message other people!
+ components:
+ - type: Sprite
+ sprite: _CorvaxNext/Objects/Devices/cartridge.rsi
+ state: cart-chat
+ - type: UIFragment
+ ui: !type:NanoChatUi
+ - type: NanoChatCartridge
+ - type: Cartridge
+ programName: nano-chat-program-name
+ icon:
+ sprite: _CorvaxNext/Misc/program_icons.rsi
+ state: nanochat
+ - type: ActiveRadio
+ channels:
+ - Common
diff --git a/Resources/Prototypes/_CorvaxNext/name_identifier_groups.yml b/Resources/Prototypes/_CorvaxNext/name_identifier_groups.yml
new file mode 100644
index 00000000000..6b7058c8f7c
--- /dev/null
+++ b/Resources/Prototypes/_CorvaxNext/name_identifier_groups.yml
@@ -0,0 +1,4 @@
+# used by the nanochatcard numbers
+- type: nameIdentifierGroup
+ id: NanoChat
+ maxValue: 9999
diff --git a/Resources/Textures/_CorvaxNext/Interface/VerbIcons/ATTRIBUTION.txt b/Resources/Textures/_CorvaxNext/Interface/VerbIcons/ATTRIBUTION.txt
new file mode 100644
index 00000000000..b2df7b85ad9
--- /dev/null
+++ b/Resources/Textures/_CorvaxNext/Interface/VerbIcons/ATTRIBUTION.txt
@@ -0,0 +1,2 @@
+bell.svg taken from https://coreui.io/icons/
+Licensed under CC BY 4.0
\ No newline at end of file
diff --git a/Resources/Textures/_CorvaxNext/Interface/VerbIcons/bell.svg b/Resources/Textures/_CorvaxNext/Interface/VerbIcons/bell.svg
new file mode 100644
index 00000000000..4274db2113f
--- /dev/null
+++ b/Resources/Textures/_CorvaxNext/Interface/VerbIcons/bell.svg
@@ -0,0 +1,5 @@
+
+
diff --git a/Resources/Textures/_CorvaxNext/Interface/VerbIcons/bell.svg.png b/Resources/Textures/_CorvaxNext/Interface/VerbIcons/bell.svg.png
new file mode 100644
index 0000000000000000000000000000000000000000..7fada9e5afde46b39b4073a0eb87fa10a212324c
GIT binary patch
literal 720
zcmV;>0x$iEP)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D0%b`=K~#8N?b|_4
z!!Qs9(1v4_dzCF)F2V|N3=UK&`&1Q3tyH9P0r0dRlZ3GoJ7Xtl=51Uu!5&ZSkV080
zrBp88{cLdhZNQ{rB`-#SRvcxImlE!aqWHjTQQ`$K^In_g{rZ(GQv~sR@6d|CRrgMw
zCgEbj57)DJ!mH{+tfC#s1utP=e<+p+krm)d_Ha17P_jnw{>AsP6ZI7*)Zj{*T-ZJD
zN&(ylCxo)W&=wBofiW!{&I&_YWm!HW
zlpe;ka5yVkbr{}4C`i;^Y7aKno|MQ#`Z-{Bv;b%qOACN@v9tha7fTC(cCoYoXctQh
zfVQ{x8;~h6snM_Kg9H_T8(^$=f)2rdlMGz}N&e^=i!1*5e161BS?20*X!+M6G%BRh
zfRHTo-GI>OkdAdaoxTu~K^>xR3!zEa@AnnbvJjGiJ}vPI3qWcO2n~b28W5TklUAC0
z6#A;0?5DVpUjssM&_@G8lVs9cU$gv!kdyZzrcVcR;Coop_X22dmhbA5&gb|!Z-cJM
zlg<-jw2Rg!5sp+pK6(#8J2~d!&-YW~v~=rmI0(Mg+HSY62#Lya4%oKN1x}T6~VzNN-34eYV`w!V;E*x+{JJJ0000$D
literal 0
HcmV?d00001
diff --git a/Resources/Textures/_CorvaxNext/Interface/VerbIcons/bell_muted.png b/Resources/Textures/_CorvaxNext/Interface/VerbIcons/bell_muted.png
new file mode 100644
index 0000000000000000000000000000000000000000..0d0ba804285d5fef5ad17207cc298fe222c9bd4d
GIT binary patch
literal 355
zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=jKx9jP7LeL$-D$|SkfJR9T^xl
z_H+M9WCij$3p^r=85sBugD~Uq{1quc!5^M3jv*CsZ*Lmr9SRU(4J>mx{Lg%Xnst%i
zM{f4N0vYq4cTH4H7B_C4_~GrI!_SXgx2o;6JsGck$nDRWHyqLrWs8+_QvN5Uwe-Hw
ze&(w1^{>iLc~=L)02UEOR|`4*_Qd267B|;~E5?l>f*zt^;^B__<%`y9Fm*L3Enrs$
zksJ^XE{e_V$CvxhCa!zJ<4x-pbo9|L_Q-Q2qkv*}yPh@O1TaS?83{1OU}*
Bg^K_H
literal 0
HcmV?d00001
diff --git a/Resources/Textures/_CorvaxNext/Misc/program_icons.rsi/meta.json b/Resources/Textures/_CorvaxNext/Misc/program_icons.rsi/meta.json
new file mode 100644
index 00000000000..935cb557bb1
--- /dev/null
+++ b/Resources/Textures/_CorvaxNext/Misc/program_icons.rsi/meta.json
@@ -0,0 +1,14 @@
+{
+ "version": 1,
+ "license": "CC0-1.0",
+ "copyright": "nanochat made by kushbreth (discord)",
+ "size": {
+ "x": 32,
+ "y": 32
+ },
+ "states": [
+ {
+ "name": "nanochat"
+ }
+ ]
+}
diff --git a/Resources/Textures/_CorvaxNext/Misc/program_icons.rsi/nanochat.png b/Resources/Textures/_CorvaxNext/Misc/program_icons.rsi/nanochat.png
new file mode 100644
index 0000000000000000000000000000000000000000..2126d34cfa157afe67c005348652597f62a59526
GIT binary patch
literal 725
zcmV;`0xJE9P)YAX9X8WNB|8RBvx=!KdMT0006;
zNkl+^`==`S%<0WV6KvZ?L4p9W;4(;RJ_exzgo4XJQZX>QZD9+G+1)G-
z!1tQ~R?Ai6o}Zt;;pFz
zSfRqH#=3*I`RMR%Vq?-Eu?*gD@gRfkQw%YU$`i`4!lOEvL
z^j@j|Zo<*!7(lxaQN{HD+W$ONigQGjaOcC0+Anwjplw8=r#g^IGl?5d^T4EH-~d#BoPK}590G`+3%V-%T)-hVl?gK6T1PYBeVL%thYqM&htt@}H)nqB
z)3F5U5>(r7O>f|kTdmaYCmR3&01jnXNoGw=04e|g00;m8000000Mb*F00000NkvXX
Hu0mjf{AWJA
literal 0
HcmV?d00001
diff --git a/Resources/Textures/_CorvaxNext/Objects/Devices/cartridge.rsi/cart-chat.png b/Resources/Textures/_CorvaxNext/Objects/Devices/cartridge.rsi/cart-chat.png
new file mode 100644
index 0000000000000000000000000000000000000000..80a921c94de591d266043d1f631e1f9c91b505f1
GIT binary patch
literal 419
zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdz&H|6fVg?3oArNM~bhqvgP*A$W
zHKHUqKdq!Zu_%?nIW?!avREOwq%*Rm@w_Lw2+fVX5qs#$DnFsLoWNJ
z&s|^rj&bhNx2%ua&aJ_gePr`bCI%1nmtqHExDHhww_Bi@uMoy~maBpPNZO=Ari66X
zvx0WV%6XPAmVdrh_Mu{??1SHSL8s!MZ@m^0dR5PX!%?Em`n0`r*P;Uw#Wo8Z-(1}@
zF>GJb?Lr3j0tR*q%LlR+85|d+#4G;;gOM*a!ZXd+mq822<^W<4xD-4YM0vXUxvX
Date: Thu, 12 Dec 2024 17:46:11 +0300
Subject: [PATCH 2/2] fixes after testing
---
Resources/Prototypes/Entities/Objects/Devices/pda.yml | 1 -
1 file changed, 1 deletion(-)
diff --git a/Resources/Prototypes/Entities/Objects/Devices/pda.yml b/Resources/Prototypes/Entities/Objects/Devices/pda.yml
index 10e834d7cd1..a66209b9faa 100644
--- a/Resources/Prototypes/Entities/Objects/Devices/pda.yml
+++ b/Resources/Prototypes/Entities/Objects/Devices/pda.yml
@@ -782,7 +782,6 @@
- WantedListCartridge
- MedTekCartridge
- AstroNavCartridge
- - SecWatchCartridge # Corvax-Next-SecWatch
- NanoChatCartridge # Corvax-Next-PDAChat
- type: entity