diff --git a/Content.Client/Construction/UI/ConstructionMenuPresenter.cs b/Content.Client/Construction/UI/ConstructionMenuPresenter.cs index d7d899fbfe..cbd7bcb62b 100644 --- a/Content.Client/Construction/UI/ConstructionMenuPresenter.cs +++ b/Content.Client/Construction/UI/ConstructionMenuPresenter.cs @@ -291,58 +291,6 @@ namespace Content.Client.Construction.UI ("name", arbitraryStep.Name)), icon); break; - - case NestedConstructionGraphStep nestedStep: - var parallelNumber = 1; - stepList.AddItem(Loc.GetString("construction-presenter-nested-step", ("step-number", stepNumber++))); - - foreach (var steps in nestedStep.Steps) - { - var subStepNumber = 1; - - foreach (var subStep in steps) - { - icon = GetTextureForStep(_resourceCache, subStep); - - switch (subStep) - { - case MaterialConstructionGraphStep materialStep: - if (prototype.Type != ConstructionType.Item) stepList.AddItem(Loc.GetString( - "construction-presenter-material-substep", - ("step-number", stepNumber), - ("parallel-number", parallelNumber), - ("substep-number", subStepNumber++), - ("amount", materialStep.Amount), - ("material", materialStep.MaterialPrototype.Name)), - icon); - break; - - case ToolConstructionGraphStep toolStep: - stepList.AddItem(Loc.GetString( - "construction-presenter-tool-substep", - ("step-number", stepNumber), - ("parallel-number", parallelNumber), - ("substep-number", subStepNumber++), - ("tool", Loc.GetString(_prototypeManager.Index(toolStep.Tool).ToolName))), - icon); - break; - - case ArbitraryInsertConstructionGraphStep arbitraryStep: - stepList.AddItem(Loc.GetString( - "construction-presenter-arbitrary-substep", - ("step-number", stepNumber), - ("parallel-number", parallelNumber), - ("substep-number", subStepNumber++), - ("name", arbitraryStep.Name)), - icon); - break; - } - } - - parallelNumber++; - } - - break; } } @@ -362,9 +310,6 @@ namespace Content.Client.Construction.UI case ArbitraryInsertConstructionGraphStep arbitraryStep: return arbitraryStep.Icon?.Frame0(); - - case NestedConstructionGraphStep: - return null; } return null; diff --git a/Content.Server/Computer/ComputerComponent.cs b/Content.Server/Computer/ComputerComponent.cs index 9e22500007..f20c1e5153 100644 --- a/Content.Server/Computer/ComputerComponent.cs +++ b/Content.Server/Computer/ComputerComponent.cs @@ -1,3 +1,4 @@ +using Content.Server.Construction; using Content.Server.Construction.Components; using Content.Server.Power.Components; using Content.Shared.Computer; @@ -59,7 +60,7 @@ namespace Content.Server.Computer { // Ensure that the construction component is aware of the board container. if (Owner.TryGetComponent(out ConstructionComponent? construction)) - construction.AddContainer("board"); + EntitySystem.Get().AddContainer(Owner.Uid, "board", construction); // We don't do anything if this is null or empty. if (string.IsNullOrEmpty(_boardPrototype)) diff --git a/Content.Server/Construction/Completions/AddContainer.cs b/Content.Server/Construction/Completions/AddContainer.cs index 529b2f46a9..a3236b0901 100644 --- a/Content.Server/Construction/Completions/AddContainer.cs +++ b/Content.Server/Construction/Completions/AddContainer.cs @@ -13,13 +13,12 @@ namespace Content.Server.Construction.Completions { [DataField("container")] public string? Container { get; private set; } = null; - public async Task PerformAction(IEntity entity, IEntity? user) + public void PerformAction(EntityUid uid, EntityUid? userUid, IEntityManager entityManager) { - if (entity.Deleted || string.IsNullOrEmpty(Container)) + if (string.IsNullOrEmpty(Container)) return; - var construction = entity.GetComponent(); - construction.AddContainer(Container); + entityManager.EntitySysManager.GetEntitySystem().AddContainer(uid, Container); } } } diff --git a/Content.Server/Construction/Completions/AttemptElectrocute.cs b/Content.Server/Construction/Completions/AttemptElectrocute.cs index 944dbcc83e..b8999907e8 100644 --- a/Content.Server/Construction/Completions/AttemptElectrocute.cs +++ b/Content.Server/Construction/Completions/AttemptElectrocute.cs @@ -1,4 +1,3 @@ -using System.Threading.Tasks; using Content.Server.Electrocution; using Content.Shared.Construction; using Robust.Shared.GameObjects; @@ -9,12 +8,12 @@ namespace Content.Server.Construction.Completions [DataDefinition] public class AttemptElectrocute : IGraphAction { - public async Task PerformAction(IEntity entity, IEntity? user) + public void PerformAction(EntityUid uid, EntityUid? userUid, IEntityManager entityManager) { - if (user == null) + if (userUid == null) return; - EntitySystem.Get().TryDoElectrifiedAct(entity.Uid, user.Uid); + EntitySystem.Get().TryDoElectrifiedAct(uid, userUid.Value); } } } diff --git a/Content.Server/Construction/Completions/BuildComputer.cs b/Content.Server/Construction/Completions/BuildComputer.cs index 8ea09c332a..948c40ce4f 100644 --- a/Content.Server/Construction/Completions/BuildComputer.cs +++ b/Content.Server/Construction/Completions/BuildComputer.cs @@ -3,6 +3,7 @@ using System.Threading.Tasks; using Content.Server.Construction.Components; using Content.Shared.Construction; using JetBrains.Annotations; +using Robust.Server.Containers; using Robust.Shared.Containers; using Robust.Shared.GameObjects; using Robust.Shared.Log; @@ -16,60 +17,57 @@ namespace Content.Server.Construction.Completions { [DataField("container")] public string Container { get; private set; } = string.Empty; - public async Task PerformAction(IEntity entity, IEntity? user) + public void PerformAction(EntityUid uid, EntityUid? userUid, IEntityManager entityManager) { - if (!entity.TryGetComponent(out ContainerManagerComponent? containerManager)) + if (!entityManager.TryGetComponent(uid, out ContainerManagerComponent? containerManager)) { - Logger.Warning($"Computer entity {entity} did not have a container manager! Aborting build computer action."); + Logger.Warning($"Computer entity {uid} did not have a container manager! Aborting build computer action."); return; } - if (!containerManager.TryGetContainer(Container, out var container)) + var containerSystem = entityManager.EntitySysManager.GetEntitySystem(); + + if (!containerSystem.TryGetContainer(uid, Container, out var container, containerManager)) { - Logger.Warning($"Computer entity {entity} did not have the specified '{Container}' container! Aborting build computer action."); + Logger.Warning($"Computer entity {uid} did not have the specified '{Container}' container! Aborting build computer action."); return; } if (container.ContainedEntities.Count != 1) { - Logger.Warning($"Computer entity {entity} did not have exactly one item in the specified '{Container}' container! Aborting build computer action."); + Logger.Warning($"Computer entity {uid} did not have exactly one item in the specified '{Container}' container! Aborting build computer action."); } var board = container.ContainedEntities[0]; if (!board.TryGetComponent(out ComputerBoardComponent? boardComponent)) { - Logger.Warning($"Computer entity {entity} had an invalid entity in container \"{Container}\"! Aborting build computer action."); + Logger.Warning($"Computer entity {uid} had an invalid entity in container \"{Container}\"! Aborting build computer action."); return; } - var entityManager = entity.EntityManager; container.Remove(board); - var computer = entityManager.SpawnEntity(boardComponent.Prototype, entity.Transform.Coordinates); - computer.Transform.LocalRotation = entity.Transform.LocalRotation; + var transform = entityManager.GetComponent(uid); + var computer = entityManager.SpawnEntity(boardComponent.Prototype, transform.Coordinates); + computer.Transform.LocalRotation = transform.LocalRotation; - var computerContainer = ContainerHelpers.EnsureContainer(computer, Container, out var existed); + var computerContainer = containerSystem.EnsureContainer(computer.Uid, Container); - if (existed) + // In case it already existed and there are any entities inside the container, delete them. + foreach (var ent in computerContainer.ContainedEntities.ToArray()) { - // In case there are any entities inside this, delete them. - foreach (var ent in computerContainer.ContainedEntities.ToArray()) - { - computerContainer.ForceRemove(ent); - ent.Delete(); - } + computerContainer.ForceRemove(ent); + ent.Delete(); } computerContainer.Insert(board); - if (computer.TryGetComponent(out ConstructionComponent? construction)) - { - // We only add this container. If some construction needs to take other containers into account, fix this. - construction.AddContainer(Container); - } + // We only add this container. If some construction needs to take other containers into account, fix this. + entityManager.EntitySysManager.GetEntitySystem().AddContainer(computer.Uid, Container); - entity.Delete(); + // Delete the original entity. + entityManager.DeleteEntity(uid); } } } diff --git a/Content.Server/Construction/Completions/BuildMachine.cs b/Content.Server/Construction/Completions/BuildMachine.cs index 7828200220..8749bd5950 100644 --- a/Content.Server/Construction/Completions/BuildMachine.cs +++ b/Content.Server/Construction/Completions/BuildMachine.cs @@ -14,58 +14,58 @@ namespace Content.Server.Construction.Completions [DataDefinition] public class BuildMachine : IGraphAction { - public async Task PerformAction(IEntity entity, IEntity? user) + public void PerformAction(EntityUid uid, EntityUid? userUid, IEntityManager entityManager) { - if (!entity.TryGetComponent(out ContainerManagerComponent? containerManager)) + if (!entityManager.TryGetComponent(uid, out ContainerManagerComponent? containerManager)) { - Logger.Warning($"Machine frame entity {entity} did not have a container manager! Aborting build machine action."); + Logger.Warning($"Machine frame entity {uid} did not have a container manager! Aborting build machine action."); return; } - if (!entity.TryGetComponent(out MachineFrameComponent? machineFrame)) + if (!entityManager.TryGetComponent(uid, out MachineFrameComponent? machineFrame)) { - Logger.Warning($"Machine frame entity {entity} did not have a machine frame component! Aborting build machine action."); + Logger.Warning($"Machine frame entity {uid} did not have a machine frame component! Aborting build machine action."); return; } if (!machineFrame.IsComplete) { - Logger.Warning($"Machine frame entity {entity} doesn't have all required parts to be built! Aborting build machine action."); + Logger.Warning($"Machine frame entity {uid} doesn't have all required parts to be built! Aborting build machine action."); return; } if (!containerManager.TryGetContainer(MachineFrameComponent.BoardContainer, out var entBoardContainer)) { - Logger.Warning($"Machine frame entity {entity} did not have the '{MachineFrameComponent.BoardContainer}' container! Aborting build machine action."); + Logger.Warning($"Machine frame entity {uid} did not have the '{MachineFrameComponent.BoardContainer}' container! Aborting build machine action."); return; } if (!containerManager.TryGetContainer(MachineFrameComponent.PartContainer, out var entPartContainer)) { - Logger.Warning($"Machine frame entity {entity} did not have the '{MachineFrameComponent.PartContainer}' container! Aborting build machine action."); + Logger.Warning($"Machine frame entity {uid} did not have the '{MachineFrameComponent.PartContainer}' container! Aborting build machine action."); return; } if (entBoardContainer.ContainedEntities.Count != 1) { - Logger.Warning($"Machine frame entity {entity} did not have exactly one item in the '{MachineFrameComponent.BoardContainer}' container! Aborting build machine action."); + Logger.Warning($"Machine frame entity {uid} did not have exactly one item in the '{MachineFrameComponent.BoardContainer}' container! Aborting build machine action."); } var board = entBoardContainer.ContainedEntities[0]; if (!board.TryGetComponent(out MachineBoardComponent? boardComponent)) { - Logger.Warning($"Machine frame entity {entity} had an invalid entity in container \"{MachineFrameComponent.BoardContainer}\"! Aborting build machine action."); + Logger.Warning($"Machine frame entity {uid} had an invalid entity in container \"{MachineFrameComponent.BoardContainer}\"! Aborting build machine action."); return; } - var entityManager = entity.EntityManager; entBoardContainer.Remove(board); - var machine = entityManager.SpawnEntity(boardComponent.Prototype, entity.Transform.Coordinates); - machine.Transform.LocalRotation = entity.Transform.LocalRotation; + var transform = entityManager.GetComponent(uid); + var machine = entityManager.SpawnEntity(boardComponent.Prototype, transform.Coordinates); + machine.Transform.LocalRotation = transform.LocalRotation; - var boardContainer = ContainerHelpers.EnsureContainer(machine, MachineFrameComponent.BoardContainer, out var existed); + var boardContainer = machine.EnsureContainer(MachineFrameComponent.BoardContainer, out var existed); if (existed) { @@ -73,7 +73,7 @@ namespace Content.Server.Construction.Completions boardContainer.CleanContainer(); } - var partContainer = ContainerHelpers.EnsureContainer(machine, MachineFrameComponent.PartContainer, out existed); + var partContainer = machine.EnsureContainer(MachineFrameComponent.PartContainer, out existed); if (existed) { @@ -90,11 +90,12 @@ namespace Content.Server.Construction.Completions partContainer.Insert(part); } + var constructionSystem = entityManager.EntitySysManager.GetEntitySystem(); if (machine.TryGetComponent(out ConstructionComponent? construction)) { // We only add these two container. If some construction needs to take other containers into account, fix this. - construction.AddContainer(MachineFrameComponent.BoardContainer); - construction.AddContainer(MachineFrameComponent.PartContainer); + constructionSystem.AddContainer(machine.Uid, MachineFrameComponent.BoardContainer, construction); + constructionSystem.AddContainer(machine.Uid, MachineFrameComponent.PartContainer, construction); } if (machine.TryGetComponent(out MachineComponent? machineComp)) @@ -102,7 +103,7 @@ namespace Content.Server.Construction.Completions machineComp.RefreshParts(); } - entity.Delete(); + entityManager.DeleteEntity(uid); } } } diff --git a/Content.Server/Construction/Completions/ConditionalAction.cs b/Content.Server/Construction/Completions/ConditionalAction.cs index 24b5956664..6fbfe0ef0b 100644 --- a/Content.Server/Construction/Completions/ConditionalAction.cs +++ b/Content.Server/Construction/Completions/ConditionalAction.cs @@ -18,15 +18,15 @@ namespace Content.Server.Construction.Completions [DataField("else")] public IGraphAction? Else { get; } = null; - public async Task PerformAction(IEntity entity, IEntity? user) + public void PerformAction(EntityUid uid, EntityUid? userUid, IEntityManager entityManager) { 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); + if (Condition.Condition(PassUser && userUid != null ? userUid.Value : uid, entityManager)) + Action.PerformAction(uid, userUid, entityManager); + else + Else?.PerformAction(uid, userUid, entityManager); } } } diff --git a/Content.Server/Construction/Completions/DeleteEntitiesInContainer.cs b/Content.Server/Construction/Completions/DeleteEntitiesInContainer.cs index ac3890d7dc..c66e8abf1d 100644 --- a/Content.Server/Construction/Completions/DeleteEntitiesInContainer.cs +++ b/Content.Server/Construction/Completions/DeleteEntitiesInContainer.cs @@ -12,10 +12,11 @@ namespace Content.Server.Construction.Completions { [DataField("container")] public string Container { get; } = string.Empty; - public async Task PerformAction(IEntity entity, IEntity? user) + public void PerformAction(EntityUid uid, EntityUid? userUid, IEntityManager entityManager) { if (string.IsNullOrEmpty(Container)) return; - if (!entity.TryGetComponent(out ContainerManagerComponent? containerMan)) return; + // TODO CONSTRUCTION: Use the new ContainerSystem methods here. + if (!entityManager.TryGetComponent(uid, out ContainerManagerComponent? containerMan)) return; if (!containerMan.TryGetContainer(Container, out var container)) return; foreach (var contained in container.ContainedEntities.ToArray()) diff --git a/Content.Server/Construction/Completions/DeleteEntity.cs b/Content.Server/Construction/Completions/DeleteEntity.cs index 762a8d68d2..7515a31088 100644 --- a/Content.Server/Construction/Completions/DeleteEntity.cs +++ b/Content.Server/Construction/Completions/DeleteEntity.cs @@ -10,11 +10,9 @@ namespace Content.Server.Construction.Completions [DataDefinition] public class DeleteEntity : IGraphAction { - public async Task PerformAction(IEntity entity, IEntity? user) + public void PerformAction(EntityUid uid, EntityUid? userUid, IEntityManager entityManager) { - if (entity.Deleted) return; - - entity.Delete(); + entityManager.DeleteEntity(uid); } } } diff --git a/Content.Server/Construction/Completions/DestroyEntity.cs b/Content.Server/Construction/Completions/DestroyEntity.cs index 82018dac97..3a2f1cfd4a 100644 --- a/Content.Server/Construction/Completions/DestroyEntity.cs +++ b/Content.Server/Construction/Completions/DestroyEntity.cs @@ -11,9 +11,10 @@ namespace Content.Server.Construction.Completions [DataDefinition] public class DestroyEntity : IGraphAction { - public async Task PerformAction(IEntity entity, IEntity? user) + public void PerformAction(EntityUid uid, EntityUid? userUid, IEntityManager entityManager) { - if (entity.Deleted) return; + if (!entityManager.TryGetEntity(uid, out var entity)) + return; // This should never happen, but. var destructibleSystem = EntitySystem.Get(); destructibleSystem.ActSystem.HandleDestruction(entity); diff --git a/Content.Server/Construction/Completions/EmptyAllContainers.cs b/Content.Server/Construction/Completions/EmptyAllContainers.cs index 799c21d9ab..376082ec07 100644 --- a/Content.Server/Construction/Completions/EmptyAllContainers.cs +++ b/Content.Server/Construction/Completions/EmptyAllContainers.cs @@ -11,14 +11,15 @@ namespace Content.Server.Construction.Completions [DataDefinition] public class EmptyAllContainers : IGraphAction { - public async Task PerformAction(IEntity entity, IEntity? user) + public void PerformAction(EntityUid uid, EntityUid? userUid, IEntityManager entityManager) { - if (entity.Deleted || !entity.TryGetComponent(out var containerManager)) + if (!entityManager.TryGetComponent(uid, out ContainerManagerComponent? containerManager)) return; + var transform = entityManager.GetComponent(uid); foreach (var container in containerManager.GetAllContainers()) { - container.EmptyContainer(true, entity.Transform.Coordinates); + container.EmptyContainer(true, transform.Coordinates); } } } diff --git a/Content.Server/Construction/Completions/EmptyContainer.cs b/Content.Server/Construction/Completions/EmptyContainer.cs index 3099e1dbff..83c19e223b 100644 --- a/Content.Server/Construction/Completions/EmptyContainer.cs +++ b/Content.Server/Construction/Completions/EmptyContainer.cs @@ -4,6 +4,7 @@ using Content.Shared.Construction; using JetBrains.Annotations; using Robust.Shared.Containers; using Robust.Shared.GameObjects; +using Robust.Shared.IoC; using Robust.Shared.Serialization.Manager.Attributes; namespace Content.Server.Construction.Completions @@ -14,18 +15,17 @@ namespace Content.Server.Construction.Completions { [DataField("container")] public string Container { get; private set; } = string.Empty; - public async Task PerformAction(IEntity entity, IEntity? user) + public void PerformAction(EntityUid uid, EntityUid? userUid, IEntityManager entityManager) { - if (entity.Deleted) return; - - if (!entity.TryGetComponent(out ContainerManagerComponent? containerManager) || + if (!entityManager.TryGetComponent(uid, out ContainerManagerComponent? containerManager) || !containerManager.TryGetContainer(Container, out var container)) return; - // TODO: Use container helpers. + // TODO: Use container system methods. + var transform = entityManager.GetComponent(uid); foreach (var contained in container.ContainedEntities.ToArray()) { container.ForceRemove(contained); - contained.Transform.Coordinates = entity.Transform.Coordinates; + contained.Transform.Coordinates = transform.Coordinates; contained.Transform.AttachToGridOrMap(); } } diff --git a/Content.Server/Construction/Completions/MachineFrameRegenerateProgress.cs b/Content.Server/Construction/Completions/MachineFrameRegenerateProgress.cs index c43dfee278..4249308063 100644 --- a/Content.Server/Construction/Completions/MachineFrameRegenerateProgress.cs +++ b/Content.Server/Construction/Completions/MachineFrameRegenerateProgress.cs @@ -11,12 +11,9 @@ namespace Content.Server.Construction.Completions [DataDefinition] public class MachineFrameRegenerateProgress : IGraphAction { - public async Task PerformAction(IEntity entity, IEntity? user) + public void PerformAction(EntityUid uid, EntityUid? userUid, IEntityManager entityManager) { - if (entity.Deleted) - return; - - if (entity.TryGetComponent(out var machineFrame)) + if (entityManager.TryGetComponent(uid, out var machineFrame)) { machineFrame.RegenerateProgress(); } diff --git a/Content.Server/Construction/Completions/MoveContainer.cs b/Content.Server/Construction/Completions/MoveContainer.cs index 47a7fd0ae1..2444de2a52 100644 --- a/Content.Server/Construction/Completions/MoveContainer.cs +++ b/Content.Server/Construction/Completions/MoveContainer.cs @@ -2,6 +2,7 @@ using System.Linq; using System.Threading.Tasks; using Content.Shared.Construction; using JetBrains.Annotations; +using Robust.Server.Containers; using Robust.Shared.Containers; using Robust.Shared.GameObjects; using Robust.Shared.Serialization.Manager.Attributes; @@ -15,13 +16,16 @@ namespace Content.Server.Construction.Completions [DataField("from")] public string? FromContainer { get; } = null; [DataField("to")] public string? ToContainer { get; } = null; - public async Task PerformAction(IEntity entity, IEntity? user) + public void PerformAction(EntityUid uid, EntityUid? userUid, IEntityManager entityManager) { if (string.IsNullOrEmpty(FromContainer) || string.IsNullOrEmpty(ToContainer)) return; - var from = entity.EnsureContainer(FromContainer); - var to = entity.EnsureContainer(ToContainer); + var containerSystem = entityManager.EntitySysManager.GetEntitySystem(); + var containerManager = entityManager.EnsureComponent(uid); + + var from = containerSystem.EnsureContainer(uid, FromContainer, containerManager); + var to = containerSystem.EnsureContainer(uid, ToContainer, containerManager); foreach (var contained in from.ContainedEntities.ToArray()) { diff --git a/Content.Server/Construction/Completions/PlaySound.cs b/Content.Server/Construction/Completions/PlaySound.cs index 3fbf6f5a1b..0324b1e385 100644 --- a/Content.Server/Construction/Completions/PlaySound.cs +++ b/Content.Server/Construction/Completions/PlaySound.cs @@ -16,9 +16,9 @@ namespace Content.Server.Construction.Completions { [DataField("sound", required: true)] public SoundSpecifier Sound { get; private set; } = default!; - public async Task PerformAction(IEntity entity, IEntity? user) + public void PerformAction(EntityUid uid, EntityUid? userUid, IEntityManager entityManager) { - SoundSystem.Play(Filter.Pvs(entity), Sound.GetSound(), entity, AudioHelpers.WithVariation(0.125f)); + SoundSystem.Play(Filter.Pvs(uid), Sound.GetSound(), uid, AudioHelpers.WithVariation(0.125f)); } } } diff --git a/Content.Server/Construction/Completions/PopupEveryone.cs b/Content.Server/Construction/Completions/PopupEveryone.cs index cec7251f5b..900d3fa276 100644 --- a/Content.Server/Construction/Completions/PopupEveryone.cs +++ b/Content.Server/Construction/Completions/PopupEveryone.cs @@ -2,6 +2,8 @@ using System.Threading.Tasks; using Content.Server.Popups; using Content.Shared.Construction; using Robust.Shared.GameObjects; +using Robust.Shared.Localization; +using Robust.Shared.Player; using Robust.Shared.Serialization.Manager.Attributes; namespace Content.Server.Construction.Completions @@ -11,9 +13,10 @@ namespace Content.Server.Construction.Completions { [DataField("text")] public string Text { get; } = string.Empty; - public async Task PerformAction(IEntity entity, IEntity? user) + public void PerformAction(EntityUid uid, EntityUid? userUid, IEntityManager entityManager) { - entity.PopupMessageEveryone(Text); + entityManager.EntitySysManager.GetEntitySystem() + .PopupEntity(Loc.GetString(Text), uid, Filter.Pvs(uid, entityManager:entityManager)); } } } diff --git a/Content.Server/Construction/Completions/PopupUser.cs b/Content.Server/Construction/Completions/PopupUser.cs index 48e8850634..0f48f07fbf 100644 --- a/Content.Server/Construction/Completions/PopupUser.cs +++ b/Content.Server/Construction/Completions/PopupUser.cs @@ -1,8 +1,11 @@ using System.Threading.Tasks; +using Content.Server.Popups; using Content.Shared.Construction; using Content.Shared.Popups; using JetBrains.Annotations; using Robust.Shared.GameObjects; +using Robust.Shared.Localization; +using Robust.Shared.Player; using Robust.Shared.Serialization.Manager.Attributes; namespace Content.Server.Construction.Completions @@ -14,14 +17,17 @@ namespace Content.Server.Construction.Completions [DataField("cursor")] public bool Cursor { get; } = false; [DataField("text")] public string Text { get; } = string.Empty; - public async Task PerformAction(IEntity entity, IEntity? user) + public void PerformAction(EntityUid uid, EntityUid? userUid, IEntityManager entityManager) { - if (user == null) return; + if (userUid == null) + return; + + var popupSystem = entityManager.EntitySysManager.GetEntitySystem(); if(Cursor) - user.PopupMessageCursor(Text); + popupSystem.PopupCursor(Loc.GetString(Text), Filter.Entities(userUid.Value)); else - entity.PopupMessage(user, Text); + popupSystem.PopupEntity(Loc.GetString(Text), uid, Filter.Entities(userUid.Value)); } } } diff --git a/Content.Server/Construction/Completions/SetAnchor.cs b/Content.Server/Construction/Completions/SetAnchor.cs index 7c9b781abe..c05f444241 100644 --- a/Content.Server/Construction/Completions/SetAnchor.cs +++ b/Content.Server/Construction/Completions/SetAnchor.cs @@ -14,11 +14,10 @@ namespace Content.Server.Construction.Completions { [DataField("value")] public bool Value { get; private set; } = true; - public async Task PerformAction(IEntity entity, IEntity? user) + public void PerformAction(EntityUid uid, EntityUid? userUid, IEntityManager entityManager) { - if (!entity.TryGetComponent(out IPhysBody? physics)) return; - - physics.BodyType = Value ? BodyType.Static : BodyType.Dynamic; + var transform = entityManager.GetComponent(uid); + transform.Anchored = Value; } } } diff --git a/Content.Server/Construction/Completions/SetStackCount.cs b/Content.Server/Construction/Completions/SetStackCount.cs index 1e10189150..0c3cbd59bf 100644 --- a/Content.Server/Construction/Completions/SetStackCount.cs +++ b/Content.Server/Construction/Completions/SetStackCount.cs @@ -13,11 +13,9 @@ namespace Content.Server.Construction.Completions { [DataField("amount")] public int Amount { get; } = 1; - public async Task PerformAction(IEntity entity, IEntity? user) + public void PerformAction(EntityUid uid, EntityUid? userUid, IEntityManager entityManager) { - if (entity.Deleted) return; - - EntitySystem.Get().SetCount(entity.Uid, Amount); + EntitySystem.Get().SetCount(uid, Amount); } } } diff --git a/Content.Server/Construction/Completions/SnapToGrid.cs b/Content.Server/Construction/Completions/SnapToGrid.cs index 5b5f7be369..5d58a9c149 100644 --- a/Content.Server/Construction/Completions/SnapToGrid.cs +++ b/Content.Server/Construction/Completions/SnapToGrid.cs @@ -14,14 +14,14 @@ namespace Content.Server.Construction.Completions { [DataField("southRotation")] public bool SouthRotation { get; private set; } = false; - public async Task PerformAction(IEntity entity, IEntity? user) + public void PerformAction(EntityUid uid, EntityUid? userUid, IEntityManager entityManager) { - if (entity.Deleted) return; + var transform = entityManager.GetComponent(uid); + transform.Coordinates = transform.Coordinates.SnapToGrid(entityManager); - entity.SnapToGrid(); if (SouthRotation) { - entity.Transform.LocalRotation = Angle.Zero; + transform.LocalRotation = Angle.Zero; } } } diff --git a/Content.Server/Construction/Completions/SpawnPrototype.cs b/Content.Server/Construction/Completions/SpawnPrototype.cs index 30ce414bcf..3ce16ba322 100644 --- a/Content.Server/Construction/Completions/SpawnPrototype.cs +++ b/Content.Server/Construction/Completions/SpawnPrototype.cs @@ -16,18 +16,18 @@ namespace Content.Server.Construction.Completions [DataField("prototype")] public string Prototype { get; private set; } = string.Empty; [DataField("amount")] public int Amount { get; private set; } = 1; - public async Task PerformAction(IEntity entity, IEntity? user) + public void PerformAction(EntityUid uid, EntityUid? userUid, IEntityManager entityManager) { - if (entity.Deleted || string.IsNullOrEmpty(Prototype)) return; + if (string.IsNullOrEmpty(Prototype)) + return; - var entityManager = IoCManager.Resolve(); - var coordinates = entity.Transform.Coordinates; + var coordinates = entityManager.GetComponent(uid).Coordinates; if (EntityPrototypeHelpers.HasComponent(Prototype)) { var stackEnt = entityManager.SpawnEntity(Prototype, coordinates); var stack = stackEnt.GetComponent(); - EntitySystem.Get().SetCount(stackEnt.Uid, Amount, stack); + entityManager.EntitySysManager.GetEntitySystem().SetCount(stackEnt.Uid, Amount, stack); } else { diff --git a/Content.Server/Construction/Completions/SpawnPrototypeAtContainer.cs b/Content.Server/Construction/Completions/SpawnPrototypeAtContainer.cs index 10ef46f30c..253bd44faf 100644 --- a/Content.Server/Construction/Completions/SpawnPrototypeAtContainer.cs +++ b/Content.Server/Construction/Completions/SpawnPrototypeAtContainer.cs @@ -1,6 +1,7 @@ using System.Threading.Tasks; using Content.Shared.Construction; using JetBrains.Annotations; +using Robust.Server.Containers; using Robust.Shared.Containers; using Robust.Shared.GameObjects; using Robust.Shared.Serialization.Manager.Attributes; @@ -15,16 +16,18 @@ namespace Content.Server.Construction.Completions [DataField("container")] public string Container { get; } = string.Empty; [DataField("amount")] public int Amount { get; } = 1; - public async Task PerformAction(IEntity entity, IEntity? user) + public void PerformAction(EntityUid uid, EntityUid? userUid, IEntityManager entityManager) { - if (entity.Deleted || string.IsNullOrEmpty(Container) || string.IsNullOrEmpty(Prototype)) + if (string.IsNullOrEmpty(Container) || string.IsNullOrEmpty(Prototype)) return; - var container = entity.EnsureContainer(Container); + var containerSystem = entityManager.EntitySysManager.GetEntitySystem(); + var container = containerSystem.EnsureContainer(uid, Container); + var coordinates = entityManager.GetComponent(uid).Coordinates; for (var i = 0; i < Amount; i++) { - container.Insert(entity.EntityManager.SpawnEntity(Prototype, entity.Transform.Coordinates)); + container.Insert(entityManager.SpawnEntity(Prototype, coordinates)); } } } diff --git a/Content.Server/Construction/Completions/SpriteChange.cs b/Content.Server/Construction/Completions/SpriteChange.cs index 1628c3d913..5813d158d2 100644 --- a/Content.Server/Construction/Completions/SpriteChange.cs +++ b/Content.Server/Construction/Completions/SpriteChange.cs @@ -15,11 +15,11 @@ namespace Content.Server.Construction.Completions [DataField("layer")] public int Layer { get; private set; } = 0; [DataField("specifier")] public SpriteSpecifier? SpriteSpecifier { get; private set; } = SpriteSpecifier.Invalid; - public async Task PerformAction(IEntity entity, IEntity? user) + public void PerformAction(EntityUid uid, EntityUid? userUid, IEntityManager entityManager) { - if (entity.Deleted || SpriteSpecifier == null || SpriteSpecifier == SpriteSpecifier.Invalid) return; + if (SpriteSpecifier == null || SpriteSpecifier == SpriteSpecifier.Invalid) return; - if (!entity.TryGetComponent(out SpriteComponent? sprite)) return; + if (!entityManager.TryGetComponent(uid, out SpriteComponent? sprite)) return; // That layer doesn't exist, we do nothing. if (sprite.LayerCount <= Layer) return; diff --git a/Content.Server/Construction/Completions/SpriteStateChange.cs b/Content.Server/Construction/Completions/SpriteStateChange.cs index ed94cec220..e7cc7bb757 100644 --- a/Content.Server/Construction/Completions/SpriteStateChange.cs +++ b/Content.Server/Construction/Completions/SpriteStateChange.cs @@ -14,14 +14,14 @@ namespace Content.Server.Construction.Completions [DataField("layer")] public int Layer { get; private set; } = 0; [DataField("state")] public string? State { get; private set; } = string.Empty; - public async Task PerformAction(IEntity entity, IEntity? user) + public void PerformAction(EntityUid uid, EntityUid? userUid, IEntityManager entityManager) { - if (entity.Deleted || string.IsNullOrEmpty(State)) return; - - if (!entity.TryGetComponent(out SpriteComponent? sprite)) return; + if (string.IsNullOrEmpty(State) || !entityManager.TryGetComponent(uid, out SpriteComponent? sprite)) + return; // That layer doesn't exist, we do nothing. - if (sprite.LayerCount <= Layer) return; + if (sprite.LayerCount <= Layer) + return; sprite.LayerSetState(Layer, State); } diff --git a/Content.Server/Construction/Completions/VisualizerDataInt.cs b/Content.Server/Construction/Completions/VisualizerDataInt.cs index 40392d0d21..d13696559f 100644 --- a/Content.Server/Construction/Completions/VisualizerDataInt.cs +++ b/Content.Server/Construction/Completions/VisualizerDataInt.cs @@ -14,23 +14,16 @@ namespace Content.Server.Construction.Completions [DataDefinition] public class VisualizerDataInt : IGraphAction, ISerializationHooks { - [Dependency] private readonly IReflectionManager _reflectionManager = default!; - - void ISerializationHooks.AfterDeserialization() - { - IoCManager.InjectDependencies(this); - } - [DataField("key")] public string Key { get; private set; } = string.Empty; [DataField("data")] public int Data { get; private set; } = 0; - public async Task PerformAction(IEntity entity, IEntity? user) + public void PerformAction(EntityUid uid, EntityUid? userUid, IEntityManager entityManager) { if (string.IsNullOrEmpty(Key)) return; - if (entity.TryGetComponent(out AppearanceComponent? appearance)) + if (entityManager.TryGetComponent(uid, out AppearanceComponent? appearance)) { - if(_reflectionManager.TryParseEnumReference(Key, out var @enum)) + if(IoCManager.Resolve().TryParseEnumReference(Key, out var @enum)) { appearance.SetData(@enum, Data); } diff --git a/Content.Server/Construction/Components/ConstructionComponent.cs b/Content.Server/Construction/Components/ConstructionComponent.cs index f9c9a7fc3c..ab656d0aa6 100644 --- a/Content.Server/Construction/Components/ConstructionComponent.cs +++ b/Content.Server/Construction/Components/ConstructionComponent.cs @@ -12,6 +12,7 @@ using Content.Shared.Construction.Prototypes; using Content.Shared.Construction.Steps; using Content.Shared.Interaction; using Content.Shared.Tools.Components; +using Robust.Shared.Analyzers; using Robust.Shared.Containers; using Robust.Shared.GameObjects; using Robust.Shared.IoC; @@ -23,499 +24,42 @@ using Robust.Shared.ViewVariables; namespace Content.Server.Construction.Components { - [RegisterComponent] - public partial class ConstructionComponent : Component, IInteractUsing + [RegisterComponent, Friend(typeof(ConstructionSystem))] + public class ConstructionComponent : Component { - [Dependency] private readonly IPrototypeManager _prototypeManager = default!; - public override string Name => "Construction"; - private bool _handling = false; + [DataField("graph", required:true)] + public string Graph { get; set; } = string.Empty; + + [DataField("node", required:true)] + public string Node { get; set; } = default!; + + [DataField("edge")] + public int? EdgeIndex { get; set; } = null; + + [DataField("step")] + public int StepIndex { get; set; } = 0; + + [DataField("containers")] + public HashSet Containers { get; set; } = new(); - private TaskCompletionSource? _handlingTask = null; - [DataField("graph")] - private string _graphIdentifier = string.Empty; - [DataField("node")] - private string _startingNodeIdentifier = string.Empty; [DataField("defaultTarget")] - private string _startingTargetNodeIdentifier = string.Empty; + public string? TargetNode { get; set; } = null; [ViewVariables] - private HashSet _containers = new(); - [ViewVariables] - public List>? EdgeNestedStepProgress = null; - - private ConstructionGraphNode? _target = null; + public int? TargetEdgeIndex { get; set; } = null; [ViewVariables] - public ConstructionGraphPrototype? GraphPrototype { get; private set; } + public Queue? NodePathfinding { get; set; } = null; - [ViewVariables] - public ConstructionGraphNode? Node { get; private set; } = null; - - [ViewVariables] - public ConstructionGraphEdge? Edge { get; private set; } = null; - - public IReadOnlyCollection Containers => _containers; - - [ViewVariables] - int IInteractUsing.Priority => 2; - - [ViewVariables] - public ConstructionGraphNode? Target - { - get => _target; - set - { - ClearTarget(); - _target = value; - UpdateTarget(); - } - } - - [ViewVariables] - public ConstructionGraphEdge? TargetNextEdge { get; private set; } = null; - - [ViewVariables] - public Queue? TargetPathfinding { get; private set; } = null; - - [ViewVariables] - public int EdgeStep { get; private set; } = 0; - - [ViewVariables] [DataField("deconstructionTarget")] - public string DeconstructionNodeIdentifier { get; private set; } = "start"; + public string? DeconstructionNode { get; set; } = "start"; - /// - /// Attempts to set a new pathfinding target. - /// - public void SetNewTarget(string node) - { - if (GraphPrototype != null && GraphPrototype.Nodes.TryGetValue(node, out var target)) - { - Target = target; - } - } + [ViewVariables] + public bool WaitingDoAfter { get; set; } = false; - public void ClearTarget() - { - _target = null; - TargetNextEdge = null; - TargetPathfinding = null; - } - - public void UpdateTarget() - { - // Can't pathfind without a target or no node. - if (Target == null || Node == null || GraphPrototype == null) return; - - // If we're at our target, stop pathfinding. - if (Target == Node) - { - ClearTarget(); - return; - } - - // If we don't have the path, set it! - if (TargetPathfinding == null) - { - var path = GraphPrototype.Path(Node.Name, Target.Name); - - if (path == null) - { - ClearTarget(); - return; - } - - TargetPathfinding = new Queue(path); - } - - // Dequeue the pathfinding queue if the next is the node we're at. - if (TargetPathfinding.Peek() == Node) - TargetPathfinding.Dequeue(); - - // If we went the wrong way, we stop pathfinding. - if (Edge != null && TargetNextEdge != Edge && EdgeStep >= Edge.Steps.Count) - { - ClearTarget(); - return; - } - - // Let's set the next target edge. - if (Edge == null && TargetNextEdge == null && TargetPathfinding != null) - TargetNextEdge = Node.GetEdge(TargetPathfinding.Peek().Name); - } - - async Task IInteractUsing.InteractUsing(InteractUsingEventArgs eventArgs) - { - if (_handling) - return true; - - _handlingTask = new TaskCompletionSource(); - _handling = true; - bool result; - - if (Edge == null) - result = await HandleNode(eventArgs); - else - result = await HandleEdge(eventArgs); - - _handling = false; - _handlingTask.SetResult(null!); - - return result; - } - - private async Task HandleNode(InteractUsingEventArgs eventArgs) - { - EdgeStep = 0; - - if (Node == null || GraphPrototype == null) return false; - - foreach (var edge in Node.Edges) - { - if(edge.Steps.Count == 0) - throw new InvalidDataException($"Edge to \"{edge.Target}\" in node \"{Node.Name}\" of graph \"{GraphPrototype.ID}\" doesn't have any steps!"); - - var firstStep = edge.Steps[0]; - switch (firstStep) - { - case MaterialConstructionGraphStep _: - case ToolConstructionGraphStep _: - case ArbitraryInsertConstructionGraphStep _: - if (await HandleStep(eventArgs, edge, firstStep)) - { - if(edge.Steps.Count > 1) - Edge = edge; - return true; - } - break; - - case NestedConstructionGraphStep nestedStep: - throw new IndexOutOfRangeException($"Nested construction step not supported as the first step in an edge! Graph: {GraphPrototype.ID} Node: {Node.Name} Edge: {edge.Target}"); - } - } - - return false; - } - - private async Task HandleStep(InteractUsingEventArgs eventArgs, ConstructionGraphEdge? edge = null, ConstructionGraphStep? step = null, bool nested = false) - { - edge ??= Edge; - step ??= edge?.Steps[EdgeStep]; - - if (edge == null || step == null) - return false; - - foreach (var condition in edge.Conditions) - { - if (!await condition.Condition(Owner)) return false; - } - - var handled = false; - - var doAfterSystem = EntitySystem.Get(); - - var doAfterArgs = new DoAfterEventArgs(eventArgs.User, step.DoAfter, default, eventArgs.Target) - { - BreakOnDamage = false, - BreakOnStun = true, - BreakOnTargetMove = true, - BreakOnUserMove = true, - NeedHand = true, - }; - - switch (step) - { - case ToolConstructionGraphStep toolStep: - if (await EntitySystem.Get().UseTool(eventArgs.Using.Uid, eventArgs.User.Uid, Owner.Uid, toolStep.Fuel, step.DoAfter, toolStep.Tool)) - { - handled = true; - } - break; - - // To prevent too much code duplication. - case EntityInsertConstructionGraphStep insertStep: - var valid = false; - var entityUsing = eventArgs.Using; - - switch (insertStep) - { - case ArbitraryInsertConstructionGraphStep arbitraryStep: - if (arbitraryStep.EntityValid(eventArgs.Using) - && await doAfterSystem.WaitDoAfter(doAfterArgs) == DoAfterStatus.Finished) - { - valid = true; - } - - break; - - case MaterialConstructionGraphStep materialStep: - if (materialStep.EntityValid(eventArgs.Using, out var stack) - && await doAfterSystem.WaitDoAfter(doAfterArgs) == DoAfterStatus.Finished) - { - var splitStack = EntitySystem.Get().Split(eventArgs.Using.Uid, materialStep.Amount, eventArgs.User.Transform.Coordinates, stack); - - if (splitStack != null) - { - entityUsing = splitStack; - valid = true; - } - } - - break; - } - - if (!valid || entityUsing == null) break; - - if(string.IsNullOrEmpty(insertStep.Store)) - { - entityUsing.Delete(); - } - else - { - _containers.Add(insertStep.Store); - var container = Owner.EnsureContainer(insertStep.Store); - container.Insert(entityUsing); - } - - handled = true; - - break; - - case NestedConstructionGraphStep nestedStep: - if(EdgeNestedStepProgress == null) - EdgeNestedStepProgress = new List>(nestedStep.Steps); - - foreach (var list in EdgeNestedStepProgress.ToArray()) - { - if (list.Count == 0) - { - EdgeNestedStepProgress.Remove(list); - continue; - } - - if (!await HandleStep(eventArgs, edge, list[0], true)) continue; - - list.RemoveAt(0); - - // We check again... - if (list.Count == 0) - EdgeNestedStepProgress.Remove(list); - } - - if (EdgeNestedStepProgress.Count == 0) - handled = true; - - break; - } - - if (handled) - { - foreach (var completed in step.Completed) - { - await completed.PerformAction(Owner, eventArgs.User); - - if (Owner.Deleted) - return false; - } - } - - if (nested && handled) return true; - if (!handled) return false; - - EdgeStep++; - - if (edge.Steps.Count == EdgeStep) - { - await HandleCompletion(edge, eventArgs.User); - } - - UpdateTarget(); - - return true; - } - - private async Task HandleCompletion(ConstructionGraphEdge edge, IEntity user) - { - if (edge.Steps.Count != EdgeStep || GraphPrototype == null) - { - return false; - } - - Edge = edge; - - UpdateTarget(); - - TargetNextEdge = null; - Edge = null; - Node = GraphPrototype.Nodes[edge.Target]; - - foreach (var completed in edge.Completed) - { - await completed.PerformAction(Owner, user); - if (Owner.Deleted) return true; - } - - // Perform node actions! - foreach (var action in Node.Actions) - { - await action.PerformAction(Owner, user); - - if (Owner.Deleted) - return false; - } - - if (Target == Node) - ClearTarget(); - - await HandleEntityChange(Node, user); - - return true; - } - - public void ResetEdge() - { - EdgeNestedStepProgress = null; - TargetNextEdge = null; - Edge = null; - EdgeStep = 0; - - UpdateTarget(); - } - - private async Task HandleEdge(InteractUsingEventArgs eventArgs) - { - if (Edge == null || EdgeStep >= Edge.Steps.Count) return false; - - return await HandleStep(eventArgs, Edge, Edge.Steps[EdgeStep]); - } - - private async Task HandleEntityChange(ConstructionGraphNode node, IEntity? user = null) - { - if (node.Entity == Owner.Prototype?.ID || string.IsNullOrEmpty(node.Entity) - || GraphPrototype == null) return false; - - var entity = Owner.EntityManager.SpawnEntity(node.Entity, Owner.Transform.Coordinates); - - entity.Transform.LocalRotation = Owner.Transform.LocalRotation; - - if (entity.TryGetComponent(out ConstructionComponent? construction)) - { - if(construction.GraphPrototype != GraphPrototype) - throw new Exception($"New entity {node.Entity}'s graph {construction.GraphPrototype?.ID ?? null} isn't the same as our graph {GraphPrototype.ID} on node {node.Name}!"); - - construction.Node = node; - construction.Target = Target; - construction._containers = new HashSet(_containers); - } - - if (Owner.TryGetComponent(out ContainerManagerComponent? containerComp)) - { - foreach (var container in _containers) - { - var otherContainer = entity.EnsureContainer(container); - var ourContainer = containerComp.GetContainer(container); - - foreach (var ent in ourContainer.ContainedEntities.ToArray()) - { - ourContainer.ForceRemove(ent); - otherContainer.Insert(ent); - } - } - } - - if (Owner.TryGetComponent(out IPhysBody? physics) && - entity.TryGetComponent(out IPhysBody? otherPhysics)) - { - otherPhysics.BodyType = physics.BodyType; - } - - Owner.QueueDelete(); - - foreach (var action in node.Actions) - { - await action.PerformAction(entity, user); - - if (entity.Deleted) - return false; - } - - return true; - } - - public bool AddContainer(string id) - { - return _containers.Add(id); - } - - protected override void Initialize() - { - base.Initialize(); - - if (string.IsNullOrEmpty(_graphIdentifier)) - { - Logger.Warning($"Prototype {Owner.Prototype?.ID}'s construction component didn't have a graph identifier!"); - return; - } - - if (_prototypeManager.TryIndex(_graphIdentifier, out ConstructionGraphPrototype? graph)) - { - GraphPrototype = graph; - - if (GraphPrototype.Nodes.TryGetValue(_startingNodeIdentifier, out var node)) - { - Node = node; - } - else - { - Logger.Error($"Couldn't find node {_startingNodeIdentifier} in graph {_graphIdentifier} in construction component!"); - } - } - else - { - Logger.Error($"Couldn't find prototype {_graphIdentifier} in construction component!"); - } - - if (!string.IsNullOrEmpty(_startingTargetNodeIdentifier)) - SetNewTarget(_startingTargetNodeIdentifier); - } - - protected override void Startup() - { - base.Startup(); - - if (Node == null) return; - - foreach (var action in Node.Actions) - { - action.PerformAction(Owner, null); - - if (Owner.Deleted) - return; - } - } - - public async Task ChangeNode(string node) - { - if (GraphPrototype == null) return; - - var graphNode = GraphPrototype.Nodes[node]; - - if (_handling && _handlingTask?.Task != null) - await _handlingTask.Task; - - Edge = null; - Node = graphNode; - - foreach (var action in Node.Actions) - { - await action.PerformAction(Owner, null); - if (Owner.Deleted) - return; - } - - await HandleEntityChange(graphNode); - } + [ViewVariables] + public readonly Queue InteractionQueue = new(); } } diff --git a/Content.Server/Construction/Components/MachineFrameComponent.cs b/Content.Server/Construction/Components/MachineFrameComponent.cs index 12772ca336..97540fc59d 100644 --- a/Content.Server/Construction/Components/MachineFrameComponent.cs +++ b/Content.Server/Construction/Components/MachineFrameComponent.cs @@ -124,7 +124,7 @@ namespace Content.Server.Construction.Components if (Owner.TryGetComponent(out var construction)) { // Attempt to set pathfinding to the machine node... - construction.SetNewTarget("machine"); + EntitySystem.Get().SetPathfindingTarget(Owner.Uid, "machine", construction); } } @@ -271,7 +271,7 @@ namespace Content.Server.Construction.Components if (Owner.TryGetComponent(out ConstructionComponent? construction)) { // So prying the components off works correctly. - construction.ResetEdge(); + EntitySystem.Get().ResetEdge(Owner.Uid, construction); } return true; diff --git a/Content.Server/Construction/Conditions/AirlockBolted.cs b/Content.Server/Construction/Conditions/AirlockBolted.cs index 29b4e75d64..7db627ba75 100644 --- a/Content.Server/Construction/Conditions/AirlockBolted.cs +++ b/Content.Server/Construction/Conditions/AirlockBolted.cs @@ -17,9 +17,10 @@ namespace Content.Server.Construction.Conditions [DataField("value")] public bool Value { get; private set; } = true; - public async Task Condition(IEntity entity) + public bool Condition(EntityUid uid, IEntityManager entityManager) { - if (!entity.TryGetComponent(out AirlockComponent? airlock)) return true; + if (!entityManager.TryGetComponent(uid, out AirlockComponent? airlock)) + return true; return airlock.BoltsDown == Value; } diff --git a/Content.Server/Construction/Conditions/AllConditions.cs b/Content.Server/Construction/Conditions/AllConditions.cs index c63b3efbe3..b9591fbe3d 100644 --- a/Content.Server/Construction/Conditions/AllConditions.cs +++ b/Content.Server/Construction/Conditions/AllConditions.cs @@ -15,11 +15,11 @@ namespace Content.Server.Construction.Conditions [DataField("conditions")] public IGraphCondition[] Conditions { get; } = Array.Empty(); - public async Task Condition(IEntity entity) + public bool Condition(EntityUid uid, IEntityManager entityManager) { foreach (var condition in Conditions) { - if (!await condition.Condition(entity)) + if (!condition.Condition(uid, entityManager)) return false; } diff --git a/Content.Server/Construction/Conditions/AllWiresCut.cs b/Content.Server/Construction/Conditions/AllWiresCut.cs index 7ec60c9aaa..d957382b29 100644 --- a/Content.Server/Construction/Conditions/AllWiresCut.cs +++ b/Content.Server/Construction/Conditions/AllWiresCut.cs @@ -19,12 +19,9 @@ namespace Content.Server.Construction.Conditions { [DataField("value")] public bool Value { get; private set; } = true; - public async Task Condition(IEntity entity) + public bool Condition(EntityUid uid, IEntityManager entityManager) { - if (entity.Deleted) - return false; - - if (!entity.TryGetComponent(out var wires)) + if (!entityManager.TryGetComponent(uid, out WiresComponent? wires)) return true; foreach (var wire in wires.WiresList) diff --git a/Content.Server/Construction/Conditions/AnyConditions.cs b/Content.Server/Construction/Conditions/AnyConditions.cs index 2aa9fc96c6..31d7408b83 100644 --- a/Content.Server/Construction/Conditions/AnyConditions.cs +++ b/Content.Server/Construction/Conditions/AnyConditions.cs @@ -1,5 +1,4 @@ using System; -using System.Threading.Tasks; using Content.Shared.Construction; using JetBrains.Annotations; using Robust.Shared.GameObjects; @@ -14,11 +13,11 @@ namespace Content.Server.Construction.Conditions [DataField("conditions")] public IGraphCondition[] Conditions { get; } = Array.Empty(); - public async Task Condition(IEntity entity) + public bool Condition(EntityUid uid, IEntityManager entityManager) { foreach (var condition in Conditions) { - if (await condition.Condition(entity)) + if (condition.Condition(uid, entityManager)) return true; } diff --git a/Content.Server/Construction/Conditions/ComponentInTile.cs b/Content.Server/Construction/Conditions/ComponentInTile.cs index 50692b3bd1..4c22e7b51e 100644 --- a/Content.Server/Construction/Conditions/ComponentInTile.cs +++ b/Content.Server/Construction/Conditions/ComponentInTile.cs @@ -15,16 +15,8 @@ namespace Content.Server.Construction.Conditions /// [UsedImplicitly] [DataDefinition] - public class ComponentInTile : IGraphCondition, ISerializationHooks + public class ComponentInTile : IGraphCondition { - [Dependency] private readonly IComponentFactory _componentFactory = default!; - [Dependency] private readonly IMapManager _mapManager = default!; - - void ISerializationHooks.AfterDeserialization() - { - IoCManager.InjectDependencies(this); - } - /// /// If true, any entity on the tile must have the component. /// If false, no entity on the tile must have the component. @@ -38,14 +30,15 @@ namespace Content.Server.Construction.Conditions [DataField("component")] public string Component { get; private set; } = string.Empty; - public async Task Condition(IEntity entity) + public bool Condition(EntityUid uid, IEntityManager entityManager) { if (string.IsNullOrEmpty(Component)) return false; - var type = _componentFactory.GetRegistration(Component).Type; + var type = IoCManager.Resolve().GetRegistration(Component).Type; - var indices = entity.Transform.Coordinates.ToVector2i(entity.EntityManager, _mapManager); - var entities = indices.GetEntitiesInTile(entity.Transform.GridID, LookupFlags.Approximate | LookupFlags.IncludeAnchored, IoCManager.Resolve()); + var transform = entityManager.GetComponent(uid); + var indices = transform.Coordinates.ToVector2i(entityManager, IoCManager.Resolve()); + var entities = indices.GetEntitiesInTile(transform.GridID, LookupFlags.Approximate | LookupFlags.IncludeAnchored, IoCManager.Resolve()); foreach (var ent in entities) { diff --git a/Content.Server/Construction/Conditions/ContainerEmpty.cs b/Content.Server/Construction/Conditions/ContainerEmpty.cs index 8bfc1878ab..091b0fa745 100644 --- a/Content.Server/Construction/Conditions/ContainerEmpty.cs +++ b/Content.Server/Construction/Conditions/ContainerEmpty.cs @@ -2,6 +2,7 @@ using Content.Shared.Construction; using Content.Shared.Examine; using JetBrains.Annotations; +using Robust.Server.Containers; using Robust.Shared.Containers; using Robust.Shared.GameObjects; using Robust.Shared.Serialization.Manager.Attributes; @@ -16,10 +17,11 @@ namespace Content.Server.Construction.Conditions [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) + public bool Condition(EntityUid uid, IEntityManager entityManager) { - if (!entity.TryGetComponent(out ContainerManagerComponent? containerManager) || - !containerManager.TryGetContainer(Container, out var container)) return true; + var containerSystem = entityManager.EntitySysManager.GetEntitySystem(); + if (!containerSystem.TryGetContainer(uid, Container, out var container)) + return false; return container.ContainedEntities.Count == 0; } diff --git a/Content.Server/Construction/Conditions/ContainerNotEmpty.cs b/Content.Server/Construction/Conditions/ContainerNotEmpty.cs index 15e3a6c5c2..7c50f65780 100644 --- a/Content.Server/Construction/Conditions/ContainerNotEmpty.cs +++ b/Content.Server/Construction/Conditions/ContainerNotEmpty.cs @@ -2,6 +2,7 @@ using System.Threading.Tasks; using Content.Shared.Construction; using Content.Shared.Examine; using JetBrains.Annotations; +using Robust.Server.Containers; using Robust.Shared.Containers; using Robust.Shared.GameObjects; using Robust.Shared.Serialization.Manager.Attributes; @@ -16,10 +17,11 @@ namespace Content.Server.Construction.Conditions [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) + public bool Condition(EntityUid uid, IEntityManager entityManager) { - if (!entity.TryGetComponent(out ContainerManagerComponent? containerManager) || - !containerManager.TryGetContainer(Container, out var container)) return false; + var containerSystem = entityManager.EntitySysManager.GetEntitySystem(); + if (!containerSystem.TryGetContainer(uid, Container, out var container)) + return false; return container.ContainedEntities.Count != 0; } diff --git a/Content.Server/Construction/Conditions/DoorWelded.cs b/Content.Server/Construction/Conditions/DoorWelded.cs index 89e7fd0fe5..135742ad3c 100644 --- a/Content.Server/Construction/Conditions/DoorWelded.cs +++ b/Content.Server/Construction/Conditions/DoorWelded.cs @@ -1,4 +1,3 @@ -using System.Threading.Tasks; using Content.Server.Doors.Components; using Content.Shared.Construction; using Content.Shared.Examine; @@ -6,8 +5,6 @@ using JetBrains.Annotations; using Robust.Shared.GameObjects; using Robust.Shared.Localization; using Robust.Shared.Serialization.Manager.Attributes; -using Robust.Shared.Utility; -using static Content.Shared.Doors.SharedDoorComponent; namespace Content.Server.Construction.Conditions { @@ -18,9 +15,10 @@ namespace Content.Server.Construction.Conditions [DataField("welded")] public bool Welded { get; private set; } = true; - public async Task Condition(IEntity entity) + public bool Condition(EntityUid uid, IEntityManager entityManager) { - if (!entity.TryGetComponent(out ServerDoorComponent? doorComponent)) return false; + if (!entityManager.TryGetComponent(uid, out ServerDoorComponent? doorComponent)) + return false; return doorComponent.IsWeldedShut == Welded; } diff --git a/Content.Server/Construction/Conditions/EntityAnchored.cs b/Content.Server/Construction/Conditions/EntityAnchored.cs index 09f8e39ba7..fd8ff50ecf 100644 --- a/Content.Server/Construction/Conditions/EntityAnchored.cs +++ b/Content.Server/Construction/Conditions/EntityAnchored.cs @@ -14,11 +14,10 @@ namespace Content.Server.Construction.Conditions { [DataField("anchored")] public bool Anchored { get; private set; } = true; - public async Task Condition(IEntity entity) + public bool Condition(EntityUid uid, IEntityManager entityManager) { - if (!entity.TryGetComponent(out IPhysBody? physics)) return false; - - return (physics.BodyType == BodyType.Static && Anchored) || (physics.BodyType != BodyType.Static && !Anchored); + var transform = entityManager.GetComponent(uid); + return transform.Anchored && Anchored || !transform.Anchored && !Anchored; } public bool DoExamine(ExaminedEvent args) diff --git a/Content.Server/Construction/Conditions/MachineFrameComplete.cs b/Content.Server/Construction/Conditions/MachineFrameComplete.cs index 7ffb8a8ee7..5d17274be8 100644 --- a/Content.Server/Construction/Conditions/MachineFrameComplete.cs +++ b/Content.Server/Construction/Conditions/MachineFrameComplete.cs @@ -17,9 +17,9 @@ namespace Content.Server.Construction.Conditions [DataDefinition] public class MachineFrameComplete : IGraphCondition { - public async Task Condition(IEntity entity) + public bool Condition(EntityUid uid, IEntityManager entityManager) { - if (entity.Deleted || !entity.TryGetComponent(out var machineFrame)) + if (!entityManager.TryGetComponent(uid, out MachineFrameComponent? machineFrame)) return false; return machineFrame.IsComplete; diff --git a/Content.Server/Construction/Conditions/ToiletLidClosed.cs b/Content.Server/Construction/Conditions/ToiletLidClosed.cs index 6671868c77..81384c3a3d 100644 --- a/Content.Server/Construction/Conditions/ToiletLidClosed.cs +++ b/Content.Server/Construction/Conditions/ToiletLidClosed.cs @@ -1,4 +1,3 @@ -using System.Threading.Tasks; using Content.Server.Toilet; using Content.Shared.Construction; using Content.Shared.Examine; @@ -6,7 +5,6 @@ using JetBrains.Annotations; using Robust.Shared.GameObjects; using Robust.Shared.Localization; using Robust.Shared.Serialization.Manager.Attributes; -using Robust.Shared.Utility; namespace Content.Server.Construction.Conditions { @@ -14,9 +12,11 @@ namespace Content.Server.Construction.Conditions [DataDefinition] public class ToiletLidClosed : IGraphCondition { - public async Task Condition(IEntity entity) + public bool Condition(EntityUid uid, IEntityManager entityManager) { - if (!entity.TryGetComponent(out ToiletComponent? toilet)) return false; + if (!entityManager.TryGetComponent(uid, out ToiletComponent? toilet)) + return false; + return !toilet.LidOpen; } diff --git a/Content.Server/Construction/Conditions/WirePanel.cs b/Content.Server/Construction/Conditions/WirePanel.cs index 66920d2354..5fb7813a60 100644 --- a/Content.Server/Construction/Conditions/WirePanel.cs +++ b/Content.Server/Construction/Conditions/WirePanel.cs @@ -17,9 +17,10 @@ namespace Content.Server.Construction.Conditions { [DataField("open")] public bool Open { get; private set; } = true; - public async Task Condition(IEntity entity) + public bool Condition(EntityUid uid, IEntityManager entityManager) { - if (!entity.TryGetComponent(out WiresComponent? wires)) return false; + if (!entityManager.TryGetComponent(uid, out WiresComponent? wires)) + return false; return wires.IsPanelOpen == Open; } diff --git a/Content.Server/Construction/ConstructionSystem.Graph.cs b/Content.Server/Construction/ConstructionSystem.Graph.cs new file mode 100644 index 0000000000..e6497b1116 --- /dev/null +++ b/Content.Server/Construction/ConstructionSystem.Graph.cs @@ -0,0 +1,240 @@ +using System.Collections.Generic; +using Content.Server.Construction.Components; +using Content.Shared.Construction; +using Content.Shared.Construction.Prototypes; +using Content.Shared.Construction.Steps; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Robust.Server.Containers; +using Robust.Shared.Containers; +using Robust.Shared.GameObjects; +using Robust.Shared.IoC; +using Robust.Shared.Prototypes; + +namespace Content.Server.Construction +{ + public partial class ConstructionSystem + { + [Dependency] private readonly ContainerSystem _containerSystem = default!; + + private void InitializeGraphs() + { + } + + public bool AddContainer(EntityUid uid, string container, ConstructionComponent? construction = null) + { + if (!Resolve(uid, ref construction)) + return false; + + return construction.Containers.Add(container); + } + + public ConstructionGraphPrototype? GetCurrentGraph(EntityUid uid, ConstructionComponent? construction = null) + { + if (!Resolve(uid, ref construction, false)) + return null; + + return _prototypeManager.TryIndex(construction.Graph, out ConstructionGraphPrototype? graph) ? graph : null; + } + + public ConstructionGraphNode? GetCurrentNode(EntityUid uid, ConstructionComponent? construction = null) + { + if (!Resolve(uid, ref construction, false)) + return null; + + if (construction.Node is not {} nodeIdentifier) + return null; + + return GetCurrentGraph(uid, construction) is not {} graph ? null : GetNodeFromGraph(graph, nodeIdentifier); + } + + public ConstructionGraphEdge? GetCurrentEdge(EntityUid uid, ConstructionComponent? construction = null) + { + if (!Resolve(uid, ref construction, false)) + return null; + + if (construction.EdgeIndex is not {} edgeIndex) + return null; + + return GetCurrentNode(uid, construction) is not {} node ? null : GetEdgeFromNode(node, edgeIndex); + } + + public ConstructionGraphStep? GetCurrentStep(EntityUid uid, ConstructionComponent? construction = null) + { + if (!Resolve(uid, ref construction, false)) + return null; + + if (GetCurrentEdge(uid, construction) is not {} edge) + return null; + + return GetStepFromEdge(edge, construction.StepIndex); + } + + public ConstructionGraphNode? GetTargetNode(EntityUid uid, ConstructionComponent? construction) + { + if (!Resolve(uid, ref construction)) + return null; + + if (construction.TargetNode is not {} targetNodeId) + return null; + + if (GetCurrentGraph(uid, construction) is not {} graph) + return null; + + return GetNodeFromGraph(graph, targetNodeId); + } + + public ConstructionGraphEdge? GetTargetEdge(EntityUid uid, ConstructionComponent? construction) + { + if (!Resolve(uid, ref construction)) + return null; + + if (construction.TargetEdgeIndex is not {} targetEdgeIndex) + return null; + + if (GetCurrentNode(uid, construction) is not {} node) + return null; + + return GetEdgeFromNode(node, targetEdgeIndex); + } + + public (ConstructionGraphEdge? edge, ConstructionGraphStep? step) GetCurrentEdgeAndStep(EntityUid uid, + ConstructionComponent? construction = null) + { + if (!Resolve(uid, ref construction, false)) + return default; + + var edge = GetCurrentEdge(uid, construction); + + if (edge == null) + return default; + + var step = GetStepFromEdge(edge, construction.StepIndex); + + return (edge, step); + } + + public ConstructionGraphNode? GetNodeFromGraph(ConstructionGraphPrototype graph, string id) + { + return graph.Nodes.TryGetValue(id, out var node) ? node : null; + } + + public ConstructionGraphEdge? GetEdgeFromNode(ConstructionGraphNode node, int index) + { + return node.Edges.Count > index ? node.Edges[index] : null; + } + + public ConstructionGraphStep? GetStepFromEdge(ConstructionGraphEdge edge, int index) + { + return edge.Steps.Count > index ? edge.Steps[index] : null; + } + + public bool ChangeNode(EntityUid uid, EntityUid? userUid, string id, bool performActions = true, ConstructionComponent? construction = null) + { + if (!Resolve(uid, ref construction)) + return false; + + if (GetCurrentGraph(uid, construction) is not {} graph + || GetNodeFromGraph(graph, id) is not {} node) + return false; + + construction.Node = id; + + if(performActions) + PerformActions(uid, userUid, node.Actions); + + // ChangeEntity will handle the pathfinding update. + if (node.Entity is {} newEntity && ChangeEntity(uid, userUid, newEntity, construction) != null) + return true; + + UpdatePathfinding(uid, construction); + return true; + } + + private EntityUid? ChangeEntity(EntityUid uid, EntityUid? userUid, string newEntity, + ConstructionComponent? construction = null, + MetaDataComponent? metaData = null, + ITransformComponent? transform = null, + ContainerManagerComponent? containerManager = null) + { + if (!Resolve(uid, ref construction, ref metaData, ref transform)) + return null; + + if (newEntity == metaData.EntityPrototype?.ID || !_prototypeManager.HasIndex(newEntity)) + return null; + + // Optional resolves. + Resolve(uid, ref containerManager, false); + + // We create the new entity. + var newUid = EntityManager.SpawnEntity(newEntity, transform.Coordinates).Uid; + + // Construction transferring. + var newConstruction = EntityManager.EnsureComponent(newUid); + + // We set the graph and node accordingly... Then we append our containers to theirs. + ChangeGraph(newUid, userUid, construction.Graph, construction.Node, false, newConstruction); + + if (construction.TargetNode is {} targetNode) + SetPathfindingTarget(newUid, targetNode, newConstruction); + + // Transfer all construction-owned containers. + newConstruction.Containers.UnionWith(construction.Containers); + + // Transfer all pending interaction events too. + while (construction.InteractionQueue.TryDequeue(out var ev)) + { + newConstruction.InteractionQueue.Enqueue(ev); + } + + // Transform transferring. + var newTransform = EntityManager.GetComponent(newUid); + newTransform.LocalRotation = transform.LocalRotation; + newTransform.Anchored = transform.Anchored; + + // Container transferring. + if (containerManager != null) + { + // Ensure the new entity has a container manager. Also for resolve goodness. + var newContainerManager = EntityManager.EnsureComponent(newUid); + + // Transfer all construction-owned containers from the old entity to the new one. + foreach (var container in construction.Containers) + { + if (!_containerSystem.TryGetContainer(uid, container, out var ourContainer, containerManager)) + continue; + + // NOTE: Only Container is supported by Construction! + var otherContainer = _containerSystem.EnsureContainer(newUid, container, newContainerManager); + + foreach (var entity in ourContainer.ContainedEntities) + { + ourContainer.ForceRemove(entity); + otherContainer.Insert(entity); + } + } + } + + EntityManager.QueueDeleteEntity(uid); + + if(GetCurrentNode(newUid, newConstruction) is {} node) + PerformActions(newUid, userUid, node.Actions); + + return newUid; + } + + public bool ChangeGraph(EntityUid uid, EntityUid? userUid, string graphId, string nodeId, bool performActions = true, ConstructionComponent? construction = null) + { + if (!Resolve(uid, ref construction)) + return false; + + if (!_prototypeManager.TryIndex(graphId, out var graph)) + return false; + + if(GetNodeFromGraph(graph, nodeId) is not {} node) + return false; + + construction.Graph = graphId; + return ChangeNode(uid, userUid, nodeId, performActions, construction); + } + } +} diff --git a/Content.Server/Construction/ConstructionSystem.Initial.cs b/Content.Server/Construction/ConstructionSystem.Initial.cs new file mode 100644 index 0000000000..282cc97a9a --- /dev/null +++ b/Content.Server/Construction/ConstructionSystem.Initial.cs @@ -0,0 +1,473 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Content.Server.Construction.Components; +using Content.Server.DoAfter; +using Content.Server.Hands.Components; +using Content.Server.Inventory.Components; +using Content.Server.Items; +using Content.Server.Storage.Components; +using Content.Shared.ActionBlocker; +using Content.Shared.Construction; +using Content.Shared.Construction.Prototypes; +using Content.Shared.Construction.Steps; +using Content.Shared.Coordinates; +using Content.Shared.Interaction.Helpers; +using Content.Shared.Popups; +using Robust.Shared.Containers; +using Robust.Shared.GameObjects; +using Robust.Shared.IoC; +using Robust.Shared.Localization; +using Robust.Shared.Maths; +using Robust.Shared.Players; +using Robust.Shared.Timing; + +namespace Content.Server.Construction +{ + public partial class ConstructionSystem + { + + // --- WARNING! LEGACY CODE AHEAD! --- + // This entire file contains the legacy code for initial construction. + // This is bound to be replaced by a better alternative (probably using dummy entities) + // but for now I've isolated them in their own little file. This code is largely unchanged. + // --- YOU HAVE BEEN WARNED! AAAH! --- + + private readonly Dictionary> _beingBuilt = new(); + + private void InitializeInitial() + { + SubscribeNetworkEvent(HandleStartStructureConstruction); + SubscribeNetworkEvent(HandleStartItemConstruction); + } + + // LEGACY CODE. See warning at the top of the file! + private IEnumerable EnumerateNearby(IEntity user) + { + if (user.TryGetComponent(out HandsComponent? hands)) + { + foreach (var itemComponent in hands?.GetAllHeldItems()!) + { + if (itemComponent.Owner.TryGetComponent(out ServerStorageComponent? storage)) + { + foreach (var storedEntity in storage.StoredEntities!) + { + yield return storedEntity; + } + } + + yield return itemComponent.Owner; + } + } + + if (user!.TryGetComponent(out InventoryComponent? inventory)) + { + foreach (var held in inventory.GetAllHeldItems()) + { + if (held.TryGetComponent(out ServerStorageComponent? storage)) + { + foreach (var storedEntity in storage.StoredEntities!) + { + yield return storedEntity; + } + } + + yield return held; + } + } + + foreach (var near in IoCManager.Resolve().GetEntitiesInRange(user!, 2f, LookupFlags.Approximate | LookupFlags.IncludeAnchored)) + { + yield return near; + } + } + + // LEGACY CODE. See warning at the top of the file! + private async Task Construct(IEntity user, string materialContainer, ConstructionGraphPrototype graph, ConstructionGraphEdge edge, ConstructionGraphNode targetNode) + { + // We need a place to hold our construction items! + var container = ContainerHelpers.EnsureContainer(user, materialContainer, out var existed); + + if (existed) + { + user.PopupMessageCursor(Loc.GetString("construction-system-construct-cannot-start-another-construction")); + return null; + } + + var containers = new Dictionary(); + + var doAfterTime = 0f; + + // HOLY SHIT THIS IS SOME HACKY CODE. + // But I'd rather do this shit than risk having collisions with other containers. + Container GetContainer(string name) + { + if (containers!.ContainsKey(name)) + return containers[name]; + + while (true) + { + var random = _robustRandom.Next(); + var c = ContainerHelpers.EnsureContainer(user!, random.ToString(), out var existed); + + if (existed) continue; + + containers[name] = c; + return c; + } + } + + void FailCleanup() + { + foreach (var entity in container!.ContainedEntities.ToArray()) + { + container.Remove(entity); + } + + foreach (var cont in containers!.Values) + { + foreach (var entity in cont.ContainedEntities.ToArray()) + { + cont.Remove(entity); + } + } + + // If we don't do this, items are invisible for some fucking reason. Nice. + Timer.Spawn(1, ShutdownContainers); + } + + void ShutdownContainers() + { + container!.Shutdown(); + foreach (var c in containers!.Values.ToArray()) + { + c.Shutdown(); + } + } + + var failed = false; + + var steps = new List(); + + foreach (var step in edge.Steps) + { + doAfterTime += step.DoAfter; + + var handled = false; + + switch (step) + { + case MaterialConstructionGraphStep materialStep: + foreach (var entity in EnumerateNearby(user)) + { + if (!materialStep.EntityValid(entity, out var stack)) + continue; + + var splitStack = _stackSystem.Split(entity.Uid, materialStep.Amount, user.ToCoordinates(), stack); + + if (splitStack == null) + continue; + + if (string.IsNullOrEmpty(materialStep.Store)) + { + if (!container.Insert(splitStack)) + continue; + } + else if (!GetContainer(materialStep.Store).Insert(splitStack)) + continue; + + handled = true; + break; + } + + break; + + case ArbitraryInsertConstructionGraphStep arbitraryStep: + foreach (var entity in EnumerateNearby(user)) + { + if (!arbitraryStep.EntityValid(entity)) + continue; + + if (string.IsNullOrEmpty(arbitraryStep.Store)) + { + if (!container.Insert(entity)) + continue; + } + else if (!GetContainer(arbitraryStep.Store).Insert(entity)) + continue; + + handled = true; + break; + } + + break; + } + + if (handled == false) + { + failed = true; + break; + } + + steps.Add(step); + } + + if (failed) + { + user.PopupMessageCursor(Loc.GetString("construction-system-construct-no-materials")); + FailCleanup(); + return null; + } + + var doAfterArgs = new DoAfterEventArgs(user, doAfterTime) + { + BreakOnDamage = true, + BreakOnStun = true, + BreakOnTargetMove = false, + BreakOnUserMove = true, + NeedHand = false, + }; + + if (await _doAfterSystem.WaitDoAfter(doAfterArgs) == DoAfterStatus.Cancelled) + { + FailCleanup(); + return null; + } + + var newEntity = EntityManager.SpawnEntity(graph.Nodes[edge.Target].Entity, user.Transform.Coordinates); + + // Yes, this should throw if it's missing the component. + var construction = newEntity.GetComponent(); + + // We attempt to set the pathfinding target. + SetPathfindingTarget(newEntity.Uid, targetNode.Name, construction); + + // We preserve the containers... + foreach (var (name, cont) in containers) + { + var newCont = ContainerHelpers.EnsureContainer(newEntity, name); + + foreach (var entity in cont.ContainedEntities.ToArray()) + { + cont.ForceRemove(entity); + newCont.Insert(entity); + } + } + + // We now get rid of all them. + ShutdownContainers(); + + // We have step completed steps! + foreach (var step in steps) + { + foreach (var completed in step.Completed) + { + completed.PerformAction(newEntity.Uid, user.Uid, EntityManager); + } + } + + // And we also have edge completed effects! + foreach (var completed in edge.Completed) + { + completed.PerformAction(newEntity.Uid, user.Uid, EntityManager); + } + + return newEntity; + } + + // LEGACY CODE. See warning at the top of the file! + private async void HandleStartItemConstruction(TryStartItemConstructionMessage ev, EntitySessionEventArgs args) + { + if (!_prototypeManager.TryIndex(ev.PrototypeName, out ConstructionPrototype? constructionPrototype)) + { + _sawmill.Error($"Tried to start construction of invalid recipe '{ev.PrototypeName}'!"); + return; + } + + if (!_prototypeManager.TryIndex(constructionPrototype.Graph, out ConstructionGraphPrototype? constructionGraph)) + { + _sawmill.Error($"Invalid construction graph '{constructionPrototype.Graph}' in recipe '{ev.PrototypeName}'!"); + return; + } + + var startNode = constructionGraph.Nodes[constructionPrototype.StartNode]; + var targetNode = constructionGraph.Nodes[constructionPrototype.TargetNode]; + var pathFind = constructionGraph.Path(startNode.Name, targetNode.Name); + + var user = args.SenderSession.AttachedEntity; + + if (user == null || !Get().CanInteract(user)) return; + + if (!user.TryGetComponent(out HandsComponent? hands)) return; + + foreach (var condition in constructionPrototype.Conditions) + { + if (!condition.Condition(user, user.ToCoordinates(), Direction.South)) + return; + } + + if(pathFind == null) + throw new InvalidDataException($"Can't find path from starting node to target node in construction! Recipe: {ev.PrototypeName}"); + + var edge = startNode.GetEdge(pathFind[0].Name); + + if(edge == null) + throw new InvalidDataException($"Can't find edge from starting node to the next node in pathfinding! Recipe: {ev.PrototypeName}"); + + // No support for conditions here! + + foreach (var step in edge.Steps) + { + switch (step) + { + case ToolConstructionGraphStep _: + throw new InvalidDataException("Invalid first step for construction recipe!"); + } + } + + var item = await Construct(user, "item_construction", constructionGraph, edge, targetNode); + + if(item != null && item.TryGetComponent(out ItemComponent? itemComp)) + hands.PutInHandOrDrop(itemComp); + } + + // LEGACY CODE. See warning at the top of the file! + private async void HandleStartStructureConstruction(TryStartStructureConstructionMessage ev, EntitySessionEventArgs args) + { + + if (!_prototypeManager.TryIndex(ev.PrototypeName, out ConstructionPrototype? constructionPrototype)) + { + _sawmill.Error($"Tried to start construction of invalid recipe '{ev.PrototypeName}'!"); + RaiseNetworkEvent(new AckStructureConstructionMessage(ev.Ack)); + return; + } + + if (!_prototypeManager.TryIndex(constructionPrototype.Graph, out ConstructionGraphPrototype? constructionGraph)) + { + _sawmill.Error($"Invalid construction graph '{constructionPrototype.Graph}' in recipe '{ev.PrototypeName}'!"); + RaiseNetworkEvent(new AckStructureConstructionMessage(ev.Ack)); + return; + } + + var user = args.SenderSession.AttachedEntity; + + if (user == null) + { + _sawmill.Error($"Client sent {nameof(TryStartStructureConstructionMessage)} with no attached entity!"); + return; + } + + if (user.IsInContainer()) + { + user.PopupMessageCursor(Loc.GetString("construction-system-inside-container")); + return; + } + + var startNode = constructionGraph.Nodes[constructionPrototype.StartNode]; + var targetNode = constructionGraph.Nodes[constructionPrototype.TargetNode]; + var pathFind = constructionGraph.Path(startNode.Name, targetNode.Name); + + + if (_beingBuilt.TryGetValue(args.SenderSession, out var set)) + { + if (!set.Add(ev.Ack)) + { + user.PopupMessageCursor(Loc.GetString("construction-system-already-building")); + return; + } + } + else + { + var newSet = new HashSet {ev.Ack}; + _beingBuilt[args.SenderSession] = newSet; + } + + foreach (var condition in constructionPrototype.Conditions) + { + if (!condition.Condition(user, ev.Location, ev.Angle.GetCardinalDir())) + { + Cleanup(); + return; + } + } + + void Cleanup() + { + _beingBuilt[args.SenderSession].Remove(ev.Ack); + } + + if (user == null + || !Get().CanInteract(user) + || !user.TryGetComponent(out HandsComponent? hands) || hands.GetActiveHand == null + || !user.InRangeUnobstructed(ev.Location, ignoreInsideBlocker:constructionPrototype.CanBuildInImpassable)) + { + Cleanup(); + return; + } + + if(pathFind == null) + throw new InvalidDataException($"Can't find path from starting node to target node in construction! Recipe: {ev.PrototypeName}"); + + var edge = startNode.GetEdge(pathFind[0].Name); + + if(edge == null) + throw new InvalidDataException($"Can't find edge from starting node to the next node in pathfinding! Recipe: {ev.PrototypeName}"); + + var valid = false; + var holding = hands.GetActiveHand?.Owner; + + if (holding == null) + { + Cleanup(); + return; + } + + // No support for conditions here! + + foreach (var step in edge.Steps) + { + switch (step) + { + case EntityInsertConstructionGraphStep entityInsert: + if (entityInsert.EntityValid(holding)) + valid = true; + break; + case ToolConstructionGraphStep _: + throw new InvalidDataException("Invalid first step for item recipe!"); + } + + if (valid) + break; + } + + if (!valid) + { + Cleanup(); + return; + } + + var structure = await Construct(user, (ev.Ack + constructionPrototype.GetHashCode()).ToString(), constructionGraph, edge, targetNode); + + if (structure == null) + { + Cleanup(); + return; + } + + // We do this to be able to move the construction to its proper position in case it's anchored... + // Oh wow transform anchoring is amazing wow I love it!!!! + var wasAnchored = structure.Transform.Anchored; + structure.Transform.Anchored = false; + + structure.Transform.Coordinates = ev.Location; + structure.Transform.LocalRotation = constructionPrototype.CanRotate ? ev.Angle : Angle.Zero; + + structure.Transform.Anchored = wasAnchored; + + RaiseNetworkEvent(new AckStructureConstructionMessage(ev.Ack)); + + Cleanup(); + } + } +} diff --git a/Content.Server/Construction/ConstructionSystem.Interactions.cs b/Content.Server/Construction/ConstructionSystem.Interactions.cs new file mode 100644 index 0000000000..e443fe827b --- /dev/null +++ b/Content.Server/Construction/ConstructionSystem.Interactions.cs @@ -0,0 +1,584 @@ +using System; +using System.Collections.Generic; +using Content.Server.Construction.Components; +using Content.Server.DoAfter; +using Content.Shared.Construction; +using Content.Shared.Construction.Steps; +using Content.Shared.Interaction; +using Robust.Shared.Containers; +using Robust.Shared.GameObjects; + +namespace Content.Server.Construction +{ + public partial class ConstructionSystem + { + private readonly HashSet _constructionUpdateQueue = new(); + + private void InitializeSteps() + { + #region DoAfter Subscriptions + + // DoAfter handling. + // The ConstructionDoAfter events are meant to be raised either directed or broadcast. + // If they're raised broadcast, we will re-raise them as directed on the target. + // This allows us to easily use the DoAfter system for our purposes. + SubscribeLocalEvent(OnDoAfterComplete); + SubscribeLocalEvent(OnDoAfterCancelled); + SubscribeLocalEvent(EnqueueEvent); + SubscribeLocalEvent(EnqueueEvent); + + #endregion + + // Event handling. Add your subscriptions here! Just make sure they're all handled by EnqueueEvent. + SubscribeLocalEvent(EnqueueEvent); + } + + /// + /// Takes in an entity with and an object event, and handles any + /// possible construction interactions, depending on the construction's state. + /// + /// When is true, this method will simply return whether the interaction + /// would be handled by the entity or not. It essentially becomes a pure method that modifies nothing. + /// The result of this interaction with the entity. + private HandleResult HandleEvent(EntityUid uid, object ev, bool validation, ConstructionComponent? construction = null) + { + if (!Resolve(uid, ref construction)) + return HandleResult.False; + + // If the state machine is in an invalid state (not on a valid node) we can't do anything, ever. + if (GetCurrentNode(uid, construction) is not {} node) + { + return HandleResult.False; + } + + // If we're currently in an edge, we'll let the edge handle or validate the interaction. + if (GetCurrentEdge(uid, construction) is {} edge) + { + return HandleEdge(uid, ev, edge, validation, construction); + } + + // If we're not on an edge, let the node handle or validate the interaction. + return HandleNode(uid, ev, node, validation, construction); + } + + /// + /// Takes in an entity, a and an object event, and handles any + /// possible construction interactions. This will check the interaction against all possible edges, + /// and if any of the edges accepts the interaction, we will enter it. + /// + /// When is true, this method will simply return whether the interaction + /// would be handled by the entity or not. It essentially becomes a pure method that modifies nothing. + /// The result of this interaction with the entity. + private HandleResult HandleNode(EntityUid uid, object ev, ConstructionGraphNode node, bool validation, ConstructionComponent? construction = null) + { + if (!Resolve(uid, ref construction)) + return HandleResult.False; + + // Let's make extra sure this is zero... + construction.StepIndex = 0; + + // When we handle a node, we're essentially testing the current event interaction against all of this node's + // edges' first steps. If any of them accepts the interaction, we stop iterating and enter that edge. + for (var i = 0; i < node.Edges.Count; i++) + { + var edge = node.Edges[i]; + if (HandleEdge(uid, ev, edge, validation, construction) is var result and not HandleResult.False) + { + // Only a True result may modify the state. + // In the case of DoAfter, we don't want it modifying the state yet, other than the waiting flag. + // In the case of validated, it should NEVER modify the state at all. + if (result is not HandleResult.True) + return result; + + // If we're not on the same edge as we were before, that means handling that edge changed the node. + if (construction.Node != node.Name) + return result; + + // If we're still in the same node, that means we entered the edge and it's still not done. + construction.EdgeIndex = i; + UpdatePathfinding(uid, construction); + + return result; + } + } + + return HandleResult.False; + } + + /// + /// Takes in an entity, a and an object event, and handles any + /// possible construction interactions. This will check the interaction against one of the steps in the edge + /// depending on the construction's . + /// + /// When is true, this method will simply return whether the interaction + /// would be handled by the entity or not. It essentially becomes a pure method that modifies nothing. + /// The result of this interaction with the entity. + private HandleResult HandleEdge(EntityUid uid, object ev, ConstructionGraphEdge edge, bool validation, ConstructionComponent? construction = null) + { + if (!Resolve(uid, ref construction)) + return HandleResult.False; + + var step = GetStepFromEdge(edge, construction.StepIndex); + + if (step == null) + { + _sawmill.Warning($"Called {nameof(HandleEdge)} on entity {uid} but the current state is not valid for that!"); + return HandleResult.False; + } + + // We need to ensure we currently satisfy any and all edge conditions. + if (!CheckConditions(uid, edge.Conditions)) + return HandleResult.False; + + // We can only perform the "step completed" logic if this returns true. + if (HandleStep(uid, ev, step, validation, out var user, construction) + is var handle and not HandleResult.True) + return handle; + + // We increase the step index, meaning we move to the next step! + construction.StepIndex++; + + // Check if the new step index is greater than the amount of steps in the edge... + if (construction.StepIndex >= edge.Steps.Count) + { + // Edge finished! + PerformActions(uid, user, edge.Completed); + construction.TargetEdgeIndex = null; + construction.EdgeIndex = null; + construction.StepIndex = 0; + + // We change the node now. + ChangeNode(uid, user, edge.Target, true, construction); + } + + return HandleResult.True; + } + + /// + /// Takes in an entity, a and an object event, and handles any possible + /// construction interaction. Unlike , if this succeeds it will perform the + /// step's completion actions. Also sets the out parameter to the user's EntityUid. + /// + /// When is true, this method will simply return whether the interaction + /// would be handled by the entity or not. It essentially becomes a pure method that modifies nothing. + /// The result of this interaction with the entity. + private HandleResult HandleStep(EntityUid uid, object ev, ConstructionGraphStep step, bool validation, out EntityUid? user, ConstructionComponent? construction = null) + { + user = null; + if (!Resolve(uid, ref construction)) + return HandleResult.False; + + // Let HandleInteraction actually handle the event for this step. + // We can only perform the rest of our logic if it returns true. + if (HandleInteraction(uid, ev, step, validation, out user, construction) + is var handle and not HandleResult.True) + return handle; + + // Actually perform the step completion actions, since the step was handled correctly. + PerformActions(uid, user, step.Completed); + + UpdatePathfinding(uid, construction); + + return HandleResult.True; + } + + /// + /// Takes in an entity, a and an object event, and handles any possible + /// construction interaction. Unlike , this only handles the interaction itself + /// and doesn't perform any step completion actions. Also sets the out parameter to the user's EntityUid. + /// + /// When is true, this method will simply return whether the interaction + /// would be handled by the entity or not. It essentially becomes a pure method that modifies nothing. + /// The result of this interaction with the entity. + private HandleResult HandleInteraction(EntityUid uid, object ev, ConstructionGraphStep step, bool validation, out EntityUid? user, ConstructionComponent? construction = null) + { + user = null; + if (!Resolve(uid, ref construction)) + return HandleResult.False; + + // Whether this event is being re-handled after a DoAfter or not. Check DoAfterState for more info. + var doAfterState = validation ? DoAfterState.Validation : DoAfterState.None; + + // Custom data from a prior HandleInteraction where a DoAfter was called... + object? doAfterData = null; + + // The DoAfter events can only perform special logic when we're not validating events. + if (!validation) + { + // Some events are handled specially... Such as doAfter. + switch (ev) + { + case ConstructionDoAfterComplete complete: + { + // DoAfter completed! + ev = complete.WrappedEvent; + doAfterState = DoAfterState.Completed; + doAfterData = complete.CustomData; + construction.WaitingDoAfter = false; + break; + } + + case ConstructionDoAfterCancelled cancelled: + { + // DoAfter failed! + ev = cancelled.WrappedEvent; + doAfterState = DoAfterState.Cancelled; + doAfterData = cancelled.CustomData; + construction.WaitingDoAfter = false; + break; + } + } + } + + // Can't perform any interactions while we're waiting for a DoAfter... + // This also makes any event validation fail. + if (construction.WaitingDoAfter) + return HandleResult.False; + + // The cases in this switch will handle the interaction and return + switch (step) + { + + // --- CONSTRUCTION STEP EVENT HANDLING START --- + #region Construction Step Event Handling + // So you want to create your own custom step for construction? + // You're looking at the right place, then! You should create + // a new case for your step here, and handle it as you see fit. + // Make extra sure you handle DoAfter (if applicable) properly! + // Also make sure your event handler properly handles validation. + // Note: Please use braces for your new case, it's convenient. + + case EntityInsertConstructionGraphStep insertStep: + { + // EntityInsert steps only work with InteractUsing! + if (ev is not InteractUsingEvent interactUsing) + break; + + // TODO: Sanity checks. + + // If this step's DoAfter was cancelled, we just fail the interaction. + if (doAfterState == DoAfterState.Cancelled) + return HandleResult.False; + + var insert = interactUsing.Used; + + // Since many things inherit this step, we delegate the "is this entity valid?" logic to them. + // While this is very OOP and I find it icky, I must admit that it simplifies the code here a lot. + if(!insertStep.EntityValid(insert)) + return HandleResult.False; + + // If we're only testing whether this step would be handled by the given event, then we're done. + if (doAfterState == DoAfterState.Validation) + return HandleResult.Validated; + + // If we still haven't completed this step's DoAfter... + if (doAfterState == DoAfterState.None && insertStep.DoAfter > 0) + { + _doAfterSystem.DoAfter( + new DoAfterEventArgs(interactUsing.User, step.DoAfter, default, interactUsing.Target) + { + BreakOnDamage = false, + BreakOnStun = true, + BreakOnTargetMove = true, + BreakOnUserMove = true, + NeedHand = true, + + // These events will be broadcast and handled by this very same system, that will + // raise them directed to the target. These events wrap the original event. + BroadcastFinishedEvent = new ConstructionDoAfterComplete(uid, ev), + BroadcastCancelledEvent = new ConstructionDoAfterCancelled(uid, ev) + }); + + // To properly signal that we're waiting for a DoAfter, we have to set the flag on the component + // and then also return the DoAfter HandleResult. + construction.WaitingDoAfter = true; + return HandleResult.DoAfter; + } + + // Material steps, which use stacks, are handled specially. Instead of inserting the whole item, + // we split the stack in two and insert the split stack. + if (insertStep is MaterialConstructionGraphStep materialInsertStep) + { + if (_stackSystem.Split(insert.Uid, materialInsertStep.Amount, interactUsing.User.Transform.Coordinates) is not { } stack) + return HandleResult.False; + + insert = stack; + } + + // Container-storage handling. + if (!string.IsNullOrEmpty(insertStep.Store)) + { + // In the case we want to store this item in a container on the entity... + var store = insertStep.Store; + + // Add this container to the collection of "construction-owned" containers. + // Containers in that set will be transferred to new entities in the case of a prototype change. + construction.Containers.Add(store); + + // The container doesn't necessarily need to exist, so we ensure it. + _containerSystem.EnsureContainer(uid, store) + .Insert(insert); + } + else + { + // If we don't store the item in a container on the entity, we just delete it right away. + insert.Delete(); + } + + // Step has been handled correctly, so we signal this. + return HandleResult.True; + } + + case ToolConstructionGraphStep toolInsertStep: + { + if (ev is not InteractUsingEvent interactUsing) + break; + + // TODO: Sanity checks. + + user = interactUsing.User.Uid; + + // If we're validating whether this event handles the step... + if (doAfterState == DoAfterState.Validation) + { + // Then we only really need to check whether the tool entity has that quality or not. + return _toolSystem.HasQuality(interactUsing.Used.Uid, toolInsertStep.Tool) + ? HandleResult.Validated : HandleResult.False; + } + + // If we're handling an event after its DoAfter finished... + if (doAfterState != DoAfterState.None) + return doAfterState == DoAfterState.Completed ? HandleResult.True : HandleResult.False; + + if (!_toolSystem.UseTool(interactUsing.Used.Uid, interactUsing.User.Uid, + uid, toolInsertStep.Fuel, toolInsertStep.DoAfter, toolInsertStep.Tool, + new ConstructionDoAfterComplete(uid, ev), new ConstructionDoAfterCancelled(uid, ev))) + return HandleResult.False; + + // In the case we're not waiting for a doAfter, then this step is complete! + if (toolInsertStep.DoAfter <= 0) + return HandleResult.True; + + construction.WaitingDoAfter = true; + return HandleResult.DoAfter; + } + + #endregion + // --- CONSTRUCTION STEP EVENT HANDLING FINISH --- + + default: + throw new ArgumentOutOfRangeException(nameof(step), + "You need to code your ConstructionGraphStep behavior by adding a case to the switch."); + } + + // If the handlers were not able to handle this event, return... + return HandleResult.False; + } + + public bool CheckConditions(EntityUid uid, IEnumerable conditions) + { + foreach (var condition in conditions) + { + if (!condition.Condition(uid, EntityManager)) + return false; + } + + return true; + } + + public void PerformActions(EntityUid uid, EntityUid? userUid, IEnumerable actions) + { + foreach (var action in actions) + { + // If an action deletes the entity, we stop performing actions. + if (!EntityManager.EntityExists(uid)) + break; + + action.PerformAction(uid, userUid, EntityManager); + } + } + + public void ResetEdge(EntityUid uid, ConstructionComponent? construction = null) + { + if (!Resolve(uid, ref construction)) + return; + + construction.TargetEdgeIndex = null; + construction.EdgeIndex = null; + construction.StepIndex = 0; + + UpdatePathfinding(uid, construction); + } + + private void UpdateInteractions() + { + // We iterate all entities waiting for their interactions to be handled. + // This is much more performant than making an EntityQuery for ConstructionComponent, + // since, for example, every single wall has a ConstructionComponent.... + foreach (var uid in _constructionUpdateQueue) + { + // Ensure the entity exists and has a Construction component. + if (!EntityManager.EntityExists(uid) || !EntityManager.TryGetComponent(uid, out ConstructionComponent? construction)) + continue; + + // Handle all queued interactions! + while (construction.InteractionQueue.TryDequeue(out var interaction)) + { + // We set validation to false because we actually want to perform the interaction here. + HandleEvent(uid, interaction, false, construction); + } + } + + _constructionUpdateQueue.Clear(); + } + + #region Event Handlers + + private void EnqueueEvent(EntityUid uid, ConstructionComponent construction, object args) + { + // Handled events get treated specially. + if (args is HandledEntityEventArgs handled) + { + // If they're already handled, we do nothing. + if (handled.Handled) + return; + + // Otherwise, let's check if this event could be handled by the construction's current state. + if (HandleEvent(uid, args, true, construction) != HandleResult.Validated) + return; // Not validated, so we don't even enqueue this event. + + handled.Handled = true; + } + + // Enqueue this event so it'll be handled in the next tick. + // This prevents some issues that could occur from entity deletion, component deletion, etc in a handler. + construction.InteractionQueue.Enqueue(args); + + // Add this entity to the queue so it'll be updated next tick. + _constructionUpdateQueue.Add(uid); + } + + private void OnDoAfterComplete(ConstructionDoAfterComplete ev) + { + // Make extra sure the target entity exists... + if (!EntityManager.EntityExists(ev.TargetUid)) + return; + + // Re-raise this event, but directed on the target UID. + RaiseLocalEvent(ev.TargetUid, ev, false); + } + + private void OnDoAfterCancelled(ConstructionDoAfterCancelled ev) + { + // Make extra sure the target entity exists... + if (!EntityManager.EntityExists(ev.TargetUid)) + return; + + // Re-raise this event, but directed on the target UID. + RaiseLocalEvent(ev.TargetUid, ev, false); + } + + #endregion + + #region Event Definitions + + /// + /// This event signals that a construction interaction's DoAfter has completed successfully. + /// This wraps the original event and also keeps some custom data that event handlers might need. + /// + private class ConstructionDoAfterComplete : EntityEventArgs + { + public readonly EntityUid TargetUid; + public readonly object WrappedEvent; + public readonly object? CustomData; + + public ConstructionDoAfterComplete(EntityUid targetUid, object wrappedEvent, object? customData = null) + { + TargetUid = targetUid; + WrappedEvent = wrappedEvent; + CustomData = customData; + } + } + + /// + /// This event signals that a construction interaction's DoAfter has failed or has been cancelled. + /// This wraps the original event and also keeps some custom data that event handlers might need. + /// + private class ConstructionDoAfterCancelled : EntityEventArgs + { + public readonly EntityUid TargetUid; + public readonly object WrappedEvent; + public readonly object? CustomData; + + public ConstructionDoAfterCancelled(EntityUid targetUid, object wrappedEvent, object? customData = null) + { + TargetUid = targetUid; + WrappedEvent = wrappedEvent; + CustomData = customData; + } + } + + #endregion + + #region Internal Enum Definitions + + /// + /// Specifies the DoAfter status for a construction step event handler. + /// + private enum DoAfterState : byte + { + /// + /// If None, this is the first time we're seeing this event and we might want to call a DoAfter + /// if the step needs it. + /// + None, + + /// + /// If Validation, we want to validate whether the specified event would handle the step or not. + /// Will NOT modify the construction state at all. + /// + Validation, + + /// + /// If Completed, this is the second (and last) time we're seeing this event, and + /// the doAfter that was called the first time successfully completed. Handle completion logic now. + /// + Completed, + + /// + /// If Cancelled, this is the second (and last) time we're seeing this event, and + /// the doAfter that was called the first time was cancelled. Handle cleanup logic now. + /// + Cancelled + } + + /// + /// Specifies the result after attempting to handle a specific step with an event. + /// + private enum HandleResult : byte + { + /// + /// The interaction wasn't handled or validated. + /// + False, + + /// + /// The interaction would be handled successfully. Nothing was modified. + /// + Validated, + + /// + /// The interaction was handled successfully. + /// + True, + + /// + /// The interaction is waiting on a DoAfter now. + /// This means the interaction started the DoAfter. + /// + DoAfter, + } + + #endregion + } +} diff --git a/Content.Server/Construction/ConstructionSystem.Pathfinding.cs b/Content.Server/Construction/ConstructionSystem.Pathfinding.cs new file mode 100644 index 0000000000..a154aafb22 --- /dev/null +++ b/Content.Server/Construction/ConstructionSystem.Pathfinding.cs @@ -0,0 +1,122 @@ +using System.Collections.Generic; +using Content.Server.Construction.Components; +using Content.Shared.Construction; +using Content.Shared.Construction.Prototypes; +using Robust.Shared.GameObjects; + +namespace Content.Server.Construction +{ + public partial class ConstructionSystem + { + public bool SetPathfindingTarget(EntityUid uid, string? targetNodeId, ConstructionComponent? construction = null) + { + if (!Resolve(uid, ref construction)) + return false; + + // Null means clear pathfinding target. + if (targetNodeId == null) + { + ClearPathfinding(uid, construction); + return true; + } + + if (GetCurrentGraph(uid, construction) is not {} graph) + return false; + + if (GetNodeFromGraph(graph, construction.Node) is not { } node) + return false; + + if (GetNodeFromGraph(graph, targetNodeId) is not {} targetNode) + return false; + + return UpdatePathfinding(uid, graph, node, targetNode, GetCurrentEdge(uid, construction), construction); + } + + public bool UpdatePathfinding(EntityUid uid, ConstructionComponent? construction = null) + { + if (!Resolve(uid, ref construction)) + return false; + + if (construction.TargetNode is not {} targetNodeId) + return false; + + if (GetCurrentGraph(uid, construction) is not {} graph + || GetNodeFromGraph(graph, construction.Node) is not {} node + || GetNodeFromGraph(graph, targetNodeId) is not {} targetNode) + return false; + + return UpdatePathfinding(uid, graph, node, targetNode, GetCurrentEdge(uid, construction), construction); + } + + private bool UpdatePathfinding(EntityUid uid, ConstructionGraphPrototype graph, + ConstructionGraphNode currentNode, ConstructionGraphNode targetNode, + ConstructionGraphEdge? currentEdge, + ConstructionComponent? construction = null) + { + if (!Resolve(uid, ref construction)) + return false; + + construction.TargetNode = targetNode.Name; + + // Check if we reached the target node. + if (currentNode == targetNode) + { + ClearPathfinding(uid, construction); + return true; + } + + // If we don't have a path, generate it. + if (construction.NodePathfinding == null) + { + var path = graph.PathId(currentNode.Name, targetNode.Name); + + if (path == null || path.Length == 0) + { + // No path. + ClearPathfinding(uid, construction); + return false; + } + + construction.NodePathfinding = new Queue(path); + } + + // If the next pathfinding node is the one we're at, dequeue it. + if (construction.NodePathfinding.Peek() == currentNode.Name) + { + construction.NodePathfinding.Dequeue(); + } + + if (currentEdge != null && construction.TargetEdgeIndex is {} targetEdgeIndex) + { + if (currentNode.Edges.Count >= targetEdgeIndex) + { + // Target edge is incorrect. + construction.TargetEdgeIndex = null; + } + else if (currentNode.Edges[targetEdgeIndex] != currentEdge) + { + // We went the wrong way, clean up! + ClearPathfinding(uid, construction); + return false; + } + } + + if (construction.EdgeIndex == null + && construction.TargetEdgeIndex == null + && construction.NodePathfinding != null) + construction.TargetEdgeIndex = (currentNode.GetEdgeIndex(construction.NodePathfinding.Peek())); + + return true; + } + + public void ClearPathfinding(EntityUid uid, ConstructionComponent? construction = null) + { + if (!Resolve(uid, ref construction)) + return; + + construction.TargetNode = null; + construction.TargetEdgeIndex = null; + construction.NodePathfinding = null; + } + } +} diff --git a/Content.Server/Construction/ConstructionSystem.cs b/Content.Server/Construction/ConstructionSystem.cs index dfac184807..01688362d2 100644 --- a/Content.Server/Construction/ConstructionSystem.cs +++ b/Content.Server/Construction/ConstructionSystem.cs @@ -1,34 +1,18 @@ -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading.Tasks; using Content.Server.Construction.Components; using Content.Server.DoAfter; -using Content.Server.Hands.Components; -using Content.Server.Inventory.Components; -using Content.Server.Items; using Content.Server.Stack; -using Content.Server.Storage.Components; -using Content.Shared.ActionBlocker; +using Content.Server.Tools; using Content.Shared.Construction; -using Content.Shared.Construction.Prototypes; -using Content.Shared.Construction.Steps; -using Content.Shared.Coordinates; using Content.Shared.Examine; -using Content.Shared.Interaction.Helpers; using Content.Shared.Popups; using Content.Shared.Verbs; using JetBrains.Annotations; -using Robust.Shared.Containers; using Robust.Shared.GameObjects; using Robust.Shared.IoC; using Robust.Shared.Localization; using Robust.Shared.Log; -using Robust.Shared.Maths; -using Robust.Shared.Players; using Robust.Shared.Prototypes; using Robust.Shared.Random; -using Robust.Shared.Timing; namespace Content.Server.Construction { @@ -36,33 +20,87 @@ namespace Content.Server.Construction /// The server-side implementation of the construction system, which is used for constructing entities in game. /// [UsedImplicitly] - internal class ConstructionSystem : SharedConstructionSystem + public partial class ConstructionSystem : SharedConstructionSystem { + [Dependency] private readonly ILogManager _logManager = default!; [Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly IRobustRandom _robustRandom = default!; [Dependency] private readonly DoAfterSystem _doAfterSystem = default!; [Dependency] private readonly StackSystem _stackSystem = default!; - [Dependency] private readonly ActionBlockerSystem _blockerSystem = default!; + [Dependency] private readonly ToolSystem _toolSystem = default!; - private readonly Dictionary> _beingBuilt = new(); + private const string SawmillName = "Construction"; + private ISawmill _sawmill = default!; public override void Initialize() { base.Initialize(); - SubscribeNetworkEvent(HandleStartStructureConstruction); - SubscribeNetworkEvent(HandleStartItemConstruction); + _sawmill = _logManager.GetSawmill(SawmillName); + + InitializeGraphs(); + InitializeSteps(); + InitializeInitial(); + + SubscribeLocalEvent(OnConstructionInit); + SubscribeLocalEvent(OnConstructionStartup); SubscribeLocalEvent(AddDeconstructVerb); SubscribeLocalEvent(HandleConstructionExamined); } + private void OnConstructionInit(EntityUid uid, ConstructionComponent construction, ComponentInit args) + { + if (GetCurrentGraph(uid, construction) is not {} graph) + { + _sawmill.Warning($"Prototype {construction.Owner.Prototype?.ID}'s construction component has an invalid graph specified."); + return; + } + + if (GetNodeFromGraph(graph, construction.Node) is not {} node) + { + _sawmill.Warning($"Prototype {construction.Owner.Prototype?.ID}'s construction component has an invalid node specified."); + return; + } + + ConstructionGraphEdge? edge = null; + if (construction.EdgeIndex is {} edgeIndex) + { + if (GetEdgeFromNode(node, edgeIndex) is not {} currentEdge) + { + _sawmill.Warning($"Prototype {construction.Owner.Prototype?.ID}'s construction component has an invalid edge index specified."); + return; + } + + edge = currentEdge; + } + + if (construction.TargetNode is {} targetNodeId) + { + if (GetNodeFromGraph(graph, targetNodeId) is not { } targetNode) + { + _sawmill.Warning($"Prototype {construction.Owner.Prototype?.ID}'s construction component has an invalid target node specified."); + return; + } + + UpdatePathfinding(uid, graph, node, targetNode, edge, construction); + } + } + + private void OnConstructionStartup(EntityUid uid, ConstructionComponent construction, ComponentStartup args) + { + if (GetCurrentNode(uid, construction) is not {} node) + return; + + PerformActions(uid, null, node.Actions); + } + private void AddDeconstructVerb(EntityUid uid, ConstructionComponent component, GetOtherVerbsEvent args) { if (!args.CanAccess) return; - if (component.Target?.Name == component.DeconstructionNodeIdentifier || - component.Node?.Name == component.DeconstructionNodeIdentifier) + if (component.TargetNode == component.DeconstructionNode || + component.Node == component.DeconstructionNode) return; Verb verb = new(); @@ -73,8 +111,8 @@ namespace Content.Server.Construction verb.Act = () => { - component.SetNewTarget(component.DeconstructionNodeIdentifier); - if (component.Target == null) + SetPathfindingTarget(uid, component.DeconstructionNode, component); + if (component.TargetNode == null) { // Maybe check, but on the flip-side a better solution might be to not make it undeconstructible in the first place, no? component.Owner.PopupMessage(args.User, Loc.GetString("deconstructible-verb-activate-no-target-text")); @@ -90,478 +128,45 @@ namespace Content.Server.Construction private void HandleConstructionExamined(EntityUid uid, ConstructionComponent component, ExaminedEvent args) { - if (component.Target != null) + if (GetTargetNode(uid, component) is {} target) { args.PushMarkup(Loc.GetString( "construction-component-to-create-header", - ("targetName", component.Target.Name)) + "\n"); + ("targetName", target.Name)) + "\n"); } - if (component.Edge == null && component.TargetNextEdge != null) + if (component.EdgeIndex == null && GetTargetEdge(uid, component) is {} targetEdge) { var preventStepExamine = false; - foreach (var condition in component.TargetNextEdge.Conditions) + foreach (var condition in targetEdge.Conditions) { preventStepExamine |= condition.DoExamine(args); } if (!preventStepExamine) - component.TargetNextEdge.Steps[0].DoExamine(args); + targetEdge.Steps[0].DoExamine(args); return; } - if (component.Edge != null) + if (GetCurrentEdge(uid, component) is {} edge) { var preventStepExamine = false; - foreach (var condition in component.Edge.Conditions) + foreach (var condition in edge.Conditions) { preventStepExamine |= condition.DoExamine(args); } if (preventStepExamine) return; } - - if (component.EdgeNestedStepProgress == null) - { - if (component.EdgeStep < component.Edge?.Steps.Count) - component.Edge.Steps[component.EdgeStep].DoExamine(args); - return; - } - - foreach (var list in component.EdgeNestedStepProgress) - { - if(list.Count == 0) continue; - - list[0].DoExamine(args); - } } - private IEnumerable EnumerateNearby(IEntity user) + public override void Update(float frameTime) { - if (user.TryGetComponent(out HandsComponent? hands)) - { - foreach (var itemComponent in hands?.GetAllHeldItems()!) - { - if (itemComponent.Owner.TryGetComponent(out ServerStorageComponent? storage)) - { - foreach (var storedEntity in storage.StoredEntities!) - { - yield return storedEntity; - } - } + base.Update(frameTime); - yield return itemComponent.Owner; - } - } - - if (user!.TryGetComponent(out InventoryComponent? inventory)) - { - foreach (var held in inventory.GetAllHeldItems()) - { - if (held.TryGetComponent(out ServerStorageComponent? storage)) - { - foreach (var storedEntity in storage.StoredEntities!) - { - yield return storedEntity; - } - } - - yield return held; - } - } - - foreach (var near in IoCManager.Resolve().GetEntitiesInRange(user!, 2f, LookupFlags.Approximate | LookupFlags.IncludeAnchored)) - { - yield return near; - } - } - - private async Task Construct(IEntity user, string materialContainer, ConstructionGraphPrototype graph, ConstructionGraphEdge edge, ConstructionGraphNode targetNode) - { - // We need a place to hold our construction items! - var container = ContainerHelpers.EnsureContainer(user, materialContainer, out var existed); - - if (existed) - { - user.PopupMessageCursor(Loc.GetString("construction-system-construct-cannot-start-another-construction")); - return null; - } - - var containers = new Dictionary(); - - var doAfterTime = 0f; - - // HOLY SHIT THIS IS SOME HACKY CODE. - // But I'd rather do this shit than risk having collisions with other containers. - Container GetContainer(string name) - { - if (containers!.ContainsKey(name)) - return containers[name]; - - while (true) - { - var random = _robustRandom.Next(); - var c = ContainerHelpers.EnsureContainer(user!, random.ToString(), out var existed); - - if (existed) continue; - - containers[name] = c; - return c; - } - } - - void FailCleanup() - { - foreach (var entity in container!.ContainedEntities.ToArray()) - { - container.Remove(entity); - } - - foreach (var cont in containers!.Values) - { - foreach (var entity in cont.ContainedEntities.ToArray()) - { - cont.Remove(entity); - } - } - - // If we don't do this, items are invisible for some fucking reason. Nice. - Timer.Spawn(1, ShutdownContainers); - } - - void ShutdownContainers() - { - container!.Shutdown(); - foreach (var c in containers!.Values.ToArray()) - { - c.Shutdown(); - } - } - - var failed = false; - - var steps = new List(); - - foreach (var step in edge.Steps) - { - doAfterTime += step.DoAfter; - - var handled = false; - - switch (step) - { - case MaterialConstructionGraphStep materialStep: - foreach (var entity in EnumerateNearby(user)) - { - if (!materialStep.EntityValid(entity, out var stack)) - continue; - - var splitStack = _stackSystem.Split(entity.Uid, materialStep.Amount, user.ToCoordinates(), stack); - - if (splitStack == null) - continue; - - if (string.IsNullOrEmpty(materialStep.Store)) - { - if (!container.Insert(splitStack)) - continue; - } - else if (!GetContainer(materialStep.Store).Insert(splitStack)) - continue; - - handled = true; - break; - } - - break; - - case ArbitraryInsertConstructionGraphStep arbitraryStep: - foreach (var entity in EnumerateNearby(user)) - { - if (!arbitraryStep.EntityValid(entity)) - continue; - - if (string.IsNullOrEmpty(arbitraryStep.Store)) - { - if (!container.Insert(entity)) - continue; - } - else if (!GetContainer(arbitraryStep.Store).Insert(entity)) - continue; - - handled = true; - break; - } - - break; - } - - if (handled == false) - { - failed = true; - break; - } - - steps.Add(step); - } - - if (failed) - { - user.PopupMessageCursor(Loc.GetString("construction-system-construct-no-materials")); - FailCleanup(); - return null; - } - - var doAfterArgs = new DoAfterEventArgs(user, doAfterTime) - { - BreakOnDamage = true, - BreakOnStun = true, - BreakOnTargetMove = false, - BreakOnUserMove = true, - NeedHand = false, - }; - - if (await _doAfterSystem.WaitDoAfter(doAfterArgs) == DoAfterStatus.Cancelled) - { - FailCleanup(); - return null; - } - - var newEntity = EntityManager.SpawnEntity(graph.Nodes[edge.Target].Entity, user.Transform.Coordinates); - - // Yes, this should throw if it's missing the component. - var construction = newEntity.GetComponent(); - - // We attempt to set the pathfinding target. - construction.Target = targetNode; - - // We preserve the containers... - foreach (var (name, cont) in containers) - { - var newCont = ContainerHelpers.EnsureContainer(newEntity, name); - - foreach (var entity in cont.ContainedEntities.ToArray()) - { - cont.ForceRemove(entity); - newCont.Insert(entity); - } - } - - // We now get rid of all them. - ShutdownContainers(); - - // We have step completed steps! - foreach (var step in steps) - { - foreach (var completed in step.Completed) - { - await completed.PerformAction(newEntity, user); - } - } - - // And we also have edge completed effects! - foreach (var completed in edge.Completed) - { - await completed.PerformAction(newEntity, user); - } - - return newEntity; - } - - private async void HandleStartItemConstruction(TryStartItemConstructionMessage ev, EntitySessionEventArgs args) - { - if (!_prototypeManager.TryIndex(ev.PrototypeName, out ConstructionPrototype? constructionPrototype)) - { - Logger.Error($"Tried to start construction of invalid recipe '{ev.PrototypeName}'!"); - return; - } - - if (!_prototypeManager.TryIndex(constructionPrototype.Graph, out ConstructionGraphPrototype? constructionGraph)) - { - Logger.Error($"Invalid construction graph '{constructionPrototype.Graph}' in recipe '{ev.PrototypeName}'!"); - return; - } - - var startNode = constructionGraph.Nodes[constructionPrototype.StartNode]; - var targetNode = constructionGraph.Nodes[constructionPrototype.TargetNode]; - var pathFind = constructionGraph.Path(startNode.Name, targetNode.Name); - - var user = args.SenderSession.AttachedEntity; - - if (user == null || !_blockerSystem.CanInteract(user)) return; - - if (!user.TryGetComponent(out HandsComponent? hands)) return; - - foreach (var condition in constructionPrototype.Conditions) - { - if (!condition.Condition(user, user.ToCoordinates(), Direction.South)) - return; - } - - if(pathFind == null) - throw new InvalidDataException($"Can't find path from starting node to target node in construction! Recipe: {ev.PrototypeName}"); - - var edge = startNode.GetEdge(pathFind[0].Name); - - if(edge == null) - throw new InvalidDataException($"Can't find edge from starting node to the next node in pathfinding! Recipe: {ev.PrototypeName}"); - - // No support for conditions here! - - foreach (var step in edge.Steps) - { - switch (step) - { - case ToolConstructionGraphStep _: - case NestedConstructionGraphStep _: - throw new InvalidDataException("Invalid first step for construction recipe!"); - } - } - - var item = await Construct(user, "item_construction", constructionGraph, edge, targetNode); - - if(item != null && item.TryGetComponent(out ItemComponent? itemComp)) - hands.PutInHandOrDrop(itemComp); - } - - private async void HandleStartStructureConstruction(TryStartStructureConstructionMessage ev, EntitySessionEventArgs args) - { - - if (!_prototypeManager.TryIndex(ev.PrototypeName, out ConstructionPrototype? constructionPrototype)) - { - Logger.Error($"Tried to start construction of invalid recipe '{ev.PrototypeName}'!"); - RaiseNetworkEvent(new AckStructureConstructionMessage(ev.Ack)); - return; - } - - if (!_prototypeManager.TryIndex(constructionPrototype.Graph, out ConstructionGraphPrototype? constructionGraph)) - { - Logger.Error($"Invalid construction graph '{constructionPrototype.Graph}' in recipe '{ev.PrototypeName}'!"); - RaiseNetworkEvent(new AckStructureConstructionMessage(ev.Ack)); - return; - } - - var user = args.SenderSession.AttachedEntity; - - if (user == null) - { - Logger.Error($"Client sent {nameof(TryStartStructureConstructionMessage)} with no attached entity!"); - return; - } - - if (user.IsInContainer()) - { - user.PopupMessageCursor(Loc.GetString("construction-system-inside-container")); - return; - } - - var startNode = constructionGraph.Nodes[constructionPrototype.StartNode]; - var targetNode = constructionGraph.Nodes[constructionPrototype.TargetNode]; - var pathFind = constructionGraph.Path(startNode.Name, targetNode.Name); - - - if (_beingBuilt.TryGetValue(args.SenderSession, out var set)) - { - if (!set.Add(ev.Ack)) - { - user.PopupMessageCursor(Loc.GetString("construction-system-already-building")); - return; - } - } - else - { - var newSet = new HashSet {ev.Ack}; - _beingBuilt[args.SenderSession] = newSet; - } - - foreach (var condition in constructionPrototype.Conditions) - { - if (!condition.Condition(user, ev.Location, ev.Angle.GetCardinalDir())) - { - Cleanup(); - return; - } - } - - void Cleanup() - { - _beingBuilt[args.SenderSession].Remove(ev.Ack); - } - - if (user == null - || !_blockerSystem.CanInteract(user) - || !user.TryGetComponent(out HandsComponent? hands) || hands.GetActiveHand == null - || !user.InRangeUnobstructed(ev.Location, ignoreInsideBlocker:constructionPrototype.CanBuildInImpassable)) - { - Cleanup(); - return; - } - - if(pathFind == null) - throw new InvalidDataException($"Can't find path from starting node to target node in construction! Recipe: {ev.PrototypeName}"); - - var edge = startNode.GetEdge(pathFind[0].Name); - - if(edge == null) - throw new InvalidDataException($"Can't find edge from starting node to the next node in pathfinding! Recipe: {ev.PrototypeName}"); - - var valid = false; - var holding = hands.GetActiveHand?.Owner; - - if (holding == null) - { - Cleanup(); - return; - } - - // No support for conditions here! - - foreach (var step in edge.Steps) - { - switch (step) - { - case EntityInsertConstructionGraphStep entityInsert: - if (entityInsert.EntityValid(holding)) - valid = true; - break; - case ToolConstructionGraphStep _: - case NestedConstructionGraphStep _: - throw new InvalidDataException("Invalid first step for item recipe!"); - } - - if (valid) - break; - } - - if (!valid) - { - Cleanup(); - return; - } - - var structure = await Construct(user, (ev.Ack + constructionPrototype.GetHashCode()).ToString(), constructionGraph, edge, targetNode); - - if (structure == null) - { - Cleanup(); - return; - } - - // We do this to be able to move the construction to its proper position in case it's anchored... - // Oh wow transform anchoring is amazing wow I love it!!!! - var wasAnchored = structure.Transform.Anchored; - structure.Transform.Anchored = false; - - structure.Transform.Coordinates = ev.Location; - structure.Transform.LocalRotation = constructionPrototype.CanRotate ? ev.Angle : Angle.Zero; - - structure.Transform.Anchored = wasAnchored; - - RaiseNetworkEvent(new AckStructureConstructionMessage(ev.Ack)); - - Cleanup(); + UpdateInteractions(); } } } diff --git a/Content.Server/Destructible/Thresholds/Behaviors/ChangeConstructionNodeBehavior.cs b/Content.Server/Destructible/Thresholds/Behaviors/ChangeConstructionNodeBehavior.cs index fbf65bd941..7e911bffef 100644 --- a/Content.Server/Destructible/Thresholds/Behaviors/ChangeConstructionNodeBehavior.cs +++ b/Content.Server/Destructible/Thresholds/Behaviors/ChangeConstructionNodeBehavior.cs @@ -1,4 +1,5 @@ using System; +using Content.Server.Construction; using Content.Server.Construction.Components; using Robust.Shared.GameObjects; using Robust.Shared.Serialization.Manager.Attributes; @@ -12,15 +13,12 @@ namespace Content.Server.Destructible.Thresholds.Behaviors [DataField("node")] public string Node { get; private set; } = string.Empty; - public async void Execute(IEntity owner, DestructibleSystem system) + public void Execute(IEntity owner, DestructibleSystem system) { - if (string.IsNullOrEmpty(Node) || - !owner.TryGetComponent(out ConstructionComponent? construction)) - { + if (string.IsNullOrEmpty(Node) || !owner.TryGetComponent(out ConstructionComponent? construction)) return; - } - await construction.ChangeNode(Node); + EntitySystem.Get().ChangeNode(owner.Uid, null, Node, true, construction); } } } diff --git a/Content.Server/Doors/Components/ServerDoorComponent.cs b/Content.Server/Doors/Components/ServerDoorComponent.cs index 08d9e55c7c..ee9bdccf6f 100644 --- a/Content.Server/Doors/Components/ServerDoorComponent.cs +++ b/Content.Server/Doors/Components/ServerDoorComponent.cs @@ -7,6 +7,7 @@ using Content.Server.Access.Components; using Content.Server.Access.Systems; using Content.Server.Atmos.Components; using Content.Server.Atmos.EntitySystems; +using Content.Server.Construction; using Content.Server.Construction.Components; using Content.Server.Hands.Components; using Content.Server.Stunnable; @@ -727,7 +728,7 @@ namespace Content.Server.Doors.Components { // Ensure that the construction component is aware of the board container. if (Owner.TryGetComponent(out ConstructionComponent? construction)) - construction.AddContainer("board"); + EntitySystem.Get().AddContainer(Owner.Uid, "board", construction); // We don't do anything if this is null or empty. if (string.IsNullOrEmpty(_boardPrototype)) diff --git a/Content.Server/Tools/ToolSystem.cs b/Content.Server/Tools/ToolSystem.cs index d65b5608d3..2d5e0e8785 100644 --- a/Content.Server/Tools/ToolSystem.cs +++ b/Content.Server/Tools/ToolSystem.cs @@ -35,6 +35,93 @@ namespace Content.Server.Tools InitializeMultipleTools(); } + /// + /// Whether a tool entity has the specified quality or not. + /// + public bool HasQuality(EntityUid uid, string quality, ToolComponent? tool = null) + { + return Resolve(uid, ref tool, false) && tool.Qualities.Contains(quality); + } + + /// + /// Whether a tool entity has all specified qualities or not. + /// + public bool HasAllQualities(EntityUid uid, IEnumerable qualities, ToolComponent? tool = null) + { + return Resolve(uid, ref tool, false) && tool.Qualities.ContainsAll(qualities); + } + + /// + /// Sync version of UseTool. + /// + /// The tool entity. + /// The entity using the tool. + /// Optionally, a target to use the tool on. + /// An optional amount of fuel or energy to consume- + /// A doAfter delay in seconds. + /// The tool qualities needed to use the tool. + /// An event to broadcast once the doAfter is completed successfully. + /// An event to broadcast once the doAfter is cancelled. + /// An optional check to perform for the doAfter. + /// The tool component. + /// Whether initially, using the tool succeeded. If there's a doAfter delay, you'll need to listen to + /// the and being broadcast + /// to see whether using the tool succeeded or not. If the is zero, + /// this simply returns whether using the tool succeeded or not. + public bool UseTool(EntityUid tool, EntityUid user, EntityUid? target, float fuel, + float doAfterDelay, IEnumerable toolQualitiesNeeded, object doAfterCompleteEvent, object doAfterCancelledEvent, + Func? doAfterCheck = null, ToolComponent? toolComponent = null) + { + // No logging here, after all that'd mean the caller would need to check if the component is there or not. + if (!Resolve(tool, ref toolComponent, false)) + return false; + + if (!ToolStartUse(tool, user, fuel, toolQualitiesNeeded, toolComponent)) + return false; + + if (doAfterDelay > 0f) + { + var doAfterArgs = new DoAfterEventArgs(user, doAfterDelay / toolComponent.SpeedModifier, default, target) + { + ExtraCheck = doAfterCheck, + BreakOnDamage = true, + BreakOnStun = true, + BreakOnTargetMove = true, + BreakOnUserMove = true, + NeedHand = true, + BroadcastFinishedEvent = doAfterCompleteEvent, + BroadcastCancelledEvent = doAfterCancelledEvent, + }; + + _doAfterSystem.DoAfter(doAfterArgs); + return true; + } + + return ToolFinishUse(tool, user, fuel, toolComponent); + } + + // This is hilariously long. + /// + public bool UseTool(EntityUid tool, EntityUid user, EntityUid? target, float fuel, + float doAfterDelay, string toolQualityNeeded, object doAfterCompleteEvent, object doAfterCancelledEvent, + Func? doAfterCheck = null, ToolComponent? toolComponent = null) + { + return UseTool(tool, user, target, fuel, doAfterDelay, new[] { toolQualityNeeded }, + doAfterCompleteEvent, doAfterCancelledEvent, doAfterCheck, toolComponent); + } + + /// + /// Async version of UseTool. + /// + /// The tool entity. + /// The entity using the tool. + /// Optionally, a target to use the tool on. + /// An optional amount of fuel or energy to consume- + /// A doAfter delay in seconds. + /// The tool qualities needed to use the tool. + /// An optional check to perform for the doAfter. + /// The tool component. + /// Whether using the tool succeeded or not. public async Task UseTool(EntityUid tool, EntityUid user, EntityUid? target, float fuel, float doAfterDelay, IEnumerable toolQualitiesNeeded, Func? doAfterCheck = null, ToolComponent? toolComponent = null) @@ -43,13 +130,7 @@ namespace Content.Server.Tools if (!Resolve(tool, ref toolComponent, false)) return false; - if (!toolComponent.Qualities.ContainsAll(toolQualitiesNeeded) || !_actionBlockerSystem.CanInteract(user)) - return false; - - var beforeAttempt = new ToolUseAttemptEvent(fuel, user); - RaiseLocalEvent(tool, beforeAttempt, false); - - if (beforeAttempt.Cancelled) + if (!ToolStartUse(tool, user, fuel, toolQualitiesNeeded, toolComponent)) return false; if (doAfterDelay > 0f) @@ -70,6 +151,37 @@ namespace Content.Server.Tools return false; } + return ToolFinishUse(tool, user, fuel, toolComponent); + } + + // This is hilariously long. + /// + public Task UseTool(EntityUid tool, EntityUid user, EntityUid? target, float fuel, + float doAfterDelay, string toolQualityNeeded, Func? doAfterCheck = null, + ToolComponent? toolComponent = null) + { + return UseTool(tool, user, target, fuel, doAfterDelay, new [] {toolQualityNeeded}, doAfterCheck, toolComponent); + } + + private bool ToolStartUse(EntityUid tool, EntityUid user, float fuel, IEnumerable toolQualitiesNeeded, ToolComponent? toolComponent = null) + { + if (!Resolve(tool, ref toolComponent)) + return false; + + if (!toolComponent.Qualities.ContainsAll(toolQualitiesNeeded) || !_actionBlockerSystem.CanInteract(user)) + return false; + + var beforeAttempt = new ToolUseAttemptEvent(fuel, user); + RaiseLocalEvent(tool, beforeAttempt, false); + + return !beforeAttempt.Cancelled; + } + + private bool ToolFinishUse(EntityUid tool, EntityUid user, float fuel, ToolComponent? toolComponent = null) + { + if (!Resolve(tool, ref toolComponent)) + return false; + var afterAttempt = new ToolUseFinishAttemptEvent(fuel, user); RaiseLocalEvent(tool, afterAttempt, false); @@ -82,13 +194,6 @@ namespace Content.Server.Tools return true; } - public Task UseTool(EntityUid tool, EntityUid user, EntityUid? target, float fuel, - float doAfterDelay, string toolQualityNeeded, Func? doAfterCheck = null, - ToolComponent? toolComponent = null) - { - return UseTool(tool, user, target, fuel, doAfterDelay, new [] {toolQualityNeeded}, doAfterCheck, toolComponent); - } - public void PlayToolSound(EntityUid uid, ToolComponent? tool = null) { if (!Resolve(uid, ref tool)) diff --git a/Content.Shared/Construction/ConstructionGraphEdge.cs b/Content.Shared/Construction/ConstructionGraphEdge.cs index 0fd82978f9..11008577f9 100644 --- a/Content.Shared/Construction/ConstructionGraphEdge.cs +++ b/Content.Shared/Construction/ConstructionGraphEdge.cs @@ -11,17 +11,17 @@ namespace Content.Shared.Construction public class ConstructionGraphEdge { [DataField("steps")] - private List _steps = new(); + private ConstructionGraphStep[] _steps = Array.Empty(); [DataField("conditions", serverOnly: true)] - private List _conditions = new(); + private IGraphCondition[] _conditions = Array.Empty(); [DataField("completed", serverOnly: true)] - private List _completed = new(); + private IGraphAction[] _completed = Array.Empty(); [ViewVariables] - [DataField("to")] - public string Target { get; private set; } = string.Empty; + [DataField("to", required:true)] + public string Target { get; } = string.Empty; [ViewVariables] public IReadOnlyList Conditions => _conditions; diff --git a/Content.Shared/Construction/ConstructionGraphNode.cs b/Content.Shared/Construction/ConstructionGraphNode.cs index 181eb53b81..81b2c30876 100644 --- a/Content.Shared/Construction/ConstructionGraphNode.cs +++ b/Content.Shared/Construction/ConstructionGraphNode.cs @@ -11,10 +11,10 @@ namespace Content.Shared.Construction public class ConstructionGraphNode { [DataField("actions", serverOnly: true)] - private List _actions = new(); + private IGraphAction[] _actions = Array.Empty(); [DataField("edges")] - private List _edges = new(); + private ConstructionGraphEdge[] _edges = Array.Empty(); [ViewVariables] [DataField("node", required: true)] @@ -41,6 +41,18 @@ namespace Content.Shared.Construction return null; } + public int? GetEdgeIndex(string target) + { + for (var i = 0; i < _edges.Length; i++) + { + var edge = _edges[i]; + if (edge.Target == target) + return i; + } + + return null; + } + public bool TryGetEdge(string target, [NotNullWhen(true)] out ConstructionGraphEdge? edge) { return (edge = GetEdge(target)) != null; diff --git a/Content.Shared/Construction/IGraphAction.cs b/Content.Shared/Construction/IGraphAction.cs index c54f3f0eaa..2347396934 100644 --- a/Content.Shared/Construction/IGraphAction.cs +++ b/Content.Shared/Construction/IGraphAction.cs @@ -7,6 +7,6 @@ namespace Content.Shared.Construction [ImplicitDataDefinitionForInheritors] public interface IGraphAction { - Task PerformAction(IEntity entity, IEntity? user); + void PerformAction(EntityUid uid, EntityUid? userUid, IEntityManager entityManager); } } diff --git a/Content.Shared/Construction/IGraphCondition.cs b/Content.Shared/Construction/IGraphCondition.cs index edbd0433bd..2a7c11a358 100644 --- a/Content.Shared/Construction/IGraphCondition.cs +++ b/Content.Shared/Construction/IGraphCondition.cs @@ -1,12 +1,13 @@ -using System.Threading.Tasks; -using Content.Shared.Examine; +using Content.Shared.Examine; using Robust.Shared.GameObjects; +using Robust.Shared.Serialization.Manager.Attributes; namespace Content.Shared.Construction { + [ImplicitDataDefinitionForInheritors] public interface IGraphCondition { - Task Condition(IEntity entity); + bool Condition(EntityUid uid, IEntityManager entityManager); bool DoExamine(ExaminedEvent args) { return false; } } } diff --git a/Content.Shared/Construction/Prototypes/ConstructionGraphPrototype.cs b/Content.Shared/Construction/Prototypes/ConstructionGraphPrototype.cs index 16b36e187b..419fa84d06 100644 --- a/Content.Shared/Construction/Prototypes/ConstructionGraphPrototype.cs +++ b/Content.Shared/Construction/Prototypes/ConstructionGraphPrototype.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; +using System.Reflection.Metadata; using Robust.Shared.Prototypes; using Robust.Shared.Serialization; using Robust.Shared.Serialization.Manager.Attributes; @@ -13,7 +14,7 @@ namespace Content.Shared.Construction.Prototypes public class ConstructionGraphPrototype : IPrototype, ISerializationHooks { private readonly Dictionary _nodes = new(); - private readonly Dictionary, ConstructionGraphNode[]?> _paths = new(); + private readonly Dictionary<(string, string), ConstructionGraphNode[]?> _paths = new(); private readonly Dictionary> _pathfinding = new(); [ViewVariables] @@ -59,9 +60,24 @@ namespace Content.Shared.Construction.Prototypes return (path = Path(startNode, finishNode)) != null; } + public string[]? PathId(string startNode, string finishNode) + { + if (Path(startNode, finishNode) is not {} path) + return null; + + var nodes = new string[path.Length]; + + for (var i = 0; i < path.Length; i++) + { + nodes[i] = path[i].Name; + } + + return nodes; + } + public ConstructionGraphNode[]? Path(string startNode, string finishNode) { - var tuple = new ValueTuple(startNode, finishNode); + var tuple = (startNode, finishNode); if (_paths.ContainsKey(tuple)) return _paths[tuple]; diff --git a/Content.Shared/Construction/Steps/ConstructionGraphStep.cs b/Content.Shared/Construction/Steps/ConstructionGraphStep.cs index 84f2e5c885..ca77db1522 100644 --- a/Content.Shared/Construction/Steps/ConstructionGraphStep.cs +++ b/Content.Shared/Construction/Steps/ConstructionGraphStep.cs @@ -9,9 +9,9 @@ namespace Content.Shared.Construction.Steps [ImplicitDataDefinitionForInheritors] public abstract class ConstructionGraphStep { - [DataField("completed", serverOnly: true)] private List _completed = new(); + [DataField("completed", serverOnly: true)] private IGraphAction[] _completed = Array.Empty(); - [DataField("doAfter")] public float DoAfter { get; private set; } + [DataField("doAfter")] public float DoAfter { get; } public IReadOnlyList Completed => _completed; diff --git a/Content.Shared/Construction/Steps/ConstructionGraphStepTypeSerializer.cs b/Content.Shared/Construction/Steps/ConstructionGraphStepTypeSerializer.cs index 9bfac04394..0ce144a67a 100644 --- a/Content.Shared/Construction/Steps/ConstructionGraphStepTypeSerializer.cs +++ b/Content.Shared/Construction/Steps/ConstructionGraphStepTypeSerializer.cs @@ -18,34 +18,33 @@ namespace Content.Shared.Construction.Steps { return typeof(MaterialConstructionGraphStep); } - else if (node.Has("tool")) + + if (node.Has("tool")) { return typeof(ToolConstructionGraphStep); } - else if (node.Has("prototype")) + + if (node.Has("prototype")) { return typeof(PrototypeConstructionGraphStep); } - else if (node.Has("component")) + + if (node.Has("component")) { return typeof(ComponentConstructionGraphStep); } - else if (node.Has("tag")) + + if (node.Has("tag")) { return typeof(TagConstructionGraphStep); } - else if (node.Has("allTags") || node.Has("anyTags")) + + if (node.Has("allTags") || node.Has("anyTags")) { return typeof(MultipleTagsConstructionGraphStep); } - else if (node.Has("steps")) - { - return typeof(NestedConstructionGraphStep); - } - else - { - return null; - } + + return null; } public DeserializationResult Read(ISerializationManager serializationManager, diff --git a/Content.Shared/Construction/Steps/MaterialConstructionGraphStep.cs b/Content.Shared/Construction/Steps/MaterialConstructionGraphStep.cs index 7af699d078..175394bc78 100644 --- a/Content.Shared/Construction/Steps/MaterialConstructionGraphStep.cs +++ b/Content.Shared/Construction/Steps/MaterialConstructionGraphStep.cs @@ -28,12 +28,12 @@ namespace Content.Shared.Construction.Steps public override bool EntityValid(IEntity entity) { - return entity.TryGetComponent(out SharedStackComponent? stack) && stack.StackTypeId.Equals(MaterialPrototypeId); + return entity.TryGetComponent(out SharedStackComponent? stack) && stack.StackTypeId.Equals(MaterialPrototypeId) && stack.Count >= Amount; } public bool EntityValid(IEntity entity, [NotNullWhen(true)] out SharedStackComponent? stack) { - if (entity.TryGetComponent(out SharedStackComponent? otherStack) && otherStack.StackTypeId.Equals(MaterialPrototypeId)) + if (entity.TryGetComponent(out SharedStackComponent? otherStack) && otherStack.StackTypeId.Equals(MaterialPrototypeId) && otherStack.Count >= Amount) stack = otherStack; else stack = null; diff --git a/Content.Shared/Construction/Steps/NestedConstructionGraphStep.cs b/Content.Shared/Construction/Steps/NestedConstructionGraphStep.cs deleted file mode 100644 index 592f339ddb..0000000000 --- a/Content.Shared/Construction/Steps/NestedConstructionGraphStep.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System.Collections.Generic; -using System.IO; -using System.Linq; -using Content.Shared.Examine; -using Robust.Shared.Serialization; -using Robust.Shared.Serialization.Manager.Attributes; - -namespace Content.Shared.Construction.Steps -{ - [DataDefinition] - public class NestedConstructionGraphStep : ConstructionGraphStep, ISerializationHooks - { - [DataField("steps")] public List> Steps { get; private set; } = new(); - - void ISerializationHooks.AfterDeserialization() - { - if (Steps.Any(inner => inner.Any(step => step is NestedConstructionGraphStep))) - { - throw new InvalidDataException("Can't have nested construction steps inside nested construction steps!"); - } - } - - public override void DoExamine(ExaminedEvent examinedEvent) - { - } - } -} diff --git a/Resources/Prototypes/Recipes/Construction/Graphs/machine.yml b/Resources/Prototypes/Recipes/Construction/Graphs/machine.yml index 53c8cb6478..b013f3b8b2 100644 --- a/Resources/Prototypes/Recipes/Construction/Graphs/machine.yml +++ b/Resources/Prototypes/Recipes/Construction/Graphs/machine.yml @@ -45,15 +45,15 @@ - node: machineFrame entity: MachineFrame actions: - - !type:MachineFrameRegenerateProgress {} + - !type:MachineFrameRegenerateProgress edges: - to: machine conditions: - - !type:EntityAnchored {} - - !type:MachineFrameComplete {} + - !type:EntityAnchored + - !type:MachineFrameComplete completed: # Yes, this is snowflaked so we don't have to make an unique graph per machine. You're welcome. - - !type:BuildMachine {} + - !type:BuildMachine steps: - tool: Screwing doAfter: 0.5