diff --git a/Content.Server/GameTicking/GameTicker.GamePreset.cs b/Content.Server/GameTicking/GameTicker.GamePreset.cs
index 97e91f04c1..9ccddd110d 100644
--- a/Content.Server/GameTicking/GameTicker.GamePreset.cs
+++ b/Content.Server/GameTicking/GameTicker.GamePreset.cs
@@ -3,6 +3,7 @@ using System.Linq;
using System.Threading.Tasks;
using Content.Server.GameTicking.Presets;
using Content.Server.Ghost.Components;
+using Content.Server.Maps;
using Content.Shared.CCVar;
using Content.Shared.Damage;
using Content.Shared.Damage.Prototypes;
@@ -17,8 +18,16 @@ namespace Content.Server.GameTicking
{
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 preset that's currently active.
+ ///
+ public GamePresetPrototype? CurrentPreset { get; private set; }
+
private bool StartPreset(IPlayerSession[] origReadyPlayers, bool force)
{
var startAttempt = new RoundStartAttemptEvent(origReadyPlayers, force);
@@ -27,7 +36,7 @@ namespace Content.Server.GameTicking
if (!startAttempt.Cancelled)
return true;
- var presetTitle = Preset != null ? Loc.GetString(Preset.ModeTitle) : string.Empty;
+ var presetTitle = CurrentPreset != null ? Loc.GetString(CurrentPreset.ModeTitle) : string.Empty;
void FailedPresetRestart()
{
@@ -93,6 +102,7 @@ namespace Content.Server.GameTicking
Preset = preset;
UpdateInfoText();
+ ValidateMap();
if (force)
{
@@ -131,12 +141,39 @@ namespace Content.Server.GameTicking
return prototype != null;
}
+ public bool IsMapEligible(GameMapPrototype map)
+ {
+ if (Preset == null)
+ return true;
+
+ if (Preset.MapPool == null || !_prototypeManager.TryIndex(Preset.MapPool, out var pool))
+ return true;
+
+ return pool.Maps.Contains(map.ID);
+ }
+
+ private void ValidateMap()
+ {
+ if (Preset == null || _gameMapManager.GetSelectedMap() is not { } map)
+ return;
+
+ if (Preset.MapPool == null ||
+ !_prototypeManager.TryIndex(Preset.MapPool, out var pool))
+ return;
+
+ if (pool.Maps.Contains(map.ID))
+ return;
+
+ _gameMapManager.SelectMapRandom();
+ }
+
[PublicAPI]
private bool AddGamePresetRules()
{
if (DummyTicker || Preset == null)
return false;
+ CurrentPreset = Preset;
foreach (var rule in Preset.Rules)
{
AddGameRule(rule);
diff --git a/Content.Server/GameTicking/GameTicker.Lobby.cs b/Content.Server/GameTicking/GameTicker.Lobby.cs
index 1050c7c568..b7b6a29a5a 100644
--- a/Content.Server/GameTicking/GameTicker.Lobby.cs
+++ b/Content.Server/GameTicking/GameTicker.Lobby.cs
@@ -44,7 +44,8 @@ namespace Content.Server.GameTicking
private string GetInfoText()
{
- if (Preset == null)
+ var preset = CurrentPreset ?? Preset;
+ if (preset == null)
{
return string.Empty;
}
@@ -72,8 +73,8 @@ namespace Content.Server.GameTicking
stationNames.Append(Loc.GetString("game-ticker-no-map-selected"));
}
- var gmTitle = Loc.GetString(Preset.ModeTitle);
- var desc = Loc.GetString(Preset.Description);
+ var gmTitle = Loc.GetString(preset.ModeTitle);
+ var desc = Loc.GetString(preset.Description);
return Loc.GetString(RunLevel == GameRunLevel.PreRoundLobby ? "game-ticker-get-info-preround-text" : "game-ticker-get-info-text",
("roundId", RoundId), ("playerCount", playerCount), ("readyCount", readyCount), ("mapName", stationNames.ToString()),("gmTitle", gmTitle),("desc", desc));
}
diff --git a/Content.Server/GameTicking/GameTicker.RoundFlow.cs b/Content.Server/GameTicking/GameTicker.RoundFlow.cs
index 4883d8e3c0..6808ca4d4a 100644
--- a/Content.Server/GameTicking/GameTicker.RoundFlow.cs
+++ b/Content.Server/GameTicking/GameTicker.RoundFlow.cs
@@ -114,6 +114,17 @@ namespace Content.Server.GameTicking
throw new Exception("invalid config; couldn't select a valid station map!");
}
+ if (CurrentPreset?.MapPool != null &&
+ _prototypeManager.TryIndex(CurrentPreset.MapPool, out var pool) &&
+ pool.Maps.Contains(mainStationMap.ID))
+ {
+ var msg = Loc.GetString("game-ticker-start-round-invalid-map",
+ ("map", mainStationMap.MapName),
+ ("mode", Loc.GetString(CurrentPreset.ModeTitle)));
+ Log.Debug(msg);
+ SendServerMessage(msg);
+ }
+
// Let game rules dictate what maps we should load.
RaiseLocalEvent(new LoadingMapsEvent(maps));
@@ -292,7 +303,7 @@ namespace Content.Server.GameTicking
_adminLogger.Add(LogType.EmergencyShuttle, LogImpact.High, $"Round ended, showing summary");
//Tell every client the round has ended.
- var gamemodeTitle = Preset != null ? Loc.GetString(Preset.ModeTitle) : string.Empty;
+ var gamemodeTitle = CurrentPreset != null ? Loc.GetString(CurrentPreset.ModeTitle) : string.Empty;
// Let things add text here.
var textEv = new RoundEndTextAppendEvent();
@@ -306,7 +317,7 @@ namespace Content.Server.GameTicking
//Generate a list of basic player info to display in the end round summary.
var listOfPlayerInfo = new List();
// Grab the great big book of all the Minds, we'll need them for this.
- var allMinds = Get().AllMinds;
+ var allMinds = _mindTracker.AllMinds;
foreach (var mind in allMinds)
{
// TODO don't list redundant observer roles?
@@ -447,6 +458,7 @@ namespace Content.Server.GameTicking
// Clear up any game rules.
ClearGameRules();
+ CurrentPreset = null;
_allPreviousGameRules.Clear();
@@ -514,7 +526,7 @@ namespace Content.Server.GameTicking
private void AnnounceRound()
{
- if (Preset == null) return;
+ if (CurrentPreset == null) return;
var options = _prototypeManager.EnumeratePrototypes().ToList();
diff --git a/Content.Server/GameTicking/GameTicker.cs b/Content.Server/GameTicking/GameTicker.cs
index 9554e00989..c50243fdf3 100644
--- a/Content.Server/GameTicking/GameTicker.cs
+++ b/Content.Server/GameTicking/GameTicker.cs
@@ -39,6 +39,7 @@ namespace Content.Server.GameTicking
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly GhostSystem _ghost = default!;
[Dependency] private readonly MindSystem _mind = default!;
+ [Dependency] private readonly MindTrackerSystem _mindTracker = default!;
[Dependency] private readonly MobStateSystem _mobState = default!;
[ViewVariables] private bool _initialized;
@@ -92,7 +93,8 @@ namespace Content.Server.GameTicking
private void SendServerMessage(string message)
{
- _chatManager.ChatMessageToAll(ChatChannel.Server, message, "", default, false, true);
+ var wrappedMessage = Loc.GetString("chat-manager-server-wrap-message", ("message", message));
+ _chatManager.ChatMessageToAll(ChatChannel.Server, message, wrappedMessage, default, false, true);
}
public override void Update(float frameTime)
diff --git a/Content.Server/GameTicking/Presets/GamePresetPrototype.cs b/Content.Server/GameTicking/Presets/GamePresetPrototype.cs
index ff6a3d17ba..e3edb894c0 100644
--- a/Content.Server/GameTicking/Presets/GamePresetPrototype.cs
+++ b/Content.Server/GameTicking/Presets/GamePresetPrototype.cs
@@ -1,5 +1,7 @@
+using Content.Server.Maps;
using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
namespace Content.Server.GameTicking.Presets
@@ -33,5 +35,12 @@ namespace Content.Server.GameTicking.Presets
[DataField("rules", customTypeSerializer: typeof(PrototypeIdListSerializer))]
public IReadOnlyList Rules { get; } = Array.Empty();
+
+ ///
+ /// If specified, the gamemode will only be run with these maps.
+ /// If none are elligible, the global fallback will be used.
+ ///
+ [DataField("supportedMaps", customTypeSerializer: typeof(PrototypeIdSerializer))]
+ public readonly string? MapPool;
}
}
diff --git a/Content.Server/Maps/GameMapManager.cs b/Content.Server/Maps/GameMapManager.cs
index a29c009cef..2fb531e5d0 100644
--- a/Content.Server/Maps/GameMapManager.cs
+++ b/Content.Server/Maps/GameMapManager.cs
@@ -1,6 +1,7 @@
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
+using Content.Server.GameTicking;
using Content.Shared.CCVar;
using Robust.Server.Player;
using Robust.Shared.Configuration;
@@ -11,24 +12,29 @@ namespace Content.Server.Maps;
public sealed class GameMapManager : IGameMapManager
{
+ [Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IConfigurationManager _configurationManager = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IRobustRandom _random = default!;
-
+
[ViewVariables(VVAccess.ReadOnly)]
private readonly Queue _previousMaps = new();
[ViewVariables(VVAccess.ReadOnly)]
- private GameMapPrototype? _configSelectedMap = default;
+ private GameMapPrototype? _configSelectedMap;
[ViewVariables(VVAccess.ReadOnly)]
- private GameMapPrototype? _selectedMap = default; // Don't change this value during a round!
+ private GameMapPrototype? _selectedMap; // Don't change this value during a round!
[ViewVariables(VVAccess.ReadOnly)]
private bool _mapRotationEnabled;
[ViewVariables(VVAccess.ReadOnly)]
private int _mapQueueDepth = 1;
+ private ISawmill _log = default!;
+
public void Initialize()
{
+ _log = Logger.GetSawmill("mapsel");
+
_configurationManager.OnValueChanged(CCVars.GameMap, value =>
{
if (TryLookupMap(value, out GameMapPrototype? map))
@@ -43,7 +49,7 @@ public sealed class GameMapManager : IGameMapManager
}
else
{
- Logger.ErrorS("mapsel", $"Unknown map prototype {value} was selected!");
+ _log.Error($"Unknown map prototype {value} was selected!");
}
}
}, true);
@@ -76,21 +82,25 @@ public sealed class GameMapManager : IGameMapManager
public IEnumerable AllVotableMaps()
{
+ var poolPrototype = _entityManager.System().Preset?.MapPool ??
+ _configurationManager.GetCVar(CCVars.GameMapPool);
+
if (_prototypeManager.TryIndex(_configurationManager.GetCVar(CCVars.GameMapPool), out var pool))
{
foreach (var map in pool.Maps)
{
if (!_prototypeManager.TryIndex(map, out var mapProto))
{
- Logger.Error("Couldn't index map " + map + " in pool " + pool.ID);
+ _log.Error($"Couldn't index map {map} in pool {poolPrototype}");
continue;
}
yield return mapProto;
}
- } else
+ }
+ else
{
- throw new Exception("Could not index map pool prototype " + _configurationManager.GetCVar(CCVars.GameMapPool) + "!");
+ throw new Exception($"Could not index map pool prototype {poolPrototype}!");
}
}
@@ -144,12 +154,12 @@ public sealed class GameMapManager : IGameMapManager
{
if (_mapRotationEnabled)
{
- Logger.InfoS("mapsel", "selecting the next map from the rotation queue");
+ _log.Info("selecting the next map from the rotation queue");
SelectMapFromRotationQueue(true);
}
else
{
- Logger.InfoS("mapsel", "selecting a random map");
+ _log.Info("selecting a random map");
SelectMapRandom();
}
}
@@ -163,7 +173,8 @@ public sealed class GameMapManager : IGameMapManager
{
return map.MaxPlayers >= _playerManager.PlayerCount &&
map.MinPlayers <= _playerManager.PlayerCount &&
- map.Conditions.All(x => x.Check(map));
+ map.Conditions.All(x => x.Check(map)) &&
+ _entityManager.System().IsMapEligible(map);
}
private bool TryLookupMap(string gameMap, [NotNullWhen(true)] out GameMapPrototype? map)
@@ -185,23 +196,22 @@ public sealed class GameMapManager : IGameMapManager
private GameMapPrototype GetFirstInRotationQueue()
{
- Logger.InfoS("mapsel", $"map queue: {string.Join(", ", _previousMaps)}");
+ _log.Info($"map queue: {string.Join(", ", _previousMaps)}");
var eligible = CurrentlyEligibleMaps()
.Select(x => (proto: x, weight: GetMapRotationQueuePriority(x.ID)))
.OrderByDescending(x => x.weight)
.ToArray();
- Logger.InfoS("mapsel", $"eligible queue: {string.Join(", ", eligible.Select(x => (x.proto.ID, x.weight)))}");
+ _log.Info($"eligible queue: {string.Join(", ", eligible.Select(x => (x.proto.ID, x.weight)))}");
// YML "should" be configured with at least one fallback map
Debug.Assert(eligible.Length != 0, $"couldn't select a map with {nameof(GetFirstInRotationQueue)}()! No eligible maps and no fallback maps!");
var weight = eligible[0].weight;
return eligible.Where(x => x.Item2 == weight)
- .OrderBy(x => x.proto.ID)
- .First()
- .proto;
+ .MinBy(x => x.proto.ID)
+ .proto;
}
private void EnqueueMap(string mapProtoName)
diff --git a/Content.Server/Voting/Managers/VoteManager.cs b/Content.Server/Voting/Managers/VoteManager.cs
index 94933a0f16..98ad8d8341 100644
--- a/Content.Server/Voting/Managers/VoteManager.cs
+++ b/Content.Server/Voting/Managers/VoteManager.cs
@@ -5,7 +5,6 @@ using System.Linq;
using Content.Server.Administration;
using Content.Server.Administration.Logs;
using Content.Server.Administration.Managers;
-using Content.Server.Afk;
using Content.Server.Chat.Managers;
using Content.Server.GameTicking;
using Content.Server.Maps;
@@ -58,13 +57,15 @@ namespace Content.Server.Voting.Managers
_playerManager.PlayerStatusChanged += PlayerManagerOnPlayerStatusChanged;
_adminMgr.OnPermsChanged += AdminPermsChanged;
- _cfg.OnValueChanged(CCVars.VoteEnabled, value => {
+ _cfg.OnValueChanged(CCVars.VoteEnabled, _ =>
+ {
DirtyCanCallVoteAll();
});
foreach (var kvp in _voteTypesToEnableCVars)
{
- _cfg.OnValueChanged(kvp.Value, value => {
+ _cfg.OnValueChanged(kvp.Value, _ =>
+ {
DirtyCanCallVoteAll();
});
}
@@ -294,7 +295,7 @@ namespace Content.Server.Voting.Managers
var votesUnavailable = new List<(StandardVoteType, TimeSpan)>();
foreach (var v in _standardVoteTypeValues)
{
- if (CanCallVote(player, v, out var _isAdmin, out var typeTimeSpan))
+ if (CanCallVote(player, v, out _, out var typeTimeSpan))
continue;
votesUnavailable.Add((v, typeTimeSpan));
}
@@ -324,7 +325,7 @@ namespace Content.Server.Voting.Managers
if (!_cfg.GetCVar(CCVars.VoteEnabled))
return false;
// Specific standard vote types can be disabled with cvars.
- if ((voteType != null) && _voteTypesToEnableCVars.TryGetValue(voteType.Value, out var cvar) && !_cfg.GetCVar(cvar))
+ if (voteType != null && _voteTypesToEnableCVars.TryGetValue(voteType.Value, out var cvar) && !_cfg.GetCVar(cvar))
return false;
// Cannot start vote if vote is already active (as non-admin).
@@ -345,7 +346,7 @@ namespace Content.Server.Voting.Managers
if (voteType == StandardVoteType.Preset)
{
var presets = GetGamePresets();
- if (presets.Count() == 1 && presets.Select(x => x.Key).Single() == EntitySystem.Get().Preset?.ID)
+ if (presets.Count == 1 && presets.Select(x => x.Key).Single() == _entityManager.System().Preset?.ID)
return false;
}
diff --git a/Resources/Locale/en-US/game-ticking/game-ticker.ftl b/Resources/Locale/en-US/game-ticking/game-ticker.ftl
index 5dbeb5088d..004b6aaae0 100644
--- a/Resources/Locale/en-US/game-ticking/game-ticker.ftl
+++ b/Resources/Locale/en-US/game-ticking/game-ticker.ftl
@@ -2,6 +2,7 @@ game-ticker-restart-round = Restarting round...
game-ticker-start-round = The round is starting now...
game-ticker-start-round-cannot-start-game-mode-fallback = Failed to start {$failedGameMode} mode! Defaulting to {$fallbackMode}...
game-ticker-start-round-cannot-start-game-mode-restart = Failed to start {$failedGameMode} mode! Restarting round...
+game-ticker-start-round-invalid-map = Selected map {$map} is inelligible for gamemode {$mode}. Gamemode may not function as intended...
game-ticker-unknown-role = Unknown
game-ticker-delay-start = Round start has been delayed for {$seconds} seconds.
game-ticker-pause-start = Round start has been paused.