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:
Morb0
2024-02-12 12:53:34 +03:00
669 changed files with 12711 additions and 8419 deletions
@@ -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;
+16 -10
View File
@@ -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;
}
}
+14 -5
View File
@@ -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">
+1 -12
View File
@@ -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);
}
}
+16 -20
View File
@@ -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;
}
}
+8 -69
View File
@@ -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));
}
}
+8 -2
View File
@@ -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()
+121
View File
@@ -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;
+5
View File
@@ -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>
+13
View File
@@ -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;
+14 -23
View File
@@ -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;
}
+1 -34
View File
@@ -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)
+2 -2
View File
@@ -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);
+1
View File
@@ -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)
+6 -3
View File
@@ -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;
+2 -1
View File
@@ -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
));
}
+174 -73
View File
@@ -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;
+4 -4
View File
@@ -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