]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
setgamepreset command rework (#30756)
authorErrant <35878406+Errant-4@users.noreply.github.com>
Fri, 9 Aug 2024 07:04:19 +0000 (09:04 +0200)
committerGitHub <noreply@github.com>
Fri, 9 Aug 2024 07:04:19 +0000 (17:04 +1000)
* gameticker.gamepreset namespace

* setgamepreset now has a finite duration

* comments, cleanup

Content.Server/GameTicking/Commands/SetGamePresetCommand.cs
Content.Server/GameTicking/GameTicker.GamePreset.cs
Content.Server/GameTicking/GameTicker.RoundFlow.cs
Resources/Locale/en-US/game-ticking/set-game-preset-command.ftl

index 83736bd92b0ef311304fd774109da43e13ae5256..78e2b452b7bedbe9bd2549fefe7c12501ae323ec 100644 (file)
@@ -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 @@ namespace Content.Server.GameTicking.Commands
 
         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 @@ namespace Content.Server.GameTicking.Commands
                 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)
index 5a2b375dd68c88fe206dd5da2b19065d0e308ff6..6c12a2ea27e99ef7e07bfc6dd23b24783c2c6909 100644 (file)
@@ -16,305 +16,327 @@ using Content.Shared.Mobs.Systems;
 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;
 
-        /// <summary>
-        /// The selected preset that will be used at the start of the next round.
-        /// </summary>
-        public GamePresetPrototype? Preset { get; private set; }
+    /// <summary>
+    /// The selected preset that will be used at the start of the next round.
+    /// </summary>
+    public GamePresetPrototype? Preset { get; private set; }
 
-        /// <summary>
-        /// The preset that's currently active.
-        /// </summary>
-        public GamePresetPrototype? CurrentPreset { get; private set; }
+    /// <summary>
+    /// The preset that's currently active.
+    /// </summary>
+    public GamePresetPrototype? CurrentPreset { get; private set; }
 
-        private bool StartPreset(ICommonSession[] origReadyPlayers, bool force)
-        {
-            var startAttempt = new RoundStartAttemptEvent(origReadyPlayers, force);
-            RaiseLocalEvent(startAttempt);
+    /// <summary>
+    /// Countdown to the preset being reset to the server default.
+    /// </summary>
+    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<GamePresetPrototype>())
-            {
-                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<GamePresetPrototype>())
         {
-            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<GameMapPoolPrototype>(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<GameMapPoolPrototype>(Preset.MapPool, out var pool))
-                return;
+        if (Preset.MapPool == null || !_prototypeManager.TryIndex<GameMapPoolPrototype>(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<GameMapPoolPrototype>(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<EntityUid>(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<EntityUid>(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<GhostComponent>(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<GhostComponent>(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<DamageableComponent>(playerEntity, out var damageable)
-                        && TryComp<MobThresholdsComponent>(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<DamageTypePrototype>("Asphyxiation"), dealtDamage);
+                //todo: what if they dont breathe lol
+                //cry deeply
 
-                    _damageable.TryChangeDamage(playerEntity, damage, true);
+                FixedPoint2 dealtDamage = 200;
+                if (TryComp<DamageableComponent>(playerEntity, out var damageable)
+                    && TryComp<MobThresholdsComponent>(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<DamageTypePrototype>("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;
     }
 }
index ca087c46ed414602f0f75f11dfd4d59b0b306552..dc242fb6c0827de5b524b6b5fea5b3bcab9fa633 100644 (file)
@@ -487,6 +487,9 @@ namespace Content.Server.GameTicking
             if (_serverUpdates.RoundEnded())
                 return;
 
+            // Check if the GamePreset needs to be reset
+            TryResetPreset();
+
             _sawmill.Info("Restarting round!");
 
             SendServerMessage(Loc.GetString("game-ticker-restart-round"));
index 46049643cb577ccc98516b717482331af1aab9fc..323d83aebafa1d6c7298aec526ad33a5046e0bef 100644 (file)
@@ -1,5 +1,7 @@
-set-game-preset-command-description = Sets the game preset for the current round.
-set-game-preset-command-help-text = setgamepreset <id>
+set-game-preset-command-description = Sets the game preset for the specified number of upcoming rounds.
+set-game-preset-command-help-text = setgamepreset <id> [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.