diff --git a/Content.Server/GameTicking/GameTicker.RoundFlow.cs b/Content.Server/GameTicking/GameTicker.RoundFlow.cs index a1c58fc4f3..a33e8f2c2a 100644 --- a/Content.Server/GameTicking/GameTicker.RoundFlow.cs +++ b/Content.Server/GameTicking/GameTicker.RoundFlow.cs @@ -71,7 +71,7 @@ namespace Content.Server.GameTicking DefaultMap = _mapManager.CreateMap(); _mapManager.AddUninitializedMap(DefaultMap); var startTime = _gameTiming.RealTime; - var maps = new List() { _gameMapManager.GetSelectedMapChecked(true) }; + var maps = new List() { _gameMapManager.GetSelectedMapChecked(true, true) }; // Let game rules dictate what maps we should load. RaiseLocalEvent(new LoadingMapsEvent(maps)); diff --git a/Content.Server/Maps/GameMapManager.cs b/Content.Server/Maps/GameMapManager.cs index 0f980d5b37..ad0ef26ffe 100644 --- a/Content.Server/Maps/GameMapManager.cs +++ b/Content.Server/Maps/GameMapManager.cs @@ -22,8 +22,11 @@ public sealed class GameMapManager : IGameMapManager [Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly IChatManager _chatManager = default!; + private readonly Queue _previousMaps = new Queue(); private GameMapPrototype _currentMap = default!; private bool _currentMapForced; + private bool _mapRotationEnabled; + private int _mapQueueDepth = 1; public void Initialize() { @@ -35,6 +38,25 @@ public sealed class GameMapManager : IGameMapManager throw new ArgumentException($"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 => + { + _mapQueueDepth = value; + // Drain excess. + while (_previousMaps.Count > _mapQueueDepth) + { + _previousMaps.Dequeue(); + } + }, true); + + var maps = AllVotableMaps().ToArray(); + _random.Shuffle(maps); + foreach (var map in maps) + { + if (_previousMaps.Count >= _mapQueueDepth) + break; + _previousMaps.Enqueue(map.ID); + } } public IEnumerable CurrentlyEligibleMaps() @@ -75,17 +97,18 @@ public sealed class GameMapManager : IGameMapManager public void SelectRandomMap() { var maps = CurrentlyEligibleMaps().ToList(); - _random.Shuffle(maps); - _currentMap = maps[0]; + _currentMap = _random.Pick(maps); _currentMapForced = false; } public GameMapPrototype GetSelectedMap() { - return _currentMap; + if (!_mapRotationEnabled || _currentMapForced) + return _currentMap; + return SelectMapInQueue() ?? CurrentlyEligibleMaps().First(); } - public GameMapPrototype GetSelectedMapChecked(bool loud = false) + public GameMapPrototype GetSelectedMapChecked(bool loud = false, bool markAsPlayed = false) { if (!_currentMapForced && !IsMapEligible(GetSelectedMap())) { @@ -100,7 +123,11 @@ public sealed class GameMapManager : IGameMapManager } } - return GetSelectedMap(); + var map = GetSelectedMap(); + + if (markAsPlayed) + _previousMaps.Enqueue(map.ID); + return map; } public bool CheckMapExists(string gameMap) @@ -127,4 +154,39 @@ public sealed class GameMapManager : IGameMapManager else return gameMap.MapName; } + + public int GetMapQueuePriority(string gameMapProtoName) + { + var i = 0; + foreach (var map in _previousMaps.Reverse()) + { + if (map == gameMapProtoName) + return i; + i++; + } + + return _mapQueueDepth; + } + + public GameMapPrototype? SelectMapInQueue() + { + var eligible = CurrentlyEligibleMaps() + .Where(x => x.Votable) + .Select(x => (proto: x, weight: GetMapQueuePriority(x.ID))) + .OrderByDescending(x => x.weight).ToArray(); + if (eligible.Length is 0) + return null; + + var weight = eligible[0].weight; + return eligible.Where(x => x.Item2 == weight).OrderBy(x => x.proto.ID).First().proto; + } + + private void EnqueueMap(string mapProtoName) + { + _previousMaps.Enqueue(mapProtoName); + while (_previousMaps.Count > _mapQueueDepth) + { + _previousMaps.Dequeue(); + } + } } diff --git a/Content.Server/Maps/IGameMapManager.cs b/Content.Server/Maps/IGameMapManager.cs index 5177220761..877e7520b0 100644 --- a/Content.Server/Maps/IGameMapManager.cs +++ b/Content.Server/Maps/IGameMapManager.cs @@ -56,7 +56,7 @@ public interface IGameMapManager /// Gets the currently selected map, double-checking if it can be used. /// /// selected map - GameMapPrototype GetSelectedMapChecked(bool loud = false); + GameMapPrototype GetSelectedMapChecked(bool loud = false, bool markAsPlayed = false); /// /// Checks if the given map exists diff --git a/Content.Shared/CCVar/CCVars.cs b/Content.Shared/CCVar/CCVars.cs index 7b99f5d948..fa093fb33c 100644 --- a/Content.Shared/CCVar/CCVars.cs +++ b/Content.Shared/CCVar/CCVars.cs @@ -126,6 +126,19 @@ namespace Content.Shared.CCVar 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. + /// + public static readonly CVarDef + GameMapMemoryDepth = CVarDef.Create("game.map_memory_depth", 16, CVar.SERVERONLY); + + /// + /// Is map rotation enabled? + /// + public static readonly CVarDef + GameMapRotation = CVarDef.Create("game.map_rotation", true, CVar.SERVERONLY); + /// /// Whether a random position offset will be applied to the station on roundstart. /// @@ -562,7 +575,7 @@ namespace Content.Shared.CCVar /// See vote.enabled, but specific to map votes /// public static readonly CVarDef VoteMapEnabled = - CVarDef.Create("vote.map_enabled", true, CVar.SERVERONLY); + CVarDef.Create("vote.map_enabled", false, CVar.SERVERONLY); /// /// The required ratio of the server that must agree for a restart round vote to go through. diff --git a/Resources/Prototypes/Maps/game.yml b/Resources/Prototypes/Maps/game.yml index e4efd65ca5..5ef35d7429 100644 --- a/Resources/Prototypes/Maps/game.yml +++ b/Resources/Prototypes/Maps/game.yml @@ -88,6 +88,7 @@ !type:NanotrasenNameGenerator prefixCreator: '14' mapPath: /Maps/knightship.yml + votable: false minPlayers: 0 maxPlayers: 8 overflowJobs: []