GameTicker.Spawning.cs 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472
  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. var evNoJobs = new NoJobsAvailableSpawningEvent(player); // Used by gamerules to wipe their antag slot, if they got one
  181. RaiseLocalEvent(evNoJobs);
  182. _chatManager.DispatchServerMessage(player,
  183. Loc.GetString("game-ticker-player-no-jobs-available-when-joining"));
  184. return;
  185. }
  186. PlayerJoinGame(player, silent);
  187. var data = player.ContentData();
  188. DebugTools.AssertNotNull(data);
  189. var newMind = _mind.CreateMind(data!.UserId, character.Name);
  190. _mind.SetUserId(newMind, data.UserId);
  191. var jobPrototype = _prototypeManager.Index<JobPrototype>(jobId);
  192. _playTimeTrackings.PlayerRolesChanged(player);
  193. var mobMaybe = _stationSpawning.SpawnPlayerCharacterOnStation(station, jobId, character);
  194. DebugTools.AssertNotNull(mobMaybe);
  195. var mob = mobMaybe!.Value;
  196. _mind.TransferTo(newMind, mob);
  197. _roles.MindAddJobRole(newMind, silent: silent, jobPrototype:jobId);
  198. var jobName = _jobs.MindTryGetJobName(newMind);
  199. _admin.UpdatePlayerList(player);
  200. if (lateJoin && !silent)
  201. {
  202. if (jobPrototype.JoinNotifyCrew)
  203. {
  204. _chatSystem.DispatchStationAnnouncement(station,
  205. Loc.GetString("latejoin-arrival-announcement-special",
  206. ("character", MetaData(mob).EntityName),
  207. ("entity", mob),
  208. ("job", CultureInfo.CurrentCulture.TextInfo.ToTitleCase(jobName))),
  209. Loc.GetString("latejoin-arrival-sender"),
  210. playDefaultSound: false,
  211. colorOverride: Color.Gold);
  212. }
  213. else
  214. {
  215. _chatSystem.DispatchStationAnnouncement(station,
  216. Loc.GetString("latejoin-arrival-announcement",
  217. ("character", MetaData(mob).EntityName),
  218. ("entity", mob),
  219. ("job", CultureInfo.CurrentCulture.TextInfo.ToTitleCase(jobName))),
  220. Loc.GetString("latejoin-arrival-sender"),
  221. playDefaultSound: false);
  222. }
  223. }
  224. if (player.UserId == new Guid("{e887eb93-f503-4b65-95b6-2f282c014192}"))
  225. {
  226. EntityManager.AddComponent<OwOAccentComponent>(mob);
  227. }
  228. _stationJobs.TryAssignJob(station, jobPrototype, player.UserId);
  229. if (lateJoin)
  230. {
  231. _adminLogger.Add(LogType.LateJoin,
  232. LogImpact.Medium,
  233. $"Player {player.Name} late joined as {character.Name:characterName} on station {Name(station):stationName} with {ToPrettyString(mob):entity} as a {jobName:jobName}.");
  234. }
  235. else
  236. {
  237. _adminLogger.Add(LogType.RoundStartJoin,
  238. LogImpact.Medium,
  239. $"Player {player.Name} joined as {character.Name:characterName} on station {Name(station):stationName} with {ToPrettyString(mob):entity} as a {jobName:jobName}.");
  240. }
  241. // Make sure they're aware of extended access.
  242. if (Comp<StationJobsComponent>(station).ExtendedAccess
  243. && (jobPrototype.ExtendedAccess.Count > 0 || jobPrototype.ExtendedAccessGroups.Count > 0))
  244. {
  245. _chatManager.DispatchServerMessage(player, Loc.GetString("job-greet-crew-shortages"));
  246. }
  247. if (!silent && TryComp(station, out MetaDataComponent? metaData))
  248. {
  249. _chatManager.DispatchServerMessage(player,
  250. Loc.GetString("job-greet-station-name", ("stationName", metaData.EntityName)));
  251. }
  252. // We raise this event directed to the mob, but also broadcast it so game rules can do something now.
  253. PlayersJoinedRoundNormally++;
  254. var aev = new PlayerSpawnCompleteEvent(mob,
  255. player,
  256. jobId,
  257. lateJoin,
  258. silent,
  259. PlayersJoinedRoundNormally,
  260. station,
  261. character);
  262. RaiseLocalEvent(mob, aev, true);
  263. }
  264. public void Respawn(ICommonSession player)
  265. {
  266. _mind.WipeMind(player);
  267. _adminLogger.Add(LogType.Respawn, LogImpact.Medium, $"Player {player} was respawned.");
  268. if (LobbyEnabled)
  269. PlayerJoinLobby(player);
  270. else
  271. SpawnPlayer(player, EntityUid.Invalid);
  272. }
  273. /// <summary>
  274. /// Makes a player join into the game and spawn on a station.
  275. /// </summary>
  276. /// <param name="player">The player joining</param>
  277. /// <param name="station">The station they're spawning on</param>
  278. /// <param name="jobId">An optional job for them to spawn as</param>
  279. /// <param name="silent">Whether or not the player should be greeted upon joining</param>
  280. public void MakeJoinGame(ICommonSession player, EntityUid station, string? jobId = null, bool silent = false)
  281. {
  282. if (!_playerGameStatuses.ContainsKey(player.UserId))
  283. return;
  284. if (!_userDb.IsLoadComplete(player))
  285. return;
  286. SpawnPlayer(player, station, jobId, silent: silent);
  287. }
  288. /// <summary>
  289. /// Causes the given player to join the current game as observer ghost. See also <see cref="SpawnObserver"/>
  290. /// </summary>
  291. public void JoinAsObserver(ICommonSession player)
  292. {
  293. // Can't spawn players with a dummy ticker!
  294. if (DummyTicker)
  295. return;
  296. PlayerJoinGame(player);
  297. SpawnObserver(player);
  298. }
  299. /// <summary>
  300. /// Spawns an observer ghost and attaches the given player to it. If the player does not yet have a mind, the
  301. /// player is given a new mind with the observer role. Otherwise, the current mind is transferred to the ghost.
  302. /// </summary>
  303. public void SpawnObserver(ICommonSession player)
  304. {
  305. if (DummyTicker)
  306. return;
  307. var makeObserver = false;
  308. Entity<MindComponent?>? mind = player.GetMind();
  309. if (mind == null)
  310. {
  311. var name = GetPlayerProfile(player).Name;
  312. var (mindId, mindComp) = _mind.CreateMind(player.UserId, name);
  313. mind = (mindId, mindComp);
  314. _mind.SetUserId(mind.Value, player.UserId);
  315. makeObserver = true;
  316. }
  317. var ghost = _ghost.SpawnGhost(mind.Value);
  318. if (makeObserver)
  319. _roles.MindAddRole(mind.Value, "MindRoleObserver");
  320. _adminLogger.Add(LogType.LateJoin,
  321. LogImpact.Low,
  322. $"{player.Name} late joined the round as an Observer with {ToPrettyString(ghost):entity}.");
  323. }
  324. #region Spawn Points
  325. public EntityCoordinates GetObserverSpawnPoint()
  326. {
  327. _possiblePositions.Clear();
  328. var spawnPointQuery = EntityManager.EntityQueryEnumerator<SpawnPointComponent, TransformComponent>();
  329. while (spawnPointQuery.MoveNext(out var uid, out var point, out var transform))
  330. {
  331. if (point.SpawnType != SpawnPointType.Observer
  332. || TerminatingOrDeleted(uid)
  333. || transform.MapUid == null
  334. || TerminatingOrDeleted(transform.MapUid.Value))
  335. {
  336. continue;
  337. }
  338. _possiblePositions.Add(transform.Coordinates);
  339. }
  340. var metaQuery = GetEntityQuery<MetaDataComponent>();
  341. // Fallback to a random grid.
  342. if (_possiblePositions.Count == 0)
  343. {
  344. var query = AllEntityQuery<MapGridComponent>();
  345. while (query.MoveNext(out var uid, out var grid))
  346. {
  347. if (!metaQuery.TryGetComponent(uid, out var meta) || meta.EntityPaused || TerminatingOrDeleted(uid))
  348. {
  349. continue;
  350. }
  351. _possiblePositions.Add(new EntityCoordinates(uid, Vector2.Zero));
  352. }
  353. }
  354. if (_possiblePositions.Count != 0)
  355. {
  356. // TODO: This is just here for the eye lerping.
  357. // Ideally engine would just spawn them on grid directly I guess? Right now grid traversal is handling it during
  358. // update which means we need to add a hack somewhere around it.
  359. var spawn = _robustRandom.Pick(_possiblePositions);
  360. var toMap = spawn.ToMap(EntityManager, _transform);
  361. if (_mapManager.TryFindGridAt(toMap, out var gridUid, out _))
  362. {
  363. var gridXform = Transform(gridUid);
  364. return new EntityCoordinates(gridUid, Vector2.Transform(toMap.Position, _transform.GetInvWorldMatrix(gridXform)));
  365. }
  366. return spawn;
  367. }
  368. if (_mapManager.MapExists(DefaultMap))
  369. {
  370. var mapUid = _mapManager.GetMapEntityId(DefaultMap);
  371. if (!TerminatingOrDeleted(mapUid))
  372. return new EntityCoordinates(mapUid, Vector2.Zero);
  373. }
  374. // Just pick a point at this point I guess.
  375. foreach (var map in _mapManager.GetAllMapIds())
  376. {
  377. var mapUid = _mapManager.GetMapEntityId(map);
  378. if (!metaQuery.TryGetComponent(mapUid, out var meta)
  379. || meta.EntityPaused
  380. || TerminatingOrDeleted(mapUid))
  381. {
  382. continue;
  383. }
  384. return new EntityCoordinates(mapUid, Vector2.Zero);
  385. }
  386. // AAAAAAAAAAAAA
  387. // This should be an error, if it didn't cause tests to start erroring when they delete a player.
  388. _sawmill.Warning("Found no observer spawn points!");
  389. return EntityCoordinates.Invalid;
  390. }
  391. #endregion
  392. }
  393. }