StationSpawningSystem.cs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282
  1. using Content.Server.Access.Systems;
  2. using Content.Server.Humanoid;
  3. using Content.Server.IdentityManagement;
  4. using Content.Server.Mind.Commands;
  5. using Content.Server.PDA;
  6. using Content.Server.Station.Components;
  7. using Content.Shared.Access.Components;
  8. using Content.Shared.Access.Systems;
  9. using Content.Shared.CCVar;
  10. using Content.Shared.Clothing;
  11. using Content.Shared.DetailExaminable;
  12. using Content.Shared.Humanoid;
  13. using Content.Shared.Humanoid.Prototypes;
  14. using Content.Shared.PDA;
  15. using Content.Shared.Preferences;
  16. using Content.Shared.Preferences.Loadouts;
  17. using Content.Shared.Random;
  18. using Content.Shared.Random.Helpers;
  19. using Content.Shared.Roles;
  20. using Content.Shared.Station;
  21. using JetBrains.Annotations;
  22. using Robust.Shared.Configuration;
  23. using Robust.Shared.Map;
  24. using Robust.Shared.Player;
  25. using Robust.Shared.Prototypes;
  26. using Robust.Shared.Random;
  27. using Robust.Shared.Utility;
  28. namespace Content.Server.Station.Systems;
  29. /// <summary>
  30. /// Manages spawning into the game, tracking available spawn points.
  31. /// Also provides helpers for spawning in the player's mob.
  32. /// </summary>
  33. [PublicAPI]
  34. public sealed class StationSpawningSystem : SharedStationSpawningSystem
  35. {
  36. [Dependency] private readonly SharedAccessSystem _accessSystem = default!;
  37. [Dependency] private readonly ActorSystem _actors = default!;
  38. [Dependency] private readonly IdCardSystem _cardSystem = default!;
  39. [Dependency] private readonly IConfigurationManager _configurationManager = default!;
  40. [Dependency] private readonly HumanoidAppearanceSystem _humanoidSystem = default!;
  41. [Dependency] private readonly IdentitySystem _identity = default!;
  42. [Dependency] private readonly MetaDataSystem _metaSystem = default!;
  43. [Dependency] private readonly PdaSystem _pdaSystem = default!;
  44. [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
  45. [Dependency] private readonly IRobustRandom _random = default!;
  46. private bool _randomizeCharacters;
  47. /// <inheritdoc/>
  48. public override void Initialize()
  49. {
  50. base.Initialize();
  51. Subs.CVar(_configurationManager, CCVars.ICRandomCharacters, e => _randomizeCharacters = e, true);
  52. }
  53. /// <summary>
  54. /// Attempts to spawn a player character onto the given station.
  55. /// </summary>
  56. /// <param name="station">Station to spawn onto.</param>
  57. /// <param name="job">The job to assign, if any.</param>
  58. /// <param name="profile">The character profile to use, if any.</param>
  59. /// <param name="stationSpawning">Resolve pattern, the station spawning component for the station.</param>
  60. /// <returns>The resulting player character, if any.</returns>
  61. /// <exception cref="ArgumentException">Thrown when the given station is not a station.</exception>
  62. /// <remarks>
  63. /// This only spawns the character, and does none of the mind-related setup you'd need for it to be playable.
  64. /// </remarks>
  65. public EntityUid? SpawnPlayerCharacterOnStation(EntityUid? station, ProtoId<JobPrototype>? job, HumanoidCharacterProfile? profile, StationSpawningComponent? stationSpawning = null)
  66. {
  67. if (station != null && !Resolve(station.Value, ref stationSpawning))
  68. throw new ArgumentException("Tried to use a non-station entity as a station!", nameof(station));
  69. var ev = new PlayerSpawningEvent(job, profile, station);
  70. RaiseLocalEvent(ev);
  71. DebugTools.Assert(ev.SpawnResult is { Valid: true } or null);
  72. return ev.SpawnResult;
  73. }
  74. //TODO: Figure out if everything in the player spawning region belongs somewhere else.
  75. #region Player spawning helpers
  76. /// <summary>
  77. /// Spawns in a player's mob according to their job and character information at the given coordinates.
  78. /// Used by systems that need to handle spawning players.
  79. /// </summary>
  80. /// <param name="coordinates">Coordinates to spawn the character at.</param>
  81. /// <param name="job">Job to assign to the character, if any.</param>
  82. /// <param name="profile">Appearance profile to use for the character.</param>
  83. /// <param name="station">The station this player is being spawned on.</param>
  84. /// <param name="entity">The entity to use, if one already exists.</param>
  85. /// <returns>The spawned entity</returns>
  86. public EntityUid SpawnPlayerMob(
  87. EntityCoordinates coordinates,
  88. ProtoId<JobPrototype>? job,
  89. HumanoidCharacterProfile? profile,
  90. EntityUid? station,
  91. EntityUid? entity = null)
  92. {
  93. _prototypeManager.TryIndex(job ?? string.Empty, out var prototype);
  94. RoleLoadout? loadout = null;
  95. // Need to get the loadout up-front to handle names if we use an entity spawn override.
  96. var jobLoadout = LoadoutSystem.GetJobPrototype(prototype?.ID);
  97. if (_prototypeManager.TryIndex(jobLoadout, out RoleLoadoutPrototype? roleProto))
  98. {
  99. profile?.Loadouts.TryGetValue(jobLoadout, out loadout);
  100. // Set to default if not present
  101. if (loadout == null)
  102. {
  103. loadout = new RoleLoadout(jobLoadout);
  104. loadout.SetDefault(profile, _actors.GetSession(entity), _prototypeManager);
  105. }
  106. }
  107. // If we're not spawning a humanoid, we're gonna exit early without doing all the humanoid stuff.
  108. if (prototype?.JobEntity != null)
  109. {
  110. DebugTools.Assert(entity is null);
  111. var jobEntity = EntityManager.SpawnEntity(prototype.JobEntity, coordinates);
  112. MakeSentientCommand.MakeSentient(jobEntity, EntityManager);
  113. // Make sure custom names get handled, what is gameticker control flow whoopy.
  114. if (loadout != null)
  115. {
  116. EquipRoleName(jobEntity, loadout, roleProto!);
  117. }
  118. DoJobSpecials(job, jobEntity);
  119. _identity.QueueIdentityUpdate(jobEntity);
  120. return jobEntity;
  121. }
  122. string speciesId;
  123. if (_randomizeCharacters)
  124. {
  125. var weightId = _configurationManager.GetCVar(CCVars.ICRandomSpeciesWeights);
  126. var weights = _prototypeManager.Index<WeightedRandomSpeciesPrototype>(weightId);
  127. speciesId = weights.Pick(_random);
  128. }
  129. else if (profile != null)
  130. {
  131. speciesId = profile.Species;
  132. }
  133. else
  134. {
  135. speciesId = SharedHumanoidAppearanceSystem.DefaultSpecies;
  136. }
  137. if (!_prototypeManager.TryIndex<SpeciesPrototype>(speciesId, out var species))
  138. throw new ArgumentException($"Invalid species prototype was used: {speciesId}");
  139. entity ??= Spawn(species.Prototype, coordinates);
  140. if (_randomizeCharacters)
  141. {
  142. profile = HumanoidCharacterProfile.RandomWithSpecies(speciesId);
  143. }
  144. if (loadout != null)
  145. {
  146. EquipRoleLoadout(entity.Value, loadout, roleProto!);
  147. }
  148. if (prototype?.StartingGear != null)
  149. {
  150. var startingGear = _prototypeManager.Index<StartingGearPrototype>(prototype.StartingGear);
  151. EquipStartingGear(entity.Value, startingGear, raiseEvent: false);
  152. }
  153. var gearEquippedEv = new StartingGearEquippedEvent(entity.Value);
  154. RaiseLocalEvent(entity.Value, ref gearEquippedEv);
  155. if (profile != null)
  156. {
  157. if (prototype != null)
  158. SetPdaAndIdCardData(entity.Value, profile.Name, prototype, station);
  159. _humanoidSystem.LoadProfile(entity.Value, profile);
  160. _metaSystem.SetEntityName(entity.Value, profile.Name);
  161. if (profile.FlavorText != "" && _configurationManager.GetCVar(CCVars.FlavorText))
  162. {
  163. AddComp<DetailExaminableComponent>(entity.Value).Content = profile.FlavorText;
  164. }
  165. }
  166. DoJobSpecials(job, entity.Value);
  167. _identity.QueueIdentityUpdate(entity.Value);
  168. return entity.Value;
  169. }
  170. private void DoJobSpecials(ProtoId<JobPrototype>? job, EntityUid entity)
  171. {
  172. if (!_prototypeManager.TryIndex(job ?? string.Empty, out JobPrototype? prototype))
  173. return;
  174. foreach (var jobSpecial in prototype.Special)
  175. {
  176. jobSpecial.AfterEquip(entity);
  177. }
  178. }
  179. /// <summary>
  180. /// Sets the ID card and PDA name, job, and access data.
  181. /// </summary>
  182. /// <param name="entity">Entity to load out.</param>
  183. /// <param name="characterName">Character name to use for the ID.</param>
  184. /// <param name="jobPrototype">Job prototype to use for the PDA and ID.</param>
  185. /// <param name="station">The station this player is being spawned on.</param>
  186. public void SetPdaAndIdCardData(EntityUid entity, string characterName, JobPrototype jobPrototype, EntityUid? station)
  187. {
  188. if (!InventorySystem.TryGetSlotEntity(entity, "id", out var idUid))
  189. return;
  190. var cardId = idUid.Value;
  191. if (TryComp<PdaComponent>(idUid, out var pdaComponent) && pdaComponent.ContainedId != null)
  192. cardId = pdaComponent.ContainedId.Value;
  193. if (!TryComp<IdCardComponent>(cardId, out var card))
  194. return;
  195. _cardSystem.TryChangeFullName(cardId, characterName, card);
  196. _cardSystem.TryChangeJobTitle(cardId, jobPrototype.LocalizedName, card);
  197. if (_prototypeManager.TryIndex(jobPrototype.Icon, out var jobIcon))
  198. _cardSystem.TryChangeJobIcon(cardId, jobIcon, card);
  199. var extendedAccess = false;
  200. if (station != null)
  201. {
  202. var data = Comp<StationJobsComponent>(station.Value);
  203. extendedAccess = data.ExtendedAccess;
  204. }
  205. _accessSystem.SetAccessToJob(cardId, jobPrototype, extendedAccess);
  206. if (pdaComponent != null)
  207. _pdaSystem.SetOwner(idUid.Value, pdaComponent, entity, characterName);
  208. }
  209. #endregion Player spawning helpers
  210. }
  211. /// <summary>
  212. /// Ordered broadcast event fired on any spawner eligible to attempt to spawn a player.
  213. /// This event's success is measured by if SpawnResult is not null.
  214. /// You should not make this event's success rely on random chance.
  215. /// This event is designed to use ordered handling. You probably want SpawnPointSystem to be the last handler.
  216. /// </summary>
  217. [PublicAPI]
  218. public sealed class PlayerSpawningEvent : EntityEventArgs
  219. {
  220. /// <summary>
  221. /// The entity spawned, if any. You should set this if you succeed at spawning the character, and leave it alone if it's not null.
  222. /// </summary>
  223. public EntityUid? SpawnResult;
  224. /// <summary>
  225. /// The job to use, if any.
  226. /// </summary>
  227. public readonly ProtoId<JobPrototype>? Job;
  228. /// <summary>
  229. /// The profile to use, if any.
  230. /// </summary>
  231. public readonly HumanoidCharacterProfile? HumanoidCharacterProfile;
  232. /// <summary>
  233. /// The target station, if any.
  234. /// </summary>
  235. public readonly EntityUid? Station;
  236. public PlayerSpawningEvent(ProtoId<JobPrototype>? job, HumanoidCharacterProfile? humanoidCharacterProfile, EntityUid? station)
  237. {
  238. Job = job;
  239. HumanoidCharacterProfile = humanoidCharacterProfile;
  240. Station = station;
  241. }
  242. }