Data-oriented Construction System (#2152)
- Powerful - Data-oriented - Approved by PJB - Powered by node graphs and AI pathfinding - Coded by the same nerd who brought you atmos Co-authored-by: Exp <theexp111@gmail.com>
This commit is contained in:
committed by
GitHub
parent
a6647e8de1
commit
745401a41e
@@ -1,29 +1,30 @@
|
||||
using System;
|
||||
#nullable enable
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Content.Server.GameObjects.Components.Construction;
|
||||
using Content.Server.GameObjects.Components.GUI;
|
||||
using Content.Server.GameObjects.Components.Interactable;
|
||||
using Content.Server.GameObjects.Components.Items.Storage;
|
||||
using Content.Server.GameObjects.Components.Stack;
|
||||
using Content.Server.GameObjects.EntitySystems.DoAfter;
|
||||
using Content.Shared.Construction;
|
||||
using Content.Shared.GameObjects.Components;
|
||||
using Content.Shared.GameObjects.Components.Interactable;
|
||||
using Content.Shared.GameObjects.EntitySystems;
|
||||
using Content.Shared.Interfaces.GameObjects.Components;
|
||||
using Content.Shared.Interfaces;
|
||||
using Content.Shared.Utility;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Server.GameObjects.EntitySystems;
|
||||
using Robust.Server.Interfaces.Player;
|
||||
using Robust.Server.GameObjects.Components.Container;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
using Robust.Shared.Interfaces.GameObjects.Components;
|
||||
using Robust.Shared.Interfaces.Random;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Players;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Timers;
|
||||
|
||||
|
||||
namespace Content.Server.GameObjects.EntitySystems
|
||||
{
|
||||
@@ -33,548 +34,425 @@ namespace Content.Server.GameObjects.EntitySystems
|
||||
[UsedImplicitly]
|
||||
internal class ConstructionSystem : SharedConstructionSystem
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _entityManager = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
[Dependency] private readonly IRobustRandom _robustRandom = default!;
|
||||
|
||||
private readonly Dictionary<string, ConstructionPrototype> _craftRecipes = new Dictionary<string, ConstructionPrototype>();
|
||||
private readonly Dictionary<ICommonSession, HashSet<int>> _beingBuilt = new Dictionary<ICommonSession, HashSet<int>>();
|
||||
|
||||
public IReadOnlyDictionary<string, ConstructionPrototype> CraftRecipes => _craftRecipes;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
foreach (var prototype in _prototypeManager.EnumeratePrototypes<ConstructionPrototype>())
|
||||
{
|
||||
_craftRecipes.Add(prototype.Result, prototype);
|
||||
}
|
||||
|
||||
SubscribeNetworkEvent<TryStartStructureConstructionMessage>(HandleStartStructureConstruction);
|
||||
SubscribeNetworkEvent<TryStartItemConstructionMessage>(HandleStartItemConstruction);
|
||||
|
||||
SubscribeLocalEvent<AfterInteractMessage>(HandleToolInteraction);
|
||||
}
|
||||
|
||||
private void HandleStartStructureConstruction(TryStartStructureConstructionMessage msg, EntitySessionEventArgs args)
|
||||
private IEnumerable<IEntity> EnumerateNearby(IEntity user)
|
||||
{
|
||||
var placingEnt = args.SenderSession.AttachedEntity;
|
||||
var result = TryStartStructureConstruction(placingEnt, msg.Location, msg.PrototypeName, msg.Angle);
|
||||
if (!result) return;
|
||||
var responseMsg = new AckStructureConstructionMessage(msg.Ack);
|
||||
var channel = ((IPlayerSession) args.SenderSession).ConnectedClient;
|
||||
RaiseNetworkEvent(responseMsg, channel);
|
||||
}
|
||||
|
||||
private void HandleStartItemConstruction(TryStartItemConstructionMessage msg, EntitySessionEventArgs args)
|
||||
{
|
||||
var placingEnt = args.SenderSession.AttachedEntity;
|
||||
TryStartItemConstruction(placingEnt, msg.PrototypeName);
|
||||
}
|
||||
|
||||
private async void HandleToolInteraction(AfterInteractMessage msg)
|
||||
{
|
||||
if(msg.Handled)
|
||||
return;
|
||||
|
||||
// You can only construct/deconstruct things within reach
|
||||
if(!msg.CanReach)
|
||||
return;
|
||||
|
||||
var targetEnt = msg.Attacked;
|
||||
var handEnt = msg.ItemInHand;
|
||||
|
||||
// A tool has to interact with an entity.
|
||||
if(targetEnt is null || handEnt is null)
|
||||
return;
|
||||
|
||||
if (!handEnt.InRangeUnobstructed(targetEnt, ignoreInsideBlocker: true))
|
||||
return;
|
||||
|
||||
// Cannot deconstruct an entity with no prototype.
|
||||
var targetPrototype = targetEnt.MetaData.EntityPrototype;
|
||||
if (targetPrototype is null)
|
||||
return;
|
||||
|
||||
// the target entity is in the process of being constructed/deconstructed
|
||||
if (msg.Attacked.TryGetComponent<ConstructionComponent>(out var constructComp))
|
||||
if (user.TryGetComponent(out HandsComponent? hands))
|
||||
{
|
||||
var result = await TryConstructEntity(constructComp, handEnt, msg.User);
|
||||
|
||||
// TryConstructEntity may delete the existing entity
|
||||
|
||||
msg.Handled = result;
|
||||
}
|
||||
else // try to start the deconstruction process
|
||||
{
|
||||
// A tool was not used on the entity.
|
||||
if (!handEnt.TryGetComponent<IToolComponent>(out var toolComp))
|
||||
return;
|
||||
|
||||
// no known recipe for entity
|
||||
if (!_craftRecipes.TryGetValue(targetPrototype.ID, out var prototype))
|
||||
return;
|
||||
|
||||
// there is a recipe, but it can't be deconstructed.
|
||||
var lastStep = prototype.Stages[^1].Backward;
|
||||
if (!(lastStep is ConstructionStepTool))
|
||||
return;
|
||||
|
||||
// wrong tool
|
||||
var caps = ((ConstructionStepTool) lastStep).ToolQuality;
|
||||
if ((toolComp.Qualities & caps) == 0)
|
||||
return;
|
||||
|
||||
// ask around and see if the deconstruction prerequisites are satisfied
|
||||
// (remove bulbs, approved access, open panels, etc)
|
||||
var deconCompMsg = new BeginDeconstructCompMsg(msg.User);
|
||||
targetEnt.SendMessage(null, deconCompMsg);
|
||||
if(deconCompMsg.BlockDeconstruct)
|
||||
return;
|
||||
|
||||
var deconEntMsg = new BeginDeconstructEntityMsg(msg.User, handEnt, targetEnt);
|
||||
RaiseLocalEvent(deconEntMsg);
|
||||
if(deconEntMsg.BlockDeconstruct)
|
||||
return;
|
||||
|
||||
// --- GOOD TO GO ---
|
||||
msg.Handled = true;
|
||||
|
||||
// pop off the material and switch to frame
|
||||
var targetEntPos = targetEnt.Transform.MapPosition;
|
||||
if (prototype.Stages.Count <= 2) // there are no intermediate stages
|
||||
foreach (var itemComponent in hands?.GetAllHeldItems()!)
|
||||
{
|
||||
targetEnt.Delete();
|
||||
|
||||
SpawnIngredient(targetEntPos, prototype.Stages[(prototype.Stages.Count - 2)].Forward as ConstructionStepMaterial);
|
||||
}
|
||||
else // replace ent with intermediate
|
||||
{
|
||||
// Spawn frame
|
||||
var frame = SpawnCopyTransform("structureconstructionframe", targetEnt.Transform);
|
||||
var construction = frame.GetComponent<ConstructionComponent>();
|
||||
SetupComponent(construction, prototype);
|
||||
construction.Stage = prototype.Stages.Count - 2;
|
||||
SetupDeconIntermediateSprite(construction, prototype);
|
||||
frame.Transform.LocalRotation = targetEnt.Transform.LocalRotation;
|
||||
|
||||
if (targetEnt.Prototype.Components.TryGetValue("Item", out var itemProtoComp))
|
||||
if (itemComponent.Owner.TryGetComponent(out ServerStorageComponent? storage))
|
||||
{
|
||||
if(frame.HasComponent<ItemComponent>())
|
||||
frame.RemoveComponent<ItemComponent>();
|
||||
|
||||
var itemComp = frame.AddComponent<ItemComponent>();
|
||||
|
||||
var serializer = YamlObjectSerializer.NewReader(itemProtoComp);
|
||||
itemComp.ExposeData(serializer);
|
||||
foreach (var storedEntity in storage.StoredEntities!)
|
||||
{
|
||||
yield return storedEntity;
|
||||
}
|
||||
}
|
||||
|
||||
ReplaceInContainerOrGround(targetEnt, frame);
|
||||
|
||||
// remove target
|
||||
targetEnt.Delete();
|
||||
|
||||
// spawn material
|
||||
SpawnIngredient(targetEntPos, prototype.Stages[(prototype.Stages.Count-2)].Forward as ConstructionStepMaterial);
|
||||
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 _entityManager.GetEntitiesInRange(user!, 2f, true))
|
||||
{
|
||||
yield return near;
|
||||
}
|
||||
}
|
||||
|
||||
private IEntity SpawnCopyTransform(string prototypeId, ITransformComponent toReplace)
|
||||
private async Task<IEntity?> Construct(IEntity user, string materialContainer, ConstructionGraphPrototype graph, ConstructionGraphEdge edge, ConstructionGraphNode targetNode)
|
||||
{
|
||||
var frame = EntityManager.SpawnEntity(prototypeId, toReplace.MapPosition);
|
||||
frame.Transform.WorldRotation = toReplace.WorldRotation;
|
||||
frame.Transform.ParentUid = toReplace.ParentUid;
|
||||
return frame;
|
||||
// We need a place to hold our construction items!
|
||||
var container = ContainerManagerComponent.Ensure<Container>(materialContainer, user, out var existed);
|
||||
|
||||
if (existed)
|
||||
{
|
||||
user.PopupMessageCursor(Loc.GetString("You can't start another construction now!"));
|
||||
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 = ContainerManagerComponent.Ensure<Container>(random.ToString(), user!, 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 sharedStack))
|
||||
continue;
|
||||
|
||||
var stack = (StackComponent) sharedStack;
|
||||
|
||||
if (!stack.Split(materialStep.Amount, user.ToCoordinates(), out var newStack))
|
||||
continue;
|
||||
|
||||
if (string.IsNullOrEmpty(materialStep.Store))
|
||||
{
|
||||
if (!container.Insert(newStack))
|
||||
continue;
|
||||
}
|
||||
else if (!GetContainer(materialStep.Store).Insert(newStack))
|
||||
continue;
|
||||
|
||||
handled = true;
|
||||
break;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case ComponentConstructionGraphStep componentStep:
|
||||
foreach (var entity in EnumerateNearby(user))
|
||||
{
|
||||
if (!componentStep.EntityValid(entity))
|
||||
continue;
|
||||
|
||||
if (string.IsNullOrEmpty(componentStep.Store))
|
||||
{
|
||||
if (!container.Insert(entity))
|
||||
continue;
|
||||
}
|
||||
else if (!GetContainer(componentStep.Store).Insert(entity))
|
||||
continue;
|
||||
|
||||
handled = true;
|
||||
break;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case PrototypeConstructionGraphStep prototypeStep:
|
||||
foreach (var entity in EnumerateNearby(user))
|
||||
{
|
||||
if (!prototypeStep.EntityValid(entity))
|
||||
continue;
|
||||
|
||||
if (string.IsNullOrEmpty(prototypeStep.Store))
|
||||
{
|
||||
if (!container.Insert(entity))
|
||||
continue;
|
||||
}
|
||||
else if (!GetContainer(prototypeStep.Store).Insert(entity))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
handled = true;
|
||||
break;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if (handled == false)
|
||||
{
|
||||
failed = true;
|
||||
break;
|
||||
}
|
||||
|
||||
steps.Add(step);
|
||||
}
|
||||
|
||||
if (failed)
|
||||
{
|
||||
user.PopupMessageCursor(Loc.GetString("You don't have the materials to build that!"));
|
||||
FailCleanup();
|
||||
return null;
|
||||
}
|
||||
|
||||
var doAfterSystem = Get<DoAfterSystem>();
|
||||
|
||||
var doAfterArgs = new DoAfterEventArgs(user, doAfterTime)
|
||||
{
|
||||
BreakOnDamage = true,
|
||||
BreakOnStun = true,
|
||||
BreakOnTargetMove = false,
|
||||
BreakOnUserMove = true,
|
||||
NeedHand = true,
|
||||
};
|
||||
|
||||
if (await doAfterSystem.DoAfter(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 = ContainerManagerComponent.Ensure<Container>(name, newEntity);
|
||||
|
||||
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 static void SetupDeconIntermediateSprite(ConstructionComponent constructionComponent, ConstructionPrototype prototype)
|
||||
private async void HandleStartItemConstruction(TryStartItemConstructionMessage ev, EntitySessionEventArgs args)
|
||||
{
|
||||
if(!constructionComponent.Owner.TryGetComponent<SpriteComponent>(out var spriteComp))
|
||||
return;
|
||||
var constructionPrototype = _prototypeManager.Index<ConstructionPrototype>(ev.PrototypeName);
|
||||
var constructionGraph = _prototypeManager.Index<ConstructionGraphPrototype>(constructionPrototype.Graph);
|
||||
var startNode = constructionGraph.Nodes[constructionPrototype.StartNode];
|
||||
var targetNode = constructionGraph.Nodes[constructionPrototype.TargetNode];
|
||||
var pathFind = constructionGraph.Path(startNode.Name, targetNode.Name);
|
||||
|
||||
for (var i = prototype.Stages.Count - 1; i >= 0; i--)
|
||||
var user = args.SenderSession.AttachedEntity;
|
||||
|
||||
if (user == null || !ActionBlockerSystem.CanInteract(user)) return;
|
||||
|
||||
if (!user.TryGetComponent(out HandsComponent? hands)) return;
|
||||
|
||||
foreach (var condition in constructionPrototype.Conditions)
|
||||
{
|
||||
if (prototype.Stages[i].Icon != null)
|
||||
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)
|
||||
{
|
||||
spriteComp.AddLayerWithSprite(prototype.Stages[1].Icon);
|
||||
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)
|
||||
{
|
||||
var constructionPrototype = _prototypeManager.Index<ConstructionPrototype>(ev.PrototypeName);
|
||||
var constructionGraph = _prototypeManager.Index<ConstructionGraphPrototype>(constructionPrototype.Graph);
|
||||
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 (_beingBuilt.TryGetValue(args.SenderSession, out var set))
|
||||
{
|
||||
if (!set.Add(ev.Ack))
|
||||
{
|
||||
user.PopupMessageCursor(Loc.GetString("You are already building that!"));
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
spriteComp.AddLayerWithSprite(prototype.Icon);
|
||||
}
|
||||
|
||||
public void SpawnIngredient(MapCoordinates position, ConstructionStepMaterial lastStep)
|
||||
{
|
||||
if(lastStep is null)
|
||||
return;
|
||||
|
||||
var material = lastStep.Material;
|
||||
var quantity = lastStep.Amount;
|
||||
|
||||
var matEnt = EntityManager.SpawnEntity(MaterialPrototypes[material], position);
|
||||
if (matEnt.TryGetComponent<StackComponent>(out var stackComp))
|
||||
void Cleanup()
|
||||
{
|
||||
stackComp.Count = quantity;
|
||||
}
|
||||
else
|
||||
{
|
||||
quantity--; // already spawned one above
|
||||
while (quantity > 0)
|
||||
{
|
||||
EntityManager.SpawnEntity(MaterialPrototypes[material], position);
|
||||
quantity--;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly Dictionary<ConstructionStepMaterial.MaterialType, string> MaterialPrototypes =
|
||||
new Dictionary<ConstructionStepMaterial.MaterialType, string>
|
||||
{
|
||||
{ ConstructionStepMaterial.MaterialType.Cable, "CableStack1" },
|
||||
{ ConstructionStepMaterial.MaterialType.Gold, "GoldStack1" },
|
||||
{ ConstructionStepMaterial.MaterialType.Metal, "SteelSheet1" },
|
||||
{ ConstructionStepMaterial.MaterialType.Glass, "GlassSheet1" }
|
||||
};
|
||||
|
||||
private bool TryStartStructureConstruction(IEntity placingEnt, EntityCoordinates loc, string prototypeName, Angle angle)
|
||||
{
|
||||
var prototype = _prototypeManager.Index<ConstructionPrototype>(prototypeName);
|
||||
|
||||
if (!placingEnt.InRangeUnobstructed(loc, ignoreInsideBlocker: prototype.CanBuildInImpassable, popup: true))
|
||||
{
|
||||
return false;
|
||||
_beingBuilt[args.SenderSession].Remove(ev.Ack);
|
||||
}
|
||||
|
||||
if (prototype.Stages.Count < 2)
|
||||
{
|
||||
throw new InvalidOperationException($"Prototype '{prototypeName}' does not have enough stages.");
|
||||
}
|
||||
|
||||
var stage0 = prototype.Stages[0];
|
||||
if (!(stage0.Forward is ConstructionStepMaterial matStep))
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
// Try to find the stack with the material in the user's hand.
|
||||
if(!placingEnt.TryGetComponent<HandsComponent>(out var hands))
|
||||
{
|
||||
return false;
|
||||
};
|
||||
var activeHand = hands.GetActiveHand?.Owner;
|
||||
if (activeHand == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!activeHand.TryGetComponent(out StackComponent stack) || !MaterialStackValidFor(matStep, stack))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!stack.Use(matStep.Amount))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// OK WE'RE GOOD CONSTRUCTION STARTED.
|
||||
Get<AudioSystem>().PlayAtCoords("/Audio/Items/deconstruct.ogg", loc);
|
||||
if (prototype.Stages.Count == 2)
|
||||
{
|
||||
// Exactly 2 stages, so don't make an intermediate frame.
|
||||
var ent = EntityManager.SpawnEntity(prototype.Result, loc);
|
||||
ent.Transform.LocalRotation = angle;
|
||||
}
|
||||
else
|
||||
{
|
||||
var frame = EntityManager.SpawnEntity("structureconstructionframe", loc);
|
||||
var construction = frame.GetComponent<ConstructionComponent>();
|
||||
SetupComponent(construction, prototype);
|
||||
frame.Transform.LocalRotation = angle;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void TryStartItemConstruction(IEntity placingEnt, string prototypeName)
|
||||
{
|
||||
if (!ActionBlockerSystem.CanInteract(placingEnt)) return;
|
||||
|
||||
var prototype = _prototypeManager.Index<ConstructionPrototype>(prototypeName);
|
||||
|
||||
if (prototype.Stages.Count < 2)
|
||||
{
|
||||
throw new InvalidOperationException($"Prototype '{prototypeName}' does not have enough stages.");
|
||||
}
|
||||
|
||||
var stage0 = prototype.Stages[0];
|
||||
if (!(stage0.Forward is ConstructionStepMaterial matStep))
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
// Try to find the stack with the material in the user's hand.
|
||||
if (!placingEnt.TryGetComponent(out HandsComponent hands)) return;
|
||||
|
||||
var activeHand = hands.GetActiveHand?.Owner;
|
||||
if (activeHand == null)
|
||||
if (user == null
|
||||
|| !ActionBlockerSystem.CanInteract(user)
|
||||
|| !user.TryGetComponent(out HandsComponent? hands) || hands.GetActiveHand == null
|
||||
|| !user.InRangeUnobstructed(ev.Location, ignoreInsideBlocker:constructionPrototype.CanBuildInImpassable))
|
||||
{
|
||||
Cleanup();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!activeHand.TryGetComponent(out StackComponent stack) || !MaterialStackValidFor(matStep, stack))
|
||||
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;
|
||||
}
|
||||
|
||||
if (!stack.Use(matStep.Amount))
|
||||
// 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;
|
||||
}
|
||||
|
||||
// OK WE'RE GOOD CONSTRUCTION STARTED.
|
||||
Get<AudioSystem>().PlayFromEntity("/Audio/Items/deconstruct.ogg", placingEnt);
|
||||
if (prototype.Stages.Count == 2)
|
||||
var structure = await Construct(user, (ev.Ack + constructionPrototype.GetHashCode()).ToString(), constructionGraph, edge, targetNode);
|
||||
|
||||
if (structure == null)
|
||||
{
|
||||
// Exactly 2 stages, so don't make an intermediate frame.
|
||||
var ent = SpawnCopyTransform(prototype.Result, placingEnt.Transform);
|
||||
hands.PutInHandOrDrop(ent.GetComponent<ItemComponent>());
|
||||
}
|
||||
else
|
||||
{
|
||||
var frame = SpawnCopyTransform("structureconstructionframe", placingEnt.Transform);
|
||||
var construction = frame.GetComponent<ConstructionComponent>();
|
||||
SetupComponent(construction, prototype);
|
||||
|
||||
var finalPrototype = _prototypeManager.Index<EntityPrototype>(prototype.Result);
|
||||
if (finalPrototype.Components.TryGetValue("Item", out var itemProtoComp))
|
||||
{
|
||||
if(frame.HasComponent<ItemComponent>())
|
||||
frame.RemoveComponent<ItemComponent>();
|
||||
|
||||
var itemComp = frame.AddComponent<ItemComponent>();
|
||||
|
||||
var serializer = YamlObjectSerializer.NewReader(itemProtoComp);
|
||||
itemComp.ExposeData(serializer);
|
||||
|
||||
hands.PutInHandOrDrop(itemComp);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<bool> TryConstructEntity(ConstructionComponent constructionComponent, IEntity handTool, IEntity user)
|
||||
{
|
||||
var constructEntity = constructionComponent.Owner;
|
||||
var spriteComponent = constructEntity.GetComponent<SpriteComponent>();
|
||||
var transformComponent = constructEntity.GetComponent<ITransformComponent>();
|
||||
|
||||
// default interaction check for AttackBy allows inside blockers, so we will check if its blocked if
|
||||
// we're not allowed to build on impassable stuff
|
||||
var constructPrototype = constructionComponent.Prototype;
|
||||
if (constructPrototype.CanBuildInImpassable == false)
|
||||
{
|
||||
if (!user.InRangeUnobstructed(constructEntity, popup: true))
|
||||
return false;
|
||||
Cleanup();
|
||||
return;
|
||||
}
|
||||
|
||||
var stage = constructPrototype.Stages[constructionComponent.Stage];
|
||||
structure.Transform.Coordinates = ev.Location;
|
||||
structure.Transform.LocalRotation = ev.Angle;
|
||||
|
||||
if (await TryProcessStep(constructEntity, stage.Forward, handTool, user, transformComponent.Coordinates))
|
||||
{
|
||||
constructionComponent.Stage++;
|
||||
if (constructionComponent.Stage == constructPrototype.Stages.Count - 1)
|
||||
{
|
||||
// Oh boy we get to finish construction!
|
||||
var ent = SpawnCopyTransform(constructPrototype.Result, transformComponent);
|
||||
ent.Transform.LocalRotation = transformComponent.LocalRotation;
|
||||
RaiseNetworkEvent(new AckStructureConstructionMessage(ev.Ack));
|
||||
|
||||
ReplaceInContainerOrGround(constructEntity, ent);
|
||||
|
||||
constructEntity.Delete();
|
||||
return true;
|
||||
}
|
||||
|
||||
stage = constructPrototype.Stages[constructionComponent.Stage];
|
||||
if (stage.Icon != null)
|
||||
{
|
||||
spriteComponent.LayerSetSprite(0, stage.Icon);
|
||||
}
|
||||
}
|
||||
|
||||
else if (await TryProcessStep(constructEntity, stage.Backward, handTool, user, transformComponent.Coordinates))
|
||||
{
|
||||
constructionComponent.Stage--;
|
||||
stage = constructPrototype.Stages[constructionComponent.Stage];
|
||||
|
||||
// If forward needed a material, drop it
|
||||
SpawnIngredient(constructEntity.Transform.MapPosition, stage.Forward as ConstructionStepMaterial);
|
||||
|
||||
if (constructionComponent.Stage == 0)
|
||||
{
|
||||
// Deconstruction complete.
|
||||
constructEntity.Delete();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (stage.Icon != null)
|
||||
{
|
||||
spriteComponent.LayerSetSprite(0, stage.Icon);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void ReplaceInContainerOrGround(IEntity oldEntity, IEntity newEntity)
|
||||
{
|
||||
var parentEntity = oldEntity.Transform.Parent?.Owner;
|
||||
if (!(parentEntity is null) && parentEntity.TryGetComponent<IContainerManager>(out var containerMan))
|
||||
{
|
||||
if (containerMan.TryGetContainer(oldEntity, out var container))
|
||||
{
|
||||
container.ForceRemove(oldEntity);
|
||||
container.Insert(newEntity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<bool> TryProcessStep(IEntity constructEntity, ConstructionStep step, IEntity slapped, IEntity user, EntityCoordinates gridCoords)
|
||||
{
|
||||
if (step == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var sound = EntitySystemManager.GetEntitySystem<AudioSystem>();
|
||||
|
||||
switch (step)
|
||||
{
|
||||
case ConstructionStepMaterial matStep:
|
||||
if (!slapped.TryGetComponent(out StackComponent stack)
|
||||
|| !MaterialStackValidFor(matStep, stack)
|
||||
|| !stack.Use(matStep.Amount))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (matStep.Material == ConstructionStepMaterial.MaterialType.Cable)
|
||||
sound.PlayAtCoords("/Audio/Items/zip.ogg", gridCoords);
|
||||
else
|
||||
sound.PlayAtCoords("/Audio/Items/deconstruct.ogg", gridCoords);
|
||||
return true;
|
||||
case ConstructionStepTool toolStep:
|
||||
if (!slapped.TryGetComponent<ToolComponent>(out var tool))
|
||||
return false;
|
||||
|
||||
// Handle welder manually since tool steps specify fuel amount needed, for some reason.
|
||||
if (toolStep.ToolQuality.HasFlag(ToolQuality.Welding))
|
||||
return slapped.TryGetComponent<WelderComponent>(out var welder)
|
||||
&& await welder.UseTool(user, constructEntity, toolStep.DoAfterDelay, toolStep.ToolQuality, toolStep.Amount);
|
||||
|
||||
return await tool.UseTool(user, constructEntity, toolStep.DoAfterDelay, toolStep.ToolQuality);
|
||||
|
||||
default:
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
// Really this should check the actual materials at play..
|
||||
private static readonly Dictionary<StackType, ConstructionStepMaterial.MaterialType> StackTypeMap
|
||||
= new Dictionary<StackType, ConstructionStepMaterial.MaterialType>
|
||||
{
|
||||
{ StackType.Cable, ConstructionStepMaterial.MaterialType.Cable },
|
||||
{ StackType.Gold, ConstructionStepMaterial.MaterialType.Gold },
|
||||
{ StackType.Glass, ConstructionStepMaterial.MaterialType.Glass },
|
||||
{ StackType.Metal, ConstructionStepMaterial.MaterialType.Metal }
|
||||
};
|
||||
|
||||
private static bool MaterialStackValidFor(ConstructionStepMaterial step, StackComponent stack)
|
||||
{
|
||||
return StackTypeMap.TryGetValue((StackType)stack.StackType, out var should) && should == step.Material;
|
||||
}
|
||||
|
||||
private void SetupComponent(ConstructionComponent constructionComponent, ConstructionPrototype prototype)
|
||||
{
|
||||
constructionComponent.Prototype = prototype;
|
||||
constructionComponent.Stage = 1;
|
||||
var spriteComp = constructionComponent.Owner.GetComponent<SpriteComponent>();
|
||||
if(prototype.Stages[1].Icon != null)
|
||||
{
|
||||
spriteComp.AddLayerWithSprite(prototype.Stages[1].Icon);
|
||||
}
|
||||
else
|
||||
{
|
||||
spriteComp.AddLayerWithSprite(prototype.Icon);
|
||||
}
|
||||
|
||||
var frame = constructionComponent.Owner;
|
||||
var finalPrototype = _prototypeManager.Index<EntityPrototype>(prototype.Result);
|
||||
|
||||
frame.Name = $"Unfinished {finalPrototype.Name}";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A system message that is raised when an entity is trying to be deconstructed.
|
||||
/// </summary>
|
||||
public class BeginDeconstructEntityMsg : EntitySystemMessage
|
||||
{
|
||||
/// <summary>
|
||||
/// Entity that initiated the deconstruction.
|
||||
/// </summary>
|
||||
public IEntity User { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Tool in the active hand of the user.
|
||||
/// </summary>
|
||||
public IEntity Hand { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Target entity that is trying to be deconstructed.
|
||||
/// </summary>
|
||||
public IEntity Target { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Set this to true if you would like to block the deconstruction from happening.
|
||||
/// </summary>
|
||||
public bool BlockDeconstruct { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Constructs a new instance of <see cref="BeginDeconstructEntityMsg"/>.
|
||||
/// </summary>
|
||||
/// <param name="user">Entity that initiated the deconstruction.</param>
|
||||
/// <param name="hand">Tool in the active hand of the user.</param>
|
||||
/// <param name="target">Target entity that is trying to be deconstructed.</param>
|
||||
public BeginDeconstructEntityMsg(IEntity user, IEntity hand, IEntity target)
|
||||
{
|
||||
User = user;
|
||||
Hand = hand;
|
||||
Target = target;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A component message that is raised when an entity is trying to be deconstructed.
|
||||
/// </summary>
|
||||
public class BeginDeconstructCompMsg : ComponentMessage
|
||||
{
|
||||
/// <summary>
|
||||
/// Entity that initiated the deconstruction.
|
||||
/// </summary>
|
||||
public IEntity User { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Set this to true if you would like to block the deconstruction from happening.
|
||||
/// </summary>
|
||||
public bool BlockDeconstruct { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Constructs a new instance of <see cref="BeginDeconstructCompMsg"/>.
|
||||
/// </summary>
|
||||
/// <param name="user">Entity that initiated the deconstruction.</param>
|
||||
public BeginDeconstructCompMsg(IEntity user)
|
||||
{
|
||||
User = user;
|
||||
Cleanup();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user