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:
parent
140fc65249
commit
2c6517953a
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 -->
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
using Content.Shared.DeltaV.NanoChat;
|
||||||
|
|
||||||
|
namespace Content.Client.DeltaV.NanoChat;
|
||||||
|
|
||||||
|
public sealed class NanoChatSystem : SharedNanoChatSystem;
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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";
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
agent-id-card-current-number = NanoChat Number
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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!
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
# used by the nanochatcard numbers
|
||||||
|
- type: nameIdentifierGroup
|
||||||
|
id: NanoChat
|
||||||
|
maxValue: 9999
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
bell.svg taken from https://coreui.io/icons/
|
||||||
|
Licensed under CC BY 4.0
|
||||||
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue