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; /// /// 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. /// 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> _entitiesInRange = []; public override void Initialize() { base.Initialize(); SubscribeLocalEvent(OnComponentStartup); SubscribeLocalEvent(OnMachineAnchorChanged); SubscribeLocalEvent(OnPartConstructionNodeChanged); SubscribeLocalEvent(OnPartAnchorChanged); } /// /// Clears the matched entity from the specified part /// /// Entity to clear the part for. /// Enum value for the part to clear. public void ClearPartEntity(Entity 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(partEnt); if (partComp.Master.HasValue) { partComp.Master = null; Dirty(partEnt, partComp); } value.Entity = null; Dirty(ent); } /// /// Performs a rescan of all parts of the machine to confirm they exist and match /// the specified requirements for offset, rotation, and components. /// /// Entity to rescan for. /// Optional user entity which has caused this rescan. /// True the state of the machine's assembly has changed, false otherwise. public bool Rescan(Entity 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(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 partsAdded = []; Dictionary 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(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(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; } /// /// Clears all entities bound to parts for a specified machine. /// Will also raise the assembly state change and dirty event for it. /// /// Machine to completely clear the parts of. private void ClearAllParts(Entity ent) { var stateHasChanged = false; Dictionary clearedParts = []; foreach (var (key, part) in ent.Comp.Parts) { if (!part.Entity.HasValue) continue; stateHasChanged = true; var partEntity = part.Entity.Value; var partComp = EnsureComp(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); } } /// /// Handles any additional setup of the MultipartMachine component. /// /// Entity/Component that just started. /// Args for the startup. private void OnComponentStartup(Entity 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); } /// /// 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. /// /// Machine entity that has been anchored or unanchored. /// Args for this event. private void OnMachineAnchorChanged(Entity ent, ref AnchorStateChangedEvent args) { if (args.Anchored) Rescan(ent); else ClearAllParts(ent); } /// /// 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. /// /// Machine part entity that has moved in a graph. /// Args for this event. private void OnPartConstructionNodeChanged(Entity 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 } } } } /// /// 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. /// /// Machine part entity that has been anchored or unanchored. /// Args for this event, notably the anchor status. private void OnPartAnchorChanged(Entity ent, ref AnchorStateChangedEvent args) { if (!args.Anchored) { if (!TryComp(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(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 } } /// /// Scans the specified coordinates for any anchored entities that might match the given /// component and rotation requirements. /// /// Origin coordinates for the machine. /// Rotation of the master entity to use when searching for this part. /// Entity query for the specific component the entity must have. /// EntityUID of the grid to use for the lookup. /// Grid to use for the lookup. /// Part we're searching for. /// True when part is found and matches requirements, false otherwise. private bool ScanPart( Vector2i machineOrigin, Angle rotation, EntityQuery 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(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; } }