1
0

VoteManager.DefaultVotes.cs 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600
  1. using System.Linq;
  2. using System.Net;
  3. using System.Net.Sockets;
  4. using Content.Server.Administration;
  5. using Content.Server.Administration.Managers;
  6. using Content.Server.Discord.WebhookMessages;
  7. using Content.Server.GameTicking;
  8. using Content.Server.GameTicking.Presets;
  9. using Content.Server.Maps;
  10. using Content.Server.Roles;
  11. using Content.Server.RoundEnd;
  12. using Content.Shared.CCVar;
  13. using Content.Shared.Chat;
  14. using Content.Shared.Database;
  15. using Content.Shared.Players;
  16. using Content.Shared.Players.PlayTimeTracking;
  17. using Content.Shared.Voting;
  18. using Robust.Shared.Configuration;
  19. using Robust.Shared.Enums;
  20. using Robust.Shared.Player;
  21. using Robust.Shared.Random;
  22. namespace Content.Server.Voting.Managers
  23. {
  24. public sealed partial class VoteManager
  25. {
  26. [Dependency] private readonly IPlayerLocator _locator = default!;
  27. [Dependency] private readonly ILogManager _logManager = default!;
  28. [Dependency] private readonly IBanManager _bans = default!;
  29. [Dependency] private readonly VoteWebhooks _voteWebhooks = default!;
  30. private VotingSystem? _votingSystem;
  31. private RoleSystem? _roleSystem;
  32. private GameTicker? _gameTicker;
  33. private static readonly Dictionary<StandardVoteType, CVarDef<bool>> VoteTypesToEnableCVars = new()
  34. {
  35. {StandardVoteType.Restart, CCVars.VoteRestartEnabled},
  36. {StandardVoteType.Preset, CCVars.VotePresetEnabled},
  37. {StandardVoteType.Map, CCVars.VoteMapEnabled},
  38. {StandardVoteType.Votekick, CCVars.VotekickEnabled}
  39. };
  40. public void CreateStandardVote(ICommonSession? initiator, StandardVoteType voteType, string[]? args = null)
  41. {
  42. if (initiator != null && args == null)
  43. _adminLogger.Add(LogType.Vote, LogImpact.Medium, $"{initiator} initiated a {voteType.ToString()} vote");
  44. else if (initiator != null && args != null)
  45. _adminLogger.Add(LogType.Vote, LogImpact.Extreme, $"{initiator} initiated a {voteType.ToString()} vote with the arguments: {String.Join(",", args)}");
  46. else
  47. _adminLogger.Add(LogType.Vote, LogImpact.Medium, $"Initiated a {voteType.ToString()} vote");
  48. _gameTicker = _entityManager.EntitySysManager.GetEntitySystem<GameTicker>();
  49. bool timeoutVote = true;
  50. switch (voteType)
  51. {
  52. case StandardVoteType.Restart:
  53. CreateRestartVote(initiator);
  54. break;
  55. case StandardVoteType.Preset:
  56. CreatePresetVote(initiator);
  57. break;
  58. case StandardVoteType.Map:
  59. CreateMapVote(initiator);
  60. break;
  61. case StandardVoteType.Votekick:
  62. timeoutVote = false; // Allows the timeout to be updated manually in the create method
  63. CreateVotekickVote(initiator, args);
  64. break;
  65. default:
  66. throw new ArgumentOutOfRangeException(nameof(voteType), voteType, null);
  67. }
  68. _gameTicker.UpdateInfoText();
  69. if (timeoutVote)
  70. TimeoutStandardVote(voteType);
  71. }
  72. private void CreateRestartVote(ICommonSession? initiator)
  73. {
  74. var playerVoteMaximum = _cfg.GetCVar(CCVars.VoteRestartMaxPlayers);
  75. var totalPlayers = _playerManager.Sessions.Count(session => session.Status != SessionStatus.Disconnected);
  76. var ghostVotePercentageRequirement = _cfg.GetCVar(CCVars.VoteRestartGhostPercentage);
  77. var ghostVoterPercentage = CalculateEligibleVoterPercentage(VoterEligibility.Ghost);
  78. if (totalPlayers <= playerVoteMaximum || ghostVoterPercentage >= ghostVotePercentageRequirement)
  79. {
  80. StartVote(initiator);
  81. }
  82. else
  83. {
  84. NotifyNotEnoughGhostPlayers(ghostVotePercentageRequirement, ghostVoterPercentage);
  85. }
  86. }
  87. /// <summary>
  88. /// Gives the current percentage of players eligible to vote, rounded to nearest percentage point.
  89. /// </summary>
  90. /// <param name="eligibility">The eligibility requirement to vote.</param>
  91. public int CalculateEligibleVoterPercentage(VoterEligibility eligibility)
  92. {
  93. var eligibleCount = CalculateEligibleVoterNumber(eligibility);
  94. var totalPlayers = _playerManager.Sessions.Count(session => session.Status != SessionStatus.Disconnected);
  95. var eligiblePercentage = 0.0;
  96. if (totalPlayers > 0)
  97. {
  98. eligiblePercentage = ((double)eligibleCount / totalPlayers) * 100;
  99. }
  100. var roundedEligiblePercentage = (int)Math.Round(eligiblePercentage);
  101. return roundedEligiblePercentage;
  102. }
  103. /// <summary>
  104. /// Gives the current number of players eligible to vote.
  105. /// </summary>
  106. /// <param name="eligibility">The eligibility requirement to vote.</param>
  107. public int CalculateEligibleVoterNumber(VoterEligibility eligibility)
  108. {
  109. var eligibleCount = 0;
  110. foreach (var player in _playerManager.Sessions)
  111. {
  112. _playerManager.UpdateState(player);
  113. if (player.Status != SessionStatus.Disconnected && CheckVoterEligibility(player, eligibility))
  114. {
  115. eligibleCount++;
  116. }
  117. }
  118. return eligibleCount;
  119. }
  120. private void StartVote(ICommonSession? initiator)
  121. {
  122. var alone = _playerManager.PlayerCount == 1 && initiator != null;
  123. var options = new VoteOptions
  124. {
  125. Title = Loc.GetString("ui-vote-restart-title"),
  126. Options =
  127. {
  128. (Loc.GetString("ui-vote-restart-yes"), "yes"),
  129. (Loc.GetString("ui-vote-restart-no"), "no"),
  130. (Loc.GetString("ui-vote-restart-abstain"), "abstain")
  131. },
  132. Duration = alone
  133. ? TimeSpan.FromSeconds(_cfg.GetCVar(CCVars.VoteTimerAlone))
  134. : TimeSpan.FromSeconds(_cfg.GetCVar(CCVars.VoteTimerRestart)),
  135. InitiatorTimeout = TimeSpan.FromMinutes(5)
  136. };
  137. if (alone)
  138. options.InitiatorTimeout = TimeSpan.FromSeconds(10);
  139. WirePresetVoteInitiator(options, initiator);
  140. var vote = CreateVote(options);
  141. vote.OnFinished += (_, _) =>
  142. {
  143. var votesYes = vote.VotesPerOption["yes"];
  144. var votesNo = vote.VotesPerOption["no"];
  145. var total = votesYes + votesNo;
  146. var ratioRequired = _cfg.GetCVar(CCVars.VoteRestartRequiredRatio);
  147. if (total > 0 && votesYes / (float)total >= ratioRequired)
  148. {
  149. // Check if an admin is online, and ignore the passed vote if the cvar is enabled
  150. if (_cfg.GetCVar(CCVars.VoteRestartNotAllowedWhenAdminOnline) && _adminMgr.ActiveAdmins.Count() != 0)
  151. {
  152. _adminLogger.Add(LogType.Vote, LogImpact.Medium, $"Restart vote attempted to pass, but an admin was online. {votesYes}/{votesNo}");
  153. }
  154. else // If the cvar is disabled or there's no admins on, proceed as normal
  155. {
  156. _adminLogger.Add(LogType.Vote, LogImpact.Medium, $"Restart vote succeeded: {votesYes}/{votesNo}");
  157. _chatManager.DispatchServerAnnouncement(Loc.GetString("ui-vote-restart-succeeded"));
  158. var roundEnd = _entityManager.EntitySysManager.GetEntitySystem<RoundEndSystem>();
  159. roundEnd.EndRound();
  160. }
  161. }
  162. else
  163. {
  164. _adminLogger.Add(LogType.Vote, LogImpact.Medium, $"Restart vote failed: {votesYes}/{votesNo}");
  165. _chatManager.DispatchServerAnnouncement(
  166. Loc.GetString("ui-vote-restart-failed", ("ratio", ratioRequired)));
  167. }
  168. };
  169. if (initiator != null)
  170. {
  171. // Cast yes vote if created the vote yourself.
  172. vote.CastVote(initiator, 0);
  173. }
  174. foreach (var player in _playerManager.Sessions)
  175. {
  176. if (player != initiator)
  177. {
  178. // Everybody else defaults to an abstain vote to say they don't mind.
  179. vote.CastVote(player, 2);
  180. }
  181. }
  182. }
  183. private void NotifyNotEnoughGhostPlayers(int ghostPercentageRequirement, int roundedGhostPercentage)
  184. {
  185. // Logic to notify that there are not enough ghost players to start a vote
  186. _adminLogger.Add(LogType.Vote, LogImpact.Medium, $"Restart vote failed: Current Ghost player percentage:{roundedGhostPercentage.ToString()}% does not meet {ghostPercentageRequirement.ToString()}%");
  187. _chatManager.DispatchServerAnnouncement(
  188. Loc.GetString("ui-vote-restart-fail-not-enough-ghost-players", ("ghostPlayerRequirement", ghostPercentageRequirement)));
  189. }
  190. private void CreatePresetVote(ICommonSession? initiator)
  191. {
  192. var presets = GetGamePresets();
  193. var alone = _playerManager.PlayerCount == 1 && initiator != null;
  194. var options = new VoteOptions
  195. {
  196. Title = Loc.GetString("ui-vote-gamemode-title"),
  197. Duration = alone
  198. ? TimeSpan.FromSeconds(_cfg.GetCVar(CCVars.VoteTimerAlone))
  199. : TimeSpan.FromSeconds(_cfg.GetCVar(CCVars.VoteTimerPreset))
  200. };
  201. if (alone)
  202. options.InitiatorTimeout = TimeSpan.FromSeconds(10);
  203. foreach (var (k, v) in presets)
  204. {
  205. options.Options.Add((Loc.GetString(v), k));
  206. }
  207. WirePresetVoteInitiator(options, initiator);
  208. var vote = CreateVote(options);
  209. vote.OnFinished += (_, args) =>
  210. {
  211. string picked;
  212. if (args.Winner == null)
  213. {
  214. picked = (string)_random.Pick(args.Winners);
  215. _chatManager.DispatchServerAnnouncement(
  216. Loc.GetString("ui-vote-gamemode-tie", ("picked", Loc.GetString(presets[picked]))));
  217. }
  218. else
  219. {
  220. picked = (string)args.Winner;
  221. _chatManager.DispatchServerAnnouncement(
  222. Loc.GetString("ui-vote-gamemode-win", ("winner", Loc.GetString(presets[picked]))));
  223. }
  224. _adminLogger.Add(LogType.Vote, LogImpact.Medium, $"Preset vote finished: {picked}");
  225. var ticker = _entityManager.EntitySysManager.GetEntitySystem<GameTicker>();
  226. ticker.SetGamePreset(picked);
  227. };
  228. }
  229. public void CreateMapVote(ICommonSession? initiator)
  230. {
  231. var maps = _gameMapManager.CurrentlyEligibleMaps().ToDictionary(map => map, map => map.MapName);
  232. var alone = _playerManager.PlayerCount == 1 && initiator != null;
  233. var options = new VoteOptions
  234. {
  235. Title = Loc.GetString("ui-vote-map-title"),
  236. Duration = alone
  237. ? TimeSpan.FromSeconds(_cfg.GetCVar(CCVars.VoteTimerAlone))
  238. : TimeSpan.FromSeconds(_cfg.GetCVar(CCVars.VoteTimerMap))
  239. };
  240. if (alone)
  241. options.InitiatorTimeout = TimeSpan.FromSeconds(10);
  242. foreach (var (k, v) in maps)
  243. {
  244. options.Options.Add((v, k));
  245. }
  246. WirePresetVoteInitiator(options, initiator);
  247. var vote = CreateVote(options);
  248. vote.OnFinished += (_, args) =>
  249. {
  250. GameMapPrototype picked;
  251. if (args.Winner == null)
  252. {
  253. picked = (GameMapPrototype)_random.Pick(args.Winners);
  254. _chatManager.DispatchServerAnnouncement(
  255. Loc.GetString("ui-vote-map-tie", ("picked", maps[picked])));
  256. }
  257. else
  258. {
  259. picked = (GameMapPrototype)args.Winner;
  260. _chatManager.DispatchServerAnnouncement(
  261. Loc.GetString("ui-vote-map-win", ("winner", maps[picked])));
  262. }
  263. _adminLogger.Add(LogType.Vote, LogImpact.Medium, $"Map vote finished: {picked.MapName}");
  264. var ticker = _entityManager.EntitySysManager.GetEntitySystem<GameTicker>();
  265. if (ticker.CanUpdateMap())
  266. {
  267. if (_gameMapManager.TrySelectMapIfEligible(picked.ID))
  268. {
  269. ticker.UpdateInfoText();
  270. }
  271. }
  272. else
  273. {
  274. _chatManager.DispatchServerAnnouncement(Loc.GetString("ui-vote-map-notlobby"));
  275. }
  276. };
  277. }
  278. private async void CreateVotekickVote(ICommonSession? initiator, string[]? args)
  279. {
  280. if (args == null || args.Length <= 1)
  281. {
  282. return;
  283. }
  284. if (_roleSystem == null)
  285. _roleSystem = _entityManager.SystemOrNull<RoleSystem>();
  286. if (_votingSystem == null)
  287. _votingSystem = _entityManager.SystemOrNull<VotingSystem>();
  288. // Check that the initiator is actually allowed to do a votekick.
  289. if (_votingSystem != null && !await _votingSystem.CheckVotekickInitEligibility(initiator))
  290. {
  291. _logManager.GetSawmill("admin.votekick").Warning($"User {initiator} attempted a votekick, despite not being eligible to!");
  292. _adminLogger.Add(LogType.Vote, LogImpact.Extreme, $"Votekick attempted by {initiator}, but they are not eligible to votekick!");
  293. DirtyCanCallVoteAll();
  294. return;
  295. }
  296. var voterEligibility = _cfg.GetCVar(CCVars.VotekickVoterGhostRequirement) ? VoterEligibility.GhostMinimumPlaytime : VoterEligibility.MinimumPlaytime;
  297. if (_cfg.GetCVar(CCVars.VotekickIgnoreGhostReqInLobby) && _gameTicker!.RunLevel == GameRunLevel.PreRoundLobby)
  298. voterEligibility = VoterEligibility.MinimumPlaytime;
  299. var eligibleVoterNumberRequirement = _cfg.GetCVar(CCVars.VotekickEligibleNumberRequirement);
  300. var eligibleVoterNumber = CalculateEligibleVoterNumber(voterEligibility);
  301. string target = args[0];
  302. string reason = args[1];
  303. // Start by getting all relevant target data
  304. var located = await _locator.LookupIdByNameOrIdAsync(target);
  305. if (located == null)
  306. {
  307. _logManager.GetSawmill("admin.votekick")
  308. .Warning($"Votekick attempted for player {target} but they couldn't be found!");
  309. _adminLogger.Add(LogType.Vote, LogImpact.Extreme, $"Votekick attempted by {initiator} for player string {target}, but they could not be found!");
  310. DirtyCanCallVoteAll();
  311. return;
  312. }
  313. var targetUid = located.UserId;
  314. var targetHWid = located.LastHWId;
  315. (IPAddress, int)? targetIP = null;
  316. if (located.LastAddress is not null)
  317. {
  318. targetIP = located.LastAddress.AddressFamily is AddressFamily.InterNetwork
  319. ? (located.LastAddress, 32) // People with ipv4 addresses get a /32 address so we ban that
  320. : (located.LastAddress, 64); // This can only be an ipv6 address. People with ipv6 address should get /64 addresses so we ban that.
  321. }
  322. if (!_playerManager.TryGetSessionById(located.UserId, out ICommonSession? targetSession))
  323. {
  324. _logManager.GetSawmill("admin.votekick")
  325. .Warning($"Votekick attempted for player {target} but their session couldn't be found!");
  326. _adminLogger.Add(LogType.Vote, LogImpact.Extreme, $"Votekick attempted by {initiator} for player string {target}, but they could not be found!");
  327. DirtyCanCallVoteAll();
  328. return;
  329. }
  330. string targetEntityName = located.Username; // Target's player-facing name when voting; uses the player's username as fallback if no entity name is found
  331. if (targetSession.AttachedEntity is { Valid: true } attached && _votingSystem != null)
  332. targetEntityName = _votingSystem.GetPlayerVoteListName(attached);
  333. var isAntagSafe = false;
  334. var targetMind = targetSession.GetMind();
  335. var playtime = _playtimeManager.GetPlayTimes(targetSession);
  336. // Check whether the target is an antag, and if they are, give them protection against the Raider votekick if they have the requisite hours.
  337. if (targetMind != null &&
  338. _roleSystem != null &&
  339. _roleSystem.MindIsAntagonist(targetMind) &&
  340. playtime.TryGetValue(PlayTimeTrackingShared.TrackerOverall, out TimeSpan overallTime) &&
  341. overallTime >= TimeSpan.FromHours(_cfg.GetCVar(CCVars.VotekickAntagRaiderProtection)))
  342. {
  343. isAntagSafe = true;
  344. }
  345. // Don't let a user votekick themselves
  346. if (initiator == targetSession)
  347. {
  348. _adminLogger.Add(LogType.Vote, LogImpact.Extreme, $"Votekick attempted by {initiator} for themselves? Votekick cancelled.");
  349. DirtyCanCallVoteAll();
  350. return;
  351. }
  352. // Cancels the vote if there's not enough voters; only the person initiating the vote gets a return message.
  353. if (eligibleVoterNumber < eligibleVoterNumberRequirement)
  354. {
  355. _adminLogger.Add(LogType.Vote, LogImpact.Extreme, $"Votekick attempted by {initiator} for player {targetSession}, but there were not enough ghost roles! {eligibleVoterNumberRequirement} required, {eligibleVoterNumber} found.");
  356. if (initiator != null)
  357. {
  358. var message = Loc.GetString("ui-vote-votekick-not-enough-eligible", ("voters", eligibleVoterNumber.ToString()), ("requirement", eligibleVoterNumberRequirement.ToString()));
  359. var wrappedMessage = Loc.GetString("chat-manager-server-wrap-message", ("message", message));
  360. _chatManager.ChatMessageToOne(ChatChannel.Server, message, wrappedMessage, default, false, initiator.Channel);
  361. }
  362. DirtyCanCallVoteAll();
  363. return;
  364. }
  365. // Check for stuff like the target being an admin. These targets shouldn't show up in the UI, but it's necessary to doublecheck in case someone writes the command in console.
  366. if (_votingSystem != null && !_votingSystem.CheckVotekickTargetEligibility(targetSession))
  367. {
  368. _adminLogger.Add(LogType.Vote, LogImpact.Extreme, $"Votekick attempted by {initiator} for player {targetSession}, but they are not eligible to be votekicked!");
  369. DirtyCanCallVoteAll();
  370. return;
  371. }
  372. // Create the vote object
  373. string voteTitle = "";
  374. NetEntity? targetNetEntity = _entityManager.GetNetEntity(targetSession.AttachedEntity);
  375. var initiatorName = initiator != null ? initiator.Name : Loc.GetString("ui-vote-votekick-unknown-initiator");
  376. voteTitle = Loc.GetString("ui-vote-votekick-title", ("initiator", initiatorName), ("targetEntity", targetEntityName), ("reason", reason));
  377. var options = new VoteOptions
  378. {
  379. Title = voteTitle,
  380. Options =
  381. {
  382. (Loc.GetString("ui-vote-votekick-yes"), "yes"),
  383. (Loc.GetString("ui-vote-votekick-no"), "no"),
  384. (Loc.GetString("ui-vote-votekick-abstain"), "abstain")
  385. },
  386. Duration = TimeSpan.FromSeconds(_cfg.GetCVar(CCVars.VotekickTimer)),
  387. InitiatorTimeout = TimeSpan.FromMinutes(_cfg.GetCVar(CCVars.VotekickTimeout)),
  388. VoterEligibility = voterEligibility,
  389. DisplayVotes = false,
  390. TargetEntity = targetNetEntity
  391. };
  392. WirePresetVoteInitiator(options, initiator);
  393. var vote = CreateVote(options);
  394. _adminLogger.Add(LogType.Vote, LogImpact.Extreme, $"Votekick for {located.Username} ({targetEntityName}) due to {reason} started, initiated by {initiator}.");
  395. // Create Discord webhook
  396. var webhookState = _voteWebhooks.CreateWebhookIfConfigured(options, _cfg.GetCVar(CCVars.DiscordVotekickWebhook), Loc.GetString("votekick-webhook-name"), options.Title + "\n" + Loc.GetString("votekick-webhook-description", ("initiator", initiatorName), ("target", targetSession)));
  397. // Time out the vote now that we know it will happen
  398. TimeoutStandardVote(StandardVoteType.Votekick);
  399. vote.OnFinished += (_, eventArgs) =>
  400. {
  401. var votesYes = vote.VotesPerOption["yes"];
  402. var votesNo = vote.VotesPerOption["no"];
  403. var total = votesYes + votesNo;
  404. // Get the voters, for logging purposes.
  405. List<ICommonSession> yesVoters = new();
  406. List<ICommonSession> noVoters = new();
  407. foreach (var (voter, castVote) in vote.CastVotes)
  408. {
  409. if (castVote == 0)
  410. {
  411. yesVoters.Add(voter);
  412. }
  413. if (castVote == 1)
  414. {
  415. noVoters.Add(voter);
  416. }
  417. }
  418. var yesVotersString = string.Join(", ", yesVoters);
  419. var noVotersString = string.Join(", ", noVoters);
  420. var ratioRequired = _cfg.GetCVar(CCVars.VotekickRequiredRatio);
  421. if (total > 0 && votesYes / (float)total >= ratioRequired)
  422. {
  423. // Some conditions that cancel the vote want to let the vote run its course first and then cancel it
  424. // so we check for that here
  425. // Check if an admin is online, and ignore the vote if the cvar is enabled
  426. if (_cfg.GetCVar(CCVars.VotekickNotAllowedWhenAdminOnline) && _adminMgr.ActiveAdmins.Count() != 0)
  427. {
  428. _adminLogger.Add(LogType.Vote, LogImpact.Extreme, $"Votekick for {located.Username} attempted to pass, but an admin was online. Yes: {votesYes} / No: {votesNo}. Yes: {yesVotersString} / No: {noVotersString}");
  429. AnnounceCancelledVotekickForVoters(targetEntityName);
  430. _voteWebhooks.UpdateCancelledWebhookIfConfigured(webhookState, Loc.GetString("votekick-webhook-cancelled-admin-online"));
  431. return;
  432. }
  433. // Check if the target is an antag and the vote reason is raiding (this is to prevent false positives)
  434. else if (isAntagSafe && reason == VotekickReasonType.Raiding.ToString())
  435. {
  436. _adminLogger.Add(LogType.Vote, LogImpact.Extreme, $"Votekick for {located.Username} due to {reason} finished, created by {initiator}, but was cancelled due to the target being an antagonist.");
  437. AnnounceCancelledVotekickForVoters(targetEntityName);
  438. _voteWebhooks.UpdateCancelledWebhookIfConfigured(webhookState, Loc.GetString("votekick-webhook-cancelled-antag-target"));
  439. return;
  440. }
  441. // Check if the target is an admin/de-admined admin
  442. else if (targetSession.AttachedEntity != null && _adminMgr.IsAdmin(targetSession.AttachedEntity.Value, true))
  443. {
  444. _adminLogger.Add(LogType.Vote, LogImpact.Extreme, $"Votekick for {located.Username} due to {reason} finished, created by {initiator}, but was cancelled due to the target being a de-admined admin.");
  445. AnnounceCancelledVotekickForVoters(targetEntityName);
  446. _voteWebhooks.UpdateCancelledWebhookIfConfigured(webhookState, Loc.GetString("votekick-webhook-cancelled-admin-target"));
  447. return;
  448. }
  449. else
  450. {
  451. _adminLogger.Add(LogType.Vote, LogImpact.Extreme, $"Votekick for {located.Username} succeeded: Yes: {votesYes} / No: {votesNo}. Yes: {yesVotersString} / No: {noVotersString}");
  452. _chatManager.DispatchServerAnnouncement(Loc.GetString("ui-vote-votekick-success", ("target", targetEntityName), ("reason", reason)));
  453. if (!Enum.TryParse(_cfg.GetCVar(CCVars.VotekickBanDefaultSeverity), out NoteSeverity severity))
  454. {
  455. _logManager.GetSawmill("admin.votekick")
  456. .Warning("Votekick ban severity could not be parsed from config! Defaulting to high.");
  457. severity = NoteSeverity.High;
  458. }
  459. // Discord webhook, success
  460. _voteWebhooks.UpdateWebhookIfConfigured(webhookState, eventArgs);
  461. uint minutes = (uint)_cfg.GetCVar(CCVars.VotekickBanDuration);
  462. _bans.CreateServerBan(targetUid, target, null, targetIP, targetHWid, minutes, severity, Loc.GetString("votekick-ban-reason", ("reason", reason)));
  463. }
  464. }
  465. else
  466. {
  467. // Discord webhook, failure
  468. _voteWebhooks.UpdateWebhookIfConfigured(webhookState, eventArgs);
  469. _adminLogger.Add(LogType.Vote, LogImpact.Extreme, $"Votekick failed: Yes: {votesYes} / No: {votesNo}. Yes: {yesVotersString} / No: {noVotersString}");
  470. _chatManager.DispatchServerAnnouncement(Loc.GetString("ui-vote-votekick-failure", ("target", targetEntityName), ("reason", reason)));
  471. }
  472. };
  473. if (initiator != null)
  474. {
  475. // Cast yes vote if created the vote yourself.
  476. vote.CastVote(initiator, 0);
  477. }
  478. }
  479. private void AnnounceCancelledVotekickForVoters(string target)
  480. {
  481. foreach (var player in _playerManager.Sessions)
  482. {
  483. if (CheckVoterEligibility(player, VoterEligibility.GhostMinimumPlaytime))
  484. {
  485. var message = Loc.GetString("ui-vote-votekick-server-cancelled", ("target", target));
  486. var wrappedMessage = Loc.GetString("chat-manager-server-wrap-message", ("message", message));
  487. _chatManager.ChatMessageToOne(ChatChannel.Server, message, wrappedMessage, default, false, player.Channel);
  488. }
  489. }
  490. }
  491. private void TimeoutStandardVote(StandardVoteType type)
  492. {
  493. var timeout = TimeSpan.FromSeconds(_cfg.GetCVar(CCVars.VoteSameTypeTimeout));
  494. _standardVoteTimeout[type] = _timing.RealTime + timeout;
  495. DirtyCanCallVoteAll();
  496. }
  497. private Dictionary<string, string> GetGamePresets()
  498. {
  499. var presets = new Dictionary<string, string>();
  500. foreach (var preset in _prototypeManager.EnumeratePrototypes<GamePresetPrototype>())
  501. {
  502. if (!preset.ShowInVote)
  503. continue;
  504. if (_playerManager.PlayerCount < (preset.MinPlayers ?? int.MinValue))
  505. continue;
  506. if (_playerManager.PlayerCount > (preset.MaxPlayers ?? int.MaxValue))
  507. continue;
  508. presets[preset.ID] = preset.ModeTitle;
  509. }
  510. return presets;
  511. }
  512. }
  513. }