using Content.Server.Administration.Managers; using Content.Server.Database; using Content.Server.GameTicking; using Content.Server.Ghost; using Content.Server.Roles.Jobs; using Content.Shared.CCVar; using Content.Shared.Ghost; using Content.Shared.Mind; using Content.Shared.Mind.Components; using Content.Shared.Voting; using Robust.Server.Player; using Robust.Shared.Configuration; using Robust.Shared.Network; using Robust.Shared.Player; using Robust.Shared.Timing; using System.Threading.Tasks; using Content.Shared.Players.PlayTimeTracking; using Content.Shared.Roles; using Content.Shared.Roles.Components; using Robust.Shared.Prototypes; namespace Content.Server.Voting; public sealed class VotingSystem : EntitySystem { [Dependency] private readonly IPlayerManager _playerManager = default!; [Dependency] private readonly IAdminManager _adminManager = default!; [Dependency] private readonly IServerDbManager _dbManager = default!; [Dependency] private readonly IGameTiming _gameTiming = default!; [Dependency] private readonly IConfigurationManager _cfg = default!; [Dependency] private readonly JobSystem _jobs = default!; [Dependency] private readonly GameTicker _gameTicker = default!; [Dependency] private readonly ISharedPlaytimeManager _playtimeManager = default!; [Dependency] private readonly SharedRoleSystem _roles = default!; [Dependency] private readonly IPrototypeManager _prototypes = default!; public override void Initialize() { base.Initialize(); SubscribeNetworkEvent(OnVotePlayerListRequestEvent); } private async void OnVotePlayerListRequestEvent(VotePlayerListRequestEvent msg, EntitySessionEventArgs args) { if (!await CheckVotekickInitEligibility(args.SenderSession)) { var deniedResponse = new VotePlayerListResponseEvent(new (NetUserId, NetEntity, string)[0], true); RaiseNetworkEvent(deniedResponse, args.SenderSession.Channel); return; } List<(NetUserId, NetEntity, string)> players = new(); foreach (var player in _playerManager.Sessions) { if (args.SenderSession == player) continue; if (_adminManager.IsAdmin(player, false)) continue; if (player.AttachedEntity is not { Valid: true } attached) { var playerName = player.Name; var netEntity = NetEntity.Invalid; players.Add((player.UserId, netEntity, playerName)); } else { var playerName = GetPlayerVoteListName(attached); var netEntity = GetNetEntity(attached); players.Add((player.UserId, netEntity, playerName)); } } var response = new VotePlayerListResponseEvent(players.ToArray(), false); RaiseNetworkEvent(response, args.SenderSession.Channel); } public string GetPlayerVoteListName(EntityUid attached) { TryComp(attached, out var mind); var jobName = GetNonAntagJobName(mind?.Mind); var playerInfo = $"{Comp(attached).EntityName} ({jobName})"; return playerInfo; } /// /// Gets the job name for a mind, excluding antagonist roles to prevent revealing game mode information in voting. /// Returns the first non-antagonist job role found, or "Unknown" if none exists. /// private string GetNonAntagJobName(EntityUid? mindId) { if (mindId == null) return Loc.GetString("generic-unknown-title"); if (!TryComp(mindId.Value, out var mindComponent)) return Loc.GetString("generic-unknown-title"); // Look for a job role that is NOT an antagonist foreach (var roleEnt in mindComponent.MindRoleContainer.ContainedEntities) { if (!TryComp(roleEnt, out var roleComponent)) continue; // Skip antagonist roles to prevent revealing game mode if (roleComponent.Antag || roleComponent.ExclusiveAntag) continue; // We found a non-antagonist role with a job if (roleComponent.JobPrototype != null && _prototypes.TryIndex(roleComponent.JobPrototype.Value, out var jobPrototype)) { return jobPrototype.LocalizedName; } } // Fallback: no non-antagonist job found return Loc.GetString("generic-unknown-title"); } /// /// Used to check whether the player initiating a votekick is allowed to do so serverside. /// /// The session initiating the votekick. public async Task CheckVotekickInitEligibility(ICommonSession? initiator) { if (initiator == null) return false; // Being an admin overrides the votekick eligibility if (initiator.AttachedEntity != null && _adminManager.IsAdmin(initiator.AttachedEntity.Value, false)) return true; // If cvar enabled, skip the ghost requirement in the preround lobby if (!_cfg.GetCVar(CCVars.VotekickIgnoreGhostReqInLobby) || (_cfg.GetCVar(CCVars.VotekickIgnoreGhostReqInLobby) && _gameTicker.RunLevel != GameRunLevel.PreRoundLobby)) { if (_cfg.GetCVar(CCVars.VotekickInitiatorGhostRequirement)) { // Must be ghost if (!TryComp(initiator.AttachedEntity, out GhostComponent? ghostComp)) return false; // Must have been dead for x seconds if ((int)_gameTiming.RealTime.Subtract(ghostComp.TimeOfDeath).TotalSeconds < _cfg.GetCVar(CCVars.VotekickEligibleVoterDeathtime)) return false; } } // Must be whitelisted if (!await _dbManager.GetWhitelistStatusAsync(initiator.UserId) && _cfg.GetCVar(CCVars.VotekickInitiatorWhitelistedRequirement)) return false; // Must be eligible to vote var playtime = _playtimeManager.GetPlayTimes(initiator); return playtime.TryGetValue(PlayTimeTrackingShared.TrackerOverall, out TimeSpan overallTime) && (overallTime >= TimeSpan.FromHours(_cfg.GetCVar(CCVars.VotekickEligibleVoterPlaytime)) || !_cfg.GetCVar(CCVars.VotekickInitiatorTimeRequirement)); } /// /// Used to check whether the player being targetted for a votekick is a valid target. /// /// The session being targetted for a votekick. public bool CheckVotekickTargetEligibility(ICommonSession? target) { if (target == null) return false; // Admins can't be votekicked if (target.AttachedEntity != null && _adminManager.IsAdmin(target.AttachedEntity.Value)) return false; return true; } }