diff --git a/Content.IntegrationTests/Tests/EntityTable/EntityTableTest.cs b/Content.IntegrationTests/Tests/EntityTable/EntityTableTest.cs index 01c8ae0932..5f064a171e 100644 --- a/Content.IntegrationTests/Tests/EntityTable/EntityTableTest.cs +++ b/Content.IntegrationTests/Tests/EntityTable/EntityTableTest.cs @@ -300,11 +300,11 @@ public sealed class EntityTableTest : GameTest // EntityTableTestEntSelectorWithCost reads cost 10 from the DynamicRuleCostComponent. // Budget 9 => not enough. - var poor = Run(Table("EntityTableTestEntSelectorWithCost"), ctx: new EntityTableContext(new() { ["Budget"] = 9f })); + var poor = Run(Table("EntityTableTestEntSelectorWithCost"), ctx: new EntityTableContext { { HasBudgetCondition.BudgetContextKey, 9f } }); Assert.That(poor, Is.Empty); // Budget 10 => enough. - var rich = Run(Table("EntityTableTestEntSelectorWithCost"), ctx: new EntityTableContext(new() { ["Budget"] = 10f })); + var rich = Run(Table("EntityTableTestEntSelectorWithCost"), ctx: new EntityTableContext { { HasBudgetCondition.BudgetContextKey, 10f } }); Assert.That(rich, Is.EquivalentTo(new[] { new EntProtoId(EntProtoWithCost) })); } @@ -313,11 +313,11 @@ public sealed class EntityTableTest : GameTest public void RequireAllConditionSemantics() { // RequireAll = true, one fails => no spawns. - var requireAllResult = Run(Table("EntityTableTestEntRequireAll"), ctx: new EntityTableContext(new() { ["Budget"] = 50f })); + var requireAllResult = Run(Table("EntityTableTestEntRequireAll"), ctx: new EntityTableContext { { HasBudgetCondition.BudgetContextKey, 50f } }); Assert.That(requireAllResult, Is.Empty); // RequireAll = false, one passes => spawns. - var requireAnyResult = Run(Table("EntityTableTestEntRequireAny"), ctx: new EntityTableContext(new() { ["Budget"] = 50f })); + var requireAnyResult = Run(Table("EntityTableTestEntRequireAny"), ctx: new EntityTableContext { { HasBudgetCondition.BudgetContextKey, 50f } }); Assert.That(requireAnyResult, Is.EquivalentTo(new[] { new EntProtoId(EntProto1) })); } @@ -328,7 +328,7 @@ public sealed class EntityTableTest : GameTest // RequireAll = false but every condition fails => no spawns. // Regression test: previously the OR-ing was seeded with `true`, so // RequireAll = false always passed regardless of the actual results. - var result = Run(Table("EntityTableTestEntRequireAnyAllFail"), ctx: new EntityTableContext(new() { ["Budget"] = 50f })); + var result = Run(Table("EntityTableTestEntRequireAnyAllFail"), ctx: new EntityTableContext { { HasBudgetCondition.BudgetContextKey, 50f } }); Assert.That(result, Is.Empty); } @@ -337,7 +337,7 @@ public sealed class EntityTableTest : GameTest public void DeepComposition_ComplexTree() { // Ent1's condition fails (Budget 50 < CostOverride 100), so Group only has Ent2. - var result = Run(Table("EntityTableTestDeepComposition"), SeededRand(1), new EntityTableContext(new() { ["Budget"] = 50f })); + var result = Run(Table("EntityTableTestDeepComposition"), SeededRand(1), new EntityTableContext { { HasBudgetCondition.BudgetContextKey, 50f } }); Assert.That(result, Is.EqualTo(new[] { new EntProtoId(EntProto2), new EntProtoId(EntProto1) })); } @@ -352,12 +352,12 @@ public sealed class EntityTableTest : GameTest // EntProto1 already recorded as spawned => blocked. var used = new HashSet { new(EntProto1) }; var blocked = Run(Table("EntityTableTestNotRepeating"), - ctx: new EntityTableContext(new() { [ExcludeEntitiesFromContextCondition.EntitiesToExclude] = used })); + ctx: new EntityTableContext { { ExcludeEntitiesFromContextCondition.EntitiesToExclude, used } }); Assert.That(blocked, Is.Empty); // Tracking enabled, but EntProto1 has not been spawned yet => allowed. var allowed = Run(Table("EntityTableTestNotRepeating"), - ctx: new EntityTableContext(new() { [ExcludeEntitiesFromContextCondition.EntitiesToExclude] = new HashSet() })); + ctx: new EntityTableContext { { ExcludeEntitiesFromContextCondition.EntitiesToExclude, new HashSet()} }); Assert.That(allowed, Is.EquivalentTo([new EntProtoId(EntProto1)])); } @@ -367,7 +367,7 @@ public sealed class EntityTableTest : GameTest { var used = new HashSet { new(EntProto1) }; var result = Run(Table("EntityTableTestAllNotRepeating"), - ctx: new EntityTableContext(new() { [ExcludeEntitiesFromContextCondition.EntitiesToExclude] = used })); + ctx: new EntityTableContext { { ExcludeEntitiesFromContextCondition.EntitiesToExclude, used } }); Assert.That(result, Is.EqualTo(new[] { new EntProtoId(EntProto2) })); } @@ -381,7 +381,7 @@ public sealed class EntityTableTest : GameTest { var used = new HashSet { new(EntProto1) }; var result = Run(Table("EntityTableTestChainNotRepeating"), SeededRand(1), - new EntityTableContext(new() { [ExcludeEntitiesFromContextCondition.EntitiesToExclude] = used })); + new EntityTableContext { { ExcludeEntitiesFromContextCondition.EntitiesToExclude, used } }); Assert.That(result, Is.EqualTo(new[] { new EntProtoId(EntProto2) })); } @@ -394,10 +394,10 @@ public sealed class EntityTableTest : GameTest [RunOnSide(Side.Server)] public void NestedSelector_CheckConditions_IncludesNestedTable() { - var poor = Run(Table("EntityTableTestChainTableWithCost"), ctx: new EntityTableContext(new() { ["Budget"] = 9f })); + var poor = Run(Table("EntityTableTestChainTableWithCost"), ctx: new EntityTableContext { { HasBudgetCondition.BudgetContextKey, 9f } }); Assert.That(poor, Is.Empty); - var rich = Run(Table("EntityTableTestChainTableWithCost"), ctx: new EntityTableContext(new() { ["Budget"] = 10f })); + var rich = Run(Table("EntityTableTestChainTableWithCost"), ctx: new EntityTableContext { { HasBudgetCondition.BudgetContextKey, 10f } }); Assert.That(rich, Is.EquivalentTo(new[] { new EntProtoId(EntProtoWithCost) })); } @@ -409,7 +409,7 @@ public sealed class EntityTableTest : GameTest [RunOnSide(Side.Server)] public void GroupSelector_ExcludesNestedSelectorWithFailingConditions() { - var result = Run(Table("EntityTableTestGroupWithCostlyNested"), SeededRand(1), new EntityTableContext(new() { ["Budget"] = 9f })); + var result = Run(Table("EntityTableTestGroupWithCostlyNested"), SeededRand(1), new EntityTableContext { { HasBudgetCondition.BudgetContextKey, 9f } }); Assert.That(result, Is.EqualTo(new[] { new EntProtoId(EntProto2) })); } @@ -437,18 +437,18 @@ public sealed class EntityTableTest : GameTest // Without the injected condition, the UsedSpawns tracking alone has no effect. var unconstrained = Run(Table("EntityTableTestEntSelector"), - ctx: new EntityTableContext(new() { [ExcludeEntitiesFromContextCondition.EntitiesToExclude] = used })); + ctx: new EntityTableContext { { ExcludeEntitiesFromContextCondition.EntitiesToExclude, used } }); Assert.That(unconstrained, Is.EquivalentTo([new EntProtoId(EntProto1)])); // Injecting ExcludeEntitiesFromContextCondition gates the selector: EntProto1 is already used => blocked. - var ctx = new EntityTableContext(new() { [ExcludeEntitiesFromContextCondition.EntitiesToExclude] = used }); + var ctx = new EntityTableContext { { ExcludeEntitiesFromContextCondition.EntitiesToExclude, used } }; ctx.SetData(EntityTableSelector.AdditionalConditionsKey, new List { new ExcludeEntitiesFromContextCondition() }); var blocked = Run(Table("EntityTableTestEntSelector"), ctx: ctx); Assert.That(blocked, Is.Empty); // With the condition injected but EntProto1 not yet used, the spawn is allowed. - var fresh = new EntityTableContext(new() { [ExcludeEntitiesFromContextCondition.EntitiesToExclude] = new HashSet() }); + var fresh = new EntityTableContext { { ExcludeEntitiesFromContextCondition.EntitiesToExclude, new HashSet() } }; fresh.SetData(EntityTableSelector.AdditionalConditionsKey, new List { new ExcludeEntitiesFromContextCondition() }); var allowed = Run(Table("EntityTableTestEntSelector"), ctx: fresh); @@ -460,11 +460,11 @@ public sealed class EntityTableTest : GameTest public void AdditionalConditions_RemainLocal() { var used = new HashSet { new(EntProto2) }; - var ctx = new EntityTableContext(new() { [ExcludeEntitiesFromContextCondition.EntitiesToExclude] = used }); + var ctx = new EntityTableContext { { ExcludeEntitiesFromContextCondition.EntitiesToExclude, used } }; var result = Run(Table("EntityTableTestLocalizedChildConditions"), ctx: ctx); - ctx.TryGetData(EntityTableSelector.AdditionalConditionsKey, out var empty); + ctx.TryGetData(EntityTableSelector.AdditionalConditionsKey, out var empty); using (Assert.EnterMultipleScope()) { Assert.That(empty, Is.Null); @@ -482,7 +482,7 @@ public sealed class EntityTableTest : GameTest // An empty container succeeds. var container = _sContainer.MakeContainer(SSpawn(EntProto1), "containerId"); - var ctx = new EntityTableContext(new() { [EmptyContainerCondition.ContainerContextKey] = container }); + var ctx = new EntityTableContext { {EmptyContainerCondition.ContainerContextKey, container } }; result = Run(Table("EntityTableTestContainerCondition"), ctx: ctx); Assert.That(result, Is.EquivalentTo(new [] { EntProto1 })); diff --git a/Content.Server/GameTicking/Rules/DynamicRuleSystem.cs b/Content.Server/GameTicking/Rules/DynamicRuleSystem.cs index 90695fe3a5..10439964a6 100644 --- a/Content.Server/GameTicking/Rules/DynamicRuleSystem.cs +++ b/Content.Server/GameTicking/Rules/DynamicRuleSystem.cs @@ -76,10 +76,10 @@ public sealed partial class DynamicRuleSystem : GameRuleSystem GetRuleSpawns(Entity entity) { UpdateBudget((entity.Owner, entity.Comp)); - var ctx = new EntityTableContext(new Dictionary + var ctx = new EntityTableContext { { HasBudgetCondition.BudgetContextKey, entity.Comp.Budget }, - }); + }; return _entityTable.GetSpawns(entity.Comp.Table, ctx: ctx); } diff --git a/Content.Server/Xenoarchaeology/Artifact/XenoArtifactSystem.ProcGen.cs b/Content.Server/Xenoarchaeology/Artifact/XenoArtifactSystem.ProcGen.cs index 8077e22e0b..773004457a 100644 --- a/Content.Server/Xenoarchaeology/Artifact/XenoArtifactSystem.ProcGen.cs +++ b/Content.Server/Xenoarchaeology/Artifact/XenoArtifactSystem.ProcGen.cs @@ -250,10 +250,10 @@ public sealed partial class XenoArtifactSystem public TriggerPoolData(int requestedSize) { _usedTriggers = new(requestedSize); - Context = new EntityTableContext(new Dictionary + Context = new EntityTableContext { - [ExcludeEntitiesFromContextCondition.EntitiesToExclude] = _usedTriggers - }); + { ExcludeEntitiesFromContextCondition.EntitiesToExclude, _usedTriggers } + }; } public readonly EntityTableContext Context; diff --git a/Content.Shared/Containers/ContainerFillSystem.cs b/Content.Shared/Containers/ContainerFillSystem.cs index e3849db824..c46b8d5f10 100644 --- a/Content.Shared/Containers/ContainerFillSystem.cs +++ b/Content.Shared/Containers/ContainerFillSystem.cs @@ -76,12 +76,10 @@ public sealed partial class ContainerFillSystem : EntitySystem if (ent.Comp.ContextContainers) { // Pass in the container being filled so conditions can look at it - ctx = new EntityTableContext( - new Dictionary - { - [EmptyContainerCondition.ContainerContextKey] = container, - } - ); + ctx = new EntityTableContext + { + { EmptyContainerCondition.ContainerContextKey, container }, + }; } var spawns = _entityTable.GetSpawns(table, ctx: ctx).ToList(); diff --git a/Content.Shared/EntityTable/Conditions/EmptyContainerCondition.cs b/Content.Shared/EntityTable/Conditions/EmptyContainerCondition.cs index a8b9bd00b2..a265e852d9 100644 --- a/Content.Shared/EntityTable/Conditions/EmptyContainerCondition.cs +++ b/Content.Shared/EntityTable/Conditions/EmptyContainerCondition.cs @@ -12,7 +12,7 @@ public sealed partial class EmptyContainerCondition : EntityTableCondition /// /// Key for to store container that should be checked by this condition. /// - public const string ContainerContextKey = "Container"; + public static readonly EntityTableContextKey ContainerContextKey = new("Container"); /// > protected override bool EvaluateImplementation( @@ -22,7 +22,7 @@ public sealed partial class EmptyContainerCondition : EntityTableCondition EntityTableContext ctx ) { - if (!ctx.TryGetData(ContainerContextKey, out var container)) + if (!ctx.TryGetData(ContainerContextKey, out var container)) return false; return container.Count == 0; diff --git a/Content.Shared/EntityTable/Conditions/ExcludeEntitiesFromContextCondition.cs b/Content.Shared/EntityTable/Conditions/ExcludeEntitiesFromContextCondition.cs index 573293ac7a..a38675a9c8 100644 --- a/Content.Shared/EntityTable/Conditions/ExcludeEntitiesFromContextCondition.cs +++ b/Content.Shared/EntityTable/Conditions/ExcludeEntitiesFromContextCondition.cs @@ -13,7 +13,7 @@ public sealed partial class ExcludeEntitiesFromContextCondition : EntityTableCon /// Context key used to track which entity prototypes should not be spawned. /// Value should be . /// - public const string EntitiesToExclude = "EntitiesToExclude"; + public static readonly EntityTableContextKey> EntitiesToExclude = new("EntitiesToExclude"); /// > protected override bool EvaluateImplementation( @@ -23,7 +23,7 @@ public sealed partial class ExcludeEntitiesFromContextCondition : EntityTableCon EntityTableContext ctx ) { - if (!ctx.TryGetData>(EntitiesToExclude, out var used)) + if (!ctx.TryGetData(EntitiesToExclude, out var used)) return true; if (root is not EntSelector entSelector) diff --git a/Content.Shared/EntityTable/Conditions/HasBudgetCondition.cs b/Content.Shared/EntityTable/Conditions/HasBudgetCondition.cs index ef854978d6..27b8be516a 100644 --- a/Content.Shared/EntityTable/Conditions/HasBudgetCondition.cs +++ b/Content.Shared/EntityTable/Conditions/HasBudgetCondition.cs @@ -1,5 +1,4 @@ using Content.Shared.EntityTable.EntitySelectors; -using Content.Shared.GameTicking.Rules; using Content.Shared.GameTicking.Rules.Components; using Robust.Shared.Prototypes; @@ -10,7 +9,7 @@ namespace Content.Shared.EntityTable.Conditions; /// public sealed partial class HasBudgetCondition : EntityTableCondition { - public const string BudgetContextKey = "Budget"; + public static readonly EntityTableContextKey BudgetContextKey = new("Budget"); /// /// Used for determining the cost for the budget. diff --git a/Content.Shared/EntityTable/EntitySelectors/EntityTableSelector.cs b/Content.Shared/EntityTable/EntitySelectors/EntityTableSelector.cs index 2c207e98de..65ee16ae52 100644 --- a/Content.Shared/EntityTable/EntitySelectors/EntityTableSelector.cs +++ b/Content.Shared/EntityTable/EntitySelectors/EntityTableSelector.cs @@ -21,7 +21,7 @@ public abstract partial class EntityTableSelector /// /// Key for , under which additional scoped conditions should be stored. /// - public const string AdditionalConditionsKey = "AdditionalConditions"; + public static readonly EntityTableContextKey> AdditionalConditionsKey = new("AdditionalConditions"); /// /// The number of times this selector is run @@ -152,7 +152,7 @@ public abstract partial class EntityTableSelector /// private bool TryGetConditions(EntityTableContext ctx, [NotNullWhen(true)] out List? conditions) { - var hasAdditionalConditions = ctx.TryGetData>(AdditionalConditionsKey, out var additionalConditions); + var hasAdditionalConditions = ctx.TryGetData(AdditionalConditionsKey, out var additionalConditions); if (Conditions.Count == 0 && !hasAdditionalConditions) { diff --git a/Content.Shared/EntityTable/EntityTableContext.cs b/Content.Shared/EntityTable/EntityTableContext.cs new file mode 100644 index 0000000000..ad1cba8936 --- /dev/null +++ b/Content.Shared/EntityTable/EntityTableContext.cs @@ -0,0 +1,75 @@ +using System.Collections; +using System.Diagnostics.CodeAnalysis; +using JetBrains.Annotations; + +namespace Content.Shared.EntityTable; + +/// +/// Context used by selectors and conditions to evaluate in generic gamestate information. +/// +public sealed class EntityTableContext : IEnumerable +{ + private readonly Dictionary _data = new(); + + /// + /// Retrieves an arbitrary piece of data from the context based on a provided key. + /// + /// A string key that corresponds to the value we are searching for. + /// The value we are trying to extract from the context object + /// The type of that we are trying to retrieve + /// If has a corresponding value of type + [PublicAPI] + public bool TryGetData(EntityTableContextKey key, [NotNullWhen(true)] out T? value) + { + value = default; + if (!_data.TryGetValue(key.Key, out var valueData) || valueData is not T castValueData) + return false; + + value = castValueData; + return true; + } + + /// + /// Sets data into context using provided key. + /// + [PublicAPI] + public void SetData(EntityTableContextKey key, T data) where T : notnull + { + _data[key.Key] = data; + } + + /// + /// Add method duplicates , is used for object initializer. + /// + /// Thrown if there is already a value stored under same key. + [PublicAPI] + public void Add(EntityTableContextKey key, T data) where T : notnull + { + _data.Add(key.Key, data); + } + + /// + /// Removes data from the context, if the key exists. + /// + [PublicAPI] + public void RemoveData(EntityTableContextKey key) + { + _data.Remove(key.Key); + } + + /// + public IEnumerator GetEnumerator() + { + return _data.GetEnumerator(); + } +} + +/// +/// Key for , used to strongly type calls for setting and getting entities, +/// should usually be static readonly fields as anchors. +/// +/// Type of value to store/extract to/from context. +/// +/// String value under which data will be stored inside. They should not have collisions, otherwise data will be rewritten. +/// +public readonly record struct EntityTableContextKey(string Key); diff --git a/Content.Shared/EntityTable/EntityTableSystem.cs b/Content.Shared/EntityTable/EntityTableSystem.cs index 7d5607e659..e18c8e4dad 100644 --- a/Content.Shared/EntityTable/EntityTableSystem.cs +++ b/Content.Shared/EntityTable/EntityTableSystem.cs @@ -1,6 +1,4 @@ -using System.Diagnostics.CodeAnalysis; using Content.Shared.EntityTable.EntitySelectors; -using JetBrains.Annotations; using Robust.Shared.Prototypes; using Robust.Shared.Random; using Robust.Shared.Utility; @@ -100,57 +98,3 @@ public sealed partial class EntityTableSystem : EntitySystem return table.AverageSpawns(EntityManager, ProtoMan, ctx); } } - -/// -/// Context used by selectors and conditions to evaluate in generic gamestate information. -/// -public sealed class EntityTableContext -{ - private readonly Dictionary _data = new(); - - public EntityTableContext() - { - - } - - public EntityTableContext(Dictionary data) - { - _data = data; - } - - /// - /// Retrieves an arbitrary piece of data from the context based on a provided key. - /// - /// A string key that corresponds to the value we are searching for. - /// The value we are trying to extract from the context object - /// The type of that we are trying to retrieve - /// If has a corresponding value of type - [PublicAPI] - public bool TryGetData([ForbidLiteral] string key, [NotNullWhen(true)] out T? value) - { - value = default; - if (!_data.TryGetValue(key, out var valueData) || valueData is not T castValueData) - return false; - - value = castValueData; - return true; - } - - /// - /// Sets data into context using provided key. - /// - [PublicAPI] - public void SetData([ForbidLiteral] string key, T data) where T : notnull - { - _data[key] = data; - } - - /// - /// Removes data from the context, if the key exists. - /// - [PublicAPI] - public void RemoveData([ForbidLiteral] string key) - { - _data.Remove(key); - } -}