Makes map vote and roundstart smart about player count. (#5418)

* Makes map vote and roundstart smart about player count.
No more Saltern with 30 players, or Knight Ship with 50.

* a typo

* Address reviews.

* Localized.
This commit is contained in:
Moony
2021-11-20 12:32:07 -06:00
committed by GitHub
parent 6487cd6d79
commit eb6d24abd0
18 changed files with 298 additions and 19 deletions

View File

@@ -89,6 +89,7 @@ namespace Content.Client.Entry
prototypes.RegisterIgnore("objective"); prototypes.RegisterIgnore("objective");
prototypes.RegisterIgnore("holiday"); prototypes.RegisterIgnore("holiday");
prototypes.RegisterIgnore("aiFaction"); prototypes.RegisterIgnore("aiFaction");
prototypes.RegisterIgnore("gameMap");
prototypes.RegisterIgnore("behaviorSet"); prototypes.RegisterIgnore("behaviorSet");
prototypes.RegisterIgnore("advertisementsPack"); prototypes.RegisterIgnore("advertisementsPack");
prototypes.RegisterIgnore("metabolizerType"); prototypes.RegisterIgnore("metabolizerType");

View File

@@ -33,7 +33,10 @@ namespace Content.IntegrationTests
(CCVars.HolidaysEnabled.Name, "false", false), (CCVars.HolidaysEnabled.Name, "false", false),
// Avoid loading a large map by default for integration tests if none has been specified. // Avoid loading a large map by default for integration tests if none has been specified.
(CCVars.GameMap.Name, "Maps/Test/empty.yml", true) (CCVars.GameMap.Name, "empty", true),
// Makes sure IGameMapManager actually listens.
(CCVars.GameMapForced.Name, "true", true)
}; };
private static void SetServerTestCvars(IntegrationOptions options) private static void SetServerTestCvars(IntegrationOptions options)

View File

@@ -24,7 +24,7 @@ namespace Content.IntegrationTests.Tests.Commands
{ {
CVarOverrides = CVarOverrides =
{ {
[CCVars.GameMap.Name] = "Maps/saltern.yml" [CCVars.GameMap.Name] = "saltern"
} }
}); });

View File

@@ -10,6 +10,7 @@ using Content.Server.EUI;
using Content.Server.GameTicking; using Content.Server.GameTicking;
using Content.Server.Holiday.Interfaces; using Content.Server.Holiday.Interfaces;
using Content.Server.IoC; using Content.Server.IoC;
using Content.Server.Maps;
using Content.Server.NodeContainer.NodeGroups; using Content.Server.NodeContainer.NodeGroups;
using Content.Server.Preferences.Managers; using Content.Server.Preferences.Managers;
using Content.Server.Sandbox; using Content.Server.Sandbox;
@@ -91,6 +92,7 @@ namespace Content.Server.Entry
IoCManager.Resolve<IAfkManager>().Initialize(); IoCManager.Resolve<IAfkManager>().Initialize();
_euiManager.Initialize(); _euiManager.Initialize();
IoCManager.Resolve<IGameMapManager>().Initialize();
IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<GameTicker>().PostInitialize(); IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<GameTicker>().PostInitialize();
IoCManager.Resolve<IBqlQueryManager>().DoAutoRegistrations(); IoCManager.Resolve<IBqlQueryManager>().DoAutoRegistrations();
} }

View File

@@ -1,4 +1,5 @@
using Content.Server.Administration; using Content.Server.Administration;
using Content.Server.Maps;
using Content.Shared.Administration; using Content.Shared.Administration;
using Content.Shared.CCVar; using Content.Shared.CCVar;
using Robust.Shared.Configuration; using Robust.Shared.Configuration;
@@ -24,10 +25,10 @@ namespace Content.Server.GameTicking.Commands
return; return;
} }
var cfg = IoCManager.Resolve<IConfigurationManager>(); var gameMap = IoCManager.Resolve<IGameMapManager>();
var name = args[0]; var name = args[0];
cfg.SetCVar(CCVars.GameMap, name); gameMap.ForceSelectMap(name);
shell.WriteLine(Loc.GetString("forcemap-command-success", ("map", name))); shell.WriteLine(Loc.GetString("forcemap-command-success", ("map", name)));
} }
} }

View File

@@ -12,9 +12,6 @@ namespace Content.Server.GameTicking
[ViewVariables] [ViewVariables]
public bool DummyTicker { get; private set; } = false; public bool DummyTicker { get; private set; } = false;
[ViewVariables]
public string ChosenMap { get; private set; } = string.Empty;
[ViewVariables] [ViewVariables]
public TimeSpan LobbyDuration { get; private set; } = TimeSpan.Zero; public TimeSpan LobbyDuration { get; private set; } = TimeSpan.Zero;
@@ -34,7 +31,6 @@ namespace Content.Server.GameTicking
{ {
_configurationManager.OnValueChanged(CCVars.GameLobbyEnabled, value => LobbyEnabled = value, true); _configurationManager.OnValueChanged(CCVars.GameLobbyEnabled, value => LobbyEnabled = value, true);
_configurationManager.OnValueChanged(CCVars.GameDummyTicker, value => DummyTicker = value, true); _configurationManager.OnValueChanged(CCVars.GameDummyTicker, value => DummyTicker = value, true);
_configurationManager.OnValueChanged(CCVars.GameMap, value => ChosenMap = value, true);
_configurationManager.OnValueChanged(CCVars.GameLobbyDuration, value => LobbyDuration = TimeSpan.FromSeconds(value), true); _configurationManager.OnValueChanged(CCVars.GameLobbyDuration, value => LobbyDuration = TimeSpan.FromSeconds(value), true);
_configurationManager.OnValueChanged(CCVars.GameDisallowLateJoins, _configurationManager.OnValueChanged(CCVars.GameDisallowLateJoins,
value => { DisallowLateJoin = value; UpdateLateJoinStatus(); UpdateJobsAvailable(); }, true); value => { DisallowLateJoin = value; UpdateLateJoinStatus(); UpdateJobsAvailable(); }, true);

View File

@@ -58,7 +58,7 @@ namespace Content.Server.GameTicking
{ {
DefaultMap = _mapManager.CreateMap(); DefaultMap = _mapManager.CreateMap();
var startTime = _gameTiming.RealTime; var startTime = _gameTiming.RealTime;
var map = ChosenMap; var map = _gameMapManager.GetSelectedMapChecked(true).MapPath;
var grid = _mapLoader.LoadBlueprint(DefaultMap, map); var grid = _mapLoader.LoadBlueprint(DefaultMap, map);
if (grid == null) if (grid == null)

View File

@@ -1,4 +1,5 @@
using Content.Server.Chat.Managers; using Content.Server.Chat.Managers;
using Content.Server.Maps;
using Content.Server.Preferences.Managers; using Content.Server.Preferences.Managers;
using Content.Shared.Chat; using Content.Shared.Chat;
using Content.Shared.GameTicking; using Content.Shared.GameTicking;
@@ -85,5 +86,6 @@ namespace Content.Server.GameTicking
[Dependency] private readonly IBaseServer _baseServer = default!; [Dependency] private readonly IBaseServer _baseServer = default!;
[Dependency] private readonly IWatchdogApi _watchdogApi = default!; [Dependency] private readonly IWatchdogApi _watchdogApi = default!;
[Dependency] private readonly IReflectionManager _reflectionManager = default!; [Dependency] private readonly IReflectionManager _reflectionManager = default!;
[Dependency] private readonly IGameMapManager _gameMapManager = default!;
} }
} }

View File

@@ -11,6 +11,7 @@ using Content.Server.DeviceNetwork;
using Content.Server.EUI; using Content.Server.EUI;
using Content.Server.Holiday; using Content.Server.Holiday;
using Content.Server.Holiday.Interfaces; using Content.Server.Holiday.Interfaces;
using Content.Server.Maps;
using Content.Server.Module; using Content.Server.Module;
using Content.Server.MoMMI; using Content.Server.MoMMI;
using Content.Server.NodeContainer.NodeGroups; using Content.Server.NodeContainer.NodeGroups;
@@ -52,6 +53,7 @@ namespace Content.Server.IoC
IoCManager.Register<INpcBehaviorManager, NpcBehaviorManager>(); IoCManager.Register<INpcBehaviorManager, NpcBehaviorManager>();
IoCManager.Register<IPlayerLocator, PlayerLocator>(); IoCManager.Register<IPlayerLocator, PlayerLocator>();
IoCManager.Register<IAfkManager, AfkManager>(); IoCManager.Register<IAfkManager, AfkManager>();
IoCManager.Register<IGameMapManager, GameMapManager>();
} }
} }
} }

View File

@@ -0,0 +1,120 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Content.Server.Chat.Managers;
using Content.Shared.CCVar;
using Robust.Server.Player;
using Robust.Shared.Configuration;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
namespace Content.Server.Maps
{
public class GameMapManager : IGameMapManager
{
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IConfigurationManager _configurationManager = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly IChatManager _chatManager = default!;
private GameMapPrototype _currentMap = default!;
private bool _currentMapForced;
public void Initialize()
{
_configurationManager.OnValueChanged(CCVars.GameMap, value =>
{
if (TryLookupMap(value, out var map))
_currentMap = map;
else
throw new ArgumentException($"Unknown map prototype {value} was selected!");
}, true);
_configurationManager.OnValueChanged(CCVars.GameMapForced, value => _currentMapForced = value, true);
}
public IEnumerable<GameMapPrototype> CurrentlyEligibleMaps()
{
var maps = AllVotableMaps().Where(IsMapEligible).ToArray();
return maps.Length == 0 ? AllMaps().Where(x => x.Fallback) : maps;
}
public IEnumerable<GameMapPrototype> AllVotableMaps()
{
return _prototypeManager.EnumeratePrototypes<GameMapPrototype>().Where(x => x.Votable);
}
public IEnumerable<GameMapPrototype> AllMaps()
{
return _prototypeManager.EnumeratePrototypes<GameMapPrototype>();
}
public bool TrySelectMap(string gameMap)
{
if (!TryLookupMap(gameMap, out var map) || !IsMapEligible(map)) return false;
_currentMap = map;
_currentMapForced = false;
return true;
}
public void ForceSelectMap(string gameMap)
{
if (!TryLookupMap(gameMap, out var map))
throw new ArgumentException($"The map \"{gameMap}\" is invalid!");
_currentMap = map;
_currentMapForced = true;
}
public void SelectRandomMap()
{
var maps = CurrentlyEligibleMaps().ToList();
_random.Shuffle(maps);
_currentMap = maps[0];
_currentMapForced = false;
}
public GameMapPrototype GetSelectedMap()
{
return _currentMap;
}
public GameMapPrototype GetSelectedMapChecked(bool loud = 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)
));
}
}
return GetSelectedMap();
}
public bool CheckMapExists(string gameMap)
{
return TryLookupMap(gameMap, out _);
}
private bool IsMapEligible(GameMapPrototype map)
{
return map.MaxPlayers >= _playerManager.PlayerCount && map.MinPlayers <= _playerManager.PlayerCount;
}
private bool TryLookupMap(string gameMap, [NotNullWhen(true)] out GameMapPrototype? map)
{
return _prototypeManager.TryIndex(gameMap, out map);
}
}
}

View File

@@ -0,0 +1,53 @@
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.ViewVariables;
namespace Content.Server.Maps
{
/// <summary>
/// Prototype data for a game map.
/// </summary>
[Prototype("gameMap")]
public class GameMapPrototype : IPrototype
{
/// <inheritdoc/>
[ViewVariables, DataField("id", required: true)]
public string ID { get; } = default!;
/// <summary>
/// Minimum players for the given map.
/// </summary>
[ViewVariables, DataField("minPlayers", required: true)]
public uint MinPlayers { get; }
/// <summary>
/// Maximum players for the given map.
/// </summary>
[ViewVariables, DataField("maxPlayers")]
public uint MaxPlayers { get; } = uint.MaxValue;
/// <summary>
/// Name of the given map.
/// </summary>
[ViewVariables, DataField("mapName", required: true)]
public string MapName { get; } = default!;
/// <summary>
/// Relative directory path to the given map, i.e. `Maps/saltern.yml`
/// </summary>
[ViewVariables, DataField("mapPath", required: true)]
public string MapPath { get; } = default!;
/// <summary>
/// Controls if the map can be used as a fallback if no maps are eligible.
/// </summary>
[ViewVariables, DataField("fallback")]
public bool Fallback { get; }
/// <summary>
/// Controls if the map can be voted for.
/// </summary>
[ViewVariables, DataField("votable")]
public bool Votable { get; } = true;
}
}

View File

@@ -0,0 +1,68 @@
using System.Collections.Generic;
namespace Content.Server.Maps
{
/// <summary>
/// Manages which station map will be used for the next round.
/// </summary>
public interface IGameMapManager
{
void Initialize();
/// <summary>
/// Returns all maps eligible to be played right now.
/// </summary>
/// <returns>enumerator of map prototypes</returns>
IEnumerable<GameMapPrototype> CurrentlyEligibleMaps();
/// <summary>
/// Returns all maps that can be voted for.
/// </summary>
/// <returns>enumerator of map prototypes</returns>
IEnumerable<GameMapPrototype> AllVotableMaps();
/// <summary>
/// Returns all maps.
/// </summary>
/// <returns>enumerator of map prototypes</returns>
IEnumerable<GameMapPrototype> AllMaps();
/// <summary>
/// Attempts to select the given map.
/// </summary>
/// <param name="gameMap">map prototype</param>
/// <returns>success or failure</returns>
bool TrySelectMap(string gameMap);
/// <summary>
/// Forces the given map, making sure the game map manager won't reselect if conditions are no longer met at round restart.
/// </summary>
/// <param name="gameMap">map prototype</param>
/// <returns>success or failure</returns>
void ForceSelectMap(string gameMap);
/// <summary>
/// Selects a random map.
/// </summary>
void SelectRandomMap();
/// <summary>
/// Gets the currently selected map, without double-checking if it can be used.
/// </summary>
/// <returns>selected map</returns>
GameMapPrototype GetSelectedMap();
/// <summary>
/// Gets the currently selected map, double-checking if it can be used.
/// </summary>
/// <returns>selected map</returns>
GameMapPrototype GetSelectedMapChecked(bool loud = false);
/// <summary>
/// Checks if the given map exists
/// </summary>
/// <param name="gameMap">name of the map</param>
/// <returns>existence</returns>
bool CheckMapExists(string gameMap);
}
}

View File

@@ -1,6 +1,8 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using Content.Server.GameTicking; using Content.Server.GameTicking;
using Content.Server.Maps;
using Content.Server.RoundEnd; using Content.Server.RoundEnd;
using Content.Shared.CCVar; using Content.Shared.CCVar;
using Content.Shared.Voting; using Content.Shared.Voting;
@@ -146,11 +148,7 @@ namespace Content.Server.Voting.Managers
private void CreateMapVote(IPlayerSession? initiator) private void CreateMapVote(IPlayerSession? initiator)
{ {
var maps = new Dictionary<string, string> var maps = _gameMapManager.CurrentlyEligibleMaps().ToDictionary(map => map, map => map.MapName);
{
["Maps/saltern.yml"] = "Saltern",
["Maps/packedstation.yml"] = "PackedStation",
};
var alone = _playerManager.PlayerCount == 1 && initiator != null; var alone = _playerManager.PlayerCount == 1 && initiator != null;
var options = new VoteOptions var options = new VoteOptions
@@ -175,21 +173,21 @@ namespace Content.Server.Voting.Managers
vote.OnFinished += (_, args) => vote.OnFinished += (_, args) =>
{ {
string picked; GameMapPrototype picked;
if (args.Winner == null) if (args.Winner == null)
{ {
picked = (string) _random.Pick(args.Winners); picked = (GameMapPrototype) _random.Pick(args.Winners);
_chatManager.DispatchServerAnnouncement( _chatManager.DispatchServerAnnouncement(
Loc.GetString("ui-vote-map-tie", ("picked", maps[picked]))); Loc.GetString("ui-vote-map-tie", ("picked", maps[picked])));
} }
else else
{ {
picked = (string) args.Winner; picked = (GameMapPrototype) args.Winner;
_chatManager.DispatchServerAnnouncement( _chatManager.DispatchServerAnnouncement(
Loc.GetString("ui-vote-map-win", ("winner", maps[picked]))); Loc.GetString("ui-vote-map-win", ("winner", maps[picked])));
} }
_cfg.SetCVar(CCVars.GameMap, picked); _gameMapManager.TrySelectMap(picked.ID);
}; };
} }

View File

@@ -8,6 +8,7 @@ using Content.Server.Administration;
using Content.Server.Administration.Managers; using Content.Server.Administration.Managers;
using Content.Server.Afk; using Content.Server.Afk;
using Content.Server.Chat.Managers; using Content.Server.Chat.Managers;
using Content.Server.Maps;
using Content.Shared.Administration; using Content.Shared.Administration;
using Content.Shared.Voting; using Content.Shared.Voting;
using Robust.Server.Player; using Robust.Server.Player;
@@ -16,6 +17,7 @@ using Robust.Shared.Enums;
using Robust.Shared.IoC; using Robust.Shared.IoC;
using Robust.Shared.Localization; using Robust.Shared.Localization;
using Robust.Shared.Network; using Robust.Shared.Network;
using Robust.Shared.Prototypes;
using Robust.Shared.Random; using Robust.Shared.Random;
using Robust.Shared.Timing; using Robust.Shared.Timing;
using Robust.Shared.Utility; using Robust.Shared.Utility;
@@ -33,6 +35,7 @@ namespace Content.Server.Voting.Managers
[Dependency] private readonly IAdminManager _adminMgr = default!; [Dependency] private readonly IAdminManager _adminMgr = default!;
[Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly IAfkManager _afkManager = default!; [Dependency] private readonly IAfkManager _afkManager = default!;
[Dependency] private readonly IGameMapManager _gameMapManager = default!;
private int _nextVoteId = 1; private int _nextVoteId = 1;

View File

@@ -75,7 +75,9 @@ namespace Content.Shared.CCVar
GameMaxCharacterSlots = CVarDef.Create("game.maxcharacterslots", 10, CVar.ARCHIVE | CVar.SERVERONLY); GameMaxCharacterSlots = CVarDef.Create("game.maxcharacterslots", 10, CVar.ARCHIVE | CVar.SERVERONLY);
public static readonly CVarDef<string> public static readonly CVarDef<string>
GameMap = CVarDef.Create("game.map", "Maps/saltern.yml", CVar.SERVERONLY); GameMap = CVarDef.Create("game.map", "saltern", CVar.SERVERONLY);
public static readonly CVarDef<bool>
GameMapForced = CVarDef.Create("game.mapforced", false, CVar.SERVERONLY);
/// <summary> /// <summary>
/// Whether a random position offset will be applied to the station on roundstart. /// Whether a random position offset will be applied to the station on roundstart.

View File

@@ -0,0 +1 @@
gamemap-could-not-use-map-error = Failed to load map {$oldMap} due to it no longer being eligible! Picking {$newMap} instead.

View File

@@ -0,0 +1,20 @@
- type: gameMap
id: saltern
mapName: Saltern
mapPath: Maps/saltern.yml
minPlayers: 0
maxPlayers: 20
fallback: true
- type: gameMap
id: packedstation
mapName: Packedstation
mapPath: Maps/packedstation.yml
minPlayers: 15
- type: gameMap
id: knightship
mapName: Knight Ship
mapPath: Maps/knightship.yml
minPlayers: 0
maxPlayers: 8

View File

@@ -0,0 +1,7 @@
- type: gameMap
id: empty
mapName: Empty
mapPath: Maps/Test/empty.yml
minPlayers: 0
maxPlayers: 0
votable: false