using System.Linq; using Content.Server.Administration.Logs; using Content.Server.Construction.Components; using Content.Server.Temperature.Components; using Content.Shared.Construction; using Content.Shared.Construction.Components; using Content.Shared.Construction.EntitySystems; using Content.Shared.Construction.Steps; using Content.Shared.DoAfter; using Content.Shared.Interaction; using Content.Shared.Interaction.Components; using Content.Shared.Prying.Systems; using Content.Shared.Radio.EntitySystems; using Content.Shared.Stacks; using Content.Shared.Temperature; using Content.Shared.Tools.Systems; using Robust.Shared.Containers; using Robust.Shared.Utility; #if EXCEPTION_TOLERANCE // ReSharper disable once RedundantUsingDirective using Robust.Shared.Exceptions; #endif namespace Content.Server.Construction { public sealed partial class ConstructionSystem { [Dependency] private readonly IAdminLogManager _adminLogger = default!; #if EXCEPTION_TOLERANCE [Dependency] private readonly IRuntimeLog _runtimeLog = default!; #endif private readonly Queue _constructionUpdateQueue = new(); private readonly HashSet _queuedUpdates = new(); private void InitializeInteractions() { SubscribeLocalEvent(EnqueueEvent); // Event handling. Add your subscriptions here! Just make sure they're all handled by EnqueueEvent. SubscribeLocalEvent(EnqueueEvent, new []{typeof(AnchorableSystem), typeof(PryingSystem), typeof(WeldableSystem)}, new []{typeof(EncryptionKeySystem)}); SubscribeLocalEvent(EnqueueEvent); SubscribeLocalEvent(EnqueueEvent); } /// /// Takes in an entity with and an object event, and handles any /// possible construction interactions, depending on the construction's state. /// /// When is true, this method will simply return whether the interaction /// would be handled by the entity or not. It essentially becomes a pure method that modifies nothing. /// The result of this interaction with the entity. private HandleResult HandleEvent(EntityUid uid, object ev, bool validation, ConstructionComponent? construction = null) { if (!Resolve(uid, ref construction)) return HandleResult.False; // If the state machine is in an invalid state (not on a valid node) we can't do anything, ever. if (GetCurrentNode(uid, construction) is not {} node) { return HandleResult.False; } // If we're currently in an edge, we'll let the edge handle or validate the interaction. if (GetCurrentEdge(uid, construction) is {} edge) { var result = HandleEdge(uid, ev, edge, validation, construction); // Reset edge index to none if this failed... if (!validation && result is HandleResult.False && construction.StepIndex == 0) construction.EdgeIndex = null; return result; } // If we're not on an edge, let the node handle or validate the interaction. return HandleNode(uid, ev, node, validation, construction); } /// /// Takes in an entity, a and an object event, and handles any /// possible construction interactions. This will check the interaction against all possible edges, /// and if any of the edges accepts the interaction, we will enter it. /// /// When is true, this method will simply return whether the interaction /// would be handled by the entity or not. It essentially becomes a pure method that modifies nothing. /// The result of this interaction with the entity. private HandleResult HandleNode(EntityUid uid, object ev, ConstructionGraphNode node, bool validation, ConstructionComponent? construction = null) { if (!Resolve(uid, ref construction)) return HandleResult.False; // Let's make extra sure this is zero... construction.StepIndex = 0; // When we handle a node, we're essentially testing the current event interaction against all of this node's // edges' first steps. If any of them accepts the interaction, we stop iterating and enter that edge. for (var i = 0; i < node.Edges.Count; i++) { var edge = node.Edges[i]; if (HandleEdge(uid, ev, edge, validation, construction) is var result and not HandleResult.False) { // Only a True result may modify the state. // In the case of DoAfter, it's only allowed to modify the waiting flag and the current edge index. // In the case of validated, it should NEVER modify the state at all. if (result is not HandleResult.True) { if (result is HandleResult.DoAfter) { construction.EdgeIndex = i; } return result; } // If we're not on the same edge as we were before, that means handling that edge changed the node. if (construction.Node != node.Name) return result; // If we're still in the same node, that means we entered the edge and it's still not done. construction.EdgeIndex = i; UpdatePathfinding(uid, construction); return result; } } return HandleResult.False; } /// /// Takes in an entity, a and an object event, and handles any /// possible construction interactions. This will check the interaction against one of the steps in the edge /// depending on the construction's . /// /// When is true, this method will simply return whether the interaction /// would be handled by the entity or not. It essentially becomes a pure method that modifies nothing. /// The result of this interaction with the entity. private HandleResult HandleEdge(EntityUid uid, object ev, ConstructionGraphEdge edge, bool validation, ConstructionComponent? construction = null) { if (!Resolve(uid, ref construction)) return HandleResult.False; var step = GetStepFromEdge(edge, construction.StepIndex); if (step == null) { Log.Warning($"Called {nameof(HandleEdge)} on entity {ToPrettyString(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; var handle = HandleStep(uid, ev, step, validation, out var user, construction); if (handle is not HandleResult.True) return handle; // Handle step should never handle the interaction during validation. DebugTools.Assert(!validation); // 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); if (construction.Deleted) return HandleResult.True; construction.TargetEdgeIndex = null; construction.EdgeIndex = null; construction.StepIndex = 0; // We change the node now. ChangeNode(uid, user, edge.Target, true, construction); } return HandleResult.True; } /// /// Takes in an entity, a and an object event, and handles any possible /// construction interaction. Unlike , if this succeeds it will perform the /// step's completion actions. Also sets the out parameter to the user's EntityUid. /// /// When is true, this method will simply return whether the interaction /// would be handled by the entity or not. It essentially becomes a pure method that modifies nothing. /// The result of this interaction with the entity. private HandleResult HandleStep(EntityUid uid, object ev, ConstructionGraphStep step, bool validation, out EntityUid? user, ConstructionComponent? construction = null) { user = null; if (!Resolve(uid, ref construction)) return HandleResult.False; // Let HandleInteraction actually handle the event for this step. // We can only perform the rest of our logic if it returns true. var handle = HandleInteraction(uid, ev, step, validation, out user, construction); if (handle is not HandleResult.True) return handle; DebugTools.Assert(!validation); // Actually perform the step completion actions, since the step was handled correctly. PerformActions(uid, user, step.Completed); UpdatePathfinding(uid, construction); return HandleResult.True; } /// /// Takes in an entity, a and an object event, and handles any possible /// construction interaction. Unlike , this only handles the interaction itself /// and doesn't perform any step completion actions. Also sets the out parameter to the user's EntityUid. /// /// When is true, this method will simply return whether the interaction /// would be handled by the entity or not. It essentially becomes a pure method that modifies nothing. /// The result of this interaction with the entity. private HandleResult HandleInteraction(EntityUid uid, object ev, ConstructionGraphStep step, bool validation, out EntityUid? user, ConstructionComponent? construction = null) { user = null; if (!Resolve(uid, ref construction)) return HandleResult.False; // Whether this event is being re-handled after a DoAfter or not. Check DoAfterState for more info. var doAfterState = DoAfterState.None; // The DoAfter events can only perform special logic when we're not validating events. if (ev is ConstructionInteractDoAfterEvent interactDoAfter) { if (interactDoAfter.Cancelled) return HandleResult.False; ev = new InteractUsingEvent( interactDoAfter.User, interactDoAfter.Used!.Value, uid, GetCoordinates(interactDoAfter.ClickLocation)); doAfterState = DoAfterState.Completed; } // 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. user = interactUsing.User; 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, EntityManager, Factory)) return HandleResult.False; // Unremovable items can't be inserted, unless they are a lingering stack if(HasComp(insert) && (!TryComp(insert, out var comp) || !comp.Lingering)) return HandleResult.False; // If we're only testing whether this step would be handled by the given event, then we're done. if (validation) return HandleResult.Validated; // If we still haven't completed this step's DoAfter... if (doAfterState == DoAfterState.None && insertStep.DoAfter > 0) { var doAfterEv = new ConstructionInteractDoAfterEvent(EntityManager, interactUsing); var doAfterEventArgs = new DoAfterArgs(EntityManager, interactUsing.User, step.DoAfter, doAfterEv, uid, uid, interactUsing.Used) { BreakOnDamage = false, BreakOnMove = true, NeedHand = true, }; var started = _doAfterSystem.TryStartDoAfter(doAfterEventArgs); if (!started) return HandleResult.False; #if DEBUG // Verify that the resulting DoAfter event will be handled by the current construction state. // if it can't what is even the point of raising this DoAfter? doAfterEv.DoAfter = new(default, doAfterEventArgs, default); var result = HandleInteraction(uid, doAfterEv, step, validation: true, out _, construction); DebugTools.Assert(result == HandleResult.Validated); #endif 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, materialInsertStep.Amount, Transform(interactUsing.User).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. _container.Insert(insert, _container.EnsureContainer(uid, store)); } else { // If we don't store the item in a container on the entity, we just delete it right away. Del(insert); } // 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; // If we're validating whether this event handles the step... if (validation) { // Then we only really need to check whether the tool entity has that quality or not. return _toolSystem.HasQuality(interactUsing.Used, toolInsertStep.Tool) ? HandleResult.Validated : HandleResult.False; } // If we're handling an event after its DoAfter finished... if (doAfterState == DoAfterState.Completed) return HandleResult.True; var result = _toolSystem.UseTool( interactUsing.Used, interactUsing.User, uid, TimeSpan.FromSeconds(toolInsertStep.DoAfter), new [] { toolInsertStep.Tool }, new ConstructionInteractDoAfterEvent(EntityManager, interactUsing), out var doAfter, toolInsertStep.Fuel); return result && doAfter != null ? HandleResult.DoAfter : HandleResult.False; } case TemperatureConstructionGraphStep temperatureChangeStep: { if (ev is not OnTemperatureChangeEvent) break; // Some things, like microwaves, might need to block the temperature construction step from kicking in, or override it entirely. var tempEvent = new OnConstructionTemperatureEvent(); RaiseLocalEvent(uid, tempEvent, true); if (tempEvent.Result is not null) return tempEvent.Result.Value; // prefer using InternalTemperature since that's more accurate for cooking. float temp; if (TryComp(uid, out var internalTemp)) { temp = internalTemp.Temperature; } else if (TryComp(uid, out var tempComp)) { temp = tempComp.CurrentTemperature; } else { return HandleResult.False; } if ((!temperatureChangeStep.MinTemperature.HasValue || temp >= temperatureChangeStep.MinTemperature.Value) && (!temperatureChangeStep.MaxTemperature.HasValue || temp <= temperatureChangeStep.MaxTemperature.Value)) { return HandleResult.True; } return HandleResult.False; } case PartAssemblyConstructionGraphStep partAssemblyStep: { if (ev is not PartAssemblyPartInsertedEvent) break; if (partAssemblyStep.Condition(uid, EntityManager)) return HandleResult.True; return HandleResult.False; } #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; } /// /// Checks whether a number of s are true for a given entity. /// /// The entity to pass to the conditions. /// The conditions to evaluate. /// This method is short-circuiting; if a condition evaluates to false, we stop checking the rest of conditions. /// Whether all conditions evaluate to true for the given entity. public bool CheckConditions(EntityUid uid, IEnumerable conditions) { foreach (var condition in conditions) { if (!condition.Condition(uid, EntityManager)) return false; } return true; } /// /// Performs a number of s for a given entity, with an optional user entity. /// /// The entity to perform the actions on. /// An optional user entity to pass into the actions. /// The actions to perform. /// This method checks whether the given target entity exists before performing any actions. /// If the entity is deleted by an action, it will short-circuit and stop performing the rest of actions. public void PerformActions(EntityUid uid, EntityUid? userUid, IEnumerable actions) { foreach (var action in actions) { // If an action deletes the entity, we stop performing the rest of actions. if (!Exists(uid)) break; action.PerformAction(uid, userUid, EntityManager); } } /// /// Resets the current construction edge status on an entity. /// /// The target entity. /// The construction component. If null, it will be resolved on the entity. /// This method updates the construction pathfinding on the entity automatically. public void ResetEdge(EntityUid uid, ConstructionComponent? construction = null) { if (!Resolve(uid, ref construction)) return; construction.TargetEdgeIndex = null; construction.EdgeIndex = null; construction.StepIndex = 0; // Update pathfinding to keep it in sync with the current construction status. 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.... while (_constructionUpdateQueue.TryDequeue(out var uid)) { _queuedUpdates.Remove(uid); // Ensure the entity exists and has a Construction component. if (!TryComp(uid, out ConstructionComponent? construction)) continue; #if EXCEPTION_TOLERANCE try { #endif // Handle all queued interactions! while (construction.InteractionQueue.TryDequeue(out var interaction)) { if (construction.Deleted) { Log.Error($"Construction component was deleted while still processing interactions." + $"Entity {ToPrettyString(uid)}, graph: {construction.Graph}, " + $"Next: {interaction.GetType().Name}, " + $"Remaining Queue: {string.Join(", ", construction.InteractionQueue.Select(x => x.GetType().Name))}"); break; } // We set validation to false because we actually want to perform the interaction here. HandleEvent(uid, interaction, false, construction); } #if EXCEPTION_TOLERANCE } catch (Exception e) { Log.Error($"Caught exception while processing construction queue. Entity {ToPrettyString(uid)}, graph: {construction.Graph}"); _runtimeLog.LogException(e, $"{nameof(ConstructionSystem)}.{nameof(UpdateInteractions)}"); Del(uid); } #endif } DebugTools.Assert(_queuedUpdates.Count == 0); } #region Event Handlers /// /// Queues a directed event to be handled by construction on the next update tick. /// Used as a handler for any events that construction can listen to. /// /// The entity the event is directed to. /// The construction component to queue the event on. /// The directed event to be queued. /// Events inheriting are treated specially by this method. /// They will only be queued if they can be validated against the current construction state, /// in which case they will also be set as handled. private void EnqueueEvent(EntityUid uid, ConstructionComponent construction, object args) { // For handled events, we will check if the event leads to a valid construction interaction. // If it does, we mark the event as handled and then enqueue it as normal. 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. if (_queuedUpdates.Add(uid)) _constructionUpdateQueue.Enqueue(uid); } #endregion #region Internal Enum Definitions /// /// Specifies the DoAfter status for a construction step event handler. /// private enum DoAfterState : byte { /// /// If None, this is the first time we're seeing this event and we might want to call a DoAfter /// if the step needs it. /// None, /// /// If Completed, this is the second (and last) time we're seeing this event, and /// the doAfter that was called the first time successfully completed. Handle completion logic now. /// Completed } } /// /// Specifies the result after attempting to handle a specific step with an event. /// public enum HandleResult : byte { /// /// The interaction wasn't handled or validated. /// False, /// /// The interaction would be handled successfully. Nothing was modified. /// Validated, /// /// The interaction was handled successfully. /// True, /// /// The interaction is waiting on a DoAfter now. /// This means the interaction started the DoAfter. /// DoAfter, } #endregion public sealed class OnConstructionTemperatureEvent : HandledEntityEventArgs { public HandleResult? Result; } }