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..ac610343682
--- /dev/null
+++ b/Content.Client/_CorvaxNext/CartridgeLoader/Cartridges/NanoChatEntry.xaml
@@ -0,0 +1,48 @@
+
+
+
\ No newline at end of file
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..a055c04879d
--- /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;
+ }
+}
\ No newline at end of file
diff --git a/Content.Client/_CorvaxNext/CartridgeLoader/Cartridges/NanoChatLogEntry.xaml b/Content.Client/_CorvaxNext/CartridgeLoader/Cartridges/NanoChatLogEntry.xaml
new file mode 100644
index 00000000000..d3014fd337f
--- /dev/null
+++ b/Content.Client/_CorvaxNext/CartridgeLoader/Cartridges/NanoChatLogEntry.xaml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
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..50aadc4b62c
--- /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;
+ }
+}
\ No newline at end of file
diff --git a/Content.Client/_CorvaxNext/CartridgeLoader/Cartridges/NanoChatMessageBubble.xaml b/Content.Client/_CorvaxNext/CartridgeLoader/Cartridges/NanoChatMessageBubble.xaml
new file mode 100644
index 00000000000..9b2390b6ae0
--- /dev/null
+++ b/Content.Client/_CorvaxNext/CartridgeLoader/Cartridges/NanoChatMessageBubble.xaml
@@ -0,0 +1,55 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
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..91eb41c7751
--- /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);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Content.Client/_CorvaxNext/CartridgeLoader/Cartridges/NanoChatUi.cs b/Content.Client/_CorvaxNext/CartridgeLoader/Cartridges/NanoChatUi.cs
new file mode 100644
index 00000000000..412ce4191e2
--- /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);
+ }
+}
\ No newline at end of file
diff --git a/Content.Client/_CorvaxNext/CartridgeLoader/Cartridges/NanoChatUiFragment.xaml b/Content.Client/_CorvaxNext/CartridgeLoader/Cartridges/NanoChatUiFragment.xaml
new file mode 100644
index 00000000000..e20595a6974
--- /dev/null
+++ b/Content.Client/_CorvaxNext/CartridgeLoader/Cartridges/NanoChatUiFragment.xaml
@@ -0,0 +1,167 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
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..ad3d62f0447
--- /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);
+ }
+}
\ No newline at end of file
diff --git a/Content.Client/_CorvaxNext/CartridgeLoader/Cartridges/NewChatPopup.xaml b/Content.Client/_CorvaxNext/CartridgeLoader/Cartridges/NewChatPopup.xaml
new file mode 100644
index 00000000000..3233ec11b21
--- /dev/null
+++ b/Content.Client/_CorvaxNext/CartridgeLoader/Cartridges/NewChatPopup.xaml
@@ -0,0 +1,52 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
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..65a1af7675d
--- /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();
+ }
+}
\ No newline at end of file
diff --git a/Content.Client/_CorvaxNext/NanoChat/NanoChatSystem.cs b/Content.Client/_CorvaxNext/NanoChat/NanoChatSystem.cs
new file mode 100644
index 00000000000..f9b33334de3
--- /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;
\ No newline at end of file
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..bd3b33836e6
--- /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);
+ }
+}
\ No newline at end of file
diff --git a/Content.Server/_CorvaxNext/CartridgeLoader/Cartridges/NanoChatCartridgeComponent.cs b/Content.Server/_CorvaxNext/CartridgeLoader/Cartridges/NanoChatCartridgeComponent.cs
new file mode 100644
index 00000000000..e424363bc3a
--- /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";
+}
\ No newline at end of file
diff --git a/Content.Server/_CorvaxNext/CartridgeLoader/Cartridges/NanoChatCartridgeSystem.cs b/Content.Server/_CorvaxNext/CartridgeLoader/Cartridges/NanoChatCartridgeSystem.cs
new file mode 100644
index 00000000000..aeff1d50c49
--- /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);
+ }
+}
\ No newline at end of file
diff --git a/Content.Server/_CorvaxNext/NanoChat/NanoChatSystem.cs b/Content.Server/_CorvaxNext/NanoChat/NanoChatSystem.cs
new file mode 100644
index 00000000000..14d1c8f801d
--- /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);
+ }
+}
\ No newline at end of file
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..6fd5e47b29e
--- /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);
\ No newline at end of file
diff --git a/Content.Shared/_CorvaxNext/CartridgeLoader/Cartridges/NanoChatUiState.cs b/Content.Shared/_CorvaxNext/CartridgeLoader/Cartridges/NanoChatUiState.cs
new file mode 100644
index 00000000000..adc434b2b1a
--- /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;
+ }
+}
\ No newline at end of file
diff --git a/Content.Shared/_CorvaxNext/NanoChat/NanoChatCardComponent.cs b/Content.Shared/_CorvaxNext/NanoChat/NanoChatCardComponent.cs
new file mode 100644
index 00000000000..17e2594878d
--- /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;
+}
\ No newline at end of file
diff --git a/Content.Shared/_CorvaxNext/NanoChat/SharedNanoChatSystem.cs b/Content.Shared/_CorvaxNext/NanoChat/SharedNanoChatSystem.cs
new file mode 100644
index 00000000000..f0f42059305
--- /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
+}
\ No newline at end of file
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..7e9d7e992c3
--- /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
\ No newline at end of file
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..769a1a9bb97
--- /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}
\ No newline at end of file
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..673c7187a60
--- /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!
\ No newline at end of file
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..333a25dfca3
--- /dev/null
+++ b/Resources/Locale/ru-RU/_corvaxnext/access/components/agent-id-card-component.ftl
@@ -0,0 +1 @@
+agent-id-card-current-number = Номер NanoChat
\ No newline at end of file
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..1a04f47467f
--- /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}
\ No newline at end of file
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..de60c61202c
--- /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} потрескивает, пока все сообщения превращаются в кашу!
\ No newline at end of file
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..a420b78bdf0 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
\ No newline at end of file
diff --git a/Resources/Prototypes/_CorvaxNext/name_identifier_groups.yml b/Resources/Prototypes/_CorvaxNext/name_identifier_groups.yml
new file mode 100644
index 00000000000..179220a1329
--- /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
\ No newline at end of file
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 00000000000..7fada9e5afd
Binary files /dev/null and b/Resources/Textures/_CorvaxNext/Interface/VerbIcons/bell.svg.png differ
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 00000000000..0d0ba804285
Binary files /dev/null and b/Resources/Textures/_CorvaxNext/Interface/VerbIcons/bell_muted.png differ
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..fe425ba8ae9
--- /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"
+ }
+ ]
+}
\ No newline at end of file
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 00000000000..2126d34cfa1
Binary files /dev/null and b/Resources/Textures/_CorvaxNext/Misc/program_icons.rsi/nanochat.png differ
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 00000000000..80a921c94de
Binary files /dev/null and b/Resources/Textures/_CorvaxNext/Objects/Devices/cartridge.rsi/cart-chat.png differ
diff --git a/Resources/Textures/_CorvaxNext/Objects/Devices/cartridge.rsi/meta.json b/Resources/Textures/_CorvaxNext/Objects/Devices/cartridge.rsi/meta.json
index 0eb27fde855..ebb40e495ca 100644
--- a/Resources/Textures/_CorvaxNext/Objects/Devices/cartridge.rsi/meta.json
+++ b/Resources/Textures/_CorvaxNext/Objects/Devices/cartridge.rsi/meta.json
@@ -1,7 +1,7 @@
{
"version": 1,
"license": "CC-BY-SA-3.0",
- "copyright": "Timfa, plus edits by portfiend",
+ "copyright": "cart-chat made by kushbreth (discord). cart-cri made by Timfa, plus edits by portfiend",
"size": {
"x": 32,
"y": 32
@@ -9,6 +9,9 @@
"states": [
{
"name": "cart-cri"
+ },
+ {
+ "name": "cart-chat"
}
]
}
\ No newline at end of file