pda messaging real (#2362)

* the shiny new toy that breaks everything else

* privacy 😌

* janky shit

* real??

* :trollface:

* ALL HAIL DUCT TAPE SOLUTION

* FUCK

* commented every shit
This commit is contained in:
Milon 2024-12-10 21:33:58 +01:00 committed by GitHub
parent 140fc65249
commit 2c6517953a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
48 changed files with 2520 additions and 21 deletions

View File

@ -26,6 +26,13 @@ namespace Content.Client.Access.UI
_window.OnNameChanged += OnNameChanged; _window.OnNameChanged += OnNameChanged;
_window.OnJobChanged += OnJobChanged; _window.OnJobChanged += OnJobChanged;
_window.OnJobIconChanged += OnJobIconChanged; _window.OnJobIconChanged += OnJobIconChanged;
_window.OnNumberChanged += OnNumberChanged; // DeltaV
}
// DeltaV - Add number change handler
private void OnNumberChanged(uint newNumber)
{
SendMessage(new AgentIDCardNumberChangedMessage(newNumber));
} }
private void OnNameChanged(string newName) private void OnNameChanged(string newName)
@ -56,6 +63,7 @@ namespace Content.Client.Access.UI
_window.SetCurrentName(cast.CurrentName); _window.SetCurrentName(cast.CurrentName);
_window.SetCurrentJob(cast.CurrentJob); _window.SetCurrentJob(cast.CurrentJob);
_window.SetAllowedIcons(cast.CurrentJobIconId); _window.SetAllowedIcons(cast.CurrentJobIconId);
_window.SetCurrentNumber(cast.CurrentNumber); // DeltaV
} }
} }
} }

View File

@ -6,6 +6,10 @@
<LineEdit Name="NameLineEdit" /> <LineEdit Name="NameLineEdit" />
<Label Name="CurrentJob" Text="{Loc 'agent-id-card-current-job'}" /> <Label Name="CurrentJob" Text="{Loc 'agent-id-card-current-job'}" />
<LineEdit Name="JobLineEdit" /> <LineEdit Name="JobLineEdit" />
<!-- DeltaV - Add NanoChat number field -->
<Label Name="CurrentNumber" Text="{Loc 'agent-id-card-current-number'}" />
<LineEdit Name="NumberLineEdit" PlaceHolder="#0000" />
<!-- DeltaV end -->
<Label Text="{Loc 'agent-id-card-job-icon-label'}"/> <Label Text="{Loc 'agent-id-card-job-icon-label'}"/>
<GridContainer Name="IconGrid" Columns="10"> <GridContainer Name="IconGrid" Columns="10">
<!-- Job icon buttons are generated in the code --> <!-- Job icon buttons are generated in the code -->

View File

@ -21,9 +21,13 @@ namespace Content.Client.Access.UI
private const int JobIconColumnCount = 10; private const int JobIconColumnCount = 10;
private const int MaxNumberLength = 4; // DeltaV - Same as NewChatPopup
public event Action<string>? OnNameChanged; public event Action<string>? OnNameChanged;
public event Action<string>? OnJobChanged; public event Action<string>? OnJobChanged;
public event Action<uint>? OnNumberChanged; // DeltaV - Add event for number changes
public event Action<ProtoId<JobIconPrototype>>? OnJobIconChanged; public event Action<ProtoId<JobIconPrototype>>? OnJobIconChanged;
public AgentIDCardWindow() public AgentIDCardWindow()
@ -37,6 +41,37 @@ namespace Content.Client.Access.UI
JobLineEdit.OnTextEntered += e => OnJobChanged?.Invoke(e.Text); JobLineEdit.OnTextEntered += e => OnJobChanged?.Invoke(e.Text);
JobLineEdit.OnFocusExit += e => OnJobChanged?.Invoke(e.Text); JobLineEdit.OnFocusExit += e => OnJobChanged?.Invoke(e.Text);
// DeltaV - Add handlers for number changes
NumberLineEdit.OnTextEntered += OnNumberEntered;
NumberLineEdit.OnFocusExit += OnNumberEntered;
// DeltaV - 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;
};
}
// DeltaV - Add number validation and event
private void OnNumberEntered(LineEdit.LineEditEventArgs args)
{
if (uint.TryParse(args.Text, out var number) && number > 0)
OnNumberChanged?.Invoke(number);
}
// DeltaV - Add setter for current number
public void SetCurrentNumber(uint? number)
{
NumberLineEdit.Text = number?.ToString("D4") ?? "";
} }
public void SetAllowedIcons(string currentJobIconId) public void SetAllowedIcons(string currentJobIconId)

View File

@ -23,6 +23,6 @@ public sealed partial class LogProbeUi : UIFragment
if (state is not LogProbeUiState logProbeUiState) if (state is not LogProbeUiState logProbeUiState)
return; return;
_fragment?.UpdateState(logProbeUiState.PulledLogs); _fragment?.UpdateState(logProbeUiState); // DeltaV - just take the state
} }
} }

View File

@ -9,10 +9,30 @@
BorderColor="#5a5a5a" BorderColor="#5a5a5a"
BorderThickness="0 0 0 1"/> BorderThickness="0 0 0 1"/>
</PanelContainer.PanelOverride> </PanelContainer.PanelOverride>
<BoxContainer Orientation="Horizontal" Margin="4 8"> <BoxContainer Orientation="Vertical" Margin="4 8">
<Label Align="Right" SetWidth="26" ClipText="True" Text="{Loc 'log-probe-label-number'}"/> <!-- DeltaV begin - Add title label -->
<Label Align="Center" SetWidth="100" ClipText="True" Text="{Loc 'log-probe-label-time'}"/> <Label Name="TitleLabel"
<Label Align="Left" SetWidth="390" ClipText="True" Text="{Loc 'log-probe-label-accessor'}"/> Text="{Loc 'log-probe-header-access'}"
StyleClasses="LabelHeading"
HorizontalAlignment="Center"
Margin="0 0 0 8"/>
<!-- DeltaV end -->
<!-- DeltaV begin - Add card number display -->
<Label Name="CardNumberLabel"
StyleClasses="LabelSubText"
HorizontalAlignment="Center"
Margin="0 0 0 8"
Visible="False"/>
<!-- DeltaV end -->
<!-- DeltaV begin - Adjust column headers -->
<BoxContainer Orientation="Horizontal">
<Label Align="Right" SetWidth="26" ClipText="True" Text="{Loc 'log-probe-label-number'}"/>
<Label Align="Center" SetWidth="100" ClipText="True" Text="{Loc 'log-probe-label-time'}"/>
<Label Name="ContentLabel" Align="Left" SetWidth="390" ClipText="True" Text="{Loc 'log-probe-label-accessor'}"/>
</BoxContainer>
<!-- DeltaV end -->
</BoxContainer> </BoxContainer>
</PanelContainer> </PanelContainer>
<ScrollContainer VerticalExpand="True" HScrollEnabled="True"> <ScrollContainer VerticalExpand="True" HScrollEnabled="True">

View File

@ -1,4 +1,7 @@
using Content.Shared.CartridgeLoader.Cartridges; using System.Linq; // DeltaV
using Content.Client.DeltaV.CartridgeLoader.Cartridges; // DeltaV
using Content.Shared.CartridgeLoader.Cartridges;
using Content.Shared.DeltaV.CartridgeLoader.Cartridges; // DeltaV
using Robust.Client.AutoGenerated; using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML; using Robust.Client.UserInterface.XAML;
@ -13,10 +16,112 @@ public sealed partial class LogProbeUiFragment : BoxContainer
RobustXamlLoader.Load(this); RobustXamlLoader.Load(this);
} }
public void UpdateState(List<PulledAccessLog> logs) // DeltaV begin - Update to handle both types of data
public void UpdateState(LogProbeUiState state)
{ {
ProbedDeviceContainer.RemoveAllChildren(); 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<string>();
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++;
}
}
}
// DeltaV end
// DeltaV - Handle this in a separate method
private void DisplayAccessLogs(List<PulledAccessLog> logs)
{
//Reverse the list so the oldest entries appear at the bottom //Reverse the list so the oldest entries appear at the bottom
logs.Reverse(); logs.Reverse();

View File

@ -0,0 +1,48 @@
<BoxContainer
xmlns="https://spacestation14.io"
xmlns:graphics="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
HorizontalExpand="True">
<Button Name="ChatButton"
StyleClasses="ButtonSquare"
HorizontalExpand="True"
MaxSize="137 64"
Margin="0 1">
<BoxContainer Orientation="Horizontal"
HorizontalExpand="True"
VerticalExpand="True"
MinWidth="132"
Margin="6 4"
VerticalAlignment="Center">
<!-- Unread indicator dot -->
<PanelContainer Name="UnreadIndicator"
MinSize="8 8"
MaxSize="8 8"
VerticalAlignment="Center"
Margin="0 0 6 0">
<PanelContainer.PanelOverride>
<graphics:StyleBoxFlat
BackgroundColor="#17c622"
BorderColor="#0f7a15" />
</PanelContainer.PanelOverride>
</PanelContainer>
<!-- Text container -->
<BoxContainer Orientation="Vertical"
HorizontalExpand="True"
VerticalExpand="True"
VerticalAlignment="Center">
<RichTextLabel Name="NameLabel"
StyleClasses="LabelHeading"
HorizontalExpand="True"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Margin="0 -2 0 0" />
<Label Name="JobLabel"
StyleClasses="LabelSubText"
HorizontalExpand="True"
ClipText="False"
HorizontalAlignment="Center" />
</BoxContainer>
</BoxContainer>
</Button>
</BoxContainer>

View File

@ -0,0 +1,39 @@
using Content.Shared.DeltaV.CartridgeLoader.Cartridges;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
namespace Content.Client.DeltaV.CartridgeLoader.Cartridges;
[GenerateTypedNameReferences]
public sealed partial class NanoChatEntry : BoxContainer
{
public event Action<uint>? OnPressed;
private uint _number;
private Action<EventArgs>? _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;
}
}

View File

@ -0,0 +1,21 @@
<BoxContainer xmlns="https://spacestation14.io"
xmlns:customControls="clr-namespace:Content.Client.Administration.UI.CustomControls"
Margin="4"
Orientation="Vertical">
<BoxContainer Orientation="Horizontal">
<Label Name="NumberLabel"
Align="Right"
SetWidth="26"
ClipText="True" />
<Label Name="TimeLabel"
Align="Center"
SetWidth="100"
ClipText="True" />
<Label Name="MessageLabel"
Align="Left"
MinWidth="390"
HorizontalExpand="True"
ClipText="False" />
</BoxContainer>
<customControls:HSeparator Margin="0 5 0 5" />
</BoxContainer>

View File

@ -0,0 +1,17 @@
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
namespace Content.Client.DeltaV.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;
}
}

View File

@ -0,0 +1,55 @@
<cartridges:NanoChatMessageBubble
xmlns="https://spacestation14.io"
xmlns:cartridges="clr-namespace:Content.Client.DeltaV.CartridgeLoader.Cartridges"
xmlns:graphics="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
HorizontalExpand="True">
<BoxContainer Name="MessageContainer"
Orientation="Horizontal"
HorizontalExpand="True">
<!-- Left spacer for other's messages -->
<Control Name="LeftSpacer"
MinSize="12 0" />
<!-- Message panel -->
<BoxContainer Name="MessageBox"
Orientation="Vertical"
MaxWidth="320"
HorizontalExpand="True">
<PanelContainer Name="MessagePanel"
MaxWidth="320"
HorizontalExpand="True">
<PanelContainer.PanelOverride>
<graphics:StyleBoxFlat
ContentMarginLeftOverride="10"
ContentMarginRightOverride="10"
ContentMarginTopOverride="6"
ContentMarginBottomOverride="6"
BorderThickness="1">
<!-- Colors set in code based on message sender -->
</graphics:StyleBoxFlat>
</PanelContainer.PanelOverride>
<RichTextLabel Name="MessageText"
HorizontalExpand="True" />
</PanelContainer>
<!-- Delivery failed text -->
<Label Name="DeliveryFailedLabel"
Text="{Loc nano-chat-delivery-failed}"
StyleClasses="LabelSmall"
HorizontalExpand="True"
HorizontalAlignment="Right"
Margin="10 2 10 0"
Visible="False" />
</BoxContainer>
<!-- Right spacer for own messages -->
<Control Name="RightSpacer"
MinSize="12 0" />
<!-- Flexible space for alignment -->
<Control Name="FlexSpace"
HorizontalExpand="True" />
</BoxContainer>
</cartridges:NanoChatMessageBubble>

View File

@ -0,0 +1,62 @@
using Content.Shared.DeltaV.CartridgeLoader.Cartridges;
using Robust.Client.AutoGenerated;
using Robust.Client.Graphics;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
namespace Content.Client.DeltaV.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);
}
}
}

View File

@ -0,0 +1,43 @@
using Content.Client.UserInterface.Fragments;
using Content.Shared.CartridgeLoader;
using Content.Shared.DeltaV.CartridgeLoader.Cartridges;
using Robust.Client.UserInterface;
namespace Content.Client.DeltaV.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);
}
}

View File

@ -0,0 +1,167 @@
<cartridges:NanoChatUiFragment
xmlns="https://spacestation14.io"
xmlns:cartridges="clr-namespace:Content.Client.DeltaV.CartridgeLoader.Cartridges"
xmlns:customControls="clr-namespace:Content.Client.Administration.UI.CustomControls"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
Orientation="Vertical"
HorizontalExpand="True"
VerticalExpand="True"
Margin="5">
<!-- Main container that fills the entire PDA screen -->
<BoxContainer Orientation="Vertical"
HorizontalExpand="True"
VerticalExpand="True">
<!-- Header with app title and new chat button -->
<controls:StripeBack MinSize="48 48"
VerticalExpand="False">
<BoxContainer Orientation="Horizontal"
HorizontalExpand="True"
Margin="0">
<TextureRect Name="AppIcon"
TexturePath="/Textures/Interface/Nano/ntlogo.svg.png"
Stretch="KeepAspectCentered"
VerticalAlignment="Center"
MinSize="32 32"
Margin="8 0 0 0" />
<Label Text="{Loc nano-chat-title}"
StyleClasses="LabelHeading"
HorizontalExpand="True"
Margin="8 0"
VerticalAlignment="Center" />
<Label Name="OwnNumberLabel"
Text="#0000"
StyleClasses="LabelSubText"
VerticalAlignment="Center"
Margin="0 0 8 0" />
<Button Name="DeleteChatButton"
MaxSize="32 32"
Visible="False"
StyleClasses="OpenBoth"
Margin="0 0 4 0"
ToolTip="{Loc nano-chat-delete}">
<TextureRect StyleClasses="ButtonSquare"
TexturePath="/Textures/Interface/VerbIcons/delete_transparent.svg.192dpi.png"
Stretch="KeepAspectCentered"
MinSize="18 18" />
</Button>
<Button Name="MuteButton"
MaxSize="32 32"
StyleClasses="OpenBoth"
Margin="0 0 4 0"
ToolTip="{Loc nano-chat-toggle-mute}">
<Control HorizontalExpand="True" VerticalExpand="True">
<TextureRect Name="BellIcon"
StyleClasses="ButtonSquare"
TexturePath="/Textures/DeltaV/Interface/VerbIcons/bell.svg.png"
Stretch="KeepAspectCentered"
MinSize="18 18" />
<TextureRect Name="BellMutedIcon"
StyleClasses="ButtonSquare"
TexturePath="/Textures/DeltaV/Interface/VerbIcons/bell_muted.png"
Stretch="KeepAspectCentered"
Visible="False"
MinSize="18 18" />
</Control>
</Button>
<Button Name="NewChatButton"
Text="+"
MinSize="32 32"
MaxSize="32 32"
Margin="0 0 4 0"
StyleClasses="OpenBoth"
ToolTip="{Loc nano-chat-new-chat}" />
</BoxContainer>
</controls:StripeBack>
<!-- Main content split -->
<BoxContainer Orientation="Horizontal"
VerticalExpand="True"
HorizontalExpand="True"
Margin="0 5 0 0">
<!-- Left panel: Chat list -->
<PanelContainer StyleClasses="AngleRect"
VerticalExpand="True"
MaxWidth="150">
<ScrollContainer VerticalExpand="True"
MinWidth="145"
HorizontalExpand="True"
HScrollEnabled="False">
<BoxContainer Name="ChatList"
Orientation="Vertical"
VerticalExpand="True"
HorizontalExpand="True"
Margin="4">
<!-- Chat entries will be added here dynamically -->
<Label Name="NoChatsLabel"
Text="{Loc nano-chat-no-chats}"
HorizontalAlignment="Center"
VerticalAlignment="Center"
StyleClasses="LabelSubText" />
</BoxContainer>
</ScrollContainer>
</PanelContainer>
<customControls:VSeparator Margin="3 0" />
<!-- Right panel: Current chat -->
<PanelContainer StyleClasses="AngleRect"
VerticalExpand="True"
HorizontalExpand="True">
<BoxContainer Orientation="Vertical"
VerticalExpand="True"
HorizontalExpand="True">
<!-- Messages area with centered "select chat" label -->
<BoxContainer Name="MessageArea"
Orientation="Vertical"
VerticalExpand="True"
HorizontalExpand="True"
Margin="0 0 0 4">
<Label Name="CurrentChatName"
Text="{Loc nano-chat-select-chat}"
HorizontalAlignment="Center"
VerticalAlignment="Center"
VerticalExpand="True" />
<ScrollContainer Name="MessagesScroll"
VerticalExpand="True"
HorizontalExpand="True"
Visible="False">
<BoxContainer Name="MessageList"
Orientation="Vertical"
VerticalExpand="True"
HorizontalExpand="True" />
</ScrollContainer>
</BoxContainer>
<!-- Message input -->
<BoxContainer Name="MessageInputContainer"
Orientation="Horizontal"
HorizontalExpand="True"
Margin="0 4 0 0"
Visible="False">
<!-- Character count -->
<Label Name="CharacterCount"
HorizontalAlignment="Right"
StyleClasses="LabelSubText"
Margin="0 0 4 2"
Visible="False" />
<!-- Input row -->
<LineEdit Name="MessageInput"
PlaceHolder="{Loc nano-chat-message-placeholder}"
HorizontalExpand="True"
StyleClasses="OpenRight" />
<Button Name="SendButton"
MinSize="32 32"
Disabled="True"
StyleClasses="OpenLeft"
Margin="4 0 0 0">
<TextureRect StyleClasses="ButtonSquare"
TexturePath="/Textures/Interface/Nano/triangle_right.png"
Stretch="KeepAspectCentered" />
</Button>
</BoxContainer>
</BoxContainer>
</PanelContainer>
</BoxContainer>
</BoxContainer>
</cartridges:NanoChatUiFragment>

View File

@ -0,0 +1,254 @@
using System.Linq;
using System.Numerics;
using Content.Shared.DeltaV.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.DeltaV.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<uint, NanoChatRecipient> _recipients = new();
private Dictionary<uint, List<NanoChatMessage>> _messages = new();
public event Action<NanoChatUiMessageType, uint?, string?, string?>? 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<NanoChatMessage>();
_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<uint, NanoChatRecipient> 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<uint, List<NanoChatMessage>> 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);
}
}

View File

@ -0,0 +1,52 @@
<DefaultWindow xmlns="https://spacestation14.io"
Title="{Loc nano-chat-new-title}"
MinSize="300 200">
<PanelContainer StyleClasses="AngleRect">
<BoxContainer Orientation="Vertical" Margin="4">
<!-- Number input -->
<BoxContainer Orientation="Vertical" Margin="0 4">
<Label Text="{Loc nano-chat-number-label}"
StyleClasses="LabelHeading" />
<PanelContainer StyleClasses="ButtonSquare">
<LineEdit Name="NumberInput"
PlaceHolder="{Loc nano-chat-number-placeholder}" />
</PanelContainer>
</BoxContainer>
<!-- Name input -->
<BoxContainer Orientation="Vertical" Margin="0 4">
<Label Text="{Loc nano-chat-name-label}"
StyleClasses="LabelHeading" />
<PanelContainer StyleClasses="ButtonSquare">
<LineEdit Name="NameInput"
PlaceHolder="{Loc nano-chat-name-placeholder}" />
</PanelContainer>
</BoxContainer>
<!-- Job input (optional) -->
<BoxContainer Orientation="Vertical" Margin="0 4">
<Label Text="{Loc nano-chat-job-label}"
StyleClasses="LabelHeading" />
<PanelContainer StyleClasses="ButtonSquare">
<LineEdit Name="JobInput"
PlaceHolder="{Loc nano-chat-job-placeholder}" />
</PanelContainer>
</BoxContainer>
<!-- Action buttons -->
<BoxContainer Orientation="Horizontal"
HorizontalAlignment="Right"
Margin="0 8 0 0">
<Button Name="CancelButton"
Text="{Loc nano-chat-cancel}"
StyleClasses="OpenRight"
MinSize="80 0" />
<Button Name="CreateButton"
Text="{Loc nano-chat-create}"
StyleClasses="OpenLeft"
MinSize="80 0"
Disabled="True" />
</BoxContainer>
</BoxContainer>
</PanelContainer>
</DefaultWindow>

View File

@ -0,0 +1,87 @@
using System.Linq;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
namespace Content.Client.DeltaV.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<uint, string, string?>? 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();
}
}

View File

@ -0,0 +1,5 @@
using Content.Shared.DeltaV.NanoChat;
namespace Content.Client.DeltaV.NanoChat;
public sealed class NanoChatSystem : SharedNanoChatSystem;

View File

@ -9,6 +9,7 @@ using Robust.Server.GameObjects;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
using Content.Shared.Roles; using Content.Shared.Roles;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using Content.Shared.DeltaV.NanoChat; // DeltaV
namespace Content.Server.Access.Systems namespace Content.Server.Access.Systems
{ {
@ -18,6 +19,7 @@ namespace Content.Server.Access.Systems
[Dependency] private readonly IdCardSystem _cardSystem = default!; [Dependency] private readonly IdCardSystem _cardSystem = default!;
[Dependency] private readonly UserInterfaceSystem _uiSystem = default!; [Dependency] private readonly UserInterfaceSystem _uiSystem = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly SharedNanoChatSystem _nanoChat = default!; // DeltaV
public override void Initialize() public override void Initialize()
{ {
@ -28,6 +30,17 @@ namespace Content.Server.Access.Systems
SubscribeLocalEvent<AgentIDCardComponent, AgentIDCardNameChangedMessage>(OnNameChanged); SubscribeLocalEvent<AgentIDCardComponent, AgentIDCardNameChangedMessage>(OnNameChanged);
SubscribeLocalEvent<AgentIDCardComponent, AgentIDCardJobChangedMessage>(OnJobChanged); SubscribeLocalEvent<AgentIDCardComponent, AgentIDCardJobChangedMessage>(OnJobChanged);
SubscribeLocalEvent<AgentIDCardComponent, AgentIDCardJobIconChangedMessage>(OnJobIconChanged); SubscribeLocalEvent<AgentIDCardComponent, AgentIDCardJobIconChangedMessage>(OnJobIconChanged);
SubscribeLocalEvent<AgentIDCardComponent, AgentIDCardNumberChangedMessage>(OnNumberChanged); // DeltaV
}
// DeltaV - Add number change handler
private void OnNumberChanged(Entity<AgentIDCardComponent> ent, ref AgentIDCardNumberChangedMessage args)
{
if (!TryComp<NanoChatCardComponent>(ent, out var comp))
return;
_nanoChat.SetNumber((ent, comp), args.Number);
Dirty(ent, comp);
} }
private void OnAfterInteract(EntityUid uid, AgentIDCardComponent component, AfterInteractEvent args) private void OnAfterInteract(EntityUid uid, AgentIDCardComponent component, AfterInteractEvent args)
@ -42,6 +55,34 @@ namespace Content.Server.Access.Systems
access.Tags.UnionWith(targetAccess.Tags); access.Tags.UnionWith(targetAccess.Tags);
var addedLength = access.Tags.Count - beforeLength; var addedLength = access.Tags.Count - beforeLength;
// DeltaV - Copy NanoChat data if available
if (TryComp<NanoChatCardComponent>(args.Target, out var targetNanoChat) &&
TryComp<NanoChatCardComponent>(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);
}
}
}
// End DeltaV
if (addedLength == 0) if (addedLength == 0)
{ {
_popupSystem.PopupEntity(Loc.GetString("agent-id-no-new", ("card", args.Target)), args.Target.Value, args.User); _popupSystem.PopupEntity(Loc.GetString("agent-id-no-new", ("card", args.Target)), args.Target.Value, args.User);
@ -67,7 +108,17 @@ namespace Content.Server.Access.Systems
if (!TryComp<IdCardComponent>(uid, out var idCard)) if (!TryComp<IdCardComponent>(uid, out var idCard))
return; return;
var state = new AgentIDCardBoundUserInterfaceState(idCard.FullName ?? "", idCard.LocalizedJobTitle ?? "", idCard.JobIcon); // DeltaV - Get current number if it exists
uint? currentNumber = null;
if (TryComp<NanoChatCardComponent>(uid, out var comp))
currentNumber = comp.Number;
var state = new AgentIDCardBoundUserInterfaceState(
idCard.FullName ?? "",
idCard.LocalizedJobTitle ?? "",
idCard.JobIcon,
currentNumber); // DeltaV - Pass current number
_uiSystem.SetUiState(uid, AgentIDCardUiKey.Key, state); _uiSystem.SetUiState(uid, AgentIDCardUiKey.Key, state);
} }

View File

@ -1,4 +1,5 @@
using Content.Shared.CartridgeLoader.Cartridges; using Content.Shared.CartridgeLoader.Cartridges;
using Content.Shared.DeltaV.CartridgeLoader.Cartridges; // DeltaV
using Robust.Shared.Audio; using Robust.Shared.Audio;
namespace Content.Server.CartridgeLoader.Cartridges; namespace Content.Server.CartridgeLoader.Cartridges;
@ -18,4 +19,10 @@ public sealed partial class LogProbeCartridgeComponent : Component
/// </summary> /// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)] [DataField, ViewVariables(VVAccess.ReadWrite)]
public SoundSpecifier SoundScan = new SoundPathSpecifier("/Audio/Machines/scan_finish.ogg"); public SoundSpecifier SoundScan = new SoundPathSpecifier("/Audio/Machines/scan_finish.ogg");
/// <summary>
/// DeltaV: The last scanned NanoChat data, if any
/// </summary>
[DataField]
public NanoChatData? ScannedNanoChatData;
} }

View File

@ -2,13 +2,14 @@ using Content.Shared.Access.Components;
using Content.Shared.Audio; using Content.Shared.Audio;
using Content.Shared.CartridgeLoader; using Content.Shared.CartridgeLoader;
using Content.Shared.CartridgeLoader.Cartridges; using Content.Shared.CartridgeLoader.Cartridges;
using Content.Shared.DeltaV.NanoChat; // DeltaV
using Content.Shared.Popups; using Content.Shared.Popups;
using Robust.Shared.Audio.Systems; using Robust.Shared.Audio.Systems;
using Robust.Shared.Random; using Robust.Shared.Random;
namespace Content.Server.CartridgeLoader.Cartridges; namespace Content.Server.CartridgeLoader.Cartridges;
public sealed class LogProbeCartridgeSystem : EntitySystem public sealed partial class LogProbeCartridgeSystem : EntitySystem // DeltaV - Made partial
{ {
[Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly CartridgeLoaderSystem? _cartridgeLoaderSystem = default!; [Dependency] private readonly CartridgeLoaderSystem? _cartridgeLoaderSystem = default!;
@ -18,6 +19,7 @@ public sealed class LogProbeCartridgeSystem : EntitySystem
public override void Initialize() public override void Initialize()
{ {
base.Initialize(); base.Initialize();
InitializeNanoChat(); // DeltaV
SubscribeLocalEvent<LogProbeCartridgeComponent, CartridgeUiReadyEvent>(OnUiReady); SubscribeLocalEvent<LogProbeCartridgeComponent, CartridgeUiReadyEvent>(OnUiReady);
SubscribeLocalEvent<LogProbeCartridgeComponent, CartridgeAfterInteractEvent>(AfterInteract); SubscribeLocalEvent<LogProbeCartridgeComponent, CartridgeAfterInteractEvent>(AfterInteract);
} }
@ -33,6 +35,15 @@ public sealed class LogProbeCartridgeSystem : EntitySystem
if (args.InteractEvent.Handled || !args.InteractEvent.CanReach || args.InteractEvent.Target is not { } target) if (args.InteractEvent.Handled || !args.InteractEvent.CanReach || args.InteractEvent.Target is not { } target)
return; return;
// DeltaV begin - Add NanoChat card scanning
if (TryComp<NanoChatCardComponent>(target, out var nanoChatCard))
{
ScanNanoChatCard(ent, args, target, nanoChatCard);
args.InteractEvent.Handled = true;
return;
}
// DeltaV end
if (!TryComp(target, out AccessReaderComponent? accessReaderComponent)) if (!TryComp(target, out AccessReaderComponent? accessReaderComponent))
return; return;
@ -41,6 +52,7 @@ public sealed class LogProbeCartridgeSystem : EntitySystem
_popupSystem.PopupCursor(Loc.GetString("log-probe-scan", ("device", target)), args.InteractEvent.User); _popupSystem.PopupCursor(Loc.GetString("log-probe-scan", ("device", target)), args.InteractEvent.User);
ent.Comp.PulledAccessLogs.Clear(); ent.Comp.PulledAccessLogs.Clear();
ent.Comp.ScannedNanoChatData = null; // DeltaV - Clear any previous NanoChat data
foreach (var accessRecord in accessReaderComponent.AccessLog) foreach (var accessRecord in accessReaderComponent.AccessLog)
{ {
@ -65,7 +77,7 @@ public sealed class LogProbeCartridgeSystem : EntitySystem
private void UpdateUiState(Entity<LogProbeCartridgeComponent> ent, EntityUid loaderUid) private void UpdateUiState(Entity<LogProbeCartridgeComponent> ent, EntityUid loaderUid)
{ {
var state = new LogProbeUiState(ent.Comp.PulledAccessLogs); var state = new LogProbeUiState(ent.Comp.PulledAccessLogs, ent.Comp.ScannedNanoChatData); // DeltaV - NanoChat support
_cartridgeLoaderSystem?.UpdateCartridgeUiState(loaderUid, state); _cartridgeLoaderSystem?.UpdateCartridgeUiState(loaderUid, state);
} }
} }

View File

@ -0,0 +1,82 @@
using Content.Shared.Audio;
using Content.Shared.CartridgeLoader;
using Content.Shared.DeltaV.CartridgeLoader.Cartridges;
using Content.Shared.DeltaV.NanoChat;
namespace Content.Server.CartridgeLoader.Cartridges;
public sealed partial class LogProbeCartridgeSystem
{
private void InitializeNanoChat()
{
SubscribeLocalEvent<NanoChatRecipientUpdatedEvent>(OnRecipientUpdated);
SubscribeLocalEvent<NanoChatMessageReceivedEvent>(OnMessageReceived);
}
private void OnRecipientUpdated(ref NanoChatRecipientUpdatedEvent args)
{
var query = EntityQueryEnumerator<LogProbeCartridgeComponent, CartridgeComponent>();
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<NanoChatCardComponent>(args.CardUid, out var card))
continue;
probe.ScannedNanoChatData = new NanoChatData(
new Dictionary<uint, NanoChatRecipient>(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<LogProbeCartridgeComponent, CartridgeComponent>();
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<NanoChatCardComponent>(args.CardUid, out var card))
continue;
probe.ScannedNanoChatData = new NanoChatData(
probe.ScannedNanoChatData.Value.Recipients,
new Dictionary<uint, List<NanoChatMessage>>(card.Messages),
card.Number,
GetNetEntity(args.CardUid));
if (cartridge.LoaderUid != null)
UpdateUiState((uid, probe), cartridge.LoaderUid.Value);
}
}
private void ScanNanoChatCard(Entity<LogProbeCartridgeComponent> 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<uint, NanoChatRecipient>(card.Recipients),
new Dictionary<uint, List<NanoChatMessage>>(card.Messages),
card.Number,
GetNetEntity(target)
);
UpdateUiState(ent, args.Loader);
}
}

View File

@ -0,0 +1,26 @@
using Content.Shared.Radio;
using Robust.Shared.Prototypes;
namespace Content.Server.DeltaV.CartridgeLoader.Cartridges;
[RegisterComponent, Access(typeof(NanoChatCartridgeSystem))]
public sealed partial class NanoChatCartridgeComponent : Component
{
/// <summary>
/// Station entity to keep track of.
/// </summary>
[DataField]
public EntityUid? Station;
/// <summary>
/// The NanoChat card to keep track of.
/// </summary>
[DataField]
public EntityUid? Card;
/// <summary>
/// The <see cref="RadioChannelPrototype" /> required to send or receive messages.
/// </summary>
[DataField]
public ProtoId<RadioChannelPrototype> RadioChannel = "Common";
}

View File

@ -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.DeltaV.CartridgeLoader.Cartridges;
using Content.Shared.DeltaV.NanoChat;
using Content.Shared.PDA;
using Content.Shared.Radio.Components;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
namespace Content.Server.DeltaV.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<NanoChatCartridgeComponent, CartridgeUiReadyEvent>(OnUiReady);
SubscribeLocalEvent<NanoChatCartridgeComponent, CartridgeMessageEvent>(OnMessage);
}
public override void Update(float frameTime)
{
base.Update(frameTime);
// Update card references for any cartridges that need it
var query = EntityQueryEnumerator<NanoChatCartridgeComponent, CartridgeComponent>();
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<PdaComponent>(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);
}
}
/// <summary>
/// Handles incoming UI messages from the NanoChat cartridge.
/// </summary>
private void OnMessage(Entity<NanoChatCartridgeComponent> 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));
}
/// <summary>
/// Gets the ID card entity associated with a PDA.
/// </summary>
/// <param name="loaderUid">The PDA entity ID</param>
/// <param name="card">Output parameter containing the found card entity and component</param>
/// <returns>True if a valid NanoChat card was found</returns>
private bool GetCardEntity(
EntityUid loaderUid,
out Entity<NanoChatCardComponent> card)
{
card = default;
// Get the PDA and check if it has an ID card
if (!TryComp<PdaComponent>(loaderUid, out var pda) ||
pda.ContainedId == null ||
!TryComp<NanoChatCardComponent>(pda.ContainedId, out var idCard))
return false;
card = (pda.ContainedId.Value, idCard);
return true;
}
/// <summary>
/// Handles creation of a new chat conversation.
/// </summary>
private void HandleNewChat(Entity<NanoChatCardComponent> 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);
}
/// <summary>
/// Handles selecting a chat conversation.
/// </summary>
private void HandleSelectChat(Entity<NanoChatCardComponent> 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 });
}
}
/// <summary>
/// Handles closing the current chat conversation.
/// </summary>
private void HandleCloseChat(Entity<NanoChatCardComponent> card)
{
_nanoChat.SetCurrentChat((card, card.Comp), null);
}
/// <summary>
/// Handles deletion of a chat conversation.
/// </summary>
private void HandleDeleteChat(Entity<NanoChatCardComponent> 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);
}
/// <summary>
/// Handles toggling notification mute state.
/// </summary>
private void HandleToggleMute(Entity<NanoChatCardComponent> card)
{
_nanoChat.SetNotificationsMuted((card, card.Comp), !_nanoChat.GetNotificationsMuted((card, card.Comp)));
UpdateUIForCard(card);
}
/// <summary>
/// Handles sending a new message in a chat conversation.
/// </summary>
private void HandleSendMessage(Entity<NanoChatCartridgeComponent> cartridge,
Entity<NanoChatCardComponent> 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);
}
}
/// <summary>
/// Ensures a recipient exists in the sender's contacts.
/// </summary>
/// <param name="card">The card to check contacts for</param>
/// <param name="recipientNumber">The recipient's number to check</param>
/// <returns>True if the recipient exists or was created successfully</returns>
private bool EnsureRecipientExists(Entity<NanoChatCardComponent> card, uint recipientNumber)
{
return _nanoChat.EnsureRecipientExists((card, card.Comp), recipientNumber, GetCardInfo(recipientNumber));
}
/// <summary>
/// Attempts to deliver a message to recipients.
/// </summary>
/// <param name="sender">The sending cartridge entity</param>
/// <param name="recipientNumber">The recipient's number</param>
/// <returns>Tuple containing delivery status and recipients if found.</returns>
private (bool failed, List<Entity<NanoChatCardComponent>> recipient) AttemptMessageDelivery(
Entity<NanoChatCartridgeComponent> 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<Entity<NanoChatCardComponent>>());
var foundRecipients = new List<Entity<NanoChatCardComponent>>();
// Find all cards with matching number
var cardQuery = EntityQueryEnumerator<NanoChatCardComponent>();
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<Entity<NanoChatCardComponent>>();
foreach (var recipient in foundRecipients)
{
// Find any cartridges that have this card
var cartridgeQuery = EntityQueryEnumerator<NanoChatCartridgeComponent, ActiveRadioComponent>();
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);
}
/// <summary>
/// Checks if there are any active telecomms servers on the given station
/// </summary>
private bool HasActiveServer(EntityUid station)
{
// I have no idea why this isn't public in the RadioSystem
var query =
EntityQueryEnumerator<TelecomServerComponent, EncryptionKeyHolderComponent, ApcPowerReceiverComponent>();
while (query.MoveNext(out var uid, out _, out _, out var power))
{
if (_station.GetOwningStation(uid) == station && power.Powered)
return true;
}
return false;
}
/// <summary>
/// Delivers a message to the recipient and handles associated notifications.
/// </summary>
/// <param name="sender">The sender's card entity</param>
/// <param name="recipient">The recipient's card entity</param>
/// <param name="message">The <see cref="NanoChatMessage" /> to deliver</param>
private void DeliverMessageToRecipient(Entity<NanoChatCardComponent> sender,
Entity<NanoChatCardComponent> 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);
}
/// <summary>
/// Handles unread message notifications and updates unread status.
/// </summary>
private void HandleUnreadNotification(Entity<NanoChatCardComponent> 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<PdaComponent>();
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 });
}
/// <summary>
/// Updates the UI for any PDAs containing the specified card.
/// </summary>
private void UpdateUIForCard(EntityUid cardUid)
{
// Find any PDA containing this card and update its UI
var query = EntityQueryEnumerator<NanoChatCartridgeComponent, CartridgeComponent>();
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);
}
}
/// <summary>
/// Gets the <see cref="NanoChatRecipient" /> for a given NanoChat number.
/// </summary>
private NanoChatRecipient? GetCardInfo(uint number)
{
// Find card with this number to get its info
var query = EntityQueryEnumerator<NanoChatCardComponent>();
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<IdCardComponent>(uid, out var idCard))
{
jobTitle = idCard.LocalizedJobTitle;
name = idCard.FullName ?? name;
}
return new NanoChatRecipient(number, name, jobTitle);
}
return null;
}
/// <summary>
/// Truncates a message to the notification maximum length.
/// </summary>
private static string TruncateMessage(string message)
{
return message.Length <= NotificationMaxLength
? message
: message[..(NotificationMaxLength - 4)] + " [...]";
}
private void OnUiReady(Entity<NanoChatCartridgeComponent> ent, ref CartridgeUiReadyEvent args)
{
_cartridge.RegisterBackgroundProgram(args.Loader, ent);
UpdateUI(ent, args.Loader);
}
private void UpdateUI(Entity<NanoChatCartridgeComponent> ent, EntityUid loader)
{
if (_station.GetOwningStation(loader) is { } station)
ent.Comp.Station = station;
var recipients = new Dictionary<uint, NanoChatRecipient>();
var messages = new Dictionary<uint, List<NanoChatMessage>>();
uint? currentChat = null;
uint ownNumber = 0;
var maxRecipients = 50;
var notificationsMuted = false;
if (ent.Comp.Card != null && TryComp<NanoChatCardComponent>(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);
}
}

View File

@ -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.DeltaV.CartridgeLoader.Cartridges;
using Content.Shared.DeltaV.NanoChat;
using Content.Shared.NameIdentifier;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
namespace Content.Server.DeltaV.NanoChat;
/// <summary>
/// Handles NanoChat features that are specific to the server but not related to the cartridge itself.
/// </summary>
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<NameIdentifierGroupPrototype> _nameIdentifierGroup = "NanoChat";
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<NanoChatCardComponent, MapInitEvent>(OnCardInit);
SubscribeLocalEvent<NanoChatCardComponent, BeingMicrowavedEvent>(OnMicrowaved, after: [typeof(IdCardSystem)]);
}
private void OnMicrowaved(Entity<NanoChatCardComponent> ent, ref BeingMicrowavedEvent args)
{
// Skip if the entity was deleted (e.g., by ID card system burning it)
if (Deleted(ent))
return;
if (!TryComp<MicrowaveComponent>(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<NanoChatMessage>();
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<NanoChatCardComponent> 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);
}
}

View File

@ -28,12 +28,26 @@ namespace Content.Shared.Access.Systems
public string CurrentName { get; } public string CurrentName { get; }
public string CurrentJob { get; } public string CurrentJob { get; }
public string CurrentJobIconId { get; } public string CurrentJobIconId { get; }
public uint? CurrentNumber { get; } // DeltaV
public AgentIDCardBoundUserInterfaceState(string currentName, string currentJob, string currentJobIconId) public AgentIDCardBoundUserInterfaceState(string currentName, string currentJob, string currentJobIconId, uint? currentNumber = null) // DeltaV - Added currentNumber
{ {
CurrentName = currentName; CurrentName = currentName;
CurrentJob = currentJob; CurrentJob = currentJob;
CurrentJobIconId = currentJobIconId; CurrentJobIconId = currentJobIconId;
CurrentNumber = currentNumber; // DeltaV
}
}
// DeltaV - Add number change message
[Serializable, NetSerializable]
public sealed class AgentIDCardNumberChangedMessage : BoundUserInterfaceMessage
{
public uint Number { get; }
public AgentIDCardNumberChangedMessage(uint number)
{
Number = number;
} }
} }

View File

@ -1,4 +1,5 @@
using Robust.Shared.Serialization; using Content.Shared.DeltaV.CartridgeLoader.Cartridges; // DeltaV
using Robust.Shared.Serialization;
namespace Content.Shared.CartridgeLoader.Cartridges; namespace Content.Shared.CartridgeLoader.Cartridges;
@ -10,9 +11,15 @@ public sealed class LogProbeUiState : BoundUserInterfaceState
/// </summary> /// </summary>
public List<PulledAccessLog> PulledLogs; public List<PulledAccessLog> PulledLogs;
public LogProbeUiState(List<PulledAccessLog> pulledLogs) /// <summary>
/// DeltaV: The NanoChat data if a card was scanned, null otherwise
/// </summary>
public NanoChatData? NanoChatData { get; }
public LogProbeUiState(List<PulledAccessLog> pulledLogs, NanoChatData? nanoChatData = null) // DeltaV - NanoChat support
{ {
PulledLogs = pulledLogs; PulledLogs = pulledLogs;
NanoChatData = nanoChatData; // DeltaV
} }
} }

View File

@ -0,0 +1,166 @@
using Content.Shared.CartridgeLoader;
using Robust.Shared.Serialization;
namespace Content.Shared.DeltaV.CartridgeLoader.Cartridges;
[Serializable, NetSerializable]
public sealed class NanoChatUiMessageEvent : CartridgeMessageEvent
{
/// <summary>
/// The type of UI message being sent.
/// </summary>
public readonly NanoChatUiMessageType Type;
/// <summary>
/// The recipient's NanoChat number, if applicable.
/// </summary>
public readonly uint? RecipientNumber;
/// <summary>
/// The content of the message or name for new chats.
/// </summary>
public readonly string? Content;
/// <summary>
/// The recipient's job title when creating a new chat.
/// </summary>
public readonly string? RecipientJob;
/// <summary>
/// Creates a new NanoChat UI message event.
/// </summary>
/// <param name="type">The type of message being sent</param>
/// <param name="recipientNumber">Optional recipient number for the message</param>
/// <param name="content">Optional content of the message</param>
/// <param name="recipientJob">Optional job title for new chat creation</param>
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
{
/// <summary>
/// The recipient's unique NanoChat number.
/// </summary>
public uint Number;
/// <summary>
/// The recipient's display name, typically from their ID card.
/// </summary>
public string Name;
/// <summary>
/// The recipient's job title, if available.
/// </summary>
public string? JobTitle;
/// <summary>
/// Whether this recipient has unread messages.
/// </summary>
public bool HasUnread;
/// <summary>
/// Creates a new NanoChat recipient.
/// </summary>
/// <param name="number">The recipient's NanoChat number</param>
/// <param name="name">The recipient's display name</param>
/// <param name="jobTitle">Optional job title for the recipient</param>
/// <param name="hasUnread">Whether there are unread messages from this recipient</param>
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
{
/// <summary>
/// When the message was sent.
/// </summary>
public TimeSpan Timestamp;
/// <summary>
/// The content of the message.
/// </summary>
public string Content;
/// <summary>
/// The NanoChat number of the sender.
/// </summary>
public uint SenderId;
/// <summary>
/// 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.
/// </summary>
public bool DeliveryFailed;
/// <summary>
/// Creates a new NanoChat message.
/// </summary>
/// <param name="timestamp">When the message was sent</param>
/// <param name="content">The content of the message</param>
/// <param name="senderId">The sender's NanoChat number</param>
/// <param name="deliveryFailed">Whether delivery to the recipient failed</param>
public NanoChatMessage(TimeSpan timestamp, string content, uint senderId, bool deliveryFailed = false)
{
Timestamp = timestamp;
Content = content;
SenderId = senderId;
DeliveryFailed = deliveryFailed;
}
}
/// <summary>
/// NanoChat log data struct
/// </summary>
/// <remarks>Used by the LogProbe</remarks>
[Serializable, NetSerializable, DataRecord]
public readonly struct NanoChatData(
Dictionary<uint, NanoChatRecipient> recipients,
Dictionary<uint, List<NanoChatMessage>> messages,
uint? cardNumber,
NetEntity card)
{
public Dictionary<uint, NanoChatRecipient> Recipients { get; } = recipients;
public Dictionary<uint, List<NanoChatMessage>> Messages { get; } = messages;
public uint? CardNumber { get; } = cardNumber;
public NetEntity Card { get; } = card;
}
/// <summary>
/// Raised on the NanoChat card whenever a recipient gets added
/// </summary>
[ByRefEvent]
public readonly record struct NanoChatRecipientUpdatedEvent(EntityUid CardUid);
/// <summary>
/// Raised on the NanoChat card whenever it receives or tries sending a messsage
/// </summary>
[ByRefEvent]
public readonly record struct NanoChatMessageReceivedEvent(EntityUid CardUid);

View File

@ -0,0 +1,30 @@
using Robust.Shared.Serialization;
namespace Content.Shared.DeltaV.CartridgeLoader.Cartridges;
[Serializable, NetSerializable]
public sealed class NanoChatUiState : BoundUserInterfaceState
{
public readonly Dictionary<uint, NanoChatRecipient> Recipients = new();
public readonly Dictionary<uint, List<NanoChatMessage>> Messages = new();
public readonly uint? CurrentChat;
public readonly uint OwnNumber;
public readonly int MaxRecipients;
public readonly bool NotificationsMuted;
public NanoChatUiState(
Dictionary<uint, NanoChatRecipient> recipients,
Dictionary<uint, List<NanoChatMessage>> messages,
uint? currentChat,
uint ownNumber,
int maxRecipients,
bool notificationsMuted)
{
Recipients = recipients;
Messages = messages;
CurrentChat = currentChat;
OwnNumber = ownNumber;
MaxRecipients = maxRecipients;
NotificationsMuted = notificationsMuted;
}
}

View File

@ -0,0 +1,52 @@
using Content.Shared.DeltaV.CartridgeLoader.Cartridges;
using Robust.Shared.GameStates;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
namespace Content.Shared.DeltaV.NanoChat;
[RegisterComponent, NetworkedComponent, Access(typeof(SharedNanoChatSystem))]
[AutoGenerateComponentPause, AutoGenerateComponentState]
public sealed partial class NanoChatCardComponent : Component
{
/// <summary>
/// The number assigned to this card.
/// </summary>
[DataField, AutoNetworkedField]
public uint? Number;
/// <summary>
/// All chat recipients stored on this card.
/// </summary>
[DataField]
public Dictionary<uint, NanoChatRecipient> Recipients = new();
/// <summary>
/// All messages stored on this card, keyed by recipient number.
/// </summary>
[DataField]
public Dictionary<uint, List<NanoChatMessage>> Messages = new();
/// <summary>
/// The currently selected chat recipient number.
/// </summary>
[DataField]
public uint? CurrentChat;
/// <summary>
/// The maximum amount of recipients this card supports.
/// </summary>
[DataField]
public int MaxRecipients = 50;
/// <summary>
/// Last time a message was sent, for rate limiting.
/// </summary>
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), AutoPausedField]
public TimeSpan LastMessageTime; // TODO: actually use this, compare against actor and not the card
/// <summary>
/// Whether to send notifications.
/// </summary>
[DataField]
public bool NotificationsMuted;
}

View File

@ -0,0 +1,273 @@
using Content.Shared.DeltaV.CartridgeLoader.Cartridges;
using Content.Shared.Examine;
using Robust.Shared.Timing;
namespace Content.Shared.DeltaV.NanoChat;
/// <summary>
/// Base system for NanoChat functionality shared between client and server.
/// </summary>
public abstract class SharedNanoChatSystem : EntitySystem
{
[Dependency] private readonly IGameTiming _timing = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<NanoChatCardComponent, ExaminedEvent>(OnExamined);
}
private void OnExamined(Entity<NanoChatCardComponent> 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
/// <summary>
/// Gets the NanoChat number for a card.
/// </summary>
public uint? GetNumber(Entity<NanoChatCardComponent?> card)
{
if (!Resolve(card, ref card.Comp))
return null;
return card.Comp.Number;
}
/// <summary>
/// Sets the NanoChat number for a card.
/// </summary>
public void SetNumber(Entity<NanoChatCardComponent?> card, uint number)
{
if (!Resolve(card, ref card.Comp))
return;
card.Comp.Number = number;
Dirty(card);
}
/// <summary>
/// Gets the recipients dictionary from a card.
/// </summary>
public IReadOnlyDictionary<uint, NanoChatRecipient> GetRecipients(Entity<NanoChatCardComponent?> card)
{
if (!Resolve(card, ref card.Comp))
return new Dictionary<uint, NanoChatRecipient>();
return card.Comp.Recipients;
}
/// <summary>
/// Gets the messages dictionary from a card.
/// </summary>
public IReadOnlyDictionary<uint, List<NanoChatMessage>> GetMessages(Entity<NanoChatCardComponent?> card)
{
if (!Resolve(card, ref card.Comp))
return new Dictionary<uint, List<NanoChatMessage>>();
return card.Comp.Messages;
}
/// <summary>
/// Sets a specific recipient in the card.
/// </summary>
public void SetRecipient(Entity<NanoChatCardComponent?> card, uint number, NanoChatRecipient recipient)
{
if (!Resolve(card, ref card.Comp))
return;
card.Comp.Recipients[number] = recipient;
Dirty(card);
}
/// <summary>
/// Gets a specific recipient from the card.
/// </summary>
public NanoChatRecipient? GetRecipient(Entity<NanoChatCardComponent?> card, uint number)
{
if (!Resolve(card, ref card.Comp) || !card.Comp.Recipients.TryGetValue(number, out var recipient))
return null;
return recipient;
}
/// <summary>
/// Gets all messages for a specific recipient.
/// </summary>
public List<NanoChatMessage>? GetMessagesForRecipient(Entity<NanoChatCardComponent?> card, uint recipientNumber)
{
if (!Resolve(card, ref card.Comp) || !card.Comp.Messages.TryGetValue(recipientNumber, out var messages))
return null;
return new List<NanoChatMessage>(messages);
}
/// <summary>
/// Adds a message to a recipient's conversation.
/// </summary>
public void AddMessage(Entity<NanoChatCardComponent?> card, uint recipientNumber, NanoChatMessage message)
{
if (!Resolve(card, ref card.Comp))
return;
if (!card.Comp.Messages.TryGetValue(recipientNumber, out var messages))
{
messages = new List<NanoChatMessage>();
card.Comp.Messages[recipientNumber] = messages;
}
messages.Add(message);
card.Comp.LastMessageTime = _timing.CurTime;
Dirty(card);
}
/// <summary>
/// Gets the currently selected chat recipient.
/// </summary>
public uint? GetCurrentChat(Entity<NanoChatCardComponent?> card)
{
if (!Resolve(card, ref card.Comp))
return null;
return card.Comp.CurrentChat;
}
/// <summary>
/// Sets the currently selected chat recipient.
/// </summary>
public void SetCurrentChat(Entity<NanoChatCardComponent?> card, uint? recipient)
{
if (!Resolve(card, ref card.Comp))
return;
card.Comp.CurrentChat = recipient;
Dirty(card);
}
/// <summary>
/// Gets whether notifications are muted.
/// </summary>
public bool GetNotificationsMuted(Entity<NanoChatCardComponent?> card)
{
if (!Resolve(card, ref card.Comp))
return false;
return card.Comp.NotificationsMuted;
}
/// <summary>
/// Sets whether notifications are muted.
/// </summary>
public void SetNotificationsMuted(Entity<NanoChatCardComponent?> card, bool muted)
{
if (!Resolve(card, ref card.Comp))
return;
card.Comp.NotificationsMuted = muted;
Dirty(card);
}
/// <summary>
/// Gets the time of the last message.
/// </summary>
public TimeSpan? GetLastMessageTime(Entity<NanoChatCardComponent?> card)
{
if (!Resolve(card, ref card.Comp))
return null;
return card.Comp.LastMessageTime;
}
/// <summary>
/// Gets if there are unread messages from a recipient.
/// </summary>
public bool HasUnreadMessages(Entity<NanoChatCardComponent?> card, uint recipientNumber)
{
if (!Resolve(card, ref card.Comp) || !card.Comp.Recipients.TryGetValue(recipientNumber, out var recipient))
return false;
return recipient.HasUnread;
}
/// <summary>
/// Clears all messages and recipients from the card.
/// </summary>
public void Clear(Entity<NanoChatCardComponent?> card)
{
if (!Resolve(card, ref card.Comp))
return;
card.Comp.Messages.Clear();
card.Comp.Recipients.Clear();
card.Comp.CurrentChat = null;
Dirty(card);
}
/// <summary>
/// Deletes a chat conversation with a recipient from the card.
/// Optionally keeps message history while removing from active chats.
/// </summary>
/// <returns>True if the chat was deleted successfully</returns>
public bool TryDeleteChat(Entity<NanoChatCardComponent?> 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;
}
/// <summary>
/// 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.
/// </summary>
/// <returns>True if the recipient was added or already existed</returns>
public bool EnsureRecipientExists(Entity<NanoChatCardComponent?> 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<NanoChatMessage>();
Dirty(card);
return true;
}
#endregion
}

View File

@ -0,0 +1 @@
agent-id-card-current-number = NanoChat Number

View File

@ -158,3 +158,45 @@ stock-trading-buy-button = Buy
stock-trading-sell-button = Sell stock-trading-sell-button = Sell
stock-trading-amount-placeholder = Amount stock-trading-amount-placeholder = Amount
stock-trading-price-history = Price History stock-trading-price-history = Price History
## NanoChat
# 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}

View File

@ -0,0 +1,7 @@
# Examine
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!

View File

@ -83,3 +83,24 @@
- type: BankClient - type: BankClient
- type: AccessReader # This is so that we can restrict who can buy stocks - type: AccessReader # This is so that we can restrict who can buy stocks
access: [["Orders"]] access: [["Orders"]]
- type: entity
parent: BaseItem
id: NanoChatCartridge
name: NanoChat cartridge
description: Lets you message other people!
components:
- type: Sprite
sprite: DeltaV/Objects/Devices/cartridge.rsi
state: cart-chat
- type: UIFragment
ui: !type:NanoChatUi
- type: NanoChatCartridge
- type: Cartridge
programName: nano-chat-program-name
icon:
sprite: DeltaV/Misc/program_icons.rsi
state: nanochat
- type: ActiveRadio
channels:
- Common

View File

@ -24,6 +24,7 @@
- NewsReaderCartridge - NewsReaderCartridge
- CrimeAssistCartridge - CrimeAssistCartridge
- SecWatchCartridge - SecWatchCartridge
- NanoChatCartridge
- type: Pda - type: Pda
id: BrigmedicIDCard id: BrigmedicIDCard
state: pda-corpsman state: pda-corpsman
@ -58,6 +59,7 @@
- NewsReaderCartridge - NewsReaderCartridge
- CrimeAssistCartridge - CrimeAssistCartridge
- SecWatchCartridge - SecWatchCartridge
- NanoChatCartridge
- type: entity - type: entity
parent: BaseJusticePDA parent: BaseJusticePDA
@ -186,6 +188,7 @@
- NotekeeperCartridge - NotekeeperCartridge
- NewsReaderCartridge - NewsReaderCartridge
- MailMetricsCartridge - MailMetricsCartridge
- NanoChatCartridge
## Alternate Job Titles ## Alternate Job Titles

View File

@ -0,0 +1,4 @@
# used by the nanochatcard numbers
- type: nameIdentifierGroup
id: NanoChat
maxValue: 9999

View File

@ -80,6 +80,7 @@
- CrewManifestCartridge - CrewManifestCartridge
- NotekeeperCartridge - NotekeeperCartridge
- NewsReaderCartridge - NewsReaderCartridge
- NanoChatCartridge # DeltaV
cartridgeSlot: cartridgeSlot:
priority: -1 priority: -1
name: device-pda-slot-component-slot-name-cartridge name: device-pda-slot-component-slot-name-cartridge
@ -124,13 +125,13 @@
abstract: true abstract: true
components: components:
- type: CartridgeLoader - type: CartridgeLoader
diskSpace: 7 # DeltaV: increase cartridge space by 2 to fit our extra cartridges
preinstalled: preinstalled:
- CrewManifestCartridge - CrewManifestCartridge
- NotekeeperCartridge - NotekeeperCartridge
- NewsReaderCartridge - NewsReaderCartridge
- CrimeAssistCartridge # DeltaV - CrimeAssistCartridge # DeltaV
- SecWatchCartridge # DeltaV: SecWatch replaces WantedList - SecWatchCartridge # DeltaV: SecWatch replaces WantedList
- NanoChatCartridge # DeltaV
- type: entity - type: entity
parent: BasePDA parent: BasePDA
@ -144,6 +145,7 @@
- NotekeeperCartridge - NotekeeperCartridge
- NewsReaderCartridge - NewsReaderCartridge
- MedTekCartridge - MedTekCartridge
- NanoChatCartridge # DeltaV
- type: entity - type: entity
parent: BasePDA parent: BasePDA
@ -399,6 +401,7 @@
- NewsReaderCartridge - NewsReaderCartridge
- MailMetricsCartridge # DeltaV - MailMetrics courier tracker - MailMetricsCartridge # DeltaV - MailMetrics courier tracker
- StockTradingCartridge # DeltaV - StockTrading - StockTradingCartridge # DeltaV - StockTrading
- NanoChatCartridge # DeltaV
- type: entity - type: entity
parent: BasePDA parent: BasePDA
@ -419,6 +422,7 @@
- NotekeeperCartridge - NotekeeperCartridge
- NewsReaderCartridge - NewsReaderCartridge
- StockTradingCartridge # DeltaV - StockTrading - StockTradingCartridge # DeltaV - StockTrading
- NanoChatCartridge # DeltaV
- type: entity - type: entity
parent: BasePDA parent: BasePDA
@ -441,6 +445,7 @@
- NotekeeperCartridge - NotekeeperCartridge
- NewsReaderCartridge - NewsReaderCartridge
- AstroNavCartridge - AstroNavCartridge
- NanoChatCartridge # DeltaV
- type: entity - type: entity
parent: BasePDA parent: BasePDA
@ -694,6 +699,7 @@
- NotekeeperCartridge - NotekeeperCartridge
- NewsReaderCartridge - NewsReaderCartridge
- GlimmerMonitorCartridge - GlimmerMonitorCartridge
- NanoChatCartridge # DeltaV
- type: entity - type: entity
parent: BasePDA parent: BasePDA
@ -715,6 +721,7 @@
- NotekeeperCartridge - NotekeeperCartridge
- NewsReaderCartridge - NewsReaderCartridge
- GlimmerMonitorCartridge - GlimmerMonitorCartridge
- NanoChatCartridge # DeltaV
- type: entity - type: entity
parent: BaseSecurityPDA parent: BaseSecurityPDA
@ -738,6 +745,7 @@
- CrimeAssistCartridge # DeltaV - CrimeAssistCartridge # DeltaV
- SecWatchCartridge # DeltaV: SecWatch replaces WantedList - SecWatchCartridge # DeltaV: SecWatch replaces WantedList
- LogProbeCartridge - LogProbeCartridge
- NanoChatCartridge # DeltaV
- type: entity - type: entity
parent: BaseSecurityPDA parent: BaseSecurityPDA
@ -796,6 +804,7 @@
- SecWatchCartridge # DeltaV: SecWatch replaces WantedList - SecWatchCartridge # DeltaV: SecWatch replaces WantedList
- LogProbeCartridge - LogProbeCartridge
- AstroNavCartridge - AstroNavCartridge
- NanoChatCartridge # DeltaV
- type: entity - type: entity
parent: CentcomPDA parent: CentcomPDA
@ -812,6 +821,7 @@
- type: CartridgeLoader - type: CartridgeLoader
uiKey: enum.PdaUiKey.Key uiKey: enum.PdaUiKey.Key
notificationsEnabled: false notificationsEnabled: false
diskSpace: 10 # DeltaV
preinstalled: preinstalled:
- CrewManifestCartridge - CrewManifestCartridge
- NotekeeperCartridge - NotekeeperCartridge
@ -821,6 +831,7 @@
- MedTekCartridge - MedTekCartridge
- AstroNavCartridge - AstroNavCartridge
- StockTradingCartridge # Delta-V - StockTradingCartridge # Delta-V
- NanoChatCartridge # DeltaV
- type: entity - type: entity
parent: CentcomPDA parent: CentcomPDA
@ -914,6 +925,7 @@
uiKey: enum.PdaUiKey.Key uiKey: enum.PdaUiKey.Key
preinstalled: preinstalled:
- NotekeeperCartridge - NotekeeperCartridge
- NanoChatCartridge # DeltaV
- type: entity - type: entity
parent: BaseSecurityPDA parent: BaseSecurityPDA
@ -941,6 +953,7 @@
- SecWatchCartridge # DeltaV: SecWatch replaces WantedList - SecWatchCartridge # DeltaV: SecWatch replaces WantedList
- LogProbeCartridge - LogProbeCartridge
- AstroNavCartridge - AstroNavCartridge
- NanoChatCartridge # DeltaV
- type: entity - type: entity
parent: ERTLeaderPDA parent: ERTLeaderPDA
@ -1045,6 +1058,7 @@
- NotekeeperCartridge - NotekeeperCartridge
- NewsReaderCartridge - NewsReaderCartridge
- StockTradingCartridge # DeltaV - StockTrading - StockTradingCartridge # DeltaV - StockTrading
- NanoChatCartridge # DeltaV
- type: entity - type: entity
parent: BasePDA parent: BasePDA
@ -1096,6 +1110,7 @@
- CrimeAssistCartridge # DeltaV - CrimeAssistCartridge # DeltaV
- SecWatchCartridge # DeltaV: SecWatch replaces WantedList - SecWatchCartridge # DeltaV: SecWatch replaces WantedList
- LogProbeCartridge - LogProbeCartridge
- NanoChatCartridge # DeltaV
- type: entity - type: entity
parent: BaseMedicalPDA parent: BaseMedicalPDA
@ -1120,6 +1135,7 @@
- CrimeAssistCartridge # DeltaV - CrimeAssistCartridge # DeltaV
- SecWatchCartridge # DeltaV: SecWatch replaces WantedList - SecWatchCartridge # DeltaV: SecWatch replaces WantedList
- MedTekCartridge - MedTekCartridge
- NanoChatCartridge # DeltaV
- type: entity - type: entity
parent: ClownPDA parent: ClownPDA
@ -1236,3 +1252,4 @@
preinstalled: preinstalled:
- NotekeeperCartridge - NotekeeperCartridge
- MedTekCartridge - MedTekCartridge
- NanoChatCartridge # DeltaV

View File

@ -24,6 +24,7 @@
- WhitelistChameleon - WhitelistChameleon
- type: StealTarget - type: StealTarget
stealGroup: IDCard stealGroup: IDCard
- type: NanoChatCard # DeltaV
#IDs with layers #IDs with layers
@ -827,3 +828,5 @@
- NuclearOperative - NuclearOperative
- SyndicateAgent - SyndicateAgent
- DV-SpareSafe # DeltaV - DV-SpareSafe # DeltaV
- type: NanoChatCard # DeltaV
notificationsMuted: true

View File

@ -40,6 +40,7 @@
- NewsReaderCartridge - NewsReaderCartridge
- CrimeAssistCartridge - CrimeAssistCartridge
- SecWatchCartridge - SecWatchCartridge
- NanoChatCartridge
- type: entity - type: entity
parent: CourierPDA # DeltaV - Gives them the MailMetrics cartbridge parent: CourierPDA # DeltaV - Gives them the MailMetrics cartbridge
@ -114,3 +115,4 @@
- NotekeeperCartridge - NotekeeperCartridge
- NewsReaderCartridge - NewsReaderCartridge
- GlimmerMonitorCartridge - GlimmerMonitorCartridge
- NanoChatCartridge

View File

@ -0,0 +1,2 @@
bell.svg taken from https://coreui.io/icons/
Licensed under CC BY 4.0

View File

@ -0,0 +1,5 @@
<!-- Generated by IcoMoon.io -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<title>bell</title>
<path d="M21.106 16.339l-2.047-3.779v-3.935c0-3.929-3.196-7.125-7.125-7.125s-7.125 3.196-7.125 7.125v3.935l-2.047 3.779c-0.085 0.155-0.136 0.339-0.136 0.536 0 0.621 0.504 1.125 1.125 1.125h4.075c-0.011 0.124-0.017 0.249-0.017 0.375 0 2.278 1.847 4.125 4.125 4.125s4.125-1.847 4.125-4.125v0c0-0.126-0.006-0.251-0.017-0.375h4.075c0 0 0 0 0 0 0.621 0 1.125-0.504 1.125-1.125 0-0.196-0.050-0.381-0.139-0.542l0.003 0.006zM14.559 18.375c0 0.001 0 0.001 0 0.002 0 1.45-1.175 2.625-2.625 2.625s-2.625-1.175-2.625-2.625c0-0.133 0.010-0.264 0.029-0.392l-0.002 0.014h5.196c0.017 0.113 0.027 0.243 0.027 0.375v0zM4.381 16.5l1.928-3.56v-4.315c0-3.107 2.518-5.625 5.625-5.625s5.625 2.518 5.625 5.625v0 4.315l1.928 3.56z"></path>
</svg>

After

Width:  |  Height:  |  Size: 873 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 720 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 355 B

View File

@ -1,14 +1,17 @@
{ {
"version": 1, "version": 1,
"license": "CC0-1.0", "license": "CC0-1.0",
"copyright": "stock_trading made by Malice", "copyright": "stock_trading made by Malice, nanochat made by kushbreth (discord)",
"size": { "size": {
"x": 32, "x": 32,
"y": 32 "y": 32
}, },
"states": [ "states": [
{ {
"name": "stock_trading" "name": "stock_trading"
} },
{
"name": "nanochat"
}
] ]
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 725 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 419 B

View File

@ -1,7 +1,7 @@
{ {
"version": 1, "version": 1,
"license": "CC-BY-SA-3.0", "license": "CC-BY-SA-3.0",
"copyright": "Monotheonist (github), edited from cart-log, cart-nav & cart-med cartridges; cart-log made by Skarletto (github), cart-nav, cart-med made by ArchRBX (github)", "copyright": "cart-chat made by kushbreth (discord), cart-cri, cart-mail, cart-psi, cart-stonk made by Monotheonist (github), edited from cart-log, cart-nav & cart-med cartridges; cart-log made by Skarletto (github), cart-nav, cart-med made by ArchRBX (github)",
"size": { "size": {
"x": 32, "x": 32,
"y": 32 "y": 32
@ -18,6 +18,9 @@
}, },
{ {
"name": "cart-stonk" "name": "cart-stonk"
},
{
"name": "cart-chat"
} }
] ]
} }