[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; }
// 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!;
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
- _banpanelSawmill = _logManager.GetSawmill("admin.banpanel");
+ _banPanelSawmill = _logManager.GetSawmill("admin.banpanel");
PlayerList.OnSelectionChanged += OnPlayerSelectionChanged;
PlayerNameLine.OnFocusExit += _ => OnPlayerNameChanged();
PlayerCheckbox.OnPressed += _ =>
TypeOption.SelectId(args.Id);
OnTypeChanged();
};
- LastConnCheckbox.OnPressed += args =>
+ LastConnCheckbox.OnPressed += _ =>
{
IpLine.ModulateSelfOverride = null;
HwidLine.ModulateSelfOverride = null;
var antagRoles = _protoMan.EnumeratePrototypes<AntagPrototype>()
.OrderBy(x => x.ID);
- CreateRoleGroup("Antagonist", Color.Red, antagRoles);
+ CreateRoleGroup(AntagPrototype.GroupName, AntagPrototype.GroupColor, antagRoles);
}
/// <summary>
{
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;
}
{
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;
}
}
/// <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)
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
{
roleGroupInnerContainer.AddChild(roleCheckboxContainer);
_roleCheckboxes.TryAdd(group, []);
- _roleCheckboxes[group].Add(roleCheckButton);
+ _roleCheckboxes[group].Add((roleCheckButton, rolePrototype));
}
public void UpdateBanFlag(bool newFlag)
newSeverity = serverSeverity;
else
{
- _banpanelSawmill
+ _banPanelSawmill
.Warning("Server ban severity could not be parsed from config!");
}
}
else
{
- _banpanelSawmill
+ _banPanelSawmill
.Warning("Role ban severity could not be parsed from config!");
}
break;
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;
}
ReasonTextEdit.GrabKeyboardFocus();
ReasonTextEdit.ModulateSelfOverride = Color.Red;
ReasonTextEdit.OnKeyBindDown += ResetTextEditor;
+
return;
}
ButtonResetOn = _gameTiming.CurTime.Add(TimeSpan.FromSeconds(3));
SubmitButton.ModulateSelfOverride = Color.Red;
SubmitButton.Text = Loc.GetString("ban-panel-confirm");
+
return;
}
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)
{
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));
}
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);
using System.Diagnostics.CodeAnalysis;
-using Content.Client.Lobby;
using Content.Shared.CCVar;
using Content.Shared.Players;
using Content.Shared.Players.JobWhitelist;
[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!;
// 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();
}
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;
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;
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
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;
[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;
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);
}
}
- 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;
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"));
}
targetUid = located.UserId;
var targetAddress = located.LastAddress;
- if (useLastIp && targetAddress != null)
+ if (ban.UseLastIp && targetAddress != null)
{
if (targetAddress.IsIPv4MappedToIPv6)
targetAddress = targetAddress.MapToIPv4();
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
{
}
}
- _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();
}
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");
{
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;
}
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)
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
_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.
#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;
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)
}
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);
-using System.Collections.Immutable;
using System.Net;
using System.Threading.Tasks;
using Content.Shared.Database;
/// <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
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;
}
/// <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>
using System.Linq;
+using Content.Server.Administration.Managers;
using Content.Server.Antag.Components;
using Content.Server.Chat.Managers;
using Content.Server.GameTicking;
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;
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!;
{
_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))
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);
}
public abstract class ServerDbBase
{
private readonly ISawmill _opsLog;
-
public event Action<DatabaseNotification>? OnNotificationReceived;
/// <param name="opsLog">Sawmill to trace log database operations to.</param>
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);
}
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)));
}
+++ /dev/null
-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;
-}
--- /dev/null
+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;
+}
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;
[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>
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;
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!;
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);
}
}
+ /// <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>
? _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
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))
{
SubscribeLocalEvent<PrototypesReloadedEventArgs>(OnPrototypesReloaded);
SubscribeLocalEvent<StationJobsGetCandidatesEvent>(OnStationJobsGetCandidates);
- SubscribeLocalEvent<IsJobAllowedEvent>(OnIsJobAllowed);
+ SubscribeLocalEvent<IsRoleAllowedEvent>(OnIsRoleAllowed);
SubscribeLocalEvent<GetDisallowedJobsEvent>(OnGetDisallowedJobs);
CacheJobs();
}
}
- 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)
{
SubscribeLocalEvent<MobStateChangedEvent>(OnMobStateChanged);
SubscribeLocalEvent<PlayerJoinedLobbyEvent>(OnPlayerJoinedLobby);
SubscribeLocalEvent<StationJobsGetCandidatesEvent>(OnStationJobsGetCandidates);
- SubscribeLocalEvent<IsJobAllowedEvent>(OnIsJobAllowed);
+ SubscribeLocalEvent<IsRoleAllowedEvent>(OnIsRoleAllowed);
SubscribeLocalEvent<GetDisallowedJobsEvent>(OnGetDisallowedJobs);
_adminManager.OnPermsChanged += AdminPermsChanged;
}
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;
RemoveDisallowedJobs(ev.Player, ev.Jobs);
}
- private void OnIsJobAllowed(ref IsJobAllowedEvent ev)
+ private void OnIsRoleAllowed(ref IsRoleAllowedEvent ev)
{
- if (!IsAllowed(ev.Player, ev.JobId))
+ if (!IsAllowed(ev.Player, ev.Jobs) || !IsAllowed(ev.Player, ev.Antags))
ev.Cancelled = true;
}
ev.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))
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)
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);
+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;
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;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Content.Shared.NPC.Prototypes;
+using Content.Shared.Roles;
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
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);
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"));
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))
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;
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]
}
}
}
+
+/// <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;
+}
using Content.Shared.Eui;
using Content.Shared.Roles;
+using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
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; }
{
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);
}
[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!;
/// <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;
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,
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;
_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;
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>
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.
+# 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
- 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
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 ]
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
- MindRoleGhostRoleSilicon
raffle:
settings: default
+ job: Borg
- type: GhostRoleMobSpawner
prototype: PlayerBorgSyndicateAssaultBattery