diff --git a/Content.IntegrationTests/Tests/Construction/ConstructionActionValid.cs b/Content.IntegrationTests/Tests/Construction/ConstructionActionValid.cs index 4197effa71..1386a7641e 100644 --- a/Content.IntegrationTests/Tests/Construction/ConstructionActionValid.cs +++ b/Content.IntegrationTests/Tests/Construction/ConstructionActionValid.cs @@ -10,6 +10,41 @@ namespace Content.IntegrationTests.Tests.Construction [TestFixture] public class ConstructionActionValid : ContentIntegrationTest { + private bool IsValid(IGraphAction action, IPrototypeManager protoMan, out string prototype) + { + switch (action) + { + case SpawnPrototype spawn: + prototype = spawn.Prototype; + return protoMan.TryIndex(spawn.Prototype, out _); + case SpawnPrototypeAtContainer spawn: + prototype = spawn.Prototype; + return protoMan.TryIndex(spawn.Prototype, out _); + case ConditionalAction conditional: + var valid = IsValid(conditional.Action, protoMan, out var protoA) & IsValid(conditional.Else, protoMan, out var protoB); + + if (!string.IsNullOrEmpty(protoA) && string.IsNullOrEmpty(protoB)) + { + prototype = protoA; + } + + else if (string.IsNullOrEmpty(protoA) && !string.IsNullOrEmpty(protoB)) + { + prototype = protoB; + } + + else + { + prototype = $"{protoA}, {protoB}"; + } + + return valid; + default: + prototype = string.Empty; + return true; + } + } + [Test] public async Task ConstructionGraphSpawnPrototypeValid() { @@ -28,20 +63,20 @@ namespace Content.IntegrationTests.Tests.Construction { foreach (var action in node.Actions) { - if (action is not SpawnPrototype spawn || protoMan.TryIndex(spawn.Prototype, out EntityPrototype _)) continue; + if (IsValid(action, protoMan, out var prototype)) continue; valid = false; - message.Append($"Invalid entity prototype \"{spawn.Prototype}\" on graph action in node \"{node.Name}\" of graph \"{graph.ID}\"\n"); + message.Append($"Invalid entity prototype \"{prototype}\" on graph action in node \"{node.Name}\" of graph \"{graph.ID}\"\n"); } foreach (var edge in node.Edges) { foreach (var action in edge.Completed) { - if (action is not SpawnPrototype spawn || protoMan.TryIndex(spawn.Prototype, out EntityPrototype _)) continue; + if (IsValid(action, protoMan, out var prototype)) continue; valid = false; - message.Append($"Invalid entity prototype \"{spawn.Prototype}\" on graph action in edge \"{edge.Target}\" of node \"{node.Name}\" of graph \"{graph.ID}\"\n"); + message.Append($"Invalid entity prototype \"{prototype}\" on graph action in edge \"{edge.Target}\" of node \"{node.Name}\" of graph \"{graph.ID}\"\n"); } } } diff --git a/Content.Server/Construction/Completions/ConditionalAction.cs b/Content.Server/Construction/Completions/ConditionalAction.cs new file mode 100644 index 0000000000..37b612de04 --- /dev/null +++ b/Content.Server/Construction/Completions/ConditionalAction.cs @@ -0,0 +1,33 @@ +#nullable enable +using System.Threading.Tasks; +using Content.Shared.Construction; +using JetBrains.Annotations; +using Robust.Shared.GameObjects; +using Robust.Shared.Serialization.Manager.Attributes; + +namespace Content.Server.Construction.Completions +{ + [UsedImplicitly] + [DataDefinition] + public class ConditionalAction : IGraphAction + { + [field: DataField("passUser")] public bool PassUser { get; } = false; + + [field: DataField("condition", required:true)] public IEdgeCondition? Condition { get; } = null; + + [field: DataField("action", required:true)] public IGraphAction? Action { get; } = null; + + [field: DataField("else")] public IGraphAction? Else { get; } = null; + + public async Task PerformAction(IEntity entity, IEntity? user) + { + if (Condition == null || Action == null) + return; + + if (await Condition.Condition(PassUser && user != null ? user : entity)) + await Action.PerformAction(entity, user); + else if (Else != null) + await Else.PerformAction(entity, user); + } + } +} diff --git a/Content.Server/Construction/Completions/DeleteEntitiesInContainer.cs b/Content.Server/Construction/Completions/DeleteEntitiesInContainer.cs new file mode 100644 index 0000000000..ed220ff884 --- /dev/null +++ b/Content.Server/Construction/Completions/DeleteEntitiesInContainer.cs @@ -0,0 +1,29 @@ +#nullable enable +using System.Linq; +using System.Threading.Tasks; +using Content.Shared.Construction; +using Robust.Shared.Containers; +using Robust.Shared.GameObjects; +using Robust.Shared.Serialization.Manager.Attributes; + +namespace Content.Server.Construction.Completions +{ + [DataDefinition] + public class DeleteEntitiesInContainer : IGraphAction + { + [field: DataField("container")] public string Container { get; } = string.Empty; + + public async Task PerformAction(IEntity entity, IEntity? user) + { + if (string.IsNullOrEmpty(Container)) return; + if (!entity.TryGetComponent(out ContainerManagerComponent? containerMan)) return; + if (!containerMan.TryGetContainer(Container, out var container)) return; + + foreach (var contained in container.ContainedEntities.ToArray()) + { + if(container.Remove(contained)) + contained.Delete(); + } + } + } +} diff --git a/Content.Server/Construction/Completions/MoveContainer.cs b/Content.Server/Construction/Completions/MoveContainer.cs new file mode 100644 index 0000000000..a0df7c0c7a --- /dev/null +++ b/Content.Server/Construction/Completions/MoveContainer.cs @@ -0,0 +1,34 @@ +#nullable enable +using System.Linq; +using System.Threading.Tasks; +using Content.Shared.Construction; +using JetBrains.Annotations; +using Robust.Shared.Containers; +using Robust.Shared.GameObjects; +using Robust.Shared.Serialization.Manager.Attributes; + +namespace Content.Server.Construction.Completions +{ + [UsedImplicitly] + [DataDefinition] + public class MoveContainer : IGraphAction + { + [field: DataField("from")] public string? FromContainer { get; } = null; + [field: DataField("to")] public string? ToContainer { get; } = null; + + public async Task PerformAction(IEntity entity, IEntity? user) + { + if (string.IsNullOrEmpty(FromContainer) || string.IsNullOrEmpty(ToContainer)) + return; + + var from = entity.EnsureContainer(FromContainer); + var to = entity.EnsureContainer(ToContainer); + + foreach (var contained in from.ContainedEntities.ToArray()) + { + if (from.Remove(contained)) + to.Insert(contained); + } + } + } +} diff --git a/Content.Server/Construction/Completions/PopupEveryone.cs b/Content.Server/Construction/Completions/PopupEveryone.cs new file mode 100644 index 0000000000..5a8bcd03f7 --- /dev/null +++ b/Content.Server/Construction/Completions/PopupEveryone.cs @@ -0,0 +1,20 @@ +#nullable enable +using System.Threading.Tasks; +using Content.Server.Utility; +using Content.Shared.Construction; +using Robust.Shared.GameObjects; +using Robust.Shared.Serialization.Manager.Attributes; + +namespace Content.Server.Construction.Completions +{ + [DataDefinition] + public class PopupEveryone : IGraphAction + { + [field: DataField("text")] public string Text { get; } = string.Empty; + + public async Task PerformAction(IEntity entity, IEntity? user) + { + entity.PopupMessageEveryone(Text); + } + } +} diff --git a/Content.Server/Construction/Completions/PopupUser.cs b/Content.Server/Construction/Completions/PopupUser.cs index 275fbd78d0..f487d72dec 100644 --- a/Content.Server/Construction/Completions/PopupUser.cs +++ b/Content.Server/Construction/Completions/PopupUser.cs @@ -12,8 +12,8 @@ namespace Content.Server.Construction.Completions [DataDefinition] public class PopupUser : IGraphAction { - [DataField("cursor")] public bool Cursor { get; private set; } = false; - [DataField("text")] public string Text { get; private set; } = string.Empty; + [field: DataField("cursor")] public bool Cursor { get; } = false; + [field: DataField("text")] public string Text { get; } = string.Empty; public async Task PerformAction(IEntity entity, IEntity? user) { diff --git a/Content.Server/Construction/Completions/SpawnPrototypeAtContainer.cs b/Content.Server/Construction/Completions/SpawnPrototypeAtContainer.cs new file mode 100644 index 0000000000..62c89169f2 --- /dev/null +++ b/Content.Server/Construction/Completions/SpawnPrototypeAtContainer.cs @@ -0,0 +1,33 @@ +#nullable enable +using System; +using System.Threading.Tasks; +using Content.Shared.Construction; +using JetBrains.Annotations; +using Robust.Shared.Containers; +using Robust.Shared.GameObjects; +using Robust.Shared.Serialization.Manager.Attributes; + +namespace Content.Server.Construction.Completions +{ + [UsedImplicitly] + [DataDefinition] + public class SpawnPrototypeAtContainer : IGraphAction + { + [field: DataField("prototype")] public string Prototype { get; } = string.Empty; + [field: DataField("container")] public string Container { get; } = string.Empty; + [field: DataField("amount")] public int Amount { get; } = 1; + + public async Task PerformAction(IEntity entity, IEntity? user) + { + if (entity.Deleted || string.IsNullOrEmpty(Container) || string.IsNullOrEmpty(Prototype)) + return; + + var container = entity.EnsureContainer(Container); + + for (var i = 0; i < Amount; i++) + { + container.Insert(entity.EntityManager.SpawnEntity(Prototype, entity.Transform.Coordinates)); + } + } + } +} diff --git a/Content.Server/Construction/Conditions/AllConditions.cs b/Content.Server/Construction/Conditions/AllConditions.cs new file mode 100644 index 0000000000..ccba846da1 --- /dev/null +++ b/Content.Server/Construction/Conditions/AllConditions.cs @@ -0,0 +1,28 @@ +using System; +using System.Threading.Tasks; +using Content.Shared.Construction; +using JetBrains.Annotations; +using Robust.Shared.GameObjects; +using Robust.Shared.Serialization.Manager.Attributes; + +namespace Content.Server.Construction.Conditions +{ + [UsedImplicitly] + [DataDefinition] + public class AllConditions : IEdgeCondition + { + [field: DataField("conditions")] + public IEdgeCondition[] Conditions { get; } = Array.Empty(); + + public async Task Condition(IEntity entity) + { + foreach (var condition in Conditions) + { + if (!await condition.Condition(entity)) + return false; + } + + return true; + } + } +} diff --git a/Content.Server/Construction/Conditions/AnyConditions.cs b/Content.Server/Construction/Conditions/AnyConditions.cs new file mode 100644 index 0000000000..2b1eec22c3 --- /dev/null +++ b/Content.Server/Construction/Conditions/AnyConditions.cs @@ -0,0 +1,28 @@ +using System; +using System.Threading.Tasks; +using Content.Shared.Construction; +using JetBrains.Annotations; +using Robust.Shared.GameObjects; +using Robust.Shared.Serialization.Manager.Attributes; + +namespace Content.Server.Construction.Conditions +{ + [UsedImplicitly] + [DataDefinition] + public class AnyConditions : IEdgeCondition + { + [field: DataField("conditions")] + public IEdgeCondition[] Conditions { get; } = Array.Empty(); + + public async Task Condition(IEntity entity) + { + foreach (var condition in Conditions) + { + if (await condition.Condition(entity)) + return true; + } + + return false; + } + } +} diff --git a/Content.Server/Construction/Conditions/ContainerNotEmpty.cs b/Content.Server/Construction/Conditions/ContainerNotEmpty.cs new file mode 100644 index 0000000000..b3957b7ee3 --- /dev/null +++ b/Content.Server/Construction/Conditions/ContainerNotEmpty.cs @@ -0,0 +1,40 @@ +#nullable enable +using System.Threading.Tasks; +using Content.Shared.Construction; +using JetBrains.Annotations; +using Robust.Shared.Containers; +using Robust.Shared.GameObjects; +using Robust.Shared.Serialization.Manager.Attributes; +using Robust.Shared.Utility; + +namespace Content.Server.Construction.Conditions +{ + [UsedImplicitly] + [DataDefinition] + public class ContainerNotEmpty : IEdgeCondition + { + [DataField("container")] public string Container { get; private set; } = string.Empty; + [DataField("text")] public string Text { get; private set; } = string.Empty; + + public async Task Condition(IEntity entity) + { + if (!entity.TryGetComponent(out ContainerManagerComponent? containerManager) || + !containerManager.TryGetContainer(Container, out var container)) return false; + + return container.ContainedEntities.Count != 0; + } + + public bool DoExamine(IEntity entity, FormattedMessage message, bool inDetailsRange) + { + if (!entity.TryGetComponent(out ContainerManagerComponent? containerManager) || + !containerManager.TryGetContainer(Container, out var container)) return false; + + if (container.ContainedEntities.Count != 0) + return false; + + message.AddMarkup(Text); + return true; + + } + } +} diff --git a/Content.Server/GameObjects/Components/Construction/ConstructionComponent.cs b/Content.Server/GameObjects/Components/Construction/ConstructionComponent.cs index 675f5140c4..fcce69485e 100644 --- a/Content.Server/GameObjects/Components/Construction/ConstructionComponent.cs +++ b/Content.Server/GameObjects/Components/Construction/ConstructionComponent.cs @@ -281,7 +281,7 @@ namespace Content.Server.GameObjects.Components.Construction else { _containers.Add(insertStep.Store); - var container = ContainerHelpers.EnsureContainer(Owner, insertStep.Store); + var container = Owner.EnsureContainer(insertStep.Store); container.Insert(entityUsing); } diff --git a/Resources/Prototypes/Recipes/Construction/Graphs/turing.yml b/Resources/Prototypes/Recipes/Construction/Graphs/turing.yml new file mode 100644 index 0000000000..9a57fb78e6 --- /dev/null +++ b/Resources/Prototypes/Recipes/Construction/Graphs/turing.yml @@ -0,0 +1,296 @@ +- type: constructionGraph + id: turing + start: a0 + graph: + - node: a0 + actions: + - !type:PopupEveryone + text: "Input bit A0..." + edges: + # Input 1 + - to: a1 + steps: + - material: Steel + amount: 1 + store: a0 + # Input 0 + - to: a1 + steps: + - material: Glass + amount: 1 + + - node: a1 + actions: + - !type:PopupEveryone + text: "Input bit A1..." + edges: + # Input 1 + - to: b0 + steps: + - material: Steel + amount: 1 + store: a1 + # Input 0 + - to: b0 + steps: + - material: Glass + amount: 1 + + - node: b0 + actions: + - !type:PopupEveryone + text: "Input bit B0..." + edges: + # Input 1 + - to: b1 + steps: + - material: Steel + amount: 1 + store: b0 + # Input 0 + - to: b1 + steps: + - material: Glass + amount: 1 + + - node: b1 + actions: + - !type:PopupEveryone + text: "Input bit B1..." + edges: + # Input 1 + - to: result + steps: + - material: Steel + amount: 1 + store: b1 + # Input 0 + - to: result + steps: + - material: Glass + amount: 1 + + - node: result + actions: + # Carry 0 + - !type:ConditionalAction + condition: + !type:AllConditions + conditions: + - !type:ContainerNotEmpty + container: a0 + - !type:ContainerNotEmpty + container: b0 + action: + !type:SpawnPrototypeAtContainer + prototype: SheetSteel1 + container: ca0 + + # Output bit 0 + - !type:ConditionalAction + condition: + !type:AnyConditions + conditions: + - !type:AllConditions + conditions: + - !type:ContainerEmpty + container: a0 + - !type:ContainerNotEmpty + container: b0 + - !type:AllConditions + conditions: + - !type:ContainerNotEmpty + container: a0 + - !type:ContainerEmpty + container: b0 + action: + !type:SpawnPrototypeAtContainer + prototype: SheetSteel1 + container: c0 + + # Temp bit 0 + - !type:ConditionalAction + condition: + !type:AnyConditions + conditions: + - !type:AllConditions + conditions: + - !type:ContainerEmpty + container: a1 + - !type:ContainerNotEmpty + container: b1 + - !type:AllConditions + conditions: + - !type:ContainerNotEmpty + container: a1 + - !type:ContainerEmpty + container: b1 + action: + !type:SpawnPrototypeAtContainer + prototype: SheetSteel1 + container: t0 + + # Output bit 1 + - !type:ConditionalAction + condition: + !type:AnyConditions + conditions: + - !type:AllConditions + conditions: + - !type:ContainerEmpty + container: t0 + - !type:ContainerNotEmpty + container: ca0 + - !type:AllConditions + conditions: + - !type:ContainerNotEmpty + container: t0 + - !type:ContainerEmpty + container: ca0 + action: + !type:SpawnPrototypeAtContainer + prototype: SheetSteel1 + container: c1 + + # Temp bit 1 + - !type:ConditionalAction + condition: + !type:AllConditions + conditions: + - !type:ContainerNotEmpty + container: a1 + - !type:ContainerNotEmpty + container: b1 + action: + !type:SpawnPrototypeAtContainer + prototype: SheetSteel1 + container: t1 + + # Temp bit 2 + - !type:ConditionalAction + condition: + !type:AllConditions + conditions: + - !type:ContainerNotEmpty + container: t0 + - !type:ContainerNotEmpty + container: ca0 + action: + !type:SpawnPrototypeAtContainer + prototype: SheetSteel1 + container: t2 + + # Output bit 2 + - !type:ConditionalAction + condition: + !type:AnyConditions + conditions: + - !type:ContainerNotEmpty + container: t1 + - !type:ContainerNotEmpty + container: t2 + action: + !type:SpawnPrototypeAtContainer + prototype: SheetSteel1 + container: c2 + + # Print the result! + - !type:ConditionalAction + condition: + !type:ContainerEmpty + container: c0 + # If c0 == 0... + action: + !type:ConditionalAction + condition: + !type:ContainerEmpty + container: c1 + # If c1 == 0... + action: + !type:ConditionalAction + condition: + !type:ContainerEmpty + container: c2 + # If c2 == 0... + action: + !type:PopupEveryone + text: "Result: 0" + # If c2 == 1... + else: + !type:PopupEveryone + text: "Result: 4" + # If c1 == 1... + else: + !type:ConditionalAction + condition: + !type:ContainerEmpty + container: c2 + # If c2 == 0... + action: + !type:PopupEveryone + text: "Result: 2" + # If c2 == 1... + else: + !type:PopupEveryone + text: "Result: 6" + # If c0 == 1... + else: + !type:ConditionalAction + condition: + !type:ContainerEmpty + container: c1 + # If c1 == 0... + action: + !type:ConditionalAction + condition: + !type:ContainerEmpty + container: c2 + # If c2 == 0... + action: + !type:PopupEveryone + text: "Result: 1" + # If c2 == 1... + else: + !type:PopupEveryone + text: "Result: 5" + # If c1 == 1... + else: + !type:ConditionalAction + condition: + !type:ContainerEmpty + container: c2 + # If c2 == 0... + action: + !type:PopupEveryone + text: "Result: 3" + # If c2 == 1... + else: + !type:PopupEveryone + text: "Result: 7" + edges: + - to: a0 + completed: + - !type:DeleteEntitiesInContainer + container: a0 + - !type:DeleteEntitiesInContainer + container: a1 + - !type:DeleteEntitiesInContainer + container: b0 + - !type:DeleteEntitiesInContainer + container: b1 + - !type:DeleteEntitiesInContainer + container: c0 + - !type:DeleteEntitiesInContainer + container: c1 + - !type:DeleteEntitiesInContainer + container: c2 + - !type:DeleteEntitiesInContainer + container: t0 + - !type:DeleteEntitiesInContainer + container: t1 + - !type:DeleteEntitiesInContainer + container: t2 + - !type:DeleteEntitiesInContainer + container: ca0 + steps: + - tool: Prying