# Conflicts: # .github/CODEOWNERS # .github/PULL_REQUEST_TEMPLATE.md # Content.Client/Changelog/ChangelogTab.xaml.cs # Content.Client/Chemistry/UI/ChemMasterWindow.xaml # Content.Client/Entry/EntryPoint.cs # Content.Client/GameTicking/Managers/ClientGameTicker.cs # Content.Client/Launcher/LauncherConnectingGui.xaml # Content.Client/Lobby/LobbyState.cs # Content.Client/Lobby/UI/LobbyGui.xaml # Content.Client/Paper/UI/PaperWindow.xaml.cs # Content.Client/StationRecords/GeneralStationRecordConsoleWindow.xaml.cs # Content.Client/Stylesheets/StyleNano.cs # Content.Client/UserInterface/Screens/SeparatedChatGameScreen.xaml # Content.Client/UserInterface/Systems/Ghost/Widgets/GhostGui.xaml.cs # Content.Client/UserInterface/Systems/MenuBar/Widgets/GameTopMenuBar.xaml # Content.IntegrationTests/Tests/Destructible/DestructibleDamageGroupTest.cs # Content.IntegrationTests/Tests/PostMapInitTest.cs # Content.IntegrationTests/Tests/Power/StationPowerTests.cs # Content.Packaging/ServerPackaging.cs # Content.Server/Administration/Systems/AdminVerbSystem.Smites.cs # Content.Server/Administration/Systems/AdminVerbSystem.Tools.cs # Content.Server/Atmos/TileAtmosphere.cs # Content.Server/Bible/BibleSystem.cs # Content.Server/Chat/Systems/ChatSystem.cs # Content.Server/Content.Server.csproj # Content.Server/Discord/DiscordLink/DiscordLink.cs # Content.Server/GameTicking/GameTicker.LobbyBackground.cs # Content.Server/GameTicking/GameTicker.Spawning.cs # Content.Server/Ghost/GhostSystem.cs # Content.Server/Kitchen/Components/MicrowaveComponent.cs # Content.Server/Kitchen/EntitySystems/MicrowaveSystem.cs # Content.Server/Mech/Systems/MechSystem.cs # Content.Server/Medical/CrewMonitoring/CrewMonitoringConsoleSystem.cs # Content.Server/Medical/DefibrillatorSystem.cs # Content.Server/Medical/HealthAnalyzerSystem.cs # Content.Server/Radio/EntitySystems/HeadsetSystem.cs # Content.Server/Remotes/DoorRemoteSystem.cs # Content.Server/RoundEnd/RoundEndSystem.cs # Content.Server/Shuttles/Systems/DockingSystem.Shuttle.cs # Content.Server/Silicons/Borgs/BorgSystem.Transponder.cs # Content.Server/Tips/TipsSystem.cs # Content.Server/Traitor/Uplink/UplinkSystem.cs # Content.Server/Weapons/Ranged/Systems/GunSystem.cs # Content.Server/Zombies/ZombieSystem.Transform.cs # Content.Server/Zombies/ZombieSystem.cs # Content.Shared/Bed/BedSystem.cs # Content.Shared/Blocking/BlockingSystem.User.cs # Content.Shared/Buckle/SharedBuckleSystem.Buckle.cs # Content.Shared/Chat/SharedSuicideSystem.cs # Content.Shared/Chemistry/Components/HyposprayComponent.cs # Content.Shared/Chemistry/EntitySystems/HypospraySystem.cs # Content.Shared/Chemistry/EntitySystems/ScoopableSolutionSystem.cs # Content.Shared/Chemistry/EntitySystems/SolutionTransferSystem.cs # Content.Shared/Chemistry/Events/HyposprayEvents.cs # Content.Shared/Content.Shared.csproj # Content.Shared/Damage/Systems/DamageableSystem.cs # Content.Shared/Friction/TileFrictionController.cs # Content.Shared/GameTicking/SharedGameTicker.cs # Content.Shared/Interaction/InteractHand.cs # Content.Shared/Lock/LockSystem.cs # Content.Shared/Medical/Healing/HealingSystem.cs # Content.Shared/Mobs/Systems/MobStateSystem.Subscribers.cs # Content.Shared/Nutrition/EntitySystems/HungerSystem.cs # Content.Shared/Silicons/StationAi/SharedStationAiSystem.Airlock.cs # Content.Shared/Throwing/ThrowingSystem.cs # Content.Shared/Trigger/Systems/TriggerOnHitscanSystem.cs # Content.Shared/Weapons/Hitscan/Systems/HitscanBasicDamageSystem.cs # Content.Shared/Weapons/Melee/SharedMeleeWeaponSystem.cs # Content.Shared/Weapons/Ranged/Components/BatteryWeaponFireModesComponent.cs # Content.Shared/Weapons/Ranged/Components/HitscanBatteryAmmoProviderComponent.cs # Content.Shared/Weapons/Ranged/Systems/BatteryWeaponFireModesSystem.cs # Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.Ballistic.cs # Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.Battery.cs # Content.Shared/Zombies/ZombieComponent.cs # Content.Shared/_Sunrise/Lobby/LobbyBackgroundPrototype.cs # README.md # Resources/Locale/en-US/_strings/borg/borg.ftl # Resources/Locale/en-US/_strings/chemistry/components/hypospray-component.ftl # Resources/Locale/en-US/_strings/chemistry/components/injector-component.ftl # Resources/Locale/en-US/_strings/commands/pauseatmos-command.ftl # Resources/Locale/en-US/_strings/commands/quick-inspect-command.ftl # Resources/Locale/en-US/_strings/commands/show-wallmounts-command.ftl # Resources/Locale/en-US/_strings/commands/substepatmos-command.ftl # Resources/Locale/en-US/_strings/fluids/components/equip-spray-component.ftl # Resources/Locale/en-US/_strings/game-ticking/game-presets/preset-revolutionary.ftl # Resources/Locale/en-US/_strings/job/job-names.ftl # Resources/Locale/en-US/_strings/lobby/lobby-state-background.ftl # Resources/Locale/en-US/_strings/reagents/meta/medicine.ftl # Resources/Locale/en-US/_strings/reagents/meta/physical-desc.ftl # Resources/Locale/en-US/_strings/silicons/borg-module.ftl # Resources/Locale/en-US/_strings/store/changeling-catalog.ftl # Resources/Locale/en-US/_strings/store/currency.ftl # Resources/Locale/en-US/_strings/store/store.ftl # Resources/Locale/en-US/_strings/store/uplink-catalog.ftl # Resources/Prototypes/Body/Organs/slime.yml # Resources/Prototypes/Catalog/Fills/Backpacks/backpack.yml # Resources/Prototypes/Catalog/Fills/Backpacks/duffelbag.yml # Resources/Prototypes/Catalog/Fills/Boxes/ammunition.yml # Resources/Prototypes/Catalog/Fills/Boxes/emergency.yml # Resources/Prototypes/Catalog/Fills/Crates/antag.yml # Resources/Prototypes/Catalog/Fills/Crates/armory.yml # Resources/Prototypes/Catalog/Fills/Crates/botany.yml # Resources/Prototypes/Catalog/Fills/Crates/fun.yml # Resources/Prototypes/Catalog/Fills/Crates/medical.yml # Resources/Prototypes/Catalog/Fills/Crates/salvage.yml # Resources/Prototypes/Catalog/Fills/Crates/security.yml # Resources/Prototypes/Catalog/Fills/Crates/service.yml # Resources/Prototypes/Catalog/Fills/Crates/syndicate.yml # Resources/Prototypes/Catalog/Fills/Items/belt.yml # Resources/Prototypes/Catalog/Fills/Items/briefcases.yml # Resources/Prototypes/Catalog/Fills/Items/firstaidkits.yml # Resources/Prototypes/Catalog/Fills/Items/toolboxes.yml # Resources/Prototypes/Catalog/Fills/Lockers/dressers.yml # Resources/Prototypes/Catalog/Fills/Lockers/heads.yml # Resources/Prototypes/Catalog/Fills/Lockers/security.yml # Resources/Prototypes/Catalog/VendingMachines/Inventories/theater.yml # Resources/Prototypes/Catalog/uplink_catalog.yml # Resources/Prototypes/Entities/Clothing/Back/backpacks.yml # Resources/Prototypes/Entities/Clothing/Shoes/magboots.yml # Resources/Prototypes/Entities/Debugging/debug_sweps.yml # Resources/Prototypes/Entities/Debugging/stress_test.yml # Resources/Prototypes/Entities/Markers/Spawners/Random/anomaly.yml # Resources/Prototypes/Entities/Markers/Spawners/Random/toy.yml # Resources/Prototypes/Entities/Mobs/Cyborgs/base_borg_chassis.yml # Resources/Prototypes/Entities/Mobs/Cyborgs/xenoborgs.yml # Resources/Prototypes/Entities/Mobs/NPCs/animals.yml # Resources/Prototypes/Entities/Mobs/NPCs/carp.yml # Resources/Prototypes/Entities/Objects/Devices/base_handheld.yml # Resources/Prototypes/Entities/Objects/Fun/Instruments/instruments_misc.yml # Resources/Prototypes/Entities/Objects/Fun/dice_bag.yml # Resources/Prototypes/Entities/Objects/Fun/snap_pops.yml # Resources/Prototypes/Entities/Objects/Materials/Sheets/other.yml # Resources/Prototypes/Entities/Objects/Materials/parts.yml # Resources/Prototypes/Entities/Objects/Misc/identification_cards.yml # Resources/Prototypes/Entities/Objects/Misc/subdermal_implants.yml # Resources/Prototypes/Entities/Objects/Shields/shields.yml # Resources/Prototypes/Entities/Objects/Specific/Chapel/bibles.yml # Resources/Prototypes/Entities/Objects/Specific/Janitorial/janitor.yml # Resources/Prototypes/Entities/Objects/Specific/Medical/hypospray.yml # Resources/Prototypes/Entities/Objects/Specific/Medical/surgery.yml # Resources/Prototypes/Entities/Objects/Specific/Robotics/borg_modules.yml # Resources/Prototypes/Entities/Objects/Specific/Robotics/mmi.yml # Resources/Prototypes/Entities/Objects/Specific/Service/barber.yml # Resources/Prototypes/Entities/Objects/Tools/glowstick.yml # Resources/Prototypes/Entities/Objects/Weapons/Guns/Ammunition/Boxes/antimateriel.yml # Resources/Prototypes/Entities/Objects/Weapons/Guns/Ammunition/Boxes/magnum.yml # Resources/Prototypes/Entities/Objects/Weapons/Guns/Ammunition/Cartridges/heavy_rifle.yml # Resources/Prototypes/Entities/Objects/Weapons/Guns/Ammunition/Cartridges/light_rifle.yml # Resources/Prototypes/Entities/Objects/Weapons/Guns/Ammunition/Cartridges/magnum.yml # Resources/Prototypes/Entities/Objects/Weapons/Guns/Ammunition/Cartridges/pistol.yml # Resources/Prototypes/Entities/Objects/Weapons/Guns/Ammunition/Cartridges/rifle.yml # Resources/Prototypes/Entities/Objects/Weapons/Guns/Ammunition/Cartridges/shotgun.yml # Resources/Prototypes/Entities/Objects/Weapons/Guns/Ammunition/Magazines/light_rifle.yml # Resources/Prototypes/Entities/Objects/Weapons/Guns/Ammunition/Magazines/magnum.yml # Resources/Prototypes/Entities/Objects/Weapons/Guns/Ammunition/Magazines/pistol.yml # Resources/Prototypes/Entities/Objects/Weapons/Guns/Ammunition/Magazines/rifle.yml # Resources/Prototypes/Entities/Objects/Weapons/Guns/Ammunition/Projectiles/grenade.yml # Resources/Prototypes/Entities/Objects/Weapons/Guns/Ammunition/SpeedLoaders/magnum.yml # Resources/Prototypes/Entities/Objects/Weapons/Guns/Ammunition/SpeedLoaders/pistol.yml # Resources/Prototypes/Entities/Objects/Weapons/Guns/Ammunition/SpeedLoaders/rifle_light.yml # Resources/Prototypes/Entities/Objects/Weapons/Guns/Ammunition/explosives.yml # Resources/Prototypes/Entities/Objects/Weapons/Guns/Battery/battery_guns.yml # Resources/Prototypes/Entities/Objects/Weapons/Guns/LMGs/lmgs.yml # Resources/Prototypes/Entities/Objects/Weapons/Guns/Pistols/pistols.yml # Resources/Prototypes/Entities/Objects/Weapons/Guns/Projectiles/hitscan.yml # Resources/Prototypes/Entities/Objects/Weapons/Guns/Projectiles/projectiles.yml # Resources/Prototypes/Entities/Objects/Weapons/Guns/Rifles/rifles.yml # Resources/Prototypes/Entities/Objects/Weapons/Guns/SMGs/smgs.yml # Resources/Prototypes/Entities/Objects/Weapons/Melee/knife.yml # Resources/Prototypes/Entities/Objects/Weapons/Throwable/grenades.yml # Resources/Prototypes/Entities/Structures/Furniture/dresser.yml # Resources/Prototypes/Entities/Structures/Machines/lathe.yml # Resources/Prototypes/Entities/Structures/Shuttles/cannons.yml # Resources/Prototypes/Entities/Structures/Walls/walls.yml # Resources/Prototypes/GameRules/roundstart.yml # Resources/Prototypes/GameRules/unknown_shuttles.yml # Resources/Prototypes/InventoryTemplates/aghost_inventory_template.yml # Resources/Prototypes/Loadouts/Jobs/Security/security_officer.yml # Resources/Prototypes/Loadouts/loadout_groups.yml # Resources/Prototypes/Loadouts/role_loadouts.yml # Resources/Prototypes/Maps/Pools/default.yml # Resources/Prototypes/Maps/amber.yml # Resources/Prototypes/Maps/oasis.yml # Resources/Prototypes/Maps/reach.yml # Resources/Prototypes/Objectives/objectiveGroups.yml # Resources/Prototypes/Reagents/Consumable/Drink/alcohol.yml # Resources/Prototypes/Reagents/medicine.yml # Resources/Prototypes/Reagents/narcotics.yml # Resources/Prototypes/Reagents/toxins.yml # Resources/Prototypes/Recipes/Reactions/medicine.yml # Resources/Prototypes/Roles/Jobs/Security/head_of_security.yml # Resources/Prototypes/Roles/Jobs/Security/security_cadet.yml # Resources/Prototypes/Roles/Jobs/Security/security_officer.yml # Resources/Prototypes/Roles/Jobs/Security/warden.yml # Resources/Prototypes/Roles/Jobs/Wildcards/zookeeper.yml # Resources/Prototypes/Roles/play_time_trackers.yml # Resources/Prototypes/Shuttles/shuttle_incoming_event.yml # Resources/Prototypes/Species/species_weights.yml # Resources/Prototypes/Store/presets.yml # Resources/Prototypes/ai_factions.yml # Resources/Prototypes/borg_types.yml # Resources/Prototypes/explosion.yml # Resources/Prototypes/game_presets.yml # Resources/Prototypes/lobbyscreens.yml # Resources/Prototypes/silicon-laws.yml # Resources/Textures/Clothing/Back/Duffels/syndicate.rsi/icon-med.png # Resources/Textures/Clothing/Back/Duffels/syndicate.rsi/meta.json # Resources/Textures/Objects/Specific/Mech/mecha.rsi/meta.json # Resources/Textures/Objects/Specific/Mech/mecha.rsi/ripley-broken.png # Resources/Textures/Objects/Specific/Mech/mecha.rsi/ripley-empty.png # Resources/Textures/Objects/Specific/Mech/mecha.rsi/ripley-open.png # Resources/Textures/Objects/Specific/Mech/mecha.rsi/ripley.png # Resources/Textures/Objects/Specific/Mech/ripley_construction.rsi/ripley_chassis.png # Resources/Textures/Objects/Specific/Mech/ripley_construction.rsi/ripley_harness+o.png # Resources/Textures/Objects/Specific/Mech/ripley_construction.rsi/ripley_harness.png # Resources/Textures/Objects/Specific/Medical/hypospray.rsi/meta.json # Resources/Textures/Objects/Specific/Medical/medical.rsi/meta.json # Resources/Textures/Objects/Weapons/Guns/Projectiles/projectiles.rsi/meta.json # Resources/Textures/Objects/Weapons/Guns/Projectiles/projectiles2.rsi/meta.json # Resources/Textures/Structures/Storage/canister.rsi/meta.json # Resources/Textures/Structures/Storage/tanks.rsi/meta.json # Resources/migration.yml # RobustToolbox # SECURITY.md # SpaceStation14.sln
708 lines
32 KiB
C#
708 lines
32 KiB
C#
using System.Collections.Specialized;
|
|
using System.Linq;
|
|
using System.Net;
|
|
using System.Net.Sockets;
|
|
using Content.Server._Sunrise.Presets;
|
|
using Content.Server.Administration;
|
|
using Content.Server.Administration.Managers;
|
|
using Content.Server.Discord.WebhookMessages;
|
|
using Content.Server.GameTicking;
|
|
using Content.Server.GameTicking.Presets;
|
|
using Content.Server.Roles;
|
|
using Content.Server.RoundEnd;
|
|
using Content.Shared._Sunrise.SunriseCCVars;
|
|
using Content.Shared.CCVar;
|
|
using Content.Shared.Chat;
|
|
using Content.Shared.Database;
|
|
using Content.Shared.Maps;
|
|
using Content.Shared.Players;
|
|
using Content.Shared.Players.PlayTimeTracking;
|
|
using Content.Shared.Voting;
|
|
using Robust.Shared.Configuration;
|
|
using Robust.Shared.Enums;
|
|
using Robust.Shared.Player;
|
|
using Robust.Shared.Random;
|
|
|
|
namespace Content.Server.Voting.Managers
|
|
{
|
|
public sealed partial class VoteManager
|
|
{
|
|
[Dependency] private readonly IPlayerLocator _locator = default!;
|
|
[Dependency] private readonly ILogManager _logManager = default!;
|
|
[Dependency] private readonly IBanManager _bans = default!;
|
|
[Dependency] private readonly VoteWebhooks _voteWebhooks = default!;
|
|
|
|
private VotingSystem? _votingSystem;
|
|
private RoleSystem? _roleSystem;
|
|
private GameTicker? _gameTicker;
|
|
|
|
private static readonly Dictionary<StandardVoteType, CVarDef<bool>> VoteTypesToEnableCVars = new()
|
|
{
|
|
{StandardVoteType.Restart, CCVars.VoteRestartEnabled},
|
|
{StandardVoteType.Preset, CCVars.VotePresetEnabled},
|
|
{StandardVoteType.Map, CCVars.VoteMapEnabled},
|
|
{StandardVoteType.Votekick, CCVars.VotekickEnabled}
|
|
};
|
|
|
|
public void CreateStandardVote(ICommonSession? initiator, StandardVoteType voteType, string[]? args = null)
|
|
{
|
|
if (initiator != null && args == null)
|
|
_adminLogger.Add(LogType.Vote, LogImpact.Medium, $"{initiator} initiated a {voteType.ToString()} vote");
|
|
else if (initiator != null && args != null)
|
|
_adminLogger.Add(LogType.Vote, LogImpact.Extreme, $"{initiator} initiated a {voteType.ToString()} vote with the arguments: {String.Join(",", args)}");
|
|
else
|
|
_adminLogger.Add(LogType.Vote, LogImpact.Medium, $"Initiated a {voteType.ToString()} vote");
|
|
|
|
_gameTicker = _entityManager.EntitySysManager.GetEntitySystem<GameTicker>();
|
|
|
|
bool timeoutVote = true;
|
|
|
|
_gameTicker = _entityManager.EntitySysManager.GetEntitySystem<GameTicker>();
|
|
_gameTicker.UpdateInfoText();
|
|
switch (voteType)
|
|
{
|
|
case StandardVoteType.Restart:
|
|
CreateRestartVote(initiator);
|
|
break;
|
|
case StandardVoteType.Preset:
|
|
CreatePresetVote(initiator);
|
|
break;
|
|
case StandardVoteType.Map:
|
|
CreateMapVote(initiator);
|
|
break;
|
|
case StandardVoteType.Votekick:
|
|
timeoutVote = false; // Allows the timeout to be updated manually in the create method
|
|
CreateVotekickVote(initiator, args);
|
|
break;
|
|
default:
|
|
throw new ArgumentOutOfRangeException(nameof(voteType), voteType, null);
|
|
}
|
|
_gameTicker.UpdateInfoText();
|
|
if (timeoutVote)
|
|
TimeoutStandardVote(voteType);
|
|
}
|
|
|
|
private void CreateRestartVote(ICommonSession? initiator)
|
|
{
|
|
|
|
var playerVoteMaximum = _cfg.GetCVar(CCVars.VoteRestartMaxPlayers);
|
|
var totalPlayers = _playerManager.Sessions.Count(session => session.Status != SessionStatus.Disconnected);
|
|
|
|
var ghostVotePercentageRequirement = _cfg.GetCVar(CCVars.VoteRestartGhostPercentage);
|
|
var ghostVoterPercentage = CalculateEligibleVoterPercentage(VoterEligibility.Ghost);
|
|
|
|
// Sunrise-Start
|
|
var showRestartVotes = _cfg.GetCVar(SunriseCCVars.ShowRestartVotes);
|
|
// Sunrise-End
|
|
|
|
if (totalPlayers <= playerVoteMaximum || ghostVoterPercentage >= ghostVotePercentageRequirement)
|
|
{
|
|
StartVote(initiator, showRestartVotes);
|
|
}
|
|
else
|
|
{
|
|
NotifyNotEnoughGhostPlayers(ghostVotePercentageRequirement, ghostVoterPercentage);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gives the current percentage of players eligible to vote, rounded to nearest percentage point.
|
|
/// </summary>
|
|
/// <param name="eligibility">The eligibility requirement to vote.</param>
|
|
public int CalculateEligibleVoterPercentage(VoterEligibility eligibility)
|
|
{
|
|
var eligibleCount = CalculateEligibleVoterNumber(eligibility);
|
|
var totalPlayers = _playerManager.Sessions.Count(session => session.Status != SessionStatus.Disconnected);
|
|
|
|
var eligiblePercentage = 0.0;
|
|
if (totalPlayers > 0)
|
|
{
|
|
eligiblePercentage = ((double)eligibleCount / totalPlayers) * 100;
|
|
}
|
|
|
|
var roundedEligiblePercentage = (int)Math.Round(eligiblePercentage);
|
|
|
|
return roundedEligiblePercentage;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gives the current number of players eligible to vote.
|
|
/// </summary>
|
|
/// <param name="eligibility">The eligibility requirement to vote.</param>
|
|
public int CalculateEligibleVoterNumber(VoterEligibility eligibility)
|
|
{
|
|
var eligibleCount = 0;
|
|
|
|
foreach (var player in _playerManager.Sessions)
|
|
{
|
|
_playerManager.UpdateState(player);
|
|
if (player.Status != SessionStatus.Disconnected && CheckVoterEligibility(player, eligibility))
|
|
{
|
|
eligibleCount++;
|
|
}
|
|
}
|
|
|
|
return eligibleCount;
|
|
}
|
|
|
|
private void StartVote(ICommonSession? initiator, bool displayVotes)
|
|
{
|
|
var alone = _playerManager.PlayerCount == 1 && initiator != null;
|
|
var options = new VoteOptions
|
|
{
|
|
Title = Loc.GetString("ui-vote-restart-title"),
|
|
Options =
|
|
{
|
|
(Loc.GetString("ui-vote-restart-yes"), "yes"),
|
|
(Loc.GetString("ui-vote-restart-no"), "no"),
|
|
(Loc.GetString("ui-vote-restart-abstain"), "abstain")
|
|
},
|
|
Duration = alone
|
|
? TimeSpan.FromSeconds(_cfg.GetCVar(CCVars.VoteTimerAlone))
|
|
: TimeSpan.FromSeconds(_cfg.GetCVar(CCVars.VoteTimerRestart)),
|
|
InitiatorTimeout = TimeSpan.FromMinutes(5),
|
|
DisplayVotes = displayVotes
|
|
};
|
|
|
|
if (alone)
|
|
options.InitiatorTimeout = TimeSpan.FromSeconds(10);
|
|
|
|
WirePresetVoteInitiator(options, initiator);
|
|
|
|
var vote = CreateVote(options);
|
|
|
|
vote.OnFinished += (_, _) =>
|
|
{
|
|
var votesYes = vote.VotesPerOption["yes"];
|
|
var votesNo = vote.VotesPerOption["no"];
|
|
var total = votesYes + votesNo;
|
|
|
|
var ratioRequired = _cfg.GetCVar(CCVars.VoteRestartRequiredRatio);
|
|
if (total > 0 && votesYes / (float) total >= ratioRequired)
|
|
{
|
|
// Check if an admin is online, and ignore the passed vote if the cvar is enabled
|
|
if (_cfg.GetCVar(CCVars.VoteRestartNotAllowedWhenAdminOnline) && _adminMgr.ActiveAdmins.Count() != 0)
|
|
{
|
|
_adminLogger.Add(LogType.Vote, LogImpact.Medium, $"Restart vote attempted to pass, but an admin was online. {votesYes}/{votesNo}");
|
|
}
|
|
else // If the cvar is disabled or there's no admins on, proceed as normal
|
|
{
|
|
_adminLogger.Add(LogType.Vote, LogImpact.Medium, $"Restart vote succeeded: {votesYes}/{votesNo}");
|
|
_chatManager.DispatchServerAnnouncement(Loc.GetString("ui-vote-restart-succeeded"));
|
|
var roundEnd = _entityManager.EntitySysManager.GetEntitySystem<RoundEndSystem>();
|
|
roundEnd.EndRound();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
_adminLogger.Add(LogType.Vote, LogImpact.Medium, $"Restart vote failed: {votesYes}/{votesNo}");
|
|
_chatManager.DispatchServerAnnouncement(
|
|
Loc.GetString("ui-vote-restart-failed", ("ratio", ratioRequired)));
|
|
}
|
|
};
|
|
|
|
if (initiator != null)
|
|
{
|
|
// Cast yes vote if created the vote yourself.
|
|
vote.CastVote(initiator, 0);
|
|
}
|
|
|
|
foreach (var player in _playerManager.Sessions)
|
|
{
|
|
if (player != initiator)
|
|
{
|
|
// Everybody else defaults to an abstain vote to say they don't mind.
|
|
vote.CastVote(player, 2);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void NotifyNotEnoughGhostPlayers(int ghostPercentageRequirement, int roundedGhostPercentage)
|
|
{
|
|
// Logic to notify that there are not enough ghost players to start a vote
|
|
_adminLogger.Add(LogType.Vote, LogImpact.Medium, $"Restart vote failed: Current Ghost player percentage:{roundedGhostPercentage.ToString()}% does not meet {ghostPercentageRequirement.ToString()}%");
|
|
_chatManager.DispatchServerAnnouncement(
|
|
Loc.GetString("ui-vote-restart-fail-not-enough-ghost-players", ("ghostPlayerRequirement", ghostPercentageRequirement)));
|
|
}
|
|
|
|
private void CreatePresetVote(ICommonSession? initiator)
|
|
{
|
|
var presets = GetGamePresets();
|
|
|
|
// Sunrise-Start
|
|
if (presets.Count == 1)
|
|
{
|
|
var singlePreset = presets.First();
|
|
_chatManager.DispatchServerAnnouncement(
|
|
Loc.GetString("ui-vote-gamemode-auto-set", ("preset", Loc.GetString(singlePreset.Value))));
|
|
var ticker = _entityManager.EntitySysManager.GetEntitySystem<GameTicker>();
|
|
ticker.SetGamePreset(singlePreset.Key);
|
|
return;
|
|
}
|
|
|
|
if (presets.Count < 1)
|
|
return;
|
|
// Sunrise-End
|
|
|
|
var alone = _playerManager.PlayerCount == 1 && initiator != null;
|
|
var options = new VoteOptions
|
|
{
|
|
Title = Loc.GetString("ui-vote-gamemode-title"),
|
|
Duration = alone
|
|
? TimeSpan.FromSeconds(_cfg.GetCVar(CCVars.VoteTimerAlone))
|
|
: TimeSpan.FromSeconds(_cfg.GetCVar(CCVars.VoteTimerPreset)),
|
|
DisplayVotes = _cfg.GetCVar(SunriseCCVars.ShowPresetVotes), // Sunrise-Edit
|
|
};
|
|
|
|
if (alone)
|
|
options.InitiatorTimeout = TimeSpan.FromSeconds(10);
|
|
|
|
foreach (var (k, v) in presets)
|
|
{
|
|
options.Options.Add((Loc.GetString(v), k));
|
|
}
|
|
|
|
WirePresetVoteInitiator(options, initiator);
|
|
|
|
var vote = CreateVote(options);
|
|
|
|
vote.OnFinished += (_, args) =>
|
|
{
|
|
string picked;
|
|
if (args.Winner == null)
|
|
{
|
|
picked = (string) _random.Pick(args.Winners);
|
|
_chatManager.DispatchServerAnnouncement(
|
|
Loc.GetString("ui-vote-gamemode-tie")); // Sunrise-Edit
|
|
}
|
|
else
|
|
{
|
|
picked = (string) args.Winner;
|
|
_chatManager.DispatchServerAnnouncement(
|
|
Loc.GetString("ui-vote-gamemode-win")); // Sunrise-Edit
|
|
}
|
|
_adminLogger.Add(LogType.Vote, LogImpact.Medium, $"Preset vote finished: {picked}");
|
|
var ticker = _entityManager.EntitySysManager.GetEntitySystem<GameTicker>();
|
|
ticker.SetGamePreset(picked);
|
|
};
|
|
}
|
|
|
|
private void CreateMapVote(ICommonSession? initiator)
|
|
{
|
|
// Sunrise-Start
|
|
var maps = new Dictionary<string, GameMapPrototype>();
|
|
var excludedMaps = _gameMapManager.CurrentlyExcludedMaps();
|
|
|
|
var eligibleMaps = _gameMapManager.CurrentlyEligibleMaps()
|
|
.Where(map => !excludedMaps.Contains(map.ID))
|
|
.ToList();
|
|
|
|
if (eligibleMaps.Count == 0)
|
|
{
|
|
_gameMapManager.ClearExcludedMaps();
|
|
eligibleMaps = _gameMapManager.CurrentlyEligibleMaps().ToList();
|
|
}
|
|
|
|
var selectedMaps = eligibleMaps.OrderBy(_ => _random.Next()).ToList();
|
|
maps.Clear();
|
|
maps.Add(Loc.GetString("ui-vote-secret-map"), _random.Pick(selectedMaps));
|
|
foreach (var map in selectedMaps)
|
|
{
|
|
maps.Add(map.MapName, map);
|
|
}
|
|
// Sunrise-End
|
|
|
|
var alone = _playerManager.PlayerCount == 1 && initiator != null;
|
|
var options = new VoteOptions
|
|
{
|
|
Title = Loc.GetString("ui-vote-map-title"),
|
|
Duration = alone
|
|
? TimeSpan.FromSeconds(_cfg.GetCVar(CCVars.VoteTimerAlone))
|
|
: TimeSpan.FromSeconds(_cfg.GetCVar(CCVars.VoteTimerMap)),
|
|
DisplayVotes = _cfg.GetCVar(SunriseCCVars.ShowMapVotes), // Sunrise-Edit
|
|
DisplayVotesAdmins = _cfg.GetCVar(SunriseCCVars.ShowMapVotes), // Sunrise-Edit
|
|
};
|
|
|
|
if (alone)
|
|
options.InitiatorTimeout = TimeSpan.FromSeconds(10);
|
|
|
|
foreach (var (k, v) in maps)
|
|
{
|
|
options.Options.Add((k, v));
|
|
}
|
|
|
|
WirePresetVoteInitiator(options, initiator);
|
|
|
|
var vote = CreateVote(options);
|
|
|
|
vote.OnFinished += (_, args) =>
|
|
{
|
|
GameMapPrototype picked;
|
|
if (args.Winner == null)
|
|
{
|
|
picked = (GameMapPrototype) _random.Pick(args.Winners);
|
|
_chatManager.DispatchServerAnnouncement(
|
|
Loc.GetString("ui-vote-map-tie"));
|
|
}
|
|
else
|
|
{
|
|
picked = (GameMapPrototype) args.Winner;
|
|
}
|
|
_chatManager.DispatchServerAnnouncement(Loc.GetString("ui-vote-map-win")); // Sunrise-Edit
|
|
|
|
_adminLogger.Add(LogType.Vote, LogImpact.Medium, $"Map vote finished: {picked.MapName}");
|
|
var ticker = _entityManager.EntitySysManager.GetEntitySystem<GameTicker>();
|
|
if (ticker.RunLevel == GameRunLevel.PreRoundLobby)
|
|
{
|
|
if (_gameMapManager.TrySelectMapIfEligible(picked.ID))
|
|
{
|
|
ticker.UpdateInfoText();
|
|
}
|
|
ticker.UpdateInfoText();
|
|
}
|
|
else
|
|
{
|
|
if (ticker.RoundPreloadTime <= TimeSpan.Zero)
|
|
{
|
|
_chatManager.DispatchServerAnnouncement(Loc.GetString("ui-vote-map-notlobby"));
|
|
}
|
|
else
|
|
{
|
|
var timeString = $"{ticker.RoundPreloadTime.Minutes:0}:{ticker.RoundPreloadTime.Seconds:00}";
|
|
_chatManager.DispatchServerAnnouncement(Loc.GetString("ui-vote-map-notlobby-time", ("time", timeString)));
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
private async void CreateVotekickVote(ICommonSession? initiator, string[]? args)
|
|
{
|
|
if (args == null || args.Length <= 1)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (_roleSystem == null)
|
|
_roleSystem = _entityManager.SystemOrNull<RoleSystem>();
|
|
if (_votingSystem == null)
|
|
_votingSystem = _entityManager.SystemOrNull<VotingSystem>();
|
|
|
|
// Check that the initiator is actually allowed to do a votekick.
|
|
if (_votingSystem != null && !await _votingSystem.CheckVotekickInitEligibility(initiator))
|
|
{
|
|
_logManager.GetSawmill("admin.votekick").Warning($"User {initiator} attempted a votekick, despite not being eligible to!");
|
|
_adminLogger.Add(LogType.Vote, LogImpact.Extreme, $"Votekick attempted by {initiator}, but they are not eligible to votekick!");
|
|
DirtyCanCallVoteAll();
|
|
return;
|
|
}
|
|
|
|
|
|
|
|
var voterEligibility = _cfg.GetCVar(CCVars.VotekickVoterGhostRequirement) ? VoterEligibility.GhostMinimumPlaytime : VoterEligibility.MinimumPlaytime;
|
|
if (_cfg.GetCVar(CCVars.VotekickIgnoreGhostReqInLobby) && _gameTicker!.RunLevel == GameRunLevel.PreRoundLobby)
|
|
voterEligibility = VoterEligibility.MinimumPlaytime;
|
|
|
|
var eligibleVoterNumberRequirement = _cfg.GetCVar(CCVars.VotekickEligibleNumberRequirement);
|
|
var eligibleVoterNumber = CalculateEligibleVoterNumber(voterEligibility);
|
|
|
|
string target = args[0];
|
|
string reason = args[1];
|
|
string note = args[2];
|
|
|
|
// Start by getting all relevant target data
|
|
var located = await _locator.LookupIdByNameOrIdAsync(target);
|
|
if (located == null)
|
|
{
|
|
_logManager.GetSawmill("admin.votekick")
|
|
.Warning($"Votekick attempted for player {target} but they couldn't be found!");
|
|
_adminLogger.Add(LogType.Vote, LogImpact.Extreme, $"Votekick attempted by {initiator} for player string {target}, but they could not be found!");
|
|
DirtyCanCallVoteAll();
|
|
return;
|
|
}
|
|
var targetUid = located.UserId;
|
|
var targetHWid = located.LastHWId;
|
|
(IPAddress, int)? targetIP = null;
|
|
|
|
if (located.LastAddress is not null)
|
|
{
|
|
targetIP = located.LastAddress.AddressFamily is AddressFamily.InterNetwork
|
|
? (located.LastAddress, 32) // People with ipv4 addresses get a /32 address so we ban that
|
|
: (located.LastAddress, 64); // This can only be an ipv6 address. People with ipv6 address should get /64 addresses so we ban that.
|
|
}
|
|
|
|
if (!_playerManager.TryGetSessionById(located.UserId, out ICommonSession? targetSession))
|
|
{
|
|
_logManager.GetSawmill("admin.votekick")
|
|
.Warning($"Votekick attempted for player {target} but their session couldn't be found!");
|
|
_adminLogger.Add(LogType.Vote, LogImpact.Extreme, $"Votekick attempted by {initiator} for player string {target}, but they could not be found!");
|
|
DirtyCanCallVoteAll();
|
|
return;
|
|
}
|
|
|
|
string targetEntityName = located.Username; // Target's player-facing name when voting; uses the player's username as fallback if no entity name is found
|
|
if (targetSession.AttachedEntity is { Valid: true } attached && _votingSystem != null)
|
|
targetEntityName = _votingSystem.GetPlayerVoteListName(attached);
|
|
|
|
var isAntagSafe = false;
|
|
var targetMind = targetSession.GetMind();
|
|
var playtime = _playtimeManager.GetPlayTimes(targetSession);
|
|
|
|
// Check whether the target is an antag, and if they are, give them protection against the Raider votekick if they have the requisite hours.
|
|
if (targetMind != null &&
|
|
_roleSystem != null &&
|
|
_roleSystem.MindIsAntagonist(targetMind) &&
|
|
playtime.TryGetValue(PlayTimeTrackingShared.TrackerOverall, out TimeSpan overallTime) &&
|
|
overallTime >= TimeSpan.FromHours(_cfg.GetCVar(CCVars.VotekickAntagRaiderProtection)))
|
|
{
|
|
isAntagSafe = true;
|
|
}
|
|
|
|
|
|
// Don't let a user votekick themselves
|
|
if (initiator == targetSession)
|
|
{
|
|
_adminLogger.Add(LogType.Vote, LogImpact.Extreme, $"Votekick attempted by {initiator} for themselves? Votekick cancelled.");
|
|
DirtyCanCallVoteAll();
|
|
return;
|
|
}
|
|
|
|
// Cancels the vote if there's not enough voters; only the person initiating the vote gets a return message.
|
|
if (eligibleVoterNumber < eligibleVoterNumberRequirement)
|
|
{
|
|
_adminLogger.Add(LogType.Vote, LogImpact.Extreme, $"Votekick attempted by {initiator} for player {targetSession}, but there were not enough ghost roles! {eligibleVoterNumberRequirement} required, {eligibleVoterNumber} found.");
|
|
if (initiator != null)
|
|
{
|
|
var message = Loc.GetString("ui-vote-votekick-not-enough-eligible", ("voters", eligibleVoterNumber.ToString()), ("requirement", eligibleVoterNumberRequirement.ToString()));
|
|
var wrappedMessage = Loc.GetString("chat-manager-server-wrap-message", ("message", message));
|
|
_chatManager.ChatMessageToOne(ChatChannel.Server, message, wrappedMessage, default, false, initiator.Channel);
|
|
}
|
|
DirtyCanCallVoteAll();
|
|
return;
|
|
}
|
|
|
|
// 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.
|
|
if (_votingSystem != null && !_votingSystem.CheckVotekickTargetEligibility(targetSession))
|
|
{
|
|
_adminLogger.Add(LogType.Vote, LogImpact.Extreme, $"Votekick attempted by {initiator} for player {targetSession}, but they are not eligible to be votekicked!");
|
|
DirtyCanCallVoteAll();
|
|
return;
|
|
}
|
|
|
|
// Create the vote object
|
|
|
|
string voteTitle = "";
|
|
NetEntity? targetNetEntity = _entityManager.GetNetEntity(targetSession.AttachedEntity);
|
|
var initiatorName = initiator != null ? initiator.Name : Loc.GetString("ui-vote-votekick-unknown-initiator");
|
|
var reasonLocalised = Loc.GetString($"ui-vote-votekick-type-{reason.ToLower()}");
|
|
|
|
voteTitle = Loc.GetString("ui-vote-votekick-title", ("initiator", initiatorName), ("targetEntity", targetEntityName), ("reason", reasonLocalised + ". " + note));
|
|
|
|
var options = new VoteOptions
|
|
{
|
|
Title = voteTitle,
|
|
Options =
|
|
{
|
|
(Loc.GetString("ui-vote-votekick-yes"), "yes"),
|
|
(Loc.GetString("ui-vote-votekick-no"), "no"),
|
|
(Loc.GetString("ui-vote-votekick-abstain"), "abstain")
|
|
},
|
|
Duration = TimeSpan.FromSeconds(_cfg.GetCVar(CCVars.VotekickTimer)),
|
|
InitiatorTimeout = TimeSpan.FromSeconds(_cfg.GetCVar(CCVars.VotekickTimeout)),
|
|
VoterEligibility = voterEligibility,
|
|
DisplayVotes = false,
|
|
TargetEntity = targetNetEntity
|
|
};
|
|
|
|
WirePresetVoteInitiator(options, initiator);
|
|
|
|
var vote = CreateVote(options);
|
|
_adminLogger.Add(LogType.Vote, LogImpact.Extreme, $"Votekick for {located.Username} ({targetEntityName}) due to {reason} started, initiated by {initiator}.");
|
|
|
|
// Create Discord webhook
|
|
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)));
|
|
|
|
// Time out the vote now that we know it will happen
|
|
TimeoutStandardVote(StandardVoteType.Votekick, TimeSpan.FromSeconds(_cfg.GetCVar(CCVars.VotekickTimeout)));
|
|
|
|
vote.OnFinished += (_, eventArgs) =>
|
|
{
|
|
|
|
var votesYes = vote.VotesPerOption["yes"];
|
|
var votesNo = vote.VotesPerOption["no"];
|
|
var total = votesYes + votesNo;
|
|
|
|
// Get the voters, for logging purposes.
|
|
List<ICommonSession> yesVoters = new();
|
|
List<ICommonSession> noVoters = new();
|
|
foreach (var (voter, castVote) in vote.CastVotes)
|
|
{
|
|
if (castVote == 0)
|
|
{
|
|
yesVoters.Add(voter);
|
|
}
|
|
if (castVote == 1)
|
|
{
|
|
noVoters.Add(voter);
|
|
}
|
|
}
|
|
var yesVotersString = string.Join(", ", yesVoters);
|
|
var noVotersString = string.Join(", ", noVoters);
|
|
|
|
var ratioRequired = _cfg.GetCVar(CCVars.VotekickRequiredRatio);
|
|
if (total > 0 && votesYes / (float)total >= ratioRequired)
|
|
{
|
|
// Some conditions that cancel the vote want to let the vote run its course first and then cancel it
|
|
// so we check for that here
|
|
|
|
// Check if an admin is online, and ignore the vote if the cvar is enabled
|
|
if (_cfg.GetCVar(CCVars.VotekickNotAllowedWhenAdminOnline) && _adminMgr.ActiveAdmins.Count() != 0)
|
|
{
|
|
_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}");
|
|
AnnounceCancelledVotekickForVoters(targetEntityName);
|
|
_voteWebhooks.UpdateCancelledWebhookIfConfigured(webhookState, Loc.GetString("votekick-webhook-cancelled-admin-online"));
|
|
return;
|
|
}
|
|
// Check if the target is an antag and the vote reason is raiding (this is to prevent false positives)
|
|
else if (isAntagSafe && reason == VotekickReasonType.Raiding.ToString())
|
|
{
|
|
_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.");
|
|
AnnounceCancelledVotekickForVoters(targetEntityName);
|
|
_voteWebhooks.UpdateCancelledWebhookIfConfigured(webhookState, Loc.GetString("votekick-webhook-cancelled-antag-target"));
|
|
return;
|
|
}
|
|
// Check if the target is an admin/de-admined admin
|
|
else if (targetSession.AttachedEntity != null && _adminMgr.IsAdmin(targetSession.AttachedEntity.Value, true))
|
|
{
|
|
_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.");
|
|
AnnounceCancelledVotekickForVoters(targetEntityName);
|
|
_voteWebhooks.UpdateCancelledWebhookIfConfigured(webhookState, Loc.GetString("votekick-webhook-cancelled-admin-target"));
|
|
return;
|
|
}
|
|
else
|
|
{
|
|
_adminLogger.Add(LogType.Vote, LogImpact.Extreme, $"Votekick for {located.Username} succeeded: Yes: {votesYes} / No: {votesNo}. Yes: {yesVotersString} / No: {noVotersString}");
|
|
_chatManager.DispatchServerAnnouncement(Loc.GetString("ui-vote-votekick-success", ("target", targetEntityName), ("reason", reasonLocalised + ". " + note)));
|
|
|
|
if (!Enum.TryParse(_cfg.GetCVar(CCVars.VotekickBanDefaultSeverity), out NoteSeverity severity))
|
|
{
|
|
_logManager.GetSawmill("admin.votekick")
|
|
.Warning("Votekick ban severity could not be parsed from config! Defaulting to high.");
|
|
severity = NoteSeverity.High;
|
|
}
|
|
|
|
// Discord webhook, success
|
|
_voteWebhooks.UpdateWebhookIfConfigured(webhookState, eventArgs);
|
|
|
|
uint minutes = (uint)_cfg.GetCVar(CCVars.VotekickBanDuration);
|
|
|
|
_bans.CreateServerBan(targetUid, target, null, targetIP, targetHWid, minutes, severity, reasonLocalised + ". " + note);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
|
|
// Discord webhook, failure
|
|
_voteWebhooks.UpdateWebhookIfConfigured(webhookState, eventArgs);
|
|
|
|
_adminLogger.Add(LogType.Vote, LogImpact.Extreme, $"Votekick failed: Yes: {votesYes} / No: {votesNo}. Yes: {yesVotersString} / No: {noVotersString}");
|
|
_chatManager.DispatchServerAnnouncement(Loc.GetString("ui-vote-votekick-failure", ("target", targetEntityName), ("reason", reason)));
|
|
}
|
|
};
|
|
|
|
if (initiator != null)
|
|
{
|
|
// Cast yes vote if created the vote yourself.
|
|
vote.CastVote(initiator, 0);
|
|
}
|
|
}
|
|
|
|
private void AnnounceCancelledVotekickForVoters(string target)
|
|
{
|
|
foreach (var player in _playerManager.Sessions)
|
|
{
|
|
if (CheckVoterEligibility(player, VoterEligibility.GhostMinimumPlaytime))
|
|
{
|
|
var message = Loc.GetString("ui-vote-votekick-server-cancelled", ("target", target));
|
|
var wrappedMessage = Loc.GetString("chat-manager-server-wrap-message", ("message", message));
|
|
_chatManager.ChatMessageToOne(ChatChannel.Server, message, wrappedMessage, default, false, player.Channel);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void TimeoutStandardVote(StandardVoteType type, TimeSpan? timeoutOverride = null)
|
|
{
|
|
var timeout = timeoutOverride ?? TimeSpan.FromSeconds(_cfg.GetCVar(CCVars.VoteSameTypeTimeout));
|
|
_standardVoteTimeout[type] = _timing.RealTime + timeout;
|
|
DirtyCanCallVoteAll();
|
|
}
|
|
|
|
// Sunrise-Start
|
|
private Dictionary<string, string> GetGamePresets()
|
|
{
|
|
var presets = new Dictionary<string, string>();
|
|
|
|
var ticker = _entityManager.EntitySysManager.GetEntitySystem<GameTicker>();
|
|
var excluded = ticker.ExcludedPresets.ToHashSet();
|
|
var validPresets = new List<GamePresetPrototype>();
|
|
var presetPoolId = _cfg.GetCVar(SunriseCCVars.GamePresetPool);
|
|
|
|
if (_prototypeManager.TryIndex<GamePresetPoolPrototype>(presetPoolId, out var presetPoolProto))
|
|
{
|
|
foreach (var (presetId, limits) in presetPoolProto.Presets)
|
|
{
|
|
if (excluded.Contains(presetId))
|
|
continue;
|
|
|
|
if (!_prototypeManager.TryIndex<GamePresetPrototype>(presetId, out var preset))
|
|
continue;
|
|
|
|
if (!preset.ShowInVote)
|
|
continue;
|
|
|
|
var minPlayers = limits.Length > 0 ? limits[0] : int.MinValue;
|
|
var maxPlayers = limits.Length > 1 ? limits[1] : int.MaxValue;
|
|
|
|
if (_playerManager.PlayerCount < minPlayers || _playerManager.PlayerCount > maxPlayers)
|
|
continue;
|
|
|
|
validPresets.Add(preset);
|
|
}
|
|
|
|
if (validPresets.Count == 0 && excluded.Count > 0)
|
|
{
|
|
ticker.ClearExcludedPresets();
|
|
foreach (var (presetId, limits) in presetPoolProto.Presets)
|
|
{
|
|
if (!_prototypeManager.TryIndex<GamePresetPrototype>(presetId, out var preset))
|
|
continue;
|
|
|
|
if (!preset.ShowInVote)
|
|
continue;
|
|
|
|
var minPlayers = limits.Length > 0 ? limits[0] : int.MinValue;
|
|
var maxPlayers = limits.Length > 1 ? limits[1] : int.MaxValue;
|
|
|
|
if (_playerManager.PlayerCount < minPlayers || _playerManager.PlayerCount > maxPlayers)
|
|
continue;
|
|
|
|
validPresets.Add(preset);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (validPresets.Count == 0)
|
|
{
|
|
Logger.Warning("No suitable game modes for the current player count.");
|
|
return presets;
|
|
}
|
|
|
|
foreach (var preset in validPresets)
|
|
{
|
|
presets[preset.ID] = preset.ModeTitle;
|
|
}
|
|
|
|
return presets;
|
|
}
|
|
// Sunrise-End
|
|
}
|
|
}
|