diff --git a/Content.IntegrationTests/Tests/GameRules/FailAndStartPresetTest.cs b/Content.IntegrationTests/Tests/GameRules/FailAndStartPresetTest.cs
index 3109df890a7d..abfd64aa4f09 100644
--- a/Content.IntegrationTests/Tests/GameRules/FailAndStartPresetTest.cs
+++ b/Content.IntegrationTests/Tests/GameRules/FailAndStartPresetTest.cs
@@ -140,9 +140,10 @@ private void OnRoundStartAttempt(RoundStartAttemptEvent args)
while (query.MoveNext(out _, out _, out var gameRule))
{
var minPlayers = gameRule.MinPlayers;
- if (!gameRule.CancelPresetOnTooFewPlayers)
+ var maxPlayers = gameRule.MaxPlayers;
+ if (!gameRule.CancelPresetOnTooFewPlayers && !gameRule.CancelPresetOnTooManyPlayers)
continue;
- if (args.Players.Length >= minPlayers)
+ if (args.Players.Length >= minPlayers && args.Players.Length <= maxPlayers)
continue;
args.Cancel();
diff --git a/Content.Server/GameTicking/Rules/GameRuleSystem.cs b/Content.Server/GameTicking/Rules/GameRuleSystem.cs
index cb5b11754952..452ffb0e2129 100644
--- a/Content.Server/GameTicking/Rules/GameRuleSystem.cs
+++ b/Content.Server/GameTicking/Rules/GameRuleSystem.cs
@@ -38,10 +38,11 @@ private void OnStartAttempt(RoundStartAttemptEvent args)
while (query.MoveNext(out var uid, out _, out var gameRule))
{
var minPlayers = gameRule.MinPlayers;
- if (args.Players.Length >= minPlayers)
+ var maxPlayers = gameRule.MaxPlayers;
+ if (args.Players.Length >= minPlayers && args.Players.Length <= maxPlayers)
continue;
- if (gameRule.CancelPresetOnTooFewPlayers)
+ if (args.Players.Length < minPlayers && gameRule.CancelPresetOnTooFewPlayers)
{
ChatManager.SendAdminAnnouncement(Loc.GetString("preset-not-enough-ready-players",
("readyPlayersCount", args.Players.Length),
@@ -49,6 +50,14 @@ private void OnStartAttempt(RoundStartAttemptEvent args)
("presetName", ToPrettyString(uid))));
args.Cancel();
}
+ else if (args.Players.Length > maxPlayers && gameRule.CancelPresetOnTooManyPlayers)
+ {
+ ChatManager.SendAdminAnnouncement(Loc.GetString("preset-too-many-ready-players",
+ ("readyPlayersCount", args.Players.Length),
+ ("maximumPlayers", maxPlayers),
+ ("presetName", ToPrettyString(uid))));
+ args.Cancel();
+ }
else
{
ForceEndSelf(uid, gameRule);
diff --git a/Content.Server/GameTicking/Rules/SecretRuleSystem.cs b/Content.Server/GameTicking/Rules/SecretRuleSystem.cs
index 5f42888e5e2b..5a88f0e83b52 100644
--- a/Content.Server/GameTicking/Rules/SecretRuleSystem.cs
+++ b/Content.Server/GameTicking/Rules/SecretRuleSystem.cs
@@ -176,6 +176,9 @@ private bool CanPick([NotNullWhen(true)] GamePresetPrototype? selected, int play
if (ruleComp.MinPlayers > players && ruleComp.CancelPresetOnTooFewPlayers)
return false;
+
+ if (ruleComp.MaxPlayers < players && ruleComp.CancelPresetOnTooManyPlayers)
+ return false;
}
if (_nextRoundAllowed.ContainsKey(selected.ID) && _nextRoundAllowed[selected.ID] > _ticker.RoundId)
diff --git a/Content.Shared/GameTicking/Components/GameRuleComponent.cs b/Content.Shared/GameTicking/Components/GameRuleComponent.cs
index 87a5822d4747..9c7a67f0337c 100644
--- a/Content.Shared/GameTicking/Components/GameRuleComponent.cs
+++ b/Content.Shared/GameTicking/Components/GameRuleComponent.cs
@@ -23,6 +23,12 @@ public sealed partial class GameRuleComponent : Component
[DataField]
public int MinPlayers;
+ ///
+ /// The maximum amount of players supported by this game rule.
+ ///
+ [DataField]
+ public int MaxPlayers = 9999; // cheap hack
+
///
/// If true, this rule not having enough players will cancel the preset selection.
/// If false, it will simply not run silently.
@@ -30,6 +36,13 @@ public sealed partial class GameRuleComponent : Component
[DataField]
public bool CancelPresetOnTooFewPlayers = true;
+ ///
+ /// If true, this rule having too many players will cancel the preset selection.
+ /// If false, it will simply not run silently.
+ ///
+ [DataField]
+ public bool CancelPresetOnTooManyPlayers = false;
+
///
/// A delay for when the rule the is started and when the starting logic actually runs.
///
diff --git a/Resources/Locale/en-US/_Impstation/game-ticking/game-presets/preset-extended-lowpop.ftl b/Resources/Locale/en-US/_Impstation/game-ticking/game-presets/preset-extended-lowpop.ftl
new file mode 100644
index 000000000000..26b47d0b0252
--- /dev/null
+++ b/Resources/Locale/en-US/_Impstation/game-ticking/game-presets/preset-extended-lowpop.ftl
@@ -0,0 +1,2 @@
+extended-lowpop-title = Extended (Lowpop)
+extended-lowpop-description = A calm experience.
diff --git a/Resources/Locale/en-US/_Impstation/game-ticking/game-presets/preset-survival-lowpop.ftl b/Resources/Locale/en-US/_Impstation/game-ticking/game-presets/preset-survival-lowpop.ftl
new file mode 100644
index 000000000000..3ca9e611c8b2
--- /dev/null
+++ b/Resources/Locale/en-US/_Impstation/game-ticking/game-presets/preset-survival-lowpop.ftl
@@ -0,0 +1,2 @@
+survival-lowpop-title = Survival (Lowpop)
+survival-lowpop-description = No internal threats, but how long can the station survive increasingly chaotic and frequent events?
diff --git a/Resources/Locale/en-US/_Impstation/game-ticking/game-presets/preset-thief-lowpop.ftl b/Resources/Locale/en-US/_Impstation/game-ticking/game-presets/preset-thief-lowpop.ftl
new file mode 100644
index 000000000000..77dea8e16c86
--- /dev/null
+++ b/Resources/Locale/en-US/_Impstation/game-ticking/game-presets/preset-thief-lowpop.ftl
@@ -0,0 +1,2 @@
+thief-lowpop-title = Thief (Lowpop)
+thief-lowpop-description = Watch your pockets.
diff --git a/Resources/Locale/en-US/_Impstation/game-ticking/game-presets/preset-traitor-lowpop.ftl b/Resources/Locale/en-US/_Impstation/game-ticking/game-presets/preset-traitor-lowpop.ftl
new file mode 100644
index 000000000000..f9931870540d
--- /dev/null
+++ b/Resources/Locale/en-US/_Impstation/game-ticking/game-presets/preset-traitor-lowpop.ftl
@@ -0,0 +1,2 @@
+traitor-lowpop-title = Traitor (Lowpop)
+traitor-lowpop-description = There are traitors among us...
diff --git a/Resources/Locale/en-US/_Impstation/game-ticking/game-ticker.ftl b/Resources/Locale/en-US/_Impstation/game-ticking/game-ticker.ftl
new file mode 100644
index 000000000000..711ff94ccda6
--- /dev/null
+++ b/Resources/Locale/en-US/_Impstation/game-ticking/game-ticker.ftl
@@ -0,0 +1 @@
+preset-too-many-ready-players = Can't start {$presetName}. Requires at most {$maximumPlayers} players but we have {$readyPlayersCount}.
\ No newline at end of file
diff --git a/Resources/Prototypes/GameRules/events.yml b/Resources/Prototypes/GameRules/events.yml
index 76f583b1365d..757abd2a34eb 100644
--- a/Resources/Prototypes/GameRules/events.yml
+++ b/Resources/Prototypes/GameRules/events.yml
@@ -41,6 +41,8 @@
id: SleeperlessAntagEventsTable
table: !type:AllSelector # we need to pass a list of rules, since rules have further restrictions to consider via StationEventComp
children:
+ - !type:NestedSelector # DeltaV
+ tableId: BasicAntagEventsTableDeltaV
- id: ClosetSkeleton
- id: DragonSpawn
- id: KingRatMigration
diff --git a/Resources/Prototypes/_Goobstation/game_presets.yml b/Resources/Prototypes/_Goobstation/game_presets.yml
index 20b6a0d16b26..8f9938419245 100644
--- a/Resources/Prototypes/_Goobstation/game_presets.yml
+++ b/Resources/Prototypes/_Goobstation/game_presets.yml
@@ -15,6 +15,7 @@
- MeteorSwarmScheduler
- SpaceTrafficControlEventScheduler
- BasicRoundstartVariation
+ - MidpopLimiter
- type: gamePreset
id: Secretling
diff --git a/Resources/Prototypes/_Impstation/GameRules/events.yml b/Resources/Prototypes/_Impstation/GameRules/events.yml
index f3464cfe7d85..1c5a01ee0e9b 100644
--- a/Resources/Prototypes/_Impstation/GameRules/events.yml
+++ b/Resources/Prototypes/_Impstation/GameRules/events.yml
@@ -1,3 +1,17 @@
+- type: entityTable
+ id: LowpopAntagEventsTable
+ table: !type:AllSelector # we need to pass a list of rules, since rules have further restrictions to consider via StationEventComp
+ children:
+ - !type:NestedSelector # DeltaV
+ tableId: BasicAntagEventsTableDeltaV
+ - id: ClosetSkeleton
+ - id: DragonSpawn
+ - id: KingRatMigration
+ - id: NinjaSpawn
+ - id: RevenantSpawn
+ - id: GoblinStowawaysEvent # imp
+ - id: GoblinCastawaysEvent # imp
+
- type: entity
id: ChangelingAwakening
parent: BaseGameRule
diff --git a/Resources/Prototypes/_Impstation/GameRules/roundstart.yml b/Resources/Prototypes/_Impstation/GameRules/roundstart.yml
index efe7876974d3..2e09aa450a96 100644
--- a/Resources/Prototypes/_Impstation/GameRules/roundstart.yml
+++ b/Resources/Prototypes/_Impstation/GameRules/roundstart.yml
@@ -98,3 +98,53 @@
forceAllPossible: true
mindRoles:
- MindRoleTraitor
+
+# population limiters
+
+- type: entity
+ parent: BaseGameRule
+ id: MidpopLimiter
+ components:
+ - type: GameRule
+ minPlayers: 20
+
+- type: entity
+ parent: BaseGameRule
+ id: LowpopLimiter
+ components:
+ - type: GameRule
+ maxPlayers: 19
+ cancelPresetOnTooManyPlayers: true
+
+# event schedulers
+
+- type: entityTable
+ id: LowpopGameRulesTable
+ table: !type:AllSelector # we need to pass a list of rules, since rules have further restrictions to consider via StationEventComp
+ children:
+ - !type:NestedSelector
+ tableId: BasicCalmEventsTable
+ - !type:NestedSelector
+ tableId: LowpopAntagEventsTable
+ - !type:NestedSelector
+ tableId: CargoGiftsTable
+ - !type:NestedSelector
+ tableId: CalmPestEventsTable
+ - !type:NestedSelector
+ tableId: SpicyPestEventsTable
+
+- type: entity
+ id: LowpopStationEventScheduler
+ parent: BaseGameRule
+ components:
+ - type: BasicStationEventScheduler
+ scheduledGameRules: !type:NestedSelector
+ tableId: LowpopGameRulesTable
+
+- type: entity
+ id: LowpopRampingStationEventScheduler
+ parent: BaseGameRule
+ components:
+ - type: RampingStationEventScheduler
+ scheduledGameRules: !type:NestedSelector
+ tableId: LowpopGameRulesTable
diff --git a/Resources/Prototypes/_Impstation/game_presets.yml b/Resources/Prototypes/_Impstation/game_presets.yml
index 19624453d3ed..f4ed58ced98f 100644
--- a/Resources/Prototypes/_Impstation/game_presets.yml
+++ b/Resources/Prototypes/_Impstation/game_presets.yml
@@ -19,3 +19,65 @@
- SpaceTrafficControlEventScheduler
- SpaceTrafficControlFriendlyEventScheduler
- BasicRoundstartVariation
+
+- type: gamePreset
+ id: TraitorLowpop
+ alias:
+ - LowpopTraitor
+ name: traitor-lowpop-title
+ description: traitor-lowpop-description
+ showInVote: false
+ rules:
+ - Traitor
+ - SubGamemodesRule
+ - LowpopStationEventScheduler
+ - MeteorSwarmScheduler
+ - SpaceTrafficControlEventScheduler
+ - BasicRoundstartVariation
+ - LowpopLimiter
+
+- type: gamePreset
+ id: ThiefLowpop
+ alias:
+ - LowpopThief
+ name: thief-lowpop-title
+ description: thief-lowpop-description
+ showInVote: false
+ rules:
+ - Thief
+ - LowpopStationEventScheduler
+ - MeteorSwarmScheduler
+ - SpaceTrafficControlEventScheduler
+ - BasicRoundstartVariation
+ - LowpopLimiter
+
+- type: gamePreset
+ id: SurvivalLowpop
+ alias:
+ - LowpopSurvival
+ name: survival-lowpop-title
+ description: survival-lowpop-description
+ showInVote: false
+ cooldown: 2
+ rules:
+ - MeteorSwarmScheduler
+ - LowpopRampingStationEventScheduler
+ - SpaceTrafficControlEventScheduler
+ - SpaceTrafficControlFriendlyEventScheduler
+ - BasicRoundstartVariation
+ - LowpopLimiter
+
+- type: gamePreset
+ id: ExtendedLowpop
+ alias:
+ - LowpopExtended
+ name: extended-lowpop-title
+ description: extended-lowpop-description
+ showInVote: false
+ cooldown: 1 # zzz
+ rules:
+ - LowpopStationEventScheduler
+ - MeteorSwarmScheduler
+ - SpaceTrafficControlEventScheduler
+ - BasicRoundstartVariation
+ - LowpopLimiter
diff --git a/Resources/Prototypes/game_presets.yml b/Resources/Prototypes/game_presets.yml
index 65b82f63e81b..9d99a7cc5c06 100644
--- a/Resources/Prototypes/game_presets.yml
+++ b/Resources/Prototypes/game_presets.yml
@@ -12,6 +12,7 @@
- SpaceTrafficControlEventScheduler
- SpaceTrafficControlFriendlyEventScheduler
- BasicRoundstartVariation
+ - MidpopLimiter
- type: gamePreset
id: KesslerSyndrome
@@ -28,6 +29,7 @@
- RampingStationEventScheduler
- SpaceTrafficControlEventScheduler
- BasicRoundstartVariation
+ - MidpopLimiter
- type: gamePreset
id: AllAtOnce
@@ -159,6 +161,7 @@
- MeteorSwarmScheduler
- SpaceTrafficControlEventScheduler
- BasicRoundstartVariation
+ - MidpopLimiter
- type: gamePreset
id: SpyVsSpy
@@ -175,6 +178,7 @@
- MeteorSwarmScheduler
- SpaceTrafficControlEventScheduler
- BasicRoundstartVariation
+ - MidpopLimiter
- type: gamePreset
id: SpyVsSpy3TC
@@ -245,6 +249,7 @@
- MeteorSwarmScheduler
- SpaceTrafficControlEventScheduler
- BasicRoundstartVariation
+ - MidpopLimiter
- type: gamePreset
id: Revolutionary
@@ -262,6 +267,7 @@
- MeteorSwarmScheduler
- SpaceTrafficControlEventScheduler
- BasicRoundstartVariation
+ - MidpopLimiter
- type: gamePreset
id: Zombie
@@ -281,6 +287,7 @@
- MeteorSwarmScheduler
- SpaceTrafficControlEventScheduler
- BasicRoundstartVariation
+ - MidpopLimiter
- type: gamePreset
id: Zombieteors
@@ -298,3 +305,4 @@
- KesslerSyndromeScheduler
- SpaceTrafficControlEventScheduler
- BasicRoundstartVariation
+ - MidpopLimiter
diff --git a/Resources/Prototypes/secret_weights.yml b/Resources/Prototypes/secret_weights.yml
index 7fdb74292edb..09d70ef09f06 100644
--- a/Resources/Prototypes/secret_weights.yml
+++ b/Resources/Prototypes/secret_weights.yml
@@ -1,6 +1,7 @@
- type: weightedRandom
id: Secret
weights:
+ # regular gamemodes, gamemodes with 20 population requirement or more
Nukeops: 0.15
Traitor: 0.50
SpyVsSpy: 0.03
@@ -10,3 +11,9 @@
Survival: 0.10
KesslerSyndrome: 0.01
Revolutionary: 0.03
+
+ # lowpop gamemodes, gamemodes with 19 population requirement or below
+ TraitorLowpop: 0.50
+ ThiefLowpop: 0.25
+ SurvivalLowpop: 0.20
+ ExtendedLowpop: 0.05