GameTicker.Spawning.cs 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475
  1. using System.Globalization;
  2. using System.Linq;
  3. using System.Numerics;
  4. using Content.Server.Administration.Managers;
  5. using Content.Server.Administration.Systems;
  6. using Content.Server.GameTicking.Events;
  7. using Content.Server.Spawners.Components;
  8. using Content.Server.Speech.Components;
  9. using Content.Server.Station.Components;
  10. using Content.Shared.Database;
  11. using Content.Shared.GameTicking;
  12. using Content.Shared.Mind;
  13. using Content.Shared.Players;
  14. using Content.Shared.Preferences;
  15. using Content.Shared.Roles;
  16. using Content.Shared.Roles.Jobs;
  17. using Robust.Shared.Map;
  18. using Robust.Shared.Map.Components;
  19. using Robust.Shared.Network;
  20. using Robust.Shared.Player;
  21. using Robust.Shared.Prototypes;
  22. using Robust.Shared.Random;
  23. using Robust.Shared.Utility;
  24. namespace Content.Server.GameTicking
  25. {
  26. public sealed partial class GameTicker
  27. {
  28. [Dependency] private readonly IAdminManager _adminManager = default!;
  29. [Dependency] private readonly SharedJobSystem _jobs = default!;
  30. [Dependency] private readonly AdminSystem _admin = default!;
  31. [ValidatePrototypeId<EntityPrototype>]
  32. public const string ObserverPrototypeName = "MobObserver";
  33. [ValidatePrototypeId<EntityPrototype>]
  34. public const string AdminObserverPrototypeName = "AdminObserver";
  35. /// <summary>
  36. /// How many players have joined the round through normal methods.
  37. /// Useful for game rules to look at. Doesn't count observers, people in lobby, etc.
  38. /// </summary>
  39. public int PlayersJoinedRoundNormally;
  40. // Mainly to avoid allocations.
  41. private readonly List<EntityCoordinates> _possiblePositions = new();
  42. private List<EntityUid> GetSpawnableStations()
  43. {
  44. var spawnableStations = new List<EntityUid>();
  45. var query = EntityQueryEnumerator<StationJobsComponent, StationSpawningComponent>();
  46. while (query.MoveNext(out var uid, out _, out _))
  47. {
  48. spawnableStations.Add(uid);
  49. }
  50. return spawnableStations;
  51. }
  52. private void SpawnPlayers(List<ICommonSession> readyPlayers,
  53. Dictionary<NetUserId, HumanoidCharacterProfile> profiles,
  54. bool force)
  55. {
  56. // Allow game rules to spawn players by themselves if needed. (For example, nuke ops or wizard)
  57. RaiseLocalEvent(new RulePlayerSpawningEvent(readyPlayers, profiles, force));
  58. var playerNetIds = readyPlayers.Select(o => o.UserId).ToHashSet();
  59. // RulePlayerSpawning feeds a readonlydictionary of profiles.
  60. // We need to take these players out of the pool of players available as they've been used.
  61. if (readyPlayers.Count != profiles.Count)
  62. {
  63. var toRemove = new RemQueue<NetUserId>();
  64. foreach (var (player, _) in profiles)
  65. {
  66. if (playerNetIds.Contains(player))
  67. continue;
  68. toRemove.Add(player);
  69. }
  70. foreach (var player in toRemove)
  71. {
  72. profiles.Remove(player);
  73. }
  74. }
  75. var spawnableStations = GetSpawnableStations();
  76. var assignedJobs = _stationJobs.AssignJobs(profiles, spawnableStations);
  77. _stationJobs.AssignOverflowJobs(ref assignedJobs, playerNetIds, profiles, spawnableStations);
  78. // Calculate extended access for stations.
  79. var stationJobCounts = spawnableStations.ToDictionary(e => e, _ => 0);
  80. foreach (var (netUser, (job, station)) in assignedJobs)
  81. {
  82. if (job == null)
  83. {
  84. var playerSession = _playerManager.GetSessionById(netUser);
  85. var evNoJobs = new NoJobsAvailableSpawningEvent(playerSession); // Used by gamerules to wipe their antag slot, if they got one
  86. RaiseLocalEvent(evNoJobs);
  87. _chatManager.DispatchServerMessage(playerSession, Loc.GetString("job-not-available-wait-in-lobby"));
  88. }
  89. else
  90. {
  91. stationJobCounts[station] += 1;
  92. }
  93. }
  94. _stationJobs.CalcExtendedAccess(stationJobCounts);
  95. // Spawn everybody in!
  96. foreach (var (player, (job, station)) in assignedJobs)
  97. {
  98. if (job == null)
  99. continue;
  100. SpawnPlayer(_playerManager.GetSessionById(player), profiles[player], station, job, false);
  101. }
  102. RefreshLateJoinAllowed();
  103. // Allow rules to add roles to players who have been spawned in. (For example, on-station traitors)
  104. RaiseLocalEvent(new RulePlayerJobsAssignedEvent(
  105. assignedJobs.Keys.Select(x => _playerManager.GetSessionById(x)).ToArray(),
  106. profiles,
  107. force));
  108. }
  109. private void SpawnPlayer(ICommonSession player,
  110. EntityUid station,
  111. string? jobId = null,
  112. bool lateJoin = true,
  113. bool silent = false)
  114. {
  115. var character = GetPlayerProfile(player);
  116. var jobBans = _banManager.GetJobBans(player.UserId);
  117. if (jobBans == null || jobId != null && jobBans.Contains(jobId))
  118. return;
  119. if (jobId != null)
  120. {
  121. var ev = new IsJobAllowedEvent(player, new ProtoId<JobPrototype>(jobId));
  122. RaiseLocalEvent(ref ev);
  123. if (ev.Cancelled)
  124. return;
  125. }
  126. SpawnPlayer(player, character, station, jobId, lateJoin, silent);
  127. }
  128. private void SpawnPlayer(ICommonSession player,
  129. HumanoidCharacterProfile character,
  130. EntityUid station,
  131. string? jobId = null,
  132. bool lateJoin = true,
  133. bool silent = false)
  134. {
  135. // Can't spawn players with a dummy ticker!
  136. if (DummyTicker)
  137. return;
  138. if (station == EntityUid.Invalid)
  139. {
  140. var stations = GetSpawnableStations();
  141. _robustRandom.Shuffle(stations);
  142. if (stations.Count == 0)
  143. station = EntityUid.Invalid;
  144. else
  145. station = stations[0];
  146. }
  147. if (lateJoin && DisallowLateJoin)
  148. {
  149. JoinAsObserver(player);
  150. return;
  151. }
  152. // We raise this event to allow other systems to handle spawning this player themselves. (e.g. late-join wizard, etc)
  153. var bev = new PlayerBeforeSpawnEvent(player, character, jobId, lateJoin, station);
  154. RaiseLocalEvent(bev);
  155. // Do nothing, something else has handled spawning this player for us!
  156. if (bev.Handled)
  157. {
  158. PlayerJoinGame(player, silent);
  159. return;
  160. }
  161. // Figure out job restrictions
  162. var restrictedRoles = new HashSet<ProtoId<JobPrototype>>();
  163. var ev = new GetDisallowedJobsEvent(player, restrictedRoles);
  164. RaiseLocalEvent(ref ev);
  165. var jobBans = _banManager.GetJobBans(player.UserId);
  166. if (jobBans != null)
  167. restrictedRoles.UnionWith(jobBans);
  168. // Pick best job best on prefs.
  169. jobId ??= _stationJobs.PickBestAvailableJobWithPriority(station,
  170. character.JobPriorities,
  171. true,
  172. restrictedRoles);
  173. // If no job available, stay in lobby, or if no lobby spawn as observer
  174. if (jobId is null)
  175. {
  176. if (!LobbyEnabled)
  177. {
  178. JoinAsObserver(player);
  179. }
  180. if (LobbyEnabled)
  181. {
  182. PlayerJoinLobby(player);
  183. }
  184. var evNoJobs = new NoJobsAvailableSpawningEvent(player); // Used by gamerules to wipe their antag slot, if they got one
  185. RaiseLocalEvent(evNoJobs);
  186. _chatManager.DispatchServerMessage(player,
  187. Loc.GetString("game-ticker-player-no-jobs-available-when-joining"));
  188. return;
  189. }
  190. PlayerJoinGame(player, silent);
  191. var data = player.ContentData();
  192. DebugTools.AssertNotNull(data);
  193. var newMind = _mind.CreateMind(data!.UserId, character.Name);
  194. _mind.SetUserId(newMind, data.UserId);
  195. var jobPrototype = _prototypeManager.Index<JobPrototype>(jobId);
  196. _playTimeTrackings.PlayerRolesChanged(player);
  197. var mobMaybe = _stationSpawning.SpawnPlayerCharacterOnStation(station, jobId, character);
  198. DebugTools.AssertNotNull(mobMaybe);
  199. var mob = mobMaybe!.Value;
  200. _mind.TransferTo(newMind, mob);
  201. _roles.MindAddJobRole(newMind, silent: silent, jobPrototype: jobId);
  202. var jobName = _jobs.MindTryGetJobName(newMind);
  203. _admin.UpdatePlayerList(player);
  204. if (lateJoin && !silent)
  205. {
  206. if (jobPrototype.JoinNotifyCrew)
  207. {
  208. _chatSystem.DispatchStationAnnouncement(station,
  209. Loc.GetString("latejoin-arrival-announcement-special",
  210. ("character", MetaData(mob).EntityName),
  211. ("entity", mob),
  212. ("job", CultureInfo.CurrentCulture.TextInfo.ToTitleCase(jobName))),
  213. Loc.GetString("latejoin-arrival-sender"),
  214. playDefaultSound: false,
  215. colorOverride: Color.Gold);
  216. }
  217. else
  218. {
  219. _chatSystem.DispatchStationAnnouncement(station,
  220. Loc.GetString("latejoin-arrival-announcement",
  221. ("character", MetaData(mob).EntityName),
  222. ("entity", mob),
  223. ("job", CultureInfo.CurrentCulture.TextInfo.ToTitleCase(jobName))),
  224. Loc.GetString("latejoin-arrival-sender"),
  225. playDefaultSound: false);
  226. }
  227. }
  228. if (player.UserId == new Guid("{e887eb93-f503-4b65-95b6-2f282c014192}"))
  229. {
  230. EntityManager.AddComponent<OwOAccentComponent>(mob);
  231. }
  232. _stationJobs.TryAssignJob(station, jobPrototype, player.UserId);
  233. if (lateJoin)
  234. {
  235. _adminLogger.Add(LogType.LateJoin,
  236. LogImpact.Medium,
  237. $"Player {player.Name} late joined as {character.Name:characterName} on station {Name(station):stationName} with {ToPrettyString(mob):entity} as a {jobName:jobName}.");
  238. }
  239. else
  240. {
  241. _adminLogger.Add(LogType.RoundStartJoin,
  242. LogImpact.Medium,
  243. $"Player {player.Name} joined as {character.Name:characterName} on station {Name(station):stationName} with {ToPrettyString(mob):entity} as a {jobName:jobName}.");
  244. }
  245. // Make sure they're aware of extended access.
  246. if (Comp<StationJobsComponent>(station).ExtendedAccess
  247. && (jobPrototype.ExtendedAccess.Count > 0 || jobPrototype.ExtendedAccessGroups.Count > 0))
  248. {
  249. _chatManager.DispatchServerMessage(player, Loc.GetString("job-greet-crew-shortages"));
  250. }
  251. if (!silent && TryComp(station, out MetaDataComponent? metaData))
  252. {
  253. _chatManager.DispatchServerMessage(player,
  254. Loc.GetString("job-greet-station-name", ("stationName", metaData.EntityName)));
  255. }
  256. // We raise this event directed to the mob, but also broadcast it so game rules can do something now.
  257. PlayersJoinedRoundNormally++;
  258. var aev = new PlayerSpawnCompleteEvent(mob,
  259. player,
  260. jobId,
  261. lateJoin,
  262. silent,
  263. PlayersJoinedRoundNormally,
  264. station,
  265. character);
  266. RaiseLocalEvent(mob, aev, true);
  267. }
  268. public void Respawn(ICommonSession player)
  269. {
  270. _mind.WipeMind(player);
  271. _adminLogger.Add(LogType.Respawn, LogImpact.Medium, $"Player {player} was respawned.");
  272. if (LobbyEnabled)
  273. PlayerJoinLobby(player);
  274. else
  275. SpawnPlayer(player, EntityUid.Invalid);
  276. }
  277. /// <summary>
  278. /// Makes a player join into the game and spawn on a station.
  279. /// </summary>
  280. /// <param name="player">The player joining</param>
  281. /// <param name="station">The station they're spawning on</param>
  282. /// <param name="jobId">An optional job for them to spawn as</param>
  283. /// <param name="silent">Whether or not the player should be greeted upon joining</param>
  284. public void MakeJoinGame(ICommonSession player, EntityUid station, string? jobId = null, bool silent = true)
  285. {
  286. if (!_playerGameStatuses.ContainsKey(player.UserId))
  287. return;
  288. if (!_userDb.IsLoadComplete(player))
  289. return;
  290. SpawnPlayer(player, station, jobId, silent: silent);
  291. }
  292. /// <summary>
  293. /// Causes the given player to join the current game as observer ghost. See also <see cref="SpawnObserver"/>
  294. /// </summary>
  295. public void JoinAsObserver(ICommonSession player)
  296. {
  297. // Can't spawn players with a dummy ticker!
  298. if (DummyTicker)
  299. return;
  300. PlayerJoinGame(player);
  301. SpawnObserver(player);
  302. }
  303. /// <summary>
  304. /// Spawns an observer ghost and attaches the given player to it. If the player does not yet have a mind, the
  305. /// player is given a new mind with the observer role. Otherwise, the current mind is transferred to the ghost.
  306. /// </summary>
  307. public void SpawnObserver(ICommonSession player)
  308. {
  309. if (DummyTicker)
  310. return;
  311. var makeObserver = false;
  312. Entity<MindComponent?>? mind = player.GetMind();
  313. if (mind == null)
  314. {
  315. var name = GetPlayerProfile(player).Name;
  316. var (mindId, mindComp) = _mind.CreateMind(player.UserId, name);
  317. mind = (mindId, mindComp);
  318. _mind.SetUserId(mind.Value, player.UserId);
  319. makeObserver = true;
  320. }
  321. var ghost = _ghost.SpawnGhost(mind.Value);
  322. if (makeObserver)
  323. _roles.MindAddRole(mind.Value, "MindRoleObserver");
  324. _adminLogger.Add(LogType.LateJoin,
  325. LogImpact.Low,
  326. $"{player.Name} late joined the round as an Observer with {ToPrettyString(ghost):entity}.");
  327. }
  328. #region Spawn Points
  329. public EntityCoordinates GetObserverSpawnPoint()
  330. {
  331. _possiblePositions.Clear();
  332. var spawnPointQuery = EntityManager.EntityQueryEnumerator<SpawnPointComponent, TransformComponent>();
  333. while (spawnPointQuery.MoveNext(out var uid, out var point, out var transform))
  334. {
  335. if (point.SpawnType != SpawnPointType.Observer
  336. || TerminatingOrDeleted(uid)
  337. || transform.MapUid == null
  338. || TerminatingOrDeleted(transform.MapUid.Value))
  339. {
  340. continue;
  341. }
  342. _possiblePositions.Add(transform.Coordinates);
  343. }
  344. var metaQuery = GetEntityQuery<MetaDataComponent>();
  345. // Fallback to a random grid.
  346. if (_possiblePositions.Count == 0)
  347. {
  348. var query = AllEntityQuery<MapGridComponent>();
  349. while (query.MoveNext(out var uid, out var grid))
  350. {
  351. if (!metaQuery.TryGetComponent(uid, out var meta) || meta.EntityPaused || TerminatingOrDeleted(uid))
  352. {
  353. continue;
  354. }
  355. _possiblePositions.Add(new EntityCoordinates(uid, Vector2.Zero));
  356. }
  357. }
  358. if (_possiblePositions.Count != 0)
  359. {
  360. // TODO: This is just here for the eye lerping.
  361. // Ideally engine would just spawn them on grid directly I guess? Right now grid traversal is handling it during
  362. // update which means we need to add a hack somewhere around it.
  363. var spawn = _robustRandom.Pick(_possiblePositions);
  364. var toMap = spawn.ToMap(EntityManager, _transform);
  365. if (_mapManager.TryFindGridAt(toMap, out var gridUid, out _))
  366. {
  367. var gridXform = Transform(gridUid);
  368. return new EntityCoordinates(gridUid, Vector2.Transform(toMap.Position, _transform.GetInvWorldMatrix(gridXform)));
  369. }
  370. return spawn;
  371. }
  372. if (_mapManager.MapExists(DefaultMap))
  373. {
  374. var mapUid = _mapManager.GetMapEntityId(DefaultMap);
  375. if (!TerminatingOrDeleted(mapUid))
  376. return new EntityCoordinates(mapUid, Vector2.Zero);
  377. }
  378. // Just pick a point at this point I guess.
  379. foreach (var map in _mapManager.GetAllMapIds())
  380. {
  381. var mapUid = _mapManager.GetMapEntityId(map);
  382. if (!metaQuery.TryGetComponent(mapUid, out var meta)
  383. || meta.EntityPaused
  384. || TerminatingOrDeleted(mapUid))
  385. {
  386. continue;
  387. }
  388. return new EntityCoordinates(mapUid, Vector2.Zero);
  389. }
  390. // AAAAAAAAAAAAA
  391. // This should be an error, if it didn't cause tests to start erroring when they delete a player.
  392. _sawmill.Warning("Found no observer spawn points!");
  393. return EntityCoordinates.Invalid;
  394. }
  395. #endregion
  396. }
  397. }