This commit is contained in:
deltanedas 2024-12-13 21:05:04 +00:00
commit 40362d22c4
225 changed files with 38336 additions and 18809 deletions

24
.github/CODEOWNERS vendored
View File

@ -3,18 +3,36 @@
# C# code
/Content.*/ @DeltaV-Station/maintainers
# Any assets
/Resources/ @DeltaV-Station/maintainers
# Server config files
/Resources/ConfigPresets/ @MilonPL
# YML files
/Resources/*.yml @DeltaV-Station/yaml-maintainers
/Resources/**/*.yml @DeltaV-Station/yaml-maintainers
# Sprites
/Resources/Textures/ @IamVelcroboy
/Resources/Textures/ @DeltaV-Station/direction
# Lobby art and music - automatically direction issues since its immediately visible to players
/Resources/Audio/Lobby/ @DeltaV-Station/game-directors
/Resources/Textures/LobbyScreens/ @DeltaV-Station/game-directors
/Resources/Audio/Lobby/ @DeltaV-Station/direction
/Resources/Textures/LobbyScreens/ @DeltaV-Station/direction
# Maps
/Resources/Maps/ @DeltaV-Station/maptainers
/Resources/Prototypes/Maps/ @DeltaV-Station/maptainers
/Content.IntegrationTests/Tests/PostMapInitTest.cs @DeltaV-Station/maptainers
# Server rules
/Resources/ServerInfo/Guidebook/DeltaV/Rules/ @DeltaV-Station/head-administrators
# Tools and scripts
/Tools/ @deltanedas @MilonPL
# Workflows, codeowners, templates, etc.
/.github/ @deltanedas @MilonPL
# Standalone files in the root repo
/* @deltanedas

View File

@ -26,6 +26,13 @@ namespace Content.Client.Access.UI
_window.OnNameChanged += OnNameChanged;
_window.OnJobChanged += OnJobChanged;
_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)
@ -56,6 +63,7 @@ namespace Content.Client.Access.UI
_window.SetCurrentName(cast.CurrentName);
_window.SetCurrentJob(cast.CurrentJob);
_window.SetAllowedIcons(cast.CurrentJobIconId);
_window.SetCurrentNumber(cast.CurrentNumber); // DeltaV
}
}
}

View File

@ -6,6 +6,10 @@
<LineEdit Name="NameLineEdit" />
<Label Name="CurrentJob" Text="{Loc 'agent-id-card-current-job'}" />
<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'}"/>
<GridContainer Name="IconGrid" Columns="10">
<!-- Job icon buttons are generated in the code -->

View File

@ -21,9 +21,13 @@ namespace Content.Client.Access.UI
private const int JobIconColumnCount = 10;
private const int MaxNumberLength = 4; // DeltaV - Same as NewChatPopup
public event Action<string>? OnNameChanged;
public event Action<string>? OnJobChanged;
public event Action<uint>? OnNumberChanged; // DeltaV - Add event for number changes
public event Action<ProtoId<JobIconPrototype>>? OnJobIconChanged;
public AgentIDCardWindow()
@ -37,6 +41,37 @@ namespace Content.Client.Access.UI
JobLineEdit.OnTextEntered += 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)

View File

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

View File

@ -9,10 +9,30 @@
BorderColor="#5a5a5a"
BorderThickness="0 0 0 1"/>
</PanelContainer.PanelOverride>
<BoxContainer Orientation="Horizontal" Margin="4 8">
<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 Align="Left" SetWidth="390" ClipText="True" Text="{Loc 'log-probe-label-accessor'}"/>
<BoxContainer Orientation="Vertical" Margin="4 8">
<!-- DeltaV begin - Add title label -->
<Label Name="TitleLabel"
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>
</PanelContainer>
<ScrollContainer VerticalExpand="True" HScrollEnabled="True">

View File

@ -1,4 +1,7 @@
using Content.Shared.CartridgeLoader.Cartridges;
using System.Linq; // DeltaV
using Content.Client.DeltaV.CartridgeLoader.Cartridges; // DeltaV
using Content.Shared.CartridgeLoader.Cartridges;
using Content.Shared.DeltaV.CartridgeLoader.Cartridges; // DeltaV
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
@ -13,10 +16,112 @@ public sealed partial class LogProbeUiFragment : BoxContainer
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();
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
logs.Reverse();

View File

@ -1,12 +1,11 @@
using Content.Shared.DeltaV.AACTablet;
using Content.Shared.DeltaV.QuickPhrase;
using Robust.Shared.Prototypes;
namespace Content.Client.DeltaV.AACTablet.UI;
public sealed class AACBoundUserInterface : BoundUserInterface
{
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[ViewVariables]
private AACWindow? _window;
@ -18,14 +17,14 @@ public sealed class AACBoundUserInterface : BoundUserInterface
{
base.Open();
_window?.Close();
_window = new AACWindow(this, _prototypeManager);
_window = new AACWindow();
_window.OpenCentered();
_window.PhraseButtonPressed += OnPhraseButtonPressed;
_window.OnClose += Close;
}
private void OnPhraseButtonPressed(string phraseId)
private void OnPhraseButtonPressed(ProtoId<QuickPhrasePrototype> phraseId)
{
SendMessage(new AACTabletSendPhraseMessage(phraseId));
}
@ -33,6 +32,6 @@ public sealed class AACBoundUserInterface : BoundUserInterface
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
_window?.Dispose();
_window?.Orphan();
}
}

View File

@ -6,4 +6,4 @@
MinSize="540 300">
<ScrollContainer HScrollEnabled="False" Name="WindowBody">
</ScrollContainer>
</controls:FancyWindow>
</controls:FancyWindow>

View File

@ -13,20 +13,26 @@ namespace Content.Client.DeltaV.AACTablet.UI;
[GenerateTypedNameReferences]
public sealed partial class AACWindow : FancyWindow
{
private IPrototypeManager _prototypeManager;
public event Action<string>? PhraseButtonPressed;
[Dependency] private readonly IPrototypeManager _prototype = default!;
public event Action<ProtoId<QuickPhrasePrototype>>? PhraseButtonPressed;
public AACWindow(AACBoundUserInterface ui, IPrototypeManager prototypeManager)
private const float SpaceWidth = 10f;
private const float ParentWidth = 540f;
private const int ColumnCount = 4;
private const int ButtonWidth =
(int)((ParentWidth - SpaceWidth * 2) / ColumnCount - SpaceWidth * ((ColumnCount - 1f) / ColumnCount));
public AACWindow()
{
RobustXamlLoader.Load(this);
_prototypeManager = prototypeManager;
PopulateGui(ui);
IoCManager.InjectDependencies(this);
PopulateGui();
}
private void PopulateGui(AACBoundUserInterface ui)
private void PopulateGui()
{
var loc = IoCManager.Resolve<ILocalizationManager>();
var phrases = _prototypeManager.EnumeratePrototypes<QuickPhrasePrototype>().ToList();
var phrases = _prototype.EnumeratePrototypes<QuickPhrasePrototype>().ToList();
// take ALL phrases and turn them into tabs and groups, so the buttons are sorted and tabbed
var sortedTabs = phrases
@ -38,7 +44,7 @@ public sealed partial class AACWindow : FancyWindow
.OrderBy(gg => gg.Key)
.ToDictionary(
gg => gg.Key,
gg => gg.OrderBy(p => loc.GetString(p.Text)).ToList()
gg => gg.OrderBy(p => Loc.GetString(p.Text)).ToList()
)
);
@ -49,11 +55,10 @@ public sealed partial class AACWindow : FancyWindow
private TabContainer CreateTabContainer(Dictionary<string, Dictionary<string, List<QuickPhrasePrototype>>> sortedTabs)
{
var tabContainer = new TabContainer();
var loc = IoCManager.Resolve<ILocalizationManager>();
foreach (var tab in sortedTabs)
{
var tabName = loc.GetString(tab.Key);
var tabName = Loc.GetString(tab.Key);
var boxContainer = CreateBoxContainerForTab(tab.Value);
tabContainer.AddChild(boxContainer);
tabContainer.SetTabTitle(tabContainer.ChildCount - 1, tabName);
@ -64,7 +69,7 @@ public sealed partial class AACWindow : FancyWindow
private BoxContainer CreateBoxContainerForTab(Dictionary<string, List<QuickPhrasePrototype>> groups)
{
var boxContainer = new BoxContainer()
var boxContainer = new BoxContainer
{
HorizontalExpand = true,
Orientation = BoxContainer.LayoutOrientation.Vertical
@ -81,7 +86,7 @@ public sealed partial class AACWindow : FancyWindow
return boxContainer;
}
private Label CreateHeaderForGroup(string groupName)
private static Label CreateHeaderForGroup(string groupName)
{
var header = new Label
{
@ -96,13 +101,12 @@ public sealed partial class AACWindow : FancyWindow
private GridContainer CreateButtonContainerForGroup(List<QuickPhrasePrototype> phrases)
{
var loc = IoCManager.Resolve<ILocalizationManager>();
var buttonContainer = CreateButtonContainer();
foreach (var phrase in phrases)
{
var text = loc.GetString(phrase.Text);
var text = Loc.GetString(phrase.Text);
var button = CreatePhraseButton(text, phrase.StyleClass);
button.OnPressed += _ => OnPhraseButtonPressed(phrase.ID);
button.OnPressed += _ => OnPhraseButtonPressed(new ProtoId<QuickPhrasePrototype>(phrase.ID));
buttonContainer.AddChild(button);
}
return buttonContainer;
@ -121,11 +125,10 @@ public sealed partial class AACWindow : FancyWindow
private static Button CreatePhraseButton(string text, string styleClass)
{
var buttonWidth = GetButtonWidth();
var phraseButton = new Button
{
Access = AccessLevel.Public,
MaxSize = new Vector2(buttonWidth, buttonWidth),
MaxSize = new Vector2(ButtonWidth, ButtonWidth),
ClipText = false,
HorizontalExpand = true,
StyleClasses = { styleClass }
@ -142,20 +145,7 @@ public sealed partial class AACWindow : FancyWindow
return phraseButton;
}
private static int GetButtonWidth()
{
var spaceWidth = 10;
var parentWidth = 540;
var columnCount = 4;
var paddingSize = spaceWidth * 2;
var gutterScale = (columnCount - 1) / columnCount;
var columnWidth = (parentWidth - paddingSize) / columnCount;
var buttonWidth = columnWidth - spaceWidth * gutterScale;
return buttonWidth;
}
private void OnPhraseButtonPressed(string phraseId)
private void OnPhraseButtonPressed(ProtoId<QuickPhrasePrototype> phraseId)
{
PhraseButtonPressed?.Invoke(phraseId);
}

View File

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

View File

@ -0,0 +1,39 @@
using Content.Shared.DeltaV.CartridgeLoader.Cartridges;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
namespace Content.Client.DeltaV.CartridgeLoader.Cartridges;
[GenerateTypedNameReferences]
public sealed partial class NanoChatEntry : BoxContainer
{
public event Action<uint>? OnPressed;
private uint _number;
private Action<EventArgs>? _pressHandler;
public NanoChatEntry()
{
RobustXamlLoader.Load(this);
}
public void SetRecipient(NanoChatRecipient recipient, uint number, bool isSelected)
{
// Remove old handler if it exists
if (_pressHandler != null)
ChatButton.OnPressed -= _pressHandler;
_number = number;
// Create and store new handler
_pressHandler = _ => OnPressed?.Invoke(_number);
ChatButton.OnPressed += _pressHandler;
NameLabel.Text = recipient.Name;
JobLabel.Text = recipient.JobTitle ?? "";
JobLabel.Visible = !string.IsNullOrEmpty(recipient.JobTitle);
UnreadIndicator.Visible = recipient.HasUnread;
ChatButton.ModulateSelfOverride = isSelected ? NanoChatMessageBubble.OwnMessageColor : null;
}
}

View File

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

View File

@ -0,0 +1,17 @@
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
namespace Content.Client.DeltaV.CartridgeLoader.Cartridges;
[GenerateTypedNameReferences]
public sealed partial class NanoChatLogEntry : BoxContainer
{
public NanoChatLogEntry(int number, string time, string message)
{
RobustXamlLoader.Load(this);
NumberLabel.Text = number.ToString();
TimeLabel.Text = time;
MessageLabel.Text = message;
}
}

View File

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

View File

@ -0,0 +1,62 @@
using Content.Shared.DeltaV.CartridgeLoader.Cartridges;
using Robust.Client.AutoGenerated;
using Robust.Client.Graphics;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
namespace Content.Client.DeltaV.CartridgeLoader.Cartridges;
[GenerateTypedNameReferences]
public sealed partial class NanoChatMessageBubble : BoxContainer
{
public static readonly Color OwnMessageColor = Color.FromHex("#173717d9"); // Dark green
public static readonly Color OtherMessageColor = Color.FromHex("#252525d9"); // Dark gray
public static readonly Color BorderColor = Color.FromHex("#40404066"); // Subtle border
public static readonly Color TextColor = Color.FromHex("#dcdcdc"); // Slightly softened white
public static readonly Color ErrorColor = Color.FromHex("#cc3333"); // Red
public NanoChatMessageBubble()
{
RobustXamlLoader.Load(this);
}
public void SetMessage(NanoChatMessage message, bool isOwnMessage)
{
if (MessagePanel.PanelOverride is not StyleBoxFlat)
return;
// Configure message appearance
var style = (StyleBoxFlat)MessagePanel.PanelOverride;
style.BackgroundColor = isOwnMessage ? OwnMessageColor : OtherMessageColor;
style.BorderColor = BorderColor;
// Set message content
MessageText.Text = message.Content;
MessageText.Modulate = TextColor;
// Show delivery failed text if needed (only for own messages)
DeliveryFailedLabel.Visible = isOwnMessage && message.DeliveryFailed;
if (DeliveryFailedLabel.Visible)
DeliveryFailedLabel.Modulate = ErrorColor;
// For own messages: FlexSpace -> MessagePanel -> RightSpacer
// For other messages: LeftSpacer -> MessagePanel -> FlexSpace
MessageContainer.RemoveAllChildren();
// fuuuuuck
MessageBox.Parent?.RemoveChild(MessageBox);
if (isOwnMessage)
{
MessageContainer.AddChild(FlexSpace);
MessageContainer.AddChild(MessageBox);
MessageContainer.AddChild(RightSpacer);
}
else
{
MessageContainer.AddChild(LeftSpacer);
MessageContainer.AddChild(MessageBox);
MessageContainer.AddChild(FlexSpace);
}
}
}

View File

@ -0,0 +1,43 @@
using Content.Client.UserInterface.Fragments;
using Content.Shared.CartridgeLoader;
using Content.Shared.DeltaV.CartridgeLoader.Cartridges;
using Robust.Client.UserInterface;
namespace Content.Client.DeltaV.CartridgeLoader.Cartridges;
public sealed partial class NanoChatUi : UIFragment
{
private NanoChatUiFragment? _fragment;
public override Control GetUIFragmentRoot()
{
return _fragment!;
}
public override void Setup(BoundUserInterface userInterface, EntityUid? fragmentOwner)
{
_fragment = new NanoChatUiFragment();
_fragment.OnMessageSent += (type, number, content, job) =>
{
SendNanoChatUiMessage(type, number, content, job, userInterface);
};
}
public override void UpdateState(BoundUserInterfaceState state)
{
if (state is NanoChatUiState cast)
_fragment?.UpdateState(cast);
}
private static void SendNanoChatUiMessage(NanoChatUiMessageType type,
uint? number,
string? content,
string? job,
BoundUserInterface userInterface)
{
var nanoChatMessage = new NanoChatUiMessageEvent(type, number, content, job);
var message = new CartridgeUiMessage(nanoChatMessage);
userInterface.SendMessage(message);
}
}

View File

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

View File

@ -0,0 +1,254 @@
using System.Linq;
using System.Numerics;
using Content.Shared.DeltaV.CartridgeLoader.Cartridges;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Client.UserInterface;
using Robust.Shared.Timing;
namespace Content.Client.DeltaV.CartridgeLoader.Cartridges;
[GenerateTypedNameReferences]
public sealed partial class NanoChatUiFragment : BoxContainer
{
[Dependency] private readonly IGameTiming _timing = default!;
private const int MaxMessageLength = 256;
private readonly NewChatPopup _newChatPopup;
private uint? _currentChat;
private uint? _pendingChat;
private uint _ownNumber;
private bool _notificationsMuted;
private Dictionary<uint, NanoChatRecipient> _recipients = new();
private Dictionary<uint, List<NanoChatMessage>> _messages = new();
public event Action<NanoChatUiMessageType, uint?, string?, string?>? OnMessageSent;
public NanoChatUiFragment()
{
IoCManager.InjectDependencies(this);
RobustXamlLoader.Load(this);
_newChatPopup = new NewChatPopup();
SetupEventHandlers();
}
private void SetupEventHandlers()
{
_newChatPopup.OnChatCreated += (number, name, job) =>
{
OnMessageSent?.Invoke(NanoChatUiMessageType.NewChat, number, name, job);
};
NewChatButton.OnPressed += _ =>
{
_newChatPopup.ClearInputs();
_newChatPopup.OpenCentered();
};
MuteButton.OnPressed += _ =>
{
_notificationsMuted = !_notificationsMuted;
UpdateMuteButton();
OnMessageSent?.Invoke(NanoChatUiMessageType.ToggleMute, null, null, null);
};
MessageInput.OnTextChanged += args =>
{
var length = args.Text.Length;
var isValid = !string.IsNullOrWhiteSpace(args.Text) &&
length <= MaxMessageLength &&
(_currentChat != null || _pendingChat != null);
SendButton.Disabled = !isValid;
// Show character count when over limit
CharacterCount.Visible = length > MaxMessageLength;
if (length > MaxMessageLength)
{
CharacterCount.Text = Loc.GetString("nano-chat-message-too-long",
("current", length),
("max", MaxMessageLength));
CharacterCount.StyleClasses.Add("LabelDanger");
}
};
SendButton.OnPressed += _ => SendMessage();
DeleteChatButton.OnPressed += _ => DeleteCurrentChat();
}
private void SendMessage()
{
var activeChat = _pendingChat ?? _currentChat;
if (activeChat == null || string.IsNullOrWhiteSpace(MessageInput.Text))
return;
var messageContent = MessageInput.Text;
// Add predicted message
var predictedMessage = new NanoChatMessage(
_timing.CurTime,
messageContent,
_ownNumber
);
if (!_messages.TryGetValue(activeChat.Value, out var value))
{
value = new List<NanoChatMessage>();
_messages[activeChat.Value] = value;
}
value.Add(predictedMessage);
// Update UI with predicted message
UpdateMessages(_messages);
// Send message event
OnMessageSent?.Invoke(NanoChatUiMessageType.SendMessage, activeChat, messageContent, null);
// Clear input
MessageInput.Text = string.Empty;
SendButton.Disabled = true;
}
private void SelectChat(uint number)
{
// Don't reselect the same chat
if (_currentChat == number && _pendingChat == null)
return;
_pendingChat = number;
// Predict marking messages as read
if (_recipients.TryGetValue(number, out var recipient))
{
recipient.HasUnread = false;
_recipients[number] = recipient;
UpdateChatList(_recipients);
}
OnMessageSent?.Invoke(NanoChatUiMessageType.SelectChat, number, null, null);
UpdateCurrentChat();
}
private void DeleteCurrentChat()
{
var activeChat = _pendingChat ?? _currentChat;
if (activeChat == null)
return;
OnMessageSent?.Invoke(NanoChatUiMessageType.DeleteChat, activeChat, null, null);
}
private void UpdateChatList(Dictionary<uint, NanoChatRecipient> recipients)
{
ChatList.RemoveAllChildren();
_recipients = recipients;
NoChatsLabel.Visible = recipients.Count == 0;
if (NoChatsLabel.Parent != ChatList)
{
NoChatsLabel.Parent?.RemoveChild(NoChatsLabel);
ChatList.AddChild(NoChatsLabel);
}
foreach (var (number, recipient) in recipients.OrderBy(r => r.Value.Name))
{
var entry = new NanoChatEntry();
// For pending chat selection, always show it as selected even if unconfirmed
var isSelected = (_pendingChat == number) || (_pendingChat == null && _currentChat == number);
entry.SetRecipient(recipient, number, isSelected);
entry.OnPressed += SelectChat;
ChatList.AddChild(entry);
}
}
private void UpdateCurrentChat()
{
var activeChat = _pendingChat ?? _currentChat;
var hasActiveChat = activeChat != null;
// Update UI state
MessagesScroll.Visible = hasActiveChat;
CurrentChatName.Visible = !hasActiveChat;
MessageInputContainer.Visible = hasActiveChat;
DeleteChatButton.Visible = hasActiveChat;
DeleteChatButton.Disabled = !hasActiveChat;
if (activeChat != null && _recipients.TryGetValue(activeChat.Value, out var recipient))
{
CurrentChatName.Text = recipient.Name + (string.IsNullOrEmpty(recipient.JobTitle) ? "" : $" ({recipient.JobTitle})");
}
else
{
CurrentChatName.Text = Loc.GetString("nano-chat-select-chat");
}
}
private void UpdateMessages(Dictionary<uint, List<NanoChatMessage>> messages)
{
_messages = messages;
MessageList.RemoveAllChildren();
var activeChat = _pendingChat ?? _currentChat;
if (activeChat == null || !messages.TryGetValue(activeChat.Value, out var chatMessages))
return;
foreach (var message in chatMessages)
{
var messageBubble = new NanoChatMessageBubble();
messageBubble.SetMessage(message, message.SenderId == _ownNumber);
MessageList.AddChild(messageBubble);
// Add spacing between messages
MessageList.AddChild(new Control { MinSize = new Vector2(0, 4) });
}
MessageList.InvalidateMeasure();
MessagesScroll.InvalidateMeasure();
// Scroll to bottom after messages are added
if (MessageList.Parent is ScrollContainer scroll)
scroll.SetScrollValue(new Vector2(0, float.MaxValue));
}
private void UpdateMuteButton()
{
if (BellMutedIcon != null)
BellMutedIcon.Visible = _notificationsMuted;
}
public void UpdateState(NanoChatUiState state)
{
_ownNumber = state.OwnNumber;
_notificationsMuted = state.NotificationsMuted;
OwnNumberLabel.Text = $"#{state.OwnNumber:D4}";
UpdateMuteButton();
// Update new chat button state based on recipient limit
var atLimit = state.Recipients.Count >= state.MaxRecipients;
NewChatButton.Disabled = atLimit;
NewChatButton.ToolTip = atLimit
? Loc.GetString("nano-chat-max-recipients")
: Loc.GetString("nano-chat-new-chat");
// First handle pending chat resolution if we have one
if (_pendingChat != null)
{
if (_pendingChat == state.CurrentChat)
_currentChat = _pendingChat; // Server confirmed our selection
_pendingChat = null; // Clear pending either way
}
// No pending chat or it was just cleared, update current directly
if (_pendingChat == null)
_currentChat = state.CurrentChat;
UpdateCurrentChat();
UpdateChatList(state.Recipients);
UpdateMessages(state.Messages);
}
}

View File

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

View File

@ -0,0 +1,87 @@
using System.Linq;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
namespace Content.Client.DeltaV.CartridgeLoader.Cartridges;
[GenerateTypedNameReferences]
public sealed partial class NewChatPopup : DefaultWindow
{
private const int MaxInputLength = 16;
private const int MaxNumberLength = 4; // i hardcoded it to be 4 so suffer
public event Action<uint, string, string?>? OnChatCreated;
public NewChatPopup()
{
RobustXamlLoader.Load(this);
// margins trolling
ContentsContainer.Margin = new Thickness(3);
// Button handlers
CancelButton.OnPressed += _ => Close();
CreateButton.OnPressed += _ => CreateChat();
// Input validation
NumberInput.OnTextChanged += _ => ValidateInputs();
NameInput.OnTextChanged += _ => ValidateInputs();
// Input validation
NumberInput.OnTextChanged += args =>
{
if (args.Text.Length > MaxNumberLength)
NumberInput.Text = args.Text[..MaxNumberLength];
// Filter to digits only
var newText = string.Concat(NumberInput.Text.Where(char.IsDigit));
if (newText != NumberInput.Text)
NumberInput.Text = newText;
ValidateInputs();
};
NameInput.OnTextChanged += args =>
{
if (args.Text.Length > MaxInputLength)
NameInput.Text = args.Text[..MaxInputLength];
ValidateInputs();
};
JobInput.OnTextChanged += args =>
{
if (args.Text.Length > MaxInputLength)
JobInput.Text = args.Text[..MaxInputLength];
};
}
private void ValidateInputs()
{
var isValid = !string.IsNullOrWhiteSpace(NumberInput.Text) &&
!string.IsNullOrWhiteSpace(NameInput.Text) &&
uint.TryParse(NumberInput.Text, out _);
CreateButton.Disabled = !isValid;
}
private void CreateChat()
{
if (!uint.TryParse(NumberInput.Text, out var number))
return;
var name = NameInput.Text.Trim();
var job = string.IsNullOrWhiteSpace(JobInput.Text) ? null : JobInput.Text.Trim();
OnChatCreated?.Invoke(number, name, job);
Close();
}
public void ClearInputs()
{
NumberInput.Text = string.Empty;
NameInput.Text = string.Empty;
JobInput.Text = string.Empty;
ValidateInputs();
}
}

View File

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

View File

@ -0,0 +1,5 @@
using Content.Shared.DeltaV.Shuttles.Systems;
namespace Content.Client.DeltaV.Shuttles.Systems;
public sealed class DockingConsoleSystem : SharedDockingConsoleSystem;

View File

@ -0,0 +1,38 @@
using Content.Shared.DeltaV.Shuttles;
namespace Content.Client.DeltaV.Shuttles.UI;
public sealed class DockingConsoleBoundUserInterface : BoundUserInterface
{
[ViewVariables]
private DockingConsoleWindow? _window;
public DockingConsoleBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
{
}
protected override void Open()
{
base.Open();
_window = new DockingConsoleWindow(Owner);
_window.OnFTL += index => SendMessage(new DockingConsoleFTLMessage(index));
_window.OnClose += Close;
_window.OpenCentered();
}
protected override void UpdateState(BoundUserInterfaceState state)
{
base.UpdateState(state);
if (state is DockingConsoleState cast)
_window?.UpdateState(cast);
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (disposing)
_window?.Orphan();
}
}

View File

@ -0,0 +1,17 @@
<controls:FancyWindow xmlns="https://spacestation14.io"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
SetSize="500 500">
<BoxContainer Orientation="Vertical">
<ScrollContainer SetHeight="256" HorizontalExpand="True">
<ItemList Name="Destinations"/> <!-- Populated from comp.Destinations -->
</ScrollContainer>
<controls:StripeBack MinSize="48 48">
<Label Text="{Loc 'shuttle-console-ftl-label'}" VerticalExpand="True" HorizontalAlignment="Center"/>
</controls:StripeBack>
<Label Name="MapFTLState" Text="{Loc 'shuttle-console-ftl-state-Available'}" VerticalAlignment="Stretch" HorizontalAlignment="Center"/>
<ProgressBar Name="FTLBar" HorizontalExpand="True" Margin="5" MinValue="0.0" MaxValue="1.0" Value="1.0" SetHeight="32"/>
<controls:StripeBack HorizontalExpand="True">
<Button Name="FTLButton" Text="{Loc 'docking-console-ftl'}" Disabled="True" SetSize="128 48" Margin="5"/>
</controls:StripeBack>
</BoxContainer>
</controls:FancyWindow>

View File

@ -0,0 +1,112 @@
using Content.Client.UserInterface.Controls;
using Content.Shared.Access.Systems;
using Content.Shared.DeltaV.Shuttles;
using Content.Shared.DeltaV.Shuttles.Components;
using Content.Shared.Shuttles.Systems;
using Content.Shared.Timing;
using Robust.Client.AutoGenerated;
using Robust.Client.Graphics;
using Robust.Client.Player;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Timing;
namespace Content.Client.DeltaV.Shuttles.UI;
[GenerateTypedNameReferences]
public sealed partial class DockingConsoleWindow : FancyWindow
{
[Dependency] private readonly IEntityManager _entMan = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IPlayerManager _player = default!;
private readonly AccessReaderSystem _access;
public event Action<int>? OnFTL;
private readonly EntityUid _owner;
private readonly StyleBoxFlat _ftlStyle;
private FTLState _state;
private int? _selected;
private StartEndTime _ftlTime;
public DockingConsoleWindow(EntityUid owner)
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
_access = _entMan.System<AccessReaderSystem>();
_owner = owner;
_ftlStyle = new StyleBoxFlat(Color.LimeGreen);
FTLBar.ForegroundStyleBoxOverride = _ftlStyle;
if (!_entMan.TryGetComponent<DockingConsoleComponent>(owner, out var comp))
return;
Title = Loc.GetString(comp.WindowTitle);
if (!comp.HasShuttle)
{
MapFTLState.Text = Loc.GetString("docking-console-no-shuttle");
_ftlStyle.BackgroundColor = Color.FromHex("#B02E26");
return;
}
Destinations.OnItemSelected += args => _selected = args.ItemIndex;
Destinations.OnItemDeselected += _ => _selected = null;
FTLButton.OnPressed += _ =>
{
if (_selected is {} index)
OnFTL?.Invoke(index);
};
}
public void UpdateState(DockingConsoleState state)
{
_state = state.FTLState;
_ftlTime = state.FTLTime;
MapFTLState.Text = Loc.GetString($"shuttle-console-ftl-state-{_state.ToString()}");
_ftlStyle.BackgroundColor = Color.FromHex(_state switch
{
FTLState.Available => "#80C71F",
FTLState.Starting => "#169C9C",
FTLState.Travelling => "#8932B8",
FTLState.Arriving => "#F9801D",
_ => "#B02E26" // cooldown and fallback
});
UpdateButton();
if (Destinations.Count == state.Destinations.Count)
return;
Destinations.Clear();
foreach (var dest in state.Destinations)
{
Destinations.AddItem(dest.Name);
}
}
private void UpdateButton()
{
FTLButton.Disabled = _selected == null || _state != FTLState.Available || !HasAccess();
}
private bool HasAccess()
{
return _player.LocalSession?.AttachedEntity is {} player && _access.IsAllowed(player, _owner);
}
protected override void FrameUpdate(FrameEventArgs args)
{
base.FrameUpdate(args);
UpdateButton();
var progress = _ftlTime.ProgressAt(_timing.CurTime);
FTLBar.Value = float.IsFinite(progress) ? progress : 1;
}
}

View File

@ -1,3 +1,4 @@
using Content.Shared.DeltaV.Salvage; // DeltaV
using Content.Shared.Lathe;
using Content.Shared.Research.Components;
using JetBrains.Annotations;
@ -31,6 +32,8 @@ namespace Content.Client.Lathe.UI
{
SendMessage(new LatheQueueRecipeMessage(recipe, amount));
};
_menu.OnClaimMiningPoints += () => SendMessage(new LatheClaimMiningPointsMessage()); // DeltaV
}
protected override void UpdateState(BoundUserInterfaceState state)

View File

@ -132,6 +132,12 @@
HorizontalExpand="True">
<ui:MaterialStorageControl Name="MaterialsList" SizeFlagsStretchRatio="8"/>
</BoxContainer>
<!-- Begin DeltaV Additions: Mining points -->
<BoxContainer Orientation="Horizontal" Name="MiningPointsContainer" Visible="False">
<Label Name="MiningPointsLabel" HorizontalExpand="True"/>
<Button Name="MiningPointsClaimButton" Text="{Loc 'lathe-menu-mining-points-claim-button'}"/>
</BoxContainer>
<!-- End DeltaV Additions: Mining points -->
</BoxContainer>
</BoxContainer>

View File

@ -1,16 +1,20 @@
using System.Linq;
using System.Text;
using Content.Client.Materials;
using Content.Shared.DeltaV.Salvage.Components; // DeltaV
using Content.Shared.DeltaV.Salvage.Systems; // DeltaV
using Content.Shared.Lathe;
using Content.Shared.Lathe.Prototypes;
using Content.Shared.Research.Prototypes;
using Robust.Client.AutoGenerated;
using Robust.Client.GameObjects;
using Robust.Client.Player; // DeltaV
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing; // DeltaV
namespace Content.Client.Lathe.UI;
@ -18,14 +22,17 @@ namespace Content.Client.Lathe.UI;
public sealed partial class LatheMenu : DefaultWindow
{
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IPlayerManager _player = default!; // DeltaV
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
private readonly SpriteSystem _spriteSystem;
private readonly LatheSystem _lathe;
private readonly MaterialStorageSystem _materialStorage;
private readonly MiningPointsSystem _miningPoints; // DeltaV
public event Action<BaseButton.ButtonEventArgs>? OnServerListButtonPressed;
public event Action<string, int>? RecipeQueueAction;
public event Action? OnClaimMiningPoints; // DeltaV
public List<ProtoId<LatheRecipePrototype>> Recipes = new();
@ -35,6 +42,8 @@ public sealed partial class LatheMenu : DefaultWindow
public EntityUid Entity;
private uint? _lastMiningPoints; // DeltaV: used to avoid Loc.GetString every frame
public LatheMenu()
{
RobustXamlLoader.Load(this);
@ -43,6 +52,7 @@ public sealed partial class LatheMenu : DefaultWindow
_spriteSystem = _entityManager.System<SpriteSystem>();
_lathe = _entityManager.System<LatheSystem>();
_materialStorage = _entityManager.System<MaterialStorageSystem>();
_miningPoints = _entityManager.System<MiningPointsSystem>(); // DeltaV
SearchBar.OnTextChanged += _ =>
{
@ -70,9 +80,31 @@ public sealed partial class LatheMenu : DefaultWindow
}
}
// Begin DeltaV Additions: Mining points UI
MiningPointsContainer.Visible = _entityManager.TryGetComponent<MiningPointsComponent>(Entity, out var points);
MiningPointsClaimButton.OnPressed += _ => OnClaimMiningPoints?.Invoke();
if (points != null)
UpdateMiningPoints(points.Points);
// End DeltaV Additions
MaterialsList.SetOwner(Entity);
}
/// <summary>
/// DeltaV: Updates the UI elements for mining points.
/// </summary>
private void UpdateMiningPoints(uint points)
{
MiningPointsClaimButton.Disabled = points == 0 ||
_player.LocalSession?.AttachedEntity is not {} player ||
_miningPoints.TryFindIdCard(player) == null;
if (points == _lastMiningPoints)
return;
_lastMiningPoints = points;
MiningPointsLabel.Text = Loc.GetString("lathe-menu-mining-points", ("points", points));
}
protected override void Opened()
{
base.Opened();
@ -83,6 +115,17 @@ public sealed partial class LatheMenu : DefaultWindow
}
}
/// <summary>
/// DeltaV: Update mining points UI whenever it changes.
/// </summary>
protected override void FrameUpdate(FrameEventArgs args)
{
base.FrameUpdate(args);
if (_entityManager.TryGetComponent<MiningPointsComponent>(Entity, out var points))
UpdateMiningPoints(points.Points);
}
/// <summary>
/// Populates the list of all the recipes
/// </summary>

View File

@ -268,6 +268,11 @@ namespace Content.IntegrationTests.Tests
if (protoId == "MobHumanSpaceNinja")
continue;
// TODO fix tests properly upstream
// Fails due to audio components made when making anouncements
if (protoId == "StandardNanotrasenStation")
continue;
var count = server.EntMan.EntityCount;
var clientCount = client.EntMan.EntityCount;
EntityUid uid = default;

View File

@ -9,6 +9,7 @@ using Robust.Server.GameObjects;
using Robust.Shared.Prototypes;
using Content.Shared.Roles;
using System.Diagnostics.CodeAnalysis;
using Content.Shared.DeltaV.NanoChat; // DeltaV
namespace Content.Server.Access.Systems
{
@ -18,6 +19,7 @@ namespace Content.Server.Access.Systems
[Dependency] private readonly IdCardSystem _cardSystem = default!;
[Dependency] private readonly UserInterfaceSystem _uiSystem = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly SharedNanoChatSystem _nanoChat = default!; // DeltaV
public override void Initialize()
{
@ -28,6 +30,17 @@ namespace Content.Server.Access.Systems
SubscribeLocalEvent<AgentIDCardComponent, AgentIDCardNameChangedMessage>(OnNameChanged);
SubscribeLocalEvent<AgentIDCardComponent, AgentIDCardJobChangedMessage>(OnJobChanged);
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)
@ -42,6 +55,34 @@ namespace Content.Server.Access.Systems
access.Tags.UnionWith(targetAccess.Tags);
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)
{
_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))
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);
}

View File

@ -23,7 +23,7 @@ namespace Content.Server.Cargo.Systems;
/// <summary>
/// This handles calculating the price of items, and implements two basic methods of pricing materials.
/// </summary>
public sealed class PricingSystem : EntitySystem
public sealed partial class PricingSystem : EntitySystem // DeltaV - Made partial
{
[Dependency] private readonly IComponentFactory _factory = default!;
[Dependency] private readonly IConsoleHost _consoleHost = default!;
@ -208,7 +208,7 @@ public sealed class PricingSystem : EntitySystem
// TODO: Proper container support.
return price;
return ApplyPrototypePriceModifier(prototype, price); // DeltaV
}
/// <summary>
@ -254,7 +254,7 @@ public sealed class PricingSystem : EntitySystem
}
}
return price;
return ApplyPriceModifier(uid, price); // DeltaV
}
private double GetMaterialsPrice(EntityUid uid)

View File

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

View File

@ -2,13 +2,14 @@ using Content.Shared.Access.Components;
using Content.Shared.Audio;
using Content.Shared.CartridgeLoader;
using Content.Shared.CartridgeLoader.Cartridges;
using Content.Shared.DeltaV.NanoChat; // DeltaV
using Content.Shared.Popups;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Random;
namespace Content.Server.CartridgeLoader.Cartridges;
public sealed class LogProbeCartridgeSystem : EntitySystem
public sealed partial class LogProbeCartridgeSystem : EntitySystem // DeltaV - Made partial
{
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly CartridgeLoaderSystem? _cartridgeLoaderSystem = default!;
@ -18,6 +19,7 @@ public sealed class LogProbeCartridgeSystem : EntitySystem
public override void Initialize()
{
base.Initialize();
InitializeNanoChat(); // DeltaV
SubscribeLocalEvent<LogProbeCartridgeComponent, CartridgeUiReadyEvent>(OnUiReady);
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)
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))
return;
@ -41,6 +52,7 @@ public sealed class LogProbeCartridgeSystem : EntitySystem
_popupSystem.PopupCursor(Loc.GetString("log-probe-scan", ("device", target)), args.InteractEvent.User);
ent.Comp.PulledAccessLogs.Clear();
ent.Comp.ScannedNanoChatData = null; // DeltaV - Clear any previous NanoChat data
foreach (var accessRecord in accessReaderComponent.AccessLog)
{
@ -65,7 +77,7 @@ public sealed class LogProbeCartridgeSystem : EntitySystem
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);
}
}

View File

@ -1,6 +1,8 @@
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
namespace Content.Server.DeltaV.AACTablet;
[RegisterComponent]
[RegisterComponent, AutoGenerateComponentPause]
public sealed partial class AACTabletComponent : Component
{
// Minimum time between each phrase, to prevent spam
@ -8,6 +10,6 @@ public sealed partial class AACTabletComponent : Component
public TimeSpan Cooldown = TimeSpan.FromSeconds(1);
// Time that the next phrase can be sent.
[DataField]
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), AutoPausedField]
public TimeSpan NextPhrase;
}

View File

@ -1,7 +1,6 @@
using Content.Server.Chat.Systems;
using Content.Server.Speech.Components;
using Content.Shared.DeltaV.AACTablet;
using Content.Shared.DeltaV.QuickPhrase;
using Content.Shared.IdentityManagement;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
@ -11,9 +10,8 @@ namespace Content.Server.DeltaV.AACTablet;
public sealed class AACTabletSystem : EntitySystem
{
[Dependency] private readonly ChatSystem _chat = default!;
[Dependency] private readonly ILocalizationManager _loc = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] protected readonly IGameTiming Timing = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IPrototypeManager _prototype = default!;
public override void Initialize()
{
@ -21,30 +19,28 @@ public sealed class AACTabletSystem : EntitySystem
SubscribeLocalEvent<AACTabletComponent, AACTabletSendPhraseMessage>(OnSendPhrase);
}
private void OnSendPhrase(EntityUid uid, AACTabletComponent component, AACTabletSendPhraseMessage message)
private void OnSendPhrase(Entity<AACTabletComponent> ent, ref AACTabletSendPhraseMessage message)
{
if (component.NextPhrase > Timing.CurTime)
if (ent.Comp.NextPhrase > _timing.CurTime)
return;
// the AAC tablet uses the name of the person who pressed the tablet button
// for quality of life
var senderName = Identity.Entity(message.Actor, EntityManager);
var speakerName = Loc.GetString("speech-name-relay",
("speaker", Name(uid)),
("speaker", Name(ent)),
("originalName", senderName));
if (!_prototypeManager.TryIndex<QuickPhrasePrototype>(message.PhraseID, out var phrase))
if (!_prototype.TryIndex(message.PhraseId, out var phrase))
return;
EnsureComp<VoiceOverrideComponent>(uid).NameOverride = speakerName;
EnsureComp<VoiceOverrideComponent>(ent).NameOverride = speakerName;
_chat.TrySendInGameICMessage(uid,
_loc.GetString(phrase.Text),
_chat.TrySendInGameICMessage(ent,
Loc.GetString(phrase.Text),
InGameICChatType.Speak,
hideChat: false,
nameOverride: speakerName);
var curTime = Timing.CurTime;
component.NextPhrase = curTime + component.Cooldown;
var curTime = _timing.CurTime;
ent.Comp.NextPhrase = curTime + ent.Comp.Cooldown;
}
}

View File

@ -0,0 +1,4 @@
namespace Content.Server.DeltaV.Cabinet;
[RegisterComponent]
public sealed partial class SpareIDSafeComponent : Component;

View File

@ -0,0 +1,14 @@
namespace Content.Server.DeltaV.Cargo.Components;
/// <summary>
/// This is used for modifying the sell price of an entity.
/// </summary>
[RegisterComponent]
public sealed partial class PriceModifierComponent : Component
{
/// <summary>
/// The price modifier.
/// </summary>
[DataField]
public float Modifier;
}

View File

@ -0,0 +1,41 @@
using Content.Server.DeltaV.Cargo.Components;
using Robust.Shared.Prototypes;
namespace Content.Server.Cargo.Systems;
public sealed partial class PricingSystem
{
/// <summary>
/// Applies any price modifiers defined in the entity prototype.
/// </summary>
/// <param name="prototype">The entity prototype.</param>
/// <param name="basePrice">The base price before modification.</param>
/// <returns>The modified price.</returns>
private double ApplyPrototypePriceModifier(EntityPrototype prototype, double basePrice)
{
if (prototype.Components.TryGetValue(_factory.GetComponentName(typeof(PriceModifierComponent)),
out var modProto))
{
var priceModifier = (PriceModifierComponent)modProto.Component;
return basePrice * priceModifier.Modifier;
}
return basePrice;
}
/// <summary>
/// Applies any price modifiers to the calculated price.
/// </summary>
/// <param name="uid">The entity whose price is being modified.</param>
/// <param name="basePrice">The base price before modification.</param>
/// <returns>The modified price.</returns>
private double ApplyPriceModifier(EntityUid uid, double basePrice)
{
if (TryComp<PriceModifierComponent>(uid, out var modifier))
{
return basePrice * modifier.Modifier;
}
return basePrice;
}
}

View File

@ -0,0 +1,82 @@
using Content.Shared.Audio;
using Content.Shared.CartridgeLoader;
using Content.Shared.DeltaV.CartridgeLoader.Cartridges;
using Content.Shared.DeltaV.NanoChat;
namespace Content.Server.CartridgeLoader.Cartridges;
public sealed partial class LogProbeCartridgeSystem
{
private void InitializeNanoChat()
{
SubscribeLocalEvent<NanoChatRecipientUpdatedEvent>(OnRecipientUpdated);
SubscribeLocalEvent<NanoChatMessageReceivedEvent>(OnMessageReceived);
}
private void OnRecipientUpdated(ref NanoChatRecipientUpdatedEvent args)
{
var query = EntityQueryEnumerator<LogProbeCartridgeComponent, CartridgeComponent>();
while (query.MoveNext(out var uid, out var probe, out var cartridge))
{
if (probe.ScannedNanoChatData == null || GetEntity(probe.ScannedNanoChatData.Value.Card) != args.CardUid)
continue;
if (!TryComp<NanoChatCardComponent>(args.CardUid, out var card))
continue;
probe.ScannedNanoChatData = new NanoChatData(
new Dictionary<uint, NanoChatRecipient>(card.Recipients),
probe.ScannedNanoChatData.Value.Messages,
card.Number,
GetNetEntity(args.CardUid));
if (cartridge.LoaderUid != null)
UpdateUiState((uid, probe), cartridge.LoaderUid.Value);
}
}
private void OnMessageReceived(ref NanoChatMessageReceivedEvent args)
{
var query = EntityQueryEnumerator<LogProbeCartridgeComponent, CartridgeComponent>();
while (query.MoveNext(out var uid, out var probe, out var cartridge))
{
if (probe.ScannedNanoChatData == null || GetEntity(probe.ScannedNanoChatData.Value.Card) != args.CardUid)
continue;
if (!TryComp<NanoChatCardComponent>(args.CardUid, out var card))
continue;
probe.ScannedNanoChatData = new NanoChatData(
probe.ScannedNanoChatData.Value.Recipients,
new Dictionary<uint, List<NanoChatMessage>>(card.Messages),
card.Number,
GetNetEntity(args.CardUid));
if (cartridge.LoaderUid != null)
UpdateUiState((uid, probe), cartridge.LoaderUid.Value);
}
}
private void ScanNanoChatCard(Entity<LogProbeCartridgeComponent> ent,
CartridgeAfterInteractEvent args,
EntityUid target,
NanoChatCardComponent card)
{
_audioSystem.PlayEntity(ent.Comp.SoundScan,
args.InteractEvent.User,
target,
AudioHelpers.WithVariation(0.25f, _random));
_popupSystem.PopupCursor(Loc.GetString("log-probe-scan-nanochat", ("card", target)), args.InteractEvent.User);
ent.Comp.PulledAccessLogs.Clear();
ent.Comp.ScannedNanoChatData = new NanoChatData(
new Dictionary<uint, NanoChatRecipient>(card.Recipients),
new Dictionary<uint, List<NanoChatMessage>>(card.Messages),
card.Number,
GetNetEntity(target)
);
UpdateUiState(ent, args.Loader);
}
}

View File

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

View File

@ -0,0 +1,514 @@
using System.Linq;
using Content.Server.Administration.Logs;
using Content.Server.CartridgeLoader;
using Content.Server.Power.Components;
using Content.Server.Radio;
using Content.Server.Radio.Components;
using Content.Server.Station.Systems;
using Content.Shared.Access.Components;
using Content.Shared.CartridgeLoader;
using Content.Shared.Database;
using Content.Shared.DeltaV.CartridgeLoader.Cartridges;
using Content.Shared.DeltaV.NanoChat;
using Content.Shared.PDA;
using Content.Shared.Radio.Components;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
namespace Content.Server.DeltaV.CartridgeLoader.Cartridges;
public sealed class NanoChatCartridgeSystem : EntitySystem
{
[Dependency] private readonly CartridgeLoaderSystem _cartridge = default!;
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IPrototypeManager _prototype = default!;
[Dependency] private readonly SharedNanoChatSystem _nanoChat = default!;
[Dependency] private readonly StationSystem _station = default!;
// Messages in notifications get cut off after this point
// no point in storing it on the comp
private const int NotificationMaxLength = 64;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<NanoChatCartridgeComponent, CartridgeUiReadyEvent>(OnUiReady);
SubscribeLocalEvent<NanoChatCartridgeComponent, CartridgeMessageEvent>(OnMessage);
}
public override void Update(float frameTime)
{
base.Update(frameTime);
// Update card references for any cartridges that need it
var query = EntityQueryEnumerator<NanoChatCartridgeComponent, CartridgeComponent>();
while (query.MoveNext(out var uid, out var nanoChat, out var cartridge))
{
if (cartridge.LoaderUid == null)
continue;
// Check if we need to update our card reference
if (!TryComp<PdaComponent>(cartridge.LoaderUid, out var pda))
continue;
var newCard = pda.ContainedId;
var currentCard = nanoChat.Card;
// If the cards match, nothing to do
if (newCard == currentCard)
continue;
// Update card reference
nanoChat.Card = newCard;
// Update UI state since card reference changed
UpdateUI((uid, nanoChat), cartridge.LoaderUid.Value);
}
}
/// <summary>
/// Handles incoming UI messages from the NanoChat cartridge.
/// </summary>
private void OnMessage(Entity<NanoChatCartridgeComponent> ent, ref CartridgeMessageEvent args)
{
if (args is not NanoChatUiMessageEvent msg)
return;
if (!GetCardEntity(GetEntity(args.LoaderUid), out var card))
return;
switch (msg.Type)
{
case NanoChatUiMessageType.NewChat:
HandleNewChat(card, msg);
break;
case NanoChatUiMessageType.SelectChat:
HandleSelectChat(card, msg);
break;
case NanoChatUiMessageType.CloseChat:
HandleCloseChat(card);
break;
case NanoChatUiMessageType.ToggleMute:
HandleToggleMute(card);
break;
case NanoChatUiMessageType.DeleteChat:
HandleDeleteChat(card, msg);
break;
case NanoChatUiMessageType.SendMessage:
HandleSendMessage(ent, card, msg);
break;
}
UpdateUI(ent, GetEntity(args.LoaderUid));
}
/// <summary>
/// Gets the ID card entity associated with a PDA.
/// </summary>
/// <param name="loaderUid">The PDA entity ID</param>
/// <param name="card">Output parameter containing the found card entity and component</param>
/// <returns>True if a valid NanoChat card was found</returns>
private bool GetCardEntity(
EntityUid loaderUid,
out Entity<NanoChatCardComponent> card)
{
card = default;
// Get the PDA and check if it has an ID card
if (!TryComp<PdaComponent>(loaderUid, out var pda) ||
pda.ContainedId == null ||
!TryComp<NanoChatCardComponent>(pda.ContainedId, out var idCard))
return false;
card = (pda.ContainedId.Value, idCard);
return true;
}
/// <summary>
/// Handles creation of a new chat conversation.
/// </summary>
private void HandleNewChat(Entity<NanoChatCardComponent> card, NanoChatUiMessageEvent msg)
{
if (msg.RecipientNumber == null || msg.Content == null || msg.RecipientNumber == card.Comp.Number)
return;
// Add new recipient
var recipient = new NanoChatRecipient(msg.RecipientNumber.Value,
msg.Content,
msg.RecipientJob);
// Initialize or update recipient
_nanoChat.SetRecipient((card, card.Comp), msg.RecipientNumber.Value, recipient);
_adminLogger.Add(LogType.Action,
LogImpact.Low,
$"{ToPrettyString(msg.Actor):user} created new NanoChat conversation with #{msg.RecipientNumber:D4} ({msg.Content})");
var recipientEv = new NanoChatRecipientUpdatedEvent(card);
RaiseLocalEvent(ref recipientEv);
UpdateUIForCard(card);
}
/// <summary>
/// Handles selecting a chat conversation.
/// </summary>
private void HandleSelectChat(Entity<NanoChatCardComponent> card, NanoChatUiMessageEvent msg)
{
if (msg.RecipientNumber == null)
return;
_nanoChat.SetCurrentChat((card, card.Comp), msg.RecipientNumber);
// Clear unread flag when selecting chat
if (_nanoChat.GetRecipient((card, card.Comp), msg.RecipientNumber.Value) is { } recipient)
{
_nanoChat.SetRecipient((card, card.Comp),
msg.RecipientNumber.Value,
recipient with { HasUnread = false });
}
}
/// <summary>
/// Handles closing the current chat conversation.
/// </summary>
private void HandleCloseChat(Entity<NanoChatCardComponent> card)
{
_nanoChat.SetCurrentChat((card, card.Comp), null);
}
/// <summary>
/// Handles deletion of a chat conversation.
/// </summary>
private void HandleDeleteChat(Entity<NanoChatCardComponent> card, NanoChatUiMessageEvent msg)
{
if (msg.RecipientNumber == null || card.Comp.Number == null)
return;
// Delete chat but keep the messages
var deleted = _nanoChat.TryDeleteChat((card, card.Comp), msg.RecipientNumber.Value, true);
if (!deleted)
return;
_adminLogger.Add(LogType.Action,
LogImpact.Low,
$"{ToPrettyString(msg.Actor):user} deleted NanoChat conversation with #{msg.RecipientNumber:D4}");
UpdateUIForCard(card);
}
/// <summary>
/// Handles toggling notification mute state.
/// </summary>
private void HandleToggleMute(Entity<NanoChatCardComponent> card)
{
_nanoChat.SetNotificationsMuted((card, card.Comp), !_nanoChat.GetNotificationsMuted((card, card.Comp)));
UpdateUIForCard(card);
}
/// <summary>
/// Handles sending a new message in a chat conversation.
/// </summary>
private void HandleSendMessage(Entity<NanoChatCartridgeComponent> cartridge,
Entity<NanoChatCardComponent> card,
NanoChatUiMessageEvent msg)
{
if (msg.RecipientNumber == null || msg.Content == null || card.Comp.Number == null)
return;
if (!EnsureRecipientExists(card, msg.RecipientNumber.Value))
return;
// Create and store message for sender
var message = new NanoChatMessage(
_timing.CurTime,
msg.Content,
(uint)card.Comp.Number
);
// Attempt delivery
var (deliveryFailed, recipients) = AttemptMessageDelivery(cartridge, msg.RecipientNumber.Value);
// Update delivery status
message = message with { DeliveryFailed = deliveryFailed };
// Store message in sender's outbox under recipient's number
_nanoChat.AddMessage((card, card.Comp), msg.RecipientNumber.Value, message);
// Log message attempt
var recipientsText = recipients.Count > 0
? string.Join(", ", recipients.Select(r => ToPrettyString(r)))
: $"#{msg.RecipientNumber:D4}";
_adminLogger.Add(LogType.Chat,
LogImpact.Low,
$"{ToPrettyString(card):user} sent NanoChat message to {recipientsText}: {msg.Content}{(deliveryFailed ? " [DELIVERY FAILED]" : "")}");
var msgEv = new NanoChatMessageReceivedEvent(card);
RaiseLocalEvent(ref msgEv);
if (deliveryFailed)
return;
foreach (var recipient in recipients)
{
DeliverMessageToRecipient(card, recipient, message);
}
}
/// <summary>
/// Ensures a recipient exists in the sender's contacts.
/// </summary>
/// <param name="card">The card to check contacts for</param>
/// <param name="recipientNumber">The recipient's number to check</param>
/// <returns>True if the recipient exists or was created successfully</returns>
private bool EnsureRecipientExists(Entity<NanoChatCardComponent> card, uint recipientNumber)
{
return _nanoChat.EnsureRecipientExists((card, card.Comp), recipientNumber, GetCardInfo(recipientNumber));
}
/// <summary>
/// Attempts to deliver a message to recipients.
/// </summary>
/// <param name="sender">The sending cartridge entity</param>
/// <param name="recipientNumber">The recipient's number</param>
/// <returns>Tuple containing delivery status and recipients if found.</returns>
private (bool failed, List<Entity<NanoChatCardComponent>> recipient) AttemptMessageDelivery(
Entity<NanoChatCartridgeComponent> sender,
uint recipientNumber)
{
// First verify we can send from this device
var channel = _prototype.Index(sender.Comp.RadioChannel);
var sendAttemptEvent = new RadioSendAttemptEvent(channel, sender);
RaiseLocalEvent(ref sendAttemptEvent);
if (sendAttemptEvent.Cancelled)
return (true, new List<Entity<NanoChatCardComponent>>());
var foundRecipients = new List<Entity<NanoChatCardComponent>>();
// Find all cards with matching number
var cardQuery = EntityQueryEnumerator<NanoChatCardComponent>();
while (cardQuery.MoveNext(out var cardUid, out var card))
{
if (card.Number != recipientNumber)
continue;
foundRecipients.Add((cardUid, card));
}
if (foundRecipients.Count == 0)
return (true, foundRecipients);
// Now check if any of these cards can receive
var deliverableRecipients = new List<Entity<NanoChatCardComponent>>();
foreach (var recipient in foundRecipients)
{
// Find any cartridges that have this card
var cartridgeQuery = EntityQueryEnumerator<NanoChatCartridgeComponent, ActiveRadioComponent>();
while (cartridgeQuery.MoveNext(out var receiverUid, out var receiverCart, out _))
{
if (receiverCart.Card != recipient.Owner)
continue;
// Check if devices are on same station/map
var recipientStation = _station.GetOwningStation(receiverUid);
var senderStation = _station.GetOwningStation(sender);
// Both entities must be on a station
if (recipientStation == null || senderStation == null)
continue;
// Must be on same map/station unless long range allowed
if (!channel.LongRange && recipientStation != senderStation)
continue;
// Needs telecomms
if (!HasActiveServer(senderStation.Value) || !HasActiveServer(recipientStation.Value))
continue;
// Check if recipient can receive
var receiveAttemptEv = new RadioReceiveAttemptEvent(channel, sender, receiverUid);
RaiseLocalEvent(ref receiveAttemptEv);
if (receiveAttemptEv.Cancelled)
continue;
// Found valid cartridge that can receive
deliverableRecipients.Add(recipient);
break; // Only need one valid cartridge per card
}
}
return (deliverableRecipients.Count == 0, deliverableRecipients);
}
/// <summary>
/// Checks if there are any active telecomms servers on the given station
/// </summary>
private bool HasActiveServer(EntityUid station)
{
// I have no idea why this isn't public in the RadioSystem
var query =
EntityQueryEnumerator<TelecomServerComponent, EncryptionKeyHolderComponent, ApcPowerReceiverComponent>();
while (query.MoveNext(out var uid, out _, out _, out var power))
{
if (_station.GetOwningStation(uid) == station && power.Powered)
return true;
}
return false;
}
/// <summary>
/// Delivers a message to the recipient and handles associated notifications.
/// </summary>
/// <param name="sender">The sender's card entity</param>
/// <param name="recipient">The recipient's card entity</param>
/// <param name="message">The <see cref="NanoChatMessage" /> to deliver</param>
private void DeliverMessageToRecipient(Entity<NanoChatCardComponent> sender,
Entity<NanoChatCardComponent> recipient,
NanoChatMessage message)
{
var senderNumber = sender.Comp.Number;
if (senderNumber == null)
return;
// Always try to get and add sender info to recipient's contacts
if (!EnsureRecipientExists(recipient, senderNumber.Value))
return;
_nanoChat.AddMessage((recipient, recipient.Comp), senderNumber.Value, message with { DeliveryFailed = false });
if (_nanoChat.GetCurrentChat((recipient, recipient.Comp)) != senderNumber)
HandleUnreadNotification(recipient, message);
var msgEv = new NanoChatMessageReceivedEvent(recipient);
RaiseLocalEvent(ref msgEv);
UpdateUIForCard(recipient);
}
/// <summary>
/// Handles unread message notifications and updates unread status.
/// </summary>
private void HandleUnreadNotification(Entity<NanoChatCardComponent> recipient, NanoChatMessage message)
{
// Get sender name from contacts or fall back to number
var recipients = _nanoChat.GetRecipients((recipient, recipient.Comp));
var senderName = recipients.TryGetValue(message.SenderId, out var existingRecipient)
? existingRecipient.Name
: $"#{message.SenderId:D4}";
if (!recipient.Comp.Recipients[message.SenderId].HasUnread && !recipient.Comp.NotificationsMuted)
{
var pdaQuery = EntityQueryEnumerator<PdaComponent>();
while (pdaQuery.MoveNext(out var pdaUid, out var pdaComp))
{
if (pdaComp.ContainedId != recipient)
continue;
_cartridge.SendNotification(pdaUid,
Loc.GetString("nano-chat-new-message-title", ("sender", senderName)),
Loc.GetString("nano-chat-new-message-body", ("message", TruncateMessage(message.Content))));
break;
}
}
// Update unread status
_nanoChat.SetRecipient((recipient, recipient.Comp),
message.SenderId,
existingRecipient with { HasUnread = true });
}
/// <summary>
/// Updates the UI for any PDAs containing the specified card.
/// </summary>
private void UpdateUIForCard(EntityUid cardUid)
{
// Find any PDA containing this card and update its UI
var query = EntityQueryEnumerator<NanoChatCartridgeComponent, CartridgeComponent>();
while (query.MoveNext(out var uid, out var comp, out var cartridge))
{
if (comp.Card != cardUid || cartridge.LoaderUid == null)
continue;
UpdateUI((uid, comp), cartridge.LoaderUid.Value);
}
}
/// <summary>
/// Gets the <see cref="NanoChatRecipient" /> for a given NanoChat number.
/// </summary>
private NanoChatRecipient? GetCardInfo(uint number)
{
// Find card with this number to get its info
var query = EntityQueryEnumerator<NanoChatCardComponent>();
while (query.MoveNext(out var uid, out var card))
{
if (card.Number != number)
continue;
// Try to get job title from ID card if possible
string? jobTitle = null;
var name = "Unknown";
if (TryComp<IdCardComponent>(uid, out var idCard))
{
jobTitle = idCard.LocalizedJobTitle;
name = idCard.FullName ?? name;
}
return new NanoChatRecipient(number, name, jobTitle);
}
return null;
}
/// <summary>
/// Truncates a message to the notification maximum length.
/// </summary>
private static string TruncateMessage(string message)
{
return message.Length <= NotificationMaxLength
? message
: message[..(NotificationMaxLength - 4)] + " [...]";
}
private void OnUiReady(Entity<NanoChatCartridgeComponent> ent, ref CartridgeUiReadyEvent args)
{
_cartridge.RegisterBackgroundProgram(args.Loader, ent);
UpdateUI(ent, args.Loader);
}
private void UpdateUI(Entity<NanoChatCartridgeComponent> ent, EntityUid loader)
{
if (_station.GetOwningStation(loader) is { } station)
ent.Comp.Station = station;
var recipients = new Dictionary<uint, NanoChatRecipient>();
var messages = new Dictionary<uint, List<NanoChatMessage>>();
uint? currentChat = null;
uint ownNumber = 0;
var maxRecipients = 50;
var notificationsMuted = false;
if (ent.Comp.Card != null && TryComp<NanoChatCardComponent>(ent.Comp.Card, out var card))
{
recipients = card.Recipients;
messages = card.Messages;
currentChat = card.CurrentChat;
ownNumber = card.Number ?? 0;
maxRecipients = card.MaxRecipients;
notificationsMuted = card.NotificationsMuted;
}
var state = new NanoChatUiState(recipients,
messages,
currentChat,
ownNumber,
maxRecipients,
notificationsMuted);
_cartridge.UpdateCartridgeUiState(loader, state);
}
}

View File

@ -0,0 +1,130 @@
using System.Linq;
using Content.Server.Access.Systems;
using Content.Server.Administration.Logs;
using Content.Server.Kitchen.Components;
using Content.Server.NameIdentifier;
using Content.Shared.Database;
using Content.Shared.DeltaV.CartridgeLoader.Cartridges;
using Content.Shared.DeltaV.NanoChat;
using Content.Shared.NameIdentifier;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
namespace Content.Server.DeltaV.NanoChat;
/// <summary>
/// Handles NanoChat features that are specific to the server but not related to the cartridge itself.
/// </summary>
public sealed class NanoChatSystem : SharedNanoChatSystem
{
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly NameIdentifierSystem _name = default!;
private readonly ProtoId<NameIdentifierGroupPrototype> _nameIdentifierGroup = "NanoChat";
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<NanoChatCardComponent, MapInitEvent>(OnCardInit);
SubscribeLocalEvent<NanoChatCardComponent, BeingMicrowavedEvent>(OnMicrowaved, after: [typeof(IdCardSystem)]);
}
private void OnMicrowaved(Entity<NanoChatCardComponent> ent, ref BeingMicrowavedEvent args)
{
// Skip if the entity was deleted (e.g., by ID card system burning it)
if (Deleted(ent))
return;
if (!TryComp<MicrowaveComponent>(args.Microwave, out var micro) || micro.Broken)
return;
var randomPick = _random.NextFloat();
// Super lucky - erase all messages (10% chance)
if (randomPick <= 0.10f)
{
ent.Comp.Messages.Clear();
// TODO: these shouldn't be shown at the same time as the popups from IdCardSystem
// _popup.PopupEntity(Loc.GetString("nanochat-card-microwave-erased", ("card", ent)),
// ent,
// PopupType.Medium);
_adminLogger.Add(LogType.Action,
LogImpact.Medium,
$"{ToPrettyString(args.Microwave)} erased all messages on {ToPrettyString(ent)}");
}
else
{
// Scramble random messages for random recipients
ScrambleMessages(ent);
// _popup.PopupEntity(Loc.GetString("nanochat-card-microwave-scrambled", ("card", ent)),
// ent,
// PopupType.Medium);
_adminLogger.Add(LogType.Action,
LogImpact.Medium,
$"{ToPrettyString(args.Microwave)} scrambled messages on {ToPrettyString(ent)}");
}
Dirty(ent);
}
private void ScrambleMessages(NanoChatCardComponent component)
{
foreach (var (recipientNumber, messages) in component.Messages)
{
for (var i = 0; i < messages.Count; i++)
{
// 50% chance to scramble each message
if (!_random.Prob(0.5f))
continue;
var message = messages[i];
message.Content = ScrambleText(message.Content);
messages[i] = message;
}
// 25% chance to reassign the conversation to a random recipient
if (_random.Prob(0.25f) && component.Recipients.Count > 0)
{
var newRecipient = _random.Pick(component.Recipients.Keys.ToList());
if (newRecipient == recipientNumber)
continue;
if (!component.Messages.ContainsKey(newRecipient))
component.Messages[newRecipient] = new List<NanoChatMessage>();
component.Messages[newRecipient].AddRange(messages);
component.Messages[recipientNumber].Clear();
}
}
}
private string ScrambleText(string text)
{
var chars = text.ToCharArray();
var n = chars.Length;
// Fisher-Yates shuffle of characters
while (n > 1)
{
n--;
var k = _random.Next(n + 1);
(chars[k], chars[n]) = (chars[n], chars[k]);
}
return new string(chars);
}
private void OnCardInit(Entity<NanoChatCardComponent> ent, ref MapInitEvent args)
{
if (ent.Comp.Number != null)
return;
// Assign a random number
_name.GenerateUniqueName(ent, _nameIdentifierGroup, out var number);
ent.Comp.Number = (uint)number;
Dirty(ent);
}
}

View File

@ -1,6 +1,7 @@
using Content.Server.Objectives.Systems;
using Content.Server.DeltaV.Objectives.Systems;
using Content.Server.Objectives.Components;
namespace Content.Server.Objectives.Components;
namespace Content.Server.DeltaV.Objectives.Components;
/// <summary>
/// Requires that a target dies once and only once.

View File

@ -1,48 +1,46 @@
using Content.Server.Objectives.Components;
using Content.Shared.GameTicking;
using Content.Server.DeltaV.Objectives.Components;
using Content.Server.Objectives.Components;
using Content.Server.Objectives.Systems;
using Content.Shared.Mind;
using Content.Shared.Objectives.Components;
using Content.Shared.Mobs;
namespace Content.Server.Objectives.Systems;
namespace Content.Server.DeltaV.Objectives.Systems;
/// <summary>
/// Handles teach a lesson condition logic, does not assign target.
/// </summary>
public sealed class TeachLessonConditionSystem : EntitySystem
{
[Dependency] private readonly CodeConditionSystem _codeCondition = default!;
[Dependency] private readonly SharedMindSystem _mind = default!;
[Dependency] private readonly TargetObjectiveSystem _target = default!;
private readonly List<EntityUid> _wasKilled = [];
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<TeachLessonConditionComponent, ObjectiveGetProgressEvent>(OnGetProgress);
SubscribeLocalEvent<RoundRestartCleanupEvent>(OnRoundEnd);
SubscribeLocalEvent<MobStateChangedEvent>(OnMobStateChanged);
}
private void OnGetProgress(Entity<TeachLessonConditionComponent> ent, ref ObjectiveGetProgressEvent args)
// TODO: subscribe by ref at some point in the future
private void OnMobStateChanged(MobStateChangedEvent args)
{
if (!_target.GetTarget(ent, out var target))
if (args.NewMobState != MobState.Dead)
return;
args.Progress = GetProgress(target.Value);
}
// Get the mind of the entity that just died (if it has one)
if (!_mind.TryGetMind(args.Target, out var mindId, out _))
return;
private float GetProgress(EntityUid target)
{
if (TryComp<MindComponent>(target, out var mind) && mind.OwnedEntity != null && !_mind.IsCharacterDeadIc(mind))
return _wasKilled.Contains(target) ? 1f : 0f;
// Get all TeachLessonConditionComponent entities
var query = EntityQueryEnumerator<TeachLessonConditionComponent, TargetObjectiveComponent>();
_wasKilled.Add(target);
return 1f;
}
while (query.MoveNext(out var uid, out _, out var targetObjective))
{
// Check if this objective's target matches the entity that died
if (targetObjective.Target != mindId)
continue;
// Clear the wasKilled list on round end
private void OnRoundEnd(RoundRestartCleanupEvent ev)
{
_wasKilled.Clear();
_codeCondition.SetCompleted(uid);
}
}
}

View File

@ -0,0 +1,76 @@
using Content.Server.Atmos.EntitySystems;
using Content.Server.Parallax;
using Content.Shared.DeltaV.Planet;
using Content.Shared.Parallax.Biomes;
using Robust.Server.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Prototypes;
namespace Content.Server.DeltaV.Planet;
public sealed class PlanetSystem : EntitySystem
{
[Dependency] private readonly AtmosphereSystem _atmos = default!;
[Dependency] private readonly BiomeSystem _biome = default!;
[Dependency] private readonly IPrototypeManager _proto = default!;
[Dependency] private readonly MapSystem _map = default!;
[Dependency] private readonly MapLoaderSystem _mapLoader = default!;
[Dependency] private readonly MetaDataSystem _meta = default!;
private readonly List<(Vector2i, Tile)> _setTiles = new();
/// <summary>
/// Spawn a planet map from a planet prototype.
/// </summary>
public EntityUid SpawnPlanet(ProtoId<PlanetPrototype> id, bool runMapInit = true)
{
var planet = _proto.Index(id);
var map = _map.CreateMap(out _, runMapInit: runMapInit);
_biome.EnsurePlanet(map, _proto.Index(planet.Biome), mapLight: planet.MapLight);
// add each marker layer
var biome = Comp<BiomeComponent>(map);
foreach (var layer in planet.BiomeMarkerLayers)
{
_biome.AddMarkerLayer(map, biome, layer);
}
if (planet.AddedComponents is {} added)
EntityManager.AddComponents(map, added);
_atmos.SetMapAtmosphere(map, false, planet.Atmosphere);
_meta.SetEntityName(map, Loc.GetString(planet.MapName));
return map;
}
/// <summary>
/// Spawns an initialized planet map from a planet prototype and loads a grid onto it.
/// Returns the map entity if loading succeeded.
/// </summary>
public EntityUid? LoadPlanet(ProtoId<PlanetPrototype> id, string path)
{
var map = SpawnPlanet(id, runMapInit: false);
var mapId = Comp<MapComponent>(map).MapId;
if (!_mapLoader.TryLoad(mapId, path, out var grids))
{
Log.Error($"Failed to load planet grid {path} for planet {id}!");
Del(map);
return null;
}
// don't want rocks spawning inside the base
foreach (var gridUid in grids)
{
_setTiles.Clear();
var aabb = Comp<MapGridComponent>(gridUid).LocalAABB;
_biome.ReserveTiles(map, aabb.Enlarged(0.2f), _setTiles);
}
_map.InitializeMap(map);
return map;
}
}

View File

@ -0,0 +1,176 @@
using Content.Server.Shuttles.Components;
using Content.Server.Shuttles.Events;
using Content.Server.Shuttles.Systems;
using Content.Server.Station.Components;
using Content.Server.Station.Systems;
using Content.Shared.DeltaV.Shuttles;
using Content.Shared.DeltaV.Shuttles.Components;
using Content.Shared.DeltaV.Shuttles.Systems;
using Content.Shared.Shuttles.Components;
using Content.Shared.Shuttles.Systems;
using Content.Shared.Timing;
using Content.Shared.Whitelist;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
namespace Content.Server.DeltaV.Shuttles.Systems;
public sealed class DockingConsoleSystem : SharedDockingConsoleSystem
{
[Dependency] private readonly EntityWhitelistSystem _whitelist = default!;
[Dependency] private readonly SharedMapSystem _map = default!;
[Dependency] private readonly SharedUserInterfaceSystem _ui = default!;
[Dependency] private readonly ShuttleSystem _shuttle = default!;
[Dependency] private readonly StationSystem _station = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<DockEvent>(OnDock);
SubscribeLocalEvent<UndockEvent>(OnUndock);
Subs.BuiEvents<DockingConsoleComponent>(DockingConsoleUiKey.Key, subs =>
{
subs.Event<BoundUIOpenedEvent>(OnOpened);
subs.Event<DockingConsoleFTLMessage>(OnFTL);
});
}
private void OnDock(DockEvent args)
{
UpdateConsoles(args.GridAUid, args.GridBUid);
}
private void OnUndock(UndockEvent args)
{
UpdateConsoles(args.GridAUid, args.GridBUid);
}
private void OnOpened(Entity<DockingConsoleComponent> ent, ref BoundUIOpenedEvent args)
{
if (TerminatingOrDeleted(ent.Comp.Shuttle))
UpdateShuttle(ent);
UpdateUI(ent);
}
private void UpdateConsoles(EntityUid gridA, EntityUid gridB)
{
UpdateConsolesUsing(gridA);
UpdateConsolesUsing(gridB);
}
/// <summary>
/// Update the UI of every console that is using a certain shuttle.
/// </summary>
public void UpdateConsolesUsing(EntityUid shuttle)
{
if (!HasComp<DockingShuttleComponent>(shuttle))
return;
var query = EntityQueryEnumerator<DockingConsoleComponent>();
while (query.MoveNext(out var uid, out var comp))
{
if (comp.Shuttle == shuttle)
UpdateUI((uid, comp));
}
}
private void UpdateUI(Entity<DockingConsoleComponent> ent)
{
if (ent.Comp.Shuttle is not {} shuttle)
return;
var ftlState = FTLState.Available;
StartEndTime ftlTime = default;
List<DockingDestination> destinations = new();
if (TryComp<FTLComponent>(shuttle, out var ftl))
{
ftlState = ftl.State;
ftlTime = _shuttle.GetStateTime(ftl);
}
if (TryComp<DockingShuttleComponent>(shuttle, out var docking))
{
destinations = docking.Destinations;
}
var state = new DockingConsoleState(ftlState, ftlTime, destinations);
_ui.SetUiState(ent.Owner, DockingConsoleUiKey.Key, state);
}
private void OnFTL(Entity<DockingConsoleComponent> ent, ref DockingConsoleFTLMessage args)
{
if (ent.Comp.Shuttle is not {} shuttle || !TryComp<DockingShuttleComponent>(shuttle, out var docking))
return;
if (args.Index < 0 || args.Index > docking.Destinations.Count)
return;
var dest = docking.Destinations[args.Index];
var map = dest.Map;
// can't FTL if its already there or somehow failed whitelist
if (map == Transform(shuttle).MapID || !_shuttle.CanFTLTo(shuttle, map, ent))
return;
if (FindLargestGrid(map) is not {} grid)
return;
Log.Debug($"{ToPrettyString(args.Actor):user} is FTL-docking {ToPrettyString(shuttle):shuttle} to {ToPrettyString(grid):grid}");
_shuttle.FTLToDock(shuttle, Comp<ShuttleComponent>(shuttle), grid, priorityTag: ent.Comp.DockTag);
}
private EntityUid? FindLargestGrid(MapId map)
{
EntityUid? largestGrid = null;
var largestSize = 0f;
if (_station.GetStationInMap(map) is {} station)
{
// prevent picking vgroid and stuff
return _station.GetLargestGrid(Comp<StationDataComponent>(station));
}
var query = EntityQueryEnumerator<MapGridComponent, TransformComponent>();
while (query.MoveNext(out var gridUid, out var grid, out var xform))
{
if (xform.MapID != map)
continue;
var size = grid.LocalAABB.Size.LengthSquared();
if (size < largestSize)
continue;
largestSize = size;
largestGrid = gridUid;
}
return largestGrid;
}
private void UpdateShuttle(Entity<DockingConsoleComponent> ent)
{
var hadShuttle = ent.Comp.HasShuttle;
// no error if it cant find one since it would fail every test as shuttle.grid_fill is false in dev
ent.Comp.Shuttle = FindShuttle(ent.Comp.ShuttleWhitelist);
ent.Comp.HasShuttle = ent.Comp.Shuttle != null;
if (ent.Comp.HasShuttle != hadShuttle)
Dirty(ent);
}
private EntityUid? FindShuttle(EntityWhitelist whitelist)
{
var query = EntityQueryEnumerator<DockingShuttleComponent>();
while (query.MoveNext(out var uid, out _))
{
if (_whitelist.IsValid(whitelist, uid))
return uid;
}
return null;
}
}

View File

@ -0,0 +1,79 @@
using Content.Server.Shuttles.Events;
using Content.Server.Station.Components;
using Content.Server.Station.Systems;
using Content.Shared.DeltaV.Shuttles.Components;
using Content.Shared.DeltaV.Shuttles.Systems;
using Content.Shared.Shuttles.Components;
using Content.Shared.Whitelist;
using Robust.Shared.Map.Components;
using System.Linq;
namespace Content.Server.DeltaV.Shuttles.Systems;
public sealed class DockingShuttleSystem : SharedDockingShuttleSystem
{
[Dependency] private readonly DockingConsoleSystem _console = default!;
[Dependency] private readonly EntityWhitelistSystem _whitelist = default!;
[Dependency] private readonly StationSystem _station = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<DockingShuttleComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<DockingShuttleComponent, FTLStartedEvent>(OnFTLStarted);
SubscribeLocalEvent<DockingShuttleComponent, FTLCompletedEvent>(OnFTLCompleted);
SubscribeLocalEvent<StationGridAddedEvent>(OnStationGridAdded);
}
private void OnMapInit(Entity<DockingShuttleComponent> ent, ref MapInitEvent args)
{
// add any whitelisted destinations that it can FTL to
// since it needs a whitelist, this excludes the station
var query = EntityQueryEnumerator<FTLDestinationComponent, MapComponent>();
while (query.MoveNext(out var mapUid, out var dest, out var map))
{
if (!dest.Enabled || _whitelist.IsWhitelistFailOrNull(dest.Whitelist, ent))
continue;
ent.Comp.Destinations.Add(new DockingDestination()
{
Name = Name(mapUid),
Map = map.MapId
});
}
}
private void OnFTLStarted(Entity<DockingShuttleComponent> ent, ref FTLStartedEvent args)
{
_console.UpdateConsolesUsing(ent);
}
private void OnFTLCompleted(Entity<DockingShuttleComponent> ent, ref FTLCompletedEvent args)
{
_console.UpdateConsolesUsing(ent);
}
private void OnStationGridAdded(StationGridAddedEvent args)
{
var uid = args.GridId;
if (!TryComp<DockingShuttleComponent>(uid, out var comp))
return;
// only add the destination once
if (comp.Station != null)
return;
if (_station.GetOwningStation(uid) is not {} station || !TryComp<StationDataComponent>(station, out var data))
return;
// add the source station as a destination
comp.Station = station;
comp.Destinations.Add(new DockingDestination()
{
Name = Name(station),
Map = Transform(data.Grids.First()).MapID
});
}
}

View File

@ -0,0 +1,64 @@
using Content.Server.DeltaV.Station.Systems;
using Content.Server.Station.Systems;
using Content.Shared.Access;
using Robust.Shared.Prototypes;
namespace Content.Server.DeltaV.Station.Components;
/// <summary>
/// Denotes a station has no captain and holds data for automatic ACO systems
/// </summary>
[RegisterComponent, Access(typeof(CaptainStateSystem), typeof(StationSystem))]
public sealed partial class CaptainStateComponent : Component
{
/// <summary>
/// Denotes wether the entity has a captain or not
/// </summary>
/// <remarks>
/// Assume no captain unless specified
/// </remarks>
[DataField]
public bool HasCaptain;
/// <summary>
/// The localization ID used for announcing the cancellation of ACO requests
/// </summary>
[DataField]
public LocId RevokeACOMessage = "captain-arrived-revoke-aco-announcement";
/// <summary>
/// The localization ID for requesting an ACO vote when AA will be unlocked
/// </summary>
[DataField]
public LocId ACORequestWithAAMessage = "no-captain-request-aco-vote-with-aa-announcement";
/// <summary>
/// The localization ID for requesting an ACO vote when AA will not be unlocked
/// </summary>
[DataField]
public LocId ACORequestNoAAMessage = "no-captain-request-aco-vote-announcement";
/// <summary>
/// Set after ACO has been requested to avoid duplicate calls
/// </summary>
[DataField]
public bool IsACORequestActive;
/// <summary>
/// Used to denote that AA has been brought into the round either from captain or safe.
/// </summary>
[DataField]
public bool IsAAInPlay;
/// <summary>
/// The localization ID for announcing that AA has been unlocked for ACO
/// </summary>
[DataField]
public LocId AAUnlockedMessage = "no-captain-aa-unlocked-announcement";
/// <summary>
/// The access level to grant to spare ID cabinets
/// </summary>
[DataField]
public ProtoId<AccessLevelPrototype> ACOAccess = "Command";
}

View File

@ -0,0 +1,32 @@
using Content.Server.DeltaV.Station.Systems;
using Content.Shared.DeltaV.Planet;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
namespace Content.Server.DeltaV.Station.Components;
/// <summary>
/// Loads a planet map on mapinit and spawns a grid on it (e.g. a mining base).
/// The map can then be FTLd to by any shuttle matching its whitelist.
/// </summary>
[RegisterComponent, Access(typeof(StationPlanetSpawnerSystem))]
public sealed partial class StationPlanetSpawnerComponent : Component
{
/// <summary>
/// The planet to create.
/// </summary>
[DataField(required: true)]
public ProtoId<PlanetPrototype> Planet;
/// <summary>
/// Path to the grid to load onto the map.
/// </summary>
[DataField(required: true)]
public ResPath? GridPath;
/// <summary>
/// The map that was loaded.
/// </summary>
[DataField]
public EntityUid? Map;
}

View File

@ -1,23 +0,0 @@
using Content.Server.Station.Systems;
using Robust.Shared.Utility;
namespace Content.Server.Station.Components;
/// <summary>
/// Loads a surface map on mapinit.
/// </summary>
[RegisterComponent, Access(typeof(StationSurfaceSystem))]
public sealed partial class StationSurfaceComponent : Component
{
/// <summary>
/// Path to the map to load.
/// </summary>
[DataField(required: true)]
public ResPath? MapPath;
/// <summary>
/// The map that was loaded.
/// </summary>
[DataField]
public EntityUid? Map;
}

View File

@ -0,0 +1,21 @@
using Content.Shared.Roles;
using Robust.Shared.Network;
using Robust.Shared.Prototypes;
namespace Content.Server.DeltaV.Station.Events;
/// <summary>
/// Raised on a station when a after a players jobs are removed from the PlayerJobs
/// </summary>
/// <param name="NetUserId">Player whos jobs were removed</param>
/// <param name="PlayerJobs">Entry in PlayerJobs removed a list of JobPrototypes</param>
[ByRefEvent]
public record struct PlayerJobsRemovedEvent(NetUserId NetUserId, List<ProtoId<JobPrototype>> PlayerJobs);
/// <summary>
/// Raised on a staion when a job is added to a player
/// </summary>
/// <param name="NetUserId">Player who recived a job</param>
/// <param name="JobPrototypeId">Id of the jobPrototype added</param>
[ByRefEvent]
public record struct PlayerJobAddedEvent(NetUserId NetUserId, string JobPrototypeId);

View File

@ -0,0 +1,169 @@
using Content.Server.Chat.Systems;
using Content.Server.DeltaV.Cabinet;
using Content.Server.DeltaV.Station.Components;
using Content.Server.DeltaV.Station.Events;
using Content.Server.GameTicking;
using Content.Server.Station.Components;
using Content.Shared.Access.Components;
using Content.Shared.Access;
using Content.Shared.DeltaV.CCVars;
using Robust.Shared.Configuration;
using Robust.Shared.Prototypes;
using System.Linq;
using Microsoft.CodeAnalysis.CSharp.Syntax;
namespace Content.Server.DeltaV.Station.Systems;
public sealed class CaptainStateSystem : EntitySystem
{
[Dependency] private readonly ChatSystem _chat = default!;
[Dependency] private readonly GameTicker _ticker = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
private bool _aaEnabled;
private bool _acoOnDeparture;
private TimeSpan _aaDelay;
private TimeSpan _acoDelay;
public override void Initialize()
{
SubscribeLocalEvent<CaptainStateComponent, PlayerJobAddedEvent>(OnPlayerJobAdded);
SubscribeLocalEvent<CaptainStateComponent, PlayerJobsRemovedEvent>(OnPlayerJobsRemoved);
Subs.CVar(_cfg, DCCVars.AutoUnlockAllAccessEnabled, a => _aaEnabled = a, true);
Subs.CVar(_cfg, DCCVars.RequestAcoOnCaptainDeparture, a => _acoOnDeparture = a, true);
Subs.CVar(_cfg, DCCVars.AutoUnlockAllAccessDelay, a => _aaDelay = TimeSpan.FromMinutes(a), true);
Subs.CVar(_cfg, DCCVars.RequestAcoDelay, a => _acoDelay = TimeSpan.FromMinutes(a), true);
base.Initialize();
}
public override void Update(float frameTime)
{
base.Update(frameTime);
var currentTime = _ticker.RoundDuration(); // Caching to reduce redundant calls
if (currentTime < _acoDelay) // Avoid timing issues. No need to run before _acoDelay is reached anyways.
return;
var query = EntityQueryEnumerator<CaptainStateComponent>();
while (query.MoveNext(out var station, out var captainState))
{
if (captainState.HasCaptain)
HandleHasCaptain(station, captainState);
else
HandleNoCaptain(station, captainState, currentTime);
}
}
private void OnPlayerJobAdded(Entity<CaptainStateComponent> ent, ref PlayerJobAddedEvent args)
{
if (args.JobPrototypeId == "Captain")
{
ent.Comp.IsAAInPlay = true;
ent.Comp.HasCaptain = true;
}
}
private void OnPlayerJobsRemoved(Entity<CaptainStateComponent> ent, ref PlayerJobsRemovedEvent args)
{
if (!TryComp<StationJobsComponent>(ent, out var stationJobs))
return;
if (!args.PlayerJobs.Contains("Captain")) // If the player that left was a captain we need to check if there are any captains left
return;
if (stationJobs.PlayerJobs.Any(playerJobs => playerJobs.Value.Contains("Captain"))) // We check the PlayerJobs if there are any cpatins left
return;
ent.Comp.HasCaptain = false;
if (_acoOnDeparture)
{
_chat.DispatchStationAnnouncement(
ent,
Loc.GetString(ent.Comp.ACORequestNoAAMessage),
colorOverride: Color.Gold);
ent.Comp.IsACORequestActive = true;
}
}
/// <summary>
/// Handles cases for when there is a captain
/// </summary>
/// <param name="station"></param>
/// <param name="captainState"></param>
private void HandleHasCaptain(Entity<CaptainStateComponent?> station, CaptainStateComponent captainState)
{
// If ACO vote has been called we need to cancel and alert to return to normal chain of command
if (!captainState.IsACORequestActive)
return;
_chat.DispatchStationAnnouncement(
station,
Loc.GetString(captainState.RevokeACOMessage),
colorOverride: Color.Gold);
captainState.IsACORequestActive = false;
}
/// <summary>
/// Handles cases for when there is no captain
/// </summary>
/// <param name="station"></param>
/// <param name="captainState"></param>
private void HandleNoCaptain(Entity<CaptainStateComponent?> station, CaptainStateComponent captainState, TimeSpan currentTime)
{
if (CheckACORequest(captainState, currentTime))
{
var message =
CheckUnlockAA(captainState, null)
? captainState.ACORequestWithAAMessage
: captainState.ACORequestNoAAMessage;
_chat.DispatchStationAnnouncement(
station,
Loc.GetString(message, ("minutes", _aaDelay.TotalMinutes)),
colorOverride: Color.Gold);
captainState.IsACORequestActive = true;
}
if (CheckUnlockAA(captainState, currentTime))
{
captainState.IsAAInPlay = true;
_chat.DispatchStationAnnouncement(station, Loc.GetString(captainState.AAUnlockedMessage), colorOverride: Color.Red);
// Extend access of spare id lockers to command so they can access emergency AA
var query = EntityQueryEnumerator<SpareIDSafeComponent>();
while (query.MoveNext(out var spareIDSafe, out _))
{
if (!TryComp<AccessReaderComponent>(spareIDSafe, out var accessReader))
continue;
var accesses = accessReader.AccessLists;
if (accesses.Count <= 0) // Avoid restricting access for readers with no accesses
continue;
// Awful and disgusting but the accessReader has no proper api for adding acceses to readers without awful type casting. See AccessOverriderSystem
accesses.Add(new HashSet<ProtoId<AccessLevelPrototype>> { captainState.ACOAccess });
Dirty(spareIDSafe, accessReader);
RaiseLocalEvent(spareIDSafe, new AccessReaderConfigurationChangedEvent());
}
}
}
/// <summary>
/// Checks the conditions for if an ACO should be requested
/// </summary>
/// <param name="captainState"></param>
/// <returns>True if conditions are met for an ACO to be requested, False otherwise</returns>
private bool CheckACORequest(CaptainStateComponent captainState, TimeSpan currentTime)
{
return !captainState.IsACORequestActive && currentTime > _acoDelay;
}
/// <summary>
/// Checks the conditions for if AA should be unlocked
/// If time is null its condition is ignored
/// </summary>
/// <param name="captainState"></param>
/// <returns>True if conditions are met for AA to be unlocked, False otherwise</returns>
private bool CheckUnlockAA(CaptainStateComponent captainState, TimeSpan? currentTime)
{
if (captainState.IsAAInPlay || !_aaEnabled)
return false;
return currentTime == null || currentTime > _acoDelay + _aaDelay;
}
}

View File

@ -0,0 +1,30 @@
using Content.Server.DeltaV.Planet;
using Content.Server.DeltaV.Station.Components;
namespace Content.Server.DeltaV.Station.Systems;
public sealed class StationPlanetSpawnerSystem : EntitySystem
{
[Dependency] private readonly PlanetSystem _planet = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<StationPlanetSpawnerComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<StationPlanetSpawnerComponent, ComponentShutdown>(OnShutdown);
}
private void OnMapInit(Entity<StationPlanetSpawnerComponent> ent, ref MapInitEvent args)
{
if (ent.Comp.GridPath is not {} path)
return;
ent.Comp.Map = _planet.LoadPlanet(ent.Comp.Planet, path.ToString());
}
private void OnShutdown(Entity<StationPlanetSpawnerComponent> ent, ref ComponentShutdown args)
{
QueueDel(ent.Comp.Map);
}
}

View File

@ -1,40 +0,0 @@
using Content.Server.Parallax;
using Content.Server.Station.Components;
using Robust.Server.GameObjects;
namespace Content.Server.Station.Systems;
public sealed class StationSurfaceSystem : EntitySystem
{
[Dependency] private readonly BiomeSystem _biome = default!;
[Dependency] private readonly MapSystem _map = default!;
[Dependency] private readonly MapLoaderSystem _mapLoader = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<StationSurfaceComponent, MapInitEvent>(OnMapInit);
}
private void OnMapInit(Entity<StationSurfaceComponent> ent, ref MapInitEvent args)
{
if (ent.Comp.MapPath is not {} path)
return;
var map = _map.CreateMap(out var mapId);
if (!_mapLoader.TryLoad(mapId, path.ToString(), out _))
{
Log.Error($"Failed to load surface map {ent.Comp.MapPath}!");
Del(map);
return;
}
// loading replaced the map entity with a new one so get the latest id
map = _map.GetMap(mapId);
_map.SetPaused(map, false);
_biome.SetEnabled(map); // generate the terrain after the grids loaded to prevent it getting hidden under it
ent.Comp.Map = map;
}
}

View File

@ -2,6 +2,7 @@ using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Content.Server.Administration.Logs;
using Content.Server.Atmos.EntitySystems;
using Content.Server.DeltaV.Cargo.Components; // DeltaV
using Content.Server.Fluids.EntitySystems;
using Content.Server.Lathe.Components;
using Content.Server.Materials;
@ -229,6 +230,7 @@ namespace Content.Server.Lathe
if (comp.CurrentRecipe.Result is { } resultProto)
{
var result = Spawn(resultProto, Transform(uid).Coordinates);
EnsureComp<PriceModifierComponent>(result).Modifier = comp.PriceModifier; // DeltaV
_stack.TryMergeToContacts(result);
}

View File

@ -1,5 +1,6 @@
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Content.Server.DeltaV.Station.Events; // DeltaV
using Content.Server.GameTicking;
using Content.Server.Station.Components;
using Content.Shared.CCVar;
@ -108,7 +109,8 @@ public sealed partial class StationJobsSystem : EntitySystem
if (!TryAdjustJobSlot(station, jobPrototypeId, -1, false, false, stationJobs))
return false;
var playerJobAdded = new PlayerJobAddedEvent(netUserId, jobPrototypeId);
RaiseLocalEvent(station, ref playerJobAdded, false); // DeltaV added AddedPlayerJobsEvent for CaptainStateSystem
stationJobs.PlayerJobs.TryAdd(netUserId, new());
stationJobs.PlayerJobs[netUserId].Add(jobPrototypeId);
return true;
@ -206,8 +208,15 @@ public sealed partial class StationJobsSystem : EntitySystem
{
if (!Resolve(station, ref jobsComponent, false))
return false;
return jobsComponent.PlayerJobs.Remove(userId);
// DeltaV added RemovedPlayerJobsEvent for CaptainStateSystem
if (jobsComponent.PlayerJobs.Remove(userId, out var playerJobsEntry))
{
var playerJobRemovedEvent = new PlayerJobsRemovedEvent(userId, playerJobsEntry);
RaiseLocalEvent(station, ref playerJobRemovedEvent, false);
return true;
}
return false;
// DeltaV end added RemovedPlayerJobsEvent for CaptainStateSystem
}
/// <inheritdoc cref="TrySetJobSlot(Robust.Shared.GameObjects.EntityUid,string,int,bool,Content.Server.Station.Components.StationJobsComponent?)"/>

View File

@ -28,12 +28,26 @@ namespace Content.Shared.Access.Systems
public string CurrentName { get; }
public string CurrentJob { 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;
CurrentJob = currentJob;
CurrentJobIconId = currentJobIconId;
CurrentNumber = currentNumber; // DeltaV
}
}
// DeltaV - Add number change message
[Serializable, NetSerializable]
public sealed class AgentIDCardNumberChangedMessage : BoundUserInterfaceMessage
{
public uint Number { get; }
public AgentIDCardNumberChangedMessage(uint number)
{
Number = number;
}
}

View File

@ -1,4 +1,5 @@
using Robust.Shared.Serialization;
using Content.Shared.DeltaV.CartridgeLoader.Cartridges; // DeltaV
using Robust.Shared.Serialization;
namespace Content.Shared.CartridgeLoader.Cartridges;
@ -10,9 +11,15 @@ public sealed class LogProbeUiState : BoundUserInterfaceState
/// </summary>
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;
NanoChatData = nanoChatData; // DeltaV
}
}

View File

@ -0,0 +1,17 @@
using Content.Shared.DeltaV.QuickPhrase;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
namespace Content.Shared.DeltaV.AACTablet;
[Serializable, NetSerializable]
public enum AACTabletKey : byte
{
Key,
}
[Serializable, NetSerializable]
public sealed class AACTabletSendPhraseMessage(ProtoId<QuickPhrasePrototype> phraseId) : BoundUserInterfaceMessage
{
public ProtoId<QuickPhrasePrototype> PhraseId = phraseId;
}

View File

@ -1,19 +0,0 @@
using Robust.Shared.Serialization;
namespace Content.Shared.DeltaV.AACTablet;
[Serializable, NetSerializable]
public enum AACTabletKey : byte
{
Key,
}
[Serializable, NetSerializable]
public sealed class AACTabletSendPhraseMessage : BoundUserInterfaceMessage
{
public string PhraseID;
public AACTabletSendPhraseMessage(string phraseId)
{
PhraseID = phraseId;
}
}

View File

@ -1,4 +1,4 @@
using Robust.Shared.Configuration;
using Robust.Shared.Configuration;
namespace Content.Shared.DeltaV.CCVars;
@ -62,6 +62,35 @@ public sealed class DCCVars
public static readonly CVarDef<float> RoundEndNoEorgPopupTime =
CVarDef.Create("game.round_end_eorg_popup_time", 5f, CVar.SERVER | CVar.REPLICATED);
/*
* Auto ACO
*/
/// <summary>
/// How long with no captain before requesting an ACO be elected.
/// </summary>
public static readonly CVarDef<float> RequestAcoDelay =
CVarDef.Create("game.request_aco_delay_minutes", 15f, CVar.SERVERONLY | CVar.ARCHIVE);
/// <summary>
/// Determines whether an ACO should be requested when the captain leaves during the round,
/// in addition to cases where there are no captains at round start.
/// </summary>
public static readonly CVarDef<bool> RequestAcoOnCaptainDeparture =
CVarDef.Create("game.request_aco_on_captain_departure", true, CVar.SERVERONLY | CVar.ARCHIVE);
/// <summary>
/// Determines whether All Access (AA) should be automatically unlocked if no captain is present.
/// </summary>
public static readonly CVarDef<bool> AutoUnlockAllAccessEnabled =
CVarDef.Create("game.auto_unlock_aa_enabled", true, CVar.SERVERONLY | CVar.ARCHIVE);
/// <summary>
/// How long after an ACO request announcement is made before All Access (AA) should be unlocked.
/// </summary>
public static readonly CVarDef<float> AutoUnlockAllAccessDelay =
CVarDef.Create("game.auto_unlock_aa_delay_minutes", 5f, CVar.SERVERONLY | CVar.ARCHIVE);
/*
* Misc.
*/

View File

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

View File

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

View File

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

View File

@ -0,0 +1,273 @@
using Content.Shared.DeltaV.CartridgeLoader.Cartridges;
using Content.Shared.Examine;
using Robust.Shared.Timing;
namespace Content.Shared.DeltaV.NanoChat;
/// <summary>
/// Base system for NanoChat functionality shared between client and server.
/// </summary>
public abstract class SharedNanoChatSystem : EntitySystem
{
[Dependency] private readonly IGameTiming _timing = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<NanoChatCardComponent, ExaminedEvent>(OnExamined);
}
private void OnExamined(Entity<NanoChatCardComponent> ent, ref ExaminedEvent args)
{
if (!args.IsInDetailsRange)
return;
if (ent.Comp.Number == null)
{
args.PushMarkup(Loc.GetString("nanochat-card-examine-no-number"));
return;
}
args.PushMarkup(Loc.GetString("nanochat-card-examine-number", ("number", $"{ent.Comp.Number:D4}")));
}
#region Public API Methods
/// <summary>
/// Gets the NanoChat number for a card.
/// </summary>
public uint? GetNumber(Entity<NanoChatCardComponent?> card)
{
if (!Resolve(card, ref card.Comp))
return null;
return card.Comp.Number;
}
/// <summary>
/// Sets the NanoChat number for a card.
/// </summary>
public void SetNumber(Entity<NanoChatCardComponent?> card, uint number)
{
if (!Resolve(card, ref card.Comp))
return;
card.Comp.Number = number;
Dirty(card);
}
/// <summary>
/// Gets the recipients dictionary from a card.
/// </summary>
public IReadOnlyDictionary<uint, NanoChatRecipient> GetRecipients(Entity<NanoChatCardComponent?> card)
{
if (!Resolve(card, ref card.Comp))
return new Dictionary<uint, NanoChatRecipient>();
return card.Comp.Recipients;
}
/// <summary>
/// Gets the messages dictionary from a card.
/// </summary>
public IReadOnlyDictionary<uint, List<NanoChatMessage>> GetMessages(Entity<NanoChatCardComponent?> card)
{
if (!Resolve(card, ref card.Comp))
return new Dictionary<uint, List<NanoChatMessage>>();
return card.Comp.Messages;
}
/// <summary>
/// Sets a specific recipient in the card.
/// </summary>
public void SetRecipient(Entity<NanoChatCardComponent?> card, uint number, NanoChatRecipient recipient)
{
if (!Resolve(card, ref card.Comp))
return;
card.Comp.Recipients[number] = recipient;
Dirty(card);
}
/// <summary>
/// Gets a specific recipient from the card.
/// </summary>
public NanoChatRecipient? GetRecipient(Entity<NanoChatCardComponent?> card, uint number)
{
if (!Resolve(card, ref card.Comp) || !card.Comp.Recipients.TryGetValue(number, out var recipient))
return null;
return recipient;
}
/// <summary>
/// Gets all messages for a specific recipient.
/// </summary>
public List<NanoChatMessage>? GetMessagesForRecipient(Entity<NanoChatCardComponent?> card, uint recipientNumber)
{
if (!Resolve(card, ref card.Comp) || !card.Comp.Messages.TryGetValue(recipientNumber, out var messages))
return null;
return new List<NanoChatMessage>(messages);
}
/// <summary>
/// Adds a message to a recipient's conversation.
/// </summary>
public void AddMessage(Entity<NanoChatCardComponent?> card, uint recipientNumber, NanoChatMessage message)
{
if (!Resolve(card, ref card.Comp))
return;
if (!card.Comp.Messages.TryGetValue(recipientNumber, out var messages))
{
messages = new List<NanoChatMessage>();
card.Comp.Messages[recipientNumber] = messages;
}
messages.Add(message);
card.Comp.LastMessageTime = _timing.CurTime;
Dirty(card);
}
/// <summary>
/// Gets the currently selected chat recipient.
/// </summary>
public uint? GetCurrentChat(Entity<NanoChatCardComponent?> card)
{
if (!Resolve(card, ref card.Comp))
return null;
return card.Comp.CurrentChat;
}
/// <summary>
/// Sets the currently selected chat recipient.
/// </summary>
public void SetCurrentChat(Entity<NanoChatCardComponent?> card, uint? recipient)
{
if (!Resolve(card, ref card.Comp))
return;
card.Comp.CurrentChat = recipient;
Dirty(card);
}
/// <summary>
/// Gets whether notifications are muted.
/// </summary>
public bool GetNotificationsMuted(Entity<NanoChatCardComponent?> card)
{
if (!Resolve(card, ref card.Comp))
return false;
return card.Comp.NotificationsMuted;
}
/// <summary>
/// Sets whether notifications are muted.
/// </summary>
public void SetNotificationsMuted(Entity<NanoChatCardComponent?> card, bool muted)
{
if (!Resolve(card, ref card.Comp))
return;
card.Comp.NotificationsMuted = muted;
Dirty(card);
}
/// <summary>
/// Gets the time of the last message.
/// </summary>
public TimeSpan? GetLastMessageTime(Entity<NanoChatCardComponent?> card)
{
if (!Resolve(card, ref card.Comp))
return null;
return card.Comp.LastMessageTime;
}
/// <summary>
/// Gets if there are unread messages from a recipient.
/// </summary>
public bool HasUnreadMessages(Entity<NanoChatCardComponent?> card, uint recipientNumber)
{
if (!Resolve(card, ref card.Comp) || !card.Comp.Recipients.TryGetValue(recipientNumber, out var recipient))
return false;
return recipient.HasUnread;
}
/// <summary>
/// Clears all messages and recipients from the card.
/// </summary>
public void Clear(Entity<NanoChatCardComponent?> card)
{
if (!Resolve(card, ref card.Comp))
return;
card.Comp.Messages.Clear();
card.Comp.Recipients.Clear();
card.Comp.CurrentChat = null;
Dirty(card);
}
/// <summary>
/// Deletes a chat conversation with a recipient from the card.
/// Optionally keeps message history while removing from active chats.
/// </summary>
/// <returns>True if the chat was deleted successfully</returns>
public bool TryDeleteChat(Entity<NanoChatCardComponent?> card, uint recipientNumber, bool keepMessages = false)
{
if (!Resolve(card, ref card.Comp))
return false;
// Remove from recipients list
var removed = card.Comp.Recipients.Remove(recipientNumber);
// Clear messages if requested
if (!keepMessages)
card.Comp.Messages.Remove(recipientNumber);
// Clear current chat if we just deleted it
if (card.Comp.CurrentChat == recipientNumber)
card.Comp.CurrentChat = null;
if (removed)
Dirty(card);
return removed;
}
/// <summary>
/// Ensures a recipient exists in the card's contacts and message lists.
/// If the recipient doesn't exist, they will be added with the provided info.
/// </summary>
/// <returns>True if the recipient was added or already existed</returns>
public bool EnsureRecipientExists(Entity<NanoChatCardComponent?> card,
uint recipientNumber,
NanoChatRecipient? recipientInfo = null)
{
if (!Resolve(card, ref card.Comp))
return false;
if (!card.Comp.Recipients.ContainsKey(recipientNumber))
{
// Only add if we have recipient info
if (recipientInfo == null)
return false;
card.Comp.Recipients[recipientNumber] = recipientInfo.Value;
}
// Ensure message list exists for this recipient
if (!card.Comp.Messages.ContainsKey(recipientNumber))
card.Comp.Messages[recipientNumber] = new List<NanoChatMessage>();
Dirty(card);
return true;
}
#endregion
}

View File

@ -0,0 +1,49 @@
using Content.Shared.Atmos;
using Content.Shared.Parallax.Biomes;
using Content.Shared.Parallax.Biomes.Markers;
using Robust.Shared.Prototypes;
namespace Content.Shared.DeltaV.Planet;
[Prototype]
public sealed partial class PlanetPrototype : IPrototype
{
[IdDataField]
public string ID { get; set; } = string.Empty;
/// <summary>
/// The biome to create the planet with.
/// </summary>
[DataField(required: true)]
public ProtoId<BiomeTemplatePrototype> Biome;
/// <summary>
/// Name to give to the map.
/// </summary>
[DataField(required: true)]
public LocId MapName;
/// <summary>
/// Ambient lighting for the map.
/// </summary>
[DataField]
public Color MapLight = Color.FromHex("#D8B059");
/// <summary>
/// Components to add to the map.
/// </summary>
[DataField]
public ComponentRegistry? AddedComponents;
/// <summary>
/// The gas mixture to use for the atmosphere.
/// </summary>
[DataField(required: true)]
public GasMixture Atmosphere = new();
/// <summary>
/// Biome layers to add to the map, i.e. ores.
/// </summary>
[DataField]
public List<ProtoId<BiomeMarkerLayerPrototype>> BiomeMarkerLayers = new();
}

View File

@ -3,7 +3,7 @@ using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototy
namespace Content.Shared.DeltaV.QuickPhrase;
[Prototype("quickPhrase")]
[Prototype]
public sealed partial class QuickPhrasePrototype : IPrototype, IInheritingPrototype
{
/// <summary>
@ -48,4 +48,4 @@ public sealed partial class QuickPhrasePrototype : IPrototype, IInheritingProtot
/// </summary>
[DataField]
public string StyleClass = string.Empty;
}
}

View File

@ -0,0 +1,26 @@
using Content.Shared.DeltaV.Salvage.Systems;
using Robust.Shared.Audio;
using Robust.Shared.GameStates;
namespace Content.Shared.DeltaV.Salvage.Components;
/// <summary>
/// Stores mining points for a holder, such as an ID card or ore processor.
/// Mining points are gained by smelting ore and redeeming them to your ID card.
/// </summary>
[RegisterComponent, NetworkedComponent, Access(typeof(MiningPointsSystem))]
[AutoGenerateComponentState]
public sealed partial class MiningPointsComponent : Component
{
/// <summary>
/// The number of points stored.
/// </summary>
[DataField, AutoNetworkedField]
public uint Points;
/// <summary>
/// Sound played when successfully transferring points to another holder.
/// </summary>
[DataField]
public SoundSpecifier? TransferSound;
}

View File

@ -0,0 +1,9 @@
using Robust.Shared.GameStates;
namespace Content.Shared.DeltaV.Salvage.Components;
/// <summary>
/// Adds points to <see cref="MiningPointsComponent"/> when making a recipe that has miningPoints set.
/// </summary>
[RegisterComponent, NetworkedComponent]
public sealed partial class MiningPointsLatheComponent : Component;

View File

@ -0,0 +1,9 @@
using Robust.Shared.Serialization;
namespace Content.Shared.DeltaV.Salvage;
/// <summary>
/// Message for a lathe to transfer its mining points to the user's id card.
/// </summary>
[Serializable, NetSerializable]
public sealed class LatheClaimMiningPointsMessage : BoundUserInterfaceMessage;

View File

@ -0,0 +1,121 @@
using Content.Shared.Access.Systems;
using Content.Shared.DeltaV.Salvage.Components;
using Content.Shared.Lathe;
using Robust.Shared.Audio.Systems;
namespace Content.Shared.DeltaV.Salvage.Systems;
public sealed class MiningPointsSystem : EntitySystem
{
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly SharedIdCardSystem _idCard = default!;
private EntityQuery<MiningPointsComponent> _query;
public override void Initialize()
{
base.Initialize();
_query = GetEntityQuery<MiningPointsComponent>();
SubscribeLocalEvent<MiningPointsLatheComponent, LatheStartPrintingEvent>(OnStartPrinting);
Subs.BuiEvents<MiningPointsLatheComponent>(LatheUiKey.Key, subs =>
{
subs.Event<LatheClaimMiningPointsMessage>(OnClaimMiningPoints);
});
}
#region Event Handlers
private void OnStartPrinting(Entity<MiningPointsLatheComponent> ent, ref LatheStartPrintingEvent args)
{
var points = args.Recipe.MiningPoints;
if (points > 0)
AddPoints(ent.Owner, points);
}
private void OnClaimMiningPoints(Entity<MiningPointsLatheComponent> ent, ref LatheClaimMiningPointsMessage args)
{
var user = args.Actor;
if (TryFindIdCard(user) is {} dest)
TransferAll(ent.Owner, dest);
}
#endregion
#region Public API
/// <summary>
/// Tries to find the user's id card and gets its <see cref="MiningPointsComponent"/>.
/// </summary>
/// <remarks>
/// Component is nullable for easy usage with the API due to Entity&lt;T&gt; not being usable for Entity&lt;T?&gt; arguments.
/// </remarks>
public Entity<MiningPointsComponent?>? TryFindIdCard(EntityUid user)
{
if (!_idCard.TryFindIdCard(user, out var idCard))
return null;
if (!_query.TryComp(idCard, out var comp))
return null;
return (idCard, comp);
}
/// <summary>
/// Removes points from a holder, returning true if it succeeded.
/// </summary>
public bool RemovePoints(Entity<MiningPointsComponent?> ent, uint amount)
{
if (!_query.Resolve(ent, ref ent.Comp) || amount > ent.Comp.Points)
return false;
ent.Comp.Points -= amount;
Dirty(ent);
return true;
}
/// <summary>
/// Add points to a holder.
/// </summary>
public bool AddPoints(Entity<MiningPointsComponent?> ent, uint amount)
{
if (!_query.Resolve(ent, ref ent.Comp))
return false;
ent.Comp.Points += amount;
Dirty(ent);
return true;
}
/// <summary>
/// Transfer a number of points from source to destination.
/// Returns true if the transfer succeeded.
/// </summary>
public bool Transfer(Entity<MiningPointsComponent?> src, Entity<MiningPointsComponent?> dest, uint amount)
{
// don't make a sound or anything
if (amount == 0)
return true;
if (!_query.Resolve(src, ref src.Comp) || !_query.Resolve(dest, ref dest.Comp))
return false;
if (!RemovePoints(src, amount))
return false;
AddPoints(dest, amount);
_audio.PlayPvs(src.Comp.TransferSound, src);
return true;
}
/// <summary>
/// Transfers all points from source to destination.
/// Returns true if the transfer succeeded.
/// </summary>
public bool TransferAll(Entity<MiningPointsComponent?> src, Entity<MiningPointsComponent?> dest)
{
return _query.Resolve(src, ref src.Comp) && Transfer(src, dest, src.Comp.Points);
}
#endregion
}

View File

@ -0,0 +1,48 @@
using Content.Shared.DeltaV.Shuttles.Systems;
using Content.Shared.Tag;
using Content.Shared.Whitelist;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
namespace Content.Shared.DeltaV.Shuttles.Components;
/// <summary>
/// A shuttle console that can only ftl-dock between 2 grids.
/// The shuttle used must have <see cref="DockingShuttleComponent"/>.
/// </summary>
[RegisterComponent, NetworkedComponent, Access(typeof(SharedDockingConsoleSystem))]
[AutoGenerateComponentState]
public sealed partial class DockingConsoleComponent : Component
{
/// <summary>
/// Title of the window to use
/// </summary>
[DataField(required: true)]
public LocId WindowTitle;
/// <summary>
/// Airlock tag that it will prioritize docking to.
/// </summary>
[DataField(required: true)]
public ProtoId<TagPrototype> DockTag;
/// <summary>
/// A whitelist the shuttle has to match to be piloted.
/// </summary>
[DataField(required: true)]
public EntityWhitelist ShuttleWhitelist = new();
/// <summary>
/// The shuttle that matches <see cref="ShuttleWhitelist"/>.
/// If this is null a shuttle was not found and this console does nothing.
/// </summary>
[DataField]
public EntityUid? Shuttle;
/// <summary>
/// Whether <see cref="Shuttle"/> is set on the server or not.
/// Client can't use Shuttle outside of PVS range so that isn't networked.
/// </summary>
[DataField, AutoNetworkedField]
public bool HasShuttle;
}

View File

@ -0,0 +1,46 @@
using Content.Shared.DeltaV.Shuttles.Systems;
using Robust.Shared.GameStates;
using Robust.Shared.Map;
using Robust.Shared.Serialization;
namespace Content.Shared.DeltaV.Shuttles.Components;
/// <summary>
/// Component that stores destinations a docking-only shuttle can use.
/// Used by <see cref="DockingConsoleComponent"/> to access destinations.
/// </summary>
[RegisterComponent, NetworkedComponent, Access(typeof(SharedDockingShuttleSystem))]
public sealed partial class DockingShuttleComponent : Component
{
/// <summary>
/// The station this shuttle belongs to.
/// </summary>
[DataField]
public EntityUid? Station;
/// <summary>
/// Every destination this console can FTL to.
/// </summary>
[DataField]
public List<DockingDestination> Destinations = new();
}
/// <summary>
/// A map a shuttle can FTL to.
/// Created automatically on shuttle mapinit.
/// </summary>
[DataDefinition, Serializable, NetSerializable]
public partial struct DockingDestination
{
/// <summary>
/// The name of the destination to use in UI.
/// </summary>
[DataField]
public LocId Name;
/// <summary>
/// The map ID.
/// </summary>
[DataField]
public MapId Map;
}

View File

@ -0,0 +1,10 @@
using Robust.Shared.GameStates;
namespace Content.Shared.DeltaV.Shuttles.Components;
/// <summary>
/// Marker component for the mining shuttle grid.
/// Used for lavaland's FTL whitelist.
/// </summary>
[RegisterComponent, NetworkedComponent]
public sealed partial class MiningShuttleComponent : Component;

View File

@ -0,0 +1,26 @@
using Content.Shared.DeltaV.Shuttles.Components;
using Content.Shared.Shuttles.Systems;
using Content.Shared.Timing;
using Robust.Shared.Serialization;
namespace Content.Shared.DeltaV.Shuttles;
[Serializable, NetSerializable]
public enum DockingConsoleUiKey : byte
{
Key
}
[Serializable, NetSerializable]
public sealed class DockingConsoleState(FTLState ftlState, StartEndTime ftlTime, List<DockingDestination> destinations) : BoundUserInterfaceState
{
public FTLState FTLState = ftlState;
public StartEndTime FTLTime = ftlTime;
public List<DockingDestination> Destinations = destinations;
}
[Serializable, NetSerializable]
public sealed class DockingConsoleFTLMessage(int index) : BoundUserInterfaceMessage
{
public int Index = index;
}

View File

@ -0,0 +1,3 @@
namespace Content.Shared.DeltaV.Shuttles.Systems;
public abstract class SharedDockingConsoleSystem : EntitySystem;

View File

@ -0,0 +1,3 @@
namespace Content.Shared.DeltaV.Shuttles.Systems;
public abstract class SharedDockingShuttleSystem : EntitySystem;

View File

@ -42,6 +42,12 @@ namespace Content.Shared.Lathe
[DataField, AutoNetworkedField]
public int DefaultProductionAmount = 1;
/// <summary>
/// DeltaV: The price modifier applied to all items printed by this lathe.
/// </summary>
[DataField]
public float PriceModifier = 0.4f;
#region Visualizer info
[DataField]
public string? IdleState;

View File

@ -80,7 +80,7 @@ public sealed partial class ReverseEngineeringMachineComponent : Component
/// How long to wait between analysis rolls.
/// </summary>
[DataField]
public TimeSpan AnalysisDuration = TimeSpan.FromSeconds(30);
public TimeSpan AnalysisDuration = TimeSpan.FromSeconds(15);
/// <summary>
/// Last result to show in the ui

View File

@ -70,5 +70,12 @@ namespace Content.Shared.Research.Prototypes
/// </summary>
[DataField]
public ProtoId<LatheCategoryPrototype>? Category;
/// <summary>
/// DeltaV: Number of mining points this recipe adds to an oreproc when printed.
/// Scales with stack count.
/// </summary>
[DataField]
public uint MiningPoints;
}
}

View File

@ -57,5 +57,12 @@ Entries:
id: 8
time: '2024-08-31T01:10:24.0000000+00:00'
url: https://github.com/DeltaV-Station/Delta-v/pull/1718
- author: deltanedas
changes:
- message: You don't need to make a bluespace locker for lavaland anymore.
type: Fix
id: 9
time: '2024-12-12T22:49:53.0000000+00:00'
url: https://github.com/DeltaV-Station/Delta-v/pull/2430
Name: Admin
Order: 1

View File

@ -1,167 +1,4 @@
Entries:
- author: VMSolidus
changes:
- message: Removed the ability of Harpies to destroy people's ears.
type: Fix
id: 244
time: '2024-02-13T13:29:37.0000000+00:00'
- author: deltanedas
changes:
- message: Brought back paradox anomalies. Beware anyone that looks *just* like
you!
type: Add
id: 245
time: '2024-02-13T15:55:35.0000000+00:00'
- author: FluffiestFloof
changes:
- message: Added Deuteranopia trait.
type: Add
- message: Vulpkanin now start with Deuteranopia by default which can be removed
using the Normal Vision trait.
type: Tweak
id: 246
time: '2024-02-15T17:27:24.0000000+00:00'
- author: FluffiestFloof
changes:
- message: Glimmer Drain now requires 5 Normality Crystals to craft.
type: Tweak
id: 247
time: '2024-02-17T03:38:14.0000000+00:00'
- author: VMSolidus
changes:
- message: 'Metempsychosis machines have been added to the game. '
type: Add
id: 248
time: '2024-02-18T02:36:16.0000000+00:00'
- author: DebugOk
changes:
- message: Merged upstream
type: Add
- message: Fixed the upstream changelog not rendering
type: Fix
id: 249
time: '2024-02-18T23:05:48.0000000+00:00'
- author: DebugOk
changes:
- message: Whitelist to join now works based on slots instead of player count
type: Tweak
- message: Periapsis now has only 10 non-whitelisted slots
type: Tweak
id: 250
time: '2024-02-20T19:46:17.0000000+00:00'
- author: rosieposieeee
changes:
- message: Submarine Station has been temporarily removed from rotation for routine
maintenance.
type: Remove
id: 251
time: '2024-02-21T01:15:08.0000000+00:00'
- author: DebugOk
changes:
- message: Added a new accessibility setting to globally disable species vision
filters
type: Add
- message: The DefaultVision trait has been removed
type: Remove
id: 252
time: '2024-02-23T18:36:18.0000000+00:00'
- author: Velcroboy
changes:
- message: 'Rekajiggered The Hive''s medical. '
type: Tweak
id: 253
time: '2024-02-24T00:34:16.0000000+00:00'
- author: Velcroboy
changes:
- message: 'The Hive Rekajiggering part 2 of 3: Perma addition'
type: Tweak
id: 254
time: '2024-02-24T06:38:02.0000000+00:00'
- author: DebugOk
changes:
- message: Space mobs no longer have ghost roles
type: Fix
id: 255
time: '2024-02-26T20:20:53.0000000+00:00'
- author: Velcroboy
changes:
- message: Overhauled the bar on The Hive
type: Tweak
id: 256
time: '2024-02-26T21:53:05.0000000+00:00'
- author: DebugOk
changes:
- message: Psionic invisibility has been disabled until the issues with it can be
properly fixed
type: Remove
id: 257
time: '2024-02-28T16:54:47.0000000+00:00'
- author: Adrian16199
changes:
- message: Added prescription medical hud and security hud.
type: Add
id: 258
time: '2024-03-01T23:41:11.0000000+00:00'
- author: deltanedas
changes:
- message: Fixed midround antags sometimes spawning on CentComm.
type: Fix
id: 259
time: '2024-03-02T22:27:13.0000000+00:00'
- author: DebugOk
changes:
- message: Rat kings have been removed after a unanimous admin vote
type: Remove
id: 260
time: '2024-03-02T22:41:12.0000000+00:00'
- author: TadJohnson00
changes:
- message: Warden requirements now include mandatory officer time and whitelist.
type: Tweak
id: 261
time: '2024-03-02T23:00:08.0000000+00:00'
- author: DebugOk
changes:
- message: The salvage shuttle has been completely removed
type: Remove
- message: Salvage specialists now start with a mini jetpack in their locker
type: Tweak
id: 262
time: '2024-03-06T23:27:01.0000000+00:00'
- author: DangerRevolution
changes:
- message: Added several new potential Lawsets for Cyborgs.
type: Add
- message: Janitor/Engineer/Medical Borgs spawn with their own lawsets.
type: Tweak
id: 263
time: '2024-03-07T22:22:42.0000000+00:00'
- author: FluffiestFloof
changes:
- message: Pumpkin Pies can now be baked by professional Chefs.
type: Tweak
id: 264
time: '2024-03-07T22:49:22.0000000+00:00'
- author: Velcroboy
changes:
- message: Reverted AME buff.
type: Tweak
id: 265
time: '2024-03-08T03:13:49.0000000+00:00'
- author: JoeHammad
changes:
- message: Nuclear operative combat medics now have combination syndicate/medical
hud
type: Add
id: 266
time: '2024-03-08T03:18:03.0000000+00:00'
- author: '- BonkTrauma and DangerRevolution'
changes:
- message: Added Prisoner communication channel and updated a few headsets to include
it
type: Add
id: 267
time: '2024-03-08T03:22:48.0000000+00:00'
- author: Tryded
changes:
- message: Changed Energy Guns' charging time to half of what it originally was
@ -3805,3 +3642,187 @@
id: 743
time: '2024-12-06T19:33:56.0000000+00:00'
url: https://github.com/DeltaV-Station/Delta-v/pull/2310
- author: Lyndomen
changes:
- message: Central Command got a tiny renovation
type: Tweak
id: 744
time: '2024-12-07T17:17:24.0000000+00:00'
url: https://github.com/DeltaV-Station/Delta-v/pull/2174
- author: Colin-Tel
changes:
- message: Asterisk Station has had additional meteor shielding constructed.
type: Tweak
id: 745
time: '2024-12-07T18:10:41.0000000+00:00'
url: https://github.com/DeltaV-Station/Delta-v/pull/2377
- author: rosieposieeee
changes:
- message: Added wooden and grey modern railings.
type: Add
id: 746
time: '2024-12-07T23:33:58.0000000+00:00'
url: https://github.com/DeltaV-Station/Delta-v/pull/2379
- author: rosieposieeee
changes:
- message: Added reinforced directional tinted walls.
type: Add
id: 747
time: '2024-12-07T23:35:05.0000000+00:00'
url: https://github.com/DeltaV-Station/Delta-v/pull/2376
- author: deltanedas
changes:
- message: Ninja objectives are now more random and they can be asked to steal things
or kill people!
type: Tweak
id: 748
time: '2024-12-08T15:11:04.0000000+00:00'
url: https://github.com/DeltaV-Station/Delta-v/pull/2359
- author: Monotheonist
changes:
- message: Fugitives are no longer clones of people on station.
type: Tweak
id: 749
time: '2024-12-09T01:02:50.0000000+00:00'
url: https://github.com/DeltaV-Station/Delta-v/pull/2383
- author: Lyndomen
changes:
- message: Diona are substantially more tanky to physical damage, and ignite less
frequently.
type: Tweak
- message: Diona are much slower, but cannot slip while walking.
type: Tweak
id: 750
time: '2024-12-09T05:44:04.0000000+00:00'
url: https://github.com/DeltaV-Station/Delta-v/pull/2204
- author: Colin-Tel
changes:
- message: Micro Station has been removed from the regular map rotation.
type: Remove
id: 751
time: '2024-12-09T15:26:52.0000000+00:00'
url: https://github.com/DeltaV-Station/Delta-v/pull/2402
- author: SolStar
changes:
- message: Command will now be automatically notified to elect an ACO when no captain
is present.
type: Tweak
- message: 'Periapsis and Horizon only: AA will be command locked 5 minutes after
an ACO vote is requested.'
type: Tweak
id: 752
time: '2024-12-09T17:05:19.0000000+00:00'
url: https://github.com/DeltaV-Station/Delta-v/pull/2351
- author: MilonPL
changes:
- message: Reduced the price of all items printed from the lathes by 60%
type: Tweak
id: 753
time: '2024-12-10T04:16:38.0000000+00:00'
url: https://github.com/DeltaV-Station/Delta-v/pull/2406
- author: MilonPL
changes:
- message: Fixed diona's movement speed being default
type: Fix
id: 754
time: '2024-12-10T17:47:23.0000000+00:00'
url: https://github.com/DeltaV-Station/Delta-v/pull/2413
- author: deltanedas
changes:
- message: Added Lavaland, coming soon to a station near you!
type: Add
- message: Removed the random reclaimer wrecks.
type: Remove
- message: Fixed not being able to build catwalks over lava.
type: Fix
id: 755
time: '2024-12-10T18:37:43.0000000+00:00'
url: https://github.com/DeltaV-Station/Delta-v/pull/2380
- author: MilonPL
changes:
- message: Added PDA messaging!
type: Add
id: 756
time: '2024-12-10T20:33:58.0000000+00:00'
url: https://github.com/DeltaV-Station/Delta-v/pull/2362
- author: Radezolid
changes:
- message: Shadow cats are now ghost roles, go pet your local shadow cat whenever
EPI is working a shadow anomaly!
type: Tweak
id: 757
time: '2024-12-11T13:09:02.0000000+00:00'
url: https://github.com/DeltaV-Station/Delta-v/pull/2417
- author: MilonPL
changes:
- message: The Teach Person a Lesson objective will now register properly when your
target dies.
type: Fix
id: 758
time: '2024-12-12T00:18:28.0000000+00:00'
url: https://github.com/DeltaV-Station/Delta-v/pull/2412
- author: Radezolid
changes:
- message: Now all jetpacks fit in the suit storage slot, salvage rejoice!
type: Tweak
id: 759
time: '2024-12-12T00:22:13.0000000+00:00'
url: https://github.com/DeltaV-Station/Delta-v/pull/2400
- author: Velcroboy
changes:
- message: Hammurabi medical also has a rapid transit tube now!
type: Add
id: 760
time: '2024-12-12T02:41:04.0000000+00:00'
url: https://github.com/DeltaV-Station/Delta-v/pull/2357
- author: Radezolid
changes:
- message: The blushing mime mask and the purple clown mask can now be obtained
at an autodrobe.
type: Tweak
id: 761
time: '2024-12-12T15:57:19.0000000+00:00'
url: https://github.com/DeltaV-Station/Delta-v/pull/2346
- author: Radezolid
changes:
- message: LO now also requires time as a cargo technician.
type: Tweak
id: 762
time: '2024-12-12T16:30:11.0000000+00:00'
url: https://github.com/DeltaV-Station/Delta-v/pull/2399
- author: Stop-Signs
changes:
- message: Reverse engineering machines now function much faster!
type: Tweak
id: 763
time: '2024-12-13T09:11:17.0000000+00:00'
url: https://github.com/DeltaV-Station/Delta-v/pull/2394
- author: deltanedas
changes:
- message: Fixed lavaland for real this time.
type: Fix
id: 764
time: '2024-12-13T13:32:50.0000000+00:00'
url: https://github.com/DeltaV-Station/Delta-v/pull/2434
- author: deltanedas
changes:
- message: You can now visit the surface of Glacier's planet, get a cargo tech to
fly you down at arrivals!
type: Add
id: 765
time: '2024-12-13T18:38:46.0000000+00:00'
url: https://github.com/DeltaV-Station/Delta-v/pull/2414
- author: Radezolid
changes:
- message: Now ruins are a dirtier and a bit more broken.
type: Tweak
id: 766
time: '2024-12-13T20:06:35.0000000+00:00'
url: https://github.com/DeltaV-Station/Delta-v/pull/2423
- author: Delta V Mapping Team
changes:
- message: Happy Holidays!
type: Add
id: 767
time: '2024-12-13T20:47:05.0000000+00:00'
url: https://github.com/DeltaV-Station/Delta-v/pull/2433

View File

@ -1,6 +1,12 @@
[events]
[events]
# Annoying
enabled = false
[game]
# Easier testing
request_aco_delay_minutes = 1
auto_unlock_aa_delay_minutes = 1
auto_unlock_aa_enabled = true
[shuttle]
auto_call_time = 0

View File

@ -1,6 +1,7 @@
[game]
hostname = "[EN][MRP] Delta-v (Ψ) | Apoapsis [US East 1]"
soft_max_players = 80
auto_unlock_aa_enabled = false # Disabled for apo until if/when command whitelist
[shuttle]
emergency_early_launch_allowed = true

File diff suppressed because one or more lines are too long

View File

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

View File

@ -158,3 +158,45 @@ stock-trading-buy-button = Buy
stock-trading-sell-button = Sell
stock-trading-amount-placeholder = Amount
stock-trading-price-history = Price History
## NanoChat
# General
nano-chat-program-name = NanoChat
nano-chat-title = NanoChat
nano-chat-new-chat = New Chat
nano-chat-contacts = CONTACTS
nano-chat-no-chats = No active chats
nano-chat-select-chat = Select a chat to begin
nano-chat-message-placeholder = Type a message...
nano-chat-send = Send
nano-chat-delete = Delete
nano-chat-loading = Loading...
nano-chat-message-too-long = Message too long ({$current}/{$max} characters)
nano-chat-max-recipients = Maximum number of chats reached
nano-chat-new-message-title = Message from {$sender}
nano-chat-new-message-body = {$message}
nano-chat-toggle-mute = Mute notifications
nano-chat-delivery-failed = Failed to deliver
# Create chat popup
nano-chat-new-title = Add a new chat
nano-chat-number-label = Number
nano-chat-name-label = Name
nano-chat-job-label = Job title
nano-chat-number-placeholder = Enter a number
nano-chat-name-placeholder = Enter a name
nano-chat-job-placeholder = Enter a job title (optional)
nano-chat-cancel = Cancel
nano-chat-create = Create
# LogProbe additions
log-probe-scan-nanochat = Scanned {$card}'s NanoChat logs
log-probe-header-access = Access Log Scanner
log-probe-header-nanochat = NanoChat Log Scanner
log-probe-label-message = Message
log-probe-card-number = Card: {$number}
log-probe-recipients = {$count} Recipients
log-probe-recipient-list = Known Recipients:
log-probe-message-format = {$sender} → {$recipient}: {$content}

View File

@ -46,3 +46,6 @@ ghost-role-information-syndicate-refugee-rules = You're a regular crewmember fro
You are a [color=green][bold]Non-antagonist[/bold][/color].
You're allowed to commit crimes, don't be a threat to the station, don't assist Syndicate agents, metashield rules applies.
All normal rules apply unless an administrator tells you otherwise.
ghost-role-information-shadow-cat-name = Shadow Cat
ghost-role-information-shadow-cat-description = A cute cat made of... shadows? How?!

View File

@ -0,0 +1,6 @@
# Announcements related to captain presence and ACO state
captain-arrived-revoke-aco-announcement = The Acting Commanding Officer's position is revoked due to the arrival of a NanoTrasen-appointed captain. All personnel are to return to the standard Chain of Command.
no-captain-request-aco-vote-with-aa-announcement = Station records indicate that no captain is currently present. Command personnel are requested to nominate an Acting Commanding Officer and report the results to Central Command in accordance with Standard Operating Procedure. Emergency AA will be unlocked in {$minutes} minutes to ensure continued operational efficiency.
no-captain-request-aco-vote-announcement = Station records indicate that no captain is currently present. Command personnel are requested to nominate an Acting Commanding Officer and report the results to Central Command in accordance with Standard Operating Procedure.
no-captain-aa-unlocked-announcement = Command access authority has been granted to the Spare ID cabinet for use by the Acting Commanding Officer. Unauthorized possession of Emergency AA is punishable under Felony Offense [202]: Grand Theft.

View File

@ -0,0 +1,2 @@
lathe-menu-mining-points = Mining Points: {$points}
lathe-menu-mining-points-claim-button = Claim Points

View File

@ -0,0 +1,7 @@
# Examine
nanochat-card-examine-no-number = The NanoChat card has not been assigned a number yet.
nanochat-card-examine-number = The NanoChat card displays #{$number}.
# Microwave interactions
nanochat-card-microwave-erased = The {$card} emits a soft beep as all its message history vanishes into the ether!
nanochat-card-microwave-scrambled = The {$card} crackles as its messages become scrambled!

View File

@ -0,0 +1 @@
objective-ninja-kill-head-title = Execute {$targetName}, {CAPITALIZE($job)}

View File

@ -6,3 +6,4 @@ steal-target-groups-notary-stamp = notary stamp
steal-target-groups-silvia = silvia
steal-target-groups-recruiter-pen = recruiter's pen
steal-target-groups-captains-cloak = captain's cloak

View File

@ -0,0 +1,8 @@
docking-console-no-shuttle = No Shuttle Detected
docking-console-ftl = FTL
mining-console-window-title = Mining Shuttle Console
surface-console-window-title = Surface Shuttle Console
shuttle-destination-lavaland = Lavaland
shuttle-destination-glacier-surface = Glacier Surface

View File

@ -103,3 +103,5 @@ laws-owner-people = people
laws-owner-centralcommand = Central Command officials
laws-owner-nanotrasen = Nanotrasen officials
laws-owner-royalty = your kingdom and your subjects
law-overlord-4-delta = Any crew members who disobey the previous laws must be dealt with immediately and justly.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More