diff --git a/Content.Benchmarks/SpawnEquipDeleteBenchmark.cs b/Content.Benchmarks/SpawnEquipDeleteBenchmark.cs index b6d5427480..e0c89bbc8e 100644 --- a/Content.Benchmarks/SpawnEquipDeleteBenchmark.cs +++ b/Content.Benchmarks/SpawnEquipDeleteBenchmark.cs @@ -3,8 +3,8 @@ using System.Threading.Tasks; using BenchmarkDotNet.Attributes; using Content.IntegrationTests; using Content.IntegrationTests.Pair; -using Content.Server.Station.Systems; using Content.Shared.Roles; +using Content.Shared.Station.Systems; using Robust.Shared; using Robust.Shared.Analyzers; using Robust.Shared.GameObjects; diff --git a/Content.Client/Administration/UI/Tabs/ObjectsTab/ObjectsTab.xaml.cs b/Content.Client/Administration/UI/Tabs/ObjectsTab/ObjectsTab.xaml.cs index 4195fe7a75..79bc3c2be4 100644 --- a/Content.Client/Administration/UI/Tabs/ObjectsTab/ObjectsTab.xaml.cs +++ b/Content.Client/Administration/UI/Tabs/ObjectsTab/ObjectsTab.xaml.cs @@ -1,6 +1,6 @@ using Content.Client.Administration.Managers; -using Content.Client.Station; using Content.Client.UserInterface.Controls; +using Content.Shared.Station.Systems; using Robust.Client.AutoGenerated; using Robust.Client.Console; using Robust.Client.Graphics; @@ -79,7 +79,7 @@ public sealed partial class ObjectsTab : Control // teleport to the station's largest grid, not to the station entity (which is in nullspace) if (!_entityManager.TryGetEntity(nent, out var station)) break; - var largestGrid = _entityManager.EntitySysManager.GetEntitySystem().GetLargestGrid(station.Value); + var largestGrid = _entityManager.System().GetLargestGrid(station.Value); if (largestGrid == null) break; _console.ExecuteCommand($"tpto {largestGrid.Value}"); @@ -107,7 +107,7 @@ public sealed partial class ObjectsTab : Control switch (selection) { case ObjectsTabSelection.Stations: - entities.AddRange(_entityManager.EntitySysManager.GetEntitySystem().GetStationNames()); + entities.AddRange(_entityManager.System().GetStationNames()); break; case ObjectsTabSelection.Grids: { diff --git a/Content.Client/Antag/ClientAntagSelectionSystem.cs b/Content.Client/Antag/ClientAntagSelectionSystem.cs new file mode 100644 index 0000000000..6c52b2a480 --- /dev/null +++ b/Content.Client/Antag/ClientAntagSelectionSystem.cs @@ -0,0 +1,32 @@ +using Content.Shared.Antag; +using Content.Shared.Antag.Components; +using Content.Shared.Roles; +using Robust.Shared.Audio; +using Robust.Shared.Player; +using Robust.Shared.Prototypes; + +namespace Content.Client.Antag; + +/// +public sealed partial class ClientAntagSelectionSystem : AntagSelectionSystem +{ + public override void SendBriefing(ICommonSession? session, string briefing, Color? briefingColor, SoundSpecifier? briefingSound) + { + + } + + public override IEnumerable> GetValidAntagPreferences(ICommonSession session, List>? filter = null) + { + yield break; + } + + public override bool IsAntagBanned(ICommonSession session, AntagSpecifierPrototype definition) + { + return false; + } + + protected override Entity? ForceGetGameRuleEnt(string id) + { + return null; + } +} diff --git a/Content.Client/Audio/ContentAudioSystem.LobbyMusic.cs b/Content.Client/Audio/ContentAudioSystem.LobbyMusic.cs index 26487fe24a..e8d9aab9ce 100644 --- a/Content.Client/Audio/ContentAudioSystem.LobbyMusic.cs +++ b/Content.Client/Audio/ContentAudioSystem.LobbyMusic.cs @@ -1,4 +1,5 @@ using System.Linq; +using Content.Client.GameTicking; using Content.Client.GameTicking.Managers; using Content.Client.Lobby; using Content.Shared.Audio.Events; diff --git a/Content.Client/Communications/UI/CommunicationsConsoleBoundUserInterface.cs b/Content.Client/Communications/UI/CommunicationsConsoleBoundUserInterface.cs index f382de1e79..eb416c3f3c 100644 --- a/Content.Client/Communications/UI/CommunicationsConsoleBoundUserInterface.cs +++ b/Content.Client/Communications/UI/CommunicationsConsoleBoundUserInterface.cs @@ -2,7 +2,7 @@ using Content.Shared.CCVar; using Content.Shared.Chat; using Content.Shared.AlertLevel; using Content.Shared.Communications; -using Content.Shared.Station; +using Content.Shared.Station.Systems; using Robust.Client.UserInterface; using Robust.Shared.Configuration; using Robust.Shared.Prototypes; @@ -18,7 +18,7 @@ namespace Content.Client.Communications.UI; public sealed partial class CommunicationsConsoleBoundUserInterface(EntityUid owner, Enum uiKey) : BoundUserInterface(owner, uiKey) { [Dependency] private IConfigurationManager _cfg = default!; - [Dependency] private SharedStationSystem _station = default!; + [Dependency] private StationSystem _station = default!; [Dependency] private AlertLevelSystem _alertLevel = default!; [ViewVariables] diff --git a/Content.Client/Entry/EntryPoint.cs b/Content.Client/Entry/EntryPoint.cs index 29ff0e85aa..3dea8e661f 100644 --- a/Content.Client/Entry/EntryPoint.cs +++ b/Content.Client/Entry/EntryPoint.cs @@ -128,8 +128,6 @@ namespace Content.Client.Entry _prototypeManager.RegisterIgnore("alertLevels"); _prototypeManager.RegisterIgnore("nukeopsRole"); _prototypeManager.RegisterIgnore("ghostRoleRaffleDecider"); - _prototypeManager.RegisterIgnore("codewordGenerator"); - _prototypeManager.RegisterIgnore("codewordFaction"); _componentFactory.GenerateNetIds(); _adminManager.Initialize(); diff --git a/Content.Client/GameTicking/ClientGameTicker.cs b/Content.Client/GameTicking/ClientGameTicker.cs new file mode 100644 index 0000000000..4109a991f7 --- /dev/null +++ b/Content.Client/GameTicking/ClientGameTicker.cs @@ -0,0 +1,190 @@ +using System.Numerics; +using Content.Client.Administration.Managers; +using Content.Client.Gameplay; +using Content.Client.Lobby; +using Content.Client.RoundEnd; +using Content.Shared.GameTicking; +using Content.Shared.GameTicking.Prototypes; +using Content.Shared.GameWindow; +using Content.Shared.Maps; +using Content.Shared.Roles; +using JetBrains.Annotations; +using Robust.Client.Graphics; +using Robust.Client.State; +using Robust.Client.UserInterface; +using Robust.Shared.Audio; +using Robust.Shared.EntitySerialization; +using Robust.Shared.Map; +using Robust.Shared.Player; +using Robust.Shared.Prototypes; + +namespace Content.Client.GameTicking; + +[UsedImplicitly] +public sealed partial class ClientGameTicker : GameTicker +{ + [Dependency] private IStateManager _stateManager = default!; + [Dependency] private IClientAdminManager _admin = default!; + [Dependency] private IClyde _clyde = default!; + [Dependency] private IUserInterfaceManager _userInterfaceManager = default!; + + private Dictionary, int?>> _jobsAvailable = new(); + private Dictionary _stationNames = new(); + private Dictionary?> _jobWeightsByStation = new(); + + [ViewVariables] public bool AreWeReady { get; private set; } + [ViewVariables] public bool IsGameStarted { get; private set; } + [ViewVariables] public ResolvedSoundSpecifier? RestartSound { get; private set; } + [ViewVariables] public ProtoId? LobbyBackground { get; private set; } + [ViewVariables] public bool DisallowedLateJoin { get; private set; } + [ViewVariables] public string? ServerInfoBlob { get; private set; } + [ViewVariables] public TimeSpan StartTime { get; private set; } + [ViewVariables] public new bool Paused { get; private set; } + + [ViewVariables] public IReadOnlyDictionary, int?>> JobsAvailable => _jobsAvailable; + [ViewVariables] public IReadOnlyDictionary StationNames => _stationNames; + [ViewVariables] public IReadOnlyDictionary?> JobWeightsByStation => _jobWeightsByStation; + + public event Action? InfoBlobUpdated; + public event Action? LobbyStatusUpdated; + public event Action? LobbyLateJoinStatusUpdated; + public event Action, int?>>>? LobbyJobsAvailableUpdated; + + public override void Initialize() + { + base.Initialize(); + + _admin.AdminStatusUpdated += OnAdminUpdated; + OnAdminUpdated(); + } + + public override void Shutdown() + { + _admin.AdminStatusUpdated -= OnAdminUpdated; + base.Shutdown(); + } + + public override IReadOnlyList LoadGameMap(GameMapPrototype proto, + out MapId mapId, + DeserializationOptions? options = null, + string? stationName = null, + Vector2? offset = null, + Angle? rot = null) + { + throw new NotImplementedException(); + } + + public override int ReadyPlayerCount() + { + throw new NotImplementedException(); + } + + public override void PlayerJoinGame(ICommonSession session, bool silent = false) + { + // TODO: Can probably move some logic into here :P + } + + private void OnAdminUpdated() + { + // TODO: We REALLY should not be logging parent changed events for parents the client cannot see :V + // Hide some map/grid related logs from clients. This is to try prevent some easy metagaming by just + // reading the console. E.g., logs like this one could leak the nuke station/grid: + // > Grid NT-Arrivals 1101 (122/n25896) changed parent. Old parent: map 10 (121/n25895). New parent: FTL (123/n26470) +#if !DEBUG + Map.Log.Level = _admin.IsAdmin() ? LogLevel.Info : LogLevel.Warning; +#endif + } + + [SubscribeNetworkEvent] + private void OnAttentionRequest(RequestWindowAttentionEvent ev) + { + _clyde.RequestWindowAttention(); + } + + [SubscribeNetworkEvent] + private void LateJoinStatus(TickerLateJoinStatusEvent message) + { + DisallowedLateJoin = message.Disallowed; + LobbyLateJoinStatusUpdated?.Invoke(); + } + + [SubscribeNetworkEvent] + private void UpdateJobsAvailable(TickerJobsAvailableEvent message) + { + _jobsAvailable.Clear(); + + foreach (var (job, data) in message.JobsAvailableByStation) + { + _jobsAvailable[job] = data; + } + + _stationNames.Clear(); + foreach (var weh in message.StationNames) + { + _stationNames[weh.Key] = weh.Value; + } + + _jobWeightsByStation.Clear(); + foreach (var (station, jobWeights) in message.JobWeightsByStation) + { + _jobWeightsByStation[station] = jobWeights; + } + + LobbyJobsAvailableUpdated?.Invoke(JobsAvailable); + } + + [SubscribeNetworkEvent] + private void JoinLobby(TickerJoinLobbyEvent message) + { + _stateManager.RequestStateChange(); + } + + [SubscribeNetworkEvent] + private void ConnectionStatus(TickerConnectionStatusEvent message) + { + RoundStartTimeSpan = message.RoundStartTimeSpan; + } + + [SubscribeNetworkEvent] + private void LobbyStatus(TickerLobbyStatusEvent message) + { + StartTime = message.StartTime; + RoundStartTimeSpan = message.RoundStartTimeSpan; + IsGameStarted = message.IsRoundStarted; + AreWeReady = message.YouAreReady; + LobbyBackground = message.LobbyBackground; + Paused = message.Paused; + + LobbyStatusUpdated?.Invoke(); + } + + [SubscribeNetworkEvent] + private void LobbyInfo(TickerLobbyInfoEvent message) + { + ServerInfoBlob = message.TextBlob; + + InfoBlobUpdated?.Invoke(); + } + + [SubscribeNetworkEvent] + private void JoinGame(TickerJoinGameEvent message) + { + _stateManager.RequestStateChange(); + } + + [SubscribeNetworkEvent] + private void LobbyCountdown(TickerLobbyCountdownEvent message) + { + StartTime = message.StartTime; + Paused = message.Paused; + } + + [SubscribeNetworkEvent] + private void RoundEnd(RoundEndMessageEvent message) + { + // Force an update in the event of this song being the same as the last. + RestartSound = message.RestartSound; + + _userInterfaceManager.GetUIController().OpenRoundEndSummaryWindow(message); + } +} diff --git a/Content.Client/GameTicking/Managers/ClientGameTicker.cs b/Content.Client/GameTicking/Managers/ClientGameTicker.cs deleted file mode 100644 index 021d3d6ca8..0000000000 --- a/Content.Client/GameTicking/Managers/ClientGameTicker.cs +++ /dev/null @@ -1,168 +0,0 @@ -using Content.Client.Administration.Managers; -using Content.Client.Gameplay; -using Content.Client.Lobby; -using Content.Client.RoundEnd; -using Content.Shared.GameTicking; -using Content.Shared.GameWindow; -using Content.Shared.Roles; -using JetBrains.Annotations; -using Robust.Client.Graphics; -using Robust.Client.State; -using Robust.Client.UserInterface; -using Robust.Shared.Prototypes; -using Robust.Shared.Audio; -using Content.Shared.GameTicking.Prototypes; - -namespace Content.Client.GameTicking.Managers -{ - [UsedImplicitly] - public sealed partial class ClientGameTicker : SharedGameTicker - { - [Dependency] private IStateManager _stateManager = default!; - [Dependency] private IClientAdminManager _admin = default!; - [Dependency] private IClyde _clyde = default!; - [Dependency] private IUserInterfaceManager _userInterfaceManager = default!; - - private Dictionary, int?>> _jobsAvailable = new(); - private Dictionary _stationNames = new(); - private Dictionary?> _jobWeightsByStation = new(); - - [ViewVariables] public bool AreWeReady { get; private set; } - [ViewVariables] public bool IsGameStarted { get; private set; } - [ViewVariables] public ResolvedSoundSpecifier? RestartSound { get; private set; } - [ViewVariables] public ProtoId? LobbyBackground { get; private set; } - [ViewVariables] public bool DisallowedLateJoin { get; private set; } - [ViewVariables] public string? ServerInfoBlob { get; private set; } - [ViewVariables] public TimeSpan StartTime { get; private set; } - [ViewVariables] public new bool Paused { get; private set; } - - public override IReadOnlyList<(TimeSpan, string)> AllPreviousGameRules => new List<(TimeSpan, string)>(); - - [ViewVariables] public IReadOnlyDictionary, int?>> JobsAvailable => _jobsAvailable; - [ViewVariables] public IReadOnlyDictionary StationNames => _stationNames; - [ViewVariables] public IReadOnlyDictionary?> JobWeightsByStation => _jobWeightsByStation; - - public event Action? InfoBlobUpdated; - public event Action? LobbyStatusUpdated; - public event Action? LobbyLateJoinStatusUpdated; - public event Action, int?>>>? LobbyJobsAvailableUpdated; - - public override void Initialize() - { - base.Initialize(); - - SubscribeNetworkEvent(JoinLobby); - SubscribeNetworkEvent(JoinGame); - SubscribeNetworkEvent(ConnectionStatus); - SubscribeNetworkEvent(LobbyStatus); - SubscribeNetworkEvent(LobbyInfo); - SubscribeNetworkEvent(LobbyCountdown); - SubscribeNetworkEvent(RoundEnd); - SubscribeNetworkEvent(OnAttentionRequest); - SubscribeNetworkEvent(LateJoinStatus); - SubscribeNetworkEvent(UpdateJobsAvailable); - - _admin.AdminStatusUpdated += OnAdminUpdated; - OnAdminUpdated(); - } - - public override void Shutdown() - { - _admin.AdminStatusUpdated -= OnAdminUpdated; - base.Shutdown(); - } - - private void OnAdminUpdated() - { - // Hide some map/grid related logs from clients. This is to try prevent some easy metagaming by just - // reading the console. E.g., logs like this one could leak the nuke station/grid: - // > Grid NT-Arrivals 1101 (122/n25896) changed parent. Old parent: map 10 (121/n25895). New parent: FTL (123/n26470) -#if !DEBUG - EntityManager.System().Log.Level = _admin.IsAdmin() ? LogLevel.Info : LogLevel.Warning; -#endif - } - - private void OnAttentionRequest(RequestWindowAttentionEvent ev) - { - _clyde.RequestWindowAttention(); - } - - private void LateJoinStatus(TickerLateJoinStatusEvent message) - { - DisallowedLateJoin = message.Disallowed; - LobbyLateJoinStatusUpdated?.Invoke(); - } - - private void UpdateJobsAvailable(TickerJobsAvailableEvent message) - { - _jobsAvailable.Clear(); - - foreach (var (job, data) in message.JobsAvailableByStation) - { - _jobsAvailable[job] = data; - } - - _stationNames.Clear(); - foreach (var weh in message.StationNames) - { - _stationNames[weh.Key] = weh.Value; - } - - _jobWeightsByStation.Clear(); - foreach (var (station, jobWeights) in message.JobWeightsByStation) - { - _jobWeightsByStation[station] = jobWeights; - } - - LobbyJobsAvailableUpdated?.Invoke(JobsAvailable); - } - - private void JoinLobby(TickerJoinLobbyEvent message) - { - _stateManager.RequestStateChange(); - } - - private void ConnectionStatus(TickerConnectionStatusEvent message) - { - RoundStartTimeSpan = message.RoundStartTimeSpan; - } - - private void LobbyStatus(TickerLobbyStatusEvent message) - { - StartTime = message.StartTime; - RoundStartTimeSpan = message.RoundStartTimeSpan; - IsGameStarted = message.IsRoundStarted; - AreWeReady = message.YouAreReady; - LobbyBackground = message.LobbyBackground; - Paused = message.Paused; - - LobbyStatusUpdated?.Invoke(); - } - - private void LobbyInfo(TickerLobbyInfoEvent message) - { - ServerInfoBlob = message.TextBlob; - - InfoBlobUpdated?.Invoke(); - } - - private void JoinGame(TickerJoinGameEvent message) - { - _stateManager.RequestStateChange(); - } - - private void LobbyCountdown(TickerLobbyCountdownEvent message) - { - StartTime = message.StartTime; - Paused = message.Paused; - } - - private void RoundEnd(RoundEndMessageEvent message) - { - // Force an update in the event of this song being the same as the last. - RestartSound = message.RestartSound; - - _userInterfaceManager.GetUIController().OpenRoundEndSummaryWindow(message); - } - } -} diff --git a/Content.Client/LateJoin/LateJoinGui.cs b/Content.Client/LateJoin/LateJoinGui.cs index bbba870e53..e6485aaec6 100644 --- a/Content.Client/LateJoin/LateJoinGui.cs +++ b/Content.Client/LateJoin/LateJoinGui.cs @@ -1,14 +1,13 @@ using System.Linq; using System.Numerics; using Content.Client.CrewManifest; -using Content.Client.GameTicking.Managers; +using Content.Client.GameTicking; using Content.Client.Lobby; using Content.Client.UserInterface.Controls; using Content.Client.Players.PlayTimeTracking; using Content.Shared.CCVar; using Content.Shared.Preferences; using Content.Shared.Roles; -using Content.Shared.StatusIcon; using Robust.Client.Console; using Robust.Client.GameObjects; using Robust.Client.UserInterface; diff --git a/Content.Client/Light/EntitySystems/SunShadowSystem.cs b/Content.Client/Light/EntitySystems/SunShadowSystem.cs index 873abd5817..1d9297026c 100644 --- a/Content.Client/Light/EntitySystems/SunShadowSystem.cs +++ b/Content.Client/Light/EntitySystems/SunShadowSystem.cs @@ -1,6 +1,6 @@ using System.Diagnostics.Contracts; using System.Numerics; -using Content.Client.GameTicking.Managers; +using Content.Shared.GameTicking; using Content.Shared.Light.Components; using Content.Shared.Light.EntitySystems; using Robust.Shared.Timing; @@ -10,7 +10,7 @@ namespace Content.Client.Light.EntitySystems; public sealed partial class SunShadowSystem : SharedSunShadowSystem { - [Dependency] private ClientGameTicker _ticker = default!; + [Dependency] private GameTicker _ticker = default!; [Dependency] private IGameTiming _timing = default!; [Dependency] private MetaDataSystem _metadata = default!; diff --git a/Content.Client/Light/LightCycleSystem.cs b/Content.Client/Light/LightCycleSystem.cs index 373431e2ef..dfb07e0faa 100644 --- a/Content.Client/Light/LightCycleSystem.cs +++ b/Content.Client/Light/LightCycleSystem.cs @@ -1,5 +1,4 @@ -using Content.Client.GameTicking.Managers; -using Content.Shared; +using Content.Shared.GameTicking; using Content.Shared.Light.Components; using Content.Shared.Light.EntitySystems; using Robust.Shared.Map.Components; @@ -10,7 +9,7 @@ namespace Content.Client.Light; /// public sealed partial class LightCycleSystem : SharedLightCycleSystem { - [Dependency] private ClientGameTicker _ticker = default!; + [Dependency] private GameTicker _ticker = default!; [Dependency] private IGameTiming _timing = default!; [Dependency] private MetaDataSystem _metadata = default!; diff --git a/Content.Client/Lobby/LobbyState.cs b/Content.Client/Lobby/LobbyState.cs index 6e8b00a3b2..b5373e0622 100644 --- a/Content.Client/Lobby/LobbyState.cs +++ b/Content.Client/Lobby/LobbyState.cs @@ -1,5 +1,5 @@ using Content.Client.Audio; -using Content.Client.GameTicking.Managers; +using Content.Client.GameTicking; using Content.Client.LateJoin; using Content.Client.Lobby.UI; using Content.Client.Message; diff --git a/Content.Client/Lobby/UI/HumanoidProfileEditor.xaml.cs b/Content.Client/Lobby/UI/HumanoidProfileEditor.xaml.cs index 0f6fe34048..6359069b7b 100644 --- a/Content.Client/Lobby/UI/HumanoidProfileEditor.xaml.cs +++ b/Content.Client/Lobby/UI/HumanoidProfileEditor.xaml.cs @@ -265,7 +265,7 @@ namespace Content.Client.Lobby.UI (int)PreferenceUnavailableMode.StayInLobby); PreferenceUnavailableButton.AddItem( Loc.GetString("humanoid-profile-editor-preference-unavailable-spawn-as-overflow-button", - ("overflowJob", Loc.GetString(SharedGameTicker.FallbackOverflowJobName))), + ("overflowJob", Loc.GetString(GameTicker.FallbackOverflowJobName))), (int)PreferenceUnavailableMode.SpawnAsOverflow); PreferenceUnavailableButton.OnItemSelected += args => diff --git a/Content.Client/Lobby/UI/ProfileEditorControls/ProfilePreviewSpriteView.Humanoid.cs b/Content.Client/Lobby/UI/ProfileEditorControls/ProfilePreviewSpriteView.Humanoid.cs index d8190eca4a..427a13a757 100644 --- a/Content.Client/Lobby/UI/ProfileEditorControls/ProfilePreviewSpriteView.Humanoid.cs +++ b/Content.Client/Lobby/UI/ProfileEditorControls/ProfilePreviewSpriteView.Humanoid.cs @@ -1,15 +1,12 @@ using System.Linq; -using Content.Client.Humanoid; -using Content.Client.Station; using Content.Shared.Body; using Content.Shared.Clothing; using Content.Shared.GameTicking; -using Content.Shared.Humanoid; -using Content.Shared.Humanoid.Prototypes; using Content.Shared.Inventory; using Content.Shared.Preferences; using Content.Shared.Preferences.Loadouts; using Content.Shared.Roles; +using Content.Shared.Station.Systems; using Robust.Shared.Map; using Robust.Shared.Prototypes; using Robust.Shared.Utility; @@ -80,7 +77,7 @@ public sealed partial class ProfilePreviewSpriteView { var highPriorityJob = profile.JobPriorities.FirstOrDefault(p => p.Value == JobPriority.High).Key; // ReSharper disable once NullCoalescingConditionIsAlwaysNotNullAccordingToAPIContract (what is resharper smoking?) - return _prototypeManager.Index(highPriorityJob.Id ?? SharedGameTicker.FallbackOverflowJob); + return _prototypeManager.Index(highPriorityJob.Id ?? GameTicker.FallbackOverflowJob); } private void GiveDummyLoadout(RoleLoadout? roleLoadout) diff --git a/Content.Client/PDA/PdaMenu.xaml.cs b/Content.Client/PDA/PdaMenu.xaml.cs index caf5e0528e..c144cca7a2 100644 --- a/Content.Client/PDA/PdaMenu.xaml.cs +++ b/Content.Client/PDA/PdaMenu.xaml.cs @@ -1,9 +1,9 @@ -using Content.Client.GameTicking.Managers; using Content.Shared.PDA; using Robust.Shared.Utility; using Content.Shared.CartridgeLoader; using Content.Client.Message; using Content.Shared.AlertLevel; +using Content.Shared.GameTicking; using Robust.Client.UserInterface; using Robust.Client.AutoGenerated; using Robust.Client.Graphics; @@ -20,7 +20,7 @@ namespace Content.Client.PDA [Dependency] private IGameTiming _gameTiming = default!; [Dependency] private IEntitySystemManager _entitySystem = default!; private readonly AlertLevelSystem _alert = default!; - private readonly ClientGameTicker _gameTicker; + private readonly GameTicker _gameTicker; public const int HomeView = 0; public const int ProgramListView = 1; @@ -45,7 +45,7 @@ namespace Content.Client.PDA { IoCManager.InjectDependencies(this); _alert = _entitySystem.GetEntitySystem(); - _gameTicker = _entitySystem.GetEntitySystem(); + _gameTicker = _entitySystem.GetEntitySystem(); RobustXamlLoader.Load(this); ViewContainer.OnChildAdded += control => control.Visible = false; diff --git a/Content.Client/RoundEnd/RoundEndSummaryUIController.cs b/Content.Client/RoundEnd/RoundEndSummaryUIController.cs index 0e8f5c5017..3ef81afaf5 100644 --- a/Content.Client/RoundEnd/RoundEndSummaryUIController.cs +++ b/Content.Client/RoundEnd/RoundEndSummaryUIController.cs @@ -1,4 +1,4 @@ -using Content.Client.GameTicking.Managers; +using Content.Client.GameTicking; using Content.Shared.GameTicking; using Content.Shared.Input; using JetBrains.Annotations; diff --git a/Content.Client/Shuttles/ClientArrivalsSystem.cs b/Content.Client/Shuttles/ClientArrivalsSystem.cs new file mode 100644 index 0000000000..28a2ce1825 --- /dev/null +++ b/Content.Client/Shuttles/ClientArrivalsSystem.cs @@ -0,0 +1,6 @@ +using Content.Shared.Shuttles.Systems; + +namespace Content.Client.Shuttles; + +/// +public sealed partial class ClientArrivalsSystem : ArrivalsSystem; diff --git a/Content.Client/Station/ClientStationSystem.cs b/Content.Client/Station/ClientStationSystem.cs new file mode 100644 index 0000000000..d1b0b3bbc0 --- /dev/null +++ b/Content.Client/Station/ClientStationSystem.cs @@ -0,0 +1,6 @@ +using Content.Shared.Station.Systems; + +namespace Content.Client.Station; + +/// +public sealed partial class ClientStationSystem : StationSystem; diff --git a/Content.Client/Station/StationSpawningSystem.cs b/Content.Client/Station/StationSpawningSystem.cs deleted file mode 100644 index 42dc677895..0000000000 --- a/Content.Client/Station/StationSpawningSystem.cs +++ /dev/null @@ -1,5 +0,0 @@ -using Content.Shared.Station; - -namespace Content.Client.Station; - -public sealed partial class StationSpawningSystem : SharedStationSpawningSystem; diff --git a/Content.Client/Station/StationSystem.cs b/Content.Client/Station/StationSystem.cs deleted file mode 100644 index e1ccd14d7b..0000000000 --- a/Content.Client/Station/StationSystem.cs +++ /dev/null @@ -1,6 +0,0 @@ -using Content.Shared.Station; - -namespace Content.Client.Station; - -/// -public sealed partial class StationSystem : SharedStationSystem; diff --git a/Content.Client/StationRecords/GeneralStationRecordConsoleBoundUserInterface.cs b/Content.Client/StationRecords/GeneralStationRecordConsoleBoundUserInterface.cs index c77735060b..e20caf59aa 100644 --- a/Content.Client/StationRecords/GeneralStationRecordConsoleBoundUserInterface.cs +++ b/Content.Client/StationRecords/GeneralStationRecordConsoleBoundUserInterface.cs @@ -1,4 +1,4 @@ -using Content.Shared.Station; +using Content.Shared.Station.Systems; using Content.Shared.StationRecords; using Content.Shared.StationRecords.Components; using Content.Shared.StationRecords.Systems; @@ -11,7 +11,7 @@ public sealed partial class GeneralStationRecordConsoleBoundUserInterface : Boun [ViewVariables] private GeneralStationRecordConsoleWindow? _window; - [Dependency] private SharedStationSystem _stationSys = default!; + [Dependency] private StationSystem _stationSys = default!; [Dependency] private StationRecordsSystem _recordsSys = default!; public GeneralStationRecordConsoleBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey) diff --git a/Content.Client/Store/Ui/StoreListingControl.xaml.cs b/Content.Client/Store/Ui/StoreListingControl.xaml.cs index 341dc78ec9..8951c0b9e9 100644 --- a/Content.Client/Store/Ui/StoreListingControl.xaml.cs +++ b/Content.Client/Store/Ui/StoreListingControl.xaml.cs @@ -1,4 +1,4 @@ -using Content.Client.GameTicking.Managers; +using Content.Shared.GameTicking; using Content.Shared.Store; using Robust.Client.AutoGenerated; using Robust.Client.Graphics; @@ -15,7 +15,7 @@ public sealed partial class StoreListingControl : Control [Dependency] private IPrototypeManager _prototype = default!; [Dependency] private IEntityManager _entity = default!; [Dependency] private IGameTiming _timing = default!; - private readonly ClientGameTicker _ticker; + private readonly GameTicker _ticker; private readonly ListingDataWithCostModifiers _data; @@ -27,7 +27,7 @@ public sealed partial class StoreListingControl : Control IoCManager.InjectDependencies(this); RobustXamlLoader.Load(this); - _ticker = _entity.System(); + _ticker = _entity.System(); _data = data; _hasBalance = hasBalance; diff --git a/Content.IntegrationTests/Pair/TestPair.Recycle.cs b/Content.IntegrationTests/Pair/TestPair.Recycle.cs index 887361a872..8e67627cdb 100644 --- a/Content.IntegrationTests/Pair/TestPair.Recycle.cs +++ b/Content.IntegrationTests/Pair/TestPair.Recycle.cs @@ -35,7 +35,7 @@ public sealed partial class TestPair protected override async Task Recycle(PairSettings next, TextWriter testOut) { // Move to pre-round lobby. Required to toggle dummy ticker on and off - var gameTicker = Server.System(); + var gameTicker = Server.System(); if (gameTicker.RunLevel != GameRunLevel.PreRoundLobby) { await testOut.WriteLineAsync($"Recycling: {Watch.Elapsed.TotalMilliseconds} ms: Restarting round."); @@ -68,7 +68,7 @@ public sealed partial class TestPair Assert.That(cfg.GetCVar(CCVars.GameLobbyEnabled), Is.EqualTo(settings.InLobby)); Assert.That(cfg.GetCVar(CCVars.GameDummyTicker), Is.EqualTo(settings.DummyTicker)); - var ticker = Server.System(); + var ticker = Server.System(); Assert.That(ticker.DummyTicker, Is.EqualTo(settings.DummyTicker)); var expectPreRound = settings.InLobby | settings.DummyTicker; diff --git a/Content.IntegrationTests/Pair/TestPair.cs b/Content.IntegrationTests/Pair/TestPair.cs index 81f99146f7..bcf95b33da 100644 --- a/Content.IntegrationTests/Pair/TestPair.cs +++ b/Content.IntegrationTests/Pair/TestPair.cs @@ -40,7 +40,7 @@ public sealed partial class TestPair : RobustIntegrationTest.TestPair var settings = (PoolSettings)Settings; if (!settings.DummyTicker) { - var gameTicker = Server.System(); + var gameTicker = Server.System(); await Server.WaitPost(() => gameTicker.RestartRound()); } } diff --git a/Content.IntegrationTests/Tests/Administration/Logs/AddTests.cs b/Content.IntegrationTests/Tests/Administration/Logs/AddTests.cs index d53b07b552..3e86667c12 100644 --- a/Content.IntegrationTests/Tests/Administration/Logs/AddTests.cs +++ b/Content.IntegrationTests/Tests/Administration/Logs/AddTests.cs @@ -25,7 +25,7 @@ public sealed class AddTests : GameTest [SidedDependency(Side.Server)] private readonly IAdminLogManager _sAdminLogManager = null!; [SidedDependency(Side.Server)] private readonly IServerDbManager _sDbManager = null!; - [SidedDependency(Side.Server)] private readonly GameTicker _sGameTicker = null!; + [SidedDependency(Side.Server)] private readonly ServerGameTicker _sGameTicker = null!; [SidedDependency(Side.Server)] private readonly IPlayerManager _sPlayerManager = null!; [Test] @@ -238,7 +238,7 @@ public sealed class PreRoundAddTests : GameTest [SidedDependency(Side.Server)] private readonly IAdminLogManager _sAdminLogManager = null!; [SidedDependency(Side.Server)] private readonly IServerDbManager _sDbManager = null!; - [SidedDependency(Side.Server)] private readonly GameTicker _sGameTicker = null!; + [SidedDependency(Side.Server)] private readonly ServerGameTicker _sGameTicker = null!; [Test] public async Task PreRoundAddAndGetSingle() diff --git a/Content.IntegrationTests/Tests/Administration/Logs/QueryTests.cs b/Content.IntegrationTests/Tests/Administration/Logs/QueryTests.cs index 55b36ebae1..20db63e47a 100644 --- a/Content.IntegrationTests/Tests/Administration/Logs/QueryTests.cs +++ b/Content.IntegrationTests/Tests/Administration/Logs/QueryTests.cs @@ -31,7 +31,7 @@ public sealed class QueryTests : GameTest var sPlayers = server.ResolveDependency(); var sAdminLogSystem = server.ResolveDependency(); - var sGamerTicker = sSystems.GetEntitySystem(); + var sGamerTicker = sSystems.GetEntitySystem(); var date = DateTime.UtcNow; var guid = Guid.NewGuid(); diff --git a/Content.IntegrationTests/Tests/Commands/RestartRoundTest.cs b/Content.IntegrationTests/Tests/Commands/RestartRoundTest.cs index bcacc8011b..da8d4adc7b 100644 --- a/Content.IntegrationTests/Tests/Commands/RestartRoundTest.cs +++ b/Content.IntegrationTests/Tests/Commands/RestartRoundTest.cs @@ -2,6 +2,7 @@ using Content.IntegrationTests.Fixtures; using Content.Server.GameTicking; using Content.Server.GameTicking.Commands; using Content.Shared.CCVar; +using Content.Shared.GameTicking; using Robust.Shared.Configuration; using Robust.Shared.GameObjects; using Robust.Shared.IoC; @@ -29,7 +30,7 @@ namespace Content.IntegrationTests.Tests.Commands var configManager = server.ResolveDependency(); var entityManager = server.ResolveDependency(); - var gameTicker = entityManager.System(); + var gameTicker = entityManager.System(); await pair.RunUntilSynced(); diff --git a/Content.IntegrationTests/Tests/EntityTest.cs b/Content.IntegrationTests/Tests/EntityTest.cs index 35298ce44e..387ad558aa 100644 --- a/Content.IntegrationTests/Tests/EntityTest.cs +++ b/Content.IntegrationTests/Tests/EntityTest.cs @@ -21,7 +21,7 @@ namespace Content.IntegrationTests.Tests [TestOf(typeof(EntityUid))] public sealed class EntityTest : GameTest { - private static readonly HashSet> IgnoredCategories = ["Spawner", "Debug"]; + private static readonly HashSet> IgnoredCategories = ["Spawner", "Debug", "GameRules"]; public override PoolSettings PoolSettings => new() { @@ -413,6 +413,7 @@ namespace Content.IntegrationTests.Tests "Broadphase", "StationData", // errors when removed mid-round "StationJobs", + "ActiveGameRule", // Requires GameRule :P "Actor", // We aren't testing actor components, those need their player session set. "BiomeSelection", // Whaddya know, requires config. "ActivatableUI", // Requires enum key diff --git a/Content.IntegrationTests/Tests/FollowerSystemTest.cs b/Content.IntegrationTests/Tests/FollowerSystemTest.cs index 56153aa8c2..225a47340b 100644 --- a/Content.IntegrationTests/Tests/FollowerSystemTest.cs +++ b/Content.IntegrationTests/Tests/FollowerSystemTest.cs @@ -33,11 +33,11 @@ public sealed class FollowerSystemTest : GameTest mapSys.CreateMap(out var map); // Spawn an observer to be followed. - var followed = entMan.SpawnEntity(GameTicker.ObserverPrototypeName, new MapCoordinates(0, 0, map)); + var followed = entMan.SpawnEntity(ServerGameTicker.ObserverPrototypeName, new MapCoordinates(0, 0, map)); logger.Info($"Spawned followed observer: {entMan.ToPrettyString(followed)}"); // Spawn an observer to follow another observer. - var follower = entMan.SpawnEntity(GameTicker.ObserverPrototypeName, new MapCoordinates(0, 0, map)); + var follower = entMan.SpawnEntity(ServerGameTicker.ObserverPrototypeName, new MapCoordinates(0, 0, map)); logger.Info($"Spawned follower observer: {entMan.ToPrettyString(follower)}"); followerSystem.StartFollowingEntity(follower, followed); diff --git a/Content.IntegrationTests/Tests/GameRules/AllGamePresetsStartTest.cs b/Content.IntegrationTests/Tests/GameRules/AllGamePresetsStartTest.cs index 5ffa82bb70..f7669dcfa6 100644 --- a/Content.IntegrationTests/Tests/GameRules/AllGamePresetsStartTest.cs +++ b/Content.IntegrationTests/Tests/GameRules/AllGamePresetsStartTest.cs @@ -2,14 +2,13 @@ using System.Linq; using Content.IntegrationTests.Fixtures.Attributes; using Content.IntegrationTests.Utility; -using Content.Server.Antag; -using Content.Server.Antag.Components; using Content.Server.GameTicking; -using Content.Server.GameTicking.Presets; using Content.Server.Shuttles.Components; using Content.Shared.Antag; +using Content.Shared.Antag.Components; using Content.Shared.CCVar; using Content.Shared.GameTicking; +using Content.Shared.GameTicking.Prototypes; using Robust.Shared.Map.Components; using Robust.Shared.Player; @@ -28,7 +27,7 @@ public sealed class AllGamePresetsStartTest : AntagTest // Tests that all game modes can start given ideal circumstances. [Test] - [TestOf(typeof(GameTicker)), TestOf(typeof(AntagSelectionSystem)), TestOf(typeof(AntagSelectionComponent))] + [TestOf(typeof(ServerGameTicker)), TestOf(typeof(AntagSelectionSystem)), TestOf(typeof(AntagSelectionComponent))] [TestCaseSource(nameof(_gamePresets))] [Description("Ensures all Game Presets are able to start and assign all antags correctly without spawning anyone in nullspace.")] [EnsureCVar(Side.Server, typeof(CCVars), nameof(CCVars.GameTickerIgnoredPresets), GameTicker.DummyGameRule)] diff --git a/Content.IntegrationTests/Tests/GameRules/AntagGhostRoleTest.cs b/Content.IntegrationTests/Tests/GameRules/AntagGhostRoleTest.cs index c9a3ec0782..f507b539a5 100644 --- a/Content.IntegrationTests/Tests/GameRules/AntagGhostRoleTest.cs +++ b/Content.IntegrationTests/Tests/GameRules/AntagGhostRoleTest.cs @@ -2,12 +2,11 @@ using System.Collections.Generic; using Content.IntegrationTests.Fixtures.Attributes; using Content.IntegrationTests.Utility; -using Content.Server.Antag; -using Content.Server.Antag.Components; using Content.Server.GameTicking; using Content.Server.Ghost.Roles; using Content.Server.Ghost.Roles.Components; using Content.Shared.Antag; +using Content.Shared.Antag.Components; using Content.Shared.Players; using Robust.Shared.GameObjects; using Robust.Shared.Map; @@ -33,7 +32,7 @@ public sealed partial class AntagGhostRoleTest : AntagTest private static readonly string[] AntagGameRules = GameDataScrounger.EntitiesWithComponent("AntagSelection"); [Test] - [TestOf(typeof(GameTicker)), TestOf(typeof(AntagSelectionSystem)), TestOf(typeof(AntagSelectionComponent)), TestOf(typeof(GhostRoleSystem))] + [TestOf(typeof(ServerGameTicker)), TestOf(typeof(AntagSelectionSystem)), TestOf(typeof(AntagSelectionComponent)), TestOf(typeof(GhostRoleSystem))] [TestCaseSource(nameof(AntagGameRules))] [Description($"Ensures all GameRule entities with {nameof(AntagSelectionComponent)} can properly spawn those roles and they can be taken.")] [RunOnSide(Side.Server)] @@ -64,7 +63,7 @@ public sealed partial class AntagGhostRoleTest : AntagTest while (roleEnumerator.MoveNext(out var spawner, out var role, out var xform)) { // Ensure the ghost role spawner spawned correctly! - Assert.That(spawner.Rule, Is.EqualTo(gameRule)); + Assert.That(spawner.Rule, Is.EqualTo(gameRule?.Owner)); Assert.That(spawner.Definition, Is.Not.Null); AssertGhostRoleTaken(spawner, role, xform); var value = rules[spawner.Definition.Value]; @@ -80,7 +79,7 @@ public sealed partial class AntagGhostRoleTest : AntagTest } [Test] - [TestOf(typeof(GameTicker)), TestOf(typeof(AntagSelectionSystem)), TestOf(typeof(AntagSelectionComponent)), TestOf(typeof(GhostRoleSystem))] + [TestOf(typeof(ServerGameTicker)), TestOf(typeof(AntagSelectionSystem)), TestOf(typeof(AntagSelectionComponent)), TestOf(typeof(GhostRoleSystem))] [Description("Ensures a player can take all antag ghost roles sequentially without transferring unwanted mind data.")] [RunOnSide(Side.Server)] public void TestAntagGhostRolesSequential() diff --git a/Content.IntegrationTests/Tests/GameRules/AntagTest.cs b/Content.IntegrationTests/Tests/GameRules/AntagTest.cs index 9f89a9fbac..f6d7a9a23b 100644 --- a/Content.IntegrationTests/Tests/GameRules/AntagTest.cs +++ b/Content.IntegrationTests/Tests/GameRules/AntagTest.cs @@ -1,7 +1,5 @@ -using System.Linq; -using Content.IntegrationTests.Fixtures; +using Content.IntegrationTests.Fixtures; using Content.IntegrationTests.Fixtures.Attributes; -using Content.Server.Antag; using Content.Server.GameTicking; using Content.Server.Mind; using Content.Shared.Antag; @@ -25,7 +23,7 @@ public abstract partial class AntagTest : GameTest }; [SidedDependency(Side.Server)] protected AntagSelectionSystem AntagSys = default!; - [SidedDependency(Side.Server)] protected GameTicker STicker = default!; + [SidedDependency(Side.Server)] protected ServerGameTicker STicker = default!; [SidedDependency(Side.Server)] protected MindSystem SMind = default!; /// diff --git a/Content.IntegrationTests/Tests/GameRules/FailAndStartPresetTest.cs b/Content.IntegrationTests/Tests/GameRules/FailAndStartPresetTest.cs index 13286c68a0..ad2d314edf 100644 --- a/Content.IntegrationTests/Tests/GameRules/FailAndStartPresetTest.cs +++ b/Content.IntegrationTests/Tests/GameRules/FailAndStartPresetTest.cs @@ -1,10 +1,10 @@ #nullable enable using Content.IntegrationTests.Fixtures; using Content.Server.GameTicking; -using Content.Server.GameTicking.Presets; using Content.Shared.CCVar; using Content.Shared.GameTicking; using Content.Shared.GameTicking.Components; +using Content.Shared.GameTicking.Prototypes; using Robust.Shared.GameObjects; namespace Content.IntegrationTests.Tests.GameRules; @@ -72,7 +72,7 @@ public sealed class FailAndStartPresetTest : GameTest var server = pair.Server; var client = pair.Client; var entMan = server.EntMan; - var ticker = server.System(); + var ticker = server.System(); server.System().Run = true; Assert.That(server.CfgMan.GetCVar(CCVars.GridFill), Is.False); diff --git a/Content.IntegrationTests/Tests/GameRules/NukeOpsTest.cs b/Content.IntegrationTests/Tests/GameRules/NukeOpsTest.cs index f9074f7dc6..5202f4f786 100644 --- a/Content.IntegrationTests/Tests/GameRules/NukeOpsTest.cs +++ b/Content.IntegrationTests/Tests/GameRules/NukeOpsTest.cs @@ -4,17 +4,17 @@ using System.Linq; using Content.IntegrationTests.Fixtures; using Content.Server.Body.Components; using Content.Server.GameTicking; -using Content.Server.GameTicking.Presets; using Content.Server.GameTicking.Rules.Components; using Content.Server.Mind; using Content.Server.Roles; using Content.Server.RoundEnd; using Content.Server.Shuttles.Components; using Content.Shared.CCVar; -using Content.Shared.Damage.Components; using Content.Shared.Damage.Systems; using Content.Shared.FixedPoint; using Content.Shared.GameTicking; +using Content.Shared.GameTicking.Prototypes; +using Content.Shared.GameTicking.Rules.Components; using Content.Shared.Hands.Components; using Content.Shared.Inventory; using Content.Shared.NPC.Prototypes; @@ -57,7 +57,7 @@ public sealed class NukeOpsTest : GameTest var client = pair.Client; var entMan = server.EntMan; var mapSys = server.System(); - var ticker = server.System(); + var ticker = server.System(); var mindSys = server.System(); var roleSys = server.System(); var invSys = server.System(); diff --git a/Content.IntegrationTests/Tests/GameRules/RuleMaxTimeRestartTest.cs b/Content.IntegrationTests/Tests/GameRules/RuleMaxTimeRestartTest.cs index 1a8fc6f6ee..638358c2a3 100644 --- a/Content.IntegrationTests/Tests/GameRules/RuleMaxTimeRestartTest.cs +++ b/Content.IntegrationTests/Tests/GameRules/RuleMaxTimeRestartTest.cs @@ -2,6 +2,7 @@ using Content.IntegrationTests.Fixtures; using Content.Server.GameTicking; using Content.Server.GameTicking.Rules; using Content.Server.GameTicking.Rules.Components; +using Content.Shared.GameTicking; using Content.Shared.GameTicking.Components; using Robust.Shared.GameObjects; using Robust.Shared.Prototypes; @@ -27,14 +28,14 @@ namespace Content.IntegrationTests.Tests.GameRules Assert.That(server.EntMan.Count(), Is.Zero); var entityManager = server.ResolveDependency(); - var sGameTicker = server.ResolveDependency().GetEntitySystem(); + var sGameTicker = server.ResolveDependency().GetEntitySystem(); var sGameTiming = server.ResolveDependency(); MaxTimeRestartRuleComponent maxTime = null; await server.WaitPost(() => { sGameTicker.StartGameRule(MaxTimeRestartGameRule, out var ruleEntity); - Assert.That(entityManager.TryGetComponent(ruleEntity, out maxTime)); + Assert.That(entityManager.TryGetComponent(ruleEntity, out maxTime)); }); Assert.That(server.EntMan.Count(), Is.EqualTo(1)); diff --git a/Content.IntegrationTests/Tests/GameRules/SecretStartsTest.cs b/Content.IntegrationTests/Tests/GameRules/SecretStartsTest.cs index efc5485e9b..444660f737 100644 --- a/Content.IntegrationTests/Tests/GameRules/SecretStartsTest.cs +++ b/Content.IntegrationTests/Tests/GameRules/SecretStartsTest.cs @@ -23,8 +23,7 @@ public sealed class SecretStartsTest : GameTest var server = pair.Server; await server.WaitIdleAsync(); - var entMan = server.ResolveDependency(); - var gameTicker = server.ResolveDependency().GetEntitySystem(); + var gameTicker = server.System(); await server.WaitAssertion(() => { diff --git a/Content.IntegrationTests/Tests/GameRules/StartEndGameRulesTest.cs b/Content.IntegrationTests/Tests/GameRules/StartEndGameRulesTest.cs index aef2fcf7cd..029704580e 100644 --- a/Content.IntegrationTests/Tests/GameRules/StartEndGameRulesTest.cs +++ b/Content.IntegrationTests/Tests/GameRules/StartEndGameRulesTest.cs @@ -26,7 +26,7 @@ public sealed class StartEndGameRulesTest : GameTest var pair = Pair; var server = pair.Server; await server.WaitIdleAsync(); - var gameTicker = server.ResolveDependency().GetEntitySystem(); + var gameTicker = server.System(); var cfg = server.ResolveDependency(); Assert.That(cfg.GetCVar(CCVars.GridFill), Is.False); diff --git a/Content.IntegrationTests/Tests/GameRules/TraitorRuleTest.cs b/Content.IntegrationTests/Tests/GameRules/TraitorRuleTest.cs index c76b349b64..84607cfcc0 100644 --- a/Content.IntegrationTests/Tests/GameRules/TraitorRuleTest.cs +++ b/Content.IntegrationTests/Tests/GameRules/TraitorRuleTest.cs @@ -8,6 +8,7 @@ using Content.Server.Mind; using Content.Server.Roles; using Content.Shared.GameTicking; using Content.Shared.GameTicking.Components; +using Content.Shared.GameTicking.Rules.Components; using Content.Shared.Mind; using Content.Shared.NPC.Prototypes; using Content.Shared.NPC.Systems; @@ -42,11 +43,10 @@ public sealed class TraitorRuleTest : GameTest var entMan = server.EntMan; var protoMan = server.ProtoMan; var compFact = server.ResolveDependency(); - var ticker = server.System(); + var ticker = server.System(); var mindSys = server.System(); var roleSys = server.System(); var factionSys = server.System(); - var traitorRuleSys = server.System(); // Look up the minimum player count and max total objective difficulty for the game rule var minPlayers = 1; @@ -96,7 +96,7 @@ public sealed class TraitorRuleTest : GameTest // Start the round ticker.StartRound(); // Force traitor mode to start (skip the delay) - ticker.StartGameRule(gameRuleEnt); + ticker.StartGameRule(gameRuleEnt!.Value); }); await pair.RunTicksSync(10); diff --git a/Content.IntegrationTests/Tests/GameTestTests/DependencyTests.cs b/Content.IntegrationTests/Tests/GameTestTests/DependencyTests.cs index 5ce2b486fd..a2cd082239 100644 --- a/Content.IntegrationTests/Tests/GameTestTests/DependencyTests.cs +++ b/Content.IntegrationTests/Tests/GameTestTests/DependencyTests.cs @@ -1,9 +1,8 @@ #nullable enable using System.Collections.Generic; -using Content.Client.GameTicking.Managers; +using Content.Client.GameTicking; using Content.IntegrationTests.Fixtures; using Content.IntegrationTests.Fixtures.Attributes; -using Content.IntegrationTests.NUnit.Constraints; using Content.Server.GameTicking; using Content.Shared.GameTicking; using Robust.Shared.GameObjects; @@ -14,8 +13,8 @@ namespace Content.IntegrationTests.Tests.GameTestTests; [TestOf(typeof(SidedDependencyAttribute))] public sealed class DependencyTests : GameTest { - [SidedDependency(Side.Server)] private readonly SharedGameTicker _sGameTicker = null!; - [SidedDependency(Side.Client)] private readonly SharedGameTicker _cGameTicker = null!; + [SidedDependency(Side.Server)] private readonly GameTicker _sGameTicker = null!; + [SidedDependency(Side.Client)] private readonly GameTicker _cGameTicker = null!; [SidedDependency(Side.Server)] private readonly EntityQuery _sXformQuery = default!; [Test] @@ -38,7 +37,7 @@ public sealed class DependencyTests : GameTest { Assert.That(!ReferenceEquals(_sGameTicker, _cGameTicker), "Server and client gametickers should be distinct"); - Assert.That(_sGameTicker, Is.TypeOf()); + Assert.That(_sGameTicker, Is.TypeOf()); Assert.That(_cGameTicker, Is.TypeOf()); } } diff --git a/Content.IntegrationTests/Tests/Internals/AutoInternalsTests.cs b/Content.IntegrationTests/Tests/Internals/AutoInternalsTests.cs index d2f67e9830..5ab785b05c 100644 --- a/Content.IntegrationTests/Tests/Internals/AutoInternalsTests.cs +++ b/Content.IntegrationTests/Tests/Internals/AutoInternalsTests.cs @@ -1,8 +1,8 @@ using Content.IntegrationTests.Fixtures; using Content.Server.Atmos.EntitySystems; using Content.Server.Body.Systems; -using Content.Server.Station.Systems; using Content.Shared.Preferences; +using Content.Shared.Station.Systems; namespace Content.IntegrationTests.Tests.Internals; diff --git a/Content.IntegrationTests/Tests/Minds/MindTests.EntityDeletion.cs b/Content.IntegrationTests/Tests/Minds/MindTests.EntityDeletion.cs index ce2cd643b2..e7b8037a0b 100644 --- a/Content.IntegrationTests/Tests/Minds/MindTests.EntityDeletion.cs +++ b/Content.IntegrationTests/Tests/Minds/MindTests.EntityDeletion.cs @@ -174,7 +174,7 @@ public sealed partial class MindTests EntityUid ghost = default!; await server.WaitAssertion(() => { - ghost = entMan.SpawnEntity(GameTicker.ObserverPrototypeName, MapCoordinates.Nullspace); + ghost = entMan.SpawnEntity(ServerGameTicker.ObserverPrototypeName, MapCoordinates.Nullspace); mindSystem.Visit(mind.Id, ghost); }); @@ -223,7 +223,7 @@ public sealed partial class MindTests var ghost = await BecomeGhost(pair); // Player is a normal ghost (not admin ghost). - Assert.That(entMan.GetComponent(player.AttachedEntity!.Value).EntityPrototype?.ID, Is.Not.EqualTo(GameTicker.AdminObserverPrototypeName)); + Assert.That(entMan.GetComponent(player.AttachedEntity!.Value).EntityPrototype?.ID, Is.Not.EqualTo(ServerGameTicker.AdminObserverPrototypeName)); // Try to become an admin ghost await server.WaitAssertion(() => serverConsole.ExecuteCommand(player, "aghost")); @@ -234,7 +234,7 @@ public sealed partial class MindTests { Assert.That(player.AttachedEntity, Is.Not.EqualTo(ghost), "Player is still attached to the old ghost"); Assert.That(entMan.HasComponent(player.AttachedEntity), "Player did not become a new ghost"); - Assert.That(entMan.GetComponent(player.AttachedEntity!.Value).EntityPrototype?.ID, Is.EqualTo(GameTicker.AdminObserverPrototypeName)); + Assert.That(entMan.GetComponent(player.AttachedEntity!.Value).EntityPrototype?.ID, Is.EqualTo(ServerGameTicker.AdminObserverPrototypeName)); }); var mindId = player.ContentData()?.Mind; diff --git a/Content.IntegrationTests/Tests/Minds/MindTests.Helpers.cs b/Content.IntegrationTests/Tests/Minds/MindTests.Helpers.cs index b3831a0cb1..5735987380 100644 --- a/Content.IntegrationTests/Tests/Minds/MindTests.Helpers.cs +++ b/Content.IntegrationTests/Tests/Minds/MindTests.Helpers.cs @@ -80,7 +80,7 @@ public sealed partial class MindTests await pair.Server.WaitAssertion(() => { var oldUid = player.AttachedEntity; - ghostUid = entMan.SpawnEntity(GameTicker.ObserverPrototypeName, MapCoordinates.Nullspace); + ghostUid = entMan.SpawnEntity(ServerGameTicker.ObserverPrototypeName, MapCoordinates.Nullspace); mindId = mindSys.GetMind(player.UserId)!.Value; Assert.That(mindId, Is.Not.EqualTo(default(EntityUid))); mind = entMan.GetComponent(mindId); diff --git a/Content.IntegrationTests/Tests/PostMapInitTest.cs b/Content.IntegrationTests/Tests/PostMapInitTest.cs index 2470568dc3..262a01709b 100644 --- a/Content.IntegrationTests/Tests/PostMapInitTest.cs +++ b/Content.IntegrationTests/Tests/PostMapInitTest.cs @@ -10,11 +10,11 @@ using Content.Server.Administration.Systems; using Content.Server.GameTicking; using Content.Server.Shuttles.Components; using Content.Server.Shuttles.Systems; -using Content.Server.Spawners.Components; -using Content.Server.Station.Components; using Content.Shared.CCVar; using Content.Shared.Maps; using Content.Shared.Roles; +using Content.Shared.Shuttles.Components; +using Content.Shared.Spawners.Components; using Content.Shared.Station.Components; using Robust.Shared.Configuration; using Robust.Shared.ContentPack; @@ -333,7 +333,7 @@ namespace Content.IntegrationTests.Tests var mapLoader = entManager.System(); var mapSystem = entManager.System(); var protoManager = server.ResolveDependency(); - var ticker = entManager.EntitySysManager.GetEntitySystem(); + var ticker = entManager.EntitySysManager.GetEntitySystem(); var shuttleSystem = entManager.EntitySysManager.GetEntitySystem(); var cfg = server.ResolveDependency(); diff --git a/Content.IntegrationTests/Tests/Power/StationPowerTests.cs b/Content.IntegrationTests/Tests/Power/StationPowerTests.cs index 236ebea2ff..ba27e106b4 100644 --- a/Content.IntegrationTests/Tests/Power/StationPowerTests.cs +++ b/Content.IntegrationTests/Tests/Power/StationPowerTests.cs @@ -50,7 +50,7 @@ public sealed class StationPowerTests : GameTest var entMan = server.EntMan; var protoMan = server.ProtoMan; - var ticker = entMan.System(); + var ticker = entMan.System(); var batterySys = entMan.System(); // Load the map @@ -108,7 +108,7 @@ public sealed class StationPowerTests : GameTest var entMan = server.EntMan; var protoMan = server.ProtoMan; - var ticker = entMan.System(); + var ticker = entMan.System(); var xform = entMan.System(); // Load the map diff --git a/Content.IntegrationTests/Tests/Preferences/LoadoutTests.cs b/Content.IntegrationTests/Tests/Preferences/LoadoutTests.cs index 62c9f1a460..7babf8f7f4 100644 --- a/Content.IntegrationTests/Tests/Preferences/LoadoutTests.cs +++ b/Content.IntegrationTests/Tests/Preferences/LoadoutTests.cs @@ -4,6 +4,7 @@ using Content.Server.Station.Systems; using Content.Shared.Inventory; using Content.Shared.Preferences; using Content.Shared.Preferences.Loadouts; +using Content.Shared.Station.Systems; using Robust.Shared.GameObjects; using Robust.Shared.Prototypes; diff --git a/Content.IntegrationTests/Tests/Replays/ReplayTests.cs b/Content.IntegrationTests/Tests/Replays/ReplayTests.cs index 4bd5f62e78..774c93f070 100644 --- a/Content.IntegrationTests/Tests/Replays/ReplayTests.cs +++ b/Content.IntegrationTests/Tests/Replays/ReplayTests.cs @@ -38,7 +38,7 @@ public sealed class ReplayTests : GameTest server.CfgMan.SetCVar(CCVars.ReplayAutoRecordName, $"c/d/{autoRecName}"); // Restart the round a few times - var ticker = server.System(); + var ticker = server.System(); await server.WaitPost(() => ticker.RestartRound()); await pair.RunTicksSync(25); Assert.That(recordMan.IsRecording, Is.True); diff --git a/Content.IntegrationTests/Tests/ResettingEntitySystemTests.cs b/Content.IntegrationTests/Tests/ResettingEntitySystemTests.cs index 17f178645a..bf646dd331 100644 --- a/Content.IntegrationTests/Tests/ResettingEntitySystemTests.cs +++ b/Content.IntegrationTests/Tests/ResettingEntitySystemTests.cs @@ -41,7 +41,7 @@ namespace Content.IntegrationTests.Tests var server = pair.Server; var entitySystemManager = server.ResolveDependency(); - var gameTicker = entitySystemManager.GetEntitySystem(); + var gameTicker = entitySystemManager.GetEntitySystem(); await server.WaitAssertion(() => { diff --git a/Content.IntegrationTests/Tests/RestartRoundTest.cs b/Content.IntegrationTests/Tests/RestartRoundTest.cs index 1ed43b0951..b32fc22a36 100644 --- a/Content.IntegrationTests/Tests/RestartRoundTest.cs +++ b/Content.IntegrationTests/Tests/RestartRoundTest.cs @@ -23,7 +23,7 @@ namespace Content.IntegrationTests.Tests await server.WaitPost(() => { - sysManager.GetEntitySystem().RestartRound(); + sysManager.GetEntitySystem().RestartRound(); }); await pair.RunUntilSynced(); diff --git a/Content.IntegrationTests/Tests/Round/JobTest.cs b/Content.IntegrationTests/Tests/Round/JobTest.cs index 04e629c1ba..2abd28d6e2 100644 --- a/Content.IntegrationTests/Tests/Round/JobTest.cs +++ b/Content.IntegrationTests/Tests/Round/JobTest.cs @@ -85,7 +85,7 @@ public sealed class JobTest : GameTest var jobSys = pair.Server.System(); var mindSys = pair.Server.System(); var roleSys = pair.Server.System(); - var ticker = pair.Server.System(); + var ticker = pair.Server.System(); user ??= pair.Client.User!.Value; @@ -110,7 +110,7 @@ public sealed class JobTest : GameTest var pair = Pair; pair.Server.CfgMan.SetCVar(CCVars.GameMap, _map); - var ticker = pair.Server.System(); + var ticker = pair.Server.System(); // Initially in the lobby Assert.That(ticker.RunLevel, Is.EqualTo(GameRunLevel.PreRoundLobby)); @@ -137,7 +137,7 @@ public sealed class JobTest : GameTest var pair = Pair; pair.Server.CfgMan.SetCVar(CCVars.GameMap, _map); - var ticker = pair.Server.System(); + var ticker = pair.Server.System(); Assert.That(ticker.RunLevel, Is.EqualTo(GameRunLevel.PreRoundLobby)); Assert.That(pair.Client.AttachedEntity, Is.Null); @@ -170,11 +170,11 @@ public sealed class JobTest : GameTest var pair = Pair; pair.Server.CfgMan.SetCVar(CCVars.GameMap, _map); - var ticker = pair.Server.System(); + var ticker = pair.Server.System(); Assert.That(ticker.RunLevel, Is.EqualTo(GameRunLevel.PreRoundLobby)); Assert.That(pair.Client.AttachedEntity, Is.Null); - var stationJobs = pair.Server.System(); + var stationJobs = pair.Server.System(); var captain = pair.Server.ProtoMan.Index(Captain); var engineer = pair.Server.ProtoMan.Index(Engineer); var passenger = pair.Server.ProtoMan.Index(Passenger); @@ -202,9 +202,9 @@ public sealed class JobTest : GameTest { var pair = Pair; pair.Server.CfgMan.SetCVar(CCVars.GameMap, JobWeightOverrideMap); - var ticker = pair.Server.System(); + var ticker = pair.Server.System(); - var stationJobs = pair.Server.System(); + var stationJobs = pair.Server.System(); var passenger = pair.Server.ProtoMan.Index(Passenger); var engineer = pair.Server.ProtoMan.Index(Engineer); var captain = pair.Server.ProtoMan.Index(Captain); @@ -256,7 +256,7 @@ public sealed class JobTest : GameTest var pair = Pair; pair.Server.CfgMan.SetCVar(CCVars.GameMap, _map); - var ticker = pair.Server.System(); + var ticker = pair.Server.System(); Assert.That(ticker.RunLevel, Is.EqualTo(GameRunLevel.PreRoundLobby)); Assert.That(pair.Client.AttachedEntity, Is.Null); diff --git a/Content.IntegrationTests/Tests/RoundEndTest.cs b/Content.IntegrationTests/Tests/RoundEndTest.cs index 4a02ede05e..72afdf480d 100644 --- a/Content.IntegrationTests/Tests/RoundEndTest.cs +++ b/Content.IntegrationTests/Tests/RoundEndTest.cs @@ -2,6 +2,7 @@ using Content.IntegrationTests.Fixtures; using Content.Server.GameTicking; using Content.Server.RoundEnd; using Content.Shared.CCVar; +using Content.Shared.GameTicking; using Robust.Shared.Configuration; using Robust.Shared.GameObjects; @@ -43,7 +44,7 @@ namespace Content.IntegrationTests.Tests var config = server.ResolveDependency(); var sysManager = server.ResolveDependency(); - var ticker = sysManager.GetEntitySystem(); + var ticker = sysManager.GetEntitySystem(); var roundEndSystem = sysManager.GetEntitySystem(); var sys = server.System(); sys.RoundCount = 0; diff --git a/Content.IntegrationTests/Tests/ShuttleTest.cs b/Content.IntegrationTests/Tests/ShuttleTest.cs index 2c77f6b8c7..8429dc7813 100644 --- a/Content.IntegrationTests/Tests/ShuttleTest.cs +++ b/Content.IntegrationTests/Tests/ShuttleTest.cs @@ -1,6 +1,7 @@ using System.Numerics; using Content.IntegrationTests.Fixtures; using Content.Server.Shuttles.Components; +using Content.Shared.Shuttles.Components; using Robust.Shared.GameObjects; using Robust.Shared.Map; using Robust.Shared.Physics; diff --git a/Content.IntegrationTests/Tests/Station/EvacShuttleTest.cs b/Content.IntegrationTests/Tests/Station/EvacShuttleTest.cs index e6a098a639..fc1d3741d4 100644 --- a/Content.IntegrationTests/Tests/Station/EvacShuttleTest.cs +++ b/Content.IntegrationTests/Tests/Station/EvacShuttleTest.cs @@ -4,6 +4,7 @@ using Content.Server.GameTicking; using Content.Server.Shuttles.Components; using Content.Server.Shuttles.Systems; using Content.Shared.CCVar; +using Content.Shared.GameTicking; using Content.Shared.Shuttles.Components; using Content.Shared.Station.Components; using Robust.Shared.GameObjects; @@ -30,7 +31,7 @@ public sealed class EvacShuttleTest : GameTest var pair = Pair; var server = pair.Server; var entMan = server.EntMan; - var ticker = server.System(); + var ticker = server.System(); // Dummy ticker tests should not have centcomm Assert.That(entMan.Count(), Is.Zero); diff --git a/Content.IntegrationTests/Tests/Station/StationJobsTest.cs b/Content.IntegrationTests/Tests/Station/StationJobsTest.cs index 7cdf0155ca..188fec54e8 100644 --- a/Content.IntegrationTests/Tests/Station/StationJobsTest.cs +++ b/Content.IntegrationTests/Tests/Station/StationJobsTest.cs @@ -6,6 +6,7 @@ using Content.Shared.CCVar; using Content.Shared.Maps; using Content.Shared.Preferences; using Content.Shared.Roles; +using Content.Shared.Station.Components; using Robust.Shared.Configuration; using Robust.Shared.GameObjects; using Robust.Shared.Network; @@ -14,7 +15,7 @@ using Robust.Shared.Prototypes; namespace Content.IntegrationTests.Tests.Station; [TestFixture] -[TestOf(typeof(StationJobsSystem))] +[TestOf(typeof(ServerStationJobsSystem))] public sealed class StationJobsTest : GameTest { private const string StationMapId = "FooStation"; @@ -137,8 +138,8 @@ public sealed class StationJobsTest : GameTest var prototypeManager = server.ResolveDependency(); var barStationProto = prototypeManager.Index(SecondStationMapId); var entSysMan = server.ResolveDependency().EntitySysManager; - var stationJobs = entSysMan.GetEntitySystem(); - var stationSystem = entSysMan.GetEntitySystem(); + var stationJobs = entSysMan.GetEntitySystem(); + var stationSystem = entSysMan.GetEntitySystem(); var firstStation = EntityUid.Invalid; var secondStation = EntityUid.Invalid; @@ -198,8 +199,8 @@ public sealed class StationJobsTest : GameTest var prototypeManager = server.ResolveDependency(); var barStationProto = prototypeManager.Index(SecondStationMapId); var entSysMan = server.ResolveDependency().EntitySysManager; - var stationJobs = entSysMan.GetEntitySystem(); - var stationSystem = entSysMan.GetEntitySystem(); + var stationJobs = entSysMan.GetEntitySystem(); + var stationSystem = entSysMan.GetEntitySystem(); var station = EntityUid.Invalid; await server.WaitPost(() => @@ -264,8 +265,8 @@ public sealed class StationJobsTest : GameTest var prototypeManager = server.ResolveDependency(); var fooStationProto = prototypeManager.Index(StationMapId); var entSysMan = server.ResolveDependency().EntitySysManager; - var stationJobs = entSysMan.GetEntitySystem(); - var stationSystem = entSysMan.GetEntitySystem(); + var stationJobs = entSysMan.GetEntitySystem(); + var stationSystem = entSysMan.GetEntitySystem(); var station = EntityUid.Invalid; await server.WaitPost(() => diff --git a/Content.MapRenderer/Painters/MapPainter.cs b/Content.MapRenderer/Painters/MapPainter.cs index c6ae9c03bf..cbef2364e6 100644 --- a/Content.MapRenderer/Painters/MapPainter.cs +++ b/Content.MapRenderer/Painters/MapPainter.cs @@ -172,7 +172,7 @@ namespace Content.MapRenderer.Painters if (_map is RenderMapPrototype) { - var mapId = sEntityManager.System().DefaultMap; + var mapId = sEntityManager.System().DefaultMap; _grids = mapSys.GetAllGrids(mapId).ToArray(); } diff --git a/Content.Server/Access/Systems/PresetIdCardSystem.cs b/Content.Server/Access/Systems/PresetIdCardSystem.cs index 63a355546e..ee224bad09 100644 --- a/Content.Server/Access/Systems/PresetIdCardSystem.cs +++ b/Content.Server/Access/Systems/PresetIdCardSystem.cs @@ -1,9 +1,9 @@ using Content.Server.Access.Components; -using Content.Server.GameTicking; -using Content.Server.Station.Components; using Content.Server.Station.Systems; using Content.Shared.Access.Systems; +using Content.Shared.GameTicking; using Content.Shared.Roles; +using Content.Shared.Station.Components; namespace Content.Server.Access.Systems; @@ -11,7 +11,7 @@ public sealed partial class PresetIdCardSystem : EntitySystem { [Dependency] private IdCardSystem _cardSystem = default!; [Dependency] private SharedAccessSystem _accessSystem = default!; - [Dependency] private StationSystem _stationSystem = default!; + [Dependency] private ServerStationSystem _stationSystem = default!; public override void Initialize() { diff --git a/Content.Server/Administration/AdminEventLog/AdminEventLogEui.cs b/Content.Server/Administration/AdminEventLog/AdminEventLogEui.cs index 5f9cea5316..5e668c7bc1 100644 --- a/Content.Server/Administration/AdminEventLog/AdminEventLogEui.cs +++ b/Content.Server/Administration/AdminEventLog/AdminEventLogEui.cs @@ -20,7 +20,7 @@ public sealed partial class AdminEventLogEui : BaseEui [Dependency] private EventWebhook _eventWebhook = default!; [Dependency] private IEntityManager _e = default!; - private int CurrentRoundId => _e.System().RoundId; + private int CurrentRoundId => _e.System().RoundId; public AdminEventLogEui() { diff --git a/Content.Server/Administration/Commands/AGhostCommand.cs b/Content.Server/Administration/Commands/AGhostCommand.cs index 5b450a2c4e..ae23427e45 100644 --- a/Content.Server/Administration/Commands/AGhostCommand.cs +++ b/Content.Server/Administration/Commands/AGhostCommand.cs @@ -75,7 +75,7 @@ public sealed partial class AGhostCommand : LocalizedCommands var metaDataSystem = _entities.System(); var ghostSystem = _entities.System(); var transformSystem = _entities.System(); - var gameTicker = _entities.System(); + var gameTicker = _entities.System(); if (!mindSystem.TryGetMind(player, out var mindId, out var mind)) { @@ -102,7 +102,7 @@ public sealed partial class AGhostCommand : LocalizedCommands coordinates = gameTicker.GetObserverSpawnPoint(); } - var ghost = _entities.SpawnEntity(GameTicker.AdminObserverPrototypeName, coordinates.Value); + var ghost = _entities.SpawnEntity(ServerGameTicker.AdminObserverPrototypeName, coordinates.Value); if (canReturn) { diff --git a/Content.Server/Administration/Commands/ForceGhostCommand.cs b/Content.Server/Administration/Commands/ForceGhostCommand.cs index 8a11a93917..b5f869ce4d 100644 --- a/Content.Server/Administration/Commands/ForceGhostCommand.cs +++ b/Content.Server/Administration/Commands/ForceGhostCommand.cs @@ -12,7 +12,7 @@ namespace Content.Server.Administration.Commands; public sealed partial class ForceGhostCommand : LocalizedEntityCommands { [Dependency] private IPlayerManager _playerManager = default!; - [Dependency] private GameTicker _gameTicker = default!; + [Dependency] private ServerGameTicker _gameTicker = default!; [Dependency] private SharedMindSystem _mind = default!; [Dependency] private GhostSystem _ghost = default!; diff --git a/Content.Server/Administration/Commands/LoadGameMapCommand.cs b/Content.Server/Administration/Commands/LoadGameMapCommand.cs index a3da721f82..019b1f0550 100644 --- a/Content.Server/Administration/Commands/LoadGameMapCommand.cs +++ b/Content.Server/Administration/Commands/LoadGameMapCommand.cs @@ -12,7 +12,7 @@ namespace Content.Server.Administration.Commands public sealed partial class LoadGameMapCommand : LocalizedEntityCommands { [Dependency] private IPrototypeManager _prototypeManager = default!; - [Dependency] private GameTicker _gameTicker = default!; + [Dependency] private ServerGameTicker _gameTicker = default!; [Dependency] private SharedMapSystem _mapSystem = default!; public override string Command => "loadgamemap"; diff --git a/Content.Server/Administration/Commands/ReadyAllCommand.cs b/Content.Server/Administration/Commands/ReadyAllCommand.cs index aa25830c4e..e173c12dd0 100644 --- a/Content.Server/Administration/Commands/ReadyAllCommand.cs +++ b/Content.Server/Administration/Commands/ReadyAllCommand.cs @@ -1,5 +1,6 @@ using Content.Server.GameTicking; using Content.Shared.Administration; +using Content.Shared.GameTicking; using Robust.Shared.Console; namespace Content.Server.Administration.Commands; @@ -7,7 +8,7 @@ namespace Content.Server.Administration.Commands; [AdminCommand(AdminFlags.Round)] public sealed partial class ReadyAllCommand : LocalizedEntityCommands { - [Dependency] private GameTicker _gameTicker = default!; + [Dependency] private ServerGameTicker _gameTicker = default!; public override string Command => "readyall"; diff --git a/Content.Server/Administration/Commands/ThrowScoreboardCommand.cs b/Content.Server/Administration/Commands/ThrowScoreboardCommand.cs index cacd043f39..1a9cabb558 100644 --- a/Content.Server/Administration/Commands/ThrowScoreboardCommand.cs +++ b/Content.Server/Administration/Commands/ThrowScoreboardCommand.cs @@ -22,6 +22,6 @@ public sealed partial class ThrowScoreboardCommand : IConsoleCommand shell.WriteLine(Help); return; } - _e.System().ShowRoundEndScoreboard(); + _e.System().ShowRoundEndScoreboard(); } } diff --git a/Content.Server/Administration/Logs/AdminLogManager.cs b/Content.Server/Administration/Logs/AdminLogManager.cs index 02884a8703..a4b16d2d93 100644 --- a/Content.Server/Administration/Logs/AdminLogManager.cs +++ b/Content.Server/Administration/Logs/AdminLogManager.cs @@ -5,11 +5,11 @@ using System.Threading.Tasks; using Content.Server.Administration.Systems; using Content.Server.Database; using Content.Server.EUI; -using Content.Server.GameTicking; using Content.Shared.Administration.Logs; using Content.Shared.CCVar; using Content.Shared.Chat; using Content.Shared.Database; +using Content.Shared.GameTicking; using Content.Shared.Mind; using Content.Shared.Players.PlayTimeTracking; using Prometheus; diff --git a/Content.Server/Administration/Logs/AdminLogSystem.cs b/Content.Server/Administration/Logs/AdminLogSystem.cs index aa3209bc39..e376750b5c 100644 --- a/Content.Server/Administration/Logs/AdminLogSystem.cs +++ b/Content.Server/Administration/Logs/AdminLogSystem.cs @@ -1,5 +1,5 @@ -using Content.Server.GameTicking; -using Content.Server.GameTicking.Events; +using Content.Shared.GameTicking; +using Content.Shared.GameTicking.Events; namespace Content.Server.Administration.Logs; diff --git a/Content.Server/Administration/Logs/AdminLogsEui.cs b/Content.Server/Administration/Logs/AdminLogsEui.cs index 184d0cf273..84ac91d247 100644 --- a/Content.Server/Administration/Logs/AdminLogsEui.cs +++ b/Content.Server/Administration/Logs/AdminLogsEui.cs @@ -52,7 +52,7 @@ public sealed partial class AdminLogsEui : BaseEui }; } - private int CurrentRoundId => _e.System().RoundId; + private int CurrentRoundId => _e.System().RoundId; public override async void Opened() { diff --git a/Content.Server/Administration/Logs/IAdminLogManager.cs b/Content.Server/Administration/Logs/IAdminLogManager.cs index 0793095ebf..cbd2eaca51 100644 --- a/Content.Server/Administration/Logs/IAdminLogManager.cs +++ b/Content.Server/Administration/Logs/IAdminLogManager.cs @@ -1,9 +1,8 @@ using System.Text.Json; using System.Threading.Tasks; using Content.Server.Database; -using Content.Server.GameTicking; using Content.Shared.Administration.Logs; -using Robust.Shared.Network; +using Content.Shared.GameTicking; using Robust.Shared.Player; namespace Content.Server.Administration.Logs; diff --git a/Content.Server/Administration/Managers/BanManager.cs b/Content.Server/Administration/Managers/BanManager.cs index 093ff092ee..dec725374d 100644 --- a/Content.Server/Administration/Managers/BanManager.cs +++ b/Content.Server/Administration/Managers/BanManager.cs @@ -276,7 +276,7 @@ public sealed partial class BanManager : IBanManager, IPostInjectInit { roundIds = [..banInfo.RoundIds]; } - else if (_systems.TryGetEntitySystem(out var ticker) && ticker.RoundId != 0) + else if (_systems.TryGetEntitySystem(out var ticker) && ticker.RoundId != 0) { roundIds = [ticker.RoundId]; } diff --git a/Content.Server/Administration/Notes/AdminNotesManager.cs b/Content.Server/Administration/Notes/AdminNotesManager.cs index 90a04c6164..2dd4b2cf34 100644 --- a/Content.Server/Administration/Notes/AdminNotesManager.cs +++ b/Content.Server/Administration/Notes/AdminNotesManager.cs @@ -122,7 +122,7 @@ public sealed partial class AdminNotesManager : IAdminNotesManager, IPostInjectI _sawmill.Info(sb.ToString()); - _systems.TryGetEntitySystem(out GameTicker? ticker); + _systems.TryGetEntitySystem(out ServerGameTicker? ticker); int? roundId = ticker == null || ticker.RoundId == 0 ? null : ticker.RoundId; var serverName = _config.GetCVar(CCVars.AdminLogsServerName); // This could probably be done another way, but this is fine. For displaying only. var createdAt = DateTime.UtcNow; diff --git a/Content.Server/Administration/ServerApi.cs b/Content.Server/Administration/ServerApi.cs index f7c5882d59..f2da1fbebb 100644 --- a/Content.Server/Administration/ServerApi.cs +++ b/Content.Server/Administration/ServerApi.cs @@ -10,14 +10,14 @@ using Content.Server.Administration.Managers; using Content.Server.Administration.Systems; using Content.Server.Database; using Content.Server.GameTicking; -using Content.Server.GameTicking.Presets; -using Content.Server.GameTicking.Rules.Components; using Content.Server.Maps; using Content.Server.RoundEnd; using Content.Shared.Administration.Managers; using Content.Shared.CCVar; using Content.Shared.Database; +using Content.Shared.GameTicking; using Content.Shared.GameTicking.Components; +using Content.Shared.GameTicking.Prototypes; using Content.Shared.Prototypes; using Robust.Server.ServerStatus; using Robust.Shared.Asynchronous; @@ -218,7 +218,7 @@ public sealed partial class ServerApi : IPostInjectInit await RunOnMainThread(async () => { - var ticker = _entitySystemManager.GetEntitySystem(); + var ticker = _entitySystemManager.GetEntitySystem(); if (ticker.RunLevel != GameRunLevel.PreRoundLobby) { await RespondError( @@ -258,7 +258,7 @@ public sealed partial class ServerApi : IPostInjectInit await RunOnMainThread(async () => { - var ticker = _entitySystemManager.GetEntitySystem(); + var ticker = _entitySystemManager.GetEntitySystem(); var gameRule = ticker .GetActiveGameRules() .FirstOrNull(rule => @@ -292,7 +292,7 @@ public sealed partial class ServerApi : IPostInjectInit await RunOnMainThread(async () => { - var ticker = _entitySystemManager.GetEntitySystem(); + var ticker = _entitySystemManager.GetEntitySystem(); if (!_prototypeManager.HasIndex(body.GameRuleId)) { await RespondError(context, @@ -302,11 +302,13 @@ public sealed partial class ServerApi : IPostInjectInit return; } - var ruleEntity = ticker.AddGameRule(body.GameRuleId); + if (ticker.AddGameRule(body.GameRuleId) is not { } ruleEntity) + return; + _sawmill.Info($"Added game rule {body.GameRuleId} by {FormatLogActor(actor)}."); if (ticker.RunLevel == GameRunLevel.InRound) { - ticker.StartGameRule(ruleEntity); + ticker.StartGameRule(ruleEntity.AsNullable()); _sawmill.Info($"Started game rule {body.GameRuleId} by {FormatLogActor(actor)}."); } @@ -418,7 +420,7 @@ public sealed partial class ServerApi : IPostInjectInit { await RunOnMainThread(async () => { - var ticker = _entitySystemManager.GetEntitySystem(); + var ticker = _entitySystemManager.GetEntitySystem(); if (ticker.RunLevel != GameRunLevel.PreRoundLobby) { @@ -441,7 +443,7 @@ public sealed partial class ServerApi : IPostInjectInit await RunOnMainThread(async () => { var roundEndSystem = _entitySystemManager.GetEntitySystem(); - var ticker = _entitySystemManager.GetEntitySystem(); + var ticker = _entitySystemManager.GetEntitySystem(); if (ticker.RunLevel != GameRunLevel.InRound) { @@ -463,7 +465,7 @@ public sealed partial class ServerApi : IPostInjectInit { await RunOnMainThread(async () => { - var ticker = _entitySystemManager.GetEntitySystem(); + var ticker = _entitySystemManager.GetEntitySystem(); ticker.RestartRound(); _sawmill.Info($"Forced instant round restart by {FormatLogActor(actor)}"); @@ -544,7 +546,7 @@ public sealed partial class ServerApi : IPostInjectInit var info = await RunOnMainThread(() => { - var ticker = _entitySystemManager.GetEntitySystem(); + var ticker = _entitySystemManager.GetEntitySystem(); var adminSystem = _entitySystemManager.GetEntitySystem(); var players = new List(); diff --git a/Content.Server/Administration/Systems/AdminSystem.cs b/Content.Server/Administration/Systems/AdminSystem.cs index 146f673211..ba580a0497 100644 --- a/Content.Server/Administration/Systems/AdminSystem.cs +++ b/Content.Server/Administration/Systems/AdminSystem.cs @@ -49,7 +49,7 @@ public sealed partial class AdminSystem : EntitySystem [Dependency] private PhysicsSystem _physics = default!; [Dependency] private PlayTimeTrackingManager _playTime = default!; [Dependency] private SharedRoleSystem _role = default!; - [Dependency] private GameTicker _gameTicker = default!; + [Dependency] private ServerGameTicker _gameTicker = default!; [Dependency] private SharedAudioSystem _audio = default!; [Dependency] private StationRecordsSystem _stationRecords = default!; [Dependency] private TransformSystem _transform = default!; diff --git a/Content.Server/Administration/Systems/AdminVerbSystem.Antags.cs b/Content.Server/Administration/Systems/AdminVerbSystem.Antags.cs index 078daa0b04..9a94d3ff46 100644 --- a/Content.Server/Administration/Systems/AdminVerbSystem.Antags.cs +++ b/Content.Server/Administration/Systems/AdminVerbSystem.Antags.cs @@ -1,10 +1,10 @@ -using Content.Server.Antag; using Content.Server.GameTicking; -using Content.Server.GameTicking.Rules.Components; using Content.Server.Zombies; using Content.Shared.Administration; using Content.Server.Clothing.Systems; +using Content.Shared.Antag; using Content.Shared.Database; +using Content.Shared.GameTicking.Rules.Components; using Content.Shared.Humanoid; using Content.Shared.Mind.Components; using Content.Shared.Roles; @@ -20,7 +20,7 @@ public sealed partial class AdminVerbSystem { [Dependency] private AntagSelectionSystem _antag = default!; [Dependency] private ZombieSystem _zombie = default!; - [Dependency] private GameTicker _gameTicker = default!; + [Dependency] private ServerGameTicker _gameTicker = default!; [Dependency] private OutfitSystem _outfit = default!; private static readonly EntProtoId DefaultTraitorRule = "Traitor"; @@ -179,14 +179,15 @@ public sealed partial class AdminVerbSystem Icon = new SpriteSpecifier.Rsi(new("/Textures/Interface/Misc/job_icons.rsi"), "ParadoxClone"), Act = () => { - var ruleEnt = _gameTicker.AddGameRule(ParadoxCloneRuleId); + if (_gameTicker.AddGameRule(ParadoxCloneRuleId) is not { } ruleEnt) + return; if (!TryComp(ruleEnt, out var paradoxCloneRuleComp)) return; paradoxCloneRuleComp.OriginalBody = args.Target; // override the target player - _gameTicker.StartGameRule(ruleEnt); + _gameTicker.StartGameRule(ruleEnt.AsNullable()); }, Impact = LogImpact.High, Message = string.Join(": ", paradoxCloneName, Loc.GetString("admin-verb-make-paradox-clone")), diff --git a/Content.Server/Administration/Systems/AdminVerbSystem.Tools.cs b/Content.Server/Administration/Systems/AdminVerbSystem.Tools.cs index da4b442382..e98c5e5a7c 100644 --- a/Content.Server/Administration/Systems/AdminVerbSystem.Tools.cs +++ b/Content.Server/Administration/Systems/AdminVerbSystem.Tools.cs @@ -47,7 +47,7 @@ public sealed partial class AdminVerbSystem [Dependency] private HandsSystem _handsSystem = default!; [Dependency] private QuickDialogSystem _quickDialog = default!; [Dependency] private AdminTestArenaSystem _adminTestArenaSystem = default!; - [Dependency] private StationJobsSystem _stationJobsSystem = default!; + [Dependency] private ServerStationJobsSystem _stationJobsSystem = default!; [Dependency] private JointSystem _jointSystem = default!; [Dependency] private SharedBatterySystem _batterySystem = default!; [Dependency] private MetaDataSystem _metaSystem = default!; diff --git a/Content.Server/Administration/Systems/AdminVerbSystem.cs b/Content.Server/Administration/Systems/AdminVerbSystem.cs index e2f0f152f1..3ef8c7ecfb 100644 --- a/Content.Server/Administration/Systems/AdminVerbSystem.cs +++ b/Content.Server/Administration/Systems/AdminVerbSystem.cs @@ -7,7 +7,6 @@ using Content.Server.Ghost.Roles; using Content.Server.Mind; using Content.Server.Prayer; using Content.Server.Silicons.Laws; -using Content.Server.Station.Systems; using Content.Shared.Administration; using Content.Shared.Administration.Systems; using Content.Shared.Chemistry.Components.SolutionManager; @@ -36,7 +35,7 @@ using Robust.Shared.Toolshed; using Robust.Shared.Utility; using System.Linq; using Content.Shared.Chemistry.Components; -using Content.Shared.Mind; +using Content.Shared.Station.Systems; using static Content.Shared.Configurable.ConfigurationComponent; namespace Content.Server.Administration.Systems diff --git a/Content.Server/Administration/Systems/BwoinkSystem.cs b/Content.Server/Administration/Systems/BwoinkSystem.cs index 59cf34cb66..4ab2ded9eb 100644 --- a/Content.Server/Administration/Systems/BwoinkSystem.cs +++ b/Content.Server/Administration/Systems/BwoinkSystem.cs @@ -38,7 +38,7 @@ namespace Content.Server.Administration.Systems [Dependency] private IConfigurationManager _config = default!; [Dependency] private IGameTiming _timing = default!; [Dependency] private IPlayerLocator _playerLocator = default!; - [Dependency] private GameTicker _gameTicker = default!; + [Dependency] private ServerGameTicker _gameTicker = default!; [Dependency] private SharedMindSystem _minds = default!; [Dependency] private IAfkManager _afkManager = default!; [Dependency] private IServerDbManager _dbManager = default!; diff --git a/Content.Server/Afk/AfkSystem.cs b/Content.Server/Afk/AfkSystem.cs index 36e61f07ab..28ee238fc3 100644 --- a/Content.Server/Afk/AfkSystem.cs +++ b/Content.Server/Afk/AfkSystem.cs @@ -2,6 +2,7 @@ using Content.Server.Administration.Managers; using Content.Server.GameTicking; using Content.Shared.Afk.Events; using Content.Shared.CCVar; +using Content.Shared.GameTicking; using Content.Shared.Instruments; using Robust.Server.Player; using Robust.Shared.Configuration; @@ -21,7 +22,7 @@ public sealed partial class AfkSystem : EntitySystem [Dependency] private IAfkManager _afkManager = default!; [Dependency] private IPlayerManager _playerManager = default!; [Dependency] private IGameTiming _timing = default!; - [Dependency] private GameTicker _ticker = default!; + [Dependency] private ServerGameTicker _ticker = default!; [Dependency] private IConfigurationManager _cfg = default!; /// diff --git a/Content.Server/AlertLevel/AlertLevelDisplaySystem.cs b/Content.Server/AlertLevel/AlertLevelDisplaySystem.cs index bc524c565f..39dc7b0667 100644 --- a/Content.Server/AlertLevel/AlertLevelDisplaySystem.cs +++ b/Content.Server/AlertLevel/AlertLevelDisplaySystem.cs @@ -7,7 +7,7 @@ namespace Content.Server.AlertLevel; public sealed partial class AlertLevelDisplaySystem : EntitySystem { - [Dependency] private StationSystem _stationSystem = default!; + [Dependency] private ServerStationSystem _stationSystem = default!; [Dependency] private SharedAppearanceSystem _appearance = default!; public override void Initialize() diff --git a/Content.Server/AlertLevel/Commands/SetAlertLevelCommand.cs b/Content.Server/AlertLevel/Commands/SetAlertLevelCommand.cs index f781b7a7da..c09e67d5dd 100644 --- a/Content.Server/AlertLevel/Commands/SetAlertLevelCommand.cs +++ b/Content.Server/AlertLevel/Commands/SetAlertLevelCommand.cs @@ -2,6 +2,7 @@ using Content.Shared.Administration; using Content.Shared.AlertLevel; using Content.Shared.Station; +using Content.Shared.Station.Systems; using Robust.Shared.Console; using Robust.Shared.Prototypes; @@ -11,7 +12,7 @@ namespace Content.Server.AlertLevel.Commands; public sealed partial class SetAlertLevelCommand : LocalizedEntityCommands { [Dependency] private AlertLevelSystem _alertLevelSystem = default!; - [Dependency] private SharedStationSystem _stationSystem = default!; + [Dependency] private StationSystem _stationSystem = default!; [Dependency] private IPrototypeManager _prototype = default!; public override string Command => "setalertlevel"; diff --git a/Content.Server/Anomaly/AnomalySystem.cs b/Content.Server/Anomaly/AnomalySystem.cs index e1514bfeaf..2693aa108b 100644 --- a/Content.Server/Anomaly/AnomalySystem.cs +++ b/Content.Server/Anomaly/AnomalySystem.cs @@ -5,12 +5,12 @@ using Content.Server.Explosion.EntitySystems; using Content.Server.Materials; using Content.Server.Radiation.Systems; using Content.Server.Radio.EntitySystems; -using Content.Server.Station.Systems; using Content.Shared.Anomaly; using Content.Shared.Anomaly.Components; using Content.Shared.Anomaly.Prototypes; using Content.Shared.Random; using Content.Shared.Random.Helpers; +using Content.Shared.Station.Systems; using Robust.Server.GameObjects; using Robust.Shared.Audio.Systems; using Robust.Shared.Configuration; diff --git a/Content.Server/Antag/AntagMultipleRoleSpawnerSystem.cs b/Content.Server/Antag/AntagMultipleRoleSpawnerSystem.cs index 485c5d46ad..ad5eba21c9 100644 --- a/Content.Server/Antag/AntagMultipleRoleSpawnerSystem.cs +++ b/Content.Server/Antag/AntagMultipleRoleSpawnerSystem.cs @@ -1,4 +1,5 @@ using Content.Server.Antag.Components; +using Content.Shared.Antag; using Robust.Shared.Random; namespace Content.Server.Antag; diff --git a/Content.Server/Antag/AntagObjectivesSystem.cs b/Content.Server/Antag/AntagObjectivesSystem.cs index 6fffe18926..560b90f08d 100644 --- a/Content.Server/Antag/AntagObjectivesSystem.cs +++ b/Content.Server/Antag/AntagObjectivesSystem.cs @@ -1,7 +1,6 @@ using Content.Server.Antag.Components; -using Content.Server.Objectives; +using Content.Shared.Antag; using Content.Shared.Mind; -using Content.Shared.Objectives.Systems; namespace Content.Server.Antag; diff --git a/Content.Server/Antag/AntagRandomObjectivesSystem.cs b/Content.Server/Antag/AntagRandomObjectivesSystem.cs index 3be62e20c9..8832f3d227 100644 --- a/Content.Server/Antag/AntagRandomObjectivesSystem.cs +++ b/Content.Server/Antag/AntagRandomObjectivesSystem.cs @@ -1,8 +1,8 @@ using Content.Server.Antag.Components; using Content.Server.Objectives; +using Content.Shared.Antag; using Content.Shared.Mind; using Content.Shared.Objectives.Components; -using Content.Shared.Objectives.Systems; using Robust.Shared.Random; namespace Content.Server.Antag; diff --git a/Content.Server/Antag/AntagRandomSpawnRule.cs b/Content.Server/Antag/AntagRandomSpawnRule.cs index 696c224287..7f2dc4c0ef 100644 --- a/Content.Server/Antag/AntagRandomSpawnRule.cs +++ b/Content.Server/Antag/AntagRandomSpawnRule.cs @@ -1,11 +1,14 @@ using Content.Server.Antag.Components; +using Content.Server.Station.Systems; +using Content.Shared.Antag; using Content.Shared.GameTicking.Components; -using Content.Server.GameTicking.Rules; +using Content.Shared.GameTicking.Rules; namespace Content.Server.Antag; public sealed partial class AntagRandomSpawnSystem : GameRuleSystem { + [Dependency] private ServerStationSystem _station = default!; [Dependency] private SharedTransformSystem _transform = default!; public override void Initialize() @@ -15,15 +18,15 @@ public sealed partial class AntagRandomSpawnSystem : GameRuleSystem(OnSelectLocation); } - protected override void Added(EntityUid uid, AntagRandomSpawnComponent comp, GameRuleComponent gameRule, GameRuleAddedEvent args) + protected override void Added(Entity rule, ref GameRuleAddedEvent args) { - base.Added(uid, comp, gameRule, args); + base.Added(rule, ref args); // we have to select this here because AntagSelectLocationEvent is raised twice because MakeAntag is called twice // once when a ghost role spawner is created and once when someone takes the ghost role - if (TryFindRandomTile(out _, out _, out _, out var coords)) - comp.Coords = coords; + if (_station.TryFindRandomTile(out _, out _, out _, out var coords)) + rule.Comp1.Coords = coords; } private void OnSelectLocation(Entity ent, ref AntagSelectLocationEvent args) diff --git a/Content.Server/Antag/AntagSpawnerSystem.cs b/Content.Server/Antag/AntagSpawnerSystem.cs index 9bbe784265..6f2089e7dd 100644 --- a/Content.Server/Antag/AntagSpawnerSystem.cs +++ b/Content.Server/Antag/AntagSpawnerSystem.cs @@ -1,4 +1,5 @@ using Content.Server.Antag.Components; +using Content.Shared.Antag; namespace Content.Server.Antag; diff --git a/Content.Server/Antag/Components/AntagMultipleRoleSpawnerComponent.cs b/Content.Server/Antag/Components/AntagMultipleRoleSpawnerComponent.cs index f375dd6dc3..84415f4d8f 100644 --- a/Content.Server/Antag/Components/AntagMultipleRoleSpawnerComponent.cs +++ b/Content.Server/Antag/Components/AntagMultipleRoleSpawnerComponent.cs @@ -1,5 +1,4 @@ using Content.Shared.Antag; -using Content.Shared.Roles; using Robust.Shared.Prototypes; namespace Content.Server.Antag.Components; diff --git a/Content.Server/Antag/Components/AntagRandomSpawnComponent.cs b/Content.Server/Antag/Components/AntagRandomSpawnComponent.cs index b73d5cdb68..c3df203e7d 100644 --- a/Content.Server/Antag/Components/AntagRandomSpawnComponent.cs +++ b/Content.Server/Antag/Components/AntagRandomSpawnComponent.cs @@ -1,3 +1,4 @@ +using Content.Shared.Antag.Components; using Robust.Shared.Map; namespace Content.Server.Antag.Components; diff --git a/Content.Server/Antag/MobReplacementRuleSystem.cs b/Content.Server/Antag/MobReplacementRuleSystem.cs index aaaf83868a..3f66d1e0f2 100644 --- a/Content.Server/Antag/MobReplacementRuleSystem.cs +++ b/Content.Server/Antag/MobReplacementRuleSystem.cs @@ -1,6 +1,7 @@ using Content.Server.Antag.Mimic; using Content.Server.GameTicking.Rules; using Content.Shared.GameTicking.Components; +using Content.Shared.GameTicking.Rules; using Content.Shared.VendingMachines.Components; using Robust.Shared.Map; using Robust.Shared.Random; diff --git a/Content.Server/Antag/ServerAntagSelectionSystem.cs b/Content.Server/Antag/ServerAntagSelectionSystem.cs new file mode 100644 index 0000000000..dddb3dc9e9 --- /dev/null +++ b/Content.Server/Antag/ServerAntagSelectionSystem.cs @@ -0,0 +1,167 @@ +using System.Diagnostics; +using System.Linq; +using Content.Server.Administration.Managers; +using Content.Server.Chat.Managers; +using Content.Server.GameTicking.Events; +using Content.Server.GameTicking.Rules.Components; +using Content.Server.Ghost.Roles; +using Content.Server.Ghost.Roles.Components; +using Content.Server.Objectives; +using Content.Server.Players.PlayTimeTracking; +using Content.Server.Preferences.Managers; +using Content.Shared.Antag; +using Content.Shared.Antag.Components; +using Content.Shared.Chat; +using Content.Shared.Roles; +using Robust.Shared.Audio; +using Robust.Shared.Player; +using Robust.Shared.Prototypes; + +namespace Content.Server.Antag; + +/// +public sealed partial class ServerAntagSelectionSystem : AntagSelectionSystem +{ + [Dependency] private IBanManager _ban = default!; + [Dependency] private IChatManager _chat = default!; + [Dependency] private IServerPreferencesManager _pref = default!; + [Dependency] private GhostRoleSystem _ghostRole = default!; + [Dependency] private PlayTimeTrackingSystem _playTime = default!; + + [SubscribeLocalEvent] + private void OnTakeGhostRole(Entity ent, ref TakeGhostRoleEvent args) + { + if (args.TookRole) + return; + + if (ent.Comp.Rule is not { } rule || ent.Comp.Definition is not { } proto) + return; + + if (!ProtoMan.Resolve(proto, out var def)) + return; + + if (!Exists(rule) || !RuleQuery.TryComp(rule, out var select)) + return; + + // Ensure the player is allowed to play this antagonist! + if (IsAntagBanned(args.Player, def) || !_playTime.IsAllowed(args.Player, def.PrefRoles)) + return; + + if (!TrySpawnAntagonist((rule, select), def, args.Player, XForm.GetMapCoordinates(ent), out var uid)) + { + Log.Error($"Tried to make {args.Player.UserId} into an antagonist but was unable to spawn an entity for them. Game rule {ToPrettyString(ent)}"); + return; + } + + // We do this after TrySpawnAntagonist so we don't have to worry about a failed spawn adding permanent pre selections to a game rule. + PreSelectSession((rule, select), def, args.Player); + InitializeAntag((rule, select), def, uid.Value, args.Player); + args.TookRole = true; + + // Move ghosts that were watching the raffle on the spawner over to the freshly spawned antag. + Follower.TransferFollowers(ent.Owner, uid.Value); + + _ghostRole.UnregisterGhostRole((ent, Comp(ent))); + } + + [SubscribeLocalEvent] + private void OnObjectivesTextGetInfo(Entity ent, ref ObjectivesTextGetInfoEvent args) + { + if (ent.Comp.AgentName is not { } name) + return; + + args.Minds = GetAntagIdentities(ent.AsNullable()).ToList(); + args.AgentName = Loc.GetString(name); + } + + [SubscribeLocalEvent] + private void OnJobNotAssigned(NoJobsAvailableSpawningEvent args) + { + // If someone fails to spawn in due to there being no jobs, they should be removed from any preselected antags. + // We only care about delayed rules, since if they're active the player should have already been removed via MakeAntag. + var query = QueryDelayedRules(); + while (query.MoveNext(out var uid, out var comp, out _, out _)) + { + if (comp.SelectionTime == AntagSelectionTime.RuleStarted) + continue; + + Debug.Assert(comp.SelectionTime != AntagSelectionTime.Never, $"Player: {args.Player.Name}, was pre selected for an game rule {ToPrettyString(uid)} which does not do pre-selections"); + + if (!comp.RemoveUponFailedSpawn) + continue; + + foreach (var antag in comp.Antags) + { + if (!comp.PreSelectedSessions.TryGetValue(antag, out var session)) + break; + session.Remove(args.Player); + } + } + } + + public override IEnumerable> GetValidAntagPreferences(ICommonSession session, List>? filter = null) + { + if (!_pref.TryGetCachedPreferences(session.UserId, out var prefs)) + yield break; + + foreach (var antag in prefs.SelectedCharacter.AntagPreferences) + { + // We also check this in IsSessionValid, but we also check it here since this is public API. + if (_ban.IsRoleBanned(session, antag) || !_playTime.IsAllowed(session, antag)) + continue; + + if (filter != null && !filter.Contains(antag)) + continue; + + yield return antag; + } + } + + public override void SendBriefing(ICommonSession? session, string briefing, Color? briefingColor, SoundSpecifier? briefingSound) + { + if (session == null) + return; + + Audio.PlayGlobal(briefingSound, session); + if (!string.IsNullOrEmpty(briefing)) + { + var wrappedMessage = Loc.GetString("chat-manager-server-wrap-message", ("message", briefing)); + _chat.ChatMessageToOne(ChatChannel.Server, briefing, wrappedMessage, default, false, session.Channel, briefingColor); + } + } + + public override bool IsAntagBanned(ICommonSession session, AntagSpecifierPrototype definition) + { + if (_ban.GetAntagBans(session.UserId) is not { } bans) + return false; + + foreach (var role in definition.PrefRoles) + { + // banned! + if (bans.Contains(role)) + return true; + } + + return false; + } + + protected override Entity? ForceGetGameRuleEnt(string id) + { + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out _, out var comp)) + { + if (MetaData(uid).EntityPrototype?.ID == id) + return (uid, comp); + } + + // Game Rule is invalid. You *really* fucked up. + if (GameTicker.AddGameRule(id) is not { } ruleEnt) + return null; + + RemComp(ruleEnt); + var antag = RuleQuery.Comp(ruleEnt); + antag.AssignmentHandled = true; // don't do normal selection. + GameTicker.StartGameRule(ruleEnt.AsNullable()); + return (ruleEnt, antag); + } +} diff --git a/Content.Server/Audio/ContentAudioSystem.cs b/Content.Server/Audio/ContentAudioSystem.cs index 90451fde77..9f4b31ea2e 100644 --- a/Content.Server/Audio/ContentAudioSystem.cs +++ b/Content.Server/Audio/ContentAudioSystem.cs @@ -5,6 +5,7 @@ using Content.Shared.Audio; using Content.Shared.Audio.Events; using Content.Shared.CCVar; using Content.Shared.GameTicking; +using Content.Shared.GameTicking.Events; using Robust.Server.Audio; using Robust.Shared.Audio; using Robust.Shared.Configuration; diff --git a/Content.Server/Audio/ServerGlobalSoundSystem.cs b/Content.Server/Audio/ServerGlobalSoundSystem.cs index 06a05834c1..3974b7b4ed 100644 --- a/Content.Server/Audio/ServerGlobalSoundSystem.cs +++ b/Content.Server/Audio/ServerGlobalSoundSystem.cs @@ -10,7 +10,7 @@ namespace Content.Server.Audio; public sealed partial class ServerGlobalSoundSystem : SharedGlobalSoundSystem { [Dependency] private IConsoleHost _conHost = default!; - [Dependency] private StationSystem _stationSystem = default!; + [Dependency] private ServerStationSystem _stationSystem = default!; [Dependency] private SharedAudioSystem _audio = default!; public override void Shutdown() diff --git a/Content.Server/Bed/Cryostorage/CryostorageSystem.cs b/Content.Server/Bed/Cryostorage/CryostorageSystem.cs index f3888f0553..0c199ec0b6 100644 --- a/Content.Server/Bed/Cryostorage/CryostorageSystem.cs +++ b/Content.Server/Bed/Cryostorage/CryostorageSystem.cs @@ -1,6 +1,6 @@ +using System.Globalization; using Content.Server.Chat.Managers; using Content.Server.Ghost; -using Content.Server.Station.Components; using Content.Server.Station.Systems; using Content.Shared.Access.Systems; using Content.Shared.Bed.Cryostorage; @@ -10,22 +10,22 @@ using Content.Shared.Clothing.Components; using Content.Shared.Database; using Content.Shared.GameTicking; using Content.Shared.Hands.Components; +using Content.Shared.Hands.EntitySystems; +using Content.Shared.Inventory; using Content.Shared.Mind.Components; +using Content.Shared.Popups; +using Content.Shared.Station.Components; +using Content.Shared.Station.Systems; using Content.Shared.StationRecords; +using Content.Shared.StationRecords.Components; +using Content.Shared.StationRecords.Systems; using Content.Shared.UserInterface; using Robust.Server.Player; +using Robust.Shared.Audio.Systems; using Robust.Shared.Containers; using Robust.Shared.Enums; using Robust.Shared.Network; using Robust.Shared.Player; -using System.Globalization; -using Content.Shared.Hands.EntitySystems; -using Content.Shared.Inventory; -using Content.Shared.Popups; -using Content.Shared.Station; -using Content.Shared.StationRecords.Components; -using Content.Shared.StationRecords.Systems; -using Robust.Shared.Audio.Systems; namespace Content.Server.Bed.Cryostorage; @@ -43,8 +43,8 @@ public sealed partial class CryostorageSystem : SharedCryostorageSystem [Dependency] private SharedHandsSystem _hands = default!; [Dependency] private InventorySystem _inventory = default!; [Dependency] private SharedPopupSystem _popup = default!; - [Dependency] private SharedStationSystem _station = default!; - [Dependency] private StationJobsSystem _stationJobs = default!; + [Dependency] private StationSystem _station = default!; + [Dependency] private ServerStationJobsSystem _stationJobs = default!; [Dependency] private StationRecordsSystem _stationRecords = default!; [Dependency] private SharedTransformSystem _transform = default!; [Dependency] private SharedUserInterfaceSystem _ui = default!; diff --git a/Content.Server/Cargo/Systems/CargoSystem.Telepad.cs b/Content.Server/Cargo/Systems/CargoSystem.Telepad.cs index d61422c9ea..7930391439 100644 --- a/Content.Server/Cargo/Systems/CargoSystem.Telepad.cs +++ b/Content.Server/Cargo/Systems/CargoSystem.Telepad.cs @@ -137,7 +137,7 @@ public sealed partial class CargoSystem if (_station.GetOwningStation(ent) is not { } station) { - station = _random.Pick(_station.GetStations().Where(HasComp).ToList()); + station = _random.Pick(_station.GetStations().Where(x => HasComp(x.Owner)).ToList()); } if (!TryComp(station, out var db) || diff --git a/Content.Server/Cargo/Systems/CargoSystem.cs b/Content.Server/Cargo/Systems/CargoSystem.cs index a753f785cf..6b2dbf103c 100644 --- a/Content.Server/Cargo/Systems/CargoSystem.cs +++ b/Content.Server/Cargo/Systems/CargoSystem.cs @@ -34,7 +34,7 @@ public sealed partial class CargoSystem : SharedCargoSystem [Dependency] private SharedAppearanceSystem _appearance = default!; [Dependency] private SharedAudioSystem _audio = default!; [Dependency] private StackSystem _stack = default!; - [Dependency] private StationSystem _station = default!; + [Dependency] private ServerStationSystem _station = default!; [Dependency] private UserInterfaceSystem _uiSystem = default!; [Dependency] private MetaDataSystem _metaSystem = default!; [Dependency] private RadioSystem _radio = default!; diff --git a/Content.Server/CartridgeLoader/Cartridges/CrewManifestCartridgeSystem.cs b/Content.Server/CartridgeLoader/Cartridges/CrewManifestCartridgeSystem.cs index add42c4953..ba3ae0dbcf 100644 --- a/Content.Server/CartridgeLoader/Cartridges/CrewManifestCartridgeSystem.cs +++ b/Content.Server/CartridgeLoader/Cartridges/CrewManifestCartridgeSystem.cs @@ -14,7 +14,7 @@ public sealed partial class CrewManifestCartridgeSystem : EntitySystem [Dependency] private CartridgeLoaderSystem _cartridgeLoader = default!; [Dependency] private IConfigurationManager _configManager = default!; [Dependency] private CrewManifestSystem _crewManifest = default!; - [Dependency] private StationSystem _stationSystem = default!; + [Dependency] private ServerStationSystem _stationSystem = default!; private static readonly EntProtoId CartridgePrototypeName = "CrewManifestCartridge"; diff --git a/Content.Server/Changeling/Systems/ServerChangelingLastResortSystem.cs b/Content.Server/Changeling/Systems/ServerChangelingLastResortSystem.cs index 76cb6c338b..de1bdb9302 100644 --- a/Content.Server/Changeling/Systems/ServerChangelingLastResortSystem.cs +++ b/Content.Server/Changeling/Systems/ServerChangelingLastResortSystem.cs @@ -1,4 +1,3 @@ -using Content.Server.Antag; using Content.Shared.Antag; using Content.Shared.Changeling.Components; using Content.Shared.Changeling.Systems; diff --git a/Content.Server/Chat/Systems/ChatSystem.cs b/Content.Server/Chat/Systems/ChatSystem.cs index 9f5a868e0c..79b9c3e7af 100644 --- a/Content.Server/Chat/Systems/ChatSystem.cs +++ b/Content.Server/Chat/Systems/ChatSystem.cs @@ -2,14 +2,13 @@ using System.Globalization; using Content.Server.Administration.Logs; using Content.Server.Administration.Managers; using Content.Server.Chat.Managers; -using Content.Server.GameTicking; using Content.Server.Station.Systems; using Content.Shared.ActionBlocker; using Content.Shared.Administration; using Content.Shared.CCVar; using Content.Shared.Chat; using Content.Shared.Examine; -using Content.Shared.Ghost; +using Content.Shared.GameTicking; using Content.Shared.Ghost.Components; using Content.Shared.Mobs.Systems; using Content.Shared.Players.RateLimiting; @@ -40,7 +39,7 @@ public sealed partial class ChatSystem : SharedChatSystem [Dependency] private IRobustRandom _random = default!; [Dependency] private IAdminLogManager _adminLogger = default!; [Dependency] private ActionBlockerSystem _actionBlocker = default!; - [Dependency] private StationSystem _stationSystem = default!; + [Dependency] private ServerStationSystem _stationSystem = default!; [Dependency] private MobStateSystem _mobStateSystem = default!; [Dependency] private SharedAudioSystem _audio = default!; [Dependency] private ReplacementAccentSystem _wordreplacement = default!; diff --git a/Content.Server/Clock/ClockSystem.cs b/Content.Server/Clock/ClockSystem.cs index 0c12a63ac4..a278e8ab70 100644 --- a/Content.Server/Clock/ClockSystem.cs +++ b/Content.Server/Clock/ClockSystem.cs @@ -1,6 +1,7 @@ using Content.Server.GameTicking.Events; using Content.Shared.Clock; using Content.Shared.Destructible; +using Content.Shared.GameTicking.Events; using Robust.Server.GameStates; using Robust.Shared.Random; diff --git a/Content.Server/Clothing/Systems/OutfitSystem.cs b/Content.Server/Clothing/Systems/OutfitSystem.cs index ff2b19f7b9..1395a87db8 100644 --- a/Content.Server/Clothing/Systems/OutfitSystem.cs +++ b/Content.Server/Clothing/Systems/OutfitSystem.cs @@ -13,6 +13,7 @@ using Content.Shared.Preferences.Loadouts; using Content.Shared.Roles; using Content.Shared.Station; using Content.Shared.Containers.ItemSlots; +using Content.Shared.Station.Systems; using Content.Shared.Storage; using Robust.Shared.Player; @@ -23,7 +24,7 @@ public sealed partial class OutfitSystem : EntitySystem [Dependency] private IServerPreferencesManager _preferenceManager = default!; [Dependency] private HandsSystem _handSystem = default!; [Dependency] private InventorySystem _invSystem = default!; - [Dependency] private SharedStationSpawningSystem _spawningSystem = default!; + [Dependency] private StationSpawningSystem _spawningSystem = default!; [Dependency] private ItemSlotsSystem _itemSlotsSystem = default!; [Dependency] private StorageSystem _storageSystem = default!; @@ -43,7 +44,7 @@ public sealed partial class OutfitSystem : EntitySystem session = actorComponent.PlayerSession; var userId = actorComponent.PlayerSession.UserId; var prefs = _preferenceManager.GetPreferences(userId); - profile = prefs.SelectedCharacter as HumanoidCharacterProfile; + profile = prefs.SelectedCharacter; } if (_invSystem.TryGetSlots(target, out var slots)) diff --git a/Content.Server/Communications/CommsHackerSystem.cs b/Content.Server/Communications/CommsHackerSystem.cs index 19613078ce..1d0ea03052 100644 --- a/Content.Server/Communications/CommsHackerSystem.cs +++ b/Content.Server/Communications/CommsHackerSystem.cs @@ -1,8 +1,8 @@ using Content.Server.Chat.Systems; -using Content.Server.GameTicking; using Content.Server.Ninja.Systems; using Content.Shared.Communications; using Content.Shared.DoAfter; +using Content.Shared.GameTicking; using Content.Shared.Interaction; using Content.Shared.Random; using Content.Shared.Random.Helpers; diff --git a/Content.Server/Communications/CommunicationsConsoleSystem.cs b/Content.Server/Communications/CommunicationsConsoleSystem.cs index afb465916e..3d128e1527 100644 --- a/Content.Server/Communications/CommunicationsConsoleSystem.cs +++ b/Content.Server/Communications/CommunicationsConsoleSystem.cs @@ -16,6 +16,7 @@ using Content.Shared.DeviceNetwork.Components; using Content.Shared.IdentityManagement; using Content.Shared.Popups; using Content.Shared.Screens; +using Content.Shared.Station.Systems; using Robust.Server.GameObjects; using Robust.Shared.Configuration; using Robust.Shared.Prototypes; diff --git a/Content.Server/Connection/ConnectionManager.cs b/Content.Server/Connection/ConnectionManager.cs index 996c7392af..e0641f4c8a 100644 --- a/Content.Server/Connection/ConnectionManager.cs +++ b/Content.Server/Connection/ConnectionManager.cs @@ -64,7 +64,7 @@ namespace Content.Server.Connection [Dependency] private IAdminManager _adminManager = default!; [Dependency] private IEntityManager _entityManager = default!; - private GameTicker? _ticker; + private ServerGameTicker? _ticker; private ISawmill _sawmill = default!; private readonly Dictionary _temporaryBypasses = []; @@ -291,7 +291,7 @@ namespace Content.Server.Connection } } - _ticker ??= _entityManager.SystemOrNull(); + _ticker ??= _entityManager.SystemOrNull(); var wasInGame = _ticker != null && _ticker.PlayerGameStatuses.TryGetValue(userId, out var status) && status == PlayerGameStatus.JoinedGame; diff --git a/Content.Server/Construction/AnchorOnlyOnStationSystem.cs b/Content.Server/Construction/AnchorOnlyOnStationSystem.cs index c66bde4061..dd731c3efb 100644 --- a/Content.Server/Construction/AnchorOnlyOnStationSystem.cs +++ b/Content.Server/Construction/AnchorOnlyOnStationSystem.cs @@ -1,6 +1,6 @@ using System.Linq; -using Content.Server.Station.Systems; using Content.Shared.Construction.Components; +using Content.Shared.Station.Systems; using Robust.Server.GameObjects; namespace Content.Server.Construction; diff --git a/Content.Server/CrewManifest/CrewManifestSystem.cs b/Content.Server/CrewManifest/CrewManifestSystem.cs index 6cdd89d09f..22e96452fd 100644 --- a/Content.Server/CrewManifest/CrewManifestSystem.cs +++ b/Content.Server/CrewManifest/CrewManifestSystem.cs @@ -20,7 +20,7 @@ namespace Content.Server.CrewManifest; public sealed partial class CrewManifestSystem : EntitySystem { - [Dependency] private StationSystem _stationSystem = default!; + [Dependency] private ServerStationSystem _stationSystem = default!; [Dependency] private StationRecordsSystem _recordsSystem = default!; [Dependency] private EuiManager _euiManager = default!; [Dependency] private IConfigurationManager _configManager = default!; diff --git a/Content.Server/CriminalRecords/Systems/CriminalRecordsConsoleSystem.cs b/Content.Server/CriminalRecords/Systems/CriminalRecordsConsoleSystem.cs index 5dc921332c..dc3578cd85 100644 --- a/Content.Server/CriminalRecords/Systems/CriminalRecordsConsoleSystem.cs +++ b/Content.Server/CriminalRecords/Systems/CriminalRecordsConsoleSystem.cs @@ -30,7 +30,7 @@ public sealed partial class CriminalRecordsConsoleSystem : SharedCriminalRecords [Dependency] private PopupSystem _popup = default!; [Dependency] private RadioSystem _radio = default!; [Dependency] private StationRecordsSystem _records = default!; - [Dependency] private StationSystem _station = default!; + [Dependency] private ServerStationSystem _station = default!; [Dependency] private UserInterfaceSystem _ui = default!; [Dependency] private IdentitySystem _identity = default!; diff --git a/Content.Server/CriminalRecords/Systems/CriminalRecordsHackerSystem.cs b/Content.Server/CriminalRecords/Systems/CriminalRecordsHackerSystem.cs index 57c2bc2028..db23c789a1 100644 --- a/Content.Server/CriminalRecords/Systems/CriminalRecordsHackerSystem.cs +++ b/Content.Server/CriminalRecords/Systems/CriminalRecordsHackerSystem.cs @@ -16,7 +16,7 @@ public sealed partial class CriminalRecordsHackerSystem : SharedCriminalRecordsH [Dependency] private ChatSystem _chat = default!; [Dependency] private CriminalRecordsSystem _criminalRecords = default!; [Dependency] private IRobustRandom _random = default!; - [Dependency] private StationSystem _station = default!; + [Dependency] private ServerStationSystem _station = default!; [Dependency] private StationRecordsSystem _records = default!; public override void Initialize() diff --git a/Content.Server/CriminalRecords/Systems/CriminalRecordsSystem.cs b/Content.Server/CriminalRecords/Systems/CriminalRecordsSystem.cs index ec870f4c7e..b97daa0f94 100644 --- a/Content.Server/CriminalRecords/Systems/CriminalRecordsSystem.cs +++ b/Content.Server/CriminalRecords/Systems/CriminalRecordsSystem.cs @@ -22,9 +22,9 @@ namespace Content.Server.CriminalRecords.Systems; /// public sealed partial class CriminalRecordsSystem : SharedCriminalRecordsSystem { - [Dependency] private GameTicker _ticker = default!; + [Dependency] private ServerGameTicker _ticker = default!; [Dependency] private StationRecordsSystem _records = default!; - [Dependency] private StationSystem _station = default!; + [Dependency] private ServerStationSystem _station = default!; [Dependency] private CartridgeLoaderSystem _cartridge = default!; public override void Initialize() diff --git a/Content.Server/Delivery/DeliverySystem.cs b/Content.Server/Delivery/DeliverySystem.cs index a8a06889d8..21aec1a9dc 100644 --- a/Content.Server/Delivery/DeliverySystem.cs +++ b/Content.Server/Delivery/DeliverySystem.cs @@ -25,7 +25,7 @@ public sealed partial class DeliverySystem : SharedDeliverySystem [Dependency] private SharedAppearanceSystem _appearance = default!; [Dependency] private SharedAudioSystem _audio = default!; [Dependency] private StationRecordsSystem _records = default!; - [Dependency] private StationSystem _station = default!; + [Dependency] private ServerStationSystem _station = default!; [Dependency] private FingerprintReaderSystem _fingerprintReader = default!; [Dependency] private LabelSystem _label = default!; [Dependency] private SharedContainerSystem _container = default!; diff --git a/Content.Server/DeviceNetwork/Systems/SingletonDeviceNetServerSystem.cs b/Content.Server/DeviceNetwork/Systems/SingletonDeviceNetServerSystem.cs index 97098c75ad..a303833f38 100644 --- a/Content.Server/DeviceNetwork/Systems/SingletonDeviceNetServerSystem.cs +++ b/Content.Server/DeviceNetwork/Systems/SingletonDeviceNetServerSystem.cs @@ -14,7 +14,7 @@ namespace Content.Server.DeviceNetwork.Systems; public sealed partial class SingletonDeviceNetServerSystem : EntitySystem { [Dependency] private DeviceNetworkSystem _deviceNetworkSystem = default!; - [Dependency] private StationSystem _stationSystem = default!; + [Dependency] private ServerStationSystem _stationSystem = default!; public override void Initialize() { diff --git a/Content.Server/DeviceNetwork/Systems/StationLimitedNetworkSystem.cs b/Content.Server/DeviceNetwork/Systems/StationLimitedNetworkSystem.cs index 7120e452f9..cfbfdc8816 100644 --- a/Content.Server/DeviceNetwork/Systems/StationLimitedNetworkSystem.cs +++ b/Content.Server/DeviceNetwork/Systems/StationLimitedNetworkSystem.cs @@ -11,7 +11,8 @@ namespace Content.Server.DeviceNetwork.Systems [UsedImplicitly] public sealed partial class StationLimitedNetworkSystem : EntitySystem { - [Dependency] private StationSystem _stationSystem = default!; + [Dependency] private ServerStationSystem _stationSystem = default!; + public override void Initialize() { base.Initialize(); diff --git a/Content.Server/Discord/WebhookMessages/VoteWebhooks.cs b/Content.Server/Discord/WebhookMessages/VoteWebhooks.cs index ba41aa4602..19d0d25906 100644 --- a/Content.Server/Discord/WebhookMessages/VoteWebhooks.cs +++ b/Content.Server/Discord/WebhookMessages/VoteWebhooks.cs @@ -39,7 +39,7 @@ public sealed partial class VoteWebhooks : IPostInjectInit fields.Add(newVote); } - var gameTicker = _entSys.GetEntitySystemOrNull(); + var gameTicker = _entSys.GetEntitySystemOrNull(); _sawmill = Logger.GetSawmill("discord"); var runLevel = gameTicker != null ? Loc.GetString($"game-run-level-{gameTicker.RunLevel}") : ""; diff --git a/Content.Server/Entry/EntryPoint.cs b/Content.Server/Entry/EntryPoint.cs index 7900836343..762a65df00 100644 --- a/Content.Server/Entry/EntryPoint.cs +++ b/Content.Server/Entry/EntryPoint.cs @@ -162,7 +162,7 @@ namespace Content.Server.Entry _discordChatLink.Initialize(); _euiManager.Initialize(); _gameMap.Initialize(); - _entSys.GetEntitySystem().PostInitialize(); + _entSys.GetEntitySystem().PostInitialize(); _ban.Initialize(); _connection.PostInit(); _multiServerKick.Initialize(); diff --git a/Content.Server/Fax/FaxSystem.cs b/Content.Server/Fax/FaxSystem.cs index a702fcece0..3f131fb2c5 100644 --- a/Content.Server/Fax/FaxSystem.cs +++ b/Content.Server/Fax/FaxSystem.cs @@ -38,7 +38,6 @@ public sealed partial class FaxSystem : EntitySystem [Dependency] private IAdminManager _adminManager = default!; [Dependency] private ItemSlotsSystem _itemSlotsSystem = default!; [Dependency] private SharedAppearanceSystem _appearanceSystem = default!; - [Dependency] private SharedGameTicker _gameTicker = default!; [Dependency] private PopupSystem _popupSystem = default!; [Dependency] private DeviceNetworkSystem _deviceNetworkSystem = default!; [Dependency] private PaperSystem _paperSystem = default!; diff --git a/Content.Server/FeedbackSystem/FeedbackSystem.cs b/Content.Server/FeedbackSystem/FeedbackSystem.cs index 4c9aae7d64..5bb7648c76 100644 --- a/Content.Server/FeedbackSystem/FeedbackSystem.cs +++ b/Content.Server/FeedbackSystem/FeedbackSystem.cs @@ -8,7 +8,7 @@ namespace Content.Server.FeedbackSystem; public sealed partial class FeedbackSystem : EntitySystem { [Dependency] private ServerFeedbackManager _feedbackManager = null!; - [Dependency] private GameTicker _gameTicker = null!; + [Dependency] private ServerGameTicker _gameTicker = null!; public override void Initialize() { diff --git a/Content.Server/GameTicking/Commands/DelayStartCommand.cs b/Content.Server/GameTicking/Commands/DelayStartCommand.cs index d82d2f0f12..df5f750e87 100644 --- a/Content.Server/GameTicking/Commands/DelayStartCommand.cs +++ b/Content.Server/GameTicking/Commands/DelayStartCommand.cs @@ -1,5 +1,6 @@ using Content.Server.Administration; using Content.Shared.Administration; +using Content.Shared.GameTicking; using Robust.Shared.Console; namespace Content.Server.GameTicking.Commands; @@ -11,7 +12,7 @@ sealed class DelayStartCommand : LocalizedEntityCommands public override void Execute(IConsoleShell shell, string argStr, string[] args) { - var ticker = EntityManager.System(); + var ticker = EntityManager.System(); if (ticker.RunLevel != GameRunLevel.PreRoundLobby) { shell.WriteLine(Loc.GetString("delaystart-preround-only")); diff --git a/Content.Server/GameTicking/Commands/EndRoundCommand.cs b/Content.Server/GameTicking/Commands/EndRoundCommand.cs index 8fa427fdc2..19f8fb6fa9 100644 --- a/Content.Server/GameTicking/Commands/EndRoundCommand.cs +++ b/Content.Server/GameTicking/Commands/EndRoundCommand.cs @@ -1,5 +1,6 @@ using Content.Server.Administration; using Content.Shared.Administration; +using Content.Shared.GameTicking; using Robust.Shared.Console; namespace Content.Server.GameTicking.Commands; @@ -7,7 +8,7 @@ namespace Content.Server.GameTicking.Commands; [AdminCommand(AdminFlags.Round)] public sealed partial class EndRoundCommand : LocalizedEntityCommands { - [Dependency] private GameTicker _gameTicker = default!; + [Dependency] private ServerGameTicker _gameTicker = default!; public override string Command => "endround"; diff --git a/Content.Server/GameTicking/Commands/ForcePresetCommand.cs b/Content.Server/GameTicking/Commands/ForcePresetCommand.cs index a7c38f5a3e..5bcfcba67a 100644 --- a/Content.Server/GameTicking/Commands/ForcePresetCommand.cs +++ b/Content.Server/GameTicking/Commands/ForcePresetCommand.cs @@ -1,7 +1,8 @@ using System.Linq; using Content.Server.Administration; -using Content.Server.GameTicking.Presets; using Content.Shared.Administration; +using Content.Shared.GameTicking; +using Content.Shared.GameTicking.Prototypes; using Robust.Shared.Console; using Robust.Shared.Prototypes; @@ -11,7 +12,7 @@ namespace Content.Server.GameTicking.Commands public sealed partial class ForcePresetCommand : LocalizedEntityCommands { [Dependency] private IPrototypeManager _prototypeManager = default!; - [Dependency] private GameTicker _ticker = default!; + [Dependency] private ServerGameTicker _ticker = default!; public override string Command => "forcepreset"; diff --git a/Content.Server/GameTicking/Commands/GoLobbyCommand.cs b/Content.Server/GameTicking/Commands/GoLobbyCommand.cs index 23d7dc4c8d..58e180e7ad 100644 --- a/Content.Server/GameTicking/Commands/GoLobbyCommand.cs +++ b/Content.Server/GameTicking/Commands/GoLobbyCommand.cs @@ -1,7 +1,7 @@ using Content.Server.Administration; -using Content.Server.GameTicking.Presets; using Content.Shared.Administration; using Content.Shared.CCVar; +using Content.Shared.GameTicking.Prototypes; using Robust.Shared.Configuration; using Robust.Shared.Console; @@ -11,7 +11,7 @@ namespace Content.Server.GameTicking.Commands public sealed partial class GoLobbyCommand : LocalizedEntityCommands { [Dependency] private IConfigurationManager _configManager = default!; - [Dependency] private GameTicker _gameTicker = default!; + [Dependency] private ServerGameTicker _gameTicker = default!; public override string Command => "golobby"; diff --git a/Content.Server/GameTicking/Commands/JoinGameCommand.cs b/Content.Server/GameTicking/Commands/JoinGameCommand.cs index 1148dd4160..20b4dd99f4 100644 --- a/Content.Server/GameTicking/Commands/JoinGameCommand.cs +++ b/Content.Server/GameTicking/Commands/JoinGameCommand.cs @@ -47,8 +47,8 @@ namespace Content.Server.GameTicking.Commands return; } - var ticker = _entManager.System(); - var stationJobs = _entManager.System(); + var ticker = _entManager.System(); + var stationJobs = _entManager.System(); if (ticker.PlayerGameStatuses.TryGetValue(player.UserId, out var status) && status == PlayerGameStatus.JoinedGame) { diff --git a/Content.Server/GameTicking/Commands/ObserveCommand.cs b/Content.Server/GameTicking/Commands/ObserveCommand.cs index 45d911887d..c61d60cdd9 100644 --- a/Content.Server/GameTicking/Commands/ObserveCommand.cs +++ b/Content.Server/GameTicking/Commands/ObserveCommand.cs @@ -23,7 +23,7 @@ namespace Content.Server.GameTicking.Commands return; } - var ticker = _e.System(); + var ticker = _e.System(); if (ticker.RunLevel == GameRunLevel.PreRoundLobby) { diff --git a/Content.Server/GameTicking/Commands/RespawnCommand.cs b/Content.Server/GameTicking/Commands/RespawnCommand.cs index 479549bd5f..f0d5caf152 100644 --- a/Content.Server/GameTicking/Commands/RespawnCommand.cs +++ b/Content.Server/GameTicking/Commands/RespawnCommand.cs @@ -12,7 +12,7 @@ namespace Content.Server.GameTicking.Commands { [Dependency] private IPlayerManager _player = default!; [Dependency] private IPlayerLocator _locator = default!; - [Dependency] private GameTicker _gameTicker = default!; + [Dependency] private ServerGameTicker _gameTicker = default!; [Dependency] private MindSystem _mind = default!; public override string Command => "respawn"; diff --git a/Content.Server/GameTicking/Commands/RestartRoundCommand.cs b/Content.Server/GameTicking/Commands/RestartRoundCommand.cs index cd9c77ce0c..c125e6fa32 100644 --- a/Content.Server/GameTicking/Commands/RestartRoundCommand.cs +++ b/Content.Server/GameTicking/Commands/RestartRoundCommand.cs @@ -1,6 +1,7 @@ using Content.Server.Administration; using Content.Server.RoundEnd; using Content.Shared.Administration; +using Content.Shared.GameTicking; using Robust.Shared.Console; namespace Content.Server.GameTicking.Commands; @@ -8,7 +9,7 @@ namespace Content.Server.GameTicking.Commands; [AdminCommand(AdminFlags.Round)] public sealed partial class RestartRoundCommand : LocalizedEntityCommands { - [Dependency] private GameTicker _gameTicker = default!; + [Dependency] private ServerGameTicker _gameTicker = default!; [Dependency] private RoundEndSystem _roundEndSystem = default!; public override string Command => "restartround"; @@ -28,7 +29,7 @@ public sealed partial class RestartRoundCommand : LocalizedEntityCommands [AdminCommand(AdminFlags.Round)] public sealed partial class RestartRoundNowCommand : LocalizedEntityCommands { - [Dependency] private GameTicker _gameTicker = default!; + [Dependency] private ServerGameTicker _gameTicker = default!; public override string Command => "restartroundnow"; diff --git a/Content.Server/GameTicking/Commands/SetGamePresetCommand.cs b/Content.Server/GameTicking/Commands/SetGamePresetCommand.cs index a577867ec4..93d885699b 100644 --- a/Content.Server/GameTicking/Commands/SetGamePresetCommand.cs +++ b/Content.Server/GameTicking/Commands/SetGamePresetCommand.cs @@ -1,7 +1,6 @@ -using System.Linq; -using Content.Server.Administration; -using Content.Server.GameTicking.Presets; +using Content.Server.Administration; using Content.Shared.Administration; +using Content.Shared.GameTicking.Prototypes; using Robust.Shared.Console; namespace Content.Server.GameTicking.Commands @@ -23,7 +22,7 @@ namespace Content.Server.GameTicking.Commands return; } - var ticker = _entity.System(); + var ticker = _entity.System(); if (!ticker.TryFindGamePreset(args[0], out var preset)) { diff --git a/Content.Server/GameTicking/Commands/StartRoundCommand.cs b/Content.Server/GameTicking/Commands/StartRoundCommand.cs index 90d4ffa35a..52b888df84 100644 --- a/Content.Server/GameTicking/Commands/StartRoundCommand.cs +++ b/Content.Server/GameTicking/Commands/StartRoundCommand.cs @@ -1,5 +1,6 @@ using Content.Server.Administration; using Content.Shared.Administration; +using Content.Shared.GameTicking; using Robust.Shared.Console; namespace Content.Server.GameTicking.Commands; @@ -7,7 +8,7 @@ namespace Content.Server.GameTicking.Commands; [AdminCommand(AdminFlags.Round)] public sealed partial class StartRoundCommand : LocalizedEntityCommands { - [Dependency] private GameTicker _gameTicker = default!; + [Dependency] private ServerGameTicker _gameTicker = default!; public override string Command => "startround"; diff --git a/Content.Server/GameTicking/Commands/ToggleReadyCommand.cs b/Content.Server/GameTicking/Commands/ToggleReadyCommand.cs index b990e92ca7..16361c4930 100644 --- a/Content.Server/GameTicking/Commands/ToggleReadyCommand.cs +++ b/Content.Server/GameTicking/Commands/ToggleReadyCommand.cs @@ -1,4 +1,5 @@ using Content.Shared.Administration; +using Content.Shared.GameTicking; using Robust.Shared.Console; namespace Content.Server.GameTicking.Commands; @@ -6,7 +7,7 @@ namespace Content.Server.GameTicking.Commands; [AnyCommand] public sealed partial class ToggleReadyCommand : LocalizedEntityCommands { - [Dependency] private GameTicker _gameTicker = default!; + [Dependency] private ServerGameTicker _gameTicker = default!; public override string Command => "toggleready"; diff --git a/Content.Server/GameTicking/GameTicker.CVars.cs b/Content.Server/GameTicking/GameTicker.CVars.cs deleted file mode 100644 index c247c66471..0000000000 --- a/Content.Server/GameTicking/GameTicker.CVars.cs +++ /dev/null @@ -1,82 +0,0 @@ -using Content.Server.Discord; -using Content.Shared.CCVar; -using Content.Shared.GameTicking; - -namespace Content.Server.GameTicking -{ - public sealed partial class GameTicker - { - [ViewVariables] - public bool LobbyEnabled { get; private set; } - - [ViewVariables] - public bool DummyTicker { get; private set; } = false; - - [ViewVariables] - public TimeSpan LobbyDuration { get; private set; } = TimeSpan.Zero; - - [ViewVariables] - public bool DisallowLateJoin { get; private set; } = false; - - [ViewVariables] - public string? ServerName { get; private set; } - - [ViewVariables] - private string? DiscordRoundEndRole { get; set; } - - private WebhookIdentifier? _webhookIdentifier; - - [ViewVariables] - private string? RoundEndSoundCollection { get; set; } - -#if EXCEPTION_TOLERANCE - [ViewVariables] - public int RoundStartFailShutdownCount { get; private set; } = 0; -#endif - - private void InitializeCVars() - { - Subs.CVar(_cfg, CCVars.GameLobbyEnabled, value => - { - LobbyEnabled = value; - foreach (var (userId, status) in _playerGameStatuses) - { - if (status == PlayerGameStatus.JoinedGame) - continue; - _playerGameStatuses[userId] = - LobbyEnabled ? PlayerGameStatus.NotReadyToPlay : PlayerGameStatus.ReadyToPlay; - } - }, true); - Subs.CVar(_cfg, CCVars.GameDummyTicker, value => DummyTicker = value, true); - Subs.CVar(_cfg, CCVars.GameLobbyDuration, value => LobbyDuration = TimeSpan.FromSeconds(value), true); - Subs.CVar(_cfg, CCVars.GameDisallowLateJoins, - value => { DisallowLateJoin = value; UpdateLateJoinStatus(); }, true); - Subs.CVar(_cfg, CCVars.AdminLogsServerName, value => - { - // TODO why tf is the server name on admin logs - ServerName = value; - }, true); - Subs.CVar(_cfg, CCVars.DiscordRoundUpdateWebhook, value => - { - if (!string.IsNullOrWhiteSpace(value)) - { - _discord.GetWebhook(value, data => _webhookIdentifier = data.ToIdentifier()); - } - }, true); - Subs.CVar(_cfg, CCVars.DiscordRoundEndRoleWebhook, value => - { - DiscordRoundEndRole = value; - - if (value == string.Empty) - { - DiscordRoundEndRole = null; - } - }, true); - Subs.CVar(_cfg, CCVars.RoundEndSoundCollection, value => RoundEndSoundCollection = value, true); -#if EXCEPTION_TOLERANCE - Subs.CVar(_cfg, CCVars.RoundStartFailShutdownCount, value => RoundStartFailShutdownCount = value, true); -#endif - Subs.CVar(_cfg, CCVars.GameTickerIgnoredPresets, value => _ignoredRules = value.Split(",")); - } - } -} diff --git a/Content.Server/GameTicking/GameTicker.GameRule.cs b/Content.Server/GameTicking/GameTicker.GameRule.cs deleted file mode 100644 index 5c2d93078c..0000000000 --- a/Content.Server/GameTicking/GameTicker.GameRule.cs +++ /dev/null @@ -1,573 +0,0 @@ -using System.Linq; -using System.Text; -using Content.Server.Administration; -using Content.Server.GameTicking.Rules.Components; -using Content.Shared.Administration; -using Content.Shared.CCVar; -using Content.Shared.Database; -using Content.Shared.GameTicking.Components; -using Content.Shared.Prototypes; -using Content.Shared.Whitelist; -using JetBrains.Annotations; -using Robust.Shared.Console; -using Robust.Shared.Map; -using Robust.Shared.Prototypes; - -namespace Content.Server.GameTicking; - -public sealed partial class GameTicker -{ - /// - /// Designated game rule that spawns a fake antagonist to discourage metagaming. - /// Has to be a string since cannot be a const. - /// - public const string DummyGameRule = "DummyNonAntag"; - - [ViewVariables] private readonly List<(TimeSpan, string)> _allPreviousGameRules = new(); - - /// - /// List of ignored game rules, these rules won't be spawned by normal means. - /// This list is populated by - /// - [ViewVariables] private string[] _ignoredRules = []; - - [Dependency] private EntityWhitelistSystem _whitelist = null!; - - /// - /// A list storing the start times of all game rules that have been started this round. - /// Game rules can be started and stopped at any time, including midround. - /// - public override IReadOnlyList<(TimeSpan, string)> AllPreviousGameRules => _allPreviousGameRules; - - private void InitializeGameRules() - { - // Add game rule command. - _consoleHost.RegisterCommand("addgamerule", - string.Empty, - "addgamerule ", - AddGameRuleCommand, - AddGameRuleCompletions); - - // End game rule command. - _consoleHost.RegisterCommand("endgamerule", - string.Empty, - "endgamerule ", - EndGameRuleCommand, - EndGameRuleCompletions); - - // Clear game rules command. - _consoleHost.RegisterCommand("cleargamerules", - string.Empty, - "cleargamerules", - ClearGameRulesCommand); - - // List game rules command. - var localizedHelp = Loc.GetString("listgamerules-command-help"); - - _consoleHost.RegisterCommand("listgamerules", - string.Empty, - $"listgamerules - {localizedHelp}", - ListGameRuleCommand); - - SubscribeLocalEvent(OnStartAttempt); - } - - private void ShutdownGameRules() - { - _consoleHost.UnregisterCommand("addgamerule"); - _consoleHost.UnregisterCommand("endgamerule"); - _consoleHost.UnregisterCommand("cleargamerules"); - _consoleHost.UnregisterCommand("listgamerules"); - } - - /// - /// Adds a game rule to the list, but does not - /// start it yet, instead waiting until the rule is actually started by other code (usually roundstart) - /// - /// The entity for the added gamerule - public EntityUid AddGameRule([ForbidLiteral] string ruleId) - { - var ruleEntity = Spawn(ruleId, MapCoordinates.Nullspace); - _sawmill.Info($"Added game rule {ToPrettyString(ruleEntity)}"); - _adminLogger.Add(LogType.EventStarted, $"Added game rule {ToPrettyString(ruleEntity)}"); - var str = Loc.GetString("station-event-system-run-event", ("eventName", ToPrettyString(ruleEntity))); -#if DEBUG - _chatManager.SendAdminAlert(str); -#else - if (RunLevel == GameRunLevel.InRound) // avoids telling admins the round type before it starts so that can be handled elsewhere. - { - _chatManager.SendAdminAlert(str); - } -#endif - Log.Info(str); - - var ev = new GameRuleAddedEvent(ruleEntity, ruleId); - RaiseLocalEvent(ruleEntity, ref ev, true); - - var currentTime = RunLevel == GameRunLevel.PreRoundLobby ? TimeSpan.Zero : RoundDuration(); - if (!HasComp(ruleEntity) && !HasComp(ruleEntity)) - { - _allPreviousGameRules.Add((currentTime, ruleId + " (Pending)")); - } - - return ruleEntity; - } - - /// - /// Tries to add a gamerule to the current round, but ignores any - /// - /// Game rule entity that we are trying to spawn - /// The entityUid of the spawned game rule, if it wasn't ignored. - public EntityUid? AddFilteredGameRule([ForbidLiteral] EntProtoId gameRule) - { - if (IsIgnored(gameRule)) - return null; - - return AddGameRule(gameRule); - } - - /// - /// Checks if this GameRule should be ignored before a spawning attempt. - /// - /// GameRule we are trying to validate - /// True if the gamerule should be ignored and not spawned. - public bool IsIgnored([ForbidLiteral] EntProtoId gameRule) - { - return _ignoredRules.Contains(gameRule); - } - - /// - /// Game rules can be 'started' separately from being added. 'Starting' them usually - /// happens at round start while they can be added and removed before then. - /// - public bool StartGameRule([ForbidLiteral] string ruleId) - { - return StartGameRule(ruleId, out _); - } - - /// - /// Game rules can be 'started' separately from being added. 'Starting' them usually - /// happens at round start while they can be added and removed before then. - /// - public bool StartGameRule([ForbidLiteral] string ruleId, out EntityUid ruleEntity) - { - ruleEntity = AddGameRule(ruleId); - return StartGameRule(ruleEntity); - } - - /// - /// Game rules can be 'started' separately from being added. 'Starting' them usually - /// happens at round start while they can be added and removed before then. - /// - public bool StartGameRule(EntityUid ruleEntity, GameRuleComponent? ruleData = null) - { - if (!Resolve(ruleEntity, ref ruleData)) - ruleData ??= EnsureComp(ruleEntity); - - // can't start an already active rule - if (HasComp(ruleEntity) || HasComp(ruleEntity)) - return false; - - if (MetaData(ruleEntity).EntityPrototype?.ID is not { } id) // you really fucked up - return false; - - // If we already have it, then we just skip the delay as it has already happened. - if (!RemComp(ruleEntity) && ruleData.Delay != null) - { - var delayTime = TimeSpan.FromSeconds(ruleData.Delay.Value.Next(_robustRandom)); - - if (delayTime > TimeSpan.Zero) - { - _sawmill.Info($"Queued start for game rule {ToPrettyString(ruleEntity)} with delay {delayTime}"); - _adminLogger.Add(LogType.EventStarted, - $"Queued start for game rule {ToPrettyString(ruleEntity)} with delay {delayTime}"); - - var delayed = EnsureComp(ruleEntity); - delayed.RuleStartTime = _gameTiming.CurTime + (delayTime); - return true; - } - } - - var currentTime = RunLevel == GameRunLevel.PreRoundLobby ? TimeSpan.Zero : RoundDuration(); - - // Remove the first occurrence of the pending entry before adding the started entry - var pendingRuleIndex = _allPreviousGameRules.FindIndex(rule => rule.Item2 == id + " (Pending)"); - if (pendingRuleIndex >= 0) - { - _allPreviousGameRules.RemoveAt(pendingRuleIndex); - } - - if (!HasComp(ruleEntity) && !HasComp(ruleEntity)) - { - _allPreviousGameRules.Add((currentTime, id)); - } - - _sawmill.Info($"Started game rule {ToPrettyString(ruleEntity)}"); - _adminLogger.Add(LogType.EventStarted, $"Started game rule {ToPrettyString(ruleEntity)}"); - - EnsureComp(ruleEntity); - ruleData.ActivatedAt = _gameTiming.CurTime; - - var ev = new GameRuleStartedEvent(ruleEntity, id); - RaiseLocalEvent(ruleEntity, ref ev, true); - return true; - } - - /// - /// Ends a game rule. - /// - [PublicAPI] - public bool EndGameRule(EntityUid ruleEntity, GameRuleComponent? ruleData = null) - { - if (!Resolve(ruleEntity, ref ruleData)) - return false; - - // don't end it multiple times - if (HasComp(ruleEntity)) - return false; - - if (MetaData(ruleEntity).EntityPrototype?.ID is not { } id) // you really fucked up - return false; - - RemComp(ruleEntity); - EnsureComp(ruleEntity); - - _sawmill.Info($"Ended game rule {ToPrettyString(ruleEntity)}"); - _adminLogger.Add(LogType.EventStopped, $"Ended game rule {ToPrettyString(ruleEntity)}"); - - var ev = new GameRuleEndedEvent(ruleEntity, id); - RaiseLocalEvent(ruleEntity, ref ev, true); - return true; - } - - /// - /// Returns true if a game rule with the given component has been added. - /// - public bool IsGameRuleAdded() - where T : IComponent - { - var query = EntityQueryEnumerator(); - while (query.MoveNext(out var uid, out _, out _)) - { - if (HasComp(uid)) - continue; - - return true; - } - - return false; - } - - public bool IsGameRuleAdded(EntityUid ruleEntity, GameRuleComponent? component = null) - { - return Resolve(ruleEntity, ref component) && !HasComp(ruleEntity); - } - - public bool IsGameRuleAdded([ForbidLiteral] string rule) - { - foreach (var ruleEntity in GetAddedGameRules()) - { - if (MetaData(ruleEntity).EntityPrototype?.ID == rule) - return true; - } - - return false; - } - - /// - /// Returns true if a game rule that passes the whitelist and blacklist has been added. - /// - /// whitelist for the game rules - /// blacklist for the game rules - public bool IsGameRuleAdded(EntityWhitelist? ruleWhitelist, EntityWhitelist? ruleBlacklist = null) - { - foreach (var ruleEntity in GetAddedGameRules()) - { - if (_whitelist.CheckBoth(ruleEntity, ruleBlacklist, ruleWhitelist)) - return true; - } - - return false; - } - - /// - /// Returns true if a game rule with the given component is active.. - /// - public bool IsGameRuleActive() - where T : IComponent - { - var query = EntityQueryEnumerator(); - // out, damned underscore!!! - while (query.MoveNext(out _, out _, out _, out _)) - { - return true; - } - - return false; - } - - public bool IsGameRuleActive(EntityUid ruleEntity, GameRuleComponent? component = null) - { - return Resolve(ruleEntity, ref component) && HasComp(ruleEntity); - } - - public bool IsGameRuleActive([ForbidLiteral] string rule) - { - foreach (var ruleEntity in GetActiveGameRules()) - { - if (MetaData(ruleEntity).EntityPrototype?.ID == rule) - return true; - } - - return false; - } - - /// - /// Returns true if a game rule that passes the whitelist and blacklist is active. - /// - /// whitelist for the game rules - /// blacklist for the game rules - public bool IsGameRuleActive(EntityWhitelist? ruleWhitelist, EntityWhitelist? ruleBlacklist = null) - { - foreach (var ruleEntity in GetActiveGameRules()) - { - if (_whitelist.CheckBoth(ruleEntity, ruleBlacklist, ruleWhitelist)) - return true; - } - - return false; - } - - public void ClearGameRules() - { - foreach (var rule in GetAddedGameRules()) - { - EndGameRule(rule); - } - } - - /// - /// Gets all the gamerule entities that have been added. - /// - public IEnumerable GetAddedGameRules() - { - var query = EntityQueryEnumerator(); - while (query.MoveNext(out var uid, out var ruleData)) - { - if (IsGameRuleAdded(uid, ruleData)) - yield return uid; - } - } - - /// - /// Gets all the gamerule entities with {T} component that have been added. - /// - public IEnumerable> GetAddedGameRules() where T : Component - { - var query = EntityQueryEnumerator(); - while (query.MoveNext(out var uid, out var comp, out var ruleData)) - { - if (IsGameRuleAdded(uid, ruleData)) - yield return (uid, comp); - } - } - - - /// - /// Gets all the gamerule entities which are currently active. - /// - public IEnumerable GetActiveGameRules() - { - var query = EntityQueryEnumerator(); - while (query.MoveNext(out var uid, out _, out _)) - { - yield return uid; - } - } - - /// - /// Gets all the gamerule entities with {T} component that are currently active. - /// - public IEnumerable> GetActiveGameRules() where T : Component - { - var query = EntityQueryEnumerator(); - while (query.MoveNext(out var uid, out var comp, out _, out _)) - { - yield return (uid, comp); - } - } - - /// - /// Gets all gamerule prototypes - /// - public IEnumerable GetAllGameRulePrototypes() - { - foreach (var proto in ProtoMan.EnumeratePrototypes()) - { - if (proto.Abstract) - continue; - - if (proto.HasComponent()) - yield return proto; - } - } - - private void UpdateGameRules() - { - var query = EntityQueryEnumerator(); - while (query.MoveNext(out var uid, out var delay, out var rule)) - { - if (_gameTiming.CurTime < delay.RuleStartTime) - continue; - - StartGameRule(uid, rule); - } - } - - private void OnStartAttempt(RoundStartAttemptEvent args) - { - if (args.Forced || args.Cancelled) - return; - - var query = EntityQueryEnumerator(); - while (query.MoveNext(out var uid, out var gameRule)) - { - var minPlayers = gameRule.MinPlayers; - var name = ToPrettyString(uid); - - if (args.Players.Length >= minPlayers) - continue; - - if (gameRule.CancelPresetOnTooFewPlayers) - { - _chatManager.SendAdminAnnouncement(Loc.GetString("preset-not-enough-ready-players", - ("readyPlayersCount", args.Players.Length), - ("minimumPlayers", minPlayers), - ("presetName", name))); - args.Cancel(); - //TODO remove this once announcements are logged - Log.Info($"Rule '{name}' requires {minPlayers} players, but only {args.Players.Length} are ready."); - } - else - { - EndGameRule(uid, gameRule); - } - } - } - - #region Command Implementations - - [AdminCommand(AdminFlags.Fun)] - private void AddGameRuleCommand(IConsoleShell shell, string argstr, string[] args) - { - if (args.Length == 0) - return; - - foreach (var rule in args) - { - if (!ProtoMan.HasIndex(rule)) - { - shell.WriteError($"Invalid game rule {rule} was skipped."); - - continue; - } - - if (shell.Player != null) - { - _adminLogger.Add(LogType.EventStarted, $"{shell.Player} tried to add game rule [{rule}] via command"); - _chatManager.SendAdminAnnouncement(Loc.GetString("add-gamerule-admin", ("rule", rule), ("admin", shell.Player))); - } - else - { - _adminLogger.Add(LogType.EventStarted, $"Unknown tried to add game rule [{rule}] via command"); - } - var ent = AddGameRule(rule); - - // Start rule if we're already in the middle of a round - if(RunLevel == GameRunLevel.InRound) - StartGameRule(ent); - - } - } - - private CompletionResult AddGameRuleCompletions(IConsoleShell shell, string[] args) - { - return CompletionResult.FromHintOptions(GetAllGameRulePrototypes().Select(p => p.ID), ""); - } - - [AdminCommand(AdminFlags.Fun)] - private void EndGameRuleCommand(IConsoleShell shell, string argstr, string[] args) - { - if (args.Length == 0) - return; - - foreach (var rule in args) - { - if (!NetEntity.TryParse(rule, out var ruleEntNet) || !TryGetEntity(ruleEntNet, out var ruleEnt)) - continue; - if (shell.Player != null) - { - _adminLogger.Add(LogType.EventStopped, $"{shell.Player} tried to end game rule [{rule}] via command"); - } - else - { - _adminLogger.Add(LogType.EventStopped, $"Unknown tried to end game rule [{rule}] via command"); - } - - EndGameRule(ruleEnt.Value); - } - } - - private CompletionResult EndGameRuleCompletions(IConsoleShell shell, string[] args) - { - var opts = GetAddedGameRules().Select(ent => new CompletionOption(ent.ToString(), ToPrettyString(ent))).ToList(); - return CompletionResult.FromHintOptions(opts, ""); - } - - [AdminCommand(AdminFlags.Fun)] - private void ClearGameRulesCommand(IConsoleShell shell, string argstr, string[] args) - { - ClearGameRules(); - } - - [AdminCommand(AdminFlags.Admin)] - private void ListGameRuleCommand(IConsoleShell shell, string argstr, string[] args) - { - _sawmill.Info($"{shell.Player} tried to get list of game rules via command"); - _adminLogger.Add(LogType.Action, $"{shell.Player} tried to get list of game rules via command"); - var message = GetGameRulesListMessage(false); - shell.WriteLine(message); - } - private string GetGameRulesListMessage(bool forChatWindow) - { - if (_allPreviousGameRules.Count > 0) - { - var sortedRules = _allPreviousGameRules.OrderBy(rule => rule.Item1).ToList(); - var message = new StringBuilder(); - message.AppendLine(); - - if (!forChatWindow) - { - var header = Loc.GetString("list-gamerule-admin-header"); - message.AppendLine(); - message.AppendLine(header); - message.AppendLine("|------------|------------------"); - } - - foreach (var (time, rule) in sortedRules) - { - var formattedTime = time.ToString(@"hh\:mm\:ss"); - message.AppendLine($"| {formattedTime,-10} | {rule,-16} "); - } - - return message.ToString().TrimEnd('\n'); - } - else - { - return Loc.GetString("list-gamerule-admin-no-rules"); - - } - } - - #endregion -} diff --git a/Content.Server/GameTicking/GameTicker.Lobby.cs b/Content.Server/GameTicking/GameTicker.Lobby.cs deleted file mode 100644 index 6be7e3abca..0000000000 --- a/Content.Server/GameTicking/GameTicker.Lobby.cs +++ /dev/null @@ -1,188 +0,0 @@ -using System.Linq; -using Content.Shared.GameTicking; -using Content.Server.Station.Components; -using Robust.Shared.Network; -using Robust.Shared.Player; -using System.Text; - -namespace Content.Server.GameTicking -{ - public sealed partial class GameTicker - { - [ViewVariables] - private readonly Dictionary _playerGameStatuses = new(); - - [ViewVariables] - private TimeSpan _roundStartTime; - - /// - /// How long before RoundStartTime do we load maps. - /// - [ViewVariables] - public TimeSpan RoundPreloadTime { get; } = TimeSpan.FromSeconds(15); - - [ViewVariables] - private TimeSpan _pauseTime; - - [ViewVariables] - public new bool Paused { get; set; } - - [ViewVariables] - private bool _roundStartCountdownHasNotStartedYetDueToNoPlayers; - - /// - /// The game status of a players user Id. May contain disconnected players - /// - public IReadOnlyDictionary PlayerGameStatuses => _playerGameStatuses; - - public void UpdateInfoText() - { - RaiseNetworkEvent(GetInfoMsg(), Filter.Empty().AddPlayers(_playerManager.NetworkedSessions)); - } - - private string GetInfoText() - { - var preset = CurrentPreset ?? Preset; - if (preset == null) - { - return string.Empty; - } - - var playerCount = $"{_playerManager.PlayerCount}"; - var readyCount = _playerGameStatuses.Values.Count(x => x == PlayerGameStatus.ReadyToPlay); - - var stationNames = new StringBuilder(); - var query = - EntityQueryEnumerator(); - - var foundOne = false; - - while (query.MoveNext(out _, out _, out var meta)) - { - foundOne = true; - if (stationNames.Length > 0) - stationNames.Append('\n'); - - stationNames.Append(meta.EntityName); - } - - if (!foundOne) - { - stationNames.Append(_gameMapManager.GetSelectedMap()?.MapName ?? - Loc.GetString("game-ticker-no-map-selected")); - } - - var gmTitle = (Decoy == null) ? Loc.GetString(preset.ModeTitle) : Loc.GetString(Decoy.ModeTitle); - var desc = (Decoy == null) ? Loc.GetString(preset.Description) : Loc.GetString(Decoy.Description); - return Loc.GetString( - RunLevel == GameRunLevel.PreRoundLobby - ? "game-ticker-get-info-preround-text" - : "game-ticker-get-info-text", - ("roundId", RoundId), - ("playerCount", playerCount), - ("readyCount", readyCount), - ("mapName", stationNames.ToString()), - ("gmTitle", gmTitle), - ("desc", desc)); - } - - private TickerConnectionStatusEvent GetConnectionStatusMsg() - { - return new TickerConnectionStatusEvent(RoundStartTimeSpan); - } - - private TickerLobbyStatusEvent GetStatusMsg(ICommonSession session) - { - _playerGameStatuses.TryGetValue(session.UserId, out var status); - return new TickerLobbyStatusEvent(RunLevel != GameRunLevel.PreRoundLobby, LobbyBackground, status == PlayerGameStatus.ReadyToPlay, _roundStartTime, RoundPreloadTime, RoundStartTimeSpan, Paused); - } - - private void SendStatusToAll() - { - foreach (var player in _playerManager.Sessions) - { - RaiseNetworkEvent(GetStatusMsg(player), player.Channel); - } - } - - 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(Loc.GetString(Paused - ? "game-ticker-pause-start" - : "game-ticker-pause-start-resumed")); - - return true; - } - - public bool TogglePause() - { - PauseStart(!Paused); - return Paused; - } - - public void ToggleReadyAll(bool ready) - { - var status = ready ? PlayerGameStatus.ReadyToPlay : PlayerGameStatus.NotReadyToPlay; - foreach (var playerUserId in _playerGameStatuses.Keys) - { - _playerGameStatuses[playerUserId] = status; - if (!_playerManager.TryGetSessionById(playerUserId, out var playerSession)) - continue; - RaiseNetworkEvent(GetStatusMsg(playerSession), playerSession.Channel); - } - } - - public void ToggleReady(ICommonSession player, bool ready) - { - if (!_playerGameStatuses.ContainsKey(player.UserId)) - return; - - if (!_userDb.IsLoadComplete(player)) - return; - - if (RunLevel != GameRunLevel.PreRoundLobby) - { - return; - } - - _playerGameStatuses[player.UserId] = ready ? PlayerGameStatus.ReadyToPlay : PlayerGameStatus.NotReadyToPlay; - RaiseNetworkEvent(GetStatusMsg(player), player.Channel); - // update server info to reflect new ready count - UpdateInfoText(); - } - - public bool UserHasJoinedGame(ICommonSession session) - => UserHasJoinedGame(session.UserId); - - public bool UserHasJoinedGame(NetUserId userId) - => PlayerGameStatuses.TryGetValue(userId, out var status) && status == PlayerGameStatus.JoinedGame; - } -} diff --git a/Content.Server/GameTicking/GameTicker.RoundFlow.cs b/Content.Server/GameTicking/GameTicker.RoundFlow.cs deleted file mode 100644 index e6ae3634c1..0000000000 --- a/Content.Server/GameTicking/GameTicker.RoundFlow.cs +++ /dev/null @@ -1,998 +0,0 @@ -using System.Linq; -using System.Numerics; -using Content.Server.Announcements; -using Content.Server.Discord; -using Content.Server.GameTicking.Events; -using Content.Server.Maps; -using Content.Server.Roles; -using Content.Shared.CCVar; -using Content.Shared.Database; -using Content.Shared.GameTicking; -using Content.Shared.Maps; -using Content.Shared.Mind; -using Content.Shared.Players; -using Content.Shared.Preferences; -using Content.Shared.Roles.Components; -using JetBrains.Annotations; -using Prometheus; -using Robust.Shared.Asynchronous; -using Robust.Shared.Audio; -using Robust.Shared.EntitySerialization; -using Robust.Shared.EntitySerialization.Systems; -using Robust.Shared.Map; -using Robust.Shared.Network; -using Robust.Shared.Player; -using Robust.Shared.Random; -using Robust.Shared.Utility; - -namespace Content.Server.GameTicking -{ - public sealed partial class GameTicker - { - [Dependency] private DiscordWebhook _discord = default!; - [Dependency] private RoleSystem _role = default!; - [Dependency] private ITaskManager _taskManager = default!; - - 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."); - -#if EXCEPTION_TOLERANCE - [ViewVariables] - private int _roundStartFailCount = 0; -#endif - - [ViewVariables] - private bool _startingRound; - - [ViewVariables] - private GameRunLevel _runLevel; - - private RoundEndMessageEvent.RoundEndPlayerInfo[]? _replayRoundPlayerInfo; - - private string? _replayRoundText; - - [ViewVariables] - public GameRunLevel RunLevel - { - get => _runLevel; - private set - { - // Game admins can run `restartroundnow` while still in-lobby, which'd break things with this check. - // if (_runLevel == value) return; - - var old = _runLevel; - _runLevel = value; - - RaiseLocalEvent(new GameRunLevelChangedEvent(old, value)); - } - } - - /// - /// Returns true if the round's map is eligible to be updated. - /// - /// - public bool CanUpdateMap() - { - return RunLevel == GameRunLevel.PreRoundLobby && - _roundStartTime - RoundPreloadTime > _gameTiming.CurTime; - } - - /// - /// Loads all the maps for the given round. - /// - /// - /// Must be called before the runlevel is set to InRound. - /// - private void LoadMaps() - { - if (_map.MapExists(DefaultMap)) - return; - - AddGamePresetRules(); - - var maps = new List(); - - // the map might have been force-set by something - // (i.e. votemap or forcemap) - var mainStationMap = _gameMapManager.GetSelectedMap(); - if (mainStationMap == null) - { - // otherwise set the map using the config rules - _gameMapManager.SelectMapByConfigRules(); - mainStationMap = _gameMapManager.GetSelectedMap(); - } - - // Small chance the above could return no map. - // ideally SelectMapByConfigRules will always find a valid map - if (mainStationMap != null) - { - maps.Add(mainStationMap); - } - else - { - throw new Exception("invalid config; couldn't select a valid station map!"); - } - - if (CurrentPreset?.MapPool != null && - ProtoMan.TryIndex(CurrentPreset.MapPool, out var pool) && - !pool.Maps.Contains(mainStationMap.ID)) - { - var msg = Loc.GetString("game-ticker-start-round-invalid-map", - ("map", mainStationMap.MapName), - ("mode", Loc.GetString(CurrentPreset.ModeTitle))); - Log.Debug(msg); - SendServerMessage(msg); - } - - // Let game rules dictate what maps we should load. - RaiseLocalEvent(new LoadingMapsEvent(maps)); - - if (maps.Count == 0) - { - _map.CreateMap(out var mapId, runMapInit: false); - DefaultMap = mapId; - return; - } - - for (var i = 0; i < maps.Count; i++) - { - LoadGameMap(maps[i], out var mapId); - DebugTools.Assert(!_map.IsInitialized(mapId)); - - if (i == 0) - DefaultMap = mapId; - } - } - - public PreGameMapLoad RaisePreLoad( - GameMapPrototype proto, - DeserializationOptions? opts = null, - Vector2? offset = null, - Angle? rot = null) - { - offset ??= proto.MaxRandomOffset != 0f - ? _robustRandom.NextVector2(proto.MaxRandomOffset) - : Vector2.Zero; - - rot ??= proto.RandomRotation - ? _robustRandom.NextAngle() - : Angle.Zero; - - opts ??= DeserializationOptions.Default; - var ev = new PreGameMapLoad(proto, opts.Value, offset.Value, rot.Value); - RaiseLocalEvent(ev); - return ev; - } - - /// - /// Loads a new map, allowing systems interested in it to handle loading events. - /// In the base game, this is required to be used if you want to load a station. - /// This does not initialze maps, unles specified via the . - /// - /// - /// This is basically a wrapper around a method that auto generate - /// some using information in a prototype, and raise some events to allow content - /// to modify the options and react to the map creation. - /// - /// Game map prototype to load in. - /// The id of the map that was loaded. - /// Entity loading options, including whether the maps should be initialized. - /// Name to assign to the loaded station. - /// All loaded entities and grids. - public IReadOnlyList LoadGameMap( - GameMapPrototype proto, - out MapId mapId, - DeserializationOptions? options = null, - string? stationName = null, - Vector2? offset = null, - Angle? rot = null) - { - var ev = RaisePreLoad(proto, options, offset, rot); - - if (ev.GameMap.IsGrid) - { - var mapUid = _map.CreateMap(out mapId, runMapInit: options?.InitializeMaps ?? false); - if (!_loader.TryLoadGrid(mapId, - ev.GameMap.MapPath, - out var grid, - ev.Options, - ev.Offset, - ev.Rotation)) - { - throw new Exception($"Failed to load game-map grid {ev.GameMap.ID}"); - } - - _metaData.SetEntityName(mapUid, proto.MapName); - var g = new List { grid.Value.Owner }; - RaiseLocalEvent(new PostGameMapLoad(proto, mapId, g, stationName)); - return g; - } - - if (!_loader.TryLoadMap(ev.GameMap.MapPath, - out var map, - out var grids, - ev.Options, - ev.Offset, - ev.Rotation)) - { - throw new Exception($"Failed to load game map {ev.GameMap.ID}"); - } - - mapId = map.Value.Comp.MapId; - _metaData.SetEntityName(map.Value.Owner, proto.MapName); - var gridUids = grids.Select(x => x.Owner).ToList(); - RaiseLocalEvent(new PostGameMapLoad(proto, mapId, gridUids, stationName)); - return gridUids; - } - - /// - /// Variant of that attempts to assign the provided to the - /// loaded map. - /// - public IReadOnlyList LoadGameMapWithId( - GameMapPrototype proto, - MapId mapId, - DeserializationOptions? opts = null, - string? stationName = null, - Vector2? offset = null, - Angle? rot = null) - { - var ev = RaisePreLoad(proto, opts, offset, rot); - - if (ev.GameMap.IsGrid) - { - var mapUid = _map.CreateMap(mapId); - if (!_loader.TryLoadGrid(mapId, - ev.GameMap.MapPath, - out var grid, - ev.Options, - ev.Offset, - ev.Rotation)) - { - throw new Exception($"Failed to load game-map grid {ev.GameMap.ID}"); - } - - _metaData.SetEntityName(mapUid, proto.MapName); - var g = new List { grid.Value.Owner }; - RaiseLocalEvent(new PostGameMapLoad(proto, mapId, g, stationName)); - return g; - } - - if (!_loader.TryLoadMapWithId( - mapId, - ev.GameMap.MapPath, - out var map, - out var grids, - ev.Options, - ev.Offset, - ev.Rotation)) - { - throw new Exception($"Failed to load map"); - } - - _metaData.SetEntityName(map.Value.Owner, proto.MapName); - var gridUids = grids.Select(x => x.Owner).ToList(); - RaiseLocalEvent(new PostGameMapLoad(proto, mapId, gridUids, stationName)); - return gridUids; - } - - /// - /// Variant of that loads and then merges a game map onto an existing map. - /// - public IReadOnlyList MergeGameMap( - GameMapPrototype proto, - MapId targetMap, - DeserializationOptions? opts = null, - string? stationName = null, - Vector2? offset = null, - Angle? rot = null) - { - // TODO MAP LOADING use a new event? - // This is quite different from the other methods, which will actually create a **new** map. - var ev = RaisePreLoad(proto, opts, offset, rot); - - if (ev.GameMap.IsGrid) - { - if (!_loader.TryLoadGrid(targetMap, - ev.GameMap.MapPath, - out var grid, - ev.Options, - ev.Offset, - ev.Rotation)) - { - throw new Exception($"Failed to load game-map grid {ev.GameMap.ID}"); - } - - var g = new List { grid.Value.Owner }; - // TODO MAP LOADING use a new event? - RaiseLocalEvent(new PostGameMapLoad(proto, targetMap, g, stationName)); - return g; - } - - if (!_loader.TryMergeMap(targetMap, - ev.GameMap.MapPath, - out var grids, - ev.Options, - ev.Offset, - ev.Rotation)) - { - throw new Exception($"Failed to load map"); - } - - var gridUids = grids.Select(x => x.Owner).ToList(); - - // TODO MAP LOADING use a new event? - RaiseLocalEvent(new PostGameMapLoad(proto, targetMap, gridUids, stationName)); - return gridUids; - } - - public int ReadyPlayerCount() - { - var total = 0; - foreach (var (userId, status) in _playerGameStatuses) - { - if (LobbyEnabled && status == PlayerGameStatus.NotReadyToPlay) - continue; - - if (!_playerManager.TryGetSessionById(userId, out _)) - continue; - - total++; - } - - return total; - } - - public void StartRound(bool force = false) - { -#if EXCEPTION_TOLERANCE - try - { -#endif - // If this game ticker is a dummy or the round is already being started, do nothing! - if (DummyTicker || _startingRound) - return; - - _startingRound = true; - - if (RoundId == 0) - IncrementRoundNumber(); - - ReplayStartRound(); - - DebugTools.Assert(RunLevel == GameRunLevel.PreRoundLobby); - _sawmill.Info("Starting round!"); - - SendServerMessage(Loc.GetString("game-ticker-start-round")); - - var readyPlayers = new List(); - var readyPlayerProfiles = new Dictionary(); - var autoDeAdmin = _cfg.GetCVar(CCVars.AdminDeadminOnJoin); - foreach (var (userId, status) in _playerGameStatuses) - { - if (LobbyEnabled && status != PlayerGameStatus.ReadyToPlay) continue; - if (!_playerManager.TryGetSessionById(userId, out var session)) continue; - - if (autoDeAdmin && _adminManager.IsAdmin(session)) - { - _adminManager.DeAdmin(session); - } -#if DEBUG - DebugTools.Assert(_userDb.IsLoadComplete(session), $"Player was readied up but didn't have user DB data loaded yet??"); -#endif - - readyPlayers.Add(session); - HumanoidCharacterProfile profile; - if (_prefsManager.TryGetCachedPreferences(userId, out var preferences)) - { - profile = (HumanoidCharacterProfile)preferences.SelectedCharacter; - } - else - { - var speciesToBlacklist = - new HashSet(_cfg.GetCVar(CCVars.ICNewAccountSpeciesBlacklist).Split(",")); - profile = HumanoidCharacterProfile.Random(speciesToBlacklist); - } - readyPlayerProfiles.Add(userId, profile); - } - - DebugTools.AssertEqual(readyPlayers.Count, ReadyPlayerCount()); - - // Just in case it hasn't been loaded previously we'll try loading it. - LoadMaps(); - - // map has been selected so update the lobby info text - // applies to players who didn't ready up - UpdateInfoText(); - - StartGamePresetRules(); - - RoundLengthMetric.Set(0); - - var startingEvent = new RoundStartingEvent(RoundId); - RaiseLocalEvent(startingEvent); - - var origReadyPlayers = readyPlayers.ToArray(); - - if (!StartPreset(origReadyPlayers, force)) - { - _startingRound = false; - return; - } - - // MapInitialize *before* spawning players, our codebase is too shit to do it afterwards... - _map.InitializeMap(DefaultMap); - - SpawnPlayers(readyPlayers, readyPlayerProfiles, force); - - _roundStartDateTime = DateTime.UtcNow; - RunLevel = GameRunLevel.InRound; - - RoundStartTimeSpan = _gameTiming.CurTime; - SendStatusToAll(); - ReqWindowAttentionAll(); - UpdateLateJoinStatus(); - AnnounceRound(); - UpdateInfoText(); - SendRoundStartedDiscordMessage(); - -#if EXCEPTION_TOLERANCE - } - catch (Exception e) - { - _roundStartFailCount++; - - if (RoundStartFailShutdownCount > 0 && _roundStartFailCount >= RoundStartFailShutdownCount) - { - _sawmill.Fatal($"Failed to start a round {_roundStartFailCount} time(s) in a row... Shutting down!"); - _runtimeLog.LogException(e, nameof(GameTicker)); - _baseServer.Shutdown("Restarting server"); - return; - } - - _sawmill.Error($"Exception caught while trying to start the round! Restarting round..."); - _runtimeLog.LogException(e, nameof(GameTicker)); - _startingRound = false; - RestartRound(); - return; - } - - // Round started successfully! Reset counter... - _roundStartFailCount = 0; -#endif - _startingRound = false; - } - - private void RefreshLateJoinAllowed() - { - var refresh = new RefreshLateJoinAllowedEvent(); - RaiseLocalEvent(refresh); - DisallowLateJoin = refresh.DisallowLateJoin; - } - - public void EndRound(string text = "") - { - // If this game ticker is a dummy, do nothing! - if (DummyTicker) - return; - - DebugTools.Assert(RunLevel == GameRunLevel.InRound); - _sawmill.Info("Ending round!"); - - RunLevel = GameRunLevel.PostRound; - - try - { - ShowRoundEndScoreboard(text); - } - catch (Exception e) - { - Log.Error($"Error while showing round end scoreboard: {e}"); - } - - try - { - SendRoundEndDiscordMessage(); - } - catch (Exception e) - { - Log.Error($"Error while sending round end Discord message: {e}"); - } - } - - public void ShowRoundEndScoreboard(string text = "") - { - // Log end of round - _adminLogger.Add(LogType.EmergencyShuttle, LogImpact.High, $"Round ended, showing summary"); - - //Tell every client the round has ended. - var gamemodeTitle = CurrentPreset != null ? Loc.GetString(CurrentPreset.ModeTitle) : string.Empty; - - // Let things add text here. - var textEv = new RoundEndTextAppendEvent(); - RaiseLocalEvent(textEv); - - var roundEndText = $"{text}\n{textEv.Text}"; - - //Get the timespan of the round. - var roundDuration = RoundDuration(); - - //Generate a list of basic player info to display in the end round summary. - var listOfPlayerInfo = new List(); - // Grab the great big book of all the Minds, we'll need them for this. - var allMinds = EntityQueryEnumerator(); - var pvsOverride = _cfg.GetCVar(CCVars.RoundEndPVSOverrides); - while (allMinds.MoveNext(out var mindId, out var mind)) - { - // TODO don't list redundant observer roles? - // I.e., if a player was an observer ghost, then a hamster ghost role, maybe just list hamster and not - // the observer role? - var userId = mind.UserId ?? mind.OriginalOwnerUserId; - - var connected = false; - var observer = _role.MindHasRole(mindId); - // Continuing - if (userId != null && _playerManager.ValidSessionId(userId.Value)) - { - connected = true; - } - ContentPlayerData? contentPlayerData = null; - if (userId != null && _playerManager.TryGetPlayerData(userId.Value, out var playerData)) - { - contentPlayerData = playerData.ContentData(); - } - // Finish - - var antag = _roles.MindIsAntagonist(mindId); - - var playerIcName = "Unknown"; - - if (mind.CharacterName != null) - playerIcName = mind.CharacterName; - else if (mind.CurrentEntity != null && TryName(mind.CurrentEntity.Value, out var icName)) - playerIcName = icName; - - if (TryGetEntity(mind.OriginalOwnedEntity, out var entity) && pvsOverride) - { - _pvsOverride.AddGlobalOverride(entity.Value); - } - - var roles = _roles.MindGetAllRoleInfo(mindId); - - var playerEndRoundInfo = new RoundEndMessageEvent.RoundEndPlayerInfo() - { - // Note that contentPlayerData?.Name sticks around after the player is disconnected. - // This is as opposed to ply?.Name which doesn't. - PlayerOOCName = contentPlayerData?.Name ?? "(IMPOSSIBLE: REGISTERED MIND WITH NO OWNER)", - // Character name takes precedence over current entity name - PlayerICName = playerIcName, - PlayerGuid = userId, - PlayerNetEntity = GetNetEntity(entity), - Role = antag - ? roles.First(role => role.Antagonist).Name - : roles.FirstOrDefault().Name ?? Loc.GetString("game-ticker-unknown-role"), - Antag = antag, - JobPrototypes = roles.Where(role => !role.Antagonist).Select(role => role.Prototype).ToArray(), - AntagPrototypes = roles.Where(role => role.Antagonist).Select(role => role.Prototype).ToArray(), - Observer = observer, - Connected = connected - }; - listOfPlayerInfo.Add(playerEndRoundInfo); - } - - // This ordering mechanism isn't great (no ordering of minds) but functions - var listOfPlayerInfoFinal = listOfPlayerInfo.OrderBy(pi => pi.PlayerOOCName).ToArray(); - var sound = RoundEndSoundCollection == null ? null : _audio.ResolveSound(new SoundCollectionSpecifier(RoundEndSoundCollection)); - - var roundEndMessageEvent = new RoundEndMessageEvent( - gamemodeTitle, - roundEndText, - roundDuration, - RoundId, - listOfPlayerInfoFinal.Length, - listOfPlayerInfoFinal, - sound - ); - RaiseNetworkEvent(roundEndMessageEvent); - RaiseLocalEvent(roundEndMessageEvent); - - _replayRoundPlayerInfo = listOfPlayerInfoFinal; - _replayRoundText = roundEndText; - } - - private async void SendRoundEndDiscordMessage() - { - try - { - if (_webhookIdentifier == null) - return; - - var duration = RoundDuration(); - var content = Loc.GetString("discord-round-notifications-end", - ("id", RoundId), - ("hours", Math.Truncate(duration.TotalHours)), - ("minutes", duration.Minutes), - ("seconds", duration.Seconds)); - var payload = new WebhookPayload { Content = content }; - - await _discord.CreateMessage(_webhookIdentifier.Value, payload); - - if (DiscordRoundEndRole == null) - return; - - content = Loc.GetString("discord-round-notifications-end-ping", ("roleId", DiscordRoundEndRole)); - payload = new WebhookPayload { Content = content }; - payload.AllowedMentions.AllowRoleMentions(); - - await _discord.CreateMessage(_webhookIdentifier.Value, payload); - } - catch (Exception e) - { - Log.Error($"Error while sending discord round end message:\n{e}"); - } - } - - public void RestartRound() - { - // If this game ticker is a dummy, do nothing! - if (DummyTicker) - return; - - ReplayEndRound(); - - // Handle restart for server update - if (_serverUpdates.RoundEnded()) - return; - - // Check if the GamePreset needs to be reset - TryResetPreset(); - - _sawmill.Info("Restarting round!"); - - SendServerMessage(Loc.GetString("game-ticker-restart-round")); - - RoundNumberMetric.Inc(); - - PlayersJoinedRoundNormally = 0; - - RunLevel = GameRunLevel.PreRoundLobby; - RandomizeLobbyBackground(); - ResettingCleanup(); - IncrementRoundNumber(); - SendRoundStartingDiscordMessage(); - - if (!LobbyEnabled) - { - StartRound(); - } - else - { - if (_playerManager.PlayerCount == 0) - _roundStartCountdownHasNotStartedYetDueToNoPlayers = true; - else - _roundStartTime = _gameTiming.CurTime + LobbyDuration; - - SendStatusToAll(); - UpdateInfoText(); - - ReqWindowAttentionAll(); - } - } - - private async void SendRoundStartingDiscordMessage() - { - try - { - if (_webhookIdentifier == null) - return; - - var content = Loc.GetString("discord-round-notifications-new"); - - var payload = new WebhookPayload { Content = content }; - - await _discord.CreateMessage(_webhookIdentifier.Value, payload); - } - catch (Exception e) - { - Log.Error($"Error while sending discord round starting message:\n{e}"); - } - } - - /// - /// 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.Sessions) - { - PlayerJoinLobby(player); - } - - // Round restart cleanup event, so entity systems can reset. - var ev = new RoundRestartCleanupEvent(); - RaiseLocalEvent(ev); - - // So clients' entity systems can clean up too... - RaiseNetworkEvent(ev); - - EntityManager.FlushEntities(); - - _banManager.Restart(); - - _gameMapManager.ClearSelectedMap(); - - // Clear up any game rules. - ClearGameRules(); - CurrentPreset = null; - - _allPreviousGameRules.Clear(); - - DisallowLateJoin = false; - _playerGameStatuses.Clear(); - foreach (var session in _playerManager.Sessions) - { - _playerGameStatuses[session.UserId] = LobbyEnabled ? PlayerGameStatus.NotReadyToPlay : PlayerGameStatus.ReadyToPlay; - } - } - - public bool DelayStart(TimeSpan time) - { - if (_runLevel != GameRunLevel.PreRoundLobby) - { - return false; - } - - _roundStartTime += time; - - RaiseNetworkEvent(new TickerLobbyCountdownEvent(_roundStartTime, Paused)); - - _chatManager.DispatchServerAnnouncement(Loc.GetString("game-ticker-delay-start", ("seconds", time.TotalSeconds))); - - return true; - } - - private void UpdateRoundFlow(float frameTime) - { - if (RunLevel == GameRunLevel.InRound) - { - RoundLengthMetric.Inc(frameTime); - } - - if (_roundStartTime == TimeSpan.Zero || - RunLevel != GameRunLevel.PreRoundLobby || - Paused || - _roundStartTime - RoundPreloadTime > _gameTiming.CurTime || - _roundStartCountdownHasNotStartedYetDueToNoPlayers) - { - return; - } - - if (_roundStartTime < _gameTiming.CurTime) - { - StartRound(); - } - // Preload maps so we can start faster - else if (_roundStartTime - RoundPreloadTime < _gameTiming.CurTime) - { - LoadMaps(); - } - } - - private void AnnounceRound() - { - if (CurrentPreset == null) return; - - var options = ProtoMan.EnumeratePrototypes().ToList(); - - if (options.Count == 0) - return; - - var proto = _robustRandom.Pick(options); - - if (proto.Message != null) - _chatSystem.DispatchGlobalAnnouncement(Loc.GetString(proto.Message), playSound: true); - - if (proto.Sound != null) - _audio.PlayGlobal(proto.Sound, Filter.Broadcast(), true); - } - - private async void SendRoundStartedDiscordMessage() - { - try - { - if (_webhookIdentifier == null) - return; - - var mapName = _gameMapManager.GetSelectedMap()?.MapName ?? Loc.GetString("discord-round-notifications-unknown-map"); - var content = Loc.GetString("discord-round-notifications-started", ("id", RoundId), ("map", mapName)); - - var payload = new WebhookPayload { Content = content }; - - await _discord.CreateMessage(_webhookIdentifier.Value, payload); - } - catch (Exception e) - { - Log.Error($"Error while sending discord round start message:\n{e}"); - } - } - } - - public enum GameRunLevel - { - PreRoundLobby = 0, - InRound = 1, - PostRound = 2 - } - - public sealed class GameRunLevelChangedEvent - { - public GameRunLevel Old { get; } - public GameRunLevel New { get; } - - public GameRunLevelChangedEvent(GameRunLevel old, GameRunLevel @new) - { - Old = old; - New = @new; - } - } - - /// - /// Event raised before maps are loaded in pre-round setup. - /// Contains a list of game map prototypes to load; modify it if you want to load different maps, - /// for example as part of a game rule. - /// - [PublicAPI] - public sealed class LoadingMapsEvent : EntityEventArgs - { - public List Maps; - - public LoadingMapsEvent(List maps) - { - Maps = maps; - } - } - - /// - /// Event raised before the game loads a given map. - /// This event is mutable, and load options should be tweaked if necessary. - /// - /// - /// You likely want to subscribe to this after StationSystem. - /// - [PublicAPI] - public sealed class PreGameMapLoad(GameMapPrototype gameMap, DeserializationOptions options, Vector2 offset, Angle rotation) : EntityEventArgs - { - public readonly GameMapPrototype GameMap = gameMap; - public DeserializationOptions Options = options; - public Vector2 Offset = offset; - public Angle Rotation = rotation; - } - - /// - /// Event raised after the game loads a given map. - /// - /// - /// You likely want to subscribe to this after StationSystem. - /// - [PublicAPI] - public sealed class PostGameMapLoad : EntityEventArgs - { - public readonly GameMapPrototype GameMap; - public readonly MapId Map; - public readonly IReadOnlyList Grids; - public readonly string? StationName; - - public PostGameMapLoad(GameMapPrototype gameMap, MapId map, IReadOnlyList grids, string? stationName) - { - GameMap = gameMap; - Map = map; - Grids = grids; - StationName = stationName; - } - } - - /// - /// Event raised to refresh the late join status. - /// If you want to disallow late joins, listen to this and call Disallow. - /// - public sealed class RefreshLateJoinAllowedEvent - { - public bool DisallowLateJoin { get; private set; } = false; - - public void Disallow() - { - DisallowLateJoin = true; - } - } - - /// - /// Attempt event raised on round start. - /// This can be listened to by GameRule systems to cancel round start if some condition is not met, like player count. - /// - public sealed class RoundStartAttemptEvent : CancellableEntityEventArgs - { - public ICommonSession[] Players { get; } - public bool Forced { get; } - - public RoundStartAttemptEvent(ICommonSession[] players, bool forced) - { - Players = players; - Forced = forced; - } - } - - /// - /// Event raised before readied up players are spawned and given jobs by the GameTicker. - /// You can use this to spawn people off-station, like in the case of nuke ops or wizard. - /// Remove the players you spawned from the PlayerPool and call on them. - /// - public sealed class RulePlayerSpawningEvent - { - /// - /// Pool of players to be spawned. - /// If you want to handle a specific player being spawned, remove it from this list and do what you need. - /// - /// If you spawn a player by yourself from this event, don't forget to call on them. - public List PlayerPool { get; } - public IReadOnlyDictionary Profiles { get; } - public bool Forced { get; } - - public RulePlayerSpawningEvent(List playerPool, IReadOnlyDictionary profiles, bool forced) - { - PlayerPool = playerPool; - Profiles = profiles; - Forced = forced; - } - } - - /// - /// Event raised after players were assigned jobs by the GameTicker and have been spawned in. - /// You can give on-station people special roles by listening to this event. - /// - public sealed class RulePlayerJobsAssignedEvent - { - public ICommonSession[] Players { get; } - public IReadOnlyDictionary Profiles { get; } - public bool Forced { get; } - - public RulePlayerJobsAssignedEvent(ICommonSession[] players, IReadOnlyDictionary profiles, bool forced) - { - Players = players; - Profiles = profiles; - Forced = forced; - } - } - - /// - /// Event raised to allow subscribers to add text to the round end summary screen. - /// - public sealed class RoundEndTextAppendEvent - { - private bool _doNewLine; - - /// - /// Text to display in the round end summary screen. - /// - public string Text { get; private set; } = string.Empty; - - /// - /// Invoke this method to add text to the round end summary screen. - /// - /// - public void AddLine(string text) - { - if (_doNewLine) - Text += "\n"; - - Text += text; - _doNewLine = true; - } - } -} diff --git a/Content.Server/GameTicking/GameTicker.StatusShell.cs b/Content.Server/GameTicking/GameTicker.StatusShell.cs deleted file mode 100644 index 87d4129a9c..0000000000 --- a/Content.Server/GameTicking/GameTicker.StatusShell.cs +++ /dev/null @@ -1,62 +0,0 @@ -using System.Linq; -using System.Text.Json.Nodes; -using Content.Shared.CCVar; -using Content.Shared.GameTicking; -using Robust.Server.ServerStatus; -using Robust.Shared.Configuration; - -namespace Content.Server.GameTicking -{ - public sealed partial class GameTicker - { - /// - /// Used for thread safety, given is called from another thread. - /// - private readonly object _statusShellLock = new(); - - /// - /// Round start time in UTC, for status shell purposes. - /// - [ViewVariables] - private DateTime _roundStartDateTime; - - /// - /// For access to CVars in status responses. - /// - [Dependency] private IConfigurationManager _cfg = default!; - /// - /// For access to the round ID in status responses. - /// - [Dependency] private SharedGameTicker _gameTicker = default!; - - private void InitializeStatusShell() - { - IoCManager.Resolve().OnStatusRequest += GetStatusResponse; - } - - private void GetStatusResponse(JsonNode jObject) - { - var preset = CurrentPreset ?? Preset; - - // This method is raised from another thread, so this better be thread safe! - lock (_statusShellLock) - { - jObject["name"] = _baseServer.ServerName; - jObject["map"] = _gameMapManager.GetSelectedMap()?.MapName; - jObject["round_id"] = _gameTicker.RoundId; - jObject["players"] = _cfg.GetCVar(CCVars.AdminsCountInReportedPlayerCount) - ? _playerManager.PlayerCount - : _playerManager.PlayerCount - _adminManager.ActiveAdmins.Count(); - jObject["soft_max_players"] = _cfg.GetCVar(CCVars.SoftMaxPlayers); - jObject["panic_bunker"] = _cfg.GetCVar(CCVars.PanicBunkerEnabled); - jObject["run_level"] = (int) _runLevel; - if (preset != null) - jObject["preset"] = (Decoy == null) ? Loc.GetString(preset.ModeTitle) : Loc.GetString(Decoy.ModeTitle); - if (_runLevel >= GameRunLevel.InRound) - { - jObject["round_start_time"] = _roundStartDateTime.ToString("o"); - } - } - } - } -} diff --git a/Content.Server/GameTicking/Presets/GamePresetPrototype.cs b/Content.Server/GameTicking/Presets/GamePresetPrototype.cs deleted file mode 100644 index febaef8270..0000000000 --- a/Content.Server/GameTicking/Presets/GamePresetPrototype.cs +++ /dev/null @@ -1,43 +0,0 @@ -using Content.Server.Maps; -using Robust.Shared.Prototypes; - -namespace Content.Server.GameTicking.Presets -{ - /// - /// A round-start setup preset, such as which antagonists to spawn. - /// - [Prototype] - public sealed partial class GamePresetPrototype : IPrototype - { - [IdDataField] - public string ID { get; private set; } = default!; - - [DataField] - public string[] Alias = Array.Empty(); - - [DataField("name")] - public string ModeTitle = "????"; - - [DataField] - public string Description = string.Empty; - - [DataField] - public bool ShowInVote; - - [DataField] - public int? MinPlayers; - - [DataField] - public int? MaxPlayers; - - [DataField] - public IReadOnlyList Rules { get; private set; } = Array.Empty(); - - /// - /// If specified, the gamemode will only be run with these maps. - /// If none are elligible, the global fallback will be used. - /// - [DataField("supportedMaps")] - public ProtoId? MapPool; - } -} diff --git a/Content.Server/GameTicking/Rules/AntagLoadProfileRuleSystem.cs b/Content.Server/GameTicking/Rules/AntagLoadProfileRuleSystem.cs index 6216abebb3..de03fd8915 100644 --- a/Content.Server/GameTicking/Rules/AntagLoadProfileRuleSystem.cs +++ b/Content.Server/GameTicking/Rules/AntagLoadProfileRuleSystem.cs @@ -1,7 +1,9 @@ using Content.Server.Antag; using Content.Server.GameTicking.Rules.Components; using Content.Server.Preferences.Managers; +using Content.Shared.Antag; using Content.Shared.Body; +using Content.Shared.GameTicking.Rules; using Content.Shared.Humanoid; using Content.Shared.Humanoid.Prototypes; using Content.Shared.Preferences; diff --git a/Content.Server/GameTicking/Rules/DeathMatchRuleSystem.cs b/Content.Server/GameTicking/Rules/DeathMatchRuleSystem.cs index 44a2121835..0305fa3d27 100644 --- a/Content.Server/GameTicking/Rules/DeathMatchRuleSystem.cs +++ b/Content.Server/GameTicking/Rules/DeathMatchRuleSystem.cs @@ -8,7 +8,9 @@ using Content.Server.Station.Systems; using Content.Shared.EntityTable; using Content.Shared.GameTicking; using Content.Shared.GameTicking.Components; +using Content.Shared.GameTicking.Rules; using Content.Shared.Points; +using Content.Shared.Station.Systems; using Robust.Server.Player; using Robust.Shared.Utility; @@ -43,7 +45,7 @@ public sealed partial class DeathMatchRuleSystem : GameRuleSystem(); while (query.MoveNext(out var uid, out var dm, out var tracker, out var point, out var rule)) { - if (!GameTicker.IsGameRuleActive(uid, rule)) + if (!GameTicker.IsGameRuleActive((uid, rule))) continue; var newMind = _mind.CreateMind(ev.Player.UserId, ev.Profile.Name); @@ -71,8 +73,9 @@ public sealed partial class DeathMatchRuleSystem : GameRuleSystem(); while (query.MoveNext(out var uid, out _, out var tracker, out var rule)) { - if (!GameTicker.IsGameRuleActive(uid, rule)) + if (!GameTicker.IsGameRuleActive((uid, rule))) continue; + _respawn.AddToTracker((ev.Mob, null), (uid, tracker)); } } @@ -82,7 +85,7 @@ public sealed partial class DeathMatchRuleSystem : GameRuleSystem(); while (query.MoveNext(out var uid, out var dm, out var point, out var rule)) { - if (!GameTicker.IsGameRuleActive(uid, rule)) + if (!GameTicker.IsGameRuleActive((uid, rule))) continue; // YOU SUICIDED OR GOT THROWN INTO LAVA! @@ -117,12 +120,12 @@ public sealed partial class DeathMatchRuleSystem : GameRuleSystem entity, ref RoundEndTextAppendEvent args) { - if (!TryComp(uid, out var point)) + if (!TryComp(entity, out var point)) return; - if (component.Victor != null && _player.TryGetPlayerData(component.Victor.Value, out var data)) + if (entity.Comp.Victor != null && _player.TryGetPlayerData(entity.Comp.Victor.Value, out var data)) { args.AddLine(Loc.GetString("point-scoreboard-winner", ("player", data.UserName))); args.AddLine(""); diff --git a/Content.Server/GameTicking/Rules/DynamicRuleSystem.cs b/Content.Server/GameTicking/Rules/DynamicRuleSystem.cs index bc9d9d01d1..f84668b4f5 100644 --- a/Content.Server/GameTicking/Rules/DynamicRuleSystem.cs +++ b/Content.Server/GameTicking/Rules/DynamicRuleSystem.cs @@ -1,4 +1,3 @@ -using System.Diagnostics; using Content.Server.Administration.Logs; using Content.Server.RoundEnd; using Content.Shared.Database; @@ -6,6 +5,7 @@ using Content.Shared.EntityTable; using Content.Shared.EntityTable.Conditions; using Content.Shared.GameTicking.Components; using Content.Shared.GameTicking.Rules; +using Content.Shared.GameTicking.Rules.Components; using Robust.Shared.Prototypes; using Robust.Shared.Random; @@ -36,13 +36,14 @@ public sealed partial class DynamicRuleSystem : GameRuleSystem rule, ref GameRuleEndedEvent args) { - base.Ended(uid, component, gameRule, args); + base.Ended(rule, ref args); - foreach (var rule in component.Rules) + foreach (var gameRule in rule.Comp.Rules) { - GameTicker.EndGameRule(rule); + GameTicker.EndGameRule(gameRule); } } @@ -102,10 +103,10 @@ public sealed partial class DynamicRuleSystem : GameRuleSystem(ruleUid, out var cost)) { @@ -130,7 +131,7 @@ public sealed partial class DynamicRuleSystem : GameRuleSystem(); while (query.MoveNext(out var uid, out _, out var comp)) { - if (!GameTicker.IsGameRuleActive(uid, comp)) + if (!GameTicker.IsGameRuleActive((uid, comp))) continue; rules.Add(uid); } diff --git a/Content.Server/GameTicking/Rules/GameRuleSystem.Utility.cs b/Content.Server/GameTicking/Rules/GameRuleSystem.Utility.cs deleted file mode 100644 index 9c8c26bdc7..0000000000 --- a/Content.Server/GameTicking/Rules/GameRuleSystem.Utility.cs +++ /dev/null @@ -1,184 +0,0 @@ -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using Content.Server.Station.Components; -using Content.Shared.GameTicking.Components; -using Content.Shared.Station.Components; -using Robust.Shared.Collections; -using Robust.Shared.Map; -using Robust.Shared.Map.Components; -using Robust.Shared.Utility; - -namespace Content.Server.GameTicking.Rules; - -public abstract partial class GameRuleSystem where T: IComponent -{ - protected EntityQueryEnumerator QueryActiveRules() - { - return EntityQueryEnumerator(); - } - - protected EntityQueryEnumerator QueryDelayedRules() - { - return EntityQueryEnumerator(); - } - - /// - /// Queries all gamerules, regardless of if they're active or not. - /// - protected EntityQueryEnumerator QueryAllRules() - { - return EntityQueryEnumerator(); - } - - /// - /// Utility function for finding a random event-eligible station entity - /// - protected bool TryGetRandomStation([NotNullWhen(true)] out EntityUid? station, Func? filter = null) - { - var stations = new ValueList(Count()); - - filter ??= _ => true; - var query = AllEntityQuery(); - - while (query.MoveNext(out var uid, out _)) - { - if (!filter(uid)) - continue; - - stations.Add(uid); - } - - if (stations.Count == 0) - { - station = null; - return false; - } - - // TODO: Engine PR. - station = stations[RobustRandom.Next(stations.Count)]; - return true; - } - - protected bool TryFindRandomTile(out Vector2i tile, - [NotNullWhen(true)] out EntityUid? targetStation, - out EntityUid targetGrid, - out EntityCoordinates targetCoords) - { - tile = default; - targetStation = EntityUid.Invalid; - targetGrid = EntityUid.Invalid; - targetCoords = EntityCoordinates.Invalid; - if (TryGetRandomStation(out targetStation)) - { - return TryFindRandomTileOnStation((targetStation.Value, Comp(targetStation.Value)), - out tile, - out targetGrid, - out targetCoords); - } - - return false; - } - - protected bool TryFindRandomTileOnStation(Entity station, - out Vector2i tile, - out EntityUid targetGrid, - out EntityCoordinates targetCoords, - int numAttempts = 10) - { - tile = default; - targetCoords = EntityCoordinates.Invalid; - targetGrid = EntityUid.Invalid; - - // Weight grid choice by tilecount - var totalTiles = 0; - var grids = new List<(Entity Entity, int Count, List Tiles)>(); - foreach (var possibleTarget in station.Comp.Grids) - { - if (!TryComp(possibleTarget, out var comp)) - continue; - - // Get the tile count for the given grid. - var tileCount = _map.GetFilledTileCount((possibleTarget, comp)); - - // Just to be sure, no empty elements. - if (tileCount > 0) - { - grids.Add(((possibleTarget, comp), tileCount, new())); - totalTiles += tileCount; - } - } - - if (grids.Count == 0) - { - targetGrid = EntityUid.Invalid; - return false; - } - - for (var i = 0; i < numAttempts; i++) - { - // Find random tile within list. - var nextTileIndex = RobustRandom.Next(totalTiles); - TileRef? randomTileRef = null; - MapGridComponent gridComp = default!; - var startIndex = 0; - for (int j = 0; j < grids.Count; j++) - { - var grid = grids[j]; - // If the index is in this particular grid, find it and remove the tile to prevent selecting it twice. - if (nextTileIndex >= startIndex + grid.Count) - { - startIndex += grid.Count; - continue; - } - - (targetGrid, gridComp) = grid.Entity; - - // Empty list: hasn't been queried yet - get our tiles. - if (grid.Tiles.Count <= 0) - { - grid.Tiles = _map.GetAllTiles(targetGrid, gridComp).ToList(); - - // Actual list count doesn't match expected count (a bug - return failure). - Debug.Assert(grid.Tiles.Count == grid.Count); - if (grid.Tiles.Count != grid.Count) - return false; - } - - var ourTileIndex = nextTileIndex - startIndex; - randomTileRef = grid.Tiles[ourTileIndex]; - grid.Tiles.RemoveSwap(ourTileIndex); - grid.Count--; - totalTiles--; - - // Empty list, remove element - if (grid.Tiles.Count <= 0) - grids.RemoveSwap(j); - - break; - } - - // Out of valid tiles, return early. - if (randomTileRef is not { } tileRef) - return false; - - // Invalid tile, try again. - if (_atmosphere.IsTileSpace(targetGrid, Transform(targetGrid).MapUid, tileRef.GridIndices) - || _atmosphere.IsTileAirBlockedCached(targetGrid, tileRef.GridIndices)) - { - continue; - } - - targetCoords = _map.GridTileToLocal(targetGrid, gridComp, tileRef.GridIndices); - tile = tileRef.GridIndices; - return true; - } - - return false; - } - - protected void ForceEndSelf(EntityUid uid, GameRuleComponent? component = null) - { - GameTicker.EndGameRule(uid, component); - } -} diff --git a/Content.Server/GameTicking/Rules/GameRuleSystem.cs b/Content.Server/GameTicking/Rules/GameRuleSystem.cs deleted file mode 100644 index 5398503ea8..0000000000 --- a/Content.Server/GameTicking/Rules/GameRuleSystem.cs +++ /dev/null @@ -1,118 +0,0 @@ -using Content.Server.Atmos.EntitySystems; -using Content.Shared.GameTicking.Components; -using Robust.Server.GameObjects; -using Robust.Shared.Random; -using Robust.Shared.Timing; - -namespace Content.Server.GameTicking.Rules; - -public abstract partial class GameRuleSystem : EntitySystem where T : IComponent -{ - [Dependency] protected IGameTiming Timing = default!; - [Dependency] protected IRobustRandom RobustRandom = default!; - [Dependency] protected GameTicker GameTicker = default!; - - // Not protected, just to be used in utility methods - [Dependency] private AtmosphereSystem _atmosphere = default!; - [Dependency] private MapSystem _map = default!; - - [Dependency] protected EntityQuery GameRuleQuery = default!; - [Dependency] protected EntityQuery RuleQuery = default!; - - public override void Initialize() - { - base.Initialize(); - - SubscribeLocalEvent(OnGameRuleAdded); - SubscribeLocalEvent(OnGameRuleStarted); - SubscribeLocalEvent(OnGameRuleEnded); - SubscribeLocalEvent(OnRoundEndTextAppend); - } - - private void OnGameRuleAdded(EntityUid uid, T component, ref GameRuleAddedEvent args) - { - if (!GameRuleQuery.TryComp(uid, out var ruleData)) - return; - - Added(uid, component, ruleData, args); - } - - private void OnGameRuleStarted(EntityUid uid, T component, ref GameRuleStartedEvent args) - { - if (!GameRuleQuery.TryComp(uid, out var ruleData)) - return; - - Started(uid, component, ruleData, args); - } - - private void OnGameRuleEnded(EntityUid uid, T component, ref GameRuleEndedEvent args) - { - if (!GameRuleQuery.TryComp(uid, out var ruleData)) - return; - - Ended(uid, component, ruleData, args); - } - - private void OnRoundEndTextAppend(RoundEndTextAppendEvent ev) - { - var query = QueryAllRules(); - while (query.MoveNext(out var uid, out var comp, out var ruleData)) - { - AppendRoundEndText(uid, comp, ruleData, ref ev); - } - } - - /// - /// Called when the gamerule is added - /// - protected virtual void Added(EntityUid uid, T component, GameRuleComponent gameRule, GameRuleAddedEvent args) - { - - } - - /// - /// Called when the gamerule begins - /// - protected virtual void Started(EntityUid uid, T component, GameRuleComponent gameRule, GameRuleStartedEvent args) - { - - } - - /// - /// Called when the gamerule ends - /// - protected virtual void Ended(EntityUid uid, T component, GameRuleComponent gameRule, GameRuleEndedEvent args) - { - - } - - /// - /// Called at the end of a round when text needs to be added for a game rule. - /// - protected virtual void AppendRoundEndText(EntityUid uid, T component, GameRuleComponent gameRule, ref RoundEndTextAppendEvent args) - { - - } - - /// - /// Called on an active gamerule entity in the Update function - /// - protected virtual void ActiveTick(EntityUid uid, T component, GameRuleComponent gameRule, float frameTime) - { - - } - - public override void Update(float frameTime) - { - base.Update(frameTime); - - var query = EntityQueryEnumerator(); - while (query.MoveNext(out var uid, out var comp1, out var comp2)) - { - if (!GameTicker.IsGameRuleActive(uid, comp2)) - continue; - - ActiveTick(uid, comp1, comp2, frameTime); - } - } -} diff --git a/Content.Server/GameTicking/Rules/InactivityTimeRestartRuleSystem.cs b/Content.Server/GameTicking/Rules/InactivityTimeRestartRuleSystem.cs index cc67b76a51..1fd287be13 100644 --- a/Content.Server/GameTicking/Rules/InactivityTimeRestartRuleSystem.cs +++ b/Content.Server/GameTicking/Rules/InactivityTimeRestartRuleSystem.cs @@ -1,7 +1,9 @@ using System.Threading; using Content.Server.Chat.Managers; using Content.Server.GameTicking.Rules.Components; +using Content.Shared.GameTicking; using Content.Shared.GameTicking.Components; +using Content.Shared.GameTicking.Rules; using Robust.Server.Player; using Robust.Shared.Player; using Timer = Robust.Shared.Timing.Timer; @@ -27,11 +29,11 @@ public sealed partial class InactivityTimeRestartRuleSystem : GameRuleSystem rule, ref GameRuleEndedEvent args) { - base.Ended(uid, component, gameRule, args); + base.Ended(rule, ref args); - StopTimer(uid, component); + StopTimer(rule, rule.Comp); } public void RestartTimer(EntityUid uid, InactivityRuleComponent? component = null) @@ -66,12 +68,9 @@ public sealed partial class InactivityTimeRestartRuleSystem : GameRuleSystem(); - while (query.MoveNext(out var uid, out var inactivity, out var gameRule)) + var query = QueryActiveRules(); + while (query.MoveNext(out var uid, out var inactivity, out _, out _)) { - if (!GameTicker.IsGameRuleActive(uid, gameRule)) - return; - switch (args.New) { case GameRunLevel.InRound: @@ -90,7 +89,7 @@ public sealed partial class InactivityTimeRestartRuleSystem : GameRuleSystem(); while (query.MoveNext(out var uid, out var inactivity, out var gameRule)) { - if (!GameTicker.IsGameRuleActive(uid, gameRule)) + if (!GameTicker.IsGameRuleActive((uid, gameRule))) return; if (GameTicker.RunLevel != GameRunLevel.InRound) diff --git a/Content.Server/GameTicking/Rules/KillCalloutRuleSystem.cs b/Content.Server/GameTicking/Rules/KillCalloutRuleSystem.cs index a61eab34c7..e8f7fc2dd6 100644 --- a/Content.Server/GameTicking/Rules/KillCalloutRuleSystem.cs +++ b/Content.Server/GameTicking/Rules/KillCalloutRuleSystem.cs @@ -3,6 +3,7 @@ using Content.Server.GameTicking.Rules.Components; using Content.Server.KillTracking; using Content.Shared.Chat; using Content.Shared.GameTicking.Components; +using Content.Shared.GameTicking.Rules; using Robust.Server.Player; using Robust.Shared.Player; using Robust.Shared.Random; @@ -31,7 +32,7 @@ public sealed partial class KillCalloutRuleSystem : GameRuleSystem(); while (query.MoveNext(out var uid, out var kill, out var rule)) { - if (!GameTicker.IsGameRuleActive(uid, rule)) + if (!GameTicker.IsGameRuleActive((uid, rule))) continue; var callout = GetCallout(kill, ev); diff --git a/Content.Server/GameTicking/Rules/MaxTimeRestartRuleSystem.cs b/Content.Server/GameTicking/Rules/MaxTimeRestartRuleSystem.cs index 5740ffe8fe..66f36caca5 100644 --- a/Content.Server/GameTicking/Rules/MaxTimeRestartRuleSystem.cs +++ b/Content.Server/GameTicking/Rules/MaxTimeRestartRuleSystem.cs @@ -1,7 +1,9 @@ using System.Threading; using Content.Server.Chat.Managers; using Content.Server.GameTicking.Rules.Components; +using Content.Shared.GameTicking; using Content.Shared.GameTicking.Components; +using Content.Shared.GameTicking.Rules; using Timer = Robust.Shared.Timing.Timer; namespace Content.Server.GameTicking.Rules; @@ -21,15 +23,15 @@ public sealed partial class MaxTimeRestartRuleSystem : GameRuleSystem rule, ref GameRuleEndedEvent args) { - base.Ended(uid, component, gameRule, args); + base.Ended(rule, ref args); - StopTimer(component); + StopTimer(rule); } public void RestartTimer(MaxTimeRestartRuleComponent component) @@ -60,7 +62,7 @@ public sealed partial class MaxTimeRestartRuleSystem : GameRuleSystem(); while (query.MoveNext(out var uid, out var timer, out var gameRule)) { - if (!GameTicker.IsGameRuleActive(uid, gameRule)) + if (!GameTicker.IsGameRuleActive((uid, gameRule))) return; switch (args.New) diff --git a/Content.Server/GameTicking/Rules/RespawnRuleSystem.cs b/Content.Server/GameTicking/Rules/RespawnRuleSystem.cs index c960cfc156..d69c2934c1 100644 --- a/Content.Server/GameTicking/Rules/RespawnRuleSystem.cs +++ b/Content.Server/GameTicking/Rules/RespawnRuleSystem.cs @@ -1,9 +1,9 @@ using Content.Server.Chat.Managers; -using Content.Server.Database.Migrations.Postgres; using Content.Server.GameTicking.Rules.Components; using Content.Server.Station.Systems; using Content.Shared.Chat; using Content.Shared.GameTicking.Components; +using Content.Shared.GameTicking.Rules; using Content.Shared.Interaction.Events; using Content.Shared.Mind; using Content.Shared.Mobs; @@ -24,7 +24,7 @@ public sealed partial class RespawnRuleSystem : GameRuleSystem public override void Initialize() @@ -54,6 +54,7 @@ public sealed partial class RespawnRuleSystem : GameRuleSystem(mind, out var mindComp) && mindComp.OwnedEntity.HasValue) QueueDel(mindComp.OwnedEntity.Value); + GameTicker.MakeJoinGame(session, station, silent: true); tracker.RespawnQueue.Remove(player); } @@ -84,7 +85,7 @@ public sealed partial class RespawnRuleSystem : GameRuleSystem(); while (query.MoveNext(out var uid, out var respawnRule, out var tracker, out var rule)) { - if (!GameTicker.IsGameRuleActive(uid, rule)) + if (!GameTicker.IsGameRuleActive((uid, rule))) continue; if (respawnRule.AlwaysRespawnDead) @@ -109,6 +110,7 @@ public sealed partial class RespawnRuleSystem : GameRuleSystem(); while (passQuery.MoveNext(out var uid, out _, out _)) { - // TODO: for some reason, ending a game rule just gives it a marker comp, - // and doesnt delete it - // so we have to check here that it isnt an ended game rule (which could happen if a preset failed to start - // or it was ended before station maps spawned etc etc etc) - if (HasComp(uid)) - continue; - RaiseLocalEvent(uid, ref passEv); } @@ -62,7 +56,7 @@ public sealed partial class RoundstartStationVariationRuleSystem : GameRuleSyste } /// -/// Raised directed on game rule entities which are added and marked as +/// Raised directed on game rule entities which are added and marked as /// when a new station is initialized that should be varied. /// /// The new station that was added, and its config & grids. diff --git a/Content.Server/GameTicking/Rules/RuleGridsSystem.cs b/Content.Server/GameTicking/Rules/RuleGridsSystem.cs index 6ad59099ce..3a50ce752e 100644 --- a/Content.Server/GameTicking/Rules/RuleGridsSystem.cs +++ b/Content.Server/GameTicking/Rules/RuleGridsSystem.cs @@ -1,6 +1,9 @@ using Content.Server.Antag; using Content.Server.GameTicking.Rules.Components; using Content.Server.Spawners.Components; +using Content.Shared.Antag; +using Content.Shared.GameTicking.Rules; +using Content.Shared.Spawners.Components; using Content.Shared.Whitelist; using Robust.Server.Physics; using Robust.Shared.Map; @@ -28,7 +31,7 @@ public sealed partial class RuleGridsSystem : GameRuleSystem private void OnGridSplit(ref GridSplitEvent args) { var rule = QueryActiveRules(); - while (rule.MoveNext(out _, out var comp, out _)) + while (rule.MoveNext(out var comp, out _, out _)) { if (!comp.MapGrids.Contains(args.Grid)) continue; diff --git a/Content.Server/GameTicking/Rules/SandboxRuleSystem.cs b/Content.Server/GameTicking/Rules/SandboxRuleSystem.cs index 88e9686402..fd1d7c6d48 100644 --- a/Content.Server/GameTicking/Rules/SandboxRuleSystem.cs +++ b/Content.Server/GameTicking/Rules/SandboxRuleSystem.cs @@ -1,6 +1,7 @@ using Content.Server.GameTicking.Rules.Components; using Content.Server.Sandbox; using Content.Shared.GameTicking.Components; +using Content.Shared.GameTicking.Rules; namespace Content.Server.GameTicking.Rules; @@ -8,15 +9,15 @@ public sealed partial class SandboxRuleSystem : GameRuleSystem rule, ref GameRuleStartedEvent args) { - base.Started(uid, component, gameRule, args); + base.Started(rule, ref args); _sandbox.IsSandboxEnabled = true; } - protected override void Ended(EntityUid uid, SandboxRuleComponent component, GameRuleComponent gameRule, GameRuleEndedEvent args) + protected override void Ended(Entity rule, ref GameRuleEndedEvent args) { - base.Ended(uid, component, gameRule, args); + base.Ended(rule, ref args); _sandbox.IsSandboxEnabled = false; } } diff --git a/Content.Server/GameTicking/Rules/SecretRuleSystem.cs b/Content.Server/GameTicking/Rules/SecretRuleSystem.cs index d8594ebf98..d249bad118 100644 --- a/Content.Server/GameTicking/Rules/SecretRuleSystem.cs +++ b/Content.Server/GameTicking/Rules/SecretRuleSystem.cs @@ -1,12 +1,14 @@ using System.Diagnostics.CodeAnalysis; using System.Linq; using Content.Server.Administration.Logs; -using Content.Server.GameTicking.Presets; using Content.Server.GameTicking.Rules.Components; using Content.Shared.GameTicking.Components; using Content.Shared.Random; using Content.Shared.CCVar; using Content.Shared.Database; +using Content.Shared.GameTicking; +using Content.Shared.GameTicking.Prototypes; +using Content.Shared.GameTicking.Rules; using Robust.Shared.Prototypes; using Robust.Shared.Random; using Robust.Shared.Configuration; @@ -48,7 +50,7 @@ public sealed partial class SecretRuleSystem : GameRuleSystem? ruleEnt; // if we're pre-round (i.e. will only be added) // then just add rules. if we're added in the middle of the round (or at any other point really) @@ -58,17 +60,22 @@ public sealed partial class SecretRuleSystem : GameRuleSystem rule, ref GameRuleEndedEvent args) { - base.Ended(uid, component, gameRule, args); + base.Ended(rule, ref args); - foreach (var rule in component.AdditionalGameRules) + foreach (var gameRule in rule.Comp.AdditionalGameRules) { - GameTicker.EndGameRule(rule); + GameTicker.EndGameRule(gameRule); } } diff --git a/Content.Server/GameTicking/Rules/NukeopsRuleSystem.cs b/Content.Server/GameTicking/Rules/ServerNukeopsRuleSystem.cs similarity index 79% rename from Content.Server/GameTicking/Rules/NukeopsRuleSystem.cs rename to Content.Server/GameTicking/Rules/ServerNukeopsRuleSystem.cs index 9b44691d20..599809db8c 100644 --- a/Content.Server/GameTicking/Rules/NukeopsRuleSystem.cs +++ b/Content.Server/GameTicking/Rules/ServerNukeopsRuleSystem.cs @@ -1,32 +1,33 @@ -using Content.Server.Antag; +using System.Linq; +using System.Text; using Content.Server.Communications; using Content.Server.GameTicking.Rules.Components; using Content.Server.Nuke; using Content.Server.NukeOps; using Content.Server.Pinpointer; using Content.Server.Popups; -using Content.Server.Roles; using Content.Server.RoundEnd; using Content.Server.Shuttles.Events; using Content.Server.Shuttles.Systems; -using Content.Server.Station.Components; +using Content.Server.Station.Systems; using Content.Server.Store.Systems; using Content.Shared.Access.Systems; -using Content.Shared.GameTicking.Components; +using Content.Shared.GameTicking; +using Content.Shared.GameTicking.Rules; +using Content.Shared.GameTicking.Rules.Components; using Content.Shared.Mind; using Content.Shared.Mind.Components; using Content.Shared.Mobs; using Content.Shared.Mobs.Components; -using Content.Shared.NPC.Components; -using Content.Shared.NPC.Systems; using Content.Shared.Nuke; using Content.Shared.NukeOps; using Content.Shared.Roles; using Content.Shared.Roles.Components; using Content.Shared.Roles.Jobs; -using Content.Shared.Station; +using Content.Shared.RoundEnd; using Content.Shared.Station.Components; using Content.Shared.StationRecords; +using Content.Shared.StationRecords.Systems; using Content.Shared.Store; using Content.Shared.Store.Components; using Content.Shared.Tag; @@ -36,30 +37,24 @@ using Robust.Shared.Containers; using Robust.Shared.Map; using Robust.Shared.Network; using Robust.Shared.Prototypes; -using Robust.Shared.Random; using Robust.Shared.Utility; -using System.Data; -using System.Linq; -using System.Text; -using Content.Shared.StationRecords.Systems; namespace Content.Server.GameTicking.Rules; -public sealed partial class NukeopsRuleSystem : GameRuleSystem +/// +public sealed partial class ServerNukeopsRuleSystem : NukeopsRuleSystem { - [Dependency] private AntagSelectionSystem _antag = default!; [Dependency] private EmergencyShuttleSystem _emergency = default!; [Dependency] private SharedIdCardSystem _idCard = default!; [Dependency] private SharedJobSystem _jobs = default!; [Dependency] private IPlayerManager _player = default!; [Dependency] private SharedMindSystem _mind = default!; [Dependency] private NavMapSystem _navMap = default!; - [Dependency] private NpcFactionSystem _npcFaction = default!; [Dependency] private PopupSystem _popupSystem = default!; [Dependency] private RoundEndSystem _roundEndSystem = default!; [Dependency] private SharedContainerSystem _containers = default!; [Dependency] private SharedRoleSystem _roles = default!; - [Dependency] private SharedStationSystem _station = default!; + [Dependency] private ServerStationSystem _station = default!; [Dependency] private StationRecordsSystem _records = default!; [Dependency] private StoreSystem _store = default!; [Dependency] private TagSystem _tag = default!; @@ -67,68 +62,14 @@ public sealed partial class NukeopsRuleSystem : GameRuleSystem TelecrystalCurrencyPrototype = "Telecrystal"; private static readonly ProtoId NukeOpsUplinkTagPrototype = "NukeOpsUplink"; - // TODO: This shouldn't be matching by ProtoId. - // It would be better if this were checked by component or something, - // but it needs to be distinct between the full Nukeops and Loneops rules, - // which NukeopsRuleComponent currently isn't. - // Better yet, maybe the behaviors this is used for could be moved to the rule component. - private static readonly EntProtoId NukeopsGameRule = "Nukeops"; - - public override void Initialize() - { - base.Initialize(); - - SubscribeLocalEvent(OnNukeExploded); - SubscribeLocalEvent(OnRunLevelChanged); - SubscribeLocalEvent(OnNukeDisarm); - - SubscribeLocalEvent(OnComponentRemove); - SubscribeLocalEvent(OnMobStateChanged); - SubscribeLocalEvent(OnOperativeZombified); - - SubscribeLocalEvent(OnGetBriefing); - - SubscribeLocalEvent(OnShuttleFTLAttempt); - SubscribeLocalEvent(OnWarDeclared); - SubscribeLocalEvent(OnShuttleCallAttempt); - - SubscribeLocalEvent(OnAfterAntagEntSelected); - SubscribeLocalEvent(OnRuleLoadedGrids); - } - - protected override void Started(EntityUid uid, - NukeopsRuleComponent component, - GameRuleComponent gameRule, - GameRuleStartedEvent args) - { - var eligible = new List>(); - var eligibleQuery = EntityQueryEnumerator(); - while (eligibleQuery.MoveNext(out var eligibleUid, out var eligibleComp, out var member)) - { - if (!_npcFaction.IsFactionHostile(component.Faction, (eligibleUid, member))) - continue; - - eligible.Add((eligibleUid, eligibleComp, member)); - } - - if (eligible.Count == 0) - return; - - component.TargetStation = RobustRandom.Pick(eligible); - var ev = new NukeopsTargetStationSelectedEvent(uid, component.TargetStation); - RaiseLocalEvent(ref ev); - } - #region Event Handlers - protected override void AppendRoundEndText(EntityUid uid, - NukeopsRuleComponent component, - GameRuleComponent gameRule, + protected override void AppendRoundEndText(Entity rule, ref RoundEndTextAppendEvent args) { - var winText = Loc.GetString($"nukeops-{component.WinType.ToString().ToLower()}"); + var winText = Loc.GetString($"nukeops-{rule.Comp.WinType.ToString().ToLower()}"); args.AddLine(winText); - foreach (var cond in component.WinConditions) + foreach (var cond in rule.Comp.WinConditions) { var text = Loc.GetString($"nukeops-cond-{cond.ToString().ToLower()}"); args.AddLine(text); @@ -136,7 +77,7 @@ public sealed partial class NukeopsRuleSystem : GameRuleSystem diskWinConditions = [WinCondition.NukeDiskOnCentCom, WinCondition.NukeDiskNotOnCentCom]; - if (component.WinConditions.Any(diskWinConditions.Contains)) + if (rule.Comp.WinConditions.Any(diskWinConditions.Contains)) { var diskQuery = AllEntityQuery(); while (diskQuery.MoveNext(out var diskUid, out _, out var transform)) @@ -193,7 +134,7 @@ public sealed partial class NukeopsRuleSystem : GameRuleSystem entity, ref ComponentRemove args) { CheckRoundShouldEnd(); } - private void OnMobStateChanged(EntityUid uid, NukeOperativeComponent component, MobStateChangedEvent ev) + [SubscribeLocalEvent] + private void OnMobStateChanged(Entity entity, ref MobStateChangedEvent ev) { if (ev.NewMobState == MobState.Dead) CheckRoundShouldEnd(); } - private void OnOperativeZombified(EntityUid uid, NukeOperativeComponent component, ref EntityZombifiedEvent args) + [SubscribeLocalEvent] + private void OnOperativeZombified(Entity entity, ref EntityZombifiedEvent args) { - RemCompDeferred(uid, component); + RemCompDeferred(entity, entity.Comp); } + [SubscribeLocalEvent] private void OnRuleLoadedGrids(Entity ent, ref RuleLoadedGridsEvent args) { // Check each nukie shuttle @@ -388,10 +339,11 @@ public sealed partial class NukeopsRuleSystem : GameRuleSystem ent, ref AfterAntagEntitySelectedEvent args) - { - var target = (ent.Comp.TargetStation is not null) ? Name(ent.Comp.TargetStation.Value) : "the target"; - - _antag.SendBriefing(args.Session, - Loc.GetString("nukeops-welcome", - ("station", target), - ("name", Name(ent))), - Color.Red, - ent.Comp.GreetSoundNotification); - } - - private void OnGetBriefing(Entity role, ref GetBriefingEvent args) - { - // TODO Different character screen briefing for the 3 nukie types - args.Append(Loc.GetString("nukeops-briefing")); - } - /// /// Is this method the shitty glue holding together the last of my sanity? yes. /// Do i have a better solution? not presently. @@ -704,20 +640,3 @@ public sealed partial class NukeopsRuleSystem : GameRuleSystem -/// Raised when a station has been assigned as a target for the NukeOps rule. -/// -[ByRefEvent] -public readonly struct NukeopsTargetStationSelectedEvent(EntityUid ruleEntity, EntityUid? targetStation) -{ - /// - /// The entity containing the NukeOps gamerule. - /// - public readonly EntityUid RuleEntity = ruleEntity; - - /// - /// The target station, if it exists. - /// - public readonly EntityUid? TargetStation = targetStation; -} diff --git a/Content.Server/GameTicking/Rules/RevolutionaryRuleSystem.cs b/Content.Server/GameTicking/Rules/ServerRevolutionaryRuleSystem.cs similarity index 87% rename from Content.Server/GameTicking/Rules/RevolutionaryRuleSystem.cs rename to Content.Server/GameTicking/Rules/ServerRevolutionaryRuleSystem.cs index 9a63c0fca1..a5648f6df6 100644 --- a/Content.Server/GameTicking/Rules/RevolutionaryRuleSystem.cs +++ b/Content.Server/GameTicking/Rules/ServerRevolutionaryRuleSystem.cs @@ -1,8 +1,6 @@ using System.Linq; using Content.Server.Administration.Logs; -using Content.Server.Antag; using Content.Server.EUI; -using Content.Server.GameTicking.Rules.Components; using Content.Server.Mind; using Content.Server.Popups; using Content.Server.Revolutionary; @@ -11,12 +9,18 @@ using Content.Server.Roles; using Content.Server.RoundEnd; using Content.Server.Shuttles.Systems; using Content.Server.Station.Systems; +using Content.Shared.Antag; +using Content.Shared.Cuffs.Components; using Content.Shared.Database; using Content.Shared.Flash; +using Content.Shared.GameTicking; using Content.Shared.GameTicking.Components; +using Content.Shared.GameTicking.Rules; +using Content.Shared.GameTicking.Rules.Components; using Content.Shared.Humanoid; using Content.Shared.IdentityManagement; using Content.Shared.Mind.Components; +using Content.Shared.Mindshield; using Content.Shared.Mobs; using Content.Shared.Mobs.Components; using Content.Shared.Mobs.Systems; @@ -25,25 +29,20 @@ using Content.Shared.NPC.Systems; using Content.Shared.Revolutionary; using Content.Shared.Revolutionary.Components; using Content.Shared.Roles.Components; +using Content.Shared.RoundEnd; using Content.Shared.Stunnable; +using Robust.Shared.Player; using Robust.Shared.Prototypes; using Robust.Shared.Timing; -using Content.Shared.Cuffs.Components; -using Robust.Shared.Player; -using Content.Shared.Mindshield; namespace Content.Server.GameTicking.Rules; -/// -/// Where all the main stuff for Revolutionaries happens (Assigning Head Revs, Command on station, and checking for the game to end.) -/// -public sealed partial class RevolutionaryRuleSystem : GameRuleSystem +public sealed partial class ServerRevolutionaryRuleSystem : RevolutionaryRuleSystem { [Dependency] private AntagSelectionSystem _antag = default!; [Dependency] private EmergencyShuttleSystem _emergencyShuttle = default!; [Dependency] private EuiManager _euiMan = default!; [Dependency] private IAdminLogManager _adminLogManager = default!; - [Dependency] private IGameTiming _timing = default!; [Dependency] private ISharedPlayerManager _player = default!; [Dependency] private MindSystem _mind = default!; [Dependency] private MobStateSystem _mobState = default!; @@ -52,7 +51,7 @@ public sealed partial class RevolutionaryRuleSystem : GameRuleSystem(OnPostFlash); SubscribeLocalEvent(OnHeadRevMobStateChanged); - SubscribeLocalEvent(OnGetBriefing); - } - protected override void Started(EntityUid uid, RevolutionaryRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args) + protected override void Started(Entity entity, ref GameRuleStartedEvent args) { - base.Started(uid, component, gameRule, args); - component.CommandCheck = _timing.CurTime + component.TimerWait; + base.Started(entity, ref args); + entity.Comp1.CommandCheck = Timing.CurTime + entity.Comp1.TimerWait; } protected override void ActiveTick(EntityUid uid, RevolutionaryRuleComponent component, GameRuleComponent gameRule, float frameTime) { base.ActiveTick(uid, component, gameRule, frameTime); - if (component.CommandCheck <= _timing.CurTime) + if (component.CommandCheck <= Timing.CurTime) { - component.CommandCheck = _timing.CurTime + component.TimerWait; + component.CommandCheck = Timing.CurTime + component.TimerWait; if (CheckCommandLose()) { _roundEnd.DoRoundEndBehavior(RoundEndBehavior.ShuttleCall, component.ShuttleCallTime); - GameTicker.EndGameRule(uid, gameRule); + GameTicker.EndGameRule((uid, gameRule)); } } } - protected override void AppendRoundEndText(EntityUid uid, - RevolutionaryRuleComponent component, - GameRuleComponent gameRule, + protected override void AppendRoundEndText(Entity rule, ref RoundEndTextAppendEvent args) { - base.AppendRoundEndText(uid, component, gameRule, ref args); + base.AppendRoundEndText(rule, ref args); var revsLost = CheckRevsLose(); var commandLost = CheckCommandLose(); @@ -106,7 +101,7 @@ public sealed partial class RevolutionaryRuleSystem : GameRuleSystem(ent); - args.Append(Loc.GetString(head ? "head-rev-briefing" : "rev-briefing")); - } - /// /// Called when a Head Rev uses a flash in melee to convert somebody else. /// diff --git a/Content.Server/GameTicking/Rules/ServerTraitorRuleSystem.cs b/Content.Server/GameTicking/Rules/ServerTraitorRuleSystem.cs new file mode 100644 index 0000000000..da437ab7ee --- /dev/null +++ b/Content.Server/GameTicking/Rules/ServerTraitorRuleSystem.cs @@ -0,0 +1,60 @@ +using Content.Server.Objectives; +using Content.Server.Traitor.Uplink; +using Content.Shared.FixedPoint; +using Content.Shared.PDA; +using Content.Shared.GameTicking.Rules; +using Content.Shared.GameTicking.Rules.Components; + +namespace Content.Server.GameTicking.Rules; + +public sealed partial class ServerTraitorRuleSystem : TraitorRuleSystem +{ + [Dependency] private UplinkSystem _uplink = default!; + + public override void Initialize() + { + base.Initialize(); + + Log.Level = LogLevel.Debug; + } + + protected override (Note[]?, string) RequestUplink(EntityUid traitor, FixedPoint2 startingBalance, string briefing) + { + var pda = _uplink.FindUplinkTarget(traitor); + + Log.Debug($"MakeTraitor {ToPrettyString(traitor)} - Uplink add"); + var uplinked = _uplink.AddUplink(traitor, startingBalance, out var code, pda, giveDiscounts: true, bindToPda: false); + + if (code != null && uplinked == AddUplinkResult.Pda) + { + Log.Debug($"MakeTraitor {ToPrettyString(traitor)} - Uplink is PDA"); + + // If giveUplink is false the uplink code part is omitted + briefing = string.Format("{0}\n{1}", + briefing, + Loc.GetString("traitor-role-uplink-code-short", ("code", string.Join("-", code).Replace("sharp", "#")))); + return (code, briefing); + } + + if (uplinked == AddUplinkResult.Implant) + { + Log.Debug($"MakeTraitor {ToPrettyString(traitor)} - Uplink is implant"); + briefing += "\n" + Loc.GetString("traitor-role-uplink-implant-short"); + } + else + { + Log.Error($"MakeTraitor failed on {ToPrettyString(traitor)} - No uplink could be added"); + } + + + return (null, briefing); + } + + // TODO: AntagCodewordsComponent + [SubscribeLocalEvent] + private void OnObjectivesTextPrepend(EntityUid uid, TraitorRuleComponent comp, ref ObjectivesTextPrependEvent args) + { + if(comp.GiveCodewords) + args.Text += "\n" + Loc.GetString("traitor-round-end-codewords", ("codewords", string.Join(", ", Codeword.GetCodewords(comp.CodewordFactionPrototypeId)))); + } +} diff --git a/Content.Server/GameTicking/Rules/ServerXenoborgsRuleSystem.cs b/Content.Server/GameTicking/Rules/ServerXenoborgsRuleSystem.cs new file mode 100644 index 0000000000..69dce2495e --- /dev/null +++ b/Content.Server/GameTicking/Rules/ServerXenoborgsRuleSystem.cs @@ -0,0 +1,43 @@ +using Content.Server.RoundEnd; +using Content.Shared.GameTicking.Components; +using Content.Shared.GameTicking.Rules; +using Content.Shared.GameTicking.Rules.Components; +using Content.Shared.Objectives.Systems; +using Robust.Shared.Timing; + +namespace Content.Server.GameTicking.Rules; + +public sealed partial class ServerXenoborgsRuleSystem : XenoborgsRuleSystem +{ + [Dependency] private IGameTiming _timing = default!; + [Dependency] private RoundEndSystem _roundEnd = default!; + [Dependency] private AliveHumanoidTargetSystem _target = default!; + + protected override void ActiveTick(EntityUid uid, XenoborgsRuleComponent component, GameRuleComponent gameRule, float frameTime) + { + base.ActiveTick(uid, component, gameRule, frameTime); + + if (!component.NextRoundEndCheck.HasValue || component.NextRoundEndCheck > _timing.CurTime) + return; + + CheckRoundEnd(component); + component.NextRoundEndCheck = _timing.CurTime + component.EndCheckDelay; + } + + private void CheckRoundEnd(XenoborgsRuleComponent xenoborgsRuleComponent) + { + var numXenoborgs = GetNumberXenoborgs(); + var numHumans = _target.GetMinds().Count; + + xenoborgsRuleComponent.MaxNumberXenoborgs = Math.Max(xenoborgsRuleComponent.MaxNumberXenoborgs, numXenoborgs); + + if (xenoborgsRuleComponent.XenoborgShuttleCalled + || (float)numXenoborgs / (numHumans + numXenoborgs) <= xenoborgsRuleComponent.XenoborgShuttleCallPercentage + || _roundEnd.IsRoundEndRequested()) + return; + + GameTicker.StationAnnouncement("xenoborg-shuttle-call", color: Color.BlueViolet); + _roundEnd.RequestRoundEnd(null, null, false, cantRecall: true); + xenoborgsRuleComponent.XenoborgShuttleCalled = true; + } +} diff --git a/Content.Server/GameTicking/Rules/ServerZombieRuleSystem.cs b/Content.Server/GameTicking/Rules/ServerZombieRuleSystem.cs new file mode 100644 index 0000000000..f61d69b6de --- /dev/null +++ b/Content.Server/GameTicking/Rules/ServerZombieRuleSystem.cs @@ -0,0 +1,60 @@ +using Content.Server.Chat.Systems; +using Content.Server.Popups; +using Content.Server.RoundEnd; +using Content.Server.Zombies; +using Content.Shared.GameTicking.Components; +using Content.Shared.GameTicking.Rules; +using Content.Shared.GameTicking.Rules.Components; +using Content.Shared.Zombies; + +namespace Content.Server.GameTicking.Rules; + +public sealed partial class ServerZombieRuleSystem : ZombieRuleSystem +{ + [Dependency] private ChatSystem _chat = default!; + [Dependency] private PopupSystem _popup = default!; + [Dependency] private RoundEndSystem _roundEnd = default!; + [Dependency] private ZombieSystem _zombie = default!; + + protected override void ActiveTick(EntityUid uid, ZombieRuleComponent component, GameRuleComponent gameRule, float frameTime) + { + base.ActiveTick(uid, component, gameRule, frameTime); + if (!component.NextRoundEndCheck.HasValue || component.NextRoundEndCheck > Timing.CurTime) + return; + CheckRoundEnd(component); + component.NextRoundEndCheck = Timing.CurTime + component.EndCheckDelay; + } + + /// + /// The big kahoona function for checking if the round is gonna end + /// + private void CheckRoundEnd(ZombieRuleComponent zombieRuleComponent) + { + var healthy = GetHealthyHumans(); + if (healthy.Count == 1) // Only one human left. spooky + _popup.PopupEntity(Loc.GetString("zombie-alone"), healthy[0], healthy[0]); + + if (GetInfectedFraction(false) > zombieRuleComponent.ZombieShuttleCallPercentage && !_roundEnd.IsRoundEndRequested()) + { + foreach (var station in Station.GetStations()) + { + _chat.DispatchStationAnnouncement(station, Loc.GetString("zombie-shuttle-call"), colorOverride: Color.Crimson); + } + + _roundEnd.DoRoundEndBehavior(zombieRuleComponent.ZombieRoundEndBehavior, zombieRuleComponent.ZombieEvacShuttleTime); + } + + // we include dead for this count because we don't want to end the round + // when everyone gets on the shuttle. + if (GetInfectedFraction() >= 1) // Oops, all zombies + _roundEnd.EndRound(); + } + + [SubscribeLocalEvent] + private void OnZombifySelf(EntityUid uid, IncurableZombieComponent component, ZombifySelfActionEvent args) + { + _zombie.ZombifyEntity(uid); + if (component.Action != null) + Del(component.Action.Value); + } +} diff --git a/Content.Server/GameTicking/Rules/SubGamemodesSystem.cs b/Content.Server/GameTicking/Rules/SubGamemodesSystem.cs index 069a4d5ccc..ff58164a40 100644 --- a/Content.Server/GameTicking/Rules/SubGamemodesSystem.cs +++ b/Content.Server/GameTicking/Rules/SubGamemodesSystem.cs @@ -1,5 +1,6 @@ using Content.Server.GameTicking.Rules.Components; using Content.Shared.GameTicking.Components; +using Content.Shared.GameTicking.Rules; using Content.Shared.Storage; namespace Content.Server.GameTicking.Rules; diff --git a/Content.Server/GameTicking/Rules/VariationPass/EntitySpawnVariationPassSystem.cs b/Content.Server/GameTicking/Rules/VariationPass/EntitySpawnVariationPassSystem.cs index 9014ad0e4d..1e9653de35 100644 --- a/Content.Server/GameTicking/Rules/VariationPass/EntitySpawnVariationPassSystem.cs +++ b/Content.Server/GameTicking/Rules/VariationPass/EntitySpawnVariationPassSystem.cs @@ -16,7 +16,7 @@ public sealed partial class EntitySpawnVariationPassSystem : VariationPassSystem for (var i = 0; i < trashTiles; i++) { - if (!TryFindRandomTileOnStation(args.Station, out _, out _, out var coords)) + if (!Stations.TryFindRandomTileOnStation(args.Station, out _, out _, out var coords)) continue; var ents = EntitySpawnCollection.GetSpawns(ent.Comp.Entities, Random); diff --git a/Content.Server/GameTicking/Rules/VariationPass/PuddleMessVariationPassSystem.cs b/Content.Server/GameTicking/Rules/VariationPass/PuddleMessVariationPassSystem.cs index 360c3378bc..28d880eca2 100644 --- a/Content.Server/GameTicking/Rules/VariationPass/PuddleMessVariationPassSystem.cs +++ b/Content.Server/GameTicking/Rules/VariationPass/PuddleMessVariationPassSystem.cs @@ -23,7 +23,7 @@ public sealed partial class PuddleMessVariationPassSystem : VariationPassSystem< for (var i = 0; i < puddleTiles; i++) { - if (!TryFindRandomTileOnStation(args.Station, out _, out _, out var coords)) + if (!Stations.TryFindRandomTileOnStation(args.Station, out _, out _, out var coords)) continue; var sol = proto.Pick(Random); diff --git a/Content.Server/GameTicking/Rules/VariationPass/VariationPassSystem.cs b/Content.Server/GameTicking/Rules/VariationPass/VariationPassSystem.cs index ff64b168f1..41df08a449 100644 --- a/Content.Server/GameTicking/Rules/VariationPass/VariationPassSystem.cs +++ b/Content.Server/GameTicking/Rules/VariationPass/VariationPassSystem.cs @@ -1,4 +1,5 @@ using Content.Server.Station.Systems; +using Content.Shared.GameTicking.Rules; using Robust.Shared.Random; namespace Content.Server.GameTicking.Rules.VariationPass; @@ -10,7 +11,7 @@ namespace Content.Server.GameTicking.Rules.VariationPass; public abstract partial class VariationPassSystem : GameRuleSystem where T: IComponent { - [Dependency] protected StationSystem Stations = default!; + [Dependency] protected ServerStationSystem Stations = default!; [Dependency] protected IRobustRandom Random = default!; public override void Initialize() diff --git a/Content.Server/GameTicking/ServerGameTicker.CVars.cs b/Content.Server/GameTicking/ServerGameTicker.CVars.cs new file mode 100644 index 0000000000..d147093008 --- /dev/null +++ b/Content.Server/GameTicking/ServerGameTicker.CVars.cs @@ -0,0 +1,101 @@ +using Content.Server.Discord; +using Content.Shared.CCVar; +using Content.Shared.GameTicking; + +namespace Content.Server.GameTicking; + +public sealed partial class ServerGameTicker +{ + [ViewVariables] + public bool LobbyEnabled { get; private set; } + + [ViewVariables] + public bool DummyTicker { get; private set; } = false; + + [ViewVariables] + public TimeSpan LobbyDuration { get; private set; } = TimeSpan.Zero; + + [ViewVariables] + public bool DisallowLateJoin { get; private set; } = false; + + [ViewVariables] + public string? ServerName { get; private set; } + + [ViewVariables] + private string? DiscordRoundEndRole { get; set; } + + private WebhookIdentifier? _webhookIdentifier; + + [ViewVariables] + private string? RoundEndSoundCollection { get; set; } + +#if EXCEPTION_TOLERANCE + [ViewVariables] + public int RoundStartFailShutdownCount { get; private set; } = 0; +#endif + + protected override void InitializeCVars() + { + base.InitializeCVars(); + + Subs.CVar(Cfg, CCVars.ICRandomCharacters, e => _randomizeCharacters = e, true); + Subs.CVar(Cfg, + CCVars.GameLobbyEnabled, + value => + { + LobbyEnabled = value; + foreach (var (userId, status) in _playerGameStatuses) + { + if (status == PlayerGameStatus.JoinedGame) + continue; + _playerGameStatuses[userId] = + LobbyEnabled ? PlayerGameStatus.NotReadyToPlay : PlayerGameStatus.ReadyToPlay; + } + }, + true); + Subs.CVar(Cfg, CCVars.GameDummyTicker, value => DummyTicker = value, true); + Subs.CVar(Cfg, CCVars.GameLobbyDuration, value => LobbyDuration = TimeSpan.FromSeconds(value), true); + Subs.CVar(Cfg, + CCVars.GameDisallowLateJoins, + value => + { + DisallowLateJoin = value; + UpdateLateJoinStatus(); + }, + true); + Subs.CVar(Cfg, + CCVars.AdminLogsServerName, + value => + { + // TODO why tf is the server name on admin logs + ServerName = value; + }, + true); + Subs.CVar(Cfg, + CCVars.DiscordRoundUpdateWebhook, + value => + { + if (!string.IsNullOrWhiteSpace(value)) + { + _discord.GetWebhook(value, data => _webhookIdentifier = data.ToIdentifier()); + } + }, + true); + Subs.CVar(Cfg, + CCVars.DiscordRoundEndRoleWebhook, + value => + { + DiscordRoundEndRole = value; + + if (value == string.Empty) + { + DiscordRoundEndRole = null; + } + }, + true); + Subs.CVar(Cfg, CCVars.RoundEndSoundCollection, value => RoundEndSoundCollection = value, true); +#if EXCEPTION_TOLERANCE + Subs.CVar(Cfg, CCVars.RoundStartFailShutdownCount, value => RoundStartFailShutdownCount = value, true); +#endif + } +} diff --git a/Content.Server/GameTicking/ServerGameTicker.Chat.cs b/Content.Server/GameTicking/ServerGameTicker.Chat.cs new file mode 100644 index 0000000000..712562db85 --- /dev/null +++ b/Content.Server/GameTicking/ServerGameTicker.Chat.cs @@ -0,0 +1,40 @@ +using Content.Shared.Station.Components; +using Robust.Shared.Audio; +using Robust.Shared.Map; +using Robust.Shared.Player; + +namespace Content.Server.GameTicking; + +public sealed partial class ServerGameTicker +{ + public override void GameAnnouncement(LocId announcement, SoundSpecifier? announcementSound = null, Color? color = null) + { + var filter = Filter.Empty().AddWhere(UserHasJoinedGame); + Chat.DispatchFilteredAnnouncement(filter, Loc.GetString(announcement), announcementSound: announcementSound, colorOverride: color); + } + + public override void MapAnnouncement(MapId map, LocId announcement, SoundSpecifier? announcementSound = null, Color? color = null) + { + var filter = Filter.Empty().AddInMap(map); + Chat.DispatchFilteredAnnouncement(filter, Loc.GetString(announcement), announcementSound: announcementSound, colorOverride: color); + } + + public override void StationMapAnnouncement(Entity station, LocId announcement, SoundSpecifier? announcementSound = null, Color? color = null) + { + var filter = Station.GetOnMap(station.Comp); + Chat.DispatchFilteredAnnouncement(filter, Loc.GetString(announcement), announcementSound: announcementSound, colorOverride: color); + } + + public override void StationAnnouncement(LocId announcement, SoundSpecifier? announcementSound = null, Color? color = null) + { + foreach (var station in Station.GetStations()) + { + StationAnnouncement(station, announcement, announcementSound, color); + } + } + + public override void StationAnnouncement(EntityUid station, LocId announcement, SoundSpecifier? announcementSound = null, Color? color = null) + { + _chatSystem.DispatchStationAnnouncement(station, Loc.GetString(announcement), announcementSound: announcementSound, colorOverride: color); + } +} diff --git a/Content.Server/GameTicking/GameTicker.GamePreset.cs b/Content.Server/GameTicking/ServerGameTicker.GamePreset.cs similarity index 75% rename from Content.Server/GameTicking/GameTicker.GamePreset.cs rename to Content.Server/GameTicking/ServerGameTicker.GamePreset.cs index 9a473cd4e6..ac3f754f36 100644 --- a/Content.Server/GameTicking/GameTicker.GamePreset.cs +++ b/Content.Server/GameTicking/ServerGameTicker.GamePreset.cs @@ -1,18 +1,14 @@ using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Threading.Tasks; -using Content.Server.GameTicking.Presets; -using Content.Server.Maps; using Content.Shared.CCVar; -using Content.Shared.GameTicking.Components; +using Content.Shared.GameTicking.Prototypes; using Content.Shared.Maps; -using JetBrains.Annotations; using Robust.Shared.Player; -using Robust.Shared.Prototypes; namespace Content.Server.GameTicking; -public sealed partial class GameTicker +public sealed partial class ServerGameTicker { public const float PresetFailedCooldownIncrease = 30f; @@ -38,7 +34,7 @@ public sealed partial class GameTicker private bool StartPreset(ICommonSession[] origReadyPlayers, bool force) { - _sawmill.Info($"Attempting to start preset '{CurrentPreset?.ID}'"); + Log.Info($"Attempting to start preset '{CurrentPreset?.ID}'"); var startAttempt = new RoundStartAttemptEvent(origReadyPlayers, force); RaiseLocalEvent(startAttempt); @@ -55,17 +51,17 @@ public sealed partial class GameTicker DelayStart(TimeSpan.FromSeconds(PresetFailedCooldownIncrease)); } - if (_cfg.GetCVar(CCVars.GameLobbyFallbackEnabled)) + if (Cfg.GetCVar(CCVars.GameLobbyFallbackEnabled)) { - var fallbackPresets = _cfg.GetCVar(CCVars.GameLobbyFallbackPreset).Split(","); + var fallbackPresets = Cfg.GetCVar(CCVars.GameLobbyFallbackPreset).Split(","); var startFailed = true; - _sawmill.Info($"Fallback - Failed to start round, attempting to start fallback presets."); + Log.Info($"Fallback - Failed to start round, attempting to start fallback presets."); foreach (var preset in fallbackPresets) { - _sawmill.Info($"Fallback - Clearing up gamerules"); + Log.Info($"Fallback - Clearing up gamerules"); ClearGameRules(); - _sawmill.Info($"Fallback - Attempting to start '{preset}'"); + Log.Info($"Fallback - Attempting to start '{preset}'"); SetGamePreset(preset, resetDelay: 1); AddGamePresetRules(); StartGamePresetRules(); @@ -83,7 +79,7 @@ public sealed partial class GameTicker startFailed = false; break; } - _sawmill.Info($"Fallback - '{preset}' failed to start."); + Log.Info($"Fallback - '{preset}' failed to start."); } if (startFailed) @@ -95,7 +91,7 @@ public sealed partial class GameTicker else { - _sawmill.Info($"Fallback - Failed to start preset but fallbacks are disabled. Returning to Lobby."); + Log.Info($"Fallback - Failed to start preset but fallbacks are disabled. Returning to Lobby."); FailedPresetRestart(); return false; } @@ -105,7 +101,7 @@ public sealed partial class GameTicker private void InitializeGamePreset() { - SetGamePreset(LobbyEnabled ? _cfg.GetCVar(CCVars.GameLobbyDefaultPreset) : "sandbox"); + SetGamePreset(LobbyEnabled ? Cfg.GetCVar(CCVars.GameLobbyDefaultPreset) : "sandbox"); } public void SetGamePreset(GamePresetPrototype? preset, bool force = false, GamePresetPrototype? decoy = null, int? resetDelay = null) @@ -203,7 +199,7 @@ public sealed partial class GameTicker CurrentPreset = Preset; foreach (var rule in Preset.Rules) { - AddFilteredGameRule(rule); + AddGameRule(rule); } return true; @@ -228,44 +224,9 @@ public sealed partial class GameTicker } } - /// - [PublicAPI] - public int GetMinimumPlayerCount(ProtoId proto) - { - if (!ProtoMan.Resolve(proto, out var preset)) - return 0; - - return GetMinimumPlayerCount(preset); - } - - /// - /// Gets the minimum number of players required for a game preset to start. - /// Checks both the preset itself, and all rules to find the minimum. - /// - /// Game preset prototype we're checking. - /// Minimum number of players required for the rule to start. - [PublicAPI] - public int GetMinimumPlayerCount(GamePresetPrototype proto) - { - var min = proto.MinPlayers ?? 0; - foreach (var entProto in proto.Rules) - { - if (!ProtoMan.Resolve(entProto, out var ent)) - continue; - - if (!ent.TryComp(out var rule, Factory)) - continue; - - min = Math.Max(min, rule.MinPlayers); - } - - return min; - } - private void IncrementRoundNumber() { var playerIds = _playerGameStatuses.Keys.Select(player => player.UserId).ToArray(); - var serverName = _cfg.GetCVar(CCVars.AdminLogsServerName); // TODO FIXME AAAAAAAAAAAAAAAAAAAH THIS IS BROKEN // Task.Run as a terrible dirty workaround to avoid synchronization context deadlock from .Result here. diff --git a/Content.Server/GameTicking/ServerGameTicker.GameRule.cs b/Content.Server/GameTicking/ServerGameTicker.GameRule.cs new file mode 100644 index 0000000000..32f7d8afdc --- /dev/null +++ b/Content.Server/GameTicking/ServerGameTicker.GameRule.cs @@ -0,0 +1,240 @@ +using System.Linq; +using System.Text; +using Content.Server.Administration; +using Content.Shared.Administration; +using Content.Shared.Database; +using Content.Shared.GameTicking; +using Content.Shared.GameTicking.Components; +using Robust.Shared.Console; +using Robust.Shared.Prototypes; + +namespace Content.Server.GameTicking; + +public sealed partial class ServerGameTicker +{ + private void InitializeGameRules() + { + // Add game rule command. + _consoleHost.RegisterCommand("addgamerule", + string.Empty, + "addgamerule ", + AddGameRuleCommand, + AddGameRuleCompletions); + + // End game rule command. + _consoleHost.RegisterCommand("endgamerule", + string.Empty, + "endgamerule ", + EndGameRuleCommand, + EndGameRuleCompletions); + + // Clear game rules command. + _consoleHost.RegisterCommand("cleargamerules", + string.Empty, + "cleargamerules", + ClearGameRulesCommand); + + // List game rules command. + var localizedHelp = Loc.GetString("listgamerules-command-help"); + + _consoleHost.RegisterCommand("listgamerules", + string.Empty, + $"listgamerules - {localizedHelp}", + ListGameRuleCommand); + + SubscribeLocalEvent(OnStartAttempt); + } + + private void ShutdownGameRules() + { + _consoleHost.UnregisterCommand("addgamerule"); + _consoleHost.UnregisterCommand("endgamerule"); + _consoleHost.UnregisterCommand("cleargamerules"); + _consoleHost.UnregisterCommand("listgamerules"); + } + + /// + /// Adds a game rule to the list, but does not + /// start it yet, instead waiting until the rule is actually started by other code (usually roundstart) + /// + /// The entity for the added gamerule + protected override Entity SpawnGameRule(EntProtoId ruleId) + { + var ruleEntity = base.SpawnGameRule(ruleId); + var str = Loc.GetString("station-event-system-run-event", ("eventName", ToPrettyString(ruleEntity))); +#if DEBUG + _chatManager.SendAdminAlert(str); +#else + if (RunLevel == GameRunLevel.InRound) // avoids telling admins the round type before it starts so that can be handled elsewhere. + _chatManager.SendAdminAlert(str); +#endif + Log.Info(str); + + return ruleEntity; + } + + public void ClearGameRules() + { + foreach (var rule in GetAddedGameRules()) + { + EndGameRule(rule); + } + } + + private void UpdateGameRules() + { + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var delay, out var rule)) + { + if (Timing.CurTime < delay.RuleStartTime) + continue; + + StartGameRule((uid, rule)); + } + } + + private void OnStartAttempt(RoundStartAttemptEvent args) + { + if (args.Forced || args.Cancelled) + return; + + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var gameRule)) + { + var minPlayers = gameRule.MinPlayers; + var name = ToPrettyString(uid); + + if (args.Players.Length >= minPlayers) + continue; + + if (gameRule.CancelPresetOnTooFewPlayers) + { + _chatManager.SendAdminAnnouncement(Loc.GetString("preset-not-enough-ready-players", + ("readyPlayersCount", args.Players.Length), + ("minimumPlayers", minPlayers), + ("presetName", name))); + args.Cancel(); + //TODO remove this once announcements are logged + Log.Info($"Rule '{name}' requires {minPlayers} players, but only {args.Players.Length} are ready."); + } + else + { + EndGameRule((uid, gameRule)); + } + } + } + + #region Command Implementations + + [AdminCommand(AdminFlags.Fun)] + private void AddGameRuleCommand(IConsoleShell shell, string argstr, string[] args) + { + if (args.Length == 0) + return; + + foreach (var rule in args) + { + if (!ProtoMan.HasIndex(rule)) + { + shell.WriteError($"Invalid game rule {rule} was skipped."); + + continue; + } + + if (shell.Player != null) + { + Admin.Add(LogType.EventStarted, $"{shell.Player} tried to add game rule [{rule}] via command"); + _chatManager.SendAdminAnnouncement(Loc.GetString("add-gamerule-admin", ("rule", rule), ("admin", shell.Player))); + } + else + { + Admin.Add(LogType.EventStarted, $"Unknown tried to add game rule [{rule}] via command"); + } + var ent = SpawnGameRule(rule); + + // Start rule if we're already in the middle of a round + // TODO: DO WE EVEN NEED THIS CHECK??? + if (RunLevel == GameRunLevel.InRound) + StartGameRule(ent.AsNullable()); + } + } + + private CompletionResult AddGameRuleCompletions(IConsoleShell shell, string[] args) + { + return CompletionResult.FromHintOptions(GetAllGameRulePrototypes().Select(p => p.ID), ""); + } + + [AdminCommand(AdminFlags.Fun)] + private void EndGameRuleCommand(IConsoleShell shell, string argstr, string[] args) + { + if (args.Length == 0) + return; + + foreach (var rule in args) + { + if (!NetEntity.TryParse(rule, out var ruleEntNet) || !TryGetEntity(ruleEntNet, out var ruleEnt)) + continue; + if (shell.Player != null) + { + Admin.Add(LogType.EventStopped, $"{shell.Player} tried to end game rule [{rule}] via command"); + } + else + { + Admin.Add(LogType.EventStopped, $"Unknown tried to end game rule [{rule}] via command"); + } + + EndGameRule(ruleEnt.Value); + } + } + + private CompletionResult EndGameRuleCompletions(IConsoleShell shell, string[] args) + { + var opts = GetAddedGameRules().Select(ent => new CompletionOption(ent.ToString(), ToPrettyString(ent))).ToList(); + return CompletionResult.FromHintOptions(opts, ""); + } + + [AdminCommand(AdminFlags.Fun)] + private void ClearGameRulesCommand(IConsoleShell shell, string argstr, string[] args) + { + ClearGameRules(); + } + + [AdminCommand(AdminFlags.Admin)] + private void ListGameRuleCommand(IConsoleShell shell, string argstr, string[] args) + { + Log.Info($"{shell.Player} tried to get list of game rules via command"); + Admin.Add(LogType.Action, $"{shell.Player} tried to get list of game rules via command"); + var message = GetGameRulesListMessage(false); + shell.WriteLine(message); + } + + private string GetGameRulesListMessage(bool forChatWindow) + { + if (AllRoundGameRules.Count > 0) + { + var message = new StringBuilder(); + message.AppendLine(); + + if (!forChatWindow) + { + var header = Loc.GetString("list-gamerule-admin-header"); + message.AppendLine(); + message.AppendLine(header); + message.AppendLine("|------------|---------------------------"); + } + + foreach (var (time, rule, stage) in AllRoundGameRules) + { + var formattedTime = time.ToString(@"hh\:mm\:ss"); + var name = RuleToString(rule, stage); + message.AppendLine($"| {formattedTime,-10} | {name,-24} "); + } + + return message.ToString().TrimEnd('\n'); + } + + return Loc.GetString("list-gamerule-admin-no-rules"); + } + + #endregion +} diff --git a/Content.Server/GameTicking/ServerGameTicker.Lobby.cs b/Content.Server/GameTicking/ServerGameTicker.Lobby.cs new file mode 100644 index 0000000000..7098562198 --- /dev/null +++ b/Content.Server/GameTicking/ServerGameTicker.Lobby.cs @@ -0,0 +1,187 @@ +using System.Linq; +using Content.Shared.GameTicking; +using Robust.Shared.Network; +using Robust.Shared.Player; +using System.Text; +using Content.Shared.Station.Components; + +namespace Content.Server.GameTicking; + +public sealed partial class ServerGameTicker +{ + [ViewVariables] + private readonly Dictionary _playerGameStatuses = new(); + + [ViewVariables] + private TimeSpan _roundStartTime; + + /// + /// How long before RoundStartTime do we load maps. + /// + [ViewVariables] + public TimeSpan RoundPreloadTime { get; } = TimeSpan.FromSeconds(15); + + [ViewVariables] + private TimeSpan _pauseTime; + + [ViewVariables] + public new bool Paused { get; set; } + + [ViewVariables] + private bool _roundStartCountdownHasNotStartedYetDueToNoPlayers; + + /// + /// The game status of a players user Id. May contain disconnected players + /// + public IReadOnlyDictionary PlayerGameStatuses => _playerGameStatuses; + + public void UpdateInfoText() + { + RaiseNetworkEvent(GetInfoMsg(), Filter.Empty().AddPlayers(_playerManager.NetworkedSessions)); + } + + // TODO: You can move this to shared. + // TODO: You *should* move this to shared >:) + // TODO: If you are reading this, move this to shared! + private string GetInfoText() + { + var preset = CurrentPreset ?? Preset; + if (preset == null) + { + return string.Empty; + } + + var playerCount = $"{_playerManager.PlayerCount}"; + var readyCount = _playerGameStatuses.Values.Count(x => x == PlayerGameStatus.ReadyToPlay); + + var stationNames = new StringBuilder(); + var query = + EntityQueryEnumerator(); + + var foundOne = false; + + while (query.MoveNext(out _, out _, out var meta)) + { + foundOne = true; + if (stationNames.Length > 0) + stationNames.Append('\n'); + + stationNames.Append(meta.EntityName); + } + + if (!foundOne) + { + stationNames.Append(_gameMapManager.GetSelectedMap()?.MapName ?? + Loc.GetString("game-ticker-no-map-selected")); + } + + var gmTitle = (Decoy == null) ? Loc.GetString(preset.ModeTitle) : Loc.GetString(Decoy.ModeTitle); + var desc = (Decoy == null) ? Loc.GetString(preset.Description) : Loc.GetString(Decoy.Description); + return Loc.GetString( + RunLevel == GameRunLevel.PreRoundLobby + ? "game-ticker-get-info-preround-text" + : "game-ticker-get-info-text", + ("roundId", RoundId), + ("playerCount", playerCount), + ("readyCount", readyCount), + ("mapName", stationNames.ToString()), + ("gmTitle", gmTitle), + ("desc", desc)); + } + + private TickerConnectionStatusEvent GetConnectionStatusMsg() + { + return new TickerConnectionStatusEvent(RoundStartTimeSpan); + } + + private TickerLobbyStatusEvent GetStatusMsg(ICommonSession session) + { + _playerGameStatuses.TryGetValue(session.UserId, out var status); + return new TickerLobbyStatusEvent(RunLevel != GameRunLevel.PreRoundLobby, LobbyBackground, status == PlayerGameStatus.ReadyToPlay, _roundStartTime, RoundPreloadTime, RoundStartTimeSpan, Paused); + } + + private void SendStatusToAll() + { + foreach (var player in _playerManager.Sessions) + { + RaiseNetworkEvent(GetStatusMsg(player), player.Channel); + } + } + + 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 = Timing.CurTime; + } + else if (_pauseTime != default) + { + _roundStartTime += Timing.CurTime - _pauseTime; + } + + RaiseNetworkEvent(new TickerLobbyCountdownEvent(_roundStartTime, Paused)); + + _chatManager.DispatchServerAnnouncement(Loc.GetString(Paused + ? "game-ticker-pause-start" + : "game-ticker-pause-start-resumed")); + + return true; + } + + public bool TogglePause() + { + PauseStart(!Paused); + return Paused; + } + + public void ToggleReadyAll(bool ready) + { + var status = ready ? PlayerGameStatus.ReadyToPlay : PlayerGameStatus.NotReadyToPlay; + foreach (var playerUserId in _playerGameStatuses.Keys) + { + _playerGameStatuses[playerUserId] = status; + if (!_playerManager.TryGetSessionById(playerUserId, out var playerSession)) + continue; + RaiseNetworkEvent(GetStatusMsg(playerSession), playerSession.Channel); + } + } + + public void ToggleReady(ICommonSession player, bool ready) + { + if (!_playerGameStatuses.ContainsKey(player.UserId)) + return; + + if (!_userDb.IsLoadComplete(player)) + return; + + if (RunLevel != GameRunLevel.PreRoundLobby) + { + return; + } + + _playerGameStatuses[player.UserId] = ready ? PlayerGameStatus.ReadyToPlay : PlayerGameStatus.NotReadyToPlay; + RaiseNetworkEvent(GetStatusMsg(player), player.Channel); + // update server info to reflect new ready count + UpdateInfoText(); + } + + public override bool UserHasJoinedGame(NetUserId userId) + => PlayerGameStatuses.TryGetValue(userId, out var status) && status == PlayerGameStatus.JoinedGame; +} diff --git a/Content.Server/GameTicking/GameTicker.LobbyBackground.cs b/Content.Server/GameTicking/ServerGameTicker.LobbyBackground.cs similarity index 92% rename from Content.Server/GameTicking/GameTicker.LobbyBackground.cs rename to Content.Server/GameTicking/ServerGameTicker.LobbyBackground.cs index bf522919a3..21b50b1bf3 100644 --- a/Content.Server/GameTicking/GameTicker.LobbyBackground.cs +++ b/Content.Server/GameTicking/ServerGameTicker.LobbyBackground.cs @@ -5,7 +5,7 @@ using System.Linq; namespace Content.Server.GameTicking; -public sealed partial class GameTicker +public sealed partial class ServerGameTicker { [ViewVariables] public ProtoId? LobbyBackground { get; private set; } @@ -37,7 +37,7 @@ public sealed partial class GameTicker private void RandomizeLobbyBackground() { if (_lobbyBackgrounds != null && _lobbyBackgrounds.Count != 0) - LobbyBackground = _robustRandom.Pick(_lobbyBackgrounds); + LobbyBackground = Random.Pick(_lobbyBackgrounds); else LobbyBackground = null; } diff --git a/Content.Server/GameTicking/GameTicker.Player.cs b/Content.Server/GameTicking/ServerGameTicker.Player.cs similarity index 85% rename from Content.Server/GameTicking/GameTicker.Player.cs rename to Content.Server/GameTicking/ServerGameTicker.Player.cs index 1ccfed4a88..5f99a02c12 100644 --- a/Content.Server/GameTicking/GameTicker.Player.cs +++ b/Content.Server/GameTicking/ServerGameTicker.Player.cs @@ -16,7 +16,7 @@ using Robust.Shared.Utility; namespace Content.Server.GameTicking { [UsedImplicitly] - public sealed partial class GameTicker + public sealed partial class ServerGameTicker { [Dependency] private IPlayerManager _playerManager = default!; @@ -29,7 +29,7 @@ namespace Content.Server.GameTicking { var session = args.Session; - if (_mind.TryGetMind(session.UserId, out var mindId, out var mind)) + if (Mind.TryGetMind(session.UserId, out var mindId, out var mind)) { if (args.NewStatus != SessionStatus.Disconnected) { @@ -67,15 +67,15 @@ namespace Content.Server.GameTicking RaiseNetworkEvent(GetConnectionStatusMsg(), session.Channel); - if (firstConnection && _cfg.GetCVar(CCVars.AdminNewPlayerJoinSound)) - _audio.PlayGlobal(new SoundPathSpecifier("/Audio/Effects/newplayerping.ogg"), + if (firstConnection && Cfg.GetCVar(CCVars.AdminNewPlayerJoinSound)) + Audio.PlayGlobal(new SoundPathSpecifier("/Audio/Effects/newplayerping.ogg"), Filter.Empty().AddPlayers(_adminManager.ActiveAdmins), false, audioParams: new AudioParams { Volume = -5f }); if (LobbyEnabled && _roundStartCountdownHasNotStartedYetDueToNoPlayers) { _roundStartCountdownHasNotStartedYetDueToNoPlayers = false; - _roundStartTime = _gameTiming.CurTime + LobbyDuration; + _roundStartTime = Timing.CurTime + LobbyDuration; } break; @@ -92,7 +92,7 @@ namespace Content.Server.GameTicking else SpawnWaitDb(); - _adminLogger.Add(LogType.Connection, LogImpact.Low, $"User {args.Session:Player} attached to {(args.Session.AttachedEntity != null ? ToPrettyString(args.Session.AttachedEntity) : "nothing"):entity} connected to the game."); + Admin.Add(LogType.Connection, LogImpact.Low, $"User {args.Session:Player} attached to {(args.Session.AttachedEntity != null ? ToPrettyString(args.Session.AttachedEntity) : "nothing"):entity} connected to the game."); break; } @@ -119,7 +119,7 @@ namespace Content.Server.GameTicking } } - _adminLogger.Add(LogType.Connection, LogImpact.Low, $"User {args.Session:Player} attached to {(args.Session.AttachedEntity != null ? ToPrettyString(args.Session.AttachedEntity) : "nothing"):entity} connected to the game."); + Admin.Add(LogType.Connection, LogImpact.Low, $"User {args.Session:Player} attached to {(args.Session.AttachedEntity != null ? ToPrettyString(args.Session.AttachedEntity) : "nothing"):entity} connected to the game."); break; } @@ -134,7 +134,7 @@ namespace Content.Server.GameTicking _userDb.ClientDisconnected(session); - _adminLogger.Add(LogType.Connection, LogImpact.Low, $"User {args.Session:Player} attached to {(args.Session.AttachedEntity != null ? ToPrettyString(args.Session.AttachedEntity) : "nothing"):entity} disconnected from the game."); + Admin.Add(LogType.Connection, LogImpact.Low, $"User {args.Session:Player} attached to {(args.Session.AttachedEntity != null ? ToPrettyString(args.Session.AttachedEntity) : "nothing"):entity} disconnected from the game."); break; } } @@ -175,7 +175,7 @@ namespace Content.Server.GameTicking async void AddPlayerToDb(Guid id) { - if (RoundId != 0 && _runLevel != GameRunLevel.PreRoundLobby) + if (RoundId != 0 && RunLevel != GameRunLevel.PreRoundLobby) { await _db.AddRoundPlayers(RoundId, id); } @@ -187,7 +187,7 @@ namespace Content.Server.GameTicking return (HumanoidCharacterProfile) _prefsManager.GetPreferences(p.UserId).SelectedCharacter; } - public void PlayerJoinGame(ICommonSession session, bool silent = false) + public override void PlayerJoinGame(ICommonSession session, bool silent = false) { if (!silent) _chatManager.DispatchServerMessage(session, Loc.GetString("game-ticker-player-join-game-message")); @@ -197,13 +197,15 @@ namespace Content.Server.GameTicking if (_adminManager.HasAdminFlag(session, AdminFlags.Admin)) { - if (_allPreviousGameRules.Count > 0) + if (AllRoundGameRules.Count > 0) { + // TODO: Try version of this method var rulesMessage = GetGameRulesListMessage(true); _chatManager.SendAdminAnnouncementMessage(session, Loc.GetString("starting-rule-selected-preset", ("preset", rulesMessage))); } } + // TODO: We may not need to network this anymore >:D RaiseNetworkEvent(new TickerJoinGameEvent(), session.Channel); } diff --git a/Content.Server/GameTicking/GameTicker.Replays.cs b/Content.Server/GameTicking/ServerGameTicker.Replays.cs similarity index 89% rename from Content.Server/GameTicking/GameTicker.Replays.cs rename to Content.Server/GameTicking/ServerGameTicker.Replays.cs index 68b391f8e4..36f71f0869 100644 --- a/Content.Server/GameTicking/GameTicker.Replays.cs +++ b/Content.Server/GameTicking/ServerGameTicker.Replays.cs @@ -3,14 +3,13 @@ using Robust.Shared; using Robust.Shared.ContentPack; using Robust.Shared.Replays; using Robust.Shared.Serialization.Manager; -using Robust.Shared.Serialization.Markdown; using Robust.Shared.Serialization.Markdown.Mapping; using Robust.Shared.Serialization.Markdown.Value; using Robust.Shared.Utility; namespace Content.Server.GameTicking; -public sealed partial class GameTicker +public sealed partial class ServerGameTicker { [Dependency] private IReplayRecordingManager _replays = default!; [Dependency] private IResourceManager _resourceManager = default!; @@ -32,7 +31,7 @@ public sealed partial class GameTicker { try { - if (!_cfg.GetCVar(CCVars.ReplayAutoRecord)) + if (!Cfg.GetCVar(CCVars.ReplayAutoRecord)) return; if (_replays.IsRecording) @@ -45,7 +44,7 @@ public sealed partial class GameTicker var finalPath = GetAutoReplayPath(); var recordPath = finalPath; - var tempDir = _cfg.GetCVar(CCVars.ReplayAutoRecordTempDir); + var tempDir = Cfg.GetCVar(CCVars.ReplayAutoRecordTempDir); ResPath? moveToPath = null; // Set the round end player and text back to null to prevent it from writing the previous round's data. @@ -54,7 +53,7 @@ public sealed partial class GameTicker if (!string.IsNullOrEmpty(tempDir)) { - var baseReplayPath = new ResPath(_cfg.GetCVar(CVars.ReplayDirectory)).ToRootedPath(); + var baseReplayPath = new ResPath(Cfg.GetCVar(CVars.ReplayDirectory)).ToRootedPath(); moveToPath = baseReplayPath / finalPath; var fileName = finalPath.Filename; @@ -127,14 +126,14 @@ public sealed partial class GameTicker metadata["gamemode"] = new ValueDataNode(CurrentPreset != null ? Loc.GetString(CurrentPreset.ModeTitle) : string.Empty); metadata["roundEndPlayers"] = _serialman.WriteValue(_replayRoundPlayerInfo); metadata["roundEndText"] = new ValueDataNode(_replayRoundText); - metadata["server_id"] = new ValueDataNode(_cfg.GetCVar(CCVars.ServerId)); - metadata["server_name"] = new ValueDataNode(_cfg.GetCVar(CCVars.AdminLogsServerName)); + metadata["server_id"] = new ValueDataNode(Cfg.GetCVar(CCVars.ServerId)); + metadata["server_name"] = new ValueDataNode(ServerName); metadata["roundId"] = new ValueDataNode(RoundId.ToString()); } private ResPath GetAutoReplayPath() { - var cfgValue = _cfg.GetCVar(CCVars.ReplayAutoRecordName); + var cfgValue = Cfg.GetCVar(CCVars.ReplayAutoRecordName); var time = DateTime.UtcNow; diff --git a/Content.Server/GameTicking/ServerGameTicker.RoundFlow.cs b/Content.Server/GameTicking/ServerGameTicker.RoundFlow.cs new file mode 100644 index 0000000000..981ea1ab9c --- /dev/null +++ b/Content.Server/GameTicking/ServerGameTicker.RoundFlow.cs @@ -0,0 +1,878 @@ +using System.Linq; +using System.Numerics; +using Content.Server.Announcements; +using Content.Server.Discord; +using Content.Server.GameTicking.Events; +using Content.Server.Roles; +using Content.Shared.CCVar; +using Content.Shared.Database; +using Content.Shared.GameTicking; +using Content.Shared.GameTicking.Events; +using Content.Shared.Maps; +using Content.Shared.Mind; +using Content.Shared.Players; +using Content.Shared.Preferences; +using Content.Shared.Roles.Components; +using JetBrains.Annotations; +using Prometheus; +using Robust.Shared.Asynchronous; +using Robust.Shared.Audio; +using Robust.Shared.EntitySerialization; +using Robust.Shared.Map; +using Robust.Shared.Network; +using Robust.Shared.Player; +using Robust.Shared.Random; +using Robust.Shared.Utility; + +namespace Content.Server.GameTicking; + +public sealed partial class ServerGameTicker +{ + [Dependency] private DiscordWebhook _discord = default!; + [Dependency] private RoleSystem _role = default!; + [Dependency] private ITaskManager _taskManager = default!; + + 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."); + +#if EXCEPTION_TOLERANCE + [ViewVariables] + private int _roundStartFailCount = 0; +#endif + + [ViewVariables] + private bool _startingRound; + + private RoundEndMessageEvent.RoundEndPlayerInfo[]? _replayRoundPlayerInfo; + + private string? _replayRoundText; + + /// + /// Returns true if the round's map is eligible to be updated. + /// + /// + public bool CanUpdateMap() + { + return RunLevel == GameRunLevel.PreRoundLobby && + _roundStartTime - RoundPreloadTime > Timing.CurTime; + } + + /// + /// Loads all the maps for the given round. + /// + /// + /// Must be called before the runlevel is set to InRound. + /// + private void LoadMaps() + { + if (Map.MapExists(DefaultMap)) + return; + + AddGamePresetRules(); + + var maps = new List(); + + // the map might have been force-set by something + // (i.e. votemap or forcemap) + var mainStationMap = _gameMapManager.GetSelectedMap(); + if (mainStationMap == null) + { + // otherwise set the map using the config rules + _gameMapManager.SelectMapByConfigRules(); + mainStationMap = _gameMapManager.GetSelectedMap(); + } + + // Small chance the above could return no map. + // ideally SelectMapByConfigRules will always find a valid map + if (mainStationMap != null) + { + maps.Add(mainStationMap); + } + else + { + throw new Exception("invalid config; couldn't select a valid station map!"); + } + + if (CurrentPreset?.MapPool != null && + ProtoMan.TryIndex(CurrentPreset.MapPool, out var pool) && + !pool.Maps.Contains(mainStationMap.ID)) + { + var msg = Loc.GetString("game-ticker-start-round-invalid-map", + ("map", mainStationMap.MapName), + ("mode", Loc.GetString(CurrentPreset.ModeTitle))); + Log.Debug(msg); + SendServerMessage(msg); + } + + // Let game rules dictate what maps we should load. + RaiseLocalEvent(new LoadingMapsEvent(maps)); + + if (maps.Count == 0) + { + Map.CreateMap(out var mapId, runMapInit: false); + DefaultMap = mapId; + return; + } + + for (var i = 0; i < maps.Count; i++) + { + LoadGameMap(maps[i], out var mapId); + DebugTools.Assert(!Map.IsInitialized(mapId)); + + if (i == 0) + DefaultMap = mapId; + } + } + + public PreGameMapLoad RaisePreLoad( + GameMapPrototype proto, + DeserializationOptions? opts = null, + Vector2? offset = null, + Angle? rot = null) + { + offset ??= proto.MaxRandomOffset != 0f + ? Random.NextVector2(proto.MaxRandomOffset) + : Vector2.Zero; + + rot ??= proto.RandomRotation + ? Random.NextAngle() + : Angle.Zero; + + opts ??= DeserializationOptions.Default; + var ev = new PreGameMapLoad(proto, opts.Value, offset.Value, rot.Value); + RaiseLocalEvent(ev); + return ev; + } + + public override IReadOnlyList LoadGameMap( + GameMapPrototype proto, + out MapId mapId, + DeserializationOptions? options = null, + string? stationName = null, + Vector2? offset = null, + Angle? rot = null) + { + var ev = RaisePreLoad(proto, options, offset, rot); + + if (ev.GameMap.IsGrid) + { + var mapUid = Map.CreateMap(out mapId, runMapInit: options?.InitializeMaps ?? false); + if (!_loader.TryLoadGrid(mapId, + ev.GameMap.MapPath, + out var grid, + ev.Options, + ev.Offset, + ev.Rotation)) + { + throw new Exception($"Failed to load game-map grid {ev.GameMap.ID}"); + } + + Meta.SetEntityName(mapUid, proto.MapName); + var g = new List { grid.Value.Owner }; + RaiseLocalEvent(new PostGameMapLoad(proto, mapId, g, stationName)); + return g; + } + + if (!_loader.TryLoadMap(ev.GameMap.MapPath, + out var map, + out var grids, + ev.Options, + ev.Offset, + ev.Rotation)) + { + throw new Exception($"Failed to load game map {ev.GameMap.ID}"); + } + + mapId = map.Value.Comp.MapId; + Meta.SetEntityName(map.Value.Owner, proto.MapName); + var gridUids = grids.Select(x => x.Owner).ToList(); + RaiseLocalEvent(new PostGameMapLoad(proto, mapId, gridUids, stationName)); + return gridUids; + } + + /// + /// Variant of that attempts to assign the provided to the + /// loaded map. + /// + public IReadOnlyList LoadGameMapWithId( + GameMapPrototype proto, + MapId mapId, + DeserializationOptions? opts = null, + string? stationName = null, + Vector2? offset = null, + Angle? rot = null) + { + var ev = RaisePreLoad(proto, opts, offset, rot); + + if (ev.GameMap.IsGrid) + { + var mapUid = Map.CreateMap(mapId); + if (!_loader.TryLoadGrid(mapId, + ev.GameMap.MapPath, + out var grid, + ev.Options, + ev.Offset, + ev.Rotation)) + { + throw new Exception($"Failed to load game-map grid {ev.GameMap.ID}"); + } + + Meta.SetEntityName(mapUid, proto.MapName); + var g = new List { grid.Value.Owner }; + RaiseLocalEvent(new PostGameMapLoad(proto, mapId, g, stationName)); + return g; + } + + if (!_loader.TryLoadMapWithId( + mapId, + ev.GameMap.MapPath, + out var map, + out var grids, + ev.Options, + ev.Offset, + ev.Rotation)) + { + throw new Exception($"Failed to load map"); + } + + Meta.SetEntityName(map.Value.Owner, proto.MapName); + var gridUids = grids.Select(x => x.Owner).ToList(); + RaiseLocalEvent(new PostGameMapLoad(proto, mapId, gridUids, stationName)); + return gridUids; + } + + /// + /// Variant of that loads and then merges a game map onto an existing map. + /// + public IReadOnlyList MergeGameMap( + GameMapPrototype proto, + MapId targetMap, + DeserializationOptions? opts = null, + string? stationName = null, + Vector2? offset = null, + Angle? rot = null) + { + // TODO MAP LOADING use a new event? + // This is quite different from the other methods, which will actually create a **new** map. + var ev = RaisePreLoad(proto, opts, offset, rot); + + if (ev.GameMap.IsGrid) + { + if (!_loader.TryLoadGrid(targetMap, + ev.GameMap.MapPath, + out var grid, + ev.Options, + ev.Offset, + ev.Rotation)) + { + throw new Exception($"Failed to load game-map grid {ev.GameMap.ID}"); + } + + var g = new List { grid.Value.Owner }; + // TODO MAP LOADING use a new event? + RaiseLocalEvent(new PostGameMapLoad(proto, targetMap, g, stationName)); + return g; + } + + if (!_loader.TryMergeMap(targetMap, + ev.GameMap.MapPath, + out var grids, + ev.Options, + ev.Offset, + ev.Rotation)) + { + throw new Exception($"Failed to load map"); + } + + var gridUids = grids.Select(x => x.Owner).ToList(); + + // TODO MAP LOADING use a new event? + RaiseLocalEvent(new PostGameMapLoad(proto, targetMap, gridUids, stationName)); + return gridUids; + } + + public override int ReadyPlayerCount() + { + var total = 0; + foreach (var (userId, status) in _playerGameStatuses) + { + if (LobbyEnabled && status == PlayerGameStatus.NotReadyToPlay) + continue; + + if (!_playerManager.TryGetSessionById(userId, out _)) + continue; + + total++; + } + + return total; + } + + public void StartRound(bool force = false) + { +#if EXCEPTION_TOLERANCE + try + { +#endif + // If this game ticker is a dummy or the round is already being started, do nothing! + if (DummyTicker || _startingRound) + return; + + _startingRound = true; + + if (RoundId == 0) + IncrementRoundNumber(); + + ReplayStartRound(); + + DebugTools.Assert(RunLevel == GameRunLevel.PreRoundLobby); + Log.Info("Starting round!"); + + SendServerMessage(Loc.GetString("game-ticker-start-round")); + + var readyPlayers = new List(); + var readyPlayerProfiles = new Dictionary(); + var autoDeAdmin = Cfg.GetCVar(CCVars.AdminDeadminOnJoin); + foreach (var (userId, status) in _playerGameStatuses) + { + if (LobbyEnabled && status != PlayerGameStatus.ReadyToPlay) + continue; + + if (!_playerManager.TryGetSessionById(userId, out var session)) + continue; + + if (autoDeAdmin && _adminManager.IsAdmin(session)) + { + _adminManager.DeAdmin(session); + } +#if DEBUG + DebugTools.Assert(_userDb.IsLoadComplete(session), $"Player was readied up but didn't have user DB data loaded yet??"); +#endif + + readyPlayers.Add(session); + HumanoidCharacterProfile profile; + if (_prefsManager.TryGetCachedPreferences(userId, out var preferences)) + { + profile = preferences.SelectedCharacter; + } + else + { + var speciesToBlacklist = + new HashSet(Cfg.GetCVar(CCVars.ICNewAccountSpeciesBlacklist).Split(",")); + profile = HumanoidCharacterProfile.Random(speciesToBlacklist); + } + readyPlayerProfiles.Add(userId, profile); + } + + DebugTools.AssertEqual(readyPlayers.Count, ReadyPlayerCount()); + + // Just in case it hasn't been loaded previously we'll try loading it. + LoadMaps(); + + // map has been selected so update the lobby info text + // applies to players who didn't ready up + UpdateInfoText(); + + StartGamePresetRules(); + + RoundLengthMetric.Set(0); + + var startingEvent = new RoundStartingEvent(RoundId); + RaiseLocalEvent(startingEvent); + + var origReadyPlayers = readyPlayers.ToArray(); + + if (!StartPreset(origReadyPlayers, force)) + { + _startingRound = false; + return; + } + + // MapInitialize *before* spawning players, our codebase is too shit to do it afterwards... + Map.InitializeMap(DefaultMap); + + SpawnPlayers(readyPlayers, readyPlayerProfiles, force); + + _roundStartDateTime = DateTime.UtcNow; + RunLevel = GameRunLevel.InRound; + + RoundStartTimeSpan = Timing.CurTime; + SendStatusToAll(); + ReqWindowAttentionAll(); + UpdateLateJoinStatus(); + AnnounceRound(); + UpdateInfoText(); + SendRoundStartedDiscordMessage(); + +#if EXCEPTION_TOLERANCE + } + catch (Exception e) + { + _roundStartFailCount++; + + if (RoundStartFailShutdownCount > 0 && _roundStartFailCount >= RoundStartFailShutdownCount) + { + Log.Fatal($"Failed to start a round {_roundStartFailCount} time(s) in a row... Shutting down!"); + _runtimeLog.LogException(e, nameof(GameTicker)); + _baseServer.Shutdown("Restarting server"); + return; + } + + Log.Error($"Exception caught while trying to start the round! Restarting round..."); + _runtimeLog.LogException(e, nameof(GameTicker)); + _startingRound = false; + RestartRound(); + return; + } + + // Round started successfully! Reset counter... + _roundStartFailCount = 0; +#endif + _startingRound = false; + } + + private void RefreshLateJoinAllowed() + { + var refresh = new RefreshLateJoinAllowedEvent(); + RaiseLocalEvent(refresh); + DisallowLateJoin = refresh.DisallowLateJoin; + } + + public override void EndRound(string text = "") + { + // If this game ticker is a dummy, do nothing! + if (DummyTicker) + return; + + DebugTools.Assert(RunLevel == GameRunLevel.InRound); + Log.Info("Ending round!"); + + RunLevel = GameRunLevel.PostRound; + + try + { + ShowRoundEndScoreboard(text); + } + catch (Exception e) + { + Log.Error($"Error while showing round end scoreboard: {e}"); + } + + try + { + SendRoundEndDiscordMessage(); + } + catch (Exception e) + { + Log.Error($"Error while sending round end Discord message: {e}"); + } + } + + public void ShowRoundEndScoreboard(string text = "") + { + // Log end of round + Admin.Add(LogType.EmergencyShuttle, LogImpact.High, $"Round ended, showing summary"); + + //Tell every client the round has ended. + var gamemodeTitle = CurrentPreset != null ? Loc.GetString(CurrentPreset.ModeTitle) : string.Empty; + + // Let things add text here. + var textEv = new RoundEndTextAppendEvent(); + RaiseLocalEvent(ref textEv); + + var roundEndText = $"{text}\n{textEv.Text}"; + + //Get the timespan of the round. + var roundDuration = RoundDuration(); + + //Generate a list of basic player info to display in the end round summary. + var listOfPlayerInfo = new List(); + // Grab the great big book of all the Minds, we'll need them for this. + var allMinds = EntityQueryEnumerator(); + var pvsOverride = Cfg.GetCVar(CCVars.RoundEndPVSOverrides); + while (allMinds.MoveNext(out var mindId, out var mind)) + { + // TODO don't list redundant observer roles? + // I.e., if a player was an observer ghost, then a hamster ghost role, maybe just list hamster and not + // the observer role? + var userId = mind.UserId ?? mind.OriginalOwnerUserId; + + var connected = false; + var observer = _role.MindHasRole(mindId); + // Continuing + if (userId != null && _playerManager.ValidSessionId(userId.Value)) + { + connected = true; + } + ContentPlayerData? contentPlayerData = null; + if (userId != null && _playerManager.TryGetPlayerData(userId.Value, out var playerData)) + { + contentPlayerData = playerData.ContentData(); + } + // Finish + + var antag = Role.MindIsAntagonist(mindId); + + var playerIcName = "Unknown"; + + if (mind.CharacterName != null) + playerIcName = mind.CharacterName; + else if (mind.CurrentEntity != null && TryName(mind.CurrentEntity.Value, out var icName)) + playerIcName = icName; + + if (TryGetEntity(mind.OriginalOwnedEntity, out var entity) && pvsOverride) + { + _pvsOverride.AddGlobalOverride(entity.Value); + } + + var roles = Role.MindGetAllRoleInfo(mindId); + + var playerEndRoundInfo = new RoundEndMessageEvent.RoundEndPlayerInfo() + { + // Note that contentPlayerData?.Name sticks around after the player is disconnected. + // This is as opposed to ply?.Name which doesn't. + PlayerOOCName = contentPlayerData?.Name ?? "(IMPOSSIBLE: REGISTERED MIND WITH NO OWNER)", + // Character name takes precedence over current entity name + PlayerICName = playerIcName, + PlayerGuid = userId, + PlayerNetEntity = GetNetEntity(entity), + Role = antag + ? roles.First(role => role.Antagonist).Name + : roles.FirstOrDefault().Name ?? Loc.GetString("game-ticker-unknown-role"), + Antag = antag, + JobPrototypes = roles.Where(role => !role.Antagonist).Select(role => role.Prototype).ToArray(), + AntagPrototypes = roles.Where(role => role.Antagonist).Select(role => role.Prototype).ToArray(), + Observer = observer, + Connected = connected + }; + listOfPlayerInfo.Add(playerEndRoundInfo); + } + + // This ordering mechanism isn't great (no ordering of minds) but functions + var listOfPlayerInfoFinal = listOfPlayerInfo.OrderBy(pi => pi.PlayerOOCName).ToArray(); + var sound = RoundEndSoundCollection == null ? null : Audio.ResolveSound(new SoundCollectionSpecifier(RoundEndSoundCollection)); + + var roundEndMessageEvent = new RoundEndMessageEvent( + gamemodeTitle, + roundEndText, + roundDuration, + RoundId, + listOfPlayerInfoFinal.Length, + listOfPlayerInfoFinal, + sound + ); + RaiseNetworkEvent(roundEndMessageEvent); + RaiseLocalEvent(roundEndMessageEvent); + + _replayRoundPlayerInfo = listOfPlayerInfoFinal; + _replayRoundText = roundEndText; + } + + private async void SendRoundEndDiscordMessage() + { + try + { + if (_webhookIdentifier == null) + return; + + var duration = RoundDuration(); + var content = Loc.GetString("discord-round-notifications-end", + ("id", RoundId), + ("hours", Math.Truncate(duration.TotalHours)), + ("minutes", duration.Minutes), + ("seconds", duration.Seconds)); + var payload = new WebhookPayload { Content = content }; + + await _discord.CreateMessage(_webhookIdentifier.Value, payload); + + if (DiscordRoundEndRole == null) + return; + + content = Loc.GetString("discord-round-notifications-end-ping", ("roleId", DiscordRoundEndRole)); + payload = new WebhookPayload { Content = content }; + payload.AllowedMentions.AllowRoleMentions(); + + await _discord.CreateMessage(_webhookIdentifier.Value, payload); + } + catch (Exception e) + { + Log.Error($"Error while sending discord round end message:\n{e}"); + } + } + + public override void RestartRound() + { + // If this game ticker is a dummy, do nothing! + if (DummyTicker) + return; + + ReplayEndRound(); + + // Handle restart for server update + if (_serverUpdates.RoundEnded()) + return; + + // Check if the GamePreset needs to be reset + TryResetPreset(); + + Log.Info("Restarting round!"); + + SendServerMessage(Loc.GetString("game-ticker-restart-round")); + + RoundNumberMetric.Inc(); + + PlayersJoinedRoundNormally = 0; + + RunLevel = GameRunLevel.PreRoundLobby; + RandomizeLobbyBackground(); + ResettingCleanup(); + IncrementRoundNumber(); + SendRoundStartingDiscordMessage(); + + if (!LobbyEnabled) + { + StartRound(); + } + else + { + if (_playerManager.PlayerCount == 0) + _roundStartCountdownHasNotStartedYetDueToNoPlayers = true; + else + _roundStartTime = Timing.CurTime + LobbyDuration; + + SendStatusToAll(); + UpdateInfoText(); + + ReqWindowAttentionAll(); + } + } + + private async void SendRoundStartingDiscordMessage() + { + try + { + if (_webhookIdentifier == null) + return; + + var content = Loc.GetString("discord-round-notifications-new"); + + var payload = new WebhookPayload { Content = content }; + + await _discord.CreateMessage(_webhookIdentifier.Value, payload); + } + catch (Exception e) + { + Log.Error($"Error while sending discord round starting message:\n{e}"); + } + } + + /// + /// 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.Sessions) + { + PlayerJoinLobby(player); + } + + // Round restart cleanup event, so entity systems can reset. + var ev = new RoundRestartCleanupEvent(); + RaiseLocalEvent(ev); + + // So clients' entity systems can clean up too... + RaiseNetworkEvent(ev); + + EntityManager.FlushEntities(); + + _banManager.Restart(); + + _gameMapManager.ClearSelectedMap(); + + // Clear up any game rules. + CurrentPreset = null; + + AllRoundGameRules.Clear(); + + DisallowLateJoin = false; + _playerGameStatuses.Clear(); + foreach (var session in _playerManager.Sessions) + { + _playerGameStatuses[session.UserId] = LobbyEnabled ? PlayerGameStatus.NotReadyToPlay : PlayerGameStatus.ReadyToPlay; + } + } + + public bool DelayStart(TimeSpan time) + { + if (RunLevel != GameRunLevel.PreRoundLobby) + { + return false; + } + + _roundStartTime += time; + + RaiseNetworkEvent(new TickerLobbyCountdownEvent(_roundStartTime, Paused)); + + _chatManager.DispatchServerAnnouncement(Loc.GetString("game-ticker-delay-start", ("seconds", time.TotalSeconds))); + + return true; + } + + private void UpdateRoundFlow(float frameTime) + { + if (RunLevel == GameRunLevel.InRound) + { + RoundLengthMetric.Inc(frameTime); + } + + if (_roundStartTime == TimeSpan.Zero || + RunLevel != GameRunLevel.PreRoundLobby || + Paused || + _roundStartTime - RoundPreloadTime > Timing.CurTime || + _roundStartCountdownHasNotStartedYetDueToNoPlayers) + { + return; + } + + if (_roundStartTime < Timing.CurTime) + { + StartRound(); + } + // Preload maps so we can start faster + else if (_roundStartTime - RoundPreloadTime < Timing.CurTime) + { + LoadMaps(); + } + } + + private void AnnounceRound() + { + if (CurrentPreset == null) + return; + + var options = ProtoMan.EnumeratePrototypes().ToList(); + + if (options.Count == 0) + return; + + var proto = Random.Pick(options); + + if (proto.Message != null) + _chatSystem.DispatchGlobalAnnouncement(Loc.GetString(proto.Message), playSound: true); + + if (proto.Sound != null) + Audio.PlayGlobal(proto.Sound, Filter.Broadcast(), true); + } + + private async void SendRoundStartedDiscordMessage() + { + try + { + if (_webhookIdentifier == null) + return; + + var mapName = _gameMapManager.GetSelectedMap()?.MapName ?? Loc.GetString("discord-round-notifications-unknown-map"); + var content = Loc.GetString("discord-round-notifications-started", ("id", RoundId), ("map", mapName)); + + var payload = new WebhookPayload { Content = content }; + + await _discord.CreateMessage(_webhookIdentifier.Value, payload); + } + catch (Exception e) + { + Log.Error($"Error while sending discord round start message:\n{e}"); + } + } +} + +/// +/// Event raised before maps are loaded in pre-round setup. +/// Contains a list of game map prototypes to load; modify it if you want to load different maps, +/// for example as part of a game rule. +/// +[PublicAPI] +public sealed class LoadingMapsEvent : EntityEventArgs +{ + public List Maps; + + public LoadingMapsEvent(List maps) + { + Maps = maps; + } +} + +/// +/// Event raised before the game loads a given map. +/// This event is mutable, and load options should be tweaked if necessary. +/// +/// +/// You likely want to subscribe to this after StationSystem. +/// +[PublicAPI] +public sealed class PreGameMapLoad(GameMapPrototype gameMap, DeserializationOptions options, Vector2 offset, Angle rotation) : EntityEventArgs +{ + public readonly GameMapPrototype GameMap = gameMap; + public DeserializationOptions Options = options; + public Vector2 Offset = offset; + public Angle Rotation = rotation; +} + +/// +/// Event raised after the game loads a given map. +/// +/// +/// You likely want to subscribe to this after StationSystem. +/// +[PublicAPI] +public sealed class PostGameMapLoad : EntityEventArgs +{ + public readonly GameMapPrototype GameMap; + public readonly MapId Map; + public readonly IReadOnlyList Grids; + public readonly string? StationName; + + public PostGameMapLoad(GameMapPrototype gameMap, MapId map, IReadOnlyList grids, string? stationName) + { + GameMap = gameMap; + Map = map; + Grids = grids; + StationName = stationName; + } +} + +/// +/// Event raised to refresh the late join status. +/// If you want to disallow late joins, listen to this and call Disallow. +/// +public sealed class RefreshLateJoinAllowedEvent +{ + public bool DisallowLateJoin { get; private set; } + + public void Disallow() + { + DisallowLateJoin = true; + } +} + +/// +/// Attempt event raised on round start. +/// This can be listened to by GameRule systems to cancel round start if some condition is not met, like player count. +/// +public sealed class RoundStartAttemptEvent : CancellableEntityEventArgs +{ + public ICommonSession[] Players { get; } + public bool Forced { get; } + + public RoundStartAttemptEvent(ICommonSession[] players, bool forced) + { + Players = players; + Forced = forced; + } +} diff --git a/Content.Server/GameTicking/GameTicker.Spawning.cs b/Content.Server/GameTicking/ServerGameTicker.Spawning.cs similarity index 85% rename from Content.Server/GameTicking/GameTicker.Spawning.cs rename to Content.Server/GameTicking/ServerGameTicker.Spawning.cs index d5b6649104..67d79be7a1 100644 --- a/Content.Server/GameTicking/GameTicker.Spawning.cs +++ b/Content.Server/GameTicking/ServerGameTicker.Spawning.cs @@ -4,8 +4,6 @@ using System.Numerics; using Content.Server.Administration.Managers; using Content.Server.Administration.Systems; using Content.Server.GameTicking.Events; -using Content.Server.Spawners.Components; -using Content.Server.Station.Components; using Content.Shared.CCVar; using Content.Shared.Database; using Content.Shared.GameTicking; @@ -17,6 +15,8 @@ using Content.Shared.Random; using Content.Shared.Random.Helpers; using Content.Shared.Roles; using Content.Shared.Roles.Jobs; +using Content.Shared.Spawners.Components; +using Content.Shared.Station.Components; using Robust.Shared.Map; using Robust.Shared.Map.Components; using Robust.Shared.Network; @@ -27,7 +27,7 @@ using Robust.Shared.Utility; namespace Content.Server.GameTicking { - public sealed partial class GameTicker + public sealed partial class ServerGameTicker { [Dependency] private IAdminManager _adminManager = default!; [Dependency] private SharedJobSystem _jobs = default!; @@ -167,7 +167,7 @@ namespace Content.Server.GameTicking if (station == EntityUid.Invalid) { var stations = GetSpawnableStations(); - _robustRandom.Shuffle(stations); + Random.Shuffle(stations); if (stations.Count == 0) station = EntityUid.Invalid; else @@ -183,7 +183,7 @@ namespace Content.Server.GameTicking string speciesId; if (_randomizeCharacters) { - var weightId = _cfg.GetCVar(CCVars.ICRandomSpeciesWeights); + var weightId = Cfg.GetCVar(CCVars.ICRandomSpeciesWeights); // If blank, choose a round start species. if (string.IsNullOrEmpty(weightId)) @@ -199,12 +199,12 @@ namespace Content.Server.GameTicking speciesId = roundStart.Count == 0 ? HumanoidCharacterProfile.DefaultSpecies - : _robustRandom.Pick(roundStart); + : Random.Pick(roundStart); } else { var weights = ProtoMan.Index(weightId); - speciesId = weights.Pick(_robustRandom); + speciesId = weights.Pick(Random); } // The random profile must retain the job priorities set by the player @@ -287,13 +287,13 @@ namespace Content.Server.GameTicking if (lateJoin) { - _adminLogger.Add(LogType.LateJoin, + Admin.Add(LogType.LateJoin, LogImpact.Medium, $"Player {player.Name} late joined as {character.Name:characterName} on station {Name(station):stationName} with {ToPrettyString(mob):entity} as a {jobName:jobName}."); } else { - _adminLogger.Add(LogType.RoundStartJoin, + Admin.Add(LogType.RoundStartJoin, LogImpact.Medium, $"Player {player.Name} joined as {character.Name:characterName} on station {Name(station):stationName} with {ToPrettyString(mob):entity} as a {jobName:jobName}."); } @@ -349,22 +349,22 @@ namespace Content.Server.GameTicking DebugTools.AssertNotNull(mobMaybe); mob = mobMaybe!.Value; - var newMind = _mind.CreateMind(data.UserId, Name(mob)); - _mind.SetUserId(newMind, data.UserId); + var newMind = Mind.CreateMind(data.UserId, Name(mob)); + Mind.SetUserId(newMind, data.UserId); _playTimeTrackings.PlayerRolesChanged(player); - _mind.TransferTo(newMind, mob); + Mind.TransferTo(newMind, mob); - _roles.MindAddJobRole(newMind, silent: silent, jobPrototype: jobId); + Role.MindAddJobRole(newMind, silent: silent, jobPrototype: jobId); jobName = _jobs.MindTryGetJobName(newMind); _admin.UpdatePlayerList(player); } public void Respawn(ICommonSession player) { - _mind.WipeMind(player); - _adminLogger.Add(LogType.Respawn, LogImpact.Medium, $"Player {player} was respawned."); + Mind.WipeMind(player); + Admin.Add(LogType.Respawn, LogImpact.Medium, $"Player {player} was respawned."); if (LobbyEnabled) PlayerJoinLobby(player); @@ -372,14 +372,8 @@ namespace Content.Server.GameTicking SpawnPlayer(player, EntityUid.Invalid); } - /// - /// Makes a player join into the game and spawn on a station. - /// - /// The player joining - /// The station they're spawning on - /// An optional job for them to spawn as - /// Whether or not the player should be greeted upon joining - public void MakeJoinGame(ICommonSession player, EntityUid station, string? jobId = null, bool silent = false) + /// + public override void MakeJoinGame(ICommonSession player, EntityUid station, string? jobId = null, bool silent = false) { if (!_playerGameStatuses.ContainsKey(player.UserId)) return; @@ -417,17 +411,17 @@ namespace Content.Server.GameTicking if (mind == null) { var name = GetPlayerProfile(player).Name; - var (mindId, mindComp) = _mind.CreateMind(player.UserId, name); + var (mindId, mindComp) = Mind.CreateMind(player.UserId, name); mind = (mindId, mindComp); - _mind.SetUserId(mind.Value, player.UserId); + Mind.SetUserId(mind.Value, player.UserId); makeObserver = true; } var ghost = _ghost.SpawnGhost(mind.Value); if (makeObserver) - _roles.MindAddRole(mind.Value, "MindRoleObserver"); + Role.MindAddRole(mind.Value, "MindRoleObserver"); - _adminLogger.Add(LogType.LateJoin, + Admin.Add(LogType.LateJoin, LogImpact.Low, $"{player.Name} late joined the round as an Observer with {ToPrettyString(ghost):entity}."); } @@ -454,14 +448,9 @@ namespace Content.Server.GameTicking // Fallback to a random grid. if (_possiblePositions.Count == 0) { - var query = AllEntityQuery(); - while (query.MoveNext(out var uid, out var grid)) + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out _)) { - if (!TryComp(uid, out MetaDataComponent? meta) || meta.EntityPaused || TerminatingOrDeleted(uid)) - { - continue; - } - _possiblePositions.Add(new EntityCoordinates(uid, Vector2.Zero)); } } @@ -471,44 +460,40 @@ namespace Content.Server.GameTicking // TODO: This is just here for the eye lerping. // Ideally engine would just spawn them on grid directly I guess? Right now grid traversal is handling it during // update which means we need to add a hack somewhere around it. - var spawn = _robustRandom.Pick(_possiblePositions); - var toMap = _transform.ToMapCoordinates(spawn); + var spawn = Random.Pick(_possiblePositions); + var toMap = XForm.ToMapCoordinates(spawn); - if (_map.TryFindGridAt(toMap, out var gridUid, out _)) + if (Map.TryFindGridAt(toMap, out var gridUid, out _)) { var gridXform = Transform(gridUid); - return new EntityCoordinates(gridUid, Vector2.Transform(toMap.Position, _transform.GetInvWorldMatrix(gridXform))); + return new EntityCoordinates(gridUid, Vector2.Transform(toMap.Position, XForm.GetInvWorldMatrix(gridXform))); } return spawn; } - if (_map.MapExists(DefaultMap)) + if (Map.MapExists(DefaultMap)) { - var mapUid = _map.GetMapOrInvalid(DefaultMap); + var mapUid = Map.GetMapOrInvalid(DefaultMap); if (!TerminatingOrDeleted(mapUid)) return new EntityCoordinates(mapUid, Vector2.Zero); } // Just pick a point at this point I guess. - foreach (var map in _map.GetAllMapIds()) + foreach (var map in Map.GetAllMapIds()) { - var mapUid = _map.GetMapOrInvalid(map); + var mapUid = Map.GetMapOrInvalid(map); + var meta = MetaData(mapUid); - if (!TryComp(mapUid, out MetaDataComponent? meta) - || meta.EntityPaused - || TerminatingOrDeleted(mapUid)) - { + if (meta.EntityPaused || TerminatingOrDeleted(mapUid, meta)) continue; - } return new EntityCoordinates(mapUid, Vector2.Zero); } - // AAAAAAAAAAAAA - // This should be an error, if it didn't cause tests to start erroring when they delete a player. - _sawmill.Warning("Found no observer spawn points!"); + // You REALLY fucked up + Log.Error("Found no observer spawn points!"); return EntityCoordinates.Invalid; } diff --git a/Content.Server/GameTicking/ServerGameTicker.StatusShell.cs b/Content.Server/GameTicking/ServerGameTicker.StatusShell.cs new file mode 100644 index 0000000000..698c6d6170 --- /dev/null +++ b/Content.Server/GameTicking/ServerGameTicker.StatusShell.cs @@ -0,0 +1,51 @@ +using System.Linq; +using System.Text.Json.Nodes; +using Content.Shared.CCVar; +using Content.Shared.GameTicking; +using Robust.Server.ServerStatus; + +namespace Content.Server.GameTicking; + +public sealed partial class ServerGameTicker +{ + /// + /// Used for thread safety, given is called from another thread. + /// + private readonly object _statusShellLock = new(); + + /// + /// Round start time in UTC, for status shell purposes. + /// + [ViewVariables] + private DateTime _roundStartDateTime; + + private void InitializeStatusShell() + { + IoCManager.Resolve().OnStatusRequest += GetStatusResponse; + } + + private void GetStatusResponse(JsonNode jObject) + { + var preset = CurrentPreset ?? Preset; + + // This method is raised from another thread, so this better be thread safe! + lock (_statusShellLock) + { + jObject["name"] = _baseServer.ServerName; + jObject["map"] = _gameMapManager.GetSelectedMap()?.MapName; + jObject["round_id"] = RoundId; + jObject["players"] = Cfg.GetCVar(CCVars.AdminsCountInReportedPlayerCount) + ? _playerManager.PlayerCount + : _playerManager.PlayerCount - _adminManager.ActiveAdmins.Count(); + jObject["soft_max_players"] = Cfg.GetCVar(CCVars.SoftMaxPlayers); + jObject["panic_bunker"] = Cfg.GetCVar(CCVars.PanicBunkerEnabled); + jObject["run_level"] = (int) RunLevel; + if (preset != null) + jObject["preset"] = (Decoy == null) ? Loc.GetString(preset.ModeTitle) : Loc.GetString(Decoy.ModeTitle); + if (RunLevel >= GameRunLevel.InRound) + { + jObject["round_start_time"] = _roundStartDateTime.ToString("o"); + } + } + } +} diff --git a/Content.Server/GameTicking/GameTicker.cs b/Content.Server/GameTicking/ServerGameTicker.cs similarity index 77% rename from Content.Server/GameTicking/GameTicker.cs rename to Content.Server/GameTicking/ServerGameTicker.cs index 3d6ad8ad4c..99edf3d636 100644 --- a/Content.Server/GameTicking/GameTicker.cs +++ b/Content.Server/GameTicking/ServerGameTicker.cs @@ -1,4 +1,3 @@ -using Content.Server.Administration.Logs; using Content.Server.Administration.Managers; using Content.Server.Chat.Managers; using Content.Server.Chat.Systems; @@ -9,19 +8,14 @@ using Content.Server.Players.PlayTimeTracking; using Content.Server.Preferences.Managers; using Content.Server.ServerUpdates; using Content.Server.Station.Systems; -using Content.Shared.CCVar; using Content.Shared.Chat; using Content.Shared.GameTicking; -using Content.Shared.Mind; -using Content.Shared.Roles; +using Content.Shared.Station.Systems; using Robust.Server; using Robust.Server.GameStates; -using Robust.Shared.Audio.Systems; using Robust.Shared.Console; using Robust.Shared.EntitySerialization.Systems; using Robust.Shared.Map; -using Robust.Shared.Random; -using Robust.Shared.Timing; using Robust.Shared.Utility; #if EXCEPTION_TOLERANCE using Robust.Shared.Exceptions; @@ -29,46 +23,35 @@ using Robust.Shared.Exceptions; namespace Content.Server.GameTicking { - public sealed partial class GameTicker : SharedGameTicker + public sealed partial class ServerGameTicker : GameTicker { - [Dependency] private IAdminLogManager _adminLogger = default!; [Dependency] private IBanManager _banManager = default!; [Dependency] private IBaseServer _baseServer = default!; [Dependency] private IChatManager _chatManager = default!; [Dependency] private IConsoleHost _consoleHost = default!; [Dependency] private IGameMapManager _gameMapManager = default!; - [Dependency] private IGameTiming _gameTiming = default!; [Dependency] private ILogManager _logManager = default!; - [Dependency] private IRobustRandom _robustRandom = default!; #if EXCEPTION_TOLERANCE [Dependency] private IRuntimeLog _runtimeLog = default!; #endif [Dependency] private IServerPreferencesManager _prefsManager = default!; [Dependency] private IServerDbManager _db = default!; [Dependency] private ChatSystem _chatSystem = default!; - [Dependency] private MapLoaderSystem _loader = default!; - [Dependency] private SharedMapSystem _map = default!; [Dependency] private GhostSystem _ghost = default!; - [Dependency] private SharedMindSystem _mind = default!; + [Dependency] private MapLoaderSystem _loader = default!; [Dependency] private PlayTimeTrackingSystem _playTimeTrackings = default!; [Dependency] private PvsOverrideSystem _pvsOverride = default!; - [Dependency] private ServerUpdateManager _serverUpdates = default!; - [Dependency] private SharedAudioSystem _audio = default!; - [Dependency] private StationJobsSystem _stationJobs = default!; - [Dependency] private StationSpawningSystem _stationSpawning = default!; - [Dependency] private SharedTransformSystem _transform = default!; - [Dependency] private UserDbDataManager _userDb = default!; - [Dependency] private MetaDataSystem _metaData = default!; - [Dependency] private SharedRoleSystem _roles = default!; [Dependency] private ServerDbEntryManager _dbEntryManager = default!; + [Dependency] private ServerUpdateManager _serverUpdates = default!; + [Dependency] private ServerStationJobsSystem _stationJobs = default!; + [Dependency] private StationSpawningSystem _stationSpawning = default!; + [Dependency] private UserDbDataManager _userDb = default!; [ViewVariables] private bool _initialized; [ViewVariables] private bool _postInitialized; [ViewVariables] public MapId DefaultMap { get; private set; } - private ISawmill _sawmill = default!; - private bool _randomizeCharacters; public override void Initialize() @@ -78,11 +61,9 @@ namespace Content.Server.GameTicking DebugTools.Assert(!_initialized); DebugTools.Assert(!_postInitialized); - _sawmill = _logManager.GetSawmill("ticker"); + // TODO: Move replays to their own bespoke system so we don't need two sawmills, or allow for name overrides for specific logs _sawmillReplays = _logManager.GetSawmill("ticker.replays"); - Subs.CVar(_cfg, CCVars.ICRandomCharacters, e => _randomizeCharacters = e, true); - // Initialize the other parts of the game ticker. InitializeStatusShell(); InitializeCVars(); @@ -132,7 +113,7 @@ namespace Content.Server.GameTicking public static int GetRoundId(IEntitySystemManager esm) { - return esm.GetEntitySystemOrNull()?.RoundId ?? 0; + return esm.GetEntitySystemOrNull()?.RoundId ?? 0; } } } diff --git a/Content.Server/Ghost/GhostCommand.cs b/Content.Server/Ghost/GhostCommand.cs index 81f278a73c..7420e1a1a8 100644 --- a/Content.Server/Ghost/GhostCommand.cs +++ b/Content.Server/Ghost/GhostCommand.cs @@ -25,7 +25,7 @@ namespace Content.Server.Ghost return; } - var gameTicker = _entities.System(); + var gameTicker = _entities.System(); if (!gameTicker.PlayerGameStatuses.TryGetValue(player.UserId, out var playerStatus) || playerStatus is not PlayerGameStatus.JoinedGame) { diff --git a/Content.Server/Ghost/GhostSystem.cs b/Content.Server/Ghost/GhostSystem.cs index 6c13006551..3c9d94c827 100644 --- a/Content.Server/Ghost/GhostSystem.cs +++ b/Content.Server/Ghost/GhostSystem.cs @@ -16,6 +16,7 @@ using Content.Shared.Eye; using Content.Shared.FixedPoint; using Content.Shared.Follower; using Content.Shared.Follower.Components; +using Content.Shared.GameTicking; using Content.Shared.Ghost.Components; using Content.Shared.Ghost.Systems; using Content.Shared.GhostTypes; @@ -65,7 +66,7 @@ namespace Content.Server.Ghost [Dependency] private IConfigurationManager _configurationManager = default!; [Dependency] private IChatManager _chatManager = default!; [Dependency] private SharedMindSystem _mind = default!; - [Dependency] private GameTicker _gameTicker = default!; + [Dependency] private ServerGameTicker _gameTicker = default!; [Dependency] private DamageableSystem _damageable = default!; [Dependency] private SharedPopupSystem _popup = default!; [Dependency] private IRobustRandom _random = default!; @@ -104,7 +105,7 @@ namespace Content.Server.Ghost SubscribeLocalEvent(OnGhostHearingAction); SubscribeLocalEvent(OnEntityStorageInsertAttempt); - SubscribeLocalEvent(_ => MakeVisible(true)); + SubscribeLocalEvent(_ => MakeVisible(true)); SubscribeLocalEvent(OnToggleGhostVisibilityToAll); SubscribeLocalEvent(OnGhostVis); @@ -503,7 +504,7 @@ namespace Content.Server.Ghost return null; } - var ghost = SpawnAtPosition(GameTicker.ObserverPrototypeName, spawnPosition.Value); + var ghost = SpawnAtPosition(ServerGameTicker.ObserverPrototypeName, spawnPosition.Value); var ghostComponent = Comp(ghost); if (TryComp(ghost, out var state)) // If more TryComps are added this should be turned into an event diff --git a/Content.Server/Holiday/HolidaySystem.cs b/Content.Server/Holiday/HolidaySystem.cs index bafc821138..7fd70476dc 100644 --- a/Content.Server/Holiday/HolidaySystem.cs +++ b/Content.Server/Holiday/HolidaySystem.cs @@ -2,6 +2,7 @@ using System.Linq; using Content.Server.Chat.Managers; using Content.Server.GameTicking; using Content.Shared.CCVar; +using Content.Shared.GameTicking; using Content.Shared.Holiday; using Robust.Shared.Configuration; @@ -87,7 +88,8 @@ namespace Content.Server.Holiday private void OnRunLevelChanged(GameRunLevelChangedEvent eventArgs) { - if (!_enabled) return; + if (!_enabled) + return; switch (eventArgs.New) { diff --git a/Content.Server/Implants/ChameleonControllerSystem.cs b/Content.Server/Implants/ChameleonControllerSystem.cs index 2fb2f5c97d..9bbd3bd3f6 100644 --- a/Content.Server/Implants/ChameleonControllerSystem.cs +++ b/Content.Server/Implants/ChameleonControllerSystem.cs @@ -8,7 +8,7 @@ using Content.Shared.Inventory; using Content.Shared.Preferences; using Content.Shared.Preferences.Loadouts; using Content.Shared.Roles; -using Content.Shared.Station; +using Content.Shared.Station.Systems; using Content.Shared.Timing.Systems; using Robust.Shared.Player; using Robust.Shared.Prototypes; @@ -18,7 +18,7 @@ namespace Content.Server.Implants; public sealed partial class ChameleonControllerSystem : SharedChameleonControllerSystem { [Dependency] private InventorySystem _inventory = default!; - [Dependency] private SharedStationSpawningSystem _stationSpawningSystem = default!; + [Dependency] private StationSpawningSystem _stationSpawningSystem = default!; [Dependency] private ChameleonClothingSystem _chameleonClothingSystem = default!; [Dependency] private IServerPreferencesManager _preferences = default!; [Dependency] private UseDelaySystem _delay = default!; diff --git a/Content.Server/Light/EntitySystems/EmergencyLightSystem.cs b/Content.Server/Light/EntitySystems/EmergencyLightSystem.cs index fce66ee6f2..0b5f661f50 100644 --- a/Content.Server/Light/EntitySystems/EmergencyLightSystem.cs +++ b/Content.Server/Light/EntitySystems/EmergencyLightSystem.cs @@ -22,7 +22,7 @@ public sealed partial class EmergencyLightSystem : SharedEmergencyLightSystem [Dependency] private BatterySystem _battery = default!; [Dependency] private PointLightSystem _pointLight = default!; [Dependency] private SharedAppearanceSystem _appearance = default!; - [Dependency] private StationSystem _station = default!; + [Dependency] private ServerStationSystem _station = default!; public override void Initialize() { diff --git a/Content.Server/Magic/MagicSystem.cs b/Content.Server/Magic/MagicSystem.cs index b66680afd7..c012591b5d 100644 --- a/Content.Server/Magic/MagicSystem.cs +++ b/Content.Server/Magic/MagicSystem.cs @@ -1,6 +1,7 @@ using Content.Server.Chat.Systems; using Content.Server.GameTicking; using Content.Server.GameTicking.Rules.Components; +using Content.Shared.GameTicking.Rules.Components; using Content.Shared.Magic; using Content.Shared.Magic.Events; using Content.Shared.Mind; @@ -12,18 +13,13 @@ namespace Content.Server.Magic; public sealed partial class MagicSystem : SharedMagicSystem { [Dependency] private ChatSystem _chat = default!; - [Dependency] private GameTicker _gameTicker = default!; + [Dependency] private ServerGameTicker _gameTicker = default!; [Dependency] private TagSystem _tag = default!; [Dependency] private SharedMindSystem _mind = default!; private static readonly ProtoId InvalidForSurvivorAntagTag = "InvalidForSurvivorAntag"; private static readonly EntProtoId SurvivorGameRule = "Survivor"; - public override void Initialize() - { - base.Initialize(); - } - public override void OnVoidApplause(VoidApplauseSpellEvent ev) { base.OnVoidApplause(ev); diff --git a/Content.Server/Mapping/MappingCommand.cs b/Content.Server/Mapping/MappingCommand.cs index 5692439a06..59daeadf94 100644 --- a/Content.Server/Mapping/MappingCommand.cs +++ b/Content.Server/Mapping/MappingCommand.cs @@ -146,7 +146,7 @@ namespace Content.Server.Mapping // map successfully created. run misc helpful mapping commands if (player.AttachedEntity is { Valid: true } playerEntity && - (EntityManager.GetComponent(playerEntity).EntityPrototype is not { } proto || proto != GameTicker.AdminObserverPrototypeName)) + (EntityManager.GetComponent(playerEntity).EntityPrototype is not { } proto || proto != ServerGameTicker.AdminObserverPrototypeName)) { shell.ExecuteCommand("aghost"); } diff --git a/Content.Server/Maps/GameMapManager.cs b/Content.Server/Maps/GameMapManager.cs index fd31c91844..5e15f51e15 100644 --- a/Content.Server/Maps/GameMapManager.cs +++ b/Content.Server/Maps/GameMapManager.cs @@ -102,14 +102,14 @@ public sealed partial class GameMapManager : IGameMapManager public IEnumerable AllVotableMaps() { - var poolPrototype = _entityManager.System().Preset?.MapPool ?? + var poolPrototype = _entityManager.System().Preset?.MapPool ?? _configurationManager.GetCVar(CCVars.GameMapPool); - if (_prototypeManager.TryIndex(poolPrototype, out var pool)) + if (_prototypeManager.TryIndex(poolPrototype, out var pool)) { foreach (var map in pool.Maps) { - if (!_prototypeManager.TryIndex(map, out var mapProto)) + if (!_prototypeManager.TryIndex(map, out var mapProto)) { _log.Error($"Couldn't index map {map} in pool {poolPrototype}"); continue; @@ -194,7 +194,7 @@ public sealed partial class GameMapManager : IGameMapManager return map.MaxPlayers >= _playerManager.PlayerCount && map.MinPlayers <= _playerManager.PlayerCount && map.Conditions.All(x => x.Check(map)) && - _entityManager.System().IsMapEligible(map); + _entityManager.System().IsMapEligible(map); } private bool TryLookupMap(string gameMap, [NotNullWhen(true)] out GameMapPrototype? map) diff --git a/Content.Server/MassMedia/Systems/NewsSystem.cs b/Content.Server/MassMedia/Systems/NewsSystem.cs index e0c27105ff..5fde3c8541 100644 --- a/Content.Server/MassMedia/Systems/NewsSystem.cs +++ b/Content.Server/MassMedia/Systems/NewsSystem.cs @@ -40,8 +40,8 @@ public sealed partial class NewsSystem : SharedNewsSystem [Dependency] private CartridgeLoaderSystem _cartridgeLoaderSystem = default!; [Dependency] private SharedAudioSystem _audio = default!; [Dependency] private PopupSystem _popup = default!; - [Dependency] private StationSystem _station = default!; - [Dependency] private GameTicker _ticker = default!; + [Dependency] private ServerStationSystem _station = default!; + [Dependency] private ServerGameTicker _ticker = default!; [Dependency] private IChatManager _chatManager = default!; [Dependency] private DiscordWebhook _discord = default!; [Dependency] private IConfigurationManager _cfg = default!; diff --git a/Content.Server/Mind/MindSystem.cs b/Content.Server/Mind/MindSystem.cs index be5dd86563..826cbe9b2a 100644 --- a/Content.Server/Mind/MindSystem.cs +++ b/Content.Server/Mind/MindSystem.cs @@ -12,12 +12,13 @@ using Robust.Shared.Network; using Robust.Shared.Player; using Robust.Shared.Utility; using System.Diagnostics.CodeAnalysis; +using Content.Shared.GameTicking; namespace Content.Server.Mind; public sealed partial class MindSystem : SharedMindSystem { - [Dependency] private GameTicker _gameTicker = default!; + [Dependency] private ServerGameTicker _gameTicker = default!; [Dependency] private IAdminLogManager _adminLogger = default!; [Dependency] private IPlayerManager _players = default!; [Dependency] private GhostSystem _ghosts = default!; @@ -210,7 +211,7 @@ public sealed partial class MindSystem : SharedMindSystem ? _transform.ToMapCoordinates(_gameTicker.GetObserverSpawnPoint()) : _transform.GetMapCoordinates(mind.OwnedEntity.Value); - entity = Spawn(GameTicker.ObserverPrototypeName, position); + entity = Spawn(ServerGameTicker.ObserverPrototypeName, position); component = EnsureComp(entity.Value); var ghostComponent = Comp(entity.Value); _ghosts.SetCanReturnToBody((entity.Value, ghostComponent), false); diff --git a/Content.Server/Nuke/NukeCodePaperSystem.cs b/Content.Server/Nuke/NukeCodePaperSystem.cs index e85066b1e6..d3e00f1363 100644 --- a/Content.Server/Nuke/NukeCodePaperSystem.cs +++ b/Content.Server/Nuke/NukeCodePaperSystem.cs @@ -14,7 +14,7 @@ namespace Content.Server.Nuke { [Dependency] private IRobustRandom _random = default!; [Dependency] private ChatSystem _chatSystem = default!; - [Dependency] private StationSystem _station = default!; + [Dependency] private ServerStationSystem _station = default!; [Dependency] private PaperSystem _paper = default!; [Dependency] private FaxSystem _faxSystem = default!; diff --git a/Content.Server/Nuke/NukeSystem.cs b/Content.Server/Nuke/NukeSystem.cs index 1dba04507f..67c2fa99fb 100644 --- a/Content.Server/Nuke/NukeSystem.cs +++ b/Content.Server/Nuke/NukeSystem.cs @@ -40,7 +40,7 @@ public sealed partial class NukeSystem : EntitySystem [Dependency] private SharedDoAfterSystem _doAfter = default!; [Dependency] private SharedTransformSystem _transform = default!; [Dependency] private SharedMapSystem _map = default!; - [Dependency] private StationSystem _station = default!; + [Dependency] private ServerStationSystem _station = default!; [Dependency] private UserInterfaceSystem _ui = default!; [Dependency] private AppearanceSystem _appearance = default!; [Dependency] private TurfSystem _turf = default!; diff --git a/Content.Server/NukeOps/WarDeclaratorComponent.cs b/Content.Server/NukeOps/WarDeclaratorComponent.cs index 462d2b1681..d3b058d6c3 100644 --- a/Content.Server/NukeOps/WarDeclaratorComponent.cs +++ b/Content.Server/NukeOps/WarDeclaratorComponent.cs @@ -9,7 +9,7 @@ namespace Content.Server.NukeOps; /// Used with NukeOps game rule to send war declaration announcement /// [RegisterComponent, AutoGenerateComponentPause] -[Access(typeof(WarDeclaratorSystem), typeof(NukeopsRuleSystem))] +[Access(typeof(WarDeclaratorSystem), typeof(ServerNukeopsRuleSystem))] public sealed partial class WarDeclaratorComponent : Component { /// diff --git a/Content.Server/Objectives/ObjectivesSystem.cs b/Content.Server/Objectives/ObjectivesSystem.cs index f01b2cc30b..44db00da87 100644 --- a/Content.Server/Objectives/ObjectivesSystem.cs +++ b/Content.Server/Objectives/ObjectivesSystem.cs @@ -13,6 +13,7 @@ using System.Linq; using System.Text; using Content.Server.Objectives.Commands; using Content.Shared.CCVar; +using Content.Shared.GameTicking; using Content.Shared.Prototypes; using Content.Shared.Roles.Jobs; using Robust.Server.Player; @@ -37,8 +38,6 @@ public sealed partial class ObjectivesSystem : SharedObjectivesSystem { base.Initialize(); - SubscribeLocalEvent(OnRoundEndText); - Subs.CVar(_cfg, CCVars.GameShowGreentext, value => _showGreentext = value, true); ProtoMan.PrototypesReloaded += CreateCompletions; @@ -54,7 +53,8 @@ public sealed partial class ObjectivesSystem : SharedObjectivesSystem /// /// Adds objective text for each game rule's players on round end. /// - private void OnRoundEndText(RoundEndTextAppendEvent ev) + [SubscribeLocalEvent] + private void OnRoundEndText(ref RoundEndTextAppendEvent ev) { // go through each gamerule getting data for the roundend summary. var summaries = new Dictionary>>(); diff --git a/Content.Server/Objectives/Systems/MultipleTraitorsRequirementSystem.cs b/Content.Server/Objectives/Systems/MultipleTraitorsRequirementSystem.cs index f5fbe9cd4b..845ab2f0df 100644 --- a/Content.Server/Objectives/Systems/MultipleTraitorsRequirementSystem.cs +++ b/Content.Server/Objectives/Systems/MultipleTraitorsRequirementSystem.cs @@ -1,5 +1,6 @@ using Content.Server.GameTicking.Rules; using Content.Server.Objectives.Components; +using Content.Shared.GameTicking.Rules; using Content.Shared.Objectives.Components; namespace Content.Server.Objectives.Systems; diff --git a/Content.Server/Objectives/Systems/ObjectiveLimitSystem.cs b/Content.Server/Objectives/Systems/ObjectiveLimitSystem.cs index 034af91c00..63aabcd77b 100644 --- a/Content.Server/Objectives/Systems/ObjectiveLimitSystem.cs +++ b/Content.Server/Objectives/Systems/ObjectiveLimitSystem.cs @@ -1,8 +1,10 @@ -using Content.Server.GameTicking.Rules.Components; using Content.Server.Objectives.Components; +using Content.Shared.GameTicking.Rules.Components; using Content.Shared.Mind; using Content.Shared.Objectives.Components; +namespace Content.Server.Objectives.Systems; + public sealed partial class ObjectiveLimitSystem : EntitySystem { public override void Initialize() diff --git a/Content.Server/PDA/PdaSystem.cs b/Content.Server/PDA/PdaSystem.cs index cfe69e6b58..a425d96b15 100644 --- a/Content.Server/PDA/PdaSystem.cs +++ b/Content.Server/PDA/PdaSystem.cs @@ -33,7 +33,7 @@ namespace Content.Server.PDA [Dependency] private CartridgeLoaderSystem _cartridgeLoader = default!; [Dependency] private InstrumentSystem _instrument = default!; [Dependency] private RingerSystem _ringer = default!; - [Dependency] private StationSystem _station = default!; + [Dependency] private ServerStationSystem _station = default!; [Dependency] private StoreSystem _store = default!; [Dependency] private IChatManager _chatManager = default!; [Dependency] private UserInterfaceSystem _ui = default!; @@ -131,13 +131,6 @@ namespace Content.Server.PDA UpdatePdaUi(uid, pda); } - public void SetOwner(EntityUid uid, PdaComponent pda, EntityUid owner, string ownerName) - { - pda.OwnerName = ownerName; - pda.PdaOwner = owner; - UpdatePdaUi(uid, pda); - } - private void OnStationRenamed(StationRenamedEvent ev) { UpdateAllPdaUisOnStation(); diff --git a/Content.Server/Pinpointer/StationMapSystem.cs b/Content.Server/Pinpointer/StationMapSystem.cs index 54180533a9..1d37befcdf 100644 --- a/Content.Server/Pinpointer/StationMapSystem.cs +++ b/Content.Server/Pinpointer/StationMapSystem.cs @@ -1,9 +1,9 @@ using Content.Server.GameTicking; -using Content.Server.GameTicking.Rules; -using Content.Server.GameTicking.Rules.Components; +using Content.Server.Station.Systems; +using Content.Shared.GameTicking.Rules; +using Content.Shared.GameTicking.Rules.Components; using Content.Shared.PowerCell; using Content.Shared.Pinpointer; -using Content.Shared.Station; using Robust.Server.GameObjects; namespace Content.Server.Pinpointer; @@ -12,9 +12,9 @@ public sealed partial class StationMapSystem : EntitySystem { [Dependency] private UserInterfaceSystem _ui = default!; [Dependency] private PowerCellSystem _cell = default!; - [Dependency] private SharedStationSystem _station = default!; + [Dependency] private ServerStationSystem _station = default!; [Dependency] private SharedTransformSystem _xform = default!; - [Dependency] private GameTicker _gameTicker = default!; + [Dependency] private ServerGameTicker _gameTicker = default!; public override void Initialize() { diff --git a/Content.Server/Power/EntitySystems/PowerMonitoringConsoleSystem.cs b/Content.Server/Power/EntitySystems/PowerMonitoringConsoleSystem.cs index 76d3e24aad..ce5dbedb49 100644 --- a/Content.Server/Power/EntitySystems/PowerMonitoringConsoleSystem.cs +++ b/Content.Server/Power/EntitySystems/PowerMonitoringConsoleSystem.cs @@ -239,7 +239,7 @@ internal sealed partial class PowerMonitoringConsoleSystem : SharedPowerMonitori private void OnPowerGridCheckStarted(ref GameRuleStartedEvent ev) { - if (!TryComp(ev.RuleEntity, out var rule)) + if (!TryComp(ev.Rule, out var rule)) return; var query = AllEntityQuery(); @@ -255,7 +255,7 @@ internal sealed partial class PowerMonitoringConsoleSystem : SharedPowerMonitori private void OnPowerGridCheckEnded(ref GameRuleEndedEvent ev) { - if (!TryComp(ev.RuleEntity, out var rule)) + if (!TryComp(ev.Rule, out var rule)) return; var query = AllEntityQuery(); diff --git a/Content.Server/PowerSink/PowerSinkSystem.cs b/Content.Server/PowerSink/PowerSinkSystem.cs index 0213d6926e..6ac4daec65 100644 --- a/Content.Server/PowerSink/PowerSinkSystem.cs +++ b/Content.Server/PowerSink/PowerSinkSystem.cs @@ -31,7 +31,7 @@ namespace Content.Server.PowerSink [Dependency] private ChatSystem _chat = default!; [Dependency] private ExplosionSystem _explosionSystem = default!; [Dependency] private SharedAudioSystem _audio = default!; - [Dependency] private StationSystem _station = default!; + [Dependency] private ServerStationSystem _station = default!; [Dependency] private BatterySystem _battery = default!; public override void Initialize() diff --git a/Content.Server/Procedural/DungeonSystem.cs b/Content.Server/Procedural/DungeonSystem.cs index 4f59df5fd9..77de45baf0 100644 --- a/Content.Server/Procedural/DungeonSystem.cs +++ b/Content.Server/Procedural/DungeonSystem.cs @@ -6,6 +6,7 @@ using Content.Server.GameTicking.Events; using Content.Shared.CCVar; using Content.Shared.Construction.EntitySystems; using Content.Shared.GameTicking; +using Content.Shared.GameTicking.Events; using Content.Shared.Maps; using Content.Shared.Physics; using Content.Shared.Procedural; diff --git a/Content.Server/Respawn/SpecialRespawnSystem.cs b/Content.Server/Respawn/SpecialRespawnSystem.cs index 9ba5dea3ee..671600da47 100644 --- a/Content.Server/Respawn/SpecialRespawnSystem.cs +++ b/Content.Server/Respawn/SpecialRespawnSystem.cs @@ -3,6 +3,7 @@ using Content.Server.Atmos.EntitySystems; using Content.Server.Chat.Managers; using Content.Server.GameTicking; using Content.Shared.Database; +using Content.Shared.GameTicking; using Content.Shared.Maps; using Content.Shared.Physics; using Content.Shared.Respawn; diff --git a/Content.Server/Revenant/EntitySystems/CorporealSystem.cs b/Content.Server/Revenant/EntitySystems/CorporealSystem.cs index e9dc405b53..444c904537 100644 --- a/Content.Server/Revenant/EntitySystems/CorporealSystem.cs +++ b/Content.Server/Revenant/EntitySystems/CorporealSystem.cs @@ -1,5 +1,6 @@ using Content.Server.GameTicking; using Content.Shared.Eye; +using Content.Shared.GameTicking; using Content.Shared.Revenant.Components; using Content.Shared.Revenant.EntitySystems; using Robust.Server.GameObjects; @@ -9,7 +10,7 @@ namespace Content.Server.Revenant.EntitySystems; public sealed partial class CorporealSystem : SharedCorporealSystem { [Dependency] private VisibilitySystem _visibilitySystem = default!; - [Dependency] private GameTicker _ticker = default!; + [Dependency] private ServerGameTicker _ticker = default!; public override void OnStartup(EntityUid uid, CorporealComponent component, ComponentStartup args) { diff --git a/Content.Server/Revenant/EntitySystems/RevenantSystem.cs b/Content.Server/Revenant/EntitySystems/RevenantSystem.cs index e6dae76609..383c9221e6 100644 --- a/Content.Server/Revenant/EntitySystems/RevenantSystem.cs +++ b/Content.Server/Revenant/EntitySystems/RevenantSystem.cs @@ -8,6 +8,7 @@ using Content.Shared.DoAfter; using Content.Shared.Examine; using Content.Shared.Eye; using Content.Shared.FixedPoint; +using Content.Shared.GameTicking; using Content.Shared.Interaction; using Content.Shared.Maps; using Content.Shared.Mobs.Systems; @@ -31,7 +32,7 @@ public sealed partial class RevenantSystem : EntitySystem [Dependency] private AtmosphereSystem _atmosphere = default!; [Dependency] private DamageableSystem _damage = default!; [Dependency] private EntityLookupSystem _lookup = default!; - [Dependency] private GameTicker _ticker = default!; + [Dependency] private ServerGameTicker _ticker = default!; [Dependency] private MobStateSystem _mobState = default!; [Dependency] private PhysicsSystem _physics = default!; [Dependency] private SharedDoAfterSystem _doAfter = default!; @@ -54,7 +55,7 @@ public sealed partial class RevenantSystem : EntitySystem SubscribeLocalEvent(OnExamine); SubscribeLocalEvent(OnStatusAdded); SubscribeLocalEvent(OnStatusEnded); - SubscribeLocalEvent(_ => MakeVisible(true)); + SubscribeLocalEvent(_ => MakeVisible(true)); SubscribeLocalEvent(OnRevenantGetVis); diff --git a/Content.Server/Roles/RoleBriefingSystem.cs b/Content.Server/Roles/RoleBriefingSystem.cs index 12e0a6fb14..ed1474b4e1 100644 --- a/Content.Server/Roles/RoleBriefingSystem.cs +++ b/Content.Server/Roles/RoleBriefingSystem.cs @@ -1,3 +1,4 @@ +using Content.Shared.Roles; using Content.Shared.Roles.Components; namespace Content.Server.Roles; diff --git a/Content.Server/Roles/RoleSystem.cs b/Content.Server/Roles/RoleSystem.cs index a15414ee27..69b105f466 100644 --- a/Content.Server/Roles/RoleSystem.cs +++ b/Content.Server/Roles/RoleSystem.cs @@ -85,42 +85,3 @@ public sealed partial class RoleSystem : SharedRoleSystem } } } - -/// -/// Event raised on the mind to get its briefing. -/// Handlers can either replace or append to the briefing, whichever is more appropriate. -/// -[ByRefEvent] -public sealed class GetBriefingEvent -{ - /// - /// The text that will be shown on the Character Screen - /// - public string? Briefing; - - /// - /// The Mind to whose Mind Role Entities the briefing is sent to - /// - public Entity Mind; - - public GetBriefingEvent(string? briefing = null) - { - Briefing = briefing; - } - - /// - /// If there is no briefing, sets it to the string. - /// If there is a briefing, adds a new line to separate it from the appended string. - /// - public void Append(string text) - { - if (Briefing == null) - { - Briefing = text; - } - else - { - Briefing += "\n" + text; - } - } -} diff --git a/Content.Server/RoundEnd/RoundEndSystem.cs b/Content.Server/RoundEnd/RoundEndSystem.cs index 590140b4ae..5870212dc1 100644 --- a/Content.Server/RoundEnd/RoundEndSystem.cs +++ b/Content.Server/RoundEnd/RoundEndSystem.cs @@ -33,11 +33,11 @@ namespace Content.Server.RoundEnd [Dependency] private IChatManager _chatManager = default!; [Dependency] private IGameTiming _gameTiming = default!; [Dependency] private ChatSystem _chatSystem = default!; - [Dependency] private GameTicker _gameTicker = default!; + [Dependency] private ServerGameTicker _gameTicker = default!; [Dependency] private DeviceNetworkSystem _deviceNetworkSystem = default!; [Dependency] private EmergencyShuttleSystem _shuttle = default!; [Dependency] private SharedAudioSystem _audio = default!; - [Dependency] private StationSystem _stationSystem = default!; + [Dependency] private ServerStationSystem _stationSystem = default!; public TimeSpan DefaultCooldownDuration { get; set; } = TimeSpan.FromSeconds(30); @@ -97,7 +97,7 @@ namespace Content.Server.RoundEnd } /// - /// Attempts to get the MapUid of the station using + /// Attempts to get the MapUid of the station using /// public EntityUid? GetStation() { @@ -394,22 +394,4 @@ namespace Content.Server.RoundEnd { public static RoundEndSystemChangedEvent Default { get; } = new(); } - - public enum RoundEndBehavior : byte - { - /// - /// Instantly end round - /// - InstantEnd, - - /// - /// Call shuttle with custom announcement - /// - ShuttleCall, - - /// - /// Do nothing - /// - Nothing - } } diff --git a/Content.Server/Salvage/JobBoard/SalvageJobBoardSystem.cs b/Content.Server/Salvage/JobBoard/SalvageJobBoardSystem.cs index f2e19d8d63..c37107c77f 100644 --- a/Content.Server/Salvage/JobBoard/SalvageJobBoardSystem.cs +++ b/Content.Server/Salvage/JobBoard/SalvageJobBoardSystem.cs @@ -26,7 +26,7 @@ public sealed partial class SalvageJobBoardSystem : EntitySystem [Dependency] private LabelSystem _label = default!; [Dependency] private PaperSystem _paper = default!; [Dependency] private RadioSystem _radio = default!; - [Dependency] private StationSystem _station = default!; + [Dependency] private ServerStationSystem _station = default!; [Dependency] private UserInterfaceSystem _ui = default!; /// diff --git a/Content.Server/Salvage/SalvageSystem.cs b/Content.Server/Salvage/SalvageSystem.cs index 06171637a8..520b98b93e 100644 --- a/Content.Server/Salvage/SalvageSystem.cs +++ b/Content.Server/Salvage/SalvageSystem.cs @@ -38,7 +38,7 @@ namespace Content.Server.Salvage [Dependency] private SharedMapSystem _mapSystem = default!; [Dependency] private ShuttleSystem _shuttle = default!; [Dependency] private ShuttleConsoleSystem _shuttleConsoles = default!; - [Dependency] private StationSystem _station = default!; + [Dependency] private ServerStationSystem _station = default!; [Dependency] private UserInterfaceSystem _ui = default!; [Dependency] private EntityQuery _gridQuery = default!; diff --git a/Content.Server/Sandbox/SandboxSystem.cs b/Content.Server/Sandbox/SandboxSystem.cs index 1cc4f58d9e..63cba9948e 100644 --- a/Content.Server/Sandbox/SandboxSystem.cs +++ b/Content.Server/Sandbox/SandboxSystem.cs @@ -1,8 +1,10 @@ +using System.Linq; using Content.Server.GameTicking; using Content.Shared.Access; using Content.Shared.Access.Components; using Content.Shared.Access.Systems; using Content.Shared.Containers.ItemSlots; +using Content.Shared.GameTicking; using Content.Shared.Hands.Components; using Content.Shared.Hands.EntitySystems; using Content.Shared.Inventory; @@ -15,7 +17,6 @@ using Robust.Server.Player; using Robust.Shared.Enums; using Robust.Shared.Player; using Robust.Shared.Prototypes; -using System.Linq; namespace Content.Server.Sandbox { @@ -28,7 +29,7 @@ namespace Content.Server.Sandbox [Dependency] private SharedAccessSystem _access = default!; [Dependency] private InventorySystem _inventory = default!; [Dependency] private ItemSlotsSystem _slots = default!; - [Dependency] private GameTicker _ticker = default!; + [Dependency] private ServerGameTicker _ticker = default!; [Dependency] private SharedHandsSystem _handsSystem = default!; private bool _isSandboxEnabled; diff --git a/Content.Server/Shuttles/Components/ShuttleComponent.cs b/Content.Server/Shuttles/Components/ShuttleComponent.cs deleted file mode 100644 index 12123de69a..0000000000 --- a/Content.Server/Shuttles/Components/ShuttleComponent.cs +++ /dev/null @@ -1,79 +0,0 @@ -using System.Numerics; - -namespace Content.Server.Shuttles.Components -{ - [RegisterComponent] - public sealed partial class ShuttleComponent : Component - { - [ViewVariables] - public bool Enabled = true; - - [ViewVariables] - public Vector2[] CenterOfThrust = new Vector2[4]; - - /// - /// Thrust gets multiplied by this value if it's for braking. - /// - public const float BrakeCoefficient = 1.5f; - - /// - /// Maximum velocity. - /// - [DataField] - public float BaseMaxLinearVelocity = 60f; - - public const float MaxAngularVelocity = 4f; - - /// - /// The cached thrust available for each cardinal direction - /// - [ViewVariables] - public readonly float[] LinearThrust = new float[4]; - - /// - /// The thrusters contributing to each direction for impulse. - /// - // No touchy - public readonly List[] LinearThrusters = new List[] - { - new(), - new(), - new(), - new(), - }; - - /// - /// The thrusters contributing to the angular impulse of the shuttle. - /// - public readonly List AngularThrusters = new(); - - [ViewVariables] - public float AngularThrust = 0f; - - /// - /// A bitmask of all the directions we are considered thrusting. - /// - [ViewVariables] - public DirectionFlag ThrustDirections = DirectionFlag.None; - - /// - /// Base damping modifier applied to the shuttle's physics component when not in FTL. - /// - [DataField] - public float BodyModifier = 0.25f; - - /// - /// Final Damping Modifier for a shuttle. - /// This value is set to 0 during FTL. And to BodyModifier when not in FTL. - /// - [DataField] - public float DampingModifier; - - /// - /// Optional override for the FTL cooldown for this shuttle. - /// If not null, then the value will be used instead of the shuttle.cooldown CCVar. - /// - [DataField] - public TimeSpan? FTLCooldownOverride = null; - } -} diff --git a/Content.Server/Shuttles/Systems/EmergencyShuttleSystem.Console.cs b/Content.Server/Shuttles/Systems/EmergencyShuttleSystem.Console.cs index 2d7e6feefc..5202162273 100644 --- a/Content.Server/Shuttles/Systems/EmergencyShuttleSystem.Console.cs +++ b/Content.Server/Shuttles/Systems/EmergencyShuttleSystem.Console.cs @@ -27,11 +27,6 @@ namespace Content.Server.Shuttles.Systems; /// public sealed partial class EmergencyShuttleSystem { - /// - /// Has the emergency shuttle arrived? - /// - public bool EmergencyShuttleArrived { get; private set; } - public bool EarlyLaunchAuthorized { get; private set; } /// diff --git a/Content.Server/Shuttles/Systems/EmergencyShuttleSystem.cs b/Content.Server/Shuttles/Systems/EmergencyShuttleSystem.cs index a6105f3e58..05309ff14a 100644 --- a/Content.Server/Shuttles/Systems/EmergencyShuttleSystem.cs +++ b/Content.Server/Shuttles/Systems/EmergencyShuttleSystem.cs @@ -21,6 +21,7 @@ using Content.Shared.CCVar; using Content.Shared.Database; using Content.Shared.DeviceNetwork.Components; using Content.Shared.GameTicking; +using Content.Shared.GameTicking.Events; using Content.Shared.Localizations; using Content.Shared.RoundEnd; using Content.Shared.Shuttles.Components; @@ -56,7 +57,7 @@ public sealed partial class EmergencyShuttleSystem : SharedEmergencyShuttleSyste [Dependency] private CommunicationsConsoleSystem _commsConsole = default!; [Dependency] private DeviceNetworkSystem _deviceNetworkSystem = default!; [Dependency] private DockingSystem _dock = default!; - [Dependency] private GameTicker _ticker = default!; + [Dependency] private ServerGameTicker _ticker = default!; [Dependency] private IdCardSystem _idSystem = default!; [Dependency] private NavMapSystem _navMap = default!; [Dependency] private MapLoaderSystem _loader = default!; @@ -64,7 +65,7 @@ public sealed partial class EmergencyShuttleSystem : SharedEmergencyShuttleSyste [Dependency] private RoundEndSystem _roundEnd = default!; [Dependency] private SharedAudioSystem _audio = default!; [Dependency] private ShuttleSystem _shuttle = default!; - [Dependency] private StationSystem _station = default!; + [Dependency] private ServerStationSystem _station = default!; [Dependency] private TransformSystem _transformSystem = default!; [Dependency] private UserInterfaceSystem _uiSystem = default!; @@ -119,15 +120,6 @@ public sealed partial class EmergencyShuttleSystem : SharedEmergencyShuttleSyste component.MapEntity = null; } - /// - /// Attempts to get the EntityUid of the emergency shuttle - /// - public EntityUid? GetShuttle() - { - AllEntityQuery().MoveNext(out var shuttle, out _); - return shuttle; - } - private void SetEmergencyShuttleEnabled(bool value) { if (_emergencyShuttleEnabled == value) diff --git a/Content.Server/Shuttles/Systems/ArrivalsSystem.cs b/Content.Server/Shuttles/Systems/ServerArrivalsSystem.cs similarity index 70% rename from Content.Server/Shuttles/Systems/ArrivalsSystem.cs rename to Content.Server/Shuttles/Systems/ServerArrivalsSystem.cs index e79a0d6b08..51e8a1b3e4 100644 --- a/Content.Server/Shuttles/Systems/ArrivalsSystem.cs +++ b/Content.Server/Shuttles/Systems/ServerArrivalsSystem.cs @@ -1,32 +1,30 @@ using System.Linq; using System.Numerics; using Content.Server.Administration; -using Content.Server.Antag; using Content.Server.Chat.Managers; using Content.Server.DeviceNetwork.Systems; -using Content.Server.GameTicking; -using Content.Server.GameTicking.Events; using Content.Server.Parallax; using Content.Server.Shuttles.Components; using Content.Server.Shuttles.Events; -using Content.Server.Spawners.Components; -using Content.Server.Spawners.EntitySystems; using Content.Server.Station.Events; -using Content.Server.Station.Systems; using Content.Shared.Administration; +using Content.Shared.Antag; using Content.Shared.CCVar; using Content.Shared.Damage.Components; using Content.Shared.DeviceNetwork.Components; using Content.Shared.GameTicking; +using Content.Shared.GameTicking.Events; using Content.Shared.Mobs.Components; using Content.Shared.Movement.Components; using Content.Shared.Parallax.Biomes; using Content.Shared.RoundEnd; using Content.Shared.Salvage; using Content.Shared.Shuttles.Components; +using Content.Shared.Shuttles.Systems; +using Content.Shared.Spawners.Components; +using Content.Shared.Station.Systems; using Content.Shared.Tiles; using Robust.Shared.Collections; -using Robust.Shared.Configuration; using Robust.Shared.Console; using Robust.Shared.EntitySerialization.Systems; using Robust.Shared.Map; @@ -34,47 +32,29 @@ using Robust.Shared.Player; using Robust.Shared.Prototypes; using Robust.Shared.Random; using Robust.Shared.Spawners; -using Robust.Shared.Timing; using Robust.Shared.Utility; namespace Content.Server.Shuttles.Systems; -/// -/// If enabled spawns players on a separate arrivals station before they can transfer to the main station. -/// -public sealed partial class ArrivalsSystem : EntitySystem +/// +public sealed partial class ServerArrivalsSystem : ArrivalsSystem { [Dependency] private IChatManager _chat = default!; - [Dependency] private IConfigurationManager _cfgManager = default!; [Dependency] private IConsoleHost _console = default!; - [Dependency] private IGameTiming _timing = default!; - [Dependency] private IRobustRandom _random = default!; [Dependency] private ActorSystem _actor = default!; + [Dependency] private AntagSelectionSystem _antag = default!; [Dependency] private BiomeSystem _biomes = default!; [Dependency] private DeviceNetworkSystem _deviceNetworkSystem = default!; - [Dependency] private GameTicker _ticker = default!; [Dependency] private MapLoaderSystem _loader = default!; [Dependency] private MetaDataSystem _metaData = default!; + [Dependency] private ShuttleSystem _shuttles = default!; [Dependency] private SharedMapSystem _mapSystem = default!; [Dependency] private SharedTransformSystem _transform = default!; - [Dependency] private ShuttleSystem _shuttles = default!; - [Dependency] private StationSpawningSystem _stationSpawning = default!; [Dependency] private StationSystem _station = default!; - [Dependency] private AntagSelectionSystem _antag = default!; - [Dependency] private EntityQuery _pendingQuery = default!; - [Dependency] private EntityQuery _blacklistQuery = default!; - [Dependency] private EntityQuery _mobQuery = default!; - - /// - /// If enabled then spawns players on an alternate map so they can take a shuttle to the station. - /// - public bool Enabled { get; private set; } - - /// - /// Flags if all players spawning at the departure terminal have godmode until they leave the terminal. - /// - public bool ArrivalsGodmode { get; private set; } + [Dependency] private EntityQuery _pendingQuery; + [Dependency] private EntityQuery _blacklistQuery; + [Dependency] private EntityQuery _mobQuery; /// /// The first arrival is a little early, to save everyone 10s @@ -92,396 +72,18 @@ public sealed partial class ArrivalsSystem : EntitySystem { base.Initialize(); - SubscribeLocalEvent(HandlePlayerSpawning, before: new []{ typeof(SpawnPointSystem)}, after: new [] { typeof(ContainerSpawnPointSystem)}); - - SubscribeLocalEvent(OnStationPostInit); - - SubscribeLocalEvent(OnShuttleStartup); - SubscribeLocalEvent(OnShuttleTag); - - SubscribeLocalEvent(OnRoundStarting); - SubscribeLocalEvent(OnArrivalsFTL); - SubscribeLocalEvent(OnArrivalsDocked); - - SubscribeLocalEvent(SendDirections); - - // Don't invoke immediately as it will get set in the natural course of things. - Enabled = _cfgManager.GetCVar(CCVars.ArrivalsShuttles); - ArrivalsGodmode = _cfgManager.GetCVar(CCVars.GodmodeArrivals); - - _cfgManager.OnValueChanged(CCVars.ArrivalsShuttles, SetArrivals); - _cfgManager.OnValueChanged(CCVars.GodmodeArrivals, b => ArrivalsGodmode = b); + Cfg.OnValueChanged(CCVars.ArrivalsShuttles, SetArrivals); // Command so admins can set these for funsies _console.RegisterCommand("arrivals", ArrivalsCommand, ArrivalsCompletion); } - private void OnShuttleTag(EntityUid uid, ArrivalsShuttleComponent component, ref FTLTagEvent args) - { - if (args.Handled) - return; - - // Just saves mappers forgetting. (v2 boogaloo) - args.Handled = true; - args.Tag = "DockArrivals"; - } - - private CompletionResult ArrivalsCompletion(IConsoleShell shell, string[] args) - { - if (args.Length != 1) - return CompletionResult.Empty; - - return new CompletionResult(new CompletionOption[] - { - // Enables and disable are separate comms in case you don't want to accidentally toggle it, compared to - // returns which doesn't have an immediate effect - new("enable", Loc.GetString("cmd-arrivals-enable-hint")), - new("disable", Loc.GetString("cmd-arrivals-disable-hint")), - new("returns", Loc.GetString("cmd-arrivals-returns-hint")), - new ("force", Loc.GetString("cmd-arrivals-force-hint")) - }, "Option"); - } - - [AdminCommand(AdminFlags.Fun)] - private void ArrivalsCommand(IConsoleShell shell, string argstr, string[] args) - { - if (args.Length != 1) - { - shell.WriteError(Loc.GetString("cmd-arrivals-invalid")); - return; - } - - switch (args[0]) - { - case "enable": - _cfgManager.SetCVar(CCVars.ArrivalsShuttles, true); - break; - case "disable": - _cfgManager.SetCVar(CCVars.ArrivalsShuttles, false); - break; - case "returns": - var existing = _cfgManager.GetCVar(CCVars.ArrivalsReturns); - _cfgManager.SetCVar(CCVars.ArrivalsReturns, !existing); - shell.WriteLine(Loc.GetString("cmd-arrivals-returns", ("value", !existing))); - break; - case "force": - var query = AllEntityQuery(); - var spawnPoints = EntityQuery().ToList(); - - TryGetArrivals(out var arrivalsUid); - - while (query.MoveNext(out var uid, out _, out var pendingXform)) - { - _random.Shuffle(spawnPoints); - - foreach (var (point, xform) in spawnPoints) - { - if (point.SpawnType != SpawnPointType.LateJoin || xform.GridUid == arrivalsUid) - continue; - - _transform.SetCoordinates(uid, pendingXform, xform.Coordinates); - break; - } - - RemCompDeferred(uid); - RemCompDeferred(uid); - shell.WriteLine(Loc.GetString("cmd-arrivals-forced", ("uid", ToPrettyString(uid)))); - } - break; - default: - shell.WriteError(Loc.GetString($"cmd-arrivals-invalid")); - break; - } - } - - /// - /// First sends shuttle timer data, then kicks people off the shuttle if it isn't leaving the arrivals terminal - /// - private void OnArrivalsFTL(EntityUid shuttleUid, ArrivalsShuttleComponent component, ref FTLStartedEvent args) - { - if (!TryGetArrivals(out EntityUid arrivals)) - return; - - if (TryComp(shuttleUid, out var netComp)) - { - TryComp(shuttleUid, out var ftlComp); - var ftlTime = TimeSpan.FromSeconds(ftlComp?.TravelTime ?? _shuttles.DefaultTravelTime); - - var payload = new ScreenShuttlePayload - { - Shuttle = shuttleUid, - ShuttleTime = ftlTime, - }; - - // unfortunate levels of spaghetti due to roundstart arrivals ftl behavior - EntityUid? sourceMap; - var arrivalsDelay = _cfgManager.GetCVar(CCVars.ArrivalsCooldown); - - if (component.FirstRun) - { - var station = _station.GetLargestGrid(component.Station); - sourceMap = station == null ? null : Transform(station.Value)?.MapUid; - arrivalsDelay += RoundStartFTLDuration; - component.FirstRun = false; - payload.DestinationMap = Transform(args.TargetCoordinates.EntityId).MapUid; - payload.DestinationTime = ftlTime; - } - else - sourceMap = args.FromMapUid; - - payload.SourceMap = sourceMap; - payload.SourceTime = ftlTime + TimeSpan.FromSeconds(arrivalsDelay); - - _deviceNetworkSystem.SendPacket(shuttleUid, null, ref payload, netComp.TransmitFrequency); - } - - // Don't do anything here when leaving arrivals. - var arrivalsMapUid = Transform(arrivals).MapUid; - if (args.FromMapUid == arrivalsMapUid) - return; - - // Any mob then yeet them off the shuttle. - if (!_cfgManager.GetCVar(CCVars.ArrivalsReturns) && args.FromMapUid != null) - DumpChildren(shuttleUid, ref args); - - var pendingQuery = AllEntityQuery(); - - // We're heading from the station back to arrivals (if leaving arrivals, would have returned above). - // Process everyone who holds a PendingClockInComponent - // Note, due to way DumpChildren works, anyone who doesn't have a PendingClockInComponent gets left in space - // and will not warp. This is intended behavior. - while (pendingQuery.MoveNext(out var pUid, out _, out var xform)) - { - if (xform.GridUid == shuttleUid) - { - // Warp all players who are still on this shuttle to a spawn point. This doesn't let them return to - // arrivals. It also ensures noobs, slow players or AFK players safely leave the shuttle. - TryTeleportToMapSpawn(pUid, component.Station, xform); - } - - // Players who have remained at arrivals keep their warp coupon (PendingClockInComponent) for now. - if (xform.MapUid == arrivalsMapUid) - continue; - - // The player has successfully left arrivals and is also not on the shuttle. Remove their warp coupon. - RemCompDeferred(pUid); - RemCompDeferred(pUid); - - if (ArrivalsGodmode) - RemCompDeferred(pUid); - - if (_actor.TryGetSession(pUid, out var session) && session is not null) - _antag.TryMakeLateJoinAntag(session); - } - } - - private void OnArrivalsDocked(EntityUid uid, ArrivalsShuttleComponent component, ref FTLCompletedEvent args) - { - var dockTime = component.NextTransfer - _timing.CurTime + TimeSpan.FromSeconds(_shuttles.DefaultStartupTime); - - if (TryComp(uid, out var netComp)) - { - var payload = new ScreenShuttlePayload - { - Shuttle = uid, - ShuttleTime = dockTime, - SourceMap = args.MapUid, - SourceTime = dockTime, - Docked = true, - }; - _deviceNetworkSystem.SendPacket(uid, null, ref payload, netComp.TransmitFrequency); - } - } - - private void DumpChildren(EntityUid uid, ref FTLStartedEvent args) - { - var toDump = new List>(); - FindDumpChildren(uid, toDump); - foreach (var (ent, xform) in toDump) - { - var rotation = xform.LocalRotation; - _transform.SetCoordinates(ent, new EntityCoordinates(args.FromMapUid!.Value, Vector2.Transform(xform.LocalPosition, args.FTLFrom))); - _transform.SetWorldRotation(ent, args.FromRotation + rotation); - if (_actor.TryGetSession(ent, out var session)) - { - _chat.DispatchServerMessage(session!, Loc.GetString("latejoin-arrivals-dumped-from-shuttle")); - } - } - } - - private void FindDumpChildren(EntityUid uid, List> toDump) - { - if (_pendingQuery.HasComponent(uid)) - return; - - var xform = Transform(uid); - - if (_mobQuery.HasComponent(uid) || _blacklistQuery.HasComponent(uid)) - { - toDump.Add((uid, xform)); - return; - } - - var children = xform.ChildEnumerator; - while (children.MoveNext(out var child)) - { - FindDumpChildren(child, toDump); - } - } - - public void HandlePlayerSpawning(PlayerSpawningEvent ev) - { - if (ev.SpawnResult != null) - return; - - // We use arrivals as the default spawn so don't check for job prio. - - // Only works on latejoin even if enabled. - if (!Enabled || _ticker.RunLevel != GameRunLevel.InRound) - return; - - if (!HasComp(ev.Station)) - return; - - TryGetArrivals(out var arrivals); - - if (!TryComp(arrivals, out TransformComponent? arrivalsXform)) - return; - - var mapId = arrivalsXform.MapID; - - var points = EntityQueryEnumerator(); - var possiblePositions = new List(); - while (points.MoveNext(out var uid, out var spawnPoint, out var xform)) - { - if (spawnPoint.SpawnType != SpawnPointType.LateJoin || xform.MapID != mapId) - continue; - - possiblePositions.Add(xform.Coordinates); - } - - if (possiblePositions.Count <= 0) - return; - - var spawnLoc = _random.Pick(possiblePositions); - ev.SpawnResult = _stationSpawning.SpawnPlayerMob( - spawnLoc, - ev.Job, - ev.HumanoidCharacterProfile, - ev.Station); - - EnsureComp(ev.SpawnResult.Value); - EnsureComp(ev.SpawnResult.Value); - - // If you're forced to spawn, you're invincible until you leave wherever you were forced to spawn. - if (ArrivalsGodmode) - EnsureComp(ev.SpawnResult.Value); - } - - private void SendDirections(PlayerSpawnCompleteEvent ev) - { - if (!Enabled || !ev.LateJoin || ev.Silent || !_pendingQuery.HasComp(ev.Mob)) - return; - - var arrival = NextShuttleArrival(); - - var message = arrival is not null - ? Loc.GetString("latejoin-arrivals-direction-time", ("time", $"{arrival:mm\\:ss}")) - : Loc.GetString("latejoin-arrivals-direction"); - - _chat.DispatchServerMessage(ev.Player, message); - } - - private bool TryTeleportToMapSpawn(EntityUid player, EntityUid stationId, TransformComponent? transform = null) - { - if (!Resolve(player, ref transform)) - return false; - - var points = EntityQueryEnumerator(); - var possiblePositions = new ValueList(32); - - // Find a spawnpoint on the same map as the player is already docked with now. - while (points.MoveNext(out var uid, out var spawnPoint, out var xform)) - { - if (spawnPoint.SpawnType == SpawnPointType.LateJoin && - _station.GetOwningStation(uid, xform) == stationId) - { - // Add to list of possible spawn locations - possiblePositions.Add(xform.Coordinates); - } - } - - if (possiblePositions.Count > 0) - { - // Move the player to a random late-join spawnpoint. - _transform.SetCoordinates(player, transform, _random.Pick(possiblePositions)); - if (_actor.TryGetSession(player, out var session)) - { - _chat.DispatchServerMessage(session!, Loc.GetString("latejoin-arrivals-teleport-to-spawn")); - } - return true; - } - - return false; - } - - private void OnShuttleStartup(EntityUid uid, ArrivalsShuttleComponent component, ComponentStartup args) - { - EnsureComp(uid); - } - - private bool TryGetArrivals(out EntityUid uid) - { - var arrivalsQuery = EntityQueryEnumerator(); - - while (arrivalsQuery.MoveNext(out uid, out _)) - { - return true; - } - - return false; - } - - /// - /// Check if an entity is on the arrivals grid. - /// - /// Entity to check. - /// True if the entity is on the arrivals grid. Returns false if not on arrivals, or there is no arrivals grid. - public bool IsOnArrivals(Entity entity) - { - if (!Resolve(entity, ref entity.Comp)) - return false; - - if (!TryGetArrivals(out var arrivals)) - return false; - - var arrivalsGridUid = Transform(arrivals).GridUid; - if (!arrivalsGridUid.HasValue) - return false; - - return entity.Comp.GridUid == Transform(arrivals).GridUid; - } - - public TimeSpan? NextShuttleArrival() - { - var query = EntityQueryEnumerator(); - var time = TimeSpan.MaxValue; - while (query.MoveNext(out var uid, out var comp)) - { - if (comp.NextArrivalsTime < time) - time = comp.NextArrivalsTime; - } - - var duration = _timing.CurTime; - return (time < duration) ? null : time - duration; - } - public override void Update(float frameTime) { base.Update(frameTime); var query = EntityQueryEnumerator(); - var curTime = _timing.CurTime; + var curTime = Timing.CurTime; TryGetArrivals(out var arrivals); if (TryComp(arrivals, out TransformComponent? arrivalsXform)) @@ -499,7 +101,7 @@ public sealed partial class ArrivalsSystem : EntitySystem if (arrivals.IsValid()) _shuttles.FTLToDock(uid, shuttle, arrivals); - comp.NextArrivalsTime = _timing.CurTime + TimeSpan.FromSeconds(tripTime); + comp.NextArrivalsTime = Timing.CurTime + TimeSpan.FromSeconds(tripTime); } // Go to station else @@ -511,27 +113,18 @@ public sealed partial class ArrivalsSystem : EntitySystem // The ArrivalsCooldown includes the trip there, so we only need to add the time taken for // the trip back. - comp.NextArrivalsTime = _timing.CurTime + TimeSpan.FromSeconds( - _cfgManager.GetCVar(CCVars.ArrivalsCooldown) + tripTime); + comp.NextArrivalsTime = Timing.CurTime + TimeSpan.FromSeconds( + Cfg.GetCVar(CCVars.ArrivalsCooldown) + tripTime); } - comp.NextTransfer += TimeSpan.FromSeconds(_cfgManager.GetCVar(CCVars.ArrivalsCooldown)); + comp.NextTransfer += TimeSpan.FromSeconds(Cfg.GetCVar(CCVars.ArrivalsCooldown)); } } } - private void OnRoundStarting(RoundStartingEvent ev) - { - // Setup arrivals station - if (!Enabled) - return; - - SetupArrivalsStation(); - } - private void SetupArrivalsStation() { - var path = new ResPath(_cfgManager.GetCVar(CCVars.ArrivalsMap)); + var path = new ResPath(Cfg.GetCVar(CCVars.ArrivalsMap)); _mapSystem.CreateMap(out var mapId, runMapInit: false); var mapUid = _mapSystem.GetMap(mapId); @@ -545,9 +138,9 @@ public sealed partial class ArrivalsSystem : EntitySystem EnsureComp(grid.Value); // Setup planet arrivals if relevant - if (_cfgManager.GetCVar(CCVars.ArrivalsPlanet)) + if (Cfg.GetCVar(CCVars.ArrivalsPlanet)) { - var template = _random.Pick(_arrivalsBiomeOptions); + var template = Random.Pick(_arrivalsBiomeOptions); _biomes.EnsurePlanet(mapUid, ProtoMan.Index(template)); var restricted = new RestrictedRangeComponent { @@ -599,15 +192,6 @@ public sealed partial class ArrivalsSystem : EntitySystem } } - private void OnStationPostInit(EntityUid uid, StationArrivalsComponent component, ref StationPostInitEvent args) - { - if (!Enabled) - return; - - // If it's a latespawn station then this will fail but that's okey - SetupShuttle(uid, component); - } - private void SetupShuttle(EntityUid uid, StationArrivalsComponent component) { if (!Deleted(component.Shuttle)) @@ -625,11 +209,297 @@ public sealed partial class ArrivalsSystem : EntitySystem arrivalsComp.Station = uid; EnsureComp(uid); _shuttles.FTLToDock(component.Shuttle, shuttleComp, arrivals, hyperspaceTime: RoundStartFTLDuration); - arrivalsComp.NextTransfer = _timing.CurTime + TimeSpan.FromSeconds(_cfgManager.GetCVar(CCVars.ArrivalsCooldown)); + arrivalsComp.NextTransfer = Timing.CurTime + TimeSpan.FromSeconds(Cfg.GetCVar(CCVars.ArrivalsCooldown)); } // Don't start the arrivals shuttle immediately docked so power has a time to stabilise? var timer = AddComp(dummpMapEntity); timer.Lifetime = 15f; } + + [SubscribeLocalEvent] + private void OnRoundStarting(RoundStartingEvent ev) + { + // Setup arrivals station + if (!Enabled) + return; + + SetupArrivalsStation(); + } + + [SubscribeLocalEvent] + private void OnStationPostInit(EntityUid uid, StationArrivalsComponent component, ref StationPostInitEvent args) + { + if (!Enabled) + return; + + // If it's a latespawn station then this will fail but that's okey + SetupShuttle(uid, component); + } + + /// + /// First sends shuttle timer data, then kicks people off the shuttle if it isn't leaving the arrivals terminal + /// + [SubscribeLocalEvent] + private void OnArrivalsFTL(EntityUid shuttleUid, ArrivalsShuttleComponent component, ref FTLStartedEvent args) + { + if (!TryGetArrivals(out EntityUid arrivals)) + return; + + if (TryComp(shuttleUid, out var netComp)) + { + TryComp(shuttleUid, out var ftlComp); + var ftlTime = TimeSpan.FromSeconds(ftlComp?.TravelTime ?? _shuttles.DefaultTravelTime); + + var payload = new ScreenShuttlePayload + { + Shuttle = shuttleUid, + ShuttleTime = ftlTime, + }; + + // unfortunate levels of spaghetti due to roundstart arrivals ftl behavior + EntityUid? sourceMap; + var arrivalsDelay = Cfg.GetCVar(CCVars.ArrivalsCooldown); + + if (component.FirstRun) + { + var station = _station.GetLargestGrid(component.Station); + sourceMap = station == null ? null : Transform(station.Value).MapUid; + arrivalsDelay += RoundStartFTLDuration; + component.FirstRun = false; + payload.DestinationMap = Transform(args.TargetCoordinates.EntityId).MapUid; + payload.DestinationTime = ftlTime; + } + else + sourceMap = args.FromMapUid; + + payload.SourceMap = sourceMap; + payload.SourceTime = ftlTime + TimeSpan.FromSeconds(arrivalsDelay); + + _deviceNetworkSystem.SendPacket(shuttleUid, null, ref payload, netComp.TransmitFrequency); + } + + // Don't do anything here when leaving arrivals. + var arrivalsMapUid = Transform(arrivals).MapUid; + if (args.FromMapUid == arrivalsMapUid) + return; + + // Any mob then yeet them off the shuttle. + if (!Cfg.GetCVar(CCVars.ArrivalsReturns) && args.FromMapUid != null) + DumpChildren(shuttleUid, ref args); + + var pendingQuery = AllEntityQuery(); + + // We're heading from the station back to arrivals (if leaving arrivals, would have returned above). + // Process everyone who holds a PendingClockInComponent + // Note, due to way DumpChildren works, anyone who doesn't have a PendingClockInComponent gets left in space + // and will not warp. This is intended behavior. + while (pendingQuery.MoveNext(out var pUid, out _, out var xform)) + { + if (xform.GridUid == shuttleUid) + { + // Warp all players who are still on this shuttle to a spawn point. This doesn't let them return to + // arrivals. It also ensures noobs, slow players or AFK players safely leave the shuttle. + TryTeleportToMapSpawn(pUid, component.Station, xform); + } + + // Players who have remained at arrivals keep their warp coupon (PendingClockInComponent) for now. + if (xform.MapUid == arrivalsMapUid) + continue; + + // The player has successfully left arrivals and is also not on the shuttle. Remove their warp coupon. + RemCompDeferred(pUid); + RemCompDeferred(pUid); + + if (ArrivalsGodmode) + RemCompDeferred(pUid); + + if (_actor.TryGetSession(pUid, out var session) && session is not null) + _antag.TryMakeLateJoinAntag(session); + } + } + + [SubscribeLocalEvent] + private void OnArrivalsDocked(EntityUid uid, ArrivalsShuttleComponent component, ref FTLCompletedEvent args) + { + var dockTime = component.NextTransfer - Timing.CurTime + TimeSpan.FromSeconds(_shuttles.DefaultStartupTime); + + if (TryComp(uid, out var netComp)) + { + var payload = new ScreenShuttlePayload + { + Shuttle = uid, + ShuttleTime = dockTime, + SourceMap = args.MapUid, + SourceTime = dockTime, + Docked = true, + }; + _deviceNetworkSystem.SendPacket(uid, null, ref payload, netComp.TransmitFrequency); + } + } + + private void DumpChildren(EntityUid uid, ref FTLStartedEvent args) + { + var toDump = new List>(); + FindDumpChildren(uid, toDump); + foreach (var (ent, xform) in toDump) + { + var rotation = xform.LocalRotation; + _transform.SetCoordinates(ent, new EntityCoordinates(args.FromMapUid!.Value, Vector2.Transform(xform.LocalPosition, args.FTLFrom))); + _transform.SetWorldRotation(ent, args.FromRotation + rotation); + if (_actor.TryGetSession(ent, out var session)) + { + _chat.DispatchServerMessage(session!, Loc.GetString("latejoin-arrivals-dumped-from-shuttle")); + } + } + } + + + private void FindDumpChildren(EntityUid uid, List> toDump) + { + if (_pendingQuery.HasComponent(uid)) + return; + + var xform = Transform(uid); + + if (_mobQuery.HasComponent(uid) || _blacklistQuery.HasComponent(uid)) + { + toDump.Add((uid, xform)); + return; + } + + var children = xform.ChildEnumerator; + while (children.MoveNext(out var child)) + { + FindDumpChildren(child, toDump); + } + } + + [SubscribeLocalEvent] + private void OnShuttleTag(EntityUid uid, ArrivalsShuttleComponent component, ref FTLTagEvent args) + { + if (args.Handled) + return; + + // Just saves mappers forgetting. (v2 boogaloo) + args.Handled = true; + args.Tag = "DockArrivals"; + } + + [SubscribeLocalEvent] + private void SendDirections(PlayerSpawnCompleteEvent ev) + { + if (!Enabled || !ev.LateJoin || ev.Silent || !_pendingQuery.HasComp(ev.Mob)) + return; + + var arrival = NextShuttleArrival(); + + var message = arrival is not null + ? Loc.GetString("latejoin-arrivals-direction-time", ("time", $"{arrival:mm\\:ss}")) + : Loc.GetString("latejoin-arrivals-direction"); + + _chat.DispatchServerMessage(ev.Player, message); + } + + private bool TryTeleportToMapSpawn(EntityUid player, EntityUid stationId, TransformComponent? transform = null) + { + if (!Resolve(player, ref transform)) + return false; + + var points = EntityQueryEnumerator(); + var possiblePositions = new ValueList(32); + + // Find a spawnpoint on the same map as the player is already docked with now. + while (points.MoveNext(out var uid, out var spawnPoint, out var xform)) + { + if (spawnPoint.SpawnType == SpawnPointType.LateJoin && + _station.GetOwningStation(uid, xform) == stationId) + { + // Add to list of possible spawn locations + possiblePositions.Add(xform.Coordinates); + } + } + + if (possiblePositions.Count > 0) + { + // Move the player to a random late-join spawnpoint. + _transform.SetCoordinates(player, transform, Random.Pick(possiblePositions)); + if (_actor.TryGetSession(player, out var session)) + { + _chat.DispatchServerMessage(session!, Loc.GetString("latejoin-arrivals-teleport-to-spawn")); + } + return true; + } + + return false; + } + + [AdminCommand(AdminFlags.Fun)] + private void ArrivalsCommand(IConsoleShell shell, string argstr, string[] args) + { + if (args.Length != 1) + { + shell.WriteError(Loc.GetString("cmd-arrivals-invalid")); + return; + } + + switch (args[0]) + { + case "enable": + Cfg.SetCVar(CCVars.ArrivalsShuttles, true); + break; + case "disable": + Cfg.SetCVar(CCVars.ArrivalsShuttles, false); + break; + case "returns": + var existing = Cfg.GetCVar(CCVars.ArrivalsReturns); + Cfg.SetCVar(CCVars.ArrivalsReturns, !existing); + shell.WriteLine(Loc.GetString("cmd-arrivals-returns", ("value", !existing))); + break; + case "force": + var query = AllEntityQuery(); + var spawnPoints = EntityQuery().ToList(); + + TryGetArrivals(out var arrivalsUid); + + while (query.MoveNext(out var uid, out _, out var pendingXform)) + { + Random.Shuffle(spawnPoints); + + foreach (var (point, xform) in spawnPoints) + { + if (point.SpawnType != SpawnPointType.LateJoin || xform.GridUid == arrivalsUid) + continue; + + _transform.SetCoordinates(uid, pendingXform, xform.Coordinates); + break; + } + + RemCompDeferred(uid); + RemCompDeferred(uid); + shell.WriteLine(Loc.GetString("cmd-arrivals-forced", ("uid", ToPrettyString(uid)))); + } + break; + default: + shell.WriteError(Loc.GetString($"cmd-arrivals-invalid")); + break; + } + } + + private CompletionResult ArrivalsCompletion(IConsoleShell shell, string[] args) + { + if (args.Length != 1) + return CompletionResult.Empty; + + return new CompletionResult(new CompletionOption[] + { + // Enables and disable are separate comms in case you don't want to accidentally toggle it, compared to + // returns which doesn't have an immediate effect + new("enable", Loc.GetString("cmd-arrivals-enable-hint")), + new("disable", Loc.GetString("cmd-arrivals-disable-hint")), + new("returns", Loc.GetString("cmd-arrivals-returns-hint")), + new ("force", Loc.GetString("cmd-arrivals-force-hint")) + }, + "Option"); + } } diff --git a/Content.Server/Shuttles/Systems/ShuttleConsoleSystem.FTL.cs b/Content.Server/Shuttles/Systems/ShuttleConsoleSystem.FTL.cs index d9ac71cc87..df62c8927b 100644 --- a/Content.Server/Shuttles/Systems/ShuttleConsoleSystem.FTL.cs +++ b/Content.Server/Shuttles/Systems/ShuttleConsoleSystem.FTL.cs @@ -1,5 +1,6 @@ using Content.Server.Shuttles.Components; using Content.Server.Shuttles.Events; +using Content.Shared.Shuttles.Components; using Content.Shared.Shuttles.Events; using Content.Shared.Shuttles.UI.MapObjects; using Robust.Shared.Map; diff --git a/Content.Server/Shuttles/Systems/ShuttleConsoleSystem.cs b/Content.Server/Shuttles/Systems/ShuttleConsoleSystem.cs index 1933c71cf3..d89533892a 100644 --- a/Content.Server/Shuttles/Systems/ShuttleConsoleSystem.cs +++ b/Content.Server/Shuttles/Systems/ShuttleConsoleSystem.cs @@ -32,7 +32,7 @@ public sealed partial class ShuttleConsoleSystem : SharedShuttleConsoleSystem [Dependency] private SharedPopupSystem _popup = default!; [Dependency] private SharedTransformSystem _transform = default!; [Dependency] private ShuttleSystem _shuttle = default!; - [Dependency] private StationSystem _station = default!; + [Dependency] private ServerStationSystem _station = default!; [Dependency] private TagSystem _tags = default!; [Dependency] private UserInterfaceSystem _ui = default!; [Dependency] private SharedContentEyeSystem _eyeSystem = default!; diff --git a/Content.Server/Shuttles/Systems/ShuttleSystem.Impact.cs b/Content.Server/Shuttles/Systems/ShuttleSystem.Impact.cs index df9ac8c551..6b90d7dce6 100644 --- a/Content.Server/Shuttles/Systems/ShuttleSystem.Impact.cs +++ b/Content.Server/Shuttles/Systems/ShuttleSystem.Impact.cs @@ -1,4 +1,3 @@ -using Content.Server.Shuttles.Components; using Content.Shared.Atmos.Components; using Content.Shared.Audio; using Content.Shared.CCVar; @@ -6,7 +5,6 @@ using Content.Shared.Damage; using Content.Shared.Database; using Content.Shared.Maps; using Content.Shared.Physics; -using Content.Shared.Projectiles; using Robust.Shared.Audio; using Robust.Shared.Map; using Robust.Shared.Map.Components; @@ -18,6 +16,7 @@ using Robust.Shared.Prototypes; using Robust.Shared.Random; using System.Numerics; using Content.Shared.Damage.Components; +using Content.Shared.Shuttles.Components; namespace Content.Server.Shuttles.Systems; diff --git a/Content.Server/Shuttles/Systems/ShuttleSystem.cs b/Content.Server/Shuttles/Systems/ShuttleSystem.cs index 965636ec39..1216b8cd74 100644 --- a/Content.Server/Shuttles/Systems/ShuttleSystem.cs +++ b/Content.Server/Shuttles/Systems/ShuttleSystem.cs @@ -2,7 +2,6 @@ using Content.Server.Administration.Logs; using Content.Server.Buckle.Systems; using Content.Server.Parallax; using Content.Server.Procedural; -using Content.Server.Shuttles.Components; using Content.Server.Shuttles.Events; using Content.Server.Station.Systems; using Content.Server.Stunnable; @@ -10,8 +9,10 @@ using Content.Shared.Buckle.Components; using Content.Shared.Damage.Systems; using Content.Shared.Gibbing; using Content.Shared.Light.Components; +using Content.Shared.Maps; using Content.Shared.Movement.Events; using Content.Shared.Salvage; +using Content.Shared.Shuttles.Components; using Content.Shared.Shuttles.Systems; using Content.Shared.Throwing; using JetBrains.Annotations; @@ -26,7 +27,6 @@ using Robust.Shared.Physics.Components; using Robust.Shared.Physics.Systems; using Robust.Shared.Random; using Robust.Shared.Timing; -using Content.Shared.Maps; namespace Content.Server.Shuttles.Systems; @@ -53,7 +53,7 @@ public sealed partial class ShuttleSystem : SharedShuttleSystem [Dependency] private SharedTransformSystem _transform = default!; [Dependency] private SharedSalvageSystem _salvage = default!; [Dependency] private ShuttleConsoleSystem _console = default!; - [Dependency] private StationSystem _station = default!; + [Dependency] private ServerStationSystem _station = default!; [Dependency] private StunSystem _stuns = default!; [Dependency] private ThrowingSystem _throwing = default!; [Dependency] private ThrusterSystem _thruster = default!; diff --git a/Content.Server/Shuttles/Systems/StationAnchorSystem.cs b/Content.Server/Shuttles/Systems/StationAnchorSystem.cs index e12c4075d4..2b0d138b74 100644 --- a/Content.Server/Shuttles/Systems/StationAnchorSystem.cs +++ b/Content.Server/Shuttles/Systems/StationAnchorSystem.cs @@ -3,6 +3,7 @@ using Content.Server.Power.EntitySystems; using Content.Server.Shuttles.Components; using Content.Shared.Construction.Components; using Content.Shared.Popups; +using Content.Shared.Shuttles.Components; namespace Content.Server.Shuttles.Systems; diff --git a/Content.Server/Silicons/Laws/IonLawSystem.cs b/Content.Server/Silicons/Laws/IonLawSystem.cs index b657d21b06..7726814658 100644 --- a/Content.Server/Silicons/Laws/IonLawSystem.cs +++ b/Content.Server/Silicons/Laws/IonLawSystem.cs @@ -1,10 +1,10 @@ using Content.Shared.Dataset; using Content.Shared.Silicons.Laws; -using Content.Shared.Station; using Content.Shared.StationRecords; using Robust.Shared.Prototypes; using Robust.Shared.Random; using System.Linq; +using Content.Server.Station.Systems; using Content.Shared.StationRecords.Components; using Content.Shared.StationRecords.Systems; @@ -16,7 +16,7 @@ namespace Content.Server.Silicons.Laws; public sealed partial class IonLawSystem : EntitySystem { [Dependency] private IRobustRandom _random = default!; - [Dependency] private SharedStationSystem _stationSystem = default!; + [Dependency] private ServerStationSystem _stationSystem = default!; [Dependency] private StationRecordsSystem _stationRecordsSystem = default!; private ISawmill _sawmill = default!; diff --git a/Content.Server/Silicons/Laws/SiliconLawSystem.cs b/Content.Server/Silicons/Laws/SiliconLawSystem.cs index 739577804f..31a4d511e3 100644 --- a/Content.Server/Silicons/Laws/SiliconLawSystem.cs +++ b/Content.Server/Silicons/Laws/SiliconLawSystem.cs @@ -31,7 +31,7 @@ public sealed partial class SiliconLawSystem : SharedSiliconLawSystem [Dependency] private IChatManager _chatManager = default!; [Dependency] private SharedMindSystem _mind = default!; [Dependency] private SharedRoleSystem _roles = default!; - [Dependency] private StationSystem _station = default!; + [Dependency] private ServerStationSystem _station = default!; [Dependency] private UserInterfaceSystem _userInterface = default!; [Dependency] private EmagSystem _emag = default!; [Dependency] private ISharedAdminLogManager _adminLogger = default!; diff --git a/Content.Server/Silicons/StationAi/StationAiSystem.cs b/Content.Server/Silicons/StationAi/StationAiSystem.cs index 3e2efa0189..871d320c27 100644 --- a/Content.Server/Silicons/StationAi/StationAiSystem.cs +++ b/Content.Server/Silicons/StationAi/StationAiSystem.cs @@ -7,8 +7,6 @@ using Content.Server.Ghost.Roles.Components; using Content.Server.Mind; using Content.Server.Power.Components; using Content.Server.Roles; -using Content.Server.Spawners.Components; -using Content.Server.Spawners.EntitySystems; using Content.Server.Station.Systems; using Content.Shared.Alert; using Content.Shared.Chat.Prototypes; @@ -27,6 +25,8 @@ using Content.Shared.Power.Components; using Content.Shared.Rejuvenate; using Content.Shared.Roles; using Content.Shared.Silicons.StationAi; +using Content.Shared.Spawners.Components; +using Content.Shared.Spawners.EntitySystems; using Content.Shared.Speech.Components; using Content.Shared.StationAi; using Content.Shared.Turrets; @@ -55,8 +55,8 @@ public sealed partial class StationAiSystem : SharedStationAiSystem [Dependency] private SharedBatterySystem _battery = default!; [Dependency] private DamageableSystem _damageable = default!; [Dependency] private SharedPopupSystem _popups = default!; - [Dependency] private StationSystem _station = default!; - [Dependency] private StationJobsSystem _stationJobs = default!; + [Dependency] private ServerStationSystem _station = default!; + [Dependency] private ServerStationJobsSystem _stationJobs = default!; [Dependency] private MobStateSystem _mobState = default!; [Dependency] private SharedAppearanceSystem _appearance = default!; diff --git a/Content.Server/Spawners/EntitySystems/ConditionalSpawnerSystem.cs b/Content.Server/Spawners/EntitySystems/ConditionalSpawnerSystem.cs index d745beee2d..07cbf27dd3 100644 --- a/Content.Server/Spawners/EntitySystems/ConditionalSpawnerSystem.cs +++ b/Content.Server/Spawners/EntitySystems/ConditionalSpawnerSystem.cs @@ -24,7 +24,7 @@ namespace Content.Server.Spawners.EntitySystems; public sealed partial class ConditionalSpawnerSystem : EntitySystem { [Dependency] private IRobustRandom _robustRandom = default!; - [Dependency] private GameTicker _ticker = default!; + [Dependency] private ServerGameTicker _ticker = default!; [Dependency] private EntityTableSystem _entityTable = default!; [Dependency] private StackSystem _stack = default!; [Dependency] private TransformSystem _xform = default!; @@ -61,9 +61,12 @@ public sealed partial class ConditionalSpawnerSystem : EntitySystem } } - public void RuleStarted(EntityUid uid, ConditionalSpawnerComponent component, GameRuleStartedEvent obj) + public void RuleStarted(EntityUid uid, ConditionalSpawnerComponent component, GameRuleStartedEvent args) { - if (component.GameRules.Contains(obj.RuleId)) + if (args.RuleId is not { } id) + return; + + if (component.GameRules.Contains(id)) Spawn(uid, component); } diff --git a/Content.Server/Station/Commands/JobsCommand.cs b/Content.Server/Station/Commands/JobsCommand.cs index c971a1cac3..3a21074516 100644 --- a/Content.Server/Station/Commands/JobsCommand.cs +++ b/Content.Server/Station/Commands/JobsCommand.cs @@ -13,12 +13,12 @@ namespace Content.Server.Station.Commands; [ToolshedCommand, AdminCommand(AdminFlags.VarEdit)] public sealed class JobsCommand : ToolshedCommand { - private StationJobsSystem? _jobs; + private ServerStationJobsSystem? _jobs; [CommandImplementation("jobs")] public IEnumerable Jobs([PipedArgument] EntityUid station) { - _jobs ??= GetSys(); + _jobs ??= GetSys(); foreach (var (job, _) in _jobs.GetJobs(station)) { @@ -33,7 +33,7 @@ public sealed class JobsCommand : ToolshedCommand [CommandImplementation("job")] public JobSlotRef Job([PipedArgument] EntityUid station, ProtoId job) { - _jobs ??= GetSys(); + _jobs ??= GetSys(); return new JobSlotRef(job.Id, station, _jobs, EntityManager); } @@ -53,7 +53,7 @@ public sealed class JobsCommand : ToolshedCommand [CommandImplementation("adjust")] public JobSlotRef Adjust([PipedArgument] JobSlotRef @ref, int by) { - _jobs ??= GetSys(); + _jobs ??= GetSys(); _jobs.TryAdjustJobSlot(@ref.Station, @ref.Job, by, true, true); return @ref; } @@ -66,7 +66,7 @@ public sealed class JobsCommand : ToolshedCommand [CommandImplementation("set")] public JobSlotRef Set([PipedArgument] JobSlotRef @ref, int by) { - _jobs ??= GetSys(); + _jobs ??= GetSys(); _jobs.TrySetJobSlot(@ref.Station, @ref.Job, by, true); return @ref; } @@ -78,7 +78,7 @@ public sealed class JobsCommand : ToolshedCommand [CommandImplementation("amount")] public int Amount([PipedArgument] JobSlotRef @ref) { - _jobs ??= GetSys(); + _jobs ??= GetSys(); _jobs.TryGetJobSlot(@ref.Station, @ref.Job, out var slots); return slots ?? 0; } @@ -89,7 +89,7 @@ public sealed class JobsCommand : ToolshedCommand } // Used for Toolshed queries. -public readonly record struct JobSlotRef(string Job, EntityUid Station, StationJobsSystem Jobs, IEntityManager EntityManager) +public readonly record struct JobSlotRef(string Job, EntityUid Station, ServerStationJobsSystem Jobs, IEntityManager EntityManager) { public override string ToString() { diff --git a/Content.Server/Station/Commands/StationCommand.cs b/Content.Server/Station/Commands/StationCommand.cs index f9dcb83d93..b3acb07d06 100644 --- a/Content.Server/Station/Commands/StationCommand.cs +++ b/Content.Server/Station/Commands/StationCommand.cs @@ -15,13 +15,13 @@ namespace Content.Server.Station.Commands; [ToolshedCommand, AdminCommand(AdminFlags.Admin)] public sealed class StationsCommand : ToolshedCommand { - private StationSystem? _station; + private ServerStationSystem? _station; private CargoSystem? _cargo; [CommandImplementation("list")] public IEnumerable List() { - _station ??= GetSys(); + _station ??= GetSys(); return _station.GetStationsSet(); } @@ -29,7 +29,7 @@ public sealed class StationsCommand : ToolshedCommand [CommandImplementation("get")] public EntityUid Get(IInvocationContext ctx) { - _station ??= GetSys(); + _station ??= GetSys(); var set = _station.GetStationsSet(); if (set.Count > 1 || set.Count == 0) @@ -45,7 +45,7 @@ public sealed class StationsCommand : ToolshedCommand [CommandImplementation("getowningstation")] public EntityUid? GetOwningStation([PipedArgument] EntityUid input) { - _station ??= GetSys(); + _station ??= GetSys(); return _station.GetOwningStation(input); } @@ -53,7 +53,7 @@ public sealed class StationsCommand : ToolshedCommand [CommandImplementation("largestgrid")] public EntityUid? LargestGrid([PipedArgument] EntityUid input) { - _station ??= GetSys(); + _station ??= GetSys(); return _station.GetLargestGrid(input); } @@ -81,21 +81,21 @@ public sealed class StationsCommand : ToolshedCommand [CommandImplementation("addgrid")] public void AddGrid([PipedArgument] EntityUid input, EntityUid grid) { - _station ??= GetSys(); + _station ??= GetSys(); _station.AddGridToStation(input, grid); } [CommandImplementation("rmgrid")] public void RmGrid([PipedArgument] EntityUid input, EntityUid grid) { - _station ??= GetSys(); + _station ??= GetSys(); _station.RemoveGridFromStation(input, grid); } [CommandImplementation("rename")] public void Rename([PipedArgument] EntityUid input, string name) { - _station ??= GetSys(); + _station ??= GetSys(); _station.RenameStation(input, name); } diff --git a/Content.Server/Station/Systems/StationJobsSystem.Roundstart.cs b/Content.Server/Station/Systems/ServerStationJobsSystem.Roundstart.cs similarity index 99% rename from Content.Server/Station/Systems/StationJobsSystem.Roundstart.cs rename to Content.Server/Station/Systems/ServerStationJobsSystem.Roundstart.cs index b0c015f154..df72bf86cd 100644 --- a/Content.Server/Station/Systems/StationJobsSystem.Roundstart.cs +++ b/Content.Server/Station/Systems/ServerStationJobsSystem.Roundstart.cs @@ -1,8 +1,8 @@ using System.Linq; using Content.Server.Administration.Managers; -using Content.Server.Antag; using Content.Server.Station.Components; using Content.Server.Station.Events; +using Content.Shared.Antag; using Content.Shared.CCVar; using Content.Shared.Preferences; using Content.Shared.Roles; @@ -16,7 +16,7 @@ using Robust.Shared.Utility; namespace Content.Server.Station.Systems; // Contains code for round-start spawning. -public sealed partial class StationJobsSystem +public sealed partial class ServerStationJobsSystem { [Dependency] private IBanManager _banManager = default!; [Dependency] private SharedJobSystem _jobs = default!; diff --git a/Content.Server/Station/Systems/StationJobsSystem.cs b/Content.Server/Station/Systems/ServerStationJobsSystem.cs similarity index 93% rename from Content.Server/Station/Systems/StationJobsSystem.cs rename to Content.Server/Station/Systems/ServerStationJobsSystem.cs index daeca7dce3..c7e6083ec7 100644 --- a/Content.Server/Station/Systems/StationJobsSystem.cs +++ b/Content.Server/Station/Systems/ServerStationJobsSystem.cs @@ -1,14 +1,13 @@ using System.Diagnostics.CodeAnalysis; using System.Linq; using Content.Server.GameTicking; -using Content.Server.Station.Components; using Content.Server.Station.Events; using Content.Shared.CCVar; -using Content.Shared.FixedPoint; using Content.Shared.GameTicking; using Content.Shared.Preferences; using Content.Shared.Roles; using Content.Shared.Station.Components; +using Content.Shared.Station.Systems; using JetBrains.Annotations; using Robust.Server.Player; using Robust.Shared.Configuration; @@ -19,16 +18,14 @@ using Robust.Shared.Random; namespace Content.Server.Station.Systems; -/// -/// Manages job slots for stations. -/// +/// [PublicAPI] -public sealed partial class StationJobsSystem : EntitySystem +public sealed partial class ServerStationJobsSystem : StationJobsSystem { [Dependency] private IConfigurationManager _configurationManager = default!; [Dependency] private IPlayerManager _player = default!; [Dependency] private IRobustRandom _random = default!; - [Dependency] private GameTicker _gameTicker = default!; + [Dependency] private ServerGameTicker _gameTicker = default!; /// public override void Initialize() @@ -91,7 +88,7 @@ public sealed partial class StationJobsSystem : EntitySystem #region Public API - /// + /// /// Station to assign a job on. /// Job to assign. /// The net user ID of the player we're assigning this job to. @@ -123,15 +120,14 @@ public sealed partial class StationJobsSystem : EntitySystem return true; } - /// + /// /// Station to adjust the job slot on. /// Job to adjust. /// Amount to adjust by. /// Whether or not it should create the slot if it doesn't exist. /// Whether or not to clamp to zero if you'd remove more jobs than are available. /// Resolve pattern, station jobs component of the station. - public bool TryAdjustJobSlot(EntityUid station, JobPrototype job, int amount, bool createSlot = false, bool clamp = false, - StationJobsComponent? stationJobs = null) + public bool TryAdjustJobSlot(EntityUid station, JobPrototype job, int amount, bool createSlot = false, bool clamp = false, StationJobsComponent? stationJobs = null) { return TryAdjustJobSlot(station, job.ID, amount, createSlot, clamp, stationJobs); } @@ -219,15 +215,14 @@ public sealed partial class StationJobsSystem : EntitySystem return jobsComponent.PlayerJobs.Remove(userId); } - /// + /// /// Station to adjust the job slot on. /// Job prototype to adjust. /// Amount to set to. /// Whether or not it should create the slot if it doesn't exist. /// Resolve pattern, station jobs component of the station. /// - public bool TrySetJobSlot(EntityUid station, JobPrototype jobPrototype, int amount, bool createSlot = false, - StationJobsComponent? stationJobs = null) + public bool TrySetJobSlot(EntityUid station, JobPrototype jobPrototype, int amount, bool createSlot = false, StationJobsComponent? stationJobs = null) { return TrySetJobSlot(station, jobPrototype.ID, amount, createSlot, stationJobs); } @@ -273,7 +268,7 @@ public sealed partial class StationJobsSystem : EntitySystem } } - /// + /// /// Station to make a job unlimited on. /// Job to make unlimited. /// Resolve pattern, station jobs component of the station. @@ -303,7 +298,7 @@ public sealed partial class StationJobsSystem : EntitySystem UpdateJobsAvailable(); } - /// + /// /// Station to check. /// Job to check. /// Resolve pattern, station jobs component of the station. @@ -328,11 +323,7 @@ public sealed partial class StationJobsSystem : EntitySystem return stationJobs.JobList.TryGetValue(jobPrototypeId, out var job) && job == null; } - /// - /// Station to get slot info from. - /// Job to get slot info for. - /// The number of slots remaining. Null if infinite. - /// Resolve pattern, station jobs component of the station. + /// public bool TryGetJobSlot(EntityUid station, JobPrototype job, out int? slots, StationJobsComponent? stationJobs = null) { return TryGetJobSlot(station, job.ID, out slots, stationJobs); @@ -506,7 +497,8 @@ public sealed partial class StationJobsSystem : EntitySystem var list = comp.JobList.ToDictionary(x => x.Key, x => x.Value); jobs.Add(netStation, list); stationNames.Add(netStation, Name(station)); - jobWeights.Add(netStation, TryComp(station, out var stationData) + jobWeights.Add(netStation, + TryComp(station, out var stationData) ? stationData.JobWeights : null); } diff --git a/Content.Server/Station/Systems/StationSystem.cs b/Content.Server/Station/Systems/ServerStationSystem.cs similarity index 79% rename from Content.Server/Station/Systems/StationSystem.cs rename to Content.Server/Station/Systems/ServerStationSystem.cs index cd2e5d0313..05fc8bf52c 100644 --- a/Content.Server/Station/Systems/StationSystem.cs +++ b/Content.Server/Station/Systems/ServerStationSystem.cs @@ -1,8 +1,11 @@ +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Linq; +using Content.Server.Atmos.EntitySystems; using Content.Server.Chat.Systems; using Content.Server.GameTicking; -using Content.Server.Station.Components; using Content.Server.Station.Events; +using Content.Shared.GameTicking; using Content.Shared.Station; using Content.Shared.Station.Components; using JetBrains.Annotations; @@ -22,13 +25,12 @@ namespace Content.Server.Station.Systems; /// For jobs, look at StationJobSystem. For spawning, look at StationSpawningSystem. /// [PublicAPI] -public sealed partial class StationSystem : SharedStationSystem +public sealed partial class ServerStationSystem : Shared.Station.Systems.StationSystem { [Dependency] private IPlayerManager _player = default!; + [Dependency] private AtmosphereSystem _atmos = default!; [Dependency] private ChatSystem _chatSystem = default!; - [Dependency] private MetaDataSystem _metaData = default!; [Dependency] private PvsOverrideSystem _pvsOverride = default!; - [Dependency] private EntityQuery _gridQuery = default!; private ISawmill _sawmill = default!; @@ -234,7 +236,7 @@ public sealed partial class StationSystem : SharedStationSystem throw new ArgumentException("Tried to use a non-station entity as a station!", nameof(station)); if (!string.IsNullOrEmpty(name)) - _metaData.SetEntityName(mapGrid, name); + Meta.SetEntityName(mapGrid, name); var stationMember = EnsureComp(mapGrid); stationMember.Station = station; @@ -285,7 +287,7 @@ public sealed partial class StationSystem : SharedStationSystem throw new ArgumentException("Tried to use a non-station entity as a station!", nameof(station)); var oldName = metaData.EntityName; - _metaData.SetEntityName(station, name, metaData); + Meta.SetEntityName(station, name, metaData); if (loud) { @@ -308,6 +310,104 @@ public sealed partial class StationSystem : SharedStationSystem QueueDel(station); } + + /// + public override bool TryFindRandomTileOnStation(Entity station, + out Vector2i tile, + [NotNullWhen(true)] out Entity? targetGrid, + out EntityCoordinates targetCoords, + int numAttempts = 10) + { + tile = default; + targetCoords = EntityCoordinates.Invalid; + targetGrid = null; + + // Weight grid choice by tilecount + var totalTiles = 0; + var grids = new List<(Entity Entity, int Count, List Tiles)>(); + foreach (var possibleTarget in station.Comp.Grids) + { + if (!GridQuery.TryComp(possibleTarget, out var comp)) + continue; + + // Get the tile count for the given grid. + var tileCount = Map.GetFilledTileCount((possibleTarget, comp)); + + // Just to be sure, no empty elements. + if (tileCount > 0) + { + grids.Add(((possibleTarget, comp), tileCount, new())); + totalTiles += tileCount; + } + } + + if (grids.Count == 0) + return false; + + for (var i = 0; i < numAttempts; i++) + { + // Find random tile within list. + var nextTileIndex = Random.Next(totalTiles); + TileRef? randomTileRef = null; + var startIndex = 0; + for (int j = 0; j < grids.Count; j++) + { + var grid = grids[j]; + // If the index is in this particular grid, find it and remove the tile to prevent selecting it twice. + if (nextTileIndex >= startIndex + grid.Count) + { + startIndex += grid.Count; + continue; + } + + targetGrid = grid.Entity; + + // Empty list: hasn't been queried yet - get our tiles. + if (grid.Tiles.Count <= 0) + { + grid.Tiles = Map.GetAllTiles(targetGrid.Value, targetGrid.Value.Comp).ToList(); + + // Actual list count doesn't match expected count (a bug - return failure). + Debug.Assert(grid.Tiles.Count == grid.Count); + if (grid.Tiles.Count != grid.Count) + return false; + } + + var ourTileIndex = nextTileIndex - startIndex; + randomTileRef = grid.Tiles[ourTileIndex]; + grid.Tiles.RemoveSwap(ourTileIndex); + grid.Count--; + totalTiles--; + + // Empty list, remove element + if (grid.Tiles.Count <= 0) + grids.RemoveSwap(j); + + break; + } + + // Out of valid tiles, return early. + if (randomTileRef is not { } tileRef) + return false; + + // Could not find a grid... + if (targetGrid == null) + continue; + + // Invalid tile, try again. + if (_atmos.IsTileSpace(targetGrid.Value.Owner, Transform(targetGrid.Value).MapUid, tileRef.GridIndices) + || _atmos.IsTileAirBlockedCached(targetGrid.Value.Owner, tileRef.GridIndices)) + { + continue; + } + + targetCoords = Map.GridTileToLocal(targetGrid.Value, targetGrid.Value, tileRef.GridIndices); + tile = tileRef.GridIndices; + return true; + } + + return false; + } } /// diff --git a/Content.Server/Station/Systems/StationBiomeSystem.cs b/Content.Server/Station/Systems/StationBiomeSystem.cs index 29d6a1af25..0d2a879892 100644 --- a/Content.Server/Station/Systems/StationBiomeSystem.cs +++ b/Content.Server/Station/Systems/StationBiomeSystem.cs @@ -7,7 +7,7 @@ namespace Content.Server.Station.Systems; public sealed partial class StationBiomeSystem : EntitySystem { [Dependency] private BiomeSystem _biome = default!; - [Dependency] private StationSystem _station = default!; + [Dependency] private ServerStationSystem _station = default!; [Dependency] private SharedMapSystem _map = default!; public override void Initialize() diff --git a/Content.Server/Station/Systems/StationNameSystem.cs b/Content.Server/Station/Systems/StationNameSystem.cs index c64d036a85..0bca57bdcc 100644 --- a/Content.Server/Station/Systems/StationNameSystem.cs +++ b/Content.Server/Station/Systems/StationNameSystem.cs @@ -8,7 +8,7 @@ namespace Content.Server.Station.Systems; /// public sealed partial class StationNameSystem : EntitySystem { - [Dependency] private StationSystem _station = default!; + [Dependency] private ServerStationSystem _station = default!; /// public override void Initialize() diff --git a/Content.Server/StationEvents/BasicStationEventSchedulerSystem.cs b/Content.Server/StationEvents/BasicStationEventSchedulerSystem.cs index e88a333ead..93e1a6e87c 100644 --- a/Content.Server/StationEvents/BasicStationEventSchedulerSystem.cs +++ b/Content.Server/StationEvents/BasicStationEventSchedulerSystem.cs @@ -1,11 +1,11 @@ using System.Linq; using Content.Server.Administration; using Content.Server.GameTicking; -using Content.Server.GameTicking.Rules; using Content.Server.StationEvents.Components; using Content.Shared.Administration; using Content.Shared.EntityTable; using Content.Shared.GameTicking.Components; +using Content.Shared.GameTicking.Rules; using JetBrains.Annotations; using Robust.Shared.Prototypes; using Robust.Shared.Random; @@ -30,10 +30,9 @@ namespace Content.Server.StationEvents component.TimeUntilNextEvent = RobustRandom.NextFloat(component.MinimumTimeUntilFirstEvent, component.MinimumTimeUntilFirstEvent + component.MaximumSpanUntilFirstEvent); } - protected override void Ended(EntityUid uid, BasicStationEventSchedulerComponent component, GameRuleComponent gameRule, - GameRuleEndedEvent args) + protected override void Ended(Entity rule, ref GameRuleEndedEvent args) { - component.TimeUntilNextEvent = component.MinimumTimeUntilFirstEvent; + rule.Comp.TimeUntilNextEvent = rule.Comp.MinimumTimeUntilFirstEvent; } @@ -47,7 +46,7 @@ namespace Content.Server.StationEvents var query = EntityQueryEnumerator(); while (query.MoveNext(out var uid, out var eventScheduler, out var gameRule)) { - if (!GameTicker.IsGameRuleActive(uid, gameRule)) + if (!GameTicker.IsGameRuleActive((uid, gameRule))) continue; if (eventScheduler.TimeUntilNextEvent > 0) @@ -86,7 +85,7 @@ namespace Content.Server.StationEvents /// /// /// This isn't perfect. Code path eventually goes into , which requires - /// state from . As a result, you should probably just run this locally and not doing + /// state from . As a result, you should probably just run this locally and not doing /// a real round (it won't pollute the state, but it will get contaminated by previously ran events in the actual round) /// and things like `MaxOccurrences` and `ReoccurrenceDelay` won't be respected. /// diff --git a/Content.Server/StationEvents/Components/MeteorSwarmComponent.cs b/Content.Server/StationEvents/Components/MeteorSwarmComponent.cs index 1cde2ac8fa..c5c95468fd 100644 --- a/Content.Server/StationEvents/Components/MeteorSwarmComponent.cs +++ b/Content.Server/StationEvents/Components/MeteorSwarmComponent.cs @@ -11,6 +11,12 @@ public sealed partial class MeteorSwarmComponent : Component [DataField, AutoPausedField] public TimeSpan NextWaveTime; + /// + /// Grid which this game rule is targeting. + /// + [DataField] + public EntityUid? TargetGrid; + /// /// We'll send a specific amount of waves of meteors towards the station per ending rather than using a timer. /// diff --git a/Content.Server/StationEvents/EventManagerSystem.cs b/Content.Server/StationEvents/EventManagerSystem.cs index 8531ebbb91..e7f2a1921b 100644 --- a/Content.Server/StationEvents/EventManagerSystem.cs +++ b/Content.Server/StationEvents/EventManagerSystem.cs @@ -9,6 +9,7 @@ using Robust.Shared.Prototypes; using Robust.Shared.Random; using Content.Shared.EntityTable.EntitySelectors; using Content.Shared.EntityTable; +using Content.Shared.GameTicking.Components; namespace Content.Server.StationEvents; @@ -18,7 +19,7 @@ public sealed partial class EventManagerSystem : EntitySystem [Dependency] private IPlayerManager _playerManager = default!; [Dependency] private IRobustRandom _random = default!; [Dependency] private EntityTableSystem _entityTable = default!; - [Dependency] public GameTicker GameTicker = default!; + [Dependency] private ServerGameTicker _gameTicker = default!; [Dependency] private RoundEndSystem _roundEnd = default!; public bool EventsEnabled { get; private set; } @@ -67,7 +68,7 @@ public sealed partial class EventManagerSystem : EntitySystem return; } - GameTicker.AddGameRule(randomLimitedEvent); + _gameTicker.AddGameRule(randomLimitedEvent); } /// @@ -105,7 +106,7 @@ public sealed partial class EventManagerSystem : EntitySystem playerCount ??= _playerManager.PlayerCount; // playerCount does a lock so we'll just keep the variable here - currentTime ??= GameTicker.RoundDuration(); + currentTime ??= _gameTicker.RoundDuration(); var totalWeight = 0f; @@ -158,11 +159,11 @@ public sealed partial class EventManagerSystem : EntitySystem playerCount ??= _playerManager.PlayerCount; // playerCount does a lock so we'll just keep the variable here - currentTime ??= GameTicker.RoundDuration(); + currentTime ??= _gameTicker.RoundDuration(); foreach (var eventid in selectedEvents) { - if (GameTicker.IsIgnored(eventid)) + if (_gameTicker.IsIgnored(eventid)) continue; if (!ProtoMan.Resolve(eventid, out var eventproto)) @@ -250,7 +251,7 @@ public sealed partial class EventManagerSystem : EntitySystem var playerCount = playerCountOverride ?? _playerManager.PlayerCount; // playerCount does a lock so we'll just keep the variable here - var currentTime = currentTimeOverride ?? GameTicker.RoundDuration(); + var currentTime = currentTimeOverride ?? _gameTicker.RoundDuration(); var result = new Dictionary(); @@ -294,59 +295,36 @@ public sealed partial class EventManagerSystem : EntitySystem return allEvents; } - private int GetOccurrences(EntityPrototype stationEvent) - { - return GetOccurrences(stationEvent.ID); - } - - private int GetOccurrences(string stationEvent) - { - return GameTicker.AllPreviousGameRules.Count(p => p.Item2 == stationEvent); - } - - public TimeSpan TimeSinceLastEvent(EntityPrototype stationEvent) - { - foreach (var (time, rule) in GameTicker.AllPreviousGameRules.Reverse()) - { - if (rule == stationEvent.ID) - return time; - } - - return TimeSpan.Zero; - } - + // TODO: WRITE A TEST TO ENSURE THAT IF A EVENT HAS MAX OCCURRENCES, THAT IT WILL PROPERLY CANCEL ONLY WHEN THEY'RE HIT private bool CanRun(EntityPrototype prototype, StationEventComponent stationEvent, int playerCount, TimeSpan currentTime) { - if (GameTicker.IsGameRuleActive(prototype.ID)) - return false; - - if (stationEvent.MaxOccurrences.HasValue && GetOccurrences(prototype) >= stationEvent.MaxOccurrences.Value) - { - return false; - } - + // Do the really simple comparisons BEFORE we create an IEnumerable for GameRules :V if (playerCount < stationEvent.MinimumPlayers) - { return false; - } if (currentTime != TimeSpan.Zero && currentTime.TotalMinutes < stationEvent.EarliestStart) - { return false; + + // Slightly slower if we don't care about MaxOccurrences, but that's not a huge issue in the context of the event scheduler. + var count = 0; + var lastRun = TimeSpan.Zero; + var ruleQuery = EntityQueryEnumerator(); + while (ruleQuery.MoveNext(out var rule, out var meta)) + { + if (meta.EntityPrototype?.Name != prototype.ID) + continue; + + count++; + if (lastRun < rule.ActivatedAt) + lastRun = rule.ActivatedAt; } - var lastRun = TimeSinceLastEvent(prototype); - if (lastRun != TimeSpan.Zero && currentTime.TotalMinutes < - stationEvent.ReoccurrenceDelay + lastRun.TotalMinutes) - { + if (stationEvent.MaxOccurrences.HasValue && count >= stationEvent.MaxOccurrences.Value) return false; - } - if (_roundEnd.IsRoundEndRequested() && !stationEvent.OccursDuringRoundEnd && !_roundEnd.CanCallOrRecall()) - { + if (lastRun != TimeSpan.Zero && currentTime.TotalMinutes < stationEvent.ReoccurrenceDelay + lastRun.TotalMinutes) return false; - } - return true; + return !_roundEnd.IsRoundEndRequested() || stationEvent.OccursDuringRoundEnd || _roundEnd.CanCallOrRecall(); } } diff --git a/Content.Server/StationEvents/Events/AlertLevelInterceptionRule.cs b/Content.Server/StationEvents/Events/AlertLevelInterceptionRule.cs index d1b039bdaa..141d4220a5 100644 --- a/Content.Server/StationEvents/Events/AlertLevelInterceptionRule.cs +++ b/Content.Server/StationEvents/Events/AlertLevelInterceptionRule.cs @@ -13,15 +13,15 @@ public sealed partial class AlertLevelInterceptionRule : StationEventSystem(chosenStation, out var stationData)) return; - var grid = StationSystem.GetLargestGrid((chosenStation.Value, stationData)); + var grid = Station.GetLargestGrid((chosenStation.Value, stationData)); if (grid is null) return; diff --git a/Content.Server/StationEvents/Events/BluespaceArtifactRule.cs b/Content.Server/StationEvents/Events/BluespaceArtifactRule.cs index 600f54d80d..a3b19d7e42 100644 --- a/Content.Server/StationEvents/Events/BluespaceArtifactRule.cs +++ b/Content.Server/StationEvents/Events/BluespaceArtifactRule.cs @@ -25,7 +25,7 @@ public sealed partial class BluespaceArtifactRule : StationEventSystem _whitelist.IsWhitelistFailOrNull(component.Blacklist, uid))) + if (!Station.TryGetRandomStation(out var chosenStation, uid => _whitelist.IsWhitelistFailOrNull(component.Blacklist, uid))) return; var stationApcs = new List>(); var query = EntityQueryEnumerator(); while (query.MoveNext(out var apcUid, out var apc, out var xform)) { - if (apc.MainBreakerEnabled && CompOrNull(xform.GridUid)?.Station == chosenStation) + if (apc.MainBreakerEnabled && CompOrNull(xform.GridUid)?.Station == chosenStation.Value.Owner) { stationApcs.Add((apcUid, apc)); } diff --git a/Content.Server/StationEvents/Events/BureaucraticErrorRule.cs b/Content.Server/StationEvents/Events/BureaucraticErrorRule.cs index 49a2220b03..bb740312d7 100644 --- a/Content.Server/StationEvents/Events/BureaucraticErrorRule.cs +++ b/Content.Server/StationEvents/Events/BureaucraticErrorRule.cs @@ -1,9 +1,7 @@ using System.Linq; -using Content.Server.Station.Components; using Content.Server.Station.Systems; using Content.Server.StationEvents.Components; using Content.Shared.GameTicking.Components; -using Content.Shared.Roles; using JetBrains.Annotations; using Robust.Shared.Random; @@ -12,13 +10,13 @@ namespace Content.Server.StationEvents.Events; [UsedImplicitly] public sealed partial class BureaucraticErrorRule : StationEventSystem { - [Dependency] private StationJobsSystem _stationJobs = default!; + [Dependency] private ServerStationJobsSystem _stationJobs = default!; protected override void Started(EntityUid uid, BureaucraticErrorRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args) { base.Started(uid, component, gameRule, args); - if (!TryGetRandomStation(out var chosenStation, HasComp)) + if (!Station.TryGetRandomStation(out var chosenStation, HasComp)) return; var jobList = _stationJobs.GetJobs(chosenStation.Value).Keys.ToList(); diff --git a/Content.Server/StationEvents/Events/CargoGiftsRule.cs b/Content.Server/StationEvents/Events/CargoGiftsRule.cs index ca6019d806..8a4992fbd5 100644 --- a/Content.Server/StationEvents/Events/CargoGiftsRule.cs +++ b/Content.Server/StationEvents/Events/CargoGiftsRule.cs @@ -13,18 +13,21 @@ public sealed partial class CargoGiftsRule : StationEventSystem rule, ref GameRuleAddedEvent args) { - if (!TryComp(uid, out var stationEvent)) + if (!TryComp(rule, out var stationEvent)) return; - var str = Loc.GetString(component.Announce, - ("sender", Loc.GetString(component.Sender)), ("description", Loc.GetString(component.Description)), ("dest", Loc.GetString(component.Dest))); + // TODO: One must question, why we're saving it to this component and forcing each system to TryComp twice instead of just calling a method other than "Added" + // TODO: Also realistically this should be called on "Started" not added... + // TODO: Second PR energy... + var str = Loc.GetString(rule.Comp1.Announce, + ("sender", Loc.GetString(rule.Comp1.Sender)), ("description", Loc.GetString(rule.Comp1.Description)), ("dest", Loc.GetString(rule.Comp1.Dest))); stationEvent.StartAnnouncement = str; - base.Added(uid, component, gameRule, args); + base.Added(rule, ref args); } /// @@ -43,7 +46,7 @@ public sealed partial class CargoGiftsRule : StationEventSystem) || + if (!Station.TryGetRandomStation(out var station, HasComp) || !TryComp(station, out var stationData)) return; @@ -81,7 +84,68 @@ public sealed partial class CargoGiftsRule : StationEventSystem(chosenStation, out var stationRecords)) diff --git a/Content.Server/StationEvents/Events/GasLeakRule.cs b/Content.Server/StationEvents/Events/GasLeakRule.cs index 29251ee394..a9bd716012 100644 --- a/Content.Server/StationEvents/Events/GasLeakRule.cs +++ b/Content.Server/StationEvents/Events/GasLeakRule.cs @@ -1,5 +1,4 @@ using Content.Server.Atmos.EntitySystems; -using Content.Server.GameTicking.Rules.Components; using Content.Server.StationEvents.Components; using Content.Shared.GameTicking.Components; using Robust.Shared.Audio; @@ -21,8 +20,9 @@ namespace Content.Server.StationEvents.Events return; // Essentially we'll pick out a target amount of gas to leak, then a rate to leak it at, then work out the duration from there. - if (TryFindRandomTile(out component.TargetTile, out var target, out component.TargetGrid, out component.TargetCoords)) + if (Station.TryFindRandomTile(out component.TargetTile, out var target, out var grid, out component.TargetCoords)) { + component.TargetGrid = grid.Value; component.TargetStation = target.Value; component.FoundTile = true; @@ -62,28 +62,28 @@ namespace Content.Server.StationEvents.Events environment?.AdjustMoles(component.LeakGas, component.LeakCooldown * component.MolesPerSecond); } - protected override void Ended(EntityUid uid, GasLeakRuleComponent component, GameRuleComponent gameRule, GameRuleEndedEvent args) + protected override void Ended(Entity rule, ref GameRuleEndedEvent args) { - base.Ended(uid, component, gameRule, args); - Spark(uid, component); + base.Ended(rule, ref args); + Spark(rule); } - private void Spark(EntityUid uid, GasLeakRuleComponent component) + private void Spark(Entity rule) { - if (RobustRandom.NextFloat() <= component.SparkChance) + if (RobustRandom.NextFloat() <= rule.Comp.SparkChance) { - if (!component.FoundTile || - component.TargetGrid == default || - (!Exists(component.TargetGrid) ? EntityLifeStage.Deleted : MetaData(component.TargetGrid).EntityLifeStage) >= EntityLifeStage.Deleted || - !_atmosphere.IsSimulatedGrid(component.TargetGrid)) + if (!rule.Comp.FoundTile || + rule.Comp.TargetGrid == default || + (!Exists(rule.Comp.TargetGrid) ? EntityLifeStage.Deleted : MetaData(rule.Comp.TargetGrid).EntityLifeStage) >= EntityLifeStage.Deleted || + !_atmosphere.IsSimulatedGrid(rule.Comp.TargetGrid)) { return; } // Don't want it to be so obnoxious as to instantly murder anyone in the area but enough that // it COULD start potentially start a bigger fire. - _atmosphere.HotspotExpose(component.TargetGrid, component.TargetTile, 700f, 50f, null, true); - Audio.PlayPvs(new SoundPathSpecifier("/Audio/Effects/sparks4.ogg"), component.TargetCoords); + _atmosphere.HotspotExpose(rule.Comp.TargetGrid, rule.Comp.TargetTile, 700f, 50f, null, true); + Audio.PlayPvs(new SoundPathSpecifier("/Audio/Effects/sparks4.ogg"), rule.Comp.TargetCoords); } } } diff --git a/Content.Server/StationEvents/Events/GreytideVirusRule.cs b/Content.Server/StationEvents/Events/GreytideVirusRule.cs index 39ea7c7ae3..2bd16123b2 100644 --- a/Content.Server/StationEvents/Events/GreytideVirusRule.cs +++ b/Content.Server/StationEvents/Events/GreytideVirusRule.cs @@ -47,7 +47,7 @@ public sealed partial class GreytideVirusRule : StationEventSystem(xform.GridUid)?.Station != chosenStation) + if (CompOrNull(xform.GridUid)?.Station != chosenStation.Value.Owner) continue; // check access @@ -90,7 +90,7 @@ public sealed partial class GreytideVirusRule : StationEventSystem(xform.GridUid)?.Station != chosenStation) + if (CompOrNull(xform.GridUid)?.Station != chosenStation.Value.Owner) continue; // use the access reader from the door electronics if they exist diff --git a/Content.Server/StationEvents/Events/ImmovableRodRule.cs b/Content.Server/StationEvents/Events/ImmovableRodRule.cs index 47d39da0a8..a5c8650b24 100644 --- a/Content.Server/StationEvents/Events/ImmovableRodRule.cs +++ b/Content.Server/StationEvents/Events/ImmovableRodRule.cs @@ -1,3 +1,4 @@ +using System.Linq; using System.Numerics; using Content.Server.ImmovableRod; using Content.Server.StationEvents.Components; @@ -5,8 +6,7 @@ using Content.Server.Weapons.Ranged.Systems; using Content.Shared.GameTicking.Components; using Content.Shared.Storage; using Robust.Shared.Prototypes; -using TimedDespawnComponent = Robust.Shared.Spawners.TimedDespawnComponent; -using System.Linq; +using Robust.Shared.Spawners; namespace Content.Server.StationEvents.Events; @@ -27,7 +27,7 @@ public sealed partial class ImmovableRodRule : StationEventSystem(out var rod, EntityManager.ComponentFactory) && proto.TryComp(out var despawn, EntityManager.ComponentFactory)) { - if (!TryFindRandomTile(out _, out _, out _, out var targetCoords)) + if (!Station.TryFindRandomTile(out _, out _, out _, out var targetCoords)) return; var speed = RobustRandom.NextFloat(rod.MinSpeed, rod.MaxSpeed); diff --git a/Content.Server/StationEvents/Events/IonStormRule.cs b/Content.Server/StationEvents/Events/IonStormRule.cs index cb46ec579a..fdc4c29e8b 100644 --- a/Content.Server/StationEvents/Events/IonStormRule.cs +++ b/Content.Server/StationEvents/Events/IonStormRule.cs @@ -14,14 +14,14 @@ public sealed partial class IonStormRule : StationEventSystem(); while (query.MoveNext(out var ent, out var lawBound, out var xform, out var target)) { // only affect law holders on the station - if (CompOrNull(xform.GridUid)?.Station != chosenStation) + if (CompOrNull(xform.GridUid)?.Station != chosenStation.Value.Owner) continue; _ionStorm.IonStormTarget((ent, lawBound, target)); diff --git a/Content.Server/StationEvents/Events/KudzuGrowthRule.cs b/Content.Server/StationEvents/Events/KudzuGrowthRule.cs index a94eaf063b..5b00ad5b0e 100644 --- a/Content.Server/StationEvents/Events/KudzuGrowthRule.cs +++ b/Content.Server/StationEvents/Events/KudzuGrowthRule.cs @@ -11,7 +11,7 @@ public sealed partial class KudzuGrowthRule : StationEventSystem rule, ref GameRuleEndedEvent args) { - base.Ended(uid, component, gameRule, args); + base.Ended(rule, ref args); - foreach (var ent in component.AffectedEntities) + foreach (var ent in rule.Comp.AffectedEntities) { RemComp(ent); } - component.AffectedEntities.Clear(); + rule.Comp.AffectedEntities.Clear(); } } diff --git a/Content.Server/StationEvents/Events/MeteorSwarmSystem.cs b/Content.Server/StationEvents/Events/MeteorSwarmSystem.cs index 1d3eebf590..0371a8d46a 100644 --- a/Content.Server/StationEvents/Events/MeteorSwarmSystem.cs +++ b/Content.Server/StationEvents/Events/MeteorSwarmSystem.cs @@ -1,15 +1,12 @@ using System.Numerics; -using Content.Server.Chat.Systems; -using Content.Server.GameTicking.Rules; using Content.Server.Station.Systems; using Content.Server.StationEvents.Components; using Content.Shared.GameTicking.Components; +using Content.Shared.GameTicking.Rules; using Content.Shared.Random.Helpers; -using Robust.Server.Audio; using Robust.Shared.Map; using Robust.Shared.Physics.Components; using Robust.Shared.Physics.Systems; -using Robust.Shared.Player; using Robust.Shared.Random; namespace Content.Server.StationEvents.Events; @@ -17,23 +14,24 @@ namespace Content.Server.StationEvents.Events; public sealed partial class MeteorSwarmSystem : GameRuleSystem { [Dependency] private SharedPhysicsSystem _physics = default!; - [Dependency] private AudioSystem _audio = default!; - [Dependency] private ChatSystem _chat = default!; - [Dependency] private StationSystem _station = default!; + [Dependency] private ServerStationSystem _station = default!; - protected override void Added(EntityUid uid, MeteorSwarmComponent component, GameRuleComponent gameRule, GameRuleAddedEvent args) + protected override void Added(Entity rule, ref GameRuleAddedEvent args) { - base.Added(uid, component, gameRule, args); + base.Added(rule, ref args); + var stations = _station.GetStations(); + if (stations.Count == 0) + return; - component.WaveCounter = component.Waves.Next(RobustRandom); + var station = RobustRandom.Pick(stations); + if (_station.GetLargestGrid(station.AsNullable()) is not { } grid) + return; - // we don't want to send to players who aren't in game (i.e. in the lobby) - Filter allPlayersInGame = Filter.Empty().AddWhere(GameTicker.UserHasJoinedGame); + rule.Comp1.TargetGrid = grid; + rule.Comp1.WaveCounter = rule.Comp1.Waves.Next(RobustRandom); - if (component.Announcement is { } locId) - _chat.DispatchFilteredAnnouncement(allPlayersInGame, Loc.GetString(locId), playSound: false, colorOverride: Color.Gold); - - _audio.PlayGlobal(component.AnnouncementSound, allPlayersInGame, true); + if (rule.Comp1.Announcement is { } locId) + GameTicker.StationMapAnnouncement(station, locId, rule.Comp1.AnnouncementSound, Color.Gold); } protected override void ActiveTick(EntityUid uid, MeteorSwarmComponent component, GameRuleComponent gameRule, float frameTime) @@ -41,16 +39,14 @@ public sealed partial class MeteorSwarmSystem : GameRuleSystem { [Dependency] private ApcSystem _apcSystem = default!; - [Dependency] private SharedStationSystem _stationSystem = default!; public override void Initialize() { @@ -32,12 +27,12 @@ namespace Content.Server.StationEvents.Events { base.Started(uid, component, gameRule, args); - if (!TryGetRandomStation(out var chosenStation)) + if (!Station.TryGetRandomStation(out var chosenStation)) return; component.AffectedStation = chosenStation.Value; - var largestGrid = _stationSystem.GetLargestGrid(chosenStation.Value); + var largestGrid = Station.GetLargestGrid(chosenStation.Value.AsNullable()); if (largestGrid == null) return; @@ -48,7 +43,7 @@ namespace Content.Server.StationEvents.Events if (!apc.MainBreakerEnabled) continue; - if (CompOrNull(transform.GridUid)?.Station != chosenStation) + if (CompOrNull(transform.GridUid)?.Station != chosenStation.Value.Owner) continue; if (transform.GridUid != largestGrid.Value) @@ -103,12 +98,12 @@ namespace Content.Server.StationEvents.Events } var activeRules = AllEntityQuery(); - while (activeRules.MoveNext(out var _entity, out var powerGridRule, out var _activeGameRule)) + while (activeRules.MoveNext(out _, out var powerGridRule, out _)) { if (stationMemberComp.Station != powerGridRule.AffectedStation) continue; - var largestGrid = _stationSystem.GetLargestGrid(powerGridRule.AffectedStation); + var largestGrid = Station.GetLargestGrid(powerGridRule.AffectedStation); if (largestGrid == null) continue; @@ -122,11 +117,11 @@ namespace Content.Server.StationEvents.Events return null; } - protected override void Ended(EntityUid uid, PowerGridCheckRuleComponent component, GameRuleComponent gameRule, GameRuleEndedEvent args) + protected override void Ended(Entity rule, ref GameRuleEndedEvent args) { - base.Ended(uid, component, gameRule, args); + base.Ended(rule, ref args); - foreach (var entity in component.Unpowered) + foreach (var entity in rule.Comp.Unpowered) { if (Deleted(entity)) continue; @@ -139,13 +134,15 @@ namespace Content.Server.StationEvents.Events } // Can't use the default EndAudio - component.AnnounceCancelToken?.Cancel(); - component.AnnounceCancelToken = new CancellationTokenSource(); - Timer.Spawn(3000, () => + rule.Comp.AnnounceCancelToken?.Cancel(); + rule.Comp.AnnounceCancelToken = new CancellationTokenSource(); + Timer.Spawn(3000, + () => { - Audio.PlayGlobal(component.PowerOnSound, Filter.Broadcast(), true); - }, component.AnnounceCancelToken.Token); - component.Unpowered.Clear(); + Audio.PlayGlobal(rule.Comp.PowerOnSound, Filter.Broadcast(), true); + }, + rule.Comp.AnnounceCancelToken.Token); + rule.Comp.Unpowered.Clear(); } protected override void ActiveTick(EntityUid uid, PowerGridCheckRuleComponent component, GameRuleComponent gameRule, float frameTime) diff --git a/Content.Server/StationEvents/Events/RandomEntityStorageSpawnRule.cs b/Content.Server/StationEvents/Events/RandomEntityStorageSpawnRule.cs index 0b959a98e9..88a25c59f4 100644 --- a/Content.Server/StationEvents/Events/RandomEntityStorageSpawnRule.cs +++ b/Content.Server/StationEvents/Events/RandomEntityStorageSpawnRule.cs @@ -15,7 +15,7 @@ public sealed partial class RandomEntityStorageSpawnRule : StationEventSystem(); @@ -24,7 +24,7 @@ public sealed partial class RandomEntityStorageSpawnRule : StationEventSystem(); while (query.MoveNext(out var ent, out var storage, out var xform)) { - if (StationSystem.GetOwningStation(ent, xform) != station) + if (Station.GetOwningStation(ent, xform) != station.Value.Owner) continue; if (!_entityStorage.CanInsert(spawn, ent, storage)) diff --git a/Content.Server/StationEvents/Events/RandomSentienceRule.cs b/Content.Server/StationEvents/Events/RandomSentienceRule.cs index d580c93b70..b3976790bb 100644 --- a/Content.Server/StationEvents/Events/RandomSentienceRule.cs +++ b/Content.Server/StationEvents/Events/RandomSentienceRule.cs @@ -19,14 +19,14 @@ public sealed partial class RandomSentienceRule : StationEventSystem>(); var query = EntityQueryEnumerator(); while (query.MoveNext(out var targetUid, out var target, out var xform)) { - if (StationSystem.GetOwningStation(targetUid, xform) != station) + if (Station.GetOwningStation(targetUid, xform) != station.Value.Owner) continue; targetList.Add((targetUid, target)); diff --git a/Content.Server/StationEvents/Events/RandomSpawnRule.cs b/Content.Server/StationEvents/Events/RandomSpawnRule.cs index 210b8cec51..3b44073909 100644 --- a/Content.Server/StationEvents/Events/RandomSpawnRule.cs +++ b/Content.Server/StationEvents/Events/RandomSpawnRule.cs @@ -10,7 +10,7 @@ public sealed partial class RandomSpawnRule : StationEventSystem(); while (query.MoveNext(out var uid, out var flare, out var gameRule)) { - if (!GameTicker.IsGameRuleActive(uid, gameRule)) + if (!GameTicker.IsGameRuleActive((uid, gameRule))) continue; if (!flare.AffectedChannels.Contains(args.Channel.ID)) diff --git a/Content.Server/StationEvents/Events/SpaceSpawnRule.cs b/Content.Server/StationEvents/Events/SpaceSpawnRule.cs index 5e0027053b..129adf497e 100644 --- a/Content.Server/StationEvents/Events/SpaceSpawnRule.cs +++ b/Content.Server/StationEvents/Events/SpaceSpawnRule.cs @@ -1,9 +1,9 @@ using Content.Server.Antag; using Content.Server.StationEvents.Components; +using Content.Shared.Antag; using Content.Shared.GameTicking.Components; using Robust.Shared.Map; using Robust.Shared.Map.Components; -using Robust.Shared.Random; namespace Content.Server.StationEvents.Events; @@ -25,14 +25,14 @@ public sealed partial class SpaceSpawnRule : StationEventSystem(gridUid, out var grid)) { Sawmill.Warning("Chosen station has no grids, cannot pick location for {ToPrettyString(uid):rule}"); diff --git a/Content.Server/StationEvents/Events/StationEventSystem.cs b/Content.Server/StationEvents/Events/StationEventSystem.cs index ae5d6ff47a..b5da5355fe 100644 --- a/Content.Server/StationEvents/Events/StationEventSystem.cs +++ b/Content.Server/StationEvents/Events/StationEventSystem.cs @@ -1,11 +1,10 @@ using Content.Server.Administration.Logs; using Content.Server.Chat.Systems; -using Content.Server.GameTicking; -using Content.Server.GameTicking.Rules; using Content.Server.Station.Systems; using Content.Server.StationEvents.Components; using Content.Shared.Database; using Content.Shared.GameTicking.Components; +using Content.Shared.GameTicking.Rules; using Robust.Shared.Audio.Systems; using Robust.Shared.Player; @@ -19,7 +18,7 @@ public abstract partial class StationEventSystem : GameRuleSystem where T [Dependency] protected IAdminLogManager AdminLogManager = default!; [Dependency] protected ChatSystem ChatSystem = default!; [Dependency] protected SharedAudioSystem Audio = default!; - [Dependency] protected StationSystem StationSystem = default!; + [Dependency] protected ServerStationSystem Station = default!; protected ISawmill Sawmill = default!; @@ -70,14 +69,14 @@ public abstract partial class StationEventSystem : GameRuleSystem where T } /// - protected override void Ended(EntityUid uid, T component, GameRuleComponent gameRule, GameRuleEndedEvent args) + protected override void Ended(Entity rule, ref GameRuleEndedEvent args) { - base.Ended(uid, component, gameRule, args); + base.Ended(rule, ref args); - if (!TryComp(uid, out var stationEvent)) + if (!TryComp(rule, out var stationEvent)) return; - AdminLogManager.Add(LogType.EventStopped, $"Event ended: {ToPrettyString(uid)}"); + AdminLogManager.Add(LogType.EventStopped, $"Event ended: {ToPrettyString(rule)}"); // we don't want to send to players who aren't in game (i.e. in the lobby) Filter allPlayersInGame = Filter.Empty().AddWhere(GameTicker.UserHasJoinedGame); @@ -100,16 +99,16 @@ public abstract partial class StationEventSystem : GameRuleSystem where T var query = EntityQueryEnumerator(); while (query.MoveNext(out var uid, out var stationEvent, out var ruleData)) { - if (!GameTicker.IsGameRuleAdded(uid, ruleData)) + if (!GameTicker.IsGameRuleAdded((uid, ruleData))) continue; - if (!GameTicker.IsGameRuleActive(uid, ruleData) && !HasComp(uid)) + if (!GameTicker.IsGameRuleActive((uid, ruleData)) && !HasComp(uid)) { - GameTicker.StartGameRule(uid, ruleData); + GameTicker.StartGameRule((uid, ruleData)); } - else if (stationEvent.EndTime != null && Timing.CurTime >= stationEvent.EndTime && GameTicker.IsGameRuleActive(uid, ruleData)) + else if (stationEvent.EndTime != null && Timing.CurTime >= stationEvent.EndTime && GameTicker.IsGameRuleActive((uid, ruleData))) { - GameTicker.EndGameRule(uid, ruleData); + GameTicker.EndGameRule((uid, ruleData)); } } } diff --git a/Content.Server/StationEvents/Events/VentClogRule.cs b/Content.Server/StationEvents/Events/VentClogRule.cs index dbd4c53fa9..befdffb994 100644 --- a/Content.Server/StationEvents/Events/VentClogRule.cs +++ b/Content.Server/StationEvents/Events/VentClogRule.cs @@ -22,7 +22,7 @@ public sealed partial class VentClogRule : StationEventSystem()) { - if (CompOrNull(transform.GridUid)?.Station != chosenStation) + if (CompOrNull(transform.GridUid)?.Station != chosenStation.Value.Owner) { continue; } diff --git a/Content.Server/StationEvents/Events/VentCrittersRule.cs b/Content.Server/StationEvents/Events/VentCrittersRule.cs index 9d75ef5c1e..a6042a481c 100644 --- a/Content.Server/StationEvents/Events/VentCrittersRule.cs +++ b/Content.Server/StationEvents/Events/VentCrittersRule.cs @@ -18,7 +18,7 @@ public sealed partial class VentCrittersRule : StationEventSystem(transform.GridUid)?.Station == station) + if (CompOrNull(transform.GridUid)?.Station == station.Value.Owner) { validLocations.Add(transform.Coordinates); foreach (var spawn in EntitySpawnCollection.GetSpawns(component.Entries, RobustRandom)) diff --git a/Content.Server/StationEvents/Events/VentHordeRule.cs b/Content.Server/StationEvents/Events/VentHordeRule.cs index 2bf4b0bcc1..7028f1d918 100644 --- a/Content.Server/StationEvents/Events/VentHordeRule.cs +++ b/Content.Server/StationEvents/Events/VentHordeRule.cs @@ -35,7 +35,7 @@ public sealed partial class VentHordeRule : StationEventSystem(uid)) continue; - if (CompOrNull(transform.GridUid)?.Station == station) + if (CompOrNull(transform.GridUid)?.Station == station.Value.Owner) { validLocations.Add(uid); } diff --git a/Content.Server/StationEvents/RampingStationEventSchedulerSystem.cs b/Content.Server/StationEvents/RampingStationEventSchedulerSystem.cs index b425254d9d..a89d3b0c03 100644 --- a/Content.Server/StationEvents/RampingStationEventSchedulerSystem.cs +++ b/Content.Server/StationEvents/RampingStationEventSchedulerSystem.cs @@ -2,6 +2,7 @@ using Content.Server.GameTicking; using Content.Server.GameTicking.Rules; using Content.Server.StationEvents.Components; using Content.Shared.GameTicking.Components; +using Content.Shared.GameTicking.Rules; using Robust.Shared.Random; namespace Content.Server.StationEvents; @@ -10,7 +11,7 @@ public sealed partial class RampingStationEventSchedulerSystem : GameRuleSystem< { [Dependency] private IRobustRandom _random = default!; [Dependency] private EventManagerSystem _event = default!; - [Dependency] private GameTicker _gameTicker = default!; + [Dependency] private ServerGameTicker _gameTicker = default!; /// /// Returns the ChaosModifier which increases as round time increases to a point. @@ -38,28 +39,20 @@ public sealed partial class RampingStationEventSchedulerSystem : GameRuleSystem< PickNextEventTime(uid, component); } - public override void Update(float frameTime) + // TODO: GO THROUGH EVERY SINGLE GAME RULE AND JUST CLEAN THIS STUFF UP!!! + protected override void ActiveTick(EntityUid entityUid, RampingStationEventSchedulerComponent component, GameRuleComponent gameRuleComponent, float frameTime) { - base.Update(frameTime); - if (!_event.EventsEnabled) return; - var query = EntityQueryEnumerator(); - while (query.MoveNext(out var uid, out var scheduler, out var gameRule)) + if (component.TimeUntilNextEvent > 0f) { - if (!GameTicker.IsGameRuleActive(uid, gameRule)) - continue; - - if (scheduler.TimeUntilNextEvent > 0f) - { - scheduler.TimeUntilNextEvent -= frameTime; - continue; - } - - PickNextEventTime(uid, scheduler); - _event.RunRandomEvent(scheduler.ScheduledGameRules); + component.TimeUntilNextEvent -= frameTime; + return; } + + PickNextEventTime(entityUid, component); + _event.RunRandomEvent(component.ScheduledGameRules); } /// diff --git a/Content.Server/Storage/EntitySystems/BluespaceLockerSystem.cs b/Content.Server/Storage/EntitySystems/BluespaceLockerSystem.cs index 14954f497f..abdd875856 100644 --- a/Content.Server/Storage/EntitySystems/BluespaceLockerSystem.cs +++ b/Content.Server/Storage/EntitySystems/BluespaceLockerSystem.cs @@ -16,8 +16,7 @@ using Robust.Shared.Containers; using Robust.Shared.Random; using Robust.Shared.Timing; using Robust.Shared.Prototypes; -using Content.Server.Shuttles.Components; -using Robust.Shared.Physics; +using Content.Shared.Shuttles.Components; namespace Content.Server.Storage.EntitySystems; diff --git a/Content.Server/Tips/TipsSystem.cs b/Content.Server/Tips/TipsSystem.cs index f1e3753132..bdaa569d95 100644 --- a/Content.Server/Tips/TipsSystem.cs +++ b/Content.Server/Tips/TipsSystem.cs @@ -3,6 +3,7 @@ using Content.Server.GameTicking; using Content.Shared.CCVar; using Content.Shared.Chat; using Content.Shared.Dataset; +using Content.Shared.GameTicking; using Content.Shared.Tips; using Robust.Shared.Configuration; using Robust.Shared.Player; @@ -18,7 +19,7 @@ public sealed partial class TipsSystem : SharedTipsSystem [Dependency] private IConfigurationManager _cfg = default!; [Dependency] private IGameTiming _timing = default!; [Dependency] private IRobustRandom _random = default!; - [Dependency] private GameTicker _ticker = default!; + [Dependency] private ServerGameTicker _ticker = default!; private bool _tipsEnabled; private float _tipTimeOutOfRound; diff --git a/Content.Server/Traitor/Components/TraitorCodePaperComponent.cs b/Content.Server/Traitor/Components/TraitorCodePaperComponent.cs index d4ce40c066..8e7d9ab78e 100644 --- a/Content.Server/Traitor/Components/TraitorCodePaperComponent.cs +++ b/Content.Server/Traitor/Components/TraitorCodePaperComponent.cs @@ -1,4 +1,4 @@ -using Content.Server.Codewords; +using Content.Shared.Codewords; using Robust.Shared.Prototypes; namespace Content.Server.Traitor.Components; diff --git a/Content.Server/Traitor/Systems/AutoTraitorSystem.cs b/Content.Server/Traitor/Systems/AutoTraitorSystem.cs index 96fdf51ab9..8f76aba0d3 100644 --- a/Content.Server/Traitor/Systems/AutoTraitorSystem.cs +++ b/Content.Server/Traitor/Systems/AutoTraitorSystem.cs @@ -1,8 +1,7 @@ -using Content.Server.Antag; using Content.Server.Traitor.Components; +using Content.Shared.Antag; using Content.Shared.Mind.Components; using Robust.Shared.Player; -using Robust.Shared.Prototypes; namespace Content.Server.Traitor.Systems; diff --git a/Content.Server/Traitor/Systems/TraitorCodePaperSystem.cs b/Content.Server/Traitor/Systems/TraitorCodePaperSystem.cs index 545b0f132d..cfbc203083 100644 --- a/Content.Server/Traitor/Systems/TraitorCodePaperSystem.cs +++ b/Content.Server/Traitor/Systems/TraitorCodePaperSystem.cs @@ -1,14 +1,10 @@ using System.Diagnostics.CodeAnalysis; -using Content.Server.GameTicking; -using Content.Server.GameTicking.Rules; -using Content.Server.GameTicking.Rules.Components; -using Content.Server.Paper; +using System.Linq; using Content.Server.Traitor.Components; +using Content.Shared.Codewords; +using Content.Shared.Paper; using Robust.Shared.Random; using Robust.Shared.Utility; -using System.Linq; -using Content.Server.Codewords; -using Content.Shared.Paper; namespace Content.Server.Traitor.Systems; diff --git a/Content.Server/Trigger/Systems/GameRuleTriggerSystem.cs b/Content.Server/Trigger/Systems/GameRuleTriggerSystem.cs index db7cf5973a..fdfb3fe568 100644 --- a/Content.Server/Trigger/Systems/GameRuleTriggerSystem.cs +++ b/Content.Server/Trigger/Systems/GameRuleTriggerSystem.cs @@ -1,6 +1,7 @@ using Content.Server.Administration.Logs; using Content.Server.GameTicking; using Content.Shared.Database; +using Content.Shared.GameTicking; using Content.Shared.Trigger; using Content.Shared.Trigger.Components.Effects; @@ -11,7 +12,7 @@ namespace Content.Server.Trigger.Systems; /// public sealed partial class GameRuleTriggerSystem : EntitySystem { - [Dependency] private GameTicker _ticker = default!; + [Dependency] private ServerGameTicker _ticker = default!; [Dependency] private IAdminLogManager _adminLogger = default!; /// @@ -27,7 +28,8 @@ public sealed partial class GameRuleTriggerSystem : EntitySystem if (args.Key != null && !ent.Comp.KeysIn.Contains(args.Key)) return; - var rule = _ticker.AddGameRule(ent.Comp.GameRule); + if (_ticker.AddGameRule(ent.Comp.GameRule) is not { } rule) + return; _adminLogger.Add(LogType.EventStarted, $"{ToPrettyString(args.User):entity} added a game rule [{ent.Comp.GameRule}]" + @@ -35,7 +37,7 @@ public sealed partial class GameRuleTriggerSystem : EntitySystem if (ent.Comp.StartRule && _ticker.RunLevel == GameRunLevel.InRound) { - _ticker.StartGameRule(rule); + _ticker.StartGameRule(rule.AsNullable()); _adminLogger.Add(LogType.EventStarted, $"{ToPrettyString(args.User):entity} started game rule [{ent.Comp.GameRule}]."); } diff --git a/Content.Server/Voting/Managers/VoteManager.DefaultVotes.cs b/Content.Server/Voting/Managers/VoteManager.DefaultVotes.cs index 0023eabc9a..d1804481e5 100644 --- a/Content.Server/Voting/Managers/VoteManager.DefaultVotes.cs +++ b/Content.Server/Voting/Managers/VoteManager.DefaultVotes.cs @@ -1,16 +1,15 @@ using System.Linq; -using System.Net; -using System.Net.Sockets; using Content.Server.Administration; using Content.Server.Administration.Managers; using Content.Server.Discord.WebhookMessages; using Content.Server.GameTicking; -using Content.Server.GameTicking.Presets; using Content.Server.Roles; using Content.Server.RoundEnd; using Content.Shared.CCVar; using Content.Shared.Chat; using Content.Shared.Database; +using Content.Shared.GameTicking; +using Content.Shared.GameTicking.Prototypes; using Content.Shared.Maps; using Content.Shared.Players; using Content.Shared.Players.PlayTimeTracking; @@ -31,7 +30,7 @@ namespace Content.Server.Voting.Managers private VotingSystem? _votingSystem; private RoleSystem? _roleSystem; - private GameTicker? _gameTicker; + private ServerGameTicker? _gameTicker; private static readonly Dictionary> VoteTypesToEnableCVars = new() { @@ -50,7 +49,7 @@ namespace Content.Server.Voting.Managers else _adminLogger.Add(LogType.Vote, LogImpact.Medium, $"Initiated a {voteType.ToString()} vote"); - _gameTicker = _entityManager.EntitySysManager.GetEntitySystem(); + _gameTicker = _entityManager.EntitySysManager.GetEntitySystem(); bool timeoutVote = true; @@ -256,7 +255,7 @@ namespace Content.Server.Voting.Managers Loc.GetString("ui-vote-gamemode-win", ("winner", Loc.GetString(presets[picked])))); } _adminLogger.Add(LogType.Vote, LogImpact.Medium, $"Preset vote finished: {picked}"); - var ticker = _entityManager.EntitySysManager.GetEntitySystem(); + var ticker = _entityManager.EntitySysManager.GetEntitySystem(); ticker.SetGamePreset(picked); }; } @@ -303,7 +302,7 @@ namespace Content.Server.Voting.Managers } _adminLogger.Add(LogType.Vote, LogImpact.Medium, $"Map vote finished: {picked.MapName}"); - var ticker = _entityManager.EntitySysManager.GetEntitySystem(); + var ticker = _entityManager.EntitySysManager.GetEntitySystem(); if (ticker.CanUpdateMap()) { if (_gameMapManager.CheckMapExists(picked.ID)) diff --git a/Content.Server/Voting/Managers/VoteManager.cs b/Content.Server/Voting/Managers/VoteManager.cs index 2f4cf272fa..824eb93877 100644 --- a/Content.Server/Voting/Managers/VoteManager.cs +++ b/Content.Server/Voting/Managers/VoteManager.cs @@ -363,7 +363,7 @@ namespace Content.Server.Voting.Managers if (voteType == StandardVoteType.Preset) { var presets = GetGamePresets(); - if (presets.Count == 1 && presets.Select(x => x.Key).Single() == _entityManager.System().Preset?.ID) + if (presets.Count == 1 && presets.Select(x => x.Key).Single() == _entityManager.System().Preset?.ID) return false; } diff --git a/Content.Server/Voting/VoteCommands.cs b/Content.Server/Voting/VoteCommands.cs index 57338490a4..eca8d9f249 100644 --- a/Content.Server/Voting/VoteCommands.cs +++ b/Content.Server/Voting/VoteCommands.cs @@ -112,7 +112,7 @@ namespace Content.Server.Voting var voteLogId = await _dbManager.CustomVoteLogAdd( title, - GameTicker.GetRoundId(_esm), + ServerGameTicker.GetRoundId(_esm), shell.Player?.UserId, [..options.Options.Select(x => x.text)]); diff --git a/Content.Server/Voting/VotingSystem.cs b/Content.Server/Voting/VotingSystem.cs index 41d7e72afc..bcad9bde90 100644 --- a/Content.Server/Voting/VotingSystem.cs +++ b/Content.Server/Voting/VotingSystem.cs @@ -13,6 +13,7 @@ using Robust.Shared.Player; using Robust.Shared.Timing; using System.Threading.Tasks; using Content.Server.Players.Whitelist; +using Content.Shared.GameTicking; using Content.Shared.Players.PlayTimeTracking; using Content.Shared.Ghost.Components; @@ -20,13 +21,12 @@ namespace Content.Server.Voting; public sealed partial class VotingSystem : EntitySystem { - [Dependency] private IPlayerManager _playerManager = default!; [Dependency] private IAdminManager _adminManager = default!; [Dependency] private IGameTiming _gameTiming = default!; [Dependency] private IConfigurationManager _cfg = default!; [Dependency] private JobSystem _jobs = default!; - [Dependency] private GameTicker _gameTicker = default!; + [Dependency] private ServerGameTicker _gameTicker = default!; [Dependency] private ISharedPlaytimeManager _playtimeManager = default!; [Dependency] private WhitelistManager _whitelistManager = default!; diff --git a/Content.Server/Xenoborgs/XenoborgSystem.cs b/Content.Server/Xenoborgs/XenoborgSystem.cs index 17a067d3e4..215fe58a94 100644 --- a/Content.Server/Xenoborgs/XenoborgSystem.cs +++ b/Content.Server/Xenoborgs/XenoborgSystem.cs @@ -1,15 +1,12 @@ -using Content.Server.Antag; using Content.Server.GameTicking.Rules; -using Content.Server.GameTicking.Rules.Components; using Content.Server.Silicons.Borgs; +using Content.Shared.Antag; using Content.Shared.Destructible; -using Content.Shared.Mind; +using Content.Shared.GameTicking.Rules.Components; using Content.Shared.Mind.Components; using Content.Shared.Roles; -using Content.Shared.Roles.Components; using Content.Shared.Silicons.Borgs.Components; using Content.Shared.Xenoborgs.Components; -using Robust.Shared.Audio; using Robust.Shared.Player; namespace Content.Server.Xenoborgs; @@ -19,7 +16,7 @@ public sealed partial class XenoborgSystem : EntitySystem [Dependency] private AntagSelectionSystem _antag = default!; [Dependency] private BorgSystem _borg = default!; [Dependency] private SharedRoleSystem _roles = default!; - [Dependency] private XenoborgsRuleSystem _xenoborgsRule = default!; + [Dependency] private ServerXenoborgsRuleSystem _xenoborgsRule = default!; private static readonly Color XenoborgBriefingColor = Color.BlueViolet; diff --git a/Content.Shared/Access/Systems/AccessReaderSystem.cs b/Content.Shared/Access/Systems/AccessReaderSystem.cs index ae32557d8e..677526ed4c 100644 --- a/Content.Shared/Access/Systems/AccessReaderSystem.cs +++ b/Content.Shared/Access/Systems/AccessReaderSystem.cs @@ -31,7 +31,7 @@ public sealed partial class AccessReaderSystem : EntitySystem [Dependency] private IGameTiming _gameTiming = default!; [Dependency] private EmagSystem _emag = default!; [Dependency] private TagSystem _tag = default!; - [Dependency] private SharedGameTicker _gameTicker = default!; + [Dependency] private GameTicker _gameTicker = default!; [Dependency] private SharedHandsSystem _handsSystem = default!; [Dependency] private SharedContainerSystem _containerSystem = default!; [Dependency] private StationRecordsSystem _recordsSystem = default!; diff --git a/Content.Shared/AlertLevel/AlertLevelSystem.cs b/Content.Shared/AlertLevel/AlertLevelSystem.cs index 73234337a8..7fe5a0afce 100644 --- a/Content.Shared/AlertLevel/AlertLevelSystem.cs +++ b/Content.Shared/AlertLevel/AlertLevelSystem.cs @@ -1,7 +1,7 @@ using System.Diagnostics.CodeAnalysis; using Content.Shared.Chat; -using Content.Shared.Station; using Content.Shared.CCVar; +using Content.Shared.Station.Systems; using Robust.Shared.Audio.Systems; using Robust.Shared.Configuration; using Robust.Shared.Network; @@ -18,7 +18,7 @@ public sealed partial class AlertLevelSystem : EntitySystem [Dependency] private IPrototypeManager _prototype = default!; [Dependency] private SharedAudioSystem _audio = default!; [Dependency] private SharedChatSystem _chat = default!; - [Dependency] private SharedStationSystem _station = default!; + [Dependency] private StationSystem _station = default!; private static string _fallbackAlertKey = "alert-level-unknown"; private static string _fallbackAlertName = $"{_fallbackAlertKey}"; diff --git a/Content.Server/Antag/AntagSelectionSystem.API.Assignment.cs b/Content.Shared/Antag/AntagSelectionSystem.API.Assignment.cs similarity index 91% rename from Content.Server/Antag/AntagSelectionSystem.API.Assignment.cs rename to Content.Shared/Antag/AntagSelectionSystem.API.Assignment.cs index 12582961d1..a6d41c6b17 100644 --- a/Content.Server/Antag/AntagSelectionSystem.API.Assignment.cs +++ b/Content.Shared/Antag/AntagSelectionSystem.API.Assignment.cs @@ -1,9 +1,6 @@ -using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Linq; -using Content.Server.Antag.Components; -using Content.Server.GameTicking.Rules.Components; -using Content.Shared.Antag; +using Content.Shared.Antag.Components; using Content.Shared.Database; using Content.Shared.Ghost.Components; using Content.Shared.Humanoid; @@ -12,11 +9,11 @@ using JetBrains.Annotations; using Robust.Shared.Player; using Robust.Shared.Prototypes; using Robust.Shared.Random; -using static Content.Server.Antag.Components.AntagSelectionTime; +using Robust.Shared.Utility; -namespace Content.Server.Antag; +namespace Content.Shared.Antag; -public sealed partial class AntagSelectionSystem +public abstract partial class AntagSelectionSystem { /// [PublicAPI] @@ -131,7 +128,7 @@ public sealed partial class AntagSelectionSystem private bool IsMindValid([NotNullWhen(true)] EntityUid? mind, AntagSpecifierPrototype def) { // The jobless can always be antag! - if (!_jobs.MindTryGetJob(mind, out var job)) + if (!Jobs.MindTryGetJob(mind, out var job)) return true; // "Sorry buddy, but you can't be a traitor and the head of security" - Urist 1984 @@ -175,7 +172,7 @@ public sealed partial class AntagSelectionSystem public bool IsEntityValid([NotNullWhen(true)] EntityUid? uid, AntagSpecifierPrototype def) { // If the player has not spawned in as any entity (e.g., in the lobby), they can be given an antag role/entity. - if (!_whitelist.CheckBoth(uid, def.Blacklist, def.Whitelist)) + if (!Whitelist.CheckBoth(uid, def.Blacklist, def.Whitelist)) return false; if (_arrivals.IsOnArrivals((uid.Value, null))) @@ -199,20 +196,7 @@ public sealed partial class AntagSelectionSystem /// The definition which a player may or may not be banned from /// True if any of the preferred roles within the definition hit a ban. [PublicAPI] - public bool IsAntagBanned(ICommonSession session, AntagSpecifierPrototype definition) - { - if (_ban.GetAntagBans(session.UserId) is not { } bans) - return false; - - foreach (var role in definition.PrefRoles) - { - // banned! - if (bans.Contains(role)) - return true; - } - - return false; - } + public abstract bool IsAntagBanned(ICommonSession session, AntagSpecifierPrototype definition); /// [PublicAPI] @@ -236,7 +220,7 @@ public sealed partial class AntagSelectionSystem ICommonSession session, bool checkPref = true) { - _adminLogger.Add(LogType.AntagSelection, + AdminLogger.Add(LogType.AntagSelection, $"Start trying to make {session} become the antagonist: {ToPrettyString(gameRule)}, {prototype.ID}"); if (!CanBeAntag(session, gameRule, prototype, checkPref)) @@ -295,10 +279,10 @@ public sealed partial class AntagSelectionSystem // This logic is effectively copy-pasted from the old system with some fixes. var query = QueryActiveRules(); var rules = new List<(EntityUid, AntagSelectionComponent)>(); - while (query.MoveNext(out var uid, out _, out var antag, out _)) + while (query.MoveNext(out var uid, out var antag, out _, out _)) { // This is intended to only be used for ghost roles so it shouldn't be assigned for late joins - if (antag.SelectionTime == Never || !antag.LateJoinAdditional) + if (antag.SelectionTime == AntagSelectionTime.Never || !antag.LateJoinAdditional) continue; rules.Add((uid, antag)); @@ -394,7 +378,7 @@ public sealed partial class AntagSelectionSystem { if (proto.SpawnerPrototype is not { } spawnerPrototype) { - Debug.Assert(!assert, $"Tried to spawn a ghost role for {proto.ID}, but it had no prototype!"); + DebugTools.Assert(!assert, $"Tried to spawn a ghost role for {proto.ID}, but it had no prototype!"); return; } @@ -409,7 +393,7 @@ public sealed partial class AntagSelectionSystem if (!TryComp(spawner, out var spawnerComp)) { Log.Error($"Antag spawner {spawner} does not have a {nameof(GhostRoleAntagSpawnerComponent)}."); - _adminLogger.Add(LogType.AntagSelection, + AdminLogger.Add(LogType.AntagSelection, $"Antag spawner {spawner} in game rule {ToPrettyString(gameRule)} failed due to not having {nameof(GhostRoleAntagSpawnerComponent)}."); Del(spawner); return; @@ -427,7 +411,8 @@ public sealed partial class AntagSelectionSystem [Obsolete] public void ForceMakeAntag(ICommonSession player, EntProtoId defaultRule) where T : Component { - var rule = ForceGetGameRuleEnt(defaultRule); + if (ForceGetGameRuleEnt(defaultRule) is not { } rule) + return; if (TryAssignNextAvailableAntag(rule, player)) return; @@ -461,7 +446,8 @@ public sealed partial class AntagSelectionSystem /// public void ForceMakeAntag(ICommonSession player, EntProtoId ruleProto, AntagSpecifierPrototype proto) where T : Component { - var rule = ForceGetGameRuleEnt(ruleProto); + if (ForceGetGameRuleEnt(ruleProto) is not { } rule) + return; foreach (var antag in rule.Comp.Antags) { @@ -482,21 +468,7 @@ public sealed partial class AntagSelectionSystem /// Note that this is private because you generally should not be forcing a gamerule and this code is evil. /// I'm not touching it any more than I have to. /// - private Entity ForceGetGameRuleEnt(string id) where T : Component - { - var query = EntityQueryEnumerator(); - while (query.MoveNext(out var uid, out _, out var comp)) - { - if (MetaData(uid).EntityPrototype?.ID == id) - return (uid, comp); - } - var ruleEnt = GameTicker.AddGameRule(id); - RemComp(ruleEnt); - var antag = RuleQuery.Comp(ruleEnt); - antag.AssignmentHandled = true; // don't do normal selection. - GameTicker.StartGameRule(ruleEnt); - return (ruleEnt, antag); - } + protected abstract Entity? ForceGetGameRuleEnt(string id) where T : Component; /// /// Assigns components to an entity based on a diff --git a/Content.Server/Antag/AntagSelectionSystem.API.cs b/Content.Shared/Antag/AntagSelectionSystem.API.cs similarity index 67% rename from Content.Server/Antag/AntagSelectionSystem.API.cs rename to Content.Shared/Antag/AntagSelectionSystem.API.cs index b125d41149..5213dddf01 100644 --- a/Content.Server/Antag/AntagSelectionSystem.API.cs +++ b/Content.Shared/Antag/AntagSelectionSystem.API.cs @@ -1,9 +1,6 @@ using System.Linq; -using Content.Server.Antag.Components; -using Content.Server.Antag.Selectors; -using Content.Shared.Antag; -using Content.Shared.Chat; -using Content.Shared.GameTicking.Components; +using Content.Shared.Antag.Components; +using Content.Shared.Antag.Selectors; using Content.Shared.Ghost.Components; using Content.Shared.Mind; using Content.Shared.Roles; @@ -13,15 +10,15 @@ using Robust.Shared.Enums; using Robust.Shared.Player; using Robust.Shared.Prototypes; -namespace Content.Server.Antag; +namespace Content.Shared.Antag; -public sealed partial class AntagSelectionSystem +public abstract partial class AntagSelectionSystem { /// [PublicAPI] public int GetActivePlayerCount() { - return GetActivePlayerCount(_playerManager.Sessions); + return GetActivePlayerCount(PlayerManager.Sessions); } /// @@ -51,7 +48,7 @@ public sealed partial class AntagSelectionSystem [PublicAPI] public IEnumerable GetActivePlayers() { - return GetActivePlayers(_playerManager.Sessions); + return GetActivePlayers(PlayerManager.Sessions); } [PublicAPI] @@ -191,7 +188,7 @@ public sealed partial class AntagSelectionSystem if (!TryComp(mind, out var mindComp) || mindComp.OriginalOwnerUserId == null) continue; - if (!_playerManager.TryGetPlayerData(mindComp.OriginalOwnerUserId.Value, out var data)) + if (!PlayerManager.TryGetPlayerData(mindComp.OriginalOwnerUserId.Value, out var data)) continue; yield return (mind, data, name); @@ -253,7 +250,7 @@ public sealed partial class AntagSelectionSystem var minds = GetAntagMinds(ent); foreach (var mind in minds) { - if (_mind.IsCharacterDeadIc(mind)) + if (Mind.IsCharacterDeadIc(mind)) continue; if (mind.Comp.OriginalOwnedEntity != null) @@ -274,7 +271,7 @@ public sealed partial class AntagSelectionSystem var minds = GetAntagMinds(ent); foreach (var mind in minds) { - if (_mind.IsCharacterDeadIc(mind)) + if (Mind.IsCharacterDeadIc(mind)) continue; numbah++; @@ -317,13 +314,13 @@ public sealed partial class AntagSelectionSystem [PublicAPI] public void SendBriefing(EntityUid entity, string briefing, Color? briefingColor, SoundSpecifier? briefingSound) { - if (!_mind.TryGetMind(entity, out _, out var mindComponent)) + if (!Mind.TryGetMind(entity, out _, out var mindComponent)) return; - if (!_playerManager.TryGetSessionById(mindComponent.UserId, out var session)) + if (!PlayerManager.TryGetSessionById(mindComponent.UserId, out var session)) return; - SendBriefing(session, briefing, briefingColor, briefingSound); + SendBriefing((ICommonSession?)session, briefing, briefingColor, briefingSound); } /// @@ -366,21 +363,114 @@ public sealed partial class AntagSelectionSystem /// The color the briefing should be, null for default /// The sound to briefing/greeting sound to play // TODO: It might take a bit of effort but this can probably be privated. - public void SendBriefing( - ICommonSession? session, - string briefing, - Color? briefingColor, - SoundSpecifier? briefingSound) - { - if (session == null) - return; + public abstract void SendBriefing(ICommonSession? session, string briefing, Color? briefingColor, SoundSpecifier? briefingSound); - _audio.PlayGlobal(briefingSound, session); - if (!string.IsNullOrEmpty(briefing)) + /// + /// Returns a list of all antag players who are constrained by a job whitelist or blacklist from an antag. + /// + /// A dictionary of antag sessions, and their job blacklists. + [PublicAPI] + public Dictionary>? Whitelist, HashSet>? Blacklist)> + GetAntagJobs() + { + var result = new Dictionary>? Whitelist, HashSet>? Blacklist)>(); + var query = QueryAllRules(); + while (query.MoveNext(out var comp, out _)) { - var wrappedMessage = Loc.GetString("chat-manager-server-wrap-message", ("message", briefing)); - _chat.ChatMessageToOne(ChatChannel.Server, briefing, wrappedMessage, default, false, session.Channel, briefingColor); + foreach (var antag in comp.Antags) + { + if (!comp.PreSelectedSessions.TryGetValue(antag, out var set) || !ProtoMan.Resolve(antag.Proto, out var proto)) + continue; + + // Check this here so we don't make a dictionary entry for a bunch of players, with empty blacklists and whitelists. + if (proto.JobBlacklist == null && proto.JobWhitelist == null) + continue; + + foreach (var player in set) + { + if (result.TryGetValue(player, out var jobs)) + { + if (proto.JobWhitelist != null) + { + if (jobs.Whitelist == null) + jobs.Whitelist = proto.JobWhitelist; + else + jobs.Whitelist.UnionWith(proto.JobWhitelist); + } + + if (proto.JobBlacklist != null) + { + if (jobs.Blacklist == null) + jobs.Blacklist = proto.JobBlacklist; + else + jobs.Blacklist.UnionWith(proto.JobBlacklist); + } + } + else + { + result.Add(player, (proto.JobWhitelist, proto.JobBlacklist)); + } + } + } } + + return result; + } + + /// + /// Returns a list of all antag players who are constrained by a job whitelist or blacklist from an antag. + /// + /// Antag prototype we're excluding for our returned job whitelist/blacklist. + /// A dictionary of antag sessions, and their job blacklists. + [PublicAPI] + public Dictionary>? Whitelist, HashSet>? Blacklist)> + GetAntagJobs(ProtoId except) + { + var result = new Dictionary>? Whitelist, HashSet>? Blacklist)>(); + var query = QueryAllRules(); + while (query.MoveNext(out var comp, out _)) + { + foreach (var antag in comp.Antags) + { + if (except == antag) + continue; + + if (!comp.PreSelectedSessions.TryGetValue(antag, out var set) || !ProtoMan.Resolve(antag.Proto, out var proto)) + continue; + + // Check this here so we don't make a dictionary entry for a bunch of players, with empty blacklists and whitelists. + if (proto.JobBlacklist == null && proto.JobWhitelist == null) + continue; + + foreach (var player in set) + { + if (result.TryGetValue(player, out var jobs)) + { + if (proto.JobWhitelist != null) + { + if (jobs.Whitelist == null) + jobs.Whitelist = proto.JobWhitelist; + else + jobs.Whitelist.UnionWith(proto.JobWhitelist); + } + + if (proto.JobBlacklist != null) + { + if (jobs.Blacklist == null) + jobs.Blacklist = proto.JobBlacklist; + else + jobs.Blacklist.UnionWith(proto.JobBlacklist); + } + } + else + { + result.Add(player, (proto.JobWhitelist, proto.JobBlacklist)); + } + } + } + } + + return result; } /// @@ -390,15 +480,12 @@ public sealed partial class AntagSelectionSystem /// A dictionary of antag sessions, and their job blacklists. [PublicAPI] public Dictionary>? Whitelist, HashSet>? Blacklist)> - GetAntagJobs(params HashSet> except) + GetAntagJobs(HashSet> except) { var result = new Dictionary>? Whitelist, HashSet>? Blacklist)>(); var query = QueryAllRules(); - while (query.MoveNext(out var uid, out var comp, out _)) + while (query.MoveNext(out var comp, out _)) { - if (HasComp(uid)) - continue; - foreach (var antag in comp.Antags) { if (except.Contains(antag)) @@ -450,16 +537,60 @@ public sealed partial class AntagSelectionSystem /// A tuple of a whitelist and blacklist hashset for this player. [PublicAPI] public (HashSet>? Whitelist, HashSet>? Blacklist) - GetAntagJobs(ICommonSession player, params HashSet> except) + GetAntagJobs(ICommonSession player, ProtoId except) { HashSet>? whitelist = null; HashSet>? blacklist = null; var query = QueryAllRules(); - while (query.MoveNext(out var uid, out var comp, out _)) + while (query.MoveNext(out var comp, out _)) { - if (HasComp(uid)) - continue; + foreach (var antag in comp.Antags) + { + if (except == antag) + continue; + if (!comp.PreSelectedSessions.TryGetValue(antag, out var set) || !set.Contains(player)) + continue; + + if (!ProtoMan.Resolve(antag.Proto, out var proto)) + continue; + + if (proto.JobWhitelist != null) + { + if (whitelist == null) + whitelist = proto.JobWhitelist; + else + whitelist.UnionWith(proto.JobWhitelist); + } + + if (proto.JobBlacklist != null) + { + if (blacklist == null) + blacklist = proto.JobBlacklist; + else + blacklist.UnionWith(proto.JobBlacklist); + } + } + } + + return (whitelist, blacklist); + } + + /// + /// Returns a list of all blacklisted and whitelisted jobs for this player + /// + /// Player we're checking the blocked jobs of + /// Antag prototypes we're excluding in our search + /// A tuple of a whitelist and blacklist hashset for this player. + [PublicAPI] + public (HashSet>? Whitelist, HashSet>? Blacklist) + GetAntagJobs(ICommonSession player, HashSet> except) + { + HashSet>? whitelist = null; + HashSet>? blacklist = null; + var query = QueryAllRules(); + while (query.MoveNext(out var comp, out _)) + { foreach (var antag in comp.Antags) { if (except.Contains(antag)) @@ -497,15 +628,36 @@ public sealed partial class AntagSelectionSystem /// /// A specific definition to be excluded from the check. [PublicAPI] - public HashSet GetPreSelectedAntagSessions(params HashSet> except) + public HashSet GetPreSelectedAntagSessions(ProtoId except) { var result = new HashSet(); var query = QueryAllRules(); - while (query.MoveNext(out var uid, out var comp, out _)) + while (query.MoveNext(out var comp, out _)) { - if (HasComp(uid)) - continue; + foreach (var antag in comp.Antags) + { + if (except == antag) + continue; + if (comp.PreSelectedSessions.TryGetValue(antag, out var set)) + result.UnionWith(set); + } + } + + return result; + } + + /// + /// Get all sessions that have been preselected for antag. + /// + /// A specific definition to be excluded from the check. + [PublicAPI] + public HashSet GetPreSelectedAntagSessions(HashSet> except) + { + var result = new HashSet(); + var query = QueryAllRules(); + while (query.MoveNext(out var comp, out _)) + { foreach (var antag in comp.Antags) { if (except.Contains(antag)) @@ -546,23 +698,7 @@ public sealed partial class AntagSelectionSystem /// Optional list of antag preferences we're specifically looking for. /// A list of all antags which the player meets the requirement for, and are contained in the filter [PublicAPI] - public IEnumerable> GetValidAntagPreferences(ICommonSession session, List>? filter = null) - { - if (!_pref.TryGetCachedPreferences(session.UserId, out var prefs)) - yield break; - - foreach (var antag in prefs.SelectedCharacter.AntagPreferences) - { - // We also check this in IsSessionValid, but we also check it here since this is public API. - if (_ban.IsRoleBanned(session, antag) || !_playTime.IsAllowed(session, antag)) - continue; - - if (filter != null && !filter.Contains(antag)) - continue; - - yield return antag; - } - } + public abstract IEnumerable> GetValidAntagPreferences(ICommonSession session, List>? filter = null); /// /// Checks if a player has been assigned antag for a specific game rule. @@ -573,7 +709,7 @@ public sealed partial class AntagSelectionSystem public bool IsAssignedAntag(Entity gameRule, ICommonSession player) { // First check our mindroles. - if (_role.PlayerIsAntagonist(player)) + if (Role.PlayerIsAntagonist(player)) return true; foreach (var (_, sessions) in gameRule.Comp.PreSelectedSessions) @@ -610,16 +746,47 @@ public sealed partial class AntagSelectionSystem /// You can only get one antag per game rule so it's fine to ignore by uid. /// True if there is a game rule giving this player antag status [PublicAPI] - public bool IsAssignedAntag(ICommonSession player, params HashSet ignored) + public bool IsAssignedAntag(ICommonSession player, EntityUid ignored) { // First check our mindroles. - if (_role.PlayerIsAntagonist(player)) + if (Role.PlayerIsAntagonist(player)) return true; var query = QueryAllRules(); while (query.MoveNext(out var uid, out var comp, out _)) { - if (ignored.Contains(uid) || HasComp(uid)) + if (ignored == uid) + continue; + + foreach (var (_, sessions) in comp.PreSelectedSessions) + { + // Session has already been preselected as antagonist, and therefore *has* been assigned antag! + if (sessions.Contains(player)) + return true; + } + } + + return false; + } + + /// + /// Checks if the given player is currently assigned antag for any game rule. + /// + /// Player who may or may not be the antagonist. + /// Game rule entities we're ignoring. + /// You can only get one antag per game rule so it's fine to ignore by uid. + /// True if there is a game rule giving this player antag status + [PublicAPI] + public bool IsAssignedAntag(ICommonSession player, HashSet ignored) + { + // First check our mindroles. + if (Role.PlayerIsAntagonist(player)) + return true; + + var query = QueryAllRules(); + while (query.MoveNext(out var uid, out var comp, out _)) + { + if (ignored.Contains(uid)) continue; foreach (var (_, sessions) in comp.PreSelectedSessions) @@ -640,16 +807,51 @@ public sealed partial class AntagSelectionSystem /// Game rule entities we're ignoring. /// True if there is a game rule giving this player antag status that is exclusive with other antags [PublicAPI] - public bool IsAssignedExclusiveAntag(ICommonSession player, params HashSet ignored) + public bool IsAssignedExclusiveAntag(ICommonSession player, EntityUid ignored) { // First check our mindroles. - if (_role.MindIsExclusiveAntagonist(player.AttachedEntity)) + if (Role.MindIsExclusiveAntagonist(player.AttachedEntity)) return true; var query = QueryAllRules(); while (query.MoveNext(out var uid, out var comp, out _)) { - if (ignored.Contains(uid) || HasComp(uid)) + if (ignored == uid) + continue; + + foreach (var (proto, sessions) in comp.PreSelectedSessions) + { + if (!ProtoMan.Resolve(proto, out var def)) + continue; // How did you even get here? + + if (!sessions.Contains(player)) + continue; + + if (def.MultiAntagSetting == AntagAcceptability.None) + return true; + } + } + + return false; + } + + /// + /// Checks if the given player is currently assigned antag for any game rule that is incompatible with other antag prototypes. + /// + /// Player who may or may not be the antagonist. + /// Game rule entities we're ignoring. + /// True if there is a game rule giving this player antag status that is exclusive with other antags + [PublicAPI] + public bool IsAssignedExclusiveAntag(ICommonSession player, HashSet ignored) + { + // First check our mindroles. + if (Role.MindIsExclusiveAntagonist(player.AttachedEntity)) + return true; + + var query = QueryAllRules(); + while (query.MoveNext(out var uid, out var comp, out _)) + { + if (ignored.Contains(uid)) continue; foreach (var (proto, sessions) in comp.PreSelectedSessions) diff --git a/Content.Server/Antag/AntagSelectionSystem.cs b/Content.Shared/Antag/AntagSelectionSystem.cs similarity index 81% rename from Content.Server/Antag/AntagSelectionSystem.cs rename to Content.Shared/Antag/AntagSelectionSystem.cs index 8ca6c70e6e..b02ebf914c 100644 --- a/Content.Server/Antag/AntagSelectionSystem.cs +++ b/Content.Shared/Antag/AntagSelectionSystem.cs @@ -1,43 +1,28 @@ -using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Linq; -using Content.Server.Administration.Managers; -using Content.Server.Antag.Components; -using Content.Server.Chat.Managers; -using Content.Server.GameTicking; -using Content.Server.GameTicking.Events; -using Content.Server.GameTicking.Rules; -using Content.Server.Ghost.Roles; -using Content.Server.Ghost.Roles.Components; -using Content.Server.Mind; -using Content.Server.Objectives; -using Content.Server.Players.PlayTimeTracking; -using Content.Server.Preferences.Managers; -using Content.Server.Roles; -using Content.Server.Roles.Jobs; -using Content.Server.Shuttles.Systems; using Content.Shared.Administration.Logs; -using Content.Shared.Antag; +using Content.Shared.Antag.Components; using Content.Shared.Clothing; using Content.Shared.Database; using Content.Shared.Follower; using Content.Shared.GameTicking; using Content.Shared.GameTicking.Components; +using Content.Shared.GameTicking.Rules; +using Content.Shared.Mind; using Content.Shared.Random.Helpers; using Content.Shared.Roles; +using Content.Shared.Roles.Jobs; +using Content.Shared.Shuttles.Systems; using Content.Shared.Whitelist; -using JetBrains.Annotations; -using Robust.Server.Audio; -using Robust.Server.GameObjects; -using Robust.Server.Player; +using Robust.Shared.Audio.Systems; using Robust.Shared.Map; using Robust.Shared.Player; using Robust.Shared.Prototypes; using Robust.Shared.Random; using Robust.Shared.Utility; -using static Content.Server.Antag.Components.AntagSelectionTime; +using static Content.Shared.Antag.Components.AntagSelectionTime; -namespace Content.Server.Antag; +namespace Content.Shared.Antag; /// /// Turns players into antags. @@ -54,24 +39,19 @@ namespace Content.Server.Antag; /// I hope this system is now readable and significantly less buggy thanks to my efforts. /// I could do more, but I've been soaped enough. Now it's your turn to fix it. /// -public sealed partial class AntagSelectionSystem : GameRuleSystem +public abstract partial class AntagSelectionSystem : GameRuleSystem { - [Dependency] private IBanManager _ban = default!; - [Dependency] private IChatManager _chat = default!; - [Dependency] private IPlayerManager _playerManager = default!; - [Dependency] private IServerPreferencesManager _pref = default!; - [Dependency] private ISharedAdminLogManager _adminLogger = default!; + [Dependency] protected ISharedPlayerManager PlayerManager = default!; + [Dependency] protected ISharedAdminLogManager AdminLogger = default!; [Dependency] private ArrivalsSystem _arrivals = default!; - [Dependency] private AudioSystem _audio = default!; - [Dependency] private EntityWhitelistSystem _whitelist = default!; - [Dependency] private FollowerSystem _follower = default!; - [Dependency] private GhostRoleSystem _ghostRole = default!; - [Dependency] private JobSystem _jobs = default!; - [Dependency] private LoadoutSystem _loadout = default!; - [Dependency] private MindSystem _mind = default!; - [Dependency] private PlayTimeTrackingSystem _playTime = default!; - [Dependency] private RoleSystem _role = default!; - [Dependency] private TransformSystem _transform = default!; + [Dependency] protected EntityWhitelistSystem Whitelist = default!; + [Dependency] protected FollowerSystem Follower = default!; + [Dependency] protected LoadoutSystem Loadout = default!; + [Dependency] protected SharedAudioSystem Audio = default!; + [Dependency] protected SharedJobSystem Jobs = default!; + [Dependency] protected SharedMindSystem Mind = default!; + [Dependency] protected SharedRoleSystem Role = default!; + [Dependency] protected SharedTransformSystem XForm = default!; // arbitrary random number to give late joining some mild interest. public const float LateJoinRandomChance = 0.5f; @@ -103,77 +83,37 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem(OnTakeGhostRole); - - SubscribeLocalEvent(OnObjectivesTextGetInfo); - // In order of how these occur. SubscribeLocalEvent(OnPlayerSpawning); - SubscribeLocalEvent(OnJobNotAssigned); SubscribeLocalEvent(OnJobsAssigned); SubscribeLocalEvent(OnSpawnComplete); } - protected override void Started(EntityUid uid, AntagSelectionComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args) + protected override void Started(Entity rule, ref GameRuleStartedEvent args) { - base.Started(uid, component, gameRule, args); + base.Started(rule, ref args); // If we're not in round, don't spawn or assign antags. Those will be handled by RulePlayerSpawning, and RulePlayerJobs if (GameTicker.RunLevel != GameRunLevel.InRound) return; - if (component.AssignmentHandled) + if (rule.Comp1.AssignmentHandled) return; // Antags haven't been selected so we need to select them! Only if we select when the game rule starts though! - if (component.PreSelectionsComplete) + if (rule.Comp1.PreSelectionsComplete) { - AssignPreSelectedSessions((uid, component)); + AssignPreSelectedSessions(rule); return; } // If pre-selections haven't completed, then we need to select and assign antags. var players = GetActivePlayers().ToArray(); - if (component.SelectionTime == RuleStarted) // Only pre-select antags if we pre-select on rule start - AssignAntags((uid, component), players); + if (rule.Comp1.SelectionTime == RuleStarted) // Only pre-select antags if we pre-select on rule start + AssignAntags(rule, players); else // Otherwise, we only spawn the ghost roles! - SpawnGhostRoles((uid, component), players.Length); - } - - private void OnTakeGhostRole(Entity ent, ref TakeGhostRoleEvent args) - { - if (args.TookRole) - return; - - if (ent.Comp.Rule is not { } rule || ent.Comp.Definition is not { } proto) - return; - - if (!ProtoMan.Resolve(proto, out var def)) - return; - - if (!Exists(rule) || !RuleQuery.TryComp(rule, out var select)) - return; - - // Ensure the player is allowed to play this antagonist! - if (IsAntagBanned(args.Player, def) || !_playTime.IsAllowed(args.Player, def.PrefRoles)) - return; - - if (!TrySpawnAntagonist((rule, select), def, args.Player, _transform.GetMapCoordinates(ent), out var uid)) - { - Log.Error($"Tried to make {args.Player.UserId} into an antagonist but was unable to spawn an entity for them. Game rule {ToPrettyString(ent)}"); - return; - } - - // We do this after TrySpawnAntagonist so we don't have to worry about a failed spawn adding permanent pre selections to a game rule. - PreSelectSession((rule, select), def, args.Player); - InitializeAntag((rule, select), def, uid.Value, args.Player); - args.TookRole = true; - - // Move ghosts that were watching the raffle on the spawner over to the freshly spawned antag. - _follower.TransferFollowers(ent.Owner, uid.Value); - - _ghostRole.UnregisterGhostRole((ent, Comp(ent))); + SpawnGhostRoles(rule, players.Length); } private void OnSpawnComplete(PlayerSpawnCompleteEvent args) @@ -197,7 +137,7 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem gameRule, int playerCount, ref List preSpawnRoles, @@ -400,7 +316,7 @@ public sealed partial class AntagSelectionSystem : GameRuleSystemGame rule which has chosen this player for antag. /// Antag prototype this player will become. /// Player. - private void PreSelectSession(Entity gameRule, ProtoId protoId, ICommonSession player) + protected void PreSelectSession(Entity gameRule, ProtoId protoId, ICommonSession player) { if (!gameRule.Comp.PreSelectedSessions.TryGetValue(protoId, out var set)) gameRule.Comp.PreSelectedSessions.Add(protoId, set = new HashSet()); @@ -582,7 +498,7 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem @@ -618,7 +534,7 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem @@ -695,7 +611,7 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem - private bool TrySpawnAntagonist(Entity gameRule, + protected bool TrySpawnAntagonist(Entity gameRule, AntagSpecifierPrototype prototype, ICommonSession player, MapCoordinates coordinates, @@ -729,7 +645,7 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem - private void InitializeAntag(Entity gameRule, AntagSpecifierPrototype prototype, EntityUid antag, ICommonSession player) + protected void InitializeAntag(Entity gameRule, AntagSpecifierPrototype prototype, EntityUid antag, ICommonSession player) { // Make sure player was properly pre-selected. - Debug.Assert(gameRule.Comp.PreSelectedSessions.TryGetValue(prototype.ID, out var value) && value.Contains(player), + DebugTools.Assert(gameRule.Comp.PreSelectedSessions.TryGetValue(prototype.ID, out var value) && value.Contains(player), $"Game rule {ToPrettyString(gameRule)}, failed to pre-assign {player.Name} to antag {prototype.ID}"); // The following is where we apply components, equipment, and other changes to our antagonist entity. @@ -788,18 +704,18 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem gameRule, ProtoId proto, EntityUid mind, EntityUid antag) + protected void AssignMind(Entity gameRule, ProtoId proto, EntityUid mind, EntityUid antag) { if (gameRule.Comp.AssignedMinds.TryGetValue(proto, out var minds)) { @@ -820,15 +736,6 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem ent, ref ObjectivesTextGetInfoEvent args) - { - if (ent.Comp.AgentName is not { } name) - return; - - args.Minds = GetAntagIdentities(ent.AsNullable()).ToList(); - args.AgentName = Loc.GetString(name); - } } /// diff --git a/Content.Server/Antag/Components/AntagSelectionComponent.cs b/Content.Shared/Antag/Components/AntagSelectionComponent.cs similarity index 92% rename from Content.Server/Antag/Components/AntagSelectionComponent.cs rename to Content.Shared/Antag/Components/AntagSelectionComponent.cs index de26f81843..84581a32b0 100644 --- a/Content.Server/Antag/Components/AntagSelectionComponent.cs +++ b/Content.Shared/Antag/Components/AntagSelectionComponent.cs @@ -1,14 +1,12 @@ -using Content.Server.Administration.Systems; -using Content.Server.Antag.Selectors; -using Content.Server.GameTicking; -using Content.Shared.Antag; +using Content.Shared.Antag.Selectors; +using Content.Shared.GameTicking; using Content.Shared.GameTicking.Components; using Robust.Shared.Player; using Robust.Shared.Prototypes; -namespace Content.Server.Antag.Components; +namespace Content.Shared.Antag.Components; -[RegisterComponent, Access(typeof(AntagSelectionSystem), typeof(AdminVerbSystem))] +[RegisterComponent, Access(typeof(AntagSelectionSystem))] public sealed partial class AntagSelectionComponent : Component { /// diff --git a/Content.Server/Antag/Components/GhostRoleAntagSpawnerComponent.cs b/Content.Shared/Antag/Components/GhostRoleAntagSpawnerComponent.cs similarity index 84% rename from Content.Server/Antag/Components/GhostRoleAntagSpawnerComponent.cs rename to Content.Shared/Antag/Components/GhostRoleAntagSpawnerComponent.cs index 0c563f3c40..5eaf887a9f 100644 --- a/Content.Server/Antag/Components/GhostRoleAntagSpawnerComponent.cs +++ b/Content.Shared/Antag/Components/GhostRoleAntagSpawnerComponent.cs @@ -1,7 +1,6 @@ -using Content.Shared.Antag; using Robust.Shared.Prototypes; -namespace Content.Server.Antag.Components; +namespace Content.Shared.Antag.Components; /// /// Ghost role spawner that creates an antag for the associated game rule. diff --git a/Content.Server/Antag/Selectors/AntagCountSelector.cs b/Content.Shared/Antag/Selectors/AntagCountSelector.cs similarity index 90% rename from Content.Server/Antag/Selectors/AntagCountSelector.cs rename to Content.Shared/Antag/Selectors/AntagCountSelector.cs index 3f02581385..351e4f06ba 100644 --- a/Content.Server/Antag/Selectors/AntagCountSelector.cs +++ b/Content.Shared/Antag/Selectors/AntagCountSelector.cs @@ -1,9 +1,8 @@ -using Content.Shared.Antag; -using Content.Shared.Destructible.Thresholds; +using Content.Shared.Destructible.Thresholds; using Robust.Shared.Prototypes; using Robust.Shared.Random; -namespace Content.Server.Antag.Selectors; +namespace Content.Shared.Antag.Selectors; /// /// An abstract class meant to return the amount of antags to spawn. diff --git a/Content.Server/Antag/Selectors/FixedAntagCount.cs b/Content.Shared/Antag/Selectors/FixedAntagCount.cs similarity index 90% rename from Content.Server/Antag/Selectors/FixedAntagCount.cs rename to Content.Shared/Antag/Selectors/FixedAntagCount.cs index 17b4dc30d0..03c7612bbc 100644 --- a/Content.Server/Antag/Selectors/FixedAntagCount.cs +++ b/Content.Shared/Antag/Selectors/FixedAntagCount.cs @@ -1,6 +1,6 @@ using Robust.Shared.Random; -namespace Content.Server.Antag.Selectors; +namespace Content.Shared.Antag.Selectors; /// /// Always spawns this many antags. diff --git a/Content.Server/Antag/Selectors/LinearAntagCount.cs b/Content.Shared/Antag/Selectors/LinearAntagCount.cs similarity index 90% rename from Content.Server/Antag/Selectors/LinearAntagCount.cs rename to Content.Shared/Antag/Selectors/LinearAntagCount.cs index 78fce94733..54e5e75fec 100644 --- a/Content.Server/Antag/Selectors/LinearAntagCount.cs +++ b/Content.Shared/Antag/Selectors/LinearAntagCount.cs @@ -1,6 +1,6 @@ using Robust.Shared.Random; -namespace Content.Server.Antag.Selectors; +namespace Content.Shared.Antag.Selectors; /// /// Spawns a constrained number of antags that scales linearly. diff --git a/Content.Shared/CCVar/CCVars.Shuttle.cs b/Content.Shared/CCVar/CCVars.Shuttle.cs index c29ee7a488..e975a9484f 100644 --- a/Content.Shared/CCVar/CCVars.Shuttle.cs +++ b/Content.Shared/CCVar/CCVars.Shuttle.cs @@ -31,7 +31,7 @@ public sealed partial class CCVars /// Whether the arrivals shuttle is enabled. /// public static readonly CVarDef ArrivalsShuttles = - CVarDef.Create("shuttle.arrivals", true, CVar.SERVERONLY); + CVarDef.Create("shuttle.arrivals", true); /// /// The map to use for the arrivals station. @@ -55,7 +55,7 @@ public sealed partial class CCVars /// Should all players who spawn at arrivals have godmode until they leave the map? /// public static readonly CVarDef GodmodeArrivals = - CVarDef.Create("shuttle.godmode_arrivals", false, CVar.SERVERONLY); + CVarDef.Create("shuttle.godmode_arrivals", false); /// /// If a grid is split then hide any smaller ones under this mass (kg) from the map. diff --git a/Content.Shared/Clock/SharedClockSystem.cs b/Content.Shared/Clock/SharedClockSystem.cs index c07fb5e83e..da8f5b6afe 100644 --- a/Content.Shared/Clock/SharedClockSystem.cs +++ b/Content.Shared/Clock/SharedClockSystem.cs @@ -6,7 +6,7 @@ namespace Content.Shared.Clock; public abstract partial class SharedClockSystem : EntitySystem { - [Dependency] private SharedGameTicker _ticker = default!; + [Dependency] private GameTicker _ticker = default!; /// public override void Initialize() diff --git a/Content.Shared/Clothing/LoadoutSystem.cs b/Content.Shared/Clothing/LoadoutSystem.cs index fcb1982dc5..fe541684d8 100644 --- a/Content.Shared/Clothing/LoadoutSystem.cs +++ b/Content.Shared/Clothing/LoadoutSystem.cs @@ -5,7 +5,7 @@ using Content.Shared.Humanoid; using Content.Shared.Preferences; using Content.Shared.Preferences.Loadouts; using Content.Shared.Roles; -using Content.Shared.Station; +using Content.Shared.Station.Systems; using Robust.Shared.Player; using Robust.Shared.Prototypes; using Robust.Shared.Random; @@ -20,7 +20,7 @@ public sealed partial class LoadoutSystem : EntitySystem // Shared so we can predict it for placement manager. [Dependency] private ActorSystem _actors = default!; - [Dependency] private SharedStationSpawningSystem _station = default!; + [Dependency] private StationSpawningSystem _station = default!; [Dependency] private IRobustRandom _random = default!; public override void Initialize() diff --git a/Content.Server/Codewords/CodewordComponent.cs b/Content.Shared/Codewords/CodewordComponent.cs similarity index 88% rename from Content.Server/Codewords/CodewordComponent.cs rename to Content.Shared/Codewords/CodewordComponent.cs index 6ceb3a513a..1116a79622 100644 --- a/Content.Server/Codewords/CodewordComponent.cs +++ b/Content.Shared/Codewords/CodewordComponent.cs @@ -1,4 +1,4 @@ -namespace Content.Server.Codewords; +namespace Content.Shared.Codewords; /// /// Container for generated codewords. diff --git a/Content.Server/Codewords/CodewordFactionPrototype.cs b/Content.Shared/Codewords/CodewordFactionPrototype.cs similarity index 93% rename from Content.Server/Codewords/CodewordFactionPrototype.cs rename to Content.Shared/Codewords/CodewordFactionPrototype.cs index 62552c1966..af5841890b 100644 --- a/Content.Server/Codewords/CodewordFactionPrototype.cs +++ b/Content.Shared/Codewords/CodewordFactionPrototype.cs @@ -1,6 +1,6 @@ using Robust.Shared.Prototypes; -namespace Content.Server.Codewords; +namespace Content.Shared.Codewords; /// /// This is a prototype for easy access to codewords using identifiers instead of magic strings. diff --git a/Content.Server/Codewords/CodewordGeneratorPrototype.cs b/Content.Shared/Codewords/CodewordGeneratorPrototype.cs similarity index 84% rename from Content.Server/Codewords/CodewordGeneratorPrototype.cs rename to Content.Shared/Codewords/CodewordGeneratorPrototype.cs index b17a40fc6a..82a8a09cf2 100644 --- a/Content.Server/Codewords/CodewordGeneratorPrototype.cs +++ b/Content.Shared/Codewords/CodewordGeneratorPrototype.cs @@ -1,7 +1,7 @@ using Content.Shared.Dataset; using Robust.Shared.Prototypes; -namespace Content.Server.Codewords; +namespace Content.Shared.Codewords; /// /// This is a prototype for specifying codeword generation @@ -17,12 +17,11 @@ public sealed partial class CodewordGeneratorPrototype : IPrototype /// List of datasets to use for word generation. All values will be concatenated into one list and then randomly chosen from /// [DataField] - public List> Words { get; private set; } = - [ - "Adjectives", - "Verbs", - ]; - + public List> Words { get; private set; } = new(2) + { + "Adjectives", + "Verbs", + }; /// /// How many codewords should be generated? diff --git a/Content.Server/Codewords/CodewordManagerComponent.cs b/Content.Shared/Codewords/CodewordManagerComponent.cs similarity index 94% rename from Content.Server/Codewords/CodewordManagerComponent.cs rename to Content.Shared/Codewords/CodewordManagerComponent.cs index 46ddc357a1..089065f598 100644 --- a/Content.Server/Codewords/CodewordManagerComponent.cs +++ b/Content.Shared/Codewords/CodewordManagerComponent.cs @@ -1,6 +1,6 @@ using Robust.Shared.Prototypes; -namespace Content.Server.Codewords; +namespace Content.Shared.Codewords; /// /// Component that defines to use and keeps track of generated codewords. diff --git a/Content.Server/Codewords/CodewordSystem.cs b/Content.Shared/Codewords/CodewordSystem.cs similarity index 90% rename from Content.Server/Codewords/CodewordSystem.cs rename to Content.Shared/Codewords/CodewordSystem.cs index 2b8251bd99..6a33dff522 100644 --- a/Content.Server/Codewords/CodewordSystem.cs +++ b/Content.Shared/Codewords/CodewordSystem.cs @@ -1,28 +1,22 @@ using System.Linq; -using Content.Server.Administration.Logs; -using Content.Server.GameTicking.Events; +using Content.Shared.Administration.Logs; using Content.Shared.Database; +using Content.Shared.GameTicking.Events; using Robust.Shared.Map; using Robust.Shared.Prototypes; using Robust.Shared.Random; -namespace Content.Server.Codewords; +namespace Content.Shared.Codewords; /// /// Gamerule that provides codewords for other gamerules that rely on them. /// public sealed partial class CodewordSystem : EntitySystem { - [Dependency] private IAdminLogManager _adminLogger = default!; + [Dependency] private ISharedAdminLogManager _adminLogger = default!; [Dependency] private IRobustRandom _random = default!; - public override void Initialize() - { - base.Initialize(); - - SubscribeLocalEvent(OnRoundStart); - } - + [SubscribeLocalEvent] private void OnRoundStart(RoundStartingEvent ev) { var manager = Spawn(); diff --git a/Content.Shared/Construction/EntitySystems/AnchorableSystem.cs b/Content.Shared/Construction/EntitySystems/AnchorableSystem.cs index 997cc4d3dd..1e5ab9e16f 100644 --- a/Content.Shared/Construction/EntitySystems/AnchorableSystem.cs +++ b/Content.Shared/Construction/EntitySystems/AnchorableSystem.cs @@ -9,7 +9,7 @@ using Content.Shared.Interaction; using Content.Shared.Movement.Pulling.Components; using Content.Shared.Movement.Pulling.Systems; using Content.Shared.Popups; -using Content.Shared.Station; +using Content.Shared.Station.Systems; using Content.Shared.Tools.Components; using Robust.Shared.Map; using Robust.Shared.Map.Components; @@ -28,7 +28,7 @@ public sealed partial class AnchorableSystem : EntitySystem [Dependency] private SharedPopupSystem _popup = default!; [Dependency] private PullingSystem _pulling = default!; [Dependency] private SharedMapSystem _map = default!; - [Dependency] private SharedStationSystem _stationSystem = null!; + [Dependency] private StationSystem _stationSystem = null!; [Dependency] private SharedToolSystem _tool = default!; [Dependency] private SharedTransformSystem _transformSystem = default!; [Dependency] private TagSystem _tagSystem = default!; diff --git a/Content.Shared/CriminalRecords/Systems/SharedCriminalRecordsConsoleSystem.cs b/Content.Shared/CriminalRecords/Systems/SharedCriminalRecordsConsoleSystem.cs index 68b1fb15be..ff22c9dff5 100644 --- a/Content.Shared/CriminalRecords/Systems/SharedCriminalRecordsConsoleSystem.cs +++ b/Content.Shared/CriminalRecords/Systems/SharedCriminalRecordsConsoleSystem.cs @@ -2,6 +2,7 @@ using Content.Shared.IdentityManagement; using Content.Shared.Security; using Content.Shared.Security.Components; using Content.Shared.Station; +using Content.Shared.Station.Systems; using Content.Shared.StationRecords; using Content.Shared.StationRecords.Systems; @@ -11,7 +12,7 @@ public abstract partial class SharedCriminalRecordsConsoleSystem : EntitySystem { [Dependency] private SharedCriminalRecordsSystem _criminalRecords = default!; [Dependency] private StationRecordsSystem _records = default!; - [Dependency] private SharedStationSystem _station = default!; + [Dependency] private StationSystem _station = default!; /// /// Checks if the new identity's name has a criminal record attached to it, and gives the entity the icon that diff --git a/Content.Shared/EntityTable/Conditions/HasBudgetCondition.cs b/Content.Shared/EntityTable/Conditions/HasBudgetCondition.cs index 0eea81f736..ef854978d6 100644 --- a/Content.Shared/EntityTable/Conditions/HasBudgetCondition.cs +++ b/Content.Shared/EntityTable/Conditions/HasBudgetCondition.cs @@ -1,5 +1,6 @@ using Content.Shared.EntityTable.EntitySelectors; using Content.Shared.GameTicking.Rules; +using Content.Shared.GameTicking.Rules.Components; using Robust.Shared.Prototypes; namespace Content.Shared.EntityTable.Conditions; diff --git a/Content.Shared/EntityTable/Conditions/MaxRuleOccurenceCondition.cs b/Content.Shared/EntityTable/Conditions/MaxRuleOccurenceCondition.cs index 1e55feb338..852230485b 100644 --- a/Content.Shared/EntityTable/Conditions/MaxRuleOccurenceCondition.cs +++ b/Content.Shared/EntityTable/Conditions/MaxRuleOccurenceCondition.cs @@ -1,4 +1,3 @@ -using System.Linq; using Content.Shared.EntityTable.EntitySelectors; using Content.Shared.GameTicking; using Robust.Shared.Prototypes; @@ -47,8 +46,8 @@ public sealed partial class MaxRuleOccurenceCondition : EntityTableCondition if (rule == string.Empty) return false; - var gameTicker = entMan.System(); + var gameTicker = entMan.System(); - return gameTicker.AllPreviousGameRules.Count(p => p.Item2 == rule) < Max; + return gameTicker.GetOccurrences(rule) < Max; } } diff --git a/Content.Shared/EntityTable/Conditions/ReoccurrenceDelayCondition.cs b/Content.Shared/EntityTable/Conditions/ReoccurrenceDelayCondition.cs index 0329592a4a..3edfdca35f 100644 --- a/Content.Shared/EntityTable/Conditions/ReoccurrenceDelayCondition.cs +++ b/Content.Shared/EntityTable/Conditions/ReoccurrenceDelayCondition.cs @@ -41,9 +41,7 @@ public sealed partial class ReoccurrenceDelayCondition : EntityTableCondition if (rule == string.Empty) return false; - var gameTicker = entMan.System(); - - return gameTicker.AllPreviousGameRules.Any( - p => p.Item2 == rule && p.Item1 + Delay <= gameTicker.RoundDuration()); + var gameTicker = entMan.System(); + return gameTicker.GetLastRuleTime(rule) + Delay <= gameTicker.RoundDuration(); } } diff --git a/Content.Shared/EntityTable/Conditions/RoundDurationCondition.cs b/Content.Shared/EntityTable/Conditions/RoundDurationCondition.cs index 518faf4bc6..f95696c577 100644 --- a/Content.Shared/EntityTable/Conditions/RoundDurationCondition.cs +++ b/Content.Shared/EntityTable/Conditions/RoundDurationCondition.cs @@ -26,7 +26,7 @@ public sealed partial class RoundDurationCondition : EntityTableCondition IPrototypeManager proto, EntityTableContext ctx) { - var gameTicker = entMan.System(); + var gameTicker = entMan.System(); var duration = gameTicker.RoundDuration(); return duration >= Min && duration <= Max; diff --git a/Content.Shared/GameTicking/Components/ActiveGameRuleComponent.cs b/Content.Shared/GameTicking/Components/ActiveGameRuleComponent.cs index 51bdd1c037..6b535465d0 100644 --- a/Content.Shared/GameTicking/Components/ActiveGameRuleComponent.cs +++ b/Content.Shared/GameTicking/Components/ActiveGameRuleComponent.cs @@ -2,7 +2,6 @@ /// /// Added to game rules before and removed before . -/// Mutually exclusive with . /// [RegisterComponent] public sealed partial class ActiveGameRuleComponent : Component; diff --git a/Content.Shared/GameTicking/Components/EndedGameRuleComponent.cs b/Content.Shared/GameTicking/Components/EndedGameRuleComponent.cs deleted file mode 100644 index 5e209ed78a..0000000000 --- a/Content.Shared/GameTicking/Components/EndedGameRuleComponent.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Content.Shared.GameTicking.Components; - -/// -/// Added to game rules before . -/// Mutually exclusive with . -/// -[RegisterComponent] -public sealed partial class EndedGameRuleComponent : Component; diff --git a/Content.Shared/GameTicking/Components/GameRuleComponent.cs b/Content.Shared/GameTicking/Components/GameRuleComponent.cs index 87a5822d47..bc36c95aa0 100644 --- a/Content.Shared/GameTicking/Components/GameRuleComponent.cs +++ b/Content.Shared/GameTicking/Components/GameRuleComponent.cs @@ -30,6 +30,13 @@ public sealed partial class GameRuleComponent : Component [DataField] public bool CancelPresetOnTooFewPlayers = true; + /// + /// Marks a gamerule as "Silent" meaning that it will run and then proceed to delete itself without logging. + /// This WILL throw a debug assert if it still exists when it's supposed to start! + /// + [DataField] + public bool Silent; + /// /// A delay for when the rule the is started and when the starting logic actually runs. /// @@ -42,18 +49,110 @@ public sealed partial class GameRuleComponent : Component /// Good for announcing station events and other such things. /// [ByRefEvent] -public readonly record struct GameRuleAddedEvent(EntityUid RuleEntity, string RuleId); +public readonly record struct GameRuleAddedEvent(Entity Rule); /// /// Raised when the rule actually begins. /// Player-facing logic should begin here. /// [ByRefEvent] -public readonly record struct GameRuleStartedEvent(EntityUid RuleEntity, string RuleId); +public readonly record struct GameRuleStartedEvent(Entity Rule, EntProtoId? RuleId); /// /// Raised when the rule ends. /// Do cleanup and other such things here. /// [ByRefEvent] -public readonly record struct GameRuleEndedEvent(EntityUid RuleEntity, string RuleId); +public readonly record struct GameRuleEndedEvent(EntityUid Rule); + +/// +/// A simple struct to keep track of the Lifespan of a Gamerule in an organized fashion. +/// +/// Time that this game rule was added, or started +/// EntityUid of the game rule +/// Current lifetime of the game rule. +[Access(typeof(GameTicker))] +public record struct GameRule(TimeSpan StartTime, EntityUid Uid, GameRuleLifeStage LifeStage = GameRuleLifeStage.Added) : IComparable, IComparable +{ + /// + /// The time that this rule has started, or was added if it hasn't started yet! + /// + public TimeSpan StartTime { get; private set; } = StartTime; + + /// + /// EntityUid for this GameRule, it's cheaper memory wise to store an int over the whole ass ProtoId, which is available through MetaDataComp lookup... + /// + public readonly EntityUid Uid = Uid; + + /// + /// The time that this rule has started, or was added if it hasn't started yet! + /// + public GameRuleLifeStage LifeStage { get; private set; } = LifeStage; + + /// + /// Marks that this GameRule has been started. + /// + /// Timespan which we started the rule. + /// True if the game rule successfully started, false if was already started or ended. + public bool StartRule(TimeSpan time) + { + if (LifeStage > GameRuleLifeStage.Added) + return false; + + LifeStage = GameRuleLifeStage.Started; + StartTime = time; + return true; + } + + /// + /// Marks that this GameRule has ended. + /// + public bool EndRule() + { + if (LifeStage == GameRuleLifeStage.Ended) + return false; + + LifeStage = GameRuleLifeStage.Ended; + return true; + } + + public int CompareTo(GameRule other) + { + return CompareTo(other.LifeStage); + } + + public int CompareTo(GameRuleLifeStage other) + { + return LifeStage.CompareTo(other); + } + + public static implicit operator GameRule((TimeSpan Time, EntityUid Rule) tuple) + { + return new GameRule(tuple.Time, tuple.Rule); + } + + public static implicit operator GameRule((TimeSpan Time, EntityUid Rule, GameRuleLifeStage Stage) tuple) + { + return new GameRule(tuple.Time, tuple.Rule, tuple.Stage); + } + + public readonly void Deconstruct(out TimeSpan time, out EntityUid rule) + { + time = StartTime; + rule = Uid; + } + + public readonly void Deconstruct(out TimeSpan time, out EntityUid rule, out GameRuleLifeStage stage) + { + time = StartTime; + rule = Uid; + stage = LifeStage; + } +} + +public enum GameRuleLifeStage : byte +{ + Added = 0, + Started = 1, + Ended = 2, +} diff --git a/Content.Server/GameTicking/Events/RoundStartingEvent.cs b/Content.Shared/GameTicking/Events/RoundStartingEvent.cs similarity index 85% rename from Content.Server/GameTicking/Events/RoundStartingEvent.cs rename to Content.Shared/GameTicking/Events/RoundStartingEvent.cs index cc3ada337b..a3a81bd263 100644 --- a/Content.Server/GameTicking/Events/RoundStartingEvent.cs +++ b/Content.Shared/GameTicking/Events/RoundStartingEvent.cs @@ -1,4 +1,4 @@ -namespace Content.Server.GameTicking.Events; +namespace Content.Shared.GameTicking.Events; /// /// Raised at the start of , after round id has been incremented diff --git a/Content.Shared/GameTicking/GameTicker.CVars.cs b/Content.Shared/GameTicking/GameTicker.CVars.cs new file mode 100644 index 0000000000..61e2048b2d --- /dev/null +++ b/Content.Shared/GameTicking/GameTicker.CVars.cs @@ -0,0 +1,11 @@ +using Content.Shared.CCVar; + +namespace Content.Shared.GameTicking; + +public abstract partial class GameTicker +{ + protected virtual void InitializeCVars() + { + Subs.CVar(Cfg, CCVars.GameTickerIgnoredPresets, value => _ignoredRules = value.Split(",")); + } +} diff --git a/Content.Shared/GameTicking/GameTicker.Chat.cs b/Content.Shared/GameTicking/GameTicker.Chat.cs new file mode 100644 index 0000000000..7d2c3d83d0 --- /dev/null +++ b/Content.Shared/GameTicking/GameTicker.Chat.cs @@ -0,0 +1,57 @@ +using Content.Shared.Station.Components; +using JetBrains.Annotations; +using Robust.Shared.Audio; +using Robust.Shared.Map; + +namespace Content.Shared.GameTicking; + +public abstract partial class GameTicker +{ + /// + /// Dispatches an announcement to all players in game, aka not in the lobby. + /// + /// LocId of the announcement we're sending. + /// Sound that plays with the announcement. + /// Optional color override for the announcement. + [PublicAPI] + public virtual void GameAnnouncement(LocId announcement, SoundSpecifier? announcementSound = null, Color? color = null) { } + + /// + /// Dispatches an announcement to all players on a given map. + /// + /// Map we are dispatching the announcement to. + /// LocId of the announcement we're sending. + /// Sound that plays with the announcement. + /// Optional color override for the announcement. + [PublicAPI] + public virtual void MapAnnouncement(MapId map, LocId announcement, SoundSpecifier? announcementSound = null, Color? color = null) { } + + /// + /// Dispatches an announcement to all players on the same map as the given station. + /// + /// Station m + /// LocId of the announcement we're sending. + /// Sound that plays with the announcement. + /// Optional color override for the announcement. + [PublicAPI] + public virtual void StationMapAnnouncement(Entity station, LocId announcement, SoundSpecifier? announcementSound = null, Color? color = null) { } + + /// + /// Dispatches an announcement to all players on all station grids. + /// + /// LocId of the announcement we're sending. + /// Sound that plays with the announcement. + /// Optional color override for the announcement. + [PublicAPI] + public virtual void StationAnnouncement(LocId announcement, SoundSpecifier? announcementSound = null, Color? color = null) { } + + /// + /// Dispatches an announcement to all players on a given station. + /// + /// Station we are dispatching the announcement to. + /// LocId of the announcement we're sending. + /// Sound that plays with the announcement. + /// Optional color override for the announcement. + [PublicAPI] + public virtual void StationAnnouncement(EntityUid station, LocId announcement, SoundSpecifier? announcementSound = null, Color? color = null) { } +} diff --git a/Content.Shared/GameTicking/GameTicker.GamePreset.cs b/Content.Shared/GameTicking/GameTicker.GamePreset.cs new file mode 100644 index 0000000000..b13f4736c6 --- /dev/null +++ b/Content.Shared/GameTicking/GameTicker.GamePreset.cs @@ -0,0 +1,44 @@ +using Content.Shared.GameTicking.Prototypes; +using Content.Shared.GameTicking.Components; +using JetBrains.Annotations; +using Robust.Shared.Prototypes; + +namespace Content.Shared.GameTicking; + +public abstract partial class GameTicker +{ + + /// + [PublicAPI] + public int GetMinimumPlayerCount(ProtoId proto) + { + if (!ProtoMan.Resolve(proto, out var preset)) + return 0; + + return GetMinimumPlayerCount(preset); + } + + /// + /// Gets the minimum number of players required for a game preset to start. + /// Checks both the preset itself, and all rules to find the minimum. + /// + /// Game preset prototype we're checking. + /// Minimum number of players required for the rule to start. + [PublicAPI] + public int GetMinimumPlayerCount(GamePresetPrototype proto) + { + var min = proto.MinPlayers ?? 0; + foreach (var entProto in proto.Rules) + { + if (!ProtoMan.Resolve(entProto, out var ent)) + continue; + + if (!ent.TryComp(out var rule, Factory)) + continue; + + min = Math.Max(min, rule.MinPlayers); + } + + return min; + } +} diff --git a/Content.Shared/GameTicking/GameTicker.GameRule.cs b/Content.Shared/GameTicking/GameTicker.GameRule.cs new file mode 100644 index 0000000000..237f22c1f5 --- /dev/null +++ b/Content.Shared/GameTicking/GameTicker.GameRule.cs @@ -0,0 +1,589 @@ +using System.Diagnostics.CodeAnalysis; +using Content.Shared.CCVar; +using Content.Shared.Database; +using Content.Shared.GameTicking.Components; +using Content.Shared.Whitelist; +using JetBrains.Annotations; +using Robust.Shared.Map; +using Robust.Shared.Prototypes; + +namespace Content.Shared.GameTicking; + +public abstract partial class GameTicker +{ + /// + /// Designated game rule that spawns a fake antagonist to discourage metagaming. + /// Has to be a string since cannot be a const. + /// + public const string DummyGameRule = "DummyNonAntag"; + + /// + /// List of ignored game rules, these rules won't be spawned by normal means. + /// This list is populated by + /// + [ViewVariables] private string[] _ignoredRules = []; + + [ViewVariables] protected readonly List AllRoundGameRules = []; + + [SubscribeLocalEvent] + private void OnGameRuleAdded(Entity rule, ref MapInitEvent args) + { + // Cache it first in case the game rule is ended due to a fail state being reached! + if (!rule.Comp.Silent && _net.IsServer) + AllRoundGameRules.Add((GetRoundTime(), rule)); + + var ev = new GameRuleAddedEvent(rule); + RaiseLocalEvent(rule, ref ev, true); + } + + [SubscribeLocalEvent] + private void OnGameRuleStarted(Entity rule, ref MapInitEvent args) + { + var ruleComp = RuleQuery.Comp(rule); + Log.Info($"Started game rule {ToPrettyString(rule)}"); + Admin.Add(LogType.EventStarted, $"Started game rule {ToPrettyString(rule)}"); + + if (!ruleComp.Silent) + StartRuleCache(rule); + + var ev = new GameRuleStartedEvent((rule, ruleComp), MetaData(rule).EntityPrototype?.ID); + RaiseLocalEvent(rule, ref ev, true); + + // If the game rule isn't cached, then it shouldn't be running past start! + if (ruleComp.Silent) + EndGameRule((rule, ruleComp)); + } + + [SubscribeLocalEvent] + private void OnGameRuleEnded(Entity rule, ref ComponentShutdown args) + { + RemComp(rule); + var ev = new GameRuleEndedEvent(rule); + RaiseLocalEvent(rule, ref ev, true); + + if (!rule.Comp.Silent) + EndRuleCache(rule); + } + + /// + /// Tries to add a gamerule to the current round, but ignores any + /// + /// Game rule entity that we are trying to spawn + /// Forces the game rule to spawn regardless of if it's ignored or not + /// The entityUid of the spawned game rule, if it wasn't ignored. + public Entity? AddGameRule([ForbidLiteral] EntProtoId gameRule, bool force = false) + { + if (!force && IsIgnored(gameRule)) + return null; + + return SpawnGameRule(gameRule); + } + + /// + /// Checks if this GameRule should be ignored before a spawning attempt. + /// + /// GameRule we are trying to validate + /// True if the gamerule should be ignored and not spawned. + public bool IsIgnored([ForbidLiteral] EntProtoId gameRule) + { + return _ignoredRules.Contains(gameRule); + } + + /// + /// Spawns a GameRule in nullspace! Protected since it ignores the rule blacklist! + /// + /// Name of the game rule we're spawning + /// EntityUid of the rule we spawned. + protected virtual Entity SpawnGameRule(EntProtoId ruleId) + { + var rule = Spawn(ruleId, MapCoordinates.Nullspace); + FlagPredicted(rule); + var meta = MetaData(rule); + Log.Info($"Added game rule {ToPrettyString((rule, meta))}"); + Admin.Add(LogType.EventStarted, $"Added game rule {ToPrettyString((rule, meta))}"); + + // This should probably be a bool on the GameRuleComponent... + if (RuleQuery.TryComp(rule, out var ruleComp)) + return (rule, ruleComp); + + Log.Error($"Entity {ToPrettyString((rule, meta))} lacked a {nameof(GameRuleComponent)}!"); + ruleComp = AddComp(rule); + return (rule, ruleComp); + } + + /// + /// Game rules can be 'started' separately from being added. 'Starting' them usually + /// happens at round start while they can be added and removed before then. + /// + [PublicAPI] + public bool StartGameRule([ForbidLiteral] EntProtoId ruleId) + { + return StartGameRule(ruleId, out _); + } + + /// + /// Game rules can be 'started' separately from being added. 'Starting' them usually + /// happens at round start while they can be added and removed before then. + /// + [PublicAPI] + public bool StartGameRule([ForbidLiteral] EntProtoId ruleId, [NotNullWhen(true)] out Entity? ruleEntity) + { + ruleEntity = AddGameRule(ruleId); + return ruleEntity != null && StartGameRule(ruleEntity.Value.AsNullable()); // Worst shit I've ever seen in C# + } + + /// + /// Game rules can be 'started' separately from being added. 'Starting' them usually + /// happens at round start while they can be added and removed before then. + /// + [PublicAPI] + public bool StartGameRule(Entity rule) + { + // Game rule has already ended itself, or this was never a game rule... + if (!RuleQuery.Resolve(rule, ref rule.Comp, false)) + return false; + + // can't start an already active rule + if (ActiveRuleQuery.HasComp(rule)) + return false; + + // TODO: Don't want to refactor this now, but this seems unnecessary and like it's hiding bugs... + // If this rule has a delay, check if the delay was already applied. If it was, then start the rule now. + if (rule.Comp.Delay == null || RemComp(rule)) + { + AddComp(rule); + rule.Comp.ActivatedAt = Timing.CurTime; + return true; + } + + var delayTime = TimeSpan.FromSeconds(rule.Comp.Delay.Value.Next(Random)); + + if (delayTime <= TimeSpan.Zero) + return true; + + Log.Info($"Queued start for game rule {ToPrettyString(rule)} with delay {delayTime}"); + Admin.Add(LogType.EventStarted, + $"Queued start for game rule {ToPrettyString(rule)} with delay {delayTime}"); + + var delayed = EnsureComp(rule); + delayed.RuleStartTime = Timing.CurTime + (delayTime); + return true; + } + + private void StartRuleCache(EntityUid uid) + { + if (_net.IsClient) + return; + + // Very likely to be a recently added rule, so we start from the top! + for (var i = AllRoundGameRules.Count - 1; i >= 0; i--) + { + var rule = AllRoundGameRules[i]; + if (rule.Uid != uid) + continue; + + if (!rule.StartRule(GetRoundTime())) + Log.Error($"Rule {ToPrettyString(uid)} tried to be started, but was already started!"); + + AllRoundGameRules[i] = rule; + return; + } + + Log.Error($"Rule {ToPrettyString(uid)} was started but had not been added yet somehow!"); + AllRoundGameRules.Add((GetRoundTime(), uid, GameRuleLifeStage.Started)); + } + + private void EndRuleCache(EntityUid uid) + { + if (_net.IsClient) + return; + + // Very likely to be a recently added rule, so we start from the top! + for (var i = AllRoundGameRules.Count - 1; i >= 0; i--) + { + var rule = AllRoundGameRules[i]; + if (rule.Uid != uid) + continue; + + if (!rule.EndRule()) + Log.Error($"Rule {ToPrettyString(uid)} tried to be ended, but was already ended!"); + + AllRoundGameRules[i] = rule; + return; + } + + // We don't log an error here because it's possible a game rule was added before MapInit, and then removed. + // Removals of game rules in this scenario should already throw errors elsewhere, logging here just fails tests. + } + + /// + /// Ends a game rule. + /// + [PublicAPI] + public bool EndGameRule(Entity rule) + { + // Don't log missing because we could've already ended the rule. + // TODO: Maybe do log missing to ensure we only ever try to end a rule once? + if (!Resolve(rule, ref rule.Comp, false)) + return false; + + RemComp(rule, rule.Comp); + + Log.Info($"Ended game rule {ToPrettyString(rule)}"); + Admin.Add(LogType.EventStopped, $"Ended game rule {ToPrettyString(rule)}"); + return true; + } + + /// + /// Returns true if a game rule with the given component has been added. + /// + [PublicAPI] + public bool IsGameRuleAdded() + where T : IComponent + { + var query = EntityQueryEnumerator(); + return query.MoveNext(out _, out _, out _); + } + + /// + /// Returns if a game rule entity has been added yet. + /// + /// Game Rule entity + /// True if the rule has been added, and has not ended yet. + [PublicAPI] + public bool IsGameRuleAdded(Entity rule) + { + return Resolve(rule, ref rule.Comp); + } + + /// + /// Checks if a game rule of a specific prototype has been added yet. + /// + /// Rule prototype we're checking for + /// True if a game rule exists with this prototype, that has not ended yet. + [PublicAPI] + [Obsolete($"Use generic {nameof(IsGameRuleAdded)} instead.")] + public bool IsGameRuleAdded([ForbidLiteral] string rule) + { + foreach (var ruleEntity in GetAddedGameRules()) + { + if (MetaData(ruleEntity).EntityPrototype?.ID == rule) + return true; + } + + return false; + } + + /// + /// Returns true if a game rule that passes the whitelist and blacklist has been added. + /// + /// whitelist for the game rules + /// blacklist for the game rules + [PublicAPI] + public bool IsGameRuleAdded(EntityWhitelist? ruleWhitelist, EntityWhitelist? ruleBlacklist = null) + { + foreach (var ruleEntity in GetAddedGameRules()) + { + if (Whitelist.CheckBoth(ruleEntity, ruleBlacklist, ruleWhitelist)) + return true; + } + + return false; + } + + /// + /// Returns true if a game rule with the given component is active.. + /// + [PublicAPI] + public bool IsGameRuleActive() + where T : IComponent + { + var query = EntityQueryEnumerator(); + // out, damned underscore!!! + while (query.MoveNext(out _, out _, out _, out _)) + { + return true; + } + + return false; + } + + /// + /// Checks if a game rule entity has been added, and has been started. + /// + /// Entity we are checking. + /// True if the game rule has been added, and has been started. + [PublicAPI] + public bool IsGameRuleActive(Entity entity) + { + return Resolve(entity, ref entity.Comp) && ActiveRuleQuery.HasComp(entity); + } + + /// + /// Checks if a game rule with a given prototype has been added, and has been started. + /// + /// Prototype we are looking for. + /// True if the game rule has been added, and has been started. + [PublicAPI] + [Obsolete($"Use generic {nameof(IsGameRuleActive)} instead.")] + public bool IsGameRuleActive([ForbidLiteral] string rule) + { + foreach (var ruleEntity in GetActiveGameRules()) + { + if (MetaData(ruleEntity).EntityPrototype?.ID == rule) + return true; + } + + return false; + } + + /// + /// Returns true if a game rule that passes the whitelist and blacklist is active. + /// + /// whitelist for the game rules + /// blacklist for the game rules + [PublicAPI] + public bool IsGameRuleActive(EntityWhitelist? ruleWhitelist, EntityWhitelist? ruleBlacklist = null) + { + foreach (var ruleEntity in GetActiveGameRules()) + { + if (Whitelist.CheckBoth(ruleEntity, ruleBlacklist, ruleWhitelist)) + return true; + } + + return false; + } + + + /// + /// Gets all the game rule entities that have been added. + /// + [PublicAPI] + public IEnumerable GetAddedGameRules() + { + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var ruleData)) + { + if (IsGameRuleAdded((uid, ruleData))) + yield return uid; + } + } + + /// + /// Gets all the game rule entities which have not ended and match the given prototype. + /// + [PublicAPI] + public IEnumerable GetAddedGameRules(EntProtoId stationEvent) + { + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var ruleData, out var meta)) + { + if (IsGameRuleAdded((uid, ruleData)) && meta.EntityPrototype is { } id && id == stationEvent) + yield return uid; + } + } + + /// + /// Gets all the gamerule entities with {T} component that have been added. + /// + [PublicAPI] + public IEnumerable> GetAddedGameRules() where T : Component + { + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var comp, out var ruleData)) + { + if (IsGameRuleAdded((uid, ruleData))) + yield return (uid, comp); + } + } + + /// + /// Gets all the game rule entities with {T} which have not ended and match the given prototype. + /// + [PublicAPI] + public IEnumerable> GetAddedGameRules(EntProtoId stationEvent) where T : Component + { + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var comp, out var ruleData, out var meta)) + { + if (IsGameRuleAdded((uid, ruleData)) && meta.EntityPrototype is { } id && id == stationEvent) + yield return (uid, comp); + } + } + + /// + /// Gets all the gamerule entities which are currently active. + /// + [PublicAPI] + public IEnumerable GetActiveGameRules() + { + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out _, out _)) + { + yield return uid; + } + } + + /// + /// Gets all the game rule entities which are currently active and match the given prototype. + /// + [PublicAPI] + public IEnumerable GetActiveGameRules(EntProtoId stationEvent) + { + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out _, out _, out var meta)) + { + if (meta.EntityPrototype is { } id && id == stationEvent) + yield return uid; + } + } + + /// + /// Gets all the gamerule entities with {T} component that are currently active. + /// + [PublicAPI] + public IEnumerable> GetActiveGameRules() where T : Component + { + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var comp, out _, out _)) + { + yield return (uid, comp); + } + } + + /// + /// Gets all the game rule entities with {T} component that are currently active and have the given prototype. + /// + [PublicAPI] + public IEnumerable> GetActiveGameRules(EntProtoId stationEvent) where T : Component + { + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var comp, out _, out _, out var meta)) + { + if (meta.EntityPrototype is { } id && id == stationEvent) + yield return (uid, comp); + } + } + + /// + /// Gets all gamerule prototypes + /// + [PublicAPI] + public IEnumerable GetAllGameRulePrototypes() + { + foreach (var proto in ProtoMan.EnumeratePrototypes()) + { + if (proto.Abstract) + continue; + + if (HasComp(proto)) + yield return proto; + } + } + + + [PublicAPI] + public int GetOccurrences(EntityPrototype stationEvent) + { + return GetOccurrences(stationEvent.ID); + } + + /// + /// Gets the total number of game rule entities which match the inputted prototype! + /// + /// Event prototype we are looking for. + /// Number of already existing events which match this prototype. + [PublicAPI] + public int GetOccurrences(EntProtoId stationEvent) + { + var count = 0; + var ruleQuery = EntityQueryEnumerator(); + while (ruleQuery.MoveNext(out _, out var meta)) + { + if (meta.EntityPrototype is { } id && id == stationEvent) + count++; + } + + return count; + } + + /// + /// Gets the last added game rule or null if there are no game rules! + /// + [PublicAPI] + public GameRule? GetLastGameRule() + { + if (AllRoundGameRules.Count == 0) + return null; + + return AllRoundGameRules[^1]; + } + + /// + /// Gets the last added game rule or null if there are no game rules! + /// + [PublicAPI] + public TimeSpan GetLastRuleTime() + { + return GetLastGameRule() is not { } rule ? TimeSpan.Zero : rule.StartTime; + } + + /// + /// Gets the last added game rule or null if there are no game rules! + /// + [PublicAPI] + public GameRule? GetLastGameRule(EntProtoId proto) + { + for (var i = AllRoundGameRules.Count - 1; i >= 0; i--) + { + var rule = AllRoundGameRules[i]; + if (Deleted(rule.Uid) || MetaData(rule.Uid).EntityPrototype is not { } id || id != proto) + continue; + + return rule; + } + + return null; + } + + /// + /// Gets the last added game rule or null if there are no game rules! + /// + [PublicAPI] + public TimeSpan GetLastRuleTime(EntProtoId proto) + { + return GetLastGameRule(proto) is not { } rule ? TimeSpan.Zero : rule.StartTime; + } + + /// + /// Returns a readable string for this Game Rule. + /// + /// GameRule struct we're searching for. + /// A readable string representing this gamerule and its status. + [PublicAPI] + public string RuleToString(GameRule rule) + { + return RuleToString(rule.Uid, rule.LifeStage); + } + + /// + [PublicAPI] + public string RuleToString(EntityUid rule, GameRuleLifeStage stage) + { + if (!TryComp(rule, out MetaDataComponent? meta)) + return "Deleted Rule"; + + var str = $"{meta.EntityPrototype?.ID ?? "Unknown Rule"} ({rule.Id})"; + switch (stage) + { + case GameRuleLifeStage.Added: + str += " - Pending"; + break; + case GameRuleLifeStage.Ended: + str += " - Ended"; + break; + } + + return str; + } +} diff --git a/Content.Shared/GameTicking/GameTicker.Lobby.cs b/Content.Shared/GameTicking/GameTicker.Lobby.cs new file mode 100644 index 0000000000..af4f6e14ba --- /dev/null +++ b/Content.Shared/GameTicking/GameTicker.Lobby.cs @@ -0,0 +1,16 @@ +using Robust.Shared.Network; +using Robust.Shared.Player; + +namespace Content.Shared.GameTicking; + +public abstract partial class GameTicker +{ + public bool UserHasJoinedGame(ICommonSession session) => UserHasJoinedGame(session.UserId); + + /// + /// Returns if a given session has currently joined the game. + /// + /// User + /// Returns true if the player has joined the game. Only can return true on server! + public virtual bool UserHasJoinedGame(NetUserId userId) => false; +} diff --git a/Content.Shared/GameTicking/GameTicker.Player.cs b/Content.Shared/GameTicking/GameTicker.Player.cs new file mode 100644 index 0000000000..ef00e7092c --- /dev/null +++ b/Content.Shared/GameTicking/GameTicker.Player.cs @@ -0,0 +1,8 @@ +using Robust.Shared.Player; + +namespace Content.Shared.GameTicking; + +public abstract partial class GameTicker +{ + public abstract void PlayerJoinGame(ICommonSession session, bool silent = false); +} diff --git a/Content.Shared/GameTicking/GameTicker.RoundFlow.cs b/Content.Shared/GameTicking/GameTicker.RoundFlow.cs new file mode 100644 index 0000000000..9796196597 --- /dev/null +++ b/Content.Shared/GameTicking/GameTicker.RoundFlow.cs @@ -0,0 +1,169 @@ +using System.Numerics; +using Content.Shared.Maps; +using Content.Shared.Preferences; +using Robust.Shared.EntitySerialization; +using Robust.Shared.EntitySerialization.Systems; +using Robust.Shared.Map; +using Robust.Shared.Network; +using Robust.Shared.Player; + +namespace Content.Shared.GameTicking; + +public abstract partial class GameTicker +{ + /// + /// Game's current run level. + /// + [ViewVariables] + public GameRunLevel RunLevel + { + get; + protected set + { + var old = field; + field = value; + + var gameRunLevelChangedEvent = new GameRunLevelChangedEvent(old, value); + RaiseLocalEvent(gameRunLevelChangedEvent); + } + } + + /// + /// Ends the current round, with custom end of round text. + /// + /// Text displayed at the end of round scoreboard. + public virtual void EndRound(string text = "") { } + + /// + /// Restarts the current round, sending the game to the lobby or starting a new round if lobby is disabled. + /// + public virtual void RestartRound() { } + + /// + /// Loads a new map, allowing systems interested in it to handle loading events. + /// In the base game, this is required to be used if you want to load a station. + /// This does not initialze maps, unles specified via the . + /// + /// + /// This is basically a wrapper around a method that auto generate + /// some using information in a prototype, and raise some events to allow content + /// to modify the options and react to the map creation. + /// + /// Game map prototype to load in. + /// The id of the map that was loaded. + /// Entity loading options, including whether the maps should be initialized. + /// Name to assign to the loaded station. + /// Coordinate offset for spawning grids within this map + /// Rotation offset for spawning grids within this map + /// All loaded entities and grids. + public abstract IReadOnlyList LoadGameMap( + GameMapPrototype proto, + out MapId mapId, + DeserializationOptions? options = null, + string? stationName = null, + Vector2? offset = null, + Angle? rot = null); + + /// + /// Gets the current round duration, returning zero if the round has not started. + /// + public TimeSpan GetRoundTime() + { + return RunLevel == GameRunLevel.PreRoundLobby ? TimeSpan.Zero : RoundDuration(); + } + + /// + /// Gets the current number of readied up players. + /// + public abstract int ReadyPlayerCount(); +} + +public enum GameRunLevel +{ + PreRoundLobby = 0, + InRound = 1, + PostRound = 2 +} + +public sealed class GameRunLevelChangedEvent +{ + public GameRunLevel Old { get; } + public GameRunLevel New { get; } + + public GameRunLevelChangedEvent(GameRunLevel old, GameRunLevel @new) + { + Old = old; + New = @new; + } +} + +/// +/// Event raised to allow subscribers to add text to the round end summary screen. +/// +[ByRefEvent] +public record struct RoundEndTextAppendEvent +{ + private bool _doNewLine; + + public RoundEndTextAppendEvent() { } + + /// + /// Text to display in the round end summary screen. + /// + public string Text { get; private set; } = string.Empty; + + /// + /// Invoke this method to add text to the round end summary screen. + /// + /// + public void AddLine(string text) + { + if (_doNewLine) + Text += "\n"; + + Text += text; + _doNewLine = true; + } +} + +/// +/// Event raised before readied up players are spawned and given jobs by the GameTicker. +/// You can use this to spawn people off-station, like in the case of nuke ops or wizard. +/// Remove the players you spawned from the PlayerPool and call on them. +/// +public sealed class RulePlayerSpawningEvent +{ + /// + /// Pool of players to be spawned. + /// If you want to handle a specific player being spawned, remove it from this list and do what you need. + /// + /// If you spawn a player by yourself from this event, don't forget to call on them. + public List PlayerPool { get; } + public IReadOnlyDictionary Profiles { get; } + public bool Forced { get; } + + public RulePlayerSpawningEvent(List playerPool, IReadOnlyDictionary profiles, bool forced) + { + PlayerPool = playerPool; + Profiles = profiles; + Forced = forced; + } +} + +/// +/// Event raised after players were assigned jobs by the GameTicker and have been spawned in. +/// You can give on-station people special roles by listening to this event. +/// +public sealed class RulePlayerJobsAssignedEvent +{ + public ICommonSession[] Players { get; } + public IReadOnlyDictionary Profiles { get; } + public bool Forced { get; } + + public RulePlayerJobsAssignedEvent(ICommonSession[] players, IReadOnlyDictionary profiles, bool forced) + { + Players = players; + Profiles = profiles; + Forced = forced; + } +} diff --git a/Content.Shared/GameTicking/GameTicker.Spawning.cs b/Content.Shared/GameTicking/GameTicker.Spawning.cs new file mode 100644 index 0000000000..258fbacb60 --- /dev/null +++ b/Content.Shared/GameTicking/GameTicker.Spawning.cs @@ -0,0 +1,15 @@ +using Robust.Shared.Player; + +namespace Content.Shared.GameTicking; + +public abstract partial class GameTicker +{ + /// + /// Makes a player join into the game and spawn on a station. + /// + /// The player joining + /// The station they're spawning on + /// An optional job for them to spawn as + /// Whether or not the player should be greeted upon joining + public virtual void MakeJoinGame(ICommonSession player, EntityUid station, string? jobId = null, bool silent = false) { } +} diff --git a/Content.Shared/GameTicking/GameTicker.cs b/Content.Shared/GameTicking/GameTicker.cs new file mode 100644 index 0000000000..36675a5437 --- /dev/null +++ b/Content.Shared/GameTicking/GameTicker.cs @@ -0,0 +1,258 @@ +using Content.Shared.Administration.Logs; +using Content.Shared.Chat; +using Content.Shared.GameTicking.Components; +using Content.Shared.GameTicking.Prototypes; +using Content.Shared.Mind; +using Content.Shared.Roles; +using Content.Shared.Station.Systems; +using Content.Shared.Whitelist; +using Robust.Shared.Audio; +using Robust.Shared.Audio.Systems; +using Robust.Shared.Configuration; +using Robust.Shared.Network; +using Robust.Shared.Prototypes; +using Robust.Shared.Random; +using Robust.Shared.Replays; +using Robust.Shared.Serialization; +using Robust.Shared.Serialization.Markdown.Mapping; +using Robust.Shared.Serialization.Markdown.Value; +using Robust.Shared.Timing; + +namespace Content.Shared.GameTicking; + +public abstract partial class GameTicker : EntitySystem +{ + // Behold! My massive wall of dependencies!!! + [Dependency] protected IConfigurationManager Cfg = default!; + [Dependency] protected IGameTiming Timing = default!; + [Dependency] private INetManager _net = default!; + [Dependency] protected ISharedAdminLogManager Admin = default!; + [Dependency] protected IReplayRecordingManager Replay = default!; + [Dependency] protected IRobustRandom Random = default!; + [Dependency] protected EntityWhitelistSystem Whitelist = default!; + [Dependency] protected MetaDataSystem Meta = default!; + [Dependency] protected SharedAudioSystem Audio = default!; + [Dependency] protected SharedChatSystem Chat = default!; + [Dependency] protected SharedMapSystem Map = default!; + [Dependency] protected SharedMindSystem Mind = default!; + [Dependency] protected SharedRoleSystem Role = default!; + [Dependency] protected StationSystem Station = default!; + [Dependency] protected SharedTransformSystem XForm = default!; + + // A second wall of dependencies has hit the GameTicker... + [Dependency] protected EntityQuery RuleQuery; + [Dependency] protected EntityQuery ActiveRuleQuery; + + // See ideally these would be pulled from the job definition or something. + // But this is easier, and at least it isn't hardcoded. + //TODO: Move these, they really belong in StationJobsSystem or a cvar. + public static readonly ProtoId FallbackOverflowJob = "Passenger"; + + public const string FallbackOverflowJobName = "job-name-passenger"; + + // TODO network. + // Probably most useful for replays, round end info, and probably things like lobby menus. + [ViewVariables] + public int RoundId { get; protected set; } + [ViewVariables] public TimeSpan RoundStartTimeSpan { get; protected set; } + + public override void Initialize() + { + base.Initialize(); + Replay.RecordingStarted += OnRecordingStart; + } + + public override void Shutdown() + { + base.Shutdown(); + Replay.RecordingStarted -= OnRecordingStart; + } + + private void OnRecordingStart(MappingDataNode metadata, List events) + { + if (RoundId != 0) + { + metadata["roundId"] = new ValueDataNode(RoundId.ToString()); + } + } + + public TimeSpan RoundDuration() + { + return Timing.CurTime.Subtract(RoundStartTimeSpan); + } +} + +[Serializable, NetSerializable] +public sealed class TickerJoinLobbyEvent : EntityEventArgs +{ +} + +[Serializable, NetSerializable] +public sealed class TickerJoinGameEvent : EntityEventArgs +{ +} + +[Serializable, NetSerializable] +public sealed class TickerLateJoinStatusEvent : EntityEventArgs +{ + // TODO: Make this a replicated CVar, honestly. + public bool Disallowed { get; } + + public TickerLateJoinStatusEvent(bool disallowed) + { + Disallowed = disallowed; + } +} + +[Serializable, NetSerializable] +public sealed class TickerConnectionStatusEvent : EntityEventArgs +{ + public TimeSpan RoundStartTimeSpan { get; } + public TickerConnectionStatusEvent(TimeSpan roundStartTimeSpan) + { + RoundStartTimeSpan = roundStartTimeSpan; + } +} + +[Serializable, NetSerializable] +public sealed class TickerLobbyStatusEvent : EntityEventArgs +{ + public bool IsRoundStarted { get; } + public ProtoId? LobbyBackground { get; } + public bool YouAreReady { get; } + // UTC. + public TimeSpan StartTime { get; } + public TimeSpan RoundStartTimeSpan { get; } + public bool Paused { get; } + + public TickerLobbyStatusEvent(bool isRoundStarted, ProtoId? lobbyBackground, bool youAreReady, TimeSpan startTime, TimeSpan preloadTime, TimeSpan roundStartTimeSpan, bool paused) + { + IsRoundStarted = isRoundStarted; + LobbyBackground = lobbyBackground; + YouAreReady = youAreReady; + StartTime = startTime; + RoundStartTimeSpan = roundStartTimeSpan; + Paused = paused; + } +} + +[Serializable, NetSerializable] +public sealed class TickerLobbyInfoEvent : EntityEventArgs +{ + public string TextBlob { get; } + + public TickerLobbyInfoEvent(string textBlob) + { + TextBlob = textBlob; + } +} + +[Serializable, NetSerializable] +public sealed class TickerLobbyCountdownEvent : EntityEventArgs +{ + /// + /// The game time that the game will start at. + /// + public TimeSpan StartTime { get; } + + /// + /// Whether or not the countdown is paused + /// + public bool Paused { get; } + + public TickerLobbyCountdownEvent(TimeSpan startTime, bool paused) + { + StartTime = startTime; + Paused = paused; + } +} + +[Serializable, NetSerializable] +public sealed class TickerJobsAvailableEvent( + Dictionary stationNames, + Dictionary, int?>> jobsAvailableByStation, + Dictionary?> jobWeightsByStation) + : EntityEventArgs +{ + /// + /// The Status of the Player in the lobby (ready, observer, ...) + /// + public Dictionary, int?>> JobsAvailableByStation { get; } = jobsAvailableByStation; + + public Dictionary StationNames { get; } = stationNames; + + public Dictionary?> JobWeightsByStation { get; } = jobWeightsByStation; +} + +[Serializable, NetSerializable, DataDefinition] +public sealed partial class RoundEndMessageEvent : EntityEventArgs +{ + [Serializable, NetSerializable, DataDefinition] + public partial struct RoundEndPlayerInfo + { + [DataField] + public string PlayerOOCName; + + [DataField] + public string? PlayerICName; + + [DataField, NonSerialized] + public NetUserId? PlayerGuid; + + public string Role; + + [DataField, NonSerialized] + public string[] JobPrototypes; + + [DataField, NonSerialized] + public string[] AntagPrototypes; + + public NetEntity? PlayerNetEntity; + + [DataField] + public bool Antag; + + [DataField] + public bool Observer; + + public bool Connected; + } + + public string GamemodeTitle { get; } + public string RoundEndText { get; } + public TimeSpan RoundDuration { get; } + public int RoundId { get; } + public int PlayerCount { get; } + public RoundEndPlayerInfo[] AllPlayersEndInfo { get; } + + /// + /// Sound gets networked due to how entity lifecycle works between client / server and to avoid clipping. + /// + public ResolvedSoundSpecifier? RestartSound; + + public RoundEndMessageEvent( + string gamemodeTitle, + string roundEndText, + TimeSpan roundDuration, + int roundId, + int playerCount, + RoundEndPlayerInfo[] allPlayersEndInfo, + ResolvedSoundSpecifier? restartSound) + { + GamemodeTitle = gamemodeTitle; + RoundEndText = roundEndText; + RoundDuration = roundDuration; + RoundId = roundId; + PlayerCount = playerCount; + AllPlayersEndInfo = allPlayersEndInfo; + RestartSound = restartSound; + } +} + +[Serializable, NetSerializable] +public enum PlayerGameStatus : sbyte +{ + NotReadyToPlay = 0, + ReadyToPlay, + JoinedGame, +} diff --git a/Content.Shared/GameTicking/Prototypes/GamePresetPrototype.cs b/Content.Shared/GameTicking/Prototypes/GamePresetPrototype.cs new file mode 100644 index 0000000000..307abf2970 --- /dev/null +++ b/Content.Shared/GameTicking/Prototypes/GamePresetPrototype.cs @@ -0,0 +1,42 @@ +using Content.Shared.Maps; +using Robust.Shared.Prototypes; + +namespace Content.Shared.GameTicking.Prototypes; + +/// +/// A round-start setup preset, such as which antagonists to spawn. +/// +[Prototype] +public sealed partial class GamePresetPrototype : IPrototype +{ + [IdDataField] + public string ID { get; private set; } = default!; + + [DataField] + public string[] Alias = Array.Empty(); + + [DataField("name")] + public string ModeTitle = "????"; + + [DataField] + public string Description = string.Empty; + + [DataField] + public bool ShowInVote; + + [DataField] + public int? MinPlayers; + + [DataField] + public int? MaxPlayers; + + [DataField] + public IReadOnlyList Rules { get; private set; } = Array.Empty(); + + /// + /// If specified, the gamemode will only be run with these maps. + /// If none are elligible, the global fallback will be used. + /// + [DataField("supportedMaps")] + public ProtoId? MapPool; +} diff --git a/Content.Server/GameTicking/Rules/Components/ChangelingRuleComponent.cs b/Content.Shared/GameTicking/Rules/Components/ChangelingRuleComponent.cs similarity index 75% rename from Content.Server/GameTicking/Rules/Components/ChangelingRuleComponent.cs rename to Content.Shared/GameTicking/Rules/Components/ChangelingRuleComponent.cs index 13891c1988..dc60f0b534 100644 --- a/Content.Server/GameTicking/Rules/Components/ChangelingRuleComponent.cs +++ b/Content.Shared/GameTicking/Rules/Components/ChangelingRuleComponent.cs @@ -1,4 +1,4 @@ -namespace Content.Server.GameTicking.Rules.Components; +namespace Content.Shared.GameTicking.Rules.Components; /// /// Gamerule component for handling a changeling antagonist. diff --git a/Content.Server/GameTicking/Rules/Components/DragonRuleComponent.cs b/Content.Shared/GameTicking/Rules/Components/DragonRuleComponent.cs similarity index 59% rename from Content.Server/GameTicking/Rules/Components/DragonRuleComponent.cs rename to Content.Shared/GameTicking/Rules/Components/DragonRuleComponent.cs index e3fab85f2d..a20107d401 100644 --- a/Content.Server/GameTicking/Rules/Components/DragonRuleComponent.cs +++ b/Content.Shared/GameTicking/Rules/Components/DragonRuleComponent.cs @@ -1,4 +1,4 @@ -namespace Content.Server.GameTicking.Rules.Components; +namespace Content.Shared.GameTicking.Rules.Components; [RegisterComponent] public sealed partial class DragonRuleComponent : Component; diff --git a/Content.Shared/GameTicking/Rules/DynamicRuleComponent.cs b/Content.Shared/GameTicking/Rules/Components/DynamicRuleComponent.cs similarity index 97% rename from Content.Shared/GameTicking/Rules/DynamicRuleComponent.cs rename to Content.Shared/GameTicking/Rules/Components/DynamicRuleComponent.cs index 7782717758..b1d21e6082 100644 --- a/Content.Shared/GameTicking/Rules/DynamicRuleComponent.cs +++ b/Content.Shared/GameTicking/Rules/Components/DynamicRuleComponent.cs @@ -1,7 +1,7 @@ using Content.Shared.EntityTable.EntitySelectors; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; -namespace Content.Shared.GameTicking.Rules; +namespace Content.Shared.GameTicking.Rules.Components; /// /// Gamerule the spawns multiple antags at intervals based on a budget diff --git a/Content.Shared/GameTicking/Rules/DynamicRuleCostComponent.cs b/Content.Shared/GameTicking/Rules/Components/DynamicRuleCostComponent.cs similarity index 85% rename from Content.Shared/GameTicking/Rules/DynamicRuleCostComponent.cs rename to Content.Shared/GameTicking/Rules/Components/DynamicRuleCostComponent.cs index 180b168dc1..458334d862 100644 --- a/Content.Shared/GameTicking/Rules/DynamicRuleCostComponent.cs +++ b/Content.Shared/GameTicking/Rules/Components/DynamicRuleCostComponent.cs @@ -1,4 +1,4 @@ -namespace Content.Shared.GameTicking.Rules; +namespace Content.Shared.GameTicking.Rules.Components; /// /// Component that tracks how much a rule "costs" for Dynamic diff --git a/Content.Server/GameTicking/Rules/Components/NukeopsRuleComponent.cs b/Content.Shared/GameTicking/Rules/Components/NukeopsRuleComponent.cs similarity index 96% rename from Content.Server/GameTicking/Rules/Components/NukeopsRuleComponent.cs rename to Content.Shared/GameTicking/Rules/Components/NukeopsRuleComponent.cs index dc118e9bb7..037a17584d 100644 --- a/Content.Server/GameTicking/Rules/Components/NukeopsRuleComponent.cs +++ b/Content.Shared/GameTicking/Rules/Components/NukeopsRuleComponent.cs @@ -1,12 +1,10 @@ -using Content.Server.RoundEnd; -using Content.Shared.Dataset; using Content.Shared.NPC.Prototypes; -using Content.Shared.Roles; +using Content.Shared.RoundEnd; using Robust.Shared.Audio; using Robust.Shared.Prototypes; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; -namespace Content.Server.GameTicking.Rules.Components; +namespace Content.Shared.GameTicking.Rules.Components; [RegisterComponent, Access(typeof(NukeopsRuleSystem))] public sealed partial class NukeopsRuleComponent : Component diff --git a/Content.Server/GameTicking/Rules/Components/ParadoxCloneRuleComponent.cs b/Content.Shared/GameTicking/Rules/Components/ParadoxCloneRuleComponent.cs similarity index 96% rename from Content.Server/GameTicking/Rules/Components/ParadoxCloneRuleComponent.cs rename to Content.Shared/GameTicking/Rules/Components/ParadoxCloneRuleComponent.cs index f1e8e41258..6856eb4cd4 100644 --- a/Content.Server/GameTicking/Rules/Components/ParadoxCloneRuleComponent.cs +++ b/Content.Shared/GameTicking/Rules/Components/ParadoxCloneRuleComponent.cs @@ -2,7 +2,7 @@ using Content.Shared.Cloning; using Content.Shared.Whitelist; using Robust.Shared.Prototypes; -namespace Content.Server.GameTicking.Rules.Components; +namespace Content.Shared.GameTicking.Rules.Components; /// /// Gamerule component for spawning a paradox clone antagonist. diff --git a/Content.Server/GameTicking/Rules/Components/RevolutionaryRuleComponent.cs b/Content.Shared/GameTicking/Rules/Components/RevolutionaryRuleComponent.cs similarity index 90% rename from Content.Server/GameTicking/Rules/Components/RevolutionaryRuleComponent.cs rename to Content.Shared/GameTicking/Rules/Components/RevolutionaryRuleComponent.cs index 3b19bbffb6..69b62183d1 100644 --- a/Content.Server/GameTicking/Rules/Components/RevolutionaryRuleComponent.cs +++ b/Content.Shared/GameTicking/Rules/Components/RevolutionaryRuleComponent.cs @@ -1,8 +1,6 @@ -using Content.Shared.Roles; -using Robust.Shared.Prototypes; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; -namespace Content.Server.GameTicking.Rules.Components; +namespace Content.Shared.GameTicking.Rules.Components; /// /// Component for the RevolutionaryRuleSystem that stores info about winning/losing, player counts required for starting, as well as prototypes for Revolutionaries and their gear. diff --git a/Content.Server/GameTicking/Rules/Components/RoundstartStationVariationRuleComponent.cs b/Content.Shared/GameTicking/Rules/Components/RoundstartStationVariationRuleComponent.cs similarity index 87% rename from Content.Server/GameTicking/Rules/Components/RoundstartStationVariationRuleComponent.cs rename to Content.Shared/GameTicking/Rules/Components/RoundstartStationVariationRuleComponent.cs index 44ae89f7b3..77e4c5e82b 100644 --- a/Content.Server/GameTicking/Rules/Components/RoundstartStationVariationRuleComponent.cs +++ b/Content.Shared/GameTicking/Rules/Components/RoundstartStationVariationRuleComponent.cs @@ -1,7 +1,6 @@ using Content.Shared.Storage; -using Robust.Shared.Prototypes; -namespace Content.Server.GameTicking.Rules.Components; +namespace Content.Shared.GameTicking.Rules.Components; /// /// This handles starting various roundstart variation rules after a station has been loaded. diff --git a/Content.Server/GameTicking/Rules/Components/StationVariationPassRuleComponent.cs b/Content.Shared/GameTicking/Rules/Components/StationVariationPassRuleComponent.cs similarity index 75% rename from Content.Server/GameTicking/Rules/Components/StationVariationPassRuleComponent.cs rename to Content.Shared/GameTicking/Rules/Components/StationVariationPassRuleComponent.cs index 9fdc62a7e0..abfbb65116 100644 --- a/Content.Server/GameTicking/Rules/Components/StationVariationPassRuleComponent.cs +++ b/Content.Shared/GameTicking/Rules/Components/StationVariationPassRuleComponent.cs @@ -1,9 +1,7 @@ -namespace Content.Server.GameTicking.Rules.Components; +namespace Content.Shared.GameTicking.Rules.Components; /// /// This is a marker component placed on rule entities which are a single "pass" of station variation. /// [RegisterComponent] -public sealed partial class StationVariationPassRuleComponent : Component -{ -} +public sealed partial class StationVariationPassRuleComponent : Component; diff --git a/Content.Server/GameTicking/Rules/Components/SurvivorRuleComponent.cs b/Content.Shared/GameTicking/Rules/Components/SurvivorRuleComponent.cs similarity index 85% rename from Content.Server/GameTicking/Rules/Components/SurvivorRuleComponent.cs rename to Content.Shared/GameTicking/Rules/Components/SurvivorRuleComponent.cs index 5e6cc203d2..08d1ccedc2 100644 --- a/Content.Server/GameTicking/Rules/Components/SurvivorRuleComponent.cs +++ b/Content.Shared/GameTicking/Rules/Components/SurvivorRuleComponent.cs @@ -1,4 +1,4 @@ -namespace Content.Server.GameTicking.Rules.Components; +namespace Content.Shared.GameTicking.Rules.Components; /// /// Component for the SurvivorRuleSystem. Game rule that turns everyone into a survivor and gives them the objective to escape centcom alive. diff --git a/Content.Server/GameTicking/Rules/Components/ThiefRuleComponent.cs b/Content.Shared/GameTicking/Rules/Components/ThiefRuleComponent.cs similarity index 57% rename from Content.Server/GameTicking/Rules/Components/ThiefRuleComponent.cs rename to Content.Shared/GameTicking/Rules/Components/ThiefRuleComponent.cs index 6ad1e17775..c46edbc00e 100644 --- a/Content.Server/GameTicking/Rules/Components/ThiefRuleComponent.cs +++ b/Content.Shared/GameTicking/Rules/Components/ThiefRuleComponent.cs @@ -1,8 +1,4 @@ -using Content.Shared.Random; -using Robust.Shared.Audio; -using Robust.Shared.Prototypes; - -namespace Content.Server.GameTicking.Rules.Components; +namespace Content.Shared.GameTicking.Rules.Components; /// /// Stores data for . diff --git a/Content.Server/GameTicking/Rules/Components/TraitorRuleComponent.cs b/Content.Shared/GameTicking/Rules/Components/TraitorRuleComponent.cs similarity index 95% rename from Content.Server/GameTicking/Rules/Components/TraitorRuleComponent.cs rename to Content.Shared/GameTicking/Rules/Components/TraitorRuleComponent.cs index 092f4b71c2..2c84307c7c 100644 --- a/Content.Server/GameTicking/Rules/Components/TraitorRuleComponent.cs +++ b/Content.Shared/GameTicking/Rules/Components/TraitorRuleComponent.cs @@ -1,14 +1,13 @@ -using Content.Server.Codewords; +using Content.Shared.Codewords; using Content.Shared.Dataset; using Content.Shared.FixedPoint; using Content.Shared.NPC.Prototypes; -using Content.Shared.Random; using Content.Shared.Roles; using Robust.Shared.Audio; using Robust.Shared.Prototypes; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; -namespace Content.Server.GameTicking.Rules.Components; +namespace Content.Shared.GameTicking.Rules.Components; [RegisterComponent, Access(typeof(TraitorRuleSystem))] public sealed partial class TraitorRuleComponent : Component diff --git a/Content.Server/GameTicking/Rules/Components/XenoborgsRuleComponent.cs b/Content.Shared/GameTicking/Rules/Components/XenoborgsRuleComponent.cs similarity index 96% rename from Content.Server/GameTicking/Rules/Components/XenoborgsRuleComponent.cs rename to Content.Shared/GameTicking/Rules/Components/XenoborgsRuleComponent.cs index 4a018d7e6c..b343e53f1e 100644 --- a/Content.Server/GameTicking/Rules/Components/XenoborgsRuleComponent.cs +++ b/Content.Shared/GameTicking/Rules/Components/XenoborgsRuleComponent.cs @@ -1,6 +1,6 @@ using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; -namespace Content.Server.GameTicking.Rules.Components; +namespace Content.Shared.GameTicking.Rules.Components; [RegisterComponent, Access(typeof(XenoborgsRuleSystem))] [AutoGenerateComponentPause] diff --git a/Content.Server/GameTicking/Rules/Components/ZombieRuleComponent.cs b/Content.Shared/GameTicking/Rules/Components/ZombieRuleComponent.cs similarity index 86% rename from Content.Server/GameTicking/Rules/Components/ZombieRuleComponent.cs rename to Content.Shared/GameTicking/Rules/Components/ZombieRuleComponent.cs index f8612a2421..eceebdfa1d 100644 --- a/Content.Server/GameTicking/Rules/Components/ZombieRuleComponent.cs +++ b/Content.Shared/GameTicking/Rules/Components/ZombieRuleComponent.cs @@ -1,10 +1,7 @@ -using Content.Shared.Roles; -using Robust.Shared.Audio; -using Robust.Shared.Prototypes; +using Content.Shared.RoundEnd; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; -using Content.Server.RoundEnd; -namespace Content.Server.GameTicking.Rules.Components; +namespace Content.Shared.GameTicking.Rules.Components; [RegisterComponent, Access(typeof(ZombieRuleSystem))] public sealed partial class ZombieRuleComponent : Component diff --git a/Content.Server/GameTicking/Rules/DragonRuleSystem.cs b/Content.Shared/GameTicking/Rules/DragonRuleSystem.cs similarity index 71% rename from Content.Server/GameTicking/Rules/DragonRuleSystem.cs rename to Content.Shared/GameTicking/Rules/DragonRuleSystem.cs index 1992fc2793..132a8fc418 100644 --- a/Content.Server/GameTicking/Rules/DragonRuleSystem.cs +++ b/Content.Shared/GameTicking/Rules/DragonRuleSystem.cs @@ -1,30 +1,22 @@ -using Content.Server.Antag; -using Content.Server.GameTicking.Rules.Components; -using Content.Server.Mind; -using Content.Server.Roles; -using Content.Server.Station.Systems; +using Content.Shared.Antag; +using Content.Shared.GameTicking.Rules.Components; using Content.Shared.Localizations; +using Content.Shared.Mind; +using Content.Shared.Roles; using Content.Shared.Roles.Components; -using Robust.Server.GameObjects; +using Content.Shared.Station.Systems; -namespace Content.Server.GameTicking.Rules; +namespace Content.Shared.GameTicking.Rules; public sealed partial class DragonRuleSystem : GameRuleSystem { - [Dependency] private TransformSystem _transform = default!; [Dependency] private AntagSelectionSystem _antag = default!; [Dependency] private StationSystem _station = default!; - [Dependency] private RoleSystem _roleSystem = default!; - [Dependency] private MindSystem _mind = default!; - - public override void Initialize() - { - base.Initialize(); - - SubscribeLocalEvent(AfterAntagEntitySelected); - SubscribeLocalEvent(UpdateBriefing); - } + [Dependency] private SharedMindSystem _mind = default!; + [Dependency] private SharedRoleSystem _roleSystem = default!; + [Dependency] private SharedTransformSystem _transform = default!; + [SubscribeLocalEvent] private void UpdateBriefing(Entity entity, ref GetBriefingEvent args) { var ent = args.Mind.Comp.OwnedEntity; @@ -35,9 +27,10 @@ public sealed partial class DragonRuleSystem : GameRuleSystem ent, ref AfterAntagEntitySelectedEvent args) { - if (!_mind.TryGetMind(args.EntityUid, out var mindId, out var mind)) + if (!_mind.TryGetMind(args.EntityUid, out var mindId, out _)) return; _roleSystem.MindHasRole(mindId, out var dragonRole); diff --git a/Content.Shared/GameTicking/Rules/GameRuleSystem.Utility.cs b/Content.Shared/GameTicking/Rules/GameRuleSystem.Utility.cs new file mode 100644 index 0000000000..e1efea9964 --- /dev/null +++ b/Content.Shared/GameTicking/Rules/GameRuleSystem.Utility.cs @@ -0,0 +1,37 @@ +using Content.Shared.GameTicking.Components; + +namespace Content.Shared.GameTicking.Rules; + +// TODO: Someone should probably make this virtual and make it into a "GameRuleSystem" with all GameRules in it. +// TODO: Then move GameTicker.GameRule into here and have GameRuleSystem inherit, since it already uses it as a dependency... +public abstract partial class GameRuleSystem where T: IComponent +{ + protected EntityQueryEnumerator QueryActiveRules() + { + return EntityQueryEnumerator(); + } + + protected EntityQueryEnumerator QueryDelayedRules() + { + return EntityQueryEnumerator(); + } + + /// + /// Queries all gamerules, regardless of if they're active or not. + /// + protected EntityQueryEnumerator QueryAllRules() + { + return EntityQueryEnumerator(); + } + + protected void ForceEndSelf(Entity entity) + { + GameTicker.EndGameRule(entity); + } + + [Obsolete("Use Entity typed version of this method instead!")] + protected void ForceEndSelf(EntityUid uid, GameRuleComponent? component = null) + { + ForceEndSelf((uid, component)); + } +} diff --git a/Content.Shared/GameTicking/Rules/GameRuleSystem.cs b/Content.Shared/GameTicking/Rules/GameRuleSystem.cs new file mode 100644 index 0000000000..1fb44b309c --- /dev/null +++ b/Content.Shared/GameTicking/Rules/GameRuleSystem.cs @@ -0,0 +1,117 @@ +using Content.Shared.GameTicking.Components; +using Robust.Shared.Random; +using Robust.Shared.Timing; + +namespace Content.Shared.GameTicking.Rules; + +public abstract partial class GameRuleSystem : EntitySystem where T : IComponent +{ + [Dependency] protected IGameTiming Timing = default!; + [Dependency] protected IRobustRandom RobustRandom = default!; + [Dependency] protected GameTicker GameTicker = default!; + [Dependency] protected EntityQuery RuleQuery = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnGameRuleAdded); + SubscribeLocalEvent(OnGameRuleStarted); + SubscribeLocalEvent(OnGameRuleEnded); + } + + private void OnGameRuleAdded(Entity entity, ref GameRuleAddedEvent args) + { + Added((entity, entity, args.Rule), ref args); + } + + private void OnGameRuleStarted(Entity entity, ref GameRuleStartedEvent args) + { + Started((entity, entity, args.Rule), ref args); + } + + private void OnGameRuleEnded(Entity entity, ref GameRuleEndedEvent args) + { + Ended((entity, entity), ref args); + } + + [SubscribeLocalEvent] + private void OnRoundEndTextAppend(ref RoundEndTextAppendEvent ev) + { + // We don't query GameRuleComponent since the rule may have ended! + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var comp)) + { + AppendRoundEndText((uid, comp), ref ev); + } + } + + /// + /// Called when the gamerule is added + /// + [Obsolete("Use Entity version instead")] + protected virtual void Added(EntityUid uid, T component, GameRuleComponent gameRule, GameRuleAddedEvent args) + { + + } + + /// + /// Called when the gamerule is added + /// + protected virtual void Added(Entity rule, ref GameRuleAddedEvent args) + { + Added(rule.Owner, rule.Comp1, rule.Comp2, args); + } + + /// + /// Called when the gamerule begins + /// + [Obsolete("Use Entity version instead")] + protected virtual void Started(EntityUid uid, T component, GameRuleComponent gameRule, GameRuleStartedEvent args) + { + + } + + /// + /// Called when the gamerule is added + /// + protected virtual void Started(Entity rule, ref GameRuleStartedEvent args) + { + Started(rule.Owner, rule.Comp1, rule.Comp2, args); + } + + /// + /// Called when the gamerule is added + /// + protected virtual void Ended(Entity rule, ref GameRuleEndedEvent args) { } + + /// w + /// Called at the end of a round when text needs to be added for a game rule. + /// + protected virtual void AppendRoundEndText(Entity rule, ref RoundEndTextAppendEvent args) + { + + } + + /// + /// Called on an active gamerule entity in the Update function + /// + protected virtual void ActiveTick(EntityUid uid, T component, GameRuleComponent gameRule, float frameTime) + { + + } + + // TODO: We probably should move this to its own rule system that way we aren't doing an Enumerator for EVERY SINGLE GameRule. + // TODO: Or have GameTicker query over all GameRules once and then raise to each one :P + // TODO: Either way something that isn't this... + public override void Update(float frameTime) + { + base.Update(frameTime); + + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var comp1, out _, out var comp2)) + { + ActiveTick(uid, comp1, comp2, frameTime); + } + } +} diff --git a/Content.Shared/GameTicking/Rules/NukeopsRuleSystem.cs b/Content.Shared/GameTicking/Rules/NukeopsRuleSystem.cs new file mode 100644 index 0000000000..e792489ea2 --- /dev/null +++ b/Content.Shared/GameTicking/Rules/NukeopsRuleSystem.cs @@ -0,0 +1,88 @@ +using Content.Shared.Antag; +using Content.Shared.GameTicking.Components; +using Content.Shared.GameTicking.Rules.Components; +using Content.Shared.NPC.Components; +using Content.Shared.NPC.Systems; +using Content.Shared.Roles; +using Content.Shared.Roles.Components; +using Content.Shared.Station.Components; +using Robust.Shared.Prototypes; +using Robust.Shared.Random; + +namespace Content.Shared.GameTicking.Rules; + +/// +/// This handles the game rule logic for the and +/// If you do not know what NukeOps is you should try playing the game. +/// +public abstract partial class NukeopsRuleSystem : GameRuleSystem +{ + [Dependency] protected AntagSelectionSystem Antag = default!; + [Dependency] private NpcFactionSystem _npcFaction = default!; + + // TODO: This shouldn't be matching by ProtoId. + // It would be better if this were checked by component or something, + // but it needs to be distinct between the full Nukeops and Loneops rules, + // which NukeopsRuleComponent currently isn't. + // Better yet, maybe the behaviors this is used for could be moved to the rule component. + public static readonly EntProtoId NukeopsGameRule = "Nukeops"; + + protected override void Started(Entity rule, ref GameRuleStartedEvent args) + { + var eligible = new List>(); + var eligibleQuery = EntityQueryEnumerator(); + while (eligibleQuery.MoveNext(out var eligibleUid, out var eligibleComp, out var member)) + { + if (!_npcFaction.IsFactionHostile(rule.Comp1.Faction, (eligibleUid, member))) + continue; + + eligible.Add((eligibleUid, eligibleComp, member)); + } + + if (eligible.Count == 0) + return; + + rule.Comp1.TargetStation = RobustRandom.Pick(eligible); + var ev = new NukeopsTargetStationSelectedEvent(rule, rule.Comp1.TargetStation); + RaiseLocalEvent(ref ev); + } + + [SubscribeLocalEvent] + private void OnGetBriefing(Entity role, ref GetBriefingEvent args) + { + // TODO Different character screen briefing for the 3 nukie types + args.Append(Loc.GetString("nukeops-briefing")); + } + + [SubscribeLocalEvent] + private void OnAfterAntagEntSelected(Entity ent, ref AfterAntagEntitySelectedEvent args) + { + var target = (ent.Comp.TargetStation is not null) ? Name(ent.Comp.TargetStation.Value) : "the target"; + + Antag.SendBriefing(args.Session, + Loc.GetString("nukeops-welcome", + ("station", target), + ("name", Name(ent))), + Color.Red, + ent.Comp.GreetSoundNotification); + } +} + + +/// +/// Raised when a station has been assigned as a target for the NukeOps rule. +/// +[ByRefEvent] +public readonly struct NukeopsTargetStationSelectedEvent(EntityUid ruleEntity, EntityUid? targetStation) +{ + /// + /// The entity containing the NukeOps gamerule. + /// + public readonly EntityUid RuleEntity = ruleEntity; + + /// + /// The target station, if it exists. + /// + public readonly EntityUid? TargetStation = targetStation; +} + diff --git a/Content.Server/GameTicking/Rules/ParadoxCloneRuleSystem.cs b/Content.Shared/GameTicking/Rules/ParadoxCloneRuleSystem.cs similarity index 79% rename from Content.Server/GameTicking/Rules/ParadoxCloneRuleSystem.cs rename to Content.Shared/GameTicking/Rules/ParadoxCloneRuleSystem.cs index 447a25499f..9a713334ba 100644 --- a/Content.Server/GameTicking/Rules/ParadoxCloneRuleSystem.cs +++ b/Content.Shared/GameTicking/Rules/ParadoxCloneRuleSystem.cs @@ -1,36 +1,27 @@ -using Content.Server.Antag; -using Content.Server.Cloning; -using Content.Server.GameTicking.Rules.Components; -using Content.Server.Medical.SuitSensors; -using Content.Server.Objectives.Components; +using Content.Shared.Antag; +using Content.Shared.Cloning; using Content.Shared.GameTicking.Components; +using Content.Shared.GameTicking.Rules.Components; using Content.Shared.Gibbing.Components; using Content.Shared.Medical.SuitSensors; using Content.Shared.Mind; +using Content.Shared.Objectives.Components; using Content.Shared.Objectives.Systems; using Robust.Shared.Random; -namespace Content.Server.GameTicking.Rules; +namespace Content.Shared.GameTicking.Rules; public sealed partial class ParadoxCloneRuleSystem : GameRuleSystem { [Dependency] private IRobustRandom _random = default!; - [Dependency] private CloningSystem _cloning = default!; + [Dependency] private SharedCloningSystem _cloning = default!; [Dependency] private SharedMindSystem _mind = default!; - [Dependency] private SuitSensorSystem _sensor = default!; + [Dependency] private SharedSuitSensorSystem _sensor = default!; [Dependency] private AliveHumanoidTargetSystem _target = default!; - public override void Initialize() + protected override void Started(Entity rule, ref GameRuleStartedEvent args) { - base.Initialize(); - - SubscribeLocalEvent(OnAntagSelectEntity); - SubscribeLocalEvent(AfterAntagEntitySelected); - } - - protected override void Started(EntityUid uid, ParadoxCloneRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args) - { - base.Started(uid, component, gameRule, args); + base.Started(rule, ref args); // check if we got enough potential cloning targets, otherwise cancel the gamerule so that the ghost role does not show up var allHumans = _target.GetMinds(); @@ -38,11 +29,12 @@ public sealed partial class ParadoxCloneRuleSystem : GameRuleSystem ent, ref AntagSelectEntityEvent args) { if (ent.Comp.OriginalBody != null) // target was overridden, for example by admin antag control @@ -92,6 +84,7 @@ public sealed partial class ParadoxCloneRuleSystem : GameRuleSystem ent, ref AfterAntagEntitySelectedEvent args) { if (ent.Comp.OriginalMind == null) diff --git a/Content.Shared/GameTicking/Rules/RevolutionaryRuleSystem.cs b/Content.Shared/GameTicking/Rules/RevolutionaryRuleSystem.cs new file mode 100644 index 0000000000..4d75ce0daa --- /dev/null +++ b/Content.Shared/GameTicking/Rules/RevolutionaryRuleSystem.cs @@ -0,0 +1,20 @@ +using Content.Shared.GameTicking.Rules.Components; +using Content.Shared.Revolutionary.Components; +using Content.Shared.Roles; +using Content.Shared.Roles.Components; + +namespace Content.Shared.GameTicking.Rules; + +/// +/// Where all the main stuff for Revolutionaries happens (Assigning Head Revs, Command on station, and checking for the game to end.) +/// +public abstract partial class RevolutionaryRuleSystem : GameRuleSystem +{ + [SubscribeLocalEvent] + private void OnGetBriefing(Entity comp, ref GetBriefingEvent args) + { + var ent = args.Mind.Comp.OwnedEntity; + var head = HasComp(ent); + args.Append(Loc.GetString(head ? "head-rev-briefing" : "rev-briefing")); + } +} diff --git a/Content.Server/GameTicking/Rules/SurvivorRuleSystem.cs b/Content.Shared/GameTicking/Rules/SurvivorRuleSystem.cs similarity index 76% rename from Content.Server/GameTicking/Rules/SurvivorRuleSystem.cs rename to Content.Shared/GameTicking/Rules/SurvivorRuleSystem.cs index 80bab5a499..21792b6b46 100644 --- a/Content.Server/GameTicking/Rules/SurvivorRuleSystem.cs +++ b/Content.Shared/GameTicking/Rules/SurvivorRuleSystem.cs @@ -1,42 +1,34 @@ -using Content.Server.Antag; -using Content.Server.GameTicking.Rules.Components; -using Content.Server.Roles; -using Content.Server.Shuttles.Systems; +using Content.Shared.Antag; using Content.Shared.GameTicking.Components; +using Content.Shared.GameTicking.Rules.Components; using Content.Shared.Mind; using Content.Shared.Mobs.Systems; using Content.Shared.Objectives.Systems; +using Content.Shared.Roles; using Content.Shared.Roles.Components; +using Content.Shared.Shuttles.Systems; using Content.Shared.Survivor.Components; using Content.Shared.Tag; -using Robust.Server.GameObjects; using Robust.Shared.Prototypes; -namespace Content.Server.GameTicking.Rules; +namespace Content.Shared.GameTicking.Rules; public sealed partial class SurvivorRuleSystem : GameRuleSystem { - [Dependency] private AntagSelectionSystem _antag = default!; - [Dependency] private EmergencyShuttleSystem _eShuttle = default!; - [Dependency] private MobStateSystem _mobState = default!; - [Dependency] private RoleSystem _role = default!; - [Dependency] private TagSystem _tag = default!; [Dependency] private AliveHumanoidTargetSystem _target = default!; - [Dependency] private TransformSystem _xform = default!; + [Dependency] private AntagSelectionSystem _antag = default!; + [Dependency] private MobStateSystem _mobState = default!; + [Dependency] private SharedEmergencyShuttleSystem _eShuttle = default!; + [Dependency] private SharedRoleSystem _role = default!; + [Dependency] private SharedTransformSystem _xform = default!; + [Dependency] private TagSystem _tag = default!; private static readonly ProtoId InvalidForSurvivorAntagTag = "InvalidForSurvivorAntag"; - public override void Initialize() - { - base.Initialize(); - - SubscribeLocalEvent(OnGetBriefing); - } - // TODO: Planned rework post wizard release when RandomGlobalSpawnSpell becomes a gamerule - protected override void Started(EntityUid uid, SurvivorRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args) + protected override void Started(Entity rule, ref GameRuleStartedEvent args) { - base.Started(uid, component, gameRule, args); + base.Started(rule, ref args); var allAliveHumanMinds = _target.GetMinds(); @@ -57,17 +49,16 @@ public sealed partial class SurvivorRuleSystem : GameRuleSystem ent, ref GetBriefingEvent args) { args.Append(Loc.GetString("survivor-role-greeting")); } - protected override void AppendRoundEndText(EntityUid uid, - SurvivorRuleComponent component, - GameRuleComponent gameRule, + protected override void AppendRoundEndText(Entity rule, ref RoundEndTextAppendEvent args) { - base.AppendRoundEndText(uid, component, gameRule, ref args); + base.AppendRoundEndText(rule, ref args); // Using this instead of alive antagonists to make checking for shuttle & if the ent is alive easier var existingSurvivors = AllEntityQuery(); diff --git a/Content.Server/GameTicking/Rules/ThiefRuleSystem.cs b/Content.Shared/GameTicking/Rules/ThiefRuleSystem.cs similarity index 73% rename from Content.Server/GameTicking/Rules/ThiefRuleSystem.cs rename to Content.Shared/GameTicking/Rules/ThiefRuleSystem.cs index d6239449a2..fb016d7596 100644 --- a/Content.Server/GameTicking/Rules/ThiefRuleSystem.cs +++ b/Content.Shared/GameTicking/Rules/ThiefRuleSystem.cs @@ -1,25 +1,17 @@ -using Content.Server.Antag; -using Content.Server.GameTicking.Rules.Components; -using Content.Server.Roles; +using Content.Shared.Antag; +using Content.Shared.GameTicking.Rules.Components; using Content.Shared.Humanoid; +using Content.Shared.Roles; using Content.Shared.Roles.Components; -namespace Content.Server.GameTicking.Rules; +namespace Content.Shared.GameTicking.Rules; public sealed partial class ThiefRuleSystem : GameRuleSystem { [Dependency] private AntagSelectionSystem _antag = default!; - public override void Initialize() - { - base.Initialize(); - - SubscribeLocalEvent(AfterAntagSelected); - - SubscribeLocalEvent(OnGetBriefing); - } - // Greeting upon thief activation + [SubscribeLocalEvent] private void AfterAntagSelected(Entity mindId, ref AfterAntagEntitySelectedEvent args) { var ent = args.EntityUid; @@ -27,6 +19,7 @@ public sealed partial class ThiefRuleSystem : GameRuleSystem } // Character screen briefing + [SubscribeLocalEvent] private void OnGetBriefing(Entity role, ref GetBriefingEvent args) { var ent = args.Mind.Comp.OwnedEntity; diff --git a/Content.Server/GameTicking/Rules/TraitorRuleSystem.cs b/Content.Shared/GameTicking/Rules/TraitorRuleSystem.cs similarity index 63% rename from Content.Server/GameTicking/Rules/TraitorRuleSystem.cs rename to Content.Shared/GameTicking/Rules/TraitorRuleSystem.cs index 9d4ba46985..bce7e76751 100644 --- a/Content.Server/GameTicking/Rules/TraitorRuleSystem.cs +++ b/Content.Shared/GameTicking/Rules/TraitorRuleSystem.cs @@ -1,9 +1,9 @@ -using Content.Server.Antag; -using Content.Server.GameTicking.Rules.Components; -using Content.Server.Mind; -using Content.Server.Objectives; -using Content.Server.Traitor.Uplink; +using System.Linq; +using System.Text; +using Content.Shared.Antag; +using Content.Shared.Codewords; using Content.Shared.FixedPoint; +using Content.Shared.GameTicking.Rules.Components; using Content.Shared.Mind; using Content.Shared.NPC.Systems; using Content.Shared.PDA; @@ -13,46 +13,33 @@ using Content.Shared.Roles.Components; using Content.Shared.Roles.Jobs; using Content.Shared.Roles.RoleCodeword; using Robust.Shared.Random; -using System.Linq; -using System.Text; -using Content.Server.Codewords; -namespace Content.Server.GameTicking.Rules; +namespace Content.Shared.GameTicking.Rules; -public sealed partial class TraitorRuleSystem : GameRuleSystem +public abstract partial class TraitorRuleSystem : GameRuleSystem { - private static readonly Color TraitorCodewordColor = Color.FromHex("#cc3b3b"); - - [Dependency] private AntagSelectionSystem _antag = default!; - [Dependency] private SharedJobSystem _jobs = default!; - [Dependency] private MindSystem _mindSystem = default!; - [Dependency] private NpcFactionSystem _npcFaction = default!; [Dependency] private IRobustRandom _random = default!; + [Dependency] private AntagSelectionSystem _antag = default!; + [Dependency] protected CodewordSystem Codeword = default!; + [Dependency] private NpcFactionSystem _npcFaction = default!; + [Dependency] private SharedJobSystem _jobs = default!; + [Dependency] private SharedMindSystem _mindSystem = default!; [Dependency] private SharedRoleCodewordSystem _roleCodewordSystem = default!; [Dependency] private SharedRoleSystem _roleSystem = default!; - [Dependency] private UplinkSystem _uplink = default!; - [Dependency] private CodewordSystem _codewordSystem = default!; - public override void Initialize() + private static readonly Color TraitorCodewordColor = Color.FromHex("#cc3b3b"); + + [SubscribeLocalEvent] + private void AfterEntitySelected(Entity rule, ref AfterAntagEntitySelectedEvent args) { - base.Initialize(); - - Log.Level = LogLevel.Debug; - - SubscribeLocalEvent(AfterEntitySelected); - SubscribeLocalEvent(OnObjectivesTextPrepend); + Log.Debug($"AfterAntagEntitySelected {ToPrettyString(rule)}"); + MakeTraitor(args.EntityUid, rule); } - private void AfterEntitySelected(Entity ent, ref AfterAntagEntitySelectedEvent args) - { - Log.Debug($"AfterAntagEntitySelected {ToPrettyString(ent)}"); - MakeTraitor(args.EntityUid, ent); - } - - public bool MakeTraitor(EntityUid traitor, TraitorRuleComponent component) + public bool MakeTraitor(EntityUid traitor, Entity rule) { Log.Debug($"MakeTraitor {ToPrettyString(traitor)} - start"); - var factionCodewords = _codewordSystem.GetCodewords(component.CodewordFactionPrototypeId); + var factionCodewords = Codeword.GetCodewords(rule.Comp.CodewordFactionPrototypeId); //Grab the mind if it wasn't provided if (!_mindSystem.TryGetMind(traitor, out var mindId, out var mind)) @@ -63,22 +50,22 @@ public sealed partial class TraitorRuleSystem : GameRuleSystem(mindId, out var traitorRole); + _roleSystem.MindHasRole((mindId, mind), out var traitorRole); if (traitorRole is not null) { Log.Debug($"MakeTraitor {ToPrettyString(traitor)} - Add traitor briefing components"); @@ -134,51 +121,17 @@ public sealed partial class TraitorRuleSystem : GameRuleSystem +public abstract partial class XenoborgsRuleSystem : GameRuleSystem { - [Dependency] private IGameTiming _timing = default!; - [Dependency] private AntagSelectionSystem _antag = default!; - [Dependency] private ChatSystem _chatSystem = default!; - [Dependency] private MobStateSystem _mobState = default!; - [Dependency] private RoundEndSystem _roundEnd = default!; - [Dependency] private SharedMindSystem _mindSystem = default!; - [Dependency] private StationSystem _station = default!; [Dependency] private AliveHumanoidTargetSystem _target = default!; + [Dependency] private AntagSelectionSystem _antag = default!; + [Dependency] private MobStateSystem _mobState = default!; + [Dependency] private SharedMindSystem _mindSystem = default!; private static readonly Color AnnouncmentColor = Color.Gold; - public void SendXenoborgDeathAnnouncement(Entity ent, bool mothershipCoreAlive) + public void SendXenoborgDeathAnnouncement(Entity ent, bool mothershipCoreAlive) { if (ent.Comp.MothershipCoreDeathAnnouncmentSent) return; var status = mothershipCoreAlive ? "alive" : "dead"; - _chatSystem.DispatchGlobalAnnouncement( - Loc.GetString($"xenoborgs-no-more-threat-mothership-core-{status}-announcement"), - colorOverride: AnnouncmentColor); + GameTicker.GameAnnouncement($"xenoborgs-no-more-threat-mothership-core-{status}-announcement", color: AnnouncmentColor); } public void SendMothershipDeathAnnouncement(Entity ent) { - _chatSystem.DispatchGlobalAnnouncement( - Loc.GetString("mothership-destroyed-announcement"), - colorOverride: AnnouncmentColor); - + GameTicker.GameAnnouncement("mothership-destroyed-announcement", color: AnnouncmentColor); ent.Comp.MothershipCoreDeathAnnouncmentSent = true; } // TODO: Refactor the end of round text - protected override void AppendRoundEndText(EntityUid uid, - XenoborgsRuleComponent component, - GameRuleComponent gameRule, + protected override void AppendRoundEndText(Entity rule, ref RoundEndTextAppendEvent args) { - base.AppendRoundEndText(uid, component, gameRule, ref args); + base.AppendRoundEndText(rule, ref args); var numXenoborgs = GetNumberXenoborgs(); var numHumans = _target.GetMinds().Count; @@ -82,11 +66,11 @@ public sealed partial class XenoborgsRuleSystem : GameRuleSystem rule, ref GameRuleStartedEvent args) { - var numXenoborgs = GetNumberXenoborgs(); - var numHumans = _target.GetMinds().Count; + base.Started(rule, ref args); - xenoborgsRuleComponent.MaxNumberXenoborgs = Math.Max(xenoborgsRuleComponent.MaxNumberXenoborgs, numXenoborgs); - - if (xenoborgsRuleComponent.XenoborgShuttleCalled - || (float)numXenoborgs / (numHumans + numXenoborgs) <= xenoborgsRuleComponent.XenoborgShuttleCallPercentage - || _roundEnd.IsRoundEndRequested()) - return; - - foreach (var station in _station.GetStations()) - { - _chatSystem.DispatchStationAnnouncement(station, Loc.GetString("xenoborg-shuttle-call"), colorOverride: Color.BlueViolet); - } - _roundEnd.RequestRoundEnd(null, null, false, cantRecall: true); - xenoborgsRuleComponent.XenoborgShuttleCalled = true; - } - - protected override void Started(EntityUid uid, XenoborgsRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args) - { - base.Started(uid, component, gameRule, args); - - component.NextRoundEndCheck = _timing.CurTime + component.EndCheckDelay; - } - - protected override void ActiveTick(EntityUid uid, XenoborgsRuleComponent component, GameRuleComponent gameRule, float frameTime) - { - base.ActiveTick(uid, component, gameRule, frameTime); - - if (!component.NextRoundEndCheck.HasValue || component.NextRoundEndCheck > _timing.CurTime) - return; - - CheckRoundEnd(component); - component.NextRoundEndCheck = _timing.CurTime + component.EndCheckDelay; + rule.Comp1.NextRoundEndCheck = Timing.CurTime + rule.Comp1.EndCheckDelay; } /// @@ -139,7 +92,7 @@ public sealed partial class XenoborgsRuleSystem : GameRuleSystemif it should only include xenoborgs with a mind /// if it should only include xenoborgs that are alive /// the number of xenoborgs - private int GetNumberXenoborgs(bool playerControlled = true, bool alive = true) + protected int GetNumberXenoborgs(bool playerControlled = true, bool alive = true) { var numberXenoborgs = 0; diff --git a/Content.Server/GameTicking/Rules/ZombieRuleSystem.cs b/Content.Shared/GameTicking/Rules/ZombieRuleSystem.cs similarity index 57% rename from Content.Server/GameTicking/Rules/ZombieRuleSystem.cs rename to Content.Shared/GameTicking/Rules/ZombieRuleSystem.cs index bf188122a5..d1fb49e7a7 100644 --- a/Content.Server/GameTicking/Rules/ZombieRuleSystem.cs +++ b/Content.Shared/GameTicking/Rules/ZombieRuleSystem.cs @@ -1,12 +1,8 @@ -using Content.Server.Antag; -using Content.Server.Chat.Systems; -using Content.Server.GameTicking.Rules.Components; -using Content.Server.Popups; -using Content.Server.Roles; -using Content.Server.RoundEnd; -using Content.Server.Station.Systems; -using Content.Server.Zombies; +using System.Globalization; +using System.Linq; +using Content.Shared.Antag; using Content.Shared.GameTicking.Components; +using Content.Shared.GameTicking.Rules.Components; using Content.Shared.Humanoid; using Content.Shared.Mind; using Content.Shared.Mobs; @@ -14,55 +10,47 @@ using Content.Shared.Mobs.Components; using Content.Shared.Mobs.Systems; using Content.Shared.Roles; using Content.Shared.Roles.Components; +using Content.Shared.Station.Systems; using Content.Shared.Zombies; using Robust.Shared.Player; -using Robust.Shared.Timing; -using System.Globalization; -using System.Linq; -namespace Content.Server.GameTicking.Rules; +namespace Content.Shared.GameTicking.Rules; -public sealed partial class ZombieRuleSystem : GameRuleSystem +public abstract partial class ZombieRuleSystem : GameRuleSystem { - [Dependency] private AntagSelectionSystem _antag = default!; - [Dependency] private ChatSystem _chat = default!; - [Dependency] private IGameTiming _timing = default!; [Dependency] private ISharedPlayerManager _player = default!; + [Dependency] private AntagSelectionSystem _antag = default!; [Dependency] private MobStateSystem _mobState = default!; - [Dependency] private PopupSystem _popup = default!; - [Dependency] private RoundEndSystem _roundEnd = default!; [Dependency] private SharedMindSystem _mindSystem = default!; [Dependency] private SharedRoleSystem _roles = default!; - [Dependency] private StationSystem _station = default!; - [Dependency] private ZombieSystem _zombie = default!; - [Dependency] private EntityQuery _zombieQuery = default!; + [Dependency] protected StationSystem Station = default!; - public override void Initialize() + [Dependency] private EntityQuery _zombieQuery; + + protected override void Started(Entity rule, ref GameRuleStartedEvent args) { - base.Initialize(); + base.Started(rule, ref args); - SubscribeLocalEvent(OnGetBriefing); - SubscribeLocalEvent(OnGetBriefing); - SubscribeLocalEvent(OnZombifySelf); + rule.Comp1.NextRoundEndCheck = Timing.CurTime + rule.Comp1.EndCheckDelay; } + [SubscribeLocalEvent] private void OnGetBriefing(Entity role, ref GetBriefingEvent args) { if (!_roles.MindHasRole(args.Mind.Owner)) args.Append(Loc.GetString("zombie-patientzero-role-greeting")); } + [SubscribeLocalEvent] private void OnGetBriefing(Entity role, ref GetBriefingEvent args) { args.Append(Loc.GetString("zombie-infection-greeting")); } - protected override void AppendRoundEndText(EntityUid uid, - ZombieRuleComponent component, - GameRuleComponent gameRule, + protected override void AppendRoundEndText(Entity rule, ref RoundEndTextAppendEvent args) { - base.AppendRoundEndText(uid, component, gameRule, ref args); + base.AppendRoundEndText(rule, ref args); // This is just the general condition thing used for determining the win/lose text var fraction = GetInfectedFraction(true, true); @@ -78,7 +66,7 @@ public sealed partial class ZombieRuleSystem : GameRuleSystem - /// The big kahoona function for checking if the round is gonna end - /// - private void CheckRoundEnd(ZombieRuleComponent zombieRuleComponent) - { - var healthy = GetHealthyHumans(); - if (healthy.Count == 1) // Only one human left. spooky - _popup.PopupEntity(Loc.GetString("zombie-alone"), healthy[0], healthy[0]); - - if (GetInfectedFraction(false) > zombieRuleComponent.ZombieShuttleCallPercentage && !_roundEnd.IsRoundEndRequested()) - { - foreach (var station in _station.GetStations()) - { - _chat.DispatchStationAnnouncement(station, Loc.GetString("zombie-shuttle-call"), colorOverride: Color.Crimson); - } - - _roundEnd.DoRoundEndBehavior(zombieRuleComponent.ZombieRoundEndBehavior, - zombieRuleComponent.ZombieEvacShuttleTime); - } - - // we include dead for this count because we don't want to end the round - // when everyone gets on the shuttle. - if (GetInfectedFraction() >= 1) // Oops, all zombies - _roundEnd.EndRound(); - } - - protected override void Started(EntityUid uid, ZombieRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args) - { - base.Started(uid, component, gameRule, args); - - component.NextRoundEndCheck = _timing.CurTime + component.EndCheckDelay; - } - - protected override void ActiveTick(EntityUid uid, ZombieRuleComponent component, GameRuleComponent gameRule, float frameTime) - { - base.ActiveTick(uid, component, gameRule, frameTime); - if (!component.NextRoundEndCheck.HasValue || component.NextRoundEndCheck > _timing.CurTime) - return; - CheckRoundEnd(component); - component.NextRoundEndCheck = _timing.CurTime + component.EndCheckDelay; - } - - private void OnZombifySelf(EntityUid uid, IncurableZombieComponent component, ZombifySelfActionEvent args) - { - _zombie.ZombifyEntity(uid); - if (component.Action != null) - Del(component.Action.Value); - } /// /// Get the fraction of players that are infected, between 0 and 1 @@ -166,7 +106,7 @@ public sealed partial class ZombieRuleSystem : GameRuleSystemInclude healthy players that are not on the station grid /// Should dead zombies be included in the count /// - private float GetInfectedFraction(bool includeOffStation = true, bool includeDead = false) + protected float GetInfectedFraction(bool includeOffStation = true, bool includeDead = false) { var players = GetHealthyHumans(includeOffStation); var zombieCount = 0; @@ -186,16 +126,16 @@ public sealed partial class ZombieRuleSystem : GameRuleSystem /// - private List GetHealthyHumans(bool includeOffStation = true) + protected List GetHealthyHumans(bool includeOffStation = true) { var healthy = new List(); var stationGrids = new HashSet(); if (!includeOffStation) { - foreach (var station in _station.GetStationsSet()) + foreach (var station in Station.GetStationsSet()) { - if (_station.GetLargestGrid(station) is { } grid) + if (Station.GetLargestGrid(station) is { } grid) stationGrids.Add(grid); } } diff --git a/Content.Shared/GameTicking/SharedGameTicker.cs b/Content.Shared/GameTicking/SharedGameTicker.cs deleted file mode 100644 index 878f6213cb..0000000000 --- a/Content.Shared/GameTicking/SharedGameTicker.cs +++ /dev/null @@ -1,237 +0,0 @@ -using Content.Shared.Roles; -using Robust.Shared.Network; -using Robust.Shared.Prototypes; -using Robust.Shared.Replays; -using Robust.Shared.Serialization; -using Robust.Shared.Serialization.Markdown.Mapping; -using Robust.Shared.Serialization.Markdown.Value; -using Robust.Shared.Timing; -using Robust.Shared.Audio; -using Content.Shared.GameTicking.Prototypes; - -namespace Content.Shared.GameTicking -{ - public abstract partial class SharedGameTicker : EntitySystem - { - [Dependency] private IReplayRecordingManager _replay = default!; - [Dependency] private IGameTiming _gameTiming = default!; - - /// - /// A list storing the start times of all game rules that have been started this round. - /// Game rules can be started and stopped at any time, including midround. - /// - public abstract IReadOnlyList<(TimeSpan, string)> AllPreviousGameRules { get; } - - // See ideally these would be pulled from the job definition or something. - // But this is easier, and at least it isn't hardcoded. - //TODO: Move these, they really belong in StationJobsSystem or a cvar. - public static readonly ProtoId FallbackOverflowJob = "Passenger"; - - public const string FallbackOverflowJobName = "job-name-passenger"; - - // TODO network. - // Probably most useful for replays, round end info, and probably things like lobby menus. - [ViewVariables] - public int RoundId { get; protected set; } - [ViewVariables] public TimeSpan RoundStartTimeSpan { get; protected set; } - - public override void Initialize() - { - base.Initialize(); - _replay.RecordingStarted += OnRecordingStart; - } - - public override void Shutdown() - { - _replay.RecordingStarted -= OnRecordingStart; - } - - private void OnRecordingStart(MappingDataNode metadata, List events) - { - if (RoundId != 0) - { - metadata["roundId"] = new ValueDataNode(RoundId.ToString()); - } - } - - public TimeSpan RoundDuration() - { - return _gameTiming.CurTime.Subtract(RoundStartTimeSpan); - } - } - - [Serializable, NetSerializable] - public sealed class TickerJoinLobbyEvent : EntityEventArgs - { - } - - [Serializable, NetSerializable] - public sealed class TickerJoinGameEvent : EntityEventArgs - { - } - - [Serializable, NetSerializable] - public sealed class TickerLateJoinStatusEvent : EntityEventArgs - { - // TODO: Make this a replicated CVar, honestly. - public bool Disallowed { get; } - - public TickerLateJoinStatusEvent(bool disallowed) - { - Disallowed = disallowed; - } - } - - [Serializable, NetSerializable] - public sealed class TickerConnectionStatusEvent : EntityEventArgs - { - public TimeSpan RoundStartTimeSpan { get; } - public TickerConnectionStatusEvent(TimeSpan roundStartTimeSpan) - { - RoundStartTimeSpan = roundStartTimeSpan; - } - } - - [Serializable, NetSerializable] - public sealed class TickerLobbyStatusEvent : EntityEventArgs - { - public bool IsRoundStarted { get; } - public ProtoId? LobbyBackground { get; } - public bool YouAreReady { get; } - // UTC. - public TimeSpan StartTime { get; } - public TimeSpan RoundStartTimeSpan { get; } - public bool Paused { get; } - - public TickerLobbyStatusEvent(bool isRoundStarted, ProtoId? lobbyBackground, bool youAreReady, TimeSpan startTime, TimeSpan preloadTime, TimeSpan roundStartTimeSpan, bool paused) - { - IsRoundStarted = isRoundStarted; - LobbyBackground = lobbyBackground; - YouAreReady = youAreReady; - StartTime = startTime; - RoundStartTimeSpan = roundStartTimeSpan; - Paused = paused; - } - } - - [Serializable, NetSerializable] - public sealed class TickerLobbyInfoEvent : EntityEventArgs - { - public string TextBlob { get; } - - public TickerLobbyInfoEvent(string textBlob) - { - TextBlob = textBlob; - } - } - - [Serializable, NetSerializable] - public sealed class TickerLobbyCountdownEvent : EntityEventArgs - { - /// - /// The game time that the game will start at. - /// - public TimeSpan StartTime { get; } - - /// - /// Whether or not the countdown is paused - /// - public bool Paused { get; } - - public TickerLobbyCountdownEvent(TimeSpan startTime, bool paused) - { - StartTime = startTime; - Paused = paused; - } - } - - [Serializable, NetSerializable] - public sealed class TickerJobsAvailableEvent( - Dictionary stationNames, - Dictionary, int?>> jobsAvailableByStation, - Dictionary?> jobWeightsByStation) - : EntityEventArgs - { - /// - /// The Status of the Player in the lobby (ready, observer, ...) - /// - public Dictionary, int?>> JobsAvailableByStation { get; } = jobsAvailableByStation; - - public Dictionary StationNames { get; } = stationNames; - - public Dictionary?> JobWeightsByStation { get; } = jobWeightsByStation; - } - - [Serializable, NetSerializable, DataDefinition] - public sealed partial class RoundEndMessageEvent : EntityEventArgs - { - [Serializable, NetSerializable, DataDefinition] - public partial struct RoundEndPlayerInfo - { - [DataField] - public string PlayerOOCName; - - [DataField] - public string? PlayerICName; - - [DataField, NonSerialized] - public NetUserId? PlayerGuid; - - public string Role; - - [DataField, NonSerialized] - public string[] JobPrototypes; - - [DataField, NonSerialized] - public string[] AntagPrototypes; - - public NetEntity? PlayerNetEntity; - - [DataField] - public bool Antag; - - [DataField] - public bool Observer; - - public bool Connected; - } - - public string GamemodeTitle { get; } - public string RoundEndText { get; } - public TimeSpan RoundDuration { get; } - public int RoundId { get; } - public int PlayerCount { get; } - public RoundEndPlayerInfo[] AllPlayersEndInfo { get; } - - /// - /// Sound gets networked due to how entity lifecycle works between client / server and to avoid clipping. - /// - public ResolvedSoundSpecifier? RestartSound; - - public RoundEndMessageEvent( - string gamemodeTitle, - string roundEndText, - TimeSpan roundDuration, - int roundId, - int playerCount, - RoundEndPlayerInfo[] allPlayersEndInfo, - ResolvedSoundSpecifier? restartSound) - { - GamemodeTitle = gamemodeTitle; - RoundEndText = roundEndText; - RoundDuration = roundDuration; - RoundId = roundId; - PlayerCount = playerCount; - AllPlayersEndInfo = allPlayersEndInfo; - RestartSound = restartSound; - } - } - - [Serializable, NetSerializable] - public enum PlayerGameStatus : sbyte - { - NotReadyToPlay = 0, - ReadyToPlay, - JoinedGame, - } -} diff --git a/Content.Server/Maps/GameMapPoolPrototype.cs b/Content.Shared/Maps/GameMapPoolPrototype.cs similarity index 90% rename from Content.Server/Maps/GameMapPoolPrototype.cs rename to Content.Shared/Maps/GameMapPoolPrototype.cs index 567570c0d5..f828f05703 100644 --- a/Content.Server/Maps/GameMapPoolPrototype.cs +++ b/Content.Shared/Maps/GameMapPoolPrototype.cs @@ -1,8 +1,7 @@ -using Content.Shared.Maps; using JetBrains.Annotations; using Robust.Shared.Prototypes; -namespace Content.Server.Maps; +namespace Content.Shared.Maps; /// /// Prototype that holds a pool of maps that can be indexed based on the map pool CCVar. diff --git a/Content.Shared/Medical/SuitSensors/SharedSuitSensorSystem.cs b/Content.Shared/Medical/SuitSensors/SharedSuitSensorSystem.cs index bfec5e0fdd..7ca15ae590 100644 --- a/Content.Shared/Medical/SuitSensors/SharedSuitSensorSystem.cs +++ b/Content.Shared/Medical/SuitSensors/SharedSuitSensorSystem.cs @@ -13,7 +13,7 @@ using Content.Shared.Mobs; using Content.Shared.Mobs.Components; using Content.Shared.Mobs.Systems; using Content.Shared.Popups; -using Content.Shared.Station; +using Content.Shared.Station.Systems; using Content.Shared.Verbs; using Robust.Shared.Containers; using Robust.Shared.Map; @@ -24,7 +24,7 @@ namespace Content.Shared.Medical.SuitSensors; public abstract partial class SharedSuitSensorSystem : EntitySystem { - [Dependency] private SharedStationSystem _stationSystem = default!; + [Dependency] private StationSystem _stationSystem = default!; [Dependency] private MobStateSystem _mobStateSystem = default!; [Dependency] private SharedPopupSystem _popupSystem = default!; [Dependency] private SharedTransformSystem _transform = default!; @@ -315,7 +315,7 @@ public abstract partial class SharedSuitSensorSystem : EntitySystem /// /// Sets mode of the of the chosen entity. - /// Makes popup when not null + /// Makes popup when not null /// /// Entity and it's component that should be changed /// Selected mode @@ -352,7 +352,7 @@ public abstract partial class SharedSuitSensorSystem : EntitySystem /// /// Attempts to get full from the /// - /// Entity to get status + /// Entity to get status /// Full of the chosen uid public SuitSensorStatus? GetSensorState(Entity ent) { diff --git a/Content.Server/Objectives/Components/TargetOverrideComponent.cs b/Content.Shared/Objectives/Components/TargetOverrideComponent.cs similarity index 90% rename from Content.Server/Objectives/Components/TargetOverrideComponent.cs rename to Content.Shared/Objectives/Components/TargetOverrideComponent.cs index faa402012c..145dc1bbe3 100644 --- a/Content.Server/Objectives/Components/TargetOverrideComponent.cs +++ b/Content.Shared/Objectives/Components/TargetOverrideComponent.cs @@ -1,4 +1,4 @@ -namespace Content.Server.Objectives.Components; +namespace Content.Shared.Objectives.Components; /// /// Sets a target objective to a specific target when receiving it. diff --git a/Content.Shared/PDA/SharedPdaSystem.cs b/Content.Shared/PDA/SharedPdaSystem.cs index 6eb665747d..055acc0653 100644 --- a/Content.Shared/PDA/SharedPdaSystem.cs +++ b/Content.Shared/PDA/SharedPdaSystem.cs @@ -84,5 +84,19 @@ namespace Content.Shared.PDA // This does nothing yet while I finish up PDA prediction // Overriden by the server } + + /// + /// Sets the owner of a PDA to a given entity, with a given name. + /// + /// Pda Entity + /// Pda Component + /// New entity owner of the PDA, this is probably an ID card + /// Name of the new owner, this is probably the name on the ID card + public void SetOwner(EntityUid uid, PdaComponent pda, EntityUid owner, string ownerName) + { + pda.OwnerName = ownerName; + pda.PdaOwner = owner; + UpdatePdaUi(uid, pda); + } } } diff --git a/Content.Shared/Players/ContentPlayerData.cs b/Content.Shared/Players/ContentPlayerData.cs index edf0228df2..90988f5382 100644 --- a/Content.Shared/Players/ContentPlayerData.cs +++ b/Content.Shared/Players/ContentPlayerData.cs @@ -29,7 +29,7 @@ public sealed class ContentPlayerData /// The currently occupied mind of the player owning this data. /// DO NOT DIRECTLY SET THIS UNLESS YOU KNOW WHAT YOU'RE DOING. /// - [ViewVariables, Access(typeof(SharedMindSystem), typeof(SharedGameTicker))] + [ViewVariables, Access(typeof(SharedMindSystem), typeof(GameTicker))] public EntityUid? Mind { get; set; } /// diff --git a/Content.Shared/Preferences/HumanoidCharacterProfile.cs b/Content.Shared/Preferences/HumanoidCharacterProfile.cs index df5bca934e..fba2b41046 100644 --- a/Content.Shared/Preferences/HumanoidCharacterProfile.cs +++ b/Content.Shared/Preferences/HumanoidCharacterProfile.cs @@ -45,7 +45,7 @@ namespace Content.Shared.Preferences private Dictionary, JobPriority> _jobPriorities = new() { { - SharedGameTicker.FallbackOverflowJob, JobPriority.High + GameTicker.FallbackOverflowJob, JobPriority.High } }; diff --git a/Content.Shared/Roles/SharedRoleSystem.cs b/Content.Shared/Roles/SharedRoleSystem.cs index a7d8e18c28..f82257b2d1 100644 --- a/Content.Shared/Roles/SharedRoleSystem.cs +++ b/Content.Shared/Roles/SharedRoleSystem.cs @@ -775,7 +775,45 @@ public abstract partial class SharedRoleSystem : EntitySystem /// Raised on the client to update Role Type on the character window, in case it happened to be open. /// [Serializable, NetSerializable] -public sealed class MindRoleTypeChangedEvent : EntityEventArgs -{ +public sealed class MindRoleTypeChangedEvent : EntityEventArgs; +/// +/// Event raised on the mind to get its briefing. +/// Handlers can either replace or append to the briefing, whichever is more appropriate. +/// +[ByRefEvent] +public record struct GetBriefingEvent +{ + /// + /// The text that will be shown on the Character Screen + /// + public string? Briefing; + + /// + /// The Mind to whose Mind Role Entities the briefing is sent to + /// + public Entity Mind; + + public GetBriefingEvent() { } + + public GetBriefingEvent(string? briefing) + { + Briefing = briefing; + } + + /// + /// If there is no briefing, sets it to the string. + /// If there is a briefing, adds a new line to separate it from the appended string. + /// + public void Append(string text) + { + if (Briefing == null) + { + Briefing = text; + } + else + { + Briefing += "\n" + text; + } + } } diff --git a/Content.Shared/RoundEnd/RoundEndBehavior.cs b/Content.Shared/RoundEnd/RoundEndBehavior.cs new file mode 100644 index 0000000000..449c79fb8a --- /dev/null +++ b/Content.Shared/RoundEnd/RoundEndBehavior.cs @@ -0,0 +1,19 @@ +namespace Content.Shared.RoundEnd; + +public enum RoundEndBehavior : byte +{ + /// + /// Instantly end round + /// + InstantEnd, + + /// + /// Call shuttle with custom announcement + /// + ShuttleCall, + + /// + /// Do nothing + /// + Nothing +} diff --git a/Content.Server/Shuttles/Components/ArrivalsBlacklistComponent.cs b/Content.Shared/Shuttles/Components/ArrivalsBlacklistComponent.cs similarity index 79% rename from Content.Server/Shuttles/Components/ArrivalsBlacklistComponent.cs rename to Content.Shared/Shuttles/Components/ArrivalsBlacklistComponent.cs index 1d5b88d482..d2b298e05e 100644 --- a/Content.Server/Shuttles/Components/ArrivalsBlacklistComponent.cs +++ b/Content.Shared/Shuttles/Components/ArrivalsBlacklistComponent.cs @@ -1,9 +1,7 @@ -namespace Content.Server.Shuttles.Components; +namespace Content.Shared.Shuttles.Components; /// /// This is used for blacklisting entities from being on the arrivals shuttle when it goes FTL. /// [RegisterComponent] -public sealed partial class ArrivalsBlacklistComponent : Component -{ -} +public sealed partial class ArrivalsBlacklistComponent : Component; diff --git a/Content.Server/Shuttles/Components/ArrivalsShuttleComponent.cs b/Content.Shared/Shuttles/Components/ArrivalsShuttleComponent.cs similarity index 89% rename from Content.Server/Shuttles/Components/ArrivalsShuttleComponent.cs rename to Content.Shared/Shuttles/Components/ArrivalsShuttleComponent.cs index b9ff375507..bb9a74e188 100644 --- a/Content.Server/Shuttles/Components/ArrivalsShuttleComponent.cs +++ b/Content.Shared/Shuttles/Components/ArrivalsShuttleComponent.cs @@ -1,7 +1,7 @@ -using Content.Server.Shuttles.Systems; +using Content.Shared.Shuttles.Systems; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; -namespace Content.Server.Shuttles.Components; +namespace Content.Shared.Shuttles.Components; [RegisterComponent, Access(typeof(ArrivalsSystem)), AutoGenerateComponentPause] public sealed partial class ArrivalsShuttleComponent : Component diff --git a/Content.Server/Shuttles/Components/ArrivalsSourceComponent.cs b/Content.Shared/Shuttles/Components/ArrivalsSourceComponent.cs similarity index 51% rename from Content.Server/Shuttles/Components/ArrivalsSourceComponent.cs rename to Content.Shared/Shuttles/Components/ArrivalsSourceComponent.cs index c7ed634b28..ab526db216 100644 --- a/Content.Server/Shuttles/Components/ArrivalsSourceComponent.cs +++ b/Content.Shared/Shuttles/Components/ArrivalsSourceComponent.cs @@ -1,12 +1,9 @@ -using Content.Server.Shuttles.Systems; +using Content.Shared.Shuttles.Systems; -namespace Content.Server.Shuttles.Components; +namespace Content.Shared.Shuttles.Components; /// /// Added to a designated arrivals station for players to spawn at, if enabled. /// [RegisterComponent, Access(typeof(ArrivalsSystem))] -public sealed partial class ArrivalsSourceComponent : Component -{ - -} +public sealed partial class ArrivalsSourceComponent : Component; diff --git a/Content.Server/Shuttles/Components/EmergencyShuttleComponent.cs b/Content.Shared/Shuttles/Components/EmergencyShuttleComponent.cs similarity index 72% rename from Content.Server/Shuttles/Components/EmergencyShuttleComponent.cs rename to Content.Shared/Shuttles/Components/EmergencyShuttleComponent.cs index aa3d2fbb7f..1dc0fe095d 100644 --- a/Content.Server/Shuttles/Components/EmergencyShuttleComponent.cs +++ b/Content.Shared/Shuttles/Components/EmergencyShuttleComponent.cs @@ -1,13 +1,8 @@ -using Content.Server.Shuttles.Systems; - -namespace Content.Server.Shuttles.Components; +namespace Content.Shared.Shuttles.Components; /// /// Added to station emergency shuttles by , /// for FTL event handlers /// [RegisterComponent] -public sealed partial class EmergencyShuttleComponent : Component -{ - -} +public sealed partial class EmergencyShuttleComponent : Component; diff --git a/Content.Server/Shuttles/Components/PendingClockInComponent.cs b/Content.Shared/Shuttles/Components/PendingClockInComponent.cs similarity index 51% rename from Content.Server/Shuttles/Components/PendingClockInComponent.cs rename to Content.Shared/Shuttles/Components/PendingClockInComponent.cs index 62cd1e8548..50dbfa8c2a 100644 --- a/Content.Server/Shuttles/Components/PendingClockInComponent.cs +++ b/Content.Shared/Shuttles/Components/PendingClockInComponent.cs @@ -1,10 +1,7 @@ -namespace Content.Server.Shuttles.Components; +namespace Content.Shared.Shuttles.Components; /// /// Added to arrivals latejoins until they have arrived at the station. /// [RegisterComponent] -public sealed partial class PendingClockInComponent : Component -{ - -} +public sealed partial class PendingClockInComponent : Component; diff --git a/Content.Shared/Shuttles/Components/ShuttleComponent.cs b/Content.Shared/Shuttles/Components/ShuttleComponent.cs new file mode 100644 index 0000000000..3d128bf505 --- /dev/null +++ b/Content.Shared/Shuttles/Components/ShuttleComponent.cs @@ -0,0 +1,81 @@ +using System.Numerics; + +namespace Content.Shared.Shuttles.Components; + +[RegisterComponent] +public sealed partial class ShuttleComponent : Component +{ + [ViewVariables] + public bool Enabled = true; + + [ViewVariables] + public Vector2[] CenterOfThrust = new Vector2[4]; + + /// + /// Thrust gets multiplied by this value if it's for braking. + /// + [DataField] + public const float BrakeCoefficient = 1.5f; + + /// + /// Maximum velocity. + /// + [DataField] + public float BaseMaxLinearVelocity = 60f; + + [DataField] + public const float MaxAngularVelocity = 4f; + + /// + /// The cached thrust available for each cardinal direction + /// + [ViewVariables] + public readonly float[] LinearThrust = new float[4]; + + /// + /// The thrusters contributing to each direction for impulse. + /// + // No touchy + public readonly List[] LinearThrusters = new List[] + { + new(), + new(), + new(), + new(), + }; + + /// + /// The thrusters contributing to the angular impulse of the shuttle. + /// + public readonly List AngularThrusters = new(); + + [ViewVariables] + public float AngularThrust = 0f; + + /// + /// A bitmask of all the directions we are considered thrusting. + /// + [ViewVariables] + public DirectionFlag ThrustDirections = DirectionFlag.None; + + /// + /// Base damping modifier applied to the shuttle's physics component when not in FTL. + /// + [DataField] + public float BodyModifier = 0.25f; + + /// + /// Final Damping Modifier for a shuttle. + /// This value is set to 0 during FTL. And to BodyModifier when not in FTL. + /// + [DataField] + public float DampingModifier; + + /// + /// Optional override for the FTL cooldown for this shuttle. + /// If not null, then the value will be used instead of the shuttle.cooldown CCVar. + /// + [DataField] + public TimeSpan? FTLCooldownOverride = null; +} + diff --git a/Content.Server/Shuttles/Components/StationArrivalsComponent.cs b/Content.Shared/Shuttles/Components/StationArrivalsComponent.cs similarity index 82% rename from Content.Server/Shuttles/Components/StationArrivalsComponent.cs rename to Content.Shared/Shuttles/Components/StationArrivalsComponent.cs index 1d469f9bff..6a5a6e1508 100644 --- a/Content.Server/Shuttles/Components/StationArrivalsComponent.cs +++ b/Content.Shared/Shuttles/Components/StationArrivalsComponent.cs @@ -1,7 +1,7 @@ -using Content.Server.Shuttles.Systems; +using Content.Shared.Shuttles.Systems; using Robust.Shared.Utility; -namespace Content.Server.Shuttles.Components; +namespace Content.Shared.Shuttles.Components; /// /// Added to a station that is available for arrivals shuttles. diff --git a/Content.Shared/Shuttles/Systems/ArrivalsSystem.cs b/Content.Shared/Shuttles/Systems/ArrivalsSystem.cs new file mode 100644 index 0000000000..e1b1750859 --- /dev/null +++ b/Content.Shared/Shuttles/Systems/ArrivalsSystem.cs @@ -0,0 +1,152 @@ +using Content.Shared.CCVar; +using Content.Shared.Damage.Components; +using Content.Shared.GameTicking; +using Content.Shared.Movement.Components; +using Content.Shared.Shuttles.Components; +using Content.Shared.Spawners.Components; +using Content.Shared.Spawners.EntitySystems; +using Content.Shared.Station.Systems; +using JetBrains.Annotations; +using Robust.Shared.Configuration; +using Robust.Shared.Map; +using Robust.Shared.Random; +using Robust.Shared.Timing; +namespace Content.Shared.Shuttles.Systems; + +/// +/// Handles the arrivals shuttle spawning system, and the arrivals shuttle. +/// +public abstract partial class ArrivalsSystem : EntitySystem +{ + [Dependency] protected IConfigurationManager Cfg = default!; + [Dependency] protected IGameTiming Timing = default!; + [Dependency] protected IRobustRandom Random = default!; + [Dependency] protected GameTicker Ticker = default!; + [Dependency] private StationSpawningSystem _stationSpawning = default!; + + /// + /// If enabled then spawns players on an alternate map so they can take a shuttle to the station. + /// + public bool Enabled { get; protected set; } + + /// + /// Flags if all players spawning at the departure terminal have godmode until they leave the terminal. + /// + public bool ArrivalsGodmode { get; private set; } + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnShuttleStartup); + + // Don't invoke immediately as it will get set in the natural course of things. + Enabled = Cfg.GetCVar(CCVars.ArrivalsShuttles); + ArrivalsGodmode = Cfg.GetCVar(CCVars.GodmodeArrivals); + + Cfg.OnValueChanged(CCVars.GodmodeArrivals, b => ArrivalsGodmode = b); + } + + [SubscribeLocalEvent(before: new []{typeof(SpawnPointSystem), typeof(ContainerSpawnPointSystem)})] + public void HandlePlayerSpawning(PlayerSpawningEvent ev) + { + if (ev.SpawnResult != null) + return; + + // We use arrivals as the default spawn so don't check for job prio. + + // Only works on latejoin even if enabled. + if (!Enabled || Ticker.RunLevel != GameRunLevel.InRound) + return; + + if (!HasComp(ev.Station)) + return; + + TryGetArrivals(out var arrivals); + + if (!TryComp(arrivals, out TransformComponent? arrivalsXform)) + return; + + var mapId = arrivalsXform.MapID; + + var points = EntityQueryEnumerator(); + var possiblePositions = new List(); + while (points.MoveNext(out _, out var spawnPoint, out var xform)) + { + if (spawnPoint.SpawnType != SpawnPointType.LateJoin || xform.MapID != mapId) + continue; + + possiblePositions.Add(xform.Coordinates); + } + + if (possiblePositions.Count <= 0) + return; + + var spawnLoc = Random.Pick(possiblePositions); + ev.SpawnResult = _stationSpawning.SpawnPlayerMob( + spawnLoc, + ev.Job, + ev.HumanoidCharacterProfile, + ev.Station); + + EnsureComp(ev.SpawnResult.Value); + EnsureComp(ev.SpawnResult.Value); + + // If you're forced to spawn, you're invincible until you leave wherever you were forced to spawn. + if (ArrivalsGodmode) + EnsureComp(ev.SpawnResult.Value); + } + + private void OnShuttleStartup(EntityUid uid, ArrivalsShuttleComponent component, ComponentStartup args) + { + EnsureComp(uid); + } + + [PublicAPI] + public bool TryGetArrivals(out EntityUid uid) + { + var arrivalsQuery = EntityQueryEnumerator(); + + while (arrivalsQuery.MoveNext(out uid, out _)) + { + return true; + } + + return false; + } + + /// + /// Check if an entity is on the arrivals grid. + /// + /// Entity to check. + /// True if the entity is on the arrivals grid. Returns false if not on arrivals, or there is no arrivals grid. + [PublicAPI] + public bool IsOnArrivals(Entity entity) + { + if (!Resolve(entity, ref entity.Comp)) + return false; + + if (!TryGetArrivals(out var arrivals)) + return false; + + var arrivalsGridUid = Transform(arrivals).GridUid; + if (!arrivalsGridUid.HasValue) + return false; + + return entity.Comp.GridUid == Transform(arrivals).GridUid; + } + + public TimeSpan? NextShuttleArrival() + { + var query = EntityQueryEnumerator(); + var time = TimeSpan.MaxValue; + while (query.MoveNext(out _, out var comp)) + { + if (comp.NextArrivalsTime < time) + time = comp.NextArrivalsTime; + } + + var duration = Timing.CurTime; + return (time < duration) ? null : time - duration; + } +} diff --git a/Content.Shared/Shuttles/Systems/SharedEmergencyShuttleSystem.cs b/Content.Shared/Shuttles/Systems/SharedEmergencyShuttleSystem.cs index 794f4d8ae3..5fbbd30d51 100644 --- a/Content.Shared/Shuttles/Systems/SharedEmergencyShuttleSystem.cs +++ b/Content.Shared/Shuttles/Systems/SharedEmergencyShuttleSystem.cs @@ -13,6 +13,11 @@ public abstract partial class SharedEmergencyShuttleSystem : EntitySystem private bool _emergencyEarlyLaunchAllowed; + /// + /// Has the emergency shuttle arrived? + /// + public bool EmergencyShuttleArrived { get; protected set; } + public override void Initialize() { base.Initialize(); @@ -33,4 +38,13 @@ public abstract partial class SharedEmergencyShuttleSystem : EntitySystem if (!args.Silent) Popup.PopupEntity(Loc.GetString("emergency-shuttle-console-no-early-launches"), ent, args.User); } + + /// + /// Attempts to get the EntityUid of the emergency shuttle + /// + public EntityUid? GetShuttle() + { + AllEntityQuery().MoveNext(out var shuttle, out _); + return shuttle; + } } diff --git a/Content.Server/Spawners/Components/ContainerSpawnPointComponent.cs b/Content.Shared/Spawners/Components/ContainerSpawnPointComponent.cs similarity index 91% rename from Content.Server/Spawners/Components/ContainerSpawnPointComponent.cs rename to Content.Shared/Spawners/Components/ContainerSpawnPointComponent.cs index 210904b567..09d44e2e30 100644 --- a/Content.Server/Spawners/Components/ContainerSpawnPointComponent.cs +++ b/Content.Shared/Spawners/Components/ContainerSpawnPointComponent.cs @@ -1,8 +1,8 @@ -using Content.Server.Spawners.EntitySystems; using Content.Shared.Roles; +using Content.Shared.Spawners.EntitySystems; using Robust.Shared.Prototypes; -namespace Content.Server.Spawners.Components; +namespace Content.Shared.Spawners.Components; /// /// A spawn point that spawns a player into a target container rather than simply spawning them at a position. diff --git a/Content.Server/Spawners/Components/ISpawnPoint.cs b/Content.Shared/Spawners/Components/ISpawnPoint.cs similarity index 62% rename from Content.Server/Spawners/Components/ISpawnPoint.cs rename to Content.Shared/Spawners/Components/ISpawnPoint.cs index e01841ef99..e44a36be3d 100644 --- a/Content.Server/Spawners/Components/ISpawnPoint.cs +++ b/Content.Shared/Spawners/Components/ISpawnPoint.cs @@ -1,4 +1,4 @@ -namespace Content.Server.Spawners.Components; +namespace Content.Shared.Spawners.Components; public interface ISpawnPoint { diff --git a/Content.Server/Spawners/Components/SpawnPointComponent.cs b/Content.Shared/Spawners/Components/SpawnPointComponent.cs similarity index 94% rename from Content.Server/Spawners/Components/SpawnPointComponent.cs rename to Content.Shared/Spawners/Components/SpawnPointComponent.cs index c48b7ec598..0bbebf81d2 100644 --- a/Content.Server/Spawners/Components/SpawnPointComponent.cs +++ b/Content.Shared/Spawners/Components/SpawnPointComponent.cs @@ -1,7 +1,7 @@ using Content.Shared.Roles; using Robust.Shared.Prototypes; -namespace Content.Server.Spawners.Components; +namespace Content.Shared.Spawners.Components; [RegisterComponent] public sealed partial class SpawnPointComponent : Component, ISpawnPoint diff --git a/Content.Server/Spawners/EntitySystems/ContainerSpawnPointSystem.cs b/Content.Shared/Spawners/EntitySystems/ContainerSpawnPointSystem.cs similarity index 88% rename from Content.Server/Spawners/EntitySystems/ContainerSpawnPointSystem.cs rename to Content.Shared/Spawners/EntitySystems/ContainerSpawnPointSystem.cs index 7af415147a..eee357f2c7 100644 --- a/Content.Server/Spawners/EntitySystems/ContainerSpawnPointSystem.cs +++ b/Content.Shared/Spawners/EntitySystems/ContainerSpawnPointSystem.cs @@ -1,27 +1,21 @@ -using Content.Server.GameTicking; -using Content.Server.Spawners.Components; -using Content.Server.Station.Systems; +using Content.Shared.GameTicking; using Content.Shared.Preferences; -using Robust.Server.Containers; +using Content.Shared.Spawners.Components; +using Content.Shared.Station.Systems; using Robust.Shared.Containers; using Robust.Shared.Random; -namespace Content.Server.Spawners.EntitySystems; +namespace Content.Shared.Spawners.EntitySystems; public sealed partial class ContainerSpawnPointSystem : EntitySystem { - [Dependency] private ContainerSystem _container = default!; + [Dependency] private SharedContainerSystem _container = default!; [Dependency] private GameTicker _gameTicker = default!; [Dependency] private IRobustRandom _random = default!; [Dependency] private StationSystem _station = default!; [Dependency] private StationSpawningSystem _stationSpawning = default!; - public override void Initialize() - { - base.Initialize(); - SubscribeLocalEvent(HandlePlayerSpawning, before: new []{ typeof(SpawnPointSystem) }); - } - + [SubscribeLocalEvent(before: new []{typeof(SpawnPointSystem)})] public void HandlePlayerSpawning(PlayerSpawningEvent args) { if (args.SpawnResult != null) diff --git a/Content.Server/Spawners/EntitySystems/SpawnPointSystem.cs b/Content.Shared/Spawners/EntitySystems/SpawnPointSystem.cs similarity index 89% rename from Content.Server/Spawners/EntitySystems/SpawnPointSystem.cs rename to Content.Shared/Spawners/EntitySystems/SpawnPointSystem.cs index bf5327cc5d..ce80e6cec8 100644 --- a/Content.Server/Spawners/EntitySystems/SpawnPointSystem.cs +++ b/Content.Shared/Spawners/EntitySystems/SpawnPointSystem.cs @@ -1,10 +1,10 @@ -using Content.Server.GameTicking; -using Content.Server.Spawners.Components; -using Content.Server.Station.Systems; +using Content.Shared.GameTicking; +using Content.Shared.Spawners.Components; +using Content.Shared.Station.Systems; using Robust.Shared.Map; using Robust.Shared.Random; -namespace Content.Server.Spawners.EntitySystems; +namespace Content.Shared.Spawners.EntitySystems; public sealed partial class SpawnPointSystem : EntitySystem { @@ -13,11 +13,7 @@ public sealed partial class SpawnPointSystem : EntitySystem [Dependency] private StationSystem _stationSystem = default!; [Dependency] private StationSpawningSystem _stationSpawning = default!; - public override void Initialize() - { - SubscribeLocalEvent(OnPlayerSpawning); - } - + [SubscribeLocalEvent] private void OnPlayerSpawning(PlayerSpawningEvent args) { if (args.SpawnResult != null) diff --git a/Content.Shared/Station/Components/BecomesStationComponent.cs b/Content.Shared/Station/Components/BecomesStationComponent.cs index cd6191ef38..dad5a1c197 100644 --- a/Content.Shared/Station/Components/BecomesStationComponent.cs +++ b/Content.Shared/Station/Components/BecomesStationComponent.cs @@ -7,7 +7,7 @@ namespace Content.Shared.Station.Components; /// Added to grids saved in maps to designate that they are the 'main station' grid. /// [RegisterComponent, NetworkedComponent, AutoGenerateComponentState] -[Access(typeof(SharedGameTicker))] +[Access(typeof(GameTicker))] public sealed partial class BecomesStationComponent : Component { /// diff --git a/Content.Shared/Station/Components/StationDataComponent.cs b/Content.Shared/Station/Components/StationDataComponent.cs index 97136f3ba8..296aaa9505 100644 --- a/Content.Shared/Station/Components/StationDataComponent.cs +++ b/Content.Shared/Station/Components/StationDataComponent.cs @@ -1,4 +1,5 @@ using Content.Shared.Roles; +using Content.Shared.Station.Systems; using Robust.Shared.GameStates; using Robust.Shared.Prototypes; @@ -8,7 +9,7 @@ namespace Content.Shared.Station.Components; /// Stores core information about a station, namely its config and associated grids. /// All station entities will have this component. /// -[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, Access(typeof(SharedStationSystem))] +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, Access(typeof(StationSystem))] public sealed partial class StationDataComponent : Component { /// diff --git a/Content.Server/Station/Components/StationEventEligibleComponent.cs b/Content.Shared/Station/Components/StationEventEligibleComponent.cs similarity index 72% rename from Content.Server/Station/Components/StationEventEligibleComponent.cs rename to Content.Shared/Station/Components/StationEventEligibleComponent.cs index 0a899446a0..22dbc632aa 100644 --- a/Content.Server/Station/Components/StationEventEligibleComponent.cs +++ b/Content.Shared/Station/Components/StationEventEligibleComponent.cs @@ -1,10 +1,7 @@ -namespace Content.Server.Station.Components; +namespace Content.Shared.Station.Components; /// /// This is used for event eligibility. /// [RegisterComponent] -public sealed partial class StationEventEligibleComponent : Component -{ - -} +public sealed partial class StationEventEligibleComponent : Component; diff --git a/Content.Server/Station/Components/StationJobsComponent.cs b/Content.Shared/Station/Components/StationJobsComponent.cs similarity index 96% rename from Content.Server/Station/Components/StationJobsComponent.cs rename to Content.Shared/Station/Components/StationJobsComponent.cs index 9addc8a016..6f2f7831c0 100644 --- a/Content.Server/Station/Components/StationJobsComponent.cs +++ b/Content.Shared/Station/Components/StationJobsComponent.cs @@ -1,11 +1,10 @@ -using System.Linq; -using Content.Server.Station.Systems; using Content.Shared.Roles; +using Content.Shared.Station.Systems; using JetBrains.Annotations; using Robust.Shared.Network; using Robust.Shared.Prototypes; -namespace Content.Server.Station.Components; +namespace Content.Shared.Station.Components; /// /// Stores information about a station's job selection. diff --git a/Content.Server/Station/Components/StationSpawningComponent.cs b/Content.Shared/Station/Components/StationSpawningComponent.cs similarity index 71% rename from Content.Server/Station/Components/StationSpawningComponent.cs rename to Content.Shared/Station/Components/StationSpawningComponent.cs index 81e68d7684..9a925b8dae 100644 --- a/Content.Server/Station/Components/StationSpawningComponent.cs +++ b/Content.Shared/Station/Components/StationSpawningComponent.cs @@ -1,11 +1,9 @@ -using Content.Server.Station.Systems; +using Content.Shared.Station.Systems; -namespace Content.Server.Station.Components; +namespace Content.Shared.Station.Components; /// /// Controls spawning on the given station, tracking spawners present on it. /// [RegisterComponent, Access(typeof(StationSpawningSystem))] -public sealed partial class StationSpawningComponent : Component -{ -} +public sealed partial class StationSpawningComponent : Component; diff --git a/Content.Shared/Station/Components/StationTrackerComponent.cs b/Content.Shared/Station/Components/StationTrackerComponent.cs index 6272b28de4..05bff975c8 100644 --- a/Content.Shared/Station/Components/StationTrackerComponent.cs +++ b/Content.Shared/Station/Components/StationTrackerComponent.cs @@ -1,3 +1,4 @@ +using Content.Shared.Station.Systems; using Robust.Shared.GameStates; namespace Content.Shared.Station.Components; @@ -6,7 +7,7 @@ namespace Content.Shared.Station.Components; /// Component that tracks which station an entity is currently on. /// Mainly used for UI purposes on the client to easily get station-specific data like alert levels. /// -[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, Access(typeof(SharedStationSystem))] +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, Access(typeof(StationSystem))] public sealed partial class StationTrackerComponent : Component { /// diff --git a/Content.Shared/Station/FlingTowardStationSystem.cs b/Content.Shared/Station/FlingTowardStationSystem.cs index 08863b848f..5ce48ab2b2 100644 --- a/Content.Shared/Station/FlingTowardStationSystem.cs +++ b/Content.Shared/Station/FlingTowardStationSystem.cs @@ -3,6 +3,7 @@ using Robust.Shared.Physics.Systems; using Robust.Shared.Random; using Robust.Shared.Physics.Components; using Content.Shared.Random.Helpers; +using Content.Shared.Station.Systems; using Robust.Shared.Timing; namespace Content.Shared.Station; @@ -10,7 +11,7 @@ namespace Content.Shared.Station; public sealed partial class FlingTowardStationSystem : EntitySystem { [Dependency] private SharedPhysicsSystem _physics = default!; - [Dependency] private SharedStationSystem _station = default!; + [Dependency] private StationSystem _station = default!; [Dependency] private SharedTransformSystem _transform = default!; [Dependency] private IGameTiming _timing = default!; @@ -23,7 +24,7 @@ public sealed partial class FlingTowardStationSystem : EntitySystem // fetch the station's grid var random = SharedRandomExtensions.PredictedRandom(_timing, GetNetEntity(ent.Owner)); var station = random.Pick(_station.GetStations()); - var grid = _station.GetLargestGrid(station); + var grid = _station.GetLargestGrid(station.AsNullable()); if (grid == null) return; diff --git a/Content.Shared/Station/SharedStationSpawningSystem.cs b/Content.Shared/Station/SharedStationSpawningSystem.cs deleted file mode 100644 index fea37ac85f..0000000000 --- a/Content.Shared/Station/SharedStationSpawningSystem.cs +++ /dev/null @@ -1,199 +0,0 @@ -using System.Linq; -using Content.Shared.Hands.Components; -using Content.Shared.Hands.EntitySystems; -using Content.Shared.Inventory; -using Content.Shared.Preferences.Loadouts; -using Content.Shared.Roles; -using Content.Shared.Storage; -using Content.Shared.Storage.EntitySystems; -using Robust.Shared.Prototypes; -using Robust.Shared.Random; - -namespace Content.Shared.Station; - -public abstract partial class SharedStationSpawningSystem : EntitySystem -{ - [Dependency] private IRobustRandom _random = default!; - [Dependency] protected InventorySystem InventorySystem = default!; - [Dependency] private SharedHandsSystem _handsSystem = default!; - [Dependency] private MetaDataSystem _metadata = default!; - [Dependency] private SharedStorageSystem _storage = default!; - [Dependency] private SharedTransformSystem _xformSystem = default!; - - [Dependency] private EntityQuery _handsQuery = default!; - [Dependency] private EntityQuery _inventoryQuery = default!; - [Dependency] private EntityQuery _storageQuery = default!; - [Dependency] private EntityQuery _xformQuery = default!; - - /// - /// Equips the data from a `RoleLoadout` onto an entity. - /// - public void EquipRoleLoadout(EntityUid entity, RoleLoadout loadout, RoleLoadoutPrototype roleProto) - { - // Order loadout selections by the order they appear on the prototype. - foreach (var group in loadout.SelectedLoadouts.OrderBy(x => roleProto.Groups.FindIndex(e => e == x.Key))) - { - foreach (var items in group.Value) - { - if (!ProtoMan.TryIndex(items.Prototype, out var loadoutProto)) - { - Log.Error($"Unable to find loadout prototype for {items.Prototype}"); - continue; - } - - EquipStartingGear(entity, loadoutProto, raiseEvent: false); - } - } - - EquipRoleName(entity, loadout, roleProto); - } - - /// - /// Applies the role's name as applicable to the entity. - /// - public void EquipRoleName(EntityUid entity, RoleLoadout loadout, RoleLoadoutPrototype roleProto) - { - string? name = null; - - if (roleProto.CanCustomizeName) - { - name = loadout.EntityName; - } - - if (string.IsNullOrEmpty(name) && ProtoMan.Resolve(roleProto.NameDataset, out var nameData)) - { - name = Loc.GetString(_random.Pick(nameData.Values)); - } - - if (!string.IsNullOrEmpty(name)) - { - _metadata.SetEntityName(entity, name); - } - } - - public void EquipStartingGear(EntityUid entity, LoadoutPrototype loadout, bool raiseEvent = true) - { - EquipStartingGear(entity, loadout.StartingGear, raiseEvent); - EquipStartingGear(entity, (IEquipmentLoadout) loadout, raiseEvent); - } - - /// - /// - /// - public void EquipStartingGear(EntityUid entity, ProtoId? startingGear, bool raiseEvent = true) - { - ProtoMan.Resolve(startingGear, out var gearProto); - EquipStartingGear(entity, gearProto, raiseEvent); - } - - /// - /// - /// - public void EquipStartingGear(EntityUid entity, StartingGearPrototype? startingGear, bool raiseEvent = true) - { - EquipStartingGear(entity, (IEquipmentLoadout?) startingGear, raiseEvent); - } - - /// - /// Equips starting gear onto the given entity. - /// - /// Entity to load out. - /// Starting gear to use. - /// Should we raise the event for equipped. Set to false if you will call this manually - public void EquipStartingGear(EntityUid entity, IEquipmentLoadout? startingGear, bool raiseEvent = true) - { - if (startingGear == null) - return; - - var xform = _xformQuery.GetComponent(entity); - - if (InventorySystem.TryGetSlots(entity, out var slotDefinitions)) - { - foreach (var slot in slotDefinitions) - { - var equipmentStr = startingGear.GetGear(slot.Name); - if (!string.IsNullOrEmpty(equipmentStr)) - { - var equipmentEntity = Spawn(equipmentStr, xform.Coordinates); - InventorySystem.TryEquip(entity, equipmentEntity, slot.Name, silent: true, force: true); - } - } - } - - if (_handsQuery.TryComp(entity, out var handsComponent)) - { - var inhand = startingGear.Inhand; - var coords = xform.Coordinates; - foreach (var prototype in inhand) - { - var inhandEntity = Spawn(prototype, coords); - - if (_handsSystem.TryGetEmptyHand((entity, handsComponent), out var emptyHand)) - { - _handsSystem.TryPickup(entity, inhandEntity, emptyHand, checkActionBlocker: false, handsComp: handsComponent); - } - } - } - - if (startingGear.Storage.Count > 0) - { - var coords = _xformSystem.GetMapCoordinates(entity); - _inventoryQuery.TryComp(entity, out var inventoryComp); - - foreach (var (slotName, entProtos) in startingGear.Storage) - { - if (entProtos == null || entProtos.Count == 0) - continue; - - if (inventoryComp != null && - InventorySystem.TryGetSlotEntity(entity, slotName, out var slotEnt, inventoryComponent: inventoryComp) && - _storageQuery.TryComp(slotEnt, out var storage)) - { - - foreach (var entProto in entProtos) - { - var spawnedEntity = Spawn(entProto, coords); - - _storage.Insert(slotEnt.Value, spawnedEntity, out _, storageComp: storage, playSound: false); - } - } - } - } - - if (raiseEvent) - { - var ev = new StartingGearEquippedEvent(entity); - RaiseLocalEvent(entity, ref ev); - } - } - - /// - /// Gets all the gear for a given slot when passed a loadout. - /// - /// The loadout to look through. - /// The slot that you want the clothing for. - /// - /// If there is a value for the given slot, it will return the proto id for that slot. - /// If nothing was found, will return null - /// - public string? GetGearForSlot(RoleLoadout? loadout, string slot) - { - if (loadout == null) - return null; - - foreach (var group in loadout.SelectedLoadouts) - { - foreach (var items in group.Value) - { - if (!ProtoMan.Resolve(items.Prototype, out var loadoutPrototype)) - return null; - - var gear = ((IEquipmentLoadout) loadoutPrototype).GetGear(slot); - if (gear != string.Empty) - return gear; - } - } - - return null; - } -} diff --git a/Content.Shared/Station/Systems/StationJobsSystem.cs b/Content.Shared/Station/Systems/StationJobsSystem.cs new file mode 100644 index 0000000000..1beea35576 --- /dev/null +++ b/Content.Shared/Station/Systems/StationJobsSystem.cs @@ -0,0 +1,6 @@ +namespace Content.Shared.Station.Systems; + +/// +/// Handles the assignment and tracking of Station jobs and their associated role entities. +/// +public abstract partial class StationJobsSystem : EntitySystem; diff --git a/Content.Server/Station/Systems/StationSpawningSystem.cs b/Content.Shared/Station/Systems/StationSpawningSystem.cs similarity index 53% rename from Content.Server/Station/Systems/StationSpawningSystem.cs rename to Content.Shared/Station/Systems/StationSpawningSystem.cs index 2e04d6d142..3a1a970fc0 100644 --- a/Content.Server/Station/Systems/StationSpawningSystem.cs +++ b/Content.Shared/Station/Systems/StationSpawningSystem.cs @@ -1,48 +1,234 @@ -using Content.Server.Access.Systems; -using Content.Server.Humanoid; -using Content.Server.Mind; -using Content.Server.PDA; -using Content.Server.Station.Components; +using System.Linq; using Content.Shared.Access.Components; using Content.Shared.Access.Systems; using Content.Shared.Body; using Content.Shared.CCVar; using Content.Shared.Clothing; using Content.Shared.DetailExaminable; +using Content.Shared.Hands.Components; +using Content.Shared.Hands.EntitySystems; using Content.Shared.Humanoid; using Content.Shared.Humanoid.Prototypes; using Content.Shared.IdentityManagement; +using Content.Shared.Inventory; +using Content.Shared.Mind; using Content.Shared.PDA; using Content.Shared.Preferences; using Content.Shared.Preferences.Loadouts; using Content.Shared.Roles; -using Content.Shared.Station; +using Content.Shared.Station.Components; +using Content.Shared.Storage; +using Content.Shared.Storage.EntitySystems; using JetBrains.Annotations; using Robust.Shared.Configuration; using Robust.Shared.Map; using Robust.Shared.Player; using Robust.Shared.Prototypes; +using Robust.Shared.Random; using Robust.Shared.Utility; -namespace Content.Server.Station.Systems; +namespace Content.Shared.Station.Systems; -/// -/// Manages spawning into the game, tracking available spawn points. -/// Also provides helpers for spawning in the player's mob. -/// -[PublicAPI] -public sealed partial class StationSpawningSystem : SharedStationSpawningSystem +public sealed partial class StationSpawningSystem : EntitySystem { - [Dependency] private SharedAccessSystem _accessSystem = default!; - [Dependency] private ActorSystem _actors = default!; - [Dependency] private IdCardSystem _cardSystem = default!; [Dependency] private IConfigurationManager _configurationManager = default!; + [Dependency] private IRobustRandom _random = default!; + [Dependency] private ActorSystem _actors = default!; [Dependency] private HumanoidProfileSystem _humanoidProfile = default!; - [Dependency] private SharedVisualBodySystem _visualBody = default!; [Dependency] private IdentitySystem _identity = default!; - [Dependency] private MetaDataSystem _metaSystem = default!; - [Dependency] private PdaSystem _pdaSystem = default!; - [Dependency] private MindSystem _mindSystem = default!; + [Dependency] private InventorySystem _inventory = default!; + [Dependency] private MetaDataSystem _metadata = default!; + [Dependency] private SharedHandsSystem _handsSystem = default!; + [Dependency] private SharedMindSystem _mindSystem = default!; + [Dependency] private SharedStorageSystem _storage = default!; + [Dependency] private SharedTransformSystem _xformSystem = default!; + [Dependency] private SharedVisualBodySystem _visualBody = default!; + [Dependency] private SharedAccessSystem _accessSystem = default!; + [Dependency] private SharedIdCardSystem _cardSystem = default!; + [Dependency] private SharedPdaSystem _pdaSystem = default!; + + [Dependency] private EntityQuery _handsQuery; + [Dependency] private EntityQuery _inventoryQuery; + [Dependency] private EntityQuery _storageQuery; + [Dependency] private EntityQuery _xformQuery; + + /// + /// Equips the data from a `RoleLoadout` onto an entity. + /// + [PublicAPI] + public void EquipRoleLoadout(EntityUid entity, RoleLoadout loadout, RoleLoadoutPrototype roleProto) + { + // Order loadout selections by the order they appear on the prototype. + foreach (var group in loadout.SelectedLoadouts.OrderBy(x => roleProto.Groups.FindIndex(e => e == x.Key))) + { + foreach (var items in group.Value) + { + if (!ProtoMan.TryIndex(items.Prototype, out var loadoutProto)) + { + Log.Error($"Unable to find loadout prototype for {items.Prototype}"); + continue; + } + + EquipStartingGear(entity, loadoutProto, raiseEvent: false); + } + } + + EquipRoleName(entity, loadout, roleProto); + } + + /// + /// Applies the role's name as applicable to the entity. + /// + [PublicAPI] + public void EquipRoleName(EntityUid entity, RoleLoadout loadout, RoleLoadoutPrototype roleProto) + { + string? name = null; + + if (roleProto.CanCustomizeName) + { + name = loadout.EntityName; + } + + if (string.IsNullOrEmpty(name) && ProtoMan.Resolve(roleProto.NameDataset, out var nameData)) + { + name = Loc.GetString(_random.Pick(nameData.Values)); + } + + if (!string.IsNullOrEmpty(name)) + { + _metadata.SetEntityName(entity, name); + } + } + + public void EquipStartingGear(EntityUid entity, LoadoutPrototype loadout, bool raiseEvent = true) + { + EquipStartingGear(entity, loadout.StartingGear, raiseEvent); + EquipStartingGear(entity, (IEquipmentLoadout) loadout, raiseEvent); + } + + /// + /// + /// + [PublicAPI] + public void EquipStartingGear(EntityUid entity, ProtoId? startingGear, bool raiseEvent = true) + { + ProtoMan.Resolve(startingGear, out var gearProto); + EquipStartingGear(entity, gearProto, raiseEvent); + } + + /// + /// + /// + [PublicAPI] + public void EquipStartingGear(EntityUid entity, StartingGearPrototype? startingGear, bool raiseEvent = true) + { + EquipStartingGear(entity, (IEquipmentLoadout?) startingGear, raiseEvent); + } + + /// + /// Equips starting gear onto the given entity. + /// + /// Entity to load out. + /// Starting gear to use. + /// Should we raise the event for equipped. Set to false if you will call this manually + [PublicAPI] + public void EquipStartingGear(EntityUid entity, IEquipmentLoadout? startingGear, bool raiseEvent = true) + { + if (startingGear == null) + return; + + var xform = _xformQuery.GetComponent(entity); + + if (_inventory.TryGetSlots(entity, out var slotDefinitions)) + { + foreach (var slot in slotDefinitions) + { + var equipmentStr = startingGear.GetGear(slot.Name); + if (!string.IsNullOrEmpty(equipmentStr)) + { + var equipmentEntity = Spawn(equipmentStr, xform.Coordinates); + _inventory.TryEquip(entity, equipmentEntity, slot.Name, silent: true, force: true); + } + } + } + + if (_handsQuery.TryComp(entity, out var handsComponent)) + { + var inhand = startingGear.Inhand; + var coords = xform.Coordinates; + foreach (var prototype in inhand) + { + var inhandEntity = Spawn(prototype, coords); + + if (_handsSystem.TryGetEmptyHand((entity, handsComponent), out var emptyHand)) + { + _handsSystem.TryPickup(entity, inhandEntity, emptyHand, checkActionBlocker: false, handsComp: handsComponent); + } + } + } + + if (startingGear.Storage.Count > 0) + { + var coords = _xformSystem.GetMapCoordinates(entity); + _inventoryQuery.TryComp(entity, out var inventoryComp); + + foreach (var (slotName, entProtos) in startingGear.Storage) + { + if (entProtos.Count == 0) + continue; + + if (inventoryComp != null && + _inventory.TryGetSlotEntity(entity, slotName, out var slotEnt, inventoryComponent: inventoryComp) && + _storageQuery.TryComp(slotEnt, out var storage)) + { + + foreach (var entProto in entProtos) + { + var spawnedEntity = Spawn(entProto, coords); + + _storage.Insert(slotEnt.Value, spawnedEntity, out _, storageComp: storage, playSound: false); + } + } + } + } + + if (raiseEvent) + { + var ev = new StartingGearEquippedEvent(entity); + RaiseLocalEvent(entity, ref ev); + } + } + + /// + /// Gets all the gear for a given slot when passed a loadout. + /// + /// The loadout to look through. + /// The slot that you want the clothing for. + /// + /// If there is a value for the given slot, it will return the proto id for that slot. + /// If nothing was found, will return null + /// + [PublicAPI] + public string? GetGearForSlot(RoleLoadout? loadout, string slot) + { + if (loadout == null) + return null; + + foreach (var group in loadout.SelectedLoadouts) + { + foreach (var items in group.Value) + { + if (!ProtoMan.Resolve(items.Prototype, out var loadoutPrototype)) + return null; + + var gear = ((IEquipmentLoadout) loadoutPrototype).GetGear(slot); + if (gear != string.Empty) + return gear; + } + } + + return null; + } /// /// Attempts to spawn a player character onto the given station. @@ -69,6 +255,48 @@ public sealed partial class StationSpawningSystem : SharedStationSpawningSystem return ev.SpawnResult; } + /// + /// Sets the ID card and PDA name, job, and access data. + /// + /// Entity to load out. + /// Character name to use for the ID. + /// Job prototype to use for the PDA and ID. + /// The station this player is being spawned on. + [PublicAPI] + public void SetPdaAndIdCardData(EntityUid entity, + string characterName, + JobPrototype jobPrototype, + EntityUid? station) + { + if (!_inventory.TryGetSlotEntity(entity, "id", out var idUid)) + return; + + var cardId = idUid.Value; + if (TryComp(idUid, out var pdaComponent) && pdaComponent.ContainedId != null) + cardId = pdaComponent.ContainedId.Value; + + if (!TryComp(cardId, out var card)) + return; + + _cardSystem.TryChangeFullName(cardId, characterName, card); + _cardSystem.TryChangeJobTitle(cardId, jobPrototype.LocalizedName, card); + + if (ProtoMan.Resolve(jobPrototype.Icon, out var jobIcon)) + _cardSystem.TryChangeJobIcon(cardId, jobIcon, card); + + var extendedAccess = false; + if (station != null) + { + var data = Comp(station.Value); + extendedAccess = data.ExtendedAccess; + } + + _accessSystem.SetAccessToJob(cardId, jobPrototype, extendedAccess); + + if (pdaComponent != null) + _pdaSystem.SetOwner(idUid.Value, pdaComponent, entity, characterName); + } + //TODO: Figure out if everything in the player spawning region belongs somewhere else. #region Player spawning helpers @@ -136,7 +364,7 @@ public sealed partial class StationSpawningSystem : SharedStationSpawningSystem { _visualBody.ApplyProfileTo(entity.Value, profile); _humanoidProfile.ApplyProfileTo(entity.Value, profile); - _metaSystem.SetEntityName(entity.Value, profile.Name); + _metadata.SetEntityName(entity.Value, profile.Name); if (profile.FlavorText != "" && _configurationManager.GetCVar(CCVars.FlavorText)) { @@ -178,46 +406,6 @@ public sealed partial class StationSpawningSystem : SharedStationSpawningSystem jobSpecial.AfterEquip(entity); } } - - /// - /// Sets the ID card and PDA name, job, and access data. - /// - /// Entity to load out. - /// Character name to use for the ID. - /// Job prototype to use for the PDA and ID. - /// The station this player is being spawned on. - public void SetPdaAndIdCardData(EntityUid entity, string characterName, JobPrototype jobPrototype, EntityUid? station) - { - if (!InventorySystem.TryGetSlotEntity(entity, "id", out var idUid)) - return; - - var cardId = idUid.Value; - if (TryComp(idUid, out var pdaComponent) && pdaComponent.ContainedId != null) - cardId = pdaComponent.ContainedId.Value; - - if (!TryComp(cardId, out var card)) - return; - - _cardSystem.TryChangeFullName(cardId, characterName, card); - _cardSystem.TryChangeJobTitle(cardId, jobPrototype.LocalizedName, card); - - if (ProtoMan.Resolve(jobPrototype.Icon, out var jobIcon)) - _cardSystem.TryChangeJobIcon(cardId, jobIcon, card); - - var extendedAccess = false; - if (station != null) - { - var data = Comp(station.Value); - extendedAccess = data.ExtendedAccess; - } - - _accessSystem.SetAccessToJob(cardId, jobPrototype, extendedAccess); - - if (pdaComponent != null) - _pdaSystem.SetOwner(idUid.Value, pdaComponent, entity, characterName); - } - - #endregion Player spawning helpers } @@ -227,7 +415,6 @@ public sealed partial class StationSpawningSystem : SharedStationSpawningSystem /// You should not make this event's success rely on random chance. /// This event is designed to use ordered handling. You probably want SpawnPointSystem to be the last handler. /// -[PublicAPI] public sealed class PlayerSpawningEvent : EntityEventArgs { /// diff --git a/Content.Shared/Station/Systems/StationSystem.Event.cs b/Content.Shared/Station/Systems/StationSystem.Event.cs new file mode 100644 index 0000000000..247ce407a5 --- /dev/null +++ b/Content.Shared/Station/Systems/StationSystem.Event.cs @@ -0,0 +1,136 @@ +using System.Diagnostics.CodeAnalysis; +using Content.Shared.Station.Components; +using JetBrains.Annotations; +using Robust.Shared.Collections; +using Robust.Shared.Map; +using Robust.Shared.Map.Components; + +namespace Content.Shared.Station.Systems; + +public abstract partial class StationSystem +{ + /// + /// Utility function for finding a random event-eligible station entity + /// TODO: Make calls of this use the below method with T = StationEventEligibleComponent and make this a lookup for NO T versions of stations!!! + /// + [PublicAPI] + [Obsolete($"Use TryGetRandomStation instead using {nameof(StationEventEligibleComponent)}!")] + public bool TryGetRandomStation([NotNullWhen(true)] out Entity? station, Func? filter = null) + { + var stations = new ValueList>(Count()); + + filter ??= _ => true; + var query = AllEntityQuery(); + + while (query.MoveNext(out var uid, out _, out var data)) + { + if (!filter(uid)) + continue; + + stations.Add((uid, data)); + } + + if (stations.Count == 0) + { + station = null; + return false; + } + + // TODO: Engine PR. + station = stations[Random.Next(stations.Count)]; + return true; + } + + /// + /// Tries to get a random station with a given component. + /// + /// Station we are returning. + /// Optional filter. + /// Component we are querying for. + /// Station Entity if it exists + [PublicAPI] + public bool TryGetRandomStation([NotNullWhen(true)] out Entity? station, Func? filter = null) where T : IComponent + { + var stations = new ValueList>(Count()); + + filter ??= _ => true; + var query = AllEntityQuery(); + + while (query.MoveNext(out var uid, out var comp, out var data)) + { + if (!filter(uid)) + continue; + + stations.Add((uid, data, comp)); + } + + if (stations.Count == 0) + { + station = null; + return false; + } + + // TODO: Engine PR. + station = stations[Random.Next(stations.Count)]; + return true; + } + + [PublicAPI] + public bool TryFindRandomTile(out Vector2i tile, + [NotNullWhen(true)] out Entity? targetStation, + [NotNullWhen(true)] out Entity? targetGrid, + out EntityCoordinates targetCoords) + { + tile = default; + targetStation = null; + targetGrid = null; + targetCoords = EntityCoordinates.Invalid; + if (TryGetRandomStation(out targetStation)) + { + return TryFindRandomTileOnStation(targetStation.Value, + out tile, + out targetGrid, + out targetCoords); + } + + return false; + } + + /// + /// Returns a random tile on a random station with the given T component. + /// + /// Tile returned. + /// Station returned + /// Station grid with the tile. + /// Coordinates of the tile. + /// Component we query for the station! + /// True if all of the above were found + [PublicAPI] + public bool TryFindRandomTile(out Vector2i tile, + [NotNullWhen(true)] out Entity? targetStation, + [NotNullWhen(true)] out Entity? targetGrid, + out EntityCoordinates targetCoords) where T : IComponent + { + tile = default; + targetStation = null; + targetGrid = null; + targetCoords = EntityCoordinates.Invalid; + return TryGetRandomStation(out targetStation) && TryFindRandomTileOnStation(targetStation.Value, + out tile, + out targetGrid, + out targetCoords); + } + + [PublicAPI] + public virtual bool TryFindRandomTileOnStation(Entity station, + out Vector2i tile, + [NotNullWhen(true)] out Entity? targetGrid, + out EntityCoordinates targetCoords, + int numAttempts = 10) + { + tile = default; + targetCoords = EntityCoordinates.Invalid; + targetGrid = null; + return false; + } +} diff --git a/Content.Shared/Station/SharedStationSystem.Tracker.cs b/Content.Shared/Station/Systems/StationSystem.Tracker.cs similarity index 76% rename from Content.Shared/Station/SharedStationSystem.Tracker.cs rename to Content.Shared/Station/Systems/StationSystem.Tracker.cs index 41129da13f..ce8a27013e 100644 --- a/Content.Shared/Station/SharedStationSystem.Tracker.cs +++ b/Content.Shared/Station/Systems/StationSystem.Tracker.cs @@ -2,34 +2,30 @@ using Content.Shared.Station.Components; using JetBrains.Annotations; using Robust.Shared.Map; -namespace Content.Shared.Station; +namespace Content.Shared.Station.Systems; -public abstract partial class SharedStationSystem +public abstract partial class StationSystem { - private void InitializeTracker() - { - SubscribeLocalEvent(OnTrackerMapInit); - SubscribeLocalEvent(OnTrackerRemove); - SubscribeLocalEvent(OnTrackerGridChanged); - SubscribeLocalEvent(OnMetaFlagRemoveAttempt); - } - + [SubscribeLocalEvent] private void OnTrackerMapInit(Entity ent, ref MapInitEvent args) { - _meta.AddFlag(ent, MetaDataFlags.ExtraTransformEvents); + Meta.AddFlag(ent, MetaDataFlags.ExtraTransformEvents); UpdateStationTracker(ent.AsNullable()); } + [SubscribeLocalEvent] private void OnTrackerRemove(Entity ent, ref ComponentRemove args) { - _meta.RemoveFlag(ent, MetaDataFlags.ExtraTransformEvents); + Meta.RemoveFlag(ent, MetaDataFlags.ExtraTransformEvents); } + [SubscribeLocalEvent] private void OnTrackerGridChanged(Entity ent, ref GridUidChangedEvent args) { UpdateStationTracker((ent, ent.Comp, args.Transform)); } + [SubscribeLocalEvent] private void OnMetaFlagRemoveAttempt(Entity ent, ref MetaFlagRemoveAttemptEvent args) { if ((args.ToRemove & MetaDataFlags.ExtraTransformEvents) != 0 && diff --git a/Content.Shared/Station/SharedStationSystem.cs b/Content.Shared/Station/Systems/StationSystem.cs similarity index 83% rename from Content.Shared/Station/SharedStationSystem.cs rename to Content.Shared/Station/Systems/StationSystem.cs index 8de80602ff..c73fa667ed 100644 --- a/Content.Shared/Station/SharedStationSystem.cs +++ b/Content.Shared/Station/Systems/StationSystem.cs @@ -5,30 +5,24 @@ using Robust.Shared.Collections; using Robust.Shared.Map; using Robust.Shared.Map.Components; using Robust.Shared.Player; +using Robust.Shared.Random; -namespace Content.Shared.Station; +namespace Content.Shared.Station.Systems; -public abstract partial class SharedStationSystem : EntitySystem +public abstract partial class StationSystem : EntitySystem { - [Dependency] private SharedMapSystem _map = default!; - [Dependency] private MetaDataSystem _meta = default!; - [Dependency] private SharedTransformSystem _transform = default!; + [Dependency] protected IRobustRandom Random = default!; [Dependency] private ISharedPlayerManager _player = default!; + [Dependency] protected MetaDataSystem Meta = default!; + [Dependency] protected SharedMapSystem Map = default!; + [Dependency] private SharedTransformSystem _transform = default!; [Dependency] private EntityQuery _stationMemberQuery; - [Dependency] private EntityQuery _gridQuery; + [Dependency] protected EntityQuery GridQuery; private ValueList _mapIds; private ValueList<(Box2Rotated Bounds, MapId MapId)> _gridBounds; - /// - public override void Initialize() - { - base.Initialize(); - - InitializeTracker(); - } - /// /// Gets the largest member grid from a station. /// @@ -67,7 +61,7 @@ public abstract partial class SharedStationSystem : EntitySystem if (!TryComp(gridUid, out var grid)) continue; - count += _map.GetAllTiles(gridUid, grid).Count(); + count += Map.GetAllTiles(gridUid, grid).Count(); } return count; @@ -123,13 +117,13 @@ public abstract partial class SharedStationSystem : EntitySystem return CompOrNull(xform.GridUid)?.Station; } - public List GetStations() + public List> GetStations() { - var stations = new List(); + var stations = new List>(); var query = EntityQueryEnumerator(); - while (query.MoveNext(out var uid, out _)) + while (query.MoveNext(out var uid, out var data)) { - stations.Add(uid); + stations.Add((uid, data)); } return stations; @@ -202,6 +196,40 @@ public abstract partial class SharedStationSystem : EntitySystem return Filter.Empty(); } + /// + /// Retrieves a filter for everything on the same map as a given station. + /// + public Filter GetOnMap(StationDataComponent dataComponent) + { + var filter = Filter.Empty(); + _mapIds.Clear(); + + // First collect all valid map IDs where station grids exist + foreach (var gridUid in dataComponent.Grids) + { + if (!TryComp(gridUid, out TransformComponent? xform)) + continue; + + var mapId = xform.MapID; + if (!_mapIds.Contains(mapId)) + _mapIds.Add(mapId); + } + + foreach (var session in Filter.GetAllPlayers(_player)) + { + var entity = session.AttachedEntity; + if (entity == null || !TryComp(entity, out TransformComponent? xform)) + continue; + + if (!_mapIds.Contains(xform.MapID)) + continue; + + filter.AddPlayer(session); + } + + return filter; + } + /// /// Retrieves a filter for everything in a particular station or near its member grids. /// @@ -226,7 +254,7 @@ public abstract partial class SharedStationSystem : EntitySystem foreach (var gridUid in dataComponent.Grids) { - if (!_gridQuery.TryComp(gridUid, out var grid) || + if (!GridQuery.TryComp(gridUid, out var grid) || !TryComp(gridUid, out TransformComponent? gridXform)) { continue; @@ -283,7 +311,8 @@ public abstract partial class SharedStationSystem : EntitySystem return filter; } - /// Returns true if a entity's parent is the station, and false if the entity is not on the station + /// + /// Returns true if an entity's parent is the station, and false if the entity is not on the station /// /// The entity to check if is on a grid /// If true, only the largest grid of each station will count, so if the entity is on diff --git a/Content.Shared/StationRecords/Systems/SharedGeneralStationRecordConsoleSystem.cs b/Content.Shared/StationRecords/Systems/SharedGeneralStationRecordConsoleSystem.cs index 8177045c3f..2ad0fd9665 100644 --- a/Content.Shared/StationRecords/Systems/SharedGeneralStationRecordConsoleSystem.cs +++ b/Content.Shared/StationRecords/Systems/SharedGeneralStationRecordConsoleSystem.cs @@ -1,4 +1,4 @@ -using Content.Shared.Station; +using Content.Shared.Station.Systems; using Content.Shared.StationRecords.Components; using Content.Shared.StationRecords.Events; @@ -6,7 +6,7 @@ namespace Content.Shared.StationRecords.Systems; public abstract partial class SharedGeneralStationRecordConsoleSystem : EntitySystem { - [Dependency] protected SharedStationSystem StationSys = default!; + [Dependency] protected StationSystem StationSys = default!; [Dependency] protected StationRecordsSystem StationRecordsSys = default!; [Dependency] protected EntityQuery RecordsQuery = default!; diff --git a/Content.Shared/Trigger/Systems/AlertLevelChangeOnTriggerSystem.cs b/Content.Shared/Trigger/Systems/AlertLevelChangeOnTriggerSystem.cs index 55125edde9..1499e679dd 100644 --- a/Content.Shared/Trigger/Systems/AlertLevelChangeOnTriggerSystem.cs +++ b/Content.Shared/Trigger/Systems/AlertLevelChangeOnTriggerSystem.cs @@ -1,13 +1,13 @@ using Content.Shared.AlertLevel; +using Content.Shared.Station.Systems; using Content.Shared.Trigger.Components.Effects; -using Content.Shared.Station; namespace Content.Shared.Trigger.Systems; public sealed partial class AlertLevelChangeOnTriggerSystem : EntitySystem { [Dependency] private AlertLevelSystem _alertLevel = default!; - [Dependency] private SharedStationSystem _station = default!; + [Dependency] private StationSystem _station = default!; public override void Initialize() { diff --git a/Resources/Prototypes/GameRules/roundstart.yml b/Resources/Prototypes/GameRules/roundstart.yml index cd4562e67e..ee0f0f7f5c 100644 --- a/Resources/Prototypes/GameRules/roundstart.yml +++ b/Resources/Prototypes/GameRules/roundstart.yml @@ -4,6 +4,13 @@ components: - type: GameRule +- type: entity + id: BaseSilentGameRule + abstract: true + components: + - type: GameRule + silent: true + - type: entity parent: BaseGameRule id: BaseRoundstartAntagRule @@ -465,7 +472,7 @@ # variation passes - type: entity id: BasicRoundstartVariation - parent: BaseGameRule + parent: BaseSilentGameRule components: - type: RoundstartStationVariationRule rules: diff --git a/Resources/Prototypes/GameRules/variation.yml b/Resources/Prototypes/GameRules/variation.yml index 5e2fe0576b..13b9b18d68 100644 --- a/Resources/Prototypes/GameRules/variation.yml +++ b/Resources/Prototypes/GameRules/variation.yml @@ -1,9 +1,9 @@ # Base - type: entity - id: BaseVariationPass - parent: BaseGameRule abstract: true + parent: BaseSilentGameRule + id: BaseVariationPass components: - type: StationVariationPassRule