Antag Rolebans (#35966)

Co-authored-by: beck-thompson <beck314159@hotmail.com>
Co-authored-by: Hannah Giovanna Dawson <karakkaraz@gmail.com>
This commit is contained in:
Errant
2025-09-17 23:59:07 +02:00
committed by GitHub
parent e1ba33814b
commit b692b6e33e
33 changed files with 898 additions and 283 deletions

View File

@@ -24,7 +24,7 @@ namespace Content.Client.Administration.UI.BanPanel;
[GenerateTypedNameReferences] [GenerateTypedNameReferences]
public sealed partial class BanPanel : DefaultWindow public sealed partial class BanPanel : DefaultWindow
{ {
public event Action<string?, (IPAddress, int)?, bool, ImmutableTypedHwid?, bool, uint, string, NoteSeverity, string[]?, bool>? BanSubmitted; public event Action<Ban>? BanSubmitted;
public event Action<string>? PlayerChanged; public event Action<string>? PlayerChanged;
private string? PlayerUsername { get; set; } private string? PlayerUsername { get; set; }
private (IPAddress, int)? IpAddress { get; set; } private (IPAddress, int)? IpAddress { get; set; }
@@ -37,8 +37,8 @@ public sealed partial class BanPanel : DefaultWindow
// This is less efficient than just holding a reference to the root control and enumerating children, but you // This is less efficient than just holding a reference to the root control and enumerating children, but you
// have to know how the controls are nested, which makes the code more complicated. // have to know how the controls are nested, which makes the code more complicated.
// Role group name -> the role buttons themselves. // Role group name -> the role buttons themselves.
private readonly Dictionary<string, List<Button>> _roleCheckboxes = new(); private readonly Dictionary<string, List<(Button, IPrototype)>> _roleCheckboxes = new();
private readonly ISawmill _banpanelSawmill; private readonly ISawmill _banPanelSawmill;
[Dependency] private readonly IGameTiming _gameTiming = default!; [Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!; [Dependency] private readonly IConfigurationManager _cfg = default!;
@@ -79,7 +79,7 @@ public sealed partial class BanPanel : DefaultWindow
{ {
RobustXamlLoader.Load(this); RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this); IoCManager.InjectDependencies(this);
_banpanelSawmill = _logManager.GetSawmill("admin.banpanel"); _banPanelSawmill = _logManager.GetSawmill("admin.banpanel");
PlayerList.OnSelectionChanged += OnPlayerSelectionChanged; PlayerList.OnSelectionChanged += OnPlayerSelectionChanged;
PlayerNameLine.OnFocusExit += _ => OnPlayerNameChanged(); PlayerNameLine.OnFocusExit += _ => OnPlayerNameChanged();
PlayerCheckbox.OnPressed += _ => PlayerCheckbox.OnPressed += _ =>
@@ -110,7 +110,7 @@ public sealed partial class BanPanel : DefaultWindow
TypeOption.SelectId(args.Id); TypeOption.SelectId(args.Id);
OnTypeChanged(); OnTypeChanged();
}; };
LastConnCheckbox.OnPressed += args => LastConnCheckbox.OnPressed += _ =>
{ {
IpLine.ModulateSelfOverride = null; IpLine.ModulateSelfOverride = null;
HwidLine.ModulateSelfOverride = null; HwidLine.ModulateSelfOverride = null;
@@ -164,7 +164,7 @@ public sealed partial class BanPanel : DefaultWindow
var antagRoles = _protoMan.EnumeratePrototypes<AntagPrototype>() var antagRoles = _protoMan.EnumeratePrototypes<AntagPrototype>()
.OrderBy(x => x.ID); .OrderBy(x => x.ID);
CreateRoleGroup("Antagonist", Color.Red, antagRoles); CreateRoleGroup(AntagPrototype.GroupName, AntagPrototype.GroupColor, antagRoles);
} }
/// <summary> /// <summary>
@@ -236,14 +236,14 @@ public sealed partial class BanPanel : DefaultWindow
{ {
foreach (var role in _roleCheckboxes[groupName]) foreach (var role in _roleCheckboxes[groupName])
{ {
role.Pressed = args.Pressed; role.Item1.Pressed = args.Pressed;
} }
if (args.Pressed) if (args.Pressed)
{ {
if (!Enum.TryParse(_cfg.GetCVar(CCVars.DepartmentBanDefaultSeverity), true, out NoteSeverity newSeverity)) if (!Enum.TryParse(_cfg.GetCVar(CCVars.DepartmentBanDefaultSeverity), true, out NoteSeverity newSeverity))
{ {
_banpanelSawmill _banPanelSawmill
.Warning("Departmental role ban severity could not be parsed from config!"); .Warning("Departmental role ban severity could not be parsed from config!");
return; return;
} }
@@ -255,14 +255,14 @@ public sealed partial class BanPanel : DefaultWindow
{ {
foreach (var button in roleButtons) foreach (var button in roleButtons)
{ {
if (button.Pressed) if (button.Item1.Pressed)
return; return;
} }
} }
if (!Enum.TryParse(_cfg.GetCVar(CCVars.RoleBanDefaultSeverity), true, out NoteSeverity newSeverity)) if (!Enum.TryParse(_cfg.GetCVar(CCVars.RoleBanDefaultSeverity), true, out NoteSeverity newSeverity))
{ {
_banpanelSawmill _banPanelSawmill
.Warning("Role ban severity could not be parsed from config!"); .Warning("Role ban severity could not be parsed from config!");
return; return;
} }
@@ -294,7 +294,7 @@ public sealed partial class BanPanel : DefaultWindow
} }
/// <summary> /// <summary>
/// Adds a checkbutton specifically for one "role" in a "group" /// Adds a check button specifically for one "role" in a "group"
/// E.g. it would add the Chief Medical Officer "role" into the "Medical" group. /// E.g. it would add the Chief Medical Officer "role" into the "Medical" group.
/// </summary> /// </summary>
private void AddRoleCheckbox(string group, string role, GridContainer roleGroupInnerContainer, Button roleGroupCheckbox) private void AddRoleCheckbox(string group, string role, GridContainer roleGroupInnerContainer, Button roleGroupCheckbox)
@@ -302,23 +302,36 @@ public sealed partial class BanPanel : DefaultWindow
var roleCheckboxContainer = new BoxContainer(); var roleCheckboxContainer = new BoxContainer();
var roleCheckButton = new Button var roleCheckButton = new Button
{ {
Name = $"{role}RoleCheckbox", Name = role,
Text = role, Text = role,
ToggleMode = true, ToggleMode = true,
}; };
roleCheckButton.OnToggled += args => roleCheckButton.OnToggled += args =>
{ {
// Checks the role group checkbox if all the children are pressed // Checks the role group checkbox if all the children are pressed
if (args.Pressed && _roleCheckboxes[group].All(e => e.Pressed)) if (args.Pressed && _roleCheckboxes[group].All(e => e.Item1.Pressed))
roleGroupCheckbox.Pressed = args.Pressed; roleGroupCheckbox.Pressed = args.Pressed;
else else
roleGroupCheckbox.Pressed = false; roleGroupCheckbox.Pressed = false;
}; };
IPrototype rolePrototype;
if (_protoMan.TryIndex<JobPrototype>(role, out var jobPrototype))
rolePrototype = jobPrototype;
else if (_protoMan.TryIndex<AntagPrototype>(role, out var antagPrototype))
rolePrototype = antagPrototype;
else
{
_banPanelSawmill.Error($"Adding a role checkbox for role {role}: role is not a JobPrototype or AntagPrototype.");
return;
}
// This is adding the icon before the role name // This is adding the icon before the role name
// TODO: This should not be using raw strings for prototypes as it means it won't be validated at all. // TODO: This should not be using raw strings for prototypes as it means it won't be validated at all.
// I know the ban manager is doing the same thing, but that should not leak into UI code. // // I know the ban manager is doing the same thing, but that should not leak into UI code.
if (_protoMan.TryIndex<JobPrototype>(role, out var jobPrototype) && _protoMan.Resolve(jobPrototype.Icon, out var iconProto)) if (jobPrototype is not null && _protoMan.TryIndex(jobPrototype.Icon, out var iconProto))
{ {
var jobIconTexture = new TextureRect var jobIconTexture = new TextureRect
{ {
@@ -335,7 +348,7 @@ public sealed partial class BanPanel : DefaultWindow
roleGroupInnerContainer.AddChild(roleCheckboxContainer); roleGroupInnerContainer.AddChild(roleCheckboxContainer);
_roleCheckboxes.TryAdd(group, []); _roleCheckboxes.TryAdd(group, []);
_roleCheckboxes[group].Add(roleCheckButton); _roleCheckboxes[group].Add((roleCheckButton, rolePrototype));
} }
public void UpdateBanFlag(bool newFlag) public void UpdateBanFlag(bool newFlag)
@@ -488,7 +501,7 @@ public sealed partial class BanPanel : DefaultWindow
newSeverity = serverSeverity; newSeverity = serverSeverity;
else else
{ {
_banpanelSawmill _banPanelSawmill
.Warning("Server ban severity could not be parsed from config!"); .Warning("Server ban severity could not be parsed from config!");
} }
@@ -501,7 +514,7 @@ public sealed partial class BanPanel : DefaultWindow
} }
else else
{ {
_banpanelSawmill _banPanelSawmill
.Warning("Role ban severity could not be parsed from config!"); .Warning("Role ban severity could not be parsed from config!");
} }
break; break;
@@ -546,34 +559,51 @@ public sealed partial class BanPanel : DefaultWindow
private void SubmitButtonOnOnPressed(BaseButton.ButtonEventArgs obj) private void SubmitButtonOnOnPressed(BaseButton.ButtonEventArgs obj)
{ {
string[]? roles = null; ProtoId<JobPrototype>[]? jobs = null;
ProtoId<AntagPrototype>[]? antags = null;
if (TypeOption.SelectedId == (int) Types.Role) if (TypeOption.SelectedId == (int) Types.Role)
{ {
var rolesList = new List<string>(); var jobList = new List<ProtoId<JobPrototype>>();
var antagList = new List<ProtoId<AntagPrototype>>();
if (_roleCheckboxes.Count == 0) if (_roleCheckboxes.Count == 0)
throw new DebugAssertException("RoleCheckboxes was empty"); throw new DebugAssertException("RoleCheckboxes was empty");
foreach (var button in _roleCheckboxes.Values.SelectMany(departmentButtons => departmentButtons)) foreach (var button in _roleCheckboxes.Values.SelectMany(departmentButtons => departmentButtons))
{ {
if (button is { Pressed: true, Text: not null }) if (button.Item1 is { Pressed: true, Name: not null })
{ {
rolesList.Add(button.Text); switch (button.Item2)
{
case JobPrototype:
jobList.Add(button.Item2.ID);
break;
case AntagPrototype:
antagList.Add(button.Item2.ID);
break;
}
} }
} }
if (rolesList.Count == 0) if (jobList.Count + antagList.Count == 0)
{ {
Tabs.CurrentTab = (int) TabNumbers.Roles; Tabs.CurrentTab = (int) TabNumbers.Roles;
return; return;
} }
roles = rolesList.ToArray(); jobs = jobList.ToArray();
antags = antagList.ToArray();
} }
if (TypeOption.SelectedId == (int) Types.None) if (TypeOption.SelectedId == (int) Types.None)
{ {
TypeOption.ModulateSelfOverride = Color.Red; TypeOption.ModulateSelfOverride = Color.Red;
Tabs.CurrentTab = (int) TabNumbers.BasicInfo; Tabs.CurrentTab = (int) TabNumbers.BasicInfo;
return; return;
} }
@@ -585,6 +615,7 @@ public sealed partial class BanPanel : DefaultWindow
ReasonTextEdit.GrabKeyboardFocus(); ReasonTextEdit.GrabKeyboardFocus();
ReasonTextEdit.ModulateSelfOverride = Color.Red; ReasonTextEdit.ModulateSelfOverride = Color.Red;
ReasonTextEdit.OnKeyBindDown += ResetTextEditor; ReasonTextEdit.OnKeyBindDown += ResetTextEditor;
return; return;
} }
@@ -593,6 +624,7 @@ public sealed partial class BanPanel : DefaultWindow
ButtonResetOn = _gameTiming.CurTime.Add(TimeSpan.FromSeconds(3)); ButtonResetOn = _gameTiming.CurTime.Add(TimeSpan.FromSeconds(3));
SubmitButton.ModulateSelfOverride = Color.Red; SubmitButton.ModulateSelfOverride = Color.Red;
SubmitButton.Text = Loc.GetString("ban-panel-confirm"); SubmitButton.Text = Loc.GetString("ban-panel-confirm");
return; return;
} }
@@ -601,7 +633,22 @@ public sealed partial class BanPanel : DefaultWindow
var useLastHwid = HwidCheckbox.Pressed && LastConnCheckbox.Pressed && Hwid is null; var useLastHwid = HwidCheckbox.Pressed && LastConnCheckbox.Pressed && Hwid is null;
var severity = (NoteSeverity) SeverityOption.SelectedId; var severity = (NoteSeverity) SeverityOption.SelectedId;
var erase = EraseCheckbox.Pressed; var erase = EraseCheckbox.Pressed;
BanSubmitted?.Invoke(player, IpAddress, useLastIp, Hwid, useLastHwid, (uint) (TimeEntered * Multiplier), reason, severity, roles, erase);
var ban = new Ban(
player,
IpAddress,
useLastIp,
Hwid,
useLastHwid,
(uint)(TimeEntered * Multiplier),
reason,
severity,
jobs,
antags,
erase
);
BanSubmitted?.Invoke(ban);
} }
protected override void FrameUpdate(FrameEventArgs args) protected override void FrameUpdate(FrameEventArgs args)

View File

@@ -14,8 +14,7 @@ public sealed class BanPanelEui : BaseEui
{ {
BanPanel = new BanPanel(); BanPanel = new BanPanel();
BanPanel.OnClose += () => SendMessage(new CloseEuiMessage()); BanPanel.OnClose += () => SendMessage(new CloseEuiMessage());
BanPanel.BanSubmitted += (player, ip, useLastIp, hwid, useLastHwid, minutes, reason, severity, roles, erase) BanPanel.BanSubmitted += ban => SendMessage(new BanPanelEuiStateMsg.CreateBanRequest(ban));
=> SendMessage(new BanPanelEuiStateMsg.CreateBanRequest(player, ip, useLastIp, hwid, useLastHwid, minutes, reason, severity, roles, erase));
BanPanel.PlayerChanged += player => SendMessage(new BanPanelEuiStateMsg.GetPlayerInfoRequest(player)); BanPanel.PlayerChanged += player => SendMessage(new BanPanelEuiStateMsg.GetPlayerInfoRequest(player));
} }

View File

@@ -660,8 +660,10 @@ namespace Content.Client.Lobby.UI
selector.Setup(items, title, 250, description, guides: antag.Guides); selector.Setup(items, title, 250, description, guides: antag.Guides);
selector.Select(Profile?.AntagPreferences.Contains(antag.ID) == true ? 0 : 1); selector.Select(Profile?.AntagPreferences.Contains(antag.ID) == true ? 0 : 1);
var requirements = _entManager.System<SharedRoleSystem>().GetAntagRequirement(antag); if (!_requirements.IsAllowed(
if (!_requirements.CheckRoleRequirements(requirements, (HumanoidCharacterProfile?)_preferencesManager.Preferences?.SelectedCharacter, out var reason)) antag,
(HumanoidCharacterProfile?)_preferencesManager.Preferences?.SelectedCharacter,
out var reason))
{ {
selector.LockRequirements(reason); selector.LockRequirements(reason);
Profile = Profile?.WithAntagPreference(antag.ID, false); Profile = Profile?.WithAntagPreference(antag.ID, false);

View File

@@ -1,5 +1,4 @@
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using Content.Client.Lobby;
using Content.Shared.CCVar; using Content.Shared.CCVar;
using Content.Shared.Players; using Content.Shared.Players;
using Content.Shared.Players.JobWhitelist; using Content.Shared.Players.JobWhitelist;
@@ -26,7 +25,8 @@ public sealed class JobRequirementsManager : ISharedPlaytimeManager
[Dependency] private readonly IPrototypeManager _prototypes = default!; [Dependency] private readonly IPrototypeManager _prototypes = default!;
private readonly Dictionary<string, TimeSpan> _roles = new(); private readonly Dictionary<string, TimeSpan> _roles = new();
private readonly List<string> _roleBans = new(); private readonly List<string> _jobBans = new();
private readonly List<string> _antagBans = new();
private readonly List<string> _jobWhitelists = new(); private readonly List<string> _jobWhitelists = new();
private ISawmill _sawmill = default!; private ISawmill _sawmill = default!;
@@ -52,16 +52,19 @@ public sealed class JobRequirementsManager : ISharedPlaytimeManager
// Reset on disconnect, just in case. // Reset on disconnect, just in case.
_roles.Clear(); _roles.Clear();
_jobWhitelists.Clear(); _jobWhitelists.Clear();
_roleBans.Clear(); _jobBans.Clear();
_antagBans.Clear();
} }
} }
private void RxRoleBans(MsgRoleBans message) private void RxRoleBans(MsgRoleBans message)
{ {
_sawmill.Debug($"Received roleban info containing {message.Bans.Count} entries."); _sawmill.Debug($"Received role ban info: {message.JobBans.Count} job ban entries and {message.AntagBans.Count} antag ban entries.");
_roleBans.Clear(); _jobBans.Clear();
_roleBans.AddRange(message.Bans); _jobBans.AddRange(message.JobBans);
_antagBans.Clear();
_antagBans.AddRange(message.AntagBans);
Updated?.Invoke(); Updated?.Invoke();
} }
@@ -90,33 +93,97 @@ public sealed class JobRequirementsManager : ISharedPlaytimeManager
Updated?.Invoke(); Updated?.Invoke();
} }
public bool IsAllowed(JobPrototype job, HumanoidCharacterProfile? profile, [NotNullWhen(false)] out FormattedMessage? reason) /// <summary>
/// Check a list of job- and antag prototypes against the current player, for requirements and bans.
/// </summary>
/// <returns>
/// False if any of the prototypes are banned or have unmet requirements.
/// </returns>>
public bool IsAllowed(
List<ProtoId<JobPrototype>>? jobs,
List<ProtoId<AntagPrototype>>? antags,
HumanoidCharacterProfile? profile,
[NotNullWhen(false)] out FormattedMessage? reason)
{ {
reason = null; reason = null;
if (_roleBans.Contains($"Job:{job.ID}")) if (antags is not null)
{
foreach (var proto in antags)
{
if (!IsAllowed(_prototypes.Index(proto), profile, out reason))
return false;
}
}
if (jobs is not null)
{
foreach (var proto in jobs)
{
if (!IsAllowed(_prototypes.Index(proto), profile, out reason))
return false;
}
}
return true;
}
/// <summary>
/// Check the job prototype against the current player, for requirements and bans
/// </summary>
public bool IsAllowed(
JobPrototype job,
HumanoidCharacterProfile? profile,
[NotNullWhen(false)] out FormattedMessage? reason)
{
// Check the player's bans
if (_jobBans.Contains(job.ID))
{ {
reason = FormattedMessage.FromUnformatted(Loc.GetString("role-ban")); reason = FormattedMessage.FromUnformatted(Loc.GetString("role-ban"));
return false; return false;
} }
// Check whitelist requirements
if (!CheckWhitelist(job, out reason)) if (!CheckWhitelist(job, out reason))
return false; return false;
var player = _playerManager.LocalSession; // Check other role requirements
if (player == null) var reqs = _entManager.System<SharedRoleSystem>().GetRoleRequirements(job);
if (!CheckRoleRequirements(reqs, profile, out reason))
return false;
return true; return true;
return CheckRoleRequirements(job, profile, out reason);
} }
public bool CheckRoleRequirements(JobPrototype job, HumanoidCharacterProfile? profile, [NotNullWhen(false)] out FormattedMessage? reason) /// <summary>
/// Check the antag prototype against the current player, for requirements and bans
/// </summary>
public bool IsAllowed(
AntagPrototype antag,
HumanoidCharacterProfile? profile,
[NotNullWhen(false)] out FormattedMessage? reason)
{ {
var reqs = _entManager.System<SharedRoleSystem>().GetJobRequirement(job); // Check the player's bans
return CheckRoleRequirements(reqs, profile, out reason); if (_antagBans.Contains(antag.ID))
{
reason = FormattedMessage.FromUnformatted(Loc.GetString("role-ban"));
return false;
} }
public bool CheckRoleRequirements(HashSet<JobRequirement>? requirements, HumanoidCharacterProfile? profile, [NotNullWhen(false)] out FormattedMessage? reason) // Check whitelist requirements
if (!CheckWhitelist(antag, out reason))
return false;
// Check other role requirements
var reqs = _entManager.System<SharedRoleSystem>().GetRoleRequirements(antag);
if (!CheckRoleRequirements(reqs, profile, out reason))
return false;
return true;
}
// This must be private so code paths can't accidentally skip requirement overrides. Call this through IsAllowed()
private bool CheckRoleRequirements(HashSet<JobRequirement>? requirements, HumanoidCharacterProfile? profile, [NotNullWhen(false)] out FormattedMessage? reason)
{ {
reason = null; reason = null;
@@ -151,6 +218,15 @@ public sealed class JobRequirementsManager : ISharedPlaytimeManager
return true; return true;
} }
public bool CheckWhitelist(AntagPrototype antag, [NotNullWhen(false)] out FormattedMessage? reason)
{
reason = default;
// TODO: Implement antag whitelisting.
return true;
}
public TimeSpan FetchOverallPlaytime() public TimeSpan FetchOverallPlaytime()
{ {
return _roles.TryGetValue("Overall", out var overallPlaytime) ? overallPlaytime : TimeSpan.Zero; return _roles.TryGetValue("Overall", out var overallPlaytime) ? overallPlaytime : TimeSpan.Zero;

View File

@@ -90,23 +90,25 @@ namespace Content.Client.UserInterface.Systems.Ghost.Controls.Roles
var spriteSystem = sysManager.GetEntitySystem<SpriteSystem>(); var spriteSystem = sysManager.GetEntitySystem<SpriteSystem>();
var requirementsManager = IoCManager.Resolve<JobRequirementsManager>(); var requirementsManager = IoCManager.Resolve<JobRequirementsManager>();
// TODO: role.Requirements value doesn't work at all as an equality key, this must be fixed
// Grouping roles // Grouping roles
var groupedRoles = ghostState.GhostRoles.GroupBy( var groupedRoles = ghostState.GhostRoles.GroupBy(
role => (role.Name, role.Description, role.Requirements)); role => (
role.Name,
role.Description,
// Check the prototypes for role requirements and bans
requirementsManager.IsAllowed(role.RolePrototypes.Item1, role.RolePrototypes.Item2, null, out var reason),
reason));
// Add a new entry for each role group // Add a new entry for each role group
foreach (var group in groupedRoles) foreach (var group in groupedRoles)
{ {
var reason = group.Key.reason;
var name = group.Key.Name; var name = group.Key.Name;
var description = group.Key.Description; var description = group.Key.Description;
var hasAccess = requirementsManager.CheckRoleRequirements( var prototypesAllowed = group.Key.Item3;
group.Key.Requirements,
null,
out var reason);
// Adding a new role // Adding a new role
_window.AddEntry(name, description, hasAccess, reason, group, spriteSystem); _window.AddEntry(name, description, prototypesAllowed, reason, group, spriteSystem);
} }
// Restore the Collapsible box state if it is saved // Restore the Collapsible box state if it is saved

View File

@@ -7,9 +7,7 @@ using Content.Server.EUI;
using Content.Shared.Administration; using Content.Shared.Administration;
using Content.Shared.Database; using Content.Shared.Database;
using Content.Shared.Eui; using Content.Shared.Eui;
using Content.Shared.Roles;
using Robust.Shared.Network; using Robust.Shared.Network;
using Robust.Shared.Prototypes;
namespace Content.Server.Administration; namespace Content.Server.Administration;
@@ -21,7 +19,6 @@ public sealed class BanPanelEui : BaseEui
[Dependency] private readonly IPlayerLocator _playerLocator = default!; [Dependency] private readonly IPlayerLocator _playerLocator = default!;
[Dependency] private readonly IChatManager _chat = default!; [Dependency] private readonly IChatManager _chat = default!;
[Dependency] private readonly IAdminManager _admins = default!; [Dependency] private readonly IAdminManager _admins = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
private readonly ISawmill _sawmill; private readonly ISawmill _sawmill;
@@ -52,7 +49,7 @@ public sealed class BanPanelEui : BaseEui
switch (msg) switch (msg)
{ {
case BanPanelEuiStateMsg.CreateBanRequest r: case BanPanelEuiStateMsg.CreateBanRequest r:
BanPlayer(r.Player, r.IpAddress, r.UseLastIp, r.Hwid, r.UseLastHwid, r.Minutes, r.Severity, r.Reason, r.Roles, r.Erase); BanPlayer(r.Ban);
break; break;
case BanPanelEuiStateMsg.GetPlayerInfoRequest r: case BanPanelEuiStateMsg.GetPlayerInfoRequest r:
ChangePlayer(r.PlayerUsername); ChangePlayer(r.PlayerUsername);
@@ -60,29 +57,26 @@ public sealed class BanPanelEui : BaseEui
} }
} }
private async void BanPlayer(string? target, string? ipAddressString, bool useLastIp, ImmutableTypedHwid? hwid, bool useLastHwid, uint minutes, NoteSeverity severity, string reason, IReadOnlyCollection<string>? roles, bool erase) private async void BanPlayer(Ban ban)
{ {
if (!_admins.HasAdminFlag(Player, AdminFlags.Ban)) if (!_admins.HasAdminFlag(Player, AdminFlags.Ban))
{ {
_sawmill.Warning($"{Player.Name} ({Player.UserId}) tried to create a ban with no ban flag"); _sawmill.Warning($"{Player.Name} ({Player.UserId}) tried to create a ban with no ban flag");
return; return;
} }
if (target == null && string.IsNullOrWhiteSpace(ipAddressString) && hwid == null)
if (ban.Target == null && string.IsNullOrWhiteSpace(ban.IpAddress) && ban.Hwid == null)
{ {
_chat.DispatchServerMessage(Player, Loc.GetString("ban-panel-no-data")); _chat.DispatchServerMessage(Player, Loc.GetString("ban-panel-no-data"));
return; return;
} }
(IPAddress, int)? addressRange = null; (IPAddress, int)? addressRange = null;
if (ipAddressString is not null) if (ban.IpAddress is not null)
{ {
var hid = "0"; if (!IPAddress.TryParse(ban.IpAddress, out var ipAddress) || !uint.TryParse(ban.IpAddressHid, out var hidInt) || hidInt > Ipv6_CIDR || hidInt > Ipv4_CIDR && ipAddress.AddressFamily == AddressFamily.InterNetwork)
var split = ipAddressString.Split('/', 2);
ipAddressString = split[0];
if (split.Length > 1)
hid = split[1];
if (!IPAddress.TryParse(ipAddressString, out var ipAddress) || !uint.TryParse(hid, out var hidInt) || hidInt > Ipv6_CIDR || hidInt > Ipv4_CIDR && ipAddress.AddressFamily == AddressFamily.InterNetwork)
{ {
_chat.DispatchServerMessage(Player, Loc.GetString("ban-panel-invalid-ip")); _chat.DispatchServerMessage(Player, Loc.GetString("ban-panel-invalid-ip"));
return; return;
@@ -94,12 +88,12 @@ public sealed class BanPanelEui : BaseEui
addressRange = (ipAddress, (int) hidInt); addressRange = (ipAddress, (int) hidInt);
} }
var targetUid = target is not null ? PlayerId : null; var targetUid = ban.Target is not null ? PlayerId : null;
addressRange = useLastIp && LastAddress is not null ? (LastAddress, LastAddress.AddressFamily == AddressFamily.InterNetworkV6 ? Ipv6_CIDR : Ipv4_CIDR) : addressRange; addressRange = ban.UseLastIp && LastAddress is not null ? (LastAddress, LastAddress.AddressFamily == AddressFamily.InterNetworkV6 ? Ipv6_CIDR : Ipv4_CIDR) : addressRange;
var targetHWid = useLastHwid ? LastHwid : hwid; var targetHWid = ban.UseLastHwid ? LastHwid : ban.Hwid;
if (target != null && target != PlayerName || Guid.TryParse(target, out var parsed) && parsed != PlayerId) if (ban.Target != null && ban.Target != PlayerName || Guid.TryParse(ban.Target, out var parsed) && parsed != PlayerId)
{ {
var located = await _playerLocator.LookupIdByNameOrIdAsync(target); var located = await _playerLocator.LookupIdByNameOrIdAsync(ban.Target);
if (located == null) if (located == null)
{ {
_chat.DispatchServerMessage(Player, Loc.GetString("cmd-ban-player")); _chat.DispatchServerMessage(Player, Loc.GetString("cmd-ban-player"));
@@ -107,7 +101,7 @@ public sealed class BanPanelEui : BaseEui
} }
targetUid = located.UserId; targetUid = located.UserId;
var targetAddress = located.LastAddress; var targetAddress = located.LastAddress;
if (useLastIp && targetAddress != null) if (ban.UseLastIp && targetAddress != null)
{ {
if (targetAddress.IsIPv4MappedToIPv6) if (targetAddress.IsIPv4MappedToIPv6)
targetAddress = targetAddress.MapToIPv4(); targetAddress = targetAddress.MapToIPv4();
@@ -116,30 +110,50 @@ public sealed class BanPanelEui : BaseEui
var hid = targetAddress.AddressFamily == AddressFamily.InterNetworkV6 ? Ipv6_CIDR : Ipv4_CIDR; var hid = targetAddress.AddressFamily == AddressFamily.InterNetworkV6 ? Ipv6_CIDR : Ipv4_CIDR;
addressRange = (targetAddress, hid); addressRange = (targetAddress, hid);
} }
targetHWid = useLastHwid ? located.LastHWId : hwid; targetHWid = ban.UseLastHwid ? located.LastHWId : ban.Hwid;
} }
if (roles?.Count > 0) if (ban.BannedJobs?.Length > 0 || ban.BannedAntags?.Length > 0)
{ {
var now = DateTimeOffset.UtcNow; var now = DateTimeOffset.UtcNow;
foreach (var role in roles) foreach (var role in ban.BannedJobs ?? [])
{ {
if (_prototypeManager.HasIndex<JobPrototype>(role)) _banManager.CreateRoleBan(
{ targetUid,
_banManager.CreateRoleBan(targetUid, target, Player.UserId, addressRange, targetHWid, role, minutes, severity, reason, now); ban.Target,
Player.UserId,
addressRange,
targetHWid,
role,
ban.BanDurationMinutes,
ban.Severity,
ban.Reason,
now
);
} }
else
foreach (var role in ban.BannedAntags ?? [])
{ {
_sawmill.Warning($"{Player.Name} ({Player.UserId}) tried to issue a job ban with an invalid job: {role}"); _banManager.CreateRoleBan(
} targetUid,
ban.Target,
Player.UserId,
addressRange,
targetHWid,
role,
ban.BanDurationMinutes,
ban.Severity,
ban.Reason,
now
);
} }
Close(); Close();
return; return;
} }
if (erase && if (ban.Erase && targetUid is not null)
targetUid != null)
{ {
try try
{ {
@@ -152,7 +166,16 @@ public sealed class BanPanelEui : BaseEui
} }
} }
_banManager.CreateServerBan(targetUid, target, Player.UserId, addressRange, targetHWid, minutes, severity, reason); _banManager.CreateServerBan(
targetUid,
ban.Target,
Player.UserId,
addressRange,
targetHWid,
ban.BanDurationMinutes,
ban.Severity,
ban.Reason
);
Close(); Close();
} }

View File

@@ -29,9 +29,10 @@ public sealed class RoleBanCommand : IConsoleCommand
public async void Execute(IConsoleShell shell, string argStr, string[] args) public async void Execute(IConsoleShell shell, string argStr, string[] args)
{ {
string target; string target;
string job; string role;
string reason; string reason;
uint minutes; uint minutes;
if (!Enum.TryParse(_cfg.GetCVar(CCVars.RoleBanDefaultSeverity), out NoteSeverity severity)) if (!Enum.TryParse(_cfg.GetCVar(CCVars.RoleBanDefaultSeverity), out NoteSeverity severity))
{ {
_sawmill ??= _log.GetSawmill("admin.role_ban"); _sawmill ??= _log.GetSawmill("admin.role_ban");
@@ -43,30 +44,33 @@ public sealed class RoleBanCommand : IConsoleCommand
{ {
case 3: case 3:
target = args[0]; target = args[0];
job = args[1]; role = args[1];
reason = args[2]; reason = args[2];
minutes = 0; minutes = 0;
break; break;
case 4: case 4:
target = args[0]; target = args[0];
job = args[1]; role = args[1];
reason = args[2]; reason = args[2];
if (!uint.TryParse(args[3], out minutes)) if (!uint.TryParse(args[3], out minutes))
{ {
shell.WriteError(Loc.GetString("cmd-roleban-minutes-parse", ("time", args[3]), ("help", Help))); shell.WriteError(Loc.GetString("cmd-roleban-minutes-parse", ("time", args[3]), ("help", Help)));
return; return;
} }
break; break;
case 5: case 5:
target = args[0]; target = args[0];
job = args[1]; role = args[1];
reason = args[2]; reason = args[2];
if (!uint.TryParse(args[3], out minutes)) if (!uint.TryParse(args[3], out minutes))
{ {
shell.WriteError(Loc.GetString("cmd-roleban-minutes-parse", ("time", args[3]), ("help", Help))); shell.WriteError(Loc.GetString("cmd-roleban-minutes-parse", ("time", args[3]), ("help", Help)));
return; return;
} }
@@ -80,12 +84,7 @@ public sealed class RoleBanCommand : IConsoleCommand
default: default:
shell.WriteError(Loc.GetString("cmd-roleban-arg-count")); shell.WriteError(Loc.GetString("cmd-roleban-arg-count"));
shell.WriteLine(Help); shell.WriteLine(Help);
return;
}
if (!_proto.HasIndex<JobPrototype>(job))
{
shell.WriteError(Loc.GetString("cmd-roleban-job-parse", ("job", job)));
return; return;
} }
@@ -93,13 +92,19 @@ public sealed class RoleBanCommand : IConsoleCommand
if (located == null) if (located == null)
{ {
shell.WriteError(Loc.GetString("cmd-roleban-name-parse")); shell.WriteError(Loc.GetString("cmd-roleban-name-parse"));
return; return;
} }
var targetUid = located.UserId; var targetUid = located.UserId;
var targetHWid = located.LastHWId; var targetHWid = located.LastHWId;
_bans.CreateRoleBan(targetUid, located.Username, shell.Player?.UserId, null, targetHWid, job, minutes, severity, reason, DateTimeOffset.UtcNow); if (_proto.HasIndex<JobPrototype>(role))
_bans.CreateRoleBan<JobPrototype>(targetUid, located.Username, shell.Player?.UserId, null, targetHWid, role, minutes, severity, reason, DateTimeOffset.UtcNow);
else if (_proto.HasIndex<AntagPrototype>(role))
_bans.CreateRoleBan<AntagPrototype>(targetUid, located.Username, shell.Player?.UserId, null, targetHWid, role, minutes, severity, reason, DateTimeOffset.UtcNow);
else
shell.WriteError(Loc.GetString("cmd-roleban-job-parse", ("job", role)));
} }
public CompletionResult GetCompletion(IConsoleShell shell, string[] args) public CompletionResult GetCompletion(IConsoleShell shell, string[] args)

View File

@@ -26,24 +26,25 @@ namespace Content.Server.Administration.Managers;
public sealed partial class BanManager : IBanManager, IPostInjectInit public sealed partial class BanManager : IBanManager, IPostInjectInit
{ {
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly IChatManager _chat = default!;
[Dependency] private readonly IServerDbManager _db = default!; [Dependency] private readonly IServerDbManager _db = default!;
[Dependency] private readonly ServerDbEntryManager _entryManager = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly ILocalizationManager _localizationManager = default!;
[Dependency] private readonly ILogManager _logManager = default!;
[Dependency] private readonly INetManager _netManager = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!; [Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IEntitySystemManager _systems = default!; [Dependency] private readonly IEntitySystemManager _systems = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly ILocalizationManager _localizationManager = default!;
[Dependency] private readonly ServerDbEntryManager _entryManager = default!;
[Dependency] private readonly IChatManager _chat = default!;
[Dependency] private readonly INetManager _netManager = default!;
[Dependency] private readonly ILogManager _logManager = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly ITaskManager _taskManager = default!; [Dependency] private readonly ITaskManager _taskManager = default!;
[Dependency] private readonly UserDbDataManager _userDbData = default!; [Dependency] private readonly UserDbDataManager _userDbData = default!;
private ISawmill _sawmill = default!; private ISawmill _sawmill = default!;
public const string SawmillId = "admin.bans"; public const string SawmillId = "admin.bans";
public const string JobPrefix = "Job:"; public const string PrefixAntag = "Antag:";
public const string PrefixJob = "Job:";
private readonly Dictionary<ICommonSession, List<ServerRoleBanDef>> _cachedRoleBans = new(); private readonly Dictionary<ICommonSession, List<ServerRoleBanDef>> _cachedRoleBans = new();
// Cached ban exemption flags are used to handle // Cached ban exemption flags are used to handle
@@ -91,30 +92,6 @@ public sealed partial class BanManager : IBanManager, IPostInjectInit
_cachedBanExemptions.Remove(player); _cachedBanExemptions.Remove(player);
} }
private async Task<bool> AddRoleBan(ServerRoleBanDef banDef)
{
banDef = await _db.AddServerRoleBanAsync(banDef);
if (banDef.UserId != null
&& _playerManager.TryGetSessionById(banDef.UserId, out var player)
&& _cachedRoleBans.TryGetValue(player, out var cachedBans))
{
cachedBans.Add(banDef);
}
return true;
}
public HashSet<string>? GetRoleBans(NetUserId playerUserId)
{
if (!_playerManager.TryGetSessionById(playerUserId, out var session))
return null;
return _cachedRoleBans.TryGetValue(session, out var roleBans)
? roleBans.Select(banDef => banDef.Role).ToHashSet()
: null;
}
public void Restart() public void Restart()
{ {
// Clear out players that have disconnected. // Clear out players that have disconnected.
@@ -232,23 +209,54 @@ public sealed partial class BanManager : IBanManager, IPostInjectInit
#endregion #endregion
#region Job Bans #region Role Bans
// If you are trying to remove timeOfBan, please don't. It's there because the note system groups role bans by time, reason and banning admin. // If you are trying to remove timeOfBan, please don't. It's there because the note system groups role bans by time, reason and banning admin.
// Removing it will clutter the note list. Please also make sure that department bans are applied to roles with the same DateTimeOffset. // Removing it will clutter the note list. Please also make sure that department bans are applied to roles with the same DateTimeOffset.
public async void CreateRoleBan(NetUserId? target, string? targetUsername, NetUserId? banningAdmin, (IPAddress, int)? addressRange, ImmutableTypedHwid? hwid, string role, uint? minutes, NoteSeverity severity, string reason, DateTimeOffset timeOfBan) public async void CreateRoleBan<T>(
NetUserId? target,
string? targetUsername,
NetUserId? banningAdmin,
(IPAddress, int)? addressRange,
ImmutableTypedHwid? hwid,
ProtoId<T> role,
uint? minutes,
NoteSeverity severity,
string reason,
DateTimeOffset timeOfBan
) where T : class, IPrototype
{ {
if (!_prototypeManager.TryIndex(role, out JobPrototype? _)) string encodedRole;
// TODO: Note that it's possible to clash IDs here between a job and an antag. The refactor that introduced
// this check has consciously avoided refactoring Job and Antag prototype.
// Refactor Job- and Antag- Prototype to introduce a common RolePrototype, which will fix this possible clash.
//TODO remove this check as part of the above refactor
if (_prototypeManager.HasIndex<JobPrototype>(role) && _prototypeManager.HasIndex<AntagPrototype>(role))
{ {
throw new ArgumentException($"Invalid role '{role}'", nameof(role)); _sawmill.Error($"Creating role ban for {role}: cannot create role ban, role is both JobPrototype and AntagPrototype.");
return;
} }
role = string.Concat(JobPrefix, role); // Don't trust the input: make sure the job or antag actually exists.
DateTimeOffset? expires = null; if (_prototypeManager.HasIndex<JobPrototype>(role))
if (minutes > 0) encodedRole = PrefixJob + role;
else if (_prototypeManager.HasIndex<AntagPrototype>(role))
encodedRole = PrefixAntag + role;
else
{ {
expires = DateTimeOffset.Now + TimeSpan.FromMinutes(minutes.Value); _sawmill.Error($"Creating role ban for {role}: cannot create role ban, role is not a JobPrototype or an AntagPrototype.");
return;
} }
DateTimeOffset? expires = null;
if (minutes > 0)
expires = DateTimeOffset.Now + TimeSpan.FromMinutes(minutes.Value);
_systems.TryGetEntitySystem(out GameTicker? ticker); _systems.TryGetEntitySystem(out GameTicker? ticker);
int? roundId = ticker == null || ticker.RoundId == 0 ? null : ticker.RoundId; int? roundId = ticker == null || ticker.RoundId == 0 ? null : ticker.RoundId;
var playtime = target == null ? TimeSpan.Zero : (await _db.GetPlayTimes(target.Value)).Find(p => p.Tracker == PlayTimeTrackingShared.TrackerOverall)?.TimeSpent ?? TimeSpan.Zero; var playtime = target == null ? TimeSpan.Zero : (await _db.GetPlayTimes(target.Value)).Find(p => p.Tracker == PlayTimeTrackingShared.TrackerOverall)?.TimeSpent ?? TimeSpan.Zero;
@@ -266,21 +274,34 @@ public sealed partial class BanManager : IBanManager, IPostInjectInit
severity, severity,
banningAdmin, banningAdmin,
null, null,
role); encodedRole);
if (!await AddRoleBan(banDef)) if (!await AddRoleBan(banDef))
{ {
_chat.SendAdminAlert(Loc.GetString("cmd-roleban-existing", ("target", targetUsername ?? "null"), ("role", role))); _chat.SendAdminAlert(Loc.GetString("cmd-roleban-existing", ("target", targetUsername ?? "null"), ("role", role)));
return; return;
} }
var length = expires == null ? Loc.GetString("cmd-roleban-inf") : Loc.GetString("cmd-roleban-until", ("expires", expires)); var length = expires == null ? Loc.GetString("cmd-roleban-inf") : Loc.GetString("cmd-roleban-until", ("expires", expires));
_chat.SendAdminAlert(Loc.GetString("cmd-roleban-success", ("target", targetUsername ?? "null"), ("role", role), ("reason", reason), ("length", length))); _chat.SendAdminAlert(Loc.GetString("cmd-roleban-success", ("target", targetUsername ?? "null"), ("role", role), ("reason", reason), ("length", length)));
if (target != null && _playerManager.TryGetSessionById(target.Value, out var session)) if (target is not null && _playerManager.TryGetSessionById(target.Value, out var session))
{
SendRoleBans(session); SendRoleBans(session);
} }
private async Task<bool> AddRoleBan(ServerRoleBanDef banDef)
{
banDef = await _db.AddServerRoleBanAsync(banDef);
if (banDef.UserId != null
&& _playerManager.TryGetSessionById(banDef.UserId, out var player)
&& _cachedRoleBans.TryGetValue(player, out var cachedBans))
{
cachedBans.Add(banDef);
}
return true;
} }
public async Task<string> PardonRoleBan(int banId, NetUserId? unbanningAdmin, DateTimeOffset unbanTime) public async Task<string> PardonRoleBan(int banId, NetUserId? unbanningAdmin, DateTimeOffset unbanTime)
@@ -319,32 +340,109 @@ public sealed partial class BanManager : IBanManager, IPostInjectInit
} }
public HashSet<ProtoId<JobPrototype>>? GetJobBans(NetUserId playerUserId) public HashSet<ProtoId<JobPrototype>>? GetJobBans(NetUserId playerUserId)
{
return GetRoleBans<JobPrototype>(playerUserId, PrefixJob);
}
public HashSet<ProtoId<AntagPrototype>>? GetAntagBans(NetUserId playerUserId)
{
return GetRoleBans<AntagPrototype>(playerUserId, PrefixAntag);
}
private HashSet<ProtoId<T>>? GetRoleBans<T>(NetUserId playerUserId, string prefix) where T : class, IPrototype
{ {
if (!_playerManager.TryGetSessionById(playerUserId, out var session)) if (!_playerManager.TryGetSessionById(playerUserId, out var session))
return null; return null;
if (!_cachedRoleBans.TryGetValue(session, out var roleBans)) return GetRoleBans<T>(session, prefix);
}
private HashSet<ProtoId<T>>? GetRoleBans<T>(ICommonSession playerSession, string prefix) where T : class, IPrototype
{
if (!_cachedRoleBans.TryGetValue(playerSession, out var roleBans))
return null; return null;
return roleBans return roleBans
.Where(ban => ban.Role.StartsWith(JobPrefix, StringComparison.Ordinal)) .Where(ban => ban.Role.StartsWith(prefix, StringComparison.Ordinal))
.Select(ban => new ProtoId<JobPrototype>(ban.Role[JobPrefix.Length..])) .Select(ban => new ProtoId<T>(ban.Role[prefix.Length..]))
.ToHashSet(); .ToHashSet();
} }
#endregion
public HashSet<string>? GetRoleBans(NetUserId playerUserId)
{
if (!_playerManager.TryGetSessionById(playerUserId, out var session))
return null;
return _cachedRoleBans.TryGetValue(session, out var roleBans)
? roleBans.Select(banDef => banDef.Role).ToHashSet()
: null;
}
public bool IsRoleBanned(ICommonSession player, List<ProtoId<JobPrototype>> jobs)
{
return IsRoleBanned(player, jobs, PrefixJob);
}
public bool IsRoleBanned(ICommonSession player, List<ProtoId<AntagPrototype>> antags)
{
return IsRoleBanned(player, antags, PrefixAntag);
}
private bool IsRoleBanned<T>(ICommonSession player, List<ProtoId<T>> roles, string prefix) where T : class, IPrototype
{
var bans = GetRoleBans(player.UserId);
if (bans is null || bans.Count == 0)
return false;
// ReSharper disable once ForeachCanBeConvertedToQueryUsingAnotherGetEnumerator
foreach (var role in roles)
{
if (bans.Contains(prefix + role))
return true;
}
return false;
}
public void SendRoleBans(ICommonSession pSession) public void SendRoleBans(ICommonSession pSession)
{ {
var roleBans = _cachedRoleBans.GetValueOrDefault(pSession) ?? new List<ServerRoleBanDef>(); var jobBans = GetRoleBans<JobPrototype>(pSession, PrefixJob);
var jobBansList = new List<string>(jobBans?.Count ?? 0);
if (jobBans is not null)
{
// ReSharper disable once ForeachCanBeConvertedToQueryUsingAnotherGetEnumerator
foreach (var encodedId in jobBans)
{
jobBansList.Add(encodedId.ToString().Replace(PrefixJob, ""));
}
}
var antagBans = GetRoleBans<AntagPrototype>(pSession, PrefixAntag);
var antagBansList = new List<string>(antagBans?.Count ?? 0);
if (antagBans is not null)
{
// ReSharper disable once ForeachCanBeConvertedToQueryUsingAnotherGetEnumerator
foreach (var encodedId in antagBans)
{
antagBansList.Add(encodedId.ToString().Replace(PrefixAntag, ""));
}
}
var bans = new MsgRoleBans() var bans = new MsgRoleBans()
{ {
Bans = roleBans.Select(o => o.Role).ToList() JobBans = jobBansList,
AntagBans = antagBansList,
}; };
_sawmill.Debug($"Sent rolebans to {pSession.Name}"); _sawmill.Debug($"Sent role bans to {pSession.Name}");
_netManager.ServerSendMessage(bans, pSession.Channel); _netManager.ServerSendMessage(bans, pSession.Channel);
} }
#endregion
public void PostInject() public void PostInject()
{ {
_sawmill = _logManager.GetSawmill(SawmillId); _sawmill = _logManager.GetSawmill(SawmillId);

View File

@@ -1,4 +1,3 @@
using System.Collections.Immutable;
using System.Net; using System.Net;
using System.Threading.Tasks; using System.Threading.Tasks;
using Content.Shared.Database; using Content.Shared.Database;
@@ -25,19 +24,63 @@ public interface IBanManager
/// <param name="severity">Severity of the resulting ban note</param> /// <param name="severity">Severity of the resulting ban note</param>
/// <param name="reason">Reason for the ban</param> /// <param name="reason">Reason for the ban</param>
public void CreateServerBan(NetUserId? target, string? targetUsername, NetUserId? banningAdmin, (IPAddress, int)? addressRange, ImmutableTypedHwid? hwid, uint? minutes, NoteSeverity severity, string reason); public void CreateServerBan(NetUserId? target, string? targetUsername, NetUserId? banningAdmin, (IPAddress, int)? addressRange, ImmutableTypedHwid? hwid, uint? minutes, NoteSeverity severity, string reason);
/// <summary>
/// Gets a list of prefixed prototype IDs with the player's role bans.
/// </summary>
public HashSet<string>? GetRoleBans(NetUserId playerUserId); public HashSet<string>? GetRoleBans(NetUserId playerUserId);
/// <summary>
/// Checks if the player is currently banned from any of the listed roles.
/// </summary>
/// <param name="player">The player.</param>
/// <param name="antags">A list of valid antag prototype IDs.</param>
/// <returns>Returns True if an active role ban is found for this player for any of the listed roles.</returns>
public bool IsRoleBanned(ICommonSession player, List<ProtoId<AntagPrototype>> antags);
/// <summary>
/// Checks if the player is currently banned from any of the listed roles.
/// </summary>
/// <param name="player">The player.</param>
/// <param name="jobs">A list of valid job prototype IDs.</param>
/// <returns>Returns True if an active role ban is found for this player for any of the listed roles.</returns>
public bool IsRoleBanned(ICommonSession player, List<ProtoId<JobPrototype>> jobs);
/// <summary>
/// Gets a list of prototype IDs with the player's job bans.
/// </summary>
public HashSet<ProtoId<JobPrototype>>? GetJobBans(NetUserId playerUserId); public HashSet<ProtoId<JobPrototype>>? GetJobBans(NetUserId playerUserId);
/// <summary>
/// Gets a list of prototype IDs with the player's antag bans.
/// </summary>
public HashSet<ProtoId<AntagPrototype>>? GetAntagBans(NetUserId playerUserId);
/// <summary> /// <summary>
/// Creates a job ban for the specified target, username or GUID /// Creates a job ban for the specified target, username or GUID
/// </summary> /// </summary>
/// <param name="target">Target user, username or GUID, null for none</param> /// <param name="target">Target user, username or GUID, null for none</param>
/// <param name="role">Role to be banned from</param> /// <param name="targetUsername">The username of the target, if known</param>
/// <param name="banningAdmin">The responsible admin for the ban</param>
/// <param name="addressRange">The range of IPs that are to be banned, if known</param>
/// <param name="hwid">The HWID to be banned, if known</param>
/// <param name="role">The role ID to be banned from. Either an AntagPrototype or a JobPrototype</param>
/// <param name="minutes">Number of minutes to ban for. 0 and null mean permanent</param>
/// <param name="severity">Severity of the resulting ban note</param> /// <param name="severity">Severity of the resulting ban note</param>
/// <param name="reason">Reason for the ban</param> /// <param name="reason">Reason for the ban</param>
/// <param name="minutes">Number of minutes to ban for. 0 and null mean permanent</param>
/// <param name="timeOfBan">Time when the ban was applied, used for grouping role bans</param> /// <param name="timeOfBan">Time when the ban was applied, used for grouping role bans</param>
public void CreateRoleBan(NetUserId? target, string? targetUsername, NetUserId? banningAdmin, (IPAddress, int)? addressRange, ImmutableTypedHwid? hwid, string role, uint? minutes, NoteSeverity severity, string reason, DateTimeOffset timeOfBan); public void CreateRoleBan<T>(
NetUserId? target,
string? targetUsername,
NetUserId? banningAdmin,
(IPAddress, int)? addressRange,
ImmutableTypedHwid? hwid,
ProtoId<T> role,
uint? minutes,
NoteSeverity severity,
string reason,
DateTimeOffset timeOfBan
) where T : class, IPrototype;
/// <summary> /// <summary>
/// Pardons a role ban for the specified target, username or GUID /// Pardons a role ban for the specified target, username or GUID

View File

@@ -2,16 +2,17 @@ using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
using Content.Server.Antag.Components; using Content.Server.Antag.Components;
using Content.Server.GameTicking.Rules.Components; using Content.Server.GameTicking.Rules.Components;
using Content.Server.Objectives;
using Content.Shared.Antag; using Content.Shared.Antag;
using Content.Shared.Chat; using Content.Shared.Chat;
using Content.Shared.GameTicking.Components; using Content.Shared.GameTicking.Components;
using Content.Shared.Mind; using Content.Shared.Mind;
using Content.Shared.Preferences; using Content.Shared.Preferences;
using Content.Shared.Roles;
using JetBrains.Annotations; using JetBrains.Annotations;
using Robust.Shared.Audio; using Robust.Shared.Audio;
using Robust.Shared.Enums; using Robust.Shared.Enums;
using Robust.Shared.Player; using Robust.Shared.Player;
using Robust.Shared.Prototypes;
namespace Content.Server.Antag; namespace Content.Server.Antag;
@@ -161,33 +162,35 @@ public sealed partial class AntagSelectionSystem
} }
/// <summary> /// <summary>
/// Checks if a given session has the primary antag preferences for a given definition /// Checks if a given session has enabled the antag preferences for a given definition,
/// and if it is blocked by any requirements or bans.
/// </summary> /// </summary>
public bool HasPrimaryAntagPreference(ICommonSession? session, AntagSelectionDefinition def) /// <returns>Returns true if at least one role from the provided list passes every condition</returns>>
public bool ValidAntagPreference(ICommonSession? session, List<ProtoId<AntagPrototype>> roles)
{ {
if (session == null) if (session == null)
return true; return true;
if (def.PrefRoles.Count == 0) if (roles.Count == 0)
return false; return false;
var pref = (HumanoidCharacterProfile) _pref.GetPreferences(session.UserId).SelectedCharacter; var pref = (HumanoidCharacterProfile) _pref.GetPreferences(session.UserId).SelectedCharacter;
return pref.AntagPreferences.Any(p => def.PrefRoles.Contains(p));
var valid = false;
// Check each individual antag role
foreach (var role in roles)
{
var list = new List<ProtoId<AntagPrototype>>{role};
if (pref.AntagPreferences.Contains(role)
&& !_ban.IsRoleBanned(session, list)
&& _playTime.IsAllowed(session, list))
valid = true;
} }
/// <summary> return valid;
/// Checks if a given session has the fallback antag preferences for a given definition
/// </summary>
public bool HasFallbackAntagPreference(ICommonSession? session, AntagSelectionDefinition def)
{
if (session == null)
return true;
if (def.FallbackRoles.Count == 0)
return false;
var pref = (HumanoidCharacterProfile) _pref.GetPreferences(session.UserId).SelectedCharacter;
return pref.AntagPreferences.Any(p => def.FallbackRoles.Contains(p));
} }
/// <summary> /// <summary>

View File

@@ -1,4 +1,5 @@
using System.Linq; using System.Linq;
using Content.Server.Administration.Managers;
using Content.Server.Antag.Components; using Content.Server.Antag.Components;
using Content.Server.Chat.Managers; using Content.Server.Chat.Managers;
using Content.Server.GameTicking; using Content.Server.GameTicking;
@@ -8,11 +9,11 @@ using Content.Server.Ghost.Roles;
using Content.Server.Ghost.Roles.Components; using Content.Server.Ghost.Roles.Components;
using Content.Server.Mind; using Content.Server.Mind;
using Content.Server.Objectives; using Content.Server.Objectives;
using Content.Server.Players.PlayTimeTracking;
using Content.Server.Preferences.Managers; using Content.Server.Preferences.Managers;
using Content.Server.Roles; using Content.Server.Roles;
using Content.Server.Roles.Jobs; using Content.Server.Roles.Jobs;
using Content.Server.Shuttles.Components; using Content.Server.Shuttles.Components;
using Content.Server.Station.Events;
using Content.Shared.Administration.Logs; using Content.Shared.Administration.Logs;
using Content.Shared.Antag; using Content.Shared.Antag;
using Content.Shared.Clothing; using Content.Shared.Clothing;
@@ -40,12 +41,14 @@ namespace Content.Server.Antag;
public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelectionComponent> public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelectionComponent>
{ {
[Dependency] private readonly AudioSystem _audio = default!; [Dependency] private readonly AudioSystem _audio = default!;
[Dependency] private readonly IBanManager _ban = default!;
[Dependency] private readonly IChatManager _chat = default!; [Dependency] private readonly IChatManager _chat = default!;
[Dependency] private readonly GhostRoleSystem _ghostRole = default!; [Dependency] private readonly GhostRoleSystem _ghostRole = default!;
[Dependency] private readonly JobSystem _jobs = default!; [Dependency] private readonly JobSystem _jobs = default!;
[Dependency] private readonly LoadoutSystem _loadout = default!; [Dependency] private readonly LoadoutSystem _loadout = default!;
[Dependency] private readonly MindSystem _mind = default!; [Dependency] private readonly MindSystem _mind = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!; [Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly PlayTimeTrackingSystem _playTime = default!;
[Dependency] private readonly IServerPreferencesManager _pref = default!; [Dependency] private readonly IServerPreferencesManager _pref = default!;
[Dependency] private readonly RoleSystem _role = default!; [Dependency] private readonly RoleSystem _role = default!;
[Dependency] private readonly TransformSystem _transform = default!; [Dependency] private readonly TransformSystem _transform = default!;
@@ -344,7 +347,7 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
{ {
_adminLogger.Add(LogType.AntagSelection, $"Start trying to make {session} become the antagonist: {ToPrettyString(ent)}"); _adminLogger.Add(LogType.AntagSelection, $"Start trying to make {session} become the antagonist: {ToPrettyString(ent)}");
if (checkPref && !HasPrimaryAntagPreference(session, def)) if (checkPref && !ValidAntagPreference(session, def.PrefRoles))
return false; return false;
if (!IsSessionValid(ent, session, def) || !IsEntityValid(session?.AttachedEntity, def)) if (!IsSessionValid(ent, session, def) || !IsEntityValid(session?.AttachedEntity, def))
@@ -497,11 +500,12 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
if (ent.Comp.PreSelectedSessions.TryGetValue(def, out var preSelected) && preSelected.Contains(session)) if (ent.Comp.PreSelectedSessions.TryGetValue(def, out var preSelected) && preSelected.Contains(session))
continue; continue;
if (HasPrimaryAntagPreference(session, def)) // Add player to the appropriate antag pool
if (ValidAntagPreference(session, def.PrefRoles))
{ {
preferredList.Add(session); preferredList.Add(session);
} }
else if (HasFallbackAntagPreference(session, def)) else if (ValidAntagPreference(session, def.FallbackRoles))
{ {
fallbackList.Add(session); fallbackList.Add(session);
} }

View File

@@ -28,7 +28,6 @@ namespace Content.Server.Database
public abstract class ServerDbBase public abstract class ServerDbBase
{ {
private readonly ISawmill _opsLog; private readonly ISawmill _opsLog;
public event Action<DatabaseNotification>? OnNotificationReceived; public event Action<DatabaseNotification>? OnNotificationReceived;
/// <param name="opsLog">Sawmill to trace log database operations to.</param> /// <param name="opsLog">Sawmill to trace log database operations to.</param>
@@ -1386,7 +1385,7 @@ INSERT INTO player_round (players_id, rounds_id) VALUES ({players[player]}, {id}
ban.LastEditedAt, ban.LastEditedAt,
ban.ExpirationTime, ban.ExpirationTime,
ban.Hidden, ban.Hidden,
new [] { ban.RoleId.Replace(BanManager.JobPrefix, null) }, new [] { ban.RoleId.Replace(BanManager.PrefixJob, null).Replace(BanManager.PrefixAntag, null) },
MakePlayerRecord(unbanningAdmin), MakePlayerRecord(unbanningAdmin),
ban.Unban?.UnbanTime); ban.Unban?.UnbanTime);
} }
@@ -1686,7 +1685,7 @@ INSERT INTO player_round (players_id, rounds_id) VALUES ({players[player]}, {id}
NormalizeDatabaseTime(firstBan.LastEditedAt), NormalizeDatabaseTime(firstBan.LastEditedAt),
NormalizeDatabaseTime(firstBan.ExpirationTime), NormalizeDatabaseTime(firstBan.ExpirationTime),
firstBan.Hidden, firstBan.Hidden,
banGroup.Select(ban => ban.RoleId.Replace(BanManager.JobPrefix, null)).ToArray(), banGroup.Select(ban => ban.RoleId.Replace(BanManager.PrefixJob, null).Replace(BanManager.PrefixAntag, null)).ToArray(),
MakePlayerRecord(unbanningAdmin), MakePlayerRecord(unbanningAdmin),
NormalizeDatabaseTime(firstBan.Unban?.UnbanTime))); NormalizeDatabaseTime(firstBan.Unban?.UnbanTime)));
} }

View File

@@ -1,13 +0,0 @@
using Content.Shared.Roles;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
namespace Content.Server.GameTicking.Events;
[ByRefEvent]
public struct IsJobAllowedEvent(ICommonSession player, ProtoId<JobPrototype> jobId, bool cancelled = false)
{
public readonly ICommonSession Player = player;
public readonly ProtoId<JobPrototype> JobId = jobId;
public bool Cancelled = cancelled;
}

View File

@@ -0,0 +1,24 @@
using Content.Shared.Roles;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
namespace Content.Server.GameTicking.Events;
/// <summary>
/// Event raised to check if a player is allowed/able to assume a role.
/// </summary>
/// <param name="player">The player.</param>
/// <param name="jobs">Optional list of job prototype IDs</param>
/// <param name="antags">Optional list of antag prototype IDs</param>
[ByRefEvent]
public struct IsRoleAllowedEvent(
ICommonSession player,
List<ProtoId<JobPrototype>>? jobs,
List<ProtoId<AntagPrototype>>? antags,
bool cancelled = false)
{
public readonly ICommonSession Player = player;
public readonly List<ProtoId<JobPrototype>>? Jobs = jobs;
public readonly List<ProtoId<AntagPrototype>>? Antags = antags;
public bool Cancelled = cancelled;
}

View File

@@ -141,12 +141,13 @@ namespace Content.Server.GameTicking
var character = GetPlayerProfile(player); var character = GetPlayerProfile(player);
var jobBans = _banManager.GetJobBans(player.UserId); var jobBans = _banManager.GetJobBans(player.UserId);
if (jobBans == null || jobId != null && jobBans.Contains(jobId)) if (jobBans == null || jobId != null && jobBans.Contains(jobId)) //TODO: use IsRoleBanned directly?
return; return;
if (jobId != null) if (jobId != null)
{ {
var ev = new IsJobAllowedEvent(player, new ProtoId<JobPrototype>(jobId)); var jobs = new List<ProtoId<JobPrototype>> {jobId};
var ev = new IsRoleAllowedEvent(player, jobs, null);
RaiseLocalEvent(ref ev); RaiseLocalEvent(ref ev);
if (ev.Cancelled) if (ev.Cancelled)
return; return;

View File

@@ -15,12 +15,6 @@ public sealed partial class GhostRoleComponent : Component
[DataField("rules")] private string _roleRules = "ghost-role-component-default-rules"; [DataField("rules")] private string _roleRules = "ghost-role-component-default-rules";
// Actually make use of / enforce this requirement?
// Why is this even here.
// Move to ghost role prototype & respect CCvars.GameRoleTimerOverride
[DataField("requirements")]
public HashSet<JobRequirement>? Requirements;
/// <summary> /// <summary>
/// Whether the <see cref="MakeSentientCommand"/> should run on the mob. /// Whether the <see cref="MakeSentientCommand"/> should run on the mob.
/// </summary> /// </summary>

View File

@@ -1,6 +1,8 @@
using System.Linq; using System.Linq;
using Content.Server.Administration.Logs; using Content.Server.Administration.Logs;
using Content.Server.Administration.Managers;
using Content.Server.EUI; using Content.Server.EUI;
using Content.Server.GameTicking.Events;
using Content.Server.Ghost.Roles.Components; using Content.Server.Ghost.Roles.Components;
using Content.Server.Ghost.Roles.Events; using Content.Server.Ghost.Roles.Events;
using Content.Shared.Ghost.Roles.Raffles; using Content.Shared.Ghost.Roles.Raffles;
@@ -32,13 +34,16 @@ using Content.Server.Popups;
using Content.Shared.Verbs; using Content.Shared.Verbs;
using Robust.Shared.Collections; using Robust.Shared.Collections;
using Content.Shared.Ghost.Roles.Components; using Content.Shared.Ghost.Roles.Components;
using Content.Shared.Roles.Components;
namespace Content.Server.Ghost.Roles; namespace Content.Server.Ghost.Roles;
[UsedImplicitly] [UsedImplicitly]
public sealed class GhostRoleSystem : EntitySystem public sealed class GhostRoleSystem : EntitySystem
{ {
[Dependency] private readonly IBanManager _ban = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!; [Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly IEntityManager _ent = default!;
[Dependency] private readonly EuiManager _euiManager = default!; [Dependency] private readonly EuiManager _euiManager = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!; [Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IAdminLogManager _adminLogger = default!; [Dependency] private readonly IAdminLogManager _adminLogger = default!;
@@ -459,6 +464,23 @@ public sealed class GhostRoleSystem : EntitySystem
if (!_ghostRoles.TryGetValue(identifier, out var roleEnt)) if (!_ghostRoles.TryGetValue(identifier, out var roleEnt))
return; return;
TryPrototypes(roleEnt, out var antags, out var jobs);
// Check role bans
if (_ban.IsRoleBanned(player, antags) || _ban.IsRoleBanned(player, jobs))
{
Log.Warning($"Server rejected ghost role request '{roleEnt.Comp.RoleName}' for '{player.Name}' - client missed ban?");
return;
}
// Check role requirements
if (!IsRoleAllowed(player, jobs, antags))
{
Log.Warning($"Server rejected ghost role request '{roleEnt.Comp.RoleName}' for '{player.Name}' - client missed requirement check?");
return;
}
// Decide to do a raffle or not
if (roleEnt.Comp.RaffleConfig is not null) if (roleEnt.Comp.RaffleConfig is not null)
{ {
JoinRaffle(player, identifier); JoinRaffle(player, identifier);
@@ -469,6 +491,78 @@ public sealed class GhostRoleSystem : EntitySystem
} }
} }
/// <summary>
/// Collect all role prototypes on the Ghostrole.
/// </summary>
/// <returns>
/// Returns true if at least on role prototype could be found.
/// </returns>
private bool TryPrototypes(
Entity<GhostRoleComponent> roleEnt,
out List<ProtoId<AntagPrototype>> antags,
out List<ProtoId<JobPrototype>> jobs)
{
antags = [];
jobs = [];
// If there is a mind already, check its mind roles.
// Not sure if this can ever actually happen.
if (TryComp<MindContainerComponent>(roleEnt, out var mindCont)
&& TryComp<MindComponent>(mindCont.Mind, out var mind))
{
foreach (var role in mind.MindRoleContainer.ContainedEntities)
{
if(!TryComp<MindRoleComponent>(role, out var comp))
continue;
if (comp.JobPrototype is not null)
jobs.Add(comp.JobPrototype.Value);
else if (comp.AntagPrototype is not null)
antags.Add(comp.AntagPrototype.Value);
}
return antags.Count > 0 || jobs.Count > 0;
}
if (roleEnt.Comp.JobProto is not null)
jobs.Add(roleEnt.Comp.JobProto.Value);
// If there is no mind, check the mindRole prototypes
foreach (var proto in roleEnt.Comp.MindRoles)
{
if (!_prototype.TryIndex(proto, out var indexed)
|| !indexed.TryGetComponent<MindRoleComponent>(out var comp, _ent.ComponentFactory))
continue;
var roleComp = (MindRoleComponent)comp;
if (roleComp.JobPrototype is not null)
jobs.Add(roleComp.JobPrototype.Value);
else if (roleComp.AntagPrototype is not null)
antags.Add(roleComp.AntagPrototype.Value);
else
Log.Debug($"Mind role '{proto}' of '{roleEnt.Comp.RoleName}' has neither a job or antag prototype specified");
}
return antags.Count > 0 || jobs.Count > 0;
}
/// <summary>
/// Checks if the player passes the requirements for the supplied roles.
/// Returns false if any role fails the check.
/// </summary>
private bool IsRoleAllowed(
ICommonSession player,
List<ProtoId<JobPrototype>>? jobIds,
List<ProtoId<AntagPrototype>>? antagIds)
{
var ev = new IsRoleAllowedEvent(player, jobIds, antagIds);
RaiseLocalEvent(ref ev);
return !ev.Cancelled;
}
/// <summary> /// <summary>
/// Attempts having the player take over the ghost role with the corresponding ID. Does not start a raffle. /// Attempts having the player take over the ghost role with the corresponding ID. Does not start a raffle.
/// </summary> /// </summary>
@@ -571,13 +665,15 @@ public sealed class GhostRoleSystem : EntitySystem
? _timing.CurTime.Add(raffle.Countdown) ? _timing.CurTime.Add(raffle.Countdown)
: TimeSpan.MinValue; : TimeSpan.MinValue;
TryPrototypes((uid, role), out var antags, out var jobs);
roles.Add(new GhostRoleInfo roles.Add(new GhostRoleInfo
{ {
Identifier = id, Identifier = id,
Name = role.RoleName, Name = role.RoleName,
Description = role.RoleDescription, Description = role.RoleDescription,
Rules = role.RoleRules, Rules = role.RoleRules,
Requirements = role.Requirements, RolePrototypes = (jobs, antags),
Kind = kind, Kind = kind,
RafflePlayerCount = rafflePlayerCount, RafflePlayerCount = rafflePlayerCount,
RaffleEndTime = raffleEndTime RaffleEndTime = raffleEndTime

View File

@@ -58,6 +58,9 @@ public sealed class JobWhitelistManager : IPostInjectInit
SendJobWhitelist(session); SendJobWhitelist(session);
} }
/// <summary>
/// Returns false if role whitelist is required but the player does not have it.
/// </summary>
public bool IsAllowed(ICommonSession session, ProtoId<JobPrototype> job) public bool IsAllowed(ICommonSession session, ProtoId<JobPrototype> job)
{ {
if (!_config.GetCVar(CCVars.GameRoleWhitelist)) if (!_config.GetCVar(CCVars.GameRoleWhitelist))

View File

@@ -23,7 +23,7 @@ public sealed class JobWhitelistSystem : EntitySystem
{ {
SubscribeLocalEvent<PrototypesReloadedEventArgs>(OnPrototypesReloaded); SubscribeLocalEvent<PrototypesReloadedEventArgs>(OnPrototypesReloaded);
SubscribeLocalEvent<StationJobsGetCandidatesEvent>(OnStationJobsGetCandidates); SubscribeLocalEvent<StationJobsGetCandidatesEvent>(OnStationJobsGetCandidates);
SubscribeLocalEvent<IsJobAllowedEvent>(OnIsJobAllowed); SubscribeLocalEvent<IsRoleAllowedEvent>(OnIsRoleAllowed);
SubscribeLocalEvent<GetDisallowedJobsEvent>(OnGetDisallowedJobs); SubscribeLocalEvent<GetDisallowedJobsEvent>(OnGetDisallowedJobs);
CacheJobs(); CacheJobs();
@@ -51,11 +51,18 @@ public sealed class JobWhitelistSystem : EntitySystem
} }
} }
private void OnIsJobAllowed(ref IsJobAllowedEvent ev) private void OnIsRoleAllowed(ref IsRoleAllowedEvent ev)
{ {
if (!_manager.IsAllowed(ev.Player, ev.JobId)) if (ev.Jobs is null)
return;
foreach (var proto in ev.Jobs)
{
if (!_manager.IsAllowed(ev.Player, proto))
ev.Cancelled = true; ev.Cancelled = true;
} }
}
//TODO: Antagonist role whitelists?
private void OnGetDisallowedJobs(ref GetDisallowedJobsEvent ev) private void OnGetDisallowedJobs(ref GetDisallowedJobsEvent ev)
{ {

View File

@@ -54,7 +54,7 @@ public sealed class PlayTimeTrackingSystem : EntitySystem
SubscribeLocalEvent<MobStateChangedEvent>(OnMobStateChanged); SubscribeLocalEvent<MobStateChangedEvent>(OnMobStateChanged);
SubscribeLocalEvent<PlayerJoinedLobbyEvent>(OnPlayerJoinedLobby); SubscribeLocalEvent<PlayerJoinedLobbyEvent>(OnPlayerJoinedLobby);
SubscribeLocalEvent<StationJobsGetCandidatesEvent>(OnStationJobsGetCandidates); SubscribeLocalEvent<StationJobsGetCandidatesEvent>(OnStationJobsGetCandidates);
SubscribeLocalEvent<IsJobAllowedEvent>(OnIsJobAllowed); SubscribeLocalEvent<IsRoleAllowedEvent>(OnIsRoleAllowed);
SubscribeLocalEvent<GetDisallowedJobsEvent>(OnGetDisallowedJobs); SubscribeLocalEvent<GetDisallowedJobsEvent>(OnGetDisallowedJobs);
_adminManager.OnPermsChanged += AdminPermsChanged; _adminManager.OnPermsChanged += AdminPermsChanged;
} }
@@ -86,6 +86,9 @@ public sealed class PlayTimeTrackingSystem : EntitySystem
trackers.UnionWith(GetTimedRoles(player)); trackers.UnionWith(GetTimedRoles(player));
} }
/// <summary>
/// Returns true if the player has an attached mob and it is alive (even if in critical).
/// </summary>
private bool IsPlayerAlive(ICommonSession session) private bool IsPlayerAlive(ICommonSession session)
{ {
var attached = session.AttachedEntity; var attached = session.AttachedEntity;
@@ -176,9 +179,9 @@ public sealed class PlayTimeTrackingSystem : EntitySystem
RemoveDisallowedJobs(ev.Player, ev.Jobs); RemoveDisallowedJobs(ev.Player, ev.Jobs);
} }
private void OnIsJobAllowed(ref IsJobAllowedEvent ev) private void OnIsRoleAllowed(ref IsRoleAllowedEvent ev)
{ {
if (!IsAllowed(ev.Player, ev.JobId)) if (!IsAllowed(ev.Player, ev.Jobs) || !IsAllowed(ev.Player, ev.Antags))
ev.Cancelled = true; ev.Cancelled = true;
} }
@@ -187,10 +190,55 @@ public sealed class PlayTimeTrackingSystem : EntitySystem
ev.Jobs.UnionWith(GetDisallowedJobs(ev.Player)); ev.Jobs.UnionWith(GetDisallowedJobs(ev.Player));
} }
public bool IsAllowed(ICommonSession player, string role) /// <summary>
/// Checks if the player meets role requirements.
/// </summary>
/// <param name="player">The player.</param>
/// <param name="jobs">A list of role prototype IDs</param>
/// <returns>Returns true if all requirements were met or there were no requirements.</returns>
public bool IsAllowed(ICommonSession player, List<ProtoId<JobPrototype>>? jobs)
{ {
if (!_prototypes.TryIndex<JobPrototype>(role, out var job) || if (jobs is null)
!_cfg.GetCVar(CCVars.GameRoleTimers)) return true;
foreach (var job in jobs)
{
if (!IsAllowed(player, job))
return false;
}
return true;
}
/// <summary>
/// Checks if the player meets role requirements.
/// </summary>
/// <param name="player">The player.</param>
/// <param name="antags">A list of role prototype IDs</param>
/// <returns>Returns true if all requirements were met or there were no requirements.</returns>
public bool IsAllowed(ICommonSession player, List<ProtoId<AntagPrototype>>? antags)
{
if (antags is null)
return true;
foreach (var antag in antags)
{
if (!IsAllowed(player, antag))
return false;
}
return true;
}
/// <summary>
/// Checks if the player meets role requirements.
/// </summary>
/// <param name="player">The player.</param>
/// <param name="job">A list of role prototype IDs</param>
/// <returns>Returns true if all requirements were met or there were no requirements.</returns>
public bool IsAllowed(ICommonSession player, ProtoId<JobPrototype> job)
{
if (!_cfg.GetCVar(CCVars.GameRoleTimers))
return true; return true;
if (!_tracking.TryGetTrackerTimes(player, out var playTimes)) if (!_tracking.TryGetTrackerTimes(player, out var playTimes))
@@ -199,7 +247,43 @@ public sealed class PlayTimeTrackingSystem : EntitySystem
playTimes = new Dictionary<string, TimeSpan>(); playTimes = new Dictionary<string, TimeSpan>();
} }
return JobRequirements.TryRequirementsMet(job, playTimes, out _, EntityManager, _prototypes, (HumanoidCharacterProfile?) _preferencesManager.GetPreferences(player.UserId).SelectedCharacter); var requirements = _roles.GetRoleRequirements(job);
return JobRequirements.TryRequirementsMet(
requirements,
playTimes,
out _,
EntityManager,
_prototypes,
(HumanoidCharacterProfile?)
_preferencesManager.GetPreferences(player.UserId).SelectedCharacter);
}
/// <summary>
/// Checks if the player meets role requirements.
/// </summary>
/// <param name="player">The player.</param>
/// <param name="antag">A list of role prototype IDs</param>
/// <returns>Returns true if all requirements were met or there were no requirements.</returns>
public bool IsAllowed(ICommonSession player, ProtoId<AntagPrototype> antag)
{
if (!_cfg.GetCVar(CCVars.GameRoleTimers))
return true;
if (!_tracking.TryGetTrackerTimes(player, out var playTimes))
{
Log.Error($"Unable to check playtimes {Environment.StackTrace}");
playTimes = new Dictionary<string, TimeSpan>();
}
var requirements = _roles.GetRoleRequirements(antag);
return JobRequirements.TryRequirementsMet(
requirements,
playTimes,
out _,
EntityManager,
_prototypes,
(HumanoidCharacterProfile?)
_preferencesManager.GetPreferences(player.UserId).SelectedCharacter);
} }
public HashSet<ProtoId<JobPrototype>> GetDisallowedJobs(ICommonSession player) public HashSet<ProtoId<JobPrototype>> GetDisallowedJobs(ICommonSession player)

View File

@@ -371,7 +371,7 @@ public sealed partial class StationJobsSystem
if (weight is not null && job.Weight != weight.Value) if (weight is not null && job.Weight != weight.Value)
continue; continue;
if (!(roleBans == null || !roleBans.Contains(jobId))) if (!(roleBans == null || !roleBans.Contains(jobId))) //TODO: Replace with IsRoleBanned
continue; continue;
availableJobs ??= new List<string>(profile.JobPriorities.Count); availableJobs ??= new List<string>(profile.JobPriorities.Count);

View File

@@ -1,7 +1,9 @@
using Content.Server.Administration.Managers;
using Content.Server.Atmos.Components; using Content.Server.Atmos.Components;
using Content.Server.Body.Components; using Content.Server.Body.Components;
using Content.Server.Chat; using Content.Server.Chat;
using Content.Server.Chat.Managers; using Content.Server.Chat.Managers;
using Content.Server.Ghost;
using Content.Server.Ghost.Roles.Components; using Content.Server.Ghost.Roles.Components;
using Content.Server.Humanoid; using Content.Server.Humanoid;
using Content.Server.IdentityManagement; using Content.Server.IdentityManagement;
@@ -14,6 +16,7 @@ using Content.Server.StationEvents.Components;
using Content.Server.Speech.Components; using Content.Server.Speech.Components;
using Content.Server.Temperature.Components; using Content.Server.Temperature.Components;
using Content.Shared.Body.Components; using Content.Shared.Body.Components;
using Content.Shared.Chat;
using Content.Shared.CombatMode; using Content.Shared.CombatMode;
using Content.Shared.CombatMode.Pacification; using Content.Shared.CombatMode.Pacification;
using Content.Shared.Damage; using Content.Shared.Damage;
@@ -40,6 +43,7 @@ using Content.Shared.Tag;
using Robust.Shared.Player; using Robust.Shared.Player;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
using Content.Shared.NPC.Prototypes; using Content.Shared.NPC.Prototypes;
using Content.Shared.Roles;
namespace Content.Server.Zombies; namespace Content.Server.Zombies;
@@ -52,23 +56,27 @@ namespace Content.Server.Zombies;
public sealed partial class ZombieSystem public sealed partial class ZombieSystem
{ {
[Dependency] private readonly SharedAudioSystem _audio = default!; [Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly IBanManager _ban = default!;
[Dependency] private readonly IChatManager _chatMan = default!; [Dependency] private readonly IChatManager _chatMan = default!;
[Dependency] private readonly SharedCombatModeSystem _combat = default!; [Dependency] private readonly SharedCombatModeSystem _combat = default!;
[Dependency] private readonly NpcFactionSystem _faction = default!; [Dependency] private readonly NpcFactionSystem _faction = default!;
[Dependency] private readonly GhostSystem _ghost = default!;
[Dependency] private readonly SharedHandsSystem _hands = default!; [Dependency] private readonly SharedHandsSystem _hands = default!;
[Dependency] private readonly HumanoidAppearanceSystem _humanoidAppearance = default!; [Dependency] private readonly HumanoidAppearanceSystem _humanoidAppearance = default!;
[Dependency] private readonly IdentitySystem _identity = default!; [Dependency] private readonly IdentitySystem _identity = default!;
[Dependency] private readonly ServerInventorySystem _inventory = default!; [Dependency] private readonly ServerInventorySystem _inventory = default!;
[Dependency] private readonly MindSystem _mind = default!; [Dependency] private readonly MindSystem _mind = default!;
[Dependency] private readonly MovementSpeedModifierSystem _movementSpeedModifier = default!; [Dependency] private readonly MovementSpeedModifierSystem _movementSpeedModifier = default!;
[Dependency] private readonly NameModifierSystem _nameMod = default!;
[Dependency] private readonly NPCSystem _npc = default!; [Dependency] private readonly NPCSystem _npc = default!;
[Dependency] private readonly TagSystem _tag = default!; [Dependency] private readonly TagSystem _tag = default!;
[Dependency] private readonly NameModifierSystem _nameMod = default!;
[Dependency] private readonly ISharedPlayerManager _player = default!; [Dependency] private readonly ISharedPlayerManager _player = default!;
private static readonly ProtoId<TagPrototype> InvalidForGlobalSpawnSpellTag = "InvalidForGlobalSpawnSpell"; private static readonly ProtoId<TagPrototype> InvalidForGlobalSpawnSpellTag = "InvalidForGlobalSpawnSpell";
private static readonly ProtoId<TagPrototype> CannotSuicideTag = "CannotSuicide"; private static readonly ProtoId<TagPrototype> CannotSuicideTag = "CannotSuicide";
private static readonly ProtoId<NpcFactionPrototype> ZombieFaction = "Zombie"; private static readonly ProtoId<NpcFactionPrototype> ZombieFaction = "Zombie";
private static readonly string MindRoleZombie = "MindRoleZombie";
private static readonly List<ProtoId<AntagPrototype>> BannableZombiePrototypes = ["Zombie"];
/// <summary> /// <summary>
/// Handles an entity turning into a zombie when they die or go into crit /// Handles an entity turning into a zombie when they die or go into crit
@@ -103,6 +111,24 @@ public sealed partial class ZombieSystem
if (!Resolve(target, ref mobState, logMissing: false)) if (!Resolve(target, ref mobState, logMissing: false))
return; return;
// Detach role-banned players before zombification
if (TryComp<ActorComponent>(target, out var actor) && _ban.IsRoleBanned(actor.PlayerSession, BannableZombiePrototypes))
{
var sess = actor.PlayerSession;
var message = Loc.GetString("zombie-roleban-ghosted");
if (_mind.TryGetMind(sess, out var playerMindEnt, out var playerMind))
{
// Detach
_ghost.SpawnGhost((playerMindEnt, playerMind), target);
// Notify
_chatMan.DispatchServerMessage(sess, message);
}
else
Log.Error($"Mind for session '{sess}' could not be found");
}
//you're a real zombie now, son. //you're a real zombie now, son.
var zombiecomp = AddComp<ZombieComponent>(target); var zombiecomp = AddComp<ZombieComponent>(target);
@@ -245,7 +271,7 @@ public sealed partial class ZombieSystem
if (hasMind && mind != null && _player.TryGetSessionById(mind.UserId, out var session)) if (hasMind && mind != null && _player.TryGetSessionById(mind.UserId, out var session))
{ {
//Zombie role for player manifest //Zombie role for player manifest
_role.MindAddRole(mindId, "MindRoleZombie", mind: null, silent: true); _role.MindAddRole(mindId, MindRoleZombie, mind: null, silent: true);
//Greeting message for new bebe zombers //Greeting message for new bebe zombers
_chatMan.DispatchServerMessage(session, Loc.GetString("zombie-infection-greeting")); _chatMan.DispatchServerMessage(session, Loc.GetString("zombie-infection-greeting"));
@@ -266,6 +292,7 @@ public sealed partial class ZombieSystem
ghostRole.RoleName = Loc.GetString("zombie-generic"); ghostRole.RoleName = Loc.GetString("zombie-generic");
ghostRole.RoleDescription = Loc.GetString("zombie-role-desc"); ghostRole.RoleDescription = Loc.GetString("zombie-role-desc");
ghostRole.RoleRules = Loc.GetString("zombie-role-rules"); ghostRole.RoleRules = Loc.GetString("zombie-role-rules");
ghostRole.MindRoles.Add(MindRoleZombie);
} }
if (TryComp<HandsComponent>(target, out var handsComp)) if (TryComp<HandsComponent>(target, out var handsComp))

View File

@@ -1,6 +1,8 @@
using System.Net; using System.Net;
using Content.Shared.Database; using Content.Shared.Database;
using Content.Shared.Eui; using Content.Shared.Eui;
using Content.Shared.Roles;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization; using Robust.Shared.Serialization;
namespace Content.Shared.Administration; namespace Content.Shared.Administration;
@@ -21,32 +23,9 @@ public sealed class BanPanelEuiState : EuiStateBase
public static class BanPanelEuiStateMsg public static class BanPanelEuiStateMsg
{ {
[Serializable, NetSerializable] [Serializable, NetSerializable]
public sealed class CreateBanRequest : EuiMessageBase public sealed class CreateBanRequest(Ban ban) : EuiMessageBase
{ {
public string? Player { get; set; } public Ban Ban { get; } = ban;
public string? IpAddress { get; set; }
public ImmutableTypedHwid? Hwid { get; set; }
public uint Minutes { get; set; }
public string Reason { get; set; }
public NoteSeverity Severity { get; set; }
public string[]? Roles { get; set; }
public bool UseLastIp { get; set; }
public bool UseLastHwid { get; set; }
public bool Erase { get; set; }
public CreateBanRequest(string? player, (IPAddress, int)? ipAddress, bool useLastIp, ImmutableTypedHwid? hwid, bool useLastHwid, uint minutes, string reason, NoteSeverity severity, string[]? roles, bool erase)
{
Player = player;
IpAddress = ipAddress == null ? null : $"{ipAddress.Value.Item1}/{ipAddress.Value.Item2}";
UseLastIp = useLastIp;
Hwid = hwid;
UseLastHwid = useLastHwid;
Minutes = minutes;
Reason = reason;
Severity = severity;
Roles = roles;
Erase = erase;
}
} }
[Serializable, NetSerializable] [Serializable, NetSerializable]
@@ -60,3 +39,50 @@ public static class BanPanelEuiStateMsg
} }
} }
} }
/// <summary>
/// Contains all the data related to a particular ban action created by the BanPanel window.
/// </summary>
[Serializable, NetSerializable]
public sealed record Ban
{
public Ban(
string? target,
(IPAddress, int)? ipAddressTuple,
bool useLastIp,
ImmutableTypedHwid? hwid,
bool useLastHwid,
uint banDurationMinutes,
string reason,
NoteSeverity severity,
ProtoId<JobPrototype>[]? bannedJobs,
ProtoId<AntagPrototype>[]? bannedAntags,
bool erase)
{
Target = target;
IpAddress = ipAddressTuple?.Item1.ToString();
IpAddressHid = ipAddressTuple?.Item2.ToString() ?? "0";
UseLastIp = useLastIp;
Hwid = hwid;
UseLastHwid = useLastHwid;
BanDurationMinutes = banDurationMinutes;
Reason = reason;
Severity = severity;
BannedJobs = bannedJobs;
BannedAntags = bannedAntags;
Erase = erase;
}
public readonly string? Target;
public readonly string? IpAddress;
public readonly string? IpAddressHid;
public readonly bool UseLastIp;
public readonly ImmutableTypedHwid? Hwid;
public readonly bool UseLastHwid;
public readonly uint BanDurationMinutes;
public readonly string Reason;
public readonly NoteSeverity Severity;
public readonly ProtoId<JobPrototype>[]? BannedJobs;
public readonly ProtoId<AntagPrototype>[]? BannedAntags;
public readonly bool Erase;
}

View File

@@ -1,5 +1,6 @@
using Content.Shared.Eui; using Content.Shared.Eui;
using Content.Shared.Roles; using Content.Shared.Roles;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization; using Robust.Shared.Serialization;
namespace Content.Shared.Ghost.Roles namespace Content.Shared.Ghost.Roles
@@ -12,11 +13,10 @@ namespace Content.Shared.Ghost.Roles
public string Description { get; set; } public string Description { get; set; }
public string Rules { get; set; } public string Rules { get; set; }
// TODO ROLE TIMERS /// <summary>
// Actually make use of / enforce this requirement? /// A list of all antag and job prototype IDs of the ghost role and its mind role(s).
// Why is this even here. /// </summary>
// Move to ghost role prototype & respect CCvars.GameRoleTimerOverride public (List<ProtoId<JobPrototype>>?,List<ProtoId<AntagPrototype>>?) RolePrototypes;
public HashSet<JobRequirement>? Requirements { get; set; }
/// <inheritdoc cref="GhostRoleKind"/> /// <inheritdoc cref="GhostRoleKind"/>
public GhostRoleKind Kind { get; set; } public GhostRoleKind Kind { get; set; }

View File

@@ -11,24 +11,40 @@ public sealed class MsgRoleBans : NetMessage
{ {
public override MsgGroups MsgGroup => MsgGroups.EntityEvent; public override MsgGroups MsgGroup => MsgGroups.EntityEvent;
public List<string> Bans = new(); public List<string> JobBans = new();
public List<string> AntagBans = new();
public override void ReadFromBuffer(NetIncomingMessage buffer, IRobustSerializer serializer) public override void ReadFromBuffer(NetIncomingMessage buffer, IRobustSerializer serializer)
{ {
var count = buffer.ReadVariableInt32(); var jobCount = buffer.ReadVariableInt32();
Bans.EnsureCapacity(count); JobBans.EnsureCapacity(jobCount);
for (var i = 0; i < count; i++) for (var i = 0; i < jobCount; i++)
{ {
Bans.Add(buffer.ReadString()); JobBans.Add(buffer.ReadString());
}
var antagCount = buffer.ReadVariableInt32();
AntagBans.EnsureCapacity(antagCount);
for (var i = 0; i < antagCount; i++)
{
AntagBans.Add(buffer.ReadString());
} }
} }
public override void WriteToBuffer(NetOutgoingMessage buffer, IRobustSerializer serializer) public override void WriteToBuffer(NetOutgoingMessage buffer, IRobustSerializer serializer)
{ {
buffer.WriteVariableInt32(Bans.Count); buffer.WriteVariableInt32(JobBans.Count);
foreach (var ban in Bans) foreach (var ban in JobBans)
{
buffer.Write(ban);
}
buffer.WriteVariableInt32(AntagBans.Count);
foreach (var ban in AntagBans)
{ {
buffer.Write(ban); buffer.Write(ban);
} }

View File

@@ -10,6 +10,12 @@ namespace Content.Shared.Roles;
[Prototype] [Prototype]
public sealed partial class AntagPrototype : IPrototype public sealed partial class AntagPrototype : IPrototype
{ {
// The name to group all antagonists under. Equivalent to DepartmentPrototype IDs.
public static readonly string GroupName = "Antagonist";
// The colour to group all antagonists using. Equivalent to DepartmentPrototype Color fields.
public static readonly Color GroupColor = Color.Red;
[ViewVariables] [ViewVariables]
[IdDataField] [IdDataField]
public string ID { get; private set; } = default!; public string ID { get; private set; } = default!;
@@ -41,8 +47,6 @@ public sealed partial class AntagPrototype : IPrototype
/// <summary> /// <summary>
/// Requirements that must be met to opt in to this antag role. /// Requirements that must be met to opt in to this antag role.
/// </summary> /// </summary>
// TODO ROLE TIMERS
// Actually check if the requirements are met. Because apparently this is actually unused.
[DataField, Access(typeof(SharedRoleSystem), Other = AccessPermissions.None)] [DataField, Access(typeof(SharedRoleSystem), Other = AccessPermissions.None)]
public HashSet<JobRequirement>? Requirements; public HashSet<JobRequirement>? Requirements;

View File

@@ -8,6 +8,13 @@ namespace Content.Shared.Roles;
public static class JobRequirements public static class JobRequirements
{ {
/// <summary>
/// Checks if the requirements of the job are met by the provided play-times.
/// </summary>
/// <param name="job"> The job to test. </param>
/// <param name="playTimes"> The playtimes used for the check. </param>
/// <param name="reason"> If the requirements were not met, details are provided here. </param>
/// <returns>Returns true if all requirements were met or there were no requirements.</returns>
public static bool TryRequirementsMet( public static bool TryRequirementsMet(
JobPrototype job, JobPrototype job,
IReadOnlyDictionary<string, TimeSpan> playTimes, IReadOnlyDictionary<string, TimeSpan> playTimes,
@@ -17,7 +24,25 @@ public static class JobRequirements
HumanoidCharacterProfile? profile) HumanoidCharacterProfile? profile)
{ {
var sys = entManager.System<SharedRoleSystem>(); var sys = entManager.System<SharedRoleSystem>();
var requirements = sys.GetJobRequirement(job); var requirements = sys.GetRoleRequirements(job);
return TryRequirementsMet(requirements, playTimes, out reason, entManager, protoManager, profile);
}
/// <summary>
/// Checks if the list of requirements are met by the provided play-times.
/// </summary>
/// <param name="requirements"> The requirements to test. </param>
/// <param name="playTimes"> The playtimes used for the check. </param>
/// <param name="reason"> If the requirements were not met, details are provided here. </param>
/// <returns>Returns true if all requirements were met or there were no requirements.</returns>
public static bool TryRequirementsMet(
HashSet<JobRequirement>? requirements,
IReadOnlyDictionary<string, TimeSpan> playTimes,
[NotNullWhen(false)] out FormattedMessage? reason,
IEntityManager entManager,
IPrototypeManager protoManager,
HumanoidCharacterProfile? profile)
{
reason = null; reason = null;
if (requirements == null) if (requirements == null)
return true; return true;

View File

@@ -667,10 +667,13 @@ public abstract class SharedRoleSystem : EntitySystem
_audio.PlayGlobal(sound, session); _audio.PlayGlobal(sound, session);
} }
// TODO ROLES Change to readonly. // TODO ROLES Change to readonly?
// Passing around a reference to a prototype's hashset makes me uncomfortable because it might be accidentally // Passing around a reference to a prototype's hashset makes me uncomfortable because it might be accidentally
// mutated. // mutated.
public HashSet<JobRequirement>? GetJobRequirement(JobPrototype job) /// <summary>
/// Returns the list of requirements for a role, or null. May be altered by requirement overrides.
/// </summary>
public HashSet<JobRequirement>? GetRoleRequirements(JobPrototype job)
{ {
if (_requirementOverride != null && _requirementOverride.Jobs.TryGetValue(job.ID, out var req)) if (_requirementOverride != null && _requirementOverride.Jobs.TryGetValue(job.ID, out var req))
return req; return req;
@@ -678,33 +681,30 @@ public abstract class SharedRoleSystem : EntitySystem
return job.Requirements; return job.Requirements;
} }
// TODO ROLES Change to readonly. // TODO ROLES Change to readonly?
public HashSet<JobRequirement>? GetJobRequirement(ProtoId<JobPrototype> job) /// <inheritdoc cref="GetRoleRequirements(JobPrototype)"/>
public HashSet<JobRequirement>? GetRoleRequirements(AntagPrototype antag)
{ {
if (_requirementOverride != null && _requirementOverride.Jobs.TryGetValue(job, out var req)) if (_requirementOverride != null && _requirementOverride.Jobs.TryGetValue(antag.ID, out var req))
return req;
return _prototypes.Index(job).Requirements;
}
// TODO ROLES Change to readonly.
public HashSet<JobRequirement>? GetAntagRequirement(ProtoId<AntagPrototype> antag)
{
if (_requirementOverride != null && _requirementOverride.Antags.TryGetValue(antag, out var req))
return req;
return _prototypes.Index(antag).Requirements;
}
// TODO ROLES Change to readonly.
public HashSet<JobRequirement>? GetAntagRequirement(AntagPrototype antag)
{
if (_requirementOverride != null && _requirementOverride.Antags.TryGetValue(antag.ID, out var req))
return req; return req;
return antag.Requirements; return antag.Requirements;
} }
// TODO ROLES Change to readonly?
/// <inheritdoc cref="GetRoleRequirements(JobPrototype)"/>
public HashSet<JobRequirement>? GetRoleRequirements(ProtoId<JobPrototype> jobId)
{
return _prototypes.TryIndex(jobId, out var job) ? GetRoleRequirements(job) : null;
}
// TODO ROLES Change to readonly?
/// <inheritdoc cref="GetRoleRequirements(JobPrototype)"/>
public HashSet<JobRequirement>? GetRoleRequirements(ProtoId<AntagPrototype> antagId)
{
return _prototypes.TryIndex(antagId, out var antag) ? GetRoleRequirements(antag) : null;
}
/// <summary> /// <summary>
/// Returns the localized name of a role type's subtype. If the provided subtype parameter turns out to be empty, it returns the localized name of the role type instead. /// Returns the localized name of a role type's subtype. If the provided subtype parameter turns out to be empty, it returns the localized name of the role type instead.
/// </summary> /// </summary>

View File

@@ -9,3 +9,5 @@ zombie-role-rules = You are a [color={role-type-team-antagonist-color}][bold]{ro
zombie-permadeath = This time, you're dead for real. zombie-permadeath = This time, you're dead for real.
zombification-resistance-coefficient-value = - [color=violet]Infection[/color] chance reduced by [color=lightblue]{$value}%[/color]. zombification-resistance-coefficient-value = - [color=violet]Infection[/color] chance reduced by [color=lightblue]{$value}%[/color].
zombie-roleban-ghosted = You have been ghosted because you are banned from playing the Zombie role.

View File

@@ -1,3 +1,7 @@
# The mind roles specified here will be overwritten by the actual entities' GhostRoleComponent when they spawn
# But the mind roles specified here are the ones checked for role bans when taking a ghost role!
# TODO make this simpler
- type: entity - type: entity
abstract: true abstract: true
parent: MarkerBase parent: MarkerBase
@@ -88,7 +92,7 @@
- type: GhostRole - type: GhostRole
rules: ghost-role-information-rules-default-team-antagonist rules: ghost-role-information-rules-default-team-antagonist
mindRoles: mindRoles:
- MindRoleGhostRoleTeamAntagonist - MindRoleNukeops
raffle: raffle:
settings: default settings: default
- type: GhostRoleMobSpawner - type: GhostRoleMobSpawner
@@ -128,7 +132,7 @@
description: roles-antag-nuclear-operative-commander-objective description: roles-antag-nuclear-operative-commander-objective
rules: ghost-role-information-rules-default-team-antagonist rules: ghost-role-information-rules-default-team-antagonist
mindRoles: mindRoles:
- MindRoleGhostRoleTeamAntagonist - MindRoleNukeopsCommander
- type: entity - type: entity
categories: [ HideSpawnMenu, Spawner ] categories: [ HideSpawnMenu, Spawner ]
@@ -140,7 +144,7 @@
description: roles-antag-nuclear-operative-agent-objective description: roles-antag-nuclear-operative-agent-objective
rules: ghost-role-information-rules-default-team-antagonist rules: ghost-role-information-rules-default-team-antagonist
mindRoles: mindRoles:
- MindRoleGhostRoleTeamAntagonist - MindRoleNukeopsMedic
- type: entity - type: entity
categories: [ HideSpawnMenu, Spawner ] categories: [ HideSpawnMenu, Spawner ]
@@ -152,7 +156,7 @@
description: roles-antag-nuclear-operative-objective description: roles-antag-nuclear-operative-objective
rules: ghost-role-information-rules-default-team-antagonist rules: ghost-role-information-rules-default-team-antagonist
mindRoles: mindRoles:
- MindRoleGhostRoleTeamAntagonist - MindRoleNukeops
- type: entity - type: entity
categories: [ HideSpawnMenu, Spawner ] categories: [ HideSpawnMenu, Spawner ]
@@ -164,7 +168,7 @@
description: ghost-role-information-space-dragon-description description: ghost-role-information-space-dragon-description
rules: ghost-role-information-space-dragon-rules rules: ghost-role-information-space-dragon-rules
mindRoles: mindRoles:
- MindRoleGhostRoleTeamAntagonist - MindRoleDragon
- type: Sprite - type: Sprite
layers: layers:
- state: green - state: green
@@ -181,7 +185,7 @@
description: ghost-role-information-space-ninja-description description: ghost-role-information-space-ninja-description
rules: ghost-role-information-antagonist-rules rules: ghost-role-information-antagonist-rules
mindRoles: mindRoles:
- MindRoleGhostRoleSoloAntagonist - MindRoleNinja
raffle: raffle:
settings: default settings: default
- type: Sprite - type: Sprite
@@ -201,7 +205,7 @@
description: ghost-role-information-paradox-clone-description description: ghost-role-information-paradox-clone-description
rules: ghost-role-information-antagonist-rules rules: ghost-role-information-antagonist-rules
mindRoles: mindRoles:
- MindRoleGhostRoleSoloAntagonist - MindRoleParadoxClone
raffle: raffle:
settings: default settings: default
- type: Sprite - type: Sprite
@@ -232,6 +236,8 @@
name: ghost-role-information-derelict-cyborg-name name: ghost-role-information-derelict-cyborg-name
description: ghost-role-information-derelict-cyborg-description description: ghost-role-information-derelict-cyborg-description
rules: ghost-role-information-silicon-rules rules: ghost-role-information-silicon-rules
mindRoles:
- MindRoleSubvertedSilicon
raffle: raffle:
settings: default settings: default
- type: Sprite - type: Sprite
@@ -300,7 +306,7 @@
name: ghost-role-information-wizard-name name: ghost-role-information-wizard-name
description: ghost-role-information-wizard-desc description: ghost-role-information-wizard-desc
mindRoles: mindRoles:
- MindRoleGhostRoleSoloAntagonist - MindRoleWizard
raffle: raffle:
settings: default settings: default
- type: Sprite - type: Sprite

View File

@@ -57,6 +57,7 @@
settings: short settings: short
mindRoles: mindRoles:
- MindRoleGhostRoleFamiliar - MindRoleGhostRoleFamiliar
job: DeathSquad
- type: Loadout - type: Loadout
prototypes: [ DeathSquadGear ] prototypes: [ DeathSquadGear ]
roleLoadout: [ RoleSurvivalEVA ] roleLoadout: [ RoleSurvivalEVA ]
@@ -536,6 +537,7 @@
rules: ghost-role-information-nonantagonist-rules rules: ghost-role-information-nonantagonist-rules
raffle: raffle:
settings: short settings: short
job: CBURN
- type: RandomMetadata - type: RandomMetadata
nameSegments: nameSegments:
- NamesMilitaryFirst - NamesMilitaryFirst
@@ -564,6 +566,7 @@
rules: ghost-role-information-nonantagonist-rules rules: ghost-role-information-nonantagonist-rules
raffle: raffle:
settings: default settings: default
job: CentralCommandOfficial
- type: Loadout - type: Loadout
prototypes: [ CentcomGear ] prototypes: [ CentcomGear ]
roleLoadout: [ RoleSurvivalStandard ] roleLoadout: [ RoleSurvivalStandard ]

View File

@@ -560,6 +560,7 @@
rules: ghost-role-information-silicon-rules rules: ghost-role-information-silicon-rules
raffle: raffle:
settings: default settings: default
job: Borg
- type: GhostTakeoverAvailable - type: GhostTakeoverAvailable
- type: entity - type: entity
@@ -593,6 +594,7 @@
rules: ghost-role-information-silicon-rules rules: ghost-role-information-silicon-rules
raffle: raffle:
settings: default settings: default
job: Borg
- type: GhostTakeoverAvailable - type: GhostTakeoverAvailable
- type: entity - type: entity
@@ -646,6 +648,7 @@
raffle: raffle:
settings: default settings: default
reregister: false reregister: false
job: Borg
- type: GhostTakeoverAvailable - type: GhostTakeoverAvailable
- type: entity - type: entity
@@ -680,6 +683,7 @@
raffle: raffle:
settings: default settings: default
reregister: false reregister: false
job: Borg
- type: GhostTakeoverAvailable - type: GhostTakeoverAvailable
- type: entity - type: entity
@@ -716,6 +720,7 @@
raffle: raffle:
settings: default settings: default
reregister: false reregister: false
job: Borg
- type: GhostTakeoverAvailable - type: GhostTakeoverAvailable
- type: entity - type: entity
@@ -753,6 +758,7 @@
raffle: raffle:
settings: default settings: default
reregister: false reregister: false
job: Borg
- type: GhostTakeoverAvailable - type: GhostTakeoverAvailable
- type: entity - type: entity
@@ -789,6 +795,7 @@
raffle: raffle:
settings: default settings: default
reregister: false reregister: false
job: Borg
- type: GhostTakeoverAvailable - type: GhostTakeoverAvailable
- type: entity - type: entity
@@ -824,4 +831,5 @@
raffle: raffle:
settings: default settings: default
reregister: false reregister: false
job: Borg
- type: GhostTakeoverAvailable - type: GhostTakeoverAvailable

View File

@@ -115,5 +115,6 @@
- MindRoleGhostRoleSilicon - MindRoleGhostRoleSilicon
raffle: raffle:
settings: default settings: default
job: Borg
- type: GhostRoleMobSpawner - type: GhostRoleMobSpawner
prototype: PlayerBorgSyndicateAssaultBattery prototype: PlayerBorgSyndicateAssaultBattery