Files
tbd-station-14/Content.Server/Machines/EntitySystems/MultipartMachineSystem.cs
2025-06-02 17:02:41 +03:00

353 lines
13 KiB
C#

using Content.Server.Construction;
using Content.Server.Construction.Components;
using Content.Shared.Machines.Components;
using Content.Shared.Machines.EntitySystems;
using Content.Shared.Machines.Events;
using Robust.Server.GameObjects;
using Robust.Shared.Map.Components;
namespace Content.Server.Machines.EntitySystems;
/// <summary>
/// Server side handling of multipart machines.
/// When requested, performs scans of the map area around the specified entity
/// to find and match parts of the machine.
/// </summary>
public sealed class MultipartMachineSystem : SharedMultipartMachineSystem
{
[Dependency] private readonly IComponentFactory _factory = default!;
[Dependency] private readonly MapSystem _mapSystem = default!;
[Dependency] private readonly EntityLookupSystem _lookupSystem = default!;
// The largest size ANY machine can theoretically have.
// Used to aid search for machines in range of parts that have been anchored/constructed.
private const float MaximumRange = 30;
private readonly HashSet<Entity<MultipartMachineComponent>> _entitiesInRange = [];
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<MultipartMachineComponent, ComponentStartup>(OnComponentStartup);
SubscribeLocalEvent<MultipartMachineComponent, AnchorStateChangedEvent>(OnMachineAnchorChanged);
SubscribeLocalEvent<MultipartMachinePartComponent, AfterConstructionChangeEntityEvent>(OnPartConstructionNodeChanged);
SubscribeLocalEvent<MultipartMachinePartComponent, AnchorStateChangedEvent>(OnPartAnchorChanged);
}
/// <summary>
/// Clears the matched entity from the specified part
/// </summary>
/// <param name="ent">Entity to clear the part for.</param>
/// <param name="part">Enum value for the part to clear.</param>
public void ClearPartEntity(Entity<MultipartMachineComponent?> ent, Enum part)
{
if (!Resolve(ent, ref ent.Comp))
return;
if (!ent.Comp.Parts.TryGetValue(part, out var value))
return;
if (!value.Entity.HasValue)
return;
var partEnt = value.Entity.Value;
var partComp = EnsureComp<MultipartMachinePartComponent>(partEnt);
if (partComp.Master.HasValue)
{
partComp.Master = null;
Dirty(partEnt, partComp);
}
value.Entity = null;
Dirty(ent);
}
/// <summary>
/// Performs a rescan of all parts of the machine to confirm they exist and match
/// the specified requirements for offset, rotation, and components.
/// </summary>
/// <param name="ent">Entity to rescan for.</param>
/// <param name="user">Optional user entity which has caused this rescan.</param>
/// <returns>True the state of the machine's assembly has changed, false otherwise.</returns>
public bool Rescan(Entity<MultipartMachineComponent> ent, EntityUid? user = null)
{
// Get all required transform information to start looking for the other parts based on their offset
if (!XformQuery.TryGetComponent(ent.Owner, out var xform) || !xform.Anchored)
return false;
var gridUid = xform.GridUid;
if (gridUid == null || gridUid != xform.ParentUid || !TryComp<MapGridComponent>(gridUid, out var grid))
return false;
// Whichever component has the MultipartMachine component should be treated as the origin
var machineOrigin = _mapSystem.TileIndicesFor(gridUid.Value, grid, xform.Coordinates);
// Set to true if any of the parts' state changes
var stateHasChanged = false;
// Keep a track of what parts were added or removed so we can inform listeners
Dictionary<Enum, EntityUid> partsAdded = [];
Dictionary<Enum, EntityUid> partsRemoved = [];
var missingParts = false;
var machineRotation = xform.LocalRotation.GetCardinalDir().ToAngle();
foreach (var (key, part) in ent.Comp.Parts)
{
var originalPart = part.Entity;
part.Entity = null;
if (!_factory.TryGetRegistration(part.Component, out var registration))
break;
var query = EntityManager.GetEntityQuery(registration.Type);
ScanPart(machineOrigin, machineRotation, query, gridUid.Value, grid, part);
if (!part.Entity.HasValue && !part.Optional)
missingParts = true;
if (part.Entity == originalPart)
continue; // Nothing has changed here
stateHasChanged = true;
MultipartMachinePartComponent comp;
EntityUid partEnt;
if (part.Entity.HasValue)
{
// This part gained an entity, add the Part component so it can find out which machine
// it's a part of
partEnt = part.Entity.Value;
comp = EnsureComp<MultipartMachinePartComponent>(partEnt);
comp.Master = ent;
partsAdded.Add(key, partEnt);
}
else
{
// This part lost its entity, ensure we clean up the old entity so it's no longer marked
// as something we care about.
partEnt = originalPart!.Value;
comp = EnsureComp<MultipartMachinePartComponent>(partEnt);
comp.Master = null;
partsRemoved.Add(key, partEnt);
}
Dirty(partEnt, comp);
}
ent.Comp.IsAssembled = !missingParts;
if (stateHasChanged)
{
var ev = new MultipartMachineAssemblyStateChanged(
ent,
ent.Comp.IsAssembled,
user,
partsAdded,
partsRemoved
);
RaiseLocalEvent(ent, ref ev);
Dirty(ent);
}
return stateHasChanged;
}
/// <summary>
/// Clears all entities bound to parts for a specified machine.
/// Will also raise the assembly state change and dirty event for it.
/// </summary>
/// <param name="ent">Machine to completely clear the parts of.</param>
private void ClearAllParts(Entity<MultipartMachineComponent> ent)
{
var stateHasChanged = false;
Dictionary<Enum, EntityUid> clearedParts = [];
foreach (var (key, part) in ent.Comp.Parts)
{
if (!part.Entity.HasValue)
continue;
stateHasChanged = true;
var partEntity = part.Entity.Value;
var partComp = EnsureComp<MultipartMachinePartComponent>(partEntity);
clearedParts.Add(key, partEntity);
part.Entity = null;
part.NetEntity = null;
partComp.Master = null;
Dirty(partEntity, partComp);
}
ent.Comp.IsAssembled = false;
if (stateHasChanged)
{
var ev = new MultipartMachineAssemblyStateChanged(
ent,
ent.Comp.IsAssembled,
null,
[],
clearedParts
);
RaiseLocalEvent(ent, ref ev);
Dirty(ent);
}
}
/// <summary>
/// Handles any additional setup of the MultipartMachine component.
/// </summary>
/// <param name="ent">Entity/Component that just started.</param>
/// <param name="args">Args for the startup.</param>
private void OnComponentStartup(Entity<MultipartMachineComponent> ent, ref ComponentStartup args)
{
// If anchored, perform a rescan of this machine when the component starts so we can immediately
// jump to an assembled state if needed.
if (XformQuery.TryGetComponent(ent.Owner, out var xform) && xform.Anchored)
Rescan(ent);
}
/// <summary>
/// Handles when a main machine entity has been anchored or unanchored by a user.
/// Rescanning is then required in order to check whether parts are still in the right places,
/// and raise a AfterConstructionChangeEntityEvent.
/// </summary>
/// <param name="ent">Machine entity that has been anchored or unanchored.</param>
/// <param name="args">Args for this event.</param>
private void OnMachineAnchorChanged(Entity<MultipartMachineComponent> ent,
ref AnchorStateChangedEvent args)
{
if (args.Anchored)
Rescan(ent);
else
ClearAllParts(ent);
}
/// <summary>
/// Handles when a machine part entity has been created due to a move in a construction graph.
/// Rescans all known multipart machines within range that have a part which matches that specific graph
/// and node IDs.
/// </summary>
/// <param name="ent">Machine part entity that has moved in a graph.</param>
/// <param name="args">Args for this event.</param>
private void OnPartConstructionNodeChanged(Entity<MultipartMachinePartComponent> ent,
ref AfterConstructionChangeEntityEvent args)
{
if (!XformQuery.TryGetComponent(ent.Owner, out var constructXform))
return;
_lookupSystem.GetEntitiesInRange(constructXform.Coordinates, MaximumRange, _entitiesInRange);
foreach (var machine in _entitiesInRange)
{
foreach (var part in machine.Comp.Parts.Values)
{
if (args.Graph == part.Graph &&
(args.PreviousNode == part.ExpectedNode || args.CurrentNode == part.ExpectedNode))
{
Rescan(machine);
break; // No need to scan the same machine again
}
}
}
}
/// <summary>
/// Handles when a machine part entity has been anchored or unanchored by a user.
/// We might be able to link an unanchored part to a machine, but anchoring a constructable entity,
/// which machine parts are, will require a rescan of all machines within range as we have no idea
/// what machine it might be a part of.
/// </summary>
/// <param name="ent">Machine part entity that has been anchored or unanchored.</param>
/// <param name="args">Args for this event, notably the anchor status.</param>
private void OnPartAnchorChanged(Entity<MultipartMachinePartComponent> ent, ref AnchorStateChangedEvent args)
{
if (!args.Anchored)
{
if (!TryComp<MultipartMachinePartComponent>(ent.Owner, out var part) || !part.Master.HasValue)
return; // This is not an entity we care about
// This is a machine part that is being unanchored, rescan its machine
if (!TryComp<MultipartMachineComponent>(part.Master, out var machine))
return;
Rescan((part.Master.Value, machine));
return;
}
// We're anchoring some construction, we have no idea which machine this might be for
// so we have to just check everyone in range and perform a rescan.
if (!XformQuery.TryGetComponent(ent.Owner, out var constructXform))
return;
_lookupSystem.GetEntitiesInRange(constructXform.Coordinates, MaximumRange, _entitiesInRange);
foreach (var machine in _entitiesInRange)
{
if (Rescan(machine) && HasPartEntity(machine.AsNullable(), ent.Owner))
return; // This machine is using this entity so we don't need to go any further
}
}
/// <summary>
/// Scans the specified coordinates for any anchored entities that might match the given
/// component and rotation requirements.
/// </summary>
/// <param name="machineOrigin">Origin coordinates for the machine.</param>
/// <param name="rotation">Rotation of the master entity to use when searching for this part.</param>
/// <param name="query">Entity query for the specific component the entity must have.</param>
/// <param name="gridUid">EntityUID of the grid to use for the lookup.</param>
/// <param name="grid">Grid to use for the lookup.</param>
/// <param name="part">Part we're searching for.</param>
/// <returns>True when part is found and matches requirements, false otherwise.</returns>
private bool ScanPart(
Vector2i machineOrigin,
Angle rotation,
EntityQuery<IComponent> query,
EntityUid gridUid,
MapGridComponent grid,
MachinePart part)
{
// Safety first, nuke any existing data
part.Entity = null;
var expectedLocation = machineOrigin + part.Offset.Rotate(rotation);
var expectedRotation = part.Rotation + rotation;
foreach (var entity in _mapSystem.GetAnchoredEntities(gridUid, grid, expectedLocation))
{
if (TerminatingOrDeleted(entity))
{
// Ignore entities which are in the process of being deleted
continue;
}
if (!query.TryGetComponent(entity, out var comp) ||
!Transform(entity).LocalRotation.EqualsApprox(expectedRotation.Theta))
{
// Either has no transform, or doesn't match the rotation
continue;
}
if (!TryComp<ConstructionComponent>(entity, out var construction) ||
construction.Graph != part.Graph ||
construction.Node != part.ExpectedNode)
{
// This constructable doesn't match the right graph we expect
continue;
}
part.Entity = entity;
part.NetEntity = GetNetEntity(entity);
return true;
}
return false;
}
}