Files
wwdpublic/Content.Server/Construction/ConstructionSystem.Interactions.cs
DEATHB4DEFEAT 47b10a01b0 Catch-Up Cherry Pick 2 (#944)
# Description

Picked 400 commits (and skipped many, many more) from WizDen since #540.
Stopped at commit 332f54a3aebe669f6e50d26e7b047f0bdc28e0fb (Lobby
Refactor).

---

# TODO

- [x] Pick
- [x] Compile
- [x] Fix runtime errors
- [ ] Fix up humanoid profile editor
- [ ] Test everything

---

# Changelog

🆑
- add: Merged 400 WizDen PRs. Happy testing!

---------

Co-authored-by: Plykiya <58439124+Plykiya@users.noreply.github.com>
Co-authored-by: FungiFellow <151778459+FungiFellow@users.noreply.github.com>
Co-authored-by: osjarw <62134478+osjarw@users.noreply.github.com>
Co-authored-by: Ubaser <134914314+UbaserB@users.noreply.github.com>
Co-authored-by: beck-thompson <107373427+beck-thompson@users.noreply.github.com>
Co-authored-by: Leon Friedrich <60421075+ElectroJr@users.noreply.github.com>
Co-authored-by: Magnus Larsen <i.am.larsenml@gmail.com>
Co-authored-by: Hanz <41141796+Hanzdegloker@users.noreply.github.com>
Co-authored-by: Kukutis96513 <146854220+Kukutis96513@users.noreply.github.com>
Co-authored-by: potato1234_x <79580518+potato1234x@users.noreply.github.com>
Co-authored-by: Gotimanga <127038462+Gotimanga@users.noreply.github.com>
Co-authored-by: Mangohydra <156087924+Mangohydra@users.noreply.github.com>
Co-authored-by: TsjipTsjip <19798667+TsjipTsjip@users.noreply.github.com>
Co-authored-by: lzk <124214523+lzk228@users.noreply.github.com>
Co-authored-by: SlamBamActionman <83650252+SlamBamActionman@users.noreply.github.com>
Co-authored-by: Morb <14136326+Morb0@users.noreply.github.com>
Co-authored-by: MilenVolf <63782763+MilenVolf@users.noreply.github.com>
Co-authored-by: KrasnoshchekovPavel <119816022+KrasnoshchekovPavel@users.noreply.github.com>
Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
Co-authored-by: Nemanja <98561806+EmoGarbage404@users.noreply.github.com>
Co-authored-by: DrSmugleaf <10968691+DrSmugleaf@users.noreply.github.com>
Co-authored-by: Ed <96445749+TheShuEd@users.noreply.github.com>
Co-authored-by: KittenColony <149278380+KittenColony@users.noreply.github.com>
Co-authored-by: ShadowCommander <shadowjjt@gmail.com>
Co-authored-by: Mr. 27 <45323883+Dutch-VanDerLinde@users.noreply.github.com>
Co-authored-by: T-Stalker <43253663+DogZeroX@users.noreply.github.com>
Co-authored-by: ERROR404 <100093430+ERORR404V1@users.noreply.github.com>
Co-authored-by: Errant <35878406+Errant-4@users.noreply.github.com>
Co-authored-by: Jezithyr <jezithyr@gmail.com>
Co-authored-by: Psychpsyo <60073468+Psychpsyo@users.noreply.github.com>
Co-authored-by: no <165581243+pissdemon@users.noreply.github.com>
Co-authored-by: K-Dynamic <20566341+K-Dynamic@users.noreply.github.com>
Co-authored-by: Ciac32 <aknoxlor@gmail.com>
Co-authored-by: deltanedas <39013340+deltanedas@users.noreply.github.com>
Co-authored-by: NotSoDana <75203942+NotSoDana@users.noreply.github.com>
Co-authored-by: Simon <63975668+Simyon264@users.noreply.github.com>
Co-authored-by: Repo <47093363+Titian3@users.noreply.github.com>
Co-authored-by: Tayrtahn <tayrtahn@gmail.com>
Co-authored-by: nao fujiwara <awkwarddryad@gmail.com>
Co-authored-by: Michael <107807667+Doc-Michael@users.noreply.github.com>
Co-authored-by: Vasilis <vasilis@pikachu.systems>
Co-authored-by: Lamrr <96937466+Lamrr@users.noreply.github.com>
Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com>
Co-authored-by: Jay <67732946+DuskyJay@users.noreply.github.com>
Co-authored-by: Just-a-Unity-Dev <67359748+Just-a-Unity-Dev@users.noreply.github.com>
Co-authored-by: nikthechampiongr <32041239+nikthechampiongr@users.noreply.github.com>
Co-authored-by: Flareguy <78941145+Flareguy@users.noreply.github.com>
Co-authored-by: Tyzemol <85772526+Tyzemol@users.noreply.github.com>
Co-authored-by: Alzore <140123969+Blackern5000@users.noreply.github.com>
Co-authored-by: Pok <113675512+Pok27@users.noreply.github.com>
Co-authored-by: RumiTiger <154005209+RumiTiger@users.noreply.github.com>
Co-authored-by: Verm <32827189+Vermidia@users.noreply.github.com>
Co-authored-by: Pieter-Jan Briers <pieterjan.briers+git@gmail.com>
Co-authored-by: Killerqu00 <47712032+Killerqu00@users.noreply.github.com>
Co-authored-by: Ty Ashley <42426760+TyAshley@users.noreply.github.com>
Co-authored-by: exincore <me@exin.xyz>
Co-authored-by: 0x6273 <0x40@keemail.me>
Co-authored-by: Kara <lunarautomaton6@gmail.com>
Co-authored-by: Ygg01 <y.laughing.man.y@gmail.com>
Co-authored-by: Łukasz Mędrek <lukasz@lukaszm.xyz>
Co-authored-by: Hannah Giovanna Dawson <karakkaraz@gmail.com>
Co-authored-by: TurboTracker <130304754+TurboTrackerss14@users.noreply.github.com>
Co-authored-by: OnsenCapy <101037138+LGRuthes@users.noreply.github.com>
Co-authored-by: pigeonpeas <147350443+pigeonpeas@users.noreply.github.com>
Co-authored-by: Cojoke <83733158+Cojoke-dot@users.noreply.github.com>
Co-authored-by: Tornado Tech <54727692+Tornado-Technology@users.noreply.github.com>
Co-authored-by: Rio <110139251+Riolume@users.noreply.github.com>
Co-authored-by: vorkathbruh <152932728+vorkathbruh@users.noreply.github.com>
Co-authored-by: Sphiral <145869023+SphiraI@users.noreply.github.com>
Co-authored-by: PrPleGoo <PrPleGoo@users.noreply.github.com>
Co-authored-by: Moomoobeef <62638182+Moomoobeef@users.noreply.github.com>
Co-authored-by: username <113782077+whateverusername0@users.noreply.github.com>
Co-authored-by: Boaz1111 <149967078+Boaz1111@users.noreply.github.com>
Co-authored-by: Джексон Миссиссиппи <tripwiregamer@gmail.com>
Co-authored-by: Brandon Li <48413902+aspiringLich@users.noreply.github.com>
Co-authored-by: Jajsha <101492056+Zap527@users.noreply.github.com>
Co-authored-by: RiceMar1244 <138547931+RiceMar1244@users.noreply.github.com>
Co-authored-by: IProduceWidgets <107586145+IProduceWidgets@users.noreply.github.com>
Co-authored-by: youtissoum <51883137+youtissoum@users.noreply.github.com>
Co-authored-by: ike709 <ike709@users.noreply.github.com>
Co-authored-by: icekot8 <93311212+icekot8@users.noreply.github.com>
Co-authored-by: keronshb <54602815+keronshb@users.noreply.github.com>
Co-authored-by: VMSolidus <evilexecutive@gmail.com>
Co-authored-by: Geekyhobo <66805063+Geekyhobo@users.noreply.github.com>
Co-authored-by: FoxxoTrystan <45297731+FoxxoTrystan@users.noreply.github.com>
# Conflicts:
#	Content.Client/Input/ContentContexts.cs
#	Content.Client/Lobby/LobbyState.cs
#	Content.Client/Lobby/UI/HumanoidProfileEditor.xaml
#	Content.Client/Lobby/UI/LobbyGui.xaml
#	Content.Client/Lobby/UI/LobbyGui.xaml.cs
#	Content.Client/Preferences/UI/CharacterSetupGui.xaml.cs
#	Content.Client/UserInterface/Systems/MenuBar/Widgets/GameTopMenuBar.xaml
#	Content.Server/Disposal/Unit/EntitySystems/DisposalUnitSystem.cs
#	Content.Server/Fluids/EntitySystems/PuddleSystem.Spillable.cs
#	Content.Server/GameTicking/GameTicker.Spawning.cs
#	Content.Shared/Alert/AlertType.cs
#	Content.Shared/Input/ContentKeyFunctions.cs
#	Content.Shared/Preferences/HumanoidCharacterProfile.cs
#	Content.Shared/Weapons/Melee/MeleeWeaponComponent.cs
#	Resources/ConfigPresets/EinsteinEngines/default.toml
#	Resources/Prototypes/Alerts/alerts.yml
#	Resources/Prototypes/Entities/Clothing/OuterClothing/coats.yml
#	Resources/Prototypes/Entities/Clothing/OuterClothing/hardsuits.yml
#	Resources/Prototypes/Entities/Clothing/Uniforms/jumpskirts.yml
#	Resources/Prototypes/Entities/Objects/Weapons/Melee/e_sword.yml
#	Resources/Prototypes/Entities/Objects/Weapons/Melee/sword.yml
#	Resources/Prototypes/Recipes/Crafting/Graphs/improvised/makeshiftstunprod.yml
#	Resources/Prototypes/Voice/speech_emotes.yml
#	Resources/keybinds.yml
2024-10-19 14:53:37 +07:00

633 lines
29 KiB
C#

using System.Linq;
using Content.Server.Administration.Logs;
using Content.Server.Construction.Components;
using Content.Server.Temperature.Components;
using Content.Server.Temperature.Systems;
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.Prying.Systems;
using Content.Shared.Radio.EntitySystems;
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<EntityUid> _constructionUpdateQueue = new();
private readonly HashSet<EntityUid> _queuedUpdates = new();
private void InitializeInteractions()
{
SubscribeLocalEvent<ConstructionComponent, ConstructionInteractDoAfterEvent>(EnqueueEvent);
// Event handling. Add your subscriptions here! Just make sure they're all handled by EnqueueEvent.
SubscribeLocalEvent<ConstructionComponent, InteractUsingEvent>(EnqueueEvent,
new []{typeof(AnchorableSystem), typeof(PryingSystem), typeof(WeldableSystem)},
new []{typeof(EncryptionKeySystem)});
SubscribeLocalEvent<ConstructionComponent, OnTemperatureChangeEvent>(EnqueueEvent);
SubscribeLocalEvent<ConstructionComponent, PartAssemblyPartInsertedEvent>(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)
{
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);
}
/// <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, 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;
}
/// <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)
{
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;
}
/// <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.
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;
}
/// <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 = 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;
// 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,
BreakOnTargetMove = true,
BreakOnUserMove = 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<Container>(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<InternalTemperatureComponent>(uid, out var internalTemp))
{
temp = internalTemp.Temperature;
}
else if (TryComp<TemperatureComponent>(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;
}
/// <summary>
/// Checks whether a number of <see cref="IGraphCondition"/>s are true for a given entity.
/// </summary>
/// <param name="uid">The entity to pass to the conditions.</param>
/// <param name="conditions">The conditions to evaluate.</param>
/// <remarks>This method is short-circuiting; if a condition evaluates to false, we stop checking the rest of conditions.</remarks>
/// <returns>Whether all conditions evaluate to true for the given entity.</returns>
public bool CheckConditions(EntityUid uid, IEnumerable<IGraphCondition> conditions)
{
foreach (var condition in conditions)
{
if (!condition.Condition(uid, EntityManager))
return false;
}
return true;
}
/// <summary>
/// Performs a number of <see cref="IGraphAction"/>s for a given entity, with an optional user entity.
/// </summary>
/// <param name="uid">The entity to perform the actions on.</param>
/// <param name="userUid">An optional user entity to pass into the actions.</param>
/// <param name="actions">The actions to perform.</param>
/// <remarks>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.</remarks>
public void PerformActions(EntityUid uid, EntityUid? userUid, IEnumerable<IGraphAction> 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);
}
}
/// <summary>
/// Resets the current construction edge status on an entity.
/// </summary>
/// <param name="uid">The target entity.</param>
/// <param name="construction">The construction component. If null, it will be resolved on the entity.</param>
/// <remarks>This method updates the construction pathfinding on the entity automatically.</remarks>
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
/// <summary>
/// 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. <seealso cref="InitializeInteractions"/>
/// </summary>
/// <param name="uid">The entity the event is directed to.</param>
/// <param name="construction">The construction component to queue the event on.</param>
/// <param name="args">The directed event to be queued.</param>
/// <remarks>Events inheriting <see cref="HandledEntityEventArgs"/> 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.</remarks>
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
/// <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 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>
/// Specifies the result after attempting to handle a specific step with an event.
/// </summary>
public 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
public sealed class OnConstructionTemperatureEvent : HandledEntityEventArgs
{
public HandleResult? Result;
}
}