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:
@@ -89,6 +89,7 @@ namespace Content.Client.Entry
|
||||
prototypes.RegisterIgnore("objective");
|
||||
prototypes.RegisterIgnore("holiday");
|
||||
prototypes.RegisterIgnore("aiFaction");
|
||||
prototypes.RegisterIgnore("gameMap");
|
||||
prototypes.RegisterIgnore("behaviorSet");
|
||||
prototypes.RegisterIgnore("advertisementsPack");
|
||||
prototypes.RegisterIgnore("metabolizerType");
|
||||
|
||||
@@ -33,7 +33,10 @@ namespace Content.IntegrationTests
|
||||
(CCVars.HolidaysEnabled.Name, "false", false),
|
||||
|
||||
// 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)
|
||||
|
||||
@@ -24,7 +24,7 @@ namespace Content.IntegrationTests.Tests.Commands
|
||||
{
|
||||
CVarOverrides =
|
||||
{
|
||||
[CCVars.GameMap.Name] = "Maps/saltern.yml"
|
||||
[CCVars.GameMap.Name] = "saltern"
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ using Content.Server.EUI;
|
||||
using Content.Server.GameTicking;
|
||||
using Content.Server.Holiday.Interfaces;
|
||||
using Content.Server.IoC;
|
||||
using Content.Server.Maps;
|
||||
using Content.Server.NodeContainer.NodeGroups;
|
||||
using Content.Server.Preferences.Managers;
|
||||
using Content.Server.Sandbox;
|
||||
@@ -91,6 +92,7 @@ namespace Content.Server.Entry
|
||||
IoCManager.Resolve<IAfkManager>().Initialize();
|
||||
_euiManager.Initialize();
|
||||
|
||||
IoCManager.Resolve<IGameMapManager>().Initialize();
|
||||
IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<GameTicker>().PostInitialize();
|
||||
IoCManager.Resolve<IBqlQueryManager>().DoAutoRegistrations();
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Content.Server.Administration;
|
||||
using Content.Server.Maps;
|
||||
using Content.Shared.Administration;
|
||||
using Content.Shared.CCVar;
|
||||
using Robust.Shared.Configuration;
|
||||
@@ -24,10 +25,10 @@ namespace Content.Server.GameTicking.Commands
|
||||
return;
|
||||
}
|
||||
|
||||
var cfg = IoCManager.Resolve<IConfigurationManager>();
|
||||
var gameMap = IoCManager.Resolve<IGameMapManager>();
|
||||
var name = args[0];
|
||||
|
||||
cfg.SetCVar(CCVars.GameMap, name);
|
||||
gameMap.ForceSelectMap(name);
|
||||
shell.WriteLine(Loc.GetString("forcemap-command-success", ("map", name)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,9 +12,6 @@ namespace Content.Server.GameTicking
|
||||
[ViewVariables]
|
||||
public bool DummyTicker { get; private set; } = false;
|
||||
|
||||
[ViewVariables]
|
||||
public string ChosenMap { get; private set; } = string.Empty;
|
||||
|
||||
[ViewVariables]
|
||||
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.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.GameDisallowLateJoins,
|
||||
value => { DisallowLateJoin = value; UpdateLateJoinStatus(); UpdateJobsAvailable(); }, true);
|
||||
|
||||
@@ -58,7 +58,7 @@ namespace Content.Server.GameTicking
|
||||
{
|
||||
DefaultMap = _mapManager.CreateMap();
|
||||
var startTime = _gameTiming.RealTime;
|
||||
var map = ChosenMap;
|
||||
var map = _gameMapManager.GetSelectedMapChecked(true).MapPath;
|
||||
var grid = _mapLoader.LoadBlueprint(DefaultMap, map);
|
||||
|
||||
if (grid == null)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Content.Server.Chat.Managers;
|
||||
using Content.Server.Maps;
|
||||
using Content.Server.Preferences.Managers;
|
||||
using Content.Shared.Chat;
|
||||
using Content.Shared.GameTicking;
|
||||
@@ -85,5 +86,6 @@ namespace Content.Server.GameTicking
|
||||
[Dependency] private readonly IBaseServer _baseServer = default!;
|
||||
[Dependency] private readonly IWatchdogApi _watchdogApi = default!;
|
||||
[Dependency] private readonly IReflectionManager _reflectionManager = default!;
|
||||
[Dependency] private readonly IGameMapManager _gameMapManager = default!;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ using Content.Server.DeviceNetwork;
|
||||
using Content.Server.EUI;
|
||||
using Content.Server.Holiday;
|
||||
using Content.Server.Holiday.Interfaces;
|
||||
using Content.Server.Maps;
|
||||
using Content.Server.Module;
|
||||
using Content.Server.MoMMI;
|
||||
using Content.Server.NodeContainer.NodeGroups;
|
||||
@@ -52,6 +53,7 @@ namespace Content.Server.IoC
|
||||
IoCManager.Register<INpcBehaviorManager, NpcBehaviorManager>();
|
||||
IoCManager.Register<IPlayerLocator, PlayerLocator>();
|
||||
IoCManager.Register<IAfkManager, AfkManager>();
|
||||
IoCManager.Register<IGameMapManager, GameMapManager>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
120
Content.Server/Maps/GameMapManager.cs
Normal file
120
Content.Server/Maps/GameMapManager.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
53
Content.Server/Maps/GameMapPrototype.cs
Normal file
53
Content.Server/Maps/GameMapPrototype.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
68
Content.Server/Maps/IGameMapManager.cs
Normal file
68
Content.Server/Maps/IGameMapManager.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Content.Server.GameTicking;
|
||||
using Content.Server.Maps;
|
||||
using Content.Server.RoundEnd;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.Voting;
|
||||
@@ -146,11 +148,7 @@ namespace Content.Server.Voting.Managers
|
||||
|
||||
private void CreateMapVote(IPlayerSession? initiator)
|
||||
{
|
||||
var maps = new Dictionary<string, string>
|
||||
{
|
||||
["Maps/saltern.yml"] = "Saltern",
|
||||
["Maps/packedstation.yml"] = "PackedStation",
|
||||
};
|
||||
var maps = _gameMapManager.CurrentlyEligibleMaps().ToDictionary(map => map, map => map.MapName);
|
||||
|
||||
var alone = _playerManager.PlayerCount == 1 && initiator != null;
|
||||
var options = new VoteOptions
|
||||
@@ -175,21 +173,21 @@ namespace Content.Server.Voting.Managers
|
||||
|
||||
vote.OnFinished += (_, args) =>
|
||||
{
|
||||
string picked;
|
||||
GameMapPrototype picked;
|
||||
if (args.Winner == null)
|
||||
{
|
||||
picked = (string) _random.Pick(args.Winners);
|
||||
picked = (GameMapPrototype) _random.Pick(args.Winners);
|
||||
_chatManager.DispatchServerAnnouncement(
|
||||
Loc.GetString("ui-vote-map-tie", ("picked", maps[picked])));
|
||||
}
|
||||
else
|
||||
{
|
||||
picked = (string) args.Winner;
|
||||
picked = (GameMapPrototype) args.Winner;
|
||||
_chatManager.DispatchServerAnnouncement(
|
||||
Loc.GetString("ui-vote-map-win", ("winner", maps[picked])));
|
||||
}
|
||||
|
||||
_cfg.SetCVar(CCVars.GameMap, picked);
|
||||
_gameMapManager.TrySelectMap(picked.ID);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ using Content.Server.Administration;
|
||||
using Content.Server.Administration.Managers;
|
||||
using Content.Server.Afk;
|
||||
using Content.Server.Chat.Managers;
|
||||
using Content.Server.Maps;
|
||||
using Content.Shared.Administration;
|
||||
using Content.Shared.Voting;
|
||||
using Robust.Server.Player;
|
||||
@@ -16,6 +17,7 @@ using Robust.Shared.Enums;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Random;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
@@ -33,6 +35,7 @@ namespace Content.Server.Voting.Managers
|
||||
[Dependency] private readonly IAdminManager _adminMgr = default!;
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
[Dependency] private readonly IAfkManager _afkManager = default!;
|
||||
[Dependency] private readonly IGameMapManager _gameMapManager = default!;
|
||||
|
||||
private int _nextVoteId = 1;
|
||||
|
||||
|
||||
@@ -75,7 +75,9 @@ namespace Content.Shared.CCVar
|
||||
GameMaxCharacterSlots = CVarDef.Create("game.maxcharacterslots", 10, CVar.ARCHIVE | CVar.SERVERONLY);
|
||||
|
||||
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>
|
||||
/// Whether a random position offset will be applied to the station on roundstart.
|
||||
|
||||
1
Resources/Locale/en-US/maps/gamemap.ftl
Normal file
1
Resources/Locale/en-US/maps/gamemap.ftl
Normal 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.
|
||||
20
Resources/Prototypes/Maps/game.yml
Normal file
20
Resources/Prototypes/Maps/game.yml
Normal 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
|
||||
7
Resources/Prototypes/Maps/test.yml
Normal file
7
Resources/Prototypes/Maps/test.yml
Normal file
@@ -0,0 +1,7 @@
|
||||
- type: gameMap
|
||||
id: empty
|
||||
mapName: Empty
|
||||
mapPath: Maps/Test/empty.yml
|
||||
minPlayers: 0
|
||||
maxPlayers: 0
|
||||
votable: false
|
||||
Reference in New Issue
Block a user