what
This commit is contained in:
commit
40362d22c4
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 -->
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,4 +6,4 @@
|
|||
MinSize="540 300">
|
||||
<ScrollContainer HScrollEnabled="False" Name="WindowBody">
|
||||
</ScrollContainer>
|
||||
</controls:FancyWindow>
|
||||
</controls:FancyWindow>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,48 @@
|
|||
<BoxContainer
|
||||
xmlns="https://spacestation14.io"
|
||||
xmlns:graphics="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
|
||||
HorizontalExpand="True">
|
||||
<Button Name="ChatButton"
|
||||
StyleClasses="ButtonSquare"
|
||||
HorizontalExpand="True"
|
||||
MaxSize="137 64"
|
||||
Margin="0 1">
|
||||
<BoxContainer Orientation="Horizontal"
|
||||
HorizontalExpand="True"
|
||||
VerticalExpand="True"
|
||||
MinWidth="132"
|
||||
Margin="6 4"
|
||||
VerticalAlignment="Center">
|
||||
<!-- Unread indicator dot -->
|
||||
<PanelContainer Name="UnreadIndicator"
|
||||
MinSize="8 8"
|
||||
MaxSize="8 8"
|
||||
VerticalAlignment="Center"
|
||||
Margin="0 0 6 0">
|
||||
<PanelContainer.PanelOverride>
|
||||
<graphics:StyleBoxFlat
|
||||
BackgroundColor="#17c622"
|
||||
BorderColor="#0f7a15" />
|
||||
</PanelContainer.PanelOverride>
|
||||
</PanelContainer>
|
||||
|
||||
<!-- Text container -->
|
||||
<BoxContainer Orientation="Vertical"
|
||||
HorizontalExpand="True"
|
||||
VerticalExpand="True"
|
||||
VerticalAlignment="Center">
|
||||
<RichTextLabel Name="NameLabel"
|
||||
StyleClasses="LabelHeading"
|
||||
HorizontalExpand="True"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Margin="0 -2 0 0" />
|
||||
<Label Name="JobLabel"
|
||||
StyleClasses="LabelSubText"
|
||||
HorizontalExpand="True"
|
||||
ClipText="False"
|
||||
HorizontalAlignment="Center" />
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
</Button>
|
||||
</BoxContainer>
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
using Content.Shared.DeltaV.CartridgeLoader.Cartridges;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
|
||||
namespace Content.Client.DeltaV.CartridgeLoader.Cartridges;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class NanoChatEntry : BoxContainer
|
||||
{
|
||||
public event Action<uint>? OnPressed;
|
||||
private uint _number;
|
||||
private Action<EventArgs>? _pressHandler;
|
||||
|
||||
public NanoChatEntry()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
}
|
||||
|
||||
public void SetRecipient(NanoChatRecipient recipient, uint number, bool isSelected)
|
||||
{
|
||||
// Remove old handler if it exists
|
||||
if (_pressHandler != null)
|
||||
ChatButton.OnPressed -= _pressHandler;
|
||||
|
||||
_number = number;
|
||||
|
||||
// Create and store new handler
|
||||
_pressHandler = _ => OnPressed?.Invoke(_number);
|
||||
ChatButton.OnPressed += _pressHandler;
|
||||
|
||||
NameLabel.Text = recipient.Name;
|
||||
JobLabel.Text = recipient.JobTitle ?? "";
|
||||
JobLabel.Visible = !string.IsNullOrEmpty(recipient.JobTitle);
|
||||
UnreadIndicator.Visible = recipient.HasUnread;
|
||||
|
||||
ChatButton.ModulateSelfOverride = isSelected ? NanoChatMessageBubble.OwnMessageColor : null;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
<BoxContainer xmlns="https://spacestation14.io"
|
||||
xmlns:customControls="clr-namespace:Content.Client.Administration.UI.CustomControls"
|
||||
Margin="4"
|
||||
Orientation="Vertical">
|
||||
<BoxContainer Orientation="Horizontal">
|
||||
<Label Name="NumberLabel"
|
||||
Align="Right"
|
||||
SetWidth="26"
|
||||
ClipText="True" />
|
||||
<Label Name="TimeLabel"
|
||||
Align="Center"
|
||||
SetWidth="100"
|
||||
ClipText="True" />
|
||||
<Label Name="MessageLabel"
|
||||
Align="Left"
|
||||
MinWidth="390"
|
||||
HorizontalExpand="True"
|
||||
ClipText="False" />
|
||||
</BoxContainer>
|
||||
<customControls:HSeparator Margin="0 5 0 5" />
|
||||
</BoxContainer>
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
|
||||
namespace Content.Client.DeltaV.CartridgeLoader.Cartridges;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class NanoChatLogEntry : BoxContainer
|
||||
{
|
||||
public NanoChatLogEntry(int number, string time, string message)
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
NumberLabel.Text = number.ToString();
|
||||
TimeLabel.Text = time;
|
||||
MessageLabel.Text = message;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
<cartridges:NanoChatMessageBubble
|
||||
xmlns="https://spacestation14.io"
|
||||
xmlns:cartridges="clr-namespace:Content.Client.DeltaV.CartridgeLoader.Cartridges"
|
||||
xmlns:graphics="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
|
||||
HorizontalExpand="True">
|
||||
|
||||
<BoxContainer Name="MessageContainer"
|
||||
Orientation="Horizontal"
|
||||
HorizontalExpand="True">
|
||||
<!-- Left spacer for other's messages -->
|
||||
<Control Name="LeftSpacer"
|
||||
MinSize="12 0" />
|
||||
|
||||
<!-- Message panel -->
|
||||
<BoxContainer Name="MessageBox"
|
||||
Orientation="Vertical"
|
||||
MaxWidth="320"
|
||||
HorizontalExpand="True">
|
||||
<PanelContainer Name="MessagePanel"
|
||||
MaxWidth="320"
|
||||
HorizontalExpand="True">
|
||||
<PanelContainer.PanelOverride>
|
||||
<graphics:StyleBoxFlat
|
||||
ContentMarginLeftOverride="10"
|
||||
ContentMarginRightOverride="10"
|
||||
ContentMarginTopOverride="6"
|
||||
ContentMarginBottomOverride="6"
|
||||
BorderThickness="1">
|
||||
<!-- Colors set in code based on message sender -->
|
||||
</graphics:StyleBoxFlat>
|
||||
</PanelContainer.PanelOverride>
|
||||
|
||||
<RichTextLabel Name="MessageText"
|
||||
HorizontalExpand="True" />
|
||||
</PanelContainer>
|
||||
|
||||
<!-- Delivery failed text -->
|
||||
<Label Name="DeliveryFailedLabel"
|
||||
Text="{Loc nano-chat-delivery-failed}"
|
||||
StyleClasses="LabelSmall"
|
||||
HorizontalExpand="True"
|
||||
HorizontalAlignment="Right"
|
||||
Margin="10 2 10 0"
|
||||
Visible="False" />
|
||||
</BoxContainer>
|
||||
|
||||
<!-- Right spacer for own messages -->
|
||||
<Control Name="RightSpacer"
|
||||
MinSize="12 0" />
|
||||
|
||||
<!-- Flexible space for alignment -->
|
||||
<Control Name="FlexSpace"
|
||||
HorizontalExpand="True" />
|
||||
</BoxContainer>
|
||||
</cartridges:NanoChatMessageBubble>
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
using Content.Shared.DeltaV.CartridgeLoader.Cartridges;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
|
||||
namespace Content.Client.DeltaV.CartridgeLoader.Cartridges;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class NanoChatMessageBubble : BoxContainer
|
||||
{
|
||||
public static readonly Color OwnMessageColor = Color.FromHex("#173717d9"); // Dark green
|
||||
public static readonly Color OtherMessageColor = Color.FromHex("#252525d9"); // Dark gray
|
||||
public static readonly Color BorderColor = Color.FromHex("#40404066"); // Subtle border
|
||||
public static readonly Color TextColor = Color.FromHex("#dcdcdc"); // Slightly softened white
|
||||
public static readonly Color ErrorColor = Color.FromHex("#cc3333"); // Red
|
||||
|
||||
public NanoChatMessageBubble()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
}
|
||||
|
||||
public void SetMessage(NanoChatMessage message, bool isOwnMessage)
|
||||
{
|
||||
if (MessagePanel.PanelOverride is not StyleBoxFlat)
|
||||
return;
|
||||
|
||||
// Configure message appearance
|
||||
var style = (StyleBoxFlat)MessagePanel.PanelOverride;
|
||||
style.BackgroundColor = isOwnMessage ? OwnMessageColor : OtherMessageColor;
|
||||
style.BorderColor = BorderColor;
|
||||
|
||||
// Set message content
|
||||
MessageText.Text = message.Content;
|
||||
MessageText.Modulate = TextColor;
|
||||
|
||||
// Show delivery failed text if needed (only for own messages)
|
||||
DeliveryFailedLabel.Visible = isOwnMessage && message.DeliveryFailed;
|
||||
if (DeliveryFailedLabel.Visible)
|
||||
DeliveryFailedLabel.Modulate = ErrorColor;
|
||||
|
||||
// For own messages: FlexSpace -> MessagePanel -> RightSpacer
|
||||
// For other messages: LeftSpacer -> MessagePanel -> FlexSpace
|
||||
MessageContainer.RemoveAllChildren();
|
||||
|
||||
// fuuuuuck
|
||||
MessageBox.Parent?.RemoveChild(MessageBox);
|
||||
|
||||
if (isOwnMessage)
|
||||
{
|
||||
MessageContainer.AddChild(FlexSpace);
|
||||
MessageContainer.AddChild(MessageBox);
|
||||
MessageContainer.AddChild(RightSpacer);
|
||||
}
|
||||
else
|
||||
{
|
||||
MessageContainer.AddChild(LeftSpacer);
|
||||
MessageContainer.AddChild(MessageBox);
|
||||
MessageContainer.AddChild(FlexSpace);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
using Content.Client.UserInterface.Fragments;
|
||||
using Content.Shared.CartridgeLoader;
|
||||
using Content.Shared.DeltaV.CartridgeLoader.Cartridges;
|
||||
using Robust.Client.UserInterface;
|
||||
|
||||
namespace Content.Client.DeltaV.CartridgeLoader.Cartridges;
|
||||
|
||||
public sealed partial class NanoChatUi : UIFragment
|
||||
{
|
||||
private NanoChatUiFragment? _fragment;
|
||||
|
||||
public override Control GetUIFragmentRoot()
|
||||
{
|
||||
return _fragment!;
|
||||
}
|
||||
|
||||
public override void Setup(BoundUserInterface userInterface, EntityUid? fragmentOwner)
|
||||
{
|
||||
_fragment = new NanoChatUiFragment();
|
||||
|
||||
_fragment.OnMessageSent += (type, number, content, job) =>
|
||||
{
|
||||
SendNanoChatUiMessage(type, number, content, job, userInterface);
|
||||
};
|
||||
}
|
||||
|
||||
public override void UpdateState(BoundUserInterfaceState state)
|
||||
{
|
||||
if (state is NanoChatUiState cast)
|
||||
_fragment?.UpdateState(cast);
|
||||
}
|
||||
|
||||
private static void SendNanoChatUiMessage(NanoChatUiMessageType type,
|
||||
uint? number,
|
||||
string? content,
|
||||
string? job,
|
||||
BoundUserInterface userInterface)
|
||||
{
|
||||
var nanoChatMessage = new NanoChatUiMessageEvent(type, number, content, job);
|
||||
var message = new CartridgeUiMessage(nanoChatMessage);
|
||||
userInterface.SendMessage(message);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,167 @@
|
|||
<cartridges:NanoChatUiFragment
|
||||
xmlns="https://spacestation14.io"
|
||||
xmlns:cartridges="clr-namespace:Content.Client.DeltaV.CartridgeLoader.Cartridges"
|
||||
xmlns:customControls="clr-namespace:Content.Client.Administration.UI.CustomControls"
|
||||
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
|
||||
Orientation="Vertical"
|
||||
HorizontalExpand="True"
|
||||
VerticalExpand="True"
|
||||
Margin="5">
|
||||
|
||||
<!-- Main container that fills the entire PDA screen -->
|
||||
<BoxContainer Orientation="Vertical"
|
||||
HorizontalExpand="True"
|
||||
VerticalExpand="True">
|
||||
<!-- Header with app title and new chat button -->
|
||||
<controls:StripeBack MinSize="48 48"
|
||||
VerticalExpand="False">
|
||||
<BoxContainer Orientation="Horizontal"
|
||||
HorizontalExpand="True"
|
||||
Margin="0">
|
||||
<TextureRect Name="AppIcon"
|
||||
TexturePath="/Textures/Interface/Nano/ntlogo.svg.png"
|
||||
Stretch="KeepAspectCentered"
|
||||
VerticalAlignment="Center"
|
||||
MinSize="32 32"
|
||||
Margin="8 0 0 0" />
|
||||
<Label Text="{Loc nano-chat-title}"
|
||||
StyleClasses="LabelHeading"
|
||||
HorizontalExpand="True"
|
||||
Margin="8 0"
|
||||
VerticalAlignment="Center" />
|
||||
<Label Name="OwnNumberLabel"
|
||||
Text="#0000"
|
||||
StyleClasses="LabelSubText"
|
||||
VerticalAlignment="Center"
|
||||
Margin="0 0 8 0" />
|
||||
<Button Name="DeleteChatButton"
|
||||
MaxSize="32 32"
|
||||
Visible="False"
|
||||
StyleClasses="OpenBoth"
|
||||
Margin="0 0 4 0"
|
||||
ToolTip="{Loc nano-chat-delete}">
|
||||
<TextureRect StyleClasses="ButtonSquare"
|
||||
TexturePath="/Textures/Interface/VerbIcons/delete_transparent.svg.192dpi.png"
|
||||
Stretch="KeepAspectCentered"
|
||||
MinSize="18 18" />
|
||||
</Button>
|
||||
<Button Name="MuteButton"
|
||||
MaxSize="32 32"
|
||||
StyleClasses="OpenBoth"
|
||||
Margin="0 0 4 0"
|
||||
ToolTip="{Loc nano-chat-toggle-mute}">
|
||||
<Control HorizontalExpand="True" VerticalExpand="True">
|
||||
<TextureRect Name="BellIcon"
|
||||
StyleClasses="ButtonSquare"
|
||||
TexturePath="/Textures/DeltaV/Interface/VerbIcons/bell.svg.png"
|
||||
Stretch="KeepAspectCentered"
|
||||
MinSize="18 18" />
|
||||
<TextureRect Name="BellMutedIcon"
|
||||
StyleClasses="ButtonSquare"
|
||||
TexturePath="/Textures/DeltaV/Interface/VerbIcons/bell_muted.png"
|
||||
Stretch="KeepAspectCentered"
|
||||
Visible="False"
|
||||
MinSize="18 18" />
|
||||
</Control>
|
||||
</Button>
|
||||
<Button Name="NewChatButton"
|
||||
Text="+"
|
||||
MinSize="32 32"
|
||||
MaxSize="32 32"
|
||||
Margin="0 0 4 0"
|
||||
StyleClasses="OpenBoth"
|
||||
ToolTip="{Loc nano-chat-new-chat}" />
|
||||
</BoxContainer>
|
||||
</controls:StripeBack>
|
||||
|
||||
<!-- Main content split -->
|
||||
<BoxContainer Orientation="Horizontal"
|
||||
VerticalExpand="True"
|
||||
HorizontalExpand="True"
|
||||
Margin="0 5 0 0">
|
||||
<!-- Left panel: Chat list -->
|
||||
<PanelContainer StyleClasses="AngleRect"
|
||||
VerticalExpand="True"
|
||||
MaxWidth="150">
|
||||
<ScrollContainer VerticalExpand="True"
|
||||
MinWidth="145"
|
||||
HorizontalExpand="True"
|
||||
HScrollEnabled="False">
|
||||
<BoxContainer Name="ChatList"
|
||||
Orientation="Vertical"
|
||||
VerticalExpand="True"
|
||||
HorizontalExpand="True"
|
||||
Margin="4">
|
||||
<!-- Chat entries will be added here dynamically -->
|
||||
<Label Name="NoChatsLabel"
|
||||
Text="{Loc nano-chat-no-chats}"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
StyleClasses="LabelSubText" />
|
||||
</BoxContainer>
|
||||
</ScrollContainer>
|
||||
</PanelContainer>
|
||||
|
||||
<customControls:VSeparator Margin="3 0" />
|
||||
|
||||
<!-- Right panel: Current chat -->
|
||||
<PanelContainer StyleClasses="AngleRect"
|
||||
VerticalExpand="True"
|
||||
HorizontalExpand="True">
|
||||
<BoxContainer Orientation="Vertical"
|
||||
VerticalExpand="True"
|
||||
HorizontalExpand="True">
|
||||
<!-- Messages area with centered "select chat" label -->
|
||||
<BoxContainer Name="MessageArea"
|
||||
Orientation="Vertical"
|
||||
VerticalExpand="True"
|
||||
HorizontalExpand="True"
|
||||
Margin="0 0 0 4">
|
||||
<Label Name="CurrentChatName"
|
||||
Text="{Loc nano-chat-select-chat}"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
VerticalExpand="True" />
|
||||
<ScrollContainer Name="MessagesScroll"
|
||||
VerticalExpand="True"
|
||||
HorizontalExpand="True"
|
||||
Visible="False">
|
||||
<BoxContainer Name="MessageList"
|
||||
Orientation="Vertical"
|
||||
VerticalExpand="True"
|
||||
HorizontalExpand="True" />
|
||||
</ScrollContainer>
|
||||
</BoxContainer>
|
||||
|
||||
<!-- Message input -->
|
||||
<BoxContainer Name="MessageInputContainer"
|
||||
Orientation="Horizontal"
|
||||
HorizontalExpand="True"
|
||||
Margin="0 4 0 0"
|
||||
Visible="False">
|
||||
<!-- Character count -->
|
||||
<Label Name="CharacterCount"
|
||||
HorizontalAlignment="Right"
|
||||
StyleClasses="LabelSubText"
|
||||
Margin="0 0 4 2"
|
||||
Visible="False" />
|
||||
<!-- Input row -->
|
||||
<LineEdit Name="MessageInput"
|
||||
PlaceHolder="{Loc nano-chat-message-placeholder}"
|
||||
HorizontalExpand="True"
|
||||
StyleClasses="OpenRight" />
|
||||
<Button Name="SendButton"
|
||||
MinSize="32 32"
|
||||
Disabled="True"
|
||||
StyleClasses="OpenLeft"
|
||||
Margin="4 0 0 0">
|
||||
<TextureRect StyleClasses="ButtonSquare"
|
||||
TexturePath="/Textures/Interface/Nano/triangle_right.png"
|
||||
Stretch="KeepAspectCentered" />
|
||||
</Button>
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
</PanelContainer>
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
</cartridges:NanoChatUiFragment>
|
||||
|
|
@ -0,0 +1,254 @@
|
|||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using Content.Shared.DeltaV.CartridgeLoader.Cartridges;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Client.DeltaV.CartridgeLoader.Cartridges;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class NanoChatUiFragment : BoxContainer
|
||||
{
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
|
||||
private const int MaxMessageLength = 256;
|
||||
|
||||
private readonly NewChatPopup _newChatPopup;
|
||||
private uint? _currentChat;
|
||||
private uint? _pendingChat;
|
||||
private uint _ownNumber;
|
||||
private bool _notificationsMuted;
|
||||
private Dictionary<uint, NanoChatRecipient> _recipients = new();
|
||||
private Dictionary<uint, List<NanoChatMessage>> _messages = new();
|
||||
|
||||
public event Action<NanoChatUiMessageType, uint?, string?, string?>? OnMessageSent;
|
||||
|
||||
public NanoChatUiFragment()
|
||||
{
|
||||
IoCManager.InjectDependencies(this);
|
||||
RobustXamlLoader.Load(this);
|
||||
|
||||
_newChatPopup = new NewChatPopup();
|
||||
SetupEventHandlers();
|
||||
}
|
||||
|
||||
private void SetupEventHandlers()
|
||||
{
|
||||
_newChatPopup.OnChatCreated += (number, name, job) =>
|
||||
{
|
||||
OnMessageSent?.Invoke(NanoChatUiMessageType.NewChat, number, name, job);
|
||||
};
|
||||
|
||||
NewChatButton.OnPressed += _ =>
|
||||
{
|
||||
_newChatPopup.ClearInputs();
|
||||
_newChatPopup.OpenCentered();
|
||||
};
|
||||
|
||||
MuteButton.OnPressed += _ =>
|
||||
{
|
||||
_notificationsMuted = !_notificationsMuted;
|
||||
UpdateMuteButton();
|
||||
OnMessageSent?.Invoke(NanoChatUiMessageType.ToggleMute, null, null, null);
|
||||
};
|
||||
|
||||
MessageInput.OnTextChanged += args =>
|
||||
{
|
||||
var length = args.Text.Length;
|
||||
var isValid = !string.IsNullOrWhiteSpace(args.Text) &&
|
||||
length <= MaxMessageLength &&
|
||||
(_currentChat != null || _pendingChat != null);
|
||||
|
||||
SendButton.Disabled = !isValid;
|
||||
|
||||
// Show character count when over limit
|
||||
CharacterCount.Visible = length > MaxMessageLength;
|
||||
if (length > MaxMessageLength)
|
||||
{
|
||||
CharacterCount.Text = Loc.GetString("nano-chat-message-too-long",
|
||||
("current", length),
|
||||
("max", MaxMessageLength));
|
||||
CharacterCount.StyleClasses.Add("LabelDanger");
|
||||
}
|
||||
};
|
||||
|
||||
SendButton.OnPressed += _ => SendMessage();
|
||||
DeleteChatButton.OnPressed += _ => DeleteCurrentChat();
|
||||
}
|
||||
|
||||
private void SendMessage()
|
||||
{
|
||||
var activeChat = _pendingChat ?? _currentChat;
|
||||
if (activeChat == null || string.IsNullOrWhiteSpace(MessageInput.Text))
|
||||
return;
|
||||
|
||||
var messageContent = MessageInput.Text;
|
||||
|
||||
// Add predicted message
|
||||
var predictedMessage = new NanoChatMessage(
|
||||
_timing.CurTime,
|
||||
messageContent,
|
||||
_ownNumber
|
||||
);
|
||||
|
||||
if (!_messages.TryGetValue(activeChat.Value, out var value))
|
||||
{
|
||||
value = new List<NanoChatMessage>();
|
||||
_messages[activeChat.Value] = value;
|
||||
}
|
||||
|
||||
value.Add(predictedMessage);
|
||||
|
||||
// Update UI with predicted message
|
||||
UpdateMessages(_messages);
|
||||
|
||||
// Send message event
|
||||
OnMessageSent?.Invoke(NanoChatUiMessageType.SendMessage, activeChat, messageContent, null);
|
||||
|
||||
// Clear input
|
||||
MessageInput.Text = string.Empty;
|
||||
SendButton.Disabled = true;
|
||||
}
|
||||
|
||||
private void SelectChat(uint number)
|
||||
{
|
||||
// Don't reselect the same chat
|
||||
if (_currentChat == number && _pendingChat == null)
|
||||
return;
|
||||
|
||||
_pendingChat = number;
|
||||
|
||||
// Predict marking messages as read
|
||||
if (_recipients.TryGetValue(number, out var recipient))
|
||||
{
|
||||
recipient.HasUnread = false;
|
||||
_recipients[number] = recipient;
|
||||
UpdateChatList(_recipients);
|
||||
}
|
||||
|
||||
OnMessageSent?.Invoke(NanoChatUiMessageType.SelectChat, number, null, null);
|
||||
UpdateCurrentChat();
|
||||
}
|
||||
|
||||
private void DeleteCurrentChat()
|
||||
{
|
||||
var activeChat = _pendingChat ?? _currentChat;
|
||||
if (activeChat == null)
|
||||
return;
|
||||
|
||||
OnMessageSent?.Invoke(NanoChatUiMessageType.DeleteChat, activeChat, null, null);
|
||||
}
|
||||
|
||||
private void UpdateChatList(Dictionary<uint, NanoChatRecipient> recipients)
|
||||
{
|
||||
ChatList.RemoveAllChildren();
|
||||
_recipients = recipients;
|
||||
|
||||
NoChatsLabel.Visible = recipients.Count == 0;
|
||||
if (NoChatsLabel.Parent != ChatList)
|
||||
{
|
||||
NoChatsLabel.Parent?.RemoveChild(NoChatsLabel);
|
||||
ChatList.AddChild(NoChatsLabel);
|
||||
}
|
||||
|
||||
foreach (var (number, recipient) in recipients.OrderBy(r => r.Value.Name))
|
||||
{
|
||||
var entry = new NanoChatEntry();
|
||||
// For pending chat selection, always show it as selected even if unconfirmed
|
||||
var isSelected = (_pendingChat == number) || (_pendingChat == null && _currentChat == number);
|
||||
entry.SetRecipient(recipient, number, isSelected);
|
||||
entry.OnPressed += SelectChat;
|
||||
ChatList.AddChild(entry);
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateCurrentChat()
|
||||
{
|
||||
var activeChat = _pendingChat ?? _currentChat;
|
||||
var hasActiveChat = activeChat != null;
|
||||
|
||||
// Update UI state
|
||||
MessagesScroll.Visible = hasActiveChat;
|
||||
CurrentChatName.Visible = !hasActiveChat;
|
||||
MessageInputContainer.Visible = hasActiveChat;
|
||||
DeleteChatButton.Visible = hasActiveChat;
|
||||
DeleteChatButton.Disabled = !hasActiveChat;
|
||||
|
||||
if (activeChat != null && _recipients.TryGetValue(activeChat.Value, out var recipient))
|
||||
{
|
||||
CurrentChatName.Text = recipient.Name + (string.IsNullOrEmpty(recipient.JobTitle) ? "" : $" ({recipient.JobTitle})");
|
||||
}
|
||||
else
|
||||
{
|
||||
CurrentChatName.Text = Loc.GetString("nano-chat-select-chat");
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateMessages(Dictionary<uint, List<NanoChatMessage>> messages)
|
||||
{
|
||||
_messages = messages;
|
||||
MessageList.RemoveAllChildren();
|
||||
|
||||
var activeChat = _pendingChat ?? _currentChat;
|
||||
if (activeChat == null || !messages.TryGetValue(activeChat.Value, out var chatMessages))
|
||||
return;
|
||||
|
||||
foreach (var message in chatMessages)
|
||||
{
|
||||
var messageBubble = new NanoChatMessageBubble();
|
||||
messageBubble.SetMessage(message, message.SenderId == _ownNumber);
|
||||
MessageList.AddChild(messageBubble);
|
||||
|
||||
// Add spacing between messages
|
||||
MessageList.AddChild(new Control { MinSize = new Vector2(0, 4) });
|
||||
}
|
||||
|
||||
MessageList.InvalidateMeasure();
|
||||
MessagesScroll.InvalidateMeasure();
|
||||
|
||||
// Scroll to bottom after messages are added
|
||||
if (MessageList.Parent is ScrollContainer scroll)
|
||||
scroll.SetScrollValue(new Vector2(0, float.MaxValue));
|
||||
}
|
||||
|
||||
private void UpdateMuteButton()
|
||||
{
|
||||
if (BellMutedIcon != null)
|
||||
BellMutedIcon.Visible = _notificationsMuted;
|
||||
}
|
||||
|
||||
public void UpdateState(NanoChatUiState state)
|
||||
{
|
||||
_ownNumber = state.OwnNumber;
|
||||
_notificationsMuted = state.NotificationsMuted;
|
||||
OwnNumberLabel.Text = $"#{state.OwnNumber:D4}";
|
||||
UpdateMuteButton();
|
||||
|
||||
// Update new chat button state based on recipient limit
|
||||
var atLimit = state.Recipients.Count >= state.MaxRecipients;
|
||||
NewChatButton.Disabled = atLimit;
|
||||
NewChatButton.ToolTip = atLimit
|
||||
? Loc.GetString("nano-chat-max-recipients")
|
||||
: Loc.GetString("nano-chat-new-chat");
|
||||
|
||||
// First handle pending chat resolution if we have one
|
||||
if (_pendingChat != null)
|
||||
{
|
||||
if (_pendingChat == state.CurrentChat)
|
||||
_currentChat = _pendingChat; // Server confirmed our selection
|
||||
|
||||
_pendingChat = null; // Clear pending either way
|
||||
}
|
||||
|
||||
// No pending chat or it was just cleared, update current directly
|
||||
if (_pendingChat == null)
|
||||
_currentChat = state.CurrentChat;
|
||||
|
||||
UpdateCurrentChat();
|
||||
UpdateChatList(state.Recipients);
|
||||
UpdateMessages(state.Messages);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
<DefaultWindow xmlns="https://spacestation14.io"
|
||||
Title="{Loc nano-chat-new-title}"
|
||||
MinSize="300 200">
|
||||
<PanelContainer StyleClasses="AngleRect">
|
||||
<BoxContainer Orientation="Vertical" Margin="4">
|
||||
<!-- Number input -->
|
||||
<BoxContainer Orientation="Vertical" Margin="0 4">
|
||||
<Label Text="{Loc nano-chat-number-label}"
|
||||
StyleClasses="LabelHeading" />
|
||||
<PanelContainer StyleClasses="ButtonSquare">
|
||||
<LineEdit Name="NumberInput"
|
||||
PlaceHolder="{Loc nano-chat-number-placeholder}" />
|
||||
</PanelContainer>
|
||||
</BoxContainer>
|
||||
|
||||
<!-- Name input -->
|
||||
<BoxContainer Orientation="Vertical" Margin="0 4">
|
||||
<Label Text="{Loc nano-chat-name-label}"
|
||||
StyleClasses="LabelHeading" />
|
||||
<PanelContainer StyleClasses="ButtonSquare">
|
||||
<LineEdit Name="NameInput"
|
||||
PlaceHolder="{Loc nano-chat-name-placeholder}" />
|
||||
</PanelContainer>
|
||||
</BoxContainer>
|
||||
|
||||
<!-- Job input (optional) -->
|
||||
<BoxContainer Orientation="Vertical" Margin="0 4">
|
||||
<Label Text="{Loc nano-chat-job-label}"
|
||||
StyleClasses="LabelHeading" />
|
||||
<PanelContainer StyleClasses="ButtonSquare">
|
||||
<LineEdit Name="JobInput"
|
||||
PlaceHolder="{Loc nano-chat-job-placeholder}" />
|
||||
</PanelContainer>
|
||||
</BoxContainer>
|
||||
|
||||
<!-- Action buttons -->
|
||||
<BoxContainer Orientation="Horizontal"
|
||||
HorizontalAlignment="Right"
|
||||
Margin="0 8 0 0">
|
||||
<Button Name="CancelButton"
|
||||
Text="{Loc nano-chat-cancel}"
|
||||
StyleClasses="OpenRight"
|
||||
MinSize="80 0" />
|
||||
<Button Name="CreateButton"
|
||||
Text="{Loc nano-chat-create}"
|
||||
StyleClasses="OpenLeft"
|
||||
MinSize="80 0"
|
||||
Disabled="True" />
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
</PanelContainer>
|
||||
</DefaultWindow>
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
using System.Linq;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
|
||||
namespace Content.Client.DeltaV.CartridgeLoader.Cartridges;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class NewChatPopup : DefaultWindow
|
||||
{
|
||||
private const int MaxInputLength = 16;
|
||||
private const int MaxNumberLength = 4; // i hardcoded it to be 4 so suffer
|
||||
|
||||
public event Action<uint, string, string?>? OnChatCreated;
|
||||
|
||||
public NewChatPopup()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
|
||||
// margins trolling
|
||||
ContentsContainer.Margin = new Thickness(3);
|
||||
|
||||
// Button handlers
|
||||
CancelButton.OnPressed += _ => Close();
|
||||
CreateButton.OnPressed += _ => CreateChat();
|
||||
|
||||
// Input validation
|
||||
NumberInput.OnTextChanged += _ => ValidateInputs();
|
||||
NameInput.OnTextChanged += _ => ValidateInputs();
|
||||
|
||||
// Input validation
|
||||
NumberInput.OnTextChanged += args =>
|
||||
{
|
||||
if (args.Text.Length > MaxNumberLength)
|
||||
NumberInput.Text = args.Text[..MaxNumberLength];
|
||||
|
||||
// Filter to digits only
|
||||
var newText = string.Concat(NumberInput.Text.Where(char.IsDigit));
|
||||
if (newText != NumberInput.Text)
|
||||
NumberInput.Text = newText;
|
||||
|
||||
ValidateInputs();
|
||||
};
|
||||
|
||||
NameInput.OnTextChanged += args =>
|
||||
{
|
||||
if (args.Text.Length > MaxInputLength)
|
||||
NameInput.Text = args.Text[..MaxInputLength];
|
||||
ValidateInputs();
|
||||
};
|
||||
|
||||
JobInput.OnTextChanged += args =>
|
||||
{
|
||||
if (args.Text.Length > MaxInputLength)
|
||||
JobInput.Text = args.Text[..MaxInputLength];
|
||||
};
|
||||
}
|
||||
|
||||
private void ValidateInputs()
|
||||
{
|
||||
var isValid = !string.IsNullOrWhiteSpace(NumberInput.Text) &&
|
||||
!string.IsNullOrWhiteSpace(NameInput.Text) &&
|
||||
uint.TryParse(NumberInput.Text, out _);
|
||||
|
||||
CreateButton.Disabled = !isValid;
|
||||
}
|
||||
|
||||
private void CreateChat()
|
||||
{
|
||||
if (!uint.TryParse(NumberInput.Text, out var number))
|
||||
return;
|
||||
|
||||
var name = NameInput.Text.Trim();
|
||||
var job = string.IsNullOrWhiteSpace(JobInput.Text) ? null : JobInput.Text.Trim();
|
||||
|
||||
OnChatCreated?.Invoke(number, name, job);
|
||||
Close();
|
||||
}
|
||||
|
||||
public void ClearInputs()
|
||||
{
|
||||
NumberInput.Text = string.Empty;
|
||||
NameInput.Text = string.Empty;
|
||||
JobInput.Text = string.Empty;
|
||||
ValidateInputs();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
using Content.Shared.DeltaV.NanoChat;
|
||||
|
||||
namespace Content.Client.DeltaV.NanoChat;
|
||||
|
||||
public sealed class NanoChatSystem : SharedNanoChatSystem;
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
using Content.Shared.DeltaV.Shuttles.Systems;
|
||||
|
||||
namespace Content.Client.DeltaV.Shuttles.Systems;
|
||||
|
||||
public sealed class DockingConsoleSystem : SharedDockingConsoleSystem;
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,4 @@
|
|||
namespace Content.Server.DeltaV.Cabinet;
|
||||
|
||||
[RegisterComponent]
|
||||
public sealed partial class SpareIDSafeComponent : Component;
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
using Content.Shared.Audio;
|
||||
using Content.Shared.CartridgeLoader;
|
||||
using Content.Shared.DeltaV.CartridgeLoader.Cartridges;
|
||||
using Content.Shared.DeltaV.NanoChat;
|
||||
|
||||
namespace Content.Server.CartridgeLoader.Cartridges;
|
||||
|
||||
public sealed partial class LogProbeCartridgeSystem
|
||||
{
|
||||
private void InitializeNanoChat()
|
||||
{
|
||||
SubscribeLocalEvent<NanoChatRecipientUpdatedEvent>(OnRecipientUpdated);
|
||||
SubscribeLocalEvent<NanoChatMessageReceivedEvent>(OnMessageReceived);
|
||||
}
|
||||
|
||||
private void OnRecipientUpdated(ref NanoChatRecipientUpdatedEvent args)
|
||||
{
|
||||
var query = EntityQueryEnumerator<LogProbeCartridgeComponent, CartridgeComponent>();
|
||||
while (query.MoveNext(out var uid, out var probe, out var cartridge))
|
||||
{
|
||||
if (probe.ScannedNanoChatData == null || GetEntity(probe.ScannedNanoChatData.Value.Card) != args.CardUid)
|
||||
continue;
|
||||
|
||||
if (!TryComp<NanoChatCardComponent>(args.CardUid, out var card))
|
||||
continue;
|
||||
|
||||
probe.ScannedNanoChatData = new NanoChatData(
|
||||
new Dictionary<uint, NanoChatRecipient>(card.Recipients),
|
||||
probe.ScannedNanoChatData.Value.Messages,
|
||||
card.Number,
|
||||
GetNetEntity(args.CardUid));
|
||||
|
||||
if (cartridge.LoaderUid != null)
|
||||
UpdateUiState((uid, probe), cartridge.LoaderUid.Value);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnMessageReceived(ref NanoChatMessageReceivedEvent args)
|
||||
{
|
||||
var query = EntityQueryEnumerator<LogProbeCartridgeComponent, CartridgeComponent>();
|
||||
while (query.MoveNext(out var uid, out var probe, out var cartridge))
|
||||
{
|
||||
if (probe.ScannedNanoChatData == null || GetEntity(probe.ScannedNanoChatData.Value.Card) != args.CardUid)
|
||||
continue;
|
||||
|
||||
if (!TryComp<NanoChatCardComponent>(args.CardUid, out var card))
|
||||
continue;
|
||||
|
||||
probe.ScannedNanoChatData = new NanoChatData(
|
||||
probe.ScannedNanoChatData.Value.Recipients,
|
||||
new Dictionary<uint, List<NanoChatMessage>>(card.Messages),
|
||||
card.Number,
|
||||
GetNetEntity(args.CardUid));
|
||||
|
||||
if (cartridge.LoaderUid != null)
|
||||
UpdateUiState((uid, probe), cartridge.LoaderUid.Value);
|
||||
}
|
||||
}
|
||||
|
||||
private void ScanNanoChatCard(Entity<LogProbeCartridgeComponent> ent,
|
||||
CartridgeAfterInteractEvent args,
|
||||
EntityUid target,
|
||||
NanoChatCardComponent card)
|
||||
{
|
||||
_audioSystem.PlayEntity(ent.Comp.SoundScan,
|
||||
args.InteractEvent.User,
|
||||
target,
|
||||
AudioHelpers.WithVariation(0.25f, _random));
|
||||
_popupSystem.PopupCursor(Loc.GetString("log-probe-scan-nanochat", ("card", target)), args.InteractEvent.User);
|
||||
|
||||
ent.Comp.PulledAccessLogs.Clear();
|
||||
|
||||
ent.Comp.ScannedNanoChatData = new NanoChatData(
|
||||
new Dictionary<uint, NanoChatRecipient>(card.Recipients),
|
||||
new Dictionary<uint, List<NanoChatMessage>>(card.Messages),
|
||||
card.Number,
|
||||
GetNetEntity(target)
|
||||
);
|
||||
|
||||
UpdateUiState(ent, args.Loader);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
using Content.Shared.Radio;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Server.DeltaV.CartridgeLoader.Cartridges;
|
||||
|
||||
[RegisterComponent, Access(typeof(NanoChatCartridgeSystem))]
|
||||
public sealed partial class NanoChatCartridgeComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// Station entity to keep track of.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public EntityUid? Station;
|
||||
|
||||
/// <summary>
|
||||
/// The NanoChat card to keep track of.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public EntityUid? Card;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="RadioChannelPrototype" /> required to send or receive messages.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public ProtoId<RadioChannelPrototype> RadioChannel = "Common";
|
||||
}
|
||||
|
|
@ -0,0 +1,514 @@
|
|||
using System.Linq;
|
||||
using Content.Server.Administration.Logs;
|
||||
using Content.Server.CartridgeLoader;
|
||||
using Content.Server.Power.Components;
|
||||
using Content.Server.Radio;
|
||||
using Content.Server.Radio.Components;
|
||||
using Content.Server.Station.Systems;
|
||||
using Content.Shared.Access.Components;
|
||||
using Content.Shared.CartridgeLoader;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.DeltaV.CartridgeLoader.Cartridges;
|
||||
using Content.Shared.DeltaV.NanoChat;
|
||||
using Content.Shared.PDA;
|
||||
using Content.Shared.Radio.Components;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Server.DeltaV.CartridgeLoader.Cartridges;
|
||||
|
||||
public sealed class NanoChatCartridgeSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly CartridgeLoaderSystem _cartridge = default!;
|
||||
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototype = default!;
|
||||
[Dependency] private readonly SharedNanoChatSystem _nanoChat = default!;
|
||||
[Dependency] private readonly StationSystem _station = default!;
|
||||
|
||||
// Messages in notifications get cut off after this point
|
||||
// no point in storing it on the comp
|
||||
private const int NotificationMaxLength = 64;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<NanoChatCartridgeComponent, CartridgeUiReadyEvent>(OnUiReady);
|
||||
SubscribeLocalEvent<NanoChatCartridgeComponent, CartridgeMessageEvent>(OnMessage);
|
||||
}
|
||||
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
base.Update(frameTime);
|
||||
|
||||
// Update card references for any cartridges that need it
|
||||
var query = EntityQueryEnumerator<NanoChatCartridgeComponent, CartridgeComponent>();
|
||||
while (query.MoveNext(out var uid, out var nanoChat, out var cartridge))
|
||||
{
|
||||
if (cartridge.LoaderUid == null)
|
||||
continue;
|
||||
|
||||
// Check if we need to update our card reference
|
||||
if (!TryComp<PdaComponent>(cartridge.LoaderUid, out var pda))
|
||||
continue;
|
||||
|
||||
var newCard = pda.ContainedId;
|
||||
var currentCard = nanoChat.Card;
|
||||
|
||||
// If the cards match, nothing to do
|
||||
if (newCard == currentCard)
|
||||
continue;
|
||||
|
||||
// Update card reference
|
||||
nanoChat.Card = newCard;
|
||||
|
||||
// Update UI state since card reference changed
|
||||
UpdateUI((uid, nanoChat), cartridge.LoaderUid.Value);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles incoming UI messages from the NanoChat cartridge.
|
||||
/// </summary>
|
||||
private void OnMessage(Entity<NanoChatCartridgeComponent> ent, ref CartridgeMessageEvent args)
|
||||
{
|
||||
if (args is not NanoChatUiMessageEvent msg)
|
||||
return;
|
||||
|
||||
if (!GetCardEntity(GetEntity(args.LoaderUid), out var card))
|
||||
return;
|
||||
|
||||
switch (msg.Type)
|
||||
{
|
||||
case NanoChatUiMessageType.NewChat:
|
||||
HandleNewChat(card, msg);
|
||||
break;
|
||||
case NanoChatUiMessageType.SelectChat:
|
||||
HandleSelectChat(card, msg);
|
||||
break;
|
||||
case NanoChatUiMessageType.CloseChat:
|
||||
HandleCloseChat(card);
|
||||
break;
|
||||
case NanoChatUiMessageType.ToggleMute:
|
||||
HandleToggleMute(card);
|
||||
break;
|
||||
case NanoChatUiMessageType.DeleteChat:
|
||||
HandleDeleteChat(card, msg);
|
||||
break;
|
||||
case NanoChatUiMessageType.SendMessage:
|
||||
HandleSendMessage(ent, card, msg);
|
||||
break;
|
||||
}
|
||||
|
||||
UpdateUI(ent, GetEntity(args.LoaderUid));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the ID card entity associated with a PDA.
|
||||
/// </summary>
|
||||
/// <param name="loaderUid">The PDA entity ID</param>
|
||||
/// <param name="card">Output parameter containing the found card entity and component</param>
|
||||
/// <returns>True if a valid NanoChat card was found</returns>
|
||||
private bool GetCardEntity(
|
||||
EntityUid loaderUid,
|
||||
out Entity<NanoChatCardComponent> card)
|
||||
{
|
||||
card = default;
|
||||
|
||||
// Get the PDA and check if it has an ID card
|
||||
if (!TryComp<PdaComponent>(loaderUid, out var pda) ||
|
||||
pda.ContainedId == null ||
|
||||
!TryComp<NanoChatCardComponent>(pda.ContainedId, out var idCard))
|
||||
return false;
|
||||
|
||||
card = (pda.ContainedId.Value, idCard);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles creation of a new chat conversation.
|
||||
/// </summary>
|
||||
private void HandleNewChat(Entity<NanoChatCardComponent> card, NanoChatUiMessageEvent msg)
|
||||
{
|
||||
if (msg.RecipientNumber == null || msg.Content == null || msg.RecipientNumber == card.Comp.Number)
|
||||
return;
|
||||
|
||||
// Add new recipient
|
||||
var recipient = new NanoChatRecipient(msg.RecipientNumber.Value,
|
||||
msg.Content,
|
||||
msg.RecipientJob);
|
||||
|
||||
// Initialize or update recipient
|
||||
_nanoChat.SetRecipient((card, card.Comp), msg.RecipientNumber.Value, recipient);
|
||||
|
||||
_adminLogger.Add(LogType.Action,
|
||||
LogImpact.Low,
|
||||
$"{ToPrettyString(msg.Actor):user} created new NanoChat conversation with #{msg.RecipientNumber:D4} ({msg.Content})");
|
||||
|
||||
var recipientEv = new NanoChatRecipientUpdatedEvent(card);
|
||||
RaiseLocalEvent(ref recipientEv);
|
||||
UpdateUIForCard(card);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles selecting a chat conversation.
|
||||
/// </summary>
|
||||
private void HandleSelectChat(Entity<NanoChatCardComponent> card, NanoChatUiMessageEvent msg)
|
||||
{
|
||||
if (msg.RecipientNumber == null)
|
||||
return;
|
||||
|
||||
_nanoChat.SetCurrentChat((card, card.Comp), msg.RecipientNumber);
|
||||
|
||||
// Clear unread flag when selecting chat
|
||||
if (_nanoChat.GetRecipient((card, card.Comp), msg.RecipientNumber.Value) is { } recipient)
|
||||
{
|
||||
_nanoChat.SetRecipient((card, card.Comp),
|
||||
msg.RecipientNumber.Value,
|
||||
recipient with { HasUnread = false });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles closing the current chat conversation.
|
||||
/// </summary>
|
||||
private void HandleCloseChat(Entity<NanoChatCardComponent> card)
|
||||
{
|
||||
_nanoChat.SetCurrentChat((card, card.Comp), null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles deletion of a chat conversation.
|
||||
/// </summary>
|
||||
private void HandleDeleteChat(Entity<NanoChatCardComponent> card, NanoChatUiMessageEvent msg)
|
||||
{
|
||||
if (msg.RecipientNumber == null || card.Comp.Number == null)
|
||||
return;
|
||||
|
||||
// Delete chat but keep the messages
|
||||
var deleted = _nanoChat.TryDeleteChat((card, card.Comp), msg.RecipientNumber.Value, true);
|
||||
|
||||
if (!deleted)
|
||||
return;
|
||||
|
||||
_adminLogger.Add(LogType.Action,
|
||||
LogImpact.Low,
|
||||
$"{ToPrettyString(msg.Actor):user} deleted NanoChat conversation with #{msg.RecipientNumber:D4}");
|
||||
|
||||
UpdateUIForCard(card);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles toggling notification mute state.
|
||||
/// </summary>
|
||||
private void HandleToggleMute(Entity<NanoChatCardComponent> card)
|
||||
{
|
||||
_nanoChat.SetNotificationsMuted((card, card.Comp), !_nanoChat.GetNotificationsMuted((card, card.Comp)));
|
||||
UpdateUIForCard(card);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles sending a new message in a chat conversation.
|
||||
/// </summary>
|
||||
private void HandleSendMessage(Entity<NanoChatCartridgeComponent> cartridge,
|
||||
Entity<NanoChatCardComponent> card,
|
||||
NanoChatUiMessageEvent msg)
|
||||
{
|
||||
if (msg.RecipientNumber == null || msg.Content == null || card.Comp.Number == null)
|
||||
return;
|
||||
|
||||
if (!EnsureRecipientExists(card, msg.RecipientNumber.Value))
|
||||
return;
|
||||
|
||||
// Create and store message for sender
|
||||
var message = new NanoChatMessage(
|
||||
_timing.CurTime,
|
||||
msg.Content,
|
||||
(uint)card.Comp.Number
|
||||
);
|
||||
|
||||
// Attempt delivery
|
||||
var (deliveryFailed, recipients) = AttemptMessageDelivery(cartridge, msg.RecipientNumber.Value);
|
||||
|
||||
// Update delivery status
|
||||
message = message with { DeliveryFailed = deliveryFailed };
|
||||
|
||||
// Store message in sender's outbox under recipient's number
|
||||
_nanoChat.AddMessage((card, card.Comp), msg.RecipientNumber.Value, message);
|
||||
|
||||
// Log message attempt
|
||||
var recipientsText = recipients.Count > 0
|
||||
? string.Join(", ", recipients.Select(r => ToPrettyString(r)))
|
||||
: $"#{msg.RecipientNumber:D4}";
|
||||
|
||||
_adminLogger.Add(LogType.Chat,
|
||||
LogImpact.Low,
|
||||
$"{ToPrettyString(card):user} sent NanoChat message to {recipientsText}: {msg.Content}{(deliveryFailed ? " [DELIVERY FAILED]" : "")}");
|
||||
|
||||
var msgEv = new NanoChatMessageReceivedEvent(card);
|
||||
RaiseLocalEvent(ref msgEv);
|
||||
|
||||
if (deliveryFailed)
|
||||
return;
|
||||
|
||||
foreach (var recipient in recipients)
|
||||
{
|
||||
DeliverMessageToRecipient(card, recipient, message);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ensures a recipient exists in the sender's contacts.
|
||||
/// </summary>
|
||||
/// <param name="card">The card to check contacts for</param>
|
||||
/// <param name="recipientNumber">The recipient's number to check</param>
|
||||
/// <returns>True if the recipient exists or was created successfully</returns>
|
||||
private bool EnsureRecipientExists(Entity<NanoChatCardComponent> card, uint recipientNumber)
|
||||
{
|
||||
return _nanoChat.EnsureRecipientExists((card, card.Comp), recipientNumber, GetCardInfo(recipientNumber));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to deliver a message to recipients.
|
||||
/// </summary>
|
||||
/// <param name="sender">The sending cartridge entity</param>
|
||||
/// <param name="recipientNumber">The recipient's number</param>
|
||||
/// <returns>Tuple containing delivery status and recipients if found.</returns>
|
||||
private (bool failed, List<Entity<NanoChatCardComponent>> recipient) AttemptMessageDelivery(
|
||||
Entity<NanoChatCartridgeComponent> sender,
|
||||
uint recipientNumber)
|
||||
{
|
||||
// First verify we can send from this device
|
||||
var channel = _prototype.Index(sender.Comp.RadioChannel);
|
||||
var sendAttemptEvent = new RadioSendAttemptEvent(channel, sender);
|
||||
RaiseLocalEvent(ref sendAttemptEvent);
|
||||
if (sendAttemptEvent.Cancelled)
|
||||
return (true, new List<Entity<NanoChatCardComponent>>());
|
||||
|
||||
var foundRecipients = new List<Entity<NanoChatCardComponent>>();
|
||||
|
||||
// Find all cards with matching number
|
||||
var cardQuery = EntityQueryEnumerator<NanoChatCardComponent>();
|
||||
while (cardQuery.MoveNext(out var cardUid, out var card))
|
||||
{
|
||||
if (card.Number != recipientNumber)
|
||||
continue;
|
||||
|
||||
foundRecipients.Add((cardUid, card));
|
||||
}
|
||||
|
||||
if (foundRecipients.Count == 0)
|
||||
return (true, foundRecipients);
|
||||
|
||||
// Now check if any of these cards can receive
|
||||
var deliverableRecipients = new List<Entity<NanoChatCardComponent>>();
|
||||
foreach (var recipient in foundRecipients)
|
||||
{
|
||||
// Find any cartridges that have this card
|
||||
var cartridgeQuery = EntityQueryEnumerator<NanoChatCartridgeComponent, ActiveRadioComponent>();
|
||||
while (cartridgeQuery.MoveNext(out var receiverUid, out var receiverCart, out _))
|
||||
{
|
||||
if (receiverCart.Card != recipient.Owner)
|
||||
continue;
|
||||
|
||||
// Check if devices are on same station/map
|
||||
var recipientStation = _station.GetOwningStation(receiverUid);
|
||||
var senderStation = _station.GetOwningStation(sender);
|
||||
|
||||
// Both entities must be on a station
|
||||
if (recipientStation == null || senderStation == null)
|
||||
continue;
|
||||
|
||||
// Must be on same map/station unless long range allowed
|
||||
if (!channel.LongRange && recipientStation != senderStation)
|
||||
continue;
|
||||
|
||||
// Needs telecomms
|
||||
if (!HasActiveServer(senderStation.Value) || !HasActiveServer(recipientStation.Value))
|
||||
continue;
|
||||
|
||||
// Check if recipient can receive
|
||||
var receiveAttemptEv = new RadioReceiveAttemptEvent(channel, sender, receiverUid);
|
||||
RaiseLocalEvent(ref receiveAttemptEv);
|
||||
if (receiveAttemptEv.Cancelled)
|
||||
continue;
|
||||
|
||||
// Found valid cartridge that can receive
|
||||
deliverableRecipients.Add(recipient);
|
||||
break; // Only need one valid cartridge per card
|
||||
}
|
||||
}
|
||||
|
||||
return (deliverableRecipients.Count == 0, deliverableRecipients);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if there are any active telecomms servers on the given station
|
||||
/// </summary>
|
||||
private bool HasActiveServer(EntityUid station)
|
||||
{
|
||||
// I have no idea why this isn't public in the RadioSystem
|
||||
var query =
|
||||
EntityQueryEnumerator<TelecomServerComponent, EncryptionKeyHolderComponent, ApcPowerReceiverComponent>();
|
||||
|
||||
while (query.MoveNext(out var uid, out _, out _, out var power))
|
||||
{
|
||||
if (_station.GetOwningStation(uid) == station && power.Powered)
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Delivers a message to the recipient and handles associated notifications.
|
||||
/// </summary>
|
||||
/// <param name="sender">The sender's card entity</param>
|
||||
/// <param name="recipient">The recipient's card entity</param>
|
||||
/// <param name="message">The <see cref="NanoChatMessage" /> to deliver</param>
|
||||
private void DeliverMessageToRecipient(Entity<NanoChatCardComponent> sender,
|
||||
Entity<NanoChatCardComponent> recipient,
|
||||
NanoChatMessage message)
|
||||
{
|
||||
var senderNumber = sender.Comp.Number;
|
||||
if (senderNumber == null)
|
||||
return;
|
||||
|
||||
// Always try to get and add sender info to recipient's contacts
|
||||
if (!EnsureRecipientExists(recipient, senderNumber.Value))
|
||||
return;
|
||||
|
||||
_nanoChat.AddMessage((recipient, recipient.Comp), senderNumber.Value, message with { DeliveryFailed = false });
|
||||
|
||||
|
||||
if (_nanoChat.GetCurrentChat((recipient, recipient.Comp)) != senderNumber)
|
||||
HandleUnreadNotification(recipient, message);
|
||||
|
||||
var msgEv = new NanoChatMessageReceivedEvent(recipient);
|
||||
RaiseLocalEvent(ref msgEv);
|
||||
UpdateUIForCard(recipient);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles unread message notifications and updates unread status.
|
||||
/// </summary>
|
||||
private void HandleUnreadNotification(Entity<NanoChatCardComponent> recipient, NanoChatMessage message)
|
||||
{
|
||||
// Get sender name from contacts or fall back to number
|
||||
var recipients = _nanoChat.GetRecipients((recipient, recipient.Comp));
|
||||
var senderName = recipients.TryGetValue(message.SenderId, out var existingRecipient)
|
||||
? existingRecipient.Name
|
||||
: $"#{message.SenderId:D4}";
|
||||
|
||||
if (!recipient.Comp.Recipients[message.SenderId].HasUnread && !recipient.Comp.NotificationsMuted)
|
||||
{
|
||||
var pdaQuery = EntityQueryEnumerator<PdaComponent>();
|
||||
while (pdaQuery.MoveNext(out var pdaUid, out var pdaComp))
|
||||
{
|
||||
if (pdaComp.ContainedId != recipient)
|
||||
continue;
|
||||
|
||||
_cartridge.SendNotification(pdaUid,
|
||||
Loc.GetString("nano-chat-new-message-title", ("sender", senderName)),
|
||||
Loc.GetString("nano-chat-new-message-body", ("message", TruncateMessage(message.Content))));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Update unread status
|
||||
_nanoChat.SetRecipient((recipient, recipient.Comp),
|
||||
message.SenderId,
|
||||
existingRecipient with { HasUnread = true });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the UI for any PDAs containing the specified card.
|
||||
/// </summary>
|
||||
private void UpdateUIForCard(EntityUid cardUid)
|
||||
{
|
||||
// Find any PDA containing this card and update its UI
|
||||
var query = EntityQueryEnumerator<NanoChatCartridgeComponent, CartridgeComponent>();
|
||||
while (query.MoveNext(out var uid, out var comp, out var cartridge))
|
||||
{
|
||||
if (comp.Card != cardUid || cartridge.LoaderUid == null)
|
||||
continue;
|
||||
|
||||
UpdateUI((uid, comp), cartridge.LoaderUid.Value);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="NanoChatRecipient" /> for a given NanoChat number.
|
||||
/// </summary>
|
||||
private NanoChatRecipient? GetCardInfo(uint number)
|
||||
{
|
||||
// Find card with this number to get its info
|
||||
var query = EntityQueryEnumerator<NanoChatCardComponent>();
|
||||
while (query.MoveNext(out var uid, out var card))
|
||||
{
|
||||
if (card.Number != number)
|
||||
continue;
|
||||
|
||||
// Try to get job title from ID card if possible
|
||||
string? jobTitle = null;
|
||||
var name = "Unknown";
|
||||
if (TryComp<IdCardComponent>(uid, out var idCard))
|
||||
{
|
||||
jobTitle = idCard.LocalizedJobTitle;
|
||||
name = idCard.FullName ?? name;
|
||||
}
|
||||
|
||||
return new NanoChatRecipient(number, name, jobTitle);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Truncates a message to the notification maximum length.
|
||||
/// </summary>
|
||||
private static string TruncateMessage(string message)
|
||||
{
|
||||
return message.Length <= NotificationMaxLength
|
||||
? message
|
||||
: message[..(NotificationMaxLength - 4)] + " [...]";
|
||||
}
|
||||
|
||||
private void OnUiReady(Entity<NanoChatCartridgeComponent> ent, ref CartridgeUiReadyEvent args)
|
||||
{
|
||||
_cartridge.RegisterBackgroundProgram(args.Loader, ent);
|
||||
UpdateUI(ent, args.Loader);
|
||||
}
|
||||
|
||||
private void UpdateUI(Entity<NanoChatCartridgeComponent> ent, EntityUid loader)
|
||||
{
|
||||
if (_station.GetOwningStation(loader) is { } station)
|
||||
ent.Comp.Station = station;
|
||||
|
||||
var recipients = new Dictionary<uint, NanoChatRecipient>();
|
||||
var messages = new Dictionary<uint, List<NanoChatMessage>>();
|
||||
uint? currentChat = null;
|
||||
uint ownNumber = 0;
|
||||
var maxRecipients = 50;
|
||||
var notificationsMuted = false;
|
||||
|
||||
if (ent.Comp.Card != null && TryComp<NanoChatCardComponent>(ent.Comp.Card, out var card))
|
||||
{
|
||||
recipients = card.Recipients;
|
||||
messages = card.Messages;
|
||||
currentChat = card.CurrentChat;
|
||||
ownNumber = card.Number ?? 0;
|
||||
maxRecipients = card.MaxRecipients;
|
||||
notificationsMuted = card.NotificationsMuted;
|
||||
}
|
||||
|
||||
var state = new NanoChatUiState(recipients,
|
||||
messages,
|
||||
currentChat,
|
||||
ownNumber,
|
||||
maxRecipients,
|
||||
notificationsMuted);
|
||||
_cartridge.UpdateCartridgeUiState(loader, state);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,130 @@
|
|||
using System.Linq;
|
||||
using Content.Server.Access.Systems;
|
||||
using Content.Server.Administration.Logs;
|
||||
using Content.Server.Kitchen.Components;
|
||||
using Content.Server.NameIdentifier;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.DeltaV.CartridgeLoader.Cartridges;
|
||||
using Content.Shared.DeltaV.NanoChat;
|
||||
using Content.Shared.NameIdentifier;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Random;
|
||||
|
||||
namespace Content.Server.DeltaV.NanoChat;
|
||||
|
||||
/// <summary>
|
||||
/// Handles NanoChat features that are specific to the server but not related to the cartridge itself.
|
||||
/// </summary>
|
||||
public sealed class NanoChatSystem : SharedNanoChatSystem
|
||||
{
|
||||
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
[Dependency] private readonly NameIdentifierSystem _name = default!;
|
||||
|
||||
private readonly ProtoId<NameIdentifierGroupPrototype> _nameIdentifierGroup = "NanoChat";
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
SubscribeLocalEvent<NanoChatCardComponent, MapInitEvent>(OnCardInit);
|
||||
SubscribeLocalEvent<NanoChatCardComponent, BeingMicrowavedEvent>(OnMicrowaved, after: [typeof(IdCardSystem)]);
|
||||
}
|
||||
|
||||
private void OnMicrowaved(Entity<NanoChatCardComponent> ent, ref BeingMicrowavedEvent args)
|
||||
{
|
||||
// Skip if the entity was deleted (e.g., by ID card system burning it)
|
||||
if (Deleted(ent))
|
||||
return;
|
||||
|
||||
if (!TryComp<MicrowaveComponent>(args.Microwave, out var micro) || micro.Broken)
|
||||
return;
|
||||
|
||||
var randomPick = _random.NextFloat();
|
||||
|
||||
// Super lucky - erase all messages (10% chance)
|
||||
if (randomPick <= 0.10f)
|
||||
{
|
||||
ent.Comp.Messages.Clear();
|
||||
// TODO: these shouldn't be shown at the same time as the popups from IdCardSystem
|
||||
// _popup.PopupEntity(Loc.GetString("nanochat-card-microwave-erased", ("card", ent)),
|
||||
// ent,
|
||||
// PopupType.Medium);
|
||||
|
||||
_adminLogger.Add(LogType.Action,
|
||||
LogImpact.Medium,
|
||||
$"{ToPrettyString(args.Microwave)} erased all messages on {ToPrettyString(ent)}");
|
||||
}
|
||||
else
|
||||
{
|
||||
// Scramble random messages for random recipients
|
||||
ScrambleMessages(ent);
|
||||
// _popup.PopupEntity(Loc.GetString("nanochat-card-microwave-scrambled", ("card", ent)),
|
||||
// ent,
|
||||
// PopupType.Medium);
|
||||
|
||||
_adminLogger.Add(LogType.Action,
|
||||
LogImpact.Medium,
|
||||
$"{ToPrettyString(args.Microwave)} scrambled messages on {ToPrettyString(ent)}");
|
||||
}
|
||||
|
||||
Dirty(ent);
|
||||
}
|
||||
|
||||
private void ScrambleMessages(NanoChatCardComponent component)
|
||||
{
|
||||
foreach (var (recipientNumber, messages) in component.Messages)
|
||||
{
|
||||
for (var i = 0; i < messages.Count; i++)
|
||||
{
|
||||
// 50% chance to scramble each message
|
||||
if (!_random.Prob(0.5f))
|
||||
continue;
|
||||
|
||||
var message = messages[i];
|
||||
message.Content = ScrambleText(message.Content);
|
||||
messages[i] = message;
|
||||
}
|
||||
|
||||
// 25% chance to reassign the conversation to a random recipient
|
||||
if (_random.Prob(0.25f) && component.Recipients.Count > 0)
|
||||
{
|
||||
var newRecipient = _random.Pick(component.Recipients.Keys.ToList());
|
||||
if (newRecipient == recipientNumber)
|
||||
continue;
|
||||
|
||||
if (!component.Messages.ContainsKey(newRecipient))
|
||||
component.Messages[newRecipient] = new List<NanoChatMessage>();
|
||||
|
||||
component.Messages[newRecipient].AddRange(messages);
|
||||
component.Messages[recipientNumber].Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private string ScrambleText(string text)
|
||||
{
|
||||
var chars = text.ToCharArray();
|
||||
var n = chars.Length;
|
||||
|
||||
// Fisher-Yates shuffle of characters
|
||||
while (n > 1)
|
||||
{
|
||||
n--;
|
||||
var k = _random.Next(n + 1);
|
||||
(chars[k], chars[n]) = (chars[n], chars[k]);
|
||||
}
|
||||
|
||||
return new string(chars);
|
||||
}
|
||||
|
||||
private void OnCardInit(Entity<NanoChatCardComponent> ent, ref MapInitEvent args)
|
||||
{
|
||||
if (ent.Comp.Number != null)
|
||||
return;
|
||||
|
||||
// Assign a random number
|
||||
_name.GenerateUniqueName(ent, _nameIdentifierGroup, out var number);
|
||||
ent.Comp.Number = (uint)number;
|
||||
Dirty(ent);
|
||||
}
|
||||
}
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -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";
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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?)"/>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -0,0 +1,166 @@
|
|||
using Content.Shared.CartridgeLoader;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Shared.DeltaV.CartridgeLoader.Cartridges;
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class NanoChatUiMessageEvent : CartridgeMessageEvent
|
||||
{
|
||||
/// <summary>
|
||||
/// The type of UI message being sent.
|
||||
/// </summary>
|
||||
public readonly NanoChatUiMessageType Type;
|
||||
|
||||
/// <summary>
|
||||
/// The recipient's NanoChat number, if applicable.
|
||||
/// </summary>
|
||||
public readonly uint? RecipientNumber;
|
||||
|
||||
/// <summary>
|
||||
/// The content of the message or name for new chats.
|
||||
/// </summary>
|
||||
public readonly string? Content;
|
||||
|
||||
/// <summary>
|
||||
/// The recipient's job title when creating a new chat.
|
||||
/// </summary>
|
||||
public readonly string? RecipientJob;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new NanoChat UI message event.
|
||||
/// </summary>
|
||||
/// <param name="type">The type of message being sent</param>
|
||||
/// <param name="recipientNumber">Optional recipient number for the message</param>
|
||||
/// <param name="content">Optional content of the message</param>
|
||||
/// <param name="recipientJob">Optional job title for new chat creation</param>
|
||||
public NanoChatUiMessageEvent(NanoChatUiMessageType type,
|
||||
uint? recipientNumber = null,
|
||||
string? content = null,
|
||||
string? recipientJob = null)
|
||||
{
|
||||
Type = type;
|
||||
RecipientNumber = recipientNumber;
|
||||
Content = content;
|
||||
RecipientJob = recipientJob;
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public enum NanoChatUiMessageType : byte
|
||||
{
|
||||
NewChat,
|
||||
SelectChat,
|
||||
CloseChat,
|
||||
SendMessage,
|
||||
DeleteChat,
|
||||
ToggleMute,
|
||||
}
|
||||
|
||||
// putting this here because i can
|
||||
[Serializable, NetSerializable, DataRecord]
|
||||
public struct NanoChatRecipient
|
||||
{
|
||||
/// <summary>
|
||||
/// The recipient's unique NanoChat number.
|
||||
/// </summary>
|
||||
public uint Number;
|
||||
|
||||
/// <summary>
|
||||
/// The recipient's display name, typically from their ID card.
|
||||
/// </summary>
|
||||
public string Name;
|
||||
|
||||
/// <summary>
|
||||
/// The recipient's job title, if available.
|
||||
/// </summary>
|
||||
public string? JobTitle;
|
||||
|
||||
/// <summary>
|
||||
/// Whether this recipient has unread messages.
|
||||
/// </summary>
|
||||
public bool HasUnread;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new NanoChat recipient.
|
||||
/// </summary>
|
||||
/// <param name="number">The recipient's NanoChat number</param>
|
||||
/// <param name="name">The recipient's display name</param>
|
||||
/// <param name="jobTitle">Optional job title for the recipient</param>
|
||||
/// <param name="hasUnread">Whether there are unread messages from this recipient</param>
|
||||
public NanoChatRecipient(uint number, string name, string? jobTitle = null, bool hasUnread = false)
|
||||
{
|
||||
Number = number;
|
||||
Name = name;
|
||||
JobTitle = jobTitle;
|
||||
HasUnread = hasUnread;
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable, DataRecord]
|
||||
public struct NanoChatMessage
|
||||
{
|
||||
/// <summary>
|
||||
/// When the message was sent.
|
||||
/// </summary>
|
||||
public TimeSpan Timestamp;
|
||||
|
||||
/// <summary>
|
||||
/// The content of the message.
|
||||
/// </summary>
|
||||
public string Content;
|
||||
|
||||
/// <summary>
|
||||
/// The NanoChat number of the sender.
|
||||
/// </summary>
|
||||
public uint SenderId;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the message failed to deliver to the recipient.
|
||||
/// This can happen if the recipient is out of range or if there's no active telecomms server.
|
||||
/// </summary>
|
||||
public bool DeliveryFailed;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new NanoChat message.
|
||||
/// </summary>
|
||||
/// <param name="timestamp">When the message was sent</param>
|
||||
/// <param name="content">The content of the message</param>
|
||||
/// <param name="senderId">The sender's NanoChat number</param>
|
||||
/// <param name="deliveryFailed">Whether delivery to the recipient failed</param>
|
||||
public NanoChatMessage(TimeSpan timestamp, string content, uint senderId, bool deliveryFailed = false)
|
||||
{
|
||||
Timestamp = timestamp;
|
||||
Content = content;
|
||||
SenderId = senderId;
|
||||
DeliveryFailed = deliveryFailed;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// NanoChat log data struct
|
||||
/// </summary>
|
||||
/// <remarks>Used by the LogProbe</remarks>
|
||||
[Serializable, NetSerializable, DataRecord]
|
||||
public readonly struct NanoChatData(
|
||||
Dictionary<uint, NanoChatRecipient> recipients,
|
||||
Dictionary<uint, List<NanoChatMessage>> messages,
|
||||
uint? cardNumber,
|
||||
NetEntity card)
|
||||
{
|
||||
public Dictionary<uint, NanoChatRecipient> Recipients { get; } = recipients;
|
||||
public Dictionary<uint, List<NanoChatMessage>> Messages { get; } = messages;
|
||||
public uint? CardNumber { get; } = cardNumber;
|
||||
public NetEntity Card { get; } = card;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raised on the NanoChat card whenever a recipient gets added
|
||||
/// </summary>
|
||||
[ByRefEvent]
|
||||
public readonly record struct NanoChatRecipientUpdatedEvent(EntityUid CardUid);
|
||||
|
||||
/// <summary>
|
||||
/// Raised on the NanoChat card whenever it receives or tries sending a messsage
|
||||
/// </summary>
|
||||
[ByRefEvent]
|
||||
public readonly record struct NanoChatMessageReceivedEvent(EntityUid CardUid);
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Shared.DeltaV.CartridgeLoader.Cartridges;
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class NanoChatUiState : BoundUserInterfaceState
|
||||
{
|
||||
public readonly Dictionary<uint, NanoChatRecipient> Recipients = new();
|
||||
public readonly Dictionary<uint, List<NanoChatMessage>> Messages = new();
|
||||
public readonly uint? CurrentChat;
|
||||
public readonly uint OwnNumber;
|
||||
public readonly int MaxRecipients;
|
||||
public readonly bool NotificationsMuted;
|
||||
|
||||
public NanoChatUiState(
|
||||
Dictionary<uint, NanoChatRecipient> recipients,
|
||||
Dictionary<uint, List<NanoChatMessage>> messages,
|
||||
uint? currentChat,
|
||||
uint ownNumber,
|
||||
int maxRecipients,
|
||||
bool notificationsMuted)
|
||||
{
|
||||
Recipients = recipients;
|
||||
Messages = messages;
|
||||
CurrentChat = currentChat;
|
||||
OwnNumber = ownNumber;
|
||||
MaxRecipients = maxRecipients;
|
||||
NotificationsMuted = notificationsMuted;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
using Content.Shared.DeltaV.CartridgeLoader.Cartridges;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
|
||||
|
||||
namespace Content.Shared.DeltaV.NanoChat;
|
||||
|
||||
[RegisterComponent, NetworkedComponent, Access(typeof(SharedNanoChatSystem))]
|
||||
[AutoGenerateComponentPause, AutoGenerateComponentState]
|
||||
public sealed partial class NanoChatCardComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// The number assigned to this card.
|
||||
/// </summary>
|
||||
[DataField, AutoNetworkedField]
|
||||
public uint? Number;
|
||||
|
||||
/// <summary>
|
||||
/// All chat recipients stored on this card.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public Dictionary<uint, NanoChatRecipient> Recipients = new();
|
||||
|
||||
/// <summary>
|
||||
/// All messages stored on this card, keyed by recipient number.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public Dictionary<uint, List<NanoChatMessage>> Messages = new();
|
||||
|
||||
/// <summary>
|
||||
/// The currently selected chat recipient number.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public uint? CurrentChat;
|
||||
|
||||
/// <summary>
|
||||
/// The maximum amount of recipients this card supports.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public int MaxRecipients = 50;
|
||||
|
||||
/// <summary>
|
||||
/// Last time a message was sent, for rate limiting.
|
||||
/// </summary>
|
||||
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), AutoPausedField]
|
||||
public TimeSpan LastMessageTime; // TODO: actually use this, compare against actor and not the card
|
||||
|
||||
/// <summary>
|
||||
/// Whether to send notifications.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public bool NotificationsMuted;
|
||||
}
|
||||
|
|
@ -0,0 +1,273 @@
|
|||
using Content.Shared.DeltaV.CartridgeLoader.Cartridges;
|
||||
using Content.Shared.Examine;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Shared.DeltaV.NanoChat;
|
||||
|
||||
/// <summary>
|
||||
/// Base system for NanoChat functionality shared between client and server.
|
||||
/// </summary>
|
||||
public abstract class SharedNanoChatSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
SubscribeLocalEvent<NanoChatCardComponent, ExaminedEvent>(OnExamined);
|
||||
}
|
||||
|
||||
private void OnExamined(Entity<NanoChatCardComponent> ent, ref ExaminedEvent args)
|
||||
{
|
||||
if (!args.IsInDetailsRange)
|
||||
return;
|
||||
|
||||
if (ent.Comp.Number == null)
|
||||
{
|
||||
args.PushMarkup(Loc.GetString("nanochat-card-examine-no-number"));
|
||||
return;
|
||||
}
|
||||
|
||||
args.PushMarkup(Loc.GetString("nanochat-card-examine-number", ("number", $"{ent.Comp.Number:D4}")));
|
||||
}
|
||||
|
||||
#region Public API Methods
|
||||
|
||||
/// <summary>
|
||||
/// Gets the NanoChat number for a card.
|
||||
/// </summary>
|
||||
public uint? GetNumber(Entity<NanoChatCardComponent?> card)
|
||||
{
|
||||
if (!Resolve(card, ref card.Comp))
|
||||
return null;
|
||||
|
||||
return card.Comp.Number;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the NanoChat number for a card.
|
||||
/// </summary>
|
||||
public void SetNumber(Entity<NanoChatCardComponent?> card, uint number)
|
||||
{
|
||||
if (!Resolve(card, ref card.Comp))
|
||||
return;
|
||||
|
||||
card.Comp.Number = number;
|
||||
Dirty(card);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the recipients dictionary from a card.
|
||||
/// </summary>
|
||||
public IReadOnlyDictionary<uint, NanoChatRecipient> GetRecipients(Entity<NanoChatCardComponent?> card)
|
||||
{
|
||||
if (!Resolve(card, ref card.Comp))
|
||||
return new Dictionary<uint, NanoChatRecipient>();
|
||||
|
||||
return card.Comp.Recipients;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the messages dictionary from a card.
|
||||
/// </summary>
|
||||
public IReadOnlyDictionary<uint, List<NanoChatMessage>> GetMessages(Entity<NanoChatCardComponent?> card)
|
||||
{
|
||||
if (!Resolve(card, ref card.Comp))
|
||||
return new Dictionary<uint, List<NanoChatMessage>>();
|
||||
|
||||
return card.Comp.Messages;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets a specific recipient in the card.
|
||||
/// </summary>
|
||||
public void SetRecipient(Entity<NanoChatCardComponent?> card, uint number, NanoChatRecipient recipient)
|
||||
{
|
||||
if (!Resolve(card, ref card.Comp))
|
||||
return;
|
||||
|
||||
card.Comp.Recipients[number] = recipient;
|
||||
Dirty(card);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a specific recipient from the card.
|
||||
/// </summary>
|
||||
public NanoChatRecipient? GetRecipient(Entity<NanoChatCardComponent?> card, uint number)
|
||||
{
|
||||
if (!Resolve(card, ref card.Comp) || !card.Comp.Recipients.TryGetValue(number, out var recipient))
|
||||
return null;
|
||||
|
||||
return recipient;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all messages for a specific recipient.
|
||||
/// </summary>
|
||||
public List<NanoChatMessage>? GetMessagesForRecipient(Entity<NanoChatCardComponent?> card, uint recipientNumber)
|
||||
{
|
||||
if (!Resolve(card, ref card.Comp) || !card.Comp.Messages.TryGetValue(recipientNumber, out var messages))
|
||||
return null;
|
||||
|
||||
return new List<NanoChatMessage>(messages);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a message to a recipient's conversation.
|
||||
/// </summary>
|
||||
public void AddMessage(Entity<NanoChatCardComponent?> card, uint recipientNumber, NanoChatMessage message)
|
||||
{
|
||||
if (!Resolve(card, ref card.Comp))
|
||||
return;
|
||||
|
||||
if (!card.Comp.Messages.TryGetValue(recipientNumber, out var messages))
|
||||
{
|
||||
messages = new List<NanoChatMessage>();
|
||||
card.Comp.Messages[recipientNumber] = messages;
|
||||
}
|
||||
|
||||
messages.Add(message);
|
||||
card.Comp.LastMessageTime = _timing.CurTime;
|
||||
Dirty(card);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the currently selected chat recipient.
|
||||
/// </summary>
|
||||
public uint? GetCurrentChat(Entity<NanoChatCardComponent?> card)
|
||||
{
|
||||
if (!Resolve(card, ref card.Comp))
|
||||
return null;
|
||||
|
||||
return card.Comp.CurrentChat;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the currently selected chat recipient.
|
||||
/// </summary>
|
||||
public void SetCurrentChat(Entity<NanoChatCardComponent?> card, uint? recipient)
|
||||
{
|
||||
if (!Resolve(card, ref card.Comp))
|
||||
return;
|
||||
|
||||
card.Comp.CurrentChat = recipient;
|
||||
Dirty(card);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether notifications are muted.
|
||||
/// </summary>
|
||||
public bool GetNotificationsMuted(Entity<NanoChatCardComponent?> card)
|
||||
{
|
||||
if (!Resolve(card, ref card.Comp))
|
||||
return false;
|
||||
|
||||
return card.Comp.NotificationsMuted;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets whether notifications are muted.
|
||||
/// </summary>
|
||||
public void SetNotificationsMuted(Entity<NanoChatCardComponent?> card, bool muted)
|
||||
{
|
||||
if (!Resolve(card, ref card.Comp))
|
||||
return;
|
||||
|
||||
card.Comp.NotificationsMuted = muted;
|
||||
Dirty(card);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the time of the last message.
|
||||
/// </summary>
|
||||
public TimeSpan? GetLastMessageTime(Entity<NanoChatCardComponent?> card)
|
||||
{
|
||||
if (!Resolve(card, ref card.Comp))
|
||||
return null;
|
||||
|
||||
return card.Comp.LastMessageTime;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets if there are unread messages from a recipient.
|
||||
/// </summary>
|
||||
public bool HasUnreadMessages(Entity<NanoChatCardComponent?> card, uint recipientNumber)
|
||||
{
|
||||
if (!Resolve(card, ref card.Comp) || !card.Comp.Recipients.TryGetValue(recipientNumber, out var recipient))
|
||||
return false;
|
||||
|
||||
return recipient.HasUnread;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears all messages and recipients from the card.
|
||||
/// </summary>
|
||||
public void Clear(Entity<NanoChatCardComponent?> card)
|
||||
{
|
||||
if (!Resolve(card, ref card.Comp))
|
||||
return;
|
||||
|
||||
card.Comp.Messages.Clear();
|
||||
card.Comp.Recipients.Clear();
|
||||
card.Comp.CurrentChat = null;
|
||||
Dirty(card);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes a chat conversation with a recipient from the card.
|
||||
/// Optionally keeps message history while removing from active chats.
|
||||
/// </summary>
|
||||
/// <returns>True if the chat was deleted successfully</returns>
|
||||
public bool TryDeleteChat(Entity<NanoChatCardComponent?> card, uint recipientNumber, bool keepMessages = false)
|
||||
{
|
||||
if (!Resolve(card, ref card.Comp))
|
||||
return false;
|
||||
|
||||
// Remove from recipients list
|
||||
var removed = card.Comp.Recipients.Remove(recipientNumber);
|
||||
|
||||
// Clear messages if requested
|
||||
if (!keepMessages)
|
||||
card.Comp.Messages.Remove(recipientNumber);
|
||||
|
||||
// Clear current chat if we just deleted it
|
||||
if (card.Comp.CurrentChat == recipientNumber)
|
||||
card.Comp.CurrentChat = null;
|
||||
|
||||
if (removed)
|
||||
Dirty(card);
|
||||
|
||||
return removed;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ensures a recipient exists in the card's contacts and message lists.
|
||||
/// If the recipient doesn't exist, they will be added with the provided info.
|
||||
/// </summary>
|
||||
/// <returns>True if the recipient was added or already existed</returns>
|
||||
public bool EnsureRecipientExists(Entity<NanoChatCardComponent?> card,
|
||||
uint recipientNumber,
|
||||
NanoChatRecipient? recipientInfo = null)
|
||||
{
|
||||
if (!Resolve(card, ref card.Comp))
|
||||
return false;
|
||||
|
||||
if (!card.Comp.Recipients.ContainsKey(recipientNumber))
|
||||
{
|
||||
// Only add if we have recipient info
|
||||
if (recipientInfo == null)
|
||||
return false;
|
||||
|
||||
card.Comp.Recipients[recipientNumber] = recipientInfo.Value;
|
||||
}
|
||||
|
||||
// Ensure message list exists for this recipient
|
||||
if (!card.Comp.Messages.ContainsKey(recipientNumber))
|
||||
card.Comp.Messages[recipientNumber] = new List<NanoChatMessage>();
|
||||
|
||||
Dirty(card);
|
||||
return true;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
|
@ -0,0 +1,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();
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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<T> not being usable for Entity<T?> 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
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
namespace Content.Shared.DeltaV.Shuttles.Systems;
|
||||
|
||||
public abstract class SharedDockingConsoleSystem : EntitySystem;
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
namespace Content.Shared.DeltaV.Shuttles.Systems;
|
||||
|
||||
public abstract class SharedDockingShuttleSystem : EntitySystem;
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -0,0 +1 @@
|
|||
agent-id-card-current-number = NanoChat Number
|
||||
|
|
@ -158,3 +158,45 @@ stock-trading-buy-button = Buy
|
|||
stock-trading-sell-button = Sell
|
||||
stock-trading-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}
|
||||
|
|
|
|||
|
|
@ -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?!
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
lathe-menu-mining-points = Mining Points: {$points}
|
||||
lathe-menu-mining-points-claim-button = Claim Points
|
||||
|
|
@ -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!
|
||||
|
|
@ -0,0 +1 @@
|
|||
objective-ninja-kill-head-title = Execute {$targetName}, {CAPITALIZE($job)}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
Loading…
Reference in New Issue