mirror of
https://github.com/space-syndicate/space-station-14.git
synced 2026-02-15 00:54:51 +01:00
Merge remote-tracking branch 'refs/remotes/upstream/master' into upstream-sync
# Conflicts: # Content.Server/Administration/Commands/AdminWhoCommand.cs # Content.Server/Administration/Commands/PlayTimeCommands.cs # Content.Server/GameTicking/Rules/NukeopsRuleSystem.cs # Content.Server/Preferences/Managers/ServerPreferencesManager.cs # Resources/Prototypes/Datasets/tips.yml # Resources/Prototypes/Entities/Mobs/Player/humanoid.yml
This commit is contained in:
@@ -27,6 +27,9 @@ namespace Content.Client.Access.UI
|
||||
private string? _lastJobTitle;
|
||||
private string? _lastJobProto;
|
||||
|
||||
// The job that will be picked if the ID doesn't have a job on the station.
|
||||
private static ProtoId<JobPrototype> _defaultJob = "Passenger";
|
||||
|
||||
public IdCardConsoleWindow(IdCardConsoleBoundUserInterface owner, IPrototypeManager prototypeManager,
|
||||
List<ProtoId<AccessLevelPrototype>> accessLevels)
|
||||
{
|
||||
@@ -65,7 +68,6 @@ namespace Content.Client.Access.UI
|
||||
}
|
||||
|
||||
JobPresetOptionButton.OnItemSelected += SelectJobPreset;
|
||||
|
||||
_accessButtons.Populate(accessLevels, prototypeManager);
|
||||
AccessLevelControlContainer.AddChild(_accessButtons);
|
||||
|
||||
@@ -172,11 +174,15 @@ namespace Content.Client.Access.UI
|
||||
new List<ProtoId<AccessLevelPrototype>>());
|
||||
|
||||
var jobIndex = _jobPrototypeIds.IndexOf(state.TargetIdJobPrototype);
|
||||
if (jobIndex >= 0)
|
||||
// If the job index is < 0 that means they don't have a job registered in the station records.
|
||||
// For example, a new ID from a box would have no job index.
|
||||
if (jobIndex < 0)
|
||||
{
|
||||
JobPresetOptionButton.SelectId(jobIndex);
|
||||
jobIndex = _jobPrototypeIds.IndexOf(_defaultJob);
|
||||
}
|
||||
|
||||
JobPresetOptionButton.SelectId(jobIndex);
|
||||
|
||||
_lastFullName = state.TargetIdFullName;
|
||||
_lastJobTitle = state.TargetIdJobTitle;
|
||||
_lastJobProto = state.TargetIdJobPrototype;
|
||||
|
||||
@@ -16,14 +16,17 @@ namespace Content.Client.Administration.UI.Bwoink
|
||||
|
||||
Bwoink.ChannelSelector.OnSelectionChanged += sel =>
|
||||
{
|
||||
if (sel is not null)
|
||||
if (sel is null)
|
||||
{
|
||||
Title = $"{sel.CharacterName} / {sel.Username}";
|
||||
Title = Loc.GetString("bwoink-none-selected");
|
||||
return;
|
||||
}
|
||||
|
||||
if (sel.OverallPlaytime != null)
|
||||
{
|
||||
Title += $" | {Loc.GetString("generic-playtime-title")}: {sel.PlaytimeString}";
|
||||
}
|
||||
Title = $"{sel.CharacterName} / {sel.Username}";
|
||||
|
||||
if (sel.OverallPlaytime != null)
|
||||
{
|
||||
Title += $" | {Loc.GetString("generic-playtime-title")}: {sel.PlaytimeString}";
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ namespace Content.Client.Administration.UI.CustomControls
|
||||
private List<PlayerInfo> _playerList = new();
|
||||
private readonly List<PlayerInfo> _sortedPlayerList = new();
|
||||
|
||||
public event Action<PlayerInfo>? OnSelectionChanged;
|
||||
public event Action<PlayerInfo?>? OnSelectionChanged;
|
||||
public IReadOnlyList<PlayerInfo> PlayerInfo => _playerList;
|
||||
|
||||
public Func<PlayerInfo, string, string>? OverrideText;
|
||||
@@ -41,12 +41,19 @@ namespace Content.Client.Administration.UI.CustomControls
|
||||
PlayerListContainer.ItemPressed += PlayerListItemPressed;
|
||||
PlayerListContainer.ItemKeyBindDown += PlayerListItemKeyBindDown;
|
||||
PlayerListContainer.GenerateItem += GenerateButton;
|
||||
PlayerListContainer.NoItemSelected += PlayerListNoItemSelected;
|
||||
PopulateList(_adminSystem.PlayerList);
|
||||
FilterLineEdit.OnTextChanged += _ => FilterList();
|
||||
_adminSystem.PlayerListChanged += PopulateList;
|
||||
BackgroundPanel.PanelOverride = new StyleBoxFlat {BackgroundColor = new Color(32, 32, 40)};
|
||||
}
|
||||
|
||||
private void PlayerListNoItemSelected()
|
||||
{
|
||||
_selectedPlayer = null;
|
||||
OnSelectionChanged?.Invoke(null);
|
||||
}
|
||||
|
||||
private void PlayerListItemPressed(BaseButton.ButtonEventArgs? args, ListData? data)
|
||||
{
|
||||
if (args == null || data is not PlayerListData {Info: var selectedPlayer})
|
||||
|
||||
@@ -1,21 +1,19 @@
|
||||
<Control xmlns="https://spacestation14.io"
|
||||
xmlns:pt="clr-namespace:Content.Client.Administration.UI.Tabs.PlayerTab"
|
||||
xmlns:cc="clr-namespace:Content.Client.Administration.UI.CustomControls">
|
||||
xmlns:cc="clr-namespace:Content.Client.Administration.UI.CustomControls"
|
||||
xmlns:co="clr-namespace:Content.Client.UserInterface.Controls">
|
||||
<BoxContainer Orientation="Vertical">
|
||||
<BoxContainer Orientation="Horizontal">
|
||||
<Label Name="PlayerCount" HorizontalExpand="True" SizeFlagsStretchRatio="0.50"
|
||||
Text="{Loc Player Count}" />
|
||||
<Button Name="ShowDisconnectedButton" HorizontalExpand="True" SizeFlagsStretchRatio="0.25"
|
||||
Text="{Loc player-tab-show-disconnected}" ToggleMode="True"/>
|
||||
<Button Name="OverlayButton" HorizontalExpand="True" SizeFlagsStretchRatio="0.25"
|
||||
Text="{Loc player-tab-overlay}" ToggleMode="True"/>
|
||||
<Label Name="PlayerCount" HorizontalExpand="True" Text="{Loc Player Count}" />
|
||||
<LineEdit Name="SearchLineEdit" HorizontalExpand="True"
|
||||
PlaceHolder="{Loc player-tab-filter-line-edit-placeholder}" />
|
||||
<Button Name="ShowDisconnectedButton" HorizontalExpand="True"
|
||||
Text="{Loc player-tab-show-disconnected}" ToggleMode="True" />
|
||||
<Button Name="OverlayButton" HorizontalExpand="True" Text="{Loc player-tab-overlay}" ToggleMode="True" />
|
||||
</BoxContainer>
|
||||
<Control MinSize="0 5" />
|
||||
<ScrollContainer HorizontalExpand="True" VerticalExpand="True">
|
||||
<BoxContainer Orientation="Vertical" Name="PlayerList">
|
||||
<pt:PlayerTabHeader Name="ListHeader" />
|
||||
<cc:HSeparator />
|
||||
</BoxContainer>
|
||||
</ScrollContainer>
|
||||
<Control MinSize="0 5"/>
|
||||
<pt:PlayerTabHeader Name="ListHeader"/>
|
||||
<cc:HSeparator/>
|
||||
<co:SearchListContainer Name="SearchList" Access="Public" VerticalExpand="True"/>
|
||||
</BoxContainer>
|
||||
</Control>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Linq;
|
||||
using Content.Client.Administration.Systems;
|
||||
using Content.Client.UserInterface.Controls;
|
||||
using Content.Shared.Administration;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.Graphics;
|
||||
@@ -28,15 +29,14 @@ namespace Content.Client.Administration.UI.Tabs.PlayerTab
|
||||
private bool _ascending = true;
|
||||
private bool _showDisconnected;
|
||||
|
||||
public event Action<PlayerTabEntry, GUIBoundKeyEventArgs>? OnEntryKeyBindDown;
|
||||
public event Action<GUIBoundKeyEventArgs, ListData>? OnEntryKeyBindDown;
|
||||
|
||||
public PlayerTab()
|
||||
{
|
||||
IoCManager.InjectDependencies(this);
|
||||
_adminSystem = _entManager.System<AdminSystem>();
|
||||
RobustXamlLoader.Load(this);
|
||||
RefreshPlayerList(_adminSystem.PlayerList);
|
||||
|
||||
_adminSystem = _entManager.System<AdminSystem>();
|
||||
_adminSystem.PlayerListChanged += RefreshPlayerList;
|
||||
_adminSystem.OverlayEnabled += OverlayEnabled;
|
||||
_adminSystem.OverlayDisabled += OverlayDisabled;
|
||||
@@ -46,8 +46,17 @@ namespace Content.Client.Administration.UI.Tabs.PlayerTab
|
||||
|
||||
ListHeader.BackgroundColorPanel.PanelOverride = new StyleBoxFlat(_altColor);
|
||||
ListHeader.OnHeaderClicked += HeaderClicked;
|
||||
|
||||
SearchList.SearchBar = SearchLineEdit;
|
||||
SearchList.GenerateItem += GenerateButton;
|
||||
SearchList.DataFilterCondition += DataFilterCondition;
|
||||
SearchList.ItemKeyBindDown += (args, data) => OnEntryKeyBindDown?.Invoke(args, data);
|
||||
|
||||
RefreshPlayerList(_adminSystem.PlayerList);
|
||||
}
|
||||
|
||||
#region Antag Overlay
|
||||
|
||||
private void OverlayEnabled()
|
||||
{
|
||||
OverlayButton.Pressed = true;
|
||||
@@ -70,6 +79,8 @@ namespace Content.Client.Administration.UI.Tabs.PlayerTab
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private void ShowDisconnectedPressed(ButtonEventArgs args)
|
||||
{
|
||||
_showDisconnected = args.Button.Pressed;
|
||||
@@ -92,14 +103,10 @@ namespace Content.Client.Administration.UI.Tabs.PlayerTab
|
||||
}
|
||||
}
|
||||
|
||||
#region ListContainer
|
||||
|
||||
private void RefreshPlayerList(IReadOnlyList<PlayerInfo> players)
|
||||
{
|
||||
foreach (var child in PlayerList.Children.ToArray())
|
||||
{
|
||||
if (child is PlayerTabEntry)
|
||||
child.Dispose();
|
||||
}
|
||||
|
||||
_players = players;
|
||||
PlayerCount.Text = $"Players: {_playerMan.PlayerCount}";
|
||||
|
||||
@@ -108,29 +115,66 @@ namespace Content.Client.Administration.UI.Tabs.PlayerTab
|
||||
|
||||
UpdateHeaderSymbols();
|
||||
|
||||
var useAltColor = false;
|
||||
foreach (var player in sortedPlayers)
|
||||
{
|
||||
if (!_showDisconnected && !player.Connected)
|
||||
continue;
|
||||
|
||||
var entry = new PlayerTabEntry(player.Username,
|
||||
player.CharacterName,
|
||||
player.IdentityName,
|
||||
player.StartingJob,
|
||||
player.Antag ? "YES" : "NO",
|
||||
new StyleBoxFlat(useAltColor ? _altColor : _defaultColor),
|
||||
player.Connected,
|
||||
player.PlaytimeString);
|
||||
entry.PlayerEntity = player.NetEntity;
|
||||
entry.OnKeyBindDown += args => OnEntryKeyBindDown?.Invoke(entry, args);
|
||||
entry.ToolTip = Loc.GetString("player-tab-entry-tooltip");
|
||||
PlayerList.AddChild(entry);
|
||||
|
||||
useAltColor ^= true;
|
||||
}
|
||||
SearchList.PopulateList(sortedPlayers.Select(info => new PlayerListData(info,
|
||||
$"{info.Username} {info.CharacterName} {info.IdentityName} {info.StartingJob}"))
|
||||
.ToList());
|
||||
}
|
||||
|
||||
private void GenerateButton(ListData data, ListContainerButton button)
|
||||
{
|
||||
if (data is not PlayerListData { Info: var player})
|
||||
return;
|
||||
|
||||
var entry = new PlayerTabEntry(player, new StyleBoxFlat(button.Index % 2 == 0 ? _altColor : _defaultColor));
|
||||
button.AddChild(entry);
|
||||
button.ToolTip = $"{player.Username}, {player.CharacterName}, {player.IdentityName}, {player.StartingJob}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether <paramref name="filter"/> is contained in <paramref name="listData"/>.FilteringString.
|
||||
/// If all characters are lowercase, the comparison ignores case.
|
||||
/// If there is an uppercase character, the comparison is case sensitive.
|
||||
/// </summary>
|
||||
/// <param name="filter"></param>
|
||||
/// <param name="listData"></param>
|
||||
/// <returns>Whether <paramref name="filter"/> is contained in <paramref name="listData"/>.FilteringString.</returns>
|
||||
private bool DataFilterCondition(string filter, ListData listData)
|
||||
{
|
||||
if (listData is not PlayerListData {Info: var info, FilteringString: var playerString})
|
||||
return false;
|
||||
|
||||
if (!_showDisconnected && !info.Connected)
|
||||
return false;
|
||||
|
||||
if (IsAllLower(filter))
|
||||
{
|
||||
if (!playerString.Contains(filter, StringComparison.CurrentCultureIgnoreCase))
|
||||
return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!playerString.Contains(filter))
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool IsAllLower(string input)
|
||||
{
|
||||
foreach (var c in input)
|
||||
{
|
||||
if (char.IsLetter(c) && !char.IsLower(c))
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Header
|
||||
|
||||
private void UpdateHeaderSymbols()
|
||||
{
|
||||
ListHeader.ResetHeaderText();
|
||||
@@ -174,5 +218,9 @@ namespace Content.Client.Administration.UI.Tabs.PlayerTab
|
||||
|
||||
RefreshPlayerList(_adminSystem.PlayerList);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
public record PlayerListData(PlayerInfo Info, string FilteringString) : ListData;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<ContainerButton xmlns="https://spacestation14.io"
|
||||
xmlns:customControls="clr-namespace:Content.Client.Administration.UI.CustomControls">
|
||||
<PanelContainer Name="BackgroundColorPanel"/>
|
||||
<PanelContainer xmlns="https://spacestation14.io"
|
||||
xmlns:customControls="clr-namespace:Content.Client.Administration.UI.CustomControls"
|
||||
Name="BackgroundColorPanel">
|
||||
<BoxContainer Orientation="Horizontal"
|
||||
HorizontalExpand="True"
|
||||
SeparationOverride="4">
|
||||
@@ -15,17 +15,18 @@
|
||||
ClipText="True"/>
|
||||
<customControls:VSeparator/>
|
||||
<Label Name="JobLabel"
|
||||
SizeFlagsStretchRatio="3"
|
||||
SizeFlagsStretchRatio="2"
|
||||
HorizontalExpand="True"
|
||||
ClipText="True"/>
|
||||
<customControls:VSeparator/>
|
||||
<Label Name="AntagonistLabel"
|
||||
SizeFlagsStretchRatio="2"
|
||||
SizeFlagsStretchRatio="1"
|
||||
HorizontalExpand="True"
|
||||
ClipText="True"/>
|
||||
<customControls:VSeparator/>
|
||||
<Label Name="OverallPlaytimeLabel"
|
||||
SizeFlagsStretchRatio="2"
|
||||
SizeFlagsStretchRatio="1"
|
||||
HorizontalExpand="True"
|
||||
ClipText="True"/>
|
||||
</BoxContainer>
|
||||
</ContainerButton>
|
||||
</PanelContainer>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Content.Shared.Administration;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
@@ -6,23 +7,24 @@ using Robust.Client.UserInterface.XAML;
|
||||
namespace Content.Client.Administration.UI.Tabs.PlayerTab;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class PlayerTabEntry : ContainerButton
|
||||
public sealed partial class PlayerTabEntry : PanelContainer
|
||||
{
|
||||
public NetEntity? PlayerEntity;
|
||||
|
||||
public PlayerTabEntry(string username, string character, string identity, string job, string antagonist, StyleBox styleBox, bool connected, string overallPlaytime)
|
||||
public PlayerTabEntry(PlayerInfo player, StyleBoxFlat styleBoxFlat)
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
|
||||
UsernameLabel.Text = username;
|
||||
if (!connected)
|
||||
UsernameLabel.Text = player.Username;
|
||||
if (!player.Connected)
|
||||
UsernameLabel.StyleClasses.Add("Disabled");
|
||||
JobLabel.Text = job;
|
||||
CharacterLabel.Text = character;
|
||||
if (identity != character)
|
||||
CharacterLabel.Text += $" [{identity}]";
|
||||
AntagonistLabel.Text = antagonist;
|
||||
BackgroundColorPanel.PanelOverride = styleBox;
|
||||
OverallPlaytimeLabel.Text = overallPlaytime;
|
||||
JobLabel.Text = player.StartingJob;
|
||||
CharacterLabel.Text = player.CharacterName;
|
||||
if (player.IdentityName != player.CharacterName)
|
||||
CharacterLabel.Text += $" [{player.IdentityName}]";
|
||||
AntagonistLabel.Text = Loc.GetString(player.Antag ? "player-tab-is-antag-yes" : "player-tab-is-antag-no");
|
||||
BackgroundColorPanel.PanelOverride = styleBoxFlat;
|
||||
OverallPlaytimeLabel.Text = player.PlaytimeString;
|
||||
PlayerEntity = player.NetEntity;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,23 +19,25 @@
|
||||
MouseFilter="Pass"/>
|
||||
<cc:VSeparator/>
|
||||
<Label Name="JobLabel"
|
||||
SizeFlagsStretchRatio="3"
|
||||
SizeFlagsStretchRatio="2"
|
||||
HorizontalExpand="True"
|
||||
ClipText="True"
|
||||
Text="{Loc player-tab-job}"
|
||||
MouseFilter="Pass"/>
|
||||
<cc:VSeparator/>
|
||||
<Label Name="AntagonistLabel"
|
||||
SizeFlagsStretchRatio="2"
|
||||
SizeFlagsStretchRatio="1"
|
||||
HorizontalExpand="True"
|
||||
ClipText="True"
|
||||
Text="{Loc player-tab-antagonist}"
|
||||
MouseFilter="Pass"/>
|
||||
<cc:VSeparator/>
|
||||
<Label Name="PlaytimeLabel"
|
||||
SizeFlagsStretchRatio="2"
|
||||
SizeFlagsStretchRatio="1"
|
||||
HorizontalExpand="True"
|
||||
ClipText="True"
|
||||
Text="{Loc player-tab-playtime}"
|
||||
MouseFilter="Pass"/>
|
||||
MouseFilter="Pass"
|
||||
ToolTip="{Loc player-tab-entry-tooltip}"/>
|
||||
</BoxContainer>
|
||||
</Control>
|
||||
|
||||
@@ -24,8 +24,11 @@ public sealed class ChasmFallingVisualsSystem : EntitySystem
|
||||
|
||||
private void OnComponentInit(EntityUid uid, ChasmFallingComponent component, ComponentInit args)
|
||||
{
|
||||
if (!TryComp<SpriteComponent>(uid, out var sprite))
|
||||
if (!TryComp<SpriteComponent>(uid, out var sprite) ||
|
||||
TerminatingOrDeleted(uid))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
component.OriginalScale = sprite.Scale;
|
||||
|
||||
|
||||
@@ -239,9 +239,7 @@ namespace Content.Client.Examine
|
||||
|
||||
if (knowTarget)
|
||||
{
|
||||
// TODO: FormattedMessage.RemoveMarkupPermissive
|
||||
// var itemName = FormattedMessage.RemoveMarkupPermissive(Identity.Name(target, EntityManager, player));
|
||||
var itemName = FormattedMessage.FromMarkupPermissive(Identity.Name(target, EntityManager, player)).ToString();
|
||||
var itemName = FormattedMessage.EscapeText(Identity.Name(target, EntityManager, player));
|
||||
var labelMessage = FormattedMessage.FromMarkupPermissive($"[bold]{itemName}[/bold]");
|
||||
var label = new RichTextLabel();
|
||||
label.SetMessage(labelMessage);
|
||||
@@ -250,7 +248,7 @@ namespace Content.Client.Examine
|
||||
else
|
||||
{
|
||||
var label = new RichTextLabel();
|
||||
label.SetMessage(FormattedMessage.FromMarkup("[bold]???[/bold]"));
|
||||
label.SetMessage(FormattedMessage.FromMarkupOrThrow("[bold]???[/bold]"));
|
||||
hBox.AddChild(label);
|
||||
}
|
||||
|
||||
|
||||
@@ -148,7 +148,7 @@ public sealed class GuidebookSystem : EntitySystem
|
||||
|
||||
public void FakeClientActivateInWorld(EntityUid activated)
|
||||
{
|
||||
var activateMsg = new ActivateInWorldEvent(GetGuidebookUser(), activated);
|
||||
var activateMsg = new ActivateInWorldEvent(GetGuidebookUser(), activated, true);
|
||||
RaiseLocalEvent(activated, activateMsg);
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,6 @@ using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Shared.Input;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Prototypes;
|
||||
using static Content.Client.Inventory.ClientInventorySystem;
|
||||
using static Robust.Client.UserInterface.Control;
|
||||
|
||||
@@ -53,9 +52,13 @@ namespace Content.Client.Inventory
|
||||
_inv = EntMan.System<InventorySystem>();
|
||||
_cuffable = EntMan.System<SharedCuffableSystem>();
|
||||
|
||||
// TODO update name when identity changes
|
||||
var title = Loc.GetString("strippable-bound-user-interface-stripping-menu-title", ("ownerName", Identity.Name(Owner, EntMan)));
|
||||
_strippingMenu = new StrippingMenu(title, this);
|
||||
_strippingMenu.OnClose += Close;
|
||||
|
||||
// TODO use global entity
|
||||
// BUIs are opened and closed while applying comp sates, so spawning entities here is probably not the best idea.
|
||||
_virtualHiddenEntity = EntMan.SpawnEntity(HiddenPocketEntityId, MapCoordinates.Nullspace);
|
||||
}
|
||||
|
||||
|
||||
@@ -23,7 +23,6 @@ using Robust.Client.UserInterface.Controllers;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization.Manager;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client.Lobby;
|
||||
@@ -71,12 +70,9 @@ public sealed partial class LobbyUIController : UIController, IOnStateEntered<Lo
|
||||
_profileEditor?.RefreshFlavorText();
|
||||
});
|
||||
|
||||
_configurationManager.OnValueChanged(CCVars.GameRoleTimers, args =>
|
||||
{
|
||||
_profileEditor?.RefreshAntags();
|
||||
_profileEditor?.RefreshJobs();
|
||||
_profileEditor?.RefreshLoadouts();
|
||||
});
|
||||
_configurationManager.OnValueChanged(CCVars.GameRoleTimers, _ => RefreshProfileEditor());
|
||||
|
||||
_configurationManager.OnValueChanged(CCVars.GameRoleWhitelist, _ => RefreshProfileEditor());
|
||||
}
|
||||
|
||||
private LobbyCharacterPreviewPanel? GetLobbyPreview()
|
||||
@@ -194,6 +190,13 @@ public sealed partial class LobbyUIController : UIController, IOnStateEntered<Lo
|
||||
PreviewPanel.SetSummaryText(humanoid.Summary);
|
||||
}
|
||||
|
||||
private void RefreshProfileEditor()
|
||||
{
|
||||
_profileEditor?.RefreshAntags();
|
||||
_profileEditor?.RefreshJobs();
|
||||
_profileEditor?.RefreshLoadouts();
|
||||
}
|
||||
|
||||
private void SaveProfile()
|
||||
{
|
||||
DebugTools.Assert(EditedProfile != null);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.Players;
|
||||
using Content.Shared.Players.JobWhitelist;
|
||||
using Content.Shared.Players.PlayTimeTracking;
|
||||
using Content.Shared.Roles;
|
||||
using Robust.Client;
|
||||
@@ -24,6 +25,7 @@ public sealed class JobRequirementsManager : ISharedPlaytimeManager
|
||||
|
||||
private readonly Dictionary<string, TimeSpan> _roles = new();
|
||||
private readonly List<string> _roleBans = new();
|
||||
private readonly List<string> _jobWhitelists = new();
|
||||
|
||||
private ISawmill _sawmill = default!;
|
||||
|
||||
@@ -36,6 +38,7 @@ public sealed class JobRequirementsManager : ISharedPlaytimeManager
|
||||
// Yeah the client manager handles role bans and playtime but the server ones are separate DEAL.
|
||||
_net.RegisterNetMessage<MsgRoleBans>(RxRoleBans);
|
||||
_net.RegisterNetMessage<MsgPlayTime>(RxPlayTime);
|
||||
_net.RegisterNetMessage<MsgJobWhitelist>(RxJobWhitelist);
|
||||
|
||||
_client.RunLevelChanged += ClientOnRunLevelChanged;
|
||||
}
|
||||
@@ -79,6 +82,13 @@ public sealed class JobRequirementsManager : ISharedPlaytimeManager
|
||||
Updated?.Invoke();
|
||||
}
|
||||
|
||||
private void RxJobWhitelist(MsgJobWhitelist message)
|
||||
{
|
||||
_jobWhitelists.Clear();
|
||||
_jobWhitelists.AddRange(message.Whitelist);
|
||||
Updated?.Invoke();
|
||||
}
|
||||
|
||||
public bool IsAllowed(JobPrototype job, [NotNullWhen(false)] out FormattedMessage? reason)
|
||||
{
|
||||
reason = null;
|
||||
@@ -89,6 +99,9 @@ public sealed class JobRequirementsManager : ISharedPlaytimeManager
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!CheckWhitelist(job, out reason))
|
||||
return false;
|
||||
|
||||
var player = _playerManager.LocalSession;
|
||||
if (player == null)
|
||||
return true;
|
||||
@@ -116,6 +129,21 @@ public sealed class JobRequirementsManager : ISharedPlaytimeManager
|
||||
return reason == null;
|
||||
}
|
||||
|
||||
public bool CheckWhitelist(JobPrototype job, [NotNullWhen(false)] out FormattedMessage? reason)
|
||||
{
|
||||
reason = default;
|
||||
if (!_cfg.GetCVar(CCVars.GameRoleWhitelist))
|
||||
return true;
|
||||
|
||||
if (job.Whitelisted && !_jobWhitelists.Contains(job.ID))
|
||||
{
|
||||
reason = FormattedMessage.FromUnformatted(Loc.GetString("role-not-whitelisted"));
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public TimeSpan FetchOverallPlaytime()
|
||||
{
|
||||
return _roles.TryGetValue("Overall", out var overallPlaytime) ? overallPlaytime : TimeSpan.Zero;
|
||||
|
||||
@@ -1,21 +1,27 @@
|
||||
using Content.Client.Power.EntitySystems;
|
||||
using Content.Shared.Popups;
|
||||
using Content.Shared.Power.Components;
|
||||
using Content.Shared.Power.EntitySystems;
|
||||
using Content.Shared.UserInterface;
|
||||
using Content.Shared.Wires;
|
||||
|
||||
namespace Content.Client.Power;
|
||||
|
||||
public sealed class ActivatableUIRequiresPowerSystem : EntitySystem
|
||||
public sealed class ActivatableUIRequiresPowerSystem : SharedActivatableUIRequiresPowerSystem
|
||||
{
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
[Dependency] private readonly SharedPopupSystem _popup = default!;
|
||||
|
||||
SubscribeLocalEvent<ActivatableUIRequiresPowerComponent, ActivatableUIOpenAttemptEvent>(OnActivate);
|
||||
}
|
||||
|
||||
private void OnActivate(EntityUid uid, ActivatableUIRequiresPowerComponent component, ActivatableUIOpenAttemptEvent args)
|
||||
protected override void OnActivate(Entity<ActivatableUIRequiresPowerComponent> ent, ref ActivatableUIOpenAttemptEvent args)
|
||||
{
|
||||
// Client can't predict the power properly at the moment so rely upon the server to do it.
|
||||
if (args.Cancelled || this.IsPowered(ent.Owner, EntityManager))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (TryComp<WiresPanelComponent>(ent.Owner, out var panel) && panel.Open)
|
||||
return;
|
||||
|
||||
_popup.PopupClient(Loc.GetString("base-computer-ui-component-not-powered", ("machine", ent.Owner)), args.User, args.User);
|
||||
args.Cancel();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
using Content.Shared.Power.Components;
|
||||
|
||||
namespace Content.Client.Power.Components;
|
||||
|
||||
[RegisterComponent]
|
||||
public sealed partial class ApcPowerReceiverComponent : SharedApcPowerReceiverComponent
|
||||
{
|
||||
}
|
||||
23
Content.Client/Power/EntitySystems/PowerReceiverSystem.cs
Normal file
23
Content.Client/Power/EntitySystems/PowerReceiverSystem.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
using Content.Client.Power.Components;
|
||||
using Content.Shared.Power.Components;
|
||||
using Content.Shared.Power.EntitySystems;
|
||||
using Robust.Shared.GameStates;
|
||||
|
||||
namespace Content.Client.Power.EntitySystems;
|
||||
|
||||
public sealed class PowerReceiverSystem : SharedPowerReceiverSystem
|
||||
{
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
SubscribeLocalEvent<ApcPowerReceiverComponent, ComponentHandleState>(OnHandleState);
|
||||
}
|
||||
|
||||
private void OnHandleState(EntityUid uid, ApcPowerReceiverComponent component, ref ComponentHandleState args)
|
||||
{
|
||||
if (args.Current is not ApcPowerReceiverComponentState state)
|
||||
return;
|
||||
|
||||
component.Powered = state.Powered;
|
||||
}
|
||||
}
|
||||
16
Content.Client/Power/EntitySystems/StaticPowerSystem.cs
Normal file
16
Content.Client/Power/EntitySystems/StaticPowerSystem.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
using Content.Client.Power.Components;
|
||||
|
||||
namespace Content.Client.Power.EntitySystems;
|
||||
|
||||
public static class StaticPowerSystem
|
||||
{
|
||||
// Using this makes the call shorter.
|
||||
// ReSharper disable once UnusedParameter.Global
|
||||
public static bool IsPowered(this EntitySystem system, EntityUid uid, IEntityManager entManager, ApcPowerReceiverComponent? receiver = null)
|
||||
{
|
||||
if (receiver == null && !entManager.TryGetComponent(uid, out receiver))
|
||||
return false;
|
||||
|
||||
return receiver.Powered;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
using Content.Shared.Store;
|
||||
using JetBrains.Annotations;
|
||||
using System.Linq;
|
||||
using Content.Shared.Store.Components;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Client.Store.Ui;
|
||||
@@ -13,9 +14,6 @@ public sealed class StoreBoundUserInterface : BoundUserInterface
|
||||
[ViewVariables]
|
||||
private StoreMenu? _menu;
|
||||
|
||||
[ViewVariables]
|
||||
private string _windowName = Loc.GetString("store-ui-default-title");
|
||||
|
||||
[ViewVariables]
|
||||
private string _search = string.Empty;
|
||||
|
||||
@@ -28,7 +26,9 @@ public sealed class StoreBoundUserInterface : BoundUserInterface
|
||||
|
||||
protected override void Open()
|
||||
{
|
||||
_menu = new StoreMenu(_windowName);
|
||||
_menu = new StoreMenu();
|
||||
if (EntMan.TryGetComponent<StoreComponent>(Owner, out var store))
|
||||
_menu.Title = Loc.GetString(store.Name);
|
||||
|
||||
_menu.OpenCentered();
|
||||
_menu.OnClose += Close;
|
||||
@@ -64,25 +64,15 @@ public sealed class StoreBoundUserInterface : BoundUserInterface
|
||||
{
|
||||
base.UpdateState(state);
|
||||
|
||||
if (_menu == null)
|
||||
return;
|
||||
|
||||
switch (state)
|
||||
{
|
||||
case StoreUpdateState msg:
|
||||
_listings = msg.Listings;
|
||||
|
||||
_menu.UpdateBalance(msg.Balance);
|
||||
_menu?.UpdateBalance(msg.Balance);
|
||||
UpdateListingsWithSearchFilter();
|
||||
_menu.SetFooterVisibility(msg.ShowFooter);
|
||||
_menu.UpdateRefund(msg.AllowRefund);
|
||||
break;
|
||||
case StoreInitializeState msg:
|
||||
_windowName = msg.Name;
|
||||
if (_menu != null && _menu.Window != null)
|
||||
{
|
||||
_menu.Window.Title = msg.Name;
|
||||
}
|
||||
_menu?.SetFooterVisibility(msg.ShowFooter);
|
||||
_menu?.UpdateRefund(msg.AllowRefund);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ public sealed partial class StoreMenu : DefaultWindow
|
||||
|
||||
private List<ListingData> _cachedListings = new();
|
||||
|
||||
public StoreMenu(string name)
|
||||
public StoreMenu()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
IoCManager.InjectDependencies(this);
|
||||
@@ -40,9 +40,6 @@ public sealed partial class StoreMenu : DefaultWindow
|
||||
WithdrawButton.OnButtonDown += OnWithdrawButtonDown;
|
||||
RefundButton.OnButtonDown += OnRefundButtonDown;
|
||||
SearchBar.OnTextChanged += _ => SearchTextUpdated?.Invoke(this, SearchBar.Text);
|
||||
|
||||
if (Window != null)
|
||||
Window.Title = name;
|
||||
}
|
||||
|
||||
public void UpdateBalance(Dictionary<ProtoId<CurrencyPrototype>, FixedPoint2> balance)
|
||||
|
||||
@@ -8,7 +8,8 @@ using Robust.Shared.Map;
|
||||
|
||||
namespace Content.Client.UserInterface.Controls;
|
||||
|
||||
public sealed class ListContainer : Control
|
||||
[Virtual]
|
||||
public class ListContainer : Control
|
||||
{
|
||||
public const string StylePropertySeparation = "separation";
|
||||
public const string StyleClassListContainerButton = "list-container-button";
|
||||
@@ -21,9 +22,26 @@ public sealed class ListContainer : Control
|
||||
set => _buttonGroup = value ? new ButtonGroup() : null;
|
||||
}
|
||||
public bool Toggle { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Called when creating a button on the UI.
|
||||
/// The provided <see cref="ListContainerButton"/> is the generated button that Controls should be parented to.
|
||||
/// </summary>
|
||||
public Action<ListData, ListContainerButton>? GenerateItem;
|
||||
public Action<BaseButton.ButtonEventArgs?, ListData?>? ItemPressed;
|
||||
public Action<GUIBoundKeyEventArgs, ListData?>? ItemKeyBindDown;
|
||||
|
||||
/// <inheritdoc cref="BaseButton.OnPressed"/>
|
||||
public Action<BaseButton.ButtonEventArgs, ListData>? ItemPressed;
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when a KeyBind is pressed on a ListContainerButton.
|
||||
/// </summary>
|
||||
public Action<GUIBoundKeyEventArgs, ListData>? ItemKeyBindDown;
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when the selected item does not exist in the new data when PopulateList is called.
|
||||
/// </summary>
|
||||
public Action? NoItemSelected;
|
||||
|
||||
public IReadOnlyList<ListData> Data => _data;
|
||||
|
||||
private const int DefaultSeparation = 3;
|
||||
@@ -72,11 +90,11 @@ public sealed class ListContainer : Control
|
||||
_vScrollBar.OnValueChanged += ScrollValueChanged;
|
||||
}
|
||||
|
||||
public void PopulateList(IReadOnlyList<ListData> data)
|
||||
public virtual void PopulateList(IReadOnlyList<ListData> data)
|
||||
{
|
||||
if ((_itemHeight == 0 || _data is {Count: 0}) && data.Count > 0)
|
||||
{
|
||||
ListContainerButton control = new(data[0]);
|
||||
ListContainerButton control = new(data[0], 0);
|
||||
GenerateItem?.Invoke(data[0], control);
|
||||
control.Measure(Vector2Helpers.Infinity);
|
||||
_itemHeight = control.DesiredSize.Y;
|
||||
@@ -97,7 +115,7 @@ public sealed class ListContainer : Control
|
||||
if (_selected != null && !data.Contains(_selected))
|
||||
{
|
||||
_selected = null;
|
||||
ItemPressed?.Invoke(null, null);
|
||||
NoItemSelected?.Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,7 +134,7 @@ public sealed class ListContainer : Control
|
||||
if (_buttons.TryGetValue(data, out var button) && Toggle)
|
||||
button.Pressed = true;
|
||||
_selected = data;
|
||||
button ??= new ListContainerButton(data);
|
||||
button ??= new ListContainerButton(data, _data.IndexOf(data));
|
||||
OnItemPressed(new BaseButton.ButtonEventArgs(button,
|
||||
new GUIBoundKeyEventArgs(EngineKeyFunctions.UIClick, BoundKeyState.Up,
|
||||
new ScreenCoordinates(0, 0, WindowId.Main), true, Vector2.Zero, Vector2.Zero)));
|
||||
@@ -260,7 +278,7 @@ public sealed class ListContainer : Control
|
||||
toRemove.Remove(data);
|
||||
else
|
||||
{
|
||||
button = new ListContainerButton(data);
|
||||
button = new ListContainerButton(data, i);
|
||||
button.OnPressed += OnItemPressed;
|
||||
button.OnKeyBindDown += args => OnItemKeyBindDown(button, args);
|
||||
button.ToggleMode = Toggle;
|
||||
@@ -360,11 +378,14 @@ public sealed class ListContainer : Control
|
||||
public sealed class ListContainerButton : ContainerButton, IEntityControl
|
||||
{
|
||||
public readonly ListData Data;
|
||||
|
||||
public readonly int Index;
|
||||
// public PanelContainer Background;
|
||||
|
||||
public ListContainerButton(ListData data)
|
||||
public ListContainerButton(ListData data, int index)
|
||||
{
|
||||
Data = data;
|
||||
Index = index;
|
||||
// AddChild(Background = new PanelContainer
|
||||
// {
|
||||
// HorizontalExpand = true,
|
||||
|
||||
68
Content.Client/UserInterface/Controls/SearchListContainer.cs
Normal file
68
Content.Client/UserInterface/Controls/SearchListContainer.cs
Normal file
@@ -0,0 +1,68 @@
|
||||
using System.Linq;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
|
||||
namespace Content.Client.UserInterface.Controls;
|
||||
|
||||
public sealed class SearchListContainer : ListContainer
|
||||
{
|
||||
private LineEdit? _searchBar;
|
||||
private List<ListData> _unfilteredData = new();
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="LineEdit"/> that is used to filter the list data.
|
||||
/// </summary>
|
||||
public LineEdit? SearchBar
|
||||
{
|
||||
get => _searchBar;
|
||||
set
|
||||
{
|
||||
if (_searchBar is not null)
|
||||
_searchBar.OnTextChanged -= FilterList;
|
||||
|
||||
_searchBar = value;
|
||||
|
||||
if (_searchBar is null)
|
||||
return;
|
||||
|
||||
_searchBar.OnTextChanged += FilterList;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs over the ListData to determine if it should pass the filter.
|
||||
/// </summary>
|
||||
public Func<string, ListData, bool>? DataFilterCondition = null;
|
||||
|
||||
public override void PopulateList(IReadOnlyList<ListData> data)
|
||||
{
|
||||
_unfilteredData = data.ToList();
|
||||
FilterList();
|
||||
}
|
||||
|
||||
private void FilterList(LineEdit.LineEditEventArgs obj)
|
||||
{
|
||||
FilterList();
|
||||
}
|
||||
|
||||
private void FilterList()
|
||||
{
|
||||
var filterText = SearchBar?.Text;
|
||||
|
||||
if (DataFilterCondition is null || string.IsNullOrEmpty(filterText))
|
||||
{
|
||||
base.PopulateList(_unfilteredData);
|
||||
return;
|
||||
}
|
||||
|
||||
var filteredData = new List<ListData>();
|
||||
foreach (var data in _unfilteredData)
|
||||
{
|
||||
if (!DataFilterCondition(filterText, data))
|
||||
continue;
|
||||
|
||||
filteredData.Add(data);
|
||||
}
|
||||
|
||||
base.PopulateList(filteredData);
|
||||
}
|
||||
}
|
||||
@@ -177,12 +177,15 @@ public sealed class AdminUIController : UIController,
|
||||
}
|
||||
}
|
||||
|
||||
private void PlayerTabEntryKeyBindDown(PlayerTabEntry entry, GUIBoundKeyEventArgs args)
|
||||
private void PlayerTabEntryKeyBindDown(GUIBoundKeyEventArgs args, ListData? data)
|
||||
{
|
||||
if (entry.PlayerEntity == null)
|
||||
if (data is not PlayerListData {Info: var info})
|
||||
return;
|
||||
|
||||
var entity = entry.PlayerEntity.Value;
|
||||
if (info.NetEntity == null)
|
||||
return;
|
||||
|
||||
var entity = info.NetEntity.Value;
|
||||
var function = args.Function;
|
||||
|
||||
if (function == EngineKeyFunctions.UIClick)
|
||||
|
||||
@@ -8,6 +8,7 @@ using Content.Shared.Verbs;
|
||||
using Robust.Client.Player;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controllers;
|
||||
using Robust.Shared.Collections;
|
||||
using Robust.Shared.Input;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
@@ -115,6 +116,13 @@ namespace Content.Client.Verbs.UI
|
||||
private void FillVerbPopup(ContextMenuPopup popup)
|
||||
{
|
||||
HashSet<string> listedCategories = new();
|
||||
var extras = new ValueList<string>(ExtraCategories.Count);
|
||||
|
||||
foreach (var cat in ExtraCategories)
|
||||
{
|
||||
extras.Add(cat.Text);
|
||||
}
|
||||
|
||||
foreach (var verb in CurrentVerbs)
|
||||
{
|
||||
if (verb.Category == null)
|
||||
@@ -122,17 +130,15 @@ namespace Content.Client.Verbs.UI
|
||||
var element = new VerbMenuElement(verb);
|
||||
_context.AddElement(popup, element);
|
||||
}
|
||||
else if (listedCategories.Add(verb.Category.Text))
|
||||
// Add the category if it's not an extra (this is to avoid shuffling if we're filling from server verbs response).
|
||||
else if (!extras.Contains(verb.Category.Text) && listedCategories.Add(verb.Category.Text))
|
||||
AddVerbCategory(verb.Category, popup);
|
||||
}
|
||||
|
||||
if (ExtraCategories != null)
|
||||
foreach (var category in ExtraCategories)
|
||||
{
|
||||
foreach (var category in ExtraCategories)
|
||||
{
|
||||
if (listedCategories.Add(category.Text))
|
||||
AddVerbCategory(category, popup);
|
||||
}
|
||||
if (listedCategories.Add(category.Text))
|
||||
AddVerbCategory(category, popup);
|
||||
}
|
||||
|
||||
popup.InvalidateMeasure();
|
||||
|
||||
69
Content.IntegrationTests/Pair/TestPair.Cvars.cs
Normal file
69
Content.IntegrationTests/Pair/TestPair.Cvars.cs
Normal file
@@ -0,0 +1,69 @@
|
||||
#nullable enable
|
||||
using System.Collections.Generic;
|
||||
using Content.Shared.CCVar;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.IntegrationTests.Pair;
|
||||
|
||||
public sealed partial class TestPair
|
||||
{
|
||||
private readonly Dictionary<string, object> _modifiedClientCvars = new();
|
||||
private readonly Dictionary<string, object> _modifiedServerCvars = new();
|
||||
|
||||
private void OnServerCvarChanged(CVarChangeInfo args)
|
||||
{
|
||||
_modifiedServerCvars.TryAdd(args.Name, args.OldValue);
|
||||
}
|
||||
|
||||
private void OnClientCvarChanged(CVarChangeInfo args)
|
||||
{
|
||||
_modifiedClientCvars.TryAdd(args.Name, args.OldValue);
|
||||
}
|
||||
|
||||
internal void ClearModifiedCvars()
|
||||
{
|
||||
_modifiedClientCvars.Clear();
|
||||
_modifiedServerCvars.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reverts any cvars that were modified during a test back to their original values.
|
||||
/// </summary>
|
||||
public async Task RevertModifiedCvars()
|
||||
{
|
||||
await Server.WaitPost(() =>
|
||||
{
|
||||
foreach (var (name, value) in _modifiedServerCvars)
|
||||
{
|
||||
if (Server.CfgMan.GetCVar(name).Equals(value))
|
||||
continue;
|
||||
Server.Log.Info($"Resetting cvar {name} to {value}");
|
||||
Server.CfgMan.SetCVar(name, value);
|
||||
}
|
||||
|
||||
// I just love order dependent cvars
|
||||
if (_modifiedServerCvars.TryGetValue(CCVars.PanicBunkerEnabled.Name, out var panik))
|
||||
Server.CfgMan.SetCVar(CCVars.PanicBunkerEnabled.Name, panik);
|
||||
|
||||
});
|
||||
|
||||
await Client.WaitPost(() =>
|
||||
{
|
||||
foreach (var (name, value) in _modifiedClientCvars)
|
||||
{
|
||||
if (Client.CfgMan.GetCVar(name).Equals(value))
|
||||
continue;
|
||||
|
||||
var flags = Client.CfgMan.GetCVarFlags(name);
|
||||
if (flags.HasFlag(CVar.REPLICATED) && flags.HasFlag(CVar.SERVER))
|
||||
continue;
|
||||
|
||||
Client.Log.Info($"Resetting cvar {name} to {value}");
|
||||
Client.CfgMan.SetCVar(name, value);
|
||||
}
|
||||
});
|
||||
|
||||
ClearModifiedCvars();
|
||||
}
|
||||
}
|
||||
@@ -40,6 +40,8 @@ public sealed partial class TestPair : IAsyncDisposable
|
||||
TestMap = null;
|
||||
}
|
||||
|
||||
await RevertModifiedCvars();
|
||||
|
||||
var usageTime = Watch.Elapsed;
|
||||
Watch.Restart();
|
||||
await _testOut.WriteLineAsync($"{nameof(CleanReturnAsync)}: Test borrowed pair {Id} for {usageTime.TotalMilliseconds} ms");
|
||||
@@ -132,6 +134,7 @@ public sealed partial class TestPair : IAsyncDisposable
|
||||
if (gameTicker.RunLevel != GameRunLevel.PreRoundLobby)
|
||||
{
|
||||
await testOut.WriteLineAsync($"Recycling: {Watch.Elapsed.TotalMilliseconds} ms: Restarting round.");
|
||||
Server.CfgMan.SetCVar(CCVars.GameDummyTicker, false);
|
||||
Assert.That(gameTicker.DummyTicker, Is.False);
|
||||
Server.CfgMan.SetCVar(CCVars.GameLobbyEnabled, true);
|
||||
await Server.WaitPost(() => gameTicker.RestartRound());
|
||||
|
||||
@@ -4,6 +4,7 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using Content.Server.GameTicking;
|
||||
using Content.Shared.Players;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Network;
|
||||
@@ -58,6 +59,9 @@ public sealed partial class TestPair
|
||||
(Server, ServerLogHandler) = await PoolManager.GenerateServer(settings, testOut);
|
||||
ActivateContext(testOut);
|
||||
|
||||
Client.CfgMan.OnCVarValueChanged += OnClientCvarChanged;
|
||||
Server.CfgMan.OnCVarValueChanged += OnServerCvarChanged;
|
||||
|
||||
if (!settings.NoLoadTestPrototypes)
|
||||
await LoadPrototypes(testPrototypes!);
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ public static partial class PoolManager
|
||||
(CCVars.NPCMaxUpdates.Name, "999999"),
|
||||
(CVars.ThreadParallelCount.Name, "1"),
|
||||
(CCVars.GameRoleTimers.Name, "false"),
|
||||
(CCVars.GameRoleWhitelist.Name, "false"),
|
||||
(CCVars.GridFill.Name, "false"),
|
||||
(CCVars.PreloadGrids.Name, "false"),
|
||||
(CCVars.ArrivalsShuttles.Name, "false"),
|
||||
|
||||
@@ -270,6 +270,8 @@ public static partial class PoolManager
|
||||
$"{nameof(GetServerClientPair)}: Retrieving pair {pair.Id} from pool took {poolRetrieveTime.TotalMilliseconds} ms");
|
||||
await testOut.WriteLineAsync(
|
||||
$"{nameof(GetServerClientPair)}: Returning pair {pair.Id}");
|
||||
|
||||
pair.ClearModifiedCvars();
|
||||
pair.Settings = poolSettings;
|
||||
pair.TestHistory.Add(currentTestName);
|
||||
pair.Watch.Restart();
|
||||
|
||||
@@ -29,6 +29,7 @@ namespace Content.IntegrationTests.Tests.Buckle
|
||||
components:
|
||||
- type: Buckle
|
||||
- type: Hands
|
||||
- type: ComplexInteraction
|
||||
- type: InputMover
|
||||
- type: Body
|
||||
prototype: Human
|
||||
|
||||
@@ -24,6 +24,7 @@ namespace Content.IntegrationTests.Tests.GameObjects.Components.ActionBlocking
|
||||
components:
|
||||
- type: Cuffable
|
||||
- type: Hands
|
||||
- type: ComplexInteraction
|
||||
- type: Body
|
||||
prototype: Human
|
||||
|
||||
|
||||
@@ -50,7 +50,6 @@ public sealed class NukeOpsTest
|
||||
var invSys = server.System<InventorySystem>();
|
||||
var factionSys = server.System<NpcFactionSystem>();
|
||||
|
||||
Assert.That(server.CfgMan.GetCVar(CCVars.GridFill), Is.False);
|
||||
server.CfgMan.SetCVar(CCVars.GridFill, true);
|
||||
|
||||
// Initially in the lobby
|
||||
@@ -200,7 +199,6 @@ public sealed class NukeOpsTest
|
||||
}
|
||||
|
||||
ticker.SetGamePreset((GamePresetPrototype?)null);
|
||||
server.CfgMan.SetCVar(CCVars.GridFill, false);
|
||||
await pair.SetAntagPref("NukeopsCommander", false);
|
||||
await pair.CleanReturnAsync();
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ using Content.Server.Interaction;
|
||||
using Content.Shared.Hands.Components;
|
||||
using Content.Shared.Hands.EntitySystems;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Interaction.Components;
|
||||
using Content.Shared.Item;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.GameObjects;
|
||||
@@ -64,6 +65,7 @@ namespace Content.IntegrationTests.Tests.Interaction.Click
|
||||
{
|
||||
user = sEntities.SpawnEntity(null, coords);
|
||||
sEntities.EnsureComponent<HandsComponent>(user);
|
||||
sEntities.EnsureComponent<ComplexInteractionComponent>(user);
|
||||
handSys.AddHand(user, "hand", HandLocation.Left);
|
||||
target = sEntities.SpawnEntity(null, coords);
|
||||
item = sEntities.SpawnEntity(null, coords);
|
||||
@@ -205,6 +207,7 @@ namespace Content.IntegrationTests.Tests.Interaction.Click
|
||||
{
|
||||
user = sEntities.SpawnEntity(null, coords);
|
||||
sEntities.EnsureComponent<HandsComponent>(user);
|
||||
sEntities.EnsureComponent<ComplexInteractionComponent>(user);
|
||||
handSys.AddHand(user, "hand", HandLocation.Left);
|
||||
target = sEntities.SpawnEntity(null, new MapCoordinates(new Vector2(SharedInteractionSystem.InteractionRange - 0.1f, 0), mapId));
|
||||
item = sEntities.SpawnEntity(null, coords);
|
||||
@@ -347,6 +350,7 @@ namespace Content.IntegrationTests.Tests.Interaction.Click
|
||||
{
|
||||
user = sEntities.SpawnEntity(null, coords);
|
||||
sEntities.EnsureComponent<HandsComponent>(user);
|
||||
sEntities.EnsureComponent<ComplexInteractionComponent>(user);
|
||||
handSys.AddHand(user, "hand", HandLocation.Left);
|
||||
target = sEntities.SpawnEntity(null, coords);
|
||||
item = sEntities.SpawnEntity(null, coords);
|
||||
|
||||
@@ -137,6 +137,7 @@ public abstract partial class InteractionTest
|
||||
prototype: Aghost
|
||||
- type: DoAfter
|
||||
- type: Hands
|
||||
- type: ComplexInteraction
|
||||
- type: MindContainer
|
||||
- type: Stripping
|
||||
- type: Tag
|
||||
|
||||
@@ -103,7 +103,7 @@ public sealed class MaterialArbitrageTest
|
||||
continue;
|
||||
|
||||
var stackProto = protoManager.Index<StackPrototype>(materialStep.MaterialPrototypeId);
|
||||
var spawnProto = protoManager.Index<EntityPrototype>(stackProto.Spawn);
|
||||
var spawnProto = protoManager.Index(stackProto.Spawn);
|
||||
|
||||
if (!spawnProto.Components.ContainsKey(materialName) ||
|
||||
!spawnProto.Components.TryGetValue(compositionName, out var compositionReg))
|
||||
|
||||
@@ -27,6 +27,7 @@ namespace Content.IntegrationTests.Tests
|
||||
id: HumanVendingDummy
|
||||
components:
|
||||
- type: Hands
|
||||
- type: ComplexInteraction
|
||||
- type: Body
|
||||
prototype: Human
|
||||
|
||||
|
||||
1913
Content.Server.Database/Migrations/Postgres/20240531011555_RoleWhitelist.Designer.cs
generated
Normal file
1913
Content.Server.Database/Migrations/Postgres/20240531011555_RoleWhitelist.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,40 @@
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
namespace Content.Server.Database.Migrations.Postgres
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class RoleWhitelist : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "role_whitelists",
|
||||
columns: table => new
|
||||
{
|
||||
player_user_id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
role_id = table.Column<string>(type: "text", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_role_whitelists", x => new { x.player_user_id, x.role_id });
|
||||
table.ForeignKey(
|
||||
name: "FK_role_whitelists_player_player_user_id",
|
||||
column: x => x.player_user_id,
|
||||
principalTable: "player",
|
||||
principalColumn: "user_id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "role_whitelists");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -907,6 +907,22 @@ namespace Content.Server.Database.Migrations.Postgres
|
||||
b.ToTable("profile_role_loadout", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Content.Server.Database.RoleWhitelist", b =>
|
||||
{
|
||||
b.Property<Guid>("PlayerUserId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("player_user_id");
|
||||
|
||||
b.Property<string>("RoleId")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("role_id");
|
||||
|
||||
b.HasKey("PlayerUserId", "RoleId")
|
||||
.HasName("PK_role_whitelists");
|
||||
|
||||
b.ToTable("role_whitelists", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Content.Server.Database.Round", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
@@ -1630,6 +1646,19 @@ namespace Content.Server.Database.Migrations.Postgres
|
||||
b.Navigation("Profile");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Content.Server.Database.RoleWhitelist", b =>
|
||||
{
|
||||
b.HasOne("Content.Server.Database.Player", "Player")
|
||||
.WithMany("JobWhitelists")
|
||||
.HasForeignKey("PlayerUserId")
|
||||
.HasPrincipalKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("FK_role_whitelists_player_player_user_id");
|
||||
|
||||
b.Navigation("Player");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Content.Server.Database.Round", b =>
|
||||
{
|
||||
b.HasOne("Content.Server.Database.Server", "Server")
|
||||
@@ -1829,6 +1858,8 @@ namespace Content.Server.Database.Migrations.Postgres
|
||||
b.Navigation("AdminWatchlistsLastEdited");
|
||||
|
||||
b.Navigation("AdminWatchlistsReceived");
|
||||
|
||||
b.Navigation("JobWhitelists");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Content.Server.Database.Preference", b =>
|
||||
|
||||
1838
Content.Server.Database/Migrations/Sqlite/20240531011549_RoleWhitelist.Designer.cs
generated
Normal file
1838
Content.Server.Database/Migrations/Sqlite/20240531011549_RoleWhitelist.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,40 @@
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
namespace Content.Server.Database.Migrations.Sqlite
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class RoleWhitelist : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "role_whitelists",
|
||||
columns: table => new
|
||||
{
|
||||
player_user_id = table.Column<Guid>(type: "TEXT", nullable: false),
|
||||
role_id = table.Column<string>(type: "TEXT", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_role_whitelists", x => new { x.player_user_id, x.role_id });
|
||||
table.ForeignKey(
|
||||
name: "FK_role_whitelists_player_player_user_id",
|
||||
column: x => x.player_user_id,
|
||||
principalTable: "player",
|
||||
principalColumn: "user_id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "role_whitelists");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -854,6 +854,22 @@ namespace Content.Server.Database.Migrations.Sqlite
|
||||
b.ToTable("profile_role_loadout", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Content.Server.Database.RoleWhitelist", b =>
|
||||
{
|
||||
b.Property<Guid>("PlayerUserId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("player_user_id");
|
||||
|
||||
b.Property<string>("RoleId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("role_id");
|
||||
|
||||
b.HasKey("PlayerUserId", "RoleId")
|
||||
.HasName("PK_role_whitelists");
|
||||
|
||||
b.ToTable("role_whitelists", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Content.Server.Database.Round", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
@@ -1555,6 +1571,19 @@ namespace Content.Server.Database.Migrations.Sqlite
|
||||
b.Navigation("Profile");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Content.Server.Database.RoleWhitelist", b =>
|
||||
{
|
||||
b.HasOne("Content.Server.Database.Player", "Player")
|
||||
.WithMany("JobWhitelists")
|
||||
.HasForeignKey("PlayerUserId")
|
||||
.HasPrincipalKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("FK_role_whitelists_player_player_user_id");
|
||||
|
||||
b.Navigation("Player");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Content.Server.Database.Round", b =>
|
||||
{
|
||||
b.HasOne("Content.Server.Database.Server", "Server")
|
||||
@@ -1754,6 +1783,8 @@ namespace Content.Server.Database.Migrations.Sqlite
|
||||
b.Navigation("AdminWatchlistsLastEdited");
|
||||
|
||||
b.Navigation("AdminWatchlistsReceived");
|
||||
|
||||
b.Navigation("JobWhitelists");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Content.Server.Database.Preference", b =>
|
||||
|
||||
@@ -40,6 +40,7 @@ namespace Content.Server.Database
|
||||
public DbSet<AdminNote> AdminNotes { get; set; } = null!;
|
||||
public DbSet<AdminWatchlist> AdminWatchlists { get; set; } = null!;
|
||||
public DbSet<AdminMessage> AdminMessages { get; set; } = null!;
|
||||
public DbSet<RoleWhitelist> RoleWhitelists { get; set; } = null!;
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
@@ -314,6 +315,13 @@ namespace Content.Server.Database
|
||||
.HasForeignKey(ban => ban.LastEditedById)
|
||||
.HasPrincipalKey(author => author.UserId)
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
modelBuilder.Entity<RoleWhitelist>()
|
||||
.HasOne(w => w.Player)
|
||||
.WithMany(p => p.JobWhitelists)
|
||||
.HasForeignKey(w => w.PlayerUserId)
|
||||
.HasPrincipalKey(p => p.UserId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
}
|
||||
|
||||
public virtual IQueryable<AdminLog> SearchLogs(IQueryable<AdminLog> query, string searchText)
|
||||
@@ -531,6 +539,7 @@ namespace Content.Server.Database
|
||||
public List<ServerBan> AdminServerBansLastEdited { get; set; } = null!;
|
||||
public List<ServerRoleBan> AdminServerRoleBansCreated { get; set; } = null!;
|
||||
public List<ServerRoleBan> AdminServerRoleBansLastEdited { get; set; } = null!;
|
||||
public List<RoleWhitelist> JobWhitelists { get; set; } = null!;
|
||||
}
|
||||
|
||||
[Table("whitelist")]
|
||||
@@ -1100,4 +1109,15 @@ namespace Content.Server.Database
|
||||
/// </summary>
|
||||
public bool Dismissed { get; set; }
|
||||
}
|
||||
|
||||
[PrimaryKey(nameof(PlayerUserId), nameof(RoleId))]
|
||||
public class RoleWhitelist
|
||||
{
|
||||
[Required, ForeignKey("Player")]
|
||||
public Guid PlayerUserId { get; set; }
|
||||
public Player Player { get; set; } = default!;
|
||||
|
||||
[Required]
|
||||
public string RoleId { get; set; } = default!;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ public sealed class ActionOnInteractSystem : EntitySystem
|
||||
|
||||
private void OnActivate(EntityUid uid, ActionOnInteractComponent component, ActivateInWorldEvent args)
|
||||
{
|
||||
if (args.Handled)
|
||||
if (args.Handled || !args.Complex)
|
||||
return;
|
||||
|
||||
if (component.ActionEntities is not {} actionEnts)
|
||||
|
||||
@@ -65,13 +65,23 @@ public sealed class BanListEui : BaseEui
|
||||
unban = new SharedServerUnban(unbanningAdmin, ban.Unban.UnbanTime.UtcDateTime);
|
||||
}
|
||||
|
||||
(string, int cidrMask)? ip = ("*Hidden*", 0);
|
||||
var hwid = "*Hidden*";
|
||||
|
||||
if (_admins.HasAdminFlag(Player, AdminFlags.Pii))
|
||||
{
|
||||
ip = ban.Address is { } address
|
||||
? (address.address.ToString(), address.cidrMask)
|
||||
: null;
|
||||
|
||||
hwid = ban.HWId == null ? null : Convert.ToBase64String(ban.HWId.Value.AsSpan());
|
||||
}
|
||||
|
||||
Bans.Add(new SharedServerBan(
|
||||
ban.Id,
|
||||
ban.UserId,
|
||||
ban.Address is { } address
|
||||
? (address.address.ToString(), address.cidrMask)
|
||||
: null,
|
||||
ban.HWId == null ? null : Convert.ToBase64String(ban.HWId.Value.AsSpan()),
|
||||
ip,
|
||||
hwid,
|
||||
ban.BanTime.UtcDateTime,
|
||||
ban.ExpirationTime?.UtcDateTime,
|
||||
ban.Reason,
|
||||
@@ -96,13 +106,22 @@ public sealed class BanListEui : BaseEui
|
||||
unban = new SharedServerUnban(unbanningAdmin, ban.Unban.UnbanTime.UtcDateTime);
|
||||
}
|
||||
|
||||
(string, int cidrMask)? ip = ("*Hidden*", 0);
|
||||
var hwid = "*Hidden*";
|
||||
|
||||
if (_admins.HasAdminFlag(Player, AdminFlags.Pii))
|
||||
{
|
||||
ip = ban.Address is { } address
|
||||
? (address.address.ToString(), address.cidrMask)
|
||||
: null;
|
||||
|
||||
hwid = ban.HWId == null ? null : Convert.ToBase64String(ban.HWId.Value.AsSpan());
|
||||
}
|
||||
RoleBans.Add(new SharedServerRoleBan(
|
||||
ban.Id,
|
||||
ban.UserId,
|
||||
ban.Address is { } address
|
||||
? (address.address.ToString(), address.cidrMask)
|
||||
: null,
|
||||
ban.HWId == null ? null : Convert.ToBase64String(ban.HWId.Value.AsSpan()),
|
||||
ip,
|
||||
hwid,
|
||||
ban.BanTime.UtcDateTime,
|
||||
ban.ExpirationTime?.UtcDateTime,
|
||||
ban.Reason,
|
||||
|
||||
@@ -5,7 +5,7 @@ using Robust.Shared.Console;
|
||||
|
||||
namespace Content.Server.Administration.Commands
|
||||
{
|
||||
[AdminCommand(AdminFlags.Admin)]
|
||||
[AdminCommand(AdminFlags.Moderator)]
|
||||
public sealed class AnnounceUiCommand : IConsoleCommand
|
||||
{
|
||||
public string Command => "announceui";
|
||||
|
||||
@@ -6,7 +6,7 @@ using Robust.Shared.Console;
|
||||
|
||||
namespace Content.Server.Administration.Commands;
|
||||
|
||||
[AdminCommand(AdminFlags.Admin)]
|
||||
[AdminCommand(AdminFlags.Ban)]
|
||||
public sealed class BanExemptionUpdateCommand : LocalizedCommands
|
||||
{
|
||||
[Dependency] private readonly IServerDbManager _dbManager = default!;
|
||||
@@ -61,7 +61,7 @@ public sealed class BanExemptionUpdateCommand : LocalizedCommands
|
||||
}
|
||||
}
|
||||
|
||||
[AdminCommand(AdminFlags.Admin)]
|
||||
[AdminCommand(AdminFlags.Ban)]
|
||||
public sealed class BanExemptionGetCommand : LocalizedCommands
|
||||
{
|
||||
[Dependency] private readonly IServerDbManager _dbManager = default!;
|
||||
|
||||
@@ -4,7 +4,7 @@ using Robust.Shared.Console;
|
||||
|
||||
namespace Content.Server.Administration.Commands
|
||||
{
|
||||
[AdminCommand(AdminFlags.Admin)]
|
||||
[AdminCommand(AdminFlags.Moderator)]
|
||||
sealed class DSay : IConsoleCommand
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _e = default!;
|
||||
|
||||
@@ -5,7 +5,7 @@ using Robust.Shared.Console;
|
||||
|
||||
namespace Content.Server.Administration.Commands;
|
||||
|
||||
[AdminCommand(AdminFlags.Admin)]
|
||||
[AdminCommand(AdminFlags.Fun)]
|
||||
public sealed class FaxUiCommand : IConsoleCommand
|
||||
{
|
||||
public string Command => "faxui";
|
||||
@@ -27,4 +27,3 @@ public sealed class FaxUiCommand : IConsoleCommand
|
||||
eui.OpenEui(ui, player);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
214
Content.Server/Administration/Commands/JobWhitelistCommands.cs
Normal file
214
Content.Server/Administration/Commands/JobWhitelistCommands.cs
Normal file
@@ -0,0 +1,214 @@
|
||||
using System.Linq;
|
||||
using Content.Server.Database;
|
||||
using Content.Server.Players.JobWhitelist;
|
||||
using Content.Shared.Administration;
|
||||
using Content.Shared.Roles;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared.Console;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Server.Administration.Commands;
|
||||
|
||||
[AdminCommand(AdminFlags.Ban)]
|
||||
public sealed class JobWhitelistAddCommand : LocalizedCommands
|
||||
{
|
||||
[Dependency] private readonly IServerDbManager _db = default!;
|
||||
[Dependency] private readonly JobWhitelistManager _jobWhitelist = default!;
|
||||
[Dependency] private readonly IPlayerLocator _playerLocator = default!;
|
||||
[Dependency] private readonly IPlayerManager _players = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototypes = default!;
|
||||
|
||||
public override string Command => "jobwhitelistadd";
|
||||
|
||||
public override async void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
if (args.Length != 2)
|
||||
{
|
||||
shell.WriteError(Loc.GetString("shell-wrong-arguments-number-need-specific",
|
||||
("properAmount", 2),
|
||||
("currentAmount", args.Length)));
|
||||
shell.WriteLine(Help);
|
||||
return;
|
||||
}
|
||||
|
||||
var player = args[0].Trim();
|
||||
var job = new ProtoId<JobPrototype>(args[1].Trim());
|
||||
if (!_prototypes.TryIndex(job, out var jobPrototype))
|
||||
{
|
||||
shell.WriteError(Loc.GetString("cmd-jobwhitelist-job-does-not-exist", ("job", job.Id)));
|
||||
shell.WriteLine(Help);
|
||||
return;
|
||||
}
|
||||
|
||||
var data = await _playerLocator.LookupIdByNameAsync(player);
|
||||
if (data != null)
|
||||
{
|
||||
var guid = data.UserId;
|
||||
var isWhitelisted = await _db.IsJobWhitelisted(guid, job);
|
||||
if (isWhitelisted)
|
||||
{
|
||||
shell.WriteLine(Loc.GetString("cmd-jobwhitelist-already-whitelisted",
|
||||
("player", player),
|
||||
("jobId", job.Id),
|
||||
("jobName", jobPrototype.LocalizedName)));
|
||||
return;
|
||||
}
|
||||
|
||||
_jobWhitelist.AddWhitelist(guid, job);
|
||||
shell.WriteLine(Loc.GetString("cmd-jobwhitelistadd-added",
|
||||
("player", player),
|
||||
("jobId", job.Id),
|
||||
("jobName", jobPrototype.LocalizedName)));
|
||||
return;
|
||||
}
|
||||
|
||||
shell.WriteError(Loc.GetString("cmd-jobwhitelist-player-not-found", ("player", player)));
|
||||
}
|
||||
|
||||
public override CompletionResult GetCompletion(IConsoleShell shell, string[] args)
|
||||
{
|
||||
if (args.Length == 1)
|
||||
{
|
||||
return CompletionResult.FromHintOptions(
|
||||
_players.Sessions.Select(s => s.Name),
|
||||
Loc.GetString("cmd-jobwhitelist-hint-player"));
|
||||
}
|
||||
|
||||
if (args.Length == 2)
|
||||
{
|
||||
return CompletionResult.FromHintOptions(
|
||||
_prototypes.EnumeratePrototypes<JobPrototype>().Select(p => p.ID),
|
||||
Loc.GetString("cmd-jobwhitelist-hint-job"));
|
||||
}
|
||||
|
||||
return CompletionResult.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
[AdminCommand(AdminFlags.Ban)]
|
||||
public sealed class GetJobWhitelistCommand : LocalizedCommands
|
||||
{
|
||||
[Dependency] private readonly IServerDbManager _db = default!;
|
||||
[Dependency] private readonly IPlayerLocator _playerLocator = default!;
|
||||
[Dependency] private readonly IPlayerManager _players = default!;
|
||||
|
||||
public override string Command => "jobwhitelistget";
|
||||
|
||||
public override async void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
if (args.Length == 0)
|
||||
{
|
||||
shell.WriteError("This command needs at least one argument.");
|
||||
shell.WriteLine(Help);
|
||||
return;
|
||||
}
|
||||
|
||||
var player = string.Join(' ', args).Trim();
|
||||
var data = await _playerLocator.LookupIdByNameAsync(player);
|
||||
if (data != null)
|
||||
{
|
||||
var guid = data.UserId;
|
||||
var whitelists = await _db.GetJobWhitelists(guid);
|
||||
if (whitelists.Count == 0)
|
||||
{
|
||||
shell.WriteLine(Loc.GetString("cmd-jobwhitelistget-whitelisted-none", ("player", player)));
|
||||
return;
|
||||
}
|
||||
|
||||
shell.WriteLine(Loc.GetString("cmd-jobwhitelistget-whitelisted-for",
|
||||
("player", player),
|
||||
("jobs", string.Join(", ", whitelists))));
|
||||
return;
|
||||
}
|
||||
|
||||
shell.WriteError(Loc.GetString("cmd-jobwhitelist-player-not-found", ("player", player)));
|
||||
}
|
||||
|
||||
public override CompletionResult GetCompletion(IConsoleShell shell, string[] args)
|
||||
{
|
||||
if (args.Length == 1)
|
||||
{
|
||||
return CompletionResult.FromHintOptions(
|
||||
_players.Sessions.Select(s => s.Name),
|
||||
Loc.GetString("cmd-jobwhitelist-hint-player"));
|
||||
}
|
||||
|
||||
return CompletionResult.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
[AdminCommand(AdminFlags.Ban)]
|
||||
public sealed class RemoveJobWhitelistCommand : LocalizedCommands
|
||||
{
|
||||
[Dependency] private readonly IServerDbManager _db = default!;
|
||||
[Dependency] private readonly JobWhitelistManager _jobWhitelist = default!;
|
||||
[Dependency] private readonly IPlayerLocator _playerLocator = default!;
|
||||
[Dependency] private readonly IPlayerManager _players = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototypes = default!;
|
||||
|
||||
public override string Command => "jobwhitelistremove";
|
||||
|
||||
public override async void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
if (args.Length != 2)
|
||||
{
|
||||
shell.WriteError(Loc.GetString("shell-wrong-arguments-number-need-specific",
|
||||
("properAmount", 2),
|
||||
("currentAmount", args.Length)));
|
||||
shell.WriteLine(Help);
|
||||
return;
|
||||
}
|
||||
|
||||
var player = args[0].Trim();
|
||||
var job = new ProtoId<JobPrototype>(args[1].Trim());
|
||||
if (!_prototypes.TryIndex(job, out var jobPrototype))
|
||||
{
|
||||
shell.WriteError(Loc.GetString("cmd-jobwhitelist-job-does-not-exist", ("job", job)));
|
||||
shell.WriteLine(Help);
|
||||
return;
|
||||
}
|
||||
|
||||
var data = await _playerLocator.LookupIdByNameAsync(player);
|
||||
if (data != null)
|
||||
{
|
||||
var guid = data.UserId;
|
||||
var isWhitelisted = await _db.IsJobWhitelisted(guid, job);
|
||||
if (!isWhitelisted)
|
||||
{
|
||||
shell.WriteError(Loc.GetString("cmd-jobwhitelistremove-was-not-whitelisted",
|
||||
("player", player),
|
||||
("jobId", job.Id),
|
||||
("jobName", jobPrototype.LocalizedName)));
|
||||
return;
|
||||
}
|
||||
|
||||
_jobWhitelist.RemoveWhitelist(guid, job);
|
||||
shell.WriteLine(Loc.GetString("cmd-jobwhitelistremove-removed",
|
||||
("player", player),
|
||||
("jobId", job.Id),
|
||||
("jobName", jobPrototype.LocalizedName)));
|
||||
return;
|
||||
}
|
||||
|
||||
shell.WriteError(Loc.GetString("cmd-jobwhitelist-player-not-found", ("player", player)));
|
||||
}
|
||||
|
||||
public override CompletionResult GetCompletion(IConsoleShell shell, string[] args)
|
||||
{
|
||||
if (args.Length == 1)
|
||||
{
|
||||
return CompletionResult.FromHintOptions(
|
||||
_players.Sessions.Select(s => s.Name),
|
||||
Loc.GetString("cmd-jobwhitelist-hint-player"));
|
||||
}
|
||||
|
||||
if (args.Length == 2)
|
||||
{
|
||||
return CompletionResult.FromHintOptions(
|
||||
_prototypes.EnumeratePrototypes<JobPrototype>().Select(p => p.ID),
|
||||
Loc.GetString("cmd-jobwhitelist-hint-job"));
|
||||
}
|
||||
|
||||
return CompletionResult.Empty;
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ using Robust.Shared.Console;
|
||||
|
||||
namespace Content.Server.Administration.Commands;
|
||||
|
||||
[AdminCommand(AdminFlags.Admin)]
|
||||
[AdminCommand(AdminFlags.Fun)]
|
||||
public sealed class LinkBluespaceLocker : IConsoleCommand
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _entManager = default!;
|
||||
|
||||
@@ -123,7 +123,7 @@ public sealed class PlayTimeAddRoleCommand : IConsoleCommand
|
||||
}
|
||||
}
|
||||
|
||||
[AdminCommand(AdminFlags.Admin)]
|
||||
[AdminCommand(AdminFlags.Moderator)]
|
||||
public sealed class PlayTimeGetOverallCommand : IConsoleCommand
|
||||
{
|
||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||
@@ -168,7 +168,7 @@ public sealed class PlayTimeGetOverallCommand : IConsoleCommand
|
||||
}
|
||||
}
|
||||
|
||||
[AdminCommand(AdminFlags.Admin)]
|
||||
[AdminCommand(AdminFlags.Moderator)]
|
||||
public sealed class PlayTimeGetRoleCommand : IConsoleCommand
|
||||
{
|
||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||
@@ -247,7 +247,7 @@ public sealed class PlayTimeGetRoleCommand : IConsoleCommand
|
||||
/// <summary>
|
||||
/// Saves the timers for a particular player immediately
|
||||
/// </summary>
|
||||
[AdminCommand(AdminFlags.Admin)]
|
||||
[AdminCommand(AdminFlags.Moderator)]
|
||||
public sealed class PlayTimeSaveCommand : IConsoleCommand
|
||||
{
|
||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||
|
||||
@@ -5,7 +5,7 @@ using Robust.Shared.Console;
|
||||
|
||||
namespace Content.Server.Administration.Commands
|
||||
{
|
||||
[AdminCommand(AdminFlags.Admin)]
|
||||
[AdminCommand(AdminFlags.NameColor)]
|
||||
internal sealed class SetAdminOOC : IConsoleCommand
|
||||
{
|
||||
public string Command => "setadminooc";
|
||||
|
||||
@@ -73,7 +73,9 @@ public sealed class BanManager : IBanManager, IPostInjectInit
|
||||
|
||||
public HashSet<string>? GetRoleBans(NetUserId playerUserId)
|
||||
{
|
||||
return _cachedRoleBans.TryGetValue(playerUserId, out var roleBans) ? roleBans.Select(banDef => banDef.Role).ToHashSet() : null;
|
||||
return _cachedRoleBans.TryGetValue(playerUserId, out var roleBans)
|
||||
? roleBans.Select(banDef => banDef.Role).ToHashSet()
|
||||
: null;
|
||||
}
|
||||
|
||||
private async Task CacheDbRoleBans(NetUserId userId, IPAddress? address = null, ImmutableArray<byte>? hwId = null)
|
||||
@@ -263,13 +265,13 @@ public sealed class BanManager : IBanManager, IPostInjectInit
|
||||
return $"Pardoned ban with id {banId}";
|
||||
}
|
||||
|
||||
public HashSet<string>? GetJobBans(NetUserId playerUserId)
|
||||
public HashSet<ProtoId<JobPrototype>>? GetJobBans(NetUserId playerUserId)
|
||||
{
|
||||
if (!_cachedRoleBans.TryGetValue(playerUserId, out var roleBans))
|
||||
return null;
|
||||
return roleBans
|
||||
.Where(ban => ban.Role.StartsWith(JobPrefix, StringComparison.Ordinal))
|
||||
.Select(ban => ban.Role[JobPrefix.Length..])
|
||||
.Select(ban => new ProtoId<JobPrototype>(ban.Role[JobPrefix.Length..]))
|
||||
.ToHashSet();
|
||||
}
|
||||
#endregion
|
||||
|
||||
@@ -2,8 +2,10 @@ using System.Collections.Immutable;
|
||||
using System.Net;
|
||||
using System.Threading.Tasks;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.Roles;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Server.Administration.Managers;
|
||||
|
||||
@@ -24,7 +26,7 @@ public interface IBanManager
|
||||
/// <param name="reason">Reason for the ban</param>
|
||||
public void CreateServerBan(NetUserId? target, string? targetUsername, NetUserId? banningAdmin, (IPAddress, int)? addressRange, ImmutableArray<byte>? hwid, uint? minutes, NoteSeverity severity, string reason);
|
||||
public HashSet<string>? GetRoleBans(NetUserId playerUserId);
|
||||
public HashSet<string>? GetJobBans(NetUserId playerUserId);
|
||||
public HashSet<ProtoId<JobPrototype>>? GetJobBans(NetUserId playerUserId);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a job ban for the specified target, username or GUID
|
||||
|
||||
@@ -302,6 +302,19 @@ namespace Content.Server.Administration.Systems
|
||||
: _adminManager.ActiveAdmins;
|
||||
var hasAdmins = admins.Any();
|
||||
|
||||
// TODO Fix order dependent Cvars
|
||||
// Please for the sake of my sanity don't make cvars & order dependent.
|
||||
// Just make a bool field on the system instead of having some cvars automatically modify other cvars.
|
||||
//
|
||||
// I.e., this:
|
||||
// /sudo cvar game.panic_bunker.enabled true
|
||||
// /sudo cvar game.panic_bunker.disable_with_admins true
|
||||
// and this:
|
||||
// /sudo cvar game.panic_bunker.disable_with_admins true
|
||||
// /sudo cvar game.panic_bunker.enabled true
|
||||
//
|
||||
// should have the same effect, but currently setting the disable_with_admins can modify enabled.
|
||||
|
||||
if (hasAdmins && PanicBunker.DisableWithAdmins)
|
||||
{
|
||||
_config.SetCVar(CCVars.PanicBunkerEnabled, false);
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
using Content.Server.Advertise.EntitySystems;
|
||||
using Content.Shared.Advertise;
|
||||
using Content.Shared.Dataset;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Server.Advertise.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Makes this entity periodically advertise by speaking a randomly selected
|
||||
/// message from a specified MessagePack into local chat.
|
||||
/// message from a specified dataset into local chat.
|
||||
/// </summary>
|
||||
[RegisterComponent, Access(typeof(AdvertiseSystem))]
|
||||
public sealed partial class AdvertiseComponent : Component
|
||||
@@ -33,10 +33,10 @@ public sealed partial class AdvertiseComponent : Component
|
||||
public bool Prewarm = true;
|
||||
|
||||
/// <summary>
|
||||
/// The identifier for the advertisements pack prototype.
|
||||
/// The identifier for the advertisements dataset prototype.
|
||||
/// </summary>
|
||||
[DataField(required: true)]
|
||||
public ProtoId<MessagePackPrototype> Pack { get; private set; }
|
||||
public ProtoId<LocalizedDatasetPrototype> Pack { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The next time an advertisement will be said.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Content.Shared.Advertise;
|
||||
using Content.Shared.Dataset;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Server.Advertise.Components;
|
||||
@@ -11,10 +11,10 @@ namespace Content.Server.Advertise.Components;
|
||||
public sealed partial class SpeakOnUIClosedComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// The identifier for the message pack prototype containing messages to be spoken by this entity.
|
||||
/// The identifier for the dataset prototype containing messages to be spoken by this entity.
|
||||
/// </summary>
|
||||
[DataField(required: true)]
|
||||
public ProtoId<MessagePackPrototype> Pack { get; private set; }
|
||||
public ProtoId<LocalizedDatasetPrototype> Pack { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Is this component active? If false, no messages will be spoken.
|
||||
|
||||
@@ -62,7 +62,7 @@ public sealed class AdvertiseSystem : EntitySystem
|
||||
return;
|
||||
|
||||
if (_prototypeManager.TryIndex(advert.Pack, out var advertisements))
|
||||
_chat.TrySendInGameICMessage(uid, Loc.GetString(_random.Pick(advertisements.Messages)), InGameICChatType.Speak, hideChat: true);
|
||||
_chat.TrySendInGameICMessage(uid, Loc.GetString(_random.Pick(advertisements.Values)), InGameICChatType.Speak, hideChat: true);
|
||||
}
|
||||
|
||||
public override void Update(float frameTime)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
using Content.Server.Advertise.Components;
|
||||
using Content.Server.Chat.Systems;
|
||||
using Content.Server.UserInterface;
|
||||
using Content.Shared.Advertise;
|
||||
using Content.Shared.Dataset;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Random;
|
||||
using ActivatableUIComponent = Content.Shared.UserInterface.ActivatableUIComponent;
|
||||
@@ -39,10 +38,10 @@ public sealed partial class SpeakOnUIClosedSystem : EntitySystem
|
||||
if (!entity.Comp.Enabled)
|
||||
return false;
|
||||
|
||||
if (!_prototypeManager.TryIndex(entity.Comp.Pack, out MessagePackPrototype? messagePack))
|
||||
if (!_prototypeManager.TryIndex(entity.Comp.Pack, out var messagePack))
|
||||
return false;
|
||||
|
||||
var message = Loc.GetString(_random.Pick(messagePack.Messages), ("name", Name(entity)));
|
||||
var message = Loc.GetString(_random.Pick(messagePack.Values), ("name", Name(entity)));
|
||||
_chat.TrySendInGameICMessage(entity, message, InGameICChatType.Speak, true);
|
||||
entity.Comp.Flag = false;
|
||||
return true;
|
||||
|
||||
@@ -8,7 +8,7 @@ using Robust.Shared.Console;
|
||||
namespace Content.Server.AlertLevel.Commands
|
||||
{
|
||||
[UsedImplicitly]
|
||||
[AdminCommand(AdminFlags.Admin)]
|
||||
[AdminCommand(AdminFlags.Fun)]
|
||||
public sealed class SetAlertLevelCommand : LocalizedCommands
|
||||
{
|
||||
[Dependency] private readonly IEntitySystemManager _entitySystems = default!;
|
||||
|
||||
@@ -6,7 +6,7 @@ using Robust.Shared.Console;
|
||||
|
||||
namespace Content.Server.Announcements
|
||||
{
|
||||
[AdminCommand(AdminFlags.Admin)]
|
||||
[AdminCommand(AdminFlags.Moderator)]
|
||||
public sealed class AnnounceCommand : IConsoleCommand
|
||||
{
|
||||
public string Command => "announce";
|
||||
|
||||
22
Content.Server/Antag/AntagRandomSpawnRule.cs
Normal file
22
Content.Server/Antag/AntagRandomSpawnRule.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
using Content.Server.Antag.Components;
|
||||
using Content.Server.GameTicking.Rules;
|
||||
|
||||
namespace Content.Server.Antag;
|
||||
|
||||
public sealed class AntagRandomSpawnSystem : GameRuleSystem<AntagRandomSpawnComponent>
|
||||
{
|
||||
[Dependency] private readonly SharedTransformSystem _transform = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<AntagRandomSpawnComponent, AntagSelectLocationEvent>(OnSelectLocation);
|
||||
}
|
||||
|
||||
private void OnSelectLocation(Entity<AntagRandomSpawnComponent> ent, ref AntagSelectLocationEvent args)
|
||||
{
|
||||
if (TryFindRandomTile(out _, out _, out _, out var coords))
|
||||
args.Coordinates.Add(_transform.ToMapCoordinates(coords));
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ using Content.Server.GameTicking.Rules.Components;
|
||||
using Content.Server.Objectives;
|
||||
using Content.Shared.Chat;
|
||||
using Content.Shared.Mind;
|
||||
using Content.Shared.Preferences;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Enums;
|
||||
@@ -156,6 +157,36 @@ public sealed partial class AntagSelectionSystem
|
||||
return ent.Comp.SelectedMinds.Select(p => p.Item1).ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a given session has the primary antag preferences for a given definition
|
||||
/// </summary>
|
||||
public bool HasPrimaryAntagPreference(ICommonSession? session, AntagSelectionDefinition def)
|
||||
{
|
||||
if (session == null)
|
||||
return true;
|
||||
|
||||
if (def.PrefRoles.Count == 0)
|
||||
return false;
|
||||
|
||||
var pref = (HumanoidCharacterProfile) _pref.GetPreferences(session.UserId).SelectedCharacter;
|
||||
return pref.AntagPreferences.Any(p => def.PrefRoles.Contains(p));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a given session has the fallback antag preferences for a given definition
|
||||
/// </summary>
|
||||
public bool HasFallbackAntagPreference(ICommonSession? session, AntagSelectionDefinition def)
|
||||
{
|
||||
if (session == null)
|
||||
return true;
|
||||
|
||||
if (def.FallbackRoles.Count == 0)
|
||||
return false;
|
||||
|
||||
var pref = (HumanoidCharacterProfile) _pref.GetPreferences(session.UserId).SelectedCharacter;
|
||||
return pref.AntagPreferences.Any(p => def.FallbackRoles.Contains(p));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns all the antagonists for this rule who are currently alive
|
||||
/// </summary>
|
||||
|
||||
@@ -18,7 +18,7 @@ using Content.Shared.GameTicking;
|
||||
using Content.Shared.Ghost;
|
||||
using Content.Shared.Humanoid;
|
||||
using Content.Shared.Players;
|
||||
using Content.Shared.Preferences;
|
||||
using Content.Shared.Whitelist;
|
||||
using Robust.Server.Audio;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Server.Player;
|
||||
@@ -42,6 +42,7 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
|
||||
[Dependency] private readonly RoleSystem _role = default!;
|
||||
[Dependency] private readonly StationSpawningSystem _stationSpawning = default!;
|
||||
[Dependency] private readonly TransformSystem _transform = default!;
|
||||
[Dependency] private readonly EntityWhitelistSystem _whitelist = default!;
|
||||
|
||||
// arbitrary random number to give late joining some mild interest.
|
||||
public const float LateJoinRandomChance = 0.5f;
|
||||
@@ -121,12 +122,15 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
|
||||
// something to figure out later.
|
||||
|
||||
var query = QueryActiveRules();
|
||||
var rules = new List<(EntityUid, AntagSelectionComponent)>();
|
||||
while (query.MoveNext(out var uid, out _, out var antag, out _))
|
||||
{
|
||||
// TODO ANTAG
|
||||
// what why aasdiuhasdopiuasdfhksad
|
||||
// stop this insanity please
|
||||
// probability of antag assignment shouldn't depend on the order in which rules are returned by the query.
|
||||
rules.Add((uid, antag));
|
||||
}
|
||||
RobustRandom.Shuffle(rules);
|
||||
|
||||
foreach (var (uid, antag) in rules)
|
||||
{
|
||||
if (!RobustRandom.Prob(LateJoinRandomChance))
|
||||
continue;
|
||||
|
||||
@@ -232,13 +236,13 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
|
||||
/// <summary>
|
||||
/// Tries to makes a given player into the specified antagonist.
|
||||
/// </summary>
|
||||
public bool TryMakeAntag(Entity<AntagSelectionComponent> ent, ICommonSession? session, AntagSelectionDefinition def, bool ignoreSpawner = false)
|
||||
public bool TryMakeAntag(Entity<AntagSelectionComponent> ent, ICommonSession? session, AntagSelectionDefinition def, bool ignoreSpawner = false, bool checkPref = true)
|
||||
{
|
||||
if (!IsSessionValid(ent, session, def) ||
|
||||
!IsEntityValid(session?.AttachedEntity, def))
|
||||
{
|
||||
if (checkPref && !HasPrimaryAntagPreference(session, def))
|
||||
return false;
|
||||
|
||||
if (!IsSessionValid(ent, session, def) || !IsEntityValid(session?.AttachedEntity, def))
|
||||
return false;
|
||||
}
|
||||
|
||||
MakeAntag(ent, session, def, ignoreSpawner);
|
||||
return true;
|
||||
@@ -338,16 +342,14 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
|
||||
var fallbackList = new List<ICommonSession>();
|
||||
foreach (var session in sessions)
|
||||
{
|
||||
if (!IsSessionValid(ent, session, def) ||
|
||||
!IsEntityValid(session.AttachedEntity, def))
|
||||
if (!IsSessionValid(ent, session, def) || !IsEntityValid(session.AttachedEntity, def))
|
||||
continue;
|
||||
|
||||
var pref = (HumanoidCharacterProfile) _pref.GetPreferences(session.UserId).SelectedCharacter;
|
||||
if (def.PrefRoles.Count != 0 && pref.AntagPreferences.Any(p => def.PrefRoles.Contains(p)))
|
||||
if (HasPrimaryAntagPreference(session, def))
|
||||
{
|
||||
preferredList.Add(session);
|
||||
}
|
||||
else if (def.FallbackRoles.Count != 0 && pref.AntagPreferences.Any(p => def.FallbackRoles.Contains(p)))
|
||||
else if (HasFallbackAntagPreference(session, def))
|
||||
{
|
||||
fallbackList.Add(session);
|
||||
}
|
||||
@@ -418,13 +420,13 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
|
||||
|
||||
if (def.Whitelist != null)
|
||||
{
|
||||
if (!def.Whitelist.IsValid(entity.Value, EntityManager))
|
||||
if (!_whitelist.IsValid(def.Whitelist, entity.Value))
|
||||
return false;
|
||||
}
|
||||
|
||||
if (def.Blacklist != null)
|
||||
{
|
||||
if (def.Blacklist.IsValid(entity.Value, EntityManager))
|
||||
if (_whitelist.IsValid(def.Blacklist, entity.Value))
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
21
Content.Server/Antag/AntagSpawnerSystem.cs
Normal file
21
Content.Server/Antag/AntagSpawnerSystem.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using Content.Server.Antag.Components;
|
||||
|
||||
namespace Content.Server.Antag;
|
||||
|
||||
/// <summary>
|
||||
/// Spawns an entity when creating an antag for <see cref="AntagSpawnerComponent"/>.
|
||||
/// </summary>
|
||||
public sealed class AntagSpawnerSystem : EntitySystem
|
||||
{
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<AntagSpawnerComponent, AntagSelectEntityEvent>(OnSelectEntity);
|
||||
}
|
||||
|
||||
private void OnSelectEntity(Entity<AntagSpawnerComponent> ent, ref AntagSelectEntityEvent args)
|
||||
{
|
||||
args.Entity = Spawn(ent.Comp.Prototype);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace Content.Server.Antag.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Spawns this rule's antags at random tiles on a station using <c>TryGetRandomTile</c>.
|
||||
/// Requires <see cref="AntagSelectionComponent"/>.
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public sealed partial class AntagRandomSpawnComponent : Component;
|
||||
17
Content.Server/Antag/Components/AntagSpawnerComponent.cs
Normal file
17
Content.Server/Antag/Components/AntagSpawnerComponent.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using Content.Server.Antag;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Server.Antag.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Spawns a prototype for antags created with a spawner.
|
||||
/// </summary>
|
||||
[RegisterComponent, Access(typeof(AntagSpawnerSystem))]
|
||||
public sealed partial class AntagSpawnerComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// The entity to spawn.
|
||||
/// </summary>
|
||||
[DataField(required: true)]
|
||||
public EntProtoId Prototype = string.Empty;
|
||||
}
|
||||
@@ -106,7 +106,7 @@ namespace Content.Server.Atmos.EntitySystems
|
||||
if (tile?.Air == null)
|
||||
continue;
|
||||
|
||||
tile.Air.CopyFromMutable(combined);
|
||||
tile.Air.CopyFrom(combined);
|
||||
InvalidateVisuals(ent, tile);
|
||||
}
|
||||
|
||||
|
||||
@@ -155,7 +155,7 @@ namespace Content.Server.Atmos.EntitySystems
|
||||
|
||||
private void OnExtinguishActivateInWorld(EntityUid uid, ExtinguishOnInteractComponent component, ActivateInWorldEvent args)
|
||||
{
|
||||
if (args.Handled)
|
||||
if (args.Handled || !args.Complex)
|
||||
return;
|
||||
|
||||
if (!TryComp(uid, out FlammableComponent? flammable))
|
||||
|
||||
@@ -246,6 +246,9 @@ public sealed class AirAlarmSystem : EntitySystem
|
||||
|
||||
private void OnActivate(EntityUid uid, AirAlarmComponent component, ActivateInWorldEvent args)
|
||||
{
|
||||
if (!args.Complex)
|
||||
return;
|
||||
|
||||
if (TryComp<WiresPanelComponent>(uid, out var panel) && panel.Open)
|
||||
{
|
||||
args.Handled = false;
|
||||
|
||||
@@ -39,7 +39,7 @@ namespace Content.Server.Atmos.Piping.Binary.EntitySystems
|
||||
var T2 = outlet.Air.Temperature;
|
||||
var pressureDelta = P1 - P2;
|
||||
|
||||
float dt = 1/_atmosphereSystem.AtmosTickRate;
|
||||
float dt = args.dt;
|
||||
float dV = 0;
|
||||
var denom = (T1*V2 + T2*V1);
|
||||
|
||||
@@ -63,7 +63,9 @@ namespace Content.Server.Atmos.Piping.Binary.EntitySystems
|
||||
var transferMoles = n1 - (n1+n2)*T2*V1 / denom;
|
||||
|
||||
// Get the volume transfered to update our flow meter.
|
||||
dV = n1*Atmospherics.R*T1/P1;
|
||||
// When you remove x from one side and add x to the other the total difference is 2x.
|
||||
// Also account for atmos speedup so that measured flow rate matches the setting on the volume pump.
|
||||
dV = 2*transferMoles*Atmospherics.R*T1/P1 / _atmosphereSystem.Speedup;
|
||||
|
||||
// Actually transfer the gas.
|
||||
_atmosphereSystem.Merge(outlet.Air, inlet.Air.Remove(transferMoles));
|
||||
|
||||
@@ -103,6 +103,9 @@ namespace Content.Server.Atmos.Piping.Binary.EntitySystems
|
||||
|
||||
private void OnPumpActivate(EntityUid uid, GasPressurePumpComponent pump, ActivateInWorldEvent args)
|
||||
{
|
||||
if (args.Handled || !args.Complex)
|
||||
return;
|
||||
|
||||
if (!EntityManager.TryGetComponent(args.User, out ActorComponent? actor))
|
||||
return;
|
||||
|
||||
|
||||
@@ -52,8 +52,12 @@ namespace Content.Server.Atmos.Piping.Binary.EntitySystems
|
||||
|
||||
private void OnActivate(EntityUid uid, GasValveComponent component, ActivateInWorldEvent args)
|
||||
{
|
||||
if (args.Handled || !args.Complex)
|
||||
return;
|
||||
|
||||
Toggle(uid, component);
|
||||
_audio.PlayPvs(component.ValveSound, uid, AudioParams.Default.WithVariation(0.25f));
|
||||
args.Handled = true;
|
||||
}
|
||||
|
||||
public void Set(EntityUid uid, GasValveComponent component, bool value)
|
||||
|
||||
@@ -133,6 +133,9 @@ namespace Content.Server.Atmos.Piping.Binary.EntitySystems
|
||||
|
||||
private void OnPumpActivate(EntityUid uid, GasVolumePumpComponent pump, ActivateInWorldEvent args)
|
||||
{
|
||||
if (args.Handled || !args.Complex)
|
||||
return;
|
||||
|
||||
if (!EntityManager.TryGetComponent(args.User, out ActorComponent? actor))
|
||||
return;
|
||||
|
||||
|
||||
@@ -99,6 +99,9 @@ namespace Content.Server.Atmos.Piping.Trinary.EntitySystems
|
||||
|
||||
private void OnFilterActivate(EntityUid uid, GasFilterComponent filter, ActivateInWorldEvent args)
|
||||
{
|
||||
if (args.Handled || !args.Complex)
|
||||
return;
|
||||
|
||||
if (!EntityManager.TryGetComponent(args.User, out ActorComponent? actor))
|
||||
return;
|
||||
|
||||
|
||||
@@ -139,6 +139,9 @@ namespace Content.Server.Atmos.Piping.Trinary.EntitySystems
|
||||
|
||||
private void OnMixerActivate(EntityUid uid, GasMixerComponent mixer, ActivateInWorldEvent args)
|
||||
{
|
||||
if (args.Handled || !args.Complex)
|
||||
return;
|
||||
|
||||
if (!EntityManager.TryGetComponent(args.User, out ActorComponent? actor))
|
||||
return;
|
||||
|
||||
|
||||
@@ -201,6 +201,9 @@ public sealed class GasCanisterSystem : EntitySystem
|
||||
|
||||
private void OnCanisterActivate(EntityUid uid, GasCanisterComponent component, ActivateInWorldEvent args)
|
||||
{
|
||||
if (!args.Complex)
|
||||
return;
|
||||
|
||||
if (!TryComp<ActorComponent>(args.User, out var actor))
|
||||
return;
|
||||
|
||||
|
||||
@@ -33,8 +33,12 @@ namespace Content.Server.Atmos.Piping.Unary.EntitySystems
|
||||
|
||||
private void OnActivate(EntityUid uid, GasOutletInjectorComponent component, ActivateInWorldEvent args)
|
||||
{
|
||||
if (args.Handled || !args.Complex)
|
||||
return;
|
||||
|
||||
component.Enabled = !component.Enabled;
|
||||
UpdateAppearance(uid, component);
|
||||
args.Handled = true;
|
||||
}
|
||||
|
||||
public void UpdateAppearance(EntityUid uid, GasOutletInjectorComponent component, AppearanceComponent? appearance = null)
|
||||
|
||||
@@ -23,6 +23,7 @@ public sealed class InternalsSystem : EntitySystem
|
||||
[Dependency] private readonly GasTankSystem _gasTank = default!;
|
||||
[Dependency] private readonly InventorySystem _inventory = default!;
|
||||
[Dependency] private readonly PopupSystem _popupSystem = default!;
|
||||
[Dependency] private readonly RespiratorSystem _respirator = default!;
|
||||
|
||||
private EntityQuery<InternalsComponent> _internalsQuery;
|
||||
|
||||
@@ -38,15 +39,30 @@ public sealed class InternalsSystem : EntitySystem
|
||||
SubscribeLocalEvent<InternalsComponent, GetVerbsEvent<InteractionVerb>>(OnGetInteractionVerbs);
|
||||
SubscribeLocalEvent<InternalsComponent, InternalsDoAfterEvent>(OnDoAfter);
|
||||
|
||||
SubscribeLocalEvent<StartingGearEquippedEvent>(OnStartingGear);
|
||||
SubscribeLocalEvent<InternalsComponent, StartingGearEquippedEvent>(OnStartingGear);
|
||||
}
|
||||
|
||||
private void OnStartingGear(ref StartingGearEquippedEvent ev)
|
||||
private void OnStartingGear(EntityUid uid, InternalsComponent component, ref StartingGearEquippedEvent args)
|
||||
{
|
||||
if (!_internalsQuery.TryComp(ev.Entity, out var internals) || internals.BreathToolEntity == null)
|
||||
if (component.BreathToolEntity == null)
|
||||
return;
|
||||
|
||||
ToggleInternals(ev.Entity, ev.Entity, force: false, internals);
|
||||
if (component.GasTankEntity != null)
|
||||
return; // already connected
|
||||
|
||||
// Can the entity breathe the air it is currently exposed to?
|
||||
if (_respirator.CanMetabolizeInhaledAir(uid))
|
||||
return;
|
||||
|
||||
var tank = FindBestGasTank(uid);
|
||||
if (tank == null)
|
||||
return;
|
||||
|
||||
// Could the entity metabolise the air in the linked gas tank?
|
||||
if (!_respirator.CanMetabolizeGas(uid, tank.Value.Comp.Air))
|
||||
return;
|
||||
|
||||
ToggleInternals(uid, uid, force: false, component);
|
||||
}
|
||||
|
||||
private void OnGetInteractionVerbs(
|
||||
@@ -243,6 +259,7 @@ public sealed class InternalsSystem : EntitySystem
|
||||
public Entity<GasTankComponent>? FindBestGasTank(
|
||||
Entity<HandsComponent?, InventoryComponent?, ContainerManagerComponent?> user)
|
||||
{
|
||||
// TODO use _respirator.CanMetabolizeGas() to prioritize metabolizable gasses
|
||||
// Prioritise
|
||||
// 1. back equipped tanks
|
||||
// 2. exo-slot tanks
|
||||
|
||||
@@ -3,6 +3,7 @@ using Content.Server.Atmos.EntitySystems;
|
||||
using Content.Server.Body.Components;
|
||||
using Content.Server.Chemistry.Containers.EntitySystems;
|
||||
using Content.Shared.Atmos;
|
||||
using Content.Shared.Chemistry.Components;
|
||||
using Content.Shared.Clothing;
|
||||
using Content.Shared.Inventory.Events;
|
||||
|
||||
@@ -77,23 +78,32 @@ public sealed class LungSystem : EntitySystem
|
||||
if (!_solutionContainerSystem.ResolveSolution(uid, lung.SolutionName, ref lung.Solution, out var solution))
|
||||
return;
|
||||
|
||||
foreach (var gas in Enum.GetValues<Gas>())
|
||||
GasToReagent(lung.Air, solution);
|
||||
_solutionContainerSystem.UpdateChemicals(lung.Solution.Value);
|
||||
}
|
||||
|
||||
private void GasToReagent(GasMixture gas, Solution solution)
|
||||
{
|
||||
foreach (var gasId in Enum.GetValues<Gas>())
|
||||
{
|
||||
var i = (int) gas;
|
||||
var moles = lung.Air[i];
|
||||
var i = (int) gasId;
|
||||
var moles = gas[i];
|
||||
if (moles <= 0)
|
||||
continue;
|
||||
|
||||
var reagent = _atmosphereSystem.GasReagents[i];
|
||||
if (reagent is null) continue;
|
||||
if (reagent is null)
|
||||
continue;
|
||||
|
||||
var amount = moles * Atmospherics.BreathMolesToReagentMultiplier;
|
||||
solution.AddReagent(reagent, amount);
|
||||
|
||||
// We don't remove the gas from the lung mix,
|
||||
// that's the responsibility of whatever gas is being metabolized.
|
||||
// Most things will just want to exhale again.
|
||||
}
|
||||
}
|
||||
|
||||
_solutionContainerSystem.UpdateChemicals(lung.Solution.Value);
|
||||
public Solution GasToReagent(GasMixture gas)
|
||||
{
|
||||
var solution = new Solution();
|
||||
GasToReagent(gas, solution);
|
||||
return solution;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,21 @@
|
||||
using Content.Server.Administration.Logs;
|
||||
using Content.Server.Atmos;
|
||||
using Content.Server.Atmos.EntitySystems;
|
||||
using Content.Server.Body.Components;
|
||||
using Content.Server.Chat.Systems;
|
||||
using Content.Server.Chemistry.Containers.EntitySystems;
|
||||
using Content.Server.Chemistry.ReagentEffectConditions;
|
||||
using Content.Server.Chemistry.ReagentEffects;
|
||||
using Content.Shared.Alert;
|
||||
using Content.Shared.Atmos;
|
||||
using Content.Shared.Body.Components;
|
||||
using Content.Shared.Body.Prototypes;
|
||||
using Content.Shared.Chemistry.Components;
|
||||
using Content.Shared.Chemistry.Reagent;
|
||||
using Content.Shared.Damage;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.Mobs.Systems;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Server.Body.Systems;
|
||||
@@ -26,9 +31,12 @@ public sealed class RespiratorSystem : EntitySystem
|
||||
[Dependency] private readonly DamageableSystem _damageableSys = default!;
|
||||
[Dependency] private readonly LungSystem _lungSystem = default!;
|
||||
[Dependency] private readonly MobStateSystem _mobState = default!;
|
||||
[Dependency] private readonly IPrototypeManager _protoMan = default!;
|
||||
[Dependency] private readonly SolutionContainerSystem _solutionContainerSystem = default!;
|
||||
[Dependency] private readonly ChatSystem _chat = default!;
|
||||
|
||||
private static readonly ProtoId<MetabolismGroupPrototype> GasId = new("Gas");
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
@@ -109,7 +117,7 @@ public sealed class RespiratorSystem : EntitySystem
|
||||
|
||||
// Inhale gas
|
||||
var ev = new InhaleLocationEvent();
|
||||
RaiseLocalEvent(uid, ref ev, broadcast: false);
|
||||
RaiseLocalEvent(uid, ref ev);
|
||||
|
||||
ev.Gas ??= _atmosSys.GetContainingMixture(uid, excite: true);
|
||||
|
||||
@@ -164,6 +172,112 @@ public sealed class RespiratorSystem : EntitySystem
|
||||
_atmosSys.Merge(ev.Gas, outGas);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check whether or not an entity can metabolize inhaled air without suffocating or taking damage (i.e., no toxic
|
||||
/// gasses).
|
||||
/// </summary>
|
||||
public bool CanMetabolizeInhaledAir(Entity<RespiratorComponent?> ent)
|
||||
{
|
||||
if (!Resolve(ent, ref ent.Comp))
|
||||
return false;
|
||||
|
||||
var ev = new InhaleLocationEvent();
|
||||
RaiseLocalEvent(ent, ref ev);
|
||||
|
||||
var gas = ev.Gas ?? _atmosSys.GetContainingMixture(ent.Owner);
|
||||
if (gas == null)
|
||||
return false;
|
||||
|
||||
return CanMetabolizeGas(ent, gas);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check whether or not an entity can metabolize the given gas mixture without suffocating or taking damage
|
||||
/// (i.e., no toxic gasses).
|
||||
/// </summary>
|
||||
public bool CanMetabolizeGas(Entity<RespiratorComponent?> ent, GasMixture gas)
|
||||
{
|
||||
if (!Resolve(ent, ref ent.Comp))
|
||||
return false;
|
||||
|
||||
var organs = _bodySystem.GetBodyOrganComponents<LungComponent>(ent);
|
||||
if (organs.Count == 0)
|
||||
return false;
|
||||
|
||||
gas = new GasMixture(gas);
|
||||
var lungRatio = 1.0f / organs.Count;
|
||||
gas.Multiply(MathF.Min(lungRatio * gas.Volume/Atmospherics.BreathVolume, lungRatio));
|
||||
var solution = _lungSystem.GasToReagent(gas);
|
||||
|
||||
float saturation = 0;
|
||||
foreach (var organ in organs)
|
||||
{
|
||||
saturation += GetSaturation(solution, organ.Comp.Owner, out var toxic);
|
||||
if (toxic)
|
||||
return false;
|
||||
}
|
||||
|
||||
return saturation > ent.Comp.UpdateInterval.TotalSeconds;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the amount of saturation that would be generated if the lung were to metabolize the given solution.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This assumes the metabolism rate is unbounded, which generally should be the case for lungs, otherwise we get
|
||||
/// back to the old pulmonary edema bug.
|
||||
/// </remarks>
|
||||
/// <param name="solution">The reagents to metabolize</param>
|
||||
/// <param name="lung">The entity doing the metabolizing</param>
|
||||
/// <param name="toxic">Whether or not any of the reagents would deal damage to the entity</param>
|
||||
private float GetSaturation(Solution solution, Entity<MetabolizerComponent?> lung, out bool toxic)
|
||||
{
|
||||
toxic = false;
|
||||
if (!Resolve(lung, ref lung.Comp))
|
||||
return 0;
|
||||
|
||||
if (lung.Comp.MetabolismGroups == null)
|
||||
return 0;
|
||||
|
||||
float saturation = 0;
|
||||
foreach (var (id, quantity) in solution.Contents)
|
||||
{
|
||||
var reagent = _protoMan.Index<ReagentPrototype>(id.Prototype);
|
||||
if (reagent.Metabolisms == null)
|
||||
continue;
|
||||
|
||||
if (!reagent.Metabolisms.TryGetValue(GasId, out var entry))
|
||||
continue;
|
||||
|
||||
foreach (var effect in entry.Effects)
|
||||
{
|
||||
if (effect is HealthChange health)
|
||||
toxic |= CanMetabolize(health) && health.Damage.AnyPositive();
|
||||
else if (effect is Oxygenate oxy && CanMetabolize(oxy))
|
||||
saturation += oxy.Factor * quantity.Float();
|
||||
}
|
||||
}
|
||||
|
||||
// TODO generalize condition checks
|
||||
// this is pretty janky, but I just want to bodge a method that checks if an entity can breathe a gas mixture
|
||||
// Applying actual reaction effects require a full ReagentEffectArgs struct.
|
||||
bool CanMetabolize(ReagentEffect effect)
|
||||
{
|
||||
if (effect.Conditions == null)
|
||||
return true;
|
||||
|
||||
foreach (var cond in effect.Conditions)
|
||||
{
|
||||
if (cond is OrganType organ && !organ.Condition(lung, EntityManager))
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return saturation;
|
||||
}
|
||||
|
||||
private void TakeSuffocationDamage(Entity<RespiratorComponent> ent)
|
||||
{
|
||||
if (ent.Comp.SuffocationCycles == 2)
|
||||
|
||||
@@ -36,7 +36,6 @@ public sealed class CardboardBoxSystem : SharedCardboardBoxSystem
|
||||
SubscribeLocalEvent<CardboardBoxComponent, StorageAfterCloseEvent>(AfterStorageClosed);
|
||||
SubscribeLocalEvent<CardboardBoxComponent, GetAdditionalAccessEvent>(OnGetAdditionalAccess);
|
||||
SubscribeLocalEvent<CardboardBoxComponent, ActivateInWorldEvent>(OnInteracted);
|
||||
SubscribeLocalEvent<CardboardBoxComponent, InteractedNoHandEvent>(OnNoHandInteracted);
|
||||
SubscribeLocalEvent<CardboardBoxComponent, EntInsertedIntoContainerMessage>(OnEntInserted);
|
||||
SubscribeLocalEvent<CardboardBoxComponent, EntRemovedFromContainerMessage>(OnEntRemoved);
|
||||
|
||||
@@ -45,9 +44,18 @@ public sealed class CardboardBoxSystem : SharedCardboardBoxSystem
|
||||
|
||||
private void OnInteracted(EntityUid uid, CardboardBoxComponent component, ActivateInWorldEvent args)
|
||||
{
|
||||
if (args.Handled)
|
||||
return;
|
||||
|
||||
if (!TryComp<EntityStorageComponent>(uid, out var box))
|
||||
return;
|
||||
|
||||
if (!args.Complex)
|
||||
{
|
||||
if (box.Open || !box.Contents.Contains(args.User))
|
||||
return;
|
||||
}
|
||||
|
||||
args.Handled = true;
|
||||
_storage.ToggleOpen(args.User, uid, box);
|
||||
|
||||
@@ -58,15 +66,6 @@ public sealed class CardboardBoxSystem : SharedCardboardBoxSystem
|
||||
}
|
||||
}
|
||||
|
||||
private void OnNoHandInteracted(EntityUid uid, CardboardBoxComponent component, InteractedNoHandEvent args)
|
||||
{
|
||||
//Free the mice please
|
||||
if (!TryComp<EntityStorageComponent>(uid, out var box) || box.Open || !box.Contents.Contains(args.User))
|
||||
return;
|
||||
|
||||
_storage.OpenStorage(uid);
|
||||
}
|
||||
|
||||
private void OnGetAdditionalAccess(EntityUid uid, CardboardBoxComponent component, ref GetAdditionalAccessEvent args)
|
||||
{
|
||||
if (component.Mover == null)
|
||||
|
||||
@@ -6,7 +6,7 @@ using Robust.Shared.Console;
|
||||
|
||||
namespace Content.Server.Chat.Commands;
|
||||
|
||||
[AdminCommand(AdminFlags.Server)]
|
||||
[AdminCommand(AdminFlags.Admin)]
|
||||
public sealed class SetOOCCommand : IConsoleCommand
|
||||
{
|
||||
public string Command => "setooc";
|
||||
|
||||
@@ -10,6 +10,7 @@ using Content.Server.Speech.EntitySystems;
|
||||
using Content.Server.Station.Components;
|
||||
using Content.Server.Station.Systems;
|
||||
using Content.Shared.ActionBlocker;
|
||||
using Content.Shared.Administration;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.Chat;
|
||||
using Content.Shared.Database;
|
||||
@@ -280,9 +281,13 @@ public sealed partial class ChatSystem : SharedChatSystem
|
||||
message = SanitizeInGameOOCMessage(message);
|
||||
|
||||
var sendType = type;
|
||||
// If dead player LOOC is disabled, unless you are an aghost, send dead messages to dead chat
|
||||
if (!_adminManager.IsAdmin(player) && !_deadLoocEnabled &&
|
||||
(HasComp<GhostComponent>(source) || _mobStateSystem.IsDead(source)))
|
||||
// If dead player LOOC is disabled, unless you are an admin with Moderator perms, send dead messages to dead chat
|
||||
if ((_adminManager.IsAdmin(player) && _adminManager.HasAdminFlag(player, AdminFlags.Moderator)) // Override if admin
|
||||
|| _deadLoocEnabled
|
||||
|| (!HasComp<GhostComponent>(source) && !_mobStateSystem.IsDead(source))) // Check that player is not dead
|
||||
{
|
||||
}
|
||||
else
|
||||
sendType = InGameOOCChatType.Dead;
|
||||
|
||||
// If crit player LOOC is disabled, don't send the message at all.
|
||||
@@ -382,7 +387,7 @@ public sealed partial class ChatSystem : SharedChatSystem
|
||||
if (!_actionBlocker.CanSpeak(source) && !ignoreActionBlocker)
|
||||
return;
|
||||
|
||||
var message = TransformSpeech(source, FormattedMessage.RemoveMarkup(originalMessage));
|
||||
var message = TransformSpeech(source, originalMessage);
|
||||
|
||||
if (message.Length == 0)
|
||||
return;
|
||||
@@ -421,7 +426,7 @@ public sealed partial class ChatSystem : SharedChatSystem
|
||||
|
||||
// To avoid logging any messages sent by entities that are not players, like vendors, cloning, etc.
|
||||
// Also doesn't log if hideLog is true.
|
||||
if (!HasComp<ActorComponent>(source) || hideLog == true)
|
||||
if (!HasComp<ActorComponent>(source) || hideLog)
|
||||
return;
|
||||
|
||||
if (originalMessage == message)
|
||||
|
||||
@@ -35,16 +35,16 @@ public sealed class HypospraySystem : SharedHypospraySystem
|
||||
SubscribeLocalEvent<HyposprayComponent, UseInHandEvent>(OnUseInHand);
|
||||
}
|
||||
|
||||
private void UseHypospray(Entity<HyposprayComponent> entity, EntityUid target, EntityUid user)
|
||||
private bool TryUseHypospray(Entity<HyposprayComponent> entity, EntityUid target, EntityUid user)
|
||||
{
|
||||
// if target is ineligible but is a container, try to draw from the container
|
||||
if (!EligibleEntity(target, EntityManager, entity)
|
||||
&& _solutionContainers.TryGetDrawableSolution(target, out var drawableSolution, out _))
|
||||
{
|
||||
TryDraw(entity, target, drawableSolution.Value, user);
|
||||
return TryDraw(entity, target, drawableSolution.Value, user);
|
||||
}
|
||||
|
||||
TryDoInject(entity, target, user);
|
||||
return TryDoInject(entity, target, user);
|
||||
}
|
||||
|
||||
private void OnUseInHand(Entity<HyposprayComponent> entity, ref UseInHandEvent args)
|
||||
@@ -52,8 +52,7 @@ public sealed class HypospraySystem : SharedHypospraySystem
|
||||
if (args.Handled)
|
||||
return;
|
||||
|
||||
TryDoInject(entity, args.User, args.User);
|
||||
args.Handled = true;
|
||||
args.Handled = TryDoInject(entity, args.User, args.User);
|
||||
}
|
||||
|
||||
public void OnAfterInteract(Entity<HyposprayComponent> entity, ref AfterInteractEvent args)
|
||||
@@ -61,8 +60,7 @@ public sealed class HypospraySystem : SharedHypospraySystem
|
||||
if (args.Handled || !args.CanReach || args.Target == null)
|
||||
return;
|
||||
|
||||
UseHypospray(entity, args.Target.Value, args.User);
|
||||
args.Handled = true;
|
||||
args.Handled = TryUseHypospray(entity, args.Target.Value, args.User);
|
||||
}
|
||||
|
||||
public void OnAttack(Entity<HyposprayComponent> entity, ref MeleeHitEvent args)
|
||||
@@ -150,12 +148,12 @@ public sealed class HypospraySystem : SharedHypospraySystem
|
||||
return true;
|
||||
}
|
||||
|
||||
private void TryDraw(Entity<HyposprayComponent> entity, Entity<BloodstreamComponent?> target, Entity<SolutionComponent> targetSolution, EntityUid user)
|
||||
private bool TryDraw(Entity<HyposprayComponent> entity, Entity<BloodstreamComponent?> target, Entity<SolutionComponent> targetSolution, EntityUid user)
|
||||
{
|
||||
if (!_solutionContainers.TryGetSolution(entity.Owner, entity.Comp.SolutionName, out var soln,
|
||||
out var solution) || solution.AvailableVolume == 0)
|
||||
{
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get transfer amount. May be smaller than _transferAmount if not enough room, also make sure there's room in the injector
|
||||
@@ -168,19 +166,20 @@ public sealed class HypospraySystem : SharedHypospraySystem
|
||||
Loc.GetString("injector-component-target-is-empty-message",
|
||||
("target", Identity.Entity(target, EntityManager))),
|
||||
entity.Owner, user);
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
var removedSolution = _solutionContainers.Draw(target.Owner, targetSolution, realTransferAmount);
|
||||
|
||||
if (!_solutionContainers.TryAddSolution(soln.Value, removedSolution))
|
||||
{
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
_popup.PopupEntity(Loc.GetString("injector-component-draw-success-message",
|
||||
("amount", removedSolution.Volume),
|
||||
("target", Identity.Entity(target, EntityManager))), entity.Owner, user);
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool EligibleEntity(EntityUid entity, IEntityManager entMan, HyposprayComponent component)
|
||||
|
||||
@@ -29,50 +29,43 @@ public sealed class InjectorSystem : SharedInjectorSystem
|
||||
SubscribeLocalEvent<InjectorComponent, AfterInteractEvent>(OnInjectorAfterInteract);
|
||||
}
|
||||
|
||||
private void UseInjector(Entity<InjectorComponent> injector, EntityUid target, EntityUid user)
|
||||
private bool TryUseInjector(Entity<InjectorComponent> injector, EntityUid target, EntityUid user)
|
||||
{
|
||||
// Handle injecting/drawing for solutions
|
||||
if (injector.Comp.ToggleState == InjectorToggleMode.Inject)
|
||||
{
|
||||
if (SolutionContainers.TryGetInjectableSolution(target, out var injectableSolution, out _))
|
||||
{
|
||||
TryInject(injector, target, injectableSolution.Value, user, false);
|
||||
}
|
||||
else if (SolutionContainers.TryGetRefillableSolution(target, out var refillableSolution, out _))
|
||||
{
|
||||
TryInject(injector, target, refillableSolution.Value, user, true);
|
||||
}
|
||||
else if (TryComp<BloodstreamComponent>(target, out var bloodstream))
|
||||
{
|
||||
TryInjectIntoBloodstream(injector, (target, bloodstream), user);
|
||||
}
|
||||
else
|
||||
{
|
||||
Popup.PopupEntity(Loc.GetString("injector-component-cannot-transfer-message",
|
||||
("target", Identity.Entity(target, EntityManager))), injector, user);
|
||||
}
|
||||
return TryInject(injector, target, injectableSolution.Value, user, false);
|
||||
|
||||
if (SolutionContainers.TryGetRefillableSolution(target, out var refillableSolution, out _))
|
||||
return TryInject(injector, target, refillableSolution.Value, user, true);
|
||||
|
||||
if (TryComp<BloodstreamComponent>(target, out var bloodstream))
|
||||
return TryInjectIntoBloodstream(injector, (target, bloodstream), user);
|
||||
|
||||
Popup.PopupEntity(Loc.GetString("injector-component-cannot-transfer-message",
|
||||
("target", Identity.Entity(target, EntityManager))), injector, user);
|
||||
return false;
|
||||
}
|
||||
else if (injector.Comp.ToggleState == InjectorToggleMode.Draw)
|
||||
|
||||
if (injector.Comp.ToggleState == InjectorToggleMode.Draw)
|
||||
{
|
||||
// Draw from a bloodstream, if the target has that
|
||||
if (TryComp<BloodstreamComponent>(target, out var stream) &&
|
||||
SolutionContainers.ResolveSolution(target, stream.BloodSolutionName, ref stream.BloodSolution))
|
||||
{
|
||||
TryDraw(injector, (target, stream), stream.BloodSolution.Value, user);
|
||||
return;
|
||||
return TryDraw(injector, (target, stream), stream.BloodSolution.Value, user);
|
||||
}
|
||||
|
||||
// Draw from an object (food, beaker, etc)
|
||||
if (SolutionContainers.TryGetDrawableSolution(target, out var drawableSolution, out _))
|
||||
{
|
||||
TryDraw(injector, target, drawableSolution.Value, user);
|
||||
}
|
||||
else
|
||||
{
|
||||
Popup.PopupEntity(Loc.GetString("injector-component-cannot-draw-message",
|
||||
("target", Identity.Entity(target, EntityManager))), injector.Owner, user);
|
||||
}
|
||||
return TryDraw(injector, target, drawableSolution.Value, user);
|
||||
|
||||
Popup.PopupEntity(Loc.GetString("injector-component-cannot-draw-message",
|
||||
("target", Identity.Entity(target, EntityManager))), injector.Owner, user);
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void OnInjectDoAfter(Entity<InjectorComponent> entity, ref InjectorDoAfterEvent args)
|
||||
@@ -80,8 +73,7 @@ public sealed class InjectorSystem : SharedInjectorSystem
|
||||
if (args.Cancelled || args.Handled || args.Args.Target == null)
|
||||
return;
|
||||
|
||||
UseInjector(entity, args.Args.Target.Value, args.Args.User);
|
||||
args.Handled = true;
|
||||
args.Handled = TryUseInjector(entity, args.Args.Target.Value, args.Args.User);
|
||||
}
|
||||
|
||||
private void OnInjectorAfterInteract(Entity<InjectorComponent> entity, ref AfterInteractEvent args)
|
||||
@@ -105,8 +97,7 @@ public sealed class InjectorSystem : SharedInjectorSystem
|
||||
return;
|
||||
}
|
||||
|
||||
UseInjector(entity, target, args.User);
|
||||
args.Handled = true;
|
||||
args.Handled = TryUseInjector(entity, target, args.User);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -214,7 +205,7 @@ public sealed class InjectorSystem : SharedInjectorSystem
|
||||
});
|
||||
}
|
||||
|
||||
private void TryInjectIntoBloodstream(Entity<InjectorComponent> injector, Entity<BloodstreamComponent> target,
|
||||
private bool TryInjectIntoBloodstream(Entity<InjectorComponent> injector, Entity<BloodstreamComponent> target,
|
||||
EntityUid user)
|
||||
{
|
||||
// Get transfer amount. May be smaller than _transferAmount if not enough room
|
||||
@@ -224,7 +215,7 @@ public sealed class InjectorSystem : SharedInjectorSystem
|
||||
Popup.PopupEntity(
|
||||
Loc.GetString("injector-component-cannot-inject-message",
|
||||
("target", Identity.Entity(target, EntityManager))), injector.Owner, user);
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
var realTransferAmount = FixedPoint2.Min(injector.Comp.TransferAmount, chemSolution.AvailableVolume);
|
||||
@@ -233,7 +224,7 @@ public sealed class InjectorSystem : SharedInjectorSystem
|
||||
Popup.PopupEntity(
|
||||
Loc.GetString("injector-component-cannot-inject-message",
|
||||
("target", Identity.Entity(target, EntityManager))), injector.Owner, user);
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Move units from attackSolution to targetSolution
|
||||
@@ -249,14 +240,15 @@ public sealed class InjectorSystem : SharedInjectorSystem
|
||||
|
||||
Dirty(injector);
|
||||
AfterInject(injector, target);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void TryInject(Entity<InjectorComponent> injector, EntityUid targetEntity,
|
||||
private bool TryInject(Entity<InjectorComponent> injector, EntityUid targetEntity,
|
||||
Entity<SolutionComponent> targetSolution, EntityUid user, bool asRefill)
|
||||
{
|
||||
if (!SolutionContainers.TryGetSolution(injector.Owner, injector.Comp.SolutionName, out var soln,
|
||||
out var solution) || solution.Volume == 0)
|
||||
return;
|
||||
return false;
|
||||
|
||||
// Get transfer amount. May be smaller than _transferAmount if not enough room
|
||||
var realTransferAmount =
|
||||
@@ -268,7 +260,7 @@ public sealed class InjectorSystem : SharedInjectorSystem
|
||||
Loc.GetString("injector-component-target-already-full-message",
|
||||
("target", Identity.Entity(targetEntity, EntityManager))),
|
||||
injector.Owner, user);
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Move units from attackSolution to targetSolution
|
||||
@@ -291,6 +283,7 @@ public sealed class InjectorSystem : SharedInjectorSystem
|
||||
|
||||
Dirty(injector);
|
||||
AfterInject(injector, targetEntity);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void AfterInject(Entity<InjectorComponent> injector, EntityUid target)
|
||||
@@ -321,13 +314,13 @@ public sealed class InjectorSystem : SharedInjectorSystem
|
||||
RaiseLocalEvent(target, ref ev);
|
||||
}
|
||||
|
||||
private void TryDraw(Entity<InjectorComponent> injector, Entity<BloodstreamComponent?> target,
|
||||
private bool TryDraw(Entity<InjectorComponent> injector, Entity<BloodstreamComponent?> target,
|
||||
Entity<SolutionComponent> targetSolution, EntityUid user)
|
||||
{
|
||||
if (!SolutionContainers.TryGetSolution(injector.Owner, injector.Comp.SolutionName, out var soln,
|
||||
out var solution) || solution.AvailableVolume == 0)
|
||||
{
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get transfer amount. May be smaller than _transferAmount if not enough room, also make sure there's room in the injector
|
||||
@@ -340,14 +333,14 @@ public sealed class InjectorSystem : SharedInjectorSystem
|
||||
Loc.GetString("injector-component-target-is-empty-message",
|
||||
("target", Identity.Entity(target, EntityManager))),
|
||||
injector.Owner, user);
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
// We have some snowflaked behavior for streams.
|
||||
if (target.Comp != null)
|
||||
{
|
||||
DrawFromBlood(injector, (target.Owner, target.Comp), soln.Value, realTransferAmount, user);
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Move units from attackSolution to targetSolution
|
||||
@@ -355,7 +348,7 @@ public sealed class InjectorSystem : SharedInjectorSystem
|
||||
|
||||
if (!SolutionContainers.TryAddSolution(soln.Value, removedSolution))
|
||||
{
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
Popup.PopupEntity(Loc.GetString("injector-component-draw-success-message",
|
||||
@@ -364,6 +357,7 @@ public sealed class InjectorSystem : SharedInjectorSystem
|
||||
|
||||
Dirty(injector);
|
||||
AfterDraw(injector, target);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void DrawFromBlood(Entity<InjectorComponent> injector, Entity<BloodstreamComponent> target,
|
||||
|
||||
@@ -25,9 +25,15 @@ namespace Content.Server.Chemistry.ReagentEffectConditions
|
||||
if (args.OrganEntity == null)
|
||||
return false;
|
||||
|
||||
if (args.EntityManager.TryGetComponent<MetabolizerComponent>(args.OrganEntity.Value, out var metabolizer)
|
||||
&& metabolizer.MetabolizerTypes != null
|
||||
&& metabolizer.MetabolizerTypes.Contains(Type))
|
||||
return Condition(args.OrganEntity.Value, args.EntityManager);
|
||||
}
|
||||
|
||||
public bool Condition(Entity<MetabolizerComponent?> metabolizer, IEntityManager entMan)
|
||||
{
|
||||
metabolizer.Comp ??= entMan.GetComponentOrNull<MetabolizerComponent>(metabolizer.Owner);
|
||||
if (metabolizer.Comp != null
|
||||
&& metabolizer.Comp.MetabolizerTypes != null
|
||||
&& metabolizer.Comp.MetabolizerTypes.Contains(Type))
|
||||
return ShouldHave;
|
||||
return !ShouldHave;
|
||||
}
|
||||
|
||||
@@ -12,9 +12,6 @@ namespace Content.Server.Chemistry.ReagentEffects.PlantMetabolism
|
||||
[DataField]
|
||||
public float Amount { get; protected set; } = 1;
|
||||
|
||||
[DataField]
|
||||
public float Prob { get; protected set; } = 1; // = (80);
|
||||
|
||||
/// <summary>
|
||||
/// Localisation key for the name of the adjusted attribute. Used for guidebook descriptions.
|
||||
/// </summary>
|
||||
@@ -45,11 +42,7 @@ namespace Content.Server.Chemistry.ReagentEffects.PlantMetabolism
|
||||
|| mustHaveAlivePlant && (plantHolderComponent.Seed == null || plantHolderComponent.Dead))
|
||||
return false;
|
||||
|
||||
if (Prob >= 1f)
|
||||
return true;
|
||||
|
||||
// Dependencies are never injected for reagents if you intend to do that for this.
|
||||
return !(Prob <= 0f) && IoCManager.Resolve<IRobustRandom>().Prob(Prob);
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys)
|
||||
|
||||
@@ -59,23 +59,27 @@ public sealed class MachineFrameSystem : EntitySystem
|
||||
return;
|
||||
}
|
||||
|
||||
// Machine parts cannot currently satisfy stack/component/tag restrictions. Similarly stacks cannot satisfy
|
||||
// component/tag restrictions. However, there is no reason this cannot be supported in the future. If this
|
||||
// changes, then RegenerateProgress() also needs to be updated.
|
||||
//
|
||||
// If this changes in the future, then RegenerateProgress() also needs to be updated.
|
||||
// Note that one entity is ALLOWED to satisfy more than one kind of component or tag requirements. This is
|
||||
// necessary in order to avoid weird entity-ordering shenanigans in RegenerateProgress().
|
||||
var stack = CompOrNull<StackComponent>(args.Used);
|
||||
var machinePart = CompOrNull<MachinePartComponent>(args.Used);
|
||||
if (stack != null && machinePart != null)
|
||||
{
|
||||
if (TryInsertPartStack(uid, args.Used, component, machinePart, stack))
|
||||
args.Handled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle parts
|
||||
if (TryComp<MachinePartComponent>(args.Used, out var machinePart))
|
||||
if (machinePart != null)
|
||||
{
|
||||
if (TryInsertPart(uid, args.Used, component, machinePart))
|
||||
args.Handled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle stacks
|
||||
if (TryComp<StackComponent>(args.Used, out var stack))
|
||||
if (stack != null)
|
||||
{
|
||||
if (TryInsertStack(uid, args.Used, component, stack))
|
||||
args.Handled = true;
|
||||
@@ -191,6 +195,44 @@ public sealed class MachineFrameSystem : EntitySystem
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <returns>Whether or not the function had any effect. Does not indicate success.</returns>
|
||||
private bool TryInsertPartStack(EntityUid uid, EntityUid used, MachineFrameComponent component, MachinePartComponent machinePart, StackComponent stack)
|
||||
{
|
||||
if (!component.Requirements.ContainsKey(machinePart.PartType))
|
||||
return false;
|
||||
|
||||
var progress = component.Progress[machinePart.PartType];
|
||||
var requirement = component.Requirements[machinePart.PartType];
|
||||
|
||||
var needed = requirement - progress;
|
||||
if (needed <= 0)
|
||||
return false;
|
||||
|
||||
var count = stack.Count;
|
||||
if (count < needed)
|
||||
{
|
||||
if (!_container.Insert(used, component.PartContainer))
|
||||
return true;
|
||||
|
||||
component.Progress[machinePart.PartType] += count;
|
||||
return true;
|
||||
}
|
||||
|
||||
var splitStack = _stack.Split(used, needed, Transform(uid).Coordinates, stack);
|
||||
|
||||
if (splitStack == null)
|
||||
return false;
|
||||
|
||||
if (!_container.Insert(splitStack.Value, component.PartContainer))
|
||||
return true;
|
||||
|
||||
component.Progress[machinePart.PartType] += needed;
|
||||
if (IsComplete(component))
|
||||
_popupSystem.PopupEntity(Loc.GetString("machine-frame-component-on-complete"), uid);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <returns>Whether or not the function had any effect. Does not indicate success.</returns>
|
||||
private bool TryInsertStack(EntityUid uid, EntityUid used, MachineFrameComponent component, StackComponent stack)
|
||||
{
|
||||
@@ -328,8 +370,6 @@ public sealed class MachineFrameSystem : EntitySystem
|
||||
{
|
||||
if (TryComp<MachinePartComponent>(part, out var machinePart))
|
||||
{
|
||||
DebugTools.Assert(!HasComp<StackComponent>(part));
|
||||
|
||||
// Check this is part of the requirements...
|
||||
if (!component.Requirements.ContainsKey(machinePart.PartType))
|
||||
continue;
|
||||
@@ -338,7 +378,6 @@ public sealed class MachineFrameSystem : EntitySystem
|
||||
component.Progress[machinePart.PartType] = 1;
|
||||
else
|
||||
component.Progress[machinePart.PartType]++;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ using Robust.Shared.Console;
|
||||
|
||||
namespace Content.Server.Damage.Commands
|
||||
{
|
||||
[AdminCommand(AdminFlags.Admin)]
|
||||
[AdminCommand(AdminFlags.Fun)]
|
||||
public sealed class GodModeCommand : IConsoleCommand
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _entManager = default!;
|
||||
|
||||
@@ -22,7 +22,7 @@ namespace Content.Server.Damage.Systems
|
||||
private void OnAttemptPacifiedThrow(Entity<DamageOnLandComponent> ent, ref AttemptPacifiedThrowEvent args)
|
||||
{
|
||||
// Allow healing projectiles, forbid any that do damage:
|
||||
if (ent.Comp.Damage.Any())
|
||||
if (ent.Comp.Damage.AnyPositive())
|
||||
{
|
||||
args.Cancel("pacified-cannot-throw");
|
||||
}
|
||||
|
||||
@@ -14,9 +14,11 @@ using Content.Shared.Humanoid;
|
||||
using Content.Shared.Humanoid.Markings;
|
||||
using Content.Shared.Preferences;
|
||||
using Content.Shared.Preferences.Loadouts;
|
||||
using Content.Shared.Roles;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Robust.Shared.Enums;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Server.Database
|
||||
@@ -1587,6 +1589,65 @@ INSERT INTO player_round (players_id, rounds_id) VALUES ({players[player]}, {id}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Job Whitelists
|
||||
|
||||
public async Task<bool> AddJobWhitelist(Guid player, ProtoId<JobPrototype> job)
|
||||
{
|
||||
await using var db = await GetDb();
|
||||
var exists = await db.DbContext.RoleWhitelists
|
||||
.Where(w => w.PlayerUserId == player)
|
||||
.Where(w => w.RoleId == job.Id)
|
||||
.AnyAsync();
|
||||
|
||||
if (exists)
|
||||
return false;
|
||||
|
||||
var whitelist = new RoleWhitelist
|
||||
{
|
||||
PlayerUserId = player,
|
||||
RoleId = job
|
||||
};
|
||||
db.DbContext.RoleWhitelists.Add(whitelist);
|
||||
await db.DbContext.SaveChangesAsync();
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<List<string>> GetJobWhitelists(Guid player, CancellationToken cancel)
|
||||
{
|
||||
await using var db = await GetDb(cancel);
|
||||
return await db.DbContext.RoleWhitelists
|
||||
.Where(w => w.PlayerUserId == player)
|
||||
.Select(w => w.RoleId)
|
||||
.ToListAsync(cancellationToken: cancel);
|
||||
}
|
||||
|
||||
public async Task<bool> IsJobWhitelisted(Guid player, ProtoId<JobPrototype> job)
|
||||
{
|
||||
await using var db = await GetDb();
|
||||
return await db.DbContext.RoleWhitelists
|
||||
.Where(w => w.PlayerUserId == player)
|
||||
.Where(w => w.RoleId == job.Id)
|
||||
.AnyAsync();
|
||||
}
|
||||
|
||||
public async Task<bool> RemoveJobWhitelist(Guid player, ProtoId<JobPrototype> job)
|
||||
{
|
||||
await using var db = await GetDb();
|
||||
var entry = await db.DbContext.RoleWhitelists
|
||||
.Where(w => w.PlayerUserId == player)
|
||||
.Where(w => w.RoleId == job.Id)
|
||||
.SingleOrDefaultAsync();
|
||||
|
||||
if (entry == null)
|
||||
return false;
|
||||
|
||||
db.DbContext.RoleWhitelists.Remove(entry);
|
||||
await db.DbContext.SaveChangesAsync();
|
||||
return true;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
// SQLite returns DateTime as Kind=Unspecified, Npgsql actually knows for sure it's Kind=Utc.
|
||||
// Normalize DateTimes here so they're always Utc. Thanks.
|
||||
protected abstract DateTime NormalizeDatabaseTime(DateTime time);
|
||||
|
||||
@@ -9,6 +9,7 @@ using Content.Shared.Administration.Logs;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.Preferences;
|
||||
using Content.Shared.Roles;
|
||||
using Microsoft.Data.Sqlite;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@@ -17,6 +18,7 @@ using Prometheus;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.ContentPack;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Prototypes;
|
||||
using LogLevel = Robust.Shared.Log.LogLevel;
|
||||
using MSLogLevel = Microsoft.Extensions.Logging.LogLevel;
|
||||
|
||||
@@ -290,6 +292,18 @@ namespace Content.Server.Database
|
||||
Task MarkMessageAsSeen(int id, bool dismissedToo);
|
||||
|
||||
#endregion
|
||||
|
||||
#region Job Whitelists
|
||||
|
||||
Task AddJobWhitelist(Guid player, ProtoId<JobPrototype> job);
|
||||
|
||||
|
||||
Task<List<string>> GetJobWhitelists(Guid player, CancellationToken cancel = default);
|
||||
Task<bool> IsJobWhitelisted(Guid player, ProtoId<JobPrototype> job);
|
||||
|
||||
Task<bool> RemoveJobWhitelist(Guid player, ProtoId<JobPrototype> job);
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
public sealed class ServerDbManager : IServerDbManager
|
||||
@@ -869,6 +883,30 @@ namespace Content.Server.Database
|
||||
return RunDbCommand(() => _db.MarkMessageAsSeen(id, dismissedToo));
|
||||
}
|
||||
|
||||
public Task AddJobWhitelist(Guid player, ProtoId<JobPrototype> job)
|
||||
{
|
||||
DbWriteOpsMetric.Inc();
|
||||
return RunDbCommand(() => _db.AddJobWhitelist(player, job));
|
||||
}
|
||||
|
||||
public Task<List<string>> GetJobWhitelists(Guid player, CancellationToken cancel = default)
|
||||
{
|
||||
DbReadOpsMetric.Inc();
|
||||
return RunDbCommand(() => _db.GetJobWhitelists(player, cancel));
|
||||
}
|
||||
|
||||
public Task<bool> IsJobWhitelisted(Guid player, ProtoId<JobPrototype> job)
|
||||
{
|
||||
DbReadOpsMetric.Inc();
|
||||
return RunDbCommand(() => _db.IsJobWhitelisted(player, job));
|
||||
}
|
||||
|
||||
public Task<bool> RemoveJobWhitelist(Guid player, ProtoId<JobPrototype> job)
|
||||
{
|
||||
DbWriteOpsMetric.Inc();
|
||||
return RunDbCommand(() => _db.RemoveJobWhitelist(player, job));
|
||||
}
|
||||
|
||||
// Wrapper functions to run DB commands from the thread pool.
|
||||
// This will avoid SynchronizationContext capturing and avoid running CPU work on the main thread.
|
||||
// For SQLite, this will also enable read parallelization (within limits).
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Content.Server.Players.PlayTimeTracking;
|
||||
using Content.Server.Preferences.Managers;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Utility;
|
||||
@@ -19,11 +17,12 @@ namespace Content.Server.Database;
|
||||
/// </remarks>
|
||||
public sealed class UserDbDataManager : IPostInjectInit
|
||||
{
|
||||
[Dependency] private readonly IServerPreferencesManager _prefs = default!;
|
||||
[Dependency] private readonly ILogManager _logManager = default!;
|
||||
[Dependency] private readonly PlayTimeTrackingManager _playTimeTracking = default!;
|
||||
|
||||
private readonly Dictionary<NetUserId, UserData> _users = new();
|
||||
private readonly List<OnLoadPlayer> _onLoadPlayer = [];
|
||||
private readonly List<OnFinishLoad> _onFinishLoad = [];
|
||||
private readonly List<OnPlayerDisconnect> _onPlayerDisconnect = [];
|
||||
|
||||
private ISawmill _sawmill = default!;
|
||||
|
||||
@@ -51,8 +50,10 @@ public sealed class UserDbDataManager : IPostInjectInit
|
||||
data.Cancel.Cancel();
|
||||
data.Cancel.Dispose();
|
||||
|
||||
_prefs.OnClientDisconnected(session);
|
||||
_playTimeTracking.ClientDisconnected(session);
|
||||
foreach (var onDisconnect in _onPlayerDisconnect)
|
||||
{
|
||||
onDisconnect(session);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task Load(ICommonSession session, CancellationToken cancel)
|
||||
@@ -62,12 +63,20 @@ public sealed class UserDbDataManager : IPostInjectInit
|
||||
// As such, this task must NOT throw a non-cancellation error!
|
||||
try
|
||||
{
|
||||
await Task.WhenAll(
|
||||
_prefs.LoadData(session, cancel),
|
||||
_playTimeTracking.LoadData(session, cancel));
|
||||
var tasks = new List<Task>();
|
||||
foreach (var action in _onLoadPlayer)
|
||||
{
|
||||
tasks.Add(action(session, cancel));
|
||||
}
|
||||
|
||||
await Task.WhenAll(tasks);
|
||||
|
||||
cancel.ThrowIfCancellationRequested();
|
||||
_prefs.FinishLoad(session);
|
||||
|
||||
foreach (var action in _onFinishLoad)
|
||||
{
|
||||
action(session);
|
||||
}
|
||||
|
||||
_sawmill.Verbose($"Load complete for user {session}");
|
||||
}
|
||||
@@ -118,10 +127,31 @@ public sealed class UserDbDataManager : IPostInjectInit
|
||||
return _users[session.UserId].Task;
|
||||
}
|
||||
|
||||
public void AddOnLoadPlayer(OnLoadPlayer action)
|
||||
{
|
||||
_onLoadPlayer.Add(action);
|
||||
}
|
||||
|
||||
public void AddOnFinishLoad(OnFinishLoad action)
|
||||
{
|
||||
_onFinishLoad.Add(action);
|
||||
}
|
||||
|
||||
public void AddOnPlayerDisconnect(OnPlayerDisconnect action)
|
||||
{
|
||||
_onPlayerDisconnect.Add(action);
|
||||
}
|
||||
|
||||
void IPostInjectInit.PostInject()
|
||||
{
|
||||
_sawmill = _logManager.GetSawmill("userdb");
|
||||
}
|
||||
|
||||
private sealed record UserData(CancellationTokenSource Cancel, Task Task);
|
||||
|
||||
public delegate Task OnLoadPlayer(ICommonSession player, CancellationToken cancel);
|
||||
|
||||
public delegate void OnFinishLoad(ICommonSession player);
|
||||
|
||||
public delegate void OnPlayerDisconnect(ICommonSession player);
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ public sealed class SignalSwitchSystem : EntitySystem
|
||||
|
||||
private void OnActivated(EntityUid uid, SignalSwitchComponent comp, ActivateInWorldEvent args)
|
||||
{
|
||||
if (args.Handled)
|
||||
if (args.Handled || !args.Complex)
|
||||
return;
|
||||
|
||||
comp.State = !comp.State;
|
||||
|
||||
@@ -152,6 +152,9 @@ public sealed class MailingUnitSystem : EntitySystem
|
||||
|
||||
private void HandleActivate(EntityUid uid, MailingUnitComponent component, ActivateInWorldEvent args)
|
||||
{
|
||||
if (args.Handled || !args.Complex)
|
||||
return;
|
||||
|
||||
if (!EntityManager.TryGetComponent(args.User, out ActorComponent? actor))
|
||||
{
|
||||
return;
|
||||
|
||||
@@ -263,6 +263,9 @@ public sealed class DisposalUnitSystem : SharedDisposalUnitSystem
|
||||
|
||||
private void OnActivate(EntityUid uid, SharedDisposalUnitComponent component, ActivateInWorldEvent args)
|
||||
{
|
||||
if (args.Handled || !args.Complex)
|
||||
return;
|
||||
|
||||
if (!TryComp(args.User, out ActorComponent? actor))
|
||||
{
|
||||
return;
|
||||
|
||||
@@ -68,6 +68,9 @@ public sealed class AirlockSystem : SharedAirlockSystem
|
||||
|
||||
private void OnActivate(EntityUid uid, AirlockComponent component, ActivateInWorldEvent args)
|
||||
{
|
||||
if (args.Handled || !args.Complex)
|
||||
return;
|
||||
|
||||
if (TryComp<WiresPanelComponent>(uid, out var panel) &&
|
||||
panel.Open &&
|
||||
TryComp<ActorComponent>(args.User, out var actor))
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
using Content.Server.GenericAntag;
|
||||
using Content.Server.Objectives.Components;
|
||||
using Content.Server.Objectives.Systems;
|
||||
using Content.Server.Popups;
|
||||
@@ -55,7 +54,6 @@ public sealed partial class DragonSystem : EntitySystem
|
||||
SubscribeLocalEvent<DragonComponent, DragonSpawnRiftActionEvent>(OnSpawnRift);
|
||||
SubscribeLocalEvent<DragonComponent, RefreshMovementSpeedModifiersEvent>(OnDragonMove);
|
||||
SubscribeLocalEvent<DragonComponent, MobStateChangedEvent>(OnMobStateChanged);
|
||||
SubscribeLocalEvent<DragonComponent, GenericAntagCreatedEvent>(OnCreated);
|
||||
SubscribeLocalEvent<DragonComponent, EntityZombifiedEvent>(OnZombified);
|
||||
}
|
||||
|
||||
@@ -192,18 +190,6 @@ public sealed partial class DragonSystem : EntitySystem
|
||||
DeleteRifts(uid, false, component);
|
||||
}
|
||||
|
||||
private void OnCreated(EntityUid uid, DragonComponent comp, ref GenericAntagCreatedEvent args)
|
||||
{
|
||||
var mindId = args.MindId;
|
||||
var mind = args.Mind;
|
||||
|
||||
_role.MindAddRole(mindId, new DragonRoleComponent(), mind);
|
||||
_role.MindAddRole(mindId, new RoleBriefingComponent()
|
||||
{
|
||||
Briefing = Loc.GetString("dragon-role-briefing")
|
||||
}, mind);
|
||||
}
|
||||
|
||||
private void OnZombified(Entity<DragonComponent> ent, ref EntityZombifiedEvent args)
|
||||
{
|
||||
// prevent carp attacking zombie dragon
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user