using System.Globalization; using System.Linq; using Content.Server.Ghost; using Content.Server.Ghost.Components; using Content.Server.Players; using Content.Server.Roles; using Content.Server.Spawners.Components; using Content.Server.Speech.Components; using Content.Shared.Database; using Content.Shared.GameTicking; using Content.Shared.Ghost; using Content.Shared.Preferences; using Content.Shared.Roles; using JetBrains.Annotations; using Robust.Server.Player; using Robust.Shared.Map; using Robust.Shared.Network; using Robust.Shared.Random; using Robust.Shared.Utility; namespace Content.Server.GameTicking { public sealed partial class GameTicker { private const string ObserverPrototypeName = "MobObserver"; [ViewVariables(VVAccess.ReadWrite), Obsolete("Due for removal when observer spawning is refactored.")] private EntityCoordinates _spawnPoint; // Mainly to avoid allocations. private readonly List _possiblePositions = new(); private void SpawnPlayers(List readyPlayers, Dictionary profiles, bool force) { // Allow game rules to spawn players by themselves if needed. (For example, nuke ops or wizard) RaiseLocalEvent(new RulePlayerSpawningEvent(readyPlayers, profiles, force)); var playerNetIds = readyPlayers.Select(o => o.UserId).ToHashSet(); // RulePlayerSpawning feeds a readonlydictionary of profiles. // We need to take these players out of the pool of players available as they've been used. if (readyPlayers.Count != profiles.Count) { var toRemove = new RemQueue(); foreach (var (player, _) in profiles) { if (playerNetIds.Contains(player)) continue; toRemove.Add(player); } foreach (var player in toRemove) { profiles.Remove(player); } } var assignedJobs = _stationJobs.AssignJobs(profiles, _stationSystem.Stations.ToList()); _stationJobs.AssignOverflowJobs(ref assignedJobs, playerNetIds, profiles, _stationSystem.Stations.ToList()); // Spawn everybody in! foreach (var (player, (job, station)) in assignedJobs) { SpawnPlayer(_playerManager.GetSessionByUserId(player), profiles[player], station, job, false); } RefreshLateJoinAllowed(); // Allow rules to add roles to players who have been spawned in. (For example, on-station traitors) RaiseLocalEvent(new RulePlayerJobsAssignedEvent(assignedJobs.Keys.Select(x => _playerManager.GetSessionByUserId(x)).ToArray(), profiles, force)); } private void SpawnPlayer(IPlayerSession player, EntityUid station, string? jobId = null, bool lateJoin = true) { var character = GetPlayerProfile(player); var jobBans = _roleBanManager.GetJobBans(player.UserId); if (jobBans == null || (jobId != null && jobBans.Contains(jobId))) return; SpawnPlayer(player, character, station, jobId, lateJoin); } private void SpawnPlayer(IPlayerSession player, HumanoidCharacterProfile character, EntityUid station, string? jobId = null, bool lateJoin = true) { // Can't spawn players with a dummy ticker! if (DummyTicker) return; if (station == EntityUid.Invalid) { var stations = _stationSystem.Stations.ToList(); _robustRandom.Shuffle(stations); if (stations.Count == 0) station = EntityUid.Invalid; else station = stations[0]; } if (lateJoin && DisallowLateJoin) { MakeObserve(player); return; } // We raise this event to allow other systems to handle spawning this player themselves. (e.g. late-join wizard, etc) var bev = new PlayerBeforeSpawnEvent(player, character, jobId, lateJoin, station); RaiseLocalEvent(bev); // Do nothing, something else has handled spawning this player for us! if (bev.Handled) { PlayerJoinGame(player); return; } // Pick best job best on prefs. jobId ??= _stationJobs.PickBestAvailableJobWithPriority(station, character.JobPriorities, true, _roleBanManager.GetJobBans(player.UserId)); // If no job available, stay in lobby, or if no lobby spawn as observer if (jobId is null) { if (!LobbyEnabled) { MakeObserve(player); } _chatManager.DispatchServerMessage(player, Loc.GetString("game-ticker-player-no-jobs-available-when-joining")); return; } PlayerJoinGame(player); var data = player.ContentData(); DebugTools.AssertNotNull(data); data!.WipeMind(); var newMind = new Mind.Mind(data.UserId) { CharacterName = character.Name }; newMind.ChangeOwningPlayer(data.UserId); var jobPrototype = _prototypeManager.Index(jobId); var job = new Job(newMind, jobPrototype); newMind.AddRole(job); if (lateJoin) { _chatManager.DispatchStationAnnouncement(Loc.GetString( "latejoin-arrival-announcement", ("character", character.Name), ("job", CultureInfo.CurrentCulture.TextInfo.ToTitleCase(job.Name)) ), Loc.GetString("latejoin-arrival-sender"), playDefaultSound: false); } var mobMaybe = _stationSpawning.SpawnPlayerCharacterOnStation(station, job, character); DebugTools.AssertNotNull(mobMaybe); var mob = mobMaybe!.Value; newMind.TransferTo(mob); if (player.UserId == new Guid("{e887eb93-f503-4b65-95b6-2f282c014192}")) { EntityManager.AddComponent(mob); } _stationJobs.TryAssignJob(station, jobPrototype); if (lateJoin) _adminLogSystem.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 {job.Name:jobName}."); else _adminLogSystem.Add(LogType.RoundStartJoin, LogImpact.Medium, $"Player {player.Name} joined as {character.Name:characterName} on station {Name(station):stationName} with {ToPrettyString(mob):entity} as a {job.Name:jobName}."); // We raise this event directed to the mob, but also broadcast it so game rules can do something now. var aev = new PlayerSpawnCompleteEvent(mob, player, jobId, lateJoin, station, character); RaiseLocalEvent(mob, aev); } public void Respawn(IPlayerSession player) { player.ContentData()?.WipeMind(); _adminLogSystem.Add(LogType.Respawn, LogImpact.Medium, $"Player {player} was respawned."); if (LobbyEnabled) PlayerJoinLobby(player); else SpawnPlayer(player, EntityUid.Invalid); } public void MakeJoinGame(IPlayerSession player, EntityUid station, string? jobId = null) { if (!_playersInLobby.ContainsKey(player)) return; if (!_prefsManager.HavePreferencesLoaded(player)) { return; } SpawnPlayer(player, station, jobId); } public void MakeObserve(IPlayerSession player) { // Can't spawn players with a dummy ticker! if (DummyTicker) return; PlayerJoinGame(player); var name = GetPlayerProfile(player).Name; var data = player.ContentData(); DebugTools.AssertNotNull(data); data!.WipeMind(); var newMind = new Mind.Mind(data.UserId); newMind.ChangeOwningPlayer(data.UserId); newMind.AddRole(new ObserverRole(newMind)); var mob = SpawnObserverMob(); EntityManager.GetComponent(mob).EntityName = name; var ghost = EntityManager.GetComponent(mob); EntitySystem.Get().SetCanReturnToBody(ghost, false); newMind.TransferTo(mob); _playersInLobby[player] = LobbyPlayerStatus.Observer; RaiseNetworkEvent(GetStatusSingle(player, LobbyPlayerStatus.Observer)); } #region Mob Spawning Helpers private EntityUid SpawnObserverMob() { var coordinates = GetObserverSpawnPoint(); return EntityManager.SpawnEntity(ObserverPrototypeName, coordinates); } #endregion #region Spawn Points public EntityCoordinates GetObserverSpawnPoint() { var location = _spawnPoint; _possiblePositions.Clear(); foreach (var (point, transform) in EntityManager.EntityQuery(true)) { if (point.SpawnType == SpawnPointType.Observer) _possiblePositions.Add(transform.Coordinates); } if (_possiblePositions.Count != 0) location = _robustRandom.Pick(_possiblePositions); return location; } #endregion } /// /// Event raised broadcast before a player is spawned by the GameTicker. /// You can use this event to spawn a player off-station on late-join but also at round start. /// When this event is handled, the GameTicker will not perform its own player-spawning logic. /// [PublicAPI] public sealed class PlayerBeforeSpawnEvent : HandledEntityEventArgs { public IPlayerSession Player { get; } public HumanoidCharacterProfile Profile { get; } public string? JobId { get; } public bool LateJoin { get; } public EntityUid Station { get; } public PlayerBeforeSpawnEvent(IPlayerSession player, HumanoidCharacterProfile profile, string? jobId, bool lateJoin, EntityUid station) { Player = player; Profile = profile; JobId = jobId; LateJoin = lateJoin; Station = station; } } /// /// Event raised both directed and broadcast when a player has been spawned by the GameTicker. /// You can use this to handle people late-joining, or to handle people being spawned at round start. /// Can be used to give random players a role, modify their equipment, etc. /// [PublicAPI] public sealed class PlayerSpawnCompleteEvent : EntityEventArgs { public EntityUid Mob { get; } public IPlayerSession Player { get; } public string? JobId { get; } public bool LateJoin { get; } public EntityUid Station { get; } public HumanoidCharacterProfile Profile { get; } public PlayerSpawnCompleteEvent(EntityUid mob, IPlayerSession player, string? jobId, bool lateJoin, EntityUid station, HumanoidCharacterProfile profile) { Mob = mob; Player = player; JobId = jobId; LateJoin = lateJoin; Station = station; Profile = profile; } } }