EntitySpawnEntry.cs 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266
  1. using System.Linq;
  2. using Robust.Shared.Prototypes;
  3. using Robust.Shared.Random;
  4. using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
  5. namespace Content.Shared.Storage;
  6. /// <summary>
  7. /// Prototype wrapper around <see cref="EntitySpawnEntry"/>
  8. /// </summary>
  9. [Prototype]
  10. public sealed partial class EntitySpawnEntryPrototype : IPrototype
  11. {
  12. [IdDataField]
  13. public string ID { get; private set; } = string.Empty;
  14. [DataField]
  15. public List<EntitySpawnEntry> Entries = new();
  16. }
  17. /// <summary>
  18. /// Dictates a list of items that can be spawned.
  19. /// </summary>
  20. [Serializable]
  21. [DataDefinition]
  22. public partial struct EntitySpawnEntry
  23. {
  24. [DataField("id")]
  25. public EntProtoId? PrototypeId = null;
  26. /// <summary>
  27. /// The probability that an item will spawn. Takes decimal form so 0.05 is 5%, 0.50 is 50% etc.
  28. /// </summary>
  29. [DataField("prob")] public float SpawnProbability = 1;
  30. /// <summary>
  31. /// orGroup signifies to pick between entities designated with an ID.
  32. /// <example>
  33. /// <para>
  34. /// To define an orGroup in a StorageFill component you
  35. /// need to add it to the entities you want to choose between and
  36. /// add a prob field. In this example there is a 50% chance the storage
  37. /// spawns with Y or Z.
  38. /// </para>
  39. /// <code>
  40. /// - type: StorageFill
  41. /// contents:
  42. /// - name: X
  43. /// - name: Y
  44. /// prob: 0.50
  45. /// orGroup: YOrZ
  46. /// - name: Z
  47. /// orGroup: YOrZ
  48. /// </code>
  49. /// </example>
  50. /// </summary>
  51. [DataField("orGroup")] public string? GroupId = null;
  52. [DataField] public int Amount = 1;
  53. /// <summary>
  54. /// How many of this can be spawned, in total.
  55. /// If this is lesser or equal to <see cref="Amount"/>, it will spawn <see cref="Amount"/> exactly.
  56. /// Otherwise, it chooses a random value between <see cref="Amount"/> and <see cref="MaxAmount"/> on spawn.
  57. /// </summary>
  58. [DataField] public int MaxAmount = 1;
  59. public EntitySpawnEntry() { }
  60. }
  61. public static class EntitySpawnCollection
  62. {
  63. public sealed class OrGroup
  64. {
  65. public List<EntitySpawnEntry> Entries { get; set; } = new();
  66. public float CumulativeProbability { get; set; } = 0f;
  67. }
  68. /// <summary>
  69. /// Using a collection of entity spawn entries, picks a random list of entity prototypes to spawn from that collection.
  70. /// </summary>
  71. /// <remarks>
  72. /// This does not spawn the entities. The caller is responsible for doing so, since it may want to do something
  73. /// special to those entities (offset them, insert them into storage, etc)
  74. /// </remarks>
  75. /// <param name="entries">The entity spawn entries.</param>
  76. /// <param name="random">Resolve param.</param>
  77. /// <returns>A list of entity prototypes that should be spawned.</returns>
  78. public static List<string> GetSpawns(IEnumerable<EntitySpawnEntry> entries,
  79. IRobustRandom? random = null)
  80. {
  81. IoCManager.Resolve(ref random);
  82. var spawned = new List<string>();
  83. var ungrouped = CollectOrGroups(entries, out var orGroupedSpawns);
  84. foreach (var entry in ungrouped)
  85. {
  86. // Check random spawn
  87. // ReSharper disable once CompareOfFloatsByEqualityOperator
  88. if (entry.SpawnProbability != 1f && !random.Prob(entry.SpawnProbability))
  89. continue;
  90. if (entry.PrototypeId == null)
  91. continue;
  92. var amount = (int) entry.GetAmount(random);
  93. for (var i = 0; i < amount; i++)
  94. {
  95. spawned.Add(entry.PrototypeId);
  96. }
  97. }
  98. // Handle OrGroup spawns
  99. foreach (var spawnValue in orGroupedSpawns)
  100. {
  101. // For each group use the added cumulative probability to roll a double in that range
  102. var diceRoll = random.NextDouble() * spawnValue.CumulativeProbability;
  103. // Add the entry's spawn probability to this value, if equals or lower, spawn item, otherwise continue to next item.
  104. var cumulative = 0.0;
  105. foreach (var entry in spawnValue.Entries)
  106. {
  107. cumulative += entry.SpawnProbability;
  108. if (diceRoll > cumulative)
  109. continue;
  110. if (entry.PrototypeId == null)
  111. break;
  112. // Dice roll succeeded, add item and break loop
  113. var amount = (int) entry.GetAmount(random);
  114. for (var i = 0; i < amount; i++)
  115. {
  116. spawned.Add(entry.PrototypeId);
  117. }
  118. break;
  119. }
  120. }
  121. return spawned;
  122. }
  123. public static List<string?> GetSpawns(IEnumerable<EntitySpawnEntry> entries,
  124. System.Random random)
  125. {
  126. var spawned = new List<string?>();
  127. var ungrouped = CollectOrGroups(entries, out var orGroupedSpawns);
  128. foreach (var entry in ungrouped)
  129. {
  130. // Check random spawn
  131. // ReSharper disable once CompareOfFloatsByEqualityOperator
  132. if (entry.SpawnProbability != 1f && !random.Prob(entry.SpawnProbability))
  133. continue;
  134. var amount = (int) entry.GetAmount(random);
  135. for (var i = 0; i < amount; i++)
  136. {
  137. spawned.Add(entry.PrototypeId);
  138. }
  139. }
  140. // Handle OrGroup spawns
  141. foreach (var spawnValue in orGroupedSpawns)
  142. {
  143. // For each group use the added cumulative probability to roll a double in that range
  144. var diceRoll = random.NextDouble() * spawnValue.CumulativeProbability;
  145. // Add the entry's spawn probability to this value, if equals or lower, spawn item, otherwise continue to next item.
  146. var cumulative = 0.0;
  147. foreach (var entry in spawnValue.Entries)
  148. {
  149. cumulative += entry.SpawnProbability;
  150. if (diceRoll > cumulative)
  151. continue;
  152. // Dice roll succeeded, add item and break loop
  153. var amount = (int) entry.GetAmount(random);
  154. for (var i = 0; i < amount; i++)
  155. {
  156. spawned.Add(entry.PrototypeId);
  157. }
  158. break;
  159. }
  160. }
  161. return spawned;
  162. }
  163. public static double GetAmount(this EntitySpawnEntry entry, System.Random random, bool getAverage = false)
  164. {
  165. // Max amount is less or equal than amount, so just return the amount
  166. if (entry.MaxAmount <= entry.Amount)
  167. return entry.Amount;
  168. // If we want the average, just calculate the expected amount
  169. if (getAverage)
  170. return (entry.Amount + entry.MaxAmount) / 2.0;
  171. // Otherwise get a random value in between
  172. return random.Next(entry.Amount, entry.MaxAmount);
  173. }
  174. /// <summary>
  175. /// Collects all entries that belong together in an OrGroup, and then returns the leftover ungrouped entries.
  176. /// </summary>
  177. /// <param name="entries">A list of entries that will be collected into OrGroups.</param>
  178. /// <param name="orGroups">A list of entries collected into OrGroups.</param>
  179. /// <returns>A list of entries that are not in an OrGroup.</returns>
  180. public static List<EntitySpawnEntry> CollectOrGroups(IEnumerable<EntitySpawnEntry> entries, out List<OrGroup> orGroups)
  181. {
  182. var ungrouped = new List<EntitySpawnEntry>();
  183. var orGroupsDict = new Dictionary<string, OrGroup>();
  184. foreach (var entry in entries)
  185. {
  186. // If the entry is in a group, collect it into an OrGroup. Otherwise just add it to a list of ungrouped
  187. // entries.
  188. if (!string.IsNullOrEmpty(entry.GroupId))
  189. {
  190. // Create a new OrGroup if necessary
  191. if (!orGroupsDict.TryGetValue(entry.GroupId, out var orGroup))
  192. {
  193. orGroup = new OrGroup();
  194. orGroupsDict.Add(entry.GroupId, orGroup);
  195. }
  196. orGroup.Entries.Add(entry);
  197. orGroup.CumulativeProbability += entry.SpawnProbability;
  198. }
  199. else
  200. {
  201. ungrouped.Add(entry);
  202. }
  203. }
  204. // We don't really need the group IDs anymore, so just return the values as a list
  205. orGroups = orGroupsDict.Values.ToList();
  206. return ungrouped;
  207. }
  208. public static double GetAmount(this EntitySpawnEntry entry, IRobustRandom? random = null, bool getAverage = false)
  209. {
  210. // Max amount is less or equal than amount, so just return the amount
  211. if (entry.MaxAmount <= entry.Amount)
  212. return entry.Amount;
  213. // If we want the average, just calculate the expected amount
  214. if (getAverage)
  215. return (entry.Amount + entry.MaxAmount) / 2.0;
  216. // Otherwise get a random value in between
  217. IoCManager.Resolve(ref random);
  218. return random.Next(entry.Amount, entry.MaxAmount);
  219. }
  220. }