GameTicker.Spawning.cs 21 KB

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