GameTicker.Spawning.cs 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524
  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 (_gameTiming.CurTime < time)
  136. {
  137. _chatManager.DispatchServerMessage(player,
  138. Loc.GetString("rule-respawn-blocked", ("seconds", time.TotalSeconds - _gameTiming.CurTime.TotalSeconds)));
  139. return;
  140. }
  141. }
  142. }
  143. }
  144. //if TDM, check if the teams are balanced
  145. var factionCount = GetPlayerFactionCounts();
  146. if (factionCount != null && factionCount.Count > 1)
  147. {
  148. //get the faction of the selected job
  149. if (jobId != null && _prototypeManager.TryIndex<JobPrototype>(jobId, out var job))
  150. {
  151. var selectedFaction = job.Faction;
  152. var currentCount = 0;
  153. var minCount = 1000;
  154. foreach (var fact in factionCount)
  155. {
  156. if (fact.Key == selectedFaction)
  157. {
  158. currentCount = fact.Value;
  159. }
  160. else if (fact.Key != selectedFaction && fact.Value < minCount)
  161. {
  162. minCount = fact.Value;
  163. }
  164. }
  165. if (currentCount > minCount)
  166. {
  167. //if the current faction is greater than the minimum faction, block the respawn
  168. _chatManager.DispatchServerMessage(player,
  169. Loc.GetString("rule-respawn-autobalance", ("this", currentCount), ("other", minCount)));
  170. return;
  171. }
  172. }
  173. }
  174. SpawnPlayer(player, character, station, jobId, lateJoin, silent);
  175. }
  176. private void SpawnPlayer(ICommonSession player,
  177. HumanoidCharacterProfile character,
  178. EntityUid station,
  179. string? jobId = null,
  180. bool lateJoin = true,
  181. bool silent = false)
  182. {
  183. // Can't spawn players with a dummy ticker!
  184. if (DummyTicker)
  185. return;
  186. if (station == EntityUid.Invalid)
  187. {
  188. var stations = GetSpawnableStations();
  189. _robustRandom.Shuffle(stations);
  190. if (stations.Count == 0)
  191. station = EntityUid.Invalid;
  192. else
  193. station = stations[0];
  194. }
  195. if (lateJoin && DisallowLateJoin)
  196. {
  197. JoinAsObserver(player);
  198. return;
  199. }
  200. // We raise this event to allow other systems to handle spawning this player themselves. (e.g. late-join wizard, etc)
  201. var bev = new PlayerBeforeSpawnEvent(player, character, jobId, lateJoin, station);
  202. RaiseLocalEvent(bev);
  203. // Do nothing, something else has handled spawning this player for us!
  204. if (bev.Handled)
  205. {
  206. PlayerJoinGame(player, silent);
  207. return;
  208. }
  209. // Figure out job restrictions
  210. var restrictedRoles = new HashSet<ProtoId<JobPrototype>>();
  211. var ev = new GetDisallowedJobsEvent(player, restrictedRoles);
  212. RaiseLocalEvent(ref ev);
  213. var jobBans = _banManager.GetJobBans(player.UserId);
  214. if (jobBans != null)
  215. restrictedRoles.UnionWith(jobBans);
  216. // Pick best job best on prefs.
  217. jobId ??= _stationJobs.PickBestAvailableJobWithPriority(station,
  218. character.JobPriorities,
  219. true,
  220. restrictedRoles);
  221. // If no job available, stay in lobby, or if no lobby spawn as observer
  222. if (jobId is null)
  223. {
  224. if (!LobbyEnabled)
  225. {
  226. JoinAsObserver(player);
  227. }
  228. if (LobbyEnabled)
  229. {
  230. PlayerJoinLobby(player);
  231. }
  232. var evNoJobs = new NoJobsAvailableSpawningEvent(player); // Used by gamerules to wipe their antag slot, if they got one
  233. RaiseLocalEvent(evNoJobs);
  234. _chatManager.DispatchServerMessage(player,
  235. Loc.GetString("game-ticker-player-no-jobs-available-when-joining"));
  236. return;
  237. }
  238. PlayerJoinGame(player, silent);
  239. var data = player.ContentData();
  240. DebugTools.AssertNotNull(data);
  241. var newMind = _mind.CreateMind(data!.UserId, character.Name);
  242. _mind.SetUserId(newMind, data.UserId);
  243. var jobPrototype = _prototypeManager.Index<JobPrototype>(jobId);
  244. _playTimeTrackings.PlayerRolesChanged(player);
  245. var mobMaybe = _stationSpawning.SpawnPlayerCharacterOnStation(station, jobId, character);
  246. DebugTools.AssertNotNull(mobMaybe);
  247. var mob = mobMaybe!.Value;
  248. _mind.TransferTo(newMind, mob);
  249. _roles.MindAddJobRole(newMind, silent: silent, jobPrototype: jobId);
  250. var jobName = _jobs.MindTryGetJobName(newMind);
  251. _admin.UpdatePlayerList(player);
  252. if (lateJoin && !silent)
  253. {
  254. if (jobPrototype.JoinNotifyCrew)
  255. {
  256. _chatSystem.DispatchStationAnnouncement(station,
  257. Loc.GetString("latejoin-arrival-announcement-special",
  258. ("character", MetaData(mob).EntityName),
  259. ("entity", mob),
  260. ("job", CultureInfo.CurrentCulture.TextInfo.ToTitleCase(jobName))),
  261. Loc.GetString("latejoin-arrival-sender"),
  262. playDefaultSound: false,
  263. colorOverride: Color.Gold);
  264. }
  265. else
  266. {
  267. _chatSystem.DispatchStationAnnouncement(station,
  268. Loc.GetString("latejoin-arrival-announcement",
  269. ("character", MetaData(mob).EntityName),
  270. ("entity", mob),
  271. ("job", CultureInfo.CurrentCulture.TextInfo.ToTitleCase(jobName))),
  272. Loc.GetString("latejoin-arrival-sender"),
  273. playDefaultSound: false);
  274. }
  275. }
  276. if (player.UserId == new Guid("{e887eb93-f503-4b65-95b6-2f282c014192}"))
  277. {
  278. EntityManager.AddComponent<OwOAccentComponent>(mob);
  279. }
  280. _stationJobs.TryAssignJob(station, jobPrototype, player.UserId);
  281. if (lateJoin)
  282. {
  283. _adminLogger.Add(LogType.LateJoin,
  284. LogImpact.Medium,
  285. $"Player {player.Name} late joined as {character.Name:characterName} on station {Name(station):stationName} with {ToPrettyString(mob):entity} as a {jobName:jobName}.");
  286. }
  287. else
  288. {
  289. _adminLogger.Add(LogType.RoundStartJoin,
  290. LogImpact.Medium,
  291. $"Player {player.Name} joined as {character.Name:characterName} on station {Name(station):stationName} with {ToPrettyString(mob):entity} as a {jobName:jobName}.");
  292. }
  293. // Make sure they're aware of extended access.
  294. if (Comp<StationJobsComponent>(station).ExtendedAccess
  295. && (jobPrototype.ExtendedAccess.Count > 0 || jobPrototype.ExtendedAccessGroups.Count > 0))
  296. {
  297. _chatManager.DispatchServerMessage(player, Loc.GetString("job-greet-crew-shortages"));
  298. }
  299. if (!silent && TryComp(station, out MetaDataComponent? metaData))
  300. {
  301. _chatManager.DispatchServerMessage(player,
  302. Loc.GetString("job-greet-station-name", ("stationName", metaData.EntityName)));
  303. }
  304. // We raise this event directed to the mob, but also broadcast it so game rules can do something now.
  305. PlayersJoinedRoundNormally++;
  306. var aev = new PlayerSpawnCompleteEvent(mob,
  307. player,
  308. jobId,
  309. lateJoin,
  310. silent,
  311. PlayersJoinedRoundNormally,
  312. station,
  313. character);
  314. RaiseLocalEvent(mob, aev, true);
  315. }
  316. public void Respawn(ICommonSession player)
  317. {
  318. _mind.WipeMind(player);
  319. _adminLogger.Add(LogType.Respawn, LogImpact.Medium, $"Player {player} was respawned.");
  320. if (LobbyEnabled)
  321. PlayerJoinLobby(player);
  322. else
  323. SpawnPlayer(player, EntityUid.Invalid);
  324. }
  325. /// <summary>
  326. /// Makes a player join into the game and spawn on a station.
  327. /// </summary>
  328. /// <param name="player">The player joining</param>
  329. /// <param name="station">The station they're spawning on</param>
  330. /// <param name="jobId">An optional job for them to spawn as</param>
  331. /// <param name="silent">Whether or not the player should be greeted upon joining</param>
  332. public void MakeJoinGame(ICommonSession player, EntityUid station, string? jobId = null, bool silent = true)
  333. {
  334. if (!_playerGameStatuses.ContainsKey(player.UserId))
  335. return;
  336. if (!_userDb.IsLoadComplete(player))
  337. return;
  338. SpawnPlayer(player, station, jobId, silent: silent);
  339. }
  340. /// <summary>
  341. /// Causes the given player to join the current game as observer ghost. See also <see cref="SpawnObserver"/>
  342. /// </summary>
  343. public void JoinAsObserver(ICommonSession player)
  344. {
  345. // Can't spawn players with a dummy ticker!
  346. if (DummyTicker)
  347. return;
  348. PlayerJoinGame(player);
  349. SpawnObserver(player);
  350. }
  351. /// <summary>
  352. /// Spawns an observer ghost and attaches the given player to it. If the player does not yet have a mind, the
  353. /// player is given a new mind with the observer role. Otherwise, the current mind is transferred to the ghost.
  354. /// </summary>
  355. public void SpawnObserver(ICommonSession player)
  356. {
  357. if (DummyTicker)
  358. return;
  359. var makeObserver = false;
  360. Entity<MindComponent?>? mind = player.GetMind();
  361. if (mind == null)
  362. {
  363. var name = GetPlayerProfile(player).Name;
  364. var (mindId, mindComp) = _mind.CreateMind(player.UserId, name);
  365. mind = (mindId, mindComp);
  366. _mind.SetUserId(mind.Value, player.UserId);
  367. makeObserver = true;
  368. }
  369. var ghost = _ghost.SpawnGhost(mind.Value);
  370. if (makeObserver)
  371. _roles.MindAddRole(mind.Value, "MindRoleObserver");
  372. _adminLogger.Add(LogType.LateJoin,
  373. LogImpact.Low,
  374. $"{player.Name} late joined the round as an Observer with {ToPrettyString(ghost):entity}.");
  375. }
  376. #region Spawn Points
  377. public EntityCoordinates GetObserverSpawnPoint()
  378. {
  379. _possiblePositions.Clear();
  380. var spawnPointQuery = EntityManager.EntityQueryEnumerator<SpawnPointComponent, TransformComponent>();
  381. while (spawnPointQuery.MoveNext(out var uid, out var point, out var transform))
  382. {
  383. if (point.SpawnType != SpawnPointType.Observer
  384. || TerminatingOrDeleted(uid)
  385. || transform.MapUid == null
  386. || TerminatingOrDeleted(transform.MapUid.Value))
  387. {
  388. continue;
  389. }
  390. _possiblePositions.Add(transform.Coordinates);
  391. }
  392. var metaQuery = GetEntityQuery<MetaDataComponent>();
  393. // Fallback to a random grid.
  394. if (_possiblePositions.Count == 0)
  395. {
  396. var query = AllEntityQuery<MapGridComponent>();
  397. while (query.MoveNext(out var uid, out var grid))
  398. {
  399. if (!metaQuery.TryGetComponent(uid, out var meta) || meta.EntityPaused || TerminatingOrDeleted(uid))
  400. {
  401. continue;
  402. }
  403. _possiblePositions.Add(new EntityCoordinates(uid, Vector2.Zero));
  404. }
  405. }
  406. if (_possiblePositions.Count != 0)
  407. {
  408. // TODO: This is just here for the eye lerping.
  409. // Ideally engine would just spawn them on grid directly I guess? Right now grid traversal is handling it during
  410. // update which means we need to add a hack somewhere around it.
  411. var spawn = _robustRandom.Pick(_possiblePositions);
  412. var toMap = _transform.ToMapCoordinates(spawn);
  413. if (_mapManager.TryFindGridAt(toMap, out var gridUid, out _))
  414. {
  415. var gridXform = Transform(gridUid);
  416. return new EntityCoordinates(gridUid, Vector2.Transform(toMap.Position, _transform.GetInvWorldMatrix(gridXform)));
  417. }
  418. return spawn;
  419. }
  420. if (_mapManager.MapExists(DefaultMap))
  421. {
  422. var mapUid = _mapManager.GetMapEntityId(DefaultMap);
  423. if (!TerminatingOrDeleted(mapUid))
  424. return new EntityCoordinates(mapUid, Vector2.Zero);
  425. }
  426. // Just pick a point at this point I guess.
  427. foreach (var map in _mapManager.GetAllMapIds())
  428. {
  429. var mapUid = _mapManager.GetMapEntityId(map);
  430. if (!metaQuery.TryGetComponent(mapUid, out var meta)
  431. || meta.EntityPaused
  432. || TerminatingOrDeleted(mapUid))
  433. {
  434. continue;
  435. }
  436. return new EntityCoordinates(mapUid, Vector2.Zero);
  437. }
  438. // AAAAAAAAAAAAA
  439. // This should be an error, if it didn't cause tests to start erroring when they delete a player.
  440. _sawmill.Warning("Found no observer spawn points!");
  441. return EntityCoordinates.Invalid;
  442. }
  443. #endregion
  444. }
  445. }