1
0

BloodstreamSystem.cs 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505
  1. using Content.Server.Body.Components;
  2. using Content.Server.EntityEffects.Effects;
  3. using Content.Server.Fluids.EntitySystems;
  4. using Content.Server.Popups;
  5. using Content.Shared.Alert;
  6. using Content.Shared.Chemistry.Components;
  7. using Content.Shared.Chemistry.EntitySystems;
  8. using Content.Shared.Chemistry.Reaction;
  9. using Content.Shared.Chemistry.Reagent;
  10. using Content.Shared.Damage;
  11. using Content.Shared.Damage.Prototypes;
  12. using Content.Shared.Drunk;
  13. using Content.Shared.FixedPoint;
  14. using Content.Shared.Forensics;
  15. using Content.Shared.Forensics.Components;
  16. using Content.Shared.HealthExaminable;
  17. using Content.Shared.Mobs.Systems;
  18. using Content.Shared.Popups;
  19. using Content.Shared.Rejuvenate;
  20. using Content.Shared.Speech.EntitySystems;
  21. using Robust.Server.Audio;
  22. using Robust.Shared.Prototypes;
  23. using Robust.Shared.Random;
  24. using Robust.Shared.Timing;
  25. namespace Content.Server.Body.Systems;
  26. public sealed class BloodstreamSystem : EntitySystem
  27. {
  28. [Dependency] private readonly IGameTiming _gameTiming = default!;
  29. [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
  30. [Dependency] private readonly IRobustRandom _robustRandom = default!;
  31. [Dependency] private readonly AudioSystem _audio = default!;
  32. [Dependency] private readonly DamageableSystem _damageableSystem = default!;
  33. [Dependency] private readonly PopupSystem _popupSystem = default!;
  34. [Dependency] private readonly PuddleSystem _puddleSystem = default!;
  35. [Dependency] private readonly MobStateSystem _mobStateSystem = default!;
  36. [Dependency] private readonly SharedDrunkSystem _drunkSystem = default!;
  37. [Dependency] private readonly SharedSolutionContainerSystem _solutionContainerSystem = default!;
  38. [Dependency] private readonly SharedStutteringSystem _stutteringSystem = default!;
  39. [Dependency] private readonly AlertsSystem _alertsSystem = default!;
  40. public override void Initialize()
  41. {
  42. base.Initialize();
  43. SubscribeLocalEvent<BloodstreamComponent, ComponentInit>(OnComponentInit);
  44. SubscribeLocalEvent<BloodstreamComponent, MapInitEvent>(OnMapInit);
  45. SubscribeLocalEvent<BloodstreamComponent, EntityUnpausedEvent>(OnUnpaused);
  46. SubscribeLocalEvent<BloodstreamComponent, DamageChangedEvent>(OnDamageChanged);
  47. SubscribeLocalEvent<BloodstreamComponent, HealthBeingExaminedEvent>(OnHealthBeingExamined);
  48. SubscribeLocalEvent<BloodstreamComponent, BeingGibbedEvent>(OnBeingGibbed);
  49. SubscribeLocalEvent<BloodstreamComponent, ApplyMetabolicMultiplierEvent>(OnApplyMetabolicMultiplier);
  50. SubscribeLocalEvent<BloodstreamComponent, ReactionAttemptEvent>(OnReactionAttempt);
  51. SubscribeLocalEvent<BloodstreamComponent, SolutionRelayEvent<ReactionAttemptEvent>>(OnReactionAttempt);
  52. SubscribeLocalEvent<BloodstreamComponent, RejuvenateEvent>(OnRejuvenate);
  53. SubscribeLocalEvent<BloodstreamComponent, GenerateDnaEvent>(OnDnaGenerated);
  54. }
  55. private void OnMapInit(Entity<BloodstreamComponent> ent, ref MapInitEvent args)
  56. {
  57. ent.Comp.NextUpdate = _gameTiming.CurTime + ent.Comp.UpdateInterval;
  58. }
  59. private void OnUnpaused(Entity<BloodstreamComponent> ent, ref EntityUnpausedEvent args)
  60. {
  61. ent.Comp.NextUpdate += args.PausedTime;
  62. }
  63. private void OnReactionAttempt(Entity<BloodstreamComponent> entity, ref ReactionAttemptEvent args)
  64. {
  65. if (args.Cancelled)
  66. return;
  67. foreach (var effect in args.Reaction.Effects)
  68. {
  69. switch (effect)
  70. {
  71. case CreateEntityReactionEffect: // Prevent entities from spawning in the bloodstream
  72. case AreaReactionEffect: // No spontaneous smoke or foam leaking out of blood vessels.
  73. args.Cancelled = true;
  74. return;
  75. }
  76. }
  77. // The area-reaction effect canceling is part of avoiding smoke-fork-bombs (create two smoke bombs, that when
  78. // ingested by mobs create more smoke). This also used to act as a rapid chemical-purge, because all the
  79. // reagents would get carried away by the smoke/foam. This does still work for the stomach (I guess people vomit
  80. // up the smoke or spawned entities?).
  81. // TODO apply organ damage instead of just blocking the reaction?
  82. // Having cheese-clots form in your veins can't be good for you.
  83. }
  84. private void OnReactionAttempt(Entity<BloodstreamComponent> entity, ref SolutionRelayEvent<ReactionAttemptEvent> args)
  85. {
  86. if (args.Name != entity.Comp.BloodSolutionName
  87. && args.Name != entity.Comp.ChemicalSolutionName
  88. && args.Name != entity.Comp.BloodTemporarySolutionName)
  89. {
  90. return;
  91. }
  92. OnReactionAttempt(entity, ref args.Event);
  93. }
  94. public override void Update(float frameTime)
  95. {
  96. base.Update(frameTime);
  97. var query = EntityQueryEnumerator<BloodstreamComponent>();
  98. while (query.MoveNext(out var uid, out var bloodstream))
  99. {
  100. if (_gameTiming.CurTime < bloodstream.NextUpdate)
  101. continue;
  102. bloodstream.NextUpdate += bloodstream.UpdateInterval;
  103. if (!_solutionContainerSystem.ResolveSolution(uid, bloodstream.BloodSolutionName, ref bloodstream.BloodSolution, out var bloodSolution))
  104. continue;
  105. // Adds blood to their blood level if it is below the maximum; Blood regeneration. Must be alive.
  106. if (bloodSolution.Volume < bloodSolution.MaxVolume && !_mobStateSystem.IsDead(uid))
  107. {
  108. TryModifyBloodLevel(uid, bloodstream.BloodRefreshAmount, bloodstream);
  109. }
  110. // Removes blood from the bloodstream based on bleed amount (bleed rate)
  111. // as well as stop their bleeding to a certain extent.
  112. if (bloodstream.BleedAmount > 0)
  113. {
  114. // Blood is removed from the bloodstream at a 1-1 rate with the bleed amount
  115. TryModifyBloodLevel(uid, (-bloodstream.BleedAmount), bloodstream);
  116. // Bleed rate is reduced by the bleed reduction amount in the bloodstream component.
  117. TryModifyBleedAmount(uid, -bloodstream.BleedReductionAmount, bloodstream);
  118. }
  119. // deal bloodloss damage if their blood level is below a threshold.
  120. var bloodPercentage = GetBloodLevelPercentage(uid, bloodstream);
  121. if (bloodPercentage < bloodstream.BloodlossThreshold && !_mobStateSystem.IsDead(uid))
  122. {
  123. // bloodloss damage is based on the base value, and modified by how low your blood level is.
  124. var amt = bloodstream.BloodlossDamage / (0.1f + bloodPercentage);
  125. _damageableSystem.TryChangeDamage(uid, amt,
  126. ignoreResistances: false, interruptsDoAfters: false);
  127. // Apply dizziness as a symptom of bloodloss.
  128. // The effect is applied in a way that it will never be cleared without being healthy.
  129. // Multiplying by 2 is arbitrary but works for this case, it just prevents the time from running out
  130. _drunkSystem.TryApplyDrunkenness(
  131. uid,
  132. (float) bloodstream.UpdateInterval.TotalSeconds * 2,
  133. applySlur: false);
  134. _stutteringSystem.DoStutter(uid, bloodstream.UpdateInterval * 2, refresh: false);
  135. // storing the drunk and stutter time so we can remove it independently from other effects additions
  136. bloodstream.StatusTime += bloodstream.UpdateInterval * 2;
  137. }
  138. else if (!_mobStateSystem.IsDead(uid))
  139. {
  140. // If they're healthy, we'll try and heal some bloodloss instead.
  141. _damageableSystem.TryChangeDamage(
  142. uid,
  143. bloodstream.BloodlossHealDamage * bloodPercentage,
  144. ignoreResistances: true, interruptsDoAfters: false);
  145. // Remove the drunk effect when healthy. Should only remove the amount of drunk and stutter added by low blood level
  146. _drunkSystem.TryRemoveDrunkenessTime(uid, bloodstream.StatusTime.TotalSeconds);
  147. _stutteringSystem.DoRemoveStutterTime(uid, bloodstream.StatusTime.TotalSeconds);
  148. // Reset the drunk and stutter time to zero
  149. bloodstream.StatusTime = TimeSpan.Zero;
  150. }
  151. }
  152. }
  153. private void OnComponentInit(Entity<BloodstreamComponent> entity, ref ComponentInit args)
  154. {
  155. if (!_solutionContainerSystem.EnsureSolution(entity.Owner,
  156. entity.Comp.ChemicalSolutionName,
  157. out var chemicalSolution) ||
  158. !_solutionContainerSystem.EnsureSolution(entity.Owner,
  159. entity.Comp.BloodSolutionName,
  160. out var bloodSolution) ||
  161. !_solutionContainerSystem.EnsureSolution(entity.Owner,
  162. entity.Comp.BloodTemporarySolutionName,
  163. out var tempSolution))
  164. return;
  165. chemicalSolution.MaxVolume = entity.Comp.ChemicalMaxVolume;
  166. bloodSolution.MaxVolume = entity.Comp.BloodMaxVolume;
  167. tempSolution.MaxVolume = entity.Comp.BleedPuddleThreshold * 4; // give some leeway, for chemstream as well
  168. // Fill blood solution with BLOOD
  169. // The DNA string might not be initialized yet, but the reagent data gets updated in the GenerateDnaEvent subscription
  170. bloodSolution.AddReagent(new ReagentId(entity.Comp.BloodReagent, GetEntityBloodData(entity.Owner)), entity.Comp.BloodMaxVolume - bloodSolution.Volume);
  171. }
  172. private void OnDamageChanged(Entity<BloodstreamComponent> ent, ref DamageChangedEvent args)
  173. {
  174. if (args.DamageDelta is null || !args.DamageIncreased)
  175. {
  176. return;
  177. }
  178. // TODO probably cache this or something. humans get hurt a lot
  179. if (!_prototypeManager.TryIndex<DamageModifierSetPrototype>(ent.Comp.DamageBleedModifiers, out var modifiers))
  180. return;
  181. var bloodloss = DamageSpecifier.ApplyModifierSet(args.DamageDelta, modifiers);
  182. if (bloodloss.Empty)
  183. return;
  184. // Does the calculation of how much bleed rate should be added/removed, then applies it
  185. var oldBleedAmount = ent.Comp.BleedAmount;
  186. var total = bloodloss.GetTotal();
  187. var totalFloat = total.Float();
  188. TryModifyBleedAmount(ent, totalFloat, ent);
  189. /// <summary>
  190. /// Critical hit. Causes target to lose blood, using the bleed rate modifier of the weapon, currently divided by 5
  191. /// The crit chance is currently the bleed rate modifier divided by 25.
  192. /// Higher damage weapons have a higher chance to crit!
  193. /// </summary>
  194. var prob = Math.Clamp(totalFloat / 25, 0, 1);
  195. if (totalFloat > 0 && _robustRandom.Prob(prob))
  196. {
  197. TryModifyBloodLevel(ent, (-total) / 5, ent);
  198. _audio.PlayPvs(ent.Comp.InstantBloodSound, ent);
  199. }
  200. // Heat damage will cauterize, causing the bleed rate to be reduced.
  201. else if (totalFloat <= ent.Comp.BloodHealedSoundThreshold && oldBleedAmount > 0)
  202. {
  203. // Magically, this damage has healed some bleeding, likely
  204. // because it's burn damage that cauterized their wounds.
  205. // We'll play a special sound and popup for feedback.
  206. _audio.PlayPvs(ent.Comp.BloodHealedSound, ent);
  207. _popupSystem.PopupEntity(Loc.GetString("bloodstream-component-wounds-cauterized"), ent,
  208. ent, PopupType.Medium);
  209. }
  210. }
  211. /// <summary>
  212. /// Shows text on health examine, based on bleed rate and blood level.
  213. /// </summary>
  214. private void OnHealthBeingExamined(Entity<BloodstreamComponent> ent, ref HealthBeingExaminedEvent args)
  215. {
  216. // Shows profusely bleeding at half the max bleed rate.
  217. if (ent.Comp.BleedAmount > ent.Comp.MaxBleedAmount / 2)
  218. {
  219. args.Message.PushNewline();
  220. args.Message.AddMarkupOrThrow(Loc.GetString("bloodstream-component-profusely-bleeding", ("target", ent.Owner)));
  221. }
  222. // Shows bleeding message when bleeding, but less than profusely.
  223. else if (ent.Comp.BleedAmount > 0)
  224. {
  225. args.Message.PushNewline();
  226. args.Message.AddMarkupOrThrow(Loc.GetString("bloodstream-component-bleeding", ("target", ent.Owner)));
  227. }
  228. // If the mob's blood level is below the damage threshhold, the pale message is added.
  229. if (GetBloodLevelPercentage(ent, ent) < ent.Comp.BloodlossThreshold)
  230. {
  231. args.Message.PushNewline();
  232. args.Message.AddMarkupOrThrow(Loc.GetString("bloodstream-component-looks-pale", ("target", ent.Owner)));
  233. }
  234. }
  235. private void OnBeingGibbed(Entity<BloodstreamComponent> ent, ref BeingGibbedEvent args)
  236. {
  237. SpillAllSolutions(ent, ent);
  238. }
  239. private void OnApplyMetabolicMultiplier(
  240. Entity<BloodstreamComponent> ent,
  241. ref ApplyMetabolicMultiplierEvent args)
  242. {
  243. // TODO REFACTOR THIS
  244. // This will slowly drift over time due to floating point errors.
  245. // Instead, raise an event with the base rates and allow modifiers to get applied to it.
  246. if (args.Apply)
  247. {
  248. ent.Comp.UpdateInterval *= args.Multiplier;
  249. return;
  250. }
  251. ent.Comp.UpdateInterval /= args.Multiplier;
  252. }
  253. private void OnRejuvenate(Entity<BloodstreamComponent> entity, ref RejuvenateEvent args)
  254. {
  255. TryModifyBleedAmount(entity.Owner, -entity.Comp.BleedAmount, entity.Comp);
  256. if (_solutionContainerSystem.ResolveSolution(entity.Owner, entity.Comp.BloodSolutionName, ref entity.Comp.BloodSolution, out var bloodSolution))
  257. TryModifyBloodLevel(entity.Owner, bloodSolution.AvailableVolume, entity.Comp);
  258. if (_solutionContainerSystem.ResolveSolution(entity.Owner, entity.Comp.ChemicalSolutionName, ref entity.Comp.ChemicalSolution))
  259. _solutionContainerSystem.RemoveAllSolution(entity.Comp.ChemicalSolution.Value);
  260. }
  261. /// <summary>
  262. /// Attempt to transfer provided solution to internal solution.
  263. /// </summary>
  264. public bool TryAddToChemicals(EntityUid uid, Solution solution, BloodstreamComponent? component = null)
  265. {
  266. return Resolve(uid, ref component, logMissing: false)
  267. && _solutionContainerSystem.ResolveSolution(uid, component.ChemicalSolutionName, ref component.ChemicalSolution)
  268. && _solutionContainerSystem.TryAddSolution(component.ChemicalSolution.Value, solution);
  269. }
  270. public bool FlushChemicals(EntityUid uid, string excludedReagentID, FixedPoint2 quantity, BloodstreamComponent? component = null)
  271. {
  272. if (!Resolve(uid, ref component, logMissing: false)
  273. || !_solutionContainerSystem.ResolveSolution(uid, component.ChemicalSolutionName, ref component.ChemicalSolution, out var chemSolution))
  274. return false;
  275. for (var i = chemSolution.Contents.Count - 1; i >= 0; i--)
  276. {
  277. var (reagentId, _) = chemSolution.Contents[i];
  278. if (reagentId.Prototype != excludedReagentID)
  279. {
  280. _solutionContainerSystem.RemoveReagent(component.ChemicalSolution.Value, reagentId, quantity);
  281. }
  282. }
  283. return true;
  284. }
  285. public float GetBloodLevelPercentage(EntityUid uid, BloodstreamComponent? component = null)
  286. {
  287. if (!Resolve(uid, ref component)
  288. || !_solutionContainerSystem.ResolveSolution(uid, component.BloodSolutionName, ref component.BloodSolution, out var bloodSolution))
  289. {
  290. return 0.0f;
  291. }
  292. return bloodSolution.FillFraction;
  293. }
  294. public void SetBloodLossThreshold(EntityUid uid, float threshold, BloodstreamComponent? comp = null)
  295. {
  296. if (!Resolve(uid, ref comp))
  297. return;
  298. comp.BloodlossThreshold = threshold;
  299. }
  300. /// <summary>
  301. /// Attempts to modify the blood level of this entity directly.
  302. /// </summary>
  303. public bool TryModifyBloodLevel(EntityUid uid, FixedPoint2 amount, BloodstreamComponent? component = null)
  304. {
  305. if (!Resolve(uid, ref component, logMissing: false)
  306. || !_solutionContainerSystem.ResolveSolution(uid, component.BloodSolutionName, ref component.BloodSolution))
  307. {
  308. return false;
  309. }
  310. if (amount >= 0)
  311. return _solutionContainerSystem.TryAddReagent(component.BloodSolution.Value, component.BloodReagent, amount, null, GetEntityBloodData(uid));
  312. // Removal is more involved,
  313. // since we also wanna handle moving it to the temporary solution
  314. // and then spilling it if necessary.
  315. var newSol = _solutionContainerSystem.SplitSolution(component.BloodSolution.Value, -amount);
  316. if (!_solutionContainerSystem.ResolveSolution(uid, component.BloodTemporarySolutionName, ref component.TemporarySolution, out var tempSolution))
  317. return true;
  318. tempSolution.AddSolution(newSol, _prototypeManager);
  319. if (tempSolution.Volume > component.BleedPuddleThreshold)
  320. {
  321. // Pass some of the chemstream into the spilled blood.
  322. if (_solutionContainerSystem.ResolveSolution(uid, component.ChemicalSolutionName, ref component.ChemicalSolution))
  323. {
  324. var temp = _solutionContainerSystem.SplitSolution(component.ChemicalSolution.Value, tempSolution.Volume / 10);
  325. tempSolution.AddSolution(temp, _prototypeManager);
  326. }
  327. _puddleSystem.TrySpillAt(uid, tempSolution, out var puddleUid, sound: false);
  328. tempSolution.RemoveAllSolution();
  329. }
  330. _solutionContainerSystem.UpdateChemicals(component.TemporarySolution.Value);
  331. return true;
  332. }
  333. /// <summary>
  334. /// Tries to make an entity bleed more or less
  335. /// </summary>
  336. public bool TryModifyBleedAmount(EntityUid uid, float amount, BloodstreamComponent? component = null)
  337. {
  338. if (!Resolve(uid, ref component, logMissing: false))
  339. return false;
  340. component.BleedAmount += amount;
  341. component.BleedAmount = Math.Clamp(component.BleedAmount, 0, component.MaxBleedAmount);
  342. if (component.BleedAmount == 0)
  343. _alertsSystem.ClearAlert(uid, component.BleedingAlert);
  344. else
  345. {
  346. var severity = (short) Math.Clamp(Math.Round(component.BleedAmount, MidpointRounding.ToZero), 0, 10);
  347. _alertsSystem.ShowAlert(uid, component.BleedingAlert, severity);
  348. }
  349. return true;
  350. }
  351. /// <summary>
  352. /// BLOOD FOR THE BLOOD GOD
  353. /// </summary>
  354. public void SpillAllSolutions(EntityUid uid, BloodstreamComponent? component = null)
  355. {
  356. if (!Resolve(uid, ref component))
  357. return;
  358. var tempSol = new Solution();
  359. if (_solutionContainerSystem.ResolveSolution(uid, component.BloodSolutionName, ref component.BloodSolution, out var bloodSolution))
  360. {
  361. tempSol.MaxVolume += bloodSolution.MaxVolume;
  362. tempSol.AddSolution(bloodSolution, _prototypeManager);
  363. _solutionContainerSystem.RemoveAllSolution(component.BloodSolution.Value);
  364. }
  365. if (_solutionContainerSystem.ResolveSolution(uid, component.ChemicalSolutionName, ref component.ChemicalSolution, out var chemSolution))
  366. {
  367. tempSol.MaxVolume += chemSolution.MaxVolume;
  368. tempSol.AddSolution(chemSolution, _prototypeManager);
  369. _solutionContainerSystem.RemoveAllSolution(component.ChemicalSolution.Value);
  370. }
  371. if (_solutionContainerSystem.ResolveSolution(uid, component.BloodTemporarySolutionName, ref component.TemporarySolution, out var tempSolution))
  372. {
  373. tempSol.MaxVolume += tempSolution.MaxVolume;
  374. tempSol.AddSolution(tempSolution, _prototypeManager);
  375. _solutionContainerSystem.RemoveAllSolution(component.TemporarySolution.Value);
  376. }
  377. _puddleSystem.TrySpillAt(uid, tempSol, out var puddleUid);
  378. }
  379. /// <summary>
  380. /// Change what someone's blood is made of, on the fly.
  381. /// </summary>
  382. public void ChangeBloodReagent(EntityUid uid, string reagent, BloodstreamComponent? component = null)
  383. {
  384. if (!Resolve(uid, ref component, logMissing: false)
  385. || reagent == component.BloodReagent)
  386. {
  387. return;
  388. }
  389. if (!_solutionContainerSystem.ResolveSolution(uid, component.BloodSolutionName, ref component.BloodSolution, out var bloodSolution))
  390. {
  391. component.BloodReagent = reagent;
  392. return;
  393. }
  394. var currentVolume = bloodSolution.RemoveReagent(component.BloodReagent, bloodSolution.Volume, ignoreReagentData: true);
  395. component.BloodReagent = reagent;
  396. if (currentVolume > 0)
  397. _solutionContainerSystem.TryAddReagent(component.BloodSolution.Value, component.BloodReagent, currentVolume, null, GetEntityBloodData(uid));
  398. }
  399. private void OnDnaGenerated(Entity<BloodstreamComponent> entity, ref GenerateDnaEvent args)
  400. {
  401. if (_solutionContainerSystem.ResolveSolution(entity.Owner, entity.Comp.BloodSolutionName, ref entity.Comp.BloodSolution, out var bloodSolution))
  402. {
  403. foreach (var reagent in bloodSolution.Contents)
  404. {
  405. List<ReagentData> reagentData = reagent.Reagent.EnsureReagentData();
  406. reagentData.RemoveAll(x => x is DnaData);
  407. reagentData.AddRange(GetEntityBloodData(entity.Owner));
  408. }
  409. }
  410. else
  411. Log.Error("Unable to set bloodstream DNA, solution entity could not be resolved");
  412. }
  413. /// <summary>
  414. /// Get the reagent data for blood that a specific entity should have.
  415. /// </summary>
  416. public List<ReagentData> GetEntityBloodData(EntityUid uid)
  417. {
  418. var bloodData = new List<ReagentData>();
  419. var dnaData = new DnaData();
  420. if (TryComp<DnaComponent>(uid, out var donorComp) && donorComp.DNA != null)
  421. dnaData.DNA = donorComp.DNA;
  422. else
  423. dnaData.DNA = Loc.GetString("forensics-dna-unknown");
  424. bloodData.Add(dnaData);
  425. return bloodData;
  426. }
  427. }