From 7f8860187e35a995e91a48b5f4790d379f1980e9 Mon Sep 17 00:00:00 2001 From: Visne <39844191+Visne@users.noreply.github.com> Date: Tue, 28 Feb 2023 16:55:25 +0100 Subject: [PATCH] Make pricing system aware of SpawnItemsOnUseComponent (#13626) * Make appraisal tool aware of SpawnItemsOnUseComponent * Move to SpawnItemsOnUseSystem --- Content.Server/Cargo/Systems/PricingSystem.cs | 21 +++- .../Components/SpawnItemsOnUseComponent.cs | 2 - .../EntitySystems/SpawnItemsOnUseSystem.cs | 43 ++++++- Content.Shared/Storage/EntitySpawnEntry.cs | 108 ++++++++++++------ .../Consumable/Food/Containers/tin.yml | 2 - 5 files changed, 132 insertions(+), 44 deletions(-) diff --git a/Content.Server/Cargo/Systems/PricingSystem.cs b/Content.Server/Cargo/Systems/PricingSystem.cs index 39bd51628c..56f8e2ffd1 100644 --- a/Content.Server/Cargo/Systems/PricingSystem.cs +++ b/Content.Server/Cargo/Systems/PricingSystem.cs @@ -3,7 +3,6 @@ using Content.Server.Administration; using Content.Server.Body.Systems; using Content.Server.Cargo.Components; using Content.Server.Chemistry.Components.SolutionManager; -using Content.Server.Stack; using Content.Shared.Administration; using Content.Shared.Body.Components; using Content.Shared.Chemistry.Reagent; @@ -88,6 +87,9 @@ public sealed class PricingSystem : EntitySystem private void CalculateMobPrice(EntityUid uid, MobPriceComponent component, ref PriceCalculationEvent args) { + if (args.Handled) + return; + if (!TryComp(uid, out var body) || !TryComp(uid, out var state)) { Logger.ErrorS("pricing", $"Tried to get the mob price of {ToPrettyString(uid)}, which has no {nameof(BodyComponent)} and no {nameof(MobStateComponent)}."); @@ -106,6 +108,9 @@ public sealed class PricingSystem : EntitySystem private void CalculateStackPrice(EntityUid uid, StackPriceComponent component, ref PriceCalculationEvent args) { + if (args.Handled) + return; + if (!TryComp(uid, out var stack)) { Logger.ErrorS("pricing", $"Tried to get the stack price of {ToPrettyString(uid)}, which has no {nameof(StackComponent)}."); @@ -117,6 +122,9 @@ public sealed class PricingSystem : EntitySystem private void CalculateSolutionPrice(EntityUid uid, SolutionContainerManagerComponent component, ref PriceCalculationEvent args) { + if (args.Handled) + return; + var price = 0f; foreach (var solution in component.Solutions.Values) @@ -133,6 +141,9 @@ public sealed class PricingSystem : EntitySystem private void CalculateStaticPrice(EntityUid uid, StaticPriceComponent component, ref PriceCalculationEvent args) { + if (args.Handled) + return; + args.Price += component.Price; } @@ -187,6 +198,9 @@ public sealed class PricingSystem : EntitySystem var ev = new PriceCalculationEvent(); RaiseLocalEvent(uid, ref ev); + if (ev.Handled) + return ev.Price; + //TODO: Add an OpaqueToAppraisal component or similar for blocking the recursive descent into containers, or preventing material pricing. if (TryComp(uid, out var material) && !HasComp(uid)) @@ -249,5 +263,10 @@ public struct PriceCalculationEvent /// public double Price = 0; + /// + /// Whether this event was already handled. + /// + public bool Handled = false; + public PriceCalculationEvent() { } } diff --git a/Content.Server/Storage/Components/SpawnItemsOnUseComponent.cs b/Content.Server/Storage/Components/SpawnItemsOnUseComponent.cs index 363561606c..95ef6b33c3 100644 --- a/Content.Server/Storage/Components/SpawnItemsOnUseComponent.cs +++ b/Content.Server/Storage/Components/SpawnItemsOnUseComponent.cs @@ -12,7 +12,6 @@ namespace Content.Server.Storage.Components /// /// The list of entities to spawn, with amounts and orGroups. /// - /// [DataField("items", required: true)] public List Items = new(); @@ -25,7 +24,6 @@ namespace Content.Server.Storage.Components /// /// How many uses before the item should delete itself. /// - /// [DataField("uses")] public int Uses = 1; } diff --git a/Content.Server/Storage/EntitySystems/SpawnItemsOnUseSystem.cs b/Content.Server/Storage/EntitySystems/SpawnItemsOnUseSystem.cs index 35936fea5c..b24348507d 100644 --- a/Content.Server/Storage/EntitySystems/SpawnItemsOnUseSystem.cs +++ b/Content.Server/Storage/EntitySystems/SpawnItemsOnUseSystem.cs @@ -1,12 +1,15 @@ using Content.Server.Administration.Logs; +using Content.Server.Cargo.Systems; using Content.Server.Storage.Components; using Content.Shared.Database; using Content.Shared.Hands.EntitySystems; using Content.Shared.Interaction.Events; using Content.Shared.Storage; using Robust.Shared.Audio; +using Robust.Shared.Map; using Robust.Shared.Player; using Robust.Shared.Random; +using static Content.Shared.Storage.EntitySpawnCollection; namespace Content.Server.Storage.EntitySystems { @@ -14,13 +17,47 @@ namespace Content.Server.Storage.EntitySystems { [Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly IAdminLogManager _adminLogger = default!; - [Dependency] private readonly SharedHandsSystem _handsSystem = default!; + [Dependency] private readonly SharedHandsSystem _hands = default!; + [Dependency] private readonly PricingSystem _pricing = default!; public override void Initialize() { base.Initialize(); SubscribeLocalEvent(OnUseInHand); + SubscribeLocalEvent(CalculatePrice, before: new[] { typeof(PricingSystem) }); + } + + private void CalculatePrice(EntityUid uid, SpawnItemsOnUseComponent component, ref PriceCalculationEvent args) + { + var ungrouped = CollectOrGroups(component.Items, out var orGroups); + + foreach (var entry in ungrouped) + { + var protUid = Spawn(entry.PrototypeId, MapCoordinates.Nullspace); + + // Calculate the average price of the possible spawned items + args.Price += _pricing.GetPrice(protUid) * entry.SpawnProbability * entry.GetAmount(getAverage: true); + + EntityManager.DeleteEntity(protUid); + } + + foreach (var group in orGroups) + { + foreach (var entry in group.Entries) + { + var protUid = Spawn(entry.PrototypeId, MapCoordinates.Nullspace); + + // Calculate the average price of the possible spawned items + args.Price += _pricing.GetPrice(protUid) * + (entry.SpawnProbability / group.CumulativeProbability) * + entry.GetAmount(getAverage: true); + + EntityManager.DeleteEntity(protUid); + } + } + + args.Handled = true; } private void OnUseInHand(EntityUid uid, SpawnItemsOnUseComponent component, UseInHandEvent args) @@ -29,7 +66,7 @@ namespace Content.Server.Storage.EntitySystems return; var coords = Transform(args.User).Coordinates; - var spawnEntities = EntitySpawnCollection.GetSpawns(component.Items, _random); + var spawnEntities = GetSpawns(component.Items, _random); EntityUid? entityToPlaceInHands = null; foreach (var proto in spawnEntities) @@ -50,7 +87,7 @@ namespace Content.Server.Storage.EntitySystems if (entityToPlaceInHands != null) { - _handsSystem.PickupOrDrop(args.User, entityToPlaceInHands.Value); + _hands.PickupOrDrop(args.User, entityToPlaceInHands.Value); } } } diff --git a/Content.Shared/Storage/EntitySpawnEntry.cs b/Content.Shared/Storage/EntitySpawnEntry.cs index d1c92c59b8..da57ca850a 100644 --- a/Content.Shared/Storage/EntitySpawnEntry.cs +++ b/Content.Shared/Storage/EntitySpawnEntry.cs @@ -1,6 +1,6 @@ -using Robust.Shared.Prototypes; +using System.Linq; +using Robust.Shared.Prototypes; using Robust.Shared.Random; -using Robust.Shared.Serialization.Manager; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; namespace Content.Shared.Storage; @@ -62,7 +62,7 @@ public struct EntitySpawnEntry public static class EntitySpawnCollection { - private sealed class OrGroup + public sealed class OrGroup { public List Entries { get; set; } = new(); public float CumulativeProbability { get; set; } = 0f; @@ -84,34 +84,16 @@ public static class EntitySpawnCollection IoCManager.Resolve(ref random); var spawned = new List(); - var orGroupedSpawns = new Dictionary(); + var ungrouped = CollectOrGroups(entries, out var orGroupedSpawns); - // collect groups together, create singular items that pass probability - foreach (var entry in entries) + foreach (var entry in ungrouped) { - // Handle "Or" groups - if (!string.IsNullOrEmpty(entry.GroupId)) - { - if (!orGroupedSpawns.TryGetValue(entry.GroupId, out OrGroup? orGroup)) - { - orGroup = new(); - orGroupedSpawns.Add(entry.GroupId, orGroup); - } - - orGroup.Entries.Add(entry); - orGroup.CumulativeProbability += entry.SpawnProbability; - continue; - } - - // else // Check random spawn // ReSharper disable once CompareOfFloatsByEqualityOperator - if (entry.SpawnProbability != 1f && !random.Prob(entry.SpawnProbability)) continue; - - var amount = entry.Amount; + if (entry.SpawnProbability != 1f && !random.Prob(entry.SpawnProbability)) + continue; - if (entry.MaxAmount > amount) - amount = random.Next(amount, entry.MaxAmount); + var amount = (int) entry.GetAmount(random); for (var i = 0; i < amount; i++) { @@ -119,25 +101,25 @@ public static class EntitySpawnCollection } } - // handle orgroup spawns - foreach (var spawnValue in orGroupedSpawns.Values) + // Handle OrGroup spawns + foreach (var spawnValue in orGroupedSpawns) { // For each group use the added cumulative probability to roll a double in that range - double diceRoll = random.NextDouble() * spawnValue.CumulativeProbability; + var diceRoll = random.NextDouble() * spawnValue.CumulativeProbability; + // Add the entry's spawn probability to this value, if equals or lower, spawn item, otherwise continue to next item. var cumulative = 0.0; + foreach (var entry in spawnValue.Entries) { cumulative += entry.SpawnProbability; - if (diceRoll > cumulative) continue; - // Dice roll succeeded, add item and break loop - - var amount = entry.Amount; + if (diceRoll > cumulative) + continue; - if (entry.MaxAmount > amount) - amount = random.Next(amount, entry.MaxAmount); + // Dice roll succeeded, add item and break loop + var amount = (int) entry.GetAmount(random); - for (var index = 0; index < amount; index++) + for (var i = 0; i < amount; i++) { spawned.Add(entry.PrototypeId); } @@ -148,4 +130,58 @@ public static class EntitySpawnCollection return spawned; } + + /// + /// Collects all entries that belong together in an OrGroup, and then returns the leftover ungrouped entries. + /// + /// A list of entries that will be collected into OrGroups. + /// A list of entries collected into OrGroups. + /// A list of entries that are not in an OrGroup. + public static List CollectOrGroups(IEnumerable entries, out List orGroups) + { + var ungrouped = new List(); + var orGroupsDict = new Dictionary(); + + foreach (var entry in entries) + { + // If the entry is in a group, collect it into an OrGroup. Otherwise just add it to a list of ungrouped + // entries. + if (!string.IsNullOrEmpty(entry.GroupId)) + { + // Create a new OrGroup if necessary + if (!orGroupsDict.TryGetValue(entry.GroupId, out var orGroup)) + { + orGroup = new OrGroup(); + orGroupsDict.Add(entry.GroupId, orGroup); + } + + orGroup.Entries.Add(entry); + orGroup.CumulativeProbability += entry.SpawnProbability; + } + else + { + ungrouped.Add(entry); + } + } + + // We don't really need the group IDs anymore, so just return the values as a list + orGroups = orGroupsDict.Values.ToList(); + + return ungrouped; + } + + public static double GetAmount(this EntitySpawnEntry entry, IRobustRandom? random = null, bool getAverage = false) + { + // Max amount is less or equal than amount, so just return the amount + if (entry.MaxAmount <= entry.Amount) + return entry.Amount; + + // If we want the average, just calculate the expected amount + if (getAverage) + return (entry.Amount + entry.MaxAmount) / 2.0; + + // Otherwise get a random value in between + IoCManager.Resolve(ref random); + return random.Next(entry.Amount, entry.MaxAmount); + } } diff --git a/Resources/Prototypes/Entities/Objects/Consumable/Food/Containers/tin.yml b/Resources/Prototypes/Entities/Objects/Consumable/Food/Containers/tin.yml index c1867876e4..84bbb670be 100644 --- a/Resources/Prototypes/Entities/Objects/Consumable/Food/Containers/tin.yml +++ b/Resources/Prototypes/Entities/Objects/Consumable/Food/Containers/tin.yml @@ -247,8 +247,6 @@ - id: FoodTinMREOpen sound: path: /Audio/Items/can_open3.ogg - - type: StaticPrice - price: 50 - type: entity -- 2.52.0