Turns GameTicker into an EntitySystem. (#4197)

* GameTicker turned into an EntitySystem

* Turns ClientGameTicker into an EntitySystem, turn NetMessages into events

* Change event names to be more consistent with the rest.

* YAML linter uses the dummy gameticker CVar override.

* Fix game ticker initialization order

* Dummy ticker won't spawn players.

* Fix character creation test
This commit is contained in:
Vera Aguilera Puerto
2021-06-20 10:09:24 +02:00
committed by GitHub
parent 15fb554c28
commit d3a611164b
81 changed files with 1711 additions and 1990 deletions

View File

@@ -2,6 +2,7 @@
using Content.Server.Administration;
using Content.Shared.Administration;
using Robust.Shared.Console;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
namespace Content.Server.GameTicking.Commands
@@ -15,7 +16,7 @@ namespace Content.Server.GameTicking.Commands
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
var ticker = IoCManager.Resolve<IGameTicker>();
var ticker = EntitySystem.Get<GameTicker>();
if (ticker.RunLevel != GameRunLevel.PreRoundLobby)
{
shell.WriteLine("This can only be executed while the game is in the pre-round lobby.");

View File

@@ -2,6 +2,7 @@
using Content.Server.Administration;
using Content.Shared.Administration;
using Robust.Shared.Console;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
namespace Content.Server.GameTicking.Commands
@@ -15,7 +16,7 @@ namespace Content.Server.GameTicking.Commands
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
var ticker = IoCManager.Resolve<IGameTicker>();
var ticker = EntitySystem.Get<GameTicker>();
if (ticker.RunLevel != GameRunLevel.InRound)
{

View File

@@ -1,6 +1,7 @@
using Content.Server.Administration;
using Content.Shared.Administration;
using Robust.Shared.Console;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
namespace Content.Server.GameTicking.Commands
@@ -14,7 +15,7 @@ namespace Content.Server.GameTicking.Commands
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
var ticker = IoCManager.Resolve<IGameTicker>();
var ticker = EntitySystem.Get<GameTicker>();
if (ticker.RunLevel != GameRunLevel.PreRoundLobby)
{
shell.WriteLine("This can only be executed while the game is in the pre-round lobby.");

View File

@@ -6,6 +6,7 @@ using Content.Shared.Administration;
using Content.Shared.CCVar;
using Robust.Shared.Configuration;
using Robust.Shared.Console;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
namespace Content.Server.GameTicking.Commands
@@ -21,7 +22,7 @@ namespace Content.Server.GameTicking.Commands
Type? preset = null;
var presetName = string.Join(" ", args);
var ticker = IoCManager.Resolve<IGameTicker>();
var ticker = EntitySystem.Get<GameTicker>();
if (args.Length > 0)
{

View File

@@ -3,6 +3,7 @@ using Content.Server.Administration;
using Content.Shared.Roles;
using Robust.Server.Player;
using Robust.Shared.Console;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Prototypes;
@@ -30,7 +31,7 @@ namespace Content.Server.GameTicking.Commands
return;
}
var ticker = IoCManager.Resolve<IGameTicker>();
var ticker = EntitySystem.Get<GameTicker>();
if (ticker.RunLevel == GameRunLevel.PreRoundLobby)
{
shell.WriteLine("Round has not started.");

View File

@@ -2,6 +2,7 @@
using Content.Server.Administration;
using Content.Shared.Administration;
using Robust.Shared.Console;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
namespace Content.Server.GameTicking.Commands
@@ -15,7 +16,7 @@ namespace Content.Server.GameTicking.Commands
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
var ticker = IoCManager.Resolve<IGameTicker>();
var ticker = EntitySystem.Get<GameTicker>();
ticker.RestartRound();
}
}

View File

@@ -1,6 +1,7 @@
using Content.Server.Administration;
using Robust.Server.Player;
using Robust.Shared.Console;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
namespace Content.Server.GameTicking.Commands
@@ -20,7 +21,7 @@ namespace Content.Server.GameTicking.Commands
return;
}
var ticker = IoCManager.Resolve<IGameTicker>();
var ticker = EntitySystem.Get<GameTicker>();
ticker.MakeObserve(player);
}
}

View File

@@ -1,6 +1,7 @@
using Content.Server.Players;
using Robust.Server.Player;
using Robust.Shared.Console;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Network;
@@ -22,7 +23,7 @@ namespace Content.Server.GameTicking.Commands
}
var playerMgr = IoCManager.Resolve<IPlayerManager>();
var ticker = IoCManager.Resolve<IGameTicker>();
var ticker = EntitySystem.Get<GameTicker>();
NetUserId userId;
if (args.Length == 0)

View File

@@ -1,6 +1,7 @@
using Content.Server.Administration;
using Content.Shared.Administration;
using Robust.Shared.Console;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
namespace Content.Server.GameTicking.Commands
@@ -20,7 +21,7 @@ namespace Content.Server.GameTicking.Commands
return;
}
var ticker = IoCManager.Resolve<IGameTicker>();
var ticker = EntitySystem.Get<GameTicker>();
ticker.SetStartPreset(args[0]);
}

View File

@@ -2,6 +2,7 @@
using Content.Server.Administration;
using Content.Shared.Administration;
using Robust.Shared.Console;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
namespace Content.Server.GameTicking.Commands
@@ -15,7 +16,7 @@ namespace Content.Server.GameTicking.Commands
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
var ticker = IoCManager.Resolve<IGameTicker>();
var ticker = EntitySystem.Get<GameTicker>();
if (ticker.RunLevel != GameRunLevel.PreRoundLobby)
{

View File

@@ -1,6 +1,9 @@
using Content.Server.Administration;
using Content.Shared.Administration;
using Content.Shared.CCVar;
using Robust.Shared.Configuration;
using Robust.Shared.Console;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
namespace Content.Server.GameTicking.Commands
@@ -20,11 +23,11 @@ namespace Content.Server.GameTicking.Commands
return;
}
var ticker = IoCManager.Resolve<IGameTicker>();
var cfgMan = IoCManager.Resolve<IConfigurationManager>();
if (bool.TryParse(args[0], out var result))
{
ticker.ToggleDisallowLateJoin(bool.Parse(args[0]));
cfgMan.SetCVar(CCVars.GameDisallowLateJoins, bool.Parse(args[0]));
shell.WriteLine(result ? "Late joining has been disabled." : "Late joining has been enabled.");
}
else

View File

@@ -1,6 +1,7 @@
using Content.Server.Administration;
using Robust.Server.Player;
using Robust.Shared.Console;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
namespace Content.Server.GameTicking.Commands
@@ -20,7 +21,7 @@ namespace Content.Server.GameTicking.Commands
return;
}
var ticker = IoCManager.Resolve<IGameTicker>();
var ticker = EntitySystem.Get<GameTicker>();
ticker.ToggleReady(player, bool.Parse(args[0]));
}
}

View File

@@ -0,0 +1,34 @@
using System;
using Content.Shared.CCVar;
using Robust.Shared.ViewVariables;
namespace Content.Server.GameTicking
{
public partial class GameTicker
{
[ViewVariables]
public bool LobbyEnabled { get; private set; } = false;
[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;
[ViewVariables]
public bool DisallowLateJoin { get; private set; } = false;
private void InitializeCVars()
{
_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, invokeImmediately:true,
onValueChanged:value => { DisallowLateJoin = value; UpdateLateJoinStatus(); UpdateJobsAvailable(); });
}
}
}

View File

@@ -0,0 +1,98 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using Content.Server.GameTicking.Presets;
using Content.Shared.CCVar;
using Content.Shared.Preferences;
using Robust.Shared.Network;
using Robust.Shared.ViewVariables;
using Robust.Shared.IoC;
namespace Content.Server.GameTicking
{
public partial class GameTicker
{
public const float PresetFailedCooldownIncrease = 30f;
[ViewVariables] private Type? _presetType;
[ViewVariables]
public GamePreset? Preset
{
get => _preset ?? MakeGamePreset(new Dictionary<NetUserId, HumanoidCharacterProfile>());
set => _preset = value;
}
public ImmutableDictionary<string, Type> Presets { get; private set; } = default!;
private GamePreset? _preset;
private void InitializeGamePreset()
{
var presets = new Dictionary<string, Type>();
foreach (var type in _reflectionManager.FindTypesWithAttribute<GamePresetAttribute>())
{
var attribute = type.GetCustomAttribute<GamePresetAttribute>();
presets.Add(attribute!.Id.ToLowerInvariant(), type);
foreach (var alias in attribute.Aliases)
{
presets.Add(alias.ToLowerInvariant(), type);
}
}
Presets = presets.ToImmutableDictionary();
SetStartPreset(_configurationManager.GetCVar(CCVars.GameLobbyDefaultPreset));
}
public bool OnGhostAttempt(Mind.Mind mind, bool canReturnGlobal)
{
return Preset?.OnGhostAttempt(mind, canReturnGlobal) ?? false;
}
public bool TryGetPreset(string name, [NotNullWhen(true)] out Type? type)
{
name = name.ToLowerInvariant();
return Presets.TryGetValue(name, out type);
}
public void SetStartPreset(Type type, bool force = false)
{
// Do nothing if this game ticker is a dummy!
if (DummyTicker)
return;
if (!typeof(GamePreset).IsAssignableFrom(type)) throw new ArgumentException("type must inherit GamePreset");
_presetType = type;
UpdateInfoText();
if (force)
{
StartRound(true);
}
}
public void SetStartPreset(string name, bool force = false)
{
if (!TryGetPreset(name, out var type))
{
throw new NotSupportedException($"No preset found with name {name}");
}
SetStartPreset(type, force);
}
private GamePreset MakeGamePreset(Dictionary<NetUserId, HumanoidCharacterProfile> readyProfiles)
{
var preset = _dynamicTypeFactory.CreateInstance<GamePreset>(_presetType ?? typeof(PresetSandbox));
preset.ReadyProfiles = readyProfiles;
return preset;
}
}
}

View File

@@ -0,0 +1,74 @@
using System;
using System.Collections.Generic;
using Content.Server.GameTicking.Rules;
using Robust.Shared.ViewVariables;
namespace Content.Server.GameTicking
{
public partial class GameTicker
{
[ViewVariables] private readonly List<GameRule> _gameRules = new();
public IEnumerable<GameRule> ActiveGameRules => _gameRules;
public T AddGameRule<T>() where T : GameRule, new()
{
var instance = _dynamicTypeFactory.CreateInstance<T>();
_gameRules.Add(instance);
instance.Added();
RaiseLocalEvent(new GameRuleAddedEvent(instance));
return instance;
}
public bool HasGameRule(string? name)
{
if (name == null)
return false;
foreach (var rule in _gameRules)
{
if (rule.GetType().Name == name)
{
return true;
}
}
return false;
}
public bool HasGameRule(Type? type)
{
if (type == null || !typeof(GameRule).IsAssignableFrom(type))
return false;
foreach (var rule in _gameRules)
{
if (rule.GetType().IsAssignableFrom(type))
return true;
}
return false;
}
public void RemoveGameRule(GameRule rule)
{
if (_gameRules.Contains(rule)) return;
rule.Removed();
_gameRules.Remove(rule);
}
}
public class GameRuleAddedEvent
{
public GameRule Rule { get; }
public GameRuleAddedEvent(GameRule rule)
{
Rule = rule;
}
}
}

View File

@@ -3,11 +3,13 @@ using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Content.Shared.GameTicking;
using Content.Shared.Preferences;
using Content.Shared.Roles;
using Robust.Server.Player;
using Robust.Shared.Localization;
using Robust.Shared.Network;
using Robust.Shared.Player;
using Robust.Shared.Random;
using Robust.Shared.Utility;
using Robust.Shared.ViewVariables;
@@ -17,6 +19,9 @@ namespace Content.Server.GameTicking
// This code is responsible for the assigning & picking of jobs.
public partial class GameTicker
{
[ViewVariables]
private readonly List<ManifestEntry> _manifest = new();
[ViewVariables]
private readonly Dictionary<string, int> _spawnedPositions = new();
@@ -187,7 +192,7 @@ namespace Content.Server.GameTicking
}
[Conditional("DEBUG")]
private void JobControllerInit()
private void InitializeJobController()
{
// Verify that the overflow role exists and has the correct name.
var role = _prototypeManager.Index<JobPrototype>(OverflowJob);
@@ -202,10 +207,23 @@ namespace Content.Server.GameTicking
_spawnedPositions[jobId] = _spawnedPositions.GetValueOrDefault(jobId, 0) + 1;
}
private TickerJobsAvailableEvent GetJobsAvailable()
{
// If late join is disallowed, return no available jobs.
if (DisallowLateJoin)
return new TickerJobsAvailableEvent(Array.Empty<string>());
var jobs = GetAvailablePositions()
.Where(e => e.Value > 0)
.Select(e => e.Key)
.ToArray();
return new TickerJobsAvailableEvent(jobs);
}
private void UpdateJobsAvailable()
{
var lobbyPlayers = _playersInLobby.Keys.Select(p => p.ConnectedClient).ToList();
_netManager.ServerSendToMany(GetJobsAvailable(), lobbyPlayers);
RaiseNetworkEvent(GetJobsAvailable(), Filter.Empty().AddPlayers(_playersInLobby.Keys));
}
}
}

View File

@@ -0,0 +1,139 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Content.Shared.GameTicking;
using Robust.Server.Player;
using Robust.Shared.Localization;
using Robust.Shared.Network;
using Robust.Shared.Player;
using Robust.Shared.Players;
using Robust.Shared.ViewVariables;
namespace Content.Server.GameTicking
{
public partial class GameTicker
{
[ViewVariables]
private readonly Dictionary<IPlayerSession, LobbyPlayerStatus> _playersInLobby = new();
[ViewVariables]
private TimeSpan _roundStartTime;
[ViewVariables]
private TimeSpan _pauseTime;
[ViewVariables]
public bool Paused { get; set; }
[ViewVariables]
private bool _roundStartCountdownHasNotStartedYetDueToNoPlayers;
private void UpdateInfoText()
{
RaiseNetworkEvent(GetInfoMsg(), Filter.Empty().AddPlayers(_playersInLobby.Keys));
}
private string GetInfoText()
{
if (Preset == null)
{
return string.Empty;
}
var gmTitle = Preset.ModeTitle;
var desc = Preset.Description;
return Loc.GetString(@"Hi and welcome to [color=white]Space Station 14![/color]
The current game mode is: [color=white]{0}[/color].
[color=yellow]{1}[/color]", gmTitle, desc);
}
private TickerLobbyReadyEvent GetStatusSingle(ICommonSession player, LobbyPlayerStatus status)
{
return new (new Dictionary<NetUserId, LobbyPlayerStatus> { { player.UserId, status } });
}
private TickerLobbyReadyEvent GetPlayerStatus()
{
var players = new Dictionary<NetUserId, LobbyPlayerStatus>();
foreach (var player in _playersInLobby.Keys)
{
_playersInLobby.TryGetValue(player, out var status);
players.Add(player.UserId, status);
}
return new TickerLobbyReadyEvent(players);
}
private TickerLobbyStatusEvent GetStatusMsg(IPlayerSession session)
{
_playersInLobby.TryGetValue(session, out var status);
return new TickerLobbyStatusEvent(RunLevel != GameRunLevel.PreRoundLobby, LobbySong, status == LobbyPlayerStatus.Ready, _roundStartTime, Paused);
}
private void SendStatusToAll()
{
foreach (var player in _playersInLobby.Keys)
{
RaiseNetworkEvent(GetStatusMsg(player), player.ConnectedClient);
}
}
private TickerLobbyInfoEvent GetInfoMsg()
{
return new (GetInfoText());
}
private void UpdateLateJoinStatus()
{
RaiseNetworkEvent(new TickerLateJoinStatusEvent(DisallowLateJoin));
}
public bool PauseStart(bool pause = true)
{
if (Paused == pause)
{
return false;
}
Paused = pause;
if (pause)
{
_pauseTime = _gameTiming.CurTime;
}
else if (_pauseTime != default)
{
_roundStartTime += _gameTiming.CurTime - _pauseTime;
}
RaiseNetworkEvent(new TickerLobbyCountdownEvent(_roundStartTime, Paused));
_chatManager.DispatchServerAnnouncement(Paused
? "Round start has been paused."
: "Round start countdown is now resumed.");
return true;
}
public bool TogglePause()
{
PauseStart(!Paused);
return Paused;
}
public void ToggleReady(IPlayerSession player, bool ready)
{
if (!_playersInLobby.ContainsKey(player)) return;
if (!_prefsManager.HavePreferencesLoaded(player))
{
return;
}
var status = ready ? LobbyPlayerStatus.Ready : LobbyPlayerStatus.NotReady;
_playersInLobby[player] = ready ? LobbyPlayerStatus.Ready : LobbyPlayerStatus.NotReady;
RaiseNetworkEvent(GetStatusMsg(player), player.ConnectedClient);
RaiseNetworkEvent(GetStatusSingle(player, status));
}
}
}

View File

@@ -0,0 +1,78 @@
using Content.Shared.Audio;
using Robust.Shared.ContentPack;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Random;
using Robust.Shared.Utility;
using Robust.Shared.ViewVariables;
namespace Content.Server.GameTicking
{
public partial class GameTicker
{
[Dependency] private readonly IResourceManager _resourceManager = default!;
private const string LobbyMusicCollection = "LobbyMusic";
[ViewVariables]
private bool _lobbyMusicInitialized = false;
[ViewVariables]
private SoundCollectionPrototype _lobbyMusicCollection = default!;
[ViewVariables]
public string? LobbySong { get; private set; }
private void InitializeLobbyMusic()
{
DebugTools.Assert(!_lobbyMusicInitialized);
_lobbyMusicCollection = _prototypeManager.Index<SoundCollectionPrototype>(LobbyMusicCollection);
// Now that the collection is set, the lobby music has been initialized and we can choose a random song.
_lobbyMusicInitialized = true;
ChooseRandomLobbySong();
}
/// <summary>
/// Sets the current lobby song, or stops it if null.
/// </summary>
/// <param name="song">The lobby song to play, or null to stop any lobby songs.</param>
public void SetLobbySong(string? song)
{
DebugTools.Assert(_lobbyMusicInitialized);
if (song == null)
{
LobbySong = null;
return;
// TODO GAMETICKER send song stop event
}
if (!_resourceManager.ContentFileExists(song))
{
Logger.ErrorS("ticker", $"Tried to set lobby song to \"{song}\", which doesn't exist!");
return;
}
LobbySong = song;
// TODO GAMETICKER send song change event
}
/// <summary>
/// Plays a random song from the LobbyMusic sound collection.
/// </summary>
public void ChooseRandomLobbySong()
{
DebugTools.Assert(_lobbyMusicInitialized);
SetLobbySong(_robustRandom.Pick(_lobbyMusicCollection.PickFiles));
}
/// <summary>
/// Stops the current lobby song being played.
/// </summary>
public void StopLobbySong()
{
SetLobbySong(null);
}
}
}

View File

@@ -0,0 +1,144 @@
using Content.Server.Players;
using Content.Shared.GameTicking;
using Content.Shared.GameWindow;
using Content.Shared.Preferences;
using JetBrains.Annotations;
using Robust.Server.Player;
using Robust.Shared.Enums;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Content.Server.GameTicking
{
[UsedImplicitly]
public partial class GameTicker
{
[Dependency] private readonly IPlayerManager _playerManager = default!;
private void InitializePlayer()
{
_playerManager.PlayerStatusChanged += PlayerStatusChanged;
}
private void PlayerStatusChanged(object? sender, SessionStatusEventArgs args)
{
var session = args.Session;
switch (args.NewStatus)
{
case SessionStatus.Connecting:
// Cancel shutdown update timer in progress.
_updateShutdownCts?.Cancel();
break;
case SessionStatus.Connected:
{
// Always make sure the client has player data. Mind gets assigned on spawn.
if (session.Data.ContentDataUncast == null)
session.Data.ContentDataUncast = new PlayerData(session.UserId);
// Make the player actually join the game.
// timer time must be > tick length
Timer.Spawn(0, args.Session.JoinGame);
_chatManager.SendAdminAnnouncement(Loc.GetString("player-join-message", ("name", args.Session.Name)));
if (LobbyEnabled && _roundStartCountdownHasNotStartedYetDueToNoPlayers)
{
_roundStartCountdownHasNotStartedYetDueToNoPlayers = false;
_roundStartTime = _gameTiming.CurTime + LobbyDuration;
}
break;
}
case SessionStatus.InGame:
{
_prefsManager.OnClientConnected(session);
var data = session.ContentData();
DebugTools.AssertNotNull(data);
if (data!.Mind == null)
{
if (LobbyEnabled)
{
PlayerJoinLobby(session);
return;
}
SpawnWaitPrefs();
}
else
{
if (data.Mind.CurrentEntity == null)
{
SpawnWaitPrefs();
}
else
{
session.AttachToEntity(data.Mind.CurrentEntity);
PlayerJoinGame(session);
}
}
break;
}
case SessionStatus.Disconnected:
{
if (_playersInLobby.ContainsKey(session)) _playersInLobby.Remove(session);
_chatManager.SendAdminAnnouncement(Loc.GetString("player-leave-message", ("name", args.Session.Name)));
ServerEmptyUpdateRestartCheck();
_prefsManager.OnClientDisconnected(session);
break;
}
}
async void SpawnWaitPrefs()
{
await _prefsManager.WaitPreferencesLoaded(session);
SpawnPlayer(session);
}
}
private HumanoidCharacterProfile GetPlayerProfile(IPlayerSession p)
{
return (HumanoidCharacterProfile) _prefsManager.GetPreferences(p.UserId).SelectedCharacter;
}
private void PlayerJoinGame(IPlayerSession session)
{
_chatManager.DispatchServerMessage(session,
"Welcome to Space Station 14! If this is your first time checking out the game, be sure to check out the tutorial in the top left!");
if (_playersInLobby.ContainsKey(session))
_playersInLobby.Remove(session);
RaiseNetworkEvent(new TickerJoinGameEvent(), session.ConnectedClient);
}
private void PlayerJoinLobby(IPlayerSession session)
{
_playersInLobby[session] = LobbyPlayerStatus.NotReady;
var client = session.ConnectedClient;
RaiseNetworkEvent(new TickerJoinLobbyEvent(), client);
RaiseNetworkEvent(GetStatusMsg(session), client);
RaiseNetworkEvent(GetInfoMsg(), client);
RaiseNetworkEvent(GetPlayerStatus(), client);
RaiseNetworkEvent(GetJobsAvailable(), client);
}
private void ReqWindowAttentionAll()
{
RaiseNetworkEvent(new RequestWindowAttentionEvent());
}
}
}

View File

@@ -0,0 +1,363 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Content.Server.Players;
using Content.Shared.CCVar;
using Content.Shared.Coordinates;
using Content.Shared.GameTicking;
using Content.Shared.Preferences;
using Prometheus;
using Robust.Server.Player;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Log;
using Robust.Shared.Random;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
using Robust.Shared.ViewVariables;
namespace Content.Server.GameTicking
{
public partial class GameTicker
{
private static readonly Counter RoundNumberMetric = Metrics.CreateCounter(
"ss14_round_number",
"Round number.");
private static readonly Gauge RoundLengthMetric = Metrics.CreateGauge(
"ss14_round_length",
"Round length in seconds.");
[ViewVariables]
private TimeSpan _roundStartTimeSpan;
[ViewVariables]
private GameRunLevel _runLevel;
[ViewVariables]
public GameRunLevel RunLevel
{
get => _runLevel;
private set
{
if (_runLevel == value) return;
var old = _runLevel;
_runLevel = value;
RaiseLocalEvent(new GameRunLevelChangedEvent(old, value));
}
}
private void PreRoundSetup()
{
DefaultMap = _mapManager.CreateMap();
var startTime = _gameTiming.RealTime;
var map = ChosenMap;
var grid = _mapLoader.LoadBlueprint(DefaultMap, map);
if (grid == null)
{
throw new InvalidOperationException($"No grid found for map {map}");
}
DefaultGridId = grid.Index;
_spawnPoint = grid.ToCoordinates();
var timeSpan = _gameTiming.RealTime - startTime;
Logger.InfoS("ticker", $"Loaded map in {timeSpan.TotalMilliseconds:N2}ms.");
}
public void StartRound(bool force = false)
{
// If this game ticker is a dummy, do nothing!
if (DummyTicker)
return;
DebugTools.Assert(RunLevel == GameRunLevel.PreRoundLobby);
Logger.InfoS("ticker", "Starting round!");
SendServerMessage("The round is starting now...");
List<IPlayerSession> readyPlayers;
if (LobbyEnabled)
{
readyPlayers = _playersInLobby.Where(p => p.Value == LobbyPlayerStatus.Ready).Select(p => p.Key).ToList();
}
else
{
readyPlayers = _playersInLobby.Keys.ToList();
}
RunLevel = GameRunLevel.InRound;
RoundLengthMetric.Set(0);
// Get the profiles for each player for easier lookup.
var profiles = _prefsManager.GetSelectedProfilesForPlayers(
readyPlayers
.Select(p => p.UserId).ToList())
.ToDictionary(p => p.Key, p => (HumanoidCharacterProfile) p.Value);
foreach (var readyPlayer in readyPlayers)
{
if (!profiles.ContainsKey(readyPlayer.UserId))
{
profiles.Add(readyPlayer.UserId, HumanoidCharacterProfile.Random());
}
}
var assignedJobs = AssignJobs(readyPlayers, profiles);
// For players without jobs, give them the overflow job if they have that set...
foreach (var player in readyPlayers)
{
if (assignedJobs.ContainsKey(player))
{
continue;
}
var profile = profiles[player.UserId];
if (profile.PreferenceUnavailable == PreferenceUnavailableMode.SpawnAsOverflow)
{
assignedJobs.Add(player, OverflowJob);
}
}
// Spawn everybody in!
foreach (var (player, job) in assignedJobs)
{
SpawnPlayer(player, profiles[player.UserId], job, false);
}
// Time to start the preset.
Preset = MakeGamePreset(profiles);
DisallowLateJoin |= Preset.DisallowLateJoin;
if (!Preset.Start(assignedJobs.Keys.ToList(), force))
{
if (_configurationManager.GetCVar(CCVars.GameLobbyFallbackEnabled))
{
SetStartPreset(_configurationManager.GetCVar(CCVars.GameLobbyFallbackPreset));
var newPreset = MakeGamePreset(profiles);
_chatManager.DispatchServerAnnouncement(
$"Failed to start {Preset.ModeTitle} mode! Defaulting to {newPreset.ModeTitle}...");
if (!newPreset.Start(readyPlayers, force))
{
throw new ApplicationException("Fallback preset failed to start!");
}
DisallowLateJoin = false;
DisallowLateJoin |= newPreset.DisallowLateJoin;
Preset = newPreset;
}
else
{
SendServerMessage($"Failed to start {Preset.ModeTitle} mode! Restarting round...");
RestartRound();
DelayStart(TimeSpan.FromSeconds(PresetFailedCooldownIncrease));
return;
}
}
Preset.OnGameStarted();
_roundStartTimeSpan = IoCManager.Resolve<IGameTiming>().RealTime;
SendStatusToAll();
ReqWindowAttentionAll();
UpdateLateJoinStatus();
UpdateJobsAvailable();
}
public void EndRound(string text = "")
{
// If this game ticker is a dummy, do nothing!
if (DummyTicker)
return;
DebugTools.Assert(RunLevel == GameRunLevel.InRound);
Logger.InfoS("ticker", "Ending round!");
RunLevel = GameRunLevel.PostRound;
//Tell every client the round has ended.
var gamemodeTitle = Preset?.ModeTitle ?? string.Empty;
var roundEndText = text + $"\n{Preset?.GetRoundEndDescription() ?? string.Empty}";
//Get the timespan of the round.
var roundDuration = IoCManager.Resolve<IGameTiming>().RealTime.Subtract(_roundStartTimeSpan);
//Generate a list of basic player info to display in the end round summary.
var listOfPlayerInfo = new List<RoundEndMessageEvent.RoundEndPlayerInfo>();
foreach (var ply in _playerManager.GetAllPlayers().OrderBy(p => p.Name))
{
var mind = ply.ContentData()?.Mind;
if (mind != null)
{
_playersInLobby.TryGetValue(ply, out var status);
var antag = mind.AllRoles.Any(role => role.Antagonist);
var playerEndRoundInfo = new RoundEndMessageEvent.RoundEndPlayerInfo()
{
PlayerOOCName = ply.Name,
PlayerICName = mind.CurrentEntity?.Name,
Role = antag
? mind.AllRoles.First(role => role.Antagonist).Name
: mind.AllRoles.FirstOrDefault()?.Name ?? Loc.GetString("Unknown"),
Antag = antag,
Observer = status == LobbyPlayerStatus.Observer,
};
listOfPlayerInfo.Add(playerEndRoundInfo);
}
}
RaiseNetworkEvent(new RoundEndMessageEvent(gamemodeTitle, roundEndText, roundDuration, listOfPlayerInfo.Count, listOfPlayerInfo.ToArray()));
}
public void RestartRound()
{
// If this game ticker is a dummy, do nothing!
if (DummyTicker)
return;
if (_updateOnRoundEnd)
{
_baseServer.Shutdown(
Loc.GetString("Server is shutting down for update and will automatically restart."));
return;
}
Logger.InfoS("ticker", "Restarting round!");
SendServerMessage("Restarting round...");
RoundNumberMetric.Inc();
RunLevel = GameRunLevel.PreRoundLobby;
LobbySong = _robustRandom.Pick(_lobbyMusicCollection.PickFiles);
ResettingCleanup();
PreRoundSetup();
if (!LobbyEnabled)
{
StartRound();
}
else
{
Preset = null;
if (_playerManager.PlayerCount == 0)
_roundStartCountdownHasNotStartedYetDueToNoPlayers = true;
else
_roundStartTime = _gameTiming.CurTime + LobbyDuration;
SendStatusToAll();
ReqWindowAttentionAll();
}
}
/// <summary>
/// Cleanup that has to run to clear up anything from the previous round.
/// Stuff like wiping the previous map clean.
/// </summary>
private void ResettingCleanup()
{
// Move everybody currently in the server to lobby.
foreach (var player in _playerManager.GetAllPlayers())
{
PlayerJoinLobby(player);
}
// Delete the minds of everybody.
// TODO: Maybe move this into a separate manager?
foreach (var unCastData in _playerManager.GetAllPlayerData())
{
unCastData.ContentData()?.WipeMind();
}
// Delete all entities.
foreach (var entity in _entityManager.GetEntities().ToList())
{
// TODO: Maybe something less naive here?
// FIXME: Actually, definitely.
entity.Delete();
}
_mapManager.Restart();
// Clear up any game rules.
foreach (var rule in _gameRules)
{
rule.Removed();
}
_gameRules.Clear();
foreach (var system in _entitySystemManager.AllSystems)
{
if (system is IResettingEntitySystem resetting)
{
resetting.Reset();
}
}
_spawnedPositions.Clear();
_manifest.Clear();
DisallowLateJoin = false;
}
public bool DelayStart(TimeSpan time)
{
if (_runLevel != GameRunLevel.PreRoundLobby)
{
return false;
}
_roundStartTime += time;
RaiseNetworkEvent(new TickerLobbyCountdownEvent(_roundStartTime, Paused));
_chatManager.DispatchServerAnnouncement($"Round start has been delayed for {time.TotalSeconds} seconds.");
return true;
}
private void UpdateRoundFlow(float frameTime)
{
if (RunLevel == GameRunLevel.InRound)
{
RoundLengthMetric.Inc(frameTime);
}
if (RunLevel != GameRunLevel.PreRoundLobby ||
Paused ||
_roundStartTime > _gameTiming.CurTime ||
_roundStartCountdownHasNotStartedYetDueToNoPlayers)
{
return;
}
StartRound();
}
}
public enum GameRunLevel
{
PreRoundLobby = 0,
InRound = 1,
PostRound = 2
}
public class GameRunLevelChangedEvent
{
public GameRunLevel Old { get; }
public GameRunLevel New { get; }
public GameRunLevelChangedEvent(GameRunLevel old, GameRunLevel @new)
{
Old = old;
New = @new;
}
}
}

View File

@@ -0,0 +1,296 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using Content.Server.Access.Components;
using Content.Server.CharacterAppearance.Components;
using Content.Server.Ghost.Components;
using Content.Server.Hands.Components;
using Content.Server.Inventory.Components;
using Content.Server.Items;
using Content.Server.PDA;
using Content.Server.Players;
using Content.Server.Roles;
using Content.Server.Spawners.Components;
using Content.Server.Speech.Components;
using Content.Shared.GameTicking;
using Content.Shared.Inventory;
using Content.Shared.Preferences;
using Content.Shared.Roles;
using Robust.Server.Player;
using Robust.Shared.GameObjects;
using Robust.Shared.Localization;
using Robust.Shared.Map;
using Robust.Shared.Random;
using Robust.Shared.Utility;
using Robust.Shared.ViewVariables;
namespace Content.Server.GameTicking
{
public partial class GameTicker
{
private const string PlayerPrototypeName = "HumanMob_Content";
private const string ObserverPrototypeName = "MobObserver";
[ViewVariables(VVAccess.ReadWrite)]
private EntityCoordinates _spawnPoint;
// Mainly to avoid allocations.
private readonly List<EntityCoordinates> _possiblePositions = new();
private void SpawnPlayer(IPlayerSession player, string? jobId = null, bool lateJoin = true)
{
var character = GetPlayerProfile(player);
SpawnPlayer(player, character, jobId, lateJoin);
UpdateJobsAvailable();
}
private void SpawnPlayer(IPlayerSession player, HumanoidCharacterProfile character, string? jobId = null, bool lateJoin = true)
{
// Can't spawn players with a dummy ticker!
if (DummyTicker)
return;
if (lateJoin && DisallowLateJoin)
{
MakeObserve(player);
return;
}
PlayerJoinGame(player);
var data = player.ContentData();
DebugTools.AssertNotNull(data);
data!.WipeMind();
data.Mind = new Mind.Mind(player.UserId)
{
CharacterName = character.Name
};
// Pick best job best on prefs.
jobId ??= PickBestAvailableJob(character);
var jobPrototype = _prototypeManager.Index<JobPrototype>(jobId);
var job = new Job(data.Mind, jobPrototype);
data.Mind.AddRole(job);
if (lateJoin)
{
_chatManager.DispatchStationAnnouncement(Loc.GetString(
"latejoin-arrival-announcement",
("character", character.Name),
("job", CultureInfo.CurrentCulture.TextInfo.ToTitleCase(job.Name))
), Loc.GetString("latejoin-arrival-sender"));
}
var mob = SpawnPlayerMob(job, character, lateJoin);
data.Mind.TransferTo(mob);
if (player.UserId == new Guid("{e887eb93-f503-4b65-95b6-2f282c014192}"))
{
mob.AddComponent<OwOAccentComponent>();
}
AddManifestEntry(character.Name, jobId);
AddSpawnedPosition(jobId);
EquipIdCard(mob, character.Name, jobPrototype);
jobPrototype.Special?.AfterEquip(mob);
Preset?.OnSpawnPlayerCompleted(player, mob, lateJoin);
}
public void Respawn(IPlayerSession player)
{
player.ContentData()?.WipeMind();
if (LobbyEnabled)
PlayerJoinLobby(player);
else
SpawnPlayer(player);
}
public void MakeJoinGame(IPlayerSession player, string? jobId = null)
{
if (!_playersInLobby.ContainsKey(player)) return;
if (!_prefsManager.HavePreferencesLoaded(player))
{
return;
}
SpawnPlayer(player, jobId);
}
public void MakeObserve(IPlayerSession player)
{
// Can't spawn players with a dummy ticker!
if (DummyTicker)
return;
if (!_playersInLobby.ContainsKey(player)) return;
PlayerJoinGame(player);
var name = GetPlayerProfile(player).Name;
var data = player.ContentData();
DebugTools.AssertNotNull(data);
data!.WipeMind();
data.Mind = new Mind.Mind(player.UserId);
var mob = SpawnObserverMob();
mob.Name = name;
mob.GetComponent<GhostComponent>().CanReturnToBody = false;
data.Mind.TransferTo(mob);
_playersInLobby[player] = LobbyPlayerStatus.Observer;
RaiseNetworkEvent(GetStatusSingle(player, LobbyPlayerStatus.Observer));
}
#region Mob Spawning Helpers
private IEntity SpawnPlayerMob(Job job, HumanoidCharacterProfile? profile, bool lateJoin = true)
{
var coordinates = lateJoin ? GetLateJoinSpawnPoint() : GetJobSpawnPoint(job.Prototype.ID);
var entity = _entityManager.SpawnEntity(PlayerPrototypeName, coordinates);
if (job.StartingGear != null)
{
var startingGear = _prototypeManager.Index<StartingGearPrototype>(job.StartingGear);
EquipStartingGear(entity, startingGear, profile);
}
if (profile != null)
{
entity.GetComponent<HumanoidAppearanceComponent>().UpdateFromProfile(profile);
entity.Name = profile.Name;
}
return entity;
}
private IEntity SpawnObserverMob()
{
var coordinates = GetObserverSpawnPoint();
return _entityManager.SpawnEntity(ObserverPrototypeName, coordinates);
}
#endregion
#region Equip Helpers
public void EquipStartingGear(IEntity entity, StartingGearPrototype startingGear, HumanoidCharacterProfile? profile)
{
if (entity.TryGetComponent(out InventoryComponent? inventory))
{
foreach (var slot in EquipmentSlotDefines.AllSlots)
{
var equipmentStr = startingGear.GetGear(slot, profile);
if (equipmentStr != "")
{
var equipmentEntity = _entityManager.SpawnEntity(equipmentStr, entity.Transform.Coordinates);
inventory.Equip(slot, equipmentEntity.GetComponent<ItemComponent>());
}
}
}
if (entity.TryGetComponent(out HandsComponent? handsComponent))
{
var inhand = startingGear.Inhand;
foreach (var (hand, prototype) in inhand)
{
var inhandEntity = _entityManager.SpawnEntity(prototype, entity.Transform.Coordinates);
handsComponent.PutInHand(inhandEntity.GetComponent<ItemComponent>(), hand);
}
}
}
public void EquipIdCard(IEntity entity, string characterName, JobPrototype jobPrototype)
{
if (!entity.TryGetComponent(out InventoryComponent? inventory))
return;
if (!inventory.TryGetSlotItem(EquipmentSlotDefines.Slots.IDCARD, out ItemComponent? item))
{
return;
}
var itemEntity = item.Owner;
if (!itemEntity.TryGetComponent(out PDAComponent? pdaComponent) || pdaComponent.ContainedID == null)
return;
var card = pdaComponent.ContainedID;
card.FullName = characterName;
card.JobTitle = jobPrototype.Name;
var access = card.Owner.GetComponent<AccessComponent>();
var accessTags = access.Tags;
accessTags.UnionWith(jobPrototype.Access);
pdaComponent.SetPDAOwner(characterName);
}
#endregion
private void AddManifestEntry(string characterName, string jobId)
{
_manifest.Add(new ManifestEntry(characterName, jobId));
}
#region Spawn Points
public EntityCoordinates GetJobSpawnPoint(string jobId)
{
var location = _spawnPoint;
_possiblePositions.Clear();
foreach (var (point, transform) in ComponentManager.EntityQuery<SpawnPointComponent, ITransformComponent>())
{
if (point.SpawnType == SpawnPointType.Job && point.Job?.ID == jobId)
_possiblePositions.Add(transform.Coordinates);
}
if (_possiblePositions.Count != 0)
location = _robustRandom.Pick(_possiblePositions);
return location;
}
public EntityCoordinates GetLateJoinSpawnPoint()
{
var location = _spawnPoint;
_possiblePositions.Clear();
foreach (var (point, transform) in ComponentManager.EntityQuery<SpawnPointComponent, ITransformComponent>())
{
if (point.SpawnType == SpawnPointType.LateJoin) _possiblePositions.Add(transform.Coordinates);
}
if (_possiblePositions.Count != 0)
location = _robustRandom.Pick(_possiblePositions);
return location;
}
public EntityCoordinates GetObserverSpawnPoint()
{
var location = _spawnPoint;
_possiblePositions.Clear();
foreach (var (point, transform) in ComponentManager.EntityQuery<SpawnPointComponent, ITransformComponent>())
{
if (point.SpawnType == SpawnPointType.Observer)
_possiblePositions.Add(transform.Coordinates);
}
if (_possiblePositions.Count != 0)
location = _robustRandom.Pick(_possiblePositions);
return location;
}
#endregion
}
}

View File

@@ -0,0 +1,34 @@
using Newtonsoft.Json.Linq;
using Robust.Server.ServerStatus;
using Robust.Shared.IoC;
namespace Content.Server.GameTicking
{
public partial class GameTicker
{
/// <summary>
/// Used for thread safety, given <see cref="IStatusHost.OnStatusRequest"/> is called from another thread.
/// </summary>
private readonly object _statusShellLock = new();
private void InitializeStatusShell()
{
IoCManager.Resolve<IStatusHost>().OnStatusRequest += GetStatusResponse;
}
private void GetStatusResponse(JObject jObject)
{
// This method is raised from another thread, so this better be thread safe!
lock (_statusShellLock)
{
jObject["name"] = _baseServer.ServerName;
jObject["players"] = _playerManager.PlayerCount;
jObject["run_level"] = (int) _runLevel;
if (_runLevel >= GameRunLevel.InRound)
{
jObject["round_start_time"] = _roundStartTime.ToString("o");
}
}
}
}
}

View File

@@ -0,0 +1,63 @@
using System;
using System.Linq;
using System.Threading;
using Robust.Shared.Enums;
using Robust.Shared.Localization;
using Robust.Shared.ViewVariables;
using Timer = Robust.Shared.Timing.Timer;
namespace Content.Server.GameTicking
{
public partial class GameTicker
{
private static readonly TimeSpan UpdateRestartDelay = TimeSpan.FromSeconds(20);
[ViewVariables]
private bool _updateOnRoundEnd;
private CancellationTokenSource? _updateShutdownCts;
private void InitializeUpdates()
{
_watchdogApi.UpdateReceived += WatchdogApiOnUpdateReceived;
}
private void WatchdogApiOnUpdateReceived()
{
_chatManager.DispatchServerAnnouncement(Loc.GetString(
"Update has been received, server will automatically restart for update at the end of this round."));
_updateOnRoundEnd = true;
ServerEmptyUpdateRestartCheck();
}
/// <summary>
/// Checks whether there are still players on the server,
/// and if not starts a timer to automatically reboot the server if an update is available.
/// </summary>
private void ServerEmptyUpdateRestartCheck()
{
// Can't simple check the current connected player count since that doesn't update
// before PlayerStatusChanged gets fired.
// So in the disconnect handler we'd still see a single player otherwise.
var playersOnline = _playerManager.GetAllPlayers().Any(p => p.Status != SessionStatus.Disconnected);
if (playersOnline || !_updateOnRoundEnd)
{
// Still somebody online.
return;
}
if (_updateShutdownCts is {IsCancellationRequested: false})
{
// Do nothing because I guess we already have a timer running..?
return;
}
_updateShutdownCts = new CancellationTokenSource();
Timer.Spawn(UpdateRestartDelay, () =>
{
_baseServer.Shutdown(
Loc.GetString("Server is shutting down for update and will automatically restart."));
}, _updateShutdownCts.Token);
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,40 +0,0 @@
using Content.Server.Players;
using Content.Shared.GameTicking;
using Robust.Server.Player;
using Robust.Shared.Enums;
using Robust.Shared.IoC;
using Robust.Shared.Timing;
#nullable enable
namespace Content.Server.GameTicking
{
/// <summary>
/// Handles some low-level GameTicker behavior such as setting up clients when they connect.
/// Does not contain lobby/round handling mechanisms.
/// </summary>
public abstract class GameTickerBase : SharedGameTicker
{
[Dependency] protected readonly IPlayerManager PlayerManager = default!;
public virtual void Initialize()
{
PlayerManager.PlayerStatusChanged += PlayerStatusChanged;
}
protected virtual void PlayerStatusChanged(object? sender, SessionStatusEventArgs args)
{
var session = args.Session;
if (args.NewStatus == SessionStatus.Connected)
{
// Always make sure the client has player data. Mind gets assigned on spawn.
if (session.Data.ContentDataUncast == null)
session.Data.ContentDataUncast = new PlayerData(session.UserId);
// timer time must be > tick length
Timer.Spawn(0, args.Session.JoinGame);
}
}
}
}

View File

@@ -1,77 +0,0 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Content.Server.GameTicking.Rules;
using Content.Shared.Preferences;
using Content.Shared.Roles;
using Robust.Server.Player;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Timing;
namespace Content.Server.GameTicking
{
/// <summary>
/// The game ticker is responsible for managing the round-by-round system of the game.
/// </summary>
public interface IGameTicker
{
GameRunLevel RunLevel { get; }
/// <summary>
/// The map loaded by the GameTicker on round start.
/// </summary>
MapId DefaultMap { get; }
/// <summary>
/// The GridId loaded by the GameTicker on round start.
/// </summary>
GridId DefaultGridId { get; }
event Action<GameRunLevelChangedEventArgs> OnRunLevelChanged;
event Action<GameRuleAddedEventArgs> OnRuleAdded;
void Initialize();
void Update(FrameEventArgs frameEventArgs);
void RestartRound();
void StartRound(bool force = false);
void EndRound(string roundEndText = "");
void Respawn(IPlayerSession targetPlayer);
void MakeObserve(IPlayerSession player);
void MakeJoinGame(IPlayerSession player, string? jobId = null);
void ToggleReady(IPlayerSession player, bool ready);
void ToggleDisallowLateJoin(bool disallowLateJoin);
/// <summary>proxy to GamePreset (actual handler)</summary>
bool OnGhostAttempt(Mind.Mind mind, bool canReturnGlobal);
EntityCoordinates GetLateJoinSpawnPoint();
EntityCoordinates GetJobSpawnPoint(string jobId);
EntityCoordinates GetObserverSpawnPoint();
void EquipStartingGear(IEntity entity, StartingGearPrototype startingGear, HumanoidCharacterProfile? profile);
// GameRule system.
T AddGameRule<T>() where T : GameRule, new();
bool HasGameRule(string? type);
bool HasGameRule(Type? type);
void RemoveGameRule(GameRule rule);
IEnumerable<GameRule> ActiveGameRules { get; }
bool TryGetPreset(string name, [NotNullWhen(true)] out Type? type);
void SetStartPreset(Type type, bool force = false);
void SetStartPreset(string name, bool force = false);
/// <returns>true if changed, false otherwise</returns>
bool PauseStart(bool pause = true);
/// <returns>true if paused, false otherwise</returns>
bool TogglePause();
bool DelayStart(TimeSpan time);
Dictionary<string, int> GetAvailablePositions();
}
}

View File

@@ -45,7 +45,7 @@ namespace Content.Server.GameTicking.Presets
mind.UnVisit();
}
var position = playerEntity?.Transform.Coordinates ?? IoCManager.Resolve<IGameTicker>().GetObserverSpawnPoint();
var position = playerEntity?.Transform.Coordinates ?? EntitySystem.Get<GameTicker>().GetObserverSpawnPoint();
// Ok, so, this is the master place for the logic for if ghosting is "too cheaty" to allow returning.
// There's no reason at this time to move it to any other place, especially given that the 'side effects required' situations would also have to be moved.
// + If CharacterDeadPhysically applies, we're physically dead. Therefore, ghosting OK, and we can return (this is critical for gibbing)

View File

@@ -1,6 +1,7 @@
using System.Collections.Generic;
using Content.Server.GameTicking.Rules;
using Robust.Server.Player;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
namespace Content.Server.GameTicking.Presets
@@ -8,11 +9,9 @@ namespace Content.Server.GameTicking.Presets
[GamePreset("deathmatch")]
public sealed class PresetDeathMatch : GamePreset
{
[Dependency] private readonly IGameTicker _gameTicker = default!;
public override bool Start(IReadOnlyList<IPlayerSession> readyPlayers, bool force = false)
{
_gameTicker.AddGameRule<RuleDeathMatch>();
EntitySystem.Get<GameTicker>().AddGameRule<RuleDeathMatch>();
return true;
}

View File

@@ -28,7 +28,6 @@ namespace Content.Server.GameTicking.Presets
public class PresetSuspicion : GamePreset
{
[Dependency] private readonly IChatManager _chatManager = default!;
[Dependency] private readonly IGameTicker _gameTicker = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
@@ -149,7 +148,7 @@ namespace Content.Server.GameTicking.Presets
traitor.GreetSuspicion(traitors, _chatManager);
}
_gameTicker.AddGameRule<RuleSuspicion>();
EntitySystem.Get<GameTicker>().AddGameRule<RuleSuspicion>();
return true;
}

View File

@@ -16,6 +16,7 @@ using Content.Shared.Inventory;
using Content.Shared.PDA;
using Robust.Server.Player;
using Robust.Shared.Configuration;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Log;
@@ -29,7 +30,6 @@ namespace Content.Server.GameTicking.Presets
[GamePreset("traitor")]
public class PresetTraitor : GamePreset
{
[Dependency] private readonly IGameTicker _gameTicker = default!;
[Dependency] private readonly IChatManager _chatManager = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
@@ -159,7 +159,7 @@ namespace Content.Server.GameTicking.Presets
traitor.GreetTraitor(codewords);
}
_gameTicker.AddGameRule<RuleTraitor>();
EntitySystem.Get<GameTicker>().AddGameRule<RuleTraitor>();
return true;
}

View File

@@ -34,7 +34,6 @@ namespace Content.Server.GameTicking.Presets
{
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IGameTicker _gameTicker = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IChatManager _chatManager = default!;
[Dependency] private readonly IRobustRandom _robustRandom = default!;
@@ -50,8 +49,9 @@ namespace Content.Server.GameTicking.Presets
public override bool Start(IReadOnlyList<IPlayerSession> readyPlayers, bool force = false)
{
_gameTicker.AddGameRule<RuleTraitorDeathMatch>();
_restarter = _gameTicker.AddGameRule<RuleMaxTimeRestart>();
var gameTicker = EntitySystem.Get<GameTicker>();
gameTicker.AddGameRule<RuleTraitorDeathMatch>();
_restarter = gameTicker.AddGameRule<RuleMaxTimeRestart>();
_restarter.RoundMaxTime = TimeSpan.FromMinutes(30);
_restarter.RestartTimer();
_safeToEndRound = true;
@@ -207,7 +207,7 @@ namespace Content.Server.GameTicking.Presets
var session = mind.Session;
if (session == null)
return false;
_gameTicker.Respawn(session);
EntitySystem.Get<GameTicker>().Respawn(session);
return true;
}

View File

@@ -1,9 +1,10 @@
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
namespace Content.Server.GameTicking.Rules
{
[PublicAPI]
public abstract class GameRule
public abstract class GameRule : IEntityEventSubscriber
{
public virtual void Added()
{

View File

@@ -26,7 +26,6 @@ namespace Content.Server.GameTicking.Rules
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IChatManager _chatManager = default!;
[Dependency] private readonly IGameTicker _gameTicker = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
private CancellationTokenSource? _checkTimerCancel;
@@ -91,7 +90,7 @@ namespace Content.Server.GameTicking.Rules
_chatManager.DispatchServerAnnouncement(Loc.GetString("Restarting in {0} seconds.", restartDelay));
Timer.Spawn(TimeSpan.FromSeconds(restartDelay), () => _gameTicker.RestartRound());
Timer.Spawn(TimeSpan.FromSeconds(restartDelay), () => EntitySystem.Get<GameTicker>().RestartRound());
}
private void PlayerManagerOnPlayerStatusChanged(object? sender, SessionStatusEventArgs e)

View File

@@ -3,6 +3,7 @@ using System;
using System.Threading;
using Content.Server.Chat.Managers;
using Robust.Server.Player;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Timer = Robust.Shared.Timing.Timer;
@@ -11,9 +12,9 @@ namespace Content.Server.GameTicking.Rules
{
public class RuleInactivityTimeRestart : GameRule
{
[Dependency] private readonly IGameTicker _gameTicker = default!;
[Dependency] private readonly IChatManager _chatManager = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IEntityManager _entityManager = default!;
private CancellationTokenSource _timerCancel = new();
@@ -24,7 +25,7 @@ namespace Content.Server.GameTicking.Rules
{
base.Added();
_gameTicker.OnRunLevelChanged += RunLevelChanged;
_entityManager.EventBus.SubscribeEvent<GameRunLevelChangedEvent>(EventSource.Local, this, RunLevelChanged);
_playerManager.PlayerStatusChanged += PlayerStatusChanged;
}
@@ -32,7 +33,7 @@ namespace Content.Server.GameTicking.Rules
{
base.Removed();
_gameTicker.OnRunLevelChanged -= RunLevelChanged;
_entityManager.EventBus.UnsubscribeEvents(this);
_playerManager.PlayerStatusChanged -= PlayerStatusChanged;
StopTimer();
@@ -52,16 +53,17 @@ namespace Content.Server.GameTicking.Rules
private void TimerFired()
{
_gameTicker.EndRound(Loc.GetString("Time has run out!"));
var gameticker = EntitySystem.Get<GameTicker>();
gameticker.EndRound(Loc.GetString("Time has run out!"));
_chatManager.DispatchServerAnnouncement(Loc.GetString("Restarting in {0} seconds.", (int) RoundEndDelay.TotalSeconds));
Timer.Spawn(RoundEndDelay, () => _gameTicker.RestartRound());
Timer.Spawn(RoundEndDelay, () => gameticker.RestartRound());
}
private void RunLevelChanged(GameRunLevelChangedEventArgs args)
private void RunLevelChanged(GameRunLevelChangedEvent args)
{
switch (args.NewRunLevel)
switch (args.New)
{
case GameRunLevel.InRound:
RestartTimer();
@@ -75,7 +77,7 @@ namespace Content.Server.GameTicking.Rules
private void PlayerStatusChanged(object? sender, SessionStatusEventArgs e)
{
if (_gameTicker.RunLevel != GameRunLevel.InRound)
if (EntitySystem.Get<GameTicker>().RunLevel != GameRunLevel.InRound)
{
return;
}

View File

@@ -1,6 +1,7 @@
using System;
using System.Threading;
using Content.Server.Chat.Managers;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Timer = Robust.Shared.Timing.Timer;
@@ -9,8 +10,8 @@ namespace Content.Server.GameTicking.Rules
{
public sealed class RuleMaxTimeRestart : GameRule
{
[Dependency] private readonly IGameTicker _gameTicker = default!;
[Dependency] private readonly IChatManager _chatManager = default!;
[Dependency] private readonly IEntityManager _entityManager = default!;
private CancellationTokenSource _timerCancel = new();
@@ -21,14 +22,14 @@ namespace Content.Server.GameTicking.Rules
{
base.Added();
_gameTicker.OnRunLevelChanged += RunLevelChanged;
_entityManager.EventBus.SubscribeEvent<GameRunLevelChangedEvent>(EventSource.Local, this, RunLevelChanged);
}
public override void Removed()
{
base.Removed();
_gameTicker.OnRunLevelChanged -= RunLevelChanged;
_entityManager.EventBus.UnsubscribeEvents(this);
StopTimer();
}
@@ -46,16 +47,16 @@ namespace Content.Server.GameTicking.Rules
private void TimerFired()
{
_gameTicker.EndRound(Loc.GetString("Time has run out!"));
EntitySystem.Get<GameTicker>().EndRound(Loc.GetString("Time has run out!"));
_chatManager.DispatchServerAnnouncement(Loc.GetString("Restarting in {0} seconds.", (int) RoundEndDelay.TotalSeconds));
Timer.Spawn(RoundEndDelay, () => _gameTicker.RestartRound());
Timer.Spawn(RoundEndDelay, () => EntitySystem.Get<GameTicker>().RestartRound());
}
private void RunLevelChanged(GameRunLevelChangedEventArgs args)
private void RunLevelChanged(GameRunLevelChangedEvent args)
{
switch (args.NewRunLevel)
switch (args.New)
{
case GameRunLevel.InRound:
RestartTimer();

View File

@@ -30,7 +30,6 @@ namespace Content.Server.GameTicking.Rules
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IChatManager _chatManager = default!;
[Dependency] private readonly IGameTicker _gameTicker = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly IGameTiming _timing = default!;
@@ -152,12 +151,13 @@ namespace Content.Server.GameTicking.Rules
break;
}
_gameTicker.EndRound(text);
var gameTicker = EntitySystem.Get<GameTicker>();
gameTicker.EndRound(text);
_chatManager.DispatchServerAnnouncement(Loc.GetString("Restarting in {0} seconds.", (int) RoundEndDelay.TotalSeconds));
_checkTimerCancel.Cancel();
Timer.Spawn(RoundEndDelay, () => _gameTicker.RestartRound());
Timer.Spawn(RoundEndDelay, () => gameTicker.RestartRound());
}
}
}