SharedBodySystem.Body.cs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338
  1. using System.Linq;
  2. using System.Numerics;
  3. using Content.Shared.Body.Components;
  4. using Content.Shared.Body.Organ;
  5. using Content.Shared.Body.Part;
  6. using Content.Shared.Body.Prototypes;
  7. using Content.Shared.DragDrop;
  8. using Content.Shared.Gibbing.Components;
  9. using Content.Shared.Gibbing.Events;
  10. using Content.Shared.Gibbing.Systems;
  11. using Content.Shared.Inventory;
  12. using Robust.Shared.Audio;
  13. using Robust.Shared.Audio.Systems;
  14. using Robust.Shared.Containers;
  15. using Robust.Shared.Map;
  16. using Robust.Shared.Utility;
  17. namespace Content.Shared.Body.Systems;
  18. public partial class SharedBodySystem
  19. {
  20. /*
  21. * tl;dr of how bobby works
  22. * - BodyComponent uses a BodyPrototype as a template.
  23. * - On MapInit we spawn the root entity in the prototype and spawn all connections outwards from here
  24. * - Each "connection" is a body part (e.g. arm, hand, etc.) and each part can also contain organs.
  25. */
  26. [Dependency] private readonly InventorySystem _inventory = default!;
  27. [Dependency] private readonly GibbingSystem _gibbingSystem = default!;
  28. [Dependency] private readonly SharedAudioSystem _audioSystem = default!;
  29. private const float GibletLaunchImpulse = 8;
  30. private const float GibletLaunchImpulseVariance = 3;
  31. private void InitializeBody()
  32. {
  33. // Body here to handle root body parts.
  34. SubscribeLocalEvent<BodyComponent, EntInsertedIntoContainerMessage>(OnBodyInserted);
  35. SubscribeLocalEvent<BodyComponent, EntRemovedFromContainerMessage>(OnBodyRemoved);
  36. SubscribeLocalEvent<BodyComponent, ComponentInit>(OnBodyInit);
  37. SubscribeLocalEvent<BodyComponent, MapInitEvent>(OnBodyMapInit);
  38. SubscribeLocalEvent<BodyComponent, CanDragEvent>(OnBodyCanDrag);
  39. }
  40. private void OnBodyInserted(Entity<BodyComponent> ent, ref EntInsertedIntoContainerMessage args)
  41. {
  42. // Root body part?
  43. var slotId = args.Container.ID;
  44. if (slotId != BodyRootContainerId)
  45. return;
  46. var insertedUid = args.Entity;
  47. if (TryComp(insertedUid, out BodyPartComponent? part))
  48. {
  49. AddPart((ent, ent), (insertedUid, part), slotId);
  50. RecursiveBodyUpdate((insertedUid, part), ent);
  51. }
  52. if (TryComp(insertedUid, out OrganComponent? organ))
  53. {
  54. AddOrgan((insertedUid, organ), ent, ent);
  55. }
  56. }
  57. private void OnBodyRemoved(Entity<BodyComponent> ent, ref EntRemovedFromContainerMessage args)
  58. {
  59. // Root body part?
  60. var slotId = args.Container.ID;
  61. if (slotId != BodyRootContainerId)
  62. return;
  63. var removedUid = args.Entity;
  64. DebugTools.Assert(!TryComp(removedUid, out BodyPartComponent? b) || b.Body == ent);
  65. DebugTools.Assert(!TryComp(removedUid, out OrganComponent? o) || o.Body == ent);
  66. if (TryComp(removedUid, out BodyPartComponent? part))
  67. {
  68. RemovePart((ent, ent), (removedUid, part), slotId);
  69. RecursiveBodyUpdate((removedUid, part), null);
  70. }
  71. if (TryComp(removedUid, out OrganComponent? organ))
  72. RemoveOrgan((removedUid, organ), ent);
  73. }
  74. private void OnBodyInit(Entity<BodyComponent> ent, ref ComponentInit args)
  75. {
  76. // Setup the initial container.
  77. ent.Comp.RootContainer = Containers.EnsureContainer<ContainerSlot>(ent, BodyRootContainerId);
  78. }
  79. private void OnBodyMapInit(Entity<BodyComponent> ent, ref MapInitEvent args)
  80. {
  81. if (ent.Comp.Prototype is null)
  82. return;
  83. // One-time setup
  84. // Obviously can't run in Init to avoid double-spawns on save / load.
  85. var prototype = Prototypes.Index(ent.Comp.Prototype.Value);
  86. MapInitBody(ent, prototype);
  87. }
  88. private void MapInitBody(EntityUid bodyEntity, BodyPrototype prototype)
  89. {
  90. var protoRoot = prototype.Slots[prototype.Root];
  91. if (protoRoot.Part is null)
  92. return;
  93. // This should already handle adding the entity to the root.
  94. var rootPartUid = SpawnInContainerOrDrop(protoRoot.Part, bodyEntity, BodyRootContainerId);
  95. var rootPart = Comp<BodyPartComponent>(rootPartUid);
  96. rootPart.Body = bodyEntity;
  97. Dirty(rootPartUid, rootPart);
  98. // Setup the rest of the body entities.
  99. SetupOrgans((rootPartUid, rootPart), protoRoot.Organs);
  100. MapInitParts(rootPartUid, prototype);
  101. }
  102. private void OnBodyCanDrag(Entity<BodyComponent> ent, ref CanDragEvent args)
  103. {
  104. args.Handled = true;
  105. }
  106. /// <summary>
  107. /// Sets up all of the relevant body parts for a particular body entity and root part.
  108. /// </summary>
  109. private void MapInitParts(EntityUid rootPartId, BodyPrototype prototype)
  110. {
  111. // Start at the root part and traverse the body graph, setting up parts as we go.
  112. // Basic BFS pathfind.
  113. var rootSlot = prototype.Root;
  114. var frontier = new Queue<string>();
  115. frontier.Enqueue(rootSlot);
  116. // Child -> Parent connection.
  117. var cameFrom = new Dictionary<string, string>();
  118. cameFrom[rootSlot] = rootSlot;
  119. // Maps slot to its relevant entity.
  120. var cameFromEntities = new Dictionary<string, EntityUid>();
  121. cameFromEntities[rootSlot] = rootPartId;
  122. while (frontier.TryDequeue(out var currentSlotId))
  123. {
  124. var currentSlot = prototype.Slots[currentSlotId];
  125. foreach (var connection in currentSlot.Connections)
  126. {
  127. // Already been handled
  128. if (!cameFrom.TryAdd(connection, currentSlotId))
  129. continue;
  130. // Setup part
  131. var connectionSlot = prototype.Slots[connection];
  132. var parentEntity = cameFromEntities[currentSlotId];
  133. var parentPartComponent = Comp<BodyPartComponent>(parentEntity);
  134. // Spawn the entity on the target
  135. // then get the body part type, create the slot, and finally
  136. // we can insert it into the container.
  137. var childPart = Spawn(connectionSlot.Part, new EntityCoordinates(parentEntity, Vector2.Zero));
  138. cameFromEntities[connection] = childPart;
  139. var childPartComponent = Comp<BodyPartComponent>(childPart);
  140. var partSlot = CreatePartSlot(parentEntity, connection, childPartComponent.PartType, parentPartComponent);
  141. var cont = Containers.GetContainer(parentEntity, GetPartSlotContainerId(connection));
  142. if (partSlot is null || !Containers.Insert(childPart, cont))
  143. {
  144. Log.Error($"Could not create slot for connection {connection} in body {prototype.ID}");
  145. QueueDel(childPart);
  146. continue;
  147. }
  148. // Add organs
  149. SetupOrgans((childPart, childPartComponent), connectionSlot.Organs);
  150. // Enqueue it so we can also get its neighbors.
  151. frontier.Enqueue(connection);
  152. }
  153. }
  154. }
  155. private void SetupOrgans(Entity<BodyPartComponent> ent, Dictionary<string, string> organs)
  156. {
  157. foreach (var (organSlotId, organProto) in organs)
  158. {
  159. var slot = CreateOrganSlot((ent, ent), organSlotId);
  160. SpawnInContainerOrDrop(organProto, ent, GetOrganContainerId(organSlotId));
  161. if (slot is null)
  162. {
  163. Log.Error($"Could not create organ for slot {organSlotId} in {ToPrettyString(ent)}");
  164. }
  165. }
  166. }
  167. /// <summary>
  168. /// Gets all body containers on this entity including the root one.
  169. /// </summary>
  170. public IEnumerable<BaseContainer> GetBodyContainers(
  171. EntityUid id,
  172. BodyComponent? body = null,
  173. BodyPartComponent? rootPart = null)
  174. {
  175. if (!Resolve(id, ref body, logMissing: false)
  176. || body.RootContainer.ContainedEntity is null
  177. || !Resolve(body.RootContainer.ContainedEntity.Value, ref rootPart))
  178. {
  179. yield break;
  180. }
  181. yield return body.RootContainer;
  182. foreach (var childContainer in GetPartContainers(body.RootContainer.ContainedEntity.Value, rootPart))
  183. {
  184. yield return childContainer;
  185. }
  186. }
  187. /// <summary>
  188. /// Gets all child body parts of this entity, including the root entity.
  189. /// </summary>
  190. public IEnumerable<(EntityUid Id, BodyPartComponent Component)> GetBodyChildren(
  191. EntityUid? id,
  192. BodyComponent? body = null,
  193. BodyPartComponent? rootPart = null)
  194. {
  195. if (id is null
  196. || !Resolve(id.Value, ref body, logMissing: false)
  197. || body.RootContainer.ContainedEntity is null
  198. || !Resolve(body.RootContainer.ContainedEntity.Value, ref rootPart))
  199. {
  200. yield break;
  201. }
  202. foreach (var child in GetBodyPartChildren(body.RootContainer.ContainedEntity.Value, rootPart))
  203. {
  204. yield return child;
  205. }
  206. }
  207. public IEnumerable<(EntityUid Id, OrganComponent Component)> GetBodyOrgans(
  208. EntityUid? bodyId,
  209. BodyComponent? body = null)
  210. {
  211. if (bodyId is null || !Resolve(bodyId.Value, ref body, logMissing: false))
  212. yield break;
  213. foreach (var part in GetBodyChildren(bodyId, body))
  214. {
  215. foreach (var organ in GetPartOrgans(part.Id, part.Component))
  216. {
  217. yield return organ;
  218. }
  219. }
  220. }
  221. /// <summary>
  222. /// Returns all body part slots for this entity.
  223. /// </summary>
  224. /// <param name="bodyId"></param>
  225. /// <param name="body"></param>
  226. /// <returns></returns>
  227. public IEnumerable<BodyPartSlot> GetBodyAllSlots(
  228. EntityUid bodyId,
  229. BodyComponent? body = null)
  230. {
  231. if (!Resolve(bodyId, ref body, logMissing: false)
  232. || body.RootContainer.ContainedEntity is null)
  233. {
  234. yield break;
  235. }
  236. foreach (var slot in GetAllBodyPartSlots(body.RootContainer.ContainedEntity.Value))
  237. {
  238. yield return slot;
  239. }
  240. }
  241. public virtual HashSet<EntityUid> GibBody(
  242. EntityUid bodyId,
  243. bool gibOrgans = false,
  244. BodyComponent? body = null,
  245. bool launchGibs = true,
  246. Vector2? splatDirection = null,
  247. float splatModifier = 1,
  248. Angle splatCone = default,
  249. SoundSpecifier? gibSoundOverride = null)
  250. {
  251. var gibs = new HashSet<EntityUid>();
  252. if (!Resolve(bodyId, ref body, logMissing: false))
  253. return gibs;
  254. var root = GetRootPartOrNull(bodyId, body);
  255. if (root != null && TryComp(root.Value.Entity, out GibbableComponent? gibbable))
  256. {
  257. gibSoundOverride ??= gibbable.GibSound;
  258. }
  259. var parts = GetBodyChildren(bodyId, body).ToArray();
  260. gibs.EnsureCapacity(parts.Length);
  261. foreach (var part in parts)
  262. {
  263. _gibbingSystem.TryGibEntityWithRef(bodyId, part.Id, GibType.Gib, GibContentsOption.Skip, ref gibs,
  264. playAudio: false, launchGibs:true, launchDirection:splatDirection, launchImpulse: GibletLaunchImpulse * splatModifier,
  265. launchImpulseVariance:GibletLaunchImpulseVariance, launchCone: splatCone);
  266. if (!gibOrgans)
  267. continue;
  268. foreach (var organ in GetPartOrgans(part.Id, part.Component))
  269. {
  270. _gibbingSystem.TryGibEntityWithRef(bodyId, organ.Id, GibType.Drop, GibContentsOption.Skip,
  271. ref gibs, playAudio: false, launchImpulse: GibletLaunchImpulse * splatModifier,
  272. launchImpulseVariance:GibletLaunchImpulseVariance, launchCone: splatCone);
  273. }
  274. }
  275. var bodyTransform = Transform(bodyId);
  276. if (TryComp<InventoryComponent>(bodyId, out var inventory))
  277. {
  278. foreach (var item in _inventory.GetHandOrInventoryEntities(bodyId))
  279. {
  280. SharedTransform.DropNextTo(item, (bodyId, bodyTransform));
  281. gibs.Add(item);
  282. }
  283. }
  284. _audioSystem.PlayPredicted(gibSoundOverride, bodyTransform.Coordinates, null);
  285. return gibs;
  286. }
  287. }