diff --git a/Content.Server/GameTicking/Commands/SetGamePresetCommand.cs b/Content.Server/GameTicking/Commands/SetGamePresetCommand.cs
index 83736bd92b0e..78e2b452b7be 100644
--- a/Content.Server/GameTicking/Commands/SetGamePresetCommand.cs
+++ b/Content.Server/GameTicking/Commands/SetGamePresetCommand.cs
@@ -2,6 +2,7 @@
using Content.Server.Administration;
using Content.Server.GameTicking.Presets;
using Content.Shared.Administration;
+using Linguini.Shared.Util;
using Robust.Shared.Console;
using Robust.Shared.Prototypes;
@@ -19,9 +20,9 @@ public sealed class SetGamePresetCommand : IConsoleCommand
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
- if (args.Length != 1)
+ if (!args.Length.InRange(1, 2))
{
- shell.WriteError(Loc.GetString("shell-wrong-arguments-number-need-specific", ("properAmount", 1), ("currentAmount", args.Length)));
+ shell.WriteError(Loc.GetString("shell-need-between-arguments", ("lower", 1), ("upper", 2), ("currentAmount", args.Length)));
return;
}
@@ -33,8 +34,16 @@ public void Execute(IConsoleShell shell, string argStr, string[] args)
return;
}
- ticker.SetGamePreset(preset);
- shell.WriteLine(Loc.GetString("set-game-preset-preset-set", ("preset", preset.ID)));
+ var rounds = 1;
+
+ if (args.Length == 2 && !int.TryParse(args[1], out rounds))
+ {
+ shell.WriteError(Loc.GetString("set-game-preset-optional-argument-not-integer"));
+ return;
+ }
+
+ ticker.SetGamePreset(preset, false, rounds);
+ shell.WriteLine(Loc.GetString("set-game-preset-preset-set-finite", ("preset", preset.ID), ("rounds", rounds.ToString())));
}
public CompletionResult GetCompletion(IConsoleShell shell, string[] args)
diff --git a/Content.Server/GameTicking/GameTicker.GamePreset.cs b/Content.Server/GameTicking/GameTicker.GamePreset.cs
index 5a2b375dd68c..6c12a2ea27e9 100644
--- a/Content.Server/GameTicking/GameTicker.GamePreset.cs
+++ b/Content.Server/GameTicking/GameTicker.GamePreset.cs
@@ -16,305 +16,327 @@
using JetBrains.Annotations;
using Robust.Shared.Player;
-namespace Content.Server.GameTicking
+namespace Content.Server.GameTicking;
+
+public sealed partial class GameTicker
{
- public sealed partial class GameTicker
- {
- [Dependency] private readonly MobThresholdSystem _mobThresholdSystem = default!;
+ [Dependency] private readonly MobThresholdSystem _mobThresholdSystem = default!;
- public const float PresetFailedCooldownIncrease = 30f;
+ public const float PresetFailedCooldownIncrease = 30f;
- ///
- /// The selected preset that will be used at the start of the next round.
- ///
- public GamePresetPrototype? Preset { get; private set; }
+ ///
+ /// The selected preset that will be used at the start of the next round.
+ ///
+ public GamePresetPrototype? Preset { get; private set; }
- ///
- /// The preset that's currently active.
- ///
- public GamePresetPrototype? CurrentPreset { get; private set; }
+ ///
+ /// The preset that's currently active.
+ ///
+ public GamePresetPrototype? CurrentPreset { get; private set; }
- private bool StartPreset(ICommonSession[] origReadyPlayers, bool force)
- {
- var startAttempt = new RoundStartAttemptEvent(origReadyPlayers, force);
- RaiseLocalEvent(startAttempt);
+ ///
+ /// Countdown to the preset being reset to the server default.
+ ///
+ public int? ResetCountdown;
- if (!startAttempt.Cancelled)
- return true;
+ private bool StartPreset(ICommonSession[] origReadyPlayers, bool force)
+ {
+ var startAttempt = new RoundStartAttemptEvent(origReadyPlayers, force);
+ RaiseLocalEvent(startAttempt);
- var presetTitle = CurrentPreset != null ? Loc.GetString(CurrentPreset.ModeTitle) : string.Empty;
+ if (!startAttempt.Cancelled)
+ return true;
- void FailedPresetRestart()
- {
- SendServerMessage(Loc.GetString("game-ticker-start-round-cannot-start-game-mode-restart",
- ("failedGameMode", presetTitle)));
- RestartRound();
- DelayStart(TimeSpan.FromSeconds(PresetFailedCooldownIncrease));
- }
+ var presetTitle = CurrentPreset != null ? Loc.GetString(CurrentPreset.ModeTitle) : string.Empty;
- if (_configurationManager.GetCVar(CCVars.GameLobbyFallbackEnabled))
+ void FailedPresetRestart()
+ {
+ SendServerMessage(Loc.GetString("game-ticker-start-round-cannot-start-game-mode-restart",
+ ("failedGameMode", presetTitle)));
+ RestartRound();
+ DelayStart(TimeSpan.FromSeconds(PresetFailedCooldownIncrease));
+ }
+
+ if (_configurationManager.GetCVar(CCVars.GameLobbyFallbackEnabled))
+ {
+ var fallbackPresets = _configurationManager.GetCVar(CCVars.GameLobbyFallbackPreset).Split(",");
+ var startFailed = true;
+
+ foreach (var preset in fallbackPresets)
{
- var fallbackPresets = _configurationManager.GetCVar(CCVars.GameLobbyFallbackPreset).Split(",");
- var startFailed = true;
+ ClearGameRules();
+ SetGamePreset(preset);
+ AddGamePresetRules();
+ StartGamePresetRules();
- foreach (var preset in fallbackPresets)
- {
- ClearGameRules();
- SetGamePreset(preset);
- AddGamePresetRules();
- StartGamePresetRules();
-
- startAttempt.Uncancel();
- RaiseLocalEvent(startAttempt);
-
- if (!startAttempt.Cancelled)
- {
- _chatManager.SendAdminAnnouncement(
- Loc.GetString("game-ticker-start-round-cannot-start-game-mode-fallback",
- ("failedGameMode", presetTitle),
- ("fallbackMode", Loc.GetString(preset))));
- RefreshLateJoinAllowed();
- startFailed = false;
- break;
- }
- }
+ startAttempt.Uncancel();
+ RaiseLocalEvent(startAttempt);
- if (startFailed)
+ if (!startAttempt.Cancelled)
{
- FailedPresetRestart();
- return false;
+ _chatManager.SendAdminAnnouncement(
+ Loc.GetString("game-ticker-start-round-cannot-start-game-mode-fallback",
+ ("failedGameMode", presetTitle),
+ ("fallbackMode", Loc.GetString(preset))));
+ RefreshLateJoinAllowed();
+ startFailed = false;
+ break;
}
}
- else
+ if (startFailed)
{
FailedPresetRestart();
return false;
}
-
- return true;
}
- private void InitializeGamePreset()
+ else
{
- SetGamePreset(LobbyEnabled ? _configurationManager.GetCVar(CCVars.GameLobbyDefaultPreset) : "sandbox");
+ FailedPresetRestart();
+ return false;
}
- public void SetGamePreset(GamePresetPrototype? preset, bool force = false)
- {
- // Do nothing if this game ticker is a dummy!
- if (DummyTicker)
- return;
+ return true;
+ }
- Preset = preset;
- ValidateMap();
- UpdateInfoText();
+ private void InitializeGamePreset()
+ {
+ SetGamePreset(LobbyEnabled ? _configurationManager.GetCVar(CCVars.GameLobbyDefaultPreset) : "sandbox");
+ }
- if (force)
- {
- StartRound(true);
- }
- }
+ public void SetGamePreset(GamePresetPrototype? preset, bool force = false, int? resetDelay = null)
+ {
+ // Do nothing if this game ticker is a dummy!
+ if (DummyTicker)
+ return;
- public void SetGamePreset(string preset, bool force = false)
+ if (resetDelay is not null)
{
- var proto = FindGamePreset(preset);
- if(proto != null)
- SetGamePreset(proto, force);
+ ResetCountdown = resetDelay.Value;
+ // Reset counter is checked and changed at the end of each round
+ // So if the game is in the lobby, the first requested round will happen before the check, and we need one less check
+ if (CurrentPreset is null)
+ ResetCountdown = resetDelay.Value -1;
}
- public GamePresetPrototype? FindGamePreset(string preset)
+ Preset = preset;
+ ValidateMap();
+ UpdateInfoText();
+
+ if (force)
{
- if (_prototypeManager.TryIndex(preset, out GamePresetPrototype? presetProto))
- return presetProto;
+ StartRound(true);
+ }
+ }
- foreach (var proto in _prototypeManager.EnumeratePrototypes())
- {
- foreach (var alias in proto.Alias)
- {
- if (preset.Equals(alias, StringComparison.InvariantCultureIgnoreCase))
- return proto;
- }
- }
+ public void SetGamePreset(string preset, bool force = false)
+ {
+ var proto = FindGamePreset(preset);
+ if(proto != null)
+ SetGamePreset(proto, force);
+ }
- return null;
- }
+ public GamePresetPrototype? FindGamePreset(string preset)
+ {
+ if (_prototypeManager.TryIndex(preset, out GamePresetPrototype? presetProto))
+ return presetProto;
- public bool TryFindGamePreset(string preset, [NotNullWhen(true)] out GamePresetPrototype? prototype)
+ foreach (var proto in _prototypeManager.EnumeratePrototypes())
{
- prototype = FindGamePreset(preset);
-
- return prototype != null;
+ foreach (var alias in proto.Alias)
+ {
+ if (preset.Equals(alias, StringComparison.InvariantCultureIgnoreCase))
+ return proto;
+ }
}
- public bool IsMapEligible(GameMapPrototype map)
- {
- if (Preset == null)
- return true;
+ return null;
+ }
- if (Preset.MapPool == null || !_prototypeManager.TryIndex(Preset.MapPool, out var pool))
- return true;
+ public bool TryFindGamePreset(string preset, [NotNullWhen(true)] out GamePresetPrototype? prototype)
+ {
+ prototype = FindGamePreset(preset);
- return pool.Maps.Contains(map.ID);
- }
+ return prototype != null;
+ }
- private void ValidateMap()
- {
- if (Preset == null || _gameMapManager.GetSelectedMap() is not { } map)
- return;
+ public bool IsMapEligible(GameMapPrototype map)
+ {
+ if (Preset == null)
+ return true;
- if (Preset.MapPool == null ||
- !_prototypeManager.TryIndex(Preset.MapPool, out var pool))
- return;
+ if (Preset.MapPool == null || !_prototypeManager.TryIndex(Preset.MapPool, out var pool))
+ return true;
- if (pool.Maps.Contains(map.ID))
- return;
+ return pool.Maps.Contains(map.ID);
+ }
- _gameMapManager.SelectMapRandom();
- }
+ private void ValidateMap()
+ {
+ if (Preset == null || _gameMapManager.GetSelectedMap() is not { } map)
+ return;
- [PublicAPI]
- private bool AddGamePresetRules()
- {
- if (DummyTicker || Preset == null)
- return false;
+ if (Preset.MapPool == null ||
+ !_prototypeManager.TryIndex(Preset.MapPool, out var pool))
+ return;
- CurrentPreset = Preset;
- foreach (var rule in Preset.Rules)
- {
- AddGameRule(rule);
- }
+ if (pool.Maps.Contains(map.ID))
+ return;
- return true;
- }
+ _gameMapManager.SelectMapRandom();
+ }
- public void StartGamePresetRules()
+ [PublicAPI]
+ private bool AddGamePresetRules()
+ {
+ if (DummyTicker || Preset == null)
+ return false;
+
+ CurrentPreset = Preset;
+ foreach (var rule in Preset.Rules)
{
- // May be touched by the preset during init.
- var rules = new List(GetAddedGameRules());
- foreach (var rule in rules)
- {
- StartGameRule(rule);
- }
+ AddGameRule(rule);
}
- public bool OnGhostAttempt(EntityUid mindId, bool canReturnGlobal, bool viaCommand = false, MindComponent? mind = null)
- {
- if (!Resolve(mindId, ref mind))
- return false;
+ return true;
+ }
+
+ private void TryResetPreset()
+ {
+ if (ResetCountdown is null || ResetCountdown-- > 0)
+ return;
- var playerEntity = mind.CurrentEntity;
+ InitializeGamePreset();
+ ResetCountdown = null;
+ }
- if (playerEntity != null && viaCommand)
- _adminLogger.Add(LogType.Mind, $"{EntityManager.ToPrettyString(playerEntity.Value):player} is attempting to ghost via command");
+ public void StartGamePresetRules()
+ {
+ // May be touched by the preset during init.
+ var rules = new List(GetAddedGameRules());
+ foreach (var rule in rules)
+ {
+ StartGameRule(rule);
+ }
+ }
- var handleEv = new GhostAttemptHandleEvent(mind, canReturnGlobal);
- RaiseLocalEvent(handleEv);
+ public bool OnGhostAttempt(EntityUid mindId, bool canReturnGlobal, bool viaCommand = false, MindComponent? mind = null)
+ {
+ if (!Resolve(mindId, ref mind))
+ return false;
- // Something else has handled the ghost attempt for us! We return its result.
- if (handleEv.Handled)
- return handleEv.Result;
+ var playerEntity = mind.CurrentEntity;
- if (mind.PreventGhosting)
- {
- if (mind.Session != null) // Logging is suppressed to prevent spam from ghost attempts caused by movement attempts
- {
- _chatManager.DispatchServerMessage(mind.Session, Loc.GetString("comp-mind-ghosting-prevented"),
- true);
- }
+ if (playerEntity != null && viaCommand)
+ _adminLogger.Add(LogType.Mind, $"{EntityManager.ToPrettyString(playerEntity.Value):player} is attempting to ghost via command");
- return false;
- }
+ var handleEv = new GhostAttemptHandleEvent(mind, canReturnGlobal);
+ RaiseLocalEvent(handleEv);
- if (TryComp(playerEntity, out var comp) && !comp.CanGhostInteract)
- return false;
+ // Something else has handled the ghost attempt for us! We return its result.
+ if (handleEv.Handled)
+ return handleEv.Result;
- if (mind.VisitingEntity != default)
+ if (mind.PreventGhosting)
+ {
+ if (mind.Session != null) // Logging is suppressed to prevent spam from ghost attempts caused by movement attempts
{
- _mind.UnVisit(mindId, mind: mind);
+ _chatManager.DispatchServerMessage(mind.Session, Loc.GetString("comp-mind-ghosting-prevented"),
+ true);
}
- var position = Exists(playerEntity)
- ? Transform(playerEntity.Value).Coordinates
- : GetObserverSpawnPoint();
+ return false;
+ }
- if (position == default)
- return false;
+ if (TryComp(playerEntity, out var comp) && !comp.CanGhostInteract)
+ return false;
- // Ok, so, this is the master place for the logic for if ghosting is "too cheaty" to allow returning.
- // There's no reason at this time to move it to any other place, especially given that the 'side effects required' situations would also have to be moved.
- // + If CharacterDeadPhysically applies, we're physically dead. Therefore, ghosting OK, and we can return (this is critical for gibbing)
- // Note that we could theoretically be ICly dead and still physically alive and vice versa.
- // (For example, a zombie could be dead ICly, but may retain memories and is definitely physically active)
- // + If we're in a mob that is critical, and we're supposed to be able to return if possible,
- // we're succumbing - the mob is killed. Therefore, character is dead. Ghosting OK.
- // (If the mob survives, that's a bug. Ghosting is kept regardless.)
- var canReturn = canReturnGlobal && _mind.IsCharacterDeadPhysically(mind);
-
- if (_configurationManager.GetCVar(CCVars.GhostKillCrit) &&
- canReturnGlobal &&
- TryComp(playerEntity, out MobStateComponent? mobState))
- {
- if (_mobState.IsCritical(playerEntity.Value, mobState))
- {
- canReturn = true;
-
- //todo: what if they dont breathe lol
- //cry deeply
+ if (mind.VisitingEntity != default)
+ {
+ _mind.UnVisit(mindId, mind: mind);
+ }
- FixedPoint2 dealtDamage = 200;
- if (TryComp(playerEntity, out var damageable)
- && TryComp(playerEntity, out var thresholds))
- {
- var playerDeadThreshold = _mobThresholdSystem.GetThresholdForState(playerEntity.Value, MobState.Dead, thresholds);
- dealtDamage = playerDeadThreshold - damageable.TotalDamage;
- }
+ var position = Exists(playerEntity)
+ ? Transform(playerEntity.Value).Coordinates
+ : GetObserverSpawnPoint();
+
+ if (position == default)
+ return false;
+
+ // Ok, so, this is the master place for the logic for if ghosting is "too cheaty" to allow returning.
+ // There's no reason at this time to move it to any other place, especially given that the 'side effects required' situations would also have to be moved.
+ // + If CharacterDeadPhysically applies, we're physically dead. Therefore, ghosting OK, and we can return (this is critical for gibbing)
+ // Note that we could theoretically be ICly dead and still physically alive and vice versa.
+ // (For example, a zombie could be dead ICly, but may retain memories and is definitely physically active)
+ // + If we're in a mob that is critical, and we're supposed to be able to return if possible,
+ // we're succumbing - the mob is killed. Therefore, character is dead. Ghosting OK.
+ // (If the mob survives, that's a bug. Ghosting is kept regardless.)
+ var canReturn = canReturnGlobal && _mind.IsCharacterDeadPhysically(mind);
+
+ if (_configurationManager.GetCVar(CCVars.GhostKillCrit) &&
+ canReturnGlobal &&
+ TryComp(playerEntity, out MobStateComponent? mobState))
+ {
+ if (_mobState.IsCritical(playerEntity.Value, mobState))
+ {
+ canReturn = true;
- DamageSpecifier damage = new(_prototypeManager.Index("Asphyxiation"), dealtDamage);
+ //todo: what if they dont breathe lol
+ //cry deeply
- _damageable.TryChangeDamage(playerEntity, damage, true);
+ FixedPoint2 dealtDamage = 200;
+ if (TryComp(playerEntity, out var damageable)
+ && TryComp(playerEntity, out var thresholds))
+ {
+ var playerDeadThreshold = _mobThresholdSystem.GetThresholdForState(playerEntity.Value, MobState.Dead, thresholds);
+ dealtDamage = playerDeadThreshold - damageable.TotalDamage;
}
- }
-
- var ghost = _ghost.SpawnGhost((mindId, mind), position, canReturn);
- if (ghost == null)
- return false;
- if (playerEntity != null)
- _adminLogger.Add(LogType.Mind, $"{EntityManager.ToPrettyString(playerEntity.Value):player} ghosted{(!canReturn ? " (non-returnable)" : "")}");
+ DamageSpecifier damage = new(_prototypeManager.Index("Asphyxiation"), dealtDamage);
- return true;
+ _damageable.TryChangeDamage(playerEntity, damage, true);
+ }
}
- private void IncrementRoundNumber()
- {
- var playerIds = _playerGameStatuses.Keys.Select(player => player.UserId).ToArray();
- var serverName = _configurationManager.GetCVar(CCVars.AdminLogsServerName);
+ var ghost = _ghost.SpawnGhost((mindId, mind), position, canReturn);
+ if (ghost == null)
+ return false;
- // TODO FIXME AAAAAAAAAAAAAAAAAAAH THIS IS BROKEN
- // Task.Run as a terrible dirty workaround to avoid synchronization context deadlock from .Result here.
- // This whole setup logic should be made asynchronous so we can properly wait on the DB AAAAAAAAAAAAAH
- var task = Task.Run(async () =>
- {
- var server = await _dbEntryManager.ServerEntity;
- return await _db.AddNewRound(server, playerIds);
- });
+ if (playerEntity != null)
+ _adminLogger.Add(LogType.Mind, $"{EntityManager.ToPrettyString(playerEntity.Value):player} ghosted{(!canReturn ? " (non-returnable)" : "")}");
- _taskManager.BlockWaitOnTask(task);
- RoundId = task.GetAwaiter().GetResult();
- }
+ return true;
}
- public sealed class GhostAttemptHandleEvent : HandledEntityEventArgs
+ private void IncrementRoundNumber()
{
- public MindComponent Mind { get; }
- public bool CanReturnGlobal { get; }
- public bool Result { get; set; }
+ var playerIds = _playerGameStatuses.Keys.Select(player => player.UserId).ToArray();
+ var serverName = _configurationManager.GetCVar(CCVars.AdminLogsServerName);
- public GhostAttemptHandleEvent(MindComponent mind, bool canReturnGlobal)
+ // TODO FIXME AAAAAAAAAAAAAAAAAAAH THIS IS BROKEN
+ // Task.Run as a terrible dirty workaround to avoid synchronization context deadlock from .Result here.
+ // This whole setup logic should be made asynchronous so we can properly wait on the DB AAAAAAAAAAAAAH
+ var task = Task.Run(async () =>
{
- Mind = mind;
- CanReturnGlobal = canReturnGlobal;
- }
+ var server = await _dbEntryManager.ServerEntity;
+ return await _db.AddNewRound(server, playerIds);
+ });
+
+ _taskManager.BlockWaitOnTask(task);
+ RoundId = task.GetAwaiter().GetResult();
+ }
+}
+
+public sealed class GhostAttemptHandleEvent : HandledEntityEventArgs
+{
+ public MindComponent Mind { get; }
+ public bool CanReturnGlobal { get; }
+ public bool Result { get; set; }
+
+ public GhostAttemptHandleEvent(MindComponent mind, bool canReturnGlobal)
+ {
+ Mind = mind;
+ CanReturnGlobal = canReturnGlobal;
}
}
diff --git a/Content.Server/GameTicking/GameTicker.RoundFlow.cs b/Content.Server/GameTicking/GameTicker.RoundFlow.cs
index ca087c46ed41..dc242fb6c082 100644
--- a/Content.Server/GameTicking/GameTicker.RoundFlow.cs
+++ b/Content.Server/GameTicking/GameTicker.RoundFlow.cs
@@ -487,6 +487,9 @@ public void RestartRound()
if (_serverUpdates.RoundEnded())
return;
+ // Check if the GamePreset needs to be reset
+ TryResetPreset();
+
_sawmill.Info("Restarting round!");
SendServerMessage(Loc.GetString("game-ticker-restart-round"));
diff --git a/Resources/Locale/en-US/game-ticking/set-game-preset-command.ftl b/Resources/Locale/en-US/game-ticking/set-game-preset-command.ftl
index 46049643cb57..323d83aebafa 100644
--- a/Resources/Locale/en-US/game-ticking/set-game-preset-command.ftl
+++ b/Resources/Locale/en-US/game-ticking/set-game-preset-command.ftl
@@ -1,5 +1,7 @@
-set-game-preset-command-description = Sets the game preset for the current round.
-set-game-preset-command-help-text = setgamepreset
+set-game-preset-command-description = Sets the game preset for the specified number of upcoming rounds.
+set-game-preset-command-help-text = setgamepreset [number of rounds, defaulting to 1]
+set-game-preset-optional-argument-not-integer = If argument 2 is provided it must be a number.
set-game-preset-preset-error = Unable to find game preset "{$preset}"
-set-game-preset-preset-set = Set game preset to "{$preset}"
+#set-game-preset-preset-set = Set game preset to "{$preset}"
+set-game-preset-preset-set-finite = Set game preset to "{$preset}" for the next {$rounds} rounds.