using System.IO; using System.Linq; using System.Threading.Tasks; using Content.Server.Construction.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.Database; using Content.Shared.DoAfter; using Content.Shared.Hands.Components; using Content.Shared.Hands.EntitySystems; using Content.Shared.Interaction; using Content.Shared.Inventory; using Content.Shared.Storage; using Content.Shared.Whitelist; using Robust.Shared.Containers; using Robust.Shared.Map; using Robust.Shared.Player; using Robust.Shared.Timing; namespace Content.Server.Construction { public sealed partial class ConstructionSystem { [Dependency] private readonly IComponentFactory _factory = default!; [Dependency] private readonly InventorySystem _inventorySystem = default!; [Dependency] private readonly SharedInteractionSystem _interactionSystem = default!; [Dependency] private readonly ActionBlockerSystem _actionBlocker = default!; [Dependency] private readonly SharedHandsSystem _handsSystem = default!; [Dependency] private readonly EntityLookupSystem _lookupSystem = default!; [Dependency] private readonly SharedTransformSystem _transformSystem = default!; [Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!; // --- WARNING! LEGACY CODE AHEAD! --- // This entire file contains the legacy code for initial construction. // This is bound to be replaced by a better alternative (probably using dummy entities) // but for now I've isolated them in their own little file. This code is largely unchanged. // --- YOU HAVE BEEN WARNED! AAAH! --- private readonly Dictionary> _beingBuilt = new(); private void InitializeInitial() { SubscribeNetworkEvent(HandleStartStructureConstruction); SubscribeNetworkEvent(HandleStartItemConstruction); } // LEGACY CODE. See warning at the top of the file! private IEnumerable EnumerateNearby(EntityUid user) { foreach (var item in _handsSystem.EnumerateHeld(user)) { if (TryComp(item, out StorageComponent? storage)) { foreach (var storedEntity in storage.Container.ContainedEntities!) { yield return storedEntity; } } yield return item; } if (_inventorySystem.TryGetContainerSlotEnumerator(user, out var containerSlotEnumerator)) { while (containerSlotEnumerator.MoveNext(out var containerSlot)) { if(!containerSlot.ContainedEntity.HasValue) continue; if (EntityManager.TryGetComponent(containerSlot.ContainedEntity.Value, out StorageComponent? storage)) { foreach (var storedEntity in storage.Container.ContainedEntities) { yield return storedEntity; } } yield return containerSlot.ContainedEntity.Value; } } var pos = _transformSystem.GetMapCoordinates(user); foreach (var near in _lookupSystem.GetEntitiesInRange(pos, 2f, LookupFlags.Contained | LookupFlags.Dynamic | LookupFlags.Sundries | LookupFlags.Approximate)) { if (near == user) continue; if (_interactionSystem.InRangeUnobstructed(pos, near, 2f) && _container.IsInSameOrParentContainer(user, near)) yield return near; } } // LEGACY CODE. See warning at the top of the file! private async Task Construct( EntityUid user, string materialContainer, ConstructionGraphPrototype graph, ConstructionGraphEdge edge, ConstructionGraphNode targetNode, EntityCoordinates coords, Angle angle = default) { // We need a place to hold our construction items! var container = _container.EnsureContainer(user, materialContainer, out var existed); if (existed) { _popup.PopupEntity(Loc.GetString("construction-system-construct-cannot-start-another-construction"), user, user); return null; } var containers = new Dictionary(); var doAfterTime = 0f; // HOLY SHIT THIS IS SOME HACKY CODE. // But I'd rather do this shit than risk having collisions with other containers. Container GetContainer(string name) { if (containers.TryGetValue(name, out var container1)) return container1; while (true) { var random = _robustRandom.Next(); var c = _container.EnsureContainer(user, random.ToString(), out var exists); if (exists) continue; containers[name] = c; return c; } } void FailCleanup() { foreach (var entity in container.ContainedEntities.ToArray()) { _container.Remove(entity, container); } foreach (var cont in containers.Values) { foreach (var entity in cont.ContainedEntities.ToArray()) { _container.Remove(entity, cont); } } // If we don't do this, items are invisible for some fucking reason. Nice. Timer.Spawn(1, ShutdownContainers); } void ShutdownContainers() { _container.ShutdownContainer(container); foreach (var c in containers.Values.ToArray()) { _container.ShutdownContainer(c); } } var failed = false; var steps = new List(); var used = new HashSet(); 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; if (used.Contains(entity)) continue; // TODO allow taking from several stacks. // Also update crafting steps to check if it works. var splitStack = _stackSystem.Split(entity, materialStep.Amount, user.ToCoordinates(0, 0), stack); if (splitStack == null) continue; if (string.IsNullOrEmpty(materialStep.Store)) { if (!_container.Insert(splitStack.Value, container)) continue; } else if (!_container.Insert(splitStack.Value, GetContainer(materialStep.Store))) continue; handled = true; break; } break; case ArbitraryInsertConstructionGraphStep arbitraryStep: foreach (var entity in new HashSet(EnumerateNearby(user))) { if (!arbitraryStep.EntityValid(entity, EntityManager, _factory)) continue; if (used.Contains(entity)) continue; // Dump out any stored entities in used entity if (TryComp(entity, out var storage)) { _container.EmptyContainer(storage.Container); } if (string.IsNullOrEmpty(arbitraryStep.Store)) { if (!_container.Insert(entity, container)) continue; } else if (!_container.Insert(entity, GetContainer(arbitraryStep.Store))) continue; handled = true; used.Add(entity); break; } break; } if (handled == false) { failed = true; break; } steps.Add(step); } if (failed) { _popup.PopupEntity(Loc.GetString("construction-system-construct-no-materials"), user, user); FailCleanup(); return null; } var doAfterArgs = new DoAfterArgs(EntityManager, user, doAfterTime, new AwaitedDoAfterEvent(), null) { BreakOnDamage = true, BreakOnMove = true, NeedHand = false, // allow simultaneously starting several construction jobs using the same stack of materials. CancelDuplicate = false, BlockDuplicate = false, }; if (await _doAfterSystem.WaitDoAfter(doAfterArgs) == DoAfterStatus.Cancelled) { FailCleanup(); return null; } var newEntityProto = graph.Nodes[edge.Target].Entity.GetId(null, user, new(EntityManager)); var newEntity = EntityManager.SpawnAttachedTo(newEntityProto, coords, rotation: angle); if (!TryComp(newEntity, out ConstructionComponent? construction)) { Log.Error($"Initial construction does not have a valid target entity! It is missing a ConstructionComponent.\nGraph: {graph.ID}, Initial Target: {edge.Target}, Ent. Prototype: {newEntityProto}\nCreated Entity {ToPrettyString(newEntity)} will be deleted."); Del(newEntity); // Screw you, make proper construction graphs. return null; } // We attempt to set the pathfinding target. SetPathfindingTarget(newEntity, targetNode.Name, construction); // We preserve the containers... foreach (var (name, cont) in containers) { var newCont = _container.EnsureContainer(newEntity, name); foreach (var entity in cont.ContainedEntities.ToArray()) { _container.Remove(entity, cont, reparent: false, force: true); _container.Insert(entity, newCont); } } // 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, user, EntityManager); } } // And we also have edge completed effects! foreach (var completed in edge.Completed) { completed.PerformAction(newEntity, user, EntityManager); } return newEntity; } private async void HandleStartItemConstruction(TryStartItemConstructionMessage ev, EntitySessionEventArgs args) { if (args.SenderSession.AttachedEntity is {Valid: true} user) await TryStartItemConstruction(ev.PrototypeName, user); } // LEGACY CODE. See warning at the top of the file! public async Task TryStartItemConstruction(string prototype, EntityUid user) { if (!PrototypeManager.TryIndex(prototype, out ConstructionPrototype? constructionPrototype)) { Log.Error($"Tried to start construction of invalid recipe '{prototype}'!"); return false; } if (!PrototypeManager.TryIndex(constructionPrototype.Graph, out ConstructionGraphPrototype? constructionGraph)) { Log.Error( $"Invalid construction graph '{constructionPrototype.Graph}' in recipe '{prototype}'!"); return false; } if (_whitelistSystem.IsWhitelistFail(constructionPrototype.EntityWhitelist, user)) { _popup.PopupEntity(Loc.GetString("construction-system-cannot-start"), user, user); return false; } var startNode = constructionGraph.Nodes[constructionPrototype.StartNode]; var targetNode = constructionGraph.Nodes[constructionPrototype.TargetNode]; var pathFind = constructionGraph.Path(startNode.Name, targetNode.Name); if (!_actionBlocker.CanInteract(user, null)) return false; if (!HasComp(user)) return false; foreach (var condition in constructionPrototype.Conditions) { if (!condition.Condition(user, user.ToCoordinates(0, 0), Direction.South)) return false; } if (pathFind == null) { throw new InvalidDataException( $"Can't find path from starting node to target node in construction! Recipe: {prototype}"); } 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: {prototype}"); } // No support for conditions here! foreach (var step in edge.Steps) { switch (step) { case ToolConstructionGraphStep _: throw new InvalidDataException("Invalid first step for construction recipe!"); } } if (await Construct( user, "item_construction", constructionGraph, edge, targetNode, Transform(user).Coordinates) is not { Valid: true } item) return false; // Just in case this is a stack, attempt to merge it. If it isn't a stack, this will just normally pick up // or drop the item as normal. _stackSystem.TryMergeToHands(item, user); return true; } // 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)) { Log.Error($"Tried to start construction of invalid recipe '{ev.PrototypeName}'!"); RaiseNetworkEvent(new AckStructureConstructionMessage(ev.Ack)); return; } if (!PrototypeManager.TryIndex(constructionPrototype.Graph, out ConstructionGraphPrototype? constructionGraph)) { Log.Error($"Invalid construction graph '{constructionPrototype.Graph}' in recipe '{ev.PrototypeName}'!"); RaiseNetworkEvent(new AckStructureConstructionMessage(ev.Ack)); return; } if (args.SenderSession.AttachedEntity is not {Valid: true} user) { Log.Error($"Client sent {nameof(TryStartStructureConstructionMessage)} with no attached entity!"); return; } if (_whitelistSystem.IsWhitelistFail(constructionPrototype.EntityWhitelist, user)) { _popup.PopupEntity(Loc.GetString("construction-system-cannot-start"), user, user); return; } if (_container.IsEntityInContainer(user)) { _popup.PopupEntity(Loc.GetString("construction-system-inside-container"), user, user); 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)) { _popup.PopupEntity(Loc.GetString("construction-system-already-building"), user, user); return; } } else { var newSet = new HashSet {ev.Ack}; _beingBuilt[args.SenderSession] = newSet; } var location = GetCoordinates(ev.Location); foreach (var condition in constructionPrototype.Conditions) { if (!condition.Condition(user, location, ev.Angle.GetCardinalDir())) { Cleanup(); return; } } void Cleanup() { _beingBuilt[args.SenderSession].Remove(ev.Ack); } if (!_actionBlocker.CanInteract(user, null) || !EntityManager.TryGetComponent(user, out HandsComponent? hands) || hands.ActiveHandEntity == null) { Cleanup(); return; } var mapPos = location.ToMap(EntityManager, _transformSystem); var predicate = GetPredicate(constructionPrototype.CanBuildInImpassable, mapPos); if (!_interactionSystem.InRangeUnobstructed(user, mapPos, predicate: predicate)) { 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; if (hands.ActiveHandEntity is not {Valid: true} holding) { Cleanup(); return; } // No support for conditions here! foreach (var step in edge.Steps) { switch (step) { case EntityInsertConstructionGraphStep entityInsert: if (entityInsert.EntityValid(holding, EntityManager, _factory)) valid = true; break; case ToolConstructionGraphStep _: throw new InvalidDataException("Invalid first step for item recipe!"); } if (valid) break; } if (!valid) { Cleanup(); return; } if (await Construct(user, (ev.Ack + constructionPrototype.GetHashCode()).ToString(), constructionGraph, edge, targetNode, GetCoordinates(ev.Location), constructionPrototype.CanRotate ? ev.Angle : Angle.Zero) is not {Valid: true} structure) { Cleanup(); return; } RaiseNetworkEvent(new AckStructureConstructionMessage(ev.Ack, GetNetEntity(structure))); _adminLogger.Add(LogType.Construction, LogImpact.Low, $"{ToPrettyString(user):player} has turned a {ev.PrototypeName} construction ghost into {ToPrettyString(structure)} at {Transform(structure).Coordinates}"); Cleanup(); } } }