From d3a611164b2df373947083233937a729015ff23f Mon Sep 17 00:00:00 2001 From: Vera Aguilera Puerto <6766154+Zumorica@users.noreply.github.com> Date: Sun, 20 Jun 2021 10:09:24 +0200 Subject: [PATCH] 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 --- Content.Client/Audio/BackgroundAudioSystem.cs | 7 +- Content.Client/Entry/EntryPoint.cs | 1 - .../GameTicking/Managers/ClientGameTicker.cs | 52 +- .../GameTicking/Managers/IClientGameTicker.cs | 27 - Content.Client/IoC/ClientContentIoC.cs | 1 - Content.Client/LateJoin/LateJoinGui.cs | 10 +- Content.Client/Lobby/LobbyState.cs | 64 +- .../RoundEnd/RoundEndSummaryWindow.cs | 3 +- .../ContentIntegrationTest.cs | 17 +- Content.IntegrationTests/DummyGameTicker.cs | 140 -- .../Tests/Commands/RestartRoundTest.cs | 2 +- .../Tests/GameRules/RuleMaxTimeRestartTest.cs | 3 +- .../Tests/Lobby/CharacterCreationTest.cs | 7 +- .../Tests/ResettingEntitySystemTests.cs | 2 +- .../Tests/RestartRoundTest.cs | 3 +- .../AI/Components/AiControllerComponent.cs | 2 +- .../Administration/Commands/AGhost.cs | 2 +- .../Administration/Commands/ReadyAll.cs | 3 +- Content.Server/Body/BodyComponent.cs | 3 +- .../Chat/Commands/SuicideCommand.cs | 2 +- Content.Server/Entry/EntryPoint.cs | 16 +- .../GameTicking/Commands/DelayStartCommand.cs | 3 +- .../GameTicking/Commands/EndRoundCommand.cs | 3 +- .../Commands/ForcePresetCommand.cs | 3 +- .../GameTicking/Commands/GoLobbyCommand.cs | 3 +- .../GameTicking/Commands/JoinGameCommand.cs | 3 +- .../GameTicking/Commands/NewRoundCommand.cs | 3 +- .../GameTicking/Commands/ObserveCommand.cs | 3 +- .../GameTicking/Commands/RespawnCommand.cs | 3 +- .../Commands/SetGamePresetCommand.cs | 3 +- .../GameTicking/Commands/StartRoundCommand.cs | 3 +- .../Commands/ToggleDisallowLateJoinCommand.cs | 7 +- .../Commands/ToggleReadyCommand.cs | 3 +- .../GameTicking/GameTicker.CVars.cs | 34 + .../GameTicking/GameTicker.GamePreset.cs | 98 ++ .../GameTicking/GameTicker.GameRule.cs | 74 ++ .../GameTicking/GameTicker.JobController.cs | 24 +- .../GameTicking/GameTicker.Lobby.cs | 139 ++ .../GameTicking/GameTicker.LobbyMusic.cs | 78 ++ .../GameTicking/GameTicker.Player.cs | 144 +++ .../GameTicking/GameTicker.RoundFlow.cs | 363 ++++++ .../GameTicking/GameTicker.Spawning.cs | 296 +++++ .../GameTicking/GameTicker.StatusShell.cs | 34 + .../GameTicking/GameTicker.Updates.cs | 63 + Content.Server/GameTicking/GameTicker.cs | 1123 +---------------- Content.Server/GameTicking/GameTickerBase.cs | 40 - Content.Server/GameTicking/IGameTicker.cs | 77 -- .../GameTicking/Presets/GamePreset.cs | 2 +- .../GameTicking/Presets/PresetDeathMatch.cs | 5 +- .../GameTicking/Presets/PresetSuspicion.cs | 3 +- .../GameTicking/Presets/PresetTraitor.cs | 4 +- .../Presets/PresetTraitorDeathMatch.cs | 8 +- Content.Server/GameTicking/Rules/GameRule.cs | 3 +- .../GameTicking/Rules/RuleDeathMatch.cs | 3 +- .../Rules/RuleInactivityTimeRestart.cs | 18 +- .../GameTicking/Rules/RuleMaxTimeRestart.cs | 15 +- .../GameTicking/Rules/RuleSuspicion.cs | 6 +- .../Ghost/Components/GhostOnMoveComponent.cs | 3 +- Content.Server/Ghost/Ghost.cs | 3 +- Content.Server/Holiday/HolidayManager.cs | 11 +- Content.Server/IoC/ServerContentIoC.cs | 1 - .../Mind/Components/MindComponent.cs | 4 +- Content.Server/Mind/MindHelpers.cs | 2 +- .../CrematoriumEntityStorageComponent.cs | 2 +- Content.Server/Players/PlayerSessionExt.cs | 14 - .../Recycling/Components/RecyclerComponent.cs | 2 +- Content.Server/RoundEnd/RoundEndSystem.cs | 6 +- Content.Server/Sandbox/SandboxManager.cs | 11 +- Content.Server/Shell/StatusShell.cs | 59 - .../Components/ConditionalSpawnerComponent.cs | 9 +- .../EntitySystems/ConditionalSpawnerSystem.cs | 26 + .../StationEvents/Events/GasLeak.cs | 2 +- .../StationEvents/Events/RadiationStorm.cs | 3 +- .../StationEvents/StationEventSystem.cs | 3 +- .../Managers/VoteManager.DefaultVotes.cs | 6 +- Content.Server/Voting/Managers/VoteManager.cs | 1 - Content.Shared/CCVar/CCVars.cs | 6 + .../GameTicking/SharedGameTicker.cs | 425 ++----- .../GameWindow/MsgRequestWindowAttention.cs | 27 - .../GameWindow/RequestWindowAttentionEvent.cs | 12 + Content.YAMLLinter/Program.cs | 2 + 81 files changed, 1711 insertions(+), 1990 deletions(-) delete mode 100644 Content.Client/GameTicking/Managers/IClientGameTicker.cs delete mode 100644 Content.IntegrationTests/DummyGameTicker.cs create mode 100644 Content.Server/GameTicking/GameTicker.CVars.cs create mode 100644 Content.Server/GameTicking/GameTicker.GamePreset.cs create mode 100644 Content.Server/GameTicking/GameTicker.GameRule.cs create mode 100644 Content.Server/GameTicking/GameTicker.Lobby.cs create mode 100644 Content.Server/GameTicking/GameTicker.LobbyMusic.cs create mode 100644 Content.Server/GameTicking/GameTicker.Player.cs create mode 100644 Content.Server/GameTicking/GameTicker.RoundFlow.cs create mode 100644 Content.Server/GameTicking/GameTicker.Spawning.cs create mode 100644 Content.Server/GameTicking/GameTicker.StatusShell.cs create mode 100644 Content.Server/GameTicking/GameTicker.Updates.cs delete mode 100644 Content.Server/GameTicking/GameTickerBase.cs delete mode 100644 Content.Server/GameTicking/IGameTicker.cs delete mode 100644 Content.Server/Players/PlayerSessionExt.cs delete mode 100644 Content.Server/Shell/StatusShell.cs create mode 100644 Content.Server/Spawners/EntitySystems/ConditionalSpawnerSystem.cs delete mode 100644 Content.Shared/GameWindow/MsgRequestWindowAttention.cs create mode 100644 Content.Shared/GameWindow/RequestWindowAttentionEvent.cs diff --git a/Content.Client/Audio/BackgroundAudioSystem.cs b/Content.Client/Audio/BackgroundAudioSystem.cs index a35bc858ab..9aa00712cd 100644 --- a/Content.Client/Audio/BackgroundAudioSystem.cs +++ b/Content.Client/Audio/BackgroundAudioSystem.cs @@ -26,7 +26,6 @@ namespace Content.Client.Audio [Dependency] private readonly IConfigurationManager _configManager = default!; [Dependency] private readonly IStateManager _stateManager = default!; [Dependency] private readonly IBaseClient _client = default!; - [Dependency] private readonly IClientGameTicker _clientGameTicker = default!; private SoundCollectionPrototype _ambientCollection = default!; @@ -50,7 +49,7 @@ namespace Content.Client.Audio _client.PlayerJoinedServer += OnJoin; _client.PlayerLeaveServer += OnLeave; - _clientGameTicker.LobbyStatusUpdated += LobbySongReceived; + Get().LobbyStatusUpdated += LobbySongReceived; } public override void Shutdown() @@ -62,7 +61,7 @@ namespace Content.Client.Audio _client.PlayerJoinedServer -= OnJoin; _client.PlayerLeaveServer -= OnLeave; - _clientGameTicker.LobbyStatusUpdated -= LobbySongReceived; + Get().LobbyStatusUpdated -= LobbySongReceived; EndAmbience(); EndLobbyMusic(); @@ -167,7 +166,7 @@ namespace Content.Client.Audio private void StartLobbyMusic() { EndLobbyMusic(); - var file = _clientGameTicker.LobbySong; + var file = Get().LobbySong; if (file == null) // We have not received the lobby song yet. { return; diff --git a/Content.Client/Entry/EntryPoint.cs b/Content.Client/Entry/EntryPoint.cs index 32528ca286..2bc5e3f265 100644 --- a/Content.Client/Entry/EntryPoint.cs +++ b/Content.Client/Entry/EntryPoint.cs @@ -167,7 +167,6 @@ namespace Content.Client.Entry IoCManager.Resolve().Initialize(); IoCManager.Resolve().Initialize(); - IoCManager.Resolve().Initialize(); var overlayMgr = IoCManager.Resolve(); overlayMgr.AddOverlay(new ParallaxOverlay()); diff --git a/Content.Client/GameTicking/Managers/ClientGameTicker.cs b/Content.Client/GameTicking/Managers/ClientGameTicker.cs index 1881b6ed1d..013625feff 100644 --- a/Content.Client/GameTicking/Managers/ClientGameTicker.cs +++ b/Content.Client/GameTicking/Managers/ClientGameTicker.cs @@ -5,6 +5,7 @@ using Content.Client.RoundEnd; using Content.Client.Viewport; using Content.Shared.GameTicking; using Content.Shared.GameWindow; +using JetBrains.Annotations; using Robust.Client.Graphics; using Robust.Client.State; using Robust.Shared.IoC; @@ -14,10 +15,11 @@ using Robust.Shared.ViewVariables; namespace Content.Client.GameTicking.Managers { - public class ClientGameTicker : SharedGameTicker, IClientGameTicker + [UsedImplicitly] + public class ClientGameTicker : SharedGameTicker { - [Dependency] private readonly IClientNetManager _netManager = default!; [Dependency] private readonly IStateManager _stateManager = default!; + [Dependency] private readonly IClientNetManager _netManager = default!; [ViewVariables] private bool _initialized; private readonly List _jobsAvailable = new(); @@ -29,7 +31,7 @@ namespace Content.Client.GameTicking.Managers [ViewVariables] public string? ServerInfoBlob { get; private set; } [ViewVariables] public TimeSpan StartTime { get; private set; } [ViewVariables] public bool Paused { get; private set; } - [ViewVariables] public Dictionary Status { get; private set; } = new(); + [ViewVariables] public Dictionary Status { get; private set; } = new(); [ViewVariables] public IReadOnlyList JobsAvailable => _jobsAvailable; public event Action? InfoBlobUpdated; @@ -38,47 +40,47 @@ namespace Content.Client.GameTicking.Managers public event Action? LobbyLateJoinStatusUpdated; public event Action>? LobbyJobsAvailableUpdated; - public void Initialize() + public override void Initialize() { DebugTools.Assert(!_initialized); - _netManager.RegisterNetMessage(nameof(MsgTickerJoinLobby), JoinLobby); - _netManager.RegisterNetMessage(nameof(MsgTickerJoinGame), JoinGame); - _netManager.RegisterNetMessage(nameof(MsgTickerLobbyStatus), LobbyStatus); - _netManager.RegisterNetMessage(nameof(MsgTickerLobbyInfo), LobbyInfo); - _netManager.RegisterNetMessage(nameof(MsgTickerLobbyCountdown), LobbyCountdown); - _netManager.RegisterNetMessage(nameof(MsgTickerLobbyReady), LobbyReady); - _netManager.RegisterNetMessage(nameof(MsgRoundEndMessage), RoundEnd); - _netManager.RegisterNetMessage(nameof(MsgRequestWindowAttention), msg => + SubscribeNetworkEvent(JoinLobby); + SubscribeNetworkEvent(JoinGame); + SubscribeNetworkEvent(LobbyStatus); + SubscribeNetworkEvent(LobbyInfo); + SubscribeNetworkEvent(LobbyCountdown); + SubscribeNetworkEvent(LobbyReady); + SubscribeNetworkEvent(RoundEnd); + SubscribeNetworkEvent(msg => { IoCManager.Resolve().RequestWindowAttention(); }); - _netManager.RegisterNetMessage(nameof(MsgTickerLateJoinStatus), LateJoinStatus); - _netManager.RegisterNetMessage(nameof(MsgTickerJobsAvailable), UpdateJobsAvailable); + SubscribeNetworkEvent(LateJoinStatus); + SubscribeNetworkEvent(UpdateJobsAvailable); - Status = new Dictionary(); + Status = new Dictionary(); _initialized = true; } - private void LateJoinStatus(MsgTickerLateJoinStatus message) + private void LateJoinStatus(TickerLateJoinStatusEvent message) { DisallowedLateJoin = message.Disallowed; LobbyLateJoinStatusUpdated?.Invoke(); } - private void UpdateJobsAvailable(MsgTickerJobsAvailable message) + private void UpdateJobsAvailable(TickerJobsAvailableEvent message) { _jobsAvailable.Clear(); _jobsAvailable.AddRange(message.JobsAvailable); LobbyJobsAvailableUpdated?.Invoke(JobsAvailable); } - private void JoinLobby(MsgTickerJoinLobby message) + private void JoinLobby(TickerJoinLobbyEvent message) { _stateManager.RequestStateChange(); } - private void LobbyStatus(MsgTickerLobbyStatus message) + private void LobbyStatus(TickerLobbyStatusEvent message) { StartTime = message.StartTime; IsGameStarted = message.IsRoundStarted; @@ -91,35 +93,35 @@ namespace Content.Client.GameTicking.Managers LobbyStatusUpdated?.Invoke(); } - private void LobbyInfo(MsgTickerLobbyInfo message) + private void LobbyInfo(TickerLobbyInfoEvent message) { ServerInfoBlob = message.TextBlob; InfoBlobUpdated?.Invoke(); } - private void JoinGame(MsgTickerJoinGame message) + private void JoinGame(TickerJoinGameEvent message) { _stateManager.RequestStateChange(); } - private void LobbyCountdown(MsgTickerLobbyCountdown message) + private void LobbyCountdown(TickerLobbyCountdownEvent message) { StartTime = message.StartTime; Paused = message.Paused; } - private void LobbyReady(MsgTickerLobbyReady message) + private void LobbyReady(TickerLobbyReadyEvent message) { // Merge the Dictionaries - foreach (var p in message.PlayerStatus) + foreach (var p in message.Status) { Status[p.Key] = p.Value; } LobbyReadyUpdated?.Invoke(); } - private void RoundEnd(MsgRoundEndMessage message) + private void RoundEnd(RoundEndMessageEvent message) { //This is not ideal at all, but I don't see an immediately better fit anywhere else. var roundEnd = new RoundEndSummaryWindow(message.GamemodeTitle, message.RoundEndText, message.RoundDuration, message.AllPlayersEndInfo); diff --git a/Content.Client/GameTicking/Managers/IClientGameTicker.cs b/Content.Client/GameTicking/Managers/IClientGameTicker.cs deleted file mode 100644 index 774ae6225c..0000000000 --- a/Content.Client/GameTicking/Managers/IClientGameTicker.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System; -using System.Collections.Generic; -using Robust.Shared.Network; -using static Content.Shared.GameTicking.SharedGameTicker; - -namespace Content.Client.GameTicking.Managers -{ - public interface IClientGameTicker - { - bool IsGameStarted { get; } - string? ServerInfoBlob { get; } - bool AreWeReady { get; } - string? LobbySong { get; } - bool DisallowedLateJoin { get; } - TimeSpan StartTime { get; } - bool Paused { get; } - Dictionary Status { get; } - IReadOnlyList JobsAvailable { get; } - - void Initialize(); - event Action InfoBlobUpdated; - event Action LobbyStatusUpdated; - event Action LobbyReadyUpdated; - event Action LobbyLateJoinStatusUpdated; - event Action> LobbyJobsAvailableUpdated; - } -} diff --git a/Content.Client/IoC/ClientContentIoC.cs b/Content.Client/IoC/ClientContentIoC.cs index fd34dacefd..990a4f7822 100644 --- a/Content.Client/IoC/ClientContentIoC.cs +++ b/Content.Client/IoC/ClientContentIoC.cs @@ -33,7 +33,6 @@ namespace Content.Client.IoC IoCManager.Register(); IoCManager.Register(); IoCManager.Register(); - IoCManager.Register(); IoCManager.Register(); IoCManager.Register(); IoCManager.Register(); diff --git a/Content.Client/LateJoin/LateJoinGui.cs b/Content.Client/LateJoin/LateJoinGui.cs index f8e028c2e0..0f0bfeed02 100644 --- a/Content.Client/LateJoin/LateJoinGui.cs +++ b/Content.Client/LateJoin/LateJoinGui.cs @@ -9,6 +9,7 @@ using Robust.Client.UserInterface; using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.CustomControls; using Robust.Client.Utility; +using Robust.Shared.GameObjects; using Robust.Shared.IoC; using Robust.Shared.Localization; using Robust.Shared.Log; @@ -22,7 +23,6 @@ namespace Content.Client.LateJoin { [Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly IClientConsoleHost _consoleHost = default!; - [Dependency] private readonly IClientGameTicker _gameTicker = default!; public event Action? SelectedId; @@ -34,6 +34,8 @@ namespace Content.Client.LateJoin MinSize = SetSize = (360, 560); IoCManager.InjectDependencies(this); + var gameTicker = EntitySystem.Get(); + Title = Loc.GetString("Late Join"); var jobList = new VBoxContainer(); @@ -131,7 +133,7 @@ namespace Content.Client.LateJoin SelectedId?.Invoke(jobButton.JobId); }; - if (!_gameTicker.JobsAvailable.Contains(job.ID)) + if (!gameTicker.JobsAvailable.Contains(job.ID)) { jobButton.Disabled = true; } @@ -147,7 +149,7 @@ namespace Content.Client.LateJoin Close(); }; - _gameTicker.LobbyJobsAvailableUpdated += JobsAvailableUpdated; + gameTicker.LobbyJobsAvailableUpdated += JobsAvailableUpdated; } private void JobsAvailableUpdated(IReadOnlyList jobs) @@ -164,7 +166,7 @@ namespace Content.Client.LateJoin if (disposing) { - _gameTicker.LobbyJobsAvailableUpdated -= JobsAvailableUpdated; + EntitySystem.Get().LobbyJobsAvailableUpdated -= JobsAvailableUpdated; _jobButtons.Clear(); _jobCategories.Clear(); } diff --git a/Content.Client/Lobby/LobbyState.cs b/Content.Client/Lobby/LobbyState.cs index dcebd4a6fc..38876d6c3d 100644 --- a/Content.Client/Lobby/LobbyState.cs +++ b/Content.Client/Lobby/LobbyState.cs @@ -10,6 +10,8 @@ using Content.Client.Preferences.UI; using Content.Client.Viewport; using Content.Client.Voting; using Content.Shared.Chat; +using Content.Shared.GameObjects.Components; +using Content.Shared.GameTicking; using Content.Shared.Input; using Robust.Client; using Robust.Client.Console; @@ -38,7 +40,6 @@ namespace Content.Client.Lobby [Dependency] private readonly IEntityManager _entityManager = default!; [Dependency] private readonly IPlayerManager _playerManager = default!; [Dependency] private readonly IResourceCache _resourceCache = default!; - [Dependency] private readonly IClientGameTicker _clientGameTicker = default!; [Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly IUserInterfaceManager _userInterfaceManager = default!; [Dependency] private readonly IClientPreferencesManager _preferencesManager = default!; @@ -50,6 +51,7 @@ namespace Content.Client.Lobby public override void Startup() { + var gameTicker = EntitySystem.Get(); _characterSetup = new CharacterSetupGui(_entityManager, _resourceCache, _preferencesManager, _prototypeManager); LayoutContainer.SetAnchorPreset(_characterSetup, LayoutContainer.LayoutPreset.Wide); @@ -105,7 +107,7 @@ namespace Content.Client.Lobby _lobby.ObserveButton.OnPressed += _ => _consoleHost.ExecuteCommand("observe"); _lobby.ReadyButton.OnPressed += _ => { - if (!_clientGameTicker.IsGameStarted) + if (!gameTicker.IsGameStarted) { return; } @@ -124,29 +126,23 @@ namespace Content.Client.Lobby UpdatePlayerList(); _playerManager.PlayerListUpdated += PlayerManagerOnPlayerListUpdated; - _clientGameTicker.InfoBlobUpdated += UpdateLobbyUi; - _clientGameTicker.LobbyStatusUpdated += LobbyStatusUpdated; - _clientGameTicker.LobbyReadyUpdated += LobbyReadyUpdated; - _clientGameTicker.LobbyLateJoinStatusUpdated += LobbyLateJoinStatusUpdated; + gameTicker.InfoBlobUpdated += UpdateLobbyUi; + gameTicker.LobbyStatusUpdated += LobbyStatusUpdated; + gameTicker.LobbyReadyUpdated += LobbyReadyUpdated; + gameTicker.LobbyLateJoinStatusUpdated += LobbyLateJoinStatusUpdated; } public override void Shutdown() { _playerManager.PlayerListUpdated -= PlayerManagerOnPlayerListUpdated; - _clientGameTicker.InfoBlobUpdated -= UpdateLobbyUi; - _clientGameTicker.LobbyStatusUpdated -= LobbyStatusUpdated; - _clientGameTicker.LobbyReadyUpdated -= LobbyReadyUpdated; - _clientGameTicker.LobbyLateJoinStatusUpdated -= LobbyLateJoinStatusUpdated; - - _clientGameTicker.Status.Clear(); - _lobby.Dispose(); _characterSetup.Dispose(); } public override void FrameUpdate(FrameEventArgs e) { - if (_clientGameTicker.IsGameStarted) + var gameTicker = EntitySystem.Get(); + if (gameTicker.IsGameStarted) { _lobby.StartTime.Text = ""; return; @@ -154,13 +150,13 @@ namespace Content.Client.Lobby string text; - if (_clientGameTicker.Paused) + if (gameTicker.Paused) { text = Loc.GetString("Paused"); } else { - var difference = _clientGameTicker.StartTime - _gameTiming.CurTime; + var difference = gameTicker.StartTime - _gameTiming.CurTime; var seconds = difference.TotalSeconds; if (seconds < 0) { @@ -177,15 +173,16 @@ namespace Content.Client.Lobby private void PlayerManagerOnPlayerListUpdated(object? sender, EventArgs e) { + var gameTicker = EntitySystem.Get(); // Remove disconnected sessions from the Ready Dict - foreach (var p in _clientGameTicker.Status) + foreach (var p in gameTicker.Status) { if (!_playerManager.SessionsDict.TryGetValue(p.Key, out _)) { // This is a shitty fix. Observers can rejoin because they are already in the game. // So we don't delete them, but keep them if they decide to rejoin - if (p.Value != PlayerStatus.Observer) - _clientGameTicker.Status.Remove(p.Key); + if (p.Value != LobbyPlayerStatus.Observer) + gameTicker.Status.Remove(p.Key); } } @@ -202,7 +199,7 @@ namespace Content.Client.Lobby private void LobbyLateJoinStatusUpdated() { - _lobby.ReadyButton.Disabled = _clientGameTicker.DisallowedLateJoin; + _lobby.ReadyButton.Disabled = EntitySystem.Get().DisallowedLateJoin; } private void UpdateLobbyUi() @@ -212,7 +209,9 @@ namespace Content.Client.Lobby return; } - if (_clientGameTicker.IsGameStarted) + var gameTicker = EntitySystem.Get(); + + if (gameTicker.IsGameStarted) { _lobby.ReadyButton.Text = Loc.GetString("Join"); _lobby.ReadyButton.ToggleMode = false; @@ -224,36 +223,37 @@ namespace Content.Client.Lobby _lobby.ReadyButton.Text = Loc.GetString("Ready Up"); _lobby.ReadyButton.ToggleMode = true; _lobby.ReadyButton.Disabled = false; - _lobby.ReadyButton.Pressed = _clientGameTicker.AreWeReady; + _lobby.ReadyButton.Pressed = gameTicker.AreWeReady; } - if (_clientGameTicker.ServerInfoBlob != null) + if (gameTicker.ServerInfoBlob != null) { - _lobby.ServerInfo.SetInfoBlob(_clientGameTicker.ServerInfoBlob); + _lobby.ServerInfo.SetInfoBlob(gameTicker.ServerInfoBlob); } } private void UpdatePlayerList() { _lobby.OnlinePlayerList.Clear(); + var gameTicker = EntitySystem.Get(); foreach (var session in _playerManager.Sessions.OrderBy(s => s.Name)) { var readyState = ""; // Don't show ready state if we're ingame - if (!_clientGameTicker.IsGameStarted) + if (!gameTicker.IsGameStarted) { - PlayerStatus status; + LobbyPlayerStatus status; if (session.UserId == _playerManager.LocalPlayer?.UserId) - status = _clientGameTicker.AreWeReady ? PlayerStatus.Ready : PlayerStatus.NotReady; + status = gameTicker.AreWeReady ? LobbyPlayerStatus.Ready : LobbyPlayerStatus.NotReady; else - _clientGameTicker.Status.TryGetValue(session.UserId, out status); + gameTicker.Status.TryGetValue(session.UserId, out status); readyState = status switch { - PlayerStatus.NotReady => Loc.GetString("Not Ready"), - PlayerStatus.Ready => Loc.GetString("Ready"), - PlayerStatus.Observer => Loc.GetString("Observer"), + LobbyPlayerStatus.NotReady => Loc.GetString("Not Ready"), + LobbyPlayerStatus.Ready => Loc.GetString("Ready"), + LobbyPlayerStatus.Observer => Loc.GetString("Observer"), _ => "", }; } @@ -264,7 +264,7 @@ namespace Content.Client.Lobby private void SetReady(bool newReady) { - if (_clientGameTicker.IsGameStarted) + if (EntitySystem.Get().IsGameStarted) { return; } diff --git a/Content.Client/RoundEnd/RoundEndSummaryWindow.cs b/Content.Client/RoundEnd/RoundEndSummaryWindow.cs index 92ef60745c..7aa4598ecb 100644 --- a/Content.Client/RoundEnd/RoundEndSummaryWindow.cs +++ b/Content.Client/RoundEnd/RoundEndSummaryWindow.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using Content.Client.Message; +using Content.Shared.GameTicking; using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.CustomControls; using Robust.Shared.Localization; @@ -15,7 +16,7 @@ namespace Content.Client.RoundEnd private VBoxContainer PlayerManifestoTab { get; } private TabContainer RoundEndWindowTabs { get; } - public RoundEndSummaryWindow(string gm, string roundEnd, TimeSpan roundTimeSpan, List info) + public RoundEndSummaryWindow(string gm, string roundEnd, TimeSpan roundTimeSpan, RoundEndMessageEvent.RoundEndPlayerInfo[] info) { MinSize = SetSize = (520, 580); diff --git a/Content.IntegrationTests/ContentIntegrationTest.cs b/Content.IntegrationTests/ContentIntegrationTest.cs index 1b1970a282..d550bc7cc9 100644 --- a/Content.IntegrationTests/ContentIntegrationTest.cs +++ b/Content.IntegrationTests/ContentIntegrationTest.cs @@ -5,10 +5,12 @@ using Content.Client.Parallax.Managers; using Content.Server.GameTicking; using Content.Server.IoC; using Content.Shared.CCVar; +using Moq; using NUnit.Framework; using Robust.Server.Maps; using Robust.Shared; using Robust.Shared.ContentPack; +using Robust.Shared.GameObjects; using Robust.Shared.IoC; using Robust.Shared.Log; using Robust.Shared.Map; @@ -57,7 +59,7 @@ namespace Content.IntegrationTests // Connecting to Discord is a massive waste of time. // Basically just makes the CI logs a mess. - options.CVarOverrides["discord.enabled"] = "false"; + options.CVarOverrides[CVars.DiscordEnabled.Name] = "false"; // Avoid preloading textures in tests. options.CVarOverrides.TryAdd(CVars.TexturePreloadingEnabled.Name, "false"); @@ -112,16 +114,9 @@ namespace Content.IntegrationTests protected ServerIntegrationInstance StartServerDummyTicker(ServerIntegrationOptions options = null) { options ??= new ServerIntegrationOptions(); - options.BeforeStart += () => - { - IoCManager.Resolve().SetModuleBaseCallbacks(new ServerModuleTestingCallbacks - { - ServerBeforeIoC = () => - { - IoCManager.Register(true); - } - }); - }; + + // Dummy game ticker. + options.CVarOverrides[CCVars.GameDummyTicker.Name] = "true"; return StartServer(options); } diff --git a/Content.IntegrationTests/DummyGameTicker.cs b/Content.IntegrationTests/DummyGameTicker.cs deleted file mode 100644 index f79ea96e4f..0000000000 --- a/Content.IntegrationTests/DummyGameTicker.cs +++ /dev/null @@ -1,140 +0,0 @@ -#nullable enable -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using Content.Server.GameTicking; -using Content.Server.GameTicking.Rules; -using Content.Server.Mind; -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.IntegrationTests -{ - public class DummyGameTicker : GameTickerBase, IGameTicker - { - public GameRunLevel RunLevel { get; } = GameRunLevel.InRound; - - public MapId DefaultMap { get; } = MapId.Nullspace; - public GridId DefaultGridId { get; } = GridId.Invalid; - - public event Action OnRunLevelChanged - { - add { } - remove { } - } - - public event Action OnRuleAdded - { - add{ } - remove { } - } - - public void Update(FrameEventArgs frameEventArgs) - { - } - - public void RestartRound() - { - } - - public void StartRound(bool force = false) - { - } - - public void EndRound(string roundEnd) - { - } - - public void Respawn(IPlayerSession targetPlayer) - { - } - - public bool OnGhostAttempt(Mind mind, bool canReturnGlobal) - { - return false; - } - - public void MakeObserve(IPlayerSession player) - { - } - - public void MakeJoinGame(IPlayerSession player, string? jobId) - { - } - - public void ToggleReady(IPlayerSession player, bool ready) - { - } - - public void ToggleDisallowLateJoin(bool disallowLateJoin) - { - } - - public EntityCoordinates GetLateJoinSpawnPoint() => EntityCoordinates.Invalid; - public EntityCoordinates GetJobSpawnPoint(string jobId) => EntityCoordinates.Invalid; - public EntityCoordinates GetObserverSpawnPoint() => EntityCoordinates.Invalid; - - public void EquipStartingGear(IEntity entity, StartingGearPrototype startingGear, HumanoidCharacterProfile? profile) - { - } - - public T AddGameRule() where T : GameRule, new() - { - return new(); - } - - public bool HasGameRule(string? type) - { - return false; - } - - public bool HasGameRule(Type? type) - { - return false; - } - - public void RemoveGameRule(GameRule rule) - { - } - - public IEnumerable ActiveGameRules { get; } = Array.Empty(); - - public bool TryGetPreset(string name, [NotNullWhen(true)] out Type? type) - { - type = default; - return false; - } - - public void SetStartPreset(Type type, bool force = false) - { - } - - public void SetStartPreset(string name, bool force = false) - { - } - - public bool DelayStart(TimeSpan time) - { - return true; - } - - public bool PauseStart(bool pause = true) - { - return true; - } - - public bool TogglePause() - { - return false; - } - - public Dictionary GetAvailablePositions() - { - return new(); - } - } -} diff --git a/Content.IntegrationTests/Tests/Commands/RestartRoundTest.cs b/Content.IntegrationTests/Tests/Commands/RestartRoundTest.cs index 0f6f4d8338..ebc11c9c2e 100644 --- a/Content.IntegrationTests/Tests/Commands/RestartRoundTest.cs +++ b/Content.IntegrationTests/Tests/Commands/RestartRoundTest.cs @@ -24,9 +24,9 @@ namespace Content.IntegrationTests.Tests.Commands await server.WaitIdleAsync(); - var gameTicker = server.ResolveDependency(); var configManager = server.ResolveDependency(); var entityManager = server.ResolveDependency(); + var gameTicker = entityManager.EntitySysManager.GetEntitySystem(); await server.WaitRunTicks(30); diff --git a/Content.IntegrationTests/Tests/GameRules/RuleMaxTimeRestartTest.cs b/Content.IntegrationTests/Tests/GameRules/RuleMaxTimeRestartTest.cs index 40eec47d23..58e8648d56 100644 --- a/Content.IntegrationTests/Tests/GameRules/RuleMaxTimeRestartTest.cs +++ b/Content.IntegrationTests/Tests/GameRules/RuleMaxTimeRestartTest.cs @@ -3,6 +3,7 @@ using System.Threading.Tasks; using Content.Server.GameTicking; using Content.Server.GameTicking.Rules; using NUnit.Framework; +using Robust.Shared.GameObjects; using Robust.Shared.Timing; namespace Content.IntegrationTests.Tests.GameRules @@ -25,7 +26,7 @@ namespace Content.IntegrationTests.Tests.GameRules await server.WaitIdleAsync(); - var sGameTicker = server.ResolveDependency(); + var sGameTicker = server.ResolveDependency().GetEntitySystem(); var sGameTiming = server.ResolveDependency(); RuleMaxTimeRestart maxTimeRule = null; diff --git a/Content.IntegrationTests/Tests/Lobby/CharacterCreationTest.cs b/Content.IntegrationTests/Tests/Lobby/CharacterCreationTest.cs index b534711fa4..fcae9955d8 100644 --- a/Content.IntegrationTests/Tests/Lobby/CharacterCreationTest.cs +++ b/Content.IntegrationTests/Tests/Lobby/CharacterCreationTest.cs @@ -13,6 +13,7 @@ using Content.Shared.Preferences; using NUnit.Framework; using Robust.Client.State; using Robust.Shared.Configuration; +using Robust.Shared.GameObjects; using Robust.Shared.Network; namespace Content.IntegrationTests.Tests.Lobby @@ -32,7 +33,7 @@ namespace Content.IntegrationTests.Tests.Lobby var clientPrefManager = client.ResolveDependency(); var serverConfig = server.ResolveDependency(); - var serverTicker = server.ResolveDependency(); + var serverTicker = server.ResolveDependency().GetEntitySystem(); var serverPrefManager = server.ResolveDependency(); await server.WaitIdleAsync(); @@ -40,12 +41,16 @@ namespace Content.IntegrationTests.Tests.Lobby await server.WaitAssertion(() => { + serverConfig.SetCVar(CCVars.GameDummyTicker, false); serverConfig.SetCVar(CCVars.GameLobbyEnabled, true); serverTicker.RestartRound(); }); Assert.That(serverTicker.RunLevel, Is.EqualTo(GameRunLevel.PreRoundLobby)); + // Need to run them in sync to receive the messages. + await RunTicksSync(client, server, 1); + await WaitUntil(client, () => clientStateManager.CurrentState is LobbyState, maxTicks: 60); Assert.NotNull(clientNetManager.ServerChannel); diff --git a/Content.IntegrationTests/Tests/ResettingEntitySystemTests.cs b/Content.IntegrationTests/Tests/ResettingEntitySystemTests.cs index 2204947180..a7b8740c73 100644 --- a/Content.IntegrationTests/Tests/ResettingEntitySystemTests.cs +++ b/Content.IntegrationTests/Tests/ResettingEntitySystemTests.cs @@ -36,8 +36,8 @@ namespace Content.IntegrationTests.Tests await server.WaitIdleAsync(); - var gameTicker = server.ResolveDependency(); var entitySystemManager = server.ResolveDependency(); + var gameTicker = entitySystemManager.GetEntitySystem(); await server.WaitAssertion(() => { diff --git a/Content.IntegrationTests/Tests/RestartRoundTest.cs b/Content.IntegrationTests/Tests/RestartRoundTest.cs index 6ecf5a2dc6..6c346677b3 100644 --- a/Content.IntegrationTests/Tests/RestartRoundTest.cs +++ b/Content.IntegrationTests/Tests/RestartRoundTest.cs @@ -1,6 +1,7 @@ using System.Threading.Tasks; using Content.Server.GameTicking; using NUnit.Framework; +using Robust.Shared.GameObjects; using Robust.Shared.IoC; namespace Content.IntegrationTests.Tests @@ -15,7 +16,7 @@ namespace Content.IntegrationTests.Tests server.Post(() => { - IoCManager.Resolve().RestartRound(); + IoCManager.Resolve().GetEntitySystem().RestartRound(); }); await RunTicksSync(client, server, 10); diff --git a/Content.Server/AI/Components/AiControllerComponent.cs b/Content.Server/AI/Components/AiControllerComponent.cs index 0ce7720bf9..138acd552e 100644 --- a/Content.Server/AI/Components/AiControllerComponent.cs +++ b/Content.Server/AI/Components/AiControllerComponent.cs @@ -46,7 +46,7 @@ namespace Content.Server.AI.Components if (StartingGearPrototype != null) { - var gameTicker = IoCManager.Resolve(); + var gameTicker = EntitySystem.Get(); var protoManager = IoCManager.Resolve(); var startingGear = protoManager.Index(StartingGearPrototype); diff --git a/Content.Server/Administration/Commands/AGhost.cs b/Content.Server/Administration/Commands/AGhost.cs index 336ee5ec8b..aff615a193 100644 --- a/Content.Server/Administration/Commands/AGhost.cs +++ b/Content.Server/Administration/Commands/AGhost.cs @@ -42,7 +42,7 @@ namespace Content.Server.Administration.Commands var canReturn = mind.CurrentEntity != null; var ghost = IoCManager.Resolve() .SpawnEntity("AdminObserver", player.AttachedEntity?.Transform.Coordinates - ?? IoCManager.Resolve().GetObserverSpawnPoint()); + ?? EntitySystem.Get().GetObserverSpawnPoint()); if (canReturn) { diff --git a/Content.Server/Administration/Commands/ReadyAll.cs b/Content.Server/Administration/Commands/ReadyAll.cs index 6575f6295b..33930bf2cd 100644 --- a/Content.Server/Administration/Commands/ReadyAll.cs +++ b/Content.Server/Administration/Commands/ReadyAll.cs @@ -3,6 +3,7 @@ using Content.Server.GameTicking; using Content.Shared.Administration; using Robust.Server.Player; using Robust.Shared.Console; +using Robust.Shared.GameObjects; using Robust.Shared.IoC; namespace Content.Server.Administration.Commands @@ -22,7 +23,7 @@ namespace Content.Server.Administration.Commands ready = bool.Parse(args[0]); } - var gameTicker = IoCManager.Resolve(); + var gameTicker = EntitySystem.Get(); var playerManager = IoCManager.Resolve(); diff --git a/Content.Server/Body/BodyComponent.cs b/Content.Server/Body/BodyComponent.cs index 88e0b366d2..9710f58a51 100644 --- a/Content.Server/Body/BodyComponent.cs +++ b/Content.Server/Body/BodyComponent.cs @@ -25,7 +25,6 @@ namespace Content.Server.Body public class BodyComponent : SharedBodyComponent, IRelayMoveInput, IGhostOnMove { private Container _partContainer = default!; - [Dependency] private readonly IGameTicker _gameTicker = default!; protected override bool CanAddPart(string slotId, SharedBodyPartComponent part) { @@ -94,7 +93,7 @@ namespace Content.Server.Body Owner.TryGetComponent(out MindComponent? mind) && mind.HasMind) { - _gameTicker.OnGhostAttempt(mind.Mind!, true); + EntitySystem.Get().OnGhostAttempt(mind.Mind!, true); } } diff --git a/Content.Server/Chat/Commands/SuicideCommand.cs b/Content.Server/Chat/Commands/SuicideCommand.cs index e3a82898e2..a17be76b56 100644 --- a/Content.Server/Chat/Commands/SuicideCommand.cs +++ b/Content.Server/Chat/Commands/SuicideCommand.cs @@ -125,7 +125,7 @@ namespace Content.Server.Chat.Commands // Prevent the player from returning to the body. // Note that mind cannot be null because otherwise owner would be null. - IoCManager.Resolve().OnGhostAttempt(mind!, false); + EntitySystem.Get().OnGhostAttempt(mind!, false); } } } diff --git a/Content.Server/Entry/EntryPoint.cs b/Content.Server/Entry/EntryPoint.cs index 31500cd8e8..42b3130724 100644 --- a/Content.Server/Entry/EntryPoint.cs +++ b/Content.Server/Entry/EntryPoint.cs @@ -14,7 +14,6 @@ using Content.Server.Notification.Managers; using Content.Server.PDA.Managers; using Content.Server.Preferences.Managers; using Content.Server.Sandbox; -using Content.Server.Shell; using Content.Server.Speech; using Content.Server.Voting.Managers; using Content.Shared.Actions; @@ -31,9 +30,7 @@ namespace Content.Server.Entry { public class EntryPoint : GameServer { - private IGameTicker _gameTicker = default!; private EuiManager _euiManager = default!; - private StatusShell _statusShell = default!; private IVoteManager _voteManager = default!; /// @@ -60,7 +57,6 @@ namespace Content.Server.Entry IoCManager.BuildGraph(); - _gameTicker = IoCManager.Resolve(); _euiManager = IoCManager.Resolve(); _voteManager = IoCManager.Resolve(); @@ -69,8 +65,6 @@ namespace Content.Server.Entry var playerManager = IoCManager.Resolve(); - _statusShell = new StatusShell(); - var logManager = IoCManager.Resolve(); logManager.GetSawmill("Storage").Level = LogLevel.Info; logManager.GetSawmill("db.ef").Level = LogLevel.Info; @@ -79,7 +73,6 @@ namespace Content.Server.Entry IoCManager.Resolve().Init(); IoCManager.Resolve().Init(); IoCManager.Resolve().Initialize(); - IoCManager.Resolve().Initialize(); IoCManager.Resolve().Initialize(); _voteManager.Initialize(); } @@ -89,7 +82,7 @@ namespace Content.Server.Entry base.PostInit(); IoCManager.Resolve().Initialize(); - _gameTicker.Initialize(); + IoCManager.Resolve().Initialize(); IoCManager.Resolve().Initialize(); IoCManager.Resolve().Initialize(); IoCManager.Resolve().Initialize(); @@ -99,6 +92,8 @@ namespace Content.Server.Entry IoCManager.Resolve().Initialize(); IoCManager.Resolve().Initialize(); _euiManager.Initialize(); + + IoCManager.Resolve().GetEntitySystem().PostInitialize(); } public override void Update(ModUpdateLevel level, FrameEventArgs frameEventArgs) @@ -107,11 +102,6 @@ namespace Content.Server.Entry switch (level) { - case ModUpdateLevel.PreEngine: - { - _gameTicker.Update(frameEventArgs); - break; - } case ModUpdateLevel.PostEngine: { _euiManager.SendUpdates(); diff --git a/Content.Server/GameTicking/Commands/DelayStartCommand.cs b/Content.Server/GameTicking/Commands/DelayStartCommand.cs index 239611b7dd..33bf53fe8f 100644 --- a/Content.Server/GameTicking/Commands/DelayStartCommand.cs +++ b/Content.Server/GameTicking/Commands/DelayStartCommand.cs @@ -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(); + var ticker = EntitySystem.Get(); if (ticker.RunLevel != GameRunLevel.PreRoundLobby) { shell.WriteLine("This can only be executed while the game is in the pre-round lobby."); diff --git a/Content.Server/GameTicking/Commands/EndRoundCommand.cs b/Content.Server/GameTicking/Commands/EndRoundCommand.cs index 88fa4f2e04..2a8d257cdf 100644 --- a/Content.Server/GameTicking/Commands/EndRoundCommand.cs +++ b/Content.Server/GameTicking/Commands/EndRoundCommand.cs @@ -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(); + var ticker = EntitySystem.Get(); if (ticker.RunLevel != GameRunLevel.InRound) { diff --git a/Content.Server/GameTicking/Commands/ForcePresetCommand.cs b/Content.Server/GameTicking/Commands/ForcePresetCommand.cs index 2af366c9be..f9e037882c 100644 --- a/Content.Server/GameTicking/Commands/ForcePresetCommand.cs +++ b/Content.Server/GameTicking/Commands/ForcePresetCommand.cs @@ -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(); + var ticker = EntitySystem.Get(); if (ticker.RunLevel != GameRunLevel.PreRoundLobby) { shell.WriteLine("This can only be executed while the game is in the pre-round lobby."); diff --git a/Content.Server/GameTicking/Commands/GoLobbyCommand.cs b/Content.Server/GameTicking/Commands/GoLobbyCommand.cs index 7af132be38..6659938e40 100644 --- a/Content.Server/GameTicking/Commands/GoLobbyCommand.cs +++ b/Content.Server/GameTicking/Commands/GoLobbyCommand.cs @@ -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(); + var ticker = EntitySystem.Get(); if (args.Length > 0) { diff --git a/Content.Server/GameTicking/Commands/JoinGameCommand.cs b/Content.Server/GameTicking/Commands/JoinGameCommand.cs index 6714719826..d0b060d59b 100644 --- a/Content.Server/GameTicking/Commands/JoinGameCommand.cs +++ b/Content.Server/GameTicking/Commands/JoinGameCommand.cs @@ -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(); + var ticker = EntitySystem.Get(); if (ticker.RunLevel == GameRunLevel.PreRoundLobby) { shell.WriteLine("Round has not started."); diff --git a/Content.Server/GameTicking/Commands/NewRoundCommand.cs b/Content.Server/GameTicking/Commands/NewRoundCommand.cs index f54ca06cbf..ec18326483 100644 --- a/Content.Server/GameTicking/Commands/NewRoundCommand.cs +++ b/Content.Server/GameTicking/Commands/NewRoundCommand.cs @@ -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(); + var ticker = EntitySystem.Get(); ticker.RestartRound(); } } diff --git a/Content.Server/GameTicking/Commands/ObserveCommand.cs b/Content.Server/GameTicking/Commands/ObserveCommand.cs index 0bbd4949fe..9fc411ae2a 100644 --- a/Content.Server/GameTicking/Commands/ObserveCommand.cs +++ b/Content.Server/GameTicking/Commands/ObserveCommand.cs @@ -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(); + var ticker = EntitySystem.Get(); ticker.MakeObserve(player); } } diff --git a/Content.Server/GameTicking/Commands/RespawnCommand.cs b/Content.Server/GameTicking/Commands/RespawnCommand.cs index 9474e90623..c8d828f6a0 100644 --- a/Content.Server/GameTicking/Commands/RespawnCommand.cs +++ b/Content.Server/GameTicking/Commands/RespawnCommand.cs @@ -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(); - var ticker = IoCManager.Resolve(); + var ticker = EntitySystem.Get(); NetUserId userId; if (args.Length == 0) diff --git a/Content.Server/GameTicking/Commands/SetGamePresetCommand.cs b/Content.Server/GameTicking/Commands/SetGamePresetCommand.cs index 82e91a99ee..d75493c28e 100644 --- a/Content.Server/GameTicking/Commands/SetGamePresetCommand.cs +++ b/Content.Server/GameTicking/Commands/SetGamePresetCommand.cs @@ -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(); + var ticker = EntitySystem.Get(); ticker.SetStartPreset(args[0]); } diff --git a/Content.Server/GameTicking/Commands/StartRoundCommand.cs b/Content.Server/GameTicking/Commands/StartRoundCommand.cs index 47261a341b..facb9595f6 100644 --- a/Content.Server/GameTicking/Commands/StartRoundCommand.cs +++ b/Content.Server/GameTicking/Commands/StartRoundCommand.cs @@ -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(); + var ticker = EntitySystem.Get(); if (ticker.RunLevel != GameRunLevel.PreRoundLobby) { diff --git a/Content.Server/GameTicking/Commands/ToggleDisallowLateJoinCommand.cs b/Content.Server/GameTicking/Commands/ToggleDisallowLateJoinCommand.cs index 0b0966ef80..106456b3a9 100644 --- a/Content.Server/GameTicking/Commands/ToggleDisallowLateJoinCommand.cs +++ b/Content.Server/GameTicking/Commands/ToggleDisallowLateJoinCommand.cs @@ -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(); + var cfgMan = IoCManager.Resolve(); 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 diff --git a/Content.Server/GameTicking/Commands/ToggleReadyCommand.cs b/Content.Server/GameTicking/Commands/ToggleReadyCommand.cs index ac105cda59..7cf36d859b 100644 --- a/Content.Server/GameTicking/Commands/ToggleReadyCommand.cs +++ b/Content.Server/GameTicking/Commands/ToggleReadyCommand.cs @@ -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(); + var ticker = EntitySystem.Get(); ticker.ToggleReady(player, bool.Parse(args[0])); } } diff --git a/Content.Server/GameTicking/GameTicker.CVars.cs b/Content.Server/GameTicking/GameTicker.CVars.cs new file mode 100644 index 0000000000..f8cd9faea6 --- /dev/null +++ b/Content.Server/GameTicking/GameTicker.CVars.cs @@ -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(); }); + } + } +} diff --git a/Content.Server/GameTicking/GameTicker.GamePreset.cs b/Content.Server/GameTicking/GameTicker.GamePreset.cs new file mode 100644 index 0000000000..e7861c7a5d --- /dev/null +++ b/Content.Server/GameTicking/GameTicker.GamePreset.cs @@ -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()); + set => _preset = value; + } + + public ImmutableDictionary Presets { get; private set; } = default!; + + private GamePreset? _preset; + + private void InitializeGamePreset() + { + var presets = new Dictionary(); + + foreach (var type in _reflectionManager.FindTypesWithAttribute()) + { + var attribute = type.GetCustomAttribute(); + + 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 readyProfiles) + { + var preset = _dynamicTypeFactory.CreateInstance(_presetType ?? typeof(PresetSandbox)); + preset.ReadyProfiles = readyProfiles; + return preset; + } + } +} diff --git a/Content.Server/GameTicking/GameTicker.GameRule.cs b/Content.Server/GameTicking/GameTicker.GameRule.cs new file mode 100644 index 0000000000..c0a9f06db9 --- /dev/null +++ b/Content.Server/GameTicking/GameTicker.GameRule.cs @@ -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 _gameRules = new(); + public IEnumerable ActiveGameRules => _gameRules; + + public T AddGameRule() where T : GameRule, new() + { + var instance = _dynamicTypeFactory.CreateInstance(); + + _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; + } + } +} diff --git a/Content.Server/GameTicking/GameTicker.JobController.cs b/Content.Server/GameTicking/GameTicker.JobController.cs index 6e4b6a9d31..a94bf6ab02 100644 --- a/Content.Server/GameTicking/GameTicker.JobController.cs +++ b/Content.Server/GameTicking/GameTicker.JobController.cs @@ -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 _manifest = new(); + [ViewVariables] private readonly Dictionary _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(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()); + + 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)); } } } diff --git a/Content.Server/GameTicking/GameTicker.Lobby.cs b/Content.Server/GameTicking/GameTicker.Lobby.cs new file mode 100644 index 0000000000..769a66d6e0 --- /dev/null +++ b/Content.Server/GameTicking/GameTicker.Lobby.cs @@ -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 _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 { { player.UserId, status } }); + } + + private TickerLobbyReadyEvent GetPlayerStatus() + { + var players = new Dictionary(); + 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)); + } + } +} diff --git a/Content.Server/GameTicking/GameTicker.LobbyMusic.cs b/Content.Server/GameTicking/GameTicker.LobbyMusic.cs new file mode 100644 index 0000000000..a13ca046a0 --- /dev/null +++ b/Content.Server/GameTicking/GameTicker.LobbyMusic.cs @@ -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(LobbyMusicCollection); + + // Now that the collection is set, the lobby music has been initialized and we can choose a random song. + _lobbyMusicInitialized = true; + + ChooseRandomLobbySong(); + } + + /// + /// Sets the current lobby song, or stops it if null. + /// + /// The lobby song to play, or null to stop any lobby songs. + 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 + } + + /// + /// Plays a random song from the LobbyMusic sound collection. + /// + public void ChooseRandomLobbySong() + { + DebugTools.Assert(_lobbyMusicInitialized); + SetLobbySong(_robustRandom.Pick(_lobbyMusicCollection.PickFiles)); + } + + /// + /// Stops the current lobby song being played. + /// + public void StopLobbySong() + { + SetLobbySong(null); + } + } +} diff --git a/Content.Server/GameTicking/GameTicker.Player.cs b/Content.Server/GameTicking/GameTicker.Player.cs new file mode 100644 index 0000000000..3831f510e9 --- /dev/null +++ b/Content.Server/GameTicking/GameTicker.Player.cs @@ -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()); + } + } +} diff --git a/Content.Server/GameTicking/GameTicker.RoundFlow.cs b/Content.Server/GameTicking/GameTicker.RoundFlow.cs new file mode 100644 index 0000000000..993de90ffe --- /dev/null +++ b/Content.Server/GameTicking/GameTicker.RoundFlow.cs @@ -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 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().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().RealTime.Subtract(_roundStartTimeSpan); + + //Generate a list of basic player info to display in the end round summary. + var listOfPlayerInfo = new List(); + 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(); + } + } + + /// + /// Cleanup that has to run to clear up anything from the previous round. + /// Stuff like wiping the previous map clean. + /// + 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; + } + } +} diff --git a/Content.Server/GameTicking/GameTicker.Spawning.cs b/Content.Server/GameTicking/GameTicker.Spawning.cs new file mode 100644 index 0000000000..7fd06d60ac --- /dev/null +++ b/Content.Server/GameTicking/GameTicker.Spawning.cs @@ -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 _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(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(); + } + + 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().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(job.StartingGear); + EquipStartingGear(entity, startingGear, profile); + } + + if (profile != null) + { + entity.GetComponent().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()); + } + } + } + + 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(), 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(); + 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()) + { + 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()) + { + 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()) + { + if (point.SpawnType == SpawnPointType.Observer) + _possiblePositions.Add(transform.Coordinates); + } + + if (_possiblePositions.Count != 0) + location = _robustRandom.Pick(_possiblePositions); + + return location; + } + #endregion + } +} diff --git a/Content.Server/GameTicking/GameTicker.StatusShell.cs b/Content.Server/GameTicking/GameTicker.StatusShell.cs new file mode 100644 index 0000000000..bf2d359807 --- /dev/null +++ b/Content.Server/GameTicking/GameTicker.StatusShell.cs @@ -0,0 +1,34 @@ +using Newtonsoft.Json.Linq; +using Robust.Server.ServerStatus; +using Robust.Shared.IoC; + +namespace Content.Server.GameTicking +{ + public partial class GameTicker + { + /// + /// Used for thread safety, given is called from another thread. + /// + private readonly object _statusShellLock = new(); + + private void InitializeStatusShell() + { + IoCManager.Resolve().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"); + } + } + } + } +} diff --git a/Content.Server/GameTicking/GameTicker.Updates.cs b/Content.Server/GameTicking/GameTicker.Updates.cs new file mode 100644 index 0000000000..26de47acaa --- /dev/null +++ b/Content.Server/GameTicking/GameTicker.Updates.cs @@ -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(); + } + + /// + /// 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. + /// + 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); + } + } +} diff --git a/Content.Server/GameTicking/GameTicker.cs b/Content.Server/GameTicking/GameTicker.cs index 0574f599ac..5400e0348a 100644 --- a/Content.Server/GameTicking/GameTicker.cs +++ b/Content.Server/GameTicking/GameTicker.cs @@ -1,47 +1,14 @@ -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Diagnostics.CodeAnalysis; -using System.Globalization; -using System.Linq; -using System.Reflection; -using System.Threading; -using Content.Server.Access.Components; -using Content.Server.CharacterAppearance.Components; using Content.Server.Chat.Managers; -using Content.Server.GameTicking.Presets; -using Content.Server.GameTicking.Rules; -using Content.Server.Ghost.Components; -using Content.Server.Hands.Components; -using Content.Server.Interfaces; -using Content.Server.Inventory.Components; -using Content.Server.Items; -using Content.Server.PDA; -using Content.Server.Players; using Content.Server.Preferences.Managers; -using Content.Server.Roles; -using Content.Server.Spawners.Components; -using Content.Server.Speech.Components; -using Content.Shared; -using Content.Shared.Audio; -using Content.Shared.CCVar; using Content.Shared.Chat; -using Content.Shared.Coordinates; using Content.Shared.GameTicking; using Content.Shared.GameWindow; -using Content.Shared.Preferences; -using Content.Shared.Roles; -using Prometheus; using Robust.Server; using Robust.Server.Maps; -using Robust.Server.Player; using Robust.Server.ServerStatus; using Robust.Shared.Configuration; -using Robust.Shared.Enums; using Robust.Shared.GameObjects; using Robust.Shared.IoC; -using Robust.Shared.Localization; -using Robust.Shared.Log; using Robust.Shared.Map; using Robust.Shared.Network; using Robust.Shared.Prototypes; @@ -50,314 +17,45 @@ using Robust.Shared.Reflection; using Robust.Shared.Timing; using Robust.Shared.Utility; using Robust.Shared.ViewVariables; -using static Content.Shared.Inventory.EquipmentSlotDefines; -using Timer = Robust.Shared.Timing.Timer; namespace Content.Server.GameTicking { - public partial class GameTicker : GameTickerBase, IGameTicker + public partial class GameTicker : SharedGameTicker { - 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."); - - private static readonly TimeSpan UpdateRestartDelay = TimeSpan.FromSeconds(20); - - public const float PresetFailedCooldownIncrease = 30f; - private const string PlayerPrototypeName = "HumanMob_Content"; - private const string ObserverPrototypeName = "MobObserver"; - private TimeSpan _roundStartTimeSpan; - - [ViewVariables] private readonly List _gameRules = new(); - [ViewVariables] private readonly List _manifest = new(); - - [ViewVariables] - private readonly Dictionary _playersInLobby = new(); - [ViewVariables] private bool _initialized; - - [ViewVariables] private Type? _presetType; - - [ViewVariables] private TimeSpan _pauseTime; - [ViewVariables] private bool _roundStartCountdownHasNotStartedYetDueToNoPlayers; - [ViewVariables] private TimeSpan _roundStartTime; - [ViewVariables] private GameRunLevel _runLevel; - [ViewVariables(VVAccess.ReadWrite)] private EntityCoordinates _spawnPoint; - - [ViewVariables] private bool DisallowLateJoin { get; set; } = false; - - [ViewVariables] private bool LobbyEnabled => _configurationManager.GetCVar(CCVars.GameLobbyEnabled); - - [ViewVariables] private bool _updateOnRoundEnd; - private CancellationTokenSource? _updateShutdownCts; - - - [ViewVariables] public bool Paused { get; private set; } + [ViewVariables] private bool _postInitialized; [ViewVariables] public MapId DefaultMap { get; private set; } - [ViewVariables] public GridId DefaultGridId { get; private set; } - [ViewVariables] - public GameRunLevel RunLevel - { - get => _runLevel; - private set - { - if (_runLevel == value) return; - - var old = _runLevel; - _runLevel = value; - - OnRunLevelChanged?.Invoke(new GameRunLevelChangedEventArgs(old, value)); - } - } - - [ViewVariables] - public GamePreset? Preset - { - get => _preset ?? MakeGamePreset(new Dictionary()); - set => _preset = value; - } - - public ImmutableDictionary Presets { get; private set; } = default!; - - private GamePreset? _preset; - - public event Action? OnRunLevelChanged; - public event Action? OnRuleAdded; - - private TimeSpan LobbyDuration => - TimeSpan.FromSeconds(_configurationManager.GetCVar(CCVars.GameLobbyDuration)); - - private SoundCollectionPrototype _lobbyCollection = default!; - [ViewVariables] public string LobbySong { get; private set; } = default!; - public override void Initialize() { base.Initialize(); DebugTools.Assert(!_initialized); + DebugTools.Assert(!_postInitialized); - var presets = new Dictionary(); - - foreach (var type in _reflectionManager.FindTypesWithAttribute()) - { - var attribute = type.GetCustomAttribute(); - - presets.Add(attribute!.Id.ToLowerInvariant(), type); - - foreach (var alias in attribute.Aliases) - { - presets.Add(alias.ToLowerInvariant(), type); - } - } - - Presets = presets.ToImmutableDictionary(); - - _lobbyCollection = _prototypeManager.Index("LobbyMusic"); - LobbySong = _robustRandom.Pick(_lobbyCollection.PickFiles); - - _netManager.RegisterNetMessage(nameof(MsgTickerJoinLobby)); - _netManager.RegisterNetMessage(nameof(MsgTickerJoinGame)); - _netManager.RegisterNetMessage(nameof(MsgTickerLobbyStatus)); - _netManager.RegisterNetMessage(nameof(MsgTickerLobbyInfo)); - _netManager.RegisterNetMessage(nameof(MsgTickerLobbyCountdown)); - _netManager.RegisterNetMessage(nameof(MsgTickerLobbyReady)); - _netManager.RegisterNetMessage(nameof(MsgRoundEndMessage)); - _netManager.RegisterNetMessage(nameof(MsgRequestWindowAttention)); - _netManager.RegisterNetMessage(nameof(MsgTickerLateJoinStatus)); - _netManager.RegisterNetMessage(nameof(MsgTickerJobsAvailable)); - - SetStartPreset(_configurationManager.GetCVar(CCVars.GameLobbyDefaultPreset)); - - RestartRound(); + // Initialize the other parts of the game ticker. + InitializeStatusShell(); + InitializeCVars(); + InitializePlayer(); + InitializeLobbyMusic(); + InitializeGamePreset(); + InitializeJobController(); + InitializeUpdates(); _initialized = true; - - JobControllerInit(); - - _watchdogApi.UpdateReceived += WatchdogApiOnUpdateReceived; } - private void WatchdogApiOnUpdateReceived() + public void PostInitialize() { - _chatManager.DispatchServerAnnouncement(Loc.GetString( - "Update has been received, server will automatically restart for update at the end of this round.")); - _updateOnRoundEnd = true; - ServerEmptyUpdateRestartCheck(); - } + DebugTools.Assert(_initialized); + DebugTools.Assert(!_postInitialized); - public void Update(FrameEventArgs frameEventArgs) - { - if (RunLevel == GameRunLevel.InRound) - { - RoundLengthMetric.Inc(frameEventArgs.DeltaSeconds); - } + // We restart the round now that entities are initialized and prototypes have been loaded. + RestartRound(); - if (RunLevel != GameRunLevel.PreRoundLobby || - Paused || - _roundStartTime > _gameTiming.CurTime || - _roundStartCountdownHasNotStartedYetDueToNoPlayers) - { - return; - } - - StartRound(); - } - - public void RestartRound() - { - 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(_lobbyCollection.PickFiles); - _resettingCleanup(); - _preRoundSetup(); - - if (!LobbyEnabled) - { - StartRound(); - } - else - { - Preset = null; - - if (PlayerManager.PlayerCount == 0) - _roundStartCountdownHasNotStartedYetDueToNoPlayers = true; - else - _roundStartTime = _gameTiming.CurTime + LobbyDuration; - - _sendStatusToAll(); - - ReqWindowAttentionAll(); - } - } - - private void ReqWindowAttentionAll() - { - foreach (var player in PlayerManager.GetAllPlayers()) - { - player.RequestWindowAttention(); - } - } - - public void StartRound(bool force = false) - { - DebugTools.Assert(RunLevel == GameRunLevel.PreRoundLobby); - Logger.InfoS("ticker", "Starting round!"); - - SendServerMessage("The round is starting now..."); - - List readyPlayers; - if (LobbyEnabled) - { - readyPlayers = _playersInLobby.Where(p => p.Value == PlayerStatus.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().RealTime; - _sendStatusToAll(); - ReqWindowAttentionAll(); - UpdateLateJoinStatus(); - UpdateJobsAvailable(); - } - - private void UpdateLateJoinStatus() - { - var msg = new MsgTickerLateJoinStatus(null!) {Disallowed = DisallowLateJoin}; - _netManager.ServerSendToAll(msg); + _postInitialized = true; } private void SendServerMessage(string message) @@ -368,762 +66,10 @@ namespace Content.Server.GameTicking IoCManager.Resolve().ServerSendToAll(msg); } - private HumanoidCharacterProfile GetPlayerProfile(IPlayerSession p) + public override void Update(float frameTime) { - return (HumanoidCharacterProfile) _prefsManager.GetPreferences(p.UserId).SelectedCharacter; - } - - public void EndRound(string roundEndText = "") - { - DebugTools.Assert(RunLevel == GameRunLevel.InRound); - Logger.InfoS("ticker", "Ending round!"); - - RunLevel = GameRunLevel.PostRound; - - //Tell every client the round has ended. - var roundEndMessage = _netManager.CreateNetMessage(); - roundEndMessage.GamemodeTitle = Preset?.ModeTitle ?? string.Empty; - roundEndMessage.RoundEndText = roundEndText + $"\n{Preset?.GetRoundEndDescription() ?? string.Empty}"; - - //Get the timespan of the round. - roundEndMessage.RoundDuration = IoCManager.Resolve().RealTime.Subtract(_roundStartTimeSpan); - - //Generate a list of basic player info to display in the end round summary. - var listOfPlayerInfo = new List(); - 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 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 == PlayerStatus.Observer, - }; - listOfPlayerInfo.Add(playerEndRoundInfo); - } - } - - roundEndMessage.AllPlayersEndInfo = listOfPlayerInfo; - _netManager.ServerSendToAll(roundEndMessage); - } - - public void Respawn(IPlayerSession targetPlayer) - { - targetPlayer.ContentData()?.WipeMind(); - - if (LobbyEnabled) - _playerJoinLobby(targetPlayer); - else - SpawnPlayer(targetPlayer); - } - - public void MakeObserve(IPlayerSession player) - { - if (!_playersInLobby.ContainsKey(player)) return; - - _spawnObserver(player); - _playersInLobby[player] = PlayerStatus.Observer; - _netManager.ServerSendToAll(GetStatusSingle(player, PlayerStatus.Observer)); - } - - public void MakeJoinGame(IPlayerSession player, string? jobId = null) - { - if (!_playersInLobby.ContainsKey(player)) return; - - if (!_prefsManager.HavePreferencesLoaded(player)) - { - return; - } - - SpawnPlayer(player, jobId); - } - - public void ToggleReady(IPlayerSession player, bool ready) - { - if (!_playersInLobby.ContainsKey(player)) return; - - if (!_prefsManager.HavePreferencesLoaded(player)) - { - return; - } - - var status = ready ? PlayerStatus.Ready : PlayerStatus.NotReady; - _playersInLobby[player] = ready ? PlayerStatus.Ready : PlayerStatus.NotReady; - _netManager.ServerSendMessage(_getStatusMsg(player), player.ConnectedClient); - _netManager.ServerSendToAll(GetStatusSingle(player, status)); - } - - public void ToggleDisallowLateJoin(bool disallowLateJoin) - { - DisallowLateJoin = disallowLateJoin; - UpdateLateJoinStatus(); - UpdateJobsAvailable(); - } - - public bool OnGhostAttempt(Mind.Mind mind, bool canReturnGlobal) - { - return Preset?.OnGhostAttempt(mind, canReturnGlobal) ?? false; - } - - public T AddGameRule() where T : GameRule, new() - { - var instance = _dynamicTypeFactory.CreateInstance(); - - _gameRules.Add(instance); - instance.Added(); - - OnRuleAdded?.Invoke(new GameRuleAddedEventArgs(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 IEnumerable ActiveGameRules => _gameRules; - - 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) - { - 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); - } - - public bool DelayStart(TimeSpan time) - { - if (_runLevel != GameRunLevel.PreRoundLobby) - { - return false; - } - - _roundStartTime += time; - - var lobbyCountdownMessage = _netManager.CreateNetMessage(); - lobbyCountdownMessage.StartTime = _roundStartTime; - lobbyCountdownMessage.Paused = Paused; - _netManager.ServerSendToAll(lobbyCountdownMessage); - - _chatManager.DispatchServerAnnouncement($"Round start has been delayed for {time.TotalSeconds} seconds."); - - return true; - } - - 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; - } - - var lobbyCountdownMessage = _netManager.CreateNetMessage(); - lobbyCountdownMessage.StartTime = _roundStartTime; - lobbyCountdownMessage.Paused = Paused; - _netManager.ServerSendToAll(lobbyCountdownMessage); - - _chatManager.DispatchServerAnnouncement(Paused - ? "Round start has been paused." - : "Round start countdown is now resumed."); - - return true; - } - - public bool TogglePause() - { - PauseStart(!Paused); - return Paused; - } - - 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(job.StartingGear); - EquipStartingGear(entity, startingGear, profile); - } - - if (profile != null) - { - entity.GetComponent().UpdateFromProfile(profile); - entity.Name = profile.Name; - } - - return entity; - } - - public void EquipStartingGear(IEntity entity, StartingGearPrototype startingGear, HumanoidCharacterProfile? profile) - { - if (entity.TryGetComponent(out InventoryComponent? inventory)) - { - foreach (var slot in AllSlots) - { - var equipmentStr = startingGear.GetGear(slot, profile); - if (equipmentStr != "") - { - var equipmentEntity = _entityManager.SpawnEntity(equipmentStr, entity.Transform.Coordinates); - inventory.Equip(slot, equipmentEntity.GetComponent()); - } - } - } - - 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(), hand); - } - } - } - - private IEntity _spawnObserverMob() - { - var coordinates = GetObserverSpawnPoint(); - return _entityManager.SpawnEntity(ObserverPrototypeName, coordinates); - } - - public EntityCoordinates GetLateJoinSpawnPoint() - { - var location = _spawnPoint; - - var possiblePoints = new List(); - foreach (var entity in _entityManager.GetEntities(new TypeEntityQuery(typeof(SpawnPointComponent)))) - { - var point = entity.GetComponent(); - if (point.SpawnType == SpawnPointType.LateJoin) possiblePoints.Add(entity.Transform.Coordinates); - } - - if (possiblePoints.Count != 0) location = _robustRandom.Pick(possiblePoints); - - return location; - } - - public EntityCoordinates GetJobSpawnPoint(string jobId) - { - var location = _spawnPoint; - - var possiblePoints = new List(); - foreach (var entity in _entityManager.GetEntities(new TypeEntityQuery(typeof(SpawnPointComponent)))) - { - var point = entity.GetComponent(); - if (point.SpawnType == SpawnPointType.Job && point.Job?.ID == jobId) - possiblePoints.Add(entity.Transform.Coordinates); - } - - if (possiblePoints.Count != 0) location = _robustRandom.Pick(possiblePoints); - - return location; - } - - public EntityCoordinates GetObserverSpawnPoint() - { - var location = _spawnPoint; - - var possiblePoints = new List(); - foreach (var entity in _entityManager.GetEntities(new TypeEntityQuery(typeof(SpawnPointComponent)))) - { - var point = entity.GetComponent(); - if (point.SpawnType == SpawnPointType.Observer) - possiblePoints.Add(entity.Transform.Coordinates); - } - - if (possiblePoints.Count != 0) location = _robustRandom.Pick(possiblePoints); - - return location; - } - - /// - /// Cleanup that has to run to clear up anything from the previous round. - /// Stuff like wiping the previous map clean. - /// - 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; - } - - private string GetMap() - { - return _configurationManager.GetCVar(CCVars.GameMap); - } - - private void _preRoundSetup() - { - DefaultMap = _mapManager.CreateMap(); - var startTime = _gameTiming.RealTime; - var map = GetMap(); - 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."); - } - - protected override void PlayerStatusChanged(object? sender, SessionStatusEventArgs args) - { - base.PlayerStatusChanged(sender, args); - - var session = args.Session; - - switch (args.NewStatus) - { - case SessionStatus.Connecting: - // Cancel shutdown update timer in progress. - _updateShutdownCts?.Cancel(); - break; - - case SessionStatus.Connected: - { - _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); - } - } - - /// - /// 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. - /// - 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 != null && !_updateShutdownCts.IsCancellationRequested) - { - // 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); - } - - private void SpawnPlayer(IPlayerSession session, string? jobId = null, bool lateJoin = true) - { - var character = GetPlayerProfile(session); - - SpawnPlayer(session, character, jobId, lateJoin); - UpdateJobsAvailable(); - } - - private void SpawnPlayer(IPlayerSession session, - HumanoidCharacterProfile character, - string? jobId = null, - bool lateJoin = true) - { - if (lateJoin && DisallowLateJoin) - { - MakeObserve(session); - return; - } - - _playerJoinGame(session); - - var data = session.ContentData(); - - DebugTools.AssertNotNull(data); - - data!.WipeMind(); - data.Mind = new Mind.Mind(session.UserId) - { - CharacterName = character.Name - }; - - // Pick best job best on prefs. - jobId ??= PickBestAvailableJob(character); - - var jobPrototype = _prototypeManager.Index(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 (session.UserId == new Guid("{e887eb93-f503-4b65-95b6-2f282c014192}")) - { - mob.AddComponent(); - } - - AddManifestEntry(character.Name, jobId); - AddSpawnedPosition(jobId); - EquipIdCard(mob, character.Name, jobPrototype); - jobPrototype.Special?.AfterEquip(mob); - - Preset?.OnSpawnPlayerCompleted(session, mob, lateJoin); - } - - private void EquipIdCard(IEntity mob, string characterName, JobPrototype jobPrototype) - { - var inventory = mob.GetComponent(); - - if (!inventory.TryGetSlotItem(Slots.IDCARD, out ItemComponent? pdaItem)) - { - return; - } - - var pda = pdaItem.Owner; - - var pdaComponent = pda.GetComponent(); - if (pdaComponent.ContainedID == null) - { - return; - } - - var card = pdaComponent.ContainedID; - card.FullName = characterName; - card.JobTitle = jobPrototype.Name; - - var access = card.Owner.GetComponent(); - var accessTags = access.Tags; - accessTags.UnionWith(jobPrototype.Access); - pdaComponent.SetPDAOwner(characterName); - } - - private void AddManifestEntry(string characterName, string jobId) - { - _manifest.Add(new ManifestEntry(characterName, jobId)); - } - - private void _spawnObserver(IPlayerSession session) - { - _playerJoinGame(session); - - var name = GetPlayerProfile(session).Name; - - var data = session.ContentData(); - - DebugTools.AssertNotNull(data); - - data!.WipeMind(); - data.Mind = new Mind.Mind(session.UserId); - - var mob = _spawnObserverMob(); - mob.Name = name; - mob.GetComponent().CanReturnToBody = false; - data.Mind.TransferTo(mob); - } - - private void _playerJoinLobby(IPlayerSession session) - { - _playersInLobby[session] = PlayerStatus.NotReady; - - _netManager.ServerSendMessage(_netManager.CreateNetMessage(), session.ConnectedClient); - _netManager.ServerSendMessage(_getStatusMsg(session), session.ConnectedClient); - _netManager.ServerSendMessage(GetInfoMsg(), session.ConnectedClient); - _netManager.ServerSendMessage(GetPlayerStatus(), session.ConnectedClient); - _netManager.ServerSendMessage(GetJobsAvailable(), session.ConnectedClient); - } - - 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); - - _netManager.ServerSendMessage(_netManager.CreateNetMessage(), session.ConnectedClient); - } - - private MsgTickerLobbyReady GetPlayerStatus() - { - var msg = _netManager.CreateNetMessage(); - msg.PlayerStatus = new Dictionary(); - foreach (var player in _playersInLobby.Keys) - { - _playersInLobby.TryGetValue(player, out var status); - msg.PlayerStatus.Add(player.UserId, status); - } - return msg; - } - - private MsgTickerJobsAvailable GetJobsAvailable() - { - var message = _netManager.CreateNetMessage(); - - // If late join is disallowed, return no available jobs. - if (DisallowLateJoin) - return message; - - message.JobsAvailable = GetAvailablePositions() - .Where(e => e.Value > 0) - .Select(e => e.Key) - .ToArray(); - - return message; - } - - private MsgTickerLobbyReady GetStatusSingle(IPlayerSession player, PlayerStatus status) - { - var msg = _netManager.CreateNetMessage(); - msg.PlayerStatus = new Dictionary - { - { player.UserId, status } - }; - return msg; - } - - private MsgTickerLobbyStatus _getStatusMsg(IPlayerSession session) - { - _playersInLobby.TryGetValue(session, out var status); - var msg = _netManager.CreateNetMessage(); - msg.IsRoundStarted = RunLevel != GameRunLevel.PreRoundLobby; - msg.StartTime = _roundStartTime; - msg.YouAreReady = status == PlayerStatus.Ready; - msg.Paused = Paused; - msg.LobbySong = LobbySong; - return msg; - } - - private MsgTickerLobbyInfo GetInfoMsg() - { - var msg = _netManager.CreateNetMessage(); - msg.TextBlob = GetInfoText(); - return msg; - } - - private void _sendStatusToAll() - { - foreach (var player in _playersInLobby.Keys) - _netManager.ServerSendMessage(_getStatusMsg(player), player.ConnectedClient); - } - - 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 void UpdateInfoText() - { - var infoMsg = GetInfoMsg(); - - _netManager.ServerSendToMany(infoMsg, _playersInLobby.Keys.Select(p => p.ConnectedClient).ToList()); - } - - private GamePreset MakeGamePreset(Dictionary readyProfiles) - { - var preset = _dynamicTypeFactory.CreateInstance(_presetType ?? typeof(PresetSandbox)); - preset.ReadyProfiles = readyProfiles; - return preset; + base.Update(frameTime); + UpdateRoundFlow(frameTime); } [Dependency] private readonly IEntityManager _entityManager = default!; @@ -1142,33 +88,4 @@ The current game mode is: [color=white]{0}[/color]. [Dependency] private readonly IEntitySystemManager _entitySystemManager = default!; [Dependency] private readonly IReflectionManager _reflectionManager = default!; } - - public enum GameRunLevel - { - PreRoundLobby = 0, - InRound = 1, - PostRound = 2 - } - - public class GameRunLevelChangedEventArgs : EventArgs - { - public GameRunLevelChangedEventArgs(GameRunLevel oldRunLevel, GameRunLevel newRunLevel) - { - OldRunLevel = oldRunLevel; - NewRunLevel = newRunLevel; - } - - public GameRunLevel OldRunLevel { get; } - public GameRunLevel NewRunLevel { get; } - } - - public class GameRuleAddedEventArgs : EventArgs - { - public GameRule GameRule { get; } - - public GameRuleAddedEventArgs(GameRule rule) - { - GameRule = rule; - } - } } diff --git a/Content.Server/GameTicking/GameTickerBase.cs b/Content.Server/GameTicking/GameTickerBase.cs deleted file mode 100644 index 56eea0343b..0000000000 --- a/Content.Server/GameTicking/GameTickerBase.cs +++ /dev/null @@ -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 -{ - /// - /// Handles some low-level GameTicker behavior such as setting up clients when they connect. - /// Does not contain lobby/round handling mechanisms. - /// - 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); - } - } - } -} diff --git a/Content.Server/GameTicking/IGameTicker.cs b/Content.Server/GameTicking/IGameTicker.cs deleted file mode 100644 index f33835da15..0000000000 --- a/Content.Server/GameTicking/IGameTicker.cs +++ /dev/null @@ -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 -{ - /// - /// The game ticker is responsible for managing the round-by-round system of the game. - /// - public interface IGameTicker - { - GameRunLevel RunLevel { get; } - - /// - /// The map loaded by the GameTicker on round start. - /// - MapId DefaultMap { get; } - - /// - /// The GridId loaded by the GameTicker on round start. - /// - GridId DefaultGridId { get; } - - event Action OnRunLevelChanged; - event Action 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); - - /// proxy to GamePreset (actual handler) - 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() where T : GameRule, new(); - bool HasGameRule(string? type); - bool HasGameRule(Type? type); - void RemoveGameRule(GameRule rule); - IEnumerable 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); - - /// true if changed, false otherwise - bool PauseStart(bool pause = true); - - /// true if paused, false otherwise - bool TogglePause(); - - bool DelayStart(TimeSpan time); - - Dictionary GetAvailablePositions(); - } -} diff --git a/Content.Server/GameTicking/Presets/GamePreset.cs b/Content.Server/GameTicking/Presets/GamePreset.cs index 3fe7a83797..99a4dfaebb 100644 --- a/Content.Server/GameTicking/Presets/GamePreset.cs +++ b/Content.Server/GameTicking/Presets/GamePreset.cs @@ -45,7 +45,7 @@ namespace Content.Server.GameTicking.Presets mind.UnVisit(); } - var position = playerEntity?.Transform.Coordinates ?? IoCManager.Resolve().GetObserverSpawnPoint(); + var position = playerEntity?.Transform.Coordinates ?? EntitySystem.Get().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) diff --git a/Content.Server/GameTicking/Presets/PresetDeathMatch.cs b/Content.Server/GameTicking/Presets/PresetDeathMatch.cs index a50e434245..39844f7362 100644 --- a/Content.Server/GameTicking/Presets/PresetDeathMatch.cs +++ b/Content.Server/GameTicking/Presets/PresetDeathMatch.cs @@ -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 readyPlayers, bool force = false) { - _gameTicker.AddGameRule(); + EntitySystem.Get().AddGameRule(); return true; } diff --git a/Content.Server/GameTicking/Presets/PresetSuspicion.cs b/Content.Server/GameTicking/Presets/PresetSuspicion.cs index 59d3a83ffc..195e5aceac 100644 --- a/Content.Server/GameTicking/Presets/PresetSuspicion.cs +++ b/Content.Server/GameTicking/Presets/PresetSuspicion.cs @@ -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(); + EntitySystem.Get().AddGameRule(); return true; } diff --git a/Content.Server/GameTicking/Presets/PresetTraitor.cs b/Content.Server/GameTicking/Presets/PresetTraitor.cs index c551356612..ae6d1daf3f 100644 --- a/Content.Server/GameTicking/Presets/PresetTraitor.cs +++ b/Content.Server/GameTicking/Presets/PresetTraitor.cs @@ -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(); + EntitySystem.Get().AddGameRule(); return true; } diff --git a/Content.Server/GameTicking/Presets/PresetTraitorDeathMatch.cs b/Content.Server/GameTicking/Presets/PresetTraitorDeathMatch.cs index aab1512755..0192bac8cf 100644 --- a/Content.Server/GameTicking/Presets/PresetTraitorDeathMatch.cs +++ b/Content.Server/GameTicking/Presets/PresetTraitorDeathMatch.cs @@ -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 readyPlayers, bool force = false) { - _gameTicker.AddGameRule(); - _restarter = _gameTicker.AddGameRule(); + var gameTicker = EntitySystem.Get(); + gameTicker.AddGameRule(); + _restarter = gameTicker.AddGameRule(); _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().Respawn(session); return true; } diff --git a/Content.Server/GameTicking/Rules/GameRule.cs b/Content.Server/GameTicking/Rules/GameRule.cs index fff89c6368..4d89345ac4 100644 --- a/Content.Server/GameTicking/Rules/GameRule.cs +++ b/Content.Server/GameTicking/Rules/GameRule.cs @@ -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() { diff --git a/Content.Server/GameTicking/Rules/RuleDeathMatch.cs b/Content.Server/GameTicking/Rules/RuleDeathMatch.cs index d87bf3ceb7..e49400a68d 100644 --- a/Content.Server/GameTicking/Rules/RuleDeathMatch.cs +++ b/Content.Server/GameTicking/Rules/RuleDeathMatch.cs @@ -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().RestartRound()); } private void PlayerManagerOnPlayerStatusChanged(object? sender, SessionStatusEventArgs e) diff --git a/Content.Server/GameTicking/Rules/RuleInactivityTimeRestart.cs b/Content.Server/GameTicking/Rules/RuleInactivityTimeRestart.cs index fed121072e..1f72ba86cb 100644 --- a/Content.Server/GameTicking/Rules/RuleInactivityTimeRestart.cs +++ b/Content.Server/GameTicking/Rules/RuleInactivityTimeRestart.cs @@ -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(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.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().RunLevel != GameRunLevel.InRound) { return; } diff --git a/Content.Server/GameTicking/Rules/RuleMaxTimeRestart.cs b/Content.Server/GameTicking/Rules/RuleMaxTimeRestart.cs index ded90f7f72..ec3f7a6fc1 100644 --- a/Content.Server/GameTicking/Rules/RuleMaxTimeRestart.cs +++ b/Content.Server/GameTicking/Rules/RuleMaxTimeRestart.cs @@ -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(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().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().RestartRound()); } - private void RunLevelChanged(GameRunLevelChangedEventArgs args) + private void RunLevelChanged(GameRunLevelChangedEvent args) { - switch (args.NewRunLevel) + switch (args.New) { case GameRunLevel.InRound: RestartTimer(); diff --git a/Content.Server/GameTicking/Rules/RuleSuspicion.cs b/Content.Server/GameTicking/Rules/RuleSuspicion.cs index 96f190f4d6..82107e6fdd 100644 --- a/Content.Server/GameTicking/Rules/RuleSuspicion.cs +++ b/Content.Server/GameTicking/Rules/RuleSuspicion.cs @@ -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.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()); } } } diff --git a/Content.Server/Ghost/Components/GhostOnMoveComponent.cs b/Content.Server/Ghost/Components/GhostOnMoveComponent.cs index d1155e2775..3153effde2 100644 --- a/Content.Server/Ghost/Components/GhostOnMoveComponent.cs +++ b/Content.Server/Ghost/Components/GhostOnMoveComponent.cs @@ -14,7 +14,6 @@ namespace Content.Server.Ghost.Components public class GhostOnMoveComponent : Component, IRelayMoveInput, IGhostOnMove { public override string Name => "GhostOnMove"; - [Dependency] private readonly IGameTicker _gameTicker = default!; [DataField("canReturn")] public bool CanReturn { get; set; } = true; @@ -24,7 +23,7 @@ namespace Content.Server.Ghost.Components if (Owner.HasComponent()) return; if (!Owner.TryGetComponent(out MindComponent? mind) || !mind.HasMind || mind.Mind!.IsVisitingEntity) return; - _gameTicker.OnGhostAttempt(mind.Mind!, CanReturn); + EntitySystem.Get().OnGhostAttempt(mind.Mind!, CanReturn); } } } diff --git a/Content.Server/Ghost/Ghost.cs b/Content.Server/Ghost/Ghost.cs index 15cc1ca265..60395091a6 100644 --- a/Content.Server/Ghost/Ghost.cs +++ b/Content.Server/Ghost/Ghost.cs @@ -4,6 +4,7 @@ using Content.Server.GameTicking; using Content.Server.Players; using Robust.Server.Player; using Robust.Shared.Console; +using Robust.Shared.GameObjects; using Robust.Shared.IoC; namespace Content.Server.Ghost @@ -31,7 +32,7 @@ namespace Content.Server.Ghost return; } - if (!IoCManager.Resolve().OnGhostAttempt(mind, true)) + if (!EntitySystem.Get().OnGhostAttempt(mind, true)) { shell?.WriteLine("You can't ghost right now."); return; diff --git a/Content.Server/Holiday/HolidayManager.cs b/Content.Server/Holiday/HolidayManager.cs index 5fe12b523a..16c6772850 100644 --- a/Content.Server/Holiday/HolidayManager.cs +++ b/Content.Server/Holiday/HolidayManager.cs @@ -6,6 +6,7 @@ using Content.Server.Holiday.Interfaces; using Content.Shared; using Content.Shared.CCVar; using Robust.Shared.Configuration; +using Robust.Shared.GameObjects; using Robust.Shared.IoC; using Robust.Shared.Prototypes; using Robust.Shared.ViewVariables; @@ -13,11 +14,11 @@ using Robust.Shared.ViewVariables; namespace Content.Server.Holiday { // ReSharper disable once ClassNeverInstantiated.Global - public class HolidayManager : IHolidayManager + public class HolidayManager : IHolidayManager, IEntityEventSubscriber { [Dependency] private readonly IConfigurationManager _configManager = default!; [Dependency] private readonly IPrototypeManager _prototypeManager = default!; - [Dependency] private readonly IGameTicker _gameTicker = default!; + [Dependency] private readonly IEntityManager _entityManager = default!; [Dependency] private readonly IChatManager _chatManager = default!; [ViewVariables] @@ -74,7 +75,7 @@ namespace Content.Server.Holiday { _configManager.OnValueChanged(CCVars.HolidaysEnabled, OnHolidaysEnableChange, true); - _gameTicker.OnRunLevelChanged += OnRunLevelChanged; + _entityManager.EventBus.SubscribeEvent(EventSource.Local, this, OnRunLevelChanged); } private void OnHolidaysEnableChange(bool enabled) @@ -84,11 +85,11 @@ namespace Content.Server.Holiday RefreshCurrentHolidays(); } - private void OnRunLevelChanged(GameRunLevelChangedEventArgs eventArgs) + private void OnRunLevelChanged(GameRunLevelChangedEvent eventArgs) { if (!_enabled) return; - switch (eventArgs.NewRunLevel) + switch (eventArgs.New) { case GameRunLevel.PreRoundLobby: RefreshCurrentHolidays(); diff --git a/Content.Server/IoC/ServerContentIoC.cs b/Content.Server/IoC/ServerContentIoC.cs index 66782d5f8e..3a9682d0b3 100644 --- a/Content.Server/IoC/ServerContentIoC.cs +++ b/Content.Server/IoC/ServerContentIoC.cs @@ -39,7 +39,6 @@ namespace Content.Server.IoC { IoCManager.Register(); IoCManager.Register(); - IoCManager.Register(); IoCManager.Register(); IoCManager.Register(); IoCManager.Register(); diff --git a/Content.Server/Mind/Components/MindComponent.cs b/Content.Server/Mind/Components/MindComponent.cs index 2c2169a0f8..4b27a312a5 100644 --- a/Content.Server/Mind/Components/MindComponent.cs +++ b/Content.Server/Mind/Components/MindComponent.cs @@ -77,7 +77,7 @@ namespace Content.Server.Mind.Components base.Shutdown(); // Let's not create ghosts if not in the middle of the round. - if (IoCManager.Resolve().RunLevel != GameRunLevel.InRound) + if (EntitySystem.Get().RunLevel != GameRunLevel.InRound) return; if (HasMind) @@ -104,7 +104,7 @@ namespace Content.Server.Mind.Components var gridId = spawnPosition.GetGridId(Owner.EntityManager); if (gridId == GridId.Invalid || !mapMan.GridExists(gridId)) { - spawnPosition = IoCManager.Resolve().GetObserverSpawnPoint(); + spawnPosition = EntitySystem.Get().GetObserverSpawnPoint(); } var ghost = Owner.EntityManager.SpawnEntity("MobObserver", spawnPosition); diff --git a/Content.Server/Mind/MindHelpers.cs b/Content.Server/Mind/MindHelpers.cs index 5a6f4bfacb..cc92ac1cdb 100644 --- a/Content.Server/Mind/MindHelpers.cs +++ b/Content.Server/Mind/MindHelpers.cs @@ -14,7 +14,7 @@ namespace Content.Server.Mind if (mind == null) return; - IoCManager.Resolve().OnGhostAttempt(mind, canReturn); + EntitySystem.Get().OnGhostAttempt(mind, canReturn); } } } diff --git a/Content.Server/Morgue/Components/CrematoriumEntityStorageComponent.cs b/Content.Server/Morgue/Components/CrematoriumEntityStorageComponent.cs index cc0753483e..a7aa8edd62 100644 --- a/Content.Server/Morgue/Components/CrematoriumEntityStorageComponent.cs +++ b/Content.Server/Morgue/Components/CrematoriumEntityStorageComponent.cs @@ -126,7 +126,7 @@ namespace Content.Server.Morgue.Components if (mind != null) { - IoCManager.Resolve().OnGhostAttempt(mind, false); + EntitySystem.Get().OnGhostAttempt(mind, false); mind.OwnedEntity?.PopupMessage(Loc.GetString("You cremate yourself!")); } diff --git a/Content.Server/Players/PlayerSessionExt.cs b/Content.Server/Players/PlayerSessionExt.cs deleted file mode 100644 index 35652f17b6..0000000000 --- a/Content.Server/Players/PlayerSessionExt.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Content.Shared.GameWindow; -using Robust.Server.Player; - -namespace Content.Server.Players -{ - public static class PlayerSessionExt - { - public static void RequestWindowAttention(this IPlayerSession session) - { - var msg = session.ConnectedClient.CreateNetMessage(); - session.ConnectedClient.SendMessage(msg); - } - } -} diff --git a/Content.Server/Recycling/Components/RecyclerComponent.cs b/Content.Server/Recycling/Components/RecyclerComponent.cs index e5582c0d16..d7f95359e5 100644 --- a/Content.Server/Recycling/Components/RecyclerComponent.cs +++ b/Content.Server/Recycling/Components/RecyclerComponent.cs @@ -153,7 +153,7 @@ namespace Content.Server.Recycling.Components if (mind != null) { - IoCManager.Resolve().OnGhostAttempt(mind, false); + EntitySystem.Get().OnGhostAttempt(mind, false); mind.OwnedEntity?.PopupMessage(Loc.GetString("You recycle yourself!")); } diff --git a/Content.Server/RoundEnd/RoundEndSystem.cs b/Content.Server/RoundEnd/RoundEndSystem.cs index 242a14b79a..b8cd6d7bcd 100644 --- a/Content.Server/RoundEnd/RoundEndSystem.cs +++ b/Content.Server/RoundEnd/RoundEndSystem.cs @@ -15,7 +15,6 @@ namespace Content.Server.RoundEnd { public class RoundEndSystem : EntitySystem, IResettingEntitySystem { - [Dependency] private readonly IGameTicker _gameTicker = default!; [Dependency] private readonly IGameTiming _gameTiming = default!; [Dependency] private readonly IChatManager _chatManager = default!; @@ -120,11 +119,12 @@ namespace Content.Server.RoundEnd private void EndRound() { OnRoundEndCountdownFinished?.Invoke(); - _gameTicker.EndRound(); + var gameTicker = EntitySystem.Get(); + gameTicker.EndRound(); _chatManager.DispatchServerAnnouncement(Loc.GetString("Restarting the round in {0} seconds...", RestartRoundTime)); - Timer.Spawn(TimeSpan.FromSeconds(RestartRoundTime), () => _gameTicker.RestartRound(), CancellationToken.None); + Timer.Spawn(TimeSpan.FromSeconds(RestartRoundTime), () => gameTicker.RestartRound(), CancellationToken.None); } } } diff --git a/Content.Server/Sandbox/SandboxManager.cs b/Content.Server/Sandbox/SandboxManager.cs index a3eabcd86b..8f4b1ad5e0 100644 --- a/Content.Server/Sandbox/SandboxManager.cs +++ b/Content.Server/Sandbox/SandboxManager.cs @@ -21,11 +21,10 @@ using static Content.Shared.Inventory.EquipmentSlotDefines; namespace Content.Server.Sandbox { - internal sealed class SandboxManager : SharedSandboxManager, ISandboxManager + internal sealed class SandboxManager : SharedSandboxManager, ISandboxManager, IEntityEventSubscriber { [Dependency] private readonly IPlayerManager _playerManager = default!; [Dependency] private readonly IServerNetManager _netManager = default!; - [Dependency] private readonly IGameTicker _gameTicker = default!; [Dependency] private readonly IPlacementManager _placementManager = default!; [Dependency] private readonly IConGroupController _conGroupController = default!; [Dependency] private readonly IEntityManager _entityManager = default!; @@ -53,7 +52,7 @@ namespace Content.Server.Sandbox _netManager.RegisterNetMessage(nameof(MsgSandboxSuicide), SandboxSuicideReceived); _playerManager.PlayerStatusChanged += OnPlayerStatusChanged; - _gameTicker.OnRunLevelChanged += GameTickerOnOnRunLevelChanged; + _entityManager.EventBus.SubscribeEvent(EventSource.Local, this, GameTickerOnOnRunLevelChanged); _placementManager.AllowPlacementFunc = placement => { @@ -74,10 +73,10 @@ namespace Content.Server.Sandbox }; } - private void GameTickerOnOnRunLevelChanged(GameRunLevelChangedEventArgs obj) + private void GameTickerOnOnRunLevelChanged(GameRunLevelChangedEvent obj) { // Automatically clear sandbox state when round resets. - if (obj.NewRunLevel == GameRunLevel.PreRoundLobby) + if (obj.New == GameRunLevel.PreRoundLobby) { IsSandboxEnabled = false; } @@ -103,7 +102,7 @@ namespace Content.Server.Sandbox } var player = _playerManager.GetSessionByChannel(message.MsgChannel); - _gameTicker.Respawn(player); + EntitySystem.Get().Respawn(player); } private void SandboxGiveAccessReceived(MsgSandboxGiveAccess message) diff --git a/Content.Server/Shell/StatusShell.cs b/Content.Server/Shell/StatusShell.cs deleted file mode 100644 index 73fbe2a4a5..0000000000 --- a/Content.Server/Shell/StatusShell.cs +++ /dev/null @@ -1,59 +0,0 @@ -using System; -using Content.Server.GameTicking; -using Newtonsoft.Json.Linq; -using Robust.Server; -using Robust.Server.Player; -using Robust.Server.ServerStatus; -using Robust.Shared.IoC; - -namespace Content.Server.Shell -{ - /// - /// Tiny helper class to handle status messages. Nothing too complicated. - /// - public class StatusShell - { - private readonly IPlayerManager _playerManager; - private readonly string _name; - private GameRunLevel _runLevel; - private DateTime _roundStartTime; - - public StatusShell() - { - _playerManager = IoCManager.Resolve(); - var baseServer = IoCManager.Resolve(); - var gameTicker = IoCManager.Resolve(); - - gameTicker.OnRunLevelChanged += _runLevelChanged; - - _name = baseServer.ServerName; - IoCManager.Resolve().OnStatusRequest += _getResponse; - } - - private void _getResponse(JObject jObject) - { - lock (this) - { - jObject["name"] = _name; - jObject["players"] = _playerManager.PlayerCount; - jObject["run_level"] = (int) _runLevel; - if (_runLevel >= GameRunLevel.InRound) - { - jObject["round_start_time"] = _roundStartTime.ToString("o"); - } - } - } - - private void _runLevelChanged(GameRunLevelChangedEventArgs eventArgs) - { - lock (this) - { - _runLevel = eventArgs.NewRunLevel; - if (eventArgs.NewRunLevel == GameRunLevel.InRound) - { - _roundStartTime = DateTime.UtcNow; - } - } - } - } -} diff --git a/Content.Server/Spawners/Components/ConditionalSpawnerComponent.cs b/Content.Server/Spawners/Components/ConditionalSpawnerComponent.cs index 5a84ca1db3..4c9936494c 100644 --- a/Content.Server/Spawners/Components/ConditionalSpawnerComponent.cs +++ b/Content.Server/Spawners/Components/ConditionalSpawnerComponent.cs @@ -12,7 +12,6 @@ namespace Content.Server.Spawners.Components [RegisterComponent] public class ConditionalSpawnerComponent : Component, IMapInit { - [Dependency] private readonly IGameTicker _gameTicker = default!; [Dependency] private readonly IRobustRandom _robustRandom = default!; public override string Name => "ConditionalSpawner"; @@ -29,9 +28,9 @@ namespace Content.Server.Spawners.Components [DataField("chance")] public float Chance { get; set; } = 1.0f; - private void RuleAdded(GameRuleAddedEventArgs obj) + public void RuleAdded(GameRuleAddedEvent obj) { - if(_gameRules.Contains(obj.GameRule.GetType().Name)) + if(_gameRules.Contains(obj.Rule.GetType().Name)) Spawn(); } @@ -45,7 +44,7 @@ namespace Content.Server.Spawners.Components foreach (var rule in _gameRules) { - if (!_gameTicker.HasGameRule(rule)) continue; + if (!EntitySystem.Get().HasGameRule(rule)) continue; Spawn(); return; } @@ -68,8 +67,6 @@ namespace Content.Server.Spawners.Components public virtual void MapInit() { - _gameTicker.OnRuleAdded += RuleAdded; - TrySpawn(); } } diff --git a/Content.Server/Spawners/EntitySystems/ConditionalSpawnerSystem.cs b/Content.Server/Spawners/EntitySystems/ConditionalSpawnerSystem.cs new file mode 100644 index 0000000000..19f14925ee --- /dev/null +++ b/Content.Server/Spawners/EntitySystems/ConditionalSpawnerSystem.cs @@ -0,0 +1,26 @@ +using Content.Server.GameTicking; +using Content.Server.Spawners.Components; +using JetBrains.Annotations; +using Robust.Shared.GameObjects; + +namespace Content.Server.Spawners.EntitySystems +{ + [UsedImplicitly] + public class ConditionalSpawnerSystem : EntitySystem + { + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnRuleAdded); + } + + private void OnRuleAdded(GameRuleAddedEvent args) + { + foreach (var spawner in ComponentManager.EntityQuery()) + { + spawner.RuleAdded(args); + } + } + } +} diff --git a/Content.Server/StationEvents/Events/GasLeak.cs b/Content.Server/StationEvents/Events/GasLeak.cs index 5b7238ee65..1b1a759993 100644 --- a/Content.Server/StationEvents/Events/GasLeak.cs +++ b/Content.Server/StationEvents/Events/GasLeak.cs @@ -167,7 +167,7 @@ namespace Content.Server.StationEvents.Events private bool TryFindRandomTile(out Vector2i tile, IRobustRandom? robustRandom = null) { tile = default; - var defaultGridId = IoCManager.Resolve().DefaultGridId; + var defaultGridId = EntitySystem.Get().DefaultGridId; if (!IoCManager.Resolve().TryGetGrid(defaultGridId, out var grid) || !IoCManager.Resolve().TryGetEntity(grid.GridEntityId, out _targetGrid)) return false; diff --git a/Content.Server/StationEvents/Events/RadiationStorm.cs b/Content.Server/StationEvents/Events/RadiationStorm.cs index eeef4c3973..f4b6eae37f 100644 --- a/Content.Server/StationEvents/Events/RadiationStorm.cs +++ b/Content.Server/StationEvents/Events/RadiationStorm.cs @@ -66,8 +66,7 @@ namespace Content.Server.StationEvents.Events if (_timeUntilPulse <= 0.0f) { var pauseManager = IoCManager.Resolve(); - var gameTicker = IoCManager.Resolve(); - var defaultGrid = IoCManager.Resolve().GetGrid(gameTicker.DefaultGridId); + var defaultGrid = IoCManager.Resolve().GetGrid(EntitySystem.Get().DefaultGridId); if (pauseManager.IsGridPaused(defaultGrid)) return; diff --git a/Content.Server/StationEvents/StationEventSystem.cs b/Content.Server/StationEvents/StationEventSystem.cs index b38dbb62cb..60a2c9f7e3 100644 --- a/Content.Server/StationEvents/StationEventSystem.cs +++ b/Content.Server/StationEvents/StationEventSystem.cs @@ -30,7 +30,6 @@ namespace Content.Server.StationEvents [Dependency] private readonly IConfigurationManager _configurationManager = default!; [Dependency] private readonly IServerNetManager _netManager = default!; [Dependency] private readonly IPlayerManager _playerManager = default!; - [Dependency] private readonly IGameTicker _gameTicker = default!; [Dependency] private readonly IConGroupController _conGroupController = default!; [Dependency] private readonly IGameTiming _gameTiming = default!; [Dependency] private readonly IRobustRandom _random = default!; @@ -214,7 +213,7 @@ namespace Content.Server.StationEvents } // Stop events from happening in lobby and force active event to end if the round ends - if (_gameTicker.RunLevel != GameRunLevel.InRound) + if (Get().RunLevel != GameRunLevel.InRound) { if (CurrentEvent != null) { diff --git a/Content.Server/Voting/Managers/VoteManager.DefaultVotes.cs b/Content.Server/Voting/Managers/VoteManager.DefaultVotes.cs index 02a469a471..42ce63700e 100644 --- a/Content.Server/Voting/Managers/VoteManager.DefaultVotes.cs +++ b/Content.Server/Voting/Managers/VoteManager.DefaultVotes.cs @@ -1,9 +1,11 @@ #nullable enable using System; using System.Collections.Generic; +using Content.Server.GameTicking; using Content.Shared; using Content.Shared.CCVar; using Robust.Server.Player; +using Robust.Shared.GameObjects; using Robust.Shared.IoC; using Robust.Shared.Localization; using Robust.Shared.Random; @@ -46,7 +48,7 @@ namespace Content.Server.Voting.Managers if (votesYes / (float) total >= ratioRequired) { _chatManager.DispatchServerAnnouncement(Loc.GetString("ui-vote-restart-succeeded")); - _ticker.RestartRound(); + EntitySystem.Get().RestartRound(); } else { @@ -109,7 +111,7 @@ namespace Content.Server.Voting.Managers Loc.GetString("ui-vote-gamemode-win", ("winner", Loc.GetString(presets[picked])))); } - _ticker.SetStartPreset(picked); + EntitySystem.Get().SetStartPreset(picked); }; } } diff --git a/Content.Server/Voting/Managers/VoteManager.cs b/Content.Server/Voting/Managers/VoteManager.cs index 5f0cd8eea3..04a20d7e95 100644 --- a/Content.Server/Voting/Managers/VoteManager.cs +++ b/Content.Server/Voting/Managers/VoteManager.cs @@ -31,7 +31,6 @@ namespace Content.Server.Voting.Managers [Dependency] private readonly IGameTiming _timing = default!; [Dependency] private readonly IPlayerManager _playerManager = default!; [Dependency] private readonly IChatManager _chatManager = default!; - [Dependency] private readonly IGameTicker _ticker = default!; [Dependency] private readonly IAdminManager _adminMgr = default!; private int _nextVoteId = 1; diff --git a/Content.Shared/CCVar/CCVars.cs b/Content.Shared/CCVar/CCVars.cs index 08e2378b55..2729f229af 100644 --- a/Content.Shared/CCVar/CCVars.cs +++ b/Content.Shared/CCVar/CCVars.cs @@ -26,12 +26,18 @@ namespace Content.Shared.CCVar public static readonly CVarDef EventsEnabled = CVarDef.Create("events.enabled", false, CVar.ARCHIVE | CVar.SERVERONLY); + public static readonly CVarDef + GameDummyTicker = CVarDef.Create("game.dummyticker", false, CVar.ARCHIVE | CVar.SERVERONLY); + public static readonly CVarDef GameLobbyEnabled = CVarDef.Create("game.lobbyenabled", false, CVar.ARCHIVE); public static readonly CVarDef GameLobbyDuration = CVarDef.Create("game.lobbyduration", 60, CVar.ARCHIVE); + public static readonly CVarDef + GameDisallowLateJoins = CVarDef.Create("game.disallowlatejoins", false, CVar.ARCHIVE | CVar.SERVERONLY); + public static readonly CVarDef GameLobbyDefaultPreset = CVarDef.Create("game.defaultpreset", "Suspicion", CVar.ARCHIVE); diff --git a/Content.Shared/GameTicking/SharedGameTicker.cs b/Content.Shared/GameTicking/SharedGameTicker.cs index 596cffb12d..471935839b 100644 --- a/Content.Shared/GameTicking/SharedGameTicker.cs +++ b/Content.Shared/GameTicking/SharedGameTicker.cs @@ -2,278 +2,126 @@ using System; using System.Collections.Generic; -using System.IO; -using Lidgren.Network; -using Robust.Shared.IoC; +using Robust.Shared.GameObjects; using Robust.Shared.Network; using Robust.Shared.Serialization; namespace Content.Shared.GameTicking { - public abstract class SharedGameTicker + public abstract class SharedGameTicker : EntitySystem { // See ideally these would be pulled from the job definition or something. // But this is easier, and at least it isn't hardcoded. public const string OverflowJob = "Assistant"; public const string OverflowJobName = "assistant"; + } - protected class MsgTickerJoinLobby : NetMessage + [Serializable, NetSerializable] + public class TickerJoinLobbyEvent : EntityEventArgs + { + } + + [Serializable, NetSerializable] + public class TickerJoinGameEvent : EntityEventArgs + { + } + + [Serializable, NetSerializable] + public class TickerLateJoinStatusEvent : EntityEventArgs + { + // TODO: Make this a replicated CVar, honestly. + public bool Disallowed { get; } + + public TickerLateJoinStatusEvent(bool disallowed) { - #region REQUIRED - - public const MsgGroups GROUP = MsgGroups.Command; - public const string NAME = nameof(MsgTickerJoinLobby); - public MsgTickerJoinLobby(INetChannel channel) : base(NAME, GROUP) { } - - #endregion - - public override void ReadFromBuffer(NetIncomingMessage buffer) - { - } - - public override void WriteToBuffer(NetOutgoingMessage buffer) - { - } + Disallowed = disallowed; } + } - protected class MsgTickerJoinGame : NetMessage + + [Serializable, NetSerializable] + public class TickerLobbyStatusEvent : EntityEventArgs + { + public bool IsRoundStarted { get; } + public string? LobbySong { get; } + public bool YouAreReady { get; } + // UTC. + public TimeSpan StartTime { get; } + public bool Paused { get; } + + public TickerLobbyStatusEvent(bool isRoundStarted, string? lobbySong, bool youAreReady, TimeSpan startTime, bool paused) { - #region REQUIRED - - public const MsgGroups GROUP = MsgGroups.Command; - public const string NAME = nameof(MsgTickerJoinGame); - public MsgTickerJoinGame(INetChannel channel) : base(NAME, GROUP) { } - - #endregion - - public override void ReadFromBuffer(NetIncomingMessage buffer) - { - } - - public override void WriteToBuffer(NetOutgoingMessage buffer) - { - } + IsRoundStarted = isRoundStarted; + LobbySong = lobbySong; + YouAreReady = youAreReady; + StartTime = startTime; + Paused = paused; } + } - protected class MsgTickerLateJoinStatus : NetMessage + [Serializable, NetSerializable] + public class TickerLobbyInfoEvent : EntityEventArgs + { + public string TextBlob { get; } + + public TickerLobbyInfoEvent(string textBlob) { - #region REQUIRED - - public const MsgGroups GROUP = MsgGroups.Command; - public const string NAME = nameof(MsgTickerLateJoinStatus); - - public bool Disallowed { get; set; } - - public MsgTickerLateJoinStatus(INetChannel channel) : base(NAME, GROUP) { } - - #endregion - - public override void ReadFromBuffer(NetIncomingMessage buffer) - { - Disallowed = buffer.ReadBoolean(); - } - - public override void WriteToBuffer(NetOutgoingMessage buffer) - { - buffer.Write(Disallowed); - } + TextBlob = textBlob; } + } + [Serializable, NetSerializable] + public class TickerLobbyCountdownEvent : EntityEventArgs + { + /// + /// The game time that the game will start at. + /// + public TimeSpan StartTime { get; } - protected class MsgTickerLobbyStatus : NetMessage + /// + /// Whether or not the countdown is paused + /// + public bool Paused { get; } + + public TickerLobbyCountdownEvent(TimeSpan startTime, bool paused) { - #region REQUIRED - - public const MsgGroups GROUP = MsgGroups.Command; - public const string NAME = nameof(MsgTickerLobbyStatus); - public MsgTickerLobbyStatus(INetChannel channel) : base(NAME, GROUP) { } - - #endregion - - public bool IsRoundStarted { get; set; } - public string? LobbySong { get; set; } - public bool YouAreReady { get; set; } - // UTC. - public TimeSpan StartTime { get; set; } - public bool Paused { get; set; } - - public override void ReadFromBuffer(NetIncomingMessage buffer) - { - IsRoundStarted = buffer.ReadBoolean(); - LobbySong = buffer.ReadString(); - - if (IsRoundStarted) - { - return; - } - - YouAreReady = buffer.ReadBoolean(); - StartTime = new TimeSpan(buffer.ReadInt64()); - Paused = buffer.ReadBoolean(); - - } - - public override void WriteToBuffer(NetOutgoingMessage buffer) - { - buffer.Write(IsRoundStarted); - buffer.Write(LobbySong); - - if (IsRoundStarted) - { - return; - } - - buffer.Write(YouAreReady); - buffer.Write(StartTime.Ticks); - buffer.Write(Paused); - - } + StartTime = startTime; + Paused = paused; } + } - protected class MsgTickerLobbyInfo : NetMessage + [Serializable, NetSerializable] + public class TickerLobbyReadyEvent : EntityEventArgs + { + /// + /// The Status of the Player in the lobby (ready, observer, ...) + /// + public Dictionary Status { get; } + + public TickerLobbyReadyEvent(Dictionary status) { - #region REQUIRED - - public const MsgGroups GROUP = MsgGroups.Command; - public const string NAME = nameof(MsgTickerLobbyInfo); - public MsgTickerLobbyInfo(INetChannel channel) : base(NAME, GROUP) { } - - #endregion - - public string TextBlob { get; set; } = string.Empty; - - public override void ReadFromBuffer(NetIncomingMessage buffer) - { - TextBlob = buffer.ReadString(); - } - - public override void WriteToBuffer(NetOutgoingMessage buffer) - { - buffer.Write(TextBlob); - } + Status = status; } + } - protected class MsgTickerLobbyCountdown : NetMessage + [Serializable, NetSerializable] + public class TickerJobsAvailableEvent : EntityEventArgs + { + /// + /// The Status of the Player in the lobby (ready, observer, ...) + /// + public string[] JobsAvailable { get; } + + public TickerJobsAvailableEvent(string[] jobsAvailable) { - #region REQUIRED - - public const MsgGroups GROUP = MsgGroups.Command; - public const string NAME = nameof(MsgTickerLobbyCountdown); - public MsgTickerLobbyCountdown(INetChannel channel) : base(NAME, GROUP) { } - - #endregion - - /// - /// The game time that the game will start at. - /// - public TimeSpan StartTime { get; set; } - - /// - /// Whether or not the countdown is paused - /// - public bool Paused { get; set; } - - public override void ReadFromBuffer(NetIncomingMessage buffer) - { - StartTime = new TimeSpan(buffer.ReadInt64()); - Paused = buffer.ReadBoolean(); - } - - public override void WriteToBuffer(NetOutgoingMessage buffer) - { - buffer.Write(StartTime.Ticks); - buffer.Write(Paused); - } - } - - protected class MsgTickerLobbyReady : NetMessage - { - #region REQUIRED - - public const MsgGroups GROUP = MsgGroups.Command; - public const string NAME = nameof(MsgTickerLobbyReady); - public MsgTickerLobbyReady(INetChannel channel) : base(NAME, GROUP) { } - - #endregion - - /// - /// The Status of the Player in the lobby (ready, observer, ...) - /// - public Dictionary PlayerStatus { get; set; } = new(); - - public override void ReadFromBuffer(NetIncomingMessage buffer) - { - PlayerStatus = new Dictionary(); - var length = buffer.ReadInt32(); - for (int i = 0; i < length; i++) - { - var serializer = IoCManager.Resolve(); - var byteLength = buffer.ReadVariableInt32(); - NetUserId userId; - using (var stream = buffer.ReadAlignedMemory(byteLength)) - { - serializer.DeserializeDirect(stream, out userId); - } - var status = (PlayerStatus)buffer.ReadByte(); - PlayerStatus.Add(userId, status); - } - } - - public override void WriteToBuffer(NetOutgoingMessage buffer) - { - var serializer = IoCManager.Resolve(); - buffer.Write(PlayerStatus.Count); - foreach (var p in PlayerStatus) - { - using (var stream = new MemoryStream()) - { - serializer.SerializeDirect(stream, p.Key); - buffer.WriteVariableInt32((int) stream.Length); - stream.TryGetBuffer(out var segment); - buffer.Write(segment); - } - buffer.Write((byte)p.Value); - } - } - } - - protected class MsgTickerJobsAvailable : NetMessage - { - #region REQUIRED - - public const MsgGroups GROUP = MsgGroups.Command; - public const string NAME = nameof(MsgTickerJobsAvailable); - public MsgTickerJobsAvailable(INetChannel channel) : base(NAME, GROUP) { } - - #endregion - - /// - /// The Status of the Player in the lobby (ready, observer, ...) - /// - public string[] JobsAvailable { get; set; } = Array.Empty(); - - public override void ReadFromBuffer(NetIncomingMessage buffer) - { - var amount = buffer.ReadInt32(); - JobsAvailable = new string[amount]; - - for (var i = 0; i < amount; i++) - { - JobsAvailable[i] = buffer.ReadString(); - } - } - - public override void WriteToBuffer(NetOutgoingMessage buffer) - { - buffer.Write(JobsAvailable.Length); - - foreach (var job in JobsAvailable) - { - buffer.Write(job); - } - } + JobsAvailable = jobsAvailable; } + } + [Serializable, NetSerializable] + public class RoundEndMessageEvent : EntityEventArgs + { + [Serializable, NetSerializable] public struct RoundEndPlayerInfo { public string PlayerOOCName; @@ -283,81 +131,30 @@ namespace Content.Shared.GameTicking public bool Observer; } - protected class MsgRoundEndMessage : NetMessage + public string GamemodeTitle { get; } + public string RoundEndText { get; } + public TimeSpan RoundDuration { get; } + public int PlayerCount { get; } + public RoundEndPlayerInfo[] AllPlayersEndInfo { get; } + + public RoundEndMessageEvent(string gamemodeTitle, string roundEndText, TimeSpan roundDuration, int playerCount, + RoundEndPlayerInfo[] allPlayersEndInfo) { - - #region REQUIRED - - public const MsgGroups GROUP = MsgGroups.Command; - public const string NAME = nameof(MsgRoundEndMessage); - public MsgRoundEndMessage(INetChannel channel) : base(NAME, GROUP) { } - - #endregion - - public string GamemodeTitle = string.Empty; - public string RoundEndText = string.Empty; - public TimeSpan RoundDuration; - - - public int PlayerCount; - - public List AllPlayersEndInfo = new(); - - public override void ReadFromBuffer(NetIncomingMessage buffer) - { - GamemodeTitle = buffer.ReadString(); - RoundEndText = buffer.ReadString(); - - var hours = buffer.ReadInt32(); - var mins = buffer.ReadInt32(); - var seconds = buffer.ReadInt32(); - RoundDuration = new TimeSpan(hours, mins, seconds); - - PlayerCount = buffer.ReadInt32(); - AllPlayersEndInfo = new List(); - for(var i = 0; i < PlayerCount; i++) - { - var readPlayerData = new RoundEndPlayerInfo - { - PlayerOOCName = buffer.ReadString(), - PlayerICName = buffer.ReadString(), - Role = buffer.ReadString(), - Antag = buffer.ReadBoolean(), - Observer = buffer.ReadBoolean(), - }; - - AllPlayersEndInfo.Add(readPlayerData); - } - } - - public override void WriteToBuffer(NetOutgoingMessage buffer) - { - buffer.Write(GamemodeTitle); - buffer.Write(RoundEndText); - buffer.Write(RoundDuration.Hours); - buffer.Write(RoundDuration.Minutes); - buffer.Write(RoundDuration.Seconds); - - - buffer.Write(AllPlayersEndInfo.Count); - foreach(var playerEndInfo in AllPlayersEndInfo) - { - buffer.Write(playerEndInfo.PlayerOOCName); - buffer.Write(playerEndInfo.PlayerICName); - buffer.Write(playerEndInfo.Role); - buffer.Write(playerEndInfo.Antag); - buffer.Write(playerEndInfo.Observer); - } - } - + GamemodeTitle = gamemodeTitle; + RoundEndText = roundEndText; + RoundDuration = roundDuration; + PlayerCount = playerCount; + AllPlayersEndInfo = allPlayersEndInfo; } + } - public enum PlayerStatus : byte - { - NotReady = 0, - Ready, - Observer, - } + + [Serializable, NetSerializable] + public enum LobbyPlayerStatus : sbyte + { + NotReady = 0, + Ready, + Observer, } } diff --git a/Content.Shared/GameWindow/MsgRequestWindowAttention.cs b/Content.Shared/GameWindow/MsgRequestWindowAttention.cs deleted file mode 100644 index aa40d2be15..0000000000 --- a/Content.Shared/GameWindow/MsgRequestWindowAttention.cs +++ /dev/null @@ -1,27 +0,0 @@ -#nullable enable -using Lidgren.Network; -using Robust.Shared.Network; - -namespace Content.Shared.GameWindow -{ - public sealed class MsgRequestWindowAttention : NetMessage - { - #region REQUIRED - - public const MsgGroups GROUP = MsgGroups.Command; - public const string NAME = nameof(MsgRequestWindowAttention); - public MsgRequestWindowAttention(INetChannel channel) : base(NAME, GROUP) { } - - #endregion - - public override void ReadFromBuffer(NetIncomingMessage buffer) - { - // Nothing - } - - public override void WriteToBuffer(NetOutgoingMessage buffer) - { - // Nothing - } - } -} diff --git a/Content.Shared/GameWindow/RequestWindowAttentionEvent.cs b/Content.Shared/GameWindow/RequestWindowAttentionEvent.cs new file mode 100644 index 0000000000..dc37f415bb --- /dev/null +++ b/Content.Shared/GameWindow/RequestWindowAttentionEvent.cs @@ -0,0 +1,12 @@ +#nullable enable +using System; +using Robust.Shared.GameObjects; +using Robust.Shared.Serialization; + +namespace Content.Shared.GameWindow +{ + [Serializable, NetSerializable] + public sealed class RequestWindowAttentionEvent : EntityEventArgs + { + } +} diff --git a/Content.YAMLLinter/Program.cs b/Content.YAMLLinter/Program.cs index 55aa3ccbef..289335f7cc 100644 --- a/Content.YAMLLinter/Program.cs +++ b/Content.YAMLLinter/Program.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Content.IntegrationTests; +using Content.Shared.CCVar; using Robust.Shared.Prototypes; using Robust.Shared.Serialization.Markdown.Validation; using Robust.Shared.Timing; @@ -69,6 +70,7 @@ namespace Content.YAMLLinter var server = StartServer(new ServerContentIntegrationOption() { FailureLogLevel = null, + CVarOverrides = { {CCVars.GameDummyTicker.Name, "true"} } }); await server.WaitIdleAsync();