GibbingSystem.cs 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340
  1. using System.Diagnostics.CodeAnalysis;
  2. using System.Numerics;
  3. using Content.Shared.Gibbing.Components;
  4. using Content.Shared.Gibbing.Events;
  5. using Robust.Shared.Audio.Systems;
  6. using Robust.Shared.Containers;
  7. using Robust.Shared.Map;
  8. using Robust.Shared.Physics.Systems;
  9. using Robust.Shared.Prototypes;
  10. using Robust.Shared.Random;
  11. namespace Content.Shared.Gibbing.Systems;
  12. public sealed class GibbingSystem : EntitySystem
  13. {
  14. [Dependency] private readonly SharedContainerSystem _containerSystem = default!;
  15. [Dependency] private readonly SharedTransformSystem _transformSystem = default!;
  16. [Dependency] private readonly SharedAudioSystem _audioSystem = default!;
  17. [Dependency] private readonly SharedPhysicsSystem _physicsSystem = default!;
  18. [Dependency] private readonly IRobustRandom _random = default!;
  19. //TODO: (future optimization) implement a system that "caps" giblet entities by deleting the oldest ones once we reach a certain limit, customizable via CVAR
  20. /// <summary>
  21. /// Attempt to gib a specified entity. That entity must have a gibable components. This method is NOT recursive will only
  22. /// work on the target and any entities it contains (depending on gibContentsOption)
  23. /// </summary>
  24. /// <param name="outerEntity">The outermost entity we care about, used to place the dropped items</param>
  25. /// <param name="gibbable">Target entity/comp we wish to gib</param>
  26. /// <param name="gibType">What type of gibing are we performing</param>
  27. /// <param name="gibContentsOption">What type of gibing do we perform on any container contents?</param>
  28. /// <param name="droppedEntities">a hashset containing all the entities that have been dropped/created</param>
  29. /// <param name="randomSpreadMod">How much to multiply the random spread on dropped giblets(if we are dropping them!)</param>
  30. /// <param name="playAudio">Should we play audio</param>
  31. /// <param name="allowedContainers">A list of containerIds on the target that permit gibing</param>
  32. /// <param name="excludedContainers">A list of containerIds on the target that DO NOT permit gibing</param>
  33. /// <param name="launchCone">The cone we are launching giblets in (if we are launching them!)</param>
  34. /// <param name="launchGibs">Should we launch giblets or just drop them</param>
  35. /// <param name="launchDirection">The direction to launch giblets (if we are launching them!)</param>
  36. /// <param name="launchImpulse">The impluse to launch giblets at(if we are launching them!)</param>
  37. /// /// <param name="logMissingGibable">Should we log if we are missing a gibbableComp when we call this function</param>
  38. /// <param name="launchImpulseVariance">The variation in giblet launch impulse (if we are launching them!)</param>
  39. /// <returns>True if successful, false if not</returns>
  40. public bool TryGibEntity(EntityUid outerEntity, Entity<GibbableComponent?> gibbable, GibType gibType,
  41. GibContentsOption gibContentsOption,
  42. out HashSet<EntityUid> droppedEntities, bool launchGibs = true,
  43. Vector2 launchDirection = default, float launchImpulse = 0f, float launchImpulseVariance = 0f,
  44. Angle launchCone = default,
  45. float randomSpreadMod = 1.0f, bool playAudio = true, List<string>? allowedContainers = null,
  46. List<string>? excludedContainers = null, bool logMissingGibable = false)
  47. {
  48. droppedEntities = new();
  49. return TryGibEntityWithRef(outerEntity, gibbable, gibType, gibContentsOption, ref droppedEntities,
  50. launchGibs, launchDirection, launchImpulse, launchImpulseVariance, launchCone, randomSpreadMod, playAudio,
  51. allowedContainers, excludedContainers, logMissingGibable);
  52. }
  53. /// <summary>
  54. /// Attempt to gib a specified entity. That entity must have a gibable components. This method is NOT recursive will only
  55. /// work on the target and any entities it contains (depending on gibContentsOption)
  56. /// </summary>
  57. /// <param name="outerEntity">The outermost entity we care about, used to place the dropped items</param>
  58. /// <param name="gibbable">Target entity/comp we wish to gib</param>
  59. /// <param name="gibType">What type of gibing are we performing</param>
  60. /// <param name="gibContentsOption">What type of gibing do we perform on any container contents?</param>
  61. /// <param name="droppedEntities">a hashset containing all the entities that have been dropped/created</param>
  62. /// <param name="randomSpreadMod">How much to multiply the random spread on dropped giblets(if we are dropping them!)</param>
  63. /// <param name="playAudio">Should we play audio</param>
  64. /// <param name="allowedContainers">A list of containerIds on the target that permit gibing</param>
  65. /// <param name="excludedContainers">A list of containerIds on the target that DO NOT permit gibing</param>
  66. /// <param name="launchCone">The cone we are launching giblets in (if we are launching them!)</param>
  67. /// <param name="launchGibs">Should we launch giblets or just drop them</param>
  68. /// <param name="launchDirection">The direction to launch giblets (if we are launching them!)</param>
  69. /// <param name="launchImpulse">The impluse to launch giblets at(if we are launching them!)</param>
  70. /// <param name="launchImpulseVariance">The variation in giblet launch impulse (if we are launching them!)</param>
  71. /// <param name="logMissingGibable">Should we log if we are missing a gibbableComp when we call this function</param>
  72. /// <returns>True if successful, false if not</returns>
  73. public bool TryGibEntityWithRef(
  74. EntityUid outerEntity,
  75. Entity<GibbableComponent?> gibbable,
  76. GibType gibType,
  77. GibContentsOption gibContentsOption,
  78. ref HashSet<EntityUid> droppedEntities,
  79. bool launchGibs = true,
  80. Vector2? launchDirection = null,
  81. float launchImpulse = 0f,
  82. float launchImpulseVariance = 0f,
  83. Angle launchCone = default,
  84. float randomSpreadMod = 1.0f,
  85. bool playAudio = true,
  86. List<string>? allowedContainers = null,
  87. List<string>? excludedContainers = null,
  88. bool logMissingGibable = false)
  89. {
  90. if (!Resolve(gibbable, ref gibbable.Comp, logMissing: false))
  91. {
  92. DropEntity(gibbable, Transform(outerEntity), randomSpreadMod, ref droppedEntities,
  93. launchGibs, launchDirection, launchImpulse, launchImpulseVariance, launchCone);
  94. if (logMissingGibable)
  95. {
  96. Log.Warning($"{ToPrettyString(gibbable)} does not have a GibbableComponent! " +
  97. $"This is not required but may cause issues contained items to not be dropped.");
  98. }
  99. return false;
  100. }
  101. if (gibType == GibType.Skip && gibContentsOption == GibContentsOption.Skip)
  102. return true;
  103. if (launchGibs)
  104. {
  105. randomSpreadMod = 0;
  106. }
  107. var parentXform = Transform(outerEntity);
  108. HashSet<BaseContainer> validContainers = new();
  109. var gibContentsAttempt =
  110. new AttemptEntityContentsGibEvent(gibbable, gibContentsOption, allowedContainers, excludedContainers);
  111. RaiseLocalEvent(gibbable, ref gibContentsAttempt);
  112. foreach (var container in _containerSystem.GetAllContainers(gibbable))
  113. {
  114. var valid = true;
  115. if (allowedContainers != null)
  116. valid = allowedContainers.Contains(container.ID);
  117. if (excludedContainers != null)
  118. valid = valid && !excludedContainers.Contains(container.ID);
  119. if (valid)
  120. validContainers.Add(container);
  121. }
  122. switch (gibContentsOption)
  123. {
  124. case GibContentsOption.Skip:
  125. break;
  126. case GibContentsOption.Drop:
  127. {
  128. foreach (var container in validContainers)
  129. {
  130. foreach (var ent in container.ContainedEntities)
  131. {
  132. DropEntity(new Entity<GibbableComponent?>(ent, null), parentXform, randomSpreadMod,
  133. ref droppedEntities, launchGibs,
  134. launchDirection, launchImpulse, launchImpulseVariance, launchCone);
  135. }
  136. }
  137. break;
  138. }
  139. case GibContentsOption.Gib:
  140. {
  141. foreach (var container in validContainers)
  142. {
  143. foreach (var ent in container.ContainedEntities)
  144. {
  145. GibEntity(new Entity<GibbableComponent?>(ent, null), parentXform, randomSpreadMod,
  146. ref droppedEntities, launchGibs,
  147. launchDirection, launchImpulse, launchImpulseVariance, launchCone);
  148. }
  149. }
  150. break;
  151. }
  152. }
  153. switch (gibType)
  154. {
  155. case GibType.Skip:
  156. break;
  157. case GibType.Drop:
  158. {
  159. DropEntity(gibbable, parentXform, randomSpreadMod, ref droppedEntities, launchGibs,
  160. launchDirection, launchImpulse, launchImpulseVariance, launchCone);
  161. break;
  162. }
  163. case GibType.Gib:
  164. {
  165. GibEntity(gibbable, parentXform, randomSpreadMod, ref droppedEntities, launchGibs,
  166. launchDirection, launchImpulse, launchImpulseVariance, launchCone);
  167. break;
  168. }
  169. }
  170. if (playAudio)
  171. {
  172. _audioSystem.PlayPredicted(gibbable.Comp.GibSound, parentXform.Coordinates, null);
  173. }
  174. if (gibType == GibType.Gib)
  175. QueueDel(gibbable);
  176. return true;
  177. }
  178. private void DropEntity(Entity<GibbableComponent?> gibbable, TransformComponent parentXform, float randomSpreadMod,
  179. ref HashSet<EntityUid> droppedEntities, bool flingEntity, Vector2? scatterDirection, float scatterImpulse,
  180. float scatterImpulseVariance, Angle scatterCone)
  181. {
  182. var gibCount = 0;
  183. if (Resolve(gibbable, ref gibbable.Comp, logMissing: false))
  184. {
  185. gibCount = gibbable.Comp.GibCount;
  186. }
  187. var gibAttemptEvent = new AttemptEntityGibEvent(gibbable, gibCount, GibType.Drop);
  188. RaiseLocalEvent(gibbable, ref gibAttemptEvent);
  189. switch (gibAttemptEvent.GibType)
  190. {
  191. case GibType.Skip:
  192. return;
  193. case GibType.Gib:
  194. GibEntity(gibbable, parentXform, randomSpreadMod, ref droppedEntities, flingEntity, scatterDirection,
  195. scatterImpulse, scatterImpulseVariance, scatterCone, deleteTarget: false);
  196. return;
  197. }
  198. _transformSystem.AttachToGridOrMap(gibbable);
  199. _transformSystem.SetCoordinates(gibbable, parentXform.Coordinates);
  200. _transformSystem.SetWorldRotation(gibbable, _random.NextAngle());
  201. droppedEntities.Add(gibbable);
  202. if (flingEntity)
  203. {
  204. FlingDroppedEntity(gibbable, scatterDirection, scatterImpulse, scatterImpulseVariance, scatterCone);
  205. }
  206. var gibbedEvent = new EntityGibbedEvent(gibbable, new List<EntityUid> {gibbable});
  207. RaiseLocalEvent(gibbable, ref gibbedEvent);
  208. }
  209. private List<EntityUid> GibEntity(Entity<GibbableComponent?> gibbable, TransformComponent parentXform,
  210. float randomSpreadMod,
  211. ref HashSet<EntityUid> droppedEntities, bool flingEntity, Vector2? scatterDirection, float scatterImpulse,
  212. float scatterImpulseVariance, Angle scatterCone, bool deleteTarget = true)
  213. {
  214. var localGibs = new List<EntityUid>();
  215. var gibCount = 0;
  216. var gibProtoCount = 0;
  217. if (Resolve(gibbable, ref gibbable.Comp, logMissing: false))
  218. {
  219. gibCount = gibbable.Comp.GibCount;
  220. gibProtoCount = gibbable.Comp.GibPrototypes.Count;
  221. }
  222. var gibAttemptEvent = new AttemptEntityGibEvent(gibbable, gibCount, GibType.Drop);
  223. RaiseLocalEvent(gibbable, ref gibAttemptEvent);
  224. switch (gibAttemptEvent.GibType)
  225. {
  226. case GibType.Skip:
  227. return localGibs;
  228. case GibType.Drop:
  229. DropEntity(gibbable, parentXform, randomSpreadMod, ref droppedEntities, flingEntity,
  230. scatterDirection, scatterImpulse, scatterImpulseVariance, scatterCone);
  231. localGibs.Add(gibbable);
  232. return localGibs;
  233. }
  234. if (gibbable.Comp != null && gibProtoCount > 0)
  235. {
  236. if (flingEntity)
  237. {
  238. for (var i = 0; i < gibAttemptEvent.GibletCount; i++)
  239. {
  240. if (!TryCreateRandomGiblet(gibbable.Comp, parentXform.Coordinates, false, out var giblet,
  241. randomSpreadMod))
  242. continue;
  243. FlingDroppedEntity(giblet.Value, scatterDirection, scatterImpulse, scatterImpulseVariance,
  244. scatterCone);
  245. droppedEntities.Add(giblet.Value);
  246. }
  247. }
  248. else
  249. {
  250. for (var i = 0; i < gibAttemptEvent.GibletCount; i++)
  251. {
  252. if (TryCreateRandomGiblet(gibbable.Comp, parentXform.Coordinates, false, out var giblet,
  253. randomSpreadMod))
  254. droppedEntities.Add(giblet.Value);
  255. }
  256. }
  257. }
  258. _transformSystem.AttachToGridOrMap(gibbable, Transform(gibbable));
  259. if (flingEntity)
  260. {
  261. FlingDroppedEntity(gibbable, scatterDirection, scatterImpulse, scatterImpulseVariance, scatterCone);
  262. }
  263. var gibbedEvent = new EntityGibbedEvent(gibbable, localGibs);
  264. RaiseLocalEvent(gibbable, ref gibbedEvent);
  265. if (deleteTarget)
  266. QueueDel(gibbable);
  267. return localGibs;
  268. }
  269. public bool TryCreateRandomGiblet(Entity<GibbableComponent?> gibbable, [NotNullWhen(true)] out EntityUid? gibletEntity,
  270. float randomSpreadModifier = 1.0f, bool playSound = true)
  271. {
  272. gibletEntity = null;
  273. return Resolve(gibbable, ref gibbable.Comp) && TryCreateRandomGiblet(gibbable.Comp, Transform(gibbable).Coordinates,
  274. playSound, out gibletEntity, randomSpreadModifier);
  275. }
  276. public bool TryCreateAndFlingRandomGiblet(Entity<GibbableComponent?> gibbable, [NotNullWhen(true)] out EntityUid? gibletEntity,
  277. Vector2 scatterDirection, float force, float scatterImpulseVariance, Angle scatterCone = default,
  278. bool playSound = true)
  279. {
  280. gibletEntity = null;
  281. if (!Resolve(gibbable, ref gibbable.Comp) ||
  282. !TryCreateRandomGiblet(gibbable.Comp, Transform(gibbable).Coordinates, playSound, out gibletEntity))
  283. return false;
  284. FlingDroppedEntity(gibletEntity.Value, scatterDirection, force, scatterImpulseVariance, scatterCone);
  285. return true;
  286. }
  287. private void FlingDroppedEntity(EntityUid target, Vector2? direction, float impulse, float impulseVariance,
  288. Angle scatterConeAngle)
  289. {
  290. var scatterAngle = direction?.ToAngle() ?? _random.NextAngle();
  291. var scatterVector = _random.NextAngle(scatterAngle - scatterConeAngle / 2, scatterAngle + scatterConeAngle / 2)
  292. .ToVec() * (impulse + _random.NextFloat(impulseVariance));
  293. _physicsSystem.ApplyLinearImpulse(target, scatterVector);
  294. }
  295. private bool TryCreateRandomGiblet(GibbableComponent gibbable, EntityCoordinates coords,
  296. bool playSound, [NotNullWhen(true)] out EntityUid? gibletEntity, float? randomSpreadModifier = null)
  297. {
  298. gibletEntity = null;
  299. if (gibbable.GibPrototypes.Count == 0)
  300. return false;
  301. gibletEntity = Spawn(gibbable.GibPrototypes[_random.Next(0, gibbable.GibPrototypes.Count)],
  302. randomSpreadModifier == null
  303. ? coords
  304. : coords.Offset(_random.NextVector2(gibbable.GibScatterRange * randomSpreadModifier.Value)));
  305. if (playSound)
  306. _audioSystem.PlayPredicted(gibbable.GibSound, coords, null);
  307. _transformSystem.SetWorldRotation(gibletEntity.Value, _random.NextAngle());
  308. return true;
  309. }
  310. }