ConstructionGL2 Part 1: ECSification, events and steps. (#5017)

- Completely rewrited the `ConstructionComponent` logic to be ECS, *without* looking too much at the original implementation.
    - The original implementation was dirty and unmaintainable, whereas this new implementation is much cleaner, well-organized and maintainable. I've made sure to leave many comments around, explaining what everything does.
- Construction now has a framework for handling events other than `InteractUsing`.
    - This means that you can now have CGL steps for things other than inserting items, using tools...
- Construction no longer uses `async` everywhere for `DoAfter`s. Instead it uses events.
- Construction event handling occurs in the `ConstructionSystem` update tick, instead of on event handlers.
    - This ensures we can delete/modify entities without worrying about "collection modified while enumerating" exceptions.
    - This also means the construction update tick is where all the fun happens, meaning it'll show up on our metrics and give us an idea of how expensive it is/how much tick time is spent in construction.
- `IGraphCondition` and `IGraphAction` have been refactored to take in `EntityUid`, `IEntityManager`, and to not be async.
- Removes nested steps, as they made maintainability significantly worse, and nothing used them yet.
- This fixes #4892 and fixes #4857

Please note, this leaves many things unchanged, as my idea is to split this into multiple PRs. Some unchanged things:
- Initial construction code is the same. In the future, it'll probably use dummy entities.
- Client-side guided steps are the same. In the future, the server will generate the guided steps and send them to clients as needed, caching these in both the server and client to save cycles and bandwidth.
- No new construction graph steps... Yet! 👀
This commit is contained in:
Vera Aguilera Puerto
2021-10-26 16:38:03 +02:00
committed by GitHub
parent d70470b99b
commit 189a5c7847
57 changed files with 1877 additions and 1265 deletions

View File

@@ -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<ToolQualityPrototype>(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;

View File

@@ -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<ConstructionSystem>().AddContainer(Owner.Uid, "board", construction);
// We don't do anything if this is null or empty.
if (string.IsNullOrEmpty(_boardPrototype))

View File

@@ -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<ConstructionComponent>();
construction.AddContainer(Container);
entityManager.EntitySysManager.GetEntitySystem<ConstructionSystem>().AddContainer(uid, Container);
}
}
}

View File

@@ -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<ElectrocutionSystem>().TryDoElectrifiedAct(entity.Uid, user.Uid);
EntitySystem.Get<ElectrocutionSystem>().TryDoElectrifiedAct(uid, userUid.Value);
}
}
}

View File

@@ -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<ContainerSystem>();
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<ITransformComponent>(uid);
var computer = entityManager.SpawnEntity(boardComponent.Prototype, transform.Coordinates);
computer.Transform.LocalRotation = transform.LocalRotation;
var computerContainer = ContainerHelpers.EnsureContainer<Container>(computer, Container, out var existed);
var computerContainer = containerSystem.EnsureContainer<Container>(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<ConstructionSystem>().AddContainer(computer.Uid, Container);
entity.Delete();
// Delete the original entity.
entityManager.DeleteEntity(uid);
}
}
}

View File

@@ -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<ITransformComponent>(uid);
var machine = entityManager.SpawnEntity(boardComponent.Prototype, transform.Coordinates);
machine.Transform.LocalRotation = transform.LocalRotation;
var boardContainer = ContainerHelpers.EnsureContainer<Container>(machine, MachineFrameComponent.BoardContainer, out var existed);
var boardContainer = machine.EnsureContainer<Container>(MachineFrameComponent.BoardContainer, out var existed);
if (existed)
{
@@ -73,7 +73,7 @@ namespace Content.Server.Construction.Completions
boardContainer.CleanContainer();
}
var partContainer = ContainerHelpers.EnsureContainer<Container>(machine, MachineFrameComponent.PartContainer, out existed);
var partContainer = machine.EnsureContainer<Container>(MachineFrameComponent.PartContainer, out existed);
if (existed)
{
@@ -90,11 +90,12 @@ namespace Content.Server.Construction.Completions
partContainer.Insert(part);
}
var constructionSystem = entityManager.EntitySysManager.GetEntitySystem<ConstructionSystem>();
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);
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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())

View File

@@ -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);
}
}
}

View File

@@ -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>();
destructibleSystem.ActSystem.HandleDestruction(entity);

View File

@@ -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<ContainerManagerComponent>(out var containerManager))
if (!entityManager.TryGetComponent(uid, out ContainerManagerComponent? containerManager))
return;
var transform = entityManager.GetComponent<ITransformComponent>(uid);
foreach (var container in containerManager.GetAllContainers())
{
container.EmptyContainer(true, entity.Transform.Coordinates);
container.EmptyContainer(true, transform.Coordinates);
}
}
}

View File

@@ -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<ITransformComponent>(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();
}
}

View File

@@ -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<MachineFrameComponent>(out var machineFrame))
if (entityManager.TryGetComponent<MachineFrameComponent>(uid, out var machineFrame))
{
machineFrame.RegenerateProgress();
}

View File

@@ -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<Container>(FromContainer);
var to = entity.EnsureContainer<Container>(ToContainer);
var containerSystem = entityManager.EntitySysManager.GetEntitySystem<ContainerSystem>();
var containerManager = entityManager.EnsureComponent<ContainerManagerComponent>(uid);
var from = containerSystem.EnsureContainer<Container>(uid, FromContainer, containerManager);
var to = containerSystem.EnsureContainer<Container>(uid, ToContainer, containerManager);
foreach (var contained in from.ContainedEntities.ToArray())
{

View File

@@ -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));
}
}
}

View File

@@ -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<PopupSystem>()
.PopupEntity(Loc.GetString(Text), uid, Filter.Pvs(uid, entityManager:entityManager));
}
}
}

View File

@@ -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<PopupSystem>();
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));
}
}
}

View File

@@ -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<ITransformComponent>(uid);
transform.Anchored = Value;
}
}
}

View File

@@ -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<StackSystem>().SetCount(entity.Uid, Amount);
EntitySystem.Get<StackSystem>().SetCount(uid, Amount);
}
}
}

View File

@@ -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<ITransformComponent>(uid);
transform.Coordinates = transform.Coordinates.SnapToGrid(entityManager);
entity.SnapToGrid();
if (SouthRotation)
{
entity.Transform.LocalRotation = Angle.Zero;
transform.LocalRotation = Angle.Zero;
}
}
}

View File

@@ -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<IEntityManager>();
var coordinates = entity.Transform.Coordinates;
var coordinates = entityManager.GetComponent<ITransformComponent>(uid).Coordinates;
if (EntityPrototypeHelpers.HasComponent<StackComponent>(Prototype))
{
var stackEnt = entityManager.SpawnEntity(Prototype, coordinates);
var stack = stackEnt.GetComponent<StackComponent>();
EntitySystem.Get<StackSystem>().SetCount(stackEnt.Uid, Amount, stack);
entityManager.EntitySysManager.GetEntitySystem<StackSystem>().SetCount(stackEnt.Uid, Amount, stack);
}
else
{

View File

@@ -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>(Container);
var containerSystem = entityManager.EntitySysManager.GetEntitySystem<ContainerSystem>();
var container = containerSystem.EnsureContainer<Container>(uid, Container);
var coordinates = entityManager.GetComponent<ITransformComponent>(uid).Coordinates;
for (var i = 0; i < Amount; i++)
{
container.Insert(entity.EntityManager.SpawnEntity(Prototype, entity.Transform.Coordinates));
container.Insert(entityManager.SpawnEntity(Prototype, coordinates));
}
}
}

View File

@@ -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;

View File

@@ -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);
}

View File

@@ -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<IReflectionManager>().TryParseEnumReference(Key, out var @enum))
{
appearance.SetData(@enum, Data);
}

View File

@@ -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<string> Containers { get; set; } = new();
private TaskCompletionSource<object>? _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<string> _containers = new();
[ViewVariables]
public List<List<ConstructionGraphStep>>? EdgeNestedStepProgress = null;
private ConstructionGraphNode? _target = null;
public int? TargetEdgeIndex { get; set; } = null;
[ViewVariables]
public ConstructionGraphPrototype? GraphPrototype { get; private set; }
public Queue<string>? NodePathfinding { get; set; } = null;
[ViewVariables]
public ConstructionGraphNode? Node { get; private set; } = null;
[ViewVariables]
public ConstructionGraphEdge? Edge { get; private set; } = null;
public IReadOnlyCollection<string> 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<ConstructionGraphNode>? 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";
/// <summary>
/// Attempts to set a new pathfinding target.
/// </summary>
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<ConstructionGraphNode>(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<bool> IInteractUsing.InteractUsing(InteractUsingEventArgs eventArgs)
{
if (_handling)
return true;
_handlingTask = new TaskCompletionSource<object>();
_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<bool> 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<bool> 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<DoAfterSystem>();
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<ToolSystem>().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<StackSystem>().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<Container>(insertStep.Store);
container.Insert(entityUsing);
}
handled = true;
break;
case NestedConstructionGraphStep nestedStep:
if(EdgeNestedStepProgress == null)
EdgeNestedStepProgress = new List<List<ConstructionGraphStep>>(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<bool> 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<bool> HandleEdge(InteractUsingEventArgs eventArgs)
{
if (Edge == null || EdgeStep >= Edge.Steps.Count) return false;
return await HandleStep(eventArgs, Edge, Edge.Steps[EdgeStep]);
}
private async Task<bool> 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<string>(_containers);
}
if (Owner.TryGetComponent(out ContainerManagerComponent? containerComp))
{
foreach (var container in _containers)
{
var otherContainer = entity.EnsureContainer<Container>(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<object> InteractionQueue = new();
}
}

View File

@@ -124,7 +124,7 @@ namespace Content.Server.Construction.Components
if (Owner.TryGetComponent<ConstructionComponent>(out var construction))
{
// Attempt to set pathfinding to the machine node...
construction.SetNewTarget("machine");
EntitySystem.Get<ConstructionSystem>().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<ConstructionSystem>().ResetEdge(Owner.Uid, construction);
}
return true;

View File

@@ -17,9 +17,10 @@ namespace Content.Server.Construction.Conditions
[DataField("value")]
public bool Value { get; private set; } = true;
public async Task<bool> 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;
}

View File

@@ -15,11 +15,11 @@ namespace Content.Server.Construction.Conditions
[DataField("conditions")]
public IGraphCondition[] Conditions { get; } = Array.Empty<IGraphCondition>();
public async Task<bool> 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;
}

View File

@@ -19,12 +19,9 @@ namespace Content.Server.Construction.Conditions
{
[DataField("value")] public bool Value { get; private set; } = true;
public async Task<bool> Condition(IEntity entity)
public bool Condition(EntityUid uid, IEntityManager entityManager)
{
if (entity.Deleted)
return false;
if (!entity.TryGetComponent<WiresComponent>(out var wires))
if (!entityManager.TryGetComponent(uid, out WiresComponent? wires))
return true;
foreach (var wire in wires.WiresList)

View File

@@ -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<IGraphCondition>();
public async Task<bool> 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;
}

View File

@@ -15,16 +15,8 @@ namespace Content.Server.Construction.Conditions
/// </summary>
[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);
}
/// <summary>
/// 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<bool> 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<IComponentFactory>().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<IEntityLookup>());
var transform = entityManager.GetComponent<ITransformComponent>(uid);
var indices = transform.Coordinates.ToVector2i(entityManager, IoCManager.Resolve<IMapManager>());
var entities = indices.GetEntitiesInTile(transform.GridID, LookupFlags.Approximate | LookupFlags.IncludeAnchored, IoCManager.Resolve<IEntityLookup>());
foreach (var ent in entities)
{

View File

@@ -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<bool> 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<ContainerSystem>();
if (!containerSystem.TryGetContainer(uid, Container, out var container))
return false;
return container.ContainedEntities.Count == 0;
}

View File

@@ -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<bool> 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<ContainerSystem>();
if (!containerSystem.TryGetContainer(uid, Container, out var container))
return false;
return container.ContainedEntities.Count != 0;
}

View File

@@ -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<bool> 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;
}

View File

@@ -14,11 +14,10 @@ namespace Content.Server.Construction.Conditions
{
[DataField("anchored")] public bool Anchored { get; private set; } = true;
public async Task<bool> 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<ITransformComponent>(uid);
return transform.Anchored && Anchored || !transform.Anchored && !Anchored;
}
public bool DoExamine(ExaminedEvent args)

View File

@@ -17,9 +17,9 @@ namespace Content.Server.Construction.Conditions
[DataDefinition]
public class MachineFrameComplete : IGraphCondition
{
public async Task<bool> Condition(IEntity entity)
public bool Condition(EntityUid uid, IEntityManager entityManager)
{
if (entity.Deleted || !entity.TryGetComponent<MachineFrameComponent>(out var machineFrame))
if (!entityManager.TryGetComponent(uid, out MachineFrameComponent? machineFrame))
return false;
return machineFrame.IsComplete;

View File

@@ -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<bool> 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;
}

View File

@@ -17,9 +17,10 @@ namespace Content.Server.Construction.Conditions
{
[DataField("open")] public bool Open { get; private set; } = true;
public async Task<bool> 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;
}

View File

@@ -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<EntityPrototype>(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<ConstructionComponent>(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<ITransformComponent>(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<ContainerManagerComponent>(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<Container>(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<ConstructionGraphPrototype>(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);
}
}
}

View File

@@ -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<ICommonSession, HashSet<int>> _beingBuilt = new();
private void InitializeInitial()
{
SubscribeNetworkEvent<TryStartStructureConstructionMessage>(HandleStartStructureConstruction);
SubscribeNetworkEvent<TryStartItemConstructionMessage>(HandleStartItemConstruction);
}
// LEGACY CODE. See warning at the top of the file!
private IEnumerable<IEntity> 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<IEntityLookup>().GetEntitiesInRange(user!, 2f, LookupFlags.Approximate | LookupFlags.IncludeAnchored))
{
yield return near;
}
}
// LEGACY CODE. See warning at the top of the file!
private async Task<IEntity?> Construct(IEntity user, string materialContainer, ConstructionGraphPrototype graph, ConstructionGraphEdge edge, ConstructionGraphNode targetNode)
{
// We need a place to hold our construction items!
var container = ContainerHelpers.EnsureContainer<Container>(user, materialContainer, out var existed);
if (existed)
{
user.PopupMessageCursor(Loc.GetString("construction-system-construct-cannot-start-another-construction"));
return null;
}
var containers = new Dictionary<string, Container>();
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<Container>(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<ConstructionGraphStep>();
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<ConstructionComponent>();
// 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<Container>(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<ActionBlockerSystem>().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<int> {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<ActionBlockerSystem>().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();
}
}
}

View File

@@ -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<EntityUid> _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<ConstructionDoAfterComplete>(OnDoAfterComplete);
SubscribeLocalEvent<ConstructionDoAfterCancelled>(OnDoAfterCancelled);
SubscribeLocalEvent<ConstructionComponent, ConstructionDoAfterComplete>(EnqueueEvent);
SubscribeLocalEvent<ConstructionComponent, ConstructionDoAfterCancelled>(EnqueueEvent);
#endregion
// Event handling. Add your subscriptions here! Just make sure they're all handled by EnqueueEvent.
SubscribeLocalEvent<ConstructionComponent, InteractUsingEvent>(EnqueueEvent);
}
/// <summary>
/// Takes in an entity with <see cref="ConstructionComponent"/> and an object event, and handles any
/// possible construction interactions, depending on the construction's state.
/// </summary>
/// <remarks>When <see cref="validation"/> 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.</remarks>
/// <returns>The result of this interaction with the entity.</returns>
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);
}
/// <summary>
/// Takes in an entity, a <see cref="ConstructionGraphNode"/> 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.
/// </summary>
/// <remarks>When <see cref="validation"/> 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.</remarks>
/// <returns>The result of this interaction with the entity.</returns>
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;
}
/// <summary>
/// Takes in an entity, a <see cref="ConstructionGraphEdge"/> 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 <see cref="ConstructionComponent.StepIndex"/>.
/// </summary>
/// <remarks>When <see cref="validation"/> 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.</remarks>
/// <returns>The result of this interaction with the entity.</returns>
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;
}
/// <summary>
/// Takes in an entity, a <see cref="ConstructionGraphStep"/> and an object event, and handles any possible
/// construction interaction. Unlike <see cref="HandleInteraction"/>, if this succeeds it will perform the
/// step's completion actions. Also sets the out parameter to the user's EntityUid.
/// </summary>
/// <remarks>When <see cref="validation"/> 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.</remarks>
/// <returns>The result of this interaction with the entity.</returns>
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;
}
/// <summary>
/// Takes in an entity, a <see cref="ConstructionGraphStep"/> and an object event, and handles any possible
/// construction interaction. Unlike <see cref="HandleStep"/>, this only handles the interaction itself
/// and doesn't perform any step completion actions. Also sets the out parameter to the user's EntityUid.
/// </summary>
/// <remarks>When <see cref="validation"/> 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.</remarks>
/// <returns>The result of this interaction with the entity.</returns>
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<Container>(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<IGraphCondition> conditions)
{
foreach (var condition in conditions)
{
if (!condition.Condition(uid, EntityManager))
return false;
}
return true;
}
public void PerformActions(EntityUid uid, EntityUid? userUid, IEnumerable<IGraphAction> 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
/// <summary>
/// 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.
/// </summary>
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;
}
}
/// <summary>
/// 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.
/// </summary>
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
/// <summary>
/// Specifies the DoAfter status for a construction step event handler.
/// </summary>
private enum DoAfterState : byte
{
/// <summary>
/// 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.
/// </summary>
None,
/// <summary>
/// If Validation, we want to validate whether the specified event would handle the step or not.
/// Will NOT modify the construction state at all.
/// </summary>
Validation,
/// <summary>
/// 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.
/// </summary>
Completed,
/// <summary>
/// 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.
/// </summary>
Cancelled
}
/// <summary>
/// Specifies the result after attempting to handle a specific step with an event.
/// </summary>
private enum HandleResult : byte
{
/// <summary>
/// The interaction wasn't handled or validated.
/// </summary>
False,
/// <summary>
/// The interaction would be handled successfully. Nothing was modified.
/// </summary>
Validated,
/// <summary>
/// The interaction was handled successfully.
/// </summary>
True,
/// <summary>
/// The interaction is waiting on a DoAfter now.
/// This means the interaction started the DoAfter.
/// </summary>
DoAfter,
}
#endregion
}
}

View File

@@ -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<string>(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;
}
}
}

View File

@@ -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.
/// </summary>
[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<ICommonSession, HashSet<int>> _beingBuilt = new();
private const string SawmillName = "Construction";
private ISawmill _sawmill = default!;
public override void Initialize()
{
base.Initialize();
SubscribeNetworkEvent<TryStartStructureConstructionMessage>(HandleStartStructureConstruction);
SubscribeNetworkEvent<TryStartItemConstructionMessage>(HandleStartItemConstruction);
_sawmill = _logManager.GetSawmill(SawmillName);
InitializeGraphs();
InitializeSteps();
InitializeInitial();
SubscribeLocalEvent<ConstructionComponent, ComponentInit>(OnConstructionInit);
SubscribeLocalEvent<ConstructionComponent, ComponentStartup>(OnConstructionStartup);
SubscribeLocalEvent<ConstructionComponent, GetOtherVerbsEvent>(AddDeconstructVerb);
SubscribeLocalEvent<ConstructionComponent, ExaminedEvent>(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<IEntity> 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<IEntityLookup>().GetEntitiesInRange(user!, 2f, LookupFlags.Approximate | LookupFlags.IncludeAnchored))
{
yield return near;
}
}
private async Task<IEntity?> Construct(IEntity user, string materialContainer, ConstructionGraphPrototype graph, ConstructionGraphEdge edge, ConstructionGraphNode targetNode)
{
// We need a place to hold our construction items!
var container = ContainerHelpers.EnsureContainer<Container>(user, materialContainer, out var existed);
if (existed)
{
user.PopupMessageCursor(Loc.GetString("construction-system-construct-cannot-start-another-construction"));
return null;
}
var containers = new Dictionary<string, Container>();
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<Container>(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<ConstructionGraphStep>();
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<ConstructionComponent>();
// We attempt to set the pathfinding target.
construction.Target = targetNode;
// We preserve the containers...
foreach (var (name, cont) in containers)
{
var newCont = ContainerHelpers.EnsureContainer<Container>(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<int> {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();
}
}
}

View File

@@ -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<ConstructionSystem>().ChangeNode(owner.Uid, null, Node, true, construction);
}
}
}

View File

@@ -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<ConstructionSystem>().AddContainer(Owner.Uid, "board", construction);
// We don't do anything if this is null or empty.
if (string.IsNullOrEmpty(_boardPrototype))

View File

@@ -35,6 +35,93 @@ namespace Content.Server.Tools
InitializeMultipleTools();
}
/// <summary>
/// Whether a tool entity has the specified quality or not.
/// </summary>
public bool HasQuality(EntityUid uid, string quality, ToolComponent? tool = null)
{
return Resolve(uid, ref tool, false) && tool.Qualities.Contains(quality);
}
/// <summary>
/// Whether a tool entity has all specified qualities or not.
/// </summary>
public bool HasAllQualities(EntityUid uid, IEnumerable<string> qualities, ToolComponent? tool = null)
{
return Resolve(uid, ref tool, false) && tool.Qualities.ContainsAll(qualities);
}
/// <summary>
/// Sync version of UseTool.
/// </summary>
/// <param name="tool">The tool entity.</param>
/// <param name="user">The entity using the tool.</param>
/// <param name="target">Optionally, a target to use the tool on.</param>
/// <param name="fuel">An optional amount of fuel or energy to consume-</param>
/// <param name="doAfterDelay">A doAfter delay in seconds.</param>
/// <param name="toolQualitiesNeeded">The tool qualities needed to use the tool.</param>
/// <param name="doAfterCompleteEvent">An event to broadcast once the doAfter is completed successfully.</param>
/// <param name="doAfterCancelledEvent">An event to broadcast once the doAfter is cancelled.</param>
/// <param name="doAfterCheck">An optional check to perform for the doAfter.</param>
/// <param name="toolComponent">The tool component.</param>
/// <returns>Whether initially, using the tool succeeded. If there's a doAfter delay, you'll need to listen to
/// the <see cref="doAfterCompleteEvent"/> and <see cref="doAfterCancelledEvent"/> being broadcast
/// to see whether using the tool succeeded or not. If the <see cref="doAfterDelay"/> is zero,
/// this simply returns whether using the tool succeeded or not.</returns>
public bool UseTool(EntityUid tool, EntityUid user, EntityUid? target, float fuel,
float doAfterDelay, IEnumerable<string> toolQualitiesNeeded, object doAfterCompleteEvent, object doAfterCancelledEvent,
Func<bool>? 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.
/// <inheritdoc cref="UseTool(Robust.Shared.GameObjects.EntityUid,Robust.Shared.GameObjects.EntityUid,System.Nullable{Robust.Shared.GameObjects.EntityUid},float,float,System.Collections.Generic.IEnumerable{string},Robust.Shared.GameObjects.EntityUid,object,object,System.Func{bool}?,Content.Server.Tools.Components.ToolComponent?)"/>
public bool UseTool(EntityUid tool, EntityUid user, EntityUid? target, float fuel,
float doAfterDelay, string toolQualityNeeded, object doAfterCompleteEvent, object doAfterCancelledEvent,
Func<bool>? doAfterCheck = null, ToolComponent? toolComponent = null)
{
return UseTool(tool, user, target, fuel, doAfterDelay, new[] { toolQualityNeeded },
doAfterCompleteEvent, doAfterCancelledEvent, doAfterCheck, toolComponent);
}
/// <summary>
/// Async version of UseTool.
/// </summary>
/// <param name="tool">The tool entity.</param>
/// <param name="user">The entity using the tool.</param>
/// <param name="target">Optionally, a target to use the tool on.</param>
/// <param name="fuel">An optional amount of fuel or energy to consume-</param>
/// <param name="doAfterDelay">A doAfter delay in seconds.</param>
/// <param name="toolQualitiesNeeded">The tool qualities needed to use the tool.</param>
/// <param name="doAfterCheck">An optional check to perform for the doAfter.</param>
/// <param name="toolComponent">The tool component.</param>
/// <returns>Whether using the tool succeeded or not.</returns>
public async Task<bool> UseTool(EntityUid tool, EntityUid user, EntityUid? target, float fuel,
float doAfterDelay, IEnumerable<string> toolQualitiesNeeded, Func<bool>? 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.
/// <inheritdoc cref="UseTool(Robust.Shared.GameObjects.EntityUid,Robust.Shared.GameObjects.EntityUid,System.Nullable{Robust.Shared.GameObjects.EntityUid},float,float,System.Collections.Generic.IEnumerable{string},Robust.Shared.GameObjects.EntityUid,object,object,System.Func{bool}?,Content.Server.Tools.Components.ToolComponent?)"/>
public Task<bool> UseTool(EntityUid tool, EntityUid user, EntityUid? target, float fuel,
float doAfterDelay, string toolQualityNeeded, Func<bool>? 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<string> 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<bool> UseTool(EntityUid tool, EntityUid user, EntityUid? target, float fuel,
float doAfterDelay, string toolQualityNeeded, Func<bool>? 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))

View File

@@ -11,17 +11,17 @@ namespace Content.Shared.Construction
public class ConstructionGraphEdge
{
[DataField("steps")]
private List<ConstructionGraphStep> _steps = new();
private ConstructionGraphStep[] _steps = Array.Empty<ConstructionGraphStep>();
[DataField("conditions", serverOnly: true)]
private List<IGraphCondition> _conditions = new();
private IGraphCondition[] _conditions = Array.Empty<IGraphCondition>();
[DataField("completed", serverOnly: true)]
private List<IGraphAction> _completed = new();
private IGraphAction[] _completed = Array.Empty<IGraphAction>();
[ViewVariables]
[DataField("to")]
public string Target { get; private set; } = string.Empty;
[DataField("to", required:true)]
public string Target { get; } = string.Empty;
[ViewVariables]
public IReadOnlyList<IGraphCondition> Conditions => _conditions;

View File

@@ -11,10 +11,10 @@ namespace Content.Shared.Construction
public class ConstructionGraphNode
{
[DataField("actions", serverOnly: true)]
private List<IGraphAction> _actions = new();
private IGraphAction[] _actions = Array.Empty<IGraphAction>();
[DataField("edges")]
private List<ConstructionGraphEdge> _edges = new();
private ConstructionGraphEdge[] _edges = Array.Empty<ConstructionGraphEdge>();
[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;

View File

@@ -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);
}
}

View File

@@ -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<bool> Condition(IEntity entity);
bool Condition(EntityUid uid, IEntityManager entityManager);
bool DoExamine(ExaminedEvent args) { return false; }
}
}

View File

@@ -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<string, ConstructionGraphNode> _nodes = new();
private readonly Dictionary<ValueTuple<string, string>, ConstructionGraphNode[]?> _paths = new();
private readonly Dictionary<(string, string), ConstructionGraphNode[]?> _paths = new();
private readonly Dictionary<string, Dictionary<ConstructionGraphNode, ConstructionGraphNode?>> _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<string, string>(startNode, finishNode);
var tuple = (startNode, finishNode);
if (_paths.ContainsKey(tuple))
return _paths[tuple];

View File

@@ -9,9 +9,9 @@ namespace Content.Shared.Construction.Steps
[ImplicitDataDefinitionForInheritors]
public abstract class ConstructionGraphStep
{
[DataField("completed", serverOnly: true)] private List<IGraphAction> _completed = new();
[DataField("completed", serverOnly: true)] private IGraphAction[] _completed = Array.Empty<IGraphAction>();
[DataField("doAfter")] public float DoAfter { get; private set; }
[DataField("doAfter")] public float DoAfter { get; }
public IReadOnlyList<IGraphAction> Completed => _completed;

View File

@@ -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,

View File

@@ -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;

View File

@@ -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<List<ConstructionGraphStep>> 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)
{
}
}
}

View File

@@ -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