Merge remote-tracking branch 'upstream/stable' into ed-311-03-2025-upstream-2

# Conflicts:
#	Content.Client/Administration/AdminNameOverlay.cs
#	Content.Client/Administration/UI/Bwoink/BwoinkControl.xaml
#	Content.Client/Guidebook/Controls/GuideReagentReaction.xaml
#	Content.Client/Options/UI/Tabs/KeyRebindTab.xaml.cs
#	Content.Client/SubFloor/SubFloorHideSystem.cs
#	Content.Server/Administration/Systems/AdminVerbSystem.Antags.cs
#	Content.Server/Antag/AntagSelectionSystem.cs
#	Content.Server/Cloning/CloningSystem.cs
#	Content.Server/GameTicking/Rules/Components/ParadoxCloneRuleComponent.cs
#	Content.Server/GameTicking/Rules/ParadoxCloneRuleSystem.cs
#	Content.Server/Roles/ParadoxCloneRoleComponent.cs
#	Content.Shared.Database/LogType.cs
#	Content.Shared/CCVar/CCVars.Interface.cs
#	Content.Shared/Cloning/CloningEvents.cs
#	Content.Shared/Cloning/CloningSettingsPrototype.cs
#	Content.Shared/Humanoid/NamingSystem.cs
#	Content.Shared/Humanoid/Prototypes/SpeciesPrototype.cs
#	Content.Shared/Light/Components/SunShadowCycleComponent.cs
#	Content.Shared/Storage/StorageComponent.cs
#	Resources/Changelog/Admin.yml
#	Resources/Changelog/Changelog.yml
#	Resources/Credits/GitHub.txt
#	Resources/Locale/en-US/paradox-clone/role.ftl
#	Resources/Maps/bagel.yml
#	Resources/Maps/loop.yml
#	Resources/Prototypes/Chemistry/mixing_types.yml
#	Resources/Prototypes/Datasets/Names/last.yml
#	Resources/Prototypes/Entities/Effects/puddle.yml
#	Resources/Prototypes/Entities/Mobs/Player/clone.yml
#	Resources/Prototypes/Entities/Mobs/Species/base.yml
#	Resources/Prototypes/Entities/Objects/Deliveries/deliveries_tables.yml
#	Resources/Prototypes/Entities/Objects/Devices/pda.yml
#	Resources/Prototypes/Entities/Objects/Tools/handheld_mass_scanner.yml
#	Resources/Prototypes/GameRules/events.yml
#	Resources/Prototypes/Maps/Pools/default.yml
#	Resources/Prototypes/Objectives/paradoxClone.yml
#	Resources/Prototypes/Reagents/Consumable/Drink/alcohol.yml
#	Resources/Textures/Clothing/Eyes/Glasses/jensen.rsi/equipped-EYES-arachnid.png
This commit is contained in:
Ed
2025-03-31 12:41:37 +03:00
1137 changed files with 49288 additions and 36602 deletions

View File

@@ -11,7 +11,7 @@ jobs:
name: Run Benchmarks
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3.6.0
- uses: actions/checkout@v4.2.2
with:
submodules: 'recursive'
- name: Get Engine version

View File

@@ -8,7 +8,7 @@ jobs:
docfx:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3.6.0
- uses: actions/checkout@v4.2.2
- name: Setup submodule
run: |
git submodule update --init --recursive
@@ -19,7 +19,7 @@ jobs:
cd RobustToolbox/
git submodule update --init --recursive
- name: Setup .NET Core
uses: actions/setup-dotnet@v3.2.0
uses: actions/setup-dotnet@v4.1.0
with:
dotnet-version: 9.0.x

View File

@@ -19,7 +19,7 @@ jobs:
steps:
- name: Checkout Master
uses: actions/checkout@v3.6.0
uses: actions/checkout@v4.2.2
- name: Setup Submodule
run: |
@@ -34,7 +34,7 @@ jobs:
git submodule update --init --recursive
- name: Setup .NET Core
uses: actions/setup-dotnet@v3.2.0
uses: actions/setup-dotnet@v4.1.0
with:
dotnet-version: 9.0.x

View File

@@ -19,7 +19,7 @@ jobs:
steps:
- name: Checkout Master
uses: actions/checkout@v3.6.0
uses: actions/checkout@v4.2.2
- name: Setup Submodule
run: |
@@ -34,7 +34,7 @@ jobs:
git submodule update --init --recursive
- name: Setup .NET Core
uses: actions/setup-dotnet@v3.2.0
uses: actions/setup-dotnet@v4.1.0
with:
dotnet-version: 9.0.x

View File

@@ -10,6 +10,6 @@ jobs:
if: github.event.pull_request.draft == false
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3.6.0
- uses: actions/checkout@v4.2.2
- name: Check for CRLF
run: Tools/check_crlf.py

View File

@@ -16,11 +16,11 @@ jobs:
- name: Install dependencies
run: sudo apt-get install -y python3-paramiko python3-lxml
- uses: actions/checkout@v3.6.0
- uses: actions/checkout@v4.2.2
with:
submodules: 'recursive'
- name: Setup .NET Core
uses: actions/setup-dotnet@v3.2.0
uses: actions/setup-dotnet@v4.1.0
with:
dotnet-version: 9.0.x

View File

@@ -11,7 +11,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3.6.0
uses: actions/checkout@v4.2.2
- name: Get changed files
id: files

View File

@@ -34,7 +34,7 @@ jobs:
steps:
- name: Checkout Master
uses: actions/checkout@v3.6.0
uses: actions/checkout@v4.2.2
- name: Setup Submodule
run: |
@@ -49,7 +49,7 @@ jobs:
git submodule update --init --recursive
- name: Setup .NET Core
uses: actions/setup-dotnet@v3.2.0
uses: actions/setup-dotnet@v4.1.0
with:
dotnet-version: 9.0.x

View File

@@ -13,7 +13,7 @@ jobs:
if: github.repository == 'space-wizards/space-station-14'
steps:
- uses: actions/checkout@v3.6.0
- uses: actions/checkout@v4.2.2
with:
ref: master

View File

@@ -12,7 +12,7 @@ jobs:
if: github.actor != 'PJBot' && github.event.pull_request.draft == false
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3.6.0
- uses: actions/checkout@v4.2.2
- name: Setup Submodule
run: git submodule update --init
- name: Pull engine updates

View File

@@ -13,7 +13,7 @@ jobs:
name: Validate RSIs
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3.6.0
- uses: actions/checkout@v4.2.2
- name: Setup Submodule
run: git submodule update --init
- name: Pull engine updates

View File

@@ -12,7 +12,7 @@ jobs:
if: github.actor != 'PJBot' && github.event.pull_request.draft == false
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3.6.0
- uses: actions/checkout@v4.2.2
- name: Setup Submodule
run: git submodule update --init
- name: Pull engine updates

View File

@@ -13,7 +13,7 @@ jobs:
if: github.actor != 'PJBot' && github.event.pull_request.draft == false
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3.6.0
- uses: actions/checkout@v4.2.2
- name: Setup submodule
run: |
git submodule update --init --recursive
@@ -24,7 +24,7 @@ jobs:
cd RobustToolbox/
git submodule update --init --recursive
- name: Setup .NET Core
uses: actions/setup-dotnet@v3.2.0
uses: actions/setup-dotnet@v4.1.0
with:
dotnet-version: 9.0.x
- name: Install dependencies

View File

@@ -1,7 +1,10 @@
using System.Linq;
using System.Numerics;
using Content.Client.Administration.Systems;
using Content.Client.Stylesheets;
using Content.Shared.Administration;
using Content.Shared.CCVar;
using Content.Shared.Ghost;
using Content.Shared.Mind;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
@@ -14,32 +17,54 @@ namespace Content.Client.Administration;
internal sealed class AdminNameOverlay : Overlay
{
[Dependency] private readonly IConfigurationManager _config = default!;
private readonly AdminSystem _system;
private readonly IEntityManager _entityManager;
private readonly IEyeManager _eyeManager;
private readonly EntityLookupSystem _entityLookup;
private readonly IUserInterfaceManager _userInterfaceManager;
private readonly Font _font;
private readonly Font _fontBold;
private bool _overlayClassic;
private bool _overlaySymbols;
private bool _overlayPlaytime;
private bool _overlayStartingJob;
private float _ghostFadeDistance;
private float _ghostHideDistance;
private int _overlayStackMax;
private float _overlayMergeDistance;
//TODO make this adjustable via GUI
private readonly ProtoId<RoleTypePrototype>[] _filter =
["SoloAntagonist", "TeamAntagonist", "SiliconAntagonist", "FreeAgent"];
private readonly string _antagLabelClassic = Loc.GetString("admin-overlay-antag-classic");
private readonly Color _antagColorClassic = Color.OrangeRed;
public AdminNameOverlay(AdminSystem system, IEntityManager entityManager, IEyeManager eyeManager, IResourceCache resourceCache, EntityLookupSystem entityLookup, IUserInterfaceManager userInterfaceManager)
public AdminNameOverlay(
AdminSystem system,
IEntityManager entityManager,
IEyeManager eyeManager,
IResourceCache resourceCache,
EntityLookupSystem entityLookup,
IUserInterfaceManager userInterfaceManager,
IConfigurationManager config)
{
IoCManager.InjectDependencies(this);
_system = system;
_entityManager = entityManager;
_eyeManager = eyeManager;
_entityLookup = entityLookup;
_userInterfaceManager = userInterfaceManager;
ZIndex = 200;
_font = new VectorFont(resourceCache.GetResource<FontResource>("/Fonts/NotoSans/NotoSans-Regular.ttf"), 10);
// Setting these to a specific ttf would break the antag symbols
_font = resourceCache.NotoStack();
_fontBold = resourceCache.NotoStack(variation: "Bold");
config.OnValueChanged(CCVars.AdminOverlayClassic, (show) => { _overlayClassic = show; }, true);
config.OnValueChanged(CCVars.AdminOverlaySymbols, (show) => { _overlaySymbols = show; }, true);
config.OnValueChanged(CCVars.AdminOverlayPlaytime, (show) => { _overlayPlaytime = show; }, true);
config.OnValueChanged(CCVars.AdminOverlayStartingJob, (show) => { _overlayStartingJob = show; }, true);
config.OnValueChanged(CCVars.AdminOverlayGhostHideDistance, (f) => { _ghostHideDistance = f; }, true);
config.OnValueChanged(CCVars.AdminOverlayGhostFadeDistance, (f) => { _ghostFadeDistance = f; }, true);
config.OnValueChanged(CCVars.AdminOverlayStackMax, (i) => { _overlayStackMax = i; }, true);
config.OnValueChanged(CCVars.AdminOverlayMergeDistance, (f) => { _overlayMergeDistance = f; }, true);
}
public override OverlaySpace Space => OverlaySpace.ScreenSpace;
@@ -47,75 +72,147 @@ internal sealed class AdminNameOverlay : Overlay
protected override void Draw(in OverlayDrawArgs args)
{
var viewport = args.WorldAABB;
var colorDisconnected = Color.White;
var uiScale = _userInterfaceManager.RootControl.UIScale;
var lineoffset = new Vector2(0f, 14f) * uiScale;
var drawnOverlays = new List<(Vector2,Vector2)>() ; // A saved list of the overlays already drawn
//TODO make this adjustable via GUI
var classic = _config.GetCVar(CCVars.AdminOverlayClassic);
var playTime = _config.GetCVar(CCVars.AdminOverlayPlaytime);
var startingJob = _config.GetCVar(CCVars.AdminOverlayStartingJob);
foreach (var playerInfo in _system.PlayerList)
// Get all player positions before drawing overlays, so they can be sorted before iteration
var sortable = new List<(PlayerInfo, Box2, EntityUid, Vector2)>();
foreach (var info in _system.PlayerList)
{
var entity = _entityManager.GetEntity(playerInfo.NetEntity);
var entity = _entityManager.GetEntity(info.NetEntity);
// Otherwise the entity can not exist yet
if (entity == null || !_entityManager.EntityExists(entity))
{
// If entity does not exist or is on a different map, skip
if (entity == null
|| !_entityManager.EntityExists(entity)
|| _entityManager.GetComponent<TransformComponent>(entity.Value).MapID != args.MapId)
continue;
}
// if not on the same map, continue
if (_entityManager.GetComponent<TransformComponent>(entity.Value).MapID != args.MapId)
{
continue;
}
var aabb = _entityLookup.GetWorldAABB(entity.Value);
// if not on screen, continue
// if not on screen, skip
if (!aabb.Intersects(in viewport))
{
continue;
}
var uiScale = _userInterfaceManager.RootControl.UIScale;
var lineoffset = new Vector2(0f, 14f) * uiScale;
var screenCoordinates = _eyeManager.WorldToScreen(aabb.Center +
new Angle(-_eyeManager.CurrentEye.Rotation).RotateVec(
aabb.TopRight - aabb.Center)) + new Vector2(1f, 7f);
// Get on-screen coordinates of player
var screenCoordinates = _eyeManager.WorldToScreen(aabb.Center).Rounded();
sortable.Add((info, aabb, entity.Value, screenCoordinates));
}
// Draw overlays for visible players, starting from the top of the screen
foreach (var info in sortable.OrderBy(s => s.Item4.Y).ToList())
{
var playerInfo = info.Item1;
var aabb = info.Item2;
var entity = info.Item3;
var screenCoordinatesCenter = info.Item4;
//the center position is kept separately, for simpler position comparison later
var centerOffset = new Vector2(28f, -18f) * uiScale;
var screenCoordinates = screenCoordinatesCenter + centerOffset;
var alpha = 1f;
//TODO make a smarter system where the starting offset can be modified by the predicted position and size of already-drawn overlays/stacks?
var currentOffset = Vector2.Zero;
args.ScreenHandle.DrawString(_font, screenCoordinates + currentOffset, playerInfo.CharacterName, uiScale, playerInfo.Connected ? Color.Aquamarine : Color.White);
// Ghosts near the cursor are made transparent/invisible
// TODO would be "cheaper" if playerinfo already contained a ghost bool, this gets called every frame for every onscreen player!
if (_entityManager.HasComponent<GhostComponent>(entity))
{
// We want the map positions here, so we don't have to worry about resolution and such shenanigans
var mobPosition = aabb.Center;
var mousePosition = _eyeManager
.ScreenToMap(_userInterfaceManager.MousePositionScaled.Position * uiScale)
.Position;
var dist = Vector2.Distance(mobPosition, mousePosition);
if (dist < _ghostHideDistance)
continue;
alpha = Math.Clamp((dist - _ghostHideDistance) / (_ghostFadeDistance - _ghostHideDistance), 0f, 1f);
colorDisconnected.A = alpha;
}
// If the new overlay text block is within merge distance of any previous ones
// merge them into a stack so they don't hide each other
var stack = drawnOverlays.FindAll(x =>
Vector2.Distance(_eyeManager.ScreenToMap(x.Item1).Position, aabb.Center) <= _overlayMergeDistance);
if (stack.Count > 0)
{
screenCoordinates = stack.First().Item1 + centerOffset;
// Replacing this overlay's coordinates for the later save with the stack root's coordinates
// so that other overlays don't try to stack to these coordinates
screenCoordinatesCenter = stack.First().Item1;
var i = 1;
foreach (var s in stack)
{
// additional entries after maximum stack size is reached will be drawn over the last entry
if (i <= _overlayStackMax - 1)
currentOffset = lineoffset + s.Item2 ;
i++;
}
}
// Character name
var color = Color.Aquamarine;
color.A = alpha;
args.ScreenHandle.DrawString(_font, screenCoordinates + currentOffset, playerInfo.CharacterName, uiScale, playerInfo.Connected ? color : colorDisconnected);
currentOffset += lineoffset;
args.ScreenHandle.DrawString(_font, screenCoordinates + currentOffset, playerInfo.Username, uiScale, playerInfo.Connected ? Color.Yellow : Color.White);
// Username
color = Color.Yellow;
color.A = alpha;
args.ScreenHandle.DrawString(_font, screenCoordinates + currentOffset, playerInfo.Username, uiScale, playerInfo.Connected ? color : colorDisconnected);
currentOffset += lineoffset;
if (!string.IsNullOrEmpty(playerInfo.PlaytimeString) && playTime)
// Playtime
if (!string.IsNullOrEmpty(playerInfo.PlaytimeString) && _overlayPlaytime)
{
args.ScreenHandle.DrawString(_font, screenCoordinates + currentOffset, playerInfo.PlaytimeString, uiScale, playerInfo.Connected ? Color.Orange : Color.White);
color = Color.Orange;
color.A = alpha;
args.ScreenHandle.DrawString(_font, screenCoordinates + currentOffset, playerInfo.PlaytimeString, uiScale, playerInfo.Connected ? color : colorDisconnected);
currentOffset += lineoffset;
}
if (!string.IsNullOrEmpty(playerInfo.StartingJob) && startingJob)
// Job
if (!string.IsNullOrEmpty(playerInfo.StartingJob) && _overlayStartingJob)
{
args.ScreenHandle.DrawString(_font, screenCoordinates + currentOffset, Loc.GetString(playerInfo.StartingJob), uiScale, playerInfo.Connected ? Color.GreenYellow : Color.White);
color = Color.GreenYellow;
color.A = alpha;
args.ScreenHandle.DrawString(_font, screenCoordinates + currentOffset, Loc.GetString(playerInfo.StartingJob), uiScale, playerInfo.Connected ? color : colorDisconnected);
currentOffset += lineoffset;
}
if (classic && playerInfo.Antag)
// Classic Antag Label
if (_overlayClassic && playerInfo.Antag)
{
args.ScreenHandle.DrawString(_font, screenCoordinates + currentOffset, _antagLabelClassic, uiScale, Color.OrangeRed);
var symbol = _overlaySymbols ? Loc.GetString("player-tab-antag-prefix") : string.Empty;
var label = _overlaySymbols
? Loc.GetString("player-tab-character-name-antag-symbol",
("symbol", symbol),
("name", _antagLabelClassic))
: _antagLabelClassic;
color = Color.OrangeRed;
color.A = alpha;
args.ScreenHandle.DrawString(_fontBold, screenCoordinates + currentOffset, label, uiScale, color);
currentOffset += lineoffset;
}
else if (!classic && _filter.Contains(playerInfo.RoleProto))
// Role Type
else if (!_overlayClassic && _filter.Contains(playerInfo.RoleProto))
{
var label = Loc.GetString(playerInfo.RoleProto.Name).ToUpper();
var color = playerInfo.RoleProto.Color;
var symbol = _overlaySymbols && playerInfo.Antag ? playerInfo.RoleProto.Symbol : string.Empty;
var role = Loc.GetString(playerInfo.RoleProto.Name).ToUpper();
var label = _overlaySymbols
? Loc.GetString("player-tab-character-name-antag-symbol", ("symbol", symbol), ("name", role))
: role;
color = playerInfo.RoleProto.Color;
color.A = alpha;
args.ScreenHandle.DrawString(_fontBold, screenCoordinates + currentOffset, label, uiScale, color);
currentOffset += lineoffset;
}
args.ScreenHandle.DrawString(_font, screenCoordinates + currentOffset, label, uiScale, color);
currentOffset += lineoffset;
}
//Save the coordinates and size of the text block, for stack merge check
drawnOverlays.Add((screenCoordinatesCenter, currentOffset));
}
}
}

View File

@@ -14,6 +14,7 @@ namespace Content.Client.Administration.Systems
[Dependency] private readonly IEyeManager _eyeManager = default!;
[Dependency] private readonly EntityLookupSystem _entityLookup = default!;
[Dependency] private readonly IUserInterfaceManager _userInterfaceManager = default!;
[Dependency] private readonly IConfigurationManager _configurationManager = default!;
private AdminNameOverlay _adminNameOverlay = default!;
@@ -22,7 +23,14 @@ namespace Content.Client.Administration.Systems
private void InitializeOverlay()
{
_adminNameOverlay = new AdminNameOverlay(this, EntityManager, _eyeManager, _resourceCache, _entityLookup, _userInterfaceManager);
_adminNameOverlay = new AdminNameOverlay(
this,
EntityManager,
_eyeManager,
_resourceCache,
_entityLookup,
_userInterfaceManager,
_configurationManager);
_adminManager.AdminStatusUpdated += OnAdminStatusUpdated;
}

View File

@@ -2,14 +2,14 @@
xmlns="https://spacestation14.io"
xmlns:cc="clr-namespace:Content.Client.Administration.UI.CustomControls">
<PanelContainer StyleClasses="BackgroundDark">
<SplitContainer Orientation="Vertical">
<SplitContainer Orientation="Vertical" ResizeMode="NotResizable">
<SplitContainer Orientation="Horizontal" VerticalExpand="True">
<cc:PlayerListControl Access="Public" Name="ChannelSelector" HorizontalExpand="True" SizeFlagsStretchRatio="2" />
<BoxContainer Orientation="Vertical" HorizontalExpand="True" SizeFlagsStretchRatio="2">
<BoxContainer Access="Public" Name="BwoinkArea" VerticalExpand="True" />
</BoxContainer>
</SplitContainer>
<BoxContainer Orientation="Horizontal" HorizontalExpand="True">
<BoxContainer Orientation="Horizontal" SetHeight="30" >
<CheckBox Name="AdminOnly" Access="Public" Text="{Loc 'admin-ahelp-admin-only'}" ToolTip="{Loc 'admin-ahelp-admin-only-tooltip'}" />
<Control HorizontalExpand="True" MinWidth="5" />
<CheckBox Name="PlaySound" Access="Public" Text="{Loc 'admin-bwoink-play-sound'}" Pressed="True" />

View File

@@ -2,11 +2,13 @@ using System.Linq;
using Content.Client.Administration.Systems;
using Content.Client.UserInterface.Controls;
using Content.Shared.Administration;
using Content.Shared.CCVar;
using Robust.Client.AutoGenerated;
using Robust.Client.Graphics;
using Robust.Client.Player;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Configuration;
using static Content.Client.Administration.UI.Tabs.PlayerTab.PlayerTabHeader;
using static Robust.Client.UserInterface.Controls.BaseButton;
@@ -16,6 +18,7 @@ namespace Content.Client.Administration.UI.Tabs.PlayerTab;
public sealed partial class PlayerTab : Control
{
[Dependency] private readonly IEntityManager _entManager = default!;
[Dependency] private readonly IConfigurationManager _config = default!;
[Dependency] private readonly IPlayerManager _playerMan = default!;
private const string ArrowUp = "↑";
@@ -41,6 +44,10 @@ public sealed partial class PlayerTab : Control
_adminSystem.OverlayEnabled += OverlayEnabled;
_adminSystem.OverlayDisabled += OverlayDisabled;
_config.OnValueChanged(CCVars.AdminPlayerlistSeparateSymbols, PlayerListSettingsChanged);
_config.OnValueChanged(CCVars.AdminPlayerlistHighlightedCharacterColor, PlayerListSettingsChanged);
_config.OnValueChanged(CCVars.AdminPlayerlistRoleTypeColor, PlayerListSettingsChanged);
OverlayButton.OnPressed += OverlayButtonPressed;
ShowDisconnectedButton.OnPressed += ShowDisconnectedPressed;
@@ -106,6 +113,11 @@ public sealed partial class PlayerTab : Control
#region ListContainer
private void PlayerListSettingsChanged(bool _)
{
RefreshPlayerList(_adminSystem.PlayerList);
}
private void RefreshPlayerList(IReadOnlyList<PlayerInfo> players)
{
_players = players;
@@ -196,8 +208,7 @@ public sealed partial class PlayerTab : Control
Header.Username => Compare(x.Username, y.Username),
Header.Character => Compare(x.CharacterName, y.CharacterName),
Header.Job => Compare(x.StartingJob, y.StartingJob),
Header.Antagonist => x.Antag.CompareTo(y.Antag),
Header.RoleType => Compare(x.RoleProto.Name , y.RoleProto.Name),
Header.RoleType => y.SortWeight - x.SortWeight,
Header.Playtime => TimeSpan.Compare(x.OverallPlaytime ?? default, y.OverallPlaytime ?? default),
_ => 1
};

View File

@@ -19,11 +19,6 @@
HorizontalExpand="True"
ClipText="True"/>
<customControls:VSeparator/>
<Label Name="AntagonistLabel"
SizeFlagsStretchRatio="1"
HorizontalExpand="True"
ClipText="True"/>
<customControls:VSeparator/>
<Label Name="RoleTypeLabel"
SizeFlagsStretchRatio="2"
HorizontalExpand="True"

View File

@@ -1,8 +1,10 @@
using Content.Shared.Administration;
using Content.Shared.CCVar;
using Robust.Client.AutoGenerated;
using Robust.Client.Graphics;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Configuration;
namespace Content.Client.Administration.UI.Tabs.PlayerTab;
@@ -14,17 +16,25 @@ public sealed partial class PlayerTabEntry : PanelContainer
public PlayerTabEntry(PlayerInfo player, StyleBoxFlat styleBoxFlat)
{
RobustXamlLoader.Load(this);
var config = IoCManager.Resolve<IConfigurationManager>();
UsernameLabel.Text = player.Username;
if (!player.Connected)
UsernameLabel.StyleClasses.Add("Disabled");
JobLabel.Text = player.StartingJob;
CharacterLabel.Text = player.CharacterName;
var separateAntagSymbols = config.GetCVar(CCVars.AdminPlayerlistSeparateSymbols);
var genericAntagSymbol = player.Antag ? Loc.GetString("player-tab-antag-prefix") : string.Empty;
var roleSymbol = player.Antag ? player.RoleProto.Symbol : string.Empty;
var symbol = separateAntagSymbols ? roleSymbol : genericAntagSymbol;
CharacterLabel.Text = Loc.GetString("player-tab-character-name-antag-symbol", ("symbol", symbol), ("name", player.CharacterName));
if (player.Antag && config.GetCVar(CCVars.AdminPlayerlistHighlightedCharacterColor))
CharacterLabel.FontColorOverride = player.RoleProto.Color;
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");
RoleTypeLabel.Text = Loc.GetString(player.RoleProto.Name);
RoleTypeLabel.FontColorOverride = player.RoleProto.Color;
if (config.GetCVar(CCVars.AdminPlayerlistRoleTypeColor))
RoleTypeLabel.FontColorOverride = player.RoleProto.Color;
BackgroundColorPanel.PanelOverride = styleBoxFlat;
OverallPlaytimeLabel.Text = player.PlaytimeString;
PlayerEntity = player.NetEntity;

View File

@@ -25,13 +25,6 @@
Text="{Loc player-tab-job}"
MouseFilter="Pass"/>
<cc:VSeparator/>
<Label Name="AntagonistLabel"
SizeFlagsStretchRatio="1"
HorizontalExpand="True"
ClipText="True"
Text="{Loc player-tab-antagonist}"
MouseFilter="Pass"/>
<cc:VSeparator/>
<Label Name="RoleTypeLabel"
SizeFlagsStretchRatio="2"
HorizontalExpand="True"

View File

@@ -18,7 +18,6 @@ public sealed partial class PlayerTabHeader : Control
UsernameLabel.OnKeyBindDown += UsernameClicked;
CharacterLabel.OnKeyBindDown += CharacterClicked;
JobLabel.OnKeyBindDown += JobClicked;
AntagonistLabel.OnKeyBindDown += AntagonistClicked;
RoleTypeLabel.OnKeyBindDown += RoleTypeClicked;
PlaytimeLabel.OnKeyBindDown += PlaytimeClicked;
}
@@ -30,7 +29,6 @@ public sealed partial class PlayerTabHeader : Control
Header.Username => UsernameLabel,
Header.Character => CharacterLabel,
Header.Job => JobLabel,
Header.Antagonist => AntagonistLabel,
Header.RoleType => RoleTypeLabel,
Header.Playtime => PlaytimeLabel,
_ => throw new ArgumentOutOfRangeException(nameof(header), header, null)
@@ -42,7 +40,6 @@ public sealed partial class PlayerTabHeader : Control
UsernameLabel.Text = Loc.GetString("player-tab-username");
CharacterLabel.Text = Loc.GetString("player-tab-character");
JobLabel.Text = Loc.GetString("player-tab-job");
AntagonistLabel.Text = Loc.GetString("player-tab-antagonist");
RoleTypeLabel.Text = Loc.GetString("player-tab-roletype");
PlaytimeLabel.Text = Loc.GetString("player-tab-playtime");
}
@@ -73,11 +70,6 @@ public sealed partial class PlayerTabHeader : Control
HeaderClicked(args, Header.Job);
}
private void AntagonistClicked(GUIBoundKeyEventArgs args)
{
HeaderClicked(args, Header.Antagonist);
}
private void RoleTypeClicked(GUIBoundKeyEventArgs args)
{
HeaderClicked(args, Header.RoleType);
@@ -97,7 +89,6 @@ public sealed partial class PlayerTabHeader : Control
UsernameLabel.OnKeyBindDown -= UsernameClicked;
CharacterLabel.OnKeyBindDown -= CharacterClicked;
JobLabel.OnKeyBindDown -= JobClicked;
AntagonistLabel.OnKeyBindDown -= AntagonistClicked;
RoleTypeLabel.OnKeyBindDown -= RoleTypeClicked;
PlaytimeLabel.OnKeyBindDown -= PlaytimeClicked;
}
@@ -108,7 +99,6 @@ public sealed partial class PlayerTabHeader : Control
Username,
Character,
Job,
Antagonist,
RoleType,
Playtime
}

View File

@@ -52,7 +52,7 @@ public sealed class ClientAlertsSystem : AlertsSystem
if (args.Current is not AlertComponentState cast)
return;
alerts.Comp.Alerts = cast.Alerts;
alerts.Comp.Alerts = new(cast.Alerts);
UpdateHud(alerts);
}

View File

@@ -0,0 +1,5 @@
using Content.Shared.CartridgeLoader.Cartridges;
namespace Content.Client.CartridgeLoader.Cartridges;
public sealed class NanoTaskCartridgeSystem : SharedNanoTaskCartridgeSystem;

View File

@@ -0,0 +1,32 @@
<Control xmlns="https://spacestation14.io" xmlns:system="clr-namespace:System;assembly=System.Runtime">
<BoxContainer Name="MainContainer"
Orientation="Horizontal"
SetWidth="250">
<Button Name="MainButton"
HorizontalExpand="True"
VerticalExpand="True"
StyleClasses="ButtonSquare"
Margin="-1 0 0 0">
<BoxContainer Orientation="Horizontal" HorizontalExpand="True">
<BoxContainer Orientation="Vertical"
VerticalExpand="True"
HorizontalExpand="True"
Margin="-5 0 0 0">
<Label Name="TaskLabel"
StyleClasses="LabelSubText" />
<Label Name="TaskForLabel"
StyleClasses="LabelSubText"
Margin="0 -5 0 0" />
</BoxContainer>
</BoxContainer>
</Button>
<Button Name="DoneButton"
VerticalExpand="True"
Text="{Loc 'nano-task-ui-done'}">
<Button.StyleClasses>
<system:String>ButtonSmall</system:String>
<system:String>OpenLeft</system:String>
</Button.StyleClasses>
</Button>
</BoxContainer>
</Control>

View File

@@ -0,0 +1,33 @@
using Robust.Client.AutoGenerated;
using Robust.Client.Graphics;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Maths;
using Content.Shared.CartridgeLoader.Cartridges;
namespace Content.Client.CartridgeLoader.Cartridges;
/// <summary>
/// Represents a single control for a single NanoTask item
/// </summary>
[GenerateTypedNameReferences]
public sealed partial class NanoTaskItemControl : Control
{
public Action<int>? OnMainPressed;
public Action<int>? OnDonePressed;
public NanoTaskItemControl(NanoTaskItemAndId item)
{
RobustXamlLoader.Load(this);
TaskLabel.Text = item.Data.Description;
TaskLabel.FontColorOverride = Color.White;
TaskForLabel.Text = item.Data.TaskIsFor;
MainButton.OnPressed += _ => OnMainPressed?.Invoke(item.Id);
DoneButton.OnPressed += _ => OnDonePressed?.Invoke(item.Id);
MainButton.Disabled = item.Data.IsTaskDone;
DoneButton.Text = item.Data.IsTaskDone ? Loc.GetString("nano-task-ui-revert-done") : Loc.GetString("nano-task-ui-done");
}
}

View File

@@ -0,0 +1,67 @@
<DefaultWindow xmlns="https://spacestation14.io"
Title="{Loc nano-task-ui-item-title}"
MinSize="300 300">
<PanelContainer StyleClasses="AngleRect">
<BoxContainer Orientation="Vertical" Margin="4">
<!-- Task Description Input -->
<BoxContainer Orientation="Vertical" Margin="0 4">
<Label Text="{Loc nano-task-ui-description-label}"
StyleClasses="LabelHeading" />
<PanelContainer StyleClasses="ButtonSquare">
<LineEdit Name="DescriptionInput"
PlaceHolder="{Loc nano-task-ui-description-placeholder}" />
</PanelContainer>
</BoxContainer>
<!-- Task Requester Input -->
<BoxContainer Orientation="Vertical" Margin="0 4">
<Label Text="{Loc nano-task-ui-requester-label}"
StyleClasses="LabelHeading" />
<PanelContainer StyleClasses="ButtonSquare">
<LineEdit Name="RequesterInput"
PlaceHolder="{Loc nano-task-ui-requester-placeholder}" />
</PanelContainer>
</BoxContainer>
<!-- Severity Buttons -->
<BoxContainer Orientation="Horizontal"
HorizontalAlignment="Center"
Margin="0 8 0 0">
<Button Name="LowButton"
Text="{Loc nano-task-ui-priority-low}"
StyleClasses="OpenRight"
MinSize="60 0" />
<Button Name="MediumButton"
Text="{Loc nano-task-ui-priority-medium}"
StyleClasses="ButtonSquare"
MinSize="60 0" />
<Button Name="HighButton"
Text="{Loc nano-task-ui-priority-high}"
StyleClasses="OpenLeft"
MinSize="60 0" />
</BoxContainer>
<!-- Verb Buttons -->
<BoxContainer Orientation="Horizontal"
HorizontalAlignment="Right"
Margin="0 8 0 0">
<Button Name="CancelButton"
Text="{Loc nano-task-ui-cancel}"
StyleClasses="OpenRight"
MinSize="60 0" />
<Button Name="DeleteButton"
Text="{Loc nano-task-ui-delete}"
StyleClasses="ButtonSquare"
MinSize="60 0" />
<Button Name="PrintButton"
Text="{Loc nano-task-ui-print}"
StyleClasses="ButtonSquare"
MinSize="60 0" />
<Button Name="SaveButton"
Text="{Loc nano-task-ui-save}"
StyleClasses="OpenLeft"
MinSize="60 0" />
</BoxContainer>
</BoxContainer>
</PanelContainer>
</DefaultWindow>

View File

@@ -0,0 +1,109 @@
using System.Linq;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
using Robust.Client.UserInterface.Controls;
using Content.Shared.CartridgeLoader.Cartridges;
namespace Content.Client.CartridgeLoader.Cartridges;
/// <summary>
/// Popup displayed to edit a NanoTask item
/// </summary>
[GenerateTypedNameReferences]
public sealed partial class NanoTaskItemPopup : DefaultWindow
{
private readonly ButtonGroup _priorityGroup = new();
private int? _editingTaskId = null;
public Action<int, NanoTaskItem>? TaskSaved;
public Action<int>? TaskDeleted;
public Action<NanoTaskItem>? TaskCreated;
public Action<NanoTaskItem>? TaskPrinted;
private NanoTaskItem MakeItem()
{
return new(
description: DescriptionInput.Text,
taskIsFor: RequesterInput.Text,
isTaskDone: false,
priority: _priorityGroup.Pressed switch {
var item when item == LowButton => NanoTaskPriority.Low,
var item when item == MediumButton => NanoTaskPriority.Medium,
var item when item == HighButton => NanoTaskPriority.High,
_ => NanoTaskPriority.Medium,
}
);
}
public NanoTaskItemPopup()
{
RobustXamlLoader.Load(this);
LowButton.Group = _priorityGroup;
MediumButton.Group = _priorityGroup;
HighButton.Group = _priorityGroup;
CancelButton.OnPressed += _ => Close();
DeleteButton.OnPressed += _ =>
{
if (_editingTaskId is int id)
{
TaskDeleted?.Invoke(id);
}
};
PrintButton.OnPressed += _ =>
{
TaskPrinted?.Invoke(MakeItem());
};
SaveButton.OnPressed += _ =>
{
if (_editingTaskId is int id)
{
TaskSaved?.Invoke(id, MakeItem());
}
else
{
TaskCreated?.Invoke(MakeItem());
}
};
DescriptionInput.OnTextChanged += args =>
{
if (args.Text.Length > NanoTaskItem.MaximumStringLength)
DescriptionInput.Text = args.Text[..NanoTaskItem.MaximumStringLength];
};
RequesterInput.OnTextChanged += args =>
{
if (args.Text.Length > NanoTaskItem.MaximumStringLength)
RequesterInput.Text = args.Text[..NanoTaskItem.MaximumStringLength];
};
}
public void SetEditingTaskId(int? id)
{
_editingTaskId = id;
DeleteButton.Visible = id is not null;
}
public void ResetInputs(NanoTaskItem? item)
{
if (item is NanoTaskItem task)
{
var button = task.Priority switch {
NanoTaskPriority.High => HighButton,
NanoTaskPriority.Medium => MediumButton,
NanoTaskPriority.Low => LowButton,
};
button.Pressed = true;
DescriptionInput.Text = task.Description;
RequesterInput.Text = task.TaskIsFor;
}
else
{
MediumButton.Pressed = true;
DescriptionInput.Text = "";
RequesterInput.Text = "";
}
}
}

View File

@@ -0,0 +1,82 @@
using System.Linq;
using Content.Client.UserInterface.Fragments;
using Content.Shared.CartridgeLoader;
using Content.Shared.CartridgeLoader.Cartridges;
using Robust.Client.GameObjects;
using Robust.Client.UserInterface;
namespace Content.Client.CartridgeLoader.Cartridges;
/// <summary>
/// UI fragment responsible for displaying NanoTask controls in a PDA and coordinating with the NanoTaskCartridgeSystem for state
/// </summary>
public sealed partial class NanoTaskUi : UIFragment
{
private NanoTaskUiFragment? _fragment;
private NanoTaskItemPopup? _popup;
public override Control GetUIFragmentRoot()
{
return _fragment!;
}
public override void Setup(BoundUserInterface userInterface, EntityUid? fragmentOwner)
{
_fragment = new NanoTaskUiFragment();
_popup = new NanoTaskItemPopup();
_fragment.NewTask += () =>
{
_popup.ResetInputs(null);
_popup.SetEditingTaskId(null);
_popup.OpenCentered();
};
_fragment.OpenTask += id =>
{
if (_fragment.Tasks.Find(task => task.Id == id) is not NanoTaskItemAndId task)
return;
_popup.ResetInputs(task.Data);
_popup.SetEditingTaskId(task.Id);
_popup.OpenCentered();
};
_fragment.ToggleTaskCompletion += id =>
{
if (_fragment.Tasks.Find(task => task.Id == id) is not NanoTaskItemAndId task)
return;
userInterface.SendMessage(new CartridgeUiMessage(new NanoTaskUiMessageEvent(new NanoTaskUpdateTask(new(id, new(
description: task.Data.Description,
taskIsFor: task.Data.TaskIsFor,
isTaskDone: !task.Data.IsTaskDone,
priority: task.Data.Priority
))))));
};
_popup.TaskSaved += (id, data) =>
{
userInterface.SendMessage(new CartridgeUiMessage(new NanoTaskUiMessageEvent(new NanoTaskUpdateTask(new(id, data)))));
_popup.Close();
};
_popup.TaskDeleted += id =>
{
userInterface.SendMessage(new CartridgeUiMessage(new NanoTaskUiMessageEvent(new NanoTaskDeleteTask(id))));
_popup.Close();
};
_popup.TaskCreated += data =>
{
userInterface.SendMessage(new CartridgeUiMessage(new NanoTaskUiMessageEvent(new NanoTaskAddTask(data))));
_popup.Close();
};
_popup.TaskPrinted += data =>
{
userInterface.SendMessage(new CartridgeUiMessage(new NanoTaskUiMessageEvent(new NanoTaskPrintTask(data))));
};
}
public override void UpdateState(BoundUserInterfaceState state)
{
if (state is not NanoTaskUiState nanoTaskState)
return;
_fragment?.UpdateState(nanoTaskState.Tasks);
}
}

View File

@@ -0,0 +1,58 @@
<cartridges:NanoTaskUiFragment xmlns:cartridges="clr-namespace:Content.Client.CartridgeLoader.Cartridges"
xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
xmlns="https://spacestation14.io" Margin="1 0 2 0">
<PanelContainer StyleClasses="BackgroundDark"></PanelContainer>
<BoxContainer Orientation="Vertical" HorizontalExpand="True" VerticalExpand="True">
<ScrollContainer HorizontalExpand="True" VerticalExpand="True">
<BoxContainer HorizontalExpand="True" VerticalExpand="True" Orientation="Vertical" Margin="8" SeparationOverride="8">
<!-- Heading for High Priority Items -->
<BoxContainer Orientation="Horizontal">
<PanelContainer SetWidth="7" Margin="0 0 8 0">
<PanelContainer.PanelOverride>
<gfx:StyleBoxFlat BackgroundColor="#e93d58"/>
</PanelContainer.PanelOverride>
</PanelContainer>
<Label Name="HighPriority" StyleClasses="LabelHeading"/>
</BoxContainer>
<!-- Location for High Priority Items -->
<GridContainer Name="HighContainer"
HorizontalExpand="True"
Access="Public"
Columns="2" />
<!-- Heading for Medium Priority Items -->
<BoxContainer Orientation="Horizontal">
<PanelContainer SetWidth="7" Margin="0 0 8 0">
<PanelContainer.PanelOverride>
<gfx:StyleBoxFlat BackgroundColor="#ef973c"/>
</PanelContainer.PanelOverride>
</PanelContainer>
<Label Name="MediumPriority" StyleClasses="LabelHeading"/>
</BoxContainer>
<!-- Location for Medium Priority Items -->
<GridContainer Name="MediumContainer"
HorizontalExpand="True"
Access="Public"
Columns="2" />
<!-- Location for Low Priority Items -->
<BoxContainer Orientation="Horizontal">
<PanelContainer SetWidth="7" Margin="0 0 8 0">
<PanelContainer.PanelOverride>
<gfx:StyleBoxFlat BackgroundColor="#3dd425"/>
</PanelContainer.PanelOverride>
</PanelContainer>
<Label Name="LowPriority" StyleClasses="LabelHeading"/>
</BoxContainer>
<!-- Location for Low Priority Items -->
<GridContainer Name="LowContainer"
HorizontalExpand="True"
Access="Public"
Columns="2" />
<Button Name="NewTaskButton" Text="{Loc 'nano-task-ui-new-task'}" HorizontalAlignment="Right"/>
</BoxContainer>
</ScrollContainer>
</BoxContainer>
</cartridges:NanoTaskUiFragment>

View File

@@ -0,0 +1,52 @@
using System.Linq;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Content.Shared.CartridgeLoader.Cartridges;
namespace Content.Client.CartridgeLoader.Cartridges;
/// <summary>
/// Class displaying the main UI of NanoTask
/// </summary>
[GenerateTypedNameReferences]
public sealed partial class NanoTaskUiFragment : BoxContainer
{
public Action<int>? OpenTask;
public Action<int>? ToggleTaskCompletion;
public Action? NewTask;
public List<NanoTaskItemAndId> Tasks = new();
public NanoTaskUiFragment()
{
RobustXamlLoader.Load(this);
Orientation = LayoutOrientation.Vertical;
HorizontalExpand = true;
VerticalExpand = true;
NewTaskButton.OnPressed += _ => NewTask?.Invoke();
}
public void UpdateState(List<NanoTaskItemAndId> tasks)
{
Tasks = tasks;
HighContainer.RemoveAllChildren();
MediumContainer.RemoveAllChildren();
LowContainer.RemoveAllChildren();
HighPriority.Text = Loc.GetString("nano-task-ui-heading-high-priority-tasks", ("amount", tasks.Count(task => task.Data.Priority == NanoTaskPriority.High)));
MediumPriority.Text = Loc.GetString("nano-task-ui-heading-medium-priority-tasks", ("amount", tasks.Count(task => task.Data.Priority == NanoTaskPriority.Medium)));
LowPriority.Text = Loc.GetString("nano-task-ui-heading-low-priority-tasks", ("amount", tasks.Count(task => task.Data.Priority == NanoTaskPriority.Low)));
foreach (var task in tasks)
{
var container = task.Data.Priority switch {
NanoTaskPriority.High => HighContainer,
NanoTaskPriority.Medium => MediumContainer,
NanoTaskPriority.Low => LowContainer,
};
var control = new NanoTaskItemControl(task);
container.AddChild(control);
control.OnMainPressed += id => OpenTask?.Invoke(id);
control.OnDonePressed += id => ToggleTaskCompletion?.Invoke(id);
}
}
}

View File

@@ -131,13 +131,13 @@ public sealed partial class ChangelogTab : Control
Margin = new Thickness(6, 0, 0, 0),
};
authorLabel.SetMessage(
FormattedMessage.FromMarkupOrThrow(Loc.GetString("changelog-author-changed", ("author", author))));
FormattedMessage.FromMarkupOrThrow(Loc.GetString("changelog-author-changed", ("author", FormattedMessage.EscapeText(author)))));
ChangelogBody.AddChild(authorLabel);
foreach (var change in groupedEntry.SelectMany(c => c.Changes))
{
var text = new RichTextLabel();
text.SetMessage(FormattedMessage.FromMarkupOrThrow(change.Message));
text.SetMessage(FormattedMessage.FromUnformatted(change.Message));
ChangelogBody.AddChild(new BoxContainer
{
Orientation = LayoutOrientation.Horizontal,

View File

@@ -169,7 +169,7 @@ public sealed class ClientClothingSystem : ClothingSystem
var state = $"equipped-{correctedSlot}";
if (clothing.EquippedPrefix != null)
if (!string.IsNullOrEmpty(clothing.EquippedPrefix))
state = $"{clothing.EquippedPrefix}-equipped-{correctedSlot}";
if (clothing.EquippedState != null)

View File

@@ -3,6 +3,7 @@ using Content.Shared.Access.Systems;
using Content.Shared.Administration;
using Content.Shared.CriminalRecords;
using Content.Shared.Dataset;
using Content.Shared.Random.Helpers;
using Content.Shared.Security;
using Content.Shared.StationRecords;
using Robust.Client.AutoGenerated;
@@ -32,7 +33,7 @@ public sealed partial class CriminalRecordsConsoleWindow : FancyWindow
public readonly EntityUid Console;
[ValidatePrototypeId<DatasetPrototype>]
[ValidatePrototypeId<LocalizedDatasetPrototype>]
private const string ReasonPlaceholders = "CriminalRecordsWantedReasonPlaceholders";
public Action<uint?>? OnKeySelected;
@@ -333,8 +334,8 @@ public sealed partial class CriminalRecordsConsoleWindow : FancyWindow
var field = "reason";
var title = Loc.GetString("criminal-records-status-" + status.ToString().ToLower());
var placeholders = _proto.Index<DatasetPrototype>(ReasonPlaceholders);
var placeholder = Loc.GetString("criminal-records-console-reason-placeholder", ("placeholder", _random.Pick(placeholders.Values))); // just funny it doesn't actually get used
var placeholders = _proto.Index<LocalizedDatasetPrototype>(ReasonPlaceholders);
var placeholder = Loc.GetString("criminal-records-console-reason-placeholder", ("placeholder", _random.Pick(placeholders))); // just funny it doesn't actually get used
var prompt = Loc.GetString("criminal-records-console-reason");
var entry = new QuickDialogEntry(field, QuickDialogEntryType.LongText, prompt, placeholder);
var entries = new List<QuickDialogEntry>() { entry };

View File

@@ -4,6 +4,7 @@ using Content.Shared.Verbs;
using Robust.Client.AutoGenerated;
using Robust.Client.Graphics;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
using Robust.Client.Utility;
using Robust.Shared.Utility;
@@ -40,7 +41,17 @@ public sealed class ExamineButton : ContainerButton
Disabled = true;
}
ToolTip = verb.Message ?? verb.Text;
TooltipSupplier = sender =>
{
var label = new RichTextLabel();
label.SetMessage(FormattedMessage.FromMarkupOrThrow(verb.Message ?? verb.Text));
var tooltip = new Tooltip();
tooltip.GetChild(0).Children.Clear();
tooltip.GetChild(0).Children.Add(label);
return tooltip;
};
Icon = new TextureRect
{

View File

@@ -298,10 +298,28 @@ namespace Content.Client.Examine
{
Name = "ExamineButtonsHBox",
Orientation = LayoutOrientation.Horizontal,
HorizontalAlignment = Control.HAlignment.Right,
HorizontalAlignment = Control.HAlignment.Stretch,
VerticalAlignment = Control.VAlignment.Bottom,
};
var hoverExamineBox = new BoxContainer
{
Name = "HoverExamineHBox",
Orientation = LayoutOrientation.Horizontal,
HorizontalAlignment = Control.HAlignment.Left,
VerticalAlignment = Control.VAlignment.Center,
HorizontalExpand = true
};
var clickExamineBox = new BoxContainer
{
Name = "ClickExamineHBox",
Orientation = LayoutOrientation.Horizontal,
HorizontalAlignment = Control.HAlignment.Right,
VerticalAlignment = Control.VAlignment.Center,
HorizontalExpand = true
};
// Examine button time
foreach (var verb in verbs)
{
@@ -316,8 +334,15 @@ namespace Content.Client.Examine
var button = new ExamineButton(examine);
button.OnPressed += VerbButtonPressed;
buttonsHBox.AddChild(button);
if (examine.HoverVerb)
{
hoverExamineBox.AddChild(button);
}
else
{
button.OnPressed += VerbButtonPressed;
clickExamineBox.AddChild(button);
}
}
var vbox = _examineTooltipOpen?.GetChild(0).GetChild(0);
@@ -334,6 +359,8 @@ namespace Content.Client.Examine
{
vbox.Children.Remove(hbox.First());
}
buttonsHBox.AddChild(hoverExamineBox);
buttonsHBox.AddChild(clickExamineBox);
vbox.AddChild(buttonsHBox);
}

View File

@@ -29,24 +29,25 @@
VerticalAlignment="Center"
Access="Public"
Visible="False" />
<!-- CP14 random reactions begin -->
<BoxContainer Name="RandomVariations"
Orientation="Vertical"
VerticalExpand="True"
VerticalAlignment="Center"
HorizontalAlignment="Center">
<controls:SplitBar MinHeight="10">
</controls:SplitBar>
<Label Name="RandomVariationsLabel"
Text="{Loc 'cp14-guidebook-random-variations-title'}"
Visible="False"
HorizontalAlignment="Center"
FontColorOverride="#1e6651" />
<controls:SplitBar MinHeight="10">
</controls:SplitBar>
</BoxContainer>
<!-- CP14 random reactions end -->
</BoxContainer>
<!-- CP14 random reactions begin -->
<BoxContainer Name="RandomVariations"
Orientation="Vertical"
VerticalExpand="True"
VerticalAlignment="Center"
HorizontalAlignment="Center">
<controls:SplitBar MinHeight="10">
</controls:SplitBar>
<Label Name="RandomVariationsLabel"
Text="{Loc 'cp14-guidebook-random-variations-title'}"
Visible="False"
HorizontalAlignment="Center"
FontColorOverride="#1e6651" />
<controls:SplitBar MinHeight="10">
</controls:SplitBar>
</BoxContainer>
<!-- CP14 random reactions end -->
</BoxContainer>
<PanelContainer StyleClasses="LowDivider" Margin="0 5 0 5" />
</BoxContainer>

View File

@@ -2,6 +2,7 @@ using Content.Shared.CCVar;
using Content.Shared.Humanoid;
using Content.Shared.Humanoid.Markings;
using Content.Shared.Humanoid.Prototypes;
using Content.Shared.Inventory;
using Content.Shared.Preferences;
using Robust.Client.GameObjects;
using Robust.Shared.Configuration;
@@ -48,7 +49,7 @@ public sealed class HumanoidAppearanceSystem : SharedHumanoidAppearanceSystem
}
private static bool IsHidden(HumanoidAppearanceComponent humanoid, HumanoidVisualLayers layer)
=> humanoid.HiddenLayers.Contains(layer) || humanoid.PermanentlyHidden.Contains(layer);
=> humanoid.HiddenLayers.ContainsKey(layer) || humanoid.PermanentlyHidden.Contains(layer);
private void UpdateLayers(HumanoidAppearanceComponent component, SpriteComponent sprite)
{
@@ -57,7 +58,7 @@ public sealed class HumanoidAppearanceSystem : SharedHumanoidAppearanceSystem
// add default species layers
var speciesProto = _prototypeManager.Index(component.Species);
var baseSprites = _prototypeManager.Index<HumanoidSpeciesBaseSpritesPrototype>(speciesProto.SpriteSet);
var baseSprites = _prototypeManager.Index(speciesProto.SpriteSet);
foreach (var (key, id) in baseSprites.Sprites)
{
oldLayers.Remove(key);
@@ -214,7 +215,7 @@ public sealed class HumanoidAppearanceSystem : SharedHumanoidAppearanceSystem
humanoid.MarkingSet = markings;
humanoid.PermanentlyHidden = new HashSet<HumanoidVisualLayers>();
humanoid.HiddenLayers = new HashSet<HumanoidVisualLayers>();
humanoid.HiddenLayers = new Dictionary<HumanoidVisualLayers, SlotFlags>();
humanoid.CustomBaseLayers = customBaseLayers;
humanoid.Sex = profile.Sex;
humanoid.Gender = profile.Gender;
@@ -402,23 +403,21 @@ public sealed class HumanoidAppearanceSystem : SharedHumanoidAppearanceSystem
}
}
protected override void SetLayerVisibility(
EntityUid uid,
HumanoidAppearanceComponent humanoid,
public override void SetLayerVisibility(
Entity<HumanoidAppearanceComponent> ent,
HumanoidVisualLayers layer,
bool visible,
bool permanent,
SlotFlags? slot,
ref bool dirty)
{
base.SetLayerVisibility(uid, humanoid, layer, visible, permanent, ref dirty);
base.SetLayerVisibility(ent, layer, visible, slot, ref dirty);
var sprite = Comp<SpriteComponent>(uid);
var sprite = Comp<SpriteComponent>(ent);
if (!sprite.LayerMapTryGet(layer, out var index))
{
if (!visible)
return;
else
index = sprite.LayerMapReserveBlank(layer);
index = sprite.LayerMapReserveBlank(layer);
}
var spriteLayer = sprite[index];
@@ -428,13 +427,14 @@ public sealed class HumanoidAppearanceSystem : SharedHumanoidAppearanceSystem
spriteLayer.Visible = visible;
// I fucking hate this. I'll get around to refactoring sprite layers eventually I swear
// Just a week away...
foreach (var markingList in humanoid.MarkingSet.Markings.Values)
foreach (var markingList in ent.Comp.MarkingSet.Markings.Values)
{
foreach (var marking in markingList)
{
if (_markingManager.TryGetMarking(marking, out var markingPrototype) && markingPrototype.BodyPart == layer)
ApplyMarking(markingPrototype, marking.MarkingColors, marking.Visible, humanoid, sprite);
ApplyMarking(markingPrototype, marking.MarkingColors, marking.Visible, ent, sprite);
}
}
}

View File

@@ -68,21 +68,13 @@ public sealed partial class LatheMenu : DefaultWindow
{
ServerListButton.Visible = false;
}
AmountLineEdit.SetText(latheComponent.DefaultProductionAmount.ToString());
}
MaterialsList.SetOwner(Entity);
}
protected override void Opened()
{
base.Opened();
if (_entityManager.TryGetComponent<LatheComponent>(Entity, out var latheComp))
{
AmountLineEdit.SetText(latheComp.DefaultProductionAmount.ToString());
}
}
/// <summary>
/// Populates the list of all the recipes
/// </summary>

View File

@@ -4,7 +4,20 @@
<BoxContainer Orientation="Vertical">
<ScrollContainer VerticalExpand="True" HScrollEnabled="False">
<BoxContainer Orientation="Vertical" Margin="8">
<Label Text="{Loc 'ui-options-admin-player-panel'}"
StyleClasses="LabelKeyText"/>
<CheckBox Name="PlayerlistSeparateSymbolsCheckBox" Text="{Loc 'ui-options-admin-playerlist-separate-symbols'}" />
<CheckBox Name="PlayerlistCharacterColorCheckBox" Text="{Loc 'ui-options-admin-playerlist-character-color'}" />
<CheckBox Name="PlayerlistRoleTypeColorCheckBox" Text="{Loc 'ui-options-admin-playerlist-roletype-color'}" />
<Label Text="{Loc 'ui-options-admin-overlay-title'}"
StyleClasses="LabelKeyText"/>
<CheckBox Name="EnableClassicOverlayCheckBox" Text="{Loc 'ui-options-enable-classic-overlay'}" />
<CheckBox Name="EnableOverlaySymbolsCheckBox" Text="{Loc 'ui-options-enable-overlay-symbols'}" />
<CheckBox Name="EnableOverlayPlaytimeCheckBox" Text="{Loc 'ui-options-enable-overlay-playtime'}" />
<CheckBox Name="EnableOverlayStartingJobCheckBox" Text="{Loc 'ui-options-enable-overlay-starting-job'}" />
<ui:OptionSlider Name="OverlayMergeDistanceSlider" Title="{Loc 'ui-options-overlay-merge-distance'}"/>
<ui:OptionSlider Name="OverlayGhostFadeSlider" Title="{Loc 'ui-options-overlay-ghost-fade-distance'}"/>
<ui:OptionSlider Name="OverlayGhostHideSlider" Title="{Loc 'ui-options-overlay-ghost-hide-distance'}"/>
</BoxContainer>
</ScrollContainer>
<ui:OptionsTabControlRow Name="Control" Access="Public" />

View File

@@ -8,13 +8,45 @@ namespace Content.Client.Options.UI.Tabs;
[GenerateTypedNameReferences]
public sealed partial class AdminOptionsTab : Control
{
private const float OverlayMergeMin = 0.05f;
private const float OverlayMergeMax = 0.95f;
private const int OverlayGhostFadeMin = 0;
private const int OverlayGhostFadeMax = 10;
private const int OverlayGhostHideMin = 0;
private const int OverlayGhostHideMax = 5;
public AdminOptionsTab()
{
RobustXamlLoader.Load(this);
Control.AddOptionCheckBox(CCVars.AdminPlayerlistSeparateSymbols, PlayerlistSeparateSymbolsCheckBox);
Control.AddOptionCheckBox(CCVars.AdminPlayerlistHighlightedCharacterColor, PlayerlistCharacterColorCheckBox);
Control.AddOptionCheckBox(CCVars.AdminPlayerlistRoleTypeColor, PlayerlistRoleTypeColorCheckBox);
Control.AddOptionCheckBox(CCVars.AdminOverlayClassic, EnableClassicOverlayCheckBox);
Control.AddOptionCheckBox(CCVars.AdminOverlaySymbols, EnableOverlaySymbolsCheckBox);
Control.AddOptionCheckBox(CCVars.AdminOverlayPlaytime, EnableOverlayPlaytimeCheckBox);
Control.AddOptionCheckBox(CCVars.AdminOverlayStartingJob, EnableOverlayStartingJobCheckBox);
Control.Initialize();
Control.AddOptionPercentSlider(
CCVars.AdminOverlayMergeDistance,
OverlayMergeDistanceSlider,
OverlayMergeMin,
OverlayMergeMax);
Control.AddOptionSlider(
CCVars.AdminOverlayGhostFadeDistance,
OverlayGhostFadeSlider,
OverlayGhostFadeMin,
OverlayGhostFadeMax);
Control.AddOptionSlider(
CCVars.AdminOverlayGhostHideDistance,
OverlayGhostHideSlider,
OverlayGhostHideMin,
OverlayGhostHideMax);
}
}

View File

@@ -314,6 +314,7 @@ namespace Content.Client.Options.UI.Tabs
AddHeader("ui-options-header-cp14");
AddButton(ContentKeyFunctions.CP14OpenSkillMenu);
//CP14 end
foreach (var control in _keyControls.Values)
{
UpdateKeyControl(control);

View File

@@ -11,13 +11,21 @@ public sealed class PaperVisualizerSystem : VisualizerSystem<PaperVisualsCompone
if (args.Sprite == null)
return;
if (AppearanceSystem.TryGetData<PaperStatus>(uid, PaperVisuals.Status , out var writingStatus, args.Component))
if (AppearanceSystem.TryGetData<PaperStatus>(uid, PaperVisuals.Status, out var writingStatus, args.Component))
args.Sprite.LayerSetVisible(PaperVisualLayers.Writing, writingStatus == PaperStatus.Written);
if (AppearanceSystem.TryGetData<string>(uid, PaperVisuals.Stamp, out var stampState, args.Component))
{
args.Sprite.LayerSetState(PaperVisualLayers.Stamp, stampState);
args.Sprite.LayerSetVisible(PaperVisualLayers.Stamp, true);
if (stampState != string.Empty)
{
args.Sprite.LayerSetState(PaperVisualLayers.Stamp, stampState);
args.Sprite.LayerSetVisible(PaperVisualLayers.Stamp, true);
}
else
{
args.Sprite.LayerSetVisible(PaperVisualLayers.Stamp, false);
}
}
}
}

View File

@@ -5,7 +5,7 @@ namespace Content.Client.Parallax.Data;
/// <summary>
/// Prototype data for a parallax.
/// </summary>
[Prototype("parallax")]
[Prototype]
public sealed partial class ParallaxPrototype : IPrototype
{
/// <inheritdoc/>

View File

@@ -237,6 +237,12 @@ namespace Content.Client.Popups
PopupEntity(message, uid, recipient.Value, type);
}
public override void PopupPredicted(string? message, EntityUid uid, EntityUid? recipient, Filter filter, bool recordReplay, PopupType type = PopupType.Small)
{
if (recipient != null && _timing.IsFirstTimePredicted)
PopupEntity(message, uid, recipient.Value, type);
}
public override void PopupPredicted(string? recipientMessage, string? othersMessage, EntityUid uid, EntityUid? recipient, PopupType type = PopupType.Small)
{
if (recipient != null && _timing.IsFirstTimePredicted)

View File

@@ -1,12 +1,15 @@
using Content.Shared.DrawDepth;
using Content.Client.UserInterface.Systems.Sandbox;
using Content.Shared.SubFloor;
using Robust.Client.GameObjects;
using Robust.Client.UserInterface;
using Robust.Shared.Player;
namespace Content.Client.SubFloor;
public sealed class SubFloorHideSystem : SharedSubFloorHideSystem
{
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly IUserInterfaceManager _ui = default!;
private bool _showAll;
@@ -18,8 +21,13 @@ public sealed class SubFloorHideSystem : SharedSubFloorHideSystem
{
if (_showAll == value) return;
_showAll = value;
_ui.GetUIController<SandboxUIController>().SetToggleSubfloors(value);
UpdateAll();
var ev = new ShowSubfloorRequestEvent()
{
Value = value,
};
RaiseNetworkEvent(ev);
}
}
@@ -28,6 +36,20 @@ public sealed class SubFloorHideSystem : SharedSubFloorHideSystem
base.Initialize();
SubscribeLocalEvent<SubFloorHideComponent, AppearanceChangeEvent>(OnAppearanceChanged);
SubscribeNetworkEvent<ShowSubfloorRequestEvent>(OnRequestReceived);
SubscribeLocalEvent<LocalPlayerDetachedEvent>(OnPlayerDetached);
}
private void OnPlayerDetached(LocalPlayerDetachedEvent ev)
{
// Vismask resets so need to reset this.
ShowAll = false;
}
private void OnRequestReceived(ShowSubfloorRequestEvent ev)
{
// When client receives request Queue an update on all vis.
UpdateAll();
}
private void OnAppearanceChanged(EntityUid uid, SubFloorHideComponent component, ref AppearanceChangeEvent args)

View File

@@ -14,6 +14,7 @@ public sealed partial class GhostGui : UIWidget
public event Action? RequestWarpsPressed;
public event Action? ReturnToBodyPressed;
public event Action? GhostRolesPressed;
private int _prevNumberRoles;
public GhostGui()
{
@@ -26,6 +27,7 @@ public sealed partial class GhostGui : UIWidget
GhostWarpButton.OnPressed += _ => RequestWarpsPressed?.Invoke();
ReturnToBodyButton.OnPressed += _ => ReturnToBodyPressed?.Invoke();
GhostRolesButton.OnPressed += _ => GhostRolesPressed?.Invoke();
GhostRolesButton.OnPressed += _ => GhostRolesButton.StyleClasses.Remove(StyleBase.ButtonCaution);
}
public void Hide()
@@ -41,14 +43,13 @@ public sealed partial class GhostGui : UIWidget
if (roles != null)
{
GhostRolesButton.Text = Loc.GetString("ghost-gui-ghost-roles-button", ("count", roles));
if (roles > 0)
if (roles > _prevNumberRoles)
{
GhostRolesButton.StyleClasses.Add(StyleBase.ButtonCaution);
}
else
{
GhostRolesButton.StyleClasses.Remove(StyleBase.ButtonCaution);
}
_prevNumberRoles = (int)roles;
}
TargetWindow.Populate();

View File

@@ -39,7 +39,6 @@ public sealed class SandboxUIController : UIController, IOnStateChanged<Gameplay
[UISystemDependency] private readonly DebugPhysicsSystem _debugPhysics = default!;
[UISystemDependency] private readonly MarkerSystem _marker = default!;
[UISystemDependency] private readonly SandboxSystem _sandbox = default!;
[UISystemDependency] private readonly SubFloorHideSystem _subfloorHide = default!;
private SandboxWindow? _window;
@@ -117,10 +116,11 @@ public sealed class SandboxUIController : UIController, IOnStateChanged<Gameplay
_window.OnOpen += () => { SandboxButton!.Pressed = true; };
_window.OnClose += () => { SandboxButton!.Pressed = false; };
// TODO: These need moving to opened so at least if they're not synced properly on open they work.
_window.ToggleLightButton.Pressed = !_light.Enabled;
_window.ToggleFovButton.Pressed = !_eye.CurrentEye.DrawFov;
_window.ToggleShadowsButton.Pressed = !_light.DrawShadows;
_window.ToggleSubfloorButton.Pressed = _subfloorHide.ShowAll;
_window.ShowMarkersButton.Pressed = _marker.MarkersVisible;
_window.ShowBbButton.Pressed = (_debugPhysics.Flags & PhysicsDebugFlags.Shapes) != 0x0;
@@ -219,4 +219,16 @@ public sealed class SandboxUIController : UIController, IOnStateChanged<Gameplay
_window.Close();
}
}
#region Buttons
public void SetToggleSubfloors(bool value)
{
if (_window == null)
return;
_window.ToggleSubfloorButton.Pressed = value;
}
#endregion
}

View File

@@ -1,4 +1,5 @@
using Robust.Client.AutoGenerated;
using Content.Client.SubFloor;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
@@ -7,8 +8,18 @@ namespace Content.Client.UserInterface.Systems.Sandbox.Windows;
[GenerateTypedNameReferences]
public sealed partial class SandboxWindow : DefaultWindow
{
[Dependency] private readonly IEntityManager _entManager = null!;
public SandboxWindow()
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
}
protected override void Opened()
{
base.Opened();
// Make sure state is up to date.
ToggleSubfloorButton.Pressed = _entManager.System<SubFloorHideSystem>().ShowAll;
}
}

View File

@@ -42,6 +42,9 @@ public sealed class StorageWindow : BaseWindow
private ValueList<EntityUid> _contained = new();
private ValueList<EntityUid> _toRemove = new();
// Manually store this because you can't have a 0x0 GridContainer but we still need to add child controls for 1x1 containers.
private Vector2i _pieceGridSize;
private TextureButton? _backButton;
private bool _isDirty;
@@ -408,11 +411,14 @@ public sealed class StorageWindow : BaseWindow
_contained.Clear();
_contained.AddRange(storageComp.Container.ContainedEntities.Reverse());
var width = boundingGrid.Width + 1;
var height = boundingGrid.Height + 1;
// Build the grid representation
if (_pieceGrid.Rows - 1 != boundingGrid.Height || _pieceGrid.Columns - 1 != boundingGrid.Width)
if (_pieceGrid.Rows != _pieceGridSize.Y || _pieceGrid.Columns != _pieceGridSize.X)
{
_pieceGrid.Rows = boundingGrid.Height + 1;
_pieceGrid.Columns = boundingGrid.Width + 1;
_pieceGrid.Rows = height;
_pieceGrid.Columns = width;
_controlGrid.Clear();
for (var y = boundingGrid.Bottom; y <= boundingGrid.Top; y++)
@@ -430,6 +436,7 @@ public sealed class StorageWindow : BaseWindow
}
}
_pieceGridSize = new(width, height);
_toRemove.Clear();
// Remove entities no longer relevant / Update existing ones

View File

@@ -3,10 +3,12 @@ using Content.Client.ContextMenu.UI;
using Content.Shared.Verbs;
using Robust.Client.GameObjects;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.Utility;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
namespace Content.Client.Verbs.UI
{
@@ -27,7 +29,17 @@ namespace Content.Client.Verbs.UI
public VerbMenuElement(Verb verb) : base(verb.Text)
{
ToolTip = verb.Message;
TooltipSupplier = sender =>
{
var label = new RichTextLabel();
label.SetMessage(FormattedMessage.FromMarkupOrThrow(verb.Message ?? verb.Text));
var tooltip = new Tooltip();
tooltip.GetChild(0).Children.Clear();
tooltip.GetChild(0).Children.Add(label);
return tooltip;
};
Disabled = verb.Disabled;
Verb = verb;

View File

@@ -213,7 +213,7 @@ namespace Content.Client.Verbs
{
// maybe send an informative pop-up message.
if (!string.IsNullOrWhiteSpace(verb.Message))
_popupSystem.PopupEntity(verb.Message, user);
_popupSystem.PopupEntity(FormattedMessage.RemoveMarkupOrThrow(verb.Message), user);
return;
}

View File

@@ -0,0 +1,94 @@
using System.Collections.Generic;
using System.Linq;
using Content.Server.DeviceLinking.Systems;
using Content.Shared.DeviceLinking;
using Content.Shared.Prototypes;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Prototypes;
namespace Content.IntegrationTests.Tests.DeviceLinking;
public sealed class DeviceLinkingTest
{
private const string PortTesterProtoId = "DeviceLinkingSinkPortTester";
[TestPrototypes]
private const string Prototypes = $@"
- type: entity
id: {PortTesterProtoId}
components:
- type: DeviceLinkSource
ports:
- Output
";
/// <summary>
/// Spawns every entity that has a <see cref="DeviceLinkSinkComponent"/>
/// and sends a signal to every port to make sure nothing causes an error.
/// </summary>
[Test]
public async Task AllDeviceLinkSinksWorkTest()
{
await using var pair = await PoolManager.GetServerClient();
var server = pair.Server;
var compFact = server.ResolveDependency<IComponentFactory>();
var mapMan = server.ResolveDependency<IMapManager>();
var mapSys = server.System<SharedMapSystem>();
var deviceLinkSys = server.System<DeviceLinkSystem>();
var prototypes = server.ProtoMan.EnumeratePrototypes<EntityPrototype>();
await server.WaitAssertion(() =>
{
Assert.Multiple(() =>
{
foreach (var proto in prototypes)
{
if (proto.Abstract || pair.IsTestPrototype(proto))
continue;
if (!proto.TryGetComponent<DeviceLinkSinkComponent>(out var protoSinkComp, compFact))
continue;
foreach (var port in protoSinkComp.Ports)
{
// Create a map for each entity/port combo so they can't interfere
mapSys.CreateMap(out var mapId);
var grid = mapMan.CreateGridEntity(mapId);
mapSys.SetTile(grid.Owner, grid.Comp, Vector2i.Zero, new Tile(1));
var coord = new EntityCoordinates(grid.Owner, 0, 0);
// Spawn the sink entity
var sinkEnt = server.EntMan.SpawnEntity(proto.ID, coord);
// Get the actual sink component, since the one we got from the prototype doesn't have its owner set up
Assert.That(server.EntMan.TryGetComponent<DeviceLinkSinkComponent>(sinkEnt, out var sinkComp),
$"{proto.ID} does not have a DeviceLinkSinkComponent!");
// Spawn the tester
var sourceEnt = server.EntMan.SpawnEntity(PortTesterProtoId, coord);
Assert.That(server.EntMan.TryGetComponent<DeviceLinkSourceComponent>(sourceEnt, out var sourceComp),
$"Tester prototype does not have a DeviceLinkSourceComponent!");
// Create a link from the tester's output to the target port on the sink
deviceLinkSys.SaveLinks(null,
sourceEnt,
sinkEnt,
[("Output", port.Id)],
sourceComp,
sinkComp);
// Send a signal to the port
Assert.DoesNotThrow(() => { deviceLinkSys.InvokePort(sourceEnt, "Output", null, sourceComp); },
$"Exception thrown while triggering port {port.Id} of sink device {proto.ID}");
mapSys.DeleteMap(mapId);
}
}
});
});
await pair.CleanReturnAsync();
}
}

View File

@@ -0,0 +1,91 @@
using Content.IntegrationTests.Tests.Interaction;
using Content.Shared.Projectiles;
using Robust.Shared.Network;
namespace Content.IntegrationTests.Tests.Embedding;
public sealed class EmbedTest : InteractionTest
{
/// <summary>
/// Embeddable entity that will be thrown at the target.
/// </summary>
private const string EmbeddableProtoId = "SurvivalKnife";
/// <summary>
/// Target entity that the thrown item will embed into.
/// </summary>
private const string TargetProtoId = "AirlockGlass";
/// <summary>
/// Embeds an entity with a <see cref="EmbeddableProjectileComponent"/> into a target,
/// then disconnects the client. Intended to reveal any clientside issues that might
/// occur due to reparenting during cleanup.
/// </summary>
[Test]
public async Task TestDisconnectWhileEmbedded()
{
// Spawn the target we're going to throw at
await SpawnTarget(TargetProtoId);
// Give the player the embeddable to throw
var projectile = await PlaceInHands(EmbeddableProtoId);
Assert.That(TryComp<EmbeddableProjectileComponent>(projectile, out var embedComp),
$"{EmbeddableProtoId} does not have EmbeddableProjectileComponent");
// Make sure the projectile isn't already embedded into anything
Assert.That(embedComp.EmbeddedIntoUid, Is.Null,
$"Projectile already embedded into {SEntMan.ToPrettyString(embedComp.EmbeddedIntoUid)}");
// Have the player throw the embeddable at the target
await ThrowItem();
// Wait a moment for the item to hit and embed
await RunSeconds(0.5f);
// Make sure the projectile is embedded into the target
Assert.That(embedComp.EmbeddedIntoUid, Is.EqualTo(ToServer(Target)),
"Projectile not embedded into target");
// Disconnect the client
var cNetMgr = Client.ResolveDependency<IClientNetManager>();
await Client.WaitPost(Client.EntMan.FlushEntities);
await Pair.RunTicksSync(1);
}
/// <summary>
/// Embeds an entity with a <see cref="EmbeddableProjectileComponent"/> into a target,
/// then deletes the target and makes sure the embeddable is not deleted.
/// </summary>
[Test]
public async Task TestEmbedDetach()
{
// Spawn the target we're going to throw at
await SpawnTarget(TargetProtoId);
// Give the player the embeddable to throw
var projectile = await PlaceInHands(EmbeddableProtoId);
Assert.That(TryComp<EmbeddableProjectileComponent>(projectile, out var embedComp),
$"{EmbeddableProtoId} does not have EmbeddableProjectileComponent");
// Make sure the projectile isn't already embedded into anything
Assert.That(embedComp.EmbeddedIntoUid, Is.Null,
$"Projectile already embedded into {SEntMan.ToPrettyString(embedComp.EmbeddedIntoUid)}");
// Have the player throw the embeddable at the target
await ThrowItem();
// Wait a moment for the item to hit and embed
await RunSeconds(0.5f);
// Make sure the projectile is embedded into the target
Assert.That(embedComp.EmbeddedIntoUid, Is.EqualTo(ToServer(Target)),
"Projectile not embedded into target");
// Delete the target
await Delete(Target.Value);
await RunTicks(1);
// Make sure the embeddable wasn't deleted with the target
AssertExists(projectile);
await AssertEntityLookup(EmbeddableProtoId);
}
}

View File

@@ -155,7 +155,7 @@ public sealed class NukeOpsTest
// The game rule exists, and all the stations/shuttles/maps are properly initialized
var rule = entMan.AllComponents<NukeopsRuleComponent>().Single();
var ruleComp = rule.Component;
var gridsRule = entMan.AllComponents<RuleGridsComponent>().Single().Component;
var gridsRule = entMan.GetComponent<RuleGridsComponent>(rule.Uid);
foreach (var grid in gridsRule.MapGrids)
{
Assert.That(entMan.EntityExists(grid));

View File

@@ -19,16 +19,19 @@ public sealed class LocalizedDatasetPrototypeTest
var protos = protoMan.EnumeratePrototypes<LocalizedDatasetPrototype>().OrderBy(p => p.ID);
// Check each prototype
foreach (var proto in protos)
Assert.Multiple(() =>
{
// Check each value in the prototype
foreach (var locId in proto.Values)
// Check each prototype
foreach (var proto in protos)
{
// Make sure the localization manager has a string for the LocId
Assert.That(localizationMan.HasString(locId), $"LocalizedDataset {proto.ID} with prefix \"{proto.Values.Prefix}\" specifies {proto.Values.Count} entries, but no localized string was found matching {locId}!");
// Check each value in the prototype
foreach (var locId in proto.Values)
{
// Make sure the localization manager has a string for the LocId
Assert.That(localizationMan.HasString(locId), $"LocalizedDataset {proto.ID} with prefix \"{proto.Values.Prefix}\" specifies {proto.Values.Count} entries, but no localized string was found matching {locId}!");
}
}
}
});
await pair.CleanReturnAsync();
}

View File

@@ -22,6 +22,7 @@ using Robust.Shared.EntitySerialization.Systems;
using Robust.Shared.IoC;
using Robust.Shared.Utility;
using YamlDotNet.RepresentationModel;
using Robust.Shared.Map.Events;
namespace Content.IntegrationTests.Tests
{
@@ -213,9 +214,12 @@ namespace Content.IntegrationTests.Tests
}
var deps = server.ResolveDependency<IEntitySystemManager>().DependencyCollection;
var ev = new BeforeEntityReadEvent();
server.EntMan.EventBus.RaiseEvent(EventSource.Local, ev);
foreach (var map in v7Maps)
{
Assert.That(IsPreInit(map, loader, deps));
Assert.That(IsPreInit(map, loader, deps, ev.RenamedPrototypes, ev.DeletedPrototypes));
}
// Check that the test actually does manage to catch post-init maps and isn't just blindly passing everything.
@@ -228,12 +232,12 @@ namespace Content.IntegrationTests.Tests
// First check that a pre-init version passes
var path = new ResPath($"{nameof(NoSavedPostMapInitTest)}.yml");
Assert.That(loader.TrySaveMap(id, path));
Assert.That(IsPreInit(path, loader, deps));
Assert.That(IsPreInit(path, loader, deps, ev.RenamedPrototypes, ev.DeletedPrototypes));
// and the post-init version fails.
await server.WaitPost(() => mapSys.InitializeMap(id));
Assert.That(loader.TrySaveMap(id, path));
Assert.That(IsPreInit(path, loader, deps), Is.False);
Assert.That(IsPreInit(path, loader, deps, ev.RenamedPrototypes, ev.DeletedPrototypes), Is.False);
await pair.CleanReturnAsync();
}
@@ -266,7 +270,11 @@ namespace Content.IntegrationTests.Tests
});
}
private bool IsPreInit(ResPath map, MapLoaderSystem loader, IDependencyCollection deps)
private bool IsPreInit(ResPath map,
MapLoaderSystem loader,
IDependencyCollection deps,
Dictionary<string, string> renamedPrototypes,
HashSet<string> deletedPrototypes)
{
if (!loader.TryReadFile(map, out var data))
{
@@ -274,7 +282,12 @@ namespace Content.IntegrationTests.Tests
return false;
}
var reader = new EntityDeserializer(deps, data, DeserializationOptions.Default);
var reader = new EntityDeserializer(deps,
data,
DeserializationOptions.Default,
renamedPrototypes,
deletedPrototypes);
if (!reader.TryProcessData())
{
Assert.Fail($"Failed to process {map}");

View File

@@ -35,7 +35,6 @@ public sealed class PrototypeSaveTest
await using var pair = await PoolManager.GetServerClient();
var server = pair.Server;
var mapManager = server.ResolveDependency<IMapManager>();
var entityMan = server.ResolveDependency<IEntityManager>();
var prototypeMan = server.ResolveDependency<IPrototypeManager>();
var seriMan = server.ResolveDependency<ISerializationManager>();

View File

@@ -102,6 +102,12 @@ public sealed class StoreTests
+ $"flag as 'true'. This marks the fact that cost modifier of discount is not applied properly!"
);
// The storeComponent returns discounted items with conditions randomly, so we remove these to sanitize the data.
foreach (var discountedItem in discountedListingItems)
{
discountedItem.Conditions = null;
}
// Refund action requests re-generation of listing items so we will be re-acquiring items from component a lot of times.
var itemIds = discountedListingItems.Select(x => x.ID);
foreach (var itemId in itemIds)
@@ -140,6 +146,9 @@ public sealed class StoreTests
// get refreshed item after refund re-generated items
discountedListingItem = storeComponent.FullListingsCatalog.First(x => x.ID == itemId);
// The storeComponent can give a discounted item a condition at random, so we remove it to sanitize the data.
discountedListingItem.Conditions = null;
var afterRefundBalance = storeComponent.Balance[UplinkSystem.TelecrystalCurrencyPrototype];
Assert.That(afterRefundBalance.Value, Is.EqualTo(originalBalance.Value), "Expected refund to return all discounted cost value.");
Assert.That(

View File

@@ -0,0 +1,205 @@
using System.Linq;
using Content.IntegrationTests.Tests.Interaction;
using Content.Server.VendingMachines;
using Content.Shared.Damage;
using Content.Shared.Damage.Prototypes;
using Content.Shared.FixedPoint;
using Content.Shared.VendingMachines;
namespace Content.IntegrationTests.Tests.Vending;
public sealed class VendingInteractionTest : InteractionTest
{
private const string VendingMachineProtoId = "InteractionTestVendingMachine";
private const string VendedItemProtoId = "InteractionTestItem";
private const string RestockBoxProtoId = "InteractionTestRestockBox";
private const string RestockBoxOtherProtoId = "InteractionTestRestockBoxOther";
[TestPrototypes]
private const string TestPrototypes = $@"
- type: entity
parent: BaseItem
id: {VendedItemProtoId}
name: {VendedItemProtoId}
- type: vendingMachineInventory
id: InteractionTestVendingInventory
startingInventory:
{VendedItemProtoId}: 5
- type: vendingMachineInventory
id: InteractionTestVendingInventoryOther
startingInventory:
{VendedItemProtoId}: 5
- type: entity
parent: BaseVendingMachineRestock
id: {RestockBoxProtoId}
components:
- type: VendingMachineRestock
canRestock:
- InteractionTestVendingInventory
- type: entity
parent: BaseVendingMachineRestock
id: {RestockBoxOtherProtoId}
components:
- type: VendingMachineRestock
canRestock:
- InteractionTestVendingInventoryOther
- type: entity
id: {VendingMachineProtoId}
parent: VendingMachine
components:
- type: VendingMachine
pack: InteractionTestVendingInventory
ejectDelay: 0 # no delay to speed up tests
- type: Sprite
sprite: error.rsi
";
[Test]
public async Task InteractUITest()
{
await SpawnTarget(VendingMachineProtoId);
// Should start with no BUI open
Assert.That(IsUiOpen(VendingMachineUiKey.Key), Is.False, "BUI was open unexpectedly.");
// Unpowered vending machine does not open BUI
await Activate();
Assert.That(IsUiOpen(VendingMachineUiKey.Key), Is.False, "BUI opened without power.");
// Power the vending machine
var apc = await SpawnEntity("APCBasic", SEntMan.GetCoordinates(TargetCoords));
await RunTicks(1);
// Interacting with powered vending machine opens BUI
await Activate();
Assert.That(IsUiOpen(VendingMachineUiKey.Key), "BUI failed to open.");
// Interacting with it again closes the BUI
await Activate();
Assert.That(IsUiOpen(VendingMachineUiKey.Key), Is.False, "BUI failed to close on interaction.");
// Reopen BUI for the next check
await Activate();
Assert.That(IsUiOpen(VendingMachineUiKey.Key), "BUI failed to reopen.");
// Remove power
await Delete(apc);
await RunTicks(1);
// The BUI should close when power is lost
Assert.That(IsUiOpen(VendingMachineUiKey.Key), Is.False, "BUI failed to close on power loss.");
}
[Test]
public async Task DispenseItemTest()
{
await SpawnTarget(VendingMachineProtoId);
var vendorEnt = SEntMan.GetEntity(Target.Value);
var vendingSystem = SEntMan.System<VendingMachineSystem>();
var items = vendingSystem.GetAllInventory(vendorEnt);
// Verify initial item count
Assert.That(items, Is.Not.Empty, $"{VendingMachineProtoId} spawned with no items.");
Assert.That(items.First().Amount, Is.EqualTo(5), $"{VendingMachineProtoId} spawned with unexpected item count.");
// Power the vending machine
await SpawnEntity("APCBasic", SEntMan.GetCoordinates(TargetCoords));
await RunTicks(1);
// Open the BUI
await Activate();
Assert.That(IsUiOpen(VendingMachineUiKey.Key), "BUI failed to open.");
// Request an item be dispensed
var ev = new VendingMachineEjectMessage(InventoryType.Regular, VendedItemProtoId);
await SendBui(VendingMachineUiKey.Key, ev);
// Make sure the stock decreased
Assert.That(items.First().Amount, Is.EqualTo(4), "Stocked item count did not decrease.");
// Make sure the dispensed item was spawned in to the world
await AssertEntityLookup(
("APCBasic", 1),
(VendedItemProtoId, 1)
);
}
[Test]
public async Task RestockTest()
{
var vendingSystem = SEntMan.System<VendingMachineSystem>();
await SpawnTarget(VendingMachineProtoId);
var vendorEnt = SEntMan.GetEntity(Target.Value);
var items = vendingSystem.GetAllInventory(vendorEnt);
Assert.That(items, Is.Not.Empty, $"{VendingMachineProtoId} spawned with no items.");
Assert.That(items.First().Amount, Is.EqualTo(5), $"{VendingMachineProtoId} spawned with unexpected item count.");
// Try to restock with the maintenance panel closed (nothing happens)
await InteractUsing(RestockBoxProtoId);
Assert.That(items.First().Amount, Is.EqualTo(5), "Restocked without opening maintenance panel.");
// Open the maintenance panel
await InteractUsing(Screw);
// Try to restock using the wrong restock box (nothing happens)
await InteractUsing(RestockBoxOtherProtoId);
Assert.That(items.First().Amount, Is.EqualTo(5), "Restocked with wrong restock box.");
// Restock the machine
await InteractUsing(RestockBoxProtoId);
Assert.That(items.First().Amount, Is.EqualTo(10), "Restocking resulted in unexpected item count.");
}
[Test]
public async Task RepairTest()
{
await SpawnTarget(VendingMachineProtoId);
// Power the vending machine
await SpawnEntity("APCBasic", SEntMan.GetCoordinates(TargetCoords));
await RunTicks(1);
// Break it
await BreakVendor();
Assert.That(IsUiOpen(VendingMachineUiKey.Key), Is.False, "BUI did not close when vending machine broke.");
// Make sure we can't open the BUI while it's broken
await Activate();
Assert.That(IsUiOpen(VendingMachineUiKey.Key), Is.False, "Opened BUI of broken vending machine.");
// Repair the vending machine
await InteractUsing(Weld);
// Make sure the BUI can open now that the machine has been repaired
await Activate();
Assert.That(IsUiOpen(VendingMachineUiKey.Key), "Failed to open BUI after repair.");
}
private async Task BreakVendor()
{
var damageableSys = SEntMan.System<DamageableSystem>();
Assert.That(TryComp<DamageableComponent>(out var damageableComp), $"{VendingMachineProtoId} does not have DamageableComponent.");
Assert.That(damageableComp.Damage.GetTotal(), Is.EqualTo(FixedPoint2.Zero), $"{VendingMachineProtoId} started with unexpected damage.");
// Damage the vending machine to the point that it breaks
var damageType = ProtoMan.Index<DamageTypePrototype>("Blunt");
var damage = new DamageSpecifier(damageType, FixedPoint2.New(100));
await Server.WaitPost(() => damageableSys.TryChangeDamage(SEntMan.GetEntity(Target), damage, ignoreResistances: true));
await RunTicks(5);
Assert.That(damageableComp.Damage.GetTotal(), Is.GreaterThan(FixedPoint2.Zero), $"{VendingMachineProtoId} did not take damage.");
}
}

View File

@@ -241,7 +241,7 @@ public sealed class AccessOverriderSystem : SharedAccessOverriderSystem
var addedTags = newAccessList.Except(oldTags).Select(tag => "+" + tag).ToList();
var removedTags = oldTags.Except(newAccessList).Select(tag => "-" + tag).ToList();
_adminLogger.Add(LogType.Action, LogImpact.Medium,
_adminLogger.Add(LogType.Action, LogImpact.High,
$"{ToPrettyString(player):player} has modified {ToPrettyString(accessReaderEnt.Value):entity} with the following allowed access level holders: [{string.Join(", ", addedTags.Union(removedTags))}] [{string.Join(", ", newAccessList)}]");
accessReaderEnt.Value.Comp.AccessLists = ConvertAccessListToHashSet(newAccessList);

View File

@@ -168,7 +168,7 @@ public sealed class IdCardConsoleSystem : SharedIdCardConsoleSystem
/*TODO: ECS SharedIdCardConsoleComponent and then log on card ejection, together with the save.
This current implementation is pretty shit as it logs 27 entries (27 lines) if someone decides to give themselves AA*/
_adminLogger.Add(LogType.Action, LogImpact.Medium,
_adminLogger.Add(LogType.Action, LogImpact.High,
$"{ToPrettyString(player):player} has modified {ToPrettyString(targetId):entity} with the following accesses: [{string.Join(", ", addedTags.Union(removedTags))}] [{string.Join(", ", newAccessList)}]");
}

View File

@@ -88,7 +88,7 @@ public sealed class IdCardSystem : SharedIdCardSystem
access.Tags.Add(random.ID);
Dirty(uid, access);
_adminLogger.Add(LogType.Action, LogImpact.Medium,
_adminLogger.Add(LogType.Action, LogImpact.High,
$"{ToPrettyString(args.Microwave)} added {random.ID} access to {ToPrettyString(uid):entity}");
}

View File

@@ -180,7 +180,7 @@ public sealed class ChangeCvarCommand : IConsoleCommand
var oldValue = _configurationManager.GetCVar<object>(cvar);
_configurationManager.SetCVar(cvar, parsed);
_adminLogManager.Add(LogType.AdminCommands,
LogImpact.High,
LogImpact.Extreme,
$"{shell.Player!.Name} ({shell.Player!.UserId}) changed CVAR {cvar} from {oldValue.ToString()} to {parsed.ToString()}"
);

View File

@@ -1,19 +1,19 @@
using Content.Server.Administration.Notes;
using System.Linq;
using Content.Server.Administration.Notes;
using Content.Shared.Administration;
using Robust.Server.Player;
using Robust.Shared.Console;
namespace Content.Server.Administration.Commands;
[AdminCommand(AdminFlags.ViewNotes)]
public sealed class OpenAdminNotesCommand : IConsoleCommand
public sealed class OpenAdminNotesCommand : LocalizedCommands
{
public const string CommandName = "adminnotes";
public string Command => CommandName;
public string Description => "Opens the admin notes panel.";
public string Help => $"Usage: {Command} <notedPlayerUserId OR notedPlayerUsername>";
public override string Command => CommandName;
public async void Execute(IConsoleShell shell, string argStr, string[] args)
public override async void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (shell.Player is not { } player)
{
@@ -33,17 +33,27 @@ public sealed class OpenAdminNotesCommand : IConsoleCommand
if (dbGuid == null)
{
shell.WriteError($"Unable to find {args[0]} netuserid");
shell.WriteError(Loc.GetString("cmd-adminnotes-wrong-target", ("user", args[0])));
return;
}
notedPlayer = dbGuid.UserId;
break;
default:
shell.WriteError($"Invalid arguments.\n{Help}");
shell.WriteError(Loc.GetString("cmd-adminnotes-args-error"));
return;
}
await IoCManager.Resolve<IAdminNotesManager>().OpenEui(player, notedPlayer);
}
public override CompletionResult GetCompletion(IConsoleShell shell, string[] args)
{
if (args.Length != 1)
return CompletionResult.Empty;
var playerMgr = IoCManager.Resolve<IPlayerManager>();
var options = playerMgr.Sessions.Select(c => c.Name).OrderBy(c => c).ToArray();
return CompletionResult.FromHintOptions(options, Loc.GetString("cmd-adminnotes-hint"));
}
}

View File

@@ -48,7 +48,7 @@ namespace Content.Server.Administration.Commands
}
else
{
if (player.Status != SessionStatus.InGame || player.AttachedEntity is not {Valid: true} playerEntity)
if (player.Status != SessionStatus.InGame || player.AttachedEntity is not { Valid: true } playerEntity)
{
shell.WriteLine("You are not in-game!");
return;
@@ -57,14 +57,16 @@ namespace Content.Server.Administration.Commands
var currentMap = _entManager.GetComponent<TransformComponent>(playerEntity).MapID;
var currentGrid = _entManager.GetComponent<TransformComponent>(playerEntity).GridUid;
var xformSystem = _entManager.System<SharedTransformSystem>();
var found = GetWarpPointByName(location)
.OrderBy(p => p.Item1, Comparer<EntityCoordinates>.Create((a, b) =>
{
// Sort so that warp points on the same grid/map are first.
// So if you have two maps loaded with the same warp points,
// it will prefer the warp points on the map you're currently on.
var aGrid = a.GetGridUid(_entManager);
var bGrid = b.GetGridUid(_entManager);
var aGrid = xformSystem.GetGrid(a);
var bGrid = xformSystem.GetGrid(b);
if (aGrid == bGrid)
{
@@ -81,8 +83,8 @@ namespace Content.Server.Administration.Commands
return 1;
}
var mapA = a.GetMapId(_entManager);
var mapB = a.GetMapId(_entManager);
var mapA = xformSystem.GetMapId(a);
var mapB = xformSystem.GetMapId(b);
if (mapA == mapB)
{
@@ -117,10 +119,8 @@ namespace Content.Server.Administration.Commands
return;
}
var xform = _entManager.GetComponent<TransformComponent>(playerEntity);
var xformSystem = _entManager.System<SharedTransformSystem>();
xform.Coordinates = coords;
xformSystem.AttachToGridOrMap(playerEntity, xform);
xformSystem.SetCoordinates(playerEntity, coords);
xformSystem.AttachToGridOrMap(playerEntity);
if (_entManager.TryGetComponent(playerEntity, out PhysicsComponent? physics))
{
_entManager.System<SharedPhysicsSystem>().SetLinearVelocity(playerEntity, Vector2.Zero, body: physics);

View File

@@ -2,14 +2,19 @@
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Content.Server.Administration.Systems;
using Content.Server.Database;
using Content.Server.GameTicking;
using Content.Shared.Administration.Logs;
using Content.Shared.CCVar;
using Content.Shared.Chat;
using Content.Shared.Database;
using Content.Shared.Players.PlayTimeTracking;
using Prometheus;
using Robust.Shared;
using Robust.Shared.Configuration;
using Robust.Shared.Network;
using Robust.Shared.Player;
using Robust.Shared.Reflection;
using Robust.Shared.Timing;
@@ -25,6 +30,9 @@ public sealed partial class AdminLogManager : SharedAdminLogManager, IAdminLogMa
[Dependency] private readonly IDynamicTypeFactory _typeFactory = default!;
[Dependency] private readonly IReflectionManager _reflection = default!;
[Dependency] private readonly IDependencyCollection _dependencies = default!;
[Dependency] private readonly ISharedPlayerManager _player = default!;
[Dependency] private readonly ISharedPlaytimeManager _playtime = default!;
[Dependency] private readonly ISharedChatManager _chat = default!;
public const string SawmillId = "admin.logs";
@@ -66,6 +74,7 @@ public sealed partial class AdminLogManager : SharedAdminLogManager, IAdminLogMa
private int _queueMax;
private int _preRoundQueueMax;
private int _dropThreshold;
private int _highImpactLogPlaytime;
// Per update
private TimeSpan _nextUpdateTime;
@@ -100,6 +109,8 @@ public sealed partial class AdminLogManager : SharedAdminLogManager, IAdminLogMa
value => _preRoundQueueMax = value, true);
_configuration.OnValueChanged(CCVars.AdminLogsDropThreshold,
value => _dropThreshold = value, true);
_configuration.OnValueChanged(CCVars.AdminLogsHighLogPlaytime,
value => _highImpactLogPlaytime = value, true);
if (_metricsEnabled)
{
@@ -300,6 +311,10 @@ public sealed partial class AdminLogManager : SharedAdminLogManager, IAdminLogMa
Players = new List<AdminLogPlayer>(players.Count)
};
var adminLog = false;
var adminSys = _entityManager.SystemOrNull<AdminSystem>();
var logMessage = message;
foreach (var id in players)
{
var player = new AdminLogPlayer
@@ -309,8 +324,39 @@ public sealed partial class AdminLogManager : SharedAdminLogManager, IAdminLogMa
};
log.Players.Add(player);
if (adminSys != null)
{
var cachedInfo = adminSys.GetCachedPlayerInfo(new NetUserId(id));
if (cachedInfo != null && cachedInfo.Antag)
{
logMessage += " [ANTAG: " + cachedInfo.CharacterName + "]";
}
}
if (adminLog)
continue;
if (impact == LogImpact.Extreme) // Always chat-notify Extreme logs
adminLog = true;
if (impact == LogImpact.High) // Only chat-notify High logs if the player is below a threshold playtime
{
if (_highImpactLogPlaytime >= 0 && _player.TryGetSessionById(new NetUserId(id), out var session))
{
var playtimes = _playtime.GetPlayTimes(session);
if (playtimes.TryGetValue(PlayTimeTrackingShared.TrackerOverall, out var overallTime) &&
overallTime <= TimeSpan.FromHours(_highImpactLogPlaytime))
{
adminLog = true;
}
}
}
}
if (adminLog)
_chat.SendAdminAlert(logMessage);
if (preRound)
{
_preRoundLogQueue.Enqueue(log);

View File

@@ -221,6 +221,7 @@ public sealed class AdminSystem : EntitySystem
var name = data.UserName;
var entityName = string.Empty;
var identityName = string.Empty;
var sortWeight = 0;
// Visible (identity) name can be different from real name
if (session?.AttachedEntity != null)
@@ -234,8 +235,10 @@ public sealed class AdminSystem : EntitySystem
// Starting role, antagonist status and role type
RoleTypePrototype roleType = new();
var startingRole = string.Empty;
if (_minds.TryGetMind(session, out var mindId, out var mindComp))
if (_minds.TryGetMind(session, out var mindId, out var mindComp) && mindComp is not null)
{
sortWeight = _role.GetRoleCompByTime(mindComp)?.Comp.SortWeight ?? 0;
if (_proto.TryIndex(mindComp.RoleType, out var role))
roleType = role;
else
@@ -259,8 +262,19 @@ public sealed class AdminSystem : EntitySystem
overallPlaytime = playTime;
}
return new PlayerInfo(name, entityName, identityName, startingRole, antag, roleType, GetNetEntity(session?.AttachedEntity), data.UserId,
connected, _roundActivePlayers.Contains(data.UserId), overallPlaytime);
return new PlayerInfo(
name,
entityName,
identityName,
startingRole,
antag,
roleType,
sortWeight,
GetNetEntity(session?.AttachedEntity),
data.UserId,
connected,
_roundActivePlayers.Contains(data.UserId),
overallPlaytime);
}
private void OnPanicBunkerChanged(bool enabled)

View File

@@ -1,10 +1,12 @@
using Content.Server._CP14.GameTicking.Rules.Components;
using Content.Server.Administration.Commands;
using Content.Server.Antag;
using Content.Server.GameTicking;
using Content.Server.GameTicking.Rules.Components;
using Content.Server.Zombies;
using Content.Shared.Administration;
using Content.Shared.Database;
using Content.Shared.Humanoid;
using Content.Shared.Mind.Components;
using Content.Shared.Roles;
using Content.Shared.Verbs;
@@ -18,6 +20,7 @@ public sealed partial class AdminVerbSystem
{
[Dependency] private readonly AntagSelectionSystem _antag = default!;
[Dependency] private readonly ZombieSystem _zombie = default!;
[Dependency] private readonly GameTicker _gameTicker = default!;
[ValidatePrototypeId<EntityPrototype>]
private const string DefaultTraitorRule = "Traitor";
@@ -37,6 +40,8 @@ public sealed partial class AdminVerbSystem
[ValidatePrototypeId<StartingGearPrototype>]
private const string PirateGearId = "PirateGear";
private readonly EntProtoId _paradoxCloneRuleId = "ParadoxCloneSpawn";
//CP14
[ValidatePrototypeId<EntityPrototype>]
private const string CP14VampireRule = "CP14Vampire";
@@ -179,6 +184,30 @@ public sealed partial class AdminVerbSystem
Message = string.Join(": ", thiefName, Loc.GetString("admin-verb-make-thief")),
};
args.Verbs.Add(thief);
var paradoxCloneName = Loc.GetString("admin-verb-text-make-paradox-clone");
Verb paradox = new()
{
Text = paradoxCloneName,
Category = VerbCategory.Antag,
Icon = new SpriteSpecifier.Rsi(new("/Textures/Interface/Misc/job_icons.rsi"), "ParadoxClone"),
Act = () =>
{
var ruleEnt = _gameTicker.AddGameRule(_paradoxCloneRuleId);
if (!TryComp<ParadoxCloneRuleComponent>(ruleEnt, out var paradoxCloneRuleComp))
return;
paradoxCloneRuleComp.OriginalBody = args.Target; // override the target player
_gameTicker.StartGameRule(ruleEnt);
},
Impact = LogImpact.High,
Message = string.Join(": ", paradoxCloneName, Loc.GetString("admin-verb-make-paradox-clone")),
};
if (HasComp<HumanoidAppearanceComponent>(args.Target)) // only humanoids can be cloned
args.Verbs.Add(paradox);
*/
}
}

View File

@@ -136,7 +136,7 @@ public sealed partial class AdminVerbSystem
Filter.PvsExcept(args.Target), true, PopupType.MediumCaution);
var board = Spawn("ChessBoard", xform.Coordinates);
var session = _tabletopSystem.EnsureSession(Comp<TabletopGameComponent>(board));
xform.Coordinates = _transformSystem.ToCoordinates(session.Position);
_transformSystem.SetMapCoordinates(args.Target, session.Position);
_transformSystem.SetWorldRotationNoLerp((args.Target, xform), Angle.Zero);
},
Impact = LogImpact.Extreme,
@@ -421,7 +421,7 @@ public sealed partial class AdminVerbSystem
{
var xform = Transform(args.Target);
var fixtures = Comp<FixturesComponent>(args.Target);
xform.Anchored = false; // Just in case.
_transformSystem.Unanchor(args.Target); // Just in case.
_physics.SetBodyType(args.Target, BodyType.Dynamic, manager: fixtures, body: physics);
_physics.SetBodyStatus(args.Target, physics, BodyStatus.InAir);
_physics.WakeBody(args.Target, manager: fixtures, body: physics);
@@ -456,7 +456,7 @@ public sealed partial class AdminVerbSystem
{
var xform = Transform(args.Target);
var fixtures = Comp<FixturesComponent>(args.Target);
xform.Anchored = false; // Just in case.
_transformSystem.Unanchor(args.Target); // Just in case.
_physics.SetBodyType(args.Target, BodyType.Dynamic, body: physics);
_physics.SetBodyStatus(args.Target, physics, BodyStatus.InAir);

View File

@@ -637,7 +637,7 @@ public sealed partial class AdminVerbSystem
{
if (_adminManager.HasAdminFlag(player, AdminFlags.Mapping))
{
if (_mapManager.IsMapPaused(map.MapId))
if (_map.IsPaused(map.MapId))
{
Verb unpauseMap = new()
{
@@ -646,7 +646,7 @@ public sealed partial class AdminVerbSystem
Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/AdminActions/play.png")),
Act = () =>
{
_mapManager.SetMapPaused(map.MapId, false);
_map.SetPaused(map.MapId, false);
},
Impact = LogImpact.Extreme,
Message = Loc.GetString("admin-trick-unpause-map-description"),
@@ -663,7 +663,7 @@ public sealed partial class AdminVerbSystem
Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/AdminActions/pause.png")),
Act = () =>
{
_mapManager.SetMapPaused(map.MapId, true);
_map.SetPaused(map.MapId, true);
},
Impact = LogImpact.Extreme,
Message = Loc.GetString("admin-trick-pause-map-description"),

View File

@@ -53,7 +53,7 @@ namespace Content.Server.Administration.Systems
[Dependency] private readonly IConsoleHost _console = default!;
[Dependency] private readonly IAdminManager _adminManager = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly SharedMapSystem _map = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly AdminSystem _adminSystem = default!;
[Dependency] private readonly DisposalTubeSystem _disposalTubes = default!;
@@ -153,12 +153,10 @@ namespace Content.Server.Administration.Systems
var profile = _ticker.GetPlayerProfile(targetActor.PlayerSession);
var mobUid = _spawning.SpawnPlayerMob(coords.Value, null, profile, stationUid);
var targetMind = _mindSystem.GetMind(args.Target);
if (targetMind != null)
{
_mindSystem.TransferTo(targetMind.Value, mobUid, true);
}
if (_mindSystem.TryGetMind(args.Target, out var mindId, out var mindComp))
_mindSystem.TransferTo(mindId, mobUid, true, mind: mindComp);
},
ConfirmationPopup = true,
Impact = LogImpact.High,

View File

@@ -14,6 +14,7 @@ internal sealed class ServerAlertsSystem : AlertsSystem
private void OnGetState(Entity<AlertsComponent> alerts, ref ComponentGetState args)
{
args.State = new AlertComponentState(alerts.Comp.Alerts);
// TODO: Use sourcegen when clone-state bug fixed.
args.State = new AlertComponentState(new(alerts.Comp.Alerts));
}
}

View File

@@ -240,7 +240,7 @@ public sealed class AmeControllerSystem : EntitySystem
return;
var humanReadableState = value ? "Inject" : "Not inject";
_adminLogger.Add(LogType.Action, LogImpact.Extreme, $"{EntityManager.ToPrettyString(user.Value):player} has set the AME to {humanReadableState}");
_adminLogger.Add(LogType.Action, LogImpact.Medium, $"{EntityManager.ToPrettyString(user.Value):player} has set the AME to {humanReadableState}");
}
public void ToggleInjecting(EntityUid uid, EntityUid? user = null, AmeControllerComponent? controller = null)
@@ -267,27 +267,15 @@ public sealed class AmeControllerSystem : EntitySystem
return;
var humanReadableState = controller.Injecting ? "Inject" : "Not inject";
_adminLogger.Add(LogType.Action, LogImpact.Extreme, $"{EntityManager.ToPrettyString(user.Value):player} has set the AME to inject {controller.InjectionAmount} while set to {humanReadableState}");
/* This needs to be information which an admin is very likely to want to be informed about in order to be an admin alert or have a sound notification.
At the time of editing, players regularly "overclock" the AME and those cases require no admin attention.
// Admin alert
var safeLimit = int.MaxValue;
if (TryGetAMENodeGroup(uid, out var group))
safeLimit = group.CoreCount * 4;
if (oldValue <= safeLimit && value > safeLimit)
{
if (_gameTiming.CurTime > controller.EffectCooldown)
{
_chatManager.SendAdminAlert(user.Value, $"increased AME over safe limit to {controller.InjectionAmount}");
_audioSystem.PlayGlobal("/Audio/Misc/adminlarm.ogg",
Filter.Empty().AddPlayers(_adminManager.ActiveAdmins), false, AudioParams.Default.WithVolume(-8f));
controller.EffectCooldown = _gameTiming.CurTime + controller.CooldownDuration;
}
}
*/
var logImpact = (oldValue <= safeLimit && value > safeLimit) ? LogImpact.Extreme : LogImpact.Medium;
_adminLogger.Add(LogType.Action, logImpact, $"{EntityManager.ToPrettyString(user.Value):player} has set the AME to inject {controller.InjectionAmount} while set to {humanReadableState}");
}
public void AdjustInjectionAmount(EntityUid uid, int delta, EntityUid? user = null, AmeControllerComponent? controller = null)

View File

@@ -6,7 +6,7 @@ namespace Content.Server.Announcements;
/// <summary>
/// Used for any announcements on the start of a round.
/// </summary>
[Prototype("roundAnnouncement")]
[Prototype]
public sealed partial class RoundAnnouncementPrototype : IPrototype
{
[IdDataField]

View File

@@ -116,7 +116,7 @@ public sealed class InnerBodyAnomalySystem : SharedInnerBodyAnomalySystem
_popup.PopupEntity(message, ent, ent, PopupType.MediumCaution);
_adminLog.Add(LogType.Anomaly,LogImpact.Extreme,$"{ToPrettyString(ent)} became anomaly host.");
_adminLog.Add(LogType.Anomaly,LogImpact.Medium,$"{ToPrettyString(ent)} became anomaly host.");
}
Dirty(ent);
}

View File

@@ -1,4 +1,5 @@
using Content.Server.Antag.Components;
using Content.Shared.GameTicking.Components;
using Content.Server.GameTicking.Rules;
namespace Content.Server.Antag;
@@ -14,9 +15,20 @@ public sealed class AntagRandomSpawnSystem : GameRuleSystem<AntagRandomSpawnComp
SubscribeLocalEvent<AntagRandomSpawnComponent, AntagSelectLocationEvent>(OnSelectLocation);
}
protected override void Added(EntityUid uid, AntagRandomSpawnComponent comp, GameRuleComponent gameRule, GameRuleAddedEvent args)
{
base.Added(uid, comp, gameRule, args);
// we have to select this here because AntagSelectLocationEvent is raised twice because MakeAntag is called twice
// once when a ghost role spawner is created and once when someone takes the ghost role
if (TryFindRandomTile(out _, out _, out _, out var coords))
comp.Coords = coords;
}
private void OnSelectLocation(Entity<AntagRandomSpawnComponent> ent, ref AntagSelectLocationEvent args)
{
if (TryFindRandomTile(out _, out _, out _, out var coords))
args.Coordinates.Add(_transform.ToMapCoordinates(coords));
if (ent.Comp.Coords != null)
args.Coordinates.Add(_transform.ToMapCoordinates(ent.Comp.Coords.Value));
}
}

View File

@@ -2,6 +2,7 @@ using System.Linq;
using Content.Server.Antag.Components;
using Content.Server.Chat.Managers;
using Content.Server.GameTicking;
using Content.Server.GameTicking.Events;
using Content.Server.GameTicking.Rules;
using Content.Server.Ghost.Roles;
using Content.Server.Ghost.Roles.Components;
@@ -65,6 +66,7 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
SubscribeLocalEvent<AntagSelectionComponent, ObjectivesTextGetInfoEvent>(OnObjectivesTextGetInfo);
SubscribeLocalEvent<NoJobsAvailableSpawningEvent>(OnJobNotAssigned);
SubscribeLocalEvent<RulePlayerSpawningEvent>(OnPlayerSpawning);
SubscribeLocalEvent<RulePlayerJobsAssignedEvent>(OnJobsAssigned);
SubscribeLocalEvent<PlayerSpawnCompleteEvent>(OnSpawnComplete);
@@ -136,6 +138,28 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
}
}
private void OnJobNotAssigned(NoJobsAvailableSpawningEvent args)
{
// If someone fails to spawn in due to there being no jobs, they should be removed from any preselected antags.
// We only care about delayed rules, since if they're active the player should have already been removed via MakeAntag.
var query = QueryDelayedRules();
while (query.MoveNext(out var uid, out _, out var comp, out _))
{
if (comp.SelectionTime != AntagSelectionTime.IntraPlayerSpawn)
continue;
if (!comp.RemoveUponFailedSpawn)
continue;
foreach (var def in comp.Definitions)
{
if (!comp.PreSelectedSessions.TryGetValue(def, out var session))
break;
session.Remove(args.Player);
}
}
}
private void OnSpawnComplete(PlayerSpawnCompleteEvent args)
{
if (!args.LateJoin)
@@ -149,8 +173,7 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
var rules = new List<(EntityUid, AntagSelectionComponent)>();
while (query.MoveNext(out var uid, out var antag, out _))
{
if (HasComp<ActiveGameRuleComponent>(uid) ||
(HasComp<DelayedStartRuleComponent>(uid) && antag.SelectionTime == AntagSelectionTime.IntraPlayerSpawn)) //IntraPlayerSpawn selects antags before spawning, but doesn't activate until after.
if (HasComp<ActiveGameRuleComponent>(uid))
rules.Add((uid, antag));
}
RobustRandom.Shuffle(rules);
@@ -171,9 +194,7 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
if (!TryGetNextAvailableDefinition((uid, antag), out var def, players))
continue;
var onlyPreSelect = (antag.SelectionTime == AntagSelectionTime.IntraPlayerSpawn && !antag.AssignmentComplete); // Don't wanna give them antag status if the rule hasn't assigned its existing ones yet
if (TryMakeAntag((uid, antag), args.Player, def.Value, onlyPreSelect: onlyPreSelect))
if (TryMakeAntag((uid, antag), args.Player, def.Value))
break;
}
}
@@ -209,16 +230,12 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
if (component.AssignmentComplete)
return;
if (!component.PreSelectionsComplete)
{
var players = _playerManager.Sessions
.Where(x => GameTicker.PlayerGameStatuses.TryGetValue(x.UserId, out var status) &&
status == PlayerGameStatus.JoinedGame)
.ToList();
ChooseAntags((uid, component), players, midround: true);
}
var players = _playerManager.Sessions
.Where(x => GameTicker.PlayerGameStatuses.TryGetValue(x.UserId, out var status) &&
status == PlayerGameStatus.JoinedGame)
.ToList();
ChooseAntags((uid, component), players, midround: true);
AssignPreSelectedSessions((uid, component));
}
@@ -230,9 +247,6 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
/// <param name="midround">Disable picking players for pre-spawn antags in the middle of a round</param>
public void ChooseAntags(Entity<AntagSelectionComponent> ent, IList<ICommonSession> pool, bool midround = false)
{
if (ent.Comp.PreSelectionsComplete)
return;
foreach (var def in ent.Comp.Definitions)
{
ChooseAntags(ent, pool, def, midround: midround);
@@ -254,7 +268,8 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
bool midround = false)
{
var playerPool = GetPlayerPool(ent, pool, def);
var count = GetTargetAntagCount(ent, GetTotalPlayerCount(pool), def);
var existingAntagCount = ent.Comp.PreSelectedSessions.TryGetValue(def, out var existingAntags) ? existingAntags.Count : 0;
var count = GetTargetAntagCount(ent, GetTotalPlayerCount(pool), def) - existingAntagCount;
// if there is both a spawner and players getting picked, let it fall back to a spawner.
var noSpawner = def.SpawnerPrototype == null;
@@ -327,6 +342,8 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
/// </summary>
public bool TryMakeAntag(Entity<AntagSelectionComponent> ent, ICommonSession? session, AntagSelectionDefinition def, bool ignoreSpawner = false, bool checkPref = true, bool onlyPreSelect = false)
{
_adminLogger.Add(LogType.AntagSelection, $"Start trying to make {session} become the antagonist: {ToPrettyString(ent)}");
if (checkPref && !HasPrimaryAntagPreference(session, def))
return false;
@@ -384,7 +401,8 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
if (antagEnt is not { } player)
{
Log.Error($"Attempted to make {session} antagonist in gamerule {ToPrettyString(ent)} but there was no valid entity for player.");
if (session != null)
_adminLogger.Add(LogType.AntagSelection,$"Attempted to make {session} antagonist in gamerule {ToPrettyString(ent)} but there was no valid entity for player.");
if (session != null && ent.Comp.RemoveUponFailedSpawn)
{
ent.Comp.AssignedSessions.Remove(session);
ent.Comp.PreSelectedSessions[def].Remove(session);
@@ -393,6 +411,11 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
return;
}
// TODO: This is really messy because this part runs twice for midround events.
// Once when the ghostrole spawner is created and once when a player takes it.
// Therefore any component subscribing to this has to make sure both subscriptions return the same value
// or the ghost role raffle location preview will be wrong.
var getPosEv = new AntagSelectLocationEvent(session, ent);
RaiseLocalEvent(ent, ref getPosEv, true);
if (getPosEv.Handled)
@@ -409,6 +432,7 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
if (!TryComp<GhostRoleAntagSpawnerComponent>(player, out var spawnerComp))
{
Log.Error($"Antag spawner {player} does not have a GhostRoleAntagSpawnerComponent.");
_adminLogger.Add(LogType.AntagSelection,$"Antag spawner {player} in gamerule {ToPrettyString(ent)} failed due to not having GhostRoleAntagSpawnerComponent.");
if (session != null)
{
ent.Comp.AssignedSessions.Remove(session);
@@ -470,6 +494,9 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
if (!IsSessionValid(ent, session, def) || !IsEntityValid(session.AttachedEntity, def))
continue;
if (ent.Comp.PreSelectedSessions.TryGetValue(def, out var preSelected) && preSelected.Contains(session))
continue;
if (HasPrimaryAntagPreference(session, def))
{
preferredList.Add(session);

View File

@@ -1,3 +1,5 @@
using Robust.Shared.Map;
namespace Content.Server.Antag.Components;
/// <summary>
@@ -5,4 +7,11 @@ namespace Content.Server.Antag.Components;
/// Requires <see cref="AntagSelectionComponent"/>.
/// </summary>
[RegisterComponent]
public sealed partial class AntagRandomSpawnComponent : Component;
public sealed partial class AntagRandomSpawnComponent : Component
{
/// <summary>
/// Location that was picked.
/// </summary>
[DataField]
public EntityCoordinates? Coords;
}

View File

@@ -61,6 +61,13 @@ public sealed partial class AntagSelectionComponent : Component
/// </summary>
[DataField]
public LocId? AgentName;
/// <summary>
/// If the player is pre-selected but fails to spawn in (e.g. due to only having antag-immune jobs selected),
/// should they be removed from the pre-selection list?
/// </summary>
[DataField]
public bool RemoveUponFailedSpawn = true;
}
[DataDefinition]

View File

@@ -5,7 +5,7 @@ using Robust.Shared.Prototypes;
namespace Content.Server.Atmos.Reactions
{
[Prototype("gasReaction")]
[Prototype]
public sealed partial class GasReactionPrototype : IPrototype
{
[ViewVariables]

View File

@@ -65,15 +65,11 @@ public sealed class BodySystem : SharedBodySystem
// TODO: Predict this probably.
base.AddPart(bodyEnt, partEnt, slotId);
if (TryComp<HumanoidAppearanceComponent>(bodyEnt, out var humanoid))
var layer = partEnt.Comp.ToHumanoidLayers();
if (layer != null)
{
var layer = partEnt.Comp.ToHumanoidLayers();
if (layer != null)
{
var layers = HumanoidVisualLayersExtension.Sublayers(layer.Value);
_humanoidSystem.SetLayersVisibility(
bodyEnt, layers, visible: true, permanent: true, humanoid);
}
var layers = HumanoidVisualLayersExtension.Sublayers(layer.Value);
_humanoidSystem.SetLayersVisibility(bodyEnt.Owner, layers, visible: true);
}
}
@@ -93,8 +89,7 @@ public sealed class BodySystem : SharedBodySystem
return;
var layers = HumanoidVisualLayersExtension.Sublayers(layer.Value);
_humanoidSystem.SetLayersVisibility(
bodyEnt, layers, visible: false, permanent: true, humanoid);
_humanoidSystem.SetLayersVisibility((bodyEnt, humanoid), layers, visible: false);
}
public override HashSet<EntityUid> GibBody(

View File

@@ -58,7 +58,7 @@ public sealed class LungSystem : EntitySystem
private void OnMaskToggled(Entity<BreathToolComponent> ent, ref ItemMaskToggledEvent args)
{
if (args.IsToggled || args.IsEquip)
if (args.Mask.Comp.IsToggled)
{
_atmos.DisconnectInternals(ent);
}
@@ -69,7 +69,7 @@ public sealed class LungSystem : EntitySystem
if (TryComp(args.Wearer, out InternalsComponent? internals))
{
ent.Comp.ConnectedInternalsEntity = args.Wearer;
_internals.ConnectBreathTool((args.Wearer, internals), ent);
_internals.ConnectBreathTool((args.Wearer.Value, internals), ent);
}
}
}

View File

@@ -51,6 +51,7 @@ public sealed class ThermalRegulatorSystem : EntitySystem
if (!Resolve(ent, ref ent.Comp2, logMissing: false))
return;
// TODO: Why do we have two datafields for this if they are only ever used once here?
var totalMetabolismTempChange = ent.Comp1.MetabolismHeat - ent.Comp1.RadiatedHeat;
// implicit heat regulation
@@ -74,7 +75,7 @@ public sealed class ThermalRegulatorSystem : EntitySystem
// if body temperature is not within comfortable, thermal regulation
// processes starts
if (tempDiff > ent.Comp1.ThermalRegulationTemperatureThreshold)
if (tempDiff < ent.Comp1.ThermalRegulationTemperatureThreshold)
return;
if (ent.Comp2.CurrentTemperature > ent.Comp1.NormalBodyTemperature)

View File

@@ -1,6 +1,7 @@
using Content.Server.Botany.Components;
using Content.Server.Botany.Systems;
using Content.Shared.Atmos;
using Content.Shared.Database;
using Content.Shared.EntityEffects;
using Content.Shared.Random;
using Robust.Shared.Audio;
@@ -11,7 +12,7 @@ using Robust.Shared.Utility;
namespace Content.Server.Botany;
[Prototype("seed")]
[Prototype]
public sealed partial class SeedPrototype : SeedData, IPrototype
{
[IdDataField] public string ID { get; private set; } = default!;
@@ -243,6 +244,18 @@ public partial class SeedData
[DataField(customTypeSerializer: typeof(PrototypeIdListSerializer<SeedPrototype>))]
public List<string> MutationPrototypes = new();
/// <summary>
/// Log impact for when the seed is planted.
/// </summary>
[DataField]
public LogImpact? PlantLogImpact = null;
/// <summary>
/// Log impact for when the seed is harvested.
/// </summary>
[DataField]
public LogImpact? HarvestLogImpact = null;
public SeedData Clone()
{
DebugTools.Assert(!Immutable, "There should be no need to clone an immutable seed.");

View File

@@ -14,6 +14,8 @@ using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Content.Shared.Administration.Logs;
using Content.Shared.Database;
namespace Content.Server.Botany.Systems;
@@ -27,6 +29,7 @@ public sealed partial class BotanySystem : EntitySystem
[Dependency] private readonly SharedSolutionContainerSystem _solutionContainerSystem = default!;
[Dependency] private readonly MetaDataSystem _metaData = default!;
[Dependency] private readonly RandomHelperSystem _randomHelper = default!;
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
public override void Initialize()
{
@@ -116,7 +119,12 @@ public sealed partial class BotanySystem : EntitySystem
{
if (position.IsValid(EntityManager) &&
proto.ProductPrototypes.Count > 0)
{
if (proto.HarvestLogImpact != null)
_adminLogger.Add(LogType.Botany, proto.HarvestLogImpact.Value, $"Auto-harvested {Loc.GetString(proto.Name):seed} at Pos:{position}.");
return GenerateProduct(proto, position, yieldMod);
}
return Enumerable.Empty<EntityUid>();
}
@@ -131,6 +139,10 @@ public sealed partial class BotanySystem : EntitySystem
var name = Loc.GetString(proto.DisplayName);
_popupSystem.PopupCursor(Loc.GetString("botany-harvest-success-message", ("name", name)), user, PopupType.Medium);
if (proto.HarvestLogImpact != null)
_adminLogger.Add(LogType.Botany, proto.HarvestLogImpact.Value, $"{ToPrettyString(user):player} harvested {Loc.GetString(proto.Name):seed} at Pos:{Transform(user).Coordinates}.");
return GenerateProduct(proto, Transform(user).Coordinates, yieldMod);
}

View File

@@ -23,7 +23,9 @@ using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Timing;
using Content.Server.Labels.Components;
using Content.Shared.Administration.Logs;
using Content.Shared.Containers.ItemSlots;
using Content.Shared.Database;
namespace Content.Server.Botany.Systems;
@@ -42,6 +44,7 @@ public sealed class PlantHolderSystem : EntitySystem
[Dependency] private readonly RandomHelperSystem _randomHelper = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly ItemSlotsSystem _itemSlots = default!;
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
public const float HydroponicsSpeedMultiplier = 1f;
@@ -188,6 +191,9 @@ public sealed class PlantHolderSystem : EntitySystem
CheckLevelSanity(uid, component);
UpdateSprite(uid, component);
if (seed.PlantLogImpact != null)
_adminLogger.Add(LogType.Botany, seed.PlantLogImpact.Value, $"{ToPrettyString(args.User):player} planted {Loc.GetString(seed.Name):seed} at Pos:{Transform(uid).Coordinates}.");
return;
}

View File

@@ -0,0 +1,151 @@
using Content.Shared.CartridgeLoader.Cartridges;
using Content.Shared.CartridgeLoader;
using Content.Shared.Hands.EntitySystems;
using Content.Shared.Interaction;
using Content.Shared.Paper;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Content.Server.CartridgeLoader.Cartridges;
/// <summary>
/// Server-side class implementing the core UI logic of NanoTask
/// </summary>
public sealed class NanoTaskCartridgeSystem : SharedNanoTaskCartridgeSystem
{
[Dependency] private readonly CartridgeLoaderSystem _cartridgeLoader = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly PaperSystem _paper = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly SharedHandsSystem _hands = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<NanoTaskCartridgeComponent, CartridgeMessageEvent>(OnUiMessage);
SubscribeLocalEvent<NanoTaskCartridgeComponent, CartridgeUiReadyEvent>(OnUiReady);
SubscribeLocalEvent<NanoTaskCartridgeComponent, CartridgeRemovedEvent>(OnCartridgeRemoved);
SubscribeLocalEvent<NanoTaskInteractionComponent, InteractUsingEvent>(OnInteractUsing);
}
private void OnCartridgeRemoved(Entity<NanoTaskCartridgeComponent> ent, ref CartridgeRemovedEvent args)
{
if (!_cartridgeLoader.HasProgram<NanoTaskCartridgeComponent>(args.Loader))
{
RemComp<NanoTaskInteractionComponent>(args.Loader);
}
}
private void OnInteractUsing(Entity<NanoTaskInteractionComponent> ent, ref InteractUsingEvent args)
{
if (!_cartridgeLoader.TryGetProgram<NanoTaskCartridgeComponent>(ent.Owner, out var uid, out var program))
{
return;
}
if (!EntityManager.TryGetComponent<NanoTaskPrintedComponent>(args.Used, out var printed))
{
return;
}
if (printed.Task is NanoTaskItem item)
{
program.Tasks.Add(new(program.Counter++, printed.Task));
args.Handled = true;
EntityManager.DeleteEntity(args.Used);
UpdateUiState(new Entity<NanoTaskCartridgeComponent>(uid.Value, program), ent.Owner);
}
}
/// <summary>
/// This gets called when the ui fragment needs to be updated for the first time after activating
/// </summary>
private void OnUiReady(Entity<NanoTaskCartridgeComponent> ent, ref CartridgeUiReadyEvent args)
{
UpdateUiState(ent, args.Loader);
}
private void SetupPrintedTask(EntityUid uid, NanoTaskItem item)
{
PaperComponent? paper = null;
NanoTaskPrintedComponent? printed = null;
if (!Resolve(uid, ref paper, ref printed))
return;
printed.Task = item;
var msg = new FormattedMessage();
msg.AddText(Loc.GetString("nano-task-printed-description", ("description", item.Description)));
msg.PushNewline();
msg.AddText(Loc.GetString("nano-task-printed-requester", ("requester", item.TaskIsFor)));
msg.PushNewline();
msg.AddText(item.Priority switch {
NanoTaskPriority.High => Loc.GetString("nano-task-printed-high-priority"),
NanoTaskPriority.Medium => Loc.GetString("nano-task-printed-medium-priority"),
NanoTaskPriority.Low => Loc.GetString("nano-task-printed-low-priority"),
_ => "",
});
_paper.SetContent((uid, paper), msg.ToMarkup());
}
/// <summary>
/// The ui messages received here get wrapped by a CartridgeMessageEvent and are relayed from the <see cref="CartridgeLoaderSystem"/>
/// </summary>
/// <remarks>
/// The cartridge specific ui message event needs to inherit from the CartridgeMessageEvent
/// </remarks>
private void OnUiMessage(Entity<NanoTaskCartridgeComponent> ent, ref CartridgeMessageEvent args)
{
if (args is not NanoTaskUiMessageEvent message)
return;
switch (message.Payload)
{
case NanoTaskAddTask task:
if (!task.Item.Validate())
return;
ent.Comp.Tasks.Add(new(ent.Comp.Counter++, task.Item));
break;
case NanoTaskUpdateTask task:
{
if (!task.Item.Data.Validate())
return;
var idx = ent.Comp.Tasks.FindIndex(t => t.Id == task.Item.Id);
if (idx != -1)
ent.Comp.Tasks[idx] = task.Item;
break;
}
case NanoTaskDeleteTask task:
ent.Comp.Tasks.RemoveAll(t => t.Id == task.Id);
break;
case NanoTaskPrintTask task:
{
if (!task.Item.Validate())
return;
if (_timing.CurTime < ent.Comp.NextPrintAllowedAfter)
return;
ent.Comp.NextPrintAllowedAfter = _timing.CurTime + ent.Comp.PrintDelay;
var printed = Spawn("PaperNanoTaskItem", Transform(message.Actor).Coordinates);
_hands.PickupOrDrop(message.Actor, printed);
_audio.PlayPvs(new SoundPathSpecifier("/Audio/Machines/printer.ogg"), ent.Owner);
SetupPrintedTask(printed, task.Item);
break;
}
}
UpdateUiState(ent, GetEntity(args.LoaderUid));
}
private void UpdateUiState(Entity<NanoTaskCartridgeComponent> ent, EntityUid loaderUid)
{
var state = new NanoTaskUiState(ent.Comp.Tasks);
_cartridgeLoader.UpdateCartridgeUiState(loaderUid, state);
}
}

View File

@@ -247,7 +247,7 @@ internal sealed partial class ChatManager : IChatManager
Color? colorOverride = null;
var wrappedMessage = Loc.GetString("chat-manager-send-ooc-wrap-message", ("playerName",player.Name), ("message", FormattedMessage.EscapeText(message)));
if (_adminManager.HasAdminFlag(player, AdminFlags.Admin))
if (_adminManager.HasAdminFlag(player, AdminFlags.NameColor))
{
var prefs = _preferencesManager.GetPreferences(player.UserId);
colorOverride = prefs.AdminOOCColor;

View File

@@ -34,6 +34,10 @@ public sealed class SpeakOnTriggerSystem : EntitySystem
return;
var message = Loc.GetString(_random.Pick(messagePack.Values));
// Chatcode moment: messages starting with "." are considered radio messages.
// Prepending ">" forces the message to be spoken instead.
// TODO chat refactor: remove this
message = '>' + message;
_chat.TrySendInGameICMessage(ent.Owner, message, InGameICChatType.Speak, true);
if (useDelay != null)

View File

@@ -0,0 +1,83 @@
using Content.Server.Forensics;
using Content.Shared.Cloning.Events;
using Content.Shared.Clothing.Components;
using Content.Shared.FixedPoint;
using Content.Shared.Labels.Components;
using Content.Shared.Labels.EntitySystems;
using Content.Shared.Paper;
using Content.Shared.Stacks;
using Content.Shared.Store;
using Content.Shared.Store.Components;
using Robust.Shared.Prototypes;
namespace Content.Server.Cloning;
/// <summary>
/// The part of item cloning responsible for copying over important components.
/// This is used for <see cref="CopyItem"/>.
/// Anything not copied over here gets reverted to the values the item had in its prototype.
/// </summary>
/// <remarks>
/// This method of copying items is of course not perfect as we cannot clone every single component, which would be pretty much impossible with our ECS.
/// We only consider the most important components so the paradox clone gets similar equipment.
/// This method of using subscriptions was chosen to make it easy for forks to add their own custom components that need to be copied.
/// </remarks>
public sealed partial class CloningSystem : EntitySystem
{
[Dependency] private readonly SharedStackSystem _stack = default!;
[Dependency] private readonly SharedLabelSystem _label = default!;
[Dependency] private readonly ForensicsSystem _forensics = default!;
[Dependency] private readonly PaperSystem _paper = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<StackComponent, CloningItemEvent>(OnCloneStack);
SubscribeLocalEvent<LabelComponent, CloningItemEvent>(OnCloneLabel);
SubscribeLocalEvent<PaperComponent, CloningItemEvent>(OnClonePaper);
SubscribeLocalEvent<ForensicsComponent, CloningItemEvent>(OnCloneForensics);
SubscribeLocalEvent<StoreComponent, CloningItemEvent>(OnCloneStore);
}
private void OnCloneStack(Entity<StackComponent> ent, ref CloningItemEvent args)
{
// if the clone is a stack as well, adjust the count of the copy
if (TryComp<StackComponent>(args.CloneUid, out var cloneStackComp))
_stack.SetCount(args.CloneUid, ent.Comp.Count, cloneStackComp);
}
private void OnCloneLabel(Entity<LabelComponent> ent, ref CloningItemEvent args)
{
// copy the label
_label.Label(args.CloneUid, ent.Comp.CurrentLabel);
}
private void OnClonePaper(Entity<PaperComponent> ent, ref CloningItemEvent args)
{
// copy the text and any stamps
if (TryComp<PaperComponent>(args.CloneUid, out var clonePaperComp))
{
_paper.SetContent((args.CloneUid, clonePaperComp), ent.Comp.Content);
_paper.CopyStamps(ent.AsNullable(), (args.CloneUid, clonePaperComp));
}
}
private void OnCloneForensics(Entity<ForensicsComponent> ent, ref CloningItemEvent args)
{
// copy any forensics to the cloned item
_forensics.CopyForensicsFrom(ent.Comp, args.CloneUid);
}
private void OnCloneStore(Entity<StoreComponent> ent, ref CloningItemEvent args)
{
// copy the current amount of currency in the store
// at the moment this takes care of uplink implants and the portable nukie uplinks
// turning a copied pda into an uplink will need some refactoring first
if (TryComp<StoreComponent>(args.CloneUid, out var cloneStoreComp))
{
cloneStoreComp.Balance = new Dictionary<ProtoId<CurrencyPrototype>, FixedPoint2>(ent.Comp.Balance);
}
}
}

View File

@@ -5,9 +5,14 @@ using Content.Shared.Cloning.Events;
using Content.Shared.Database;
using Content.Shared.Humanoid;
using Content.Shared.Inventory;
using Content.Shared.Implants;
using Content.Shared.Implants.Components;
using Content.Shared.NameModifier.Components;
using Content.Shared.StatusEffect;
using Content.Shared.Storage;
using Content.Shared.Storage.EntitySystems;
using Content.Shared.Whitelist;
using Robust.Shared.Containers;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
using System.Diagnostics.CodeAnalysis;
@@ -19,7 +24,7 @@ namespace Content.Server.Cloning;
/// System responsible for making a copy of a humanoid's body.
/// For the cloning machines themselves look at CloningPodSystem, CloningConsoleSystem and MedicalScannerSystem instead.
/// </summary>
public sealed class CloningSystem : EntitySystem
public sealed partial class CloningSystem : EntitySystem
{
[Dependency] private readonly IComponentFactory _componentFactory = default!;
[Dependency] private readonly HumanoidAppearanceSystem _humanoidSystem = default!;
@@ -28,6 +33,9 @@ public sealed class CloningSystem : EntitySystem
[Dependency] private readonly IPrototypeManager _prototype = default!;
[Dependency] private readonly EntityWhitelistSystem _whitelist = default!;
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
[Dependency] private readonly SharedContainerSystem _container = default!;
[Dependency] private readonly SharedStorageSystem _storage = default!;
[Dependency] private readonly SharedSubdermalImplantSystem _subdermalImplant = default!;
/// <summary>
/// Spawns a clone of the given humanoid mob at the specified location or in nullspace.
@@ -81,6 +89,15 @@ public sealed class CloningSystem : EntitySystem
if (settings.CopyEquipment != null)
CopyEquipment(original, clone.Value, settings.CopyEquipment.Value, settings.Whitelist, settings.Blacklist);
// Copy storage on the mob itself as well.
// This is needed for slime storage.
if (settings.CopyInternalStorage)
CopyStorage(original, clone.Value, settings.Whitelist, settings.Blacklist);
// copy implants and their storage contents
if (settings.CopyImplants)
CopyImplants(original, clone.Value, settings.CopyInternalStorage, settings.Whitelist, settings.Blacklist);
var originalName = Name(original);
if (TryComp<NameModifierComponent>(original, out var nameModComp)) // if the originals name was modified, use the unmodified name
originalName = nameModComp.BaseName;
@@ -100,24 +117,129 @@ public sealed class CloningSystem : EntitySystem
/// Copies the equipment the original has to the clone.
/// This uses the original prototype of the items, so any changes to components that are done after spawning are lost!
/// </summary>
public void CopyEquipment(EntityUid original, EntityUid clone, SlotFlags slotFlags, EntityWhitelist? whitelist = null, EntityWhitelist? blacklist = null)
public void CopyEquipment(Entity<InventoryComponent?> original, Entity<InventoryComponent?> clone, SlotFlags slotFlags, EntityWhitelist? whitelist = null, EntityWhitelist? blacklist = null)
{
if (!TryComp<InventoryComponent>(original, out var originalInventory) || !TryComp<InventoryComponent>(clone, out var cloneInventory))
if (!Resolve(original, ref original.Comp) || !Resolve(clone, ref clone.Comp))
return;
var coords = Transform(clone).Coordinates;
// Iterate over all inventory slots
var slotEnumerator = _inventory.GetSlotEnumerator((original, originalInventory), slotFlags);
var slotEnumerator = _inventory.GetSlotEnumerator(original, slotFlags);
while (slotEnumerator.NextItem(out var item, out var slot))
{
// Spawn a copy of the item using the original prototype.
// This means any changes done to the item after spawning will be reset, but that should not be a problem for simple items like clothing etc.
// we use a whitelist and blacklist to be sure to exclude any problematic entities
var cloneItem = CopyItem(item, coords, whitelist, blacklist);
if (_whitelist.IsWhitelistFail(whitelist, item) || _whitelist.IsBlacklistPass(blacklist, item))
continue;
var prototype = MetaData(item).EntityPrototype;
if (prototype != null)
_inventory.SpawnItemInSlot(clone, slot.Name, prototype.ID, silent: true, inventory: cloneInventory);
if (cloneItem != null && !_inventory.TryEquip(clone, cloneItem.Value, slot.Name, silent: true, inventory: clone.Comp))
Del(cloneItem); // delete it again if the clone cannot equip it
}
}
/// <summary>
/// Copies an item and its storage recursively, placing all items at the same position in grid storage.
/// This uses the original prototype of the items, so any changes to components that are done after spawning are lost!
/// </summary>
/// <remarks>
/// This is not perfect and only considers item in storage containers.
/// Some components have their own additional spawn logic on map init, so we cannot just copy all containers.
/// </remarks>
public EntityUid? CopyItem(EntityUid original, EntityCoordinates coords, EntityWhitelist? whitelist = null, EntityWhitelist? blacklist = null)
{
// we use a whitelist and blacklist to be sure to exclude any problematic entities
if (!_whitelist.CheckBoth(original, blacklist, whitelist))
return null;
var prototype = MetaData(original).EntityPrototype?.ID;
if (prototype == null)
return null;
var spawned = EntityManager.SpawnAtPosition(prototype, coords);
// copy over important component data
var ev = new CloningItemEvent(spawned);
RaiseLocalEvent(original, ref ev);
// if the original has items inside its storage, copy those as well
if (TryComp<StorageComponent>(original, out var originalStorage) && TryComp<StorageComponent>(spawned, out var spawnedStorage))
{
// remove all items that spawned with the entity inside its storage
// this ignores other containers, but this should be good enough for our purposes
_container.CleanContainer(spawnedStorage.Container);
// recursively replace them
// surely no one will ever create two items that contain each other causing an infinite loop, right?
foreach ((var itemUid, var itemLocation) in originalStorage.StoredItems)
{
var copy = CopyItem(itemUid, coords, whitelist, blacklist);
if (copy != null)
_storage.InsertAt((spawned, spawnedStorage), copy.Value, itemLocation, out _, playSound: false);
}
}
return spawned;
}
/// <summary>
/// Copies an item's storage recursively to another storage.
/// The storage grids should have the same shape or it will drop on the floor.
/// Basically the same as CopyItem, but we don't copy the outermost container.
/// </summary>
public void CopyStorage(Entity<StorageComponent?> original, Entity<StorageComponent?> target, EntityWhitelist? whitelist = null, EntityWhitelist? blacklist = null)
{
if (!Resolve(original, ref original.Comp, false) || !Resolve(target, ref target.Comp, false))
return;
var coords = Transform(target).Coordinates;
// delete all items in the target storage
_container.CleanContainer(target.Comp.Container);
// recursively replace them
foreach ((var itemUid, var itemLocation) in original.Comp.StoredItems)
{
var copy = CopyItem(itemUid, coords, whitelist, blacklist);
if (copy != null)
_storage.InsertAt(target, copy.Value, itemLocation, out _, playSound: false);
}
}
/// <summary>
/// Copies all implants from one mob to another.
/// Might result in duplicates if the target already has them.
/// Can copy the storage inside a storage implant according to a whitelist and blacklist.
/// </summary>
/// <param name="original">Entity to copy implants from.</param>
/// <param name="target">Entity to copy implants to.</param>
/// <param name="copyStorage">If true will copy storage of the implants (E.g storage implant)</param>
/// <param name="whitelist">Whitelist for the storage copy (If copyStorage is true)</param>
/// <param name="blacklist">Blacklist for the storage copy (If copyStorage is true)</param>
public void CopyImplants(Entity<ImplantedComponent?> original, EntityUid target, bool copyStorage = false, EntityWhitelist? whitelist = null, EntityWhitelist? blacklist = null)
{
if (!Resolve(original, ref original.Comp, false))
return; // they don't have any implants to copy!
foreach (var originalImplant in original.Comp.ImplantContainer.ContainedEntities)
{
if (!HasComp<SubdermalImplantComponent>(originalImplant))
continue; // not an implant (should only happen with admin shenanigans)
var implantId = MetaData(originalImplant).EntityPrototype?.ID;
if (implantId == null)
continue;
var targetImplant = _subdermalImplant.AddImplant(target, implantId);
if (targetImplant == null)
continue;
// copy over important component data
var ev = new CloningItemEvent(targetImplant.Value);
RaiseLocalEvent(originalImplant, ref ev);
if (copyStorage)
CopyStorage(originalImplant, targetImplant.Value, whitelist, blacklist); // only needed for storage implants
}
}
}

View File

@@ -82,7 +82,7 @@ public sealed class CursedMaskSystem : SharedCursedMaskSystem
{
_mind.TransferTo(ent.Comp.StolenMind.Value, args.Wearer);
_adminLog.Add(LogType.Action,
LogImpact.Extreme,
LogImpact.Medium,
$"{ToPrettyString(args.Wearer):player} was restored to their body after the removal of {ToPrettyString(ent):entity}.");
ent.Comp.StolenMind = null;
}

View File

@@ -312,7 +312,7 @@ namespace Content.Server.Communications
}
_roundEndSystem.RequestRoundEnd(uid);
_adminLogger.Add(LogType.Action, LogImpact.Extreme, $"{ToPrettyString(mob):player} has called the shuttle.");
_adminLogger.Add(LogType.Action, LogImpact.High, $"{ToPrettyString(mob):player} has called the shuttle.");
}
private void OnRecallShuttleMessage(EntityUid uid, CommunicationsConsoleComponent comp, CommunicationsConsoleRecallEmergencyShuttleMessage message)
@@ -327,7 +327,7 @@ namespace Content.Server.Communications
}
_roundEndSystem.CancelRoundEndCountdown(uid);
_adminLogger.Add(LogType.Action, LogImpact.Extreme, $"{ToPrettyString(message.Actor):player} has recalled the shuttle.");
_adminLogger.Add(LogType.Action, LogImpact.High, $"{ToPrettyString(message.Actor):player} has recalled the shuttle.");
}
}

View File

@@ -17,7 +17,7 @@ namespace Content.Server.Connection.Whitelist;
/// Next means the next condition in the list is checked.
/// If the condition doesn't match, the next condition is checked.
/// </summary>
[Prototype("playerConnectionWhitelist")]
[Prototype]
public sealed partial class PlayerConnectionWhitelistPrototype : IPrototype
{
[IdDataField]

View File

@@ -25,7 +25,7 @@ public sealed partial class AdminLog : IGraphAction
var logManager = IoCManager.Resolve<IAdminLogManager>();
if (userUid.HasValue)
logManager.Add(LogType, Impact, $"{Message} - Entity: {entityManager.ToPrettyString(uid):entity}, User: {entityManager.ToPrettyString(userUid.Value):user}");
logManager.Add(LogType, Impact, $"{Message} - Entity: {entityManager.ToPrettyString(uid):entity}, User: {entityManager.ToPrettyString(userUid.Value):player}");
else
logManager.Add(LogType, Impact, $"{Message} - Entity: {entityManager.ToPrettyString(uid):entity}");
}

View File

@@ -225,12 +225,12 @@ namespace Content.Server.Decals
if (eventArgs.SenderSession.AttachedEntity != null)
{
_adminLogger.Add(LogType.CrayonDraw, LogImpact.High,
_adminLogger.Add(LogType.CrayonDraw, LogImpact.Low,
$"{ToPrettyString(eventArgs.SenderSession.AttachedEntity.Value):actor} drew a {ev.Decal.Color} {ev.Decal.Id} at {ev.Coordinates}");
}
else
{
_adminLogger.Add(LogType.CrayonDraw, LogImpact.High,
_adminLogger.Add(LogType.CrayonDraw, LogImpact.Low,
$"{eventArgs.SenderSession.Name} drew a {ev.Decal.Color} {ev.Decal.Id} at {ev.Coordinates}");
}
}
@@ -259,12 +259,12 @@ namespace Content.Server.Decals
{
if (eventArgs.SenderSession.AttachedEntity != null)
{
_adminLogger.Add(LogType.CrayonDraw, LogImpact.High,
_adminLogger.Add(LogType.CrayonDraw, LogImpact.Low,
$"{ToPrettyString(eventArgs.SenderSession.AttachedEntity.Value):actor} removed a {decal.Color} {decal.Id} at {ev.Coordinates}");
}
else
{
_adminLogger.Add(LogType.CrayonDraw, LogImpact.High,
_adminLogger.Add(LogType.CrayonDraw, LogImpact.Low,
$"{eventArgs.SenderSession.Name} removed a {decal.Color} {decal.Id} at {ev.Coordinates}");
}

View File

@@ -21,6 +21,8 @@ using Robust.Shared.Containers;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using System.Linq;
using Content.Shared.Humanoid;
using Robust.Shared.Player;
namespace Content.Server.Destructible
{
@@ -61,9 +63,12 @@ namespace Content.Server.Destructible
{
RaiseLocalEvent(uid, new DamageThresholdReached(component, threshold), true);
var logImpact = LogImpact.Low;
// Convert behaviors into string for logs
var triggeredBehaviors = string.Join(", ", threshold.Behaviors.Select(b =>
{
if (logImpact <= b.Impact)
logImpact = b.Impact;
if (b is DoActsBehavior doActsBehavior)
{
return $"{b.GetType().Name}:{doActsBehavior.Acts.ToString()}";
@@ -71,14 +76,20 @@ namespace Content.Server.Destructible
return b.GetType().Name;
}));
// If it doesn't have a humanoid component, it's probably not particularly notable?
if (logImpact > LogImpact.Medium && !HasComp<HumanoidAppearanceComponent>(uid))
logImpact = LogImpact.Medium;
if (args.Origin != null)
{
_adminLogger.Add(LogType.Damaged, LogImpact.Medium,
_adminLogger.Add(LogType.Damaged,
logImpact,
$"{ToPrettyString(args.Origin.Value):actor} caused {ToPrettyString(uid):subject} to trigger [{triggeredBehaviors}]");
}
else
{
_adminLogger.Add(LogType.Damaged, LogImpact.Medium,
_adminLogger.Add(LogType.Damaged,
logImpact,
$"Unknown damage source caused {ToPrettyString(uid):subject} to trigger [{triggeredBehaviors}]");
}

View File

@@ -1,4 +1,5 @@
using Content.Shared.Body.Components;
using Content.Shared.Database;
using JetBrains.Annotations;
namespace Content.Server.Destructible.Thresholds.Behaviors
@@ -9,6 +10,8 @@ namespace Content.Server.Destructible.Thresholds.Behaviors
{
[DataField("recursive")] private bool _recursive = true;
public LogImpact Impact => LogImpact.Extreme;
public void Execute(EntityUid owner, DestructibleSystem system, EntityUid? cause = null)
{
if (system.EntityManager.TryGetComponent(owner, out BodyComponent? body))

View File

@@ -1,7 +1,11 @@
namespace Content.Server.Destructible.Thresholds.Behaviors
using Content.Shared.Database;
namespace Content.Server.Destructible.Thresholds.Behaviors
{
public interface IThresholdBehavior
{
public LogImpact Impact => LogImpact.Low;
/// <summary>
/// Executes this behavior.
/// </summary>

View File

@@ -256,11 +256,12 @@ public sealed partial class ExplosionSystem : SharedExplosionSystem
}
else
{
_adminLogger.Add(LogType.Explosion, LogImpact.High,
$"{ToPrettyString(user.Value):user} caused {ToPrettyString(uid):entity} to explode ({typeId}) at Pos:{(posFound ? $"{gridPos:coordinates}" : "[Grid or Map not found]")} with intensity {totalIntensity} slope {slope}");
var alertMinExplosionIntensity = _cfg.GetCVar(CCVars.AdminAlertExplosionMinIntensity);
if (alertMinExplosionIntensity > -1 && totalIntensity >= alertMinExplosionIntensity)
_chat.SendAdminAlert(user.Value, $"caused {ToPrettyString(uid)} to explode ({typeId}:{totalIntensity}) at Pos:{(posFound ? $"{gridPos:coordinates}" : "[Grid or Map not found]")}");
var logImpact = (alertMinExplosionIntensity > -1 && totalIntensity >= alertMinExplosionIntensity)
? LogImpact.Extreme
: LogImpact.High;
_adminLogger.Add(LogType.Explosion, logImpact,
$"{ToPrettyString(user.Value):user} caused {ToPrettyString(uid):entity} to explode ({typeId}) at Pos:{(posFound ? $"{gridPos:coordinates}" : "[Grid or Map not found]")} with intensity {totalIntensity} slope {slope}");
}
}

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