]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Antag Rolebans (#35966)
authorErrant <35878406+Errant-4@users.noreply.github.com>
Wed, 17 Sep 2025 21:59:07 +0000 (23:59 +0200)
committerGitHub <noreply@github.com>
Wed, 17 Sep 2025 21:59:07 +0000 (23:59 +0200)
Co-authored-by: beck-thompson <beck314159@hotmail.com>
Co-authored-by: Hannah Giovanna Dawson <karakkaraz@gmail.com>
33 files changed:
Content.Client/Administration/UI/BanPanel/BanPanel.xaml.cs
Content.Client/Administration/UI/BanPanel/BanPanelEui.cs
Content.Client/Lobby/UI/HumanoidProfileEditor.xaml.cs
Content.Client/Players/PlayTimeTracking/JobRequirementsManager.cs
Content.Client/UserInterface/Systems/Ghost/Controls/Roles/GhostRolesEui.cs
Content.Server/Administration/BanPanelEui.cs
Content.Server/Administration/Commands/RoleBanCommand.cs
Content.Server/Administration/Managers/BanManager.cs
Content.Server/Administration/Managers/IBanManager.cs
Content.Server/Antag/AntagSelectionSystem.API.cs
Content.Server/Antag/AntagSelectionSystem.cs
Content.Server/Database/ServerDbBase.cs
Content.Server/GameTicking/Events/IsJobAllowedEvent.cs [deleted file]
Content.Server/GameTicking/Events/IsRoleAllowedEvent.cs [new file with mode: 0644]
Content.Server/GameTicking/GameTicker.Spawning.cs
Content.Server/Ghost/Roles/Components/GhostRoleComponent.cs
Content.Server/Ghost/Roles/GhostRoleSystem.cs
Content.Server/Players/JobWhitelist/JobWhitelistManager.cs
Content.Server/Players/JobWhitelist/JobWhitelistSystem.cs
Content.Server/Players/PlayTimeTracking/PlayTimeTrackingSystem.cs
Content.Server/Station/Systems/StationJobsSystem.Roundstart.cs
Content.Server/Zombies/ZombieSystem.Transform.cs
Content.Shared/Administration/BanPanelEuiState.cs
Content.Shared/Ghost/Roles/GhostRolesEuiMessages.cs
Content.Shared/Players/MsgRoleBans.cs
Content.Shared/Roles/AntagPrototype.cs
Content.Shared/Roles/JobRequirements.cs
Content.Shared/Roles/SharedRoleSystem.cs
Resources/Locale/en-US/zombies/zombie.ftl
Resources/Prototypes/Entities/Markers/Spawners/ghost_roles.yml
Resources/Prototypes/Entities/Mobs/Player/humanoid.yml
Resources/Prototypes/Entities/Mobs/Player/silicon.yml
Resources/Prototypes/Entities/Objects/Devices/Syndicate_Gadgets/reinforcement_teleporter.yml

index c7aed30c1cdf98f32b449509dfae2b04261a7034..d20c7416732de11a2f76079e41993b83bd1ad623 100644 (file)
@@ -24,7 +24,7 @@ namespace Content.Client.Administration.UI.BanPanel;
 [GenerateTypedNameReferences]
 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;
     private string? PlayerUsername { 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
     // have to know how the controls are nested, which makes the code more complicated.
     // Role group name -> the role buttons themselves.
-    private readonly Dictionary<string, List<Button>> _roleCheckboxes = new();
-    private readonly ISawmill _banpanelSawmill;
+    private readonly Dictionary<string, List<(Button, IPrototype)>> _roleCheckboxes = new();
+    private readonly ISawmill _banPanelSawmill;
 
     [Dependency] private readonly IGameTiming _gameTiming = default!;
     [Dependency] private readonly IConfigurationManager _cfg = default!;
@@ -79,7 +79,7 @@ public sealed partial class BanPanel : DefaultWindow
     {
         RobustXamlLoader.Load(this);
         IoCManager.InjectDependencies(this);
-        _banpanelSawmill = _logManager.GetSawmill("admin.banpanel");
+        _banPanelSawmill = _logManager.GetSawmill("admin.banpanel");
         PlayerList.OnSelectionChanged += OnPlayerSelectionChanged;
         PlayerNameLine.OnFocusExit += _ => OnPlayerNameChanged();
         PlayerCheckbox.OnPressed += _ =>
@@ -110,7 +110,7 @@ public sealed partial class BanPanel : DefaultWindow
             TypeOption.SelectId(args.Id);
             OnTypeChanged();
         };
-        LastConnCheckbox.OnPressed += args =>
+        LastConnCheckbox.OnPressed += _ =>
         {
             IpLine.ModulateSelfOverride = null;
             HwidLine.ModulateSelfOverride = null;
@@ -164,7 +164,7 @@ public sealed partial class BanPanel : DefaultWindow
 
         var antagRoles = _protoMan.EnumeratePrototypes<AntagPrototype>()
                                   .OrderBy(x => x.ID);
-        CreateRoleGroup("Antagonist", Color.Red, antagRoles);
+        CreateRoleGroup(AntagPrototype.GroupName, AntagPrototype.GroupColor, antagRoles);
     }
 
     /// <summary>
@@ -236,14 +236,14 @@ public sealed partial class BanPanel : DefaultWindow
         {
             foreach (var role in _roleCheckboxes[groupName])
             {
-                role.Pressed = args.Pressed;
+                role.Item1.Pressed = args.Pressed;
             }
 
             if (args.Pressed)
             {
                 if (!Enum.TryParse(_cfg.GetCVar(CCVars.DepartmentBanDefaultSeverity), true, out NoteSeverity newSeverity))
                 {
-                    _banpanelSawmill
+                    _banPanelSawmill
                         .Warning("Departmental role ban severity could not be parsed from config!");
                     return;
                 }
@@ -255,14 +255,14 @@ public sealed partial class BanPanel : DefaultWindow
                 {
                     foreach (var button in roleButtons)
                     {
-                        if (button.Pressed)
+                        if (button.Item1.Pressed)
                             return;
                     }
                 }
 
                 if (!Enum.TryParse(_cfg.GetCVar(CCVars.RoleBanDefaultSeverity), true, out NoteSeverity newSeverity))
                 {
-                    _banpanelSawmill
+                    _banPanelSawmill
                         .Warning("Role ban severity could not be parsed from config!");
                     return;
                 }
@@ -294,7 +294,7 @@ public sealed partial class BanPanel : DefaultWindow
     }
 
     /// <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.
     /// </summary>
     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 roleCheckButton = new Button
         {
-            Name = $"{role}RoleCheckbox",
+            Name = role,
             Text = role,
             ToggleMode = true,
         };
         roleCheckButton.OnToggled += args =>
         {
             // 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;
             else
                 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
         // 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.
-        if (_protoMan.TryIndex<JobPrototype>(role, out var jobPrototype) && _protoMan.Resolve(jobPrototype.Icon, out var iconProto))
+        // // I know the ban manager is doing the same thing, but that should not leak into UI code.
+        if (jobPrototype is not null && _protoMan.TryIndex(jobPrototype.Icon, out var iconProto))
         {
             var jobIconTexture = new TextureRect
             {
@@ -335,7 +348,7 @@ public sealed partial class BanPanel : DefaultWindow
         roleGroupInnerContainer.AddChild(roleCheckboxContainer);
 
         _roleCheckboxes.TryAdd(group, []);
-        _roleCheckboxes[group].Add(roleCheckButton);
+        _roleCheckboxes[group].Add((roleCheckButton, rolePrototype));
     }
 
     public void UpdateBanFlag(bool newFlag)
@@ -488,7 +501,7 @@ public sealed partial class BanPanel : DefaultWindow
                     newSeverity = serverSeverity;
                 else
                 {
-                    _banpanelSawmill
+                    _banPanelSawmill
                         .Warning("Server ban severity could not be parsed from config!");
                 }
 
@@ -501,7 +514,7 @@ public sealed partial class BanPanel : DefaultWindow
                     }
                     else
                     {
-                        _banpanelSawmill
+                        _banPanelSawmill
                             .Warning("Role ban severity could not be parsed from config!");
                     }
                     break;
@@ -546,34 +559,51 @@ public sealed partial class BanPanel : DefaultWindow
 
     private void SubmitButtonOnOnPressed(BaseButton.ButtonEventArgs obj)
     {
-        string[]? roles = null;
+        ProtoId<JobPrototype>[]? jobs = null;
+        ProtoId<AntagPrototype>[]? antags = null;
+
         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)
                 throw new DebugAssertException("RoleCheckboxes was empty");
 
             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;
+
                 return;
             }
 
-            roles = rolesList.ToArray();
+            jobs = jobList.ToArray();
+            antags = antagList.ToArray();
         }
 
         if (TypeOption.SelectedId == (int) Types.None)
         {
             TypeOption.ModulateSelfOverride = Color.Red;
             Tabs.CurrentTab = (int) TabNumbers.BasicInfo;
+
             return;
         }
 
@@ -585,6 +615,7 @@ public sealed partial class BanPanel : DefaultWindow
             ReasonTextEdit.GrabKeyboardFocus();
             ReasonTextEdit.ModulateSelfOverride = Color.Red;
             ReasonTextEdit.OnKeyBindDown += ResetTextEditor;
+
             return;
         }
 
@@ -593,6 +624,7 @@ public sealed partial class BanPanel : DefaultWindow
             ButtonResetOn = _gameTiming.CurTime.Add(TimeSpan.FromSeconds(3));
             SubmitButton.ModulateSelfOverride = Color.Red;
             SubmitButton.Text = Loc.GetString("ban-panel-confirm");
+
             return;
         }
 
@@ -601,7 +633,22 @@ public sealed partial class BanPanel : DefaultWindow
         var useLastHwid = HwidCheckbox.Pressed && LastConnCheckbox.Pressed && Hwid is null;
         var severity = (NoteSeverity) SeverityOption.SelectedId;
         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)
index 940a55e010a4372dc64f9e67cb17d36eb684e996..ac17576361cab8d4f3da7189669eb4e460f84dc5 100644 (file)
@@ -14,8 +14,7 @@ public sealed class BanPanelEui : BaseEui
     {
         BanPanel = new BanPanel();
         BanPanel.OnClose += () => SendMessage(new CloseEuiMessage());
-        BanPanel.BanSubmitted += (player, ip, useLastIp, hwid, useLastHwid, minutes, reason, severity, roles, erase)
-            => SendMessage(new BanPanelEuiStateMsg.CreateBanRequest(player, ip, useLastIp, hwid, useLastHwid, minutes, reason, severity, roles, erase));
+        BanPanel.BanSubmitted += ban => SendMessage(new BanPanelEuiStateMsg.CreateBanRequest(ban));
         BanPanel.PlayerChanged += player => SendMessage(new BanPanelEuiStateMsg.GetPlayerInfoRequest(player));
     }
 
index dfdfece979adaf361233d2c7f64e2d9f984aae12..609b633fe440b9a7829ae7ba9c3dc464f39b8474 100644 (file)
@@ -660,8 +660,10 @@ namespace Content.Client.Lobby.UI
                 selector.Setup(items, title, 250, description, guides: antag.Guides);
                 selector.Select(Profile?.AntagPreferences.Contains(antag.ID) == true ? 0 : 1);
 
-                var requirements = _entManager.System<SharedRoleSystem>().GetAntagRequirement(antag);
-                if (!_requirements.CheckRoleRequirements(requirements, (HumanoidCharacterProfile?)_preferencesManager.Preferences?.SelectedCharacter, out var reason))
+                if (!_requirements.IsAllowed(
+                        antag,
+                        (HumanoidCharacterProfile?)_preferencesManager.Preferences?.SelectedCharacter,
+                        out var reason))
                 {
                     selector.LockRequirements(reason);
                     Profile = Profile?.WithAntagPreference(antag.ID, false);
index 314b59eda96fe482b50031141d6b9964c0f659f0..d085d9005cdf5757e58d892a4ad5f0c0b53b032b 100644 (file)
@@ -1,5 +1,4 @@
 using System.Diagnostics.CodeAnalysis;
-using Content.Client.Lobby;
 using Content.Shared.CCVar;
 using Content.Shared.Players;
 using Content.Shared.Players.JobWhitelist;
@@ -26,7 +25,8 @@ public sealed class JobRequirementsManager : ISharedPlaytimeManager
     [Dependency] private readonly IPrototypeManager _prototypes = default!;
 
     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 ISawmill _sawmill = default!;
@@ -52,16 +52,19 @@ public sealed class JobRequirementsManager : ISharedPlaytimeManager
             // Reset on disconnect, just in case.
             _roles.Clear();
             _jobWhitelists.Clear();
-            _roleBans.Clear();
+            _jobBans.Clear();
+            _antagBans.Clear();
         }
     }
 
     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();
-        _roleBans.AddRange(message.Bans);
+        _jobBans.Clear();
+        _jobBans.AddRange(message.JobBans);
+        _antagBans.Clear();
+        _antagBans.AddRange(message.AntagBans);
         Updated?.Invoke();
     }
 
@@ -90,33 +93,97 @@ public sealed class JobRequirementsManager : ISharedPlaytimeManager
         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;
 
-        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"));
             return false;
         }
 
+        // Check whitelist requirements
         if (!CheckWhitelist(job, out reason))
             return false;
 
-        var player = _playerManager.LocalSession;
-        if (player == null)
-            return true;
+        // Check other role requirements
+        var reqs = _entManager.System<SharedRoleSystem>().GetRoleRequirements(job);
+        if (!CheckRoleRequirements(reqs, profile, out reason))
+            return false;
 
-        return CheckRoleRequirements(job, profile, out reason);
+        return true;
     }
 
-    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);
-        return CheckRoleRequirements(reqs, profile, out reason);
+        // Check the player's bans
+        if (_antagBans.Contains(antag.ID))
+        {
+            reason = FormattedMessage.FromUnformatted(Loc.GetString("role-ban"));
+            return false;
+        }
+
+        // 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;
     }
 
-    public bool CheckRoleRequirements(HashSet<JobRequirement>? requirements, HumanoidCharacterProfile? profile, [NotNullWhen(false)] out FormattedMessage? reason)
+    // 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;
 
@@ -151,6 +218,15 @@ public sealed class JobRequirementsManager : ISharedPlaytimeManager
         return true;
     }
 
+    public bool CheckWhitelist(AntagPrototype antag, [NotNullWhen(false)] out FormattedMessage? reason)
+    {
+        reason = default;
+
+        // TODO: Implement antag whitelisting.
+
+        return true;
+    }
+
     public TimeSpan FetchOverallPlaytime()
     {
         return _roles.TryGetValue("Overall", out var overallPlaytime) ? overallPlaytime : TimeSpan.Zero;
index 1cf1e55103d3c2c8a626cd6202c0420f32fa4288..86dd6d209286fce0614c985544ea90b880e58a1c 100644 (file)
@@ -90,23 +90,25 @@ namespace Content.Client.UserInterface.Systems.Ghost.Controls.Roles
             var spriteSystem = sysManager.GetEntitySystem<SpriteSystem>();
             var requirementsManager = IoCManager.Resolve<JobRequirementsManager>();
 
-            // TODO: role.Requirements value doesn't work at all as an equality key, this must be fixed
             // Grouping roles
             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
             foreach (var group in groupedRoles)
             {
+                var reason = group.Key.reason;
                 var name = group.Key.Name;
                 var description = group.Key.Description;
-                var hasAccess = requirementsManager.CheckRoleRequirements(
-                    group.Key.Requirements,
-                    null,
-                    out var reason);
+                var prototypesAllowed = group.Key.Item3;
 
                 // 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
index 0a09ad557f511906a3cd2c5820c925edfeb9d30c..4a4b7218729b3bce2056948907bc98bbaf67aef6 100644 (file)
@@ -7,9 +7,7 @@ using Content.Server.EUI;
 using Content.Shared.Administration;
 using Content.Shared.Database;
 using Content.Shared.Eui;
-using Content.Shared.Roles;
 using Robust.Shared.Network;
-using Robust.Shared.Prototypes;
 
 namespace Content.Server.Administration;
 
@@ -21,7 +19,6 @@ public sealed class BanPanelEui : BaseEui
     [Dependency] private readonly IPlayerLocator _playerLocator = default!;
     [Dependency] private readonly IChatManager _chat = default!;
     [Dependency] private readonly IAdminManager _admins = default!;
-    [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
 
     private readonly ISawmill _sawmill;
 
@@ -52,7 +49,7 @@ public sealed class BanPanelEui : BaseEui
         switch (msg)
         {
             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;
             case BanPanelEuiStateMsg.GetPlayerInfoRequest r:
                 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))
         {
             _sawmill.Warning($"{Player.Name} ({Player.UserId}) tried to create a ban with no ban flag");
+
             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"));
+
             return;
         }
 
         (IPAddress, int)? addressRange = null;
-        if (ipAddressString is not null)
+        if (ban.IpAddress is not null)
         {
-            var hid = "0";
-            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)
+            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)
             {
                 _chat.DispatchServerMessage(Player, Loc.GetString("ban-panel-invalid-ip"));
                 return;
@@ -94,12 +88,12 @@ public sealed class BanPanelEui : BaseEui
             addressRange = (ipAddress, (int) hidInt);
         }
 
-        var targetUid = target is not null ? PlayerId : null;
-        addressRange = useLastIp && LastAddress is not null ? (LastAddress, LastAddress.AddressFamily == AddressFamily.InterNetworkV6 ? Ipv6_CIDR : Ipv4_CIDR) : addressRange;
-        var targetHWid = useLastHwid ? LastHwid : hwid;
-        if (target != null && target != PlayerName || Guid.TryParse(target, out var parsed) && parsed != PlayerId)
+        var targetUid = ban.Target is not null ? PlayerId : null;
+        addressRange = ban.UseLastIp && LastAddress is not null ? (LastAddress, LastAddress.AddressFamily == AddressFamily.InterNetworkV6 ? Ipv6_CIDR : Ipv4_CIDR) : addressRange;
+        var targetHWid = ban.UseLastHwid ? LastHwid : ban.Hwid;
+        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)
             {
                 _chat.DispatchServerMessage(Player, Loc.GetString("cmd-ban-player"));
@@ -107,7 +101,7 @@ public sealed class BanPanelEui : BaseEui
             }
             targetUid = located.UserId;
             var targetAddress = located.LastAddress;
-            if (useLastIp && targetAddress != null)
+            if (ban.UseLastIp && targetAddress != null)
             {
                 if (targetAddress.IsIPv4MappedToIPv6)
                     targetAddress = targetAddress.MapToIPv4();
@@ -116,30 +110,50 @@ public sealed class BanPanelEui : BaseEui
                 var hid = targetAddress.AddressFamily == AddressFamily.InterNetworkV6 ? Ipv6_CIDR : Ipv4_CIDR;
                 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;
-            foreach (var role in roles)
+            foreach (var role in ban.BannedJobs ?? [])
             {
-                if (_prototypeManager.HasIndex<JobPrototype>(role))
-                {
-                    _banManager.CreateRoleBan(targetUid, target, Player.UserId, addressRange, targetHWid, role, minutes, severity, reason, now);
-                }
-                else
-                {
-                    _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
+                );
+            }
+
+            foreach (var role in ban.BannedAntags ?? [])
+            {
+                _banManager.CreateRoleBan(
+                    targetUid,
+                    ban.Target,
+                    Player.UserId,
+                    addressRange,
+                    targetHWid,
+                    role,
+                    ban.BanDurationMinutes,
+                    ban.Severity,
+                    ban.Reason,
+                    now
+                );
             }
 
             Close();
+
             return;
         }
 
-        if (erase &&
-            targetUid != null)
+        if (ban.Erase && targetUid is not null)
         {
             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();
     }
index 7058803d2f020d8232852199b0718b63ea5710ae..c49af32881b8ae08aea9547cc38fe3aba2390db6 100644 (file)
@@ -29,9 +29,10 @@ public sealed class RoleBanCommand : IConsoleCommand
     public async void Execute(IConsoleShell shell, string argStr, string[] args)
     {
         string target;
-        string job;
+        string role;
         string reason;
         uint minutes;
+
         if (!Enum.TryParse(_cfg.GetCVar(CCVars.RoleBanDefaultSeverity), out NoteSeverity severity))
         {
             _sawmill ??= _log.GetSawmill("admin.role_ban");
@@ -43,30 +44,33 @@ public sealed class RoleBanCommand : IConsoleCommand
         {
             case 3:
                 target = args[0];
-                job = args[1];
+                role = args[1];
                 reason = args[2];
                 minutes = 0;
+
                 break;
             case 4:
                 target = args[0];
-                job = args[1];
+                role = args[1];
                 reason = args[2];
 
                 if (!uint.TryParse(args[3], out minutes))
                 {
                     shell.WriteError(Loc.GetString("cmd-roleban-minutes-parse", ("time", args[3]), ("help", Help)));
+
                     return;
                 }
 
                 break;
             case 5:
                 target = args[0];
-                job = args[1];
+                role = args[1];
                 reason = args[2];
 
                 if (!uint.TryParse(args[3], out minutes))
                 {
                     shell.WriteError(Loc.GetString("cmd-roleban-minutes-parse", ("time", args[3]), ("help", Help)));
+
                     return;
                 }
 
@@ -80,26 +84,27 @@ public sealed class RoleBanCommand : IConsoleCommand
             default:
                 shell.WriteError(Loc.GetString("cmd-roleban-arg-count"));
                 shell.WriteLine(Help);
-                return;
-        }
 
-        if (!_proto.HasIndex<JobPrototype>(job))
-        {
-            shell.WriteError(Loc.GetString("cmd-roleban-job-parse", ("job", job)));
-            return;
+                return;
         }
 
         var located = await _locator.LookupIdByNameOrIdAsync(target);
         if (located == null)
         {
             shell.WriteError(Loc.GetString("cmd-roleban-name-parse"));
+
             return;
         }
 
         var targetUid = located.UserId;
         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)
index 2d76c434e9df0aebac2633530dd4f407c2f41760..17f796e699ca01b90aeaa11fc063279d6dde1ebf 100644 (file)
@@ -26,24 +26,25 @@ namespace Content.Server.Administration.Managers;
 
 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 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 IPrototypeManager _prototypeManager = 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 UserDbDataManager _userDbData = default!;
 
     private ISawmill _sawmill = default!;
 
     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();
     // Cached ban exemption flags are used to handle
@@ -91,30 +92,6 @@ public sealed partial class BanManager : IBanManager, IPostInjectInit
         _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()
     {
         // Clear out players that have disconnected.
@@ -232,22 +209,53 @@ public sealed partial class BanManager : IBanManager, IPostInjectInit
 
     #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.
     // 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))
+        {
+            _sawmill.Error($"Creating role ban for {role}: cannot create role ban, role is both JobPrototype and AntagPrototype.");
+
+            return;
+        }
+
+        // Don't trust the input: make sure the job or antag actually exists.
+        if (_prototypeManager.HasIndex<JobPrototype>(role))
+            encodedRole = PrefixJob + role;
+        else if (_prototypeManager.HasIndex<AntagPrototype>(role))
+            encodedRole = PrefixAntag + role;
+        else
         {
-            throw new ArgumentException($"Invalid role '{role}'", nameof(role));
+            _sawmill.Error($"Creating role ban for {role}: cannot create role ban, role is not a JobPrototype or an AntagPrototype.");
+
+            return;
         }
 
-        role = string.Concat(JobPrefix, role);
         DateTimeOffset? expires = null;
+
         if (minutes > 0)
-        {
             expires = DateTimeOffset.Now + TimeSpan.FromMinutes(minutes.Value);
-        }
 
         _systems.TryGetEntitySystem(out GameTicker? ticker);
         int? roundId = ticker == null || ticker.RoundId == 0 ? null : ticker.RoundId;
@@ -266,21 +274,34 @@ public sealed partial class BanManager : IBanManager, IPostInjectInit
             severity,
             banningAdmin,
             null,
-            role);
+            encodedRole);
 
         if (!await AddRoleBan(banDef))
         {
             _chat.SendAdminAlert(Loc.GetString("cmd-roleban-existing", ("target", targetUsername ?? "null"), ("role", role)));
+
             return;
         }
 
         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)));
 
-        if (target != null && _playerManager.TryGetSessionById(target.Value, out var session))
-        {
+        if (target is not null && _playerManager.TryGetSessionById(target.Value, out var 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)
@@ -319,32 +340,109 @@ public sealed partial class BanManager : IBanManager, IPostInjectInit
     }
 
     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))
             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 roleBans
-            .Where(ban => ban.Role.StartsWith(JobPrefix, StringComparison.Ordinal))
-            .Select(ban => new ProtoId<JobPrototype>(ban.Role[JobPrefix.Length..]))
+            .Where(ban => ban.Role.StartsWith(prefix, StringComparison.Ordinal))
+            .Select(ban => new ProtoId<T>(ban.Role[prefix.Length..]))
             .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)
     {
-        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()
         {
-            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);
     }
 
+    #endregion
+
     public void PostInject()
     {
         _sawmill = _logManager.GetSawmill(SawmillId);
index fc192cc3066eaf7b59ac1c8006b82910255eb1a4..1912ebe9ecd04dbe7d3e665d55b31931b19b5155 100644 (file)
@@ -1,4 +1,3 @@
-using System.Collections.Immutable;
 using System.Net;
 using System.Threading.Tasks;
 using Content.Shared.Database;
@@ -25,19 +24,63 @@ public interface IBanManager
     /// <param name="severity">Severity of the resulting ban note</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);
+
+    /// <summary>
+    /// Gets a list of prefixed prototype IDs with the player's role bans.
+    /// </summary>
     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);
 
+    /// <summary>
+    /// Gets a list of prototype IDs with the player's antag bans.
+    /// </summary>
+    public HashSet<ProtoId<AntagPrototype>>? GetAntagBans(NetUserId playerUserId);
+
     /// <summary>
     /// Creates a job ban for the specified target, username or GUID
     /// </summary>
     /// <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="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>
-    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>
     /// Pardons a role ban for the specified target, username or GUID
index 975c802eedd1672d22a1d94faafbf237b3057fec..6703b7b7ca6ae6d90a5d59c10f358bd99a812ce0 100644 (file)
@@ -2,16 +2,17 @@ using System.Diagnostics.CodeAnalysis;
 using System.Linq;
 using Content.Server.Antag.Components;
 using Content.Server.GameTicking.Rules.Components;
-using Content.Server.Objectives;
 using Content.Shared.Antag;
 using Content.Shared.Chat;
 using Content.Shared.GameTicking.Components;
 using Content.Shared.Mind;
 using Content.Shared.Preferences;
+using Content.Shared.Roles;
 using JetBrains.Annotations;
 using Robust.Shared.Audio;
 using Robust.Shared.Enums;
 using Robust.Shared.Player;
+using Robust.Shared.Prototypes;
 
 namespace Content.Server.Antag;
 
@@ -161,33 +162,35 @@ public sealed partial class AntagSelectionSystem
     }
 
     /// <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>
-    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)
             return true;
 
-        if (def.PrefRoles.Count == 0)
+        if (roles.Count == 0)
             return false;
 
         var pref = (HumanoidCharacterProfile) _pref.GetPreferences(session.UserId).SelectedCharacter;
-        return pref.AntagPreferences.Any(p => def.PrefRoles.Contains(p));
-    }
 
-    /// <summary>
-    /// Checks if a given session has the fallback antag preferences for a given definition
-    /// </summary>
-    public bool HasFallbackAntagPreference(ICommonSession? session, AntagSelectionDefinition def)
-    {
-        if (session == null)
-            return true;
+        var valid = false;
 
-        if (def.FallbackRoles.Count == 0)
-            return false;
+        // Check each individual antag role
+        foreach (var role in roles)
+        {
+            var list = new List<ProtoId<AntagPrototype>>{role};
 
-        var pref = (HumanoidCharacterProfile) _pref.GetPreferences(session.UserId).SelectedCharacter;
-        return pref.AntagPreferences.Any(p => def.FallbackRoles.Contains(p));
+
+            if (pref.AntagPreferences.Contains(role)
+                && !_ban.IsRoleBanned(session, list)
+                && _playTime.IsAllowed(session, list))
+                valid = true;
+        }
+
+        return valid;
     }
 
     /// <summary>
index 7fdf812fbe8c92d0a28d305625bd4ccd5aa67bfe..2d484a2aa9566471800e4accbe7d3ab6974cb094 100644 (file)
@@ -1,4 +1,5 @@
 using System.Linq;
+using Content.Server.Administration.Managers;
 using Content.Server.Antag.Components;
 using Content.Server.Chat.Managers;
 using Content.Server.GameTicking;
@@ -8,11 +9,11 @@ using Content.Server.Ghost.Roles;
 using Content.Server.Ghost.Roles.Components;
 using Content.Server.Mind;
 using Content.Server.Objectives;
+using Content.Server.Players.PlayTimeTracking;
 using Content.Server.Preferences.Managers;
 using Content.Server.Roles;
 using Content.Server.Roles.Jobs;
 using Content.Server.Shuttles.Components;
-using Content.Server.Station.Events;
 using Content.Shared.Administration.Logs;
 using Content.Shared.Antag;
 using Content.Shared.Clothing;
@@ -40,12 +41,14 @@ namespace Content.Server.Antag;
 public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelectionComponent>
 {
     [Dependency] private readonly AudioSystem _audio = default!;
+    [Dependency] private readonly IBanManager _ban = default!;
     [Dependency] private readonly IChatManager _chat = default!;
     [Dependency] private readonly GhostRoleSystem _ghostRole = default!;
     [Dependency] private readonly JobSystem _jobs = default!;
     [Dependency] private readonly LoadoutSystem _loadout = default!;
     [Dependency] private readonly MindSystem _mind = default!;
     [Dependency] private readonly IPlayerManager _playerManager = default!;
+    [Dependency] private readonly PlayTimeTrackingSystem _playTime = default!;
     [Dependency] private readonly IServerPreferencesManager _pref = default!;
     [Dependency] private readonly RoleSystem _role = 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)}");
 
-        if (checkPref && !HasPrimaryAntagPreference(session, def))
+        if (checkPref && !ValidAntagPreference(session, def.PrefRoles))
             return false;
 
         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))
                 continue;
 
-            if (HasPrimaryAntagPreference(session, def))
+            // Add player to the appropriate antag pool
+            if (ValidAntagPreference(session, def.PrefRoles))
             {
                 preferredList.Add(session);
             }
-            else if (HasFallbackAntagPreference(session, def))
+            else if (ValidAntagPreference(session, def.FallbackRoles))
             {
                 fallbackList.Add(session);
             }
index 107e69b38df75917031e112c2b5ab52b30978d3c..b1e55978949ae3654083073070d0ce60de89c895 100644 (file)
@@ -28,7 +28,6 @@ namespace Content.Server.Database
     public abstract class ServerDbBase
     {
         private readonly ISawmill _opsLog;
-
         public event Action<DatabaseNotification>? OnNotificationReceived;
 
         /// <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.ExpirationTime,
                 ban.Hidden,
-                new [] { ban.RoleId.Replace(BanManager.JobPrefix, null) },
+                new [] { ban.RoleId.Replace(BanManager.PrefixJob, null).Replace(BanManager.PrefixAntag, null) },
                 MakePlayerRecord(unbanningAdmin),
                 ban.Unban?.UnbanTime);
         }
@@ -1686,7 +1685,7 @@ INSERT INTO player_round (players_id, rounds_id) VALUES ({players[player]}, {id}
                     NormalizeDatabaseTime(firstBan.LastEditedAt),
                     NormalizeDatabaseTime(firstBan.ExpirationTime),
                     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),
                     NormalizeDatabaseTime(firstBan.Unban?.UnbanTime)));
             }
diff --git a/Content.Server/GameTicking/Events/IsJobAllowedEvent.cs b/Content.Server/GameTicking/Events/IsJobAllowedEvent.cs
deleted file mode 100644 (file)
index 51969d6..0000000
+++ /dev/null
@@ -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;
-}
diff --git a/Content.Server/GameTicking/Events/IsRoleAllowedEvent.cs b/Content.Server/GameTicking/Events/IsRoleAllowedEvent.cs
new file mode 100644 (file)
index 0000000..76d2805
--- /dev/null
@@ -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;
+}
index 194f6c4997d7f2019692507277f51da6e94d9651..2338d4f1fe328ba3fe13b14e441f8804179f5077 100644 (file)
@@ -141,12 +141,13 @@ namespace Content.Server.GameTicking
             var character = GetPlayerProfile(player);
 
             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;
 
             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);
                 if (ev.Cancelled)
                     return;
index 5dd390bd722c1e765d9b80a97b93355563e3485f..98aaf672c2379463cf759c2ed981150be6ef252d 100644 (file)
@@ -15,12 +15,6 @@ public sealed partial class GhostRoleComponent : Component
 
     [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>
     /// Whether the <see cref="MakeSentientCommand"/> should run on the mob.
     /// </summary>
index 182d8e968ef614a95d16f73cac9a693bf72bbfa8..b2cbd6a15244cf4bb59691439e7bfbcdd30255f3 100644 (file)
@@ -1,6 +1,8 @@
 using System.Linq;
 using Content.Server.Administration.Logs;
+using Content.Server.Administration.Managers;
 using Content.Server.EUI;
+using Content.Server.GameTicking.Events;
 using Content.Server.Ghost.Roles.Components;
 using Content.Server.Ghost.Roles.Events;
 using Content.Shared.Ghost.Roles.Raffles;
@@ -32,13 +34,16 @@ using Content.Server.Popups;
 using Content.Shared.Verbs;
 using Robust.Shared.Collections;
 using Content.Shared.Ghost.Roles.Components;
+using Content.Shared.Roles.Components;
 
 namespace Content.Server.Ghost.Roles;
 
 [UsedImplicitly]
 public sealed class GhostRoleSystem : EntitySystem
 {
+    [Dependency] private readonly IBanManager _ban = default!;
     [Dependency] private readonly IConfigurationManager _cfg = default!;
+    [Dependency] private readonly IEntityManager _ent = default!;
     [Dependency] private readonly EuiManager _euiManager = default!;
     [Dependency] private readonly IPlayerManager _playerManager = default!;
     [Dependency] private readonly IAdminLogManager _adminLogger = default!;
@@ -459,6 +464,23 @@ public sealed class GhostRoleSystem : EntitySystem
         if (!_ghostRoles.TryGetValue(identifier, out var roleEnt))
             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)
         {
             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>
     /// Attempts having the player take over the ghost role with the corresponding ID. Does not start a raffle.
     /// </summary>
@@ -571,13 +665,15 @@ public sealed class GhostRoleSystem : EntitySystem
                 ? _timing.CurTime.Add(raffle.Countdown)
                 : TimeSpan.MinValue;
 
+            TryPrototypes((uid, role), out var antags, out var jobs);
+
             roles.Add(new GhostRoleInfo
             {
                 Identifier = id,
                 Name = role.RoleName,
                 Description = role.RoleDescription,
                 Rules = role.RoleRules,
-                Requirements = role.Requirements,
+                RolePrototypes = (jobs, antags),
                 Kind = kind,
                 RafflePlayerCount = rafflePlayerCount,
                 RaffleEndTime = raffleEndTime
index 72f18e00cbe8b1eb7f7d0fd7082dedb5535e4526..c47ffa691fab24a8a6231786e754f9ec19d06439 100644 (file)
@@ -58,6 +58,9 @@ public sealed class JobWhitelistManager : IPostInjectInit
             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)
     {
         if (!_config.GetCVar(CCVars.GameRoleWhitelist))
index aaada99dea94239becd51048ec773a8694da975c..2e2848fea3b2e1c8017a4bf338d055842ed35cf7 100644 (file)
@@ -23,7 +23,7 @@ public sealed class JobWhitelistSystem : EntitySystem
     {
         SubscribeLocalEvent<PrototypesReloadedEventArgs>(OnPrototypesReloaded);
         SubscribeLocalEvent<StationJobsGetCandidatesEvent>(OnStationJobsGetCandidates);
-        SubscribeLocalEvent<IsJobAllowedEvent>(OnIsJobAllowed);
+        SubscribeLocalEvent<IsRoleAllowedEvent>(OnIsRoleAllowed);
         SubscribeLocalEvent<GetDisallowedJobsEvent>(OnGetDisallowedJobs);
 
         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))
-            ev.Cancelled = true;
+        if (ev.Jobs is null)
+            return;
+
+        foreach (var proto in ev.Jobs)
+        {
+            if (!_manager.IsAllowed(ev.Player, proto))
+                ev.Cancelled = true;
+        }
     }
+    //TODO: Antagonist role whitelists?
 
     private void OnGetDisallowedJobs(ref GetDisallowedJobsEvent ev)
     {
index d55920f83c87def6e918e91ffdb9017d3fc5d219..f218de1c779d9f66a9c64e8ac8f03f7d6764a19c 100644 (file)
@@ -54,7 +54,7 @@ public sealed class PlayTimeTrackingSystem : EntitySystem
         SubscribeLocalEvent<MobStateChangedEvent>(OnMobStateChanged);
         SubscribeLocalEvent<PlayerJoinedLobbyEvent>(OnPlayerJoinedLobby);
         SubscribeLocalEvent<StationJobsGetCandidatesEvent>(OnStationJobsGetCandidates);
-        SubscribeLocalEvent<IsJobAllowedEvent>(OnIsJobAllowed);
+        SubscribeLocalEvent<IsRoleAllowedEvent>(OnIsRoleAllowed);
         SubscribeLocalEvent<GetDisallowedJobsEvent>(OnGetDisallowedJobs);
         _adminManager.OnPermsChanged += AdminPermsChanged;
     }
@@ -86,6 +86,9 @@ public sealed class PlayTimeTrackingSystem : EntitySystem
         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)
     {
         var attached = session.AttachedEntity;
@@ -176,9 +179,9 @@ public sealed class PlayTimeTrackingSystem : EntitySystem
         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;
     }
 
@@ -187,10 +190,83 @@ public sealed class PlayTimeTrackingSystem : EntitySystem
         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) ||
-            !_cfg.GetCVar(CCVars.GameRoleTimers))
+        if (jobs is null)
+            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;
+
+        if (!_tracking.TryGetTrackerTimes(player, out var playTimes))
+        {
+            Log.Error($"Unable to check playtimes {Environment.StackTrace}");
+            playTimes = new Dictionary<string, TimeSpan>();
+        }
+
+        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))
@@ -199,7 +275,15 @@ public sealed class PlayTimeTrackingSystem : EntitySystem
             playTimes = new Dictionary<string, TimeSpan>();
         }
 
-        return JobRequirements.TryRequirementsMet(job, playTimes, out _, EntityManager, _prototypes, (HumanoidCharacterProfile?) _preferencesManager.GetPreferences(player.UserId).SelectedCharacter);
+        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)
index 3510aca85ecbd016c7c2e28539881785b174a246..1dd7b70f8d67386a1ccf4325d5ed99739c725507 100644 (file)
@@ -371,7 +371,7 @@ public sealed partial class StationJobsSystem
                 if (weight is not null && job.Weight != weight.Value)
                     continue;
 
-                if (!(roleBans == null || !roleBans.Contains(jobId)))
+                if (!(roleBans == null || !roleBans.Contains(jobId))) //TODO: Replace with IsRoleBanned
                     continue;
 
                 availableJobs ??= new List<string>(profile.JobPriorities.Count);
index 9c7d4b0699c5da6cbfec23bb5b92a0e275ca016d..8b5db4561cca3d8cc62003c0ce12235a331e30ae 100644 (file)
@@ -1,7 +1,9 @@
+using Content.Server.Administration.Managers;
 using Content.Server.Atmos.Components;
 using Content.Server.Body.Components;
 using Content.Server.Chat;
 using Content.Server.Chat.Managers;
+using Content.Server.Ghost;
 using Content.Server.Ghost.Roles.Components;
 using Content.Server.Humanoid;
 using Content.Server.IdentityManagement;
@@ -14,6 +16,7 @@ using Content.Server.StationEvents.Components;
 using Content.Server.Speech.Components;
 using Content.Server.Temperature.Components;
 using Content.Shared.Body.Components;
+using Content.Shared.Chat;
 using Content.Shared.CombatMode;
 using Content.Shared.CombatMode.Pacification;
 using Content.Shared.Damage;
@@ -40,6 +43,7 @@ using Content.Shared.Tag;
 using Robust.Shared.Player;
 using Robust.Shared.Prototypes;
 using Content.Shared.NPC.Prototypes;
+using Content.Shared.Roles;
 
 namespace Content.Server.Zombies;
 
@@ -52,23 +56,27 @@ namespace Content.Server.Zombies;
 public sealed partial class ZombieSystem
 {
     [Dependency] private readonly SharedAudioSystem _audio = default!;
+    [Dependency] private readonly IBanManager _ban = default!;
     [Dependency] private readonly IChatManager _chatMan = default!;
     [Dependency] private readonly SharedCombatModeSystem _combat = default!;
     [Dependency] private readonly NpcFactionSystem _faction = default!;
+    [Dependency] private readonly GhostSystem _ghost = default!;
     [Dependency] private readonly SharedHandsSystem _hands = default!;
     [Dependency] private readonly HumanoidAppearanceSystem _humanoidAppearance = default!;
     [Dependency] private readonly IdentitySystem _identity = default!;
     [Dependency] private readonly ServerInventorySystem _inventory = default!;
     [Dependency] private readonly MindSystem _mind = default!;
     [Dependency] private readonly MovementSpeedModifierSystem _movementSpeedModifier = default!;
+    [Dependency] private readonly NameModifierSystem _nameMod = default!;
     [Dependency] private readonly NPCSystem _npc = default!;
     [Dependency] private readonly TagSystem _tag = default!;
-    [Dependency] private readonly NameModifierSystem _nameMod = default!;
     [Dependency] private readonly ISharedPlayerManager _player = default!;
 
     private static readonly ProtoId<TagPrototype> InvalidForGlobalSpawnSpellTag = "InvalidForGlobalSpawnSpell";
     private static readonly ProtoId<TagPrototype> CannotSuicideTag = "CannotSuicide";
     private static readonly ProtoId<NpcFactionPrototype> ZombieFaction = "Zombie";
+    private static readonly string MindRoleZombie = "MindRoleZombie";
+    private static readonly List<ProtoId<AntagPrototype>> BannableZombiePrototypes = ["Zombie"];
 
     /// <summary>
     /// 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))
             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.
         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))
         {
             //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
             _chatMan.DispatchServerMessage(session, Loc.GetString("zombie-infection-greeting"));
@@ -266,6 +292,7 @@ public sealed partial class ZombieSystem
             ghostRole.RoleName = Loc.GetString("zombie-generic");
             ghostRole.RoleDescription = Loc.GetString("zombie-role-desc");
             ghostRole.RoleRules = Loc.GetString("zombie-role-rules");
+            ghostRole.MindRoles.Add(MindRoleZombie);
         }
 
         if (TryComp<HandsComponent>(target, out var handsComp))
index 74c340566b5e678bac96e9b1248a277444d47da3..76de3d8e3f3d4991c9e163717caeb27cec60887c 100644 (file)
@@ -1,6 +1,8 @@
 using System.Net;
 using Content.Shared.Database;
 using Content.Shared.Eui;
+using Content.Shared.Roles;
+using Robust.Shared.Prototypes;
 using Robust.Shared.Serialization;
 
 namespace Content.Shared.Administration;
@@ -21,32 +23,9 @@ public sealed class BanPanelEuiState : EuiStateBase
 public static class BanPanelEuiStateMsg
 {
     [Serializable, NetSerializable]
-    public sealed class CreateBanRequest : EuiMessageBase
+    public sealed class CreateBanRequest(Ban ban) : EuiMessageBase
     {
-        public string? Player { get; set; }
-        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;
-        }
+        public Ban Ban { get; } = ban;
     }
 
     [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;
+}
index b5d8fedbd92129b3da88d993b8463230c4a37d1b..38086da8562263ee774873d7de9656361d61d39c 100644 (file)
@@ -1,5 +1,6 @@
 using Content.Shared.Eui;
 using Content.Shared.Roles;
+using Robust.Shared.Prototypes;
 using Robust.Shared.Serialization;
 
 namespace Content.Shared.Ghost.Roles
@@ -12,11 +13,10 @@ namespace Content.Shared.Ghost.Roles
         public string Description { get; set; }
         public string Rules { get; set; }
 
-        // TODO ROLE TIMERS
-        // Actually make use of / enforce this requirement?
-        // Why is this even here.
-        // Move to ghost role prototype & respect CCvars.GameRoleTimerOverride
-        public HashSet<JobRequirement>? Requirements { get; set; }
+        /// <summary>
+        /// A list of all antag and job prototype IDs of the ghost role and its mind role(s).
+        /// </summary>
+        public (List<ProtoId<JobPrototype>>?,List<ProtoId<AntagPrototype>>?)  RolePrototypes;
 
         /// <inheritdoc cref="GhostRoleKind"/>
         public GhostRoleKind Kind { get; set; }
index fd90f62b0b5b3e5f05d24866715e5cfec914779f..bcc28d01d2e89d942ccadddbec4d3ff5ef0dec7f 100644 (file)
@@ -11,24 +11,40 @@ public sealed class MsgRoleBans : NetMessage
 {
     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)
     {
-        var count = buffer.ReadVariableInt32();
-        Bans.EnsureCapacity(count);
+        var jobCount = buffer.ReadVariableInt32();
+        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)
     {
-        buffer.WriteVariableInt32(Bans.Count);
+        buffer.WriteVariableInt32(JobBans.Count);
+
+        foreach (var ban in JobBans)
+        {
+            buffer.Write(ban);
+        }
+
+        buffer.WriteVariableInt32(AntagBans.Count);
 
-        foreach (var ban in Bans)
+        foreach (var ban in AntagBans)
         {
             buffer.Write(ban);
         }
index ff2712600a294d03acce0b68c3c0e7b3e99c0ec0..367b05c3dd18954ca2381db9e85b05fd52a36092 100644 (file)
@@ -10,6 +10,12 @@ namespace Content.Shared.Roles;
 [Prototype]
 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]
     [IdDataField]
     public string ID { get; private set; } = default!;
@@ -41,8 +47,6 @@ public sealed partial class AntagPrototype : IPrototype
     /// <summary>
     ///     Requirements that must be met to opt in to this antag role.
     /// </summary>
-    // TODO ROLE TIMERS
-    // Actually check if the requirements are met. Because apparently this is actually unused.
     [DataField, Access(typeof(SharedRoleSystem), Other = AccessPermissions.None)]
     public HashSet<JobRequirement>? Requirements;
 
index 17f5f7bd6a29af7ecf060a3aa3c2a247b5d1ff9a..62d50f84890496bc3ab14cf81bf6b00076b96891 100644 (file)
@@ -8,6 +8,13 @@ namespace Content.Shared.Roles;
 
 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(
         JobPrototype job,
         IReadOnlyDictionary<string, TimeSpan> playTimes,
@@ -17,7 +24,25 @@ public static class JobRequirements
         HumanoidCharacterProfile? profile)
     {
         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;
         if (requirements == null)
             return true;
index ea25555257f314af2e560bfddba714f4bb2f84a8..eeab32966154de884e8f15c0d5ce1e9f9d05e82e 100644 (file)
@@ -667,10 +667,13 @@ public abstract class SharedRoleSystem : EntitySystem
             _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
     // 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))
             return req;
@@ -678,31 +681,28 @@ public abstract class SharedRoleSystem : EntitySystem
         return job.Requirements;
     }
 
-    // TODO ROLES Change to readonly.
-    public HashSet<JobRequirement>? GetJobRequirement(ProtoId<JobPrototype> job)
+    // TODO ROLES Change to readonly?
+    /// <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;
+        return antag.Requirements;
     }
 
-    // TODO ROLES Change to readonly.
-    public HashSet<JobRequirement>? GetAntagRequirement(ProtoId<AntagPrototype> antag)
+    // TODO ROLES Change to readonly?
+    /// <inheritdoc cref="GetRoleRequirements(JobPrototype)"/>
+    public HashSet<JobRequirement>? GetRoleRequirements(ProtoId<JobPrototype> jobId)
     {
-        if (_requirementOverride != null && _requirementOverride.Antags.TryGetValue(antag, out var req))
-            return req;
-
-        return _prototypes.Index(antag).Requirements;
+        return _prototypes.TryIndex(jobId, out var job) ? GetRoleRequirements(job) : null;
     }
 
-    // TODO ROLES Change to readonly.
-    public HashSet<JobRequirement>? GetAntagRequirement(AntagPrototype antag)
+    // TODO ROLES Change to readonly?
+    /// <inheritdoc cref="GetRoleRequirements(JobPrototype)"/>
+    public HashSet<JobRequirement>? GetRoleRequirements(ProtoId<AntagPrototype> antagId)
     {
-        if (_requirementOverride != null && _requirementOverride.Antags.TryGetValue(antag.ID, out var req))
-            return req;
-
-        return antag.Requirements;
+        return _prototypes.TryIndex(antagId, out var antag) ? GetRoleRequirements(antag) : null;
     }
 
     /// <summary>
index 4643cd228ba1ecc51d0185b62541161944368ee8..39ee550bf995264a57830b55f0611f6245e06939 100644 (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.
 
 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.
index 5815dbba4795cd39074eed49e84708b7e0ad840c..d3f2e172ec56878c25d0f7b546812a2d9e4a49e3 100644 (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
   abstract: true
   parent: MarkerBase
@@ -88,7 +92,7 @@
   - type: GhostRole
     rules: ghost-role-information-rules-default-team-antagonist
     mindRoles:
-    - MindRoleGhostRoleTeamAntagonist
+    - MindRoleNukeops
     raffle:
       settings: default
   - type: GhostRoleMobSpawner
     description: roles-antag-nuclear-operative-commander-objective
     rules: ghost-role-information-rules-default-team-antagonist
     mindRoles:
-    - MindRoleGhostRoleTeamAntagonist
+    - MindRoleNukeopsCommander
 
 - type: entity
   categories: [ HideSpawnMenu, Spawner ]
     description: roles-antag-nuclear-operative-agent-objective
     rules: ghost-role-information-rules-default-team-antagonist
     mindRoles:
-    - MindRoleGhostRoleTeamAntagonist
+    - MindRoleNukeopsMedic
 
 - type: entity
   categories: [ HideSpawnMenu, Spawner ]
     description: roles-antag-nuclear-operative-objective
     rules: ghost-role-information-rules-default-team-antagonist
     mindRoles:
-    - MindRoleGhostRoleTeamAntagonist
+    - MindRoleNukeops
 
 - type: entity
   categories: [ HideSpawnMenu, Spawner ]
     description: ghost-role-information-space-dragon-description
     rules: ghost-role-information-space-dragon-rules
     mindRoles:
-    - MindRoleGhostRoleTeamAntagonist
+    - MindRoleDragon
   - type: Sprite
     layers:
     - state: green
     description: ghost-role-information-space-ninja-description
     rules: ghost-role-information-antagonist-rules
     mindRoles:
-    - MindRoleGhostRoleSoloAntagonist
+    - MindRoleNinja
     raffle:
       settings: default
   - type: Sprite
     description: ghost-role-information-paradox-clone-description
     rules: ghost-role-information-antagonist-rules
     mindRoles:
-    - MindRoleGhostRoleSoloAntagonist
+    - MindRoleParadoxClone
     raffle:
       settings: default
   - type: Sprite
     name: ghost-role-information-derelict-cyborg-name
     description: ghost-role-information-derelict-cyborg-description
     rules: ghost-role-information-silicon-rules
+    mindRoles:
+    - MindRoleSubvertedSilicon
     raffle:
       settings: default
   - type: Sprite
     name: ghost-role-information-wizard-name
     description: ghost-role-information-wizard-desc
     mindRoles:
-    - MindRoleGhostRoleSoloAntagonist
+    - MindRoleWizard
     raffle:
       settings: default
   - type: Sprite
index 1f7ab7ac5cbf894ec481256d821eb7d0f7a6e995..f8cbae86a4b16c4ccc2c61acf29f45dc103e8772 100644 (file)
@@ -57,6 +57,7 @@
         settings: short
       mindRoles:
       - MindRoleGhostRoleFamiliar
+      job: DeathSquad
     - type: Loadout
       prototypes: [ DeathSquadGear ]
       roleLoadout: [ RoleSurvivalEVA ]
       rules: ghost-role-information-nonantagonist-rules
       raffle:
         settings: short
+      job: CBURN
     - type: RandomMetadata
       nameSegments:
       - NamesMilitaryFirst
       rules: ghost-role-information-nonantagonist-rules
       raffle:
         settings: default
+      job: CentralCommandOfficial
     - type: Loadout
       prototypes: [ CentcomGear ]
       roleLoadout: [ RoleSurvivalStandard ]
index 50a2e0b58e8c19719fea30cd4860230e54321c94..1c416083bc32296801342ee5c7dd9f06def11cc6 100644 (file)
   suffix: Empty
   components:
   - type: Anchorable
-    flags: 
+    flags:
     - Anchorable
   - type: Rotatable
   - type: WarpPoint
       rules: ghost-role-information-silicon-rules
       raffle:
         settings: default
+      job: Borg
     - type: GhostTakeoverAvailable
 
 - type: entity
       rules: ghost-role-information-silicon-rules
       raffle:
         settings: default
+      job: Borg
     - type: GhostTakeoverAvailable
 
 - type: entity
     raffle:
       settings: default
     reregister: false
+    job: Borg
   - type: GhostTakeoverAvailable
 
 - type: entity
       raffle:
         settings: default
       reregister: false
+      job: Borg
     - type: GhostTakeoverAvailable
 
 - type: entity
     raffle:
       settings: default
     reregister: false
+    job: Borg
   - type: GhostTakeoverAvailable
 
 - type: entity
     raffle:
       settings: default
     reregister: false
+    job: Borg
   - type: GhostTakeoverAvailable
 
 - type: entity
     raffle:
       settings: default
     reregister: false
+    job: Borg
   - type: GhostTakeoverAvailable
 
 - type: entity
     raffle:
       settings: default
     reregister: false
+    job: Borg
   - type: GhostTakeoverAvailable
index 79a10b628715d4ee8ea5eedde80301f73056f0ac..27d077df2fa852bf6021630faa96ef506aec4f42 100644 (file)
       - MindRoleGhostRoleSilicon
       raffle:
         settings: default
+      job: Borg
     - type: GhostRoleMobSpawner
       prototype: PlayerBorgSyndicateAssaultBattery