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.