feat: Various Admin QOL features (#5436)

* feat: added lslaws command

* feat: add lswatchlisted command

* tweak: nicer formatting for lswatchlisted

* tweak: nicer formatting for lslaws

* feat: lsobjectives lists everyone's objectives if no user specified

* feat: mark ghosted players in players tab

* docs: add missing DeltaV patch comment

* feat: only alert admins if meatspiking player characters

* feat: always alert about station explosions, don't spam off-station alerts

* feat: stripping alert tweaks

* feat: admin alert on low hour latejoin

* refactor: forgot to make onspawncomplete non-async, wasn't necessary after all

* feat: EORG alerts

* feat: getping command

* refactor: remove unused imports, format

* refactor: early return in onspawncomplete

* feat: add coordinate link to eorg destroy alert

* tweak: cache watchlist data to avoid db calls, use in lswatchlisted

* fix: null-check in OnNoteAdded

* feat: watchlist indicator on admin overlay

* feat: optionally mark watchlisted in f7 players

* feat: refresh admin playerlist data on wl change

* fix: update player list after cache write

* feat: add tp links to prayer messages

* tweak: don't alert when uncuffing self

* tweak: lower uncuff other alert impact if mindshielded

* docs: add deltav comments to imports

* refactor: address review comments

* style: deltav comments on same line as change

* performance: get metadata only if objectives > 0

* fix: actually check user if explosion gridPos not on station

* style: remove extraneous newlines

* refactor: use fancy getOrNew method

* feat: refresh player list UI if marking pref changed

* feat: latejoin alert hours configurable

* fix: also list objectives for non-humanoids and dead people
This commit is contained in:
DisposableCrewmember42 2026-04-04 14:38:32 +00:00 committed by GitHub
parent c703999890
commit 3c0d4ab253
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 687 additions and 24 deletions

View File

@ -196,8 +196,22 @@ internal sealed class AdminNameOverlay : Overlay
// Username
color = Color.Yellow;
color.A = alpha;
args.ScreenHandle.DrawString(_font, screenCoordinates + currentOffset, playerInfo.Username, uiScale, playerInfo.Connected ? color : colorDisconnected);
currentOffset += lineoffset;
var usernameSpace = args.ScreenHandle.DrawString(_font, screenCoordinates + currentOffset, playerInfo.Username, uiScale, playerInfo.Connected ? color : colorDisconnected); // DeltaV - store return value
// DeltaV - add watchlist suffix START
if (playerInfo.Watchlisted)
{
color = Color.Red;
color.A = alpha;
args.ScreenHandle.DrawString(_fontBold,
screenCoordinates + currentOffset + usernameSpace with { Y = 0 },
" " + Loc.GetString("admin-overlay-watchlisted-username-suffix"),
uiScale,
color);
}
// DeltaV - add watchlist suffix END
currentOffset += lineoffset; // DeltaV - moved down from username block
// Playtime
if (!string.IsNullOrEmpty(playerInfo.PlaytimeString) && _overlayPlaytime)

View File

@ -35,6 +35,8 @@ public sealed partial class PlayerTab : Control
private AdminPlayerTabColorOption _playerTabColorSetting;
private AdminPlayerTabRoleTypeOption _playerTabRoleSetting;
private AdminPlayerTabSymbolOption _playerTabSymbolSetting;
private bool _markGhosted; // DeltaV
private bool _markWatchlisted; // DeltaV
public event Action<GUIBoundKeyEventArgs, ListData>? OnEntryKeyBindDown;
@ -51,6 +53,8 @@ public sealed partial class PlayerTab : Control
_config.OnValueChanged(CCVars.AdminPlayerTabRoleSetting, RoleSettingChanged, true);
_config.OnValueChanged(CCVars.AdminPlayerTabColorSetting, ColorSettingChanged, true);
_config.OnValueChanged(CCVars.AdminPlayerTabSymbolSetting, SymbolSettingChanged, true);
_config.OnValueChanged(CCVars.AdminPlayerTabMarkGhosted, MarkGhostedChanged, true); // DeltaV
_config.OnValueChanged(CCVars.AdminPlayerTabMarkWatchlisted, MarkWatchlistedChanged, true); // DeltaV
OverlayButton.OnPressed += OverlayButtonPressed;
ShowDisconnectedButton.OnPressed += ShowDisconnectedPressed;
@ -66,6 +70,20 @@ public sealed partial class PlayerTab : Control
RefreshPlayerList(_adminSystem.PlayerList);
}
// DeltaV - mark ghosted, watchlisted START
private void MarkGhostedChanged(bool value)
{
_markGhosted = value;
RefreshPlayerList(_players);
}
private void MarkWatchlistedChanged(bool value)
{
_markWatchlisted = value;
RefreshPlayerList(_players);
}
// DeltaV - mark ghosted, watchlisted END
#region Antag Overlay
private void OverlayEnabled()
@ -152,8 +170,25 @@ public sealed partial class PlayerTab : Control
UpdateHeaderSymbols();
SearchList.PopulateList(sortedPlayers.Select(info => new PlayerListData(info,
$"{info.Username} {info.CharacterName} {info.IdentityName} {info.StartingJob}"))
SearchList.PopulateList(sortedPlayers.Select(info =>
{
// DeltaV - additions START
var filteringStringAdditions = "";
if (_markGhosted && info.Ghost)
{
filteringStringAdditions += " (G)";
}
if (_markWatchlisted && info.Watchlisted)
{
filteringStringAdditions += " (WL)";
}
// DeltaV - additions END
return new PlayerListData(info,
$"{info.Username} {info.CharacterName} {info.IdentityName} {info.StartingJob}" +
filteringStringAdditions); // DeltaV - append filteringStringAdditions
})
.ToList());
}
@ -167,7 +202,10 @@ public sealed partial class PlayerTab : Control
new StyleBoxFlat(button.Index % 2 == 0 ? _altColor : _defaultColor),
_playerTabColorSetting,
_playerTabRoleSetting,
_playerTabSymbolSetting);
_playerTabSymbolSetting,
_markGhosted, // DeltaV - Add _markGhosted
_markWatchlisted // DeltaV - Add _markWatchlisted
);
button.AddChild(entry);
button.ToolTip = $"{player.Username}, {player.CharacterName}, {player.IdentityName}, {player.StartingJob}";
button.StyleClasses.Clear();

View File

@ -20,7 +20,10 @@ public sealed partial class PlayerTabEntry : PanelContainer
StyleBoxFlat styleBoxFlat,
AdminPlayerTabColorOption colorOption,
AdminPlayerTabRoleTypeOption roleSetting,
AdminPlayerTabSymbolOption symbolSetting)
AdminPlayerTabSymbolOption symbolSetting,
bool markGhosted, // DeltaV - Add markGhosted
bool markWatchlisted // DeltaV - Add markWatchlisted
)
{
IoCManager.InjectDependencies(this);
RobustXamlLoader.Load(this);
@ -73,6 +76,17 @@ public sealed partial class PlayerTabEntry : PanelContainer
if (player.IdentityName != player.CharacterName)
CharacterLabel.Text += $" [{player.IdentityName}]";
// DeltaV - Mark ghosted/watchlisted players START
if (player.Ghost && markGhosted)
{
CharacterLabel.Text = $"(G) {CharacterLabel.Text}";
}
if (player.Watchlisted && markWatchlisted)
{
CharacterLabel.Text = $"(WL) {CharacterLabel.Text}";
}
// DeltaV - Mark ghosted/watchlisted players END
var roletype = RoleTypeLabel.Text = Loc.GetString(rolePrototype?.Name ?? RoleTypePrototype.FallbackName);
var subtype = roles.GetRoleSubtypeLabel(rolePrototype?.Name ?? RoleTypePrototype.FallbackName, player.Subtype);
switch (roleSetting)

View File

@ -9,6 +9,8 @@
<ui:OptionDropDown Name="DropDownPlayerTabSymbolSetting" Title="{Loc 'ui-options-admin-player-tab-symbol-setting'}" />
<ui:OptionDropDown Name="DropDownPlayerTabRoleSetting" Title="{Loc 'ui-options-admin-player-tab-role-setting'}" />
<ui:OptionDropDown Name="DropDownPlayerTabColorSetting" Title="{Loc 'ui-options-admin-player-tab-color-setting'}" />
<CheckBox Name="EnablePlayerTabMarkGhosted" Text="{Loc 'ui-options-admin-player-tab-mark-ghosted'}" ToolTip="{Loc 'ui-options-admin-player-tab-mark-ghosted-tooltip'}" /> <!-- DeltaV - Add MarkGhosted -->
<CheckBox Name="EnablePlayerTabMarkWatchlisted" Text="{Loc 'ui-options-admin-player-tab-mark-watchlisted'}" ToolTip="{Loc 'ui-options-admin-player-tab-mark-watchlisted-tooltip'}" /> <!-- DeltaV - Add MarkWatchlisted -->
<Label Text="{Loc 'ui-options-admin-logs-title'}"
StyleClasses="LabelKeyText"/>
<ui:OptionColorSlider Name="ColorSliderLogsHighlight" Title="{Loc 'ui-options-admin-logs-highlight-color'}" />

View File

@ -56,6 +56,8 @@ public sealed partial class AdminOptionsTab : Control
Control.AddOptionDropDown(CCVars.AdminPlayerTabSymbolSetting, DropDownPlayerTabSymbolSetting, playerTabSymbolSettings);
Control.AddOptionDropDown(CCVars.AdminPlayerTabRoleSetting, DropDownPlayerTabRoleSetting, playerTabRoleSettings);
Control.AddOptionDropDown(CCVars.AdminPlayerTabColorSetting, DropDownPlayerTabColorSetting, playerTabColorSettings);
Control.AddOptionCheckBox(CCVars.AdminPlayerTabMarkGhosted, EnablePlayerTabMarkGhosted); // DeltaV
Control.AddOptionCheckBox(CCVars.AdminPlayerTabMarkWatchlisted, EnablePlayerTabMarkWatchlisted); // DeltaV
Control.AddOptionDropDown(CCVars.AdminOverlayAntagFormat, DropDownOverlayAntagFormat, antagFormats);
Control.AddOptionDropDown(CCVars.AdminOverlaySymbolStyle, DropDownOverlayAntagSymbol, antagSymbolStyles);

View File

@ -495,7 +495,8 @@ public sealed partial class AdminLogManager : SharedAdminLogManager, IAdminLogMa
/// <summary>
/// Creates a list of tpto command links of the given players
/// </summary>
private bool CreateTpLinks(List<(NetEntity NetEnt, string CharacterName)> players, out string outString)
// DeltaV - Make public static
public static bool CreateTpLinks(List<(NetEntity NetEnt, string CharacterName)> players, out string outString)
{
outString = string.Empty;
@ -519,7 +520,8 @@ public sealed partial class AdminLogManager : SharedAdminLogManager, IAdminLogMa
/// <summary>
/// Creates a list of toto command links for the given map coordinates.
/// </summary>
private bool CreateCordLinks(List<MapCoordinates> cords, out string outString)
// DeltaV - Make public static
public static bool CreateCordLinks(List<MapCoordinates> cords, out string outString)
{
outString = string.Empty;
@ -543,7 +545,8 @@ public sealed partial class AdminLogManager : SharedAdminLogManager, IAdminLogMa
/// <summary>
/// Escape the given text to not allow breakouts of the cmdlink tags.
/// </summary>
private string EscapeText(string text)
// DeltaV - Make public static
public static string EscapeText(string text)
{
return FormattedMessage.EscapeText(text).Replace("\"", "\\\"").Replace("'", "\\'");
}

View File

@ -1,12 +1,16 @@
using System.Collections.ObjectModel; // DeltaV - Admin QOL
using System.Linq;
using Content.Server.Administration.Commands;
using Content.Server.Administration.Systems; // DeltaV - Admin QOL
using Content.Server.Chat.Managers;
using Content.Server.EUI;
using Content.Shared.Administration.Notes; // DeltaV - Admin QOL
using Content.Shared.Database;
using Content.Shared.Verbs;
using Robust.Server.Player;
using Robust.Shared.Console;
using Robust.Shared.Enums;
using Robust.Shared.Network; // DeltaV - Admin QOL
using Robust.Shared.Player;
using Robust.Shared.Utility;
@ -19,13 +23,79 @@ public sealed class AdminNotesSystem : EntitySystem
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IChatManager _chat = default!;
[Dependency] private readonly EuiManager _euis = default!;
[Dependency] private readonly AdminSystem _admin = default!; // DeltaV
// DeltaV - watchlist cache defs START
// For use by other systems
// Used by lswatchlisted command to avoid querying database for every connected user every time it's invoked.
public ReadOnlyDictionary<NetUserId, List<SharedAdminNote>> ConnectedPlayerWatchlists => _connectedPlayerWatchlists.AsReadOnly();
private readonly Dictionary<NetUserId, List<SharedAdminNote>> _connectedPlayerWatchlists = new();
// DeltaV - watchlist cache defs END
public override void Initialize()
{
SubscribeLocalEvent<GetVerbsEvent<Verb>>(AddVerbs);
_playerManager.PlayerStatusChanged += OnPlayerStatusChanged;
// DeltaV - track watchlist changes START
_notes.NoteAdded += OnNoteAdded;
_notes.NoteModified += OnNoteModified;
_notes.NoteDeleted += OnNoteDeleted;
// DeltaV - track watchlist changes END
}
// DeltaV - track watchlist changes START
public override void Shutdown()
{
base.Shutdown();
_notes.NoteAdded -= OnNoteAdded;
_notes.NoteModified -= OnNoteModified;
_notes.NoteDeleted -= OnNoteDeleted;
}
private void OnNoteAdded(SharedAdminNote note)
{
if (note.NoteType != NoteType.Watchlist)
return;
var notes = _connectedPlayerWatchlists.GetOrNew(note.Player);
notes.Add(note);
if (_playerManager.TryGetSessionById(note.Player, out var session))
_admin.UpdatePlayerList(session);
}
private void OnNoteModified(SharedAdminNote note)
{
if (note.NoteType != NoteType.Watchlist)
return;
var modifiedIndex = _connectedPlayerWatchlists[note.Player].FindIndex(n => n.Id == note.Id);
if (modifiedIndex != -1)
_connectedPlayerWatchlists[note.Player][modifiedIndex] = note;
if (_playerManager.TryGetSessionById(note.Player, out var session))
_admin.UpdatePlayerList(session);
}
private void OnNoteDeleted(SharedAdminNote note)
{
if (note.NoteType != NoteType.Watchlist)
return;
var deletedIndex = _connectedPlayerWatchlists[note.Player].FindIndex(n => n.Id == note.Id);
if (deletedIndex != -1)
_connectedPlayerWatchlists[note.Player].RemoveAt(deletedIndex);
if (_connectedPlayerWatchlists[note.Player].Count == 0)
_connectedPlayerWatchlists.Remove(note.Player);
if (_playerManager.TryGetSessionById(note.Player, out var session))
_admin.UpdatePlayerList(session);
}
// DeltaV - track watchlist changes END
private void AddVerbs(GetVerbsEvent<Verb> ev)
{
if (EntityManager.GetComponentOrNull<ActorComponent>(ev.User) is not {PlayerSession: var user} ||
@ -53,12 +123,25 @@ public sealed class AdminNotesSystem : EntitySystem
private async void OnPlayerStatusChanged(object? sender, SessionStatusEventArgs e)
{
// DeltaV - clear watchlist cache on disconnect START
if (e.NewStatus == SessionStatus.Disconnected)
{
_connectedPlayerWatchlists.Remove(e.Session.UserId);
return;
}
// DeltaV - clear watchlist cache on disconnect END
if (e.NewStatus != SessionStatus.InGame)
return;
var messages = await _notes.GetNewMessages(e.Session.UserId);
var watchlists = await _notes.GetActiveWatchlists(e.Session.UserId);
// DeltaV - write to cache START
if (watchlists.Count != 0)
_connectedPlayerWatchlists[e.Session.UserId] = watchlists.Select(r => r.ToShared()).ToList();
// DeltaV - write to cache END
if (!_playerManager.TryGetPlayerData(e.Session.UserId, out var playerData))
{
Log.Error($"Could not get player data for ID {e.Session.UserId}");

View File

@ -1,5 +1,6 @@
using System.Linq;
using Content.Server.Administration.Managers;
using Content.Server.Administration.Notes; // DeltaV - Admin QOL
using Content.Server.Chat.Managers;
using Content.Server.GameTicking;
using Content.Server.Hands.Systems;
@ -12,6 +13,7 @@ using Content.Shared.Administration.Events;
using Content.Shared.CCVar;
using Content.Shared.Forensics.Components;
using Content.Shared.GameTicking;
using Content.Shared.Ghost; // DeltaV - Admin QOL
using Content.Shared.Hands.Components;
using Content.Shared.IdentityManagement;
using Content.Shared.Inventory;
@ -55,6 +57,7 @@ public sealed class AdminSystem : EntitySystem
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly StationRecordsSystem _stationRecords = default!;
[Dependency] private readonly TransformSystem _transform = default!;
[Dependency] private readonly AdminNotesSystem _notes = default!; // DeltaV
private readonly Dictionary<NetUserId, PlayerInfo> _playerList = new();
@ -220,12 +223,14 @@ public sealed class AdminSystem : EntitySystem
var entityName = string.Empty;
var identityName = string.Empty;
var sortWeight = 0;
var ghost = false; // DeltaV
// Visible (identity) name can be different from real name
if (session?.AttachedEntity != null)
{
entityName = Comp<MetaDataComponent>(session.AttachedEntity.Value).EntityName;
identityName = Identity.Name(session.AttachedEntity.Value, EntityManager);
ghost = HasComp<GhostComponent>(session.AttachedEntity.Value); // DeltaV
}
var antag = false;
@ -252,6 +257,7 @@ public sealed class AdminSystem : EntitySystem
// Connection status and playtime
var connected = session != null && session.Status is SessionStatus.Connected or SessionStatus.InGame;
var watchlisted = connected && _notes.ConnectedPlayerWatchlists.ContainsKey(session!.UserId); // DeltaV
// Start with the last available playtime data
var cachedInfo = GetCachedPlayerInfo(data.UserId);
@ -277,7 +283,10 @@ public sealed class AdminSystem : EntitySystem
data.UserId,
connected,
_roundActivePlayers.Contains(data.UserId),
overallPlaytime);
overallPlaytime,
ghost, // DeltaV - Add ghost
watchlisted // DeltaV - Add watchlisted
);
}
private void OnPanicBunkerChanged(bool enabled)

View File

@ -1,5 +1,6 @@
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Content.Server._DV.Administration; // DeltaV - Admin QOL
using Content.Server.Administration.Logs;
using Content.Server.Atmos.EntitySystems;
using Content.Server.Body.Systems;
@ -83,6 +84,8 @@ namespace Content.Server.Destructible
if (args.Origin != null)
{
RaiseLocalEvent(uid, new EventAlertSystem.BrokenWithOriginEvent(args.Origin.Value), true); // DeltaV
AdminLogger.Add(LogType.Damaged,
logImpact,
$"{ToPrettyString(args.Origin.Value):actor} caused {ToPrettyString(uid):subject} to trigger [{triggeredBehaviors}]");

View File

@ -6,6 +6,7 @@ using Content.Server.Atmos.EntitySystems;
using Content.Server.Destructible;
using Content.Server.NodeContainer.EntitySystems;
using Content.Server.NPC.Pathfinding;
using Content.Server.Station.Systems; // DeltaV - Admin QOL
using Content.Shared.Atmos.Components;
using Content.Shared.Camera;
using Content.Shared.CCVar;
@ -54,6 +55,7 @@ public sealed partial class ExplosionSystem : SharedExplosionSystem
[Dependency] private readonly SharedMapSystem _map = default!;
[Dependency] private readonly FlammableSystem _flammableSystem = default!;
[Dependency] private readonly DestructibleSystem _destructibleSystem = default!;
[Dependency] private readonly StationSystem _stationSystem = default!; // DeltaV
private EntityQuery<FlammableComponent> _flammableQuery;
private EntityQuery<PhysicsComponent> _physicsQuery;
@ -246,19 +248,47 @@ public sealed partial class ExplosionSystem : SharedExplosionSystem
if (!addLog)
return;
// DeltaV - check if on station START
string? stationName = null;
var station = _stationSystem.GetOwningStation(gridPos?.EntityId);
// just in case: is the user on station?
if (station is null && user is not null)
{
station = _stationSystem.GetOwningStation(user);
}
if (station is not null)
{
if (_stationSystem.TryGetNetEntity(station, out var stationNetEnt))
{
stationName = _stationSystem.GetStationNames().Find(x => x.Entity == stationNetEnt).Name;
}
}
// DeltaV - check if on station END
if (user == null)
{
_adminLogger.Add(LogType.Explosion, LogImpact.High,
$"{ToPrettyString(uid):entity} exploded ({typeId}) at Pos:{(posFound ? $"{gridPos:coordinates}" : "[Grid or Map not found]")} with intensity {totalIntensity} slope {slope}");
_adminLogger.Add(LogType.Explosion, station is not null ? LogImpact.Extreme : LogImpact.High, // DeltaV - Set to Extreme if onStation (always alert), add station name if available
$"{ToPrettyString(uid):entity} exploded ({typeId}) at {(stationName != null ? $"{stationName} " : "")}Pos:{(posFound ? $"{gridPos:coordinates}" : "[Grid or Map not found]")} with intensity {totalIntensity} slope {slope}");
}
else
{
var alertMinExplosionIntensity = _cfg.GetCVar(CCVars.AdminAlertExplosionMinIntensity);
var logImpact = (alertMinExplosionIntensity > -1 && totalIntensity >= alertMinExplosionIntensity)
? LogImpact.Extreme
: LogImpact.High;
: LogImpact.Medium; // DeltaV - If false, Medium instead of High
// DeltaV - Set to Extreme if onStation (always alert) START
if (station is not null)
logImpact = LogImpact.Extreme;
// DeltaV - Set to Extreme if onStation (always alert) END
if (posFound)
_adminLogger.Add(LogType.Explosion, logImpact, $"{ToPrettyString(user.Value):user} caused {ToPrettyString(uid):entity} to explode ({typeId}) at Pos:{gridPos:coordinates} with intensity {totalIntensity} slope {slope}");
{
_adminLogger.Add(LogType.Explosion,
logImpact,
$"{ToPrettyString(user.Value):user} caused {ToPrettyString(uid):entity} to explode ({typeId}) at {(stationName != null ? $"{stationName} " : "")}Pos:{gridPos:coordinates} with intensity {totalIntensity} slope {slope}"); // DeltaV - Add stationName to log message if available
}
else
_adminLogger.Add(LogType.Explosion, logImpact, $"{ToPrettyString(user.Value):user} caused {ToPrettyString(uid):entity} to explode ({typeId}) at Pos:[Grid or Map not found] with intensity {totalIntensity} slope {slope}");
}

View File

@ -3,6 +3,7 @@ using Content.Server.Administration;
using Content.Shared.Administration;
using Content.Shared.Mind;
using Content.Shared.Objectives.Systems;
using Content.Shared.Players;
using Robust.Server.Player;
using Robust.Shared.Console;
using Robust.Shared.Player;
@ -23,7 +24,14 @@ namespace Content.Server.Objectives.Commands
if (args.Length > 0)
_players.TryGetSessionByUsername(args[0], out player);
else
player = shell.Player;
{
// DeltaV - Print everyone's objectives START
// previously: player = shell.Player
// implemented like this to make it easier to rip out later
ListAllObjectives(shell);
return;
// DeltaV - Print everyone's objectives END
}
if (player == null)
{
@ -62,6 +70,45 @@ namespace Content.Server.Objectives.Commands
}
}
// DeltaV - Added function START
private void ListAllObjectives(IConsoleShell shell)
{
var minds = _entities.EntityQueryEnumerator<MindComponent>();
while (minds.MoveNext(out var mindId, out var mindComp))
{
ICommonSession? player = null;
if (mindComp.OwnedEntity is not null)
{
_players.TryGetSessionByEntity(mindComp.OwnedEntity.Value, out player);
}
if (mindComp.Objectives.Count > 0)
{
_entities.TryGetComponent<MetaDataComponent>(mindComp.OwnedEntity, out var metaData);
shell.WriteMarkup($"\n[bold]{metaData?.EntityName}[/bold] ({player?.Name ?? "No player attached?"})");
var objectivesSystem = _entities.System<SharedObjectivesSystem>();
var objectives = mindComp.Objectives;
for (var i = 0; i < objectives.Count; i++)
{
var info = objectivesSystem.GetInfo(objectives[i], mindId);
if (info == null)
{
shell.WriteLine($"- [{i}] {objectives[i]} - INVALID");
}
else
{
var progress = (int) (info.Value.Progress * 100f);
shell.WriteLine($"- [{i}] {objectives[i]} ({info.Value.Title}) ({progress}%)");
}
}
}
}
}
// DeltaV - Added function END
public override CompletionResult GetCompletion(IConsoleShell shell, string[] args)
{
if (args.Length == 1)

View File

@ -1,3 +1,4 @@
using Content.Server._DV.Administration; // DeltaV - Admin QOL
using Content.Server.Administration;
using Content.Server.Administration.Logs;
using Content.Server.Bible.Components;
@ -24,6 +25,7 @@ public sealed class PrayerSystem : EntitySystem
[Dependency] private readonly PopupSystem _popupSystem = default!;
[Dependency] private readonly IChatManager _chatManager = default!;
[Dependency] private readonly QuickDialogSystem _quickDialog = default!;
[Dependency] private readonly EventAlertSystem _eventAlert = default!; // DeltaV
public override void Initialize()
{
@ -105,6 +107,7 @@ public sealed class PrayerSystem : EntitySystem
_popupSystem.PopupEntity(Loc.GetString(comp.SentMessage), sender.AttachedEntity.Value, sender, PopupType.Medium);
_chatManager.SendAdminAnnouncement($"{Loc.GetString(comp.NotificationPrefix)} <{sender.Name}>: {message}");
_eventAlert.SendLinks(sender.AttachedEntity); // DeltaV - send tp links for prayers
_adminLogger.Add(LogType.AdminMessage, LogImpact.Low, $"{ToPrettyString(sender.AttachedEntity.Value):player} sent prayer ({Loc.GetString(comp.NotificationPrefix)}): {message}");
}
}

View File

@ -0,0 +1,32 @@
using Content.Server.Administration;
using Content.Shared.Administration;
using Robust.Server.Player;
using Robust.Shared.Console;
namespace Content.Server._DV.Administration.Commands;
[AdminCommand(AdminFlags.Admin)]
public sealed class GetPingCommand : LocalizedEntityCommands
{
[Dependency] private readonly IPlayerManager _player = default!;
public override string Command => "getping";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (args.Length != 1 || !_player.TryGetSessionByUsername(args[0], out var session))
{
shell.WriteError(Loc.GetString("cmd-getping-err"));
return;
}
shell.WriteLine($"{session.Name}'s ping: {session.Ping}");
}
public override CompletionResult GetCompletion(IConsoleShell shell, string[] args)
{
return args.Length == 1
? CompletionResult.FromHintOptions(CompletionHelper.SessionNames(), "username")
: CompletionResult.Empty;
}
}

View File

@ -0,0 +1,82 @@
using Content.Server.Administration;
using Content.Shared.Administration;
using Content.Shared.Silicons.Laws.Components;
using Robust.Server.Player;
using Robust.Shared.Console;
namespace Content.Server._DV.Administration.Commands;
[AdminCommand(AdminFlags.Admin)]
public sealed class ListLawsCommand : LocalizedEntityCommands
{
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IPlayerManager _player = default!;
public override string Command => "lslaws";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (args.Length > 1)
{
shell.WriteLine(Help);
return;
}
switch (args.Length)
{
case 0:
foreach (var (ent, lawProvider) in _entityManager.AllEntities<SiliconLawProviderComponent>())
{
WriteLawReport(shell, ent, lawProvider);
}
break;
case 1:
if (!_player.TryGetSessionByUsername(args[0], out var session) ||
!_entityManager.TryGetComponent<SiliconLawProviderComponent>(session.AttachedEntity,
out var provider))
{
shell.WriteError(Loc.GetString("cmd-lslaws-error-bad-player"));
return;
}
WriteLawReport(shell, session.AttachedEntity.Value, provider);
break;
default:
shell.WriteLine(Help);
break;
}
}
private void WriteLawReport(IConsoleShell shell, EntityUid ent, SiliconLawProviderComponent lawProvider)
{
shell.WriteLine("");
_entityManager.TryGetComponent<MetaDataComponent>(ent, out var metaData);
var entityName = metaData?.EntityName;
shell.WriteMarkup(_player.TryGetSessionByEntity(ent, out var session)
? $"[bold]{entityName}[/bold] ({ent.Id}, [color=red]{session.Name}[/color], subverted: {lawProvider.Subverted})"
: $"[bold]{entityName}[/bold] ({ent.Id}, subverted: {lawProvider.Subverted})");
shell.WriteLine($"Base Lawset: {lawProvider.Laws.Id}");
if (lawProvider.Lawset is { } lawset)
{
foreach (var siliconLaw in lawset.Laws)
{
shell.WriteLine($"{siliconLaw.Order}: {Loc.GetString(siliconLaw.LawString)}");
}
}
else
{
shell.WriteLine("Unable to retrieve laws.");
}
}
public override CompletionResult GetCompletion(IConsoleShell shell, string[] args)
{
return args.Length == 1
? CompletionResult.FromOptions(CompletionHelper.SessionNames())
: CompletionResult.Empty;
}
}

View File

@ -0,0 +1,54 @@
using System.Globalization;
using Content.Server.Administration;
using Content.Server.Administration.Notes;
using Content.Shared.Administration;
using Robust.Server.Player;
using Robust.Shared.Console;
namespace Content.Server._DV.Administration.Commands;
[AdminCommand(AdminFlags.Admin)]
public sealed class ListWatchlistedCommand : LocalizedEntityCommands
{
[Dependency] private readonly IPlayerManager _player = default!;
[Dependency] private readonly AdminNotesSystem _notes = default!;
public override string Command => "lswatchlisted";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (_notes.ConnectedPlayerWatchlists.Count == 0)
{
shell.WriteLine("No watchlisted players online.");
return;
}
foreach (var (playerId, records) in _notes.ConnectedPlayerWatchlists)
{
if (!_player.TryGetSessionById(playerId, out var sessionData))
return;
shell.WriteMarkup($"\n[bold]{sessionData.Name}[/bold]\n");
foreach (var record in records)
{
shell.WriteLine("");
record.CreatedAt.Deconstruct(out var date, out _);
shell.WriteLine($"Created: {date.ToString("O", CultureInfo.InvariantCulture)}");
if (record.ExpiryTime is { } expirationTime)
{
expirationTime.Deconstruct(out var expirationDate, out _);
shell.WriteLine($"Expires: {expirationDate.ToString("O", CultureInfo.InvariantCulture)}");
}
else
{
shell.WriteLine("Expires: PERMANENT");
}
foreach (var line in record.Message.Split('\n', StringSplitOptions.TrimEntries))
{
shell.WriteLine($"> {line}");
}
}
}
}
}

View File

@ -0,0 +1,153 @@
using Content.Server.Administration.Logs;
using Content.Server.Chat.Managers;
using Content.Server.Destructible;
using Content.Server.GameTicking;
using Content.Server.Players.PlayTimeTracking;
using Content.Shared._DV.CCVars;
using Content.Shared.GameTicking;
using Content.Shared.Trigger;
using Content.Shared.Trigger.Components;
using Content.Shared.Weapons.Melee.Events;
using Robust.Server.GameObjects;
using Robust.Shared.Configuration;
using Robust.Shared.Map;
using Robust.Shared.Player;
namespace Content.Server._DV.Administration;
public sealed class EventAlertSystem : EntitySystem
{
[Dependency] private readonly PlayTimeTrackingManager _playTime = default!;
[Dependency] private readonly IChatManager _chat = default!;
[Dependency] private readonly IConfigurationManager _config = default!;
[Dependency] private readonly IEntityManager _entity = default!;
[Dependency] private readonly GameTicker _gameTicker = default!;
[Dependency] private readonly TransformSystem _transform = default!;
private double _lateJoinAlertMaxHours;
private HashSet<EntityUid> _eorgAlerted = new();
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<PlayerSpawnCompleteEvent>(OnSpawnComplete);
SubscribeLocalEvent<DestructibleComponent, BrokenWithOriginEvent>(OnBrokenWithOrigin);
SubscribeLocalEvent<TimerTriggerComponent, ActiveTimerTriggerEvent>(OnTimerTrigger);
SubscribeLocalEvent<GameRunLevelChangedEvent>(OnRunLevelChanged);
SubscribeLocalEvent<ActorComponent, AttackedEvent>(OnPlayerAttacked);
_config.OnValueChanged(DCCVars.LateJoinAlertMaxHours, OnMaxHoursCvarChanged, true); // DeltaV
}
private void OnMaxHoursCvarChanged(double hours)
{
_lateJoinAlertMaxHours = hours;
}
private void OnTimerTrigger(Entity<TimerTriggerComponent> ent, ref ActiveTimerTriggerEvent args)
{
if (_gameTicker.RunLevel != GameRunLevel.PostRound || args.User is not { } user)
return;
if (_eorgAlerted.Add(user))
{
AlertWithLink(
$"[EORG] {ToPrettyString(user):player} activated timer trigger of {ToPrettyString(ent):item}.",
user);
}
}
// Alert if player starts destroying stuff at EOR.
// Only sent if it's the first instance of possible EORG for that player to avoid spam.
private void OnBrokenWithOrigin(Entity<DestructibleComponent> target, ref BrokenWithOriginEvent ev)
{
if (_gameTicker.RunLevel != GameRunLevel.PostRound || !HasComp<ActorComponent>(ev.Origin))
return;
if (_eorgAlerted.Add(ev.Origin))
{
AlertWithLink($"[EORG] {ToPrettyString(ev.Origin):player} destroyed {ToPrettyString(target):entity}.",
ev.Origin);
}
}
// Alert if player starts attacking others at EOR.
// Only sent if it's the first instance of possible EORG for that player to avoid spam.
private void OnPlayerAttacked(Entity<ActorComponent> targetPlayer, ref AttackedEvent ev)
{
if (_gameTicker.RunLevel != GameRunLevel.PostRound)
return;
if (_eorgAlerted.Add(ev.User))
{
AlertWithLink(
$"[EORG] {ToPrettyString(ev.User)} attacked {ToPrettyString(targetPlayer):player} using {(ev.Used == ev.User ? "Hands" : ToPrettyString(ev.Used))}.",
ev.User);
}
}
private void OnRunLevelChanged(GameRunLevelChangedEvent ev)
{
_eorgAlerted.Clear();
}
// Alert when overall playtime is lower than this and player latejoins.
// Useful for raiders that first-join and then wait in Lobby for a while to slip in or briefly join to get a bit of playtime.
private void OnSpawnComplete(PlayerSpawnCompleteEvent ev)
{
if (!ev.LateJoin)
return;
var playtimeHours = _playTime.GetOverallPlaytime(ev.Player).TotalHours;
if (playtimeHours < _lateJoinAlertMaxHours)
{
AlertWithLink($"New player {ev.Player.Name} [{playtimeHours:0.#} hours] joined the round.", ev.Mob);
}
}
public void AlertWithLink(string message, EntityUid playerEnt, MapCoordinates? coords = null)
{
_chat.SendAdminAlert(message);
SendLinks(playerEnt, coords);
}
public void SendLinks(EntityUid? playerEnt = null, MapCoordinates? mapCoords = null)
{
List<MapCoordinates> coords = new();
if (playerEnt is not null)
{
var originName = "Actor";
if (TryComp(playerEnt, out MetaDataComponent? meta))
{
originName = meta.EntityName;
}
if (_entity.GetNetEntity(playerEnt) is { } netEnt &&
AdminLogManager.CreateTpLinks([(netEnt, originName)], out var tpLinks))
{
_chat.SendAdminAlertNoFormatOrEscape(tpLinks);
coords.Add(_transform.GetMapCoordinates(playerEnt.Value));
}
}
if (mapCoords is not null && mapCoords.Value != MapCoordinates.Nullspace)
{
coords.Add(mapCoords.Value);
}
if (coords.Count != 0 && AdminLogManager.CreateCordLinks(coords, out var coordLinks))
{
_chat.SendAdminAlertNoFormatOrEscape(coordLinks);
}
}
public sealed class BrokenWithOriginEvent : EntityEventArgs
{
public readonly EntityUid Origin;
public BrokenWithOriginEvent(EntityUid origin)
{
Origin = origin;
}
}
}

View File

@ -19,7 +19,10 @@ public sealed record PlayerInfo(
NetUserId SessionId,
bool Connected,
bool ActiveThisRound,
TimeSpan? OverallPlaytime)
TimeSpan? OverallPlaytime,
bool Ghost, // DeltaV - Add Ghost
bool Watchlisted // DeltaV - Add Watchlisted
)
{
private string? _playtimeString;

View File

@ -108,7 +108,7 @@ public sealed partial class CCVars
/// Minimum explosion intensity to create an admin alert message. -1 to disable the alert.
/// </summary>
public static readonly CVarDef<int> AdminAlertExplosionMinIntensity =
CVarDef.Create("admin.alert.explosion_min_intensity", 60, CVar.SERVERONLY);
CVarDef.Create("admin.alert.explosion_min_intensity", -1, CVar.SERVERONLY); // DeltaV - disable, previously set to 60
/// <summary>
/// Minimum particle accelerator strength to create an admin alert message.

View File

@ -93,6 +93,21 @@ public sealed partial class CCVars
public static readonly CVarDef<string> AdminPlayerTabRoleSetting =
CVarDef.Create("ui.admin_player_tab_role", "Subtype", CVar.CLIENTONLY | CVar.ARCHIVE);
// DeltaV - Additions START
/// <summary>
/// Whether to prepend "(G)" to player character names in the players tab
/// </summary>
public static readonly CVarDef<bool> AdminPlayerTabMarkGhosted =
CVarDef.Create("ui.admin_player_tab_mark_ghosted", true, CVar.CLIENTONLY | CVar.ARCHIVE);
/// <summary>
/// Whether to prepend "(WL)" to player character names in the players tab
/// </summary>
public static readonly CVarDef<bool> AdminPlayerTabMarkWatchlisted =
CVarDef.Create("ui.admin_player_tab_mark_watchlisted", true, CVar.CLIENTONLY | CVar.ARCHIVE);
// DeltaV - Additions END
/// <summary>
/// Determines how antagonist status/roletype is displayed. Based on AdminOverlayAntagSymbolStyles enum
/// Off: No symbol is shown.

View File

@ -21,6 +21,7 @@ using Content.Shared.Inventory;
using Content.Shared.Inventory.Events;
using Content.Shared.Inventory.VirtualItem;
using Content.Shared.Item;
using Content.Shared.Mindshield.Components; // DeltaV - Admin QOL
using Content.Shared.Movement.Events;
using Content.Shared.Movement.Pulling.Events;
using Content.Shared.Popups;
@ -665,7 +666,11 @@ namespace Content.Shared.Cuffs
if (!_doAfter.TryStartDoAfter(doAfterEventArgs))
return;
_adminLog.Add(LogType.Action, LogImpact.High, $"{ToPrettyString(user):player} is trying to uncuff {ToPrettyString(target):subject}");
// DeltaV - Conditional Impact START
var isMindShielded = HasComp<MindShieldComponent>(user);
// Only alert if new user tries to uncuff someone else and if they aren't mindshielded, stops spam during escape attempts or for cadets. LogImpact previously always High.
_adminLog.Add(LogType.Action, isOwner || isMindShielded ? LogImpact.Medium : LogImpact.High, $"{ToPrettyString(user):player} is trying to uncuff {ToPrettyString(target):subject}");
// DeltaV - Conditional impact END
var popupText = user == target.Owner
? "cuffable-component-start-uncuffing-self-observer"
@ -765,7 +770,7 @@ namespace Content.Shared.Cuffs
{
_popup.PopupEntity(Loc.GetString("cuffable-component-remove-cuffs-by-other-success-message",
("otherName", Identity.Name(user.Value, EntityManager, user))), target, target);
_adminLog.Add(LogType.Action, LogImpact.High,
_adminLog.Add(LogType.Action, HasComp<MindShieldComponent>(user) ? LogImpact.Medium : LogImpact.High, // DeltaV - make impact conditional, previously always high
$"{ToPrettyString(user):player} has successfully uncuffed {ToPrettyString(target):player}");
}
else

View File

@ -15,11 +15,13 @@ using Content.Shared.Interaction.Events;
using Content.Shared.Inventory.Events;
using Content.Shared.Item;
using Content.Shared.Kitchen.Components;
using Content.Shared.Mind.Components; // DeltaV - Admin QOL
using Content.Shared.Mobs.Systems;
using Content.Shared.Movement.Events;
using Content.Shared.Nutrition.Components;
using Content.Shared.Popups;
using Content.Shared.Random.Helpers;
using Content.Shared.SSDIndicator; // DeltaV - Admin QOL
using Content.Shared.Throwing;
using Content.Shared.Verbs;
using Robust.Shared.Audio.Systems;
@ -240,12 +242,30 @@ public sealed class SharedKitchenSpikeSystem : EntitySystem
args.Target.Value,
ent);
// normally medium severity, but for humanoids high severity, so new players get relay'd to admin alerts.
var logSeverity = HasComp<HumanoidAppearanceComponent>(args.Target) ? LogImpact.High : LogImpact.Medium;
// DeltaV - Replace logSeverity START
var logSeverity = LogImpact.Medium;
// Extreme impact if SSD indicator comp is present on target (as of writing only regular player characters have it), always alerting
if (HasComp<SSDIndicatorComponent>(args.Target.Value))
{
logSeverity = LogImpact.Extreme;
}
var hasMind = false;
// Extreme impact if a mind is attached to the target, always alerting
if (TryComp<MindContainerComponent>(args.Target.Value, out var mindContainer))
{
if (mindContainer.HasMind)
{
hasMind = true;
logSeverity = LogImpact.Extreme;
}
}
// DeltaV - Replace logSeverity END
_logger.Add(LogType.Action,
logSeverity,
$"{ToPrettyString(args.User):user} put {ToPrettyString(args.Target):target} on the {ToPrettyString(ent):spike}");
$"{ToPrettyString(args.User):user} put {ToPrettyString(args.Target):target}{(hasMind ? " (MIND ATTACHED)" : "")} on the {ToPrettyString(ent):spike}"); // DeltaV - Add hasMind indicator
_audioSystem.PlayPredicted(ent.Comp.SpikeSound, ent, args.User);
}

View File

@ -15,6 +15,7 @@ using Content.Shared.Interaction.Events;
using Content.Shared.Inventory;
using Content.Shared.Inventory.VirtualItem;
using Content.Shared.Popups;
using Content.Shared.SSDIndicator; // DeltaV - Admin QOL
using Content.Shared.Strip.Components;
using Content.Shared.Verbs;
using Robust.Shared.Utility;
@ -36,6 +37,9 @@ public abstract class SharedStrippableSystem : EntitySystem
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
private readonly string[] _highImpactOnStrip = ["id", "belt", "back"]; // DeltaV - high impact on player, key items
private readonly string[] _extremeImpactOnStrip = ["jumpsuit"]; // DeltaV - people shouldn't be stripping each others clothes off
public override void Initialize()
{
base.Initialize();
@ -351,7 +355,29 @@ public abstract class SharedStrippableSystem : EntitySystem
RaiseLocalEvent(item, new DroppedEvent(user), true); // Gas tank internals etc.
_handsSystem.PickupOrDrop(user, item, animateUser: stealth, animate: !stealth);
_adminLogger.Add(LogType.Stripping, LogImpact.High, $"{ToPrettyString(user):actor} has stripped the item {ToPrettyString(item):item} from {ToPrettyString(target):target}'s {slot} slot");
// DeltaV - LogImpact Additions START
// Previously High by default. Stop chat spam from searches in Sec. Somebody with bad intentions is likely to strip from the specified slots.
var logImpact = LogImpact.Medium;
if (_highImpactOnStrip.Contains(slot.ToLower()))
{
logImpact = LogImpact.High;
}
if (_extremeImpactOnStrip.Contains(slot.ToLower()))
{
logImpact = LogImpact.Extreme;
}
var isSsd = false;
if (TryComp<SSDIndicatorComponent>(target, out var ssdIndicator) && ssdIndicator.IsSSD)
{
isSsd = true;
logImpact = LogImpact.Extreme;
}
// DeltaV - LogImpact Additions END
_adminLogger.Add(LogType.Stripping, logImpact, $"{ToPrettyString(user):actor} has stripped the item {ToPrettyString(item):item} from {(isSsd ? "[SSD] " : "")}{ToPrettyString(target):target}'s {slot} slot"); // DeltaV - replace default LogImpact, insert SSD indicator
}
/// <summary>
@ -565,8 +591,8 @@ public abstract class SharedStrippableSystem : EntitySystem
_handsSystem.TryDrop(target, item, checkActionBlocker: false);
_handsSystem.PickupOrDrop(user, item, animateUser: stealth, animate: !stealth, handsComp: user.Comp);
_adminLogger.Add(LogType.Stripping, LogImpact.High, $"{ToPrettyString(user):actor} has stripped the item {ToPrettyString(item):item} from {ToPrettyString(target):target}'s hands");
_adminLogger.Add(LogType.Stripping, LogImpact.Medium, $"{ToPrettyString(user):actor} has stripped the item {ToPrettyString(item):item} from {ToPrettyString(target):target}'s hands"); // DeltaV - Lower LogImpact to Medium, if someone is stripping from hands, the item was probably being offered to them. If not, the target is much more likely to notice.
// Hand update will trigger strippable update.
}

View File

@ -202,6 +202,12 @@ public sealed partial class DCCVars
public static readonly CVarDef<string> DiscordReplyColor =
CVarDef.Create("admin.discord_reply_color", string.Empty, CVar.SERVERONLY);
/// <summary>
/// The maximum amount of hours that will trigger an admin alert on late join.
/// </summary>
public static readonly CVarDef<double> LateJoinAlertMaxHours =
CVarDef.Create("admin.alerts.latejoin_max_hours", 2.0, CVar.SERVERONLY);
/// <summary>
/// Whether or not to disable the preset selecting test rule from running. Should be disabled in production. DeltaV specific, attached to Impstation Secret concurrent feature.
/// </summary>

View File

@ -0,0 +1,18 @@
# Commands
cmd-lslaws-desc = Lists laws of all lawbound entities or a specific player if specified
cmd-lslaws-help = lslaws [username]
cmd-lslaws-error-bad-player = Unable to find lawbound entity attached to that user.
cmd-lswatchlisted-desc = Prints an overview of all connected players with watchlists
cmd-lswatchlisted-help = lswatchlisted
cmd-getping-desc = Prints the specified player's current ping
cmd-getping-help = getping <username>
cmd-getping-err = Unable to find specified player
# UI
ui-options-admin-player-tab-mark-ghosted = Mark ghosted players
ui-options-admin-player-tab-mark-ghosted-tooltip = Ghosts will have a "(G)" added to their character names (e.g. "(G) Glip-Glub")
ui-options-admin-player-tab-mark-watchlisted = Mark watchlisted players
ui-options-admin-player-tab-mark-watchlisted-tooltip = Watchlisted players will have a "(WL)" added to their character names (e.g. "(WL) Confusion Bot 2007")

View File

@ -0,0 +1 @@
admin-overlay-watchlisted-username-suffix = [WL]