StationSpawningSystem.cs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300
  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. // Equip starting gear if specified in the job prototype
  149. if (prototype != null)
  150. {
  151. StartingGearPrototype? startingGearProto = null;
  152. // Check if random gears are available and populated
  153. if (prototype.RandomStartingGears != null && prototype.RandomStartingGears.Count > 0)
  154. {
  155. var startingGearId = _random.Pick(prototype.RandomStartingGears); // Safe now
  156. _prototypeManager.TryIndex(startingGearId, out startingGearProto);
  157. }
  158. // Otherwise, check if the single starting gear is specified
  159. else if (prototype.StartingGear != null)
  160. {
  161. _prototypeManager.TryIndex(prototype.StartingGear.Value, out startingGearProto); // Safe now, using .Value for ProtoId?
  162. }
  163. // If we found a valid gear prototype (either random or specific), equip it
  164. if (startingGearProto != null)
  165. {
  166. EquipStartingGear(entity.Value, startingGearProto, raiseEvent: false);
  167. }
  168. }
  169. var gearEquippedEv = new StartingGearEquippedEvent(entity.Value);
  170. RaiseLocalEvent(entity.Value, ref gearEquippedEv);
  171. if (profile != null)
  172. {
  173. if (prototype != null)
  174. SetPdaAndIdCardData(entity.Value, profile.Name, prototype, station);
  175. _humanoidSystem.LoadProfile(entity.Value, profile);
  176. _metaSystem.SetEntityName(entity.Value, profile.Name);
  177. if (profile.FlavorText != "" && _configurationManager.GetCVar(CCVars.FlavorText))
  178. {
  179. AddComp<DetailExaminableComponent>(entity.Value).Content = profile.FlavorText;
  180. }
  181. }
  182. DoJobSpecials(job, entity.Value);
  183. _identity.QueueIdentityUpdate(entity.Value);
  184. return entity.Value;
  185. }
  186. private void DoJobSpecials(ProtoId<JobPrototype>? job, EntityUid entity)
  187. {
  188. if (!_prototypeManager.TryIndex(job ?? string.Empty, out JobPrototype? prototype))
  189. return;
  190. foreach (var jobSpecial in prototype.Special)
  191. {
  192. jobSpecial.AfterEquip(entity);
  193. }
  194. }
  195. /// <summary>
  196. /// Sets the ID card and PDA name, job, and access data.
  197. /// </summary>
  198. /// <param name="entity">Entity to load out.</param>
  199. /// <param name="characterName">Character name to use for the ID.</param>
  200. /// <param name="jobPrototype">Job prototype to use for the PDA and ID.</param>
  201. /// <param name="station">The station this player is being spawned on.</param>
  202. public void SetPdaAndIdCardData(EntityUid entity, string characterName, JobPrototype jobPrototype, EntityUid? station)
  203. {
  204. if (!InventorySystem.TryGetSlotEntity(entity, "id", out var idUid))
  205. return;
  206. var cardId = idUid.Value;
  207. if (TryComp<PdaComponent>(idUid, out var pdaComponent) && pdaComponent.ContainedId != null)
  208. cardId = pdaComponent.ContainedId.Value;
  209. if (!TryComp<IdCardComponent>(cardId, out var card))
  210. return;
  211. _cardSystem.TryChangeFullName(cardId, characterName, card);
  212. _cardSystem.TryChangeJobTitle(cardId, jobPrototype.LocalizedName, card);
  213. if (_prototypeManager.TryIndex(jobPrototype.Icon, out var jobIcon))
  214. _cardSystem.TryChangeJobIcon(cardId, jobIcon, card);
  215. var extendedAccess = false;
  216. if (station != null)
  217. {
  218. var data = Comp<StationJobsComponent>(station.Value);
  219. extendedAccess = data.ExtendedAccess;
  220. }
  221. _accessSystem.SetAccessToJob(cardId, jobPrototype, extendedAccess);
  222. if (pdaComponent != null)
  223. _pdaSystem.SetOwner(idUid.Value, pdaComponent, entity, characterName);
  224. }
  225. #endregion Player spawning helpers
  226. }
  227. /// <summary>
  228. /// Ordered broadcast event fired on any spawner eligible to attempt to spawn a player.
  229. /// This event's success is measured by if SpawnResult is not null.
  230. /// You should not make this event's success rely on random chance.
  231. /// This event is designed to use ordered handling. You probably want SpawnPointSystem to be the last handler.
  232. /// </summary>
  233. [PublicAPI]
  234. public sealed class PlayerSpawningEvent : EntityEventArgs
  235. {
  236. /// <summary>
  237. /// 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.
  238. /// </summary>
  239. public EntityUid? SpawnResult;
  240. /// <summary>
  241. /// The job to use, if any.
  242. /// </summary>
  243. public readonly ProtoId<JobPrototype>? Job;
  244. /// <summary>
  245. /// The profile to use, if any.
  246. /// </summary>
  247. public readonly HumanoidCharacterProfile? HumanoidCharacterProfile;
  248. /// <summary>
  249. /// The target station, if any.
  250. /// </summary>
  251. public readonly EntityUid? Station;
  252. public PlayerSpawningEvent(ProtoId<JobPrototype>? job, HumanoidCharacterProfile? humanoidCharacterProfile, EntityUid? station)
  253. {
  254. Job = job;
  255. HumanoidCharacterProfile = humanoidCharacterProfile;
  256. Station = station;
  257. }
  258. }