diff --git a/Content.IntegrationTests/PoolManager.cs b/Content.IntegrationTests/PoolManager.cs index 3ada170851..804140602a 100644 --- a/Content.IntegrationTests/PoolManager.cs +++ b/Content.IntegrationTests/PoolManager.cs @@ -43,11 +43,9 @@ public static class PoolManager (CCVars.DatabaseSqliteDelay.Name, "0", false), (CCVars.HolidaysEnabled.Name, "false", false), (CCVars.GameMap.Name, "Empty", true), - (CCVars.GameMapForced.Name, "true", true), (CCVars.AdminLogsQueueSendDelay.Name, "0", true), (CCVars.NetPVS.Name, "false", true), (CCVars.NPCMaxUpdates.Name, "999999", true), - (CCVars.GameMapForced.Name, "true", true), (CCVars.SysWinTickPeriod.Name, "0", true), (CCVars.ContactMinimumThreads.Name, "1", true), (CCVars.ContactMultithreadThreshold.Name, "999", true), diff --git a/Content.Server/GameTicking/Commands/ForceMapCommand.cs b/Content.Server/GameTicking/Commands/ForceMapCommand.cs index c0007de76c..5d2191fb52 100644 --- a/Content.Server/GameTicking/Commands/ForceMapCommand.cs +++ b/Content.Server/GameTicking/Commands/ForceMapCommand.cs @@ -1,7 +1,9 @@ -using System.Linq; +using System.Linq; using Content.Server.Administration; using Content.Server.Maps; using Content.Shared.Administration; +using Content.Shared.CCVar; +using Robust.Shared.Configuration; using Robust.Shared.Console; using Robust.Shared.Prototypes; @@ -10,6 +12,8 @@ namespace Content.Server.GameTicking.Commands [AdminCommand(AdminFlags.Round)] sealed class ForceMapCommand : IConsoleCommand { + [Dependency] private readonly IConfigurationManager _configurationManager = default!; + public string Command => "forcemap"; public string Description => Loc.GetString("forcemap-command-description"); public string Help => Loc.GetString("forcemap-command-help"); @@ -25,7 +29,7 @@ namespace Content.Server.GameTicking.Commands var gameMap = IoCManager.Resolve(); var name = args[0]; - gameMap.ForceSelectMap(name); + _configurationManager.SetCVar(CCVars.GameMap, name); shell.WriteLine(Loc.GetString("forcemap-command-success", ("map", name))); } diff --git a/Content.Server/GameTicking/GameTicker.Lobby.cs b/Content.Server/GameTicking/GameTicker.Lobby.cs index 2af5bd0c60..728d8bad93 100644 --- a/Content.Server/GameTicking/GameTicker.Lobby.cs +++ b/Content.Server/GameTicking/GameTicker.Lobby.cs @@ -1,8 +1,11 @@ using Content.Shared.GameTicking; +using Content.Server.Station.Systems; +using Content.Server.Station.Components; using Robust.Server.Player; using Robust.Shared.Network; using Robust.Shared.Player; using Robust.Shared.Players; +using System.Text; namespace Content.Server.GameTicking { @@ -39,13 +42,33 @@ namespace Content.Server.GameTicking { return string.Empty; } - + var playerCount = $"{_playerManager.PlayerCount}"; - var map = _gameMapManager.GetSelectedMap(); - var mapName = map?.MapName ?? Loc.GetString("game-ticker-no-map-selected"); + + StringBuilder stationNames = new StringBuilder(); + if (_stationSystem.Stations.Count != 0) + { + foreach (EntityUid entUID in _stationSystem.Stations) + { + StationDataComponent? stationData = null; + MetaDataComponent? metaData = null; + if (Resolve(entUID, ref stationData, ref metaData, logMissing: true)) + { + if (stationNames.Length > 0) + stationNames.Append('\n'); + + stationNames.Append(metaData.EntityName); + } + } + } + else + { + stationNames.Append(Loc.GetString("game-ticker-no-map-selected")); + } + var gmTitle = Loc.GetString(Preset.ModeTitle); var desc = Loc.GetString(Preset.Description); - return Loc.GetString("game-ticker-get-info-text",("roundId", RoundId), ("playerCount", playerCount),("mapName", mapName),("gmTitle", gmTitle),("desc", desc)); + return Loc.GetString("game-ticker-get-info-text",("roundId", RoundId), ("playerCount", playerCount),("mapName", stationNames.ToString()),("gmTitle", gmTitle),("desc", desc)); } private TickerLobbyReadyEvent GetStatusSingle(ICommonSession player, PlayerGameStatus gameStatus) diff --git a/Content.Server/GameTicking/GameTicker.RoundFlow.cs b/Content.Server/GameTicking/GameTicker.RoundFlow.cs index 5f7acf19c5..78df4f397f 100644 --- a/Content.Server/GameTicking/GameTicker.RoundFlow.cs +++ b/Content.Server/GameTicking/GameTicker.RoundFlow.cs @@ -81,7 +81,29 @@ namespace Content.Server.GameTicking DefaultMap = _mapManager.CreateMap(); _mapManager.AddUninitializedMap(DefaultMap); var startTime = _gameTiming.RealTime; - var maps = new List() { _gameMapManager.GetSelectedMapChecked(true, true) }; + + var maps = new List(); + + // the map might have been force-set by something + // (i.e. votemap or forcemap) + var mainStationMap = _gameMapManager.GetSelectedMap(); + if (mainStationMap == null) + { + // otherwise set the map using the config rules + _gameMapManager.SelectMapByConfigRules(); + mainStationMap = _gameMapManager.GetSelectedMap(); + } + + // Small chance the above could return no map. + // ideally SelectMapByConfigRules will always find a valid map + if (mainStationMap != null) + { + maps.Add(mainStationMap); + } + else + { + throw new Exception("invalid config; couldn't select a valid station map!"); + } // Let game rules dictate what maps we should load. RaiseLocalEvent(new LoadingMapsEvent(maps)); @@ -149,6 +171,10 @@ namespace Content.Server.GameTicking LoadMaps(); + // map has been selected so update the lobby info text + // applies to players who didn't ready up + UpdateInfoText(); + StartGamePresetRules(); RoundLengthMetric.Set(0); @@ -423,6 +449,8 @@ namespace Content.Server.GameTicking _roleBanManager.Restart(); + _gameMapManager.ClearSelectedMap(); + // Clear up any game rules. ClearGameRules(); diff --git a/Content.Server/Maps/GameMapManager.cs b/Content.Server/Maps/GameMapManager.cs index 28c9413e7f..a29c009cef 100644 --- a/Content.Server/Maps/GameMapManager.cs +++ b/Content.Server/Maps/GameMapManager.cs @@ -1,6 +1,6 @@ +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Linq; -using Content.Server.Chat.Managers; using Content.Shared.CCVar; using Robust.Server.Player; using Robust.Shared.Configuration; @@ -15,29 +15,38 @@ public sealed class GameMapManager : IGameMapManager [Dependency] private readonly IConfigurationManager _configurationManager = default!; [Dependency] private readonly IPlayerManager _playerManager = default!; [Dependency] private readonly IRobustRandom _random = default!; - [Dependency] private readonly IChatManager _chatManager = default!; - - [ViewVariables] + + [ViewVariables(VVAccess.ReadOnly)] private readonly Queue _previousMaps = new(); - [ViewVariables] - private GameMapPrototype _currentMap = default!; - [ViewVariables] - private bool _currentMapForced; - [ViewVariables] + [ViewVariables(VVAccess.ReadOnly)] + private GameMapPrototype? _configSelectedMap = default; + [ViewVariables(VVAccess.ReadOnly)] + private GameMapPrototype? _selectedMap = default; // Don't change this value during a round! + [ViewVariables(VVAccess.ReadOnly)] private bool _mapRotationEnabled; - [ViewVariables] + [ViewVariables(VVAccess.ReadOnly)] private int _mapQueueDepth = 1; public void Initialize() { _configurationManager.OnValueChanged(CCVars.GameMap, value => { - if (TryLookupMap(value, out var map)) - _currentMap = map; + if (TryLookupMap(value, out GameMapPrototype? map)) + { + _configSelectedMap = map; + } else - throw new ArgumentException($"Unknown map prototype {value} was selected!"); + { + if (string.IsNullOrEmpty(value)) + { + _configSelectedMap = default!; + } + else + { + Logger.ErrorS("mapsel", $"Unknown map prototype {value} was selected!"); + } + } }, true); - _configurationManager.OnValueChanged(CCVars.GameMapForced, value => _currentMapForced = value, true); _configurationManager.OnValueChanged(CCVars.GameMapRotation, value => _mapRotationEnabled = value, true); _configurationManager.OnValueChanged(CCVars.GameMapMemoryDepth, value => { @@ -62,7 +71,6 @@ public sealed class GameMapManager : IGameMapManager public IEnumerable CurrentlyEligibleMaps() { var maps = AllVotableMaps().Where(IsMapEligible).ToArray(); - return maps.Length == 0 ? AllMaps().Where(x => x.Fallback) : maps; } @@ -91,64 +99,59 @@ public sealed class GameMapManager : IGameMapManager return _prototypeManager.EnumeratePrototypes(); } - public bool TrySelectMap(string gameMap) + public GameMapPrototype? GetSelectedMap() { - if (!TryLookupMap(gameMap, out var map) || !IsMapEligible(map)) return false; - - _currentMap = map; - _currentMapForced = false; - var ticker = EntitySystem.Get(); - ticker.UpdateInfoText(); - return true; - + return _configSelectedMap ?? _selectedMap; } - public void ForceSelectMap(string gameMap) + public void ClearSelectedMap() + { + _selectedMap = default!; + } + + public bool TrySelectMapIfEligible(string gameMap) + { + if (!TryLookupMap(gameMap, out var map) || !IsMapEligible(map)) + return false; + _selectedMap = map; + return true; + } + + public void SelectMap(string gameMap) { if (!TryLookupMap(gameMap, out var map)) throw new ArgumentException($"The map \"{gameMap}\" is invalid!"); - _currentMap = map; - _currentMapForced = true; - var ticker = EntitySystem.Get(); - ticker.UpdateInfoText(); + _selectedMap = map; } - public void SelectRandomMap() + public void SelectMapRandom() { var maps = CurrentlyEligibleMaps().ToList(); - _currentMap = _random.Pick(maps); - _currentMapForced = false; - var ticker = EntitySystem.Get(); - ticker.UpdateInfoText(); + _selectedMap = _random.Pick(maps); } - public GameMapPrototype GetSelectedMap() + public void SelectMapFromRotationQueue(bool markAsPlayed = false) { - if (!_mapRotationEnabled || _currentMapForced) - return _currentMap; - return SelectMapInQueue() ?? CurrentlyEligibleMaps().First(); - } + var map = GetFirstInRotationQueue(); - public GameMapPrototype GetSelectedMapChecked(bool loud = false, bool markAsPlayed = false) - { - if (!_currentMapForced && !IsMapEligible(GetSelectedMap())) - { - var oldMap = GetSelectedMap().MapName; - SelectRandomMap(); - if (loud) - { - _chatManager.DispatchServerAnnouncement( - Loc.GetString("gamemap-could-not-use-map-error", - ("oldMap", oldMap), ("newMap", GetSelectedMap().MapName) - )); - } - } - - var map = GetSelectedMap(); + _selectedMap = map; if (markAsPlayed) EnqueueMap(map.ID); - return map; + } + + public void SelectMapByConfigRules() + { + if (_mapRotationEnabled) + { + Logger.InfoS("mapsel", "selecting the next map from the rotation queue"); + SelectMapFromRotationQueue(true); + } + else + { + Logger.InfoS("mapsel", "selecting a random map"); + SelectMapRandom(); + } } public bool CheckMapExists(string gameMap) @@ -168,7 +171,7 @@ public sealed class GameMapManager : IGameMapManager return _prototypeManager.TryIndex(gameMap, out map); } - public int GetMapQueuePriority(string gameMapProtoName) + private int GetMapRotationQueuePriority(string gameMapProtoName) { var i = 0; foreach (var map in _previousMaps.Reverse()) @@ -177,22 +180,28 @@ public sealed class GameMapManager : IGameMapManager return i; i++; } - return _mapQueueDepth; } - public GameMapPrototype? SelectMapInQueue() + private GameMapPrototype GetFirstInRotationQueue() { - Logger.InfoS("mapsel", string.Join(", ", _previousMaps)); + Logger.InfoS("mapsel", $"map queue: {string.Join(", ", _previousMaps)}"); + var eligible = CurrentlyEligibleMaps() - .Select(x => (proto: x, weight: GetMapQueuePriority(x.ID))) - .OrderByDescending(x => x.weight).ToArray(); - Logger.InfoS("mapsel", string.Join(", ", eligible.Select(x => (x.proto.ID, x.weight)))); - if (eligible.Length is 0) - return null; + .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)))}"); + + // 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; + return eligible.Where(x => x.Item2 == weight) + .OrderBy(x => x.proto.ID) + .First() + .proto; } private void EnqueueMap(string mapProtoName) diff --git a/Content.Server/Maps/GameMapPrototype.cs b/Content.Server/Maps/GameMapPrototype.cs index 3b2b16c710..d609a80cd3 100644 --- a/Content.Server/Maps/GameMapPrototype.cs +++ b/Content.Server/Maps/GameMapPrototype.cs @@ -2,6 +2,7 @@ using Content.Server.Station; using JetBrains.Annotations; using Robust.Shared.Prototypes; using Robust.Shared.Utility; +using System.Diagnostics; namespace Content.Server.Maps; @@ -13,6 +14,7 @@ namespace Content.Server.Maps; /// Make a new partial for your fancy new feature, it'll save you time later. /// [Prototype("gameMap"), PublicAPI] +[DebuggerDisplay("GameMapPrototype [{ID} - {MapName}]")] public sealed partial class GameMapPrototype : IPrototype { /// diff --git a/Content.Server/Maps/IGameMapManager.cs b/Content.Server/Maps/IGameMapManager.cs index da8735649f..02433c7b79 100644 --- a/Content.Server/Maps/IGameMapManager.cs +++ b/Content.Server/Maps/IGameMapManager.cs @@ -26,35 +26,45 @@ public interface IGameMapManager IEnumerable AllMaps(); /// - /// Attempts to select the given map. + /// Gets the currently selected map + /// + /// selected map + GameMapPrototype? GetSelectedMap(); + + /// + /// Clears the selected map, if any + /// + void ClearSelectedMap(); + + /// + /// Attempts to select the given map, checking eligibility criteria /// /// map prototype /// success or failure - bool TrySelectMap(string gameMap); + bool TrySelectMapIfEligible(string gameMap); /// - /// Forces the given map, making sure the game map manager won't reselect if conditions are no longer met at round restart. + /// Select the given map regardless of eligibility /// /// map prototype /// success or failure - void ForceSelectMap(string gameMap); + void SelectMap(string gameMap); /// - /// Selects a random map. + /// Selects a random map eligible map /// - void SelectRandomMap(); + void SelectMapRandom(); /// - /// Gets the currently selected map, without double-checking if it can be used. + /// Selects the map at the front of the rotation queue /// /// selected map - GameMapPrototype GetSelectedMap(); + void SelectMapFromRotationQueue(bool markAsPlayed = false); /// - /// Gets the currently selected map, double-checking if it can be used. + /// Selects the map by following rules set in the config /// - /// selected map - GameMapPrototype GetSelectedMapChecked(bool loud = false, bool markAsPlayed = false); + public void SelectMapByConfigRules(); /// /// Checks if the given map exists diff --git a/Content.Server/Voting/Managers/VoteManager.DefaultVotes.cs b/Content.Server/Voting/Managers/VoteManager.DefaultVotes.cs index 07a87018dd..28f259d996 100644 --- a/Content.Server/Voting/Managers/VoteManager.DefaultVotes.cs +++ b/Content.Server/Voting/Managers/VoteManager.DefaultVotes.cs @@ -36,7 +36,7 @@ namespace Content.Server.Voting.Managers default: throw new ArgumentOutOfRangeException(nameof(voteType), voteType, null); } - var ticker = EntitySystem.Get(); + var ticker = _entityManager.EntitySysManager.GetEntitySystem(); ticker.UpdateInfoText(); TimeoutStandardVote(voteType); } @@ -76,7 +76,8 @@ namespace Content.Server.Voting.Managers if (total > 0 && votesYes / (float) total >= ratioRequired) { _chatManager.DispatchServerAnnouncement(Loc.GetString("ui-vote-restart-succeeded")); - EntitySystem.Get().EndRound(); + var roundEnd = _entityManager.EntitySysManager.GetEntitySystem(); + roundEnd.EndRound(); } else { @@ -141,8 +142,8 @@ namespace Content.Server.Voting.Managers _chatManager.DispatchServerAnnouncement( Loc.GetString("ui-vote-gamemode-win", ("winner", Loc.GetString(presets[picked])))); } - - EntitySystem.Get().SetGamePreset(picked); + var ticker = _entityManager.EntitySysManager.GetEntitySystem(); + ticker.SetGamePreset(picked); }; } @@ -187,7 +188,18 @@ namespace Content.Server.Voting.Managers Loc.GetString("ui-vote-map-win", ("winner", maps[picked]))); } - _gameMapManager.TrySelectMap(picked.ID); + var ticker = _entityManager.EntitySysManager.GetEntitySystem(); + if (ticker.RunLevel == GameRunLevel.PreRoundLobby) + { + if (_gameMapManager.TrySelectMapIfEligible(picked.ID)) + { + ticker.UpdateInfoText(); + } + } + else + { + _chatManager.DispatchServerAnnouncement(Loc.GetString("ui-vote-map-notlobby")); + } }; } diff --git a/Content.Server/Voting/Managers/VoteManager.cs b/Content.Server/Voting/Managers/VoteManager.cs index ac59fec476..a2733d6ba4 100644 --- a/Content.Server/Voting/Managers/VoteManager.cs +++ b/Content.Server/Voting/Managers/VoteManager.cs @@ -34,6 +34,7 @@ namespace Content.Server.Voting.Managers [Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly IGameMapManager _gameMapManager = default!; + [Dependency] private readonly IEntityManager _entityManager = default!; private int _nextVoteId = 1; diff --git a/Content.Shared/CCVar/CCVars.cs b/Content.Shared/CCVar/CCVars.cs index 5d23745f17..8668c58c42 100644 --- a/Content.Shared/CCVar/CCVars.cs +++ b/Content.Shared/CCVar/CCVars.cs @@ -188,7 +188,7 @@ namespace Content.Shared.CCVar /// Controls the game map prototype to load. SS14 stores these prototypes in Prototypes/Maps. /// public static readonly CVarDef - GameMap = CVarDef.Create("game.map", "Saltern", CVar.SERVERONLY); + GameMap = CVarDef.Create("game.map", string.Empty, CVar.SERVERONLY); /// /// Prototype to use for map pool. @@ -196,12 +196,6 @@ namespace Content.Shared.CCVar public static readonly CVarDef GameMapPool = CVarDef.Create("game.map_pool", "DefaultMapPool", CVar.SERVERONLY); - /// - /// Controls if the game should obey map criteria or not. Overriden if a map vote or similar occurs. - /// - public static readonly CVarDef - GameMapForced = CVarDef.Create("game.mapforced", false, CVar.SERVERONLY); - /// /// The depth of the queue used to calculate which map is next in rotation. /// This is how long the game "remembers" that some map was put in play. Default is 16 rounds. diff --git a/Resources/Locale/en-US/game-ticking/game-ticker.ftl b/Resources/Locale/en-US/game-ticking/game-ticker.ftl index 4719531e0f..6fd3b2aaa0 100644 --- a/Resources/Locale/en-US/game-ticking/game-ticker.ftl +++ b/Resources/Locale/en-US/game-ticking/game-ticker.ftl @@ -13,7 +13,7 @@ game-ticker-get-info-text = Hi and welcome to [color=white]Space Station 14![/co The current map is: [color=white]{$mapName}[/color] The current game mode is: [color=white]{$gmTitle}[/color] >[color=yellow]{$desc}[/color] -game-ticker-no-map-selected = [color=red]No map selected![/color] +game-ticker-no-map-selected = [color=yellow]Map not yet selected![/color] game-ticker-player-no-jobs-available-when-joining = When attempting to join to the game, no jobs were available. # Displayed in chat to admins when a player joins diff --git a/Resources/Locale/en-US/voting/managers/vote-manager.ftl b/Resources/Locale/en-US/voting/managers/vote-manager.ftl index 1534d6d177..33129a95d5 100644 --- a/Resources/Locale/en-US/voting/managers/vote-manager.ftl +++ b/Resources/Locale/en-US/voting/managers/vote-manager.ftl @@ -17,3 +17,4 @@ ui-vote-gamemode-win = { $winner } won the gamemode vote! ui-vote-map-title = Next map ui-vote-map-tie = Tie for map vote! Picking... { $picked } ui-vote-map-win = { $winner } won the map vote! +ui-vote-map-notlobby = Voting for maps is only valid in the pre-round lobby! \ No newline at end of file