mirror of
https://github.com/space-syndicate/space-station-14.git
synced 2026-06-09 15:06:34 +02:00
Merge remote-tracking branch 'upstream/master' into upstream-sync
# Conflicts: # Content.Shared/Preferences/HumanoidCharacterProfile.cs # README.md # Resources/Prototypes/Entities/Structures/Machines/lathe.yml # Resources/ServerInfo/Guidebook/Cargo/Cargo.xml # Resources/ServerInfo/Guidebook/Medical/Cloning.xml # Resources/ServerInfo/Guidebook/Survival.xml # Resources/Textures/Clothing/Head/Hats/beret_qm.rsi/meta.json # Resources/Textures/Objects/Tools/t-ray.rsi/meta.json # Resources/Textures/Objects/Tools/t-ray.rsi/tray-off.png # Resources/Textures/Objects/Tools/t-ray.rsi/tray-on.png # Resources/Textures/Structures/Doors/Airlocks/Glass/cargo.rsi/open.png
This commit is contained in:
@@ -4,8 +4,8 @@ using BenchmarkDotNet.Attributes;
|
||||
using Content.IntegrationTests;
|
||||
using Content.IntegrationTests.Pair;
|
||||
using Content.IntegrationTests.Tests.DeviceNetwork;
|
||||
using Content.Server.DeviceNetwork;
|
||||
using Content.Server.DeviceNetwork.Systems;
|
||||
using Content.Shared.DeviceNetwork;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.Analyzers;
|
||||
using Robust.Shared.GameObjects;
|
||||
|
||||
@@ -25,8 +25,10 @@ public sealed class BackgroundAudioSystem : EntitySystem
|
||||
[Dependency] private readonly IStateManager _stateManager = default!;
|
||||
|
||||
private readonly AudioParams _lobbyParams = new(-5f, 1, "Master", 0, 0, 0, true, 0f);
|
||||
private readonly AudioParams _roundEndParams = new(-5f, 1, "Master", 0, 0, 0, false, 0f);
|
||||
|
||||
public EntityUid? LobbyStream;
|
||||
public EntityUid? LobbyMusicStream;
|
||||
public EntityUid? LobbyRoundRestartAudioStream;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
@@ -115,7 +117,7 @@ public sealed class BackgroundAudioSystem : EntitySystem
|
||||
|
||||
public void StartLobbyMusic()
|
||||
{
|
||||
if (LobbyStream != null || !_configManager.GetCVar(CCVars.LobbyMusicEnabled))
|
||||
if (LobbyMusicStream != null || !_configManager.GetCVar(CCVars.LobbyMusicEnabled))
|
||||
return;
|
||||
|
||||
var file = _gameTicker.LobbySong;
|
||||
@@ -124,18 +126,21 @@ public sealed class BackgroundAudioSystem : EntitySystem
|
||||
return;
|
||||
}
|
||||
|
||||
LobbyStream = _audio.PlayGlobal(file, Filter.Local(), false,
|
||||
LobbyMusicStream = _audio.PlayGlobal(
|
||||
file,
|
||||
Filter.Local(),
|
||||
false,
|
||||
_lobbyParams.WithVolume(_lobbyParams.Volume + SharedAudioSystem.GainToVolume(_configManager.GetCVar(CCVars.LobbyMusicVolume))))?.Entity;
|
||||
}
|
||||
|
||||
private void EndLobbyMusic()
|
||||
{
|
||||
LobbyStream = _audio.Stop(LobbyStream);
|
||||
LobbyMusicStream = _audio.Stop(LobbyMusicStream);
|
||||
}
|
||||
|
||||
private void PlayRestartSound(RoundRestartCleanupEvent ev)
|
||||
{
|
||||
if (!_configManager.GetCVar(CCVars.LobbyMusicEnabled))
|
||||
if (!_configManager.GetCVar(CCVars.RestartSoundsEnabled))
|
||||
return;
|
||||
|
||||
var file = _gameTicker.RestartSound;
|
||||
@@ -144,10 +149,11 @@ public sealed class BackgroundAudioSystem : EntitySystem
|
||||
return;
|
||||
}
|
||||
|
||||
var volume = _lobbyParams.WithVolume(_lobbyParams.Volume +
|
||||
SharedAudioSystem.GainToVolume(
|
||||
_configManager.GetCVar(CCVars.LobbyMusicVolume)));
|
||||
|
||||
_audio.PlayGlobal(file, Filter.Local(), false, volume);
|
||||
LobbyRoundRestartAudioStream = _audio.PlayGlobal(
|
||||
file,
|
||||
Filter.Local(),
|
||||
false,
|
||||
_roundEndParams.WithVolume(_roundEndParams.Volume + SharedAudioSystem.GainToVolume(_configManager.GetCVar(CCVars.LobbyMusicVolume)))
|
||||
)?.Entity;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,15 +51,24 @@ public sealed partial class ContentAudioSystem : SharedContentAudioSystem
|
||||
_fadingOut.Clear();
|
||||
|
||||
// Preserve lobby music but everything else should get dumped.
|
||||
var lobbyStream = EntityManager.System<BackgroundAudioSystem>().LobbyStream;
|
||||
TryComp(lobbyStream, out AudioComponent? audioComp);
|
||||
var oldGain = audioComp?.Gain;
|
||||
var lobbyMusic = EntityManager.System<BackgroundAudioSystem>().LobbyMusicStream;
|
||||
TryComp(lobbyMusic, out AudioComponent? lobbyMusicComp);
|
||||
var oldMusicGain = lobbyMusicComp?.Gain;
|
||||
|
||||
var restartAudio = EntityManager.System<BackgroundAudioSystem>().LobbyRoundRestartAudioStream;
|
||||
TryComp(restartAudio, out AudioComponent? restartComp);
|
||||
var oldAudioGain = restartComp?.Gain;
|
||||
|
||||
SilenceAudio();
|
||||
|
||||
if (oldGain != null)
|
||||
if (oldMusicGain != null)
|
||||
{
|
||||
Audio.SetGain(lobbyStream, oldGain.Value, audioComp);
|
||||
Audio.SetGain(lobbyMusic, oldMusicGain.Value, lobbyMusicComp);
|
||||
}
|
||||
|
||||
if (oldAudioGain != null)
|
||||
{
|
||||
Audio.SetGain(restartAudio, oldAudioGain.Value, restartComp);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,16 +4,16 @@
|
||||
Orientation="Vertical">
|
||||
<BoxContainer Orientation="Horizontal">
|
||||
<Label Name="NumberLabel"
|
||||
Align="Center"
|
||||
SetWidth="60"
|
||||
Align="Right"
|
||||
SetWidth="26"
|
||||
ClipText="True"/>
|
||||
<Label Name="TimeLabel"
|
||||
Align="Center"
|
||||
SetWidth="280"
|
||||
SetWidth="100"
|
||||
ClipText="True"/>
|
||||
<Label Name="AccessorLabel"
|
||||
Align="Center"
|
||||
SetWidth="110"
|
||||
Align="Left"
|
||||
SetWidth="390"
|
||||
ClipText="True"/>
|
||||
</BoxContainer>
|
||||
<customControls:HSeparator Margin="0 5 0 5"/>
|
||||
|
||||
@@ -9,10 +9,10 @@
|
||||
BorderColor="#5a5a5a"
|
||||
BorderThickness="0 0 0 1"/>
|
||||
</PanelContainer.PanelOverride>
|
||||
<BoxContainer Orientation="Horizontal" Align="Center" Margin="8">
|
||||
<Label HorizontalExpand="True" Text="{Loc 'log-probe-label-number'}"/>
|
||||
<Label HorizontalExpand="True" Text="{Loc 'log-probe-label-time'}"/>
|
||||
<Label HorizontalExpand="True" Text="{Loc 'log-probe-label-accessor'}"/>
|
||||
<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>
|
||||
</PanelContainer>
|
||||
<ScrollContainer VerticalExpand="True" HScrollEnabled="True">
|
||||
|
||||
@@ -182,20 +182,9 @@ namespace Content.Client.Chat.UI
|
||||
return msg;
|
||||
}
|
||||
|
||||
protected string ExtractSpeechSubstring(ChatMessage message, string tag)
|
||||
{
|
||||
var rawmsg = message.WrappedMessage;
|
||||
var tagStart = rawmsg.IndexOf($"[{tag}]");
|
||||
var tagEnd = rawmsg.IndexOf($"[/{tag}]");
|
||||
if (tagStart < 0 || tagEnd < 0) //the above return -1 if the tag's not found, which in turn will cause the below to throw an exception. a blank speech bubble is far more noticeably broken than the bubble not appearing at all -bhijn
|
||||
return "";
|
||||
tagStart += tag.Length + 2;
|
||||
return rawmsg.Substring(tagStart, tagEnd - tagStart);
|
||||
}
|
||||
|
||||
protected FormattedMessage ExtractAndFormatSpeechSubstring(ChatMessage message, string tag, Color? fontColor = null)
|
||||
{
|
||||
return FormatSpeech(ExtractSpeechSubstring(message, tag), fontColor);
|
||||
return FormatSpeech(SharedChatSystem.GetStringInsideTag(message, tag), fontColor);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
<controls:FancyWindow xmlns="https://spacestation14.io"
|
||||
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
|
||||
Title="{Loc 'criminal-records-console-crime-history'}"
|
||||
MinSize="660 400">
|
||||
<BoxContainer Orientation="Vertical" HorizontalExpand="True" Margin="5">
|
||||
<BoxContainer Name="Editing" Orientation="Horizontal" HorizontalExpand="True" Align="Center" Margin="5">
|
||||
<Button Name="AddButton" Text="{Loc 'criminal-records-add-history'}"/>
|
||||
<Button Name="DeleteButton" Text="{Loc 'criminal-records-delete-history'}" Disabled="True"/>
|
||||
</BoxContainer>
|
||||
<Label Name="NoHistory" Text="{Loc 'criminal-records-no-history'}" HorizontalExpand="True" HorizontalAlignment="Center"/>
|
||||
<ScrollContainer VerticalExpand="True">
|
||||
<ItemList Name="History"/> <!-- Populated when window opened -->
|
||||
</ScrollContainer>
|
||||
</BoxContainer>
|
||||
</controls:FancyWindow>
|
||||
@@ -0,0 +1,107 @@
|
||||
using Content.Shared.Administration;
|
||||
using Content.Shared.CriminalRecords;
|
||||
using Content.Client.UserInterface.Controls;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
|
||||
namespace Content.Client.CriminalRecords;
|
||||
|
||||
/// <summary>
|
||||
/// Window opened when Crime History button is pressed
|
||||
/// </summary>
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class CrimeHistoryWindow : FancyWindow
|
||||
{
|
||||
public Action<string>? OnAddHistory;
|
||||
public Action<uint>? OnDeleteHistory;
|
||||
|
||||
private uint _maxLength;
|
||||
private uint? _index;
|
||||
private DialogWindow? _dialog;
|
||||
|
||||
public CrimeHistoryWindow(uint maxLength)
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
|
||||
_maxLength = maxLength;
|
||||
|
||||
OnClose += () =>
|
||||
{
|
||||
_dialog?.Close();
|
||||
// deselect so when reopening the window it doesnt try to use invalid index
|
||||
_index = null;
|
||||
};
|
||||
|
||||
AddButton.OnPressed += _ =>
|
||||
{
|
||||
if (_dialog != null)
|
||||
{
|
||||
_dialog.MoveToFront();
|
||||
return;
|
||||
}
|
||||
|
||||
var field = "line";
|
||||
var prompt = Loc.GetString("criminal-records-console-reason");
|
||||
var placeholder = Loc.GetString("criminal-records-history-placeholder");
|
||||
var entry = new QuickDialogEntry(field, QuickDialogEntryType.LongText, prompt, placeholder);
|
||||
var entries = new List<QuickDialogEntry> { entry };
|
||||
_dialog = new DialogWindow(Title!, entries);
|
||||
|
||||
_dialog.OnConfirmed += responses =>
|
||||
{
|
||||
var line = responses[field];
|
||||
if (line.Length < 1 || line.Length > _maxLength)
|
||||
return;
|
||||
|
||||
OnAddHistory?.Invoke(line);
|
||||
// adding deselects so prevent deleting yeah
|
||||
_index = null;
|
||||
DeleteButton.Disabled = true;
|
||||
};
|
||||
|
||||
// prevent MoveToFront being called on a closed window and double closing
|
||||
_dialog.OnClose += () => { _dialog = null; };
|
||||
};
|
||||
DeleteButton.OnPressed += _ =>
|
||||
{
|
||||
if (_index is not {} index)
|
||||
return;
|
||||
|
||||
OnDeleteHistory?.Invoke(index);
|
||||
// prevent total spam wiping
|
||||
History.ClearSelected();
|
||||
_index = null;
|
||||
DeleteButton.Disabled = true;
|
||||
};
|
||||
|
||||
History.OnItemSelected += args =>
|
||||
{
|
||||
_index = (uint) args.ItemIndex;
|
||||
DeleteButton.Disabled = false;
|
||||
};
|
||||
History.OnItemDeselected += args =>
|
||||
{
|
||||
_index = null;
|
||||
DeleteButton.Disabled = true;
|
||||
};
|
||||
}
|
||||
|
||||
public void UpdateHistory(CriminalRecord record, bool access)
|
||||
{
|
||||
History.Clear();
|
||||
Editing.Visible = access;
|
||||
|
||||
NoHistory.Visible = record.History.Count == 0;
|
||||
|
||||
foreach (var entry in record.History)
|
||||
{
|
||||
var time = entry.AddTime;
|
||||
var line = $"{time.Hours:00}:{time.Minutes:00}:{time.Seconds:00} - {entry.Crime}";
|
||||
History.AddItem(line);
|
||||
}
|
||||
|
||||
// deselect if something goes wrong
|
||||
if (_index is {} index && record.History.Count >= index)
|
||||
_index = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
using Content.Shared.Access.Systems;
|
||||
using Content.Shared.CriminalRecords;
|
||||
using Content.Shared.CriminalRecords.Components;
|
||||
using Content.Shared.Security;
|
||||
using Content.Shared.StationRecords;
|
||||
using Robust.Client.Player;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Random;
|
||||
|
||||
namespace Content.Client.CriminalRecords;
|
||||
|
||||
public sealed class CriminalRecordsConsoleBoundUserInterface : BoundUserInterface
|
||||
{
|
||||
[Dependency] private readonly IPrototypeManager _proto = default!;
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||
private readonly AccessReaderSystem _accessReader;
|
||||
|
||||
private CriminalRecordsConsoleWindow? _window;
|
||||
private CrimeHistoryWindow? _historyWindow;
|
||||
|
||||
public CriminalRecordsConsoleBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
|
||||
{
|
||||
_accessReader = EntMan.System<AccessReaderSystem>();
|
||||
}
|
||||
|
||||
protected override void Open()
|
||||
{
|
||||
base.Open();
|
||||
|
||||
var comp = EntMan.GetComponent<CriminalRecordsConsoleComponent>(Owner);
|
||||
|
||||
_window = new(Owner, comp.MaxStringLength, _playerManager, _proto, _random, _accessReader);
|
||||
_window.OnKeySelected += key =>
|
||||
SendMessage(new SelectStationRecord(key));
|
||||
_window.OnFiltersChanged += (type, filterValue) =>
|
||||
SendMessage(new SetStationRecordFilter(type, filterValue));
|
||||
_window.OnStatusSelected += status =>
|
||||
SendMessage(new CriminalRecordChangeStatus(status, null));
|
||||
_window.OnDialogConfirmed += (_, reason) =>
|
||||
SendMessage(new CriminalRecordChangeStatus(SecurityStatus.Wanted, reason));
|
||||
_window.OnHistoryUpdated += UpdateHistory;
|
||||
_window.OnHistoryClosed += () => _historyWindow?.Close();
|
||||
_window.OnClose += Close;
|
||||
|
||||
_historyWindow = new(comp.MaxStringLength);
|
||||
_historyWindow.OnAddHistory += line => SendMessage(new CriminalRecordAddHistory(line));
|
||||
_historyWindow.OnDeleteHistory += index => SendMessage(new CriminalRecordDeleteHistory(index));
|
||||
|
||||
_historyWindow.Close(); // leave closed until user opens it
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates or opens a new history window.
|
||||
/// </summary>
|
||||
private void UpdateHistory(CriminalRecord record, bool access, bool open)
|
||||
{
|
||||
_historyWindow!.UpdateHistory(record, access);
|
||||
|
||||
if (open)
|
||||
_historyWindow.OpenCentered();
|
||||
}
|
||||
|
||||
protected override void UpdateState(BoundUserInterfaceState state)
|
||||
{
|
||||
base.UpdateState(state);
|
||||
|
||||
if (state is not CriminalRecordsConsoleState cast)
|
||||
return;
|
||||
|
||||
_window?.UpdateState(cast);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
|
||||
_window?.Close();
|
||||
_historyWindow?.Close();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<controls:FancyWindow xmlns="https://spacestation14.io"
|
||||
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
|
||||
Title="{Loc 'criminal-records-console-window-title'}"
|
||||
MinSize="660 400">
|
||||
<BoxContainer Orientation="Vertical">
|
||||
<!-- Record search bar
|
||||
TODO: make this into a control shared with general records -->
|
||||
<BoxContainer Margin="5 5 5 10" HorizontalExpand="true" VerticalAlignment="Center">
|
||||
<OptionButton Name="FilterType" MinWidth="200" Margin="0 0 10 0"/> <!-- Populated in constructor -->
|
||||
<LineEdit Name="FilterText" PlaceHolder="{Loc 'criminal-records-filter-placeholder'}" HorizontalExpand="True"/>
|
||||
</BoxContainer>
|
||||
<BoxContainer Orientation="Horizontal" VerticalExpand="True">
|
||||
<!-- Record listing -->
|
||||
<BoxContainer Orientation="Vertical" Margin="5" MinWidth="250" MaxWidth="250">
|
||||
<Label Name="RecordListingTitle" Text="{Loc 'criminal-records-console-records-list-title'}" HorizontalExpand="True" Align="Center"/>
|
||||
<Label Name="NoRecords" Text="{Loc 'criminal-records-console-no-records'}" HorizontalExpand="True" Align="Center" FontColorOverride="DarkGray"/>
|
||||
<ScrollContainer VerticalExpand="True">
|
||||
<ItemList Name="RecordListing"/> <!-- Populated when loading state -->
|
||||
</ScrollContainer>
|
||||
</BoxContainer>
|
||||
<Label Name="RecordUnselected" Text="{Loc 'criminal-records-console-select-record-info'}" HorizontalExpand="True" Align="Center" FontColorOverride="DarkGray"/>
|
||||
<!-- Selected record info -->
|
||||
<BoxContainer Name="PersonContainer" Orientation="Vertical" Margin="5" Visible="False">
|
||||
<Label Name="PersonName" StyleClasses="LabelBig"/>
|
||||
<Label Name="PersonPrints"/>
|
||||
<Label Name="PersonDna"/>
|
||||
<PanelContainer StyleClasses="LowDivider" Margin="0 5 0 5" />
|
||||
<BoxContainer Orientation="Horizontal" Margin="5 5 5 5">
|
||||
<Label Name="StatusLabel" Text="{Loc 'criminal-records-console-status'}" FontColorOverride="DarkGray"/>
|
||||
<OptionButton Name="StatusOptionButton"/> <!-- Populated in constructor -->
|
||||
</BoxContainer>
|
||||
<RichTextLabel Name="WantedReason" Visible="False"/>
|
||||
<Button Name="HistoryButton" Text="{Loc 'criminal-records-console-crime-history'}"/>
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
</controls:FancyWindow>
|
||||
@@ -0,0 +1,264 @@
|
||||
using Content.Client.UserInterface.Controls;
|
||||
using Content.Shared.Access.Systems;
|
||||
using Content.Shared.Administration;
|
||||
using Content.Shared.CriminalRecords;
|
||||
using Content.Shared.Dataset;
|
||||
using Content.Shared.Security;
|
||||
using Content.Shared.StationRecords;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.Player;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Random;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client.CriminalRecords;
|
||||
|
||||
// TODO: dedupe shitcode from general records theres a lot
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class CriminalRecordsConsoleWindow : FancyWindow
|
||||
{
|
||||
private readonly IPlayerManager _player;
|
||||
private readonly IPrototypeManager _proto;
|
||||
private readonly IRobustRandom _random;
|
||||
private readonly AccessReaderSystem _accessReader;
|
||||
|
||||
public readonly EntityUid Console;
|
||||
|
||||
[ValidatePrototypeId<DatasetPrototype>]
|
||||
private const string ReasonPlaceholders = "CriminalRecordsWantedReasonPlaceholders";
|
||||
|
||||
public Action<uint?>? OnKeySelected;
|
||||
public Action<StationRecordFilterType, string>? OnFiltersChanged;
|
||||
public Action<SecurityStatus>? OnStatusSelected;
|
||||
public Action<CriminalRecord, bool, bool>? OnHistoryUpdated;
|
||||
public Action? OnHistoryClosed;
|
||||
public Action<SecurityStatus, string>? OnDialogConfirmed;
|
||||
|
||||
private uint _maxLength;
|
||||
private bool _isPopulating;
|
||||
private bool _access;
|
||||
private uint? _selectedKey;
|
||||
private CriminalRecord? _selectedRecord;
|
||||
|
||||
private DialogWindow? _reasonDialog;
|
||||
|
||||
private StationRecordFilterType _currentFilterType;
|
||||
|
||||
public CriminalRecordsConsoleWindow(EntityUid console, uint maxLength, IPlayerManager playerManager, IPrototypeManager prototypeManager, IRobustRandom robustRandom, AccessReaderSystem accessReader)
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
|
||||
Console = console;
|
||||
_player = playerManager;
|
||||
_proto = prototypeManager;
|
||||
_random = robustRandom;
|
||||
_accessReader = accessReader;
|
||||
|
||||
_maxLength = maxLength;
|
||||
_currentFilterType = StationRecordFilterType.Name;
|
||||
|
||||
OpenCentered();
|
||||
|
||||
foreach (var item in Enum.GetValues<StationRecordFilterType>())
|
||||
{
|
||||
FilterType.AddItem(GetTypeFilterLocals(item), (int)item);
|
||||
}
|
||||
|
||||
foreach (var status in Enum.GetValues<SecurityStatus>())
|
||||
{
|
||||
AddStatusSelect(status);
|
||||
}
|
||||
|
||||
OnClose += () => _reasonDialog?.Close();
|
||||
|
||||
RecordListing.OnItemSelected += args =>
|
||||
{
|
||||
if (_isPopulating || RecordListing[args.ItemIndex].Metadata is not uint cast)
|
||||
return;
|
||||
|
||||
OnKeySelected?.Invoke(cast);
|
||||
};
|
||||
|
||||
RecordListing.OnItemDeselected += _ =>
|
||||
{
|
||||
if (!_isPopulating)
|
||||
OnKeySelected?.Invoke(null);
|
||||
};
|
||||
|
||||
FilterType.OnItemSelected += eventArgs =>
|
||||
{
|
||||
var type = (StationRecordFilterType)eventArgs.Id;
|
||||
|
||||
if (_currentFilterType != type)
|
||||
{
|
||||
_currentFilterType = type;
|
||||
FilterListingOfRecords(FilterText.Text);
|
||||
}
|
||||
};
|
||||
|
||||
FilterText.OnTextEntered += args =>
|
||||
{
|
||||
FilterListingOfRecords(args.Text);
|
||||
};
|
||||
|
||||
StatusOptionButton.OnItemSelected += args =>
|
||||
{
|
||||
SetStatus((SecurityStatus) args.Id);
|
||||
};
|
||||
|
||||
HistoryButton.OnPressed += _ =>
|
||||
{
|
||||
if (_selectedRecord is {} record)
|
||||
OnHistoryUpdated?.Invoke(record, _access, true);
|
||||
};
|
||||
}
|
||||
|
||||
public void UpdateState(CriminalRecordsConsoleState state)
|
||||
{
|
||||
if (state.Filter != null)
|
||||
{
|
||||
if (state.Filter.Type != _currentFilterType)
|
||||
{
|
||||
_currentFilterType = state.Filter.Type;
|
||||
}
|
||||
|
||||
if (state.Filter.Value != FilterText.Text)
|
||||
{
|
||||
FilterText.Text = state.Filter.Value;
|
||||
}
|
||||
}
|
||||
|
||||
_selectedKey = state.SelectedKey;
|
||||
|
||||
FilterType.SelectId((int)_currentFilterType);
|
||||
|
||||
// set up the records listing panel
|
||||
RecordListing.Clear();
|
||||
|
||||
var hasRecords = state.RecordListing != null && state.RecordListing.Count > 0;
|
||||
NoRecords.Visible = !hasRecords;
|
||||
if (hasRecords)
|
||||
PopulateRecordListing(state.RecordListing!);
|
||||
|
||||
// set up the selected person's record
|
||||
var selected = _selectedKey != null;
|
||||
|
||||
PersonContainer.Visible = selected;
|
||||
RecordUnselected.Visible = !selected;
|
||||
|
||||
_access = _player.LocalSession?.AttachedEntity is {} player
|
||||
&& _accessReader.IsAllowed(player, Console);
|
||||
|
||||
// hide access-required editing parts when no access
|
||||
var editing = _access && selected;
|
||||
StatusOptionButton.Disabled = !editing;
|
||||
|
||||
if (state is { CriminalRecord: not null, StationRecord: not null })
|
||||
{
|
||||
PopulateRecordContainer(state.StationRecord, state.CriminalRecord);
|
||||
OnHistoryUpdated?.Invoke(state.CriminalRecord, _access, false);
|
||||
_selectedRecord = state.CriminalRecord;
|
||||
}
|
||||
else
|
||||
{
|
||||
_selectedRecord = null;
|
||||
OnHistoryClosed?.Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
private void PopulateRecordListing(Dictionary<uint, string> listing)
|
||||
{
|
||||
_isPopulating = true;
|
||||
|
||||
foreach (var (key, name) in listing)
|
||||
{
|
||||
var item = RecordListing.AddItem(name);
|
||||
item.Metadata = key;
|
||||
item.Selected = key == _selectedKey;
|
||||
}
|
||||
_isPopulating = false;
|
||||
|
||||
RecordListing.SortItemsByText();
|
||||
}
|
||||
|
||||
private void PopulateRecordContainer(GeneralStationRecord stationRecord, CriminalRecord criminalRecord)
|
||||
{
|
||||
var na = Loc.GetString("generic-not-available-shorthand");
|
||||
PersonName.Text = stationRecord.Name;
|
||||
PersonPrints.Text = Loc.GetString("general-station-record-console-record-fingerprint", ("fingerprint", stationRecord.Fingerprint ?? na));
|
||||
PersonDna.Text = Loc.GetString("general-station-record-console-record-dna", ("dna", stationRecord.DNA ?? na));
|
||||
|
||||
StatusOptionButton.SelectId((int) criminalRecord.Status);
|
||||
if (criminalRecord.Reason is {} reason)
|
||||
{
|
||||
var message = FormattedMessage.FromMarkup(Loc.GetString("criminal-records-console-wanted-reason"));
|
||||
message.AddText($": {reason}");
|
||||
WantedReason.SetMessage(message);
|
||||
WantedReason.Visible = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
WantedReason.Visible = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void AddStatusSelect(SecurityStatus status)
|
||||
{
|
||||
var name = Loc.GetString($"criminal-records-status-{status.ToString().ToLower()}");
|
||||
StatusOptionButton.AddItem(name, (int)status);
|
||||
}
|
||||
|
||||
private void FilterListingOfRecords(string text = "")
|
||||
{
|
||||
if (!_isPopulating)
|
||||
{
|
||||
OnFiltersChanged?.Invoke(_currentFilterType, text);
|
||||
}
|
||||
}
|
||||
|
||||
private void SetStatus(SecurityStatus status)
|
||||
{
|
||||
if (status == SecurityStatus.Wanted)
|
||||
{
|
||||
GetWantedReason();
|
||||
return;
|
||||
}
|
||||
|
||||
OnStatusSelected?.Invoke(status);
|
||||
}
|
||||
|
||||
private void GetWantedReason()
|
||||
{
|
||||
if (_reasonDialog != null)
|
||||
{
|
||||
_reasonDialog.MoveToFront();
|
||||
return;
|
||||
}
|
||||
|
||||
var field = "reason";
|
||||
var title = Loc.GetString("criminal-records-status-wanted");
|
||||
var placeholders = _proto.Index<DatasetPrototype>(ReasonPlaceholders);
|
||||
var placeholder = Loc.GetString("criminal-records-console-reason-placeholder", ("placeholder", _random.Pick(placeholders.Values))); // just funny it doesn't actually get used
|
||||
var prompt = Loc.GetString("criminal-records-console-reason");
|
||||
var entry = new QuickDialogEntry(field, QuickDialogEntryType.LongText, prompt, placeholder);
|
||||
var entries = new List<QuickDialogEntry>() { entry };
|
||||
_reasonDialog = new DialogWindow(title, entries);
|
||||
|
||||
_reasonDialog.OnConfirmed += responses =>
|
||||
{
|
||||
var reason = responses[field];
|
||||
if (reason.Length < 1 || reason.Length > _maxLength)
|
||||
return;
|
||||
|
||||
OnDialogConfirmed?.Invoke(SecurityStatus.Wanted, reason);
|
||||
};
|
||||
|
||||
_reasonDialog.OnClose += () => { _reasonDialog = null; };
|
||||
}
|
||||
|
||||
private string GetTypeFilterLocals(StationRecordFilterType type)
|
||||
{
|
||||
return Loc.GetString($"criminal-records-{type.ToString().ToLower()}-filter");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
using Content.Shared.CriminalRecords.Systems;
|
||||
|
||||
namespace Content.Client.CriminalRecords.Systems;
|
||||
|
||||
public sealed class CriminalRecordsConsoleSystem : SharedCriminalRecordsConsoleSystem
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
using Content.Shared.DeviceLinking;
|
||||
|
||||
namespace Content.Client.DeviceLinking;
|
||||
|
||||
public sealed class DeviceLinkSystem : SharedDeviceLinkSystem
|
||||
{
|
||||
|
||||
}
|
||||
@@ -20,8 +20,6 @@ namespace Content.Client.GameTicking.Managers
|
||||
{
|
||||
[Dependency] private readonly IStateManager _stateManager = default!;
|
||||
[Dependency] private readonly IEntityManager _entityManager = default!;
|
||||
[Dependency] private readonly IConfigurationManager _configManager = default!;
|
||||
[Dependency] private readonly SharedAudioSystem _audio = default!;
|
||||
|
||||
[ViewVariables] private bool _initialized;
|
||||
private Dictionary<NetEntity, Dictionary<string, uint?>> _jobsAvailable = new();
|
||||
|
||||
@@ -98,22 +98,33 @@ public sealed partial class GuidebookWindow : FancyWindow, ILinkClickHandler
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerable<GuideEntry> GetSortedRootEntries(List<string>? rootEntries)
|
||||
private IEnumerable<GuideEntry> GetSortedEntries(List<string>? rootEntries)
|
||||
{
|
||||
if (rootEntries == null)
|
||||
{
|
||||
HashSet<string> entries = new(_entries.Keys);
|
||||
foreach (var entry in _entries.Values)
|
||||
{
|
||||
if (entry.Children.Count > 0)
|
||||
{
|
||||
var sortedChildren = entry.Children
|
||||
.Select(childId => _entries[childId])
|
||||
.OrderBy(childEntry => childEntry.Priority)
|
||||
.ThenBy(childEntry => Loc.GetString(childEntry.Name))
|
||||
.Select(childEntry => childEntry.Id)
|
||||
.ToList();
|
||||
|
||||
entry.Children = sortedChildren;
|
||||
}
|
||||
entries.ExceptWith(entry.Children);
|
||||
}
|
||||
rootEntries = entries.ToList();
|
||||
}
|
||||
|
||||
return rootEntries
|
||||
.Select(x => _entries[x])
|
||||
.OrderBy(x => x.Priority)
|
||||
.ThenBy(x => Loc.GetString(x.Name));
|
||||
.Select(rootEntryId => _entries[rootEntryId])
|
||||
.OrderBy(rootEntry => rootEntry.Priority)
|
||||
.ThenBy(rootEntry => Loc.GetString(rootEntry.Name));
|
||||
}
|
||||
|
||||
private void RepopulateTree(List<string>? roots = null, string? forcedRoot = null)
|
||||
@@ -123,7 +134,7 @@ public sealed partial class GuidebookWindow : FancyWindow, ILinkClickHandler
|
||||
HashSet<string> addedEntries = new();
|
||||
|
||||
TreeItem? parent = forcedRoot == null ? null : AddEntry(forcedRoot, null, addedEntries);
|
||||
foreach (var entry in GetSortedRootEntries(roots))
|
||||
foreach (var entry in GetSortedEntries(roots))
|
||||
{
|
||||
AddEntry(entry.Id, parent, addedEntries);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<DefaultWindow xmlns="https://spacestation14.io"
|
||||
<controls:FancyWindow
|
||||
xmlns="https://spacestation14.io"
|
||||
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
|
||||
SetSize="250 100">
|
||||
<ScrollContainer
|
||||
VerticalExpand="True">
|
||||
@@ -12,6 +14,16 @@
|
||||
Name="PatientDataContainer"
|
||||
Orientation="Vertical"
|
||||
Margin="0 0 5 10">
|
||||
<BoxContainer Name="ScanModePanel" HorizontalExpand="True" Visible="False" Margin="0 5 0 0">
|
||||
<Label
|
||||
Name="ScanMode"
|
||||
Align="Left"
|
||||
Text="{Loc health-analyzer-window-scan-mode-text}"/>
|
||||
<Label
|
||||
Name="ScanModeText"
|
||||
Align="Right"
|
||||
HorizontalExpand="True"/>
|
||||
</BoxContainer>
|
||||
<Label
|
||||
Name="PatientName"/>
|
||||
<Label
|
||||
@@ -30,4 +42,4 @@
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
</ScrollContainer>
|
||||
</DefaultWindow>
|
||||
</controls:FancyWindow>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using Content.Client.UserInterface.Controls;
|
||||
using Content.Shared.Damage;
|
||||
using Content.Shared.Damage.Prototypes;
|
||||
using Content.Shared.FixedPoint;
|
||||
@@ -7,7 +8,6 @@ using Content.Shared.IdentityManagement;
|
||||
using Content.Shared.MedicalScanner;
|
||||
using Content.Shared.Nutrition.Components;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.Graphics;
|
||||
@@ -20,7 +20,7 @@ using Robust.Shared.Utility;
|
||||
namespace Content.Client.HealthAnalyzer.UI
|
||||
{
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class HealthAnalyzerWindow : DefaultWindow
|
||||
public sealed partial class HealthAnalyzerWindow : FancyWindow
|
||||
{
|
||||
private readonly IEntityManager _entityManager;
|
||||
private readonly SpriteSystem _spriteSystem;
|
||||
@@ -62,6 +62,17 @@ namespace Content.Client.HealthAnalyzer.UI
|
||||
entityName = Identity.Name(target.Value, _entityManager);
|
||||
}
|
||||
|
||||
if (msg.ScanMode.HasValue)
|
||||
{
|
||||
ScanModePanel.Visible = true;
|
||||
ScanModeText.Text = Loc.GetString(msg.ScanMode.Value ? "health-analyzer-window-scan-mode-active" : "health-analyzer-window-scan-mode-inactive");
|
||||
ScanModeText.FontColorOverride = msg.ScanMode.Value ? Color.Green : Color.Red;
|
||||
}
|
||||
else
|
||||
{
|
||||
ScanModePanel.Visible = false;
|
||||
}
|
||||
|
||||
PatientName.Text = Loc.GetString(
|
||||
"health-analyzer-window-entity-health-text",
|
||||
("entityName", entityName)
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
FontColorOverride="{xNamespace:Static s:StyleNano.NanoGold}"
|
||||
StyleClasses="LabelKeyText"/>
|
||||
<CheckBox Name="ReducedMotionCheckBox" Text="{Loc 'ui-options-reduced-motion'}" />
|
||||
<CheckBox Name="EnableColorNameCheckBox" Text="{Loc 'ui-options-enable-color-name'}" />
|
||||
<BoxContainer Orientation="Horizontal">
|
||||
<Label Text="{Loc 'ui-options-screen-shake-intensity'}" Margin="8 0" />
|
||||
<Slider Name="ScreenShakeIntensitySlider"
|
||||
|
||||
@@ -63,6 +63,7 @@ namespace Content.Client.Options.UI.Tabs
|
||||
OpaqueStorageWindowCheckBox.OnToggled += OnCheckBoxToggled;
|
||||
FancySpeechBubblesCheckBox.OnToggled += OnCheckBoxToggled;
|
||||
FancyNameBackgroundsCheckBox.OnToggled += OnCheckBoxToggled;
|
||||
EnableColorNameCheckBox.OnToggled += OnCheckBoxToggled;
|
||||
ReducedMotionCheckBox.OnToggled += OnCheckBoxToggled;
|
||||
ScreenShakeIntensitySlider.OnValueChanged += OnScreenShakeIntensitySliderChanged;
|
||||
// ToggleWalk.OnToggled += OnCheckBoxToggled;
|
||||
@@ -76,6 +77,7 @@ namespace Content.Client.Options.UI.Tabs
|
||||
OpaqueStorageWindowCheckBox.Pressed = _cfg.GetCVar(CCVars.OpaqueStorageWindow);
|
||||
FancySpeechBubblesCheckBox.Pressed = _cfg.GetCVar(CCVars.ChatEnableFancyBubbles);
|
||||
FancyNameBackgroundsCheckBox.Pressed = _cfg.GetCVar(CCVars.ChatFancyNameBackground);
|
||||
EnableColorNameCheckBox.Pressed = _cfg.GetCVar(CCVars.ChatEnableColorName);
|
||||
ReducedMotionCheckBox.Pressed = _cfg.GetCVar(CCVars.ReducedMotion);
|
||||
ScreenShakeIntensitySlider.Value = _cfg.GetCVar(CCVars.ScreenShakeIntensity) * 100f;
|
||||
// ToggleWalk.Pressed = _cfg.GetCVar(CCVars.ToggleWalk);
|
||||
@@ -120,6 +122,7 @@ namespace Content.Client.Options.UI.Tabs
|
||||
_cfg.SetCVar(CCVars.LoocAboveHeadShow, ShowLoocAboveHeadCheckBox.Pressed);
|
||||
_cfg.SetCVar(CCVars.ChatEnableFancyBubbles, FancySpeechBubblesCheckBox.Pressed);
|
||||
_cfg.SetCVar(CCVars.ChatFancyNameBackground, FancyNameBackgroundsCheckBox.Pressed);
|
||||
_cfg.SetCVar(CCVars.ChatEnableColorName, EnableColorNameCheckBox.Pressed);
|
||||
_cfg.SetCVar(CCVars.ReducedMotion, ReducedMotionCheckBox.Pressed);
|
||||
_cfg.SetCVar(CCVars.ScreenShakeIntensity, ScreenShakeIntensitySlider.Value / 100f);
|
||||
// _cfg.SetCVar(CCVars.ToggleWalk, ToggleWalk.Pressed);
|
||||
@@ -145,6 +148,7 @@ namespace Content.Client.Options.UI.Tabs
|
||||
var isLoocShowSame = ShowLoocAboveHeadCheckBox.Pressed == _cfg.GetCVar(CCVars.LoocAboveHeadShow);
|
||||
var isFancyChatSame = FancySpeechBubblesCheckBox.Pressed == _cfg.GetCVar(CCVars.ChatEnableFancyBubbles);
|
||||
var isFancyBackgroundSame = FancyNameBackgroundsCheckBox.Pressed == _cfg.GetCVar(CCVars.ChatFancyNameBackground);
|
||||
var isEnableColorNameSame = EnableColorNameCheckBox.Pressed == _cfg.GetCVar(CCVars.ChatEnableColorName);
|
||||
var isReducedMotionSame = ReducedMotionCheckBox.Pressed == _cfg.GetCVar(CCVars.ReducedMotion);
|
||||
var isScreenShakeIntensitySame = Math.Abs(ScreenShakeIntensitySlider.Value / 100f - _cfg.GetCVar(CCVars.ScreenShakeIntensity)) < 0.01f;
|
||||
// var isToggleWalkSame = ToggleWalk.Pressed == _cfg.GetCVar(CCVars.ToggleWalk);
|
||||
@@ -159,6 +163,7 @@ namespace Content.Client.Options.UI.Tabs
|
||||
isLoocShowSame &&
|
||||
isFancyChatSame &&
|
||||
isFancyBackgroundSame &&
|
||||
isEnableColorNameSame &&
|
||||
isReducedMotionSame &&
|
||||
isScreenShakeIntensitySame &&
|
||||
// isToggleWalkSame &&
|
||||
|
||||
@@ -1,19 +1,12 @@
|
||||
using System.Numerics;
|
||||
using Content.Shared.Pointing.Components;
|
||||
using System.Numerics;
|
||||
|
||||
namespace Content.Client.Pointing.Components;
|
||||
[RegisterComponent]
|
||||
public sealed partial class PointingArrowComponent : SharedPointingArrowComponent
|
||||
{
|
||||
/// <summary>
|
||||
/// How long it takes to go from the bottom of the animation to the top.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField("animationTime")]
|
||||
public float AnimationTime = 0.5f;
|
||||
|
||||
/// <summary>
|
||||
/// How far it goes in any direction.
|
||||
/// How far the arrow moves up and down during the floating phase.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField("offset")]
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
using Content.Client.Pointing.Components;
|
||||
using Content.Shared.Pointing;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.Animations;
|
||||
using Robust.Shared.Animations;
|
||||
using System.Numerics;
|
||||
|
||||
namespace Content.Client.Pointing;
|
||||
|
||||
public sealed partial class PointingSystem : SharedPointingSystem
|
||||
{
|
||||
[Dependency] private readonly AnimationPlayerSystem _animationPlayer = default!;
|
||||
|
||||
public void InitializeVisualizer()
|
||||
{
|
||||
SubscribeLocalEvent<PointingArrowComponent, AnimationCompletedEvent>(OnAnimationCompleted);
|
||||
}
|
||||
|
||||
private void OnAnimationCompleted(EntityUid uid, PointingArrowComponent component, AnimationCompletedEvent args)
|
||||
{
|
||||
if (args.Key == component.AnimationKey)
|
||||
_animationPlayer.Stop(uid, component.AnimationKey);
|
||||
}
|
||||
|
||||
private void BeginPointAnimation(EntityUid uid, Vector2 startPosition, Vector2 offset, string animationKey)
|
||||
{
|
||||
if (_animationPlayer.HasRunningAnimation(uid, animationKey))
|
||||
return;
|
||||
|
||||
var animation = new Animation
|
||||
{
|
||||
Length = PointDuration,
|
||||
AnimationTracks =
|
||||
{
|
||||
new AnimationTrackComponentProperty
|
||||
{
|
||||
ComponentType = typeof(SpriteComponent),
|
||||
Property = nameof(SpriteComponent.Offset),
|
||||
InterpolationMode = AnimationInterpolationMode.Cubic,
|
||||
KeyFrames =
|
||||
{
|
||||
// We pad here to prevent improper looping and tighten the overshoot, just a touch
|
||||
new AnimationTrackProperty.KeyFrame(startPosition, 0f),
|
||||
new AnimationTrackProperty.KeyFrame(Vector2.Lerp(startPosition, offset, 0.9f), PointKeyTimeMove),
|
||||
new AnimationTrackProperty.KeyFrame(offset, PointKeyTimeMove),
|
||||
new AnimationTrackProperty.KeyFrame(Vector2.Zero, PointKeyTimeMove),
|
||||
new AnimationTrackProperty.KeyFrame(offset, PointKeyTimeHover),
|
||||
new AnimationTrackProperty.KeyFrame(Vector2.Zero, PointKeyTimeHover),
|
||||
new AnimationTrackProperty.KeyFrame(offset, PointKeyTimeHover),
|
||||
new AnimationTrackProperty.KeyFrame(Vector2.Zero, PointKeyTimeHover),
|
||||
new AnimationTrackProperty.KeyFrame(offset, PointKeyTimeHover),
|
||||
new AnimationTrackProperty.KeyFrame(Vector2.Zero, PointKeyTimeHover),
|
||||
new AnimationTrackProperty.KeyFrame(offset, PointKeyTimeHover),
|
||||
new AnimationTrackProperty.KeyFrame(Vector2.Zero, PointKeyTimeHover),
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
_animationPlayer.Play(uid, animation, animationKey);
|
||||
}
|
||||
}
|
||||
@@ -1,32 +1,25 @@
|
||||
using Content.Client.Pointing.Components;
|
||||
using Content.Client.Gravity;
|
||||
using Content.Shared.Mobs.Systems;
|
||||
using Content.Shared.Pointing;
|
||||
using Content.Shared.Verbs;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Utility;
|
||||
using DrawDepth = Content.Shared.DrawDepth.DrawDepth;
|
||||
|
||||
namespace Content.Client.Pointing;
|
||||
|
||||
public sealed class PointingSystem : SharedPointingSystem
|
||||
public sealed partial class PointingSystem : SharedPointingSystem
|
||||
{
|
||||
[Dependency] private readonly MobStateSystem _mobState = default!;
|
||||
[Dependency] private readonly FloatingVisualizerSystem _floatingSystem = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<GetVerbsEvent<Verb>>(AddPointingVerb);
|
||||
SubscribeLocalEvent<PointingArrowComponent, ComponentStartup>(OnArrowStartup);
|
||||
SubscribeLocalEvent<PointingArrowComponent, AnimationCompletedEvent>(OnArrowAnimation);
|
||||
SubscribeLocalEvent<RoguePointingArrowComponent, ComponentStartup>(OnRogueArrowStartup);
|
||||
}
|
||||
SubscribeLocalEvent<PointingArrowComponent, ComponentHandleState>(HandleCompState);
|
||||
|
||||
private void OnArrowAnimation(EntityUid uid, PointingArrowComponent component, AnimationCompletedEvent args)
|
||||
{
|
||||
_floatingSystem.FloatAnimation(uid, component.Offset, component.AnimationKey, component.AnimationTime);
|
||||
InitializeVisualizer();
|
||||
}
|
||||
|
||||
private void AddPointingVerb(GetVerbsEvent<Verb> args)
|
||||
@@ -38,15 +31,11 @@ public sealed class PointingSystem : SharedPointingSystem
|
||||
// I'm just adding this verb exclusively to clients so that the verb-loading pop-in on the verb menu isn't
|
||||
// as bad. Important for this verb seeing as its usually an option on just about any entity.
|
||||
|
||||
// this is a pointing arrow. no pointing here...
|
||||
if (HasComp<PointingArrowComponent>(args.Target))
|
||||
{
|
||||
// this is a pointing arrow. no pointing here...
|
||||
return;
|
||||
}
|
||||
|
||||
// Can the user point? Checking mob state directly instead of some action blocker, as many action blockers are blocked for
|
||||
// ghosts and there is no obvious choice for pointing (unless ghosts CanEmote?).
|
||||
if (_mobState.IsIncapacitated(args.User))
|
||||
if (!CanPoint(args.User))
|
||||
return;
|
||||
|
||||
// We won't check in range or visibility, as this verb is currently only executable via the context menu,
|
||||
@@ -66,11 +55,9 @@ public sealed class PointingSystem : SharedPointingSystem
|
||||
private void OnArrowStartup(EntityUid uid, PointingArrowComponent component, ComponentStartup args)
|
||||
{
|
||||
if (TryComp<SpriteComponent>(uid, out var sprite))
|
||||
{
|
||||
sprite.DrawDepth = (int) DrawDepth.Overlays;
|
||||
}
|
||||
|
||||
_floatingSystem.FloatAnimation(uid, component.Offset, component.AnimationKey, component.AnimationTime);
|
||||
BeginPointAnimation(uid, component.StartPosition, component.Offset, component.AnimationKey);
|
||||
}
|
||||
|
||||
private void OnRogueArrowStartup(EntityUid uid, RoguePointingArrowComponent arrow, ComponentStartup args)
|
||||
@@ -81,4 +68,13 @@ public sealed class PointingSystem : SharedPointingSystem
|
||||
sprite.NoRotation = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleCompState(Entity<PointingArrowComponent> entity, ref ComponentHandleState args)
|
||||
{
|
||||
if (args.Current is not SharedPointingArrowComponentState state)
|
||||
return;
|
||||
|
||||
entity.Comp.StartPosition = state.StartPosition;
|
||||
entity.Comp.EndTime = state.EndTime;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
using System.Numerics;
|
||||
using Content.Client.Examine;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.Examine;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Popups;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Player;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.Configuration;
|
||||
@@ -26,11 +20,9 @@ public sealed class PopupOverlay : Overlay
|
||||
private readonly IPlayerManager _playerMgr;
|
||||
private readonly IUserInterfaceManager _uiManager;
|
||||
private readonly PopupSystem _popup;
|
||||
private readonly PopupUIController _controller;
|
||||
|
||||
private readonly ShaderInstance _shader;
|
||||
private readonly Font _smallFont;
|
||||
private readonly Font _mediumFont;
|
||||
private readonly Font _largeFont;
|
||||
|
||||
public override OverlaySpace Space => OverlaySpace.ScreenSpace;
|
||||
|
||||
@@ -39,8 +31,8 @@ public sealed class PopupOverlay : Overlay
|
||||
IEntityManager entManager,
|
||||
IPlayerManager playerMgr,
|
||||
IPrototypeManager protoManager,
|
||||
IResourceCache cache,
|
||||
IUserInterfaceManager uiManager,
|
||||
PopupUIController controller,
|
||||
PopupSystem popup)
|
||||
{
|
||||
_configManager = configManager;
|
||||
@@ -48,11 +40,9 @@ public sealed class PopupOverlay : Overlay
|
||||
_playerMgr = playerMgr;
|
||||
_uiManager = uiManager;
|
||||
_popup = popup;
|
||||
_controller = controller;
|
||||
|
||||
_shader = protoManager.Index<ShaderPrototype>("unshaded").Instance();
|
||||
_smallFont = new VectorFont(cache.GetResource<FontResource>("/Fonts/NotoSans/NotoSans-Italic.ttf"), 10);
|
||||
_mediumFont = new VectorFont(cache.GetResource<FontResource>("/Fonts/NotoSans/NotoSans-Italic.ttf"), 12);
|
||||
_largeFont = new VectorFont(cache.GetResource<FontResource>("/Fonts/NotoSans/NotoSans-BoldItalic.ttf"), 14);
|
||||
}
|
||||
|
||||
protected override void Draw(in OverlayDrawArgs args)
|
||||
@@ -68,19 +58,18 @@ public sealed class PopupOverlay : Overlay
|
||||
scale = _uiManager.DefaultUIScale;
|
||||
|
||||
DrawWorld(args.ScreenHandle, args, scale);
|
||||
DrawScreen(args.ScreenHandle, args, scale);
|
||||
|
||||
args.DrawingHandle.UseShader(null);
|
||||
}
|
||||
|
||||
private void DrawWorld(DrawingHandleScreen worldHandle, OverlayDrawArgs args, float scale)
|
||||
{
|
||||
if (_popup.WorldLabels.Count == 0)
|
||||
if (_popup.WorldLabels.Count == 0 || args.ViewportControl == null)
|
||||
return;
|
||||
|
||||
var matrix = args.ViewportControl!.GetWorldToScreenMatrix();
|
||||
var matrix = args.ViewportControl.GetWorldToScreenMatrix();
|
||||
var viewPos = new MapCoordinates(args.WorldAABB.Center, args.MapId);
|
||||
var ourEntity = _playerMgr.LocalPlayer?.ControlledEntity;
|
||||
var ourEntity = _playerMgr.LocalEntity;
|
||||
|
||||
foreach (var popup in _popup.WorldLabels)
|
||||
{
|
||||
@@ -92,62 +81,12 @@ public sealed class PopupOverlay : Overlay
|
||||
var distance = (mapPos.Position - args.WorldBounds.Center).Length();
|
||||
|
||||
// Should handle fade here too wyci.
|
||||
if (!args.WorldAABB.Contains(mapPos.Position) || !ExamineSystemShared.InRangeUnOccluded(viewPos, mapPos, distance,
|
||||
if (!args.WorldBounds.Contains(mapPos.Position) || !ExamineSystemShared.InRangeUnOccluded(viewPos, mapPos, distance,
|
||||
e => e == popup.InitialPos.EntityId || e == ourEntity, entMan: _entManager))
|
||||
continue;
|
||||
|
||||
var pos = matrix.Transform(mapPos.Position);
|
||||
DrawPopup(popup, worldHandle, pos, scale);
|
||||
_controller.DrawPopup(popup, worldHandle, pos, scale);
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawScreen(DrawingHandleScreen screenHandle, OverlayDrawArgs args, float scale)
|
||||
{
|
||||
foreach (var popup in _popup.CursorLabels)
|
||||
{
|
||||
// Different window
|
||||
if (popup.InitialPos.Window != args.ViewportControl?.Window?.Id)
|
||||
continue;
|
||||
|
||||
DrawPopup(popup, screenHandle, popup.InitialPos.Position, scale);
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawPopup(PopupSystem.PopupLabel popup, DrawingHandleScreen handle, Vector2 position, float scale)
|
||||
{
|
||||
var lifetime = PopupSystem.GetPopupLifetime(popup);
|
||||
|
||||
// Keep alpha at 1 until TotalTime passes half its lifetime, then gradually decrease to 0.
|
||||
var alpha = MathF.Min(1f, 1f - MathF.Max(0f, popup.TotalTime - lifetime / 2) * 2 / lifetime);
|
||||
|
||||
var updatedPosition = position - new Vector2(0f, MathF.Min(8f, 12f * (popup.TotalTime * popup.TotalTime + popup.TotalTime)));
|
||||
var font = _smallFont;
|
||||
var color = Color.White.WithAlpha(alpha);
|
||||
|
||||
switch (popup.Type)
|
||||
{
|
||||
case PopupType.SmallCaution:
|
||||
color = Color.Red;
|
||||
break;
|
||||
case PopupType.Medium:
|
||||
font = _mediumFont;
|
||||
color = Color.LightGray;
|
||||
break;
|
||||
case PopupType.MediumCaution:
|
||||
font = _mediumFont;
|
||||
color = Color.Red;
|
||||
break;
|
||||
case PopupType.Large:
|
||||
font = _largeFont;
|
||||
color = Color.LightGray;
|
||||
break;
|
||||
case PopupType.LargeCaution:
|
||||
font = _largeFont;
|
||||
color = Color.Red;
|
||||
break;
|
||||
}
|
||||
|
||||
var dimensions = handle.GetDimensions(font, popup.Text, scale);
|
||||
handle.DrawString(font, updatedPosition - dimensions / 2f, popup.Text, scale, color.WithAlpha(alpha));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@ namespace Content.Client.Popups
|
||||
[Dependency] private readonly IOverlayManager _overlay = default!;
|
||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototype = default!;
|
||||
[Dependency] private readonly IResourceCache _resource = default!;
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
[Dependency] private readonly IUserInterfaceManager _uiManager = default!;
|
||||
[Dependency] private readonly IReplayRecordingManager _replayRecording = default!;
|
||||
@@ -45,7 +44,14 @@ namespace Content.Client.Popups
|
||||
SubscribeNetworkEvent<PopupEntityEvent>(OnPopupEntityEvent);
|
||||
SubscribeNetworkEvent<RoundRestartCleanupEvent>(OnRoundRestart);
|
||||
_overlay
|
||||
.AddOverlay(new PopupOverlay(_configManager, EntityManager, _playerManager, _prototype, _resource, _uiManager, this));
|
||||
.AddOverlay(new PopupOverlay(
|
||||
_configManager,
|
||||
EntityManager,
|
||||
_playerManager,
|
||||
_prototype,
|
||||
_uiManager,
|
||||
_uiManager.GetUIController<PopupUIController>(),
|
||||
this));
|
||||
}
|
||||
|
||||
public override void Shutdown()
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
using System.Numerics;
|
||||
using Content.Client.Gameplay;
|
||||
using Content.Shared.Popups;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controllers;
|
||||
|
||||
namespace Content.Client.Popups;
|
||||
|
||||
/// <summary>
|
||||
/// Handles screens-space popups. World popups are handled via PopupOverlay.
|
||||
/// </summary>
|
||||
public sealed class PopupUIController : UIController, IOnStateEntered<GameplayState>, IOnStateExited<GameplayState>
|
||||
{
|
||||
[UISystemDependency] private readonly PopupSystem? _popup = default!;
|
||||
|
||||
private Font _smallFont = default!;
|
||||
private Font _mediumFont = default!;
|
||||
private Font _largeFont = default!;
|
||||
|
||||
private PopupRootControl? _popupControl;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
var cache = IoCManager.Resolve<IResourceCache>();
|
||||
|
||||
_smallFont = new VectorFont(cache.GetResource<FontResource>("/Fonts/NotoSans/NotoSans-Italic.ttf"), 10);
|
||||
_mediumFont = new VectorFont(cache.GetResource<FontResource>("/Fonts/NotoSans/NotoSans-Italic.ttf"), 12);
|
||||
_largeFont = new VectorFont(cache.GetResource<FontResource>("/Fonts/NotoSans/NotoSans-BoldItalic.ttf"), 14);
|
||||
}
|
||||
|
||||
public void OnStateEntered(GameplayState state)
|
||||
{
|
||||
_popupControl = new PopupRootControl(_popup, this);
|
||||
|
||||
UIManager.RootControl.AddChild(_popupControl);
|
||||
}
|
||||
|
||||
public void OnStateExited(GameplayState state)
|
||||
{
|
||||
if (_popupControl == null)
|
||||
return;
|
||||
|
||||
UIManager.RootControl.RemoveChild(_popupControl);
|
||||
_popupControl = null;
|
||||
}
|
||||
|
||||
public void DrawPopup(PopupSystem.PopupLabel popup, DrawingHandleScreen handle, Vector2 position, float scale)
|
||||
{
|
||||
var lifetime = PopupSystem.GetPopupLifetime(popup);
|
||||
|
||||
// Keep alpha at 1 until TotalTime passes half its lifetime, then gradually decrease to 0.
|
||||
var alpha = MathF.Min(1f, 1f - MathF.Max(0f, popup.TotalTime - lifetime / 2) * 2 / lifetime);
|
||||
|
||||
var updatedPosition = position - new Vector2(0f, MathF.Min(8f, 12f * (popup.TotalTime * popup.TotalTime + popup.TotalTime)));
|
||||
var font = _smallFont;
|
||||
var color = Color.White.WithAlpha(alpha);
|
||||
|
||||
switch (popup.Type)
|
||||
{
|
||||
case PopupType.SmallCaution:
|
||||
color = Color.Red;
|
||||
break;
|
||||
case PopupType.Medium:
|
||||
font = _mediumFont;
|
||||
color = Color.LightGray;
|
||||
break;
|
||||
case PopupType.MediumCaution:
|
||||
font = _mediumFont;
|
||||
color = Color.Red;
|
||||
break;
|
||||
case PopupType.Large:
|
||||
font = _largeFont;
|
||||
color = Color.LightGray;
|
||||
break;
|
||||
case PopupType.LargeCaution:
|
||||
font = _largeFont;
|
||||
color = Color.Red;
|
||||
break;
|
||||
}
|
||||
|
||||
var dimensions = handle.GetDimensions(font, popup.Text, scale);
|
||||
handle.DrawString(font, updatedPosition - dimensions / 2f, popup.Text, scale, color.WithAlpha(alpha));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles drawing all screen popups.
|
||||
/// </summary>
|
||||
private sealed class PopupRootControl : Control
|
||||
{
|
||||
private readonly PopupSystem? _popup;
|
||||
private readonly PopupUIController _controller;
|
||||
|
||||
public PopupRootControl(PopupSystem? system, PopupUIController controller)
|
||||
{
|
||||
_popup = system;
|
||||
_controller = controller;
|
||||
}
|
||||
|
||||
protected override void Draw(DrawingHandleScreen handle)
|
||||
{
|
||||
base.Draw(handle);
|
||||
|
||||
if (_popup == null)
|
||||
return;
|
||||
|
||||
// Different window
|
||||
var windowId = UserInterfaceManager.RootControl.Window.Id;
|
||||
|
||||
foreach (var popup in _popup.CursorLabels)
|
||||
{
|
||||
if (popup.InitialPos.Window != windowId)
|
||||
continue;
|
||||
|
||||
_controller.DrawPopup(popup, handle, popup.InitialPos.Position, UIScale);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
using Content.Shared.StationRecords;
|
||||
using Robust.Client.GameObjects;
|
||||
|
||||
namespace Content.Client.StationRecords;
|
||||
|
||||
@@ -17,33 +16,21 @@ public sealed class GeneralStationRecordConsoleBoundUserInterface : BoundUserInt
|
||||
base.Open();
|
||||
|
||||
_window = new();
|
||||
_window.OnKeySelected += OnKeySelected;
|
||||
_window.OnFiltersChanged += OnFiltersChanged;
|
||||
_window.OnKeySelected += key =>
|
||||
SendMessage(new SelectStationRecord(key));
|
||||
_window.OnFiltersChanged += (type, filterValue) =>
|
||||
SendMessage(new SetStationRecordFilter(type, filterValue));
|
||||
_window.OnClose += Close;
|
||||
|
||||
_window.OpenCentered();
|
||||
}
|
||||
|
||||
private void OnKeySelected((NetEntity, uint)? key)
|
||||
{
|
||||
SendMessage(new SelectGeneralStationRecord(key));
|
||||
}
|
||||
|
||||
private void OnFiltersChanged(
|
||||
GeneralStationRecordFilterType type, string filterValue)
|
||||
{
|
||||
GeneralStationRecordsFilterMsg msg = new(type, filterValue);
|
||||
SendMessage(msg);
|
||||
}
|
||||
|
||||
protected override void UpdateState(BoundUserInterfaceState state)
|
||||
{
|
||||
base.UpdateState(state);
|
||||
|
||||
if (state is not GeneralStationRecordConsoleState cast)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_window?.UpdateState(cast);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
using System.Linq;
|
||||
using Content.Shared.StationRecords;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface;
|
||||
@@ -11,31 +10,29 @@ namespace Content.Client.StationRecords;
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class GeneralStationRecordConsoleWindow : DefaultWindow
|
||||
{
|
||||
public Action<(NetEntity, uint)?>? OnKeySelected;
|
||||
public Action<uint?>? OnKeySelected;
|
||||
|
||||
public Action<GeneralStationRecordFilterType, string>? OnFiltersChanged;
|
||||
public Action<StationRecordFilterType, string>? OnFiltersChanged;
|
||||
|
||||
private bool _isPopulating;
|
||||
|
||||
private GeneralStationRecordFilterType _currentFilterType;
|
||||
private StationRecordFilterType _currentFilterType;
|
||||
|
||||
public GeneralStationRecordConsoleWindow()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
|
||||
_currentFilterType = GeneralStationRecordFilterType.Name;
|
||||
_currentFilterType = StationRecordFilterType.Name;
|
||||
|
||||
foreach (var item in Enum.GetValues<GeneralStationRecordFilterType>())
|
||||
foreach (var item in Enum.GetValues<StationRecordFilterType>())
|
||||
{
|
||||
StationRecordsFilterType.AddItem(GetTypeFilterLocals(item), (int)item);
|
||||
}
|
||||
|
||||
RecordListing.OnItemSelected += args =>
|
||||
{
|
||||
if (_isPopulating || RecordListing[args.ItemIndex].Metadata is not ValueTuple<NetEntity, uint> cast)
|
||||
{
|
||||
if (_isPopulating || RecordListing[args.ItemIndex].Metadata is not uint cast)
|
||||
return;
|
||||
}
|
||||
|
||||
OnKeySelected?.Invoke(cast);
|
||||
};
|
||||
@@ -48,7 +45,7 @@ public sealed partial class GeneralStationRecordConsoleWindow : DefaultWindow
|
||||
|
||||
StationRecordsFilterType.OnItemSelected += eventArgs =>
|
||||
{
|
||||
var type = (GeneralStationRecordFilterType)eventArgs.Id;
|
||||
var type = (StationRecordFilterType) eventArgs.Id;
|
||||
|
||||
if (_currentFilterType != type)
|
||||
{
|
||||
@@ -123,7 +120,7 @@ public sealed partial class GeneralStationRecordConsoleWindow : DefaultWindow
|
||||
RecordContainer.RemoveAllChildren();
|
||||
}
|
||||
}
|
||||
private void PopulateRecordListing(Dictionary<(NetEntity, uint), string> listing, (NetEntity, uint)? selected)
|
||||
private void PopulateRecordListing(Dictionary<uint, string> listing, uint? selected)
|
||||
{
|
||||
RecordListing.Clear();
|
||||
RecordListing.ClearSelected();
|
||||
@@ -134,10 +131,7 @@ public sealed partial class GeneralStationRecordConsoleWindow : DefaultWindow
|
||||
{
|
||||
var item = RecordListing.AddItem(name);
|
||||
item.Metadata = key;
|
||||
if (selected != null && key.Item1 == selected.Value.Item1 && key.Item2 == selected.Value.Item2)
|
||||
{
|
||||
item.Selected = true;
|
||||
}
|
||||
item.Selected = key == selected;
|
||||
}
|
||||
_isPopulating = false;
|
||||
|
||||
@@ -197,7 +191,7 @@ public sealed partial class GeneralStationRecordConsoleWindow : DefaultWindow
|
||||
}
|
||||
}
|
||||
|
||||
private string GetTypeFilterLocals(GeneralStationRecordFilterType type)
|
||||
private string GetTypeFilterLocals(StationRecordFilterType type)
|
||||
{
|
||||
return Loc.GetString($"general-station-record-{type.ToString().ToLower()}-filter");
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ public sealed class EntityStorageSystem : SharedEntityStorageSystem
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
SubscribeLocalEvent<EntityStorageComponent, EntityUnpausedEvent>(OnEntityUnpausedEvent);
|
||||
SubscribeLocalEvent<EntityStorageComponent, ComponentInit>(OnComponentInit);
|
||||
SubscribeLocalEvent<EntityStorageComponent, ComponentStartup>(OnComponentStartup);
|
||||
SubscribeLocalEvent<EntityStorageComponent, ActivateInWorldEvent>(OnInteract, after: new[] { typeof(LockSystem) });
|
||||
|
||||
@@ -48,6 +48,11 @@ public sealed class StoreBoundUserInterface : BoundUserInterface
|
||||
{
|
||||
SendMessage(new StoreRequestUpdateInterfaceMessage());
|
||||
};
|
||||
|
||||
_menu.OnRefundAttempt += (_) =>
|
||||
{
|
||||
SendMessage(new StoreRequestRefundMessage());
|
||||
};
|
||||
}
|
||||
protected override void UpdateState(BoundUserInterfaceState state)
|
||||
{
|
||||
@@ -64,6 +69,7 @@ public sealed class StoreBoundUserInterface : BoundUserInterface
|
||||
|
||||
_menu.UpdateListing(msg.Listings.ToList());
|
||||
_menu.SetFooterVisibility(msg.ShowFooter);
|
||||
_menu.UpdateRefund(msg.AllowRefund);
|
||||
break;
|
||||
case StoreInitializeState msg:
|
||||
_windowName = msg.Name;
|
||||
|
||||
@@ -22,6 +22,11 @@
|
||||
MinWidth="64"
|
||||
HorizontalAlignment="Right"
|
||||
Text="{Loc 'store-ui-default-withdraw-text'}" />
|
||||
<Button
|
||||
Name="RefundButton"
|
||||
MinWidth="64"
|
||||
HorizontalAlignment="Right"
|
||||
Text="Refund" />
|
||||
</BoxContainer>
|
||||
<PanelContainer VerticalExpand="True">
|
||||
<PanelContainer.PanelOverride>
|
||||
|
||||
@@ -31,6 +31,7 @@ public sealed partial class StoreMenu : DefaultWindow
|
||||
public event Action<BaseButton.ButtonEventArgs, string>? OnCategoryButtonPressed;
|
||||
public event Action<BaseButton.ButtonEventArgs, string, int>? OnWithdrawAttempt;
|
||||
public event Action<BaseButton.ButtonEventArgs>? OnRefreshButtonPressed;
|
||||
public event Action<BaseButton.ButtonEventArgs>? OnRefundAttempt;
|
||||
|
||||
public Dictionary<string, FixedPoint2> Balance = new();
|
||||
public string CurrentCategory = string.Empty;
|
||||
@@ -44,6 +45,8 @@ public sealed partial class StoreMenu : DefaultWindow
|
||||
|
||||
WithdrawButton.OnButtonDown += OnWithdrawButtonDown;
|
||||
RefreshButton.OnButtonDown += OnRefreshButtonDown;
|
||||
RefundButton.OnButtonDown += OnRefundButtonDown;
|
||||
|
||||
if (Window != null)
|
||||
Window.Title = name;
|
||||
}
|
||||
@@ -116,6 +119,11 @@ public sealed partial class StoreMenu : DefaultWindow
|
||||
_withdrawWindow.OnWithdrawAttempt += OnWithdrawAttempt;
|
||||
}
|
||||
|
||||
private void OnRefundButtonDown(BaseButton.ButtonEventArgs args)
|
||||
{
|
||||
OnRefundAttempt?.Invoke(args);
|
||||
}
|
||||
|
||||
private void AddListingGui(ListingData listing)
|
||||
{
|
||||
if (!listing.Categories.Contains(CurrentCategory))
|
||||
@@ -262,6 +270,11 @@ public sealed partial class StoreMenu : DefaultWindow
|
||||
_withdrawWindow?.Close();
|
||||
}
|
||||
|
||||
public void UpdateRefund(bool allowRefund)
|
||||
{
|
||||
RefundButton.Disabled = !allowRefund;
|
||||
}
|
||||
|
||||
private sealed class StoreCategoryButton : Button
|
||||
{
|
||||
public string? Id;
|
||||
|
||||
@@ -15,10 +15,12 @@ using Content.Client.UserInterface.Systems.Gameplay;
|
||||
using Content.Shared.Administration;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.Chat;
|
||||
using Content.Shared.Decals;
|
||||
using Content.Shared.Damage.ForceSay;
|
||||
using Content.Shared.Examine;
|
||||
using Content.Shared.Input;
|
||||
using Content.Shared.Radio;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Input;
|
||||
using Robust.Client.Player;
|
||||
@@ -27,9 +29,11 @@ using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controllers;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.GameObjects.Components.Localization;
|
||||
using Robust.Shared.Input.Binding;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Replays;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
@@ -42,9 +46,11 @@ public sealed class ChatUIController : UIController
|
||||
[Dependency] private readonly IChatManager _manager = default!;
|
||||
[Dependency] private readonly IConfigurationManager _config = default!;
|
||||
[Dependency] private readonly IEyeManager _eye = default!;
|
||||
[Dependency] private readonly IEntityManager _ent = default!;
|
||||
[Dependency] private readonly IInputManager _input = default!;
|
||||
[Dependency] private readonly IClientNetManager _net = default!;
|
||||
[Dependency] private readonly IPlayerManager _player = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
[Dependency] private readonly IStateManager _state = default!;
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
[Dependency] private readonly IReplayRecordingManager _replayRecording = default!;
|
||||
@@ -55,6 +61,11 @@ public sealed class ChatUIController : UIController
|
||||
[UISystemDependency] private readonly TypingIndicatorSystem? _typingIndicator = default;
|
||||
[UISystemDependency] private readonly ChatSystem? _chatSys = default;
|
||||
|
||||
[ValidatePrototypeId<ColorPalettePrototype>]
|
||||
private const string ChatNamePalette = "ChatNames";
|
||||
private string[] _chatNameColors = default!;
|
||||
private bool _chatNameColorsEnabled;
|
||||
|
||||
private ISawmill _sawmill = default!;
|
||||
|
||||
public static readonly Dictionary<char, ChatSelectChannel> PrefixToChannel = new()
|
||||
@@ -168,6 +179,8 @@ public sealed class ChatUIController : UIController
|
||||
_net.RegisterNetMessage<MsgChatMessage>(OnChatMessage);
|
||||
_net.RegisterNetMessage<MsgDeleteChatMessagesBy>(OnDeleteChatMessagesBy);
|
||||
SubscribeNetworkEvent<DamageForceSayEvent>(OnDamageForceSay);
|
||||
_cfg.OnValueChanged(CCVars.ChatEnableColorName, (value) => { _chatNameColorsEnabled = value; });
|
||||
_chatNameColorsEnabled = _cfg.GetCVar(CCVars.ChatEnableColorName);
|
||||
|
||||
_speechBubbleRoot = new LayoutContainer();
|
||||
|
||||
@@ -212,6 +225,13 @@ public sealed class ChatUIController : UIController
|
||||
var gameplayStateLoad = UIManager.GetUIController<GameplayStateLoadController>();
|
||||
gameplayStateLoad.OnScreenLoad += OnScreenLoad;
|
||||
gameplayStateLoad.OnScreenUnload += OnScreenUnload;
|
||||
|
||||
var nameColors = _prototypeManager.Index<ColorPalettePrototype>(ChatNamePalette).Colors.Values.ToArray();
|
||||
_chatNameColors = new string[nameColors.Length];
|
||||
for (var i = 0; i < nameColors.Length; i++)
|
||||
{
|
||||
_chatNameColors[i] = nameColors[i].ToHex();
|
||||
}
|
||||
}
|
||||
|
||||
public void OnScreenLoad()
|
||||
@@ -757,6 +777,14 @@ public sealed class ChatUIController : UIController
|
||||
|
||||
public void ProcessChatMessage(ChatMessage msg, bool speechBubble = true)
|
||||
{
|
||||
// color the name unless it's something like "the old man"
|
||||
if ((msg.Channel == ChatChannel.Local || msg.Channel == ChatChannel.Whisper) && _chatNameColorsEnabled)
|
||||
{
|
||||
var grammar = _ent.GetComponentOrNull<GrammarComponent>(_ent.GetEntity(msg.SenderEntity));
|
||||
if (grammar != null && grammar.ProperNoun == true)
|
||||
msg.WrappedMessage = SharedChatSystem.InjectTagInsideTag(msg, "Name", "color", GetNameColor(SharedChatSystem.GetStringInsideTag(msg, "Name")));
|
||||
}
|
||||
|
||||
// Log all incoming chat to repopulate when filter is un-toggled
|
||||
if (!msg.HideChat)
|
||||
{
|
||||
@@ -852,6 +880,17 @@ public sealed class ChatUIController : UIController
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the chat name color for a mob
|
||||
/// </summary>
|
||||
/// <param name="name">Name of the mob</param>
|
||||
/// <returns>Hex value of the color</returns>
|
||||
public string GetNameColor(string name)
|
||||
{
|
||||
var colorIdx = Math.Abs(name.GetHashCode() % _chatNameColors.Length);
|
||||
return _chatNameColors[colorIdx];
|
||||
}
|
||||
|
||||
private readonly record struct SpeechBubbleData(ChatMessage Message, SpeechBubble.SpeechType Type);
|
||||
|
||||
private sealed class SpeechBubbleQueueData
|
||||
|
||||
@@ -30,6 +30,7 @@ public sealed class SandboxUIController : UIController, IOnStateChanged<Gameplay
|
||||
[Dependency] private readonly IEyeManager _eye = default!;
|
||||
[Dependency] private readonly IInputManager _input = default!;
|
||||
[Dependency] private readonly ILightManager _light = default!;
|
||||
[Dependency] private readonly IClientAdminManager _admin = default!;
|
||||
|
||||
[UISystemDependency] private readonly DebugPhysicsSystem _debugPhysics = default!;
|
||||
[UISystemDependency] private readonly MarkerSystem _marker = default!;
|
||||
@@ -53,13 +54,28 @@ public sealed class SandboxUIController : UIController, IOnStateChanged<Gameplay
|
||||
CheckSandboxVisibility();
|
||||
|
||||
_input.SetInputCommand(ContentKeyFunctions.OpenEntitySpawnWindow,
|
||||
InputCmdHandler.FromDelegate(_ => EntitySpawningController.ToggleWindow()));
|
||||
InputCmdHandler.FromDelegate(_ =>
|
||||
{
|
||||
if (!_admin.CanAdminPlace())
|
||||
return;
|
||||
EntitySpawningController.ToggleWindow();
|
||||
}));
|
||||
_input.SetInputCommand(ContentKeyFunctions.OpenSandboxWindow,
|
||||
InputCmdHandler.FromDelegate(_ => ToggleWindow()));
|
||||
_input.SetInputCommand(ContentKeyFunctions.OpenTileSpawnWindow,
|
||||
InputCmdHandler.FromDelegate(_ => TileSpawningController.ToggleWindow()));
|
||||
InputCmdHandler.FromDelegate(_ =>
|
||||
{
|
||||
if (!_admin.CanAdminPlace())
|
||||
return;
|
||||
TileSpawningController.ToggleWindow();
|
||||
}));
|
||||
_input.SetInputCommand(ContentKeyFunctions.OpenDecalSpawnWindow,
|
||||
InputCmdHandler.FromDelegate(_ => DecalPlacerController.ToggleWindow()));
|
||||
InputCmdHandler.FromDelegate(_ =>
|
||||
{
|
||||
if (!_admin.CanAdminPlace())
|
||||
return;
|
||||
DecalPlacerController.ToggleWindow();
|
||||
}));
|
||||
|
||||
CommandBinds.Builder
|
||||
.Bind(ContentKeyFunctions.EditorCopyObject, new PointerInputCmdHandler(Copy))
|
||||
|
||||
@@ -2,6 +2,7 @@ using System.Numerics;
|
||||
using Content.Server.DeviceNetwork;
|
||||
using Content.Server.DeviceNetwork.Components;
|
||||
using Content.Server.DeviceNetwork.Systems;
|
||||
using Content.Shared.DeviceNetwork;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Map;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
|
||||
using Content.Server.DeviceNetwork;
|
||||
using Content.Server.DeviceNetwork.Components;
|
||||
using Content.Server.DeviceNetwork.Systems;
|
||||
using Content.Shared.DeviceNetwork;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Reflection;
|
||||
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
using Content.Shared.Hands.Components;
|
||||
using Content.Shared.Prototypes;
|
||||
using Content.Shared.Pulling.Components;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.IntegrationTests.Tests.Puller;
|
||||
|
||||
#nullable enable
|
||||
|
||||
[TestFixture]
|
||||
public sealed class PullerTest
|
||||
{
|
||||
/// <summary>
|
||||
/// Checks that needsHands on PullerComponent is not set on mobs that don't even have hands.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task PullerSanityTest()
|
||||
{
|
||||
await using var pair = await PoolManager.GetServerClient();
|
||||
var server = pair.Server;
|
||||
|
||||
var compFactory = server.ResolveDependency<IComponentFactory>();
|
||||
var protoManager = server.ResolveDependency<IPrototypeManager>();
|
||||
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
foreach (var proto in protoManager.EnumeratePrototypes<EntityPrototype>())
|
||||
{
|
||||
if (!proto.TryGetComponent(out SharedPullerComponent? puller))
|
||||
continue;
|
||||
|
||||
if (!puller.NeedsHands)
|
||||
continue;
|
||||
|
||||
Assert.That(proto.HasComponent<HandsComponent>(compFactory), $"Found puller {proto} with NeedsHand pulling but has no hands?");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
await pair.CleanReturnAsync();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
using Content.Server.Storage.EntitySystems;
|
||||
using Content.Shared.Damage;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.GameObjects;
|
||||
|
||||
namespace Content.IntegrationTests.Tests.Storage;
|
||||
|
||||
[TestFixture]
|
||||
public sealed class EntityStorageTests
|
||||
{
|
||||
[TestPrototypes]
|
||||
private const string Prototypes = @"
|
||||
- type: entity
|
||||
id: EntityStorageTest
|
||||
name: box
|
||||
components:
|
||||
- type: EntityStorage
|
||||
- type: Damageable
|
||||
damageContainer: Inorganic
|
||||
- type: Destructible
|
||||
thresholds:
|
||||
- trigger:
|
||||
!type:DamageTrigger
|
||||
damage: 10
|
||||
behaviors:
|
||||
- !type:DoActsBehavior
|
||||
acts: [ Destruction ]
|
||||
";
|
||||
|
||||
[Test]
|
||||
public async Task TestContainerDestruction()
|
||||
{
|
||||
await using var pair = await PoolManager.GetServerClient();
|
||||
var server = pair.Server;
|
||||
var map = await pair.CreateTestMap();
|
||||
|
||||
EntityUid box = default;
|
||||
EntityUid crowbar = default;
|
||||
await server.WaitPost(() => box = server.EntMan.SpawnEntity("EntityStorageTest", map.GridCoords));
|
||||
await server.WaitPost(() => crowbar = server.EntMan.SpawnEntity("Crowbar", map.GridCoords));
|
||||
|
||||
// Initially the crowbar is not in a contaienr.
|
||||
var sys = server.System<SharedContainerSystem>();
|
||||
Assert.That(sys.IsEntityInContainer(crowbar), Is.False);
|
||||
|
||||
// Open then close the storage entity
|
||||
var storage = server.System<EntityStorageSystem>();
|
||||
await server.WaitPost(() =>
|
||||
{
|
||||
storage.OpenStorage(box);
|
||||
storage.CloseStorage(box);
|
||||
});
|
||||
|
||||
// Crowbar is now in the box
|
||||
Assert.That(sys.IsEntityInContainer(crowbar));
|
||||
|
||||
// Damage the box
|
||||
var damage = new DamageSpecifier();
|
||||
damage.DamageDict.Add("Blunt", 100);
|
||||
await server.WaitPost(() => server.System<DamageableSystem>().TryChangeDamage(box, damage));
|
||||
|
||||
// Box has been destroyed, contents have been emptied. Destruction uses deffered deletion.
|
||||
Assert.That(server.EntMan.IsQueuedForDeletion(box));
|
||||
Assert.That(sys.IsEntityInContainer(crowbar), Is.False);
|
||||
|
||||
// Opening and closing the soon-to-be-deleted box should not re-insert the crowbar
|
||||
await server.WaitPost(() =>
|
||||
{
|
||||
storage.OpenStorage(box);
|
||||
storage.CloseStorage(box);
|
||||
});
|
||||
Assert.That(sys.IsEntityInContainer(crowbar), Is.False);
|
||||
|
||||
// Entity gets deleted after a few ticks
|
||||
await server.WaitRunTicks(5);
|
||||
Assert.That(server.EntMan.Deleted(box));
|
||||
Assert.That(server.EntMan.Deleted(crowbar), Is.False);
|
||||
|
||||
await pair.CleanReturnAsync();
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,8 @@ using Content.Shared.Interaction;
|
||||
using Content.Shared.StatusIcon;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Content.Shared.Roles;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace Content.Server.Access.Systems
|
||||
{
|
||||
@@ -98,6 +100,24 @@ namespace Content.Server.Access.Systems
|
||||
}
|
||||
|
||||
_cardSystem.TryChangeJobIcon(uid, jobIcon, idCard);
|
||||
|
||||
if (TryFindJobProtoFromIcon(jobIcon, out var job))
|
||||
_cardSystem.TryChangeJobDepartment(uid, job, idCard);
|
||||
}
|
||||
|
||||
private bool TryFindJobProtoFromIcon(StatusIconPrototype jobIcon, [NotNullWhen(true)] out JobPrototype? job)
|
||||
{
|
||||
foreach (var jobPrototype in _prototypeManager.EnumeratePrototypes<JobPrototype>())
|
||||
{
|
||||
if(jobPrototype.Icon == jobIcon.ID)
|
||||
{
|
||||
job = jobPrototype;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
job = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
using Content.Server.Station.Systems;
|
||||
using Content.Server.StationRecords.Systems;
|
||||
using Content.Shared.Access.Components;
|
||||
using Content.Shared.Access.Systems;
|
||||
@@ -21,7 +20,6 @@ public sealed class IdCardConsoleSystem : SharedIdCardConsoleSystem
|
||||
{
|
||||
[Dependency] private readonly IPrototypeManager _prototype = default!;
|
||||
[Dependency] private readonly StationRecordsSystem _record = default!;
|
||||
[Dependency] private readonly StationSystem _station = default!;
|
||||
[Dependency] private readonly UserInterfaceSystem _userInterface = default!;
|
||||
[Dependency] private readonly AccessReaderSystem _accessReader = default!;
|
||||
[Dependency] private readonly AccessSystem _access = default!;
|
||||
@@ -85,10 +83,9 @@ public sealed class IdCardConsoleSystem : SharedIdCardConsoleSystem
|
||||
var targetAccessComponent = EntityManager.GetComponent<AccessComponent>(targetId);
|
||||
|
||||
var jobProto = string.Empty;
|
||||
if (_station.GetOwningStation(uid) is { } station
|
||||
&& EntityManager.TryGetComponent<StationRecordKeyStorageComponent>(targetId, out var keyStorage)
|
||||
&& keyStorage.Key != null
|
||||
&& _record.TryGetRecord<GeneralStationRecord>(station, keyStorage.Key.Value, out var record))
|
||||
if (TryComp<StationRecordKeyStorageComponent>(targetId, out var keyStorage)
|
||||
&& keyStorage.Key is {} key
|
||||
&& _record.TryGetRecord<GeneralStationRecord>(key, out var record))
|
||||
{
|
||||
jobProto = record.JobPrototype;
|
||||
}
|
||||
@@ -103,7 +100,7 @@ public sealed class IdCardConsoleSystem : SharedIdCardConsoleSystem
|
||||
possibleAccess,
|
||||
jobProto,
|
||||
privilegedIdName,
|
||||
EntityManager.GetComponent<MetaDataComponent>(targetId).EntityName);
|
||||
Name(targetId));
|
||||
}
|
||||
|
||||
_userInterface.TrySetUiState(uid, IdCardConsoleUiKey.Key, newState);
|
||||
@@ -134,6 +131,7 @@ public sealed class IdCardConsoleSystem : SharedIdCardConsoleSystem
|
||||
&& _prototype.TryIndex<StatusIconPrototype>(job.Icon, out var jobIcon))
|
||||
{
|
||||
_idCard.TryChangeJobIcon(targetId, jobIcon, player: player);
|
||||
_idCard.TryChangeJobDepartment(targetId, job);
|
||||
}
|
||||
|
||||
if (!newAccessList.TrueForAll(x => component.AccessLevels.Contains(x)))
|
||||
@@ -184,7 +182,7 @@ public sealed class IdCardConsoleSystem : SharedIdCardConsoleSystem
|
||||
if (!Resolve(uid, ref component))
|
||||
return true;
|
||||
|
||||
if (!EntityManager.TryGetComponent<AccessReaderComponent>(uid, out var reader))
|
||||
if (!TryComp<AccessReaderComponent>(uid, out var reader))
|
||||
return true;
|
||||
|
||||
var privilegedId = component.PrivilegedIdSlot.Item;
|
||||
@@ -193,10 +191,9 @@ public sealed class IdCardConsoleSystem : SharedIdCardConsoleSystem
|
||||
|
||||
private void UpdateStationRecord(EntityUid uid, EntityUid targetId, string newFullName, string newJobTitle, JobPrototype? newJobProto)
|
||||
{
|
||||
if (_station.GetOwningStation(uid) is not { } station
|
||||
|| !EntityManager.TryGetComponent<StationRecordKeyStorageComponent>(targetId, out var keyStorage)
|
||||
if (!TryComp<StationRecordKeyStorageComponent>(targetId, out var keyStorage)
|
||||
|| keyStorage.Key is not { } key
|
||||
|| !_record.TryGetRecord<GeneralStationRecord>(station, key, out var record))
|
||||
|| !_record.TryGetRecord<GeneralStationRecord>(key, out var record))
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -210,6 +207,6 @@ public sealed class IdCardConsoleSystem : SharedIdCardConsoleSystem
|
||||
record.JobIcon = newJobProto.Icon;
|
||||
}
|
||||
|
||||
_record.Synchronize(station);
|
||||
_record.Synchronize(key);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,6 +149,7 @@ public sealed class IdCardSystem : SharedIdCardSystem
|
||||
if (!Resolve(uid, ref id))
|
||||
return false;
|
||||
|
||||
id.JobDepartments.Clear();
|
||||
foreach (var department in _prototypeManager.EnumeratePrototypes<DepartmentPrototype>())
|
||||
{
|
||||
if (department.Roles.Contains(job.ID))
|
||||
|
||||
@@ -11,8 +11,8 @@ public sealed class FollowCommand : IConsoleCommand
|
||||
[Dependency] private readonly IEntityManager _entManager = default!;
|
||||
|
||||
public string Command => "follow";
|
||||
public string Description => Loc.GetString("add-uplink-command-description");
|
||||
public string Help => Loc.GetString("add-uplink-command-help");
|
||||
public string Description => Loc.GetString("follow-command-description");
|
||||
public string Help => Loc.GetString("follow-command-help");
|
||||
|
||||
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
|
||||
@@ -359,7 +359,7 @@ namespace Content.Server.Administration.Systems
|
||||
if (TryComp(item, out PdaComponent? pda) &&
|
||||
TryComp(pda.ContainedId, out StationRecordKeyStorageComponent? keyStorage) &&
|
||||
keyStorage.Key is { } key &&
|
||||
_stationRecords.TryGetRecord(key.OriginStation, key, out GeneralStationRecord? record))
|
||||
_stationRecords.TryGetRecord(key, out GeneralStationRecord? record))
|
||||
{
|
||||
if (TryComp(entity, out DnaComponent? dna) &&
|
||||
dna.DNA != record.DNA)
|
||||
@@ -373,7 +373,7 @@ namespace Content.Server.Administration.Systems
|
||||
continue;
|
||||
}
|
||||
|
||||
_stationRecords.RemoveRecord(key.OriginStation, key);
|
||||
_stationRecords.RemoveRecord(key);
|
||||
Del(item);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ using Content.Shared.Atmos.Monitor;
|
||||
using Content.Shared.Atmos.Monitor.Components;
|
||||
using Content.Shared.Atmos.Piping.Unary.Components;
|
||||
using Content.Shared.DeviceLinking;
|
||||
using Content.Shared.DeviceNetwork;
|
||||
using Content.Shared.DeviceNetwork.Systems;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Wires;
|
||||
|
||||
@@ -6,6 +6,7 @@ using Content.Server.DeviceNetwork.Components;
|
||||
using Content.Server.DeviceNetwork.Systems;
|
||||
using Content.Server.Power.Components;
|
||||
using Content.Shared.Atmos.Monitor;
|
||||
using Content.Shared.DeviceNetwork;
|
||||
using Content.Shared.Tag;
|
||||
using Robust.Server.Audio;
|
||||
using Robust.Server.GameObjects;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using Content.Server.DeviceNetwork;
|
||||
using Content.Server.DeviceNetwork.Systems;
|
||||
using Content.Shared.Atmos.Monitor.Components;
|
||||
using Content.Shared.DeviceNetwork;
|
||||
|
||||
namespace Content.Server.Atmos.Monitor.Systems;
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ using Content.Server.Power.Components;
|
||||
using Content.Server.Power.EntitySystems;
|
||||
using Content.Shared.Atmos;
|
||||
using Content.Shared.Atmos.Monitor;
|
||||
using Content.Shared.DeviceNetwork;
|
||||
using Content.Shared.Tag;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ using Content.Shared.Atmos.Piping.Binary.Components;
|
||||
using Content.Shared.Atmos.Visuals;
|
||||
using Content.Shared.Audio;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.DeviceNetwork;
|
||||
using Content.Shared.Examine;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Popups;
|
||||
|
||||
@@ -17,6 +17,7 @@ using Content.Server.Power.EntitySystems;
|
||||
using Content.Shared.UserInterface;
|
||||
using Content.Shared.Administration.Logs;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.DeviceNetwork;
|
||||
using Content.Shared.Examine;
|
||||
|
||||
namespace Content.Server.Atmos.Piping.Unary.EntitySystems
|
||||
|
||||
@@ -16,6 +16,7 @@ using Content.Shared.Atmos.Monitor;
|
||||
using Content.Shared.Atmos.Piping.Unary.Components;
|
||||
using Content.Shared.Atmos.Visuals;
|
||||
using Content.Shared.Audio;
|
||||
using Content.Shared.DeviceNetwork;
|
||||
using Content.Shared.Examine;
|
||||
using Content.Shared.Tools.Systems;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
@@ -15,6 +15,7 @@ using Content.Shared.Atmos.Piping.Unary.Visuals;
|
||||
using Content.Shared.Atmos.Monitor;
|
||||
using Content.Shared.Atmos.Piping.Unary.Components;
|
||||
using Content.Shared.Audio;
|
||||
using Content.Shared.DeviceNetwork;
|
||||
using Content.Shared.Tools.Systems;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Server.GameObjects;
|
||||
|
||||
@@ -15,6 +15,7 @@ using Robust.Shared.Player;
|
||||
using Robust.Shared.Random;
|
||||
using Robust.Shared.Timing;
|
||||
using System.Numerics;
|
||||
using Content.Shared.Gibbing.Components;
|
||||
using Content.Shared.Movement.Systems;
|
||||
using Robust.Shared.Audio.Systems;
|
||||
|
||||
@@ -106,7 +107,17 @@ public sealed class BodySystem : SharedBodySystem
|
||||
_humanoidSystem.SetLayersVisibility(bodyUid, layers, false, true, humanoid);
|
||||
}
|
||||
|
||||
public override HashSet<EntityUid> GibBody(EntityUid bodyId, bool gibOrgans = false, BodyComponent? body = null, bool deleteItems = false, bool deleteBrain = false)
|
||||
public override HashSet<EntityUid> GibBody(
|
||||
EntityUid bodyId,
|
||||
bool gibOrgans = false,
|
||||
BodyComponent? body = null ,
|
||||
bool deleteItems = false,
|
||||
bool launchGibs = true,
|
||||
Vector2? splatDirection = null,
|
||||
float splatModifier = 1,
|
||||
Angle splatCone = default,
|
||||
SoundSpecifier? gibSoundOverride = null
|
||||
)
|
||||
{
|
||||
if (!Resolve(bodyId, ref body, false))
|
||||
return new HashSet<EntityUid>();
|
||||
@@ -118,28 +129,8 @@ public sealed class BodySystem : SharedBodySystem
|
||||
if (xform.MapUid == null)
|
||||
return new HashSet<EntityUid>();
|
||||
|
||||
var gibs = base.GibBody(bodyId, gibOrgans, body, deleteItems, deleteBrain);
|
||||
|
||||
var coordinates = xform.Coordinates;
|
||||
var filter = Filter.Pvs(bodyId, entityManager: EntityManager);
|
||||
var audio = AudioParams.Default.WithVariation(0.025f);
|
||||
|
||||
_audio.PlayStatic(body.GibSound, filter, coordinates, true, audio);
|
||||
|
||||
foreach (var entity in gibs)
|
||||
{
|
||||
if (deleteItems)
|
||||
{
|
||||
if (!HasComp<BrainComponent>(entity) || deleteBrain)
|
||||
{
|
||||
QueueDel(entity);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
SharedTransform.SetCoordinates(entity, coordinates.Offset(_random.NextVector2(.3f)));
|
||||
}
|
||||
}
|
||||
var gibs = base.GibBody(bodyId, gibOrgans, body, deleteItems, launchGibs: launchGibs,
|
||||
splatDirection: splatDirection, splatModifier: splatModifier, splatCone:splatCone);
|
||||
RaiseLocalEvent(bodyId, new BeingGibbedEvent(gibs));
|
||||
QueueDel(bodyId);
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ using Content.Shared.Body.Components;
|
||||
using Content.Shared.Body.Events;
|
||||
using Content.Shared.Mind;
|
||||
using Content.Shared.Mind.Components;
|
||||
using Content.Shared.Pointing;
|
||||
|
||||
namespace Content.Server.Body.Systems
|
||||
{
|
||||
@@ -17,6 +18,7 @@ namespace Content.Server.Body.Systems
|
||||
|
||||
SubscribeLocalEvent<BrainComponent, AddedToPartInBodyEvent>((uid, _, args) => HandleMind(args.Body, uid));
|
||||
SubscribeLocalEvent<BrainComponent, RemovedFromPartInBodyEvent>((uid, _, args) => HandleMind(uid, args.OldBody));
|
||||
SubscribeLocalEvent<BrainComponent, PointAttemptEvent>(OnPointAttempt);
|
||||
}
|
||||
|
||||
private void HandleMind(EntityUid newEntity, EntityUid oldEntity)
|
||||
@@ -36,5 +38,10 @@ namespace Content.Server.Body.Systems
|
||||
|
||||
_mindSystem.TransferTo(mindId, newEntity, mind: mind);
|
||||
}
|
||||
|
||||
private void OnPointAttempt(EntityUid uid, BrainComponent component, PointAttemptEvent args)
|
||||
{
|
||||
args.Cancel();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,7 +131,7 @@ public sealed class MutationSystem : EntitySystem
|
||||
|
||||
// Hybrids have a high chance of being seedless. Balances very
|
||||
// effective hybrid crossings.
|
||||
if (a.Name == result.Name && Random(0.7f))
|
||||
if (a.Name != result.Name && Random(0.7f))
|
||||
{
|
||||
result.Seedless = true;
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ using Content.Shared.ActionBlocker;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.Chat;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.Decals;
|
||||
using Content.Shared.Ghost;
|
||||
using Content.Shared.Humanoid;
|
||||
using Content.Shared.IdentityManagement;
|
||||
@@ -27,7 +26,6 @@ using Robust.Shared.Audio;
|
||||
using Robust.Shared.Audio.Systems;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Console;
|
||||
using Robust.Shared.GameObjects.Components.Localization;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Prototypes;
|
||||
@@ -71,10 +69,6 @@ public sealed partial class ChatSystem : SharedChatSystem
|
||||
private bool _critLoocEnabled;
|
||||
private readonly bool _adminLoocEnabled = true;
|
||||
|
||||
[ValidatePrototypeId<ColorPalettePrototype>]
|
||||
private const string _chatNamePalette = "Material";
|
||||
private string[] _chatNameColors = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
@@ -84,13 +78,6 @@ public sealed partial class ChatSystem : SharedChatSystem
|
||||
_configurationManager.OnValueChanged(CCVars.CritLoocEnabled, OnCritLoocEnabledChanged, true);
|
||||
|
||||
SubscribeLocalEvent<GameRunLevelChangedEvent>(OnGameChange);
|
||||
|
||||
var nameColors = _prototypeManager.Index<ColorPalettePrototype>(_chatNamePalette).Colors.Values.ToArray();
|
||||
_chatNameColors = new string[nameColors.Length];
|
||||
for (var i = 0; i < nameColors.Length; i++)
|
||||
{
|
||||
_chatNameColors[i] = nameColors[i].ToHex();
|
||||
}
|
||||
}
|
||||
|
||||
public override void Shutdown()
|
||||
@@ -426,13 +413,8 @@ public sealed partial class ChatSystem : SharedChatSystem
|
||||
|
||||
name = FormattedMessage.EscapeText(name);
|
||||
|
||||
// color the name unless it's something like "the old man"
|
||||
string coloredName = name;
|
||||
if (!TryComp<GrammarComponent>(source, out var grammar) || grammar.ProperNoun == true)
|
||||
coloredName = $"[color={GetNameColor(name)}]{name}[/color]";
|
||||
|
||||
var wrappedMessage = Loc.GetString(speech.Bold ? "chat-manager-entity-say-bold-wrap-message" : "chat-manager-entity-say-wrap-message",
|
||||
("entityName", coloredName),
|
||||
("entityName", name),
|
||||
("verb", Loc.GetString(_random.Pick(speech.SpeechVerbStrings))),
|
||||
("fontType", speech.FontId),
|
||||
("fontSize", speech.FontSize),
|
||||
@@ -501,10 +483,6 @@ public sealed partial class ChatSystem : SharedChatSystem
|
||||
}
|
||||
name = FormattedMessage.EscapeText(name);
|
||||
|
||||
// color the name unless it's something like "the old man"
|
||||
if (!TryComp<GrammarComponent>(source, out var grammar) || grammar.ProperNoun == true)
|
||||
name = $"[color={GetNameColor(name)}]{name}[/color]";
|
||||
|
||||
var wrappedMessage = Loc.GetString("chat-manager-entity-whisper-wrap-message",
|
||||
("entityName", name), ("message", FormattedMessage.EscapeText(message)));
|
||||
|
||||
@@ -645,17 +623,6 @@ public sealed partial class ChatSystem : SharedChatSystem
|
||||
|
||||
#region Utility
|
||||
|
||||
/// <summary>
|
||||
/// Returns the chat name color for a mob
|
||||
/// </summary>
|
||||
/// <param name="name">Name of the mob</param>
|
||||
/// <returns>Hex value of the color</returns>
|
||||
public string GetNameColor(string name)
|
||||
{
|
||||
var colorIdx = Math.Abs(name.GetHashCode() % _chatNameColors.Length);
|
||||
return _chatNameColors[colorIdx];
|
||||
}
|
||||
|
||||
private enum MessageRangeCheckResult
|
||||
{
|
||||
Disallowed,
|
||||
|
||||
@@ -19,6 +19,7 @@ using Content.Shared.CCVar;
|
||||
using Content.Shared.Chat;
|
||||
using Content.Shared.Communications;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.DeviceNetwork;
|
||||
using Content.Shared.Emag.Components;
|
||||
using Content.Shared.Popups;
|
||||
using Robust.Server.GameObjects;
|
||||
|
||||
@@ -0,0 +1,232 @@
|
||||
using Content.Server.Popups;
|
||||
using Content.Server.Radio.EntitySystems;
|
||||
using Content.Server.Station.Systems;
|
||||
using Content.Server.StationRecords;
|
||||
using Content.Server.StationRecords.Systems;
|
||||
using Content.Shared.Access.Systems;
|
||||
using Content.Shared.CriminalRecords;
|
||||
using Content.Shared.CriminalRecords.Components;
|
||||
using Content.Shared.CriminalRecords.Systems;
|
||||
using Content.Shared.Security;
|
||||
using Content.Shared.StationRecords;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.Player;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace Content.Server.CriminalRecords.Systems;
|
||||
|
||||
/// <summary>
|
||||
/// Handles all UI for criminal records console
|
||||
/// </summary>
|
||||
public sealed class CriminalRecordsConsoleSystem : SharedCriminalRecordsConsoleSystem
|
||||
{
|
||||
[Dependency] private readonly AccessReaderSystem _access = default!;
|
||||
[Dependency] private readonly CriminalRecordsSystem _criminalRecords = default!;
|
||||
[Dependency] private readonly PopupSystem _popup = default!;
|
||||
[Dependency] private readonly RadioSystem _radio = default!;
|
||||
[Dependency] private readonly SharedIdCardSystem _idCard = default!;
|
||||
[Dependency] private readonly StationRecordsSystem _stationRecords = default!;
|
||||
[Dependency] private readonly StationSystem _station = default!;
|
||||
[Dependency] private readonly UserInterfaceSystem _ui = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
SubscribeLocalEvent<CriminalRecordsConsoleComponent, RecordModifiedEvent>(UpdateUserInterface);
|
||||
SubscribeLocalEvent<CriminalRecordsConsoleComponent, AfterGeneralRecordCreatedEvent>(UpdateUserInterface);
|
||||
|
||||
Subs.BuiEvents<CriminalRecordsConsoleComponent>(CriminalRecordsConsoleKey.Key, subs =>
|
||||
{
|
||||
subs.Event<BoundUIOpenedEvent>(UpdateUserInterface);
|
||||
subs.Event<SelectStationRecord>(OnKeySelected);
|
||||
subs.Event<SetStationRecordFilter>(OnFiltersChanged);
|
||||
subs.Event<CriminalRecordChangeStatus>(OnChangeStatus);
|
||||
subs.Event<CriminalRecordAddHistory>(OnAddHistory);
|
||||
subs.Event<CriminalRecordDeleteHistory>(OnDeleteHistory);
|
||||
});
|
||||
}
|
||||
|
||||
private void UpdateUserInterface<T>(Entity<CriminalRecordsConsoleComponent> ent, ref T args)
|
||||
{
|
||||
// TODO: this is probably wasteful, maybe better to send a message to modify the exact state?
|
||||
UpdateUserInterface(ent);
|
||||
}
|
||||
|
||||
private void OnKeySelected(Entity<CriminalRecordsConsoleComponent> ent, ref SelectStationRecord msg)
|
||||
{
|
||||
// no concern of sus client since record retrieval will fail if invalid id is given
|
||||
ent.Comp.ActiveKey = msg.SelectedKey;
|
||||
UpdateUserInterface(ent);
|
||||
}
|
||||
|
||||
private void OnFiltersChanged(Entity<CriminalRecordsConsoleComponent> ent, ref SetStationRecordFilter msg)
|
||||
{
|
||||
if (ent.Comp.Filter == null ||
|
||||
ent.Comp.Filter.Type != msg.Type || ent.Comp.Filter.Value != msg.Value)
|
||||
{
|
||||
ent.Comp.Filter = new StationRecordsFilter(msg.Type, msg.Value);
|
||||
UpdateUserInterface(ent);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnChangeStatus(Entity<CriminalRecordsConsoleComponent> ent, ref CriminalRecordChangeStatus msg)
|
||||
{
|
||||
// prevent malf client violating wanted/reason nullability
|
||||
if ((msg.Status == SecurityStatus.Wanted) != (msg.Reason != null))
|
||||
return;
|
||||
|
||||
if (!CheckSelected(ent, msg.Session, out var mob, out var key))
|
||||
return;
|
||||
|
||||
if (!_stationRecords.TryGetRecord<CriminalRecord>(key.Value, out var record) || record.Status == msg.Status)
|
||||
return;
|
||||
|
||||
// validate the reason
|
||||
string? reason = null;
|
||||
if (msg.Reason != null)
|
||||
{
|
||||
reason = msg.Reason.Trim();
|
||||
if (reason.Length < 1 || reason.Length > ent.Comp.MaxStringLength)
|
||||
return;
|
||||
}
|
||||
|
||||
// when arresting someone add it to history automatically
|
||||
// fallback exists if the player was not set to wanted beforehand
|
||||
if (msg.Status == SecurityStatus.Detained)
|
||||
{
|
||||
var oldReason = record.Reason ?? Loc.GetString("criminal-records-console-unspecified-reason");
|
||||
var history = Loc.GetString("criminal-records-console-auto-history", ("reason", oldReason));
|
||||
_criminalRecords.TryAddHistory(key.Value, history);
|
||||
}
|
||||
|
||||
var oldStatus = record.Status;
|
||||
|
||||
// will probably never fail given the checks above
|
||||
_criminalRecords.TryChangeStatus(key.Value, msg.Status, msg.Reason);
|
||||
|
||||
var name = RecordName(key.Value);
|
||||
var officer = Loc.GetString("criminal-records-console-unknown-officer");
|
||||
if (_idCard.TryFindIdCard(mob.Value, out var id) && id.Comp.FullName is {} fullName)
|
||||
officer = fullName;
|
||||
|
||||
(string, object)[] args;
|
||||
if (reason != null)
|
||||
args = new (string, object)[] { ("name", name), ("officer", officer), ("reason", reason) };
|
||||
else
|
||||
args = new (string, object)[] { ("name", name), ("officer", officer) };
|
||||
|
||||
// figure out which radio message to send depending on transition
|
||||
var statusString = (oldStatus, msg.Status) switch
|
||||
{
|
||||
// going from wanted or detained on the spot
|
||||
(_, SecurityStatus.Detained) => "detained",
|
||||
// prisoner did their time
|
||||
(SecurityStatus.Detained, SecurityStatus.None) => "released",
|
||||
// going from wanted to none, must have been a mistake
|
||||
(_, SecurityStatus.None) => "not-wanted",
|
||||
// going from none or detained, AOS or prisonbreak / lazy secoff never set them to released and they reoffended
|
||||
(_, SecurityStatus.Wanted) => "wanted",
|
||||
// this is impossible
|
||||
_ => "not-wanted"
|
||||
};
|
||||
_radio.SendRadioMessage(ent, Loc.GetString($"criminal-records-console-{statusString}", args), ent.Comp.SecurityChannel, ent);
|
||||
|
||||
UpdateUserInterface(ent);
|
||||
}
|
||||
|
||||
private void OnAddHistory(Entity<CriminalRecordsConsoleComponent> ent, ref CriminalRecordAddHistory msg)
|
||||
{
|
||||
if (!CheckSelected(ent, msg.Session, out _, out var key))
|
||||
return;
|
||||
|
||||
var line = msg.Line.Trim();
|
||||
if (line.Length < 1 || line.Length > ent.Comp.MaxStringLength)
|
||||
return;
|
||||
|
||||
if (!_criminalRecords.TryAddHistory(key.Value, line))
|
||||
return;
|
||||
|
||||
// no radio message since its not crucial to officers patrolling
|
||||
|
||||
UpdateUserInterface(ent);
|
||||
}
|
||||
|
||||
private void OnDeleteHistory(Entity<CriminalRecordsConsoleComponent> ent, ref CriminalRecordDeleteHistory msg)
|
||||
{
|
||||
if (!CheckSelected(ent, msg.Session, out _, out var key))
|
||||
return;
|
||||
|
||||
if (!_criminalRecords.TryDeleteHistory(key.Value, msg.Index))
|
||||
return;
|
||||
|
||||
// a bit sus but not crucial to officers patrolling
|
||||
|
||||
UpdateUserInterface(ent);
|
||||
}
|
||||
|
||||
private void UpdateUserInterface(Entity<CriminalRecordsConsoleComponent> ent)
|
||||
{
|
||||
var (uid, console) = ent;
|
||||
var owningStation = _station.GetOwningStation(uid);
|
||||
|
||||
if (!TryComp<StationRecordsComponent>(owningStation, out var stationRecords))
|
||||
{
|
||||
_ui.TrySetUiState(uid, CriminalRecordsConsoleKey.Key, new CriminalRecordsConsoleState());
|
||||
return;
|
||||
}
|
||||
|
||||
var listing = _stationRecords.BuildListing((owningStation.Value, stationRecords), console.Filter);
|
||||
|
||||
var state = new CriminalRecordsConsoleState(listing, console.Filter);
|
||||
if (console.ActiveKey is {} id)
|
||||
{
|
||||
// get records to display when a crewmember is selected
|
||||
var key = new StationRecordKey(id, owningStation.Value);
|
||||
_stationRecords.TryGetRecord(key, out state.StationRecord, stationRecords);
|
||||
_stationRecords.TryGetRecord(key, out state.CriminalRecord, stationRecords);
|
||||
state.SelectedKey = id;
|
||||
}
|
||||
|
||||
_ui.TrySetUiState(uid, CriminalRecordsConsoleKey.Key, state);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Boilerplate that most actions use, if they require that a record be selected.
|
||||
/// Obviously shouldn't be used for selecting records.
|
||||
/// </summary>
|
||||
private bool CheckSelected(Entity<CriminalRecordsConsoleComponent> ent, ICommonSession session,
|
||||
[NotNullWhen(true)] out EntityUid? mob, [NotNullWhen(true)] out StationRecordKey? key)
|
||||
{
|
||||
key = null;
|
||||
mob = null;
|
||||
if (session.AttachedEntity is not {} user)
|
||||
return false;
|
||||
|
||||
if (!_access.IsAllowed(user, ent))
|
||||
{
|
||||
_popup.PopupEntity(Loc.GetString("criminal-records-permission-denied"), ent, session);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ent.Comp.ActiveKey is not {} id)
|
||||
return false;
|
||||
|
||||
// checking the console's station since the user might be off-grid using on-grid console
|
||||
if (_station.GetOwningStation(ent) is not {} station)
|
||||
return false;
|
||||
|
||||
key = new StationRecordKey(id, station);
|
||||
mob = user;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name from a record, or empty string if this somehow fails.
|
||||
/// </summary>
|
||||
private string RecordName(StationRecordKey key)
|
||||
{
|
||||
if (!_stationRecords.TryGetRecord<GeneralStationRecord>(key, out var record))
|
||||
return "";
|
||||
|
||||
return record.Name;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Content.Server.StationRecords.Systems;
|
||||
using Content.Shared.CriminalRecords;
|
||||
using Content.Shared.Security;
|
||||
using Content.Shared.StationRecords;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Server.CriminalRecords.Systems;
|
||||
|
||||
/// <summary>
|
||||
/// Criminal records
|
||||
///
|
||||
/// Criminal Records inherit Station Records' core and add role-playing tools for Security:
|
||||
/// - Ability to track a person's status (Detained/Wanted/None)
|
||||
/// - See security officers' actions in Criminal Records in the radio
|
||||
/// - See reasons for any action with no need to ask the officer personally
|
||||
/// </summary>
|
||||
public sealed class CriminalRecordsSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
[Dependency] private readonly StationRecordsSystem _stationRecords = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<AfterGeneralRecordCreatedEvent>(OnGeneralRecordCreated);
|
||||
}
|
||||
|
||||
private void OnGeneralRecordCreated(AfterGeneralRecordCreatedEvent ev)
|
||||
{
|
||||
_stationRecords.AddRecordEntry(ev.Key, new CriminalRecord());
|
||||
_stationRecords.Synchronize(ev.Key);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to change the status of the record found by the StationRecordKey.
|
||||
/// Reason should only be passed if status is Wanted.
|
||||
/// </summary>
|
||||
/// <returns>True if the status is changed, false if not</returns>
|
||||
public bool TryChangeStatus(StationRecordKey key, SecurityStatus status, string? reason)
|
||||
{
|
||||
// don't do anything if its the same status
|
||||
if (!_stationRecords.TryGetRecord<CriminalRecord>(key, out var record)
|
||||
|| status == record.Status)
|
||||
return false;
|
||||
|
||||
record.Status = status;
|
||||
record.Reason = reason;
|
||||
|
||||
_stationRecords.Synchronize(key);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to add a history entry to a criminal record.
|
||||
/// </summary>
|
||||
/// <returns>True if adding succeeded, false if not</returns>
|
||||
public bool TryAddHistory(StationRecordKey key, CrimeHistory entry)
|
||||
{
|
||||
if (!_stationRecords.TryGetRecord<CriminalRecord>(key, out var record))
|
||||
return false;
|
||||
|
||||
record.History.Add(entry);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates and tries to add a history entry using the current time.
|
||||
/// </summary>
|
||||
public bool TryAddHistory(StationRecordKey key, string line)
|
||||
{
|
||||
var entry = new CrimeHistory(_timing.CurTime, line);
|
||||
return TryAddHistory(key, entry);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to delete a sepcific line of history from a criminal record, by index.
|
||||
/// </summary>
|
||||
/// <returns>True if the line was removed, false if not</returns>
|
||||
public bool TryDeleteHistory(StationRecordKey key, uint index)
|
||||
{
|
||||
if (!_stationRecords.TryGetRecord<CriminalRecord>(key, out var record))
|
||||
return false;
|
||||
|
||||
if (index >= record.History.Count)
|
||||
return false;
|
||||
|
||||
record.History.RemoveAt((int) index);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
using Content.Shared.DeviceLinking;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
||||
|
||||
namespace Content.Server.DeviceLinking.Components
|
||||
{
|
||||
[RegisterComponent]
|
||||
public sealed partial class TwoWayLeverComponent : Component
|
||||
{
|
||||
[DataField("state")]
|
||||
public TwoWayLeverState State;
|
||||
|
||||
[DataField("nextSignalLeft")]
|
||||
public bool NextSignalLeft;
|
||||
|
||||
[DataField("leftPort", customTypeSerializer: typeof(PrototypeIdSerializer<SourcePortPrototype>))]
|
||||
public string LeftPort = "Left";
|
||||
|
||||
[DataField("rightPort", customTypeSerializer: typeof(PrototypeIdSerializer<SourcePortPrototype>))]
|
||||
public string RightPort = "Right";
|
||||
|
||||
[DataField("middlePort", customTypeSerializer: typeof(PrototypeIdSerializer<SourcePortPrototype>))]
|
||||
public string MiddlePort = "Middle";
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using Content.Server.DeviceNetwork;
|
||||
using Content.Shared.DeviceNetwork;
|
||||
|
||||
namespace Content.Server.DeviceLinking.Events;
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ using Content.Server.DeviceNetwork;
|
||||
using Content.Server.DeviceNetwork.Components;
|
||||
using Content.Server.DeviceNetwork.Systems;
|
||||
using Content.Shared.DeviceLinking;
|
||||
using Content.Shared.DeviceNetwork;
|
||||
|
||||
namespace Content.Server.DeviceLinking.Systems;
|
||||
|
||||
@@ -35,15 +36,7 @@ public sealed class DeviceLinkSystem : SharedDeviceLinkSystem
|
||||
}
|
||||
|
||||
#region Sending & Receiving
|
||||
/// <summary>
|
||||
/// Sends a network payload directed at the sink entity.
|
||||
/// Just raises a <see cref="SignalReceivedEvent"/> without data if the source or the sink doesn't have a <see cref="DeviceNetworkComponent"/>
|
||||
/// </summary>
|
||||
/// <param name="uid">The source uid that invokes the port</param>
|
||||
/// <param name="port">The port to invoke</param>
|
||||
/// <param name="data">Optional data to send along</param>
|
||||
/// <param name="sourceComponent"></param>
|
||||
public void InvokePort(EntityUid uid, string port, NetworkPayload? data = null, DeviceLinkSourceComponent? sourceComponent = null)
|
||||
public override void InvokePort(EntityUid uid, string port, NetworkPayload? data = null, DeviceLinkSourceComponent? sourceComponent = null)
|
||||
{
|
||||
if (!Resolve(uid, ref sourceComponent) || !sourceComponent.Outputs.TryGetValue(port, out var sinks))
|
||||
return;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using Content.Server.DeviceLinking.Components;
|
||||
using Content.Server.DeviceNetwork;
|
||||
using Content.Server.Doors.Systems;
|
||||
using Content.Shared.DeviceNetwork;
|
||||
using Content.Shared.Doors.Components;
|
||||
using Content.Shared.Doors;
|
||||
using JetBrains.Annotations;
|
||||
@@ -59,8 +60,7 @@ namespace Content.Server.DeviceLinking.Systems
|
||||
{
|
||||
if (state == SignalState.High || state == SignalState.Momentary)
|
||||
{
|
||||
if (door.State is DoorState.Closed or DoorState.Open)
|
||||
_doorSystem.TryToggleDoor(uid, door);
|
||||
_doorSystem.TryToggleDoor(uid, door);
|
||||
}
|
||||
}
|
||||
else if (args.Port == component.InBolt)
|
||||
|
||||
@@ -68,7 +68,7 @@ public sealed class SignalTimerSystem : EntitySystem
|
||||
|
||||
if (TryComp<AppearanceComponent>(uid, out var appearance))
|
||||
{
|
||||
_appearanceSystem.SetData(uid, TextScreenVisuals.ScreenText, new[] { signalTimer.Label }, appearance);
|
||||
_appearanceSystem.SetData(uid, TextScreenVisuals.ScreenText, signalTimer.Label, appearance);
|
||||
}
|
||||
|
||||
_audio.PlayPvs(signalTimer.DoneSound, uid);
|
||||
@@ -142,7 +142,7 @@ public sealed class SignalTimerSystem : EntitySystem
|
||||
component.Label = args.Text[..Math.Min(5, args.Text.Length)];
|
||||
|
||||
if (!HasComp<ActiveSignalTimerComponent>(uid))
|
||||
_appearanceSystem.SetData(uid, TextScreenVisuals.ScreenText, new string?[] { component.Label });
|
||||
_appearanceSystem.SetData(uid, TextScreenVisuals.ScreenText, component.Label);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -186,7 +186,7 @@ public sealed class SignalTimerSystem : EntitySystem
|
||||
if (appearance != null)
|
||||
{
|
||||
_appearanceSystem.SetData(uid, TextScreenVisuals.TargetTime, timer.TriggerTime, appearance);
|
||||
_appearanceSystem.SetData(uid, TextScreenVisuals.ScreenText, new string?[] { }, appearance);
|
||||
_appearanceSystem.SetData(uid, TextScreenVisuals.ScreenText, string.Empty, appearance);
|
||||
}
|
||||
|
||||
_signalSystem.InvokePort(uid, component.StartPort);
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
using Robust.Shared.Utility;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace Content.Server.DeviceNetwork
|
||||
{
|
||||
public sealed class NetworkPayload : Dictionary<string, object?>
|
||||
{
|
||||
/// <summary>
|
||||
/// Tries to get a value from the payload and checks if that value is of type T.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type that should be casted to</typeparam>
|
||||
/// <returns>Whether the value was present in the payload and of the required type</returns>
|
||||
public bool TryGetValue<T>(string key, [NotNullWhen(true)] out T? value)
|
||||
{
|
||||
if (this.TryCastValue(key, out T? result))
|
||||
{
|
||||
value = result;
|
||||
return true;
|
||||
}
|
||||
|
||||
value = default;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
using Content.Server.DeviceNetwork.Components;
|
||||
using Content.Server.DeviceNetwork.Components.Devices;
|
||||
using Content.Shared.DeviceNetwork;
|
||||
using Content.Shared.Interaction;
|
||||
|
||||
namespace Content.Server.DeviceNetwork.Systems.Devices
|
||||
|
||||
@@ -4,6 +4,7 @@ using Content.Server.DeviceNetwork.Components;
|
||||
using Content.Server.DeviceNetwork.Systems;
|
||||
using Content.Server.Disposal.Unit.EntitySystems;
|
||||
using Content.Server.Power.Components;
|
||||
using Content.Shared.DeviceNetwork;
|
||||
using Content.Shared.Disposal;
|
||||
using Content.Shared.Interaction;
|
||||
using Robust.Server.GameObjects;
|
||||
|
||||
@@ -36,8 +36,6 @@ namespace Content.Server.Disposal.Unit.EntitySystems
|
||||
private EntityQuery<PhysicsComponent> _physicsQuery;
|
||||
private EntityQuery<TransformComponent> _xformQuery;
|
||||
|
||||
private List<EntityUid> _entList = new();
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
@@ -119,15 +117,17 @@ namespace Content.Server.Disposal.Unit.EntitySystems
|
||||
}
|
||||
}
|
||||
|
||||
_entList.Clear();
|
||||
_entList.AddRange(holder.Container.ContainedEntities);
|
||||
|
||||
foreach (var entity in _entList)
|
||||
// We're purposely iterating over all the holder's children
|
||||
// because the holder might have something teleported into it,
|
||||
// outside the usual container insertion logic.
|
||||
var children = holderTransform.ChildEnumerator;
|
||||
while (children.MoveNext(out var entity))
|
||||
{
|
||||
RemComp<BeingDisposedComponent>(entity);
|
||||
|
||||
var meta = _metaQuery.GetComponent(entity);
|
||||
_containerSystem.Remove((entity, null, meta), holder.Container, reparent: false, force: true);
|
||||
if (holder.Container.Contains(entity))
|
||||
_containerSystem.Remove((entity, null, meta), holder.Container, reparent: false, force: true);
|
||||
|
||||
var xform = _xformQuery.GetComponent(entity);
|
||||
if (xform.ParentUid != uid)
|
||||
|
||||
@@ -136,13 +136,13 @@ public sealed class DoorSystem : SharedDoorSystem
|
||||
if (!door.BumpOpen)
|
||||
return;
|
||||
|
||||
if (door.State != DoorState.Closed)
|
||||
if (door.State is not (DoorState.Closed or DoorState.Denying))
|
||||
return;
|
||||
|
||||
var otherUid = args.OtherEntity;
|
||||
|
||||
if (Tags.HasTag(otherUid, "DoorBumpOpener"))
|
||||
TryOpen(uid, door, otherUid);
|
||||
TryOpen(uid, door, otherUid, quiet: door.State == DoorState.Denying);
|
||||
}
|
||||
private void OnEmagged(EntityUid uid, DoorComponent door, ref GotEmaggedEvent args)
|
||||
{
|
||||
|
||||
@@ -62,9 +62,7 @@ public sealed class ElectrocutionSystem : SharedElectrocutionSystem
|
||||
[ValidatePrototypeId<DamageTypePrototype>]
|
||||
private const string DamageType = "Shock";
|
||||
|
||||
// Yes, this is absurdly small for a reason.
|
||||
private const float ElectrifiedScalePerWatt = 1E-6f;
|
||||
|
||||
// Multiply and shift the log scale for shock damage.
|
||||
private const float RecursiveDamageMultiplier = 0.75f;
|
||||
private const float RecursiveTimeMultiplier = 0.8f;
|
||||
|
||||
@@ -214,6 +212,16 @@ public sealed class ElectrocutionSystem : SharedElectrocutionSystem
|
||||
TryDoElectrifiedAct(uid, args.User, siemens, electrified);
|
||||
}
|
||||
|
||||
private float CalculateElectrifiedDamageScale(float power)
|
||||
{
|
||||
// A logarithm allows a curve of damage that grows quickly, but slows down dramatically past a value. This keeps the damage to a reasonable range.
|
||||
const float DamageShift = 1.67f; // Shifts the curve for an overall higher or lower damage baseline
|
||||
const float CeilingCoefficent = 1.35f; // Adjusts the approach to maximum damage, higher = Higher top damage
|
||||
const float LogGrowth = 0.00001f; // Adjusts the growth speed of the curve
|
||||
|
||||
return DamageShift + MathF.Log(power * LogGrowth) * CeilingCoefficent;
|
||||
}
|
||||
|
||||
public bool TryDoElectrifiedAct(EntityUid uid, EntityUid targetUid,
|
||||
float siemens = 1,
|
||||
ElectrifiedComponent? electrified = null,
|
||||
@@ -264,7 +272,9 @@ public sealed class ElectrocutionSystem : SharedElectrocutionSystem
|
||||
return false;
|
||||
|
||||
// Initial damage scales off of the available supply on the principle that the victim has shorted the entire powernet through their body.
|
||||
var damageScale = supp * ElectrifiedScalePerWatt;
|
||||
var damageScale = CalculateElectrifiedDamageScale(supp);
|
||||
if (damageScale <= 0f)
|
||||
return false;
|
||||
|
||||
{
|
||||
var lastRet = true;
|
||||
@@ -275,7 +285,7 @@ public sealed class ElectrocutionSystem : SharedElectrocutionSystem
|
||||
entity,
|
||||
uid,
|
||||
node,
|
||||
(int) (electrified.ShockDamage * damageScale * MathF.Pow(RecursiveDamageMultiplier, depth)),
|
||||
(int) MathF.Ceiling(electrified.ShockDamage * damageScale * MathF.Pow(RecursiveDamageMultiplier, depth)),
|
||||
TimeSpan.FromSeconds(electrified.ShockTime * MathF.Min(1f + MathF.Log2(1f + damageScale), 3f) * MathF.Pow(RecursiveTimeMultiplier, depth)),
|
||||
true,
|
||||
electrified.SiemensCoefficient);
|
||||
|
||||
@@ -12,6 +12,7 @@ using Content.Shared.UserInterface;
|
||||
using Content.Shared.Administration.Logs;
|
||||
using Content.Shared.Containers.ItemSlots;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.DeviceNetwork;
|
||||
using Content.Shared.Emag.Components;
|
||||
using Content.Shared.Emag.Systems;
|
||||
using Content.Shared.Fax;
|
||||
|
||||
@@ -13,10 +13,7 @@ public sealed partial class PuddleSystem
|
||||
[ValidatePrototypeId<ReagentPrototype>]
|
||||
private const string Water = "Water";
|
||||
|
||||
[ValidatePrototypeId<ReagentPrototype>]
|
||||
private const string SoapyWater = "SoapyWater";
|
||||
|
||||
public static string[] EvaporationReagents = new[] { Water, SoapyWater };
|
||||
public static string[] EvaporationReagents = new[] { Water };
|
||||
|
||||
private void OnEvaporationMapInit(Entity<EvaporationComponent> entity, ref MapInitEvent args)
|
||||
{
|
||||
|
||||
@@ -527,9 +527,6 @@ namespace Content.Server.GameTicking
|
||||
{
|
||||
_playerGameStatuses[session.UserId] = LobbyEnabled ? PlayerGameStatus.NotReadyToPlay : PlayerGameStatus.ReadyToPlay;
|
||||
}
|
||||
|
||||
// Put a bangin' donk on it.
|
||||
_audio.PlayGlobal(_audio.GetSound(new SoundCollectionSpecifier("RoundEnd")), Filter.Broadcast(), true);
|
||||
}
|
||||
|
||||
public bool DelayStart(TimeSpan time)
|
||||
|
||||
@@ -77,13 +77,16 @@ public sealed class GlueSystem : SharedGlueSystem
|
||||
{
|
||||
base.Update(frameTime);
|
||||
|
||||
var query = EntityQueryEnumerator<GluedComponent, UnremoveableComponent>();
|
||||
while (query.MoveNext(out var uid, out var glue, out _))
|
||||
var query = EntityQueryEnumerator<GluedComponent, UnremoveableComponent, MetaDataComponent>();
|
||||
while (query.MoveNext(out var uid, out var glue, out var _, out var meta))
|
||||
{
|
||||
if (_timing.CurTime < glue.Until)
|
||||
continue;
|
||||
|
||||
_metaData.SetEntityName(uid, glue.BeforeGluedEntityName);
|
||||
// Instead of string matching, just reconstruct the expected name and compare
|
||||
if (meta.EntityName == Loc.GetString("glued-name-prefix", ("target", glue.BeforeGluedEntityName)))
|
||||
_metaData.SetEntityName(uid, glue.BeforeGluedEntityName);
|
||||
|
||||
RemComp<UnremoveableComponent>(uid);
|
||||
RemComp<GluedComponent>(uid);
|
||||
}
|
||||
|
||||
@@ -131,6 +131,7 @@ public sealed class SubdermalImplantSystem : SharedSubdermalImplantSystem
|
||||
break;
|
||||
}
|
||||
_xform.SetWorldPosition(ent, targetCoords.Position);
|
||||
_xform.AttachToGridOrMap(ent, xform);
|
||||
_audio.PlayPvs(implant.TeleportSound, ent);
|
||||
|
||||
args.Handled = true;
|
||||
|
||||
@@ -1,32 +1,54 @@
|
||||
using Content.Server.UserInterface;
|
||||
using Content.Shared.MedicalScanner;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
|
||||
|
||||
namespace Content.Server.Medical.Components
|
||||
namespace Content.Server.Medical.Components;
|
||||
|
||||
/// <summary>
|
||||
/// After scanning, retrieves the target Uid to use with its related UI.
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
[Access(typeof(HealthAnalyzerSystem))]
|
||||
public sealed partial class HealthAnalyzerComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// After scanning, retrieves the target Uid to use with its related UI.
|
||||
/// When should the next update be sent for the patient
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public sealed partial class HealthAnalyzerComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// How long it takes to scan someone.
|
||||
/// </summary>
|
||||
[DataField("scanDelay")]
|
||||
public float ScanDelay = 0.8f;
|
||||
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
|
||||
public TimeSpan NextUpdate = TimeSpan.Zero;
|
||||
|
||||
/// <summary>
|
||||
/// Sound played on scanning begin
|
||||
/// </summary>
|
||||
[DataField("scanningBeginSound")]
|
||||
public SoundSpecifier? ScanningBeginSound;
|
||||
/// <summary>
|
||||
/// The delay between patient health updates
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public TimeSpan UpdateInterval = TimeSpan.FromSeconds(1);
|
||||
|
||||
/// <summary>
|
||||
/// Sound played on scanning end
|
||||
/// </summary>
|
||||
[DataField("scanningEndSound")]
|
||||
public SoundSpecifier? ScanningEndSound;
|
||||
}
|
||||
/// <summary>
|
||||
/// How long it takes to scan someone.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public TimeSpan ScanDelay = TimeSpan.FromSeconds(0.8);
|
||||
|
||||
/// <summary>
|
||||
/// Which entity has been scanned, for continuous updates
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public EntityUid? ScannedEntity;
|
||||
|
||||
/// <summary>
|
||||
/// The maximum range in tiles at which the analyzer can receive continuous updates
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public float MaxScanRange = 2.5f;
|
||||
|
||||
/// <summary>
|
||||
/// Sound played on scanning begin
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public SoundSpecifier? ScanningBeginSound;
|
||||
|
||||
/// <summary>
|
||||
/// Sound played on scanning end
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public SoundSpecifier? ScanningEndSound;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ using Content.Server.DeviceNetwork.Systems;
|
||||
using Content.Server.Medical.SuitSensors;
|
||||
using Content.Server.Power.Components;
|
||||
using Content.Server.Station.Systems;
|
||||
using Content.Shared.DeviceNetwork;
|
||||
using Content.Shared.Medical.SuitSensor;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
|
||||
@@ -195,7 +195,8 @@ public sealed partial class CryoPodSystem : SharedCryoPodSystem
|
||||
(bloodstream != null && _solutionContainerSystem.ResolveSolution(entity.Comp.BodyContainer.ContainedEntity.Value,
|
||||
bloodstream.BloodSolutionName, ref bloodstream.BloodSolution, out var bloodSolution))
|
||||
? bloodSolution.FillFraction
|
||||
: 0
|
||||
: 0,
|
||||
null
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
@@ -6,94 +6,195 @@ using Content.Server.Temperature.Components;
|
||||
using Content.Shared.Damage;
|
||||
using Content.Shared.DoAfter;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Interaction.Events;
|
||||
using Content.Shared.MedicalScanner;
|
||||
using Content.Shared.Mobs.Components;
|
||||
using Content.Shared.PowerCell;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.Audio.Systems;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Server.Medical
|
||||
namespace Content.Server.Medical;
|
||||
|
||||
public sealed class HealthAnalyzerSystem : EntitySystem
|
||||
{
|
||||
public sealed class HealthAnalyzerSystem : EntitySystem
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
[Dependency] private readonly PowerCellSystem _cell = default!;
|
||||
[Dependency] private readonly SharedAudioSystem _audio = default!;
|
||||
[Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!;
|
||||
[Dependency] private readonly SolutionContainerSystem _solutionContainerSystem = default!;
|
||||
[Dependency] private readonly UserInterfaceSystem _uiSystem = default!;
|
||||
[Dependency] private readonly TransformSystem _transformSystem = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
[Dependency] private readonly PowerCellSystem _cell = default!;
|
||||
[Dependency] private readonly SharedAudioSystem _audio = default!;
|
||||
[Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!;
|
||||
[Dependency] private readonly SolutionContainerSystem _solutionContainerSystem = default!;
|
||||
[Dependency] private readonly UserInterfaceSystem _uiSystem = default!;
|
||||
SubscribeLocalEvent<HealthAnalyzerComponent, EntityUnpausedEvent>(OnEntityUnpaused);
|
||||
SubscribeLocalEvent<HealthAnalyzerComponent, AfterInteractEvent>(OnAfterInteract);
|
||||
SubscribeLocalEvent<HealthAnalyzerComponent, HealthAnalyzerDoAfterEvent>(OnDoAfter);
|
||||
SubscribeLocalEvent<HealthAnalyzerComponent, EntGotInsertedIntoContainerMessage>(OnInsertedIntoContainer);
|
||||
SubscribeLocalEvent<HealthAnalyzerComponent, PowerCellSlotEmptyEvent>(OnPowerCellSlotEmpty);
|
||||
SubscribeLocalEvent<HealthAnalyzerComponent, DroppedEvent>(OnDropped);
|
||||
}
|
||||
|
||||
public override void Initialize()
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
var analyzerQuery = EntityQueryEnumerator<HealthAnalyzerComponent, TransformComponent>();
|
||||
while (analyzerQuery.MoveNext(out var uid, out var component, out var transform))
|
||||
{
|
||||
base.Initialize();
|
||||
SubscribeLocalEvent<HealthAnalyzerComponent, AfterInteractEvent>(OnAfterInteract);
|
||||
SubscribeLocalEvent<HealthAnalyzerComponent, HealthAnalyzerDoAfterEvent>(OnDoAfter);
|
||||
}
|
||||
//Update rate limited to 1 second
|
||||
if (component.NextUpdate > _timing.CurTime)
|
||||
continue;
|
||||
|
||||
private void OnAfterInteract(Entity<HealthAnalyzerComponent> entity, ref AfterInteractEvent args)
|
||||
{
|
||||
if (args.Target == null || !args.CanReach || !HasComp<MobStateComponent>(args.Target) || !_cell.HasActivatableCharge(entity.Owner, user: args.User))
|
||||
return;
|
||||
if (component.ScannedEntity is not {} patient)
|
||||
continue;
|
||||
|
||||
_audio.PlayPvs(entity.Comp.ScanningBeginSound, entity);
|
||||
component.NextUpdate = _timing.CurTime + component.UpdateInterval;
|
||||
|
||||
_doAfterSystem.TryStartDoAfter(new DoAfterArgs(EntityManager, args.User, TimeSpan.FromSeconds(entity.Comp.ScanDelay), new HealthAnalyzerDoAfterEvent(), entity.Owner, target: args.Target, used: entity.Owner)
|
||||
//Get distance between health analyzer and the scanned entity
|
||||
var patientCoordinates = Transform(patient).Coordinates;
|
||||
if (!patientCoordinates.InRange(EntityManager, _transformSystem, transform.Coordinates, component.MaxScanRange))
|
||||
{
|
||||
BreakOnTargetMove = true,
|
||||
BreakOnUserMove = true,
|
||||
NeedHand = true
|
||||
});
|
||||
}
|
||||
//Range too far, disable updates
|
||||
StopAnalyzingEntity((uid, component), patient);
|
||||
continue;
|
||||
}
|
||||
|
||||
private void OnDoAfter(Entity<HealthAnalyzerComponent> entity, ref HealthAnalyzerDoAfterEvent args)
|
||||
{
|
||||
if (args.Handled || args.Cancelled || args.Target == null || !_cell.TryUseActivatableCharge(entity.Owner, user: args.User))
|
||||
return;
|
||||
|
||||
_audio.PlayPvs(entity.Comp.ScanningEndSound, args.User);
|
||||
|
||||
UpdateScannedUser(entity, args.User, args.Target.Value, entity.Comp);
|
||||
args.Handled = true;
|
||||
}
|
||||
|
||||
private void OpenUserInterface(EntityUid user, EntityUid analyzer)
|
||||
{
|
||||
if (!TryComp<ActorComponent>(user, out var actor) || !_uiSystem.TryGetUi(analyzer, HealthAnalyzerUiKey.Key, out var ui))
|
||||
return;
|
||||
|
||||
_uiSystem.OpenUi(ui ,actor.PlayerSession);
|
||||
}
|
||||
|
||||
public void UpdateScannedUser(EntityUid uid, EntityUid user, EntityUid? target, HealthAnalyzerComponent? healthAnalyzer)
|
||||
{
|
||||
if (!Resolve(uid, ref healthAnalyzer))
|
||||
return;
|
||||
|
||||
if (target == null || !_uiSystem.TryGetUi(uid, HealthAnalyzerUiKey.Key, out var ui))
|
||||
return;
|
||||
|
||||
if (!HasComp<DamageableComponent>(target))
|
||||
return;
|
||||
|
||||
float bodyTemperature;
|
||||
if (TryComp<TemperatureComponent>(target, out var temp))
|
||||
bodyTemperature = temp.CurrentTemperature;
|
||||
else
|
||||
bodyTemperature = float.NaN;
|
||||
|
||||
float bloodAmount;
|
||||
if (TryComp<BloodstreamComponent>(target, out var bloodstream) &&
|
||||
_solutionContainerSystem.ResolveSolution(target.Value, bloodstream.BloodSolutionName, ref bloodstream.BloodSolution, out var bloodSolution))
|
||||
bloodAmount = bloodSolution.FillFraction;
|
||||
else
|
||||
bloodAmount = float.NaN;
|
||||
|
||||
OpenUserInterface(user, uid);
|
||||
|
||||
_uiSystem.SendUiMessage(ui, new HealthAnalyzerScannedUserMessage(
|
||||
GetNetEntity(target),
|
||||
bodyTemperature,
|
||||
bloodAmount
|
||||
));
|
||||
UpdateScannedUser(uid, patient, true);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnEntityUnpaused(Entity<HealthAnalyzerComponent> ent, ref EntityUnpausedEvent args)
|
||||
{
|
||||
ent.Comp.NextUpdate += args.PausedTime;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Trigger the doafter for scanning
|
||||
/// </summary>
|
||||
private void OnAfterInteract(Entity<HealthAnalyzerComponent> uid, ref AfterInteractEvent args)
|
||||
{
|
||||
if (args.Target == null || !args.CanReach || !HasComp<MobStateComponent>(args.Target) || !_cell.HasDrawCharge(uid, user: args.User))
|
||||
return;
|
||||
|
||||
_audio.PlayPvs(uid.Comp.ScanningBeginSound, uid);
|
||||
|
||||
_doAfterSystem.TryStartDoAfter(new DoAfterArgs(EntityManager, args.User, uid.Comp.ScanDelay, new HealthAnalyzerDoAfterEvent(), uid, target: args.Target, used: uid)
|
||||
{
|
||||
BreakOnTargetMove = true,
|
||||
BreakOnUserMove = true,
|
||||
NeedHand = true
|
||||
});
|
||||
}
|
||||
|
||||
private void OnDoAfter(Entity<HealthAnalyzerComponent> uid, ref HealthAnalyzerDoAfterEvent args)
|
||||
{
|
||||
if (args.Handled || args.Cancelled || args.Target == null || !_cell.HasDrawCharge(uid, user: args.User))
|
||||
return;
|
||||
|
||||
_audio.PlayPvs(uid.Comp.ScanningEndSound, uid);
|
||||
|
||||
OpenUserInterface(args.User, uid);
|
||||
BeginAnalyzingEntity(uid, args.Target.Value);
|
||||
args.Handled = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Turn off when placed into a storage item or moved between slots/hands
|
||||
/// </summary>
|
||||
private void OnInsertedIntoContainer(Entity<HealthAnalyzerComponent> uid, ref EntGotInsertedIntoContainerMessage args)
|
||||
{
|
||||
if (uid.Comp.ScannedEntity is { } patient)
|
||||
StopAnalyzingEntity(uid, patient);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disable continuous updates once battery is dead
|
||||
/// </summary>
|
||||
private void OnPowerCellSlotEmpty(Entity<HealthAnalyzerComponent> uid, ref PowerCellSlotEmptyEvent args)
|
||||
{
|
||||
if (uid.Comp.ScannedEntity is { } patient)
|
||||
StopAnalyzingEntity(uid, patient);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Turn off the analyser when dropped
|
||||
/// </summary>
|
||||
private void OnDropped(Entity<HealthAnalyzerComponent> uid, ref DroppedEvent args)
|
||||
{
|
||||
if (uid.Comp.ScannedEntity is { } patient)
|
||||
StopAnalyzingEntity(uid, patient);
|
||||
}
|
||||
|
||||
private void OpenUserInterface(EntityUid user, EntityUid analyzer)
|
||||
{
|
||||
if (!TryComp<ActorComponent>(user, out var actor) || !_uiSystem.TryGetUi(analyzer, HealthAnalyzerUiKey.Key, out var ui))
|
||||
return;
|
||||
|
||||
_uiSystem.OpenUi(ui, actor.PlayerSession);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Mark the entity as having its health analyzed, and link the analyzer to it
|
||||
/// </summary>
|
||||
/// <param name="healthAnalyzer">The health analyzer that should receive the updates</param>
|
||||
/// <param name="target">The entity to start analyzing</param>
|
||||
private void BeginAnalyzingEntity(Entity<HealthAnalyzerComponent> healthAnalyzer, EntityUid target)
|
||||
{
|
||||
//Link the health analyzer to the scanned entity
|
||||
healthAnalyzer.Comp.ScannedEntity = target;
|
||||
|
||||
_cell.SetPowerCellDrawEnabled(healthAnalyzer, true);
|
||||
|
||||
UpdateScannedUser(healthAnalyzer, target, true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remove the analyzer from the active list, and remove the component if it has no active analyzers
|
||||
/// </summary>
|
||||
/// <param name="healthAnalyzer">The health analyzer that's receiving the updates</param>
|
||||
/// <param name="target">The entity to analyze</param>
|
||||
private void StopAnalyzingEntity(Entity<HealthAnalyzerComponent> healthAnalyzer, EntityUid target)
|
||||
{
|
||||
//Unlink the analyzer
|
||||
healthAnalyzer.Comp.ScannedEntity = null;
|
||||
|
||||
_cell.SetPowerCellDrawEnabled(target, false);
|
||||
|
||||
UpdateScannedUser(healthAnalyzer, target, false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Send an update for the target to the healthAnalyzer
|
||||
/// </summary>
|
||||
/// <param name="healthAnalyzer">The health analyzer</param>
|
||||
/// <param name="target">The entity being scanned</param>
|
||||
/// <param name="scanMode">True makes the UI show ACTIVE, False makes the UI show INACTIVE</param>
|
||||
public void UpdateScannedUser(EntityUid healthAnalyzer, EntityUid target, bool scanMode)
|
||||
{
|
||||
if (!_uiSystem.TryGetUi(healthAnalyzer, HealthAnalyzerUiKey.Key, out var ui))
|
||||
return;
|
||||
|
||||
if (!HasComp<DamageableComponent>(target))
|
||||
return;
|
||||
|
||||
var bodyTemperature = float.NaN;
|
||||
|
||||
if (TryComp<TemperatureComponent>(target, out var temp))
|
||||
bodyTemperature = temp.CurrentTemperature;
|
||||
|
||||
var bloodAmount = float.NaN;
|
||||
|
||||
if (TryComp<BloodstreamComponent>(target, out var bloodstream) &&
|
||||
_solutionContainerSystem.ResolveSolution(target, bloodstream.BloodSolutionName, ref bloodstream.BloodSolution, out var bloodSolution))
|
||||
bloodAmount = bloodSolution.FillFraction;
|
||||
|
||||
_uiSystem.SendUiMessage(ui, new HealthAnalyzerScannedUserMessage(
|
||||
GetNetEntity(target),
|
||||
bodyTemperature,
|
||||
bloodAmount,
|
||||
scanMode
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,4 +72,17 @@ public sealed partial class SuitSensorComponent : Component
|
||||
/// </summary>
|
||||
[DataField("server")]
|
||||
public string? ConnectedServer = null;
|
||||
|
||||
/// <summary>
|
||||
/// The previous mode of the suit. This is used to restore the state when an EMP effect ends.
|
||||
/// </summary>
|
||||
[DataField, ViewVariables]
|
||||
public SuitSensorMode PreviousMode = SuitSensorMode.SensorOff;
|
||||
|
||||
/// <summary>
|
||||
/// The previous locked status of the controls. This is used to restore the state when an EMP effect ends.
|
||||
/// This keeps prisoner jumpsuits/internal implants from becoming unlocked after an EMP.
|
||||
/// </summary>
|
||||
[DataField, ViewVariables]
|
||||
public bool PreviousControlsLocked = false;
|
||||
}
|
||||
|
||||
@@ -2,11 +2,14 @@ using Content.Server.Access.Systems;
|
||||
using Content.Server.DeviceNetwork;
|
||||
using Content.Server.DeviceNetwork.Components;
|
||||
using Content.Server.DeviceNetwork.Systems;
|
||||
using Content.Server.Emp;
|
||||
using Content.Server.GameTicking;
|
||||
using Content.Server.Medical.CrewMonitoring;
|
||||
using Content.Server.Popups;
|
||||
using Content.Server.Station.Systems;
|
||||
using Content.Shared.Damage;
|
||||
using Content.Shared.DeviceNetwork;
|
||||
using Content.Shared.Emp;
|
||||
using Content.Shared.Examine;
|
||||
using Content.Shared.Inventory.Events;
|
||||
using Content.Shared.Medical.SuitSensor;
|
||||
@@ -44,6 +47,8 @@ public sealed class SuitSensorSystem : EntitySystem
|
||||
SubscribeLocalEvent<SuitSensorComponent, GetVerbsEvent<Verb>>(OnVerb);
|
||||
SubscribeLocalEvent<SuitSensorComponent, EntGotInsertedIntoContainerMessage>(OnInsert);
|
||||
SubscribeLocalEvent<SuitSensorComponent, EntGotRemovedFromContainerMessage>(OnRemove);
|
||||
SubscribeLocalEvent<SuitSensorComponent, EmpPulseEvent>(OnEmpPulse);
|
||||
SubscribeLocalEvent<SuitSensorComponent, EmpDisabledRemoved>(OnEmpFinished);
|
||||
}
|
||||
|
||||
private void OnUnpaused(EntityUid uid, SuitSensorComponent component, ref EntityUnpausedEvent args)
|
||||
@@ -239,6 +244,24 @@ public sealed class SuitSensorSystem : EntitySystem
|
||||
component.User = null;
|
||||
}
|
||||
|
||||
private void OnEmpPulse(EntityUid uid, SuitSensorComponent component, ref EmpPulseEvent args)
|
||||
{
|
||||
args.Affected = true;
|
||||
args.Disabled = true;
|
||||
|
||||
component.PreviousMode = component.Mode;
|
||||
SetSensor(uid, SuitSensorMode.SensorOff, null, component);
|
||||
|
||||
component.PreviousControlsLocked = component.ControlsLocked;
|
||||
component.ControlsLocked = true;
|
||||
}
|
||||
|
||||
private void OnEmpFinished(EntityUid uid, SuitSensorComponent component, ref EmpDisabledRemoved args)
|
||||
{
|
||||
SetSensor(uid, component.PreviousMode, null, component);
|
||||
component.ControlsLocked = component.PreviousControlsLocked;
|
||||
}
|
||||
|
||||
private Verb CreateVerb(EntityUid uid, SuitSensorComponent component, EntityUid userUid, SuitSensorMode mode)
|
||||
{
|
||||
return new Verb()
|
||||
|
||||
@@ -68,18 +68,14 @@ public sealed class RenameCommand : IConsoleCommand
|
||||
// This is done here because ID cards are linked to station records
|
||||
if (_entManager.TrySystem<StationRecordsSystem>(out var recordsSystem)
|
||||
&& _entManager.TryGetComponent(idCard, out StationRecordKeyStorageComponent? keyStorage)
|
||||
&& keyStorage.Key != null)
|
||||
&& keyStorage.Key is {} key)
|
||||
{
|
||||
var origin = keyStorage.Key.Value.OriginStation;
|
||||
|
||||
if (recordsSystem.TryGetRecord<GeneralStationRecord>(origin,
|
||||
keyStorage.Key.Value,
|
||||
out var generalRecord))
|
||||
if (recordsSystem.TryGetRecord<GeneralStationRecord>(key, out var generalRecord))
|
||||
{
|
||||
generalRecord.Name = name;
|
||||
}
|
||||
|
||||
recordsSystem.Synchronize(origin);
|
||||
recordsSystem.Synchronize(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
using System.Linq;
|
||||
using Content.Server.Administration.Logs;
|
||||
using Content.Server.Pointing.Components;
|
||||
using Content.Shared.Bed.Sleep;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.Examine;
|
||||
using Content.Shared.Eye;
|
||||
@@ -10,13 +9,13 @@ using Content.Shared.IdentityManagement;
|
||||
using Content.Shared.Input;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Mind;
|
||||
using Content.Shared.Mobs.Systems;
|
||||
using Content.Shared.Pointing;
|
||||
using Content.Shared.Popups;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared.Enums;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Input.Binding;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Player;
|
||||
@@ -34,7 +33,6 @@ namespace Content.Server.Pointing.EntitySystems
|
||||
[Dependency] private readonly ITileDefinitionManager _tileDefinitionManager = default!;
|
||||
[Dependency] private readonly IGameTiming _gameTiming = default!;
|
||||
[Dependency] private readonly RotateToFaceSystem _rotateToFaceSystem = default!;
|
||||
[Dependency] private readonly MobStateSystem _mobState = default!;
|
||||
[Dependency] private readonly SharedPopupSystem _popup = default!;
|
||||
[Dependency] private readonly VisibilitySystem _visibilitySystem = default!;
|
||||
[Dependency] private readonly SharedMindSystem _minds = default!;
|
||||
@@ -50,6 +48,15 @@ namespace Content.Server.Pointing.EntitySystems
|
||||
|
||||
private const float PointingRange = 15f;
|
||||
|
||||
private void GetCompState(Entity<PointingArrowComponent> entity, ref ComponentGetState args)
|
||||
{
|
||||
args.State = new SharedPointingArrowComponentState
|
||||
{
|
||||
StartPosition = entity.Comp.StartPosition,
|
||||
EndTime = entity.Comp.EndTime
|
||||
};
|
||||
}
|
||||
|
||||
private void OnPlayerStatusChanged(object? sender, SessionStatusEventArgs e)
|
||||
{
|
||||
if (e.NewStatus != SessionStatus.Disconnected)
|
||||
@@ -97,7 +104,7 @@ namespace Content.Server.Pointing.EntitySystems
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryPoint(ICommonSession? session, EntityCoordinates coords, EntityUid pointed)
|
||||
public bool TryPoint(ICommonSession? session, EntityCoordinates coordsPointed, EntityUid pointed)
|
||||
{
|
||||
if (session?.AttachedEntity is not { } player)
|
||||
{
|
||||
@@ -105,9 +112,9 @@ namespace Content.Server.Pointing.EntitySystems
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!coords.IsValid(EntityManager))
|
||||
if (!coordsPointed.IsValid(EntityManager))
|
||||
{
|
||||
Log.Warning($"Player {ToPrettyString(player)} attempted to point at invalid coordinates: {coords}");
|
||||
Log.Warning($"Player {ToPrettyString(player)} attempted to point at invalid coordinates: {coordsPointed}");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -123,33 +130,30 @@ namespace Content.Server.Pointing.EntitySystems
|
||||
return false;
|
||||
}
|
||||
|
||||
// Checking mob state directly instead of some action blocker, as many action blockers are blocked for
|
||||
// ghosts and there is no obvious choice for pointing.
|
||||
if (_mobState.IsIncapacitated(player))
|
||||
if (!CanPoint(player))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (HasComp<SleepingComponent>(player))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!InRange(player, coords))
|
||||
if (!InRange(player, coordsPointed))
|
||||
{
|
||||
_popup.PopupEntity(Loc.GetString("pointing-system-try-point-cannot-reach"), player, player);
|
||||
return false;
|
||||
}
|
||||
|
||||
var mapCoordsPointed = coordsPointed.ToMap(EntityManager);
|
||||
_rotateToFaceSystem.TryFaceCoordinates(player, mapCoordsPointed.Position);
|
||||
|
||||
var mapCoords = coords.ToMap(EntityManager);
|
||||
_rotateToFaceSystem.TryFaceCoordinates(player, mapCoords.Position);
|
||||
|
||||
var arrow = EntityManager.SpawnEntity("PointingArrow", coords);
|
||||
var arrow = EntityManager.SpawnEntity("PointingArrow", coordsPointed);
|
||||
|
||||
if (TryComp<PointingArrowComponent>(arrow, out var pointing))
|
||||
{
|
||||
pointing.EndTime = _gameTiming.CurTime + TimeSpan.FromSeconds(4);
|
||||
if (TryComp(player, out TransformComponent? xformPlayer))
|
||||
pointing.StartPosition = EntityCoordinates.FromMap(arrow, xformPlayer.Coordinates.ToMap(EntityManager)).Position;
|
||||
|
||||
pointing.EndTime = _gameTiming.CurTime + PointDuration;
|
||||
|
||||
Dirty(arrow, pointing);
|
||||
}
|
||||
|
||||
if (EntityQuery<PointingArrowAngeringComponent>().FirstOrDefault() != null)
|
||||
@@ -215,10 +219,10 @@ namespace Content.Server.Pointing.EntitySystems
|
||||
TileRef? tileRef = null;
|
||||
string? position = null;
|
||||
|
||||
if (_mapManager.TryFindGridAt(mapCoords, out var gridUid, out var grid))
|
||||
if (_mapManager.TryFindGridAt(mapCoordsPointed, out var gridUid, out var grid))
|
||||
{
|
||||
position = $"EntId={gridUid} {grid.WorldToTile(mapCoords.Position)}";
|
||||
tileRef = grid.GetTileRef(grid.WorldToTile(mapCoords.Position));
|
||||
position = $"EntId={gridUid} {grid.WorldToTile(mapCoordsPointed.Position)}";
|
||||
tileRef = grid.GetTileRef(grid.WorldToTile(mapCoordsPointed.Position));
|
||||
}
|
||||
|
||||
var tileDef = _tileDefinitionManager[tileRef?.Tile.TypeId ?? 0];
|
||||
@@ -228,7 +232,7 @@ namespace Content.Server.Pointing.EntitySystems
|
||||
|
||||
viewerMessage = Loc.GetString("pointing-system-other-point-at-tile", ("otherName", playerName), ("tileName", name));
|
||||
|
||||
_adminLogger.Add(LogType.Action, LogImpact.Low, $"{ToPrettyString(player):user} pointed at {name} {(position == null ? mapCoords : position)}");
|
||||
_adminLogger.Add(LogType.Action, LogImpact.Low, $"{ToPrettyString(player):user} pointed at {name} {(position == null ? mapCoordsPointed : position)}");
|
||||
}
|
||||
|
||||
_pointers[session] = _gameTiming.CurTime;
|
||||
@@ -242,6 +246,8 @@ namespace Content.Server.Pointing.EntitySystems
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<PointingArrowComponent, ComponentGetState>(GetCompState);
|
||||
|
||||
SubscribeNetworkEvent<PointingAttemptEvent>(OnPointAttempt);
|
||||
|
||||
_playerManager.PlayerStatusChanged += OnPlayerStatusChanged;
|
||||
@@ -255,8 +261,8 @@ namespace Content.Server.Pointing.EntitySystems
|
||||
{
|
||||
var target = GetEntity(ev.Target);
|
||||
|
||||
if (TryComp(target, out TransformComponent? xform))
|
||||
TryPoint(args.SenderSession, xform.Coordinates, target);
|
||||
if (TryComp(target, out TransformComponent? xformTarget))
|
||||
TryPoint(args.SenderSession, xformTarget.Coordinates, target);
|
||||
else
|
||||
Log.Warning($"User {args.SenderSession} attempted to point at a non-existent entity uid: {ev.Target}");
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ using Content.Server.DeviceNetwork.Systems;
|
||||
using Content.Server.NodeContainer;
|
||||
using Content.Server.NodeContainer.Nodes;
|
||||
using Content.Server.Power.Components;
|
||||
using Content.Shared.DeviceNetwork;
|
||||
using Content.Shared.Examine;
|
||||
using Content.Shared.Power.Generation.Teg;
|
||||
using Content.Shared.Rounding;
|
||||
|
||||
@@ -10,6 +10,7 @@ using Content.Server.Doors.Systems;
|
||||
using Content.Server.Power.EntitySystems;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.Interaction.Events;
|
||||
using Content.Shared.Examine;
|
||||
using static Content.Server.Remotes.DoorRemoteComponent;
|
||||
|
||||
namespace Content.Server.Remotes
|
||||
@@ -65,10 +66,9 @@ namespace Content.Server.Remotes
|
||||
if (args.Handled
|
||||
|| args.Target == null
|
||||
|| !TryComp<DoorComponent>(args.Target, out var doorComp) // If it isn't a door we don't use it
|
||||
// The remote can be used anywhere the user can see the door.
|
||||
// This doesn't work that well, but I don't know of an alternative
|
||||
|| !_interactionSystem.InRangeUnobstructed(args.User, args.Target.Value,
|
||||
SharedInteractionSystem.MaxRaycastRange, CollisionGroup.Opaque))
|
||||
// Only able to control doors if they are within your vision and within your max range.
|
||||
// Not affected by mobs or machines anymore.
|
||||
|| !ExamineSystemShared.InRangeUnOccluded(args.User, args.Target.Value, SharedInteractionSystem.MaxRaycastRange, null))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ using Content.Shared.Hands.EntitySystems;
|
||||
using Content.Shared.Interaction.Events;
|
||||
using Content.Shared.Inventory;
|
||||
using Content.Shared.Movement.Events;
|
||||
using Content.Shared.Movement.Systems;
|
||||
using Content.Shared.Resist;
|
||||
using Content.Shared.Storage;
|
||||
using Robust.Shared.Containers;
|
||||
@@ -38,6 +39,9 @@ public sealed class EscapeInventorySystem : EntitySystem
|
||||
|
||||
private void OnRelayMovement(EntityUid uid, CanEscapeInventoryComponent component, ref MoveInputEvent args)
|
||||
{
|
||||
if (!args.HasDirectionalMovement)
|
||||
return;
|
||||
|
||||
if (!_containerSystem.TryGetContainingContainer(uid, out var container) || !_actionBlockerSystem.CanInteract(uid, container.Owner))
|
||||
return;
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ using Content.Server.Shuttles.Systems;
|
||||
using Content.Server.Station.Components;
|
||||
using Content.Server.Station.Systems;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.DeviceNetwork;
|
||||
using Content.Shared.GameTicking;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Audio.Systems;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using Content.Server.DeviceNetwork;
|
||||
using Content.Server.DeviceNetwork.Systems;
|
||||
using Content.Server.Power.Components;
|
||||
using Content.Shared.DeviceNetwork;
|
||||
|
||||
namespace Content.Server.SensorMonitoring;
|
||||
|
||||
|
||||
@@ -10,13 +10,13 @@ using Content.Server.Power.Generation.Teg;
|
||||
using Content.Shared.Atmos.Monitor;
|
||||
using Content.Shared.Atmos.Piping.Binary.Components;
|
||||
using Content.Shared.Atmos.Piping.Unary.Components;
|
||||
using Content.Shared.DeviceNetwork;
|
||||
using Content.Shared.DeviceNetwork.Components;
|
||||
using Content.Shared.DeviceNetwork.Systems;
|
||||
using Content.Shared.SensorMonitoring;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
using ConsoleUIState = Content.Shared.SensorMonitoring.SensorMonitoringConsoleBoundInterfaceState;
|
||||
|
||||
namespace Content.Server.SensorMonitoring;
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ using Content.Server.Station.Components;
|
||||
using Content.Server.Station.Systems;
|
||||
using Content.Shared.Administration;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.DeviceNetwork;
|
||||
using Content.Shared.Mobs.Components;
|
||||
using Content.Shared.Movement.Components;
|
||||
using Content.Shared.Parallax.Biomes;
|
||||
|
||||
@@ -8,6 +8,7 @@ using Content.Shared.UserInterface;
|
||||
using Content.Shared.Access;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.DeviceNetwork;
|
||||
using Content.Shared.Popups;
|
||||
using Content.Shared.Shuttles.BUIStates;
|
||||
using Content.Shared.Shuttles.Events;
|
||||
|
||||
@@ -5,7 +5,6 @@ using Content.Server.Administration.Logs;
|
||||
using Content.Server.Administration.Managers;
|
||||
using Content.Server.Chat.Systems;
|
||||
using Content.Server.Communications;
|
||||
using Content.Server.DeviceNetwork;
|
||||
using Content.Server.DeviceNetwork.Components;
|
||||
using Content.Server.DeviceNetwork.Systems;
|
||||
using Content.Server.GameTicking.Events;
|
||||
@@ -19,6 +18,7 @@ using Content.Server.Station.Systems;
|
||||
using Content.Shared.Access.Systems;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.DeviceNetwork;
|
||||
using Content.Shared.Shuttles.Components;
|
||||
using Content.Shared.Shuttles.Events;
|
||||
using Content.Shared.Tag;
|
||||
|
||||
@@ -14,6 +14,7 @@ using Content.Shared.Mind.Components;
|
||||
using Content.Shared.Mobs;
|
||||
using Content.Shared.Mobs.Systems;
|
||||
using Content.Shared.Movement.Systems;
|
||||
using Content.Shared.Pointing;
|
||||
using Content.Shared.PowerCell;
|
||||
using Content.Shared.PowerCell.Components;
|
||||
using Content.Shared.Roles;
|
||||
@@ -67,6 +68,7 @@ public sealed partial class BorgSystem : SharedBorgSystem
|
||||
SubscribeLocalEvent<BorgChassisComponent, GetCharactedDeadIcEvent>(OnGetDeadIC);
|
||||
|
||||
SubscribeLocalEvent<BorgBrainComponent, MindAddedMessage>(OnBrainMindAdded);
|
||||
SubscribeLocalEvent<BorgBrainComponent, PointAttemptEvent>(OnBrainPointAttempt);
|
||||
|
||||
InitializeModules();
|
||||
InitializeMMI();
|
||||
@@ -242,6 +244,11 @@ public sealed partial class BorgSystem : SharedBorgSystem
|
||||
_mind.TransferTo(mindId, containerEnt, mind: mind);
|
||||
}
|
||||
|
||||
private void OnBrainPointAttempt(EntityUid uid, BorgBrainComponent component, PointAttemptEvent args)
|
||||
{
|
||||
args.Cancel();
|
||||
}
|
||||
|
||||
private void UpdateBatteryAlert(EntityUid uid, PowerCellSlotComponent? slotComponent = null)
|
||||
{
|
||||
if (!_powerCell.TryGetBatteryFromSlot(uid, out var battery, slotComponent))
|
||||
|
||||
@@ -121,14 +121,19 @@ public sealed class SiliconLawSystem : SharedSiliconLawSystem
|
||||
|
||||
private void OnIonStormLaws(EntityUid uid, SiliconLawProviderComponent component, ref IonStormLawsEvent args)
|
||||
{
|
||||
component.Lawset = args.Lawset;
|
||||
// Emagged borgs are immune to ion storm
|
||||
if (!HasComp<EmaggedComponent>(uid))
|
||||
{
|
||||
component.Lawset = args.Lawset;
|
||||
|
||||
// gotta tell player to check their laws
|
||||
NotifyLawsChanged(uid);
|
||||
// gotta tell player to check their laws
|
||||
NotifyLawsChanged(uid);
|
||||
|
||||
// new laws may allow antagonist behaviour so make it clear for admins
|
||||
if (TryComp<EmagSiliconLawComponent>(uid, out var emag))
|
||||
EnsureEmaggedRole(uid, emag);
|
||||
// new laws may allow antagonist behaviour so make it clear for admins
|
||||
if (TryComp<EmagSiliconLawComponent>(uid, out var emag))
|
||||
EnsureEmaggedRole(uid, emag);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private void OnEmagLawsAdded(EntityUid uid, SiliconLawProviderComponent component, ref GotEmaggedEvent args)
|
||||
@@ -148,7 +153,7 @@ public sealed class SiliconLawSystem : SharedSiliconLawSystem
|
||||
component.Lawset?.Laws.Add(new SiliconLaw
|
||||
{
|
||||
LawString = Loc.GetString("law-emag-secrecy", ("faction", Loc.GetString(component.Lawset.ObeysTo))),
|
||||
Order = component.Lawset.Laws.Count
|
||||
Order = component.Lawset.Laws.Max(law => law.Order) + 1
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -11,12 +11,22 @@ namespace Content.Server.Singularity.Components;
|
||||
public sealed partial class RadiationCollectorComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// How much joules will collector generate for each rad.
|
||||
/// Power output (in Watts) per unit of radiation collected.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public float ChargeModifier = 30000f;
|
||||
|
||||
/// <summary>
|
||||
/// Number of power ticks that the power supply can remain active for. This is needed since
|
||||
/// power and radiation don't update at the same tickrate, and since radiation does not provide
|
||||
/// an update when radiation is removed. When this goes to zero, zero out the power supplier
|
||||
/// to model the radiation source going away.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public int PowerTicksLeft = 0;
|
||||
|
||||
/// <summary>
|
||||
/// Is the machine enabled.
|
||||
/// </summary>
|
||||
|
||||
@@ -38,6 +38,7 @@ public sealed class RadiationCollectorSystem : EntitySystem
|
||||
SubscribeLocalEvent<RadiationCollectorComponent, MapInitEvent>(OnMapInit);
|
||||
SubscribeLocalEvent<RadiationCollectorComponent, EntInsertedIntoContainerMessage>(OnTankChanged);
|
||||
SubscribeLocalEvent<RadiationCollectorComponent, EntRemovedFromContainerMessage>(OnTankChanged);
|
||||
SubscribeLocalEvent<NetworkBatteryPostSync>(PostSync);
|
||||
}
|
||||
|
||||
private bool TryGetLoadedGasTank(EntityUid uid, [NotNullWhen(true)] out GasTankComponent? gasTankComponent)
|
||||
@@ -107,20 +108,34 @@ public sealed class RadiationCollectorSystem : EntitySystem
|
||||
}
|
||||
}
|
||||
|
||||
// No idea if this is even vaguely accurate to the previous logic.
|
||||
// The maths is copied from that logic even though it works differently.
|
||||
// But the previous logic would also make the radiation collectors never ever stop providing energy.
|
||||
// And since frameTime was used there, I'm assuming that this is what the intent was.
|
||||
// This still won't stop things being potentially hilariously unbalanced though.
|
||||
if (TryComp<BatteryComponent>(uid, out var batteryComponent))
|
||||
if (TryComp<PowerSupplierComponent>(uid, out var comp))
|
||||
{
|
||||
_batterySystem.SetCharge(uid, charge, batteryComponent);
|
||||
int powerHoldoverTicks = _gameTiming.TickRate * 2; // number of ticks to hold radiation
|
||||
component.PowerTicksLeft = powerHoldoverTicks;
|
||||
comp.MaxSupply = component.Enabled ? charge : 0;
|
||||
}
|
||||
|
||||
// Update appearance
|
||||
UpdatePressureIndicatorAppearance(uid, component, gasTankComponent);
|
||||
}
|
||||
|
||||
private void PostSync(NetworkBatteryPostSync ev)
|
||||
{
|
||||
// This is run every power tick. Used to decrement the PowerTicksLeft counter.
|
||||
var query = EntityQueryEnumerator<RadiationCollectorComponent>();
|
||||
while (query.MoveNext(out var uid, out var component))
|
||||
{
|
||||
if (component.PowerTicksLeft > 0)
|
||||
{
|
||||
component.PowerTicksLeft -= 1;
|
||||
}
|
||||
else if (TryComp<PowerSupplierComponent>(uid, out var comp))
|
||||
{
|
||||
comp.MaxSupply = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnExamined(EntityUid uid, RadiationCollectorComponent component, ExaminedEvent args)
|
||||
{
|
||||
if (!TryGetLoadedGasTank(uid, out var gasTank))
|
||||
|
||||
@@ -29,15 +29,16 @@ public sealed class ClericalErrorRule : StationEventSystem<ClericalErrorRuleComp
|
||||
var min = (int) Math.Max(1, Math.Round(component.MinToRemove * recordCount));
|
||||
var max = (int) Math.Max(min, Math.Round(component.MaxToRemove * recordCount));
|
||||
var toRemove = RobustRandom.Next(min, max);
|
||||
var keys = new List<StationRecordKey>();
|
||||
var keys = new List<uint>();
|
||||
for (var i = 0; i < toRemove; i++)
|
||||
{
|
||||
keys.Add(RobustRandom.Pick(stationRecords.Records.Keys));
|
||||
}
|
||||
|
||||
foreach (var key in keys)
|
||||
foreach (var id in keys)
|
||||
{
|
||||
_stationRecords.RemoveRecord(chosenStation.Value, key, stationRecords);
|
||||
var key = new StationRecordKey(id, chosenStation.Value);
|
||||
_stationRecords.RemoveRecord(key, stationRecords);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,21 @@
|
||||
using Content.Server.StationRecords.Systems;
|
||||
using Content.Shared.StationRecords;
|
||||
|
||||
namespace Content.Server.StationRecords;
|
||||
namespace Content.Server.StationRecords.Components;
|
||||
|
||||
[RegisterComponent]
|
||||
[RegisterComponent, Access(typeof(GeneralStationRecordConsoleSystem))]
|
||||
public sealed partial class GeneralStationRecordConsoleComponent : Component
|
||||
{
|
||||
public (NetEntity, uint)? ActiveKey { get; set; }
|
||||
public GeneralStationRecordsFilter? Filter { get; set; }
|
||||
/// <summary>
|
||||
/// Selected crewmember record id.
|
||||
/// Station always uses the station that owns the console.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public uint? ActiveKey;
|
||||
|
||||
/// <summary>
|
||||
/// Qualities to filter a search by.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public StationRecordsFilter? Filter;
|
||||
}
|
||||
|
||||
@@ -6,9 +6,10 @@ using Robust.Shared.Utility;
|
||||
namespace Content.Server.StationRecords;
|
||||
|
||||
/// <summary>
|
||||
/// Set of station records. StationRecordsComponent stores these.
|
||||
/// Keyed by StationRecordKey, which should be obtained from
|
||||
/// Set of station records for a single station. StationRecordsComponent stores these.
|
||||
/// Keyed by the record id, which should be obtained from
|
||||
/// an entity that stores a reference to it.
|
||||
/// A StationRecordKey has both the station entity (use to get the record set) and id (use for this).
|
||||
/// </summary>
|
||||
[DataDefinition]
|
||||
public sealed partial class StationRecordSet
|
||||
@@ -16,22 +17,31 @@ public sealed partial class StationRecordSet
|
||||
[DataField("currentRecordId")]
|
||||
private uint _currentRecordId;
|
||||
|
||||
// TODO add custom type serializer so that keys don't have to be written twice.
|
||||
[DataField("keys")]
|
||||
public HashSet<StationRecordKey> Keys = new();
|
||||
/// <summary>
|
||||
/// Every key id that has a record(s) stored.
|
||||
/// Presumably this is faster than iterating the dictionary to check if any tables have a key.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public HashSet<uint> Keys = new();
|
||||
|
||||
[DataField("recentlyAccessed")]
|
||||
private HashSet<StationRecordKey> _recentlyAccessed = new();
|
||||
/// <summary>
|
||||
/// Recently accessed key ids which are used to synchronize them efficiently.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
private HashSet<uint> _recentlyAccessed = new();
|
||||
|
||||
[DataField("tables")] // TODO ensure all of this data is serializable.
|
||||
private Dictionary<Type, Dictionary<StationRecordKey, object>> _tables = new();
|
||||
/// <summary>
|
||||
/// Dictionary between a record's type and then each record indexed by id.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
private Dictionary<Type, Dictionary<uint, object>> _tables = new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets all records of a specific type stored in the record set.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of record to fetch.</typeparam>
|
||||
/// <returns>An enumerable object that contains a pair of both a station key, and the record associated with it.</returns>
|
||||
public IEnumerable<(StationRecordKey, T)> GetRecordsOfType<T>()
|
||||
public IEnumerable<(uint, T)> GetRecordsOfType<T>()
|
||||
{
|
||||
if (!_tables.ContainsKey(typeof(T)))
|
||||
{
|
||||
@@ -52,43 +62,44 @@ public sealed partial class StationRecordSet
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add an entry into a record.
|
||||
/// Create a new record with an entry.
|
||||
/// Returns an id that can only be used to access the record for this station.
|
||||
/// </summary>
|
||||
/// <param name="entry">Entry to add.</param>
|
||||
/// <typeparam name="T">Type of the entry that's being added.</typeparam>
|
||||
public StationRecordKey AddRecordEntry<T>(EntityUid station, T entry)
|
||||
public uint? AddRecordEntry<T>(T entry)
|
||||
{
|
||||
if (entry == null)
|
||||
return StationRecordKey.Invalid;
|
||||
return null;
|
||||
|
||||
var key = new StationRecordKey(_currentRecordId++, station);
|
||||
var key = _currentRecordId++;
|
||||
AddRecordEntry(key, entry);
|
||||
return key;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add an entry into a record.
|
||||
/// Add an entry into an existing record.
|
||||
/// </summary>
|
||||
/// <param name="key">Key for the record.</param>
|
||||
/// <param name="key">Key id for the record.</param>
|
||||
/// <param name="entry">Entry to add.</param>
|
||||
/// <typeparam name="T">Type of the entry that's being added.</typeparam>
|
||||
public void AddRecordEntry<T>(StationRecordKey key, T entry)
|
||||
public void AddRecordEntry<T>(uint key, T entry)
|
||||
{
|
||||
if (entry == null)
|
||||
return;
|
||||
|
||||
if (Keys.Add(key))
|
||||
_tables.GetOrNew(typeof(T))[key] = entry;
|
||||
Keys.Add(key);
|
||||
_tables.GetOrNew(typeof(T))[key] = entry;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Try to get an record entry by type, from this record key.
|
||||
/// </summary>
|
||||
/// <param name="key">The StationRecordKey to get the entries from.</param>
|
||||
/// <param name="key">The record id to get the entries from.</param>
|
||||
/// <param name="entry">The entry that is retrieved from the record set.</param>
|
||||
/// <typeparam name="T">The type of entry to search for.</typeparam>
|
||||
/// <returns>True if the record exists and was retrieved, false otherwise.</returns>
|
||||
public bool TryGetRecordEntry<T>(StationRecordKey key, [NotNullWhen(true)] out T? entry)
|
||||
public bool TryGetRecordEntry<T>(uint key, [NotNullWhen(true)] out T? entry)
|
||||
{
|
||||
entry = default;
|
||||
|
||||
@@ -108,10 +119,10 @@ public sealed partial class StationRecordSet
|
||||
/// <summary>
|
||||
/// Checks if the record associated with this key has an entry of a certain type.
|
||||
/// </summary>
|
||||
/// <param name="key">The record key.</param>
|
||||
/// <param name="key">The record key id.</param>
|
||||
/// <typeparam name="T">Type to check.</typeparam>
|
||||
/// <returns>True if the entry exists, false otherwise.</returns>
|
||||
public bool HasRecordEntry<T>(StationRecordKey key)
|
||||
public bool HasRecordEntry<T>(uint key)
|
||||
{
|
||||
return Keys.Contains(key)
|
||||
&& _tables.TryGetValue(typeof(T), out var table)
|
||||
@@ -122,7 +133,7 @@ public sealed partial class StationRecordSet
|
||||
/// Get the recently accessed keys from this record set.
|
||||
/// </summary>
|
||||
/// <returns>All recently accessed keys from this record set.</returns>
|
||||
public IEnumerable<StationRecordKey> GetRecentlyAccessed()
|
||||
public IEnumerable<uint> GetRecentlyAccessed()
|
||||
{
|
||||
return _recentlyAccessed.ToArray();
|
||||
}
|
||||
@@ -135,17 +146,23 @@ public sealed partial class StationRecordSet
|
||||
_recentlyAccessed.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes a recently accessed key from the set.
|
||||
/// </summary>
|
||||
public void RemoveFromRecentlyAccessed(uint key)
|
||||
{
|
||||
_recentlyAccessed.Remove(key);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes all record entries related to this key from this set.
|
||||
/// </summary>
|
||||
/// <param name="key">The key to remove.</param>
|
||||
/// <returns>True if successful, false otherwise.</returns>
|
||||
public bool RemoveAllRecords(StationRecordKey key)
|
||||
public bool RemoveAllRecords(uint key)
|
||||
{
|
||||
if (!Keys.Remove(key))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (var table in _tables.Values)
|
||||
{
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Linq;
|
||||
using Content.Server.Station.Systems;
|
||||
using Content.Server.StationRecords.Components;
|
||||
using Content.Shared.StationRecords;
|
||||
using Robust.Server.GameObjects;
|
||||
|
||||
@@ -7,126 +8,78 @@ namespace Content.Server.StationRecords.Systems;
|
||||
|
||||
public sealed class GeneralStationRecordConsoleSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly UserInterfaceSystem _userInterface = default!;
|
||||
[Dependency] private readonly StationSystem _stationSystem = default!;
|
||||
[Dependency] private readonly StationRecordsSystem _stationRecordsSystem = default!;
|
||||
[Dependency] private readonly UserInterfaceSystem _ui = default!;
|
||||
[Dependency] private readonly StationSystem _station = default!;
|
||||
[Dependency] private readonly StationRecordsSystem _stationRecords = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
SubscribeLocalEvent<GeneralStationRecordConsoleComponent, BoundUIOpenedEvent>(UpdateUserInterface);
|
||||
SubscribeLocalEvent<GeneralStationRecordConsoleComponent, SelectGeneralStationRecord>(OnKeySelected);
|
||||
SubscribeLocalEvent<GeneralStationRecordConsoleComponent, GeneralStationRecordsFilterMsg>(OnFiltersChanged);
|
||||
SubscribeLocalEvent<GeneralStationRecordConsoleComponent, RecordModifiedEvent>(UpdateUserInterface);
|
||||
SubscribeLocalEvent<GeneralStationRecordConsoleComponent, AfterGeneralRecordCreatedEvent>(UpdateUserInterface);
|
||||
SubscribeLocalEvent<GeneralStationRecordConsoleComponent, RecordRemovedEvent>(UpdateUserInterface);
|
||||
}
|
||||
|
||||
private void UpdateUserInterface<T>(EntityUid uid, GeneralStationRecordConsoleComponent component, T ev)
|
||||
{
|
||||
UpdateUserInterface(uid, component);
|
||||
}
|
||||
|
||||
private void OnKeySelected(EntityUid uid, GeneralStationRecordConsoleComponent component,
|
||||
SelectGeneralStationRecord msg)
|
||||
{
|
||||
component.ActiveKey = msg.SelectedKey;
|
||||
UpdateUserInterface(uid, component);
|
||||
}
|
||||
|
||||
private void OnFiltersChanged(EntityUid uid,
|
||||
GeneralStationRecordConsoleComponent component, GeneralStationRecordsFilterMsg msg)
|
||||
{
|
||||
if (component.Filter == null ||
|
||||
component.Filter.Type != msg.Type || component.Filter.Value != msg.Value)
|
||||
Subs.BuiEvents<GeneralStationRecordConsoleComponent>(GeneralStationRecordConsoleKey.Key, subs =>
|
||||
{
|
||||
component.Filter = new GeneralStationRecordsFilter(msg.Type, msg.Value);
|
||||
UpdateUserInterface(uid, component);
|
||||
subs.Event<BoundUIOpenedEvent>(UpdateUserInterface);
|
||||
subs.Event<SelectStationRecord>(OnKeySelected);
|
||||
subs.Event<SetStationRecordFilter>(OnFiltersChanged);
|
||||
});
|
||||
}
|
||||
|
||||
private void UpdateUserInterface<T>(Entity<GeneralStationRecordConsoleComponent> ent, ref T args)
|
||||
{
|
||||
UpdateUserInterface(ent);
|
||||
}
|
||||
|
||||
// TODO: instead of copy paste shitcode for each record console, have a shared records console comp they all use
|
||||
// then have this somehow play nicely with creating ui state
|
||||
// if that gets done put it in StationRecordsSystem console helpers section :)
|
||||
private void OnKeySelected(Entity<GeneralStationRecordConsoleComponent> ent, ref SelectStationRecord msg)
|
||||
{
|
||||
ent.Comp.ActiveKey = msg.SelectedKey;
|
||||
UpdateUserInterface(ent);
|
||||
}
|
||||
|
||||
private void OnFiltersChanged(Entity<GeneralStationRecordConsoleComponent> ent, ref SetStationRecordFilter msg)
|
||||
{
|
||||
if (ent.Comp.Filter == null ||
|
||||
ent.Comp.Filter.Type != msg.Type || ent.Comp.Filter.Value != msg.Value)
|
||||
{
|
||||
ent.Comp.Filter = new StationRecordsFilter(msg.Type, msg.Value);
|
||||
UpdateUserInterface(ent);
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateUserInterface(EntityUid uid,
|
||||
GeneralStationRecordConsoleComponent? console = null)
|
||||
private void UpdateUserInterface(Entity<GeneralStationRecordConsoleComponent> ent)
|
||||
{
|
||||
if (!Resolve(uid, ref console))
|
||||
var (uid, console) = ent;
|
||||
var owningStation = _station.GetOwningStation(uid);
|
||||
|
||||
if (!TryComp<StationRecordsComponent>(owningStation, out var stationRecords))
|
||||
{
|
||||
_ui.TrySetUiState(uid, GeneralStationRecordConsoleKey.Key, new GeneralStationRecordConsoleState());
|
||||
return;
|
||||
}
|
||||
|
||||
var owningStation = _stationSystem.GetOwningStation(uid);
|
||||
var listing = _stationRecords.BuildListing((owningStation.Value, stationRecords), console.Filter);
|
||||
|
||||
if (!TryComp<StationRecordsComponent>(owningStation, out var stationRecordsComponent))
|
||||
switch (listing.Count)
|
||||
{
|
||||
GeneralStationRecordConsoleState state = new(null, null, null, null);
|
||||
SetStateForInterface(uid, state);
|
||||
case 0:
|
||||
_ui.TrySetUiState(uid, GeneralStationRecordConsoleKey.Key, new GeneralStationRecordConsoleState());
|
||||
return;
|
||||
case 1:
|
||||
console.ActiveKey = listing.Keys.First();
|
||||
break;
|
||||
}
|
||||
|
||||
if (console.ActiveKey is not { } id)
|
||||
return;
|
||||
}
|
||||
|
||||
var consoleRecords =
|
||||
_stationRecordsSystem.GetRecordsOfType<GeneralStationRecord>(owningStation.Value, stationRecordsComponent);
|
||||
var key = new StationRecordKey(id, owningStation.Value);
|
||||
_stationRecords.TryGetRecord<GeneralStationRecord>(key, out var record, stationRecords);
|
||||
|
||||
var listing = new Dictionary<(NetEntity, uint), string>();
|
||||
|
||||
foreach (var pair in consoleRecords)
|
||||
{
|
||||
if (console.Filter != null && IsSkippedRecord(console.Filter, pair.Item2))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
listing.Add(_stationRecordsSystem.Convert(pair.Item1), pair.Item2.Name);
|
||||
}
|
||||
|
||||
if (listing.Count == 0)
|
||||
{
|
||||
GeneralStationRecordConsoleState state = new(null, null, null, console.Filter);
|
||||
SetStateForInterface(uid, state);
|
||||
return;
|
||||
}
|
||||
else if (listing.Count == 1)
|
||||
{
|
||||
console.ActiveKey = listing.Keys.First();
|
||||
}
|
||||
|
||||
GeneralStationRecord? record = null;
|
||||
if (console.ActiveKey != null)
|
||||
{
|
||||
_stationRecordsSystem.TryGetRecord(owningStation.Value, _stationRecordsSystem.Convert(console.ActiveKey.Value), out record,
|
||||
stationRecordsComponent);
|
||||
}
|
||||
|
||||
GeneralStationRecordConsoleState newState = new(console.ActiveKey, record, listing, console.Filter);
|
||||
SetStateForInterface(uid, newState);
|
||||
}
|
||||
|
||||
private void SetStateForInterface(EntityUid uid, GeneralStationRecordConsoleState newState)
|
||||
{
|
||||
_userInterface.TrySetUiState(uid, GeneralStationRecordConsoleKey.Key, newState);
|
||||
}
|
||||
|
||||
private bool IsSkippedRecord(GeneralStationRecordsFilter filter,
|
||||
GeneralStationRecord someRecord)
|
||||
{
|
||||
bool isFilter = filter.Value.Length > 0;
|
||||
string filterLowerCaseValue = "";
|
||||
|
||||
if (!isFilter)
|
||||
return false;
|
||||
|
||||
filterLowerCaseValue = filter.Value.ToLower();
|
||||
|
||||
return filter.Type switch
|
||||
{
|
||||
GeneralStationRecordFilterType.Name =>
|
||||
!someRecord.Name.ToLower().Contains(filterLowerCaseValue),
|
||||
GeneralStationRecordFilterType.Prints => someRecord.Fingerprint != null
|
||||
&& IsFilterWithSomeCodeValue(someRecord.Fingerprint, filterLowerCaseValue),
|
||||
GeneralStationRecordFilterType.DNA => someRecord.DNA != null
|
||||
&& IsFilterWithSomeCodeValue(someRecord.DNA, filterLowerCaseValue),
|
||||
};
|
||||
}
|
||||
|
||||
private bool IsFilterWithSomeCodeValue(string value, string filter)
|
||||
{
|
||||
return !value.ToLower().StartsWith(filter);
|
||||
GeneralStationRecordConsoleState newState = new(id, record, listing, console.Filter);
|
||||
_ui.TrySetUiState(uid, GeneralStationRecordConsoleKey.Key, newState);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,8 +32,8 @@ namespace Content.Server.StationRecords.Systems;
|
||||
/// </summary>
|
||||
public sealed class StationRecordsSystem : SharedStationRecordsSystem
|
||||
{
|
||||
[Dependency] private readonly InventorySystem _inventorySystem = default!;
|
||||
[Dependency] private readonly StationRecordKeyStorageSystem _keyStorageSystem = default!;
|
||||
[Dependency] private readonly InventorySystem _inventory = default!;
|
||||
[Dependency] private readonly StationRecordKeyStorageSystem _keyStorage = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
|
||||
public override void Initialize()
|
||||
@@ -45,26 +45,22 @@ public sealed class StationRecordsSystem : SharedStationRecordsSystem
|
||||
|
||||
private void OnPlayerSpawn(PlayerSpawnCompleteEvent args)
|
||||
{
|
||||
if (!HasComp<StationRecordsComponent>(args.Station))
|
||||
if (!TryComp<StationRecordsComponent>(args.Station, out var stationRecords))
|
||||
return;
|
||||
|
||||
CreateGeneralRecord(args.Station, args.Mob, args.Profile, args.JobId);
|
||||
CreateGeneralRecord(args.Station, args.Mob, args.Profile, args.JobId, stationRecords);
|
||||
}
|
||||
|
||||
private void CreateGeneralRecord(EntityUid station, EntityUid player, HumanoidCharacterProfile profile,
|
||||
string? jobId, StationRecordsComponent? records = null)
|
||||
string? jobId, StationRecordsComponent records)
|
||||
{
|
||||
if (!Resolve(station, ref records)
|
||||
|| string.IsNullOrEmpty(jobId)
|
||||
// TODO make PlayerSpawnCompleteEvent.JobId a ProtoId
|
||||
if (string.IsNullOrEmpty(jobId)
|
||||
|| !_prototypeManager.HasIndex<JobPrototype>(jobId))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_inventorySystem.TryGetSlotEntity(player, "id", out var idUid))
|
||||
{
|
||||
if (!_inventory.TryGetSlotEntity(player, "id", out var idUid))
|
||||
return;
|
||||
}
|
||||
|
||||
TryComp<FingerprintComponent>(player, out var fingerprintComponent);
|
||||
TryComp<DnaComponent>(player, out var dnaComponent);
|
||||
@@ -100,17 +96,28 @@ public sealed class StationRecordsSystem : SharedStationRecordsSystem
|
||||
/// Optional - other systems should anticipate this.
|
||||
/// </param>
|
||||
/// <param name="records">Station records component.</param>
|
||||
public void CreateGeneralRecord(EntityUid station, EntityUid? idUid, string name, int age, string species, Gender gender, string jobId, string? mobFingerprint, string? dna, HumanoidCharacterProfile? profile = null,
|
||||
StationRecordsComponent? records = null)
|
||||
public void CreateGeneralRecord(
|
||||
EntityUid station,
|
||||
EntityUid? idUid,
|
||||
string name,
|
||||
int age,
|
||||
string species,
|
||||
Gender gender,
|
||||
string jobId,
|
||||
string? mobFingerprint,
|
||||
string? dna,
|
||||
HumanoidCharacterProfile profile,
|
||||
StationRecordsComponent records)
|
||||
{
|
||||
if (!Resolve(station, ref records))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_prototypeManager.TryIndex(jobId, out JobPrototype? jobPrototype))
|
||||
{
|
||||
if (!_prototypeManager.TryIndex<JobPrototype>(jobId, out var jobPrototype))
|
||||
throw new ArgumentException($"Invalid job prototype ID: {jobId}");
|
||||
|
||||
// when adding a record that already exists use the old one
|
||||
// this happens when respawning as the same character
|
||||
if (GetRecordByName(station, name, records) is {} id)
|
||||
{
|
||||
SetIdKey(idUid, new StationRecordKey(id, station));
|
||||
return;
|
||||
}
|
||||
|
||||
var record = new GeneralStationRecord()
|
||||
@@ -129,40 +136,47 @@ public sealed class StationRecordsSystem : SharedStationRecordsSystem
|
||||
|
||||
var key = AddRecordEntry(station, record);
|
||||
if (!key.IsValid())
|
||||
return;
|
||||
|
||||
if (idUid != null)
|
||||
{
|
||||
var keyStorageEntity = idUid;
|
||||
if (TryComp(idUid, out PdaComponent? pdaComponent) && pdaComponent.ContainedId != null)
|
||||
{
|
||||
keyStorageEntity = pdaComponent.IdSlot.Item;
|
||||
}
|
||||
|
||||
if (keyStorageEntity != null)
|
||||
{
|
||||
_keyStorageSystem.AssignKey(keyStorageEntity.Value, key);
|
||||
}
|
||||
Log.Warning($"Failed to add general record entry for {name}");
|
||||
return;
|
||||
}
|
||||
|
||||
RaiseLocalEvent(new AfterGeneralRecordCreatedEvent(station, key, record, profile));
|
||||
SetIdKey(idUid, key);
|
||||
|
||||
RaiseLocalEvent(new AfterGeneralRecordCreatedEvent(key, record, profile));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set the station records key for an id/pda.
|
||||
/// </summary>
|
||||
public void SetIdKey(EntityUid? uid, StationRecordKey key)
|
||||
{
|
||||
if (uid is not {} idUid)
|
||||
return;
|
||||
|
||||
var keyStorageEntity = idUid;
|
||||
if (TryComp<PdaComponent>(idUid, out var pda) && pda.ContainedId is {} id)
|
||||
{
|
||||
keyStorageEntity = id;
|
||||
}
|
||||
|
||||
_keyStorage.AssignKey(keyStorageEntity, key);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes a record from this station.
|
||||
/// </summary>
|
||||
/// <param name="station">Station to remove the record from.</param>
|
||||
/// <param name="key">The key to remove.</param>
|
||||
/// <param name="key">The station and key to remove.</param>
|
||||
/// <param name="records">Station records component.</param>
|
||||
/// <returns>True if the record was removed, false otherwise.</returns>
|
||||
public bool RemoveRecord(EntityUid station, StationRecordKey key, StationRecordsComponent? records = null)
|
||||
public bool RemoveRecord(StationRecordKey key, StationRecordsComponent? records = null)
|
||||
{
|
||||
if (!Resolve(station, ref records))
|
||||
if (!Resolve(key.OriginStation, ref records))
|
||||
return false;
|
||||
|
||||
if (records.Records.RemoveAllRecords(key))
|
||||
if (records.Records.RemoveAllRecords(key.Id))
|
||||
{
|
||||
RaiseLocalEvent(new RecordRemovedEvent(station, key));
|
||||
RaiseLocalEvent(new RecordRemovedEvent(key));
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -174,20 +188,39 @@ public sealed class StationRecordsSystem : SharedStationRecordsSystem
|
||||
/// from the provided station record key. Will always return
|
||||
/// null if the key does not match the station.
|
||||
/// </summary>
|
||||
/// <param name="station">Station to get the record from.</param>
|
||||
/// <param name="key">Key to try and index from the record set.</param>
|
||||
/// <param name="key">Station and key to try and index from the record set.</param>
|
||||
/// <param name="entry">The resulting entry.</param>
|
||||
/// <param name="records">Station record component.</param>
|
||||
/// <typeparam name="T">Type to get from the record set.</typeparam>
|
||||
/// <returns>True if the record was obtained, false otherwise.</returns>
|
||||
public bool TryGetRecord<T>(EntityUid station, StationRecordKey key, [NotNullWhen(true)] out T? entry, StationRecordsComponent? records = null)
|
||||
public bool TryGetRecord<T>(StationRecordKey key, [NotNullWhen(true)] out T? entry, StationRecordsComponent? records = null)
|
||||
{
|
||||
entry = default;
|
||||
|
||||
if (!Resolve(station, ref records))
|
||||
if (!Resolve(key.OriginStation, ref records))
|
||||
return false;
|
||||
|
||||
return records.Records.TryGetRecordEntry(key, out entry);
|
||||
return records.Records.TryGetRecordEntry(key.Id, out entry);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns an id if a record with the same name exists.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Linear search so O(n) time complexity.
|
||||
/// </remarks>
|
||||
public uint? GetRecordByName(EntityUid station, string name, StationRecordsComponent? records = null)
|
||||
{
|
||||
if (!Resolve(station, ref records))
|
||||
return null;
|
||||
|
||||
foreach (var (id, record) in GetRecordsOfType<GeneralStationRecord>(station, records))
|
||||
{
|
||||
if (record.Name == name)
|
||||
return id;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -197,30 +230,47 @@ public sealed class StationRecordsSystem : SharedStationRecordsSystem
|
||||
/// <param name="records">Station records component.</param>
|
||||
/// <typeparam name="T">Type of record to fetch</typeparam>
|
||||
/// <returns>Enumerable of pairs with a station record key, and the entry in question of type T.</returns>
|
||||
public IEnumerable<(StationRecordKey, T)> GetRecordsOfType<T>(EntityUid station, StationRecordsComponent? records = null)
|
||||
public IEnumerable<(uint, T)> GetRecordsOfType<T>(EntityUid station, StationRecordsComponent? records = null)
|
||||
{
|
||||
if (!Resolve(station, ref records))
|
||||
{
|
||||
return Array.Empty<(StationRecordKey, T)>();
|
||||
}
|
||||
return Array.Empty<(uint, T)>();
|
||||
|
||||
return records.Records.GetRecordsOfType<T>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a record entry to a station's record set.
|
||||
/// Adds a new record entry to a station's record set.
|
||||
/// </summary>
|
||||
/// <param name="station">The station to add the record to.</param>
|
||||
/// <param name="record">The record to add.</param>
|
||||
/// <param name="records">Station records component.</param>
|
||||
/// <typeparam name="T">The type of record to add.</typeparam>
|
||||
public StationRecordKey AddRecordEntry<T>(EntityUid station, T record,
|
||||
StationRecordsComponent? records = null)
|
||||
public StationRecordKey AddRecordEntry<T>(EntityUid station, T record, StationRecordsComponent? records = null)
|
||||
{
|
||||
if (!Resolve(station, ref records))
|
||||
return StationRecordKey.Invalid;
|
||||
|
||||
return records.Records.AddRecordEntry(station, record);
|
||||
var id = records.Records.AddRecordEntry(record);
|
||||
if (id == null)
|
||||
return StationRecordKey.Invalid;
|
||||
|
||||
return new StationRecordKey(id.Value, station);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a record to an existing entry.
|
||||
/// </summary>
|
||||
/// <param name="key">The station and id of the existing entry.</param>
|
||||
/// <param name="record">The record to add.</param>
|
||||
/// <param name="records">Station records component.</param>
|
||||
/// <typeparam name="T">The type of record to add.</typeparam>
|
||||
public void AddRecordEntry<T>(StationRecordKey key, T record,
|
||||
StationRecordsComponent? records = null)
|
||||
{
|
||||
if (!Resolve(key.OriginStation, ref records))
|
||||
return;
|
||||
|
||||
records.Records.AddRecordEntry(key.Id, record);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -231,17 +281,99 @@ public sealed class StationRecordsSystem : SharedStationRecordsSystem
|
||||
public void Synchronize(EntityUid station, StationRecordsComponent? records = null)
|
||||
{
|
||||
if (!Resolve(station, ref records))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var key in records.Records.GetRecentlyAccessed())
|
||||
{
|
||||
RaiseLocalEvent(new RecordModifiedEvent(station, key));
|
||||
RaiseLocalEvent(new RecordModifiedEvent(new StationRecordKey(key, station)));
|
||||
}
|
||||
|
||||
records.Records.ClearRecentlyAccessed();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Synchronizes a single record's entries for a station.
|
||||
/// </summary>
|
||||
/// <param name="key">The station and id of the record</param>
|
||||
/// <param name="records">Station records component.</param>
|
||||
public void Synchronize(StationRecordKey key, StationRecordsComponent? records = null)
|
||||
{
|
||||
if (!Resolve(key.OriginStation, ref records))
|
||||
return;
|
||||
|
||||
RaiseLocalEvent(new RecordModifiedEvent(key));
|
||||
|
||||
records.Records.RemoveFromRecentlyAccessed(key.Id);
|
||||
}
|
||||
|
||||
#region Console system helpers
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a record should be skipped given a filter.
|
||||
/// Takes general record since even if you are using this for e.g. criminal records,
|
||||
/// you don't want to duplicate basic info like name and dna.
|
||||
/// Station records lets you do this nicely with multiple types having their own data.
|
||||
/// </summary>
|
||||
public bool IsSkipped(StationRecordsFilter? filter, GeneralStationRecord someRecord)
|
||||
{
|
||||
// if nothing is being filtered, show everything
|
||||
if (filter == null)
|
||||
return false;
|
||||
if (filter.Value.Length == 0)
|
||||
return false;
|
||||
|
||||
var filterLowerCaseValue = filter.Value.ToLower();
|
||||
|
||||
return filter.Type switch
|
||||
{
|
||||
StationRecordFilterType.Name =>
|
||||
!someRecord.Name.ToLower().Contains(filterLowerCaseValue),
|
||||
StationRecordFilterType.Prints => someRecord.Fingerprint != null
|
||||
&& IsFilterWithSomeCodeValue(someRecord.Fingerprint, filterLowerCaseValue),
|
||||
StationRecordFilterType.DNA => someRecord.DNA != null
|
||||
&& IsFilterWithSomeCodeValue(someRecord.DNA, filterLowerCaseValue),
|
||||
};
|
||||
}
|
||||
|
||||
private bool IsFilterWithSomeCodeValue(string value, string filter)
|
||||
{
|
||||
return !value.ToLower().StartsWith(filter);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Build a record listing of id to name for a station and filter.
|
||||
/// </summary>
|
||||
public Dictionary<uint, string> BuildListing(Entity<StationRecordsComponent> station, StationRecordsFilter? filter)
|
||||
{
|
||||
var listing = new Dictionary<uint, string>();
|
||||
|
||||
var records = GetRecordsOfType<GeneralStationRecord>(station, station.Comp);
|
||||
foreach (var pair in records)
|
||||
{
|
||||
if (IsSkipped(filter, pair.Item2))
|
||||
continue;
|
||||
|
||||
listing.Add(pair.Item1, pair.Item2.Name);
|
||||
}
|
||||
|
||||
return listing;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Base event for station record events
|
||||
/// </summary>
|
||||
public abstract class StationRecordEvent : EntityEventArgs
|
||||
{
|
||||
public readonly StationRecordKey Key;
|
||||
public EntityUid Station => Key.OriginStation;
|
||||
|
||||
protected StationRecordEvent(StationRecordKey key)
|
||||
{
|
||||
Key = key;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -250,23 +382,19 @@ public sealed class StationRecordsSystem : SharedStationRecordsSystem
|
||||
/// listening to this event, as it contains the character's record key.
|
||||
/// Also stores the general record reference, to save some time.
|
||||
/// </summary>
|
||||
public sealed class AfterGeneralRecordCreatedEvent : EntityEventArgs
|
||||
public sealed class AfterGeneralRecordCreatedEvent : StationRecordEvent
|
||||
{
|
||||
public readonly EntityUid Station;
|
||||
public StationRecordKey Key { get; }
|
||||
public GeneralStationRecord Record { get; }
|
||||
public readonly GeneralStationRecord Record;
|
||||
/// <summary>
|
||||
/// Profile for the related player. This is so that other systems can get further information
|
||||
/// about the player character.
|
||||
/// Optional - other systems should anticipate this.
|
||||
/// </summary>
|
||||
public HumanoidCharacterProfile? Profile { get; }
|
||||
public readonly HumanoidCharacterProfile Profile;
|
||||
|
||||
public AfterGeneralRecordCreatedEvent(EntityUid station, StationRecordKey key, GeneralStationRecord record,
|
||||
HumanoidCharacterProfile? profile)
|
||||
public AfterGeneralRecordCreatedEvent(StationRecordKey key, GeneralStationRecord record,
|
||||
HumanoidCharacterProfile profile) : base(key)
|
||||
{
|
||||
Station = station;
|
||||
Key = key;
|
||||
Record = record;
|
||||
Profile = profile;
|
||||
}
|
||||
@@ -278,15 +406,10 @@ public sealed class AfterGeneralRecordCreatedEvent : EntityEventArgs
|
||||
/// that store record keys can then remove the key from their internal
|
||||
/// fields.
|
||||
/// </summary>
|
||||
public sealed class RecordRemovedEvent : EntityEventArgs
|
||||
public sealed class RecordRemovedEvent : StationRecordEvent
|
||||
{
|
||||
public readonly EntityUid Station;
|
||||
public StationRecordKey Key { get; }
|
||||
|
||||
public RecordRemovedEvent(EntityUid station, StationRecordKey key)
|
||||
public RecordRemovedEvent(StationRecordKey key) : base(key)
|
||||
{
|
||||
Station = station;
|
||||
Key = key;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -295,14 +418,9 @@ public sealed class RecordRemovedEvent : EntityEventArgs
|
||||
/// inform other systems that records stored in this key
|
||||
/// may have changed.
|
||||
/// </summary>
|
||||
public sealed class RecordModifiedEvent : EntityEventArgs
|
||||
public sealed class RecordModifiedEvent : StationRecordEvent
|
||||
{
|
||||
public readonly EntityUid Station;
|
||||
public StationRecordKey Key { get; }
|
||||
|
||||
public RecordModifiedEvent(EntityUid station, StationRecordKey key)
|
||||
public RecordModifiedEvent(StationRecordKey key) : base(key)
|
||||
{
|
||||
Station = station;
|
||||
Key = key;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ public sealed class EntityStorageSystem : SharedEntityStorageSystem
|
||||
base.Initialize();
|
||||
|
||||
/* CompRef things */
|
||||
SubscribeLocalEvent<EntityStorageComponent, EntityUnpausedEvent>(OnEntityUnpausedEvent);
|
||||
SubscribeLocalEvent<EntityStorageComponent, ComponentInit>(OnComponentInit);
|
||||
SubscribeLocalEvent<EntityStorageComponent, ComponentStartup>(OnComponentStartup);
|
||||
SubscribeLocalEvent<EntityStorageComponent, ActivateInWorldEvent>(OnInteract, after: new[] { typeof(LockSystem) });
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user