]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Add multipart machines system (#35969)
authorBarryNorfolk <barrynorfolkman@protonmail.com>
Mon, 2 Jun 2025 14:02:41 +0000 (16:02 +0200)
committerGitHub <noreply@github.com>
Mon, 2 Jun 2025 14:02:41 +0000 (17:02 +0300)
32 files changed:
Content.Client/Machines/Components/MultipartMachineGhostComponent.cs [new file with mode: 0644]
Content.Client/Machines/EntitySystems/MultipartMachineSystem.cs [new file with mode: 0644]
Content.Client/ParticleAccelerator/UI/ParticleAcceleratorControlMenu.xaml
Content.Client/ParticleAccelerator/UI/ParticleAcceleratorControlMenu.xaml.cs
Content.Server/Construction/ConstructionSystem.Graph.cs
Content.Server/Entry/IgnoredComponents.cs
Content.Server/Machines/EntitySystems/MultipartMachineSystem.cs [new file with mode: 0644]
Content.Server/ParticleAccelerator/Components/ParticleAcceleratorControlBoxComponent.cs
Content.Server/ParticleAccelerator/Components/ParticleAcceleratorPartComponent.cs [deleted file]
Content.Server/ParticleAccelerator/EntitySystems/ParticleAcceleratorSystem.ControlBox.cs
Content.Server/ParticleAccelerator/EntitySystems/ParticleAcceleratorSystem.Emitter.cs
Content.Server/ParticleAccelerator/EntitySystems/ParticleAcceleratorSystem.Parts.cs [deleted file]
Content.Server/ParticleAccelerator/EntitySystems/ParticleAcceleratorSystem.PowerBox.cs
Content.Server/ParticleAccelerator/EntitySystems/ParticleAcceleratorSystem.cs
Content.Server/ParticleAccelerator/Wires/ParticleAcceleratorToggleWireAction.cs
Content.Server/Singularity/StartSingularityEngineCommand.cs
Content.Shared/Machines/Components/MultipartMachineComponent.cs [new file with mode: 0644]
Content.Shared/Machines/Components/MultipartMachinePartComponent.cs [new file with mode: 0644]
Content.Shared/Machines/EntitySystems/SharedMultipartMachineSystem.cs [new file with mode: 0644]
Content.Shared/Machines/Events/MultipartMachineEvents.cs [new file with mode: 0644]
Content.Shared/ParticleAccelerator/AcceleratorParts.cs [new file with mode: 0644]
Content.Shared/ParticleAccelerator/Components/ParticleAcceleratorEmitterComponent.cs [moved from Content.Server/ParticleAccelerator/Components/ParticleAcceleratorEmitterComponent.cs with 90% similarity]
Content.Shared/ParticleAccelerator/Components/ParticleAcceleratorEndCapComponent.cs [moved from Content.Server/ParticleAccelerator/Components/ParticleAcceleratorEndCapComponent.cs with 62% similarity]
Content.Shared/ParticleAccelerator/Components/ParticleAcceleratorFuelChamberComponent.cs [moved from Content.Server/ParticleAccelerator/Components/ParticleAcceleratorFuelChamberComponent.cs with 63% similarity]
Content.Shared/ParticleAccelerator/Components/ParticleAcceleratorPowerBoxComponent.cs [moved from Content.Server/ParticleAccelerator/Components/ParticleAcceleratorPowerBoxComponent.cs with 62% similarity]
Resources/Prototypes/Entities/Structures/Machines/multipart.yml [new file with mode: 0644]
Resources/Prototypes/Entities/Structures/Power/Generation/PA/base_particleaccelerator.yml
Resources/Prototypes/Entities/Structures/Power/Generation/PA/control_box.yml
Resources/Prototypes/Entities/Structures/Power/Generation/PA/emitter.yml
Resources/Prototypes/Entities/Structures/Power/Generation/PA/end_cap.yml
Resources/Prototypes/Entities/Structures/Power/Generation/PA/fuel_chamber.yml
Resources/Prototypes/Entities/Structures/Power/Generation/PA/power_box.yml

diff --git a/Content.Client/Machines/Components/MultipartMachineGhostComponent.cs b/Content.Client/Machines/Components/MultipartMachineGhostComponent.cs
new file mode 100644 (file)
index 0000000..8fe4f3d
--- /dev/null
@@ -0,0 +1,14 @@
+namespace Content.Client.Machines.Components;
+
+/// <summary>
+/// Component attached to all multipart machine ghosts
+/// Intended for client side usage only, but used on prototypes.
+/// </summary>
+[RegisterComponent]
+public sealed partial class MultipartMachineGhostComponent : Component
+{
+    /// <summary>
+    /// Machine this particular ghost is linked to.
+    /// </summary>
+    public EntityUid? LinkedMachine = null;
+}
diff --git a/Content.Client/Machines/EntitySystems/MultipartMachineSystem.cs b/Content.Client/Machines/EntitySystems/MultipartMachineSystem.cs
new file mode 100644 (file)
index 0000000..4919a5e
--- /dev/null
@@ -0,0 +1,109 @@
+using Content.Client.Examine;
+using Content.Client.Machines.Components;
+using Content.Shared.Machines.Components;
+using Content.Shared.Machines.EntitySystems;
+using Robust.Client.GameObjects;
+using Robust.Shared.Map;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization.Manager;
+using Robust.Shared.Spawners;
+
+namespace Content.Client.Machines.EntitySystems;
+
+/// <summary>
+/// Client side handling of multipart machines.
+/// Handles client side examination events to show the expected layout of the machine
+/// based on the origin of the main entity.
+/// </summary>
+public sealed class MultipartMachineSystem : SharedMultipartMachineSystem
+{
+    private readonly EntProtoId _ghostPrototype = "MultipartMachineGhost";
+    private readonly Color _partiallyTransparent = new Color(255, 255, 255, 180);
+
+    [Dependency] private readonly SpriteSystem _sprite = default!;
+    [Dependency] private readonly IPrototypeManager _prototype = default!;
+    [Dependency] private readonly MetaDataSystem _metaData = default!;
+    [Dependency] private readonly ISerializationManager _serialization= default!;
+
+    public override void Initialize()
+    {
+        base.Initialize();
+
+        SubscribeLocalEvent<MultipartMachineComponent, ClientExaminedEvent>(OnMachineExamined);
+        SubscribeLocalEvent<MultipartMachineComponent, AfterAutoHandleStateEvent>(OnHandleState);
+        SubscribeLocalEvent<MultipartMachineGhostComponent, TimedDespawnEvent>(OnGhostDespawned);
+    }
+
+    /// <summary>
+    /// Handles spawning several ghost sprites to show where the different parts of the machine
+    /// should go and the rotations they're expected to have.
+    /// Can only show one set of ghost parts at a time and their location depends on the current map/grid
+    /// location of the origin machine.
+    /// </summary>
+    /// <param name="ent">Entity/Component that has been inspected.</param>
+    /// <param name="args">Args for the event.</param>
+    private void OnMachineExamined(Entity<MultipartMachineComponent> ent, ref ClientExaminedEvent args)
+    {
+        if (ent.Comp.Ghosts.Count != 0)
+        {
+            // Already showing some part ghosts
+            return;
+        }
+
+        foreach (var part in ent.Comp.Parts.Values)
+        {
+            if (part.Entity.HasValue)
+                continue;
+
+            var entityCoords = new EntityCoordinates(ent.Owner, part.Offset);
+            var ghostEnt = Spawn(_ghostPrototype, entityCoords);
+
+            if (!XformQuery.TryGetComponent(ghostEnt, out var xform))
+                break;
+
+            xform.LocalRotation = part.Rotation;
+
+            Comp<MultipartMachineGhostComponent>(ghostEnt).LinkedMachine = ent;
+
+            ent.Comp.Ghosts.Add(ghostEnt);
+
+            if (part.GhostProto == null)
+                continue;
+
+            var entProto = _prototype.Index(part.GhostProto.Value);
+            if (!entProto.Components.TryGetComponent("Sprite", out var s) || s is not SpriteComponent protoSprite)
+                return;
+
+            var ghostSprite = EnsureComp<SpriteComponent>(ghostEnt);
+            _serialization.CopyTo(protoSprite, ref ghostSprite, notNullableOverride: true);
+
+            _sprite.SetColor((ghostEnt, ghostSprite), _partiallyTransparent);
+
+            _metaData.SetEntityName(ghostEnt, entProto.Name);
+            _metaData.SetEntityDescription(ghostEnt, entProto.Description);
+        }
+    }
+
+    private void OnHandleState(Entity<MultipartMachineComponent> ent, ref AfterAutoHandleStateEvent args)
+    {
+        foreach (var part in ent.Comp.Parts.Values)
+        {
+            part.Entity = part.NetEntity.HasValue ? EnsureEntity<MultipartMachinePartComponent>(part.NetEntity.Value, ent) : null;
+        }
+    }
+
+    /// <summary>
+    /// Handles when a ghost part despawns after its short lifetime.
+    /// Will attempt to remove itself from the list of known ghost entities in the main multipart
+    /// machine component.
+    /// </summary>
+    /// <param name="ent">Ghost entity that has been despawned.</param>
+    /// <param name="args">Args for the event.</param>
+    private void OnGhostDespawned(Entity<MultipartMachineGhostComponent> ent, ref TimedDespawnEvent args)
+    {
+        if (!TryComp<MultipartMachineComponent>(ent.Comp.LinkedMachine, out var machine))
+            return;
+
+        machine.Ghosts.Remove(ent);
+    }
+}
index 7cef7d58b63d47fc82076d83aa6b68aa101c7ee6..d05262f72de72b81b3aeb2b429cd704a3aade10d 100644 (file)
                             <Control/>
                             <ui:PASegmentControl Name="EndCapTexture" BaseState="end_cap"/>
                             <Control/>
-                            <ui:PASegmentControl Name="ControlBoxTexture" BaseState="control_box"/>
+                            <ui:PASegmentControl Name="ControlBoxTexture" BaseState="control_box" DefaultVisible="True"/>
                             <ui:PASegmentControl Name="FuelChamberTexture" BaseState="fuel_chamber"/>
                             <Control/>
                             <Control/>
index 8b21e7d94bd5bfdf0151db480e770d23d75bee48..cc5016c4a70a62aa7ea5f9ebc8f422bcd9c1dae2 100644 (file)
@@ -268,6 +268,7 @@ public sealed class PASegmentControl : Control
     private RSI? _rsi;
 
     public string BaseState { get; set; } = "control_box";
+    public bool DefaultVisible { get; set; } = false;
 
     public PASegmentControl()
     {
@@ -283,12 +284,14 @@ public sealed class PASegmentControl : Control
         _rsi = IoCManager.Resolve<IResourceCache>().GetResource<RSIResource>($"/Textures/Structures/Power/Generation/PA/{BaseState}.rsi").RSI;
         MinSize = _rsi.Size;
         _base.Texture = _rsi["completed"].Frame0;
+
+        SetVisible(DefaultVisible);
+        _unlit.Visible = DefaultVisible;
     }
 
     public void SetPowerState(ParticleAcceleratorUIState state, bool exists)
     {
-        _base.ShaderOverride = exists ? null : _greyScaleShader;
-        _base.ModulateSelfOverride = exists ? null : new Color(127, 127, 127);
+        SetVisible(exists);
 
         if (!state.Enabled || !exists)
         {
@@ -319,4 +322,23 @@ public sealed class PASegmentControl : Control
 
         _unlit.Texture = rState.Frame0;
     }
+
+    /// <summary>
+    /// Adds/Removes the shading to the part in the control menu based on the
+    /// input state.
+    /// </summary>
+    /// <param name="state">True if the part exists, false otherwise</param>
+    private void SetVisible(bool state)
+    {
+        if (state)
+        {
+            _base.ShaderOverride = null;
+            _base.ModulateSelfOverride = null;
+        }
+        else
+        {
+            _base.ShaderOverride = _greyScaleShader;
+            _base.ModulateSelfOverride = new Color(127, 127, 127);
+        }
+    }
 }
index 0027b941f85ce5c7ad5ff34671f241bb7ee8a71d..7d4dd6153dc07b817c4b096e9fe01e1af0f67fff 100644 (file)
@@ -258,7 +258,7 @@ namespace Content.Server.Construction
 
             // ChangeEntity will handle the pathfinding update.
             if (node.Entity.GetId(uid, userUid, new(EntityManager)) is { } newEntity
-                && ChangeEntity(uid, userUid, newEntity, construction) != null)
+                && ChangeEntity(uid, userUid, newEntity, construction, oldNode) != null)
                 return true;
 
             if (performActions)
@@ -281,6 +281,7 @@ namespace Content.Server.Construction
         /// <param name="userUid">An optional user entity, for actions.</param>
         /// <param name="newEntity">The entity prototype identifier for the new entity.</param>
         /// <param name="construction">The construction component of the target entity. Will be resolved if null.</param>
+        /// <param name="previousNode">The previous node, if any, this graph was on before changing entity.</param>
         /// <param name="metaData">The metadata component of the target entity. Will be resolved if null.</param>
         /// <param name="transform">The transform component of the target entity. Will be resolved if null.</param>
         /// <param name="containerManager">The container manager component of the target entity. Will be resolved if null,
@@ -288,6 +289,7 @@ namespace Content.Server.Construction
         /// <returns>The new entity, or null if the method did not succeed.</returns>
         private EntityUid? ChangeEntity(EntityUid uid, EntityUid? userUid, string newEntity,
             ConstructionComponent? construction = null,
+            string? previousNode = null,
             MetaDataComponent? metaData = null,
             TransformComponent? transform = null,
             ContainerManagerComponent? containerManager = null)
@@ -407,6 +409,11 @@ namespace Content.Server.Construction
 
             QueueDel(uid);
 
+            // If ChangeEntity has ran, then the entity uid has changed and the
+            // new entity should be initialized by this point.
+            var afterChangeEv = new AfterConstructionChangeEntityEvent(construction.Graph, construction.Node, previousNode);
+            RaiseLocalEvent(newUid, ref afterChangeEv);
+
             return newUid;
         }
 
@@ -453,4 +460,16 @@ namespace Content.Server.Construction
             Old = oldUid;
         }
     }
+
+    /// <summary>
+    /// This event is raised after an entity changes prototype/uid during construction.
+    /// This is only raised at the new entity, after it has been initialized.
+    /// </summary>
+    /// <param name="Graph">Construction graph for this entity.</param>
+    /// <param name="CurrentNode">New node that has become active.</param>
+    /// <param name="PreviousNode">Previous node that was active on the graph.</param>
+    [ByRefEvent]
+    public record struct AfterConstructionChangeEntityEvent(string Graph, string CurrentNode, string? PreviousNode)
+    {
+    }
 }
index 04c45662272b4c3d88d99fa1da1587517fb8e2eb..58264e14adbe2847c88e7a100a6d100c3c8150f4 100644 (file)
@@ -19,7 +19,8 @@ namespace Content.Server.Entry
             "InventorySlots",
             "LightFade",
             "HolidayRsiSwap",
-            "OptionsVisualizer"
+            "OptionsVisualizer",
+            "MultipartMachineGhost"
         };
     }
 }
diff --git a/Content.Server/Machines/EntitySystems/MultipartMachineSystem.cs b/Content.Server/Machines/EntitySystems/MultipartMachineSystem.cs
new file mode 100644 (file)
index 0000000..0490324
--- /dev/null
@@ -0,0 +1,352 @@
+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;
+    }
+}
index b460e96accf8425ba048db3bf23efc560287165f..35d9dbde8fd8f3c8bc1c939b8babdd505ad2d39e 100644 (file)
@@ -13,12 +13,6 @@ namespace Content.Server.ParticleAccelerator.Components;
 [RegisterComponent]
 public sealed partial class ParticleAcceleratorControlBoxComponent : Component
 {
-    /// <summary>
-    /// Whether the PA parts have been correctly arranged to make a functional device.
-    /// </summary>
-    [ViewVariables]
-    public bool Assembled = false;
-
     /// <summary>
     /// Whether the PA is currently set to fire at the console.
     /// Requires <see cref="Assembled"/> to be true.
@@ -40,12 +34,6 @@ public sealed partial class ParticleAcceleratorControlBoxComponent : Component
     [ViewVariables]
     public bool Firing = false;
 
-    /// <summary>
-    /// Block re-entrant rescanning.
-    /// </summary>
-    [ViewVariables(VVAccess.ReadWrite)]
-    public bool CurrentlyRescanning = false;
-
     /// <summary>
     /// Whether the PA is currently firing or charging to fire.
     /// Bounded by <see cref="ParticleAcceleratorPowerState.Standby"/> and <see cref="MaxStrength"/>.
@@ -61,48 +49,6 @@ public sealed partial class ParticleAcceleratorControlBoxComponent : Component
     [ViewVariables]
     public ParticleAcceleratorPowerState MaxStrength = ParticleAcceleratorPowerState.Level2;
 
-    /// <summary>
-    /// The power supply unit of the assembled particle accelerator.
-    /// Implies the existance of a <see cref="ParticleAcceleratorPowerBoxComponent"/> attached to this entity.
-    /// </summary>
-    [ViewVariables]
-    public EntityUid? PowerBox;
-
-    /// <summary>
-    /// Whether the PA is currently firing or charging to fire.
-    /// Implies the existance of a <see cref="ParticleAcceleratorEndCapComponent"/> attached to this entity.
-    /// </summary>
-    [ViewVariables]
-    public EntityUid? EndCap;
-
-    /// <summary>
-    /// Whether the PA is currently firing or charging to fire.
-    /// Implies the existance of a <see cref="ParticleAcceleratorFuelChamberComponent"/> attached to this entity.
-    /// </summary>
-    [ViewVariables]
-    public EntityUid? FuelChamber;
-
-    /// <summary>
-    /// Whether the PA is currently firing or charging to fire.
-    /// Implies the existance of a <see cref="ParticleAcceleratorEmitterComponent"/> attached to this entity.
-    /// </summary>
-    [ViewVariables]
-    public EntityUid? PortEmitter;
-
-    /// <summary>
-    /// Whether the PA is currently firing or charging to fire.
-    /// Implies the existance of a <see cref="ParticleAcceleratorEmitterComponent"/> attached to this entity.
-    /// </summary>
-    [ViewVariables]
-    public EntityUid? ForeEmitter;
-
-    /// <summary>
-    /// Whether the PA is currently firing or charging to fire.
-    /// Implies the existance of a <see cref="ParticleAcceleratorEmitterComponent"/> attached to this entity.
-    /// </summary>
-    [ViewVariables]
-    public EntityUid? StarboardEmitter;
-
     /// <summary>
     /// The amount of power the particle accelerator must be provided with relative to the expected power draw to function.
     /// </summary>
diff --git a/Content.Server/ParticleAccelerator/Components/ParticleAcceleratorPartComponent.cs b/Content.Server/ParticleAccelerator/Components/ParticleAcceleratorPartComponent.cs
deleted file mode 100644 (file)
index 6d2b7b8..0000000
+++ /dev/null
@@ -1,8 +0,0 @@
-namespace Content.Server.ParticleAccelerator.Components;
-
-[RegisterComponent]
-public sealed partial class ParticleAcceleratorPartComponent : Component
-{
-    [ViewVariables]
-    public EntityUid? Master;
-}
index 90a5cb2ea0e2e5e05409585692e223ae48d2df0f..783c1b6174e74d856685768d561d053eca553768 100644 (file)
@@ -1,6 +1,7 @@
 using Content.Server.ParticleAccelerator.Components;
 using Content.Server.Power.Components;
 using Content.Shared.Database;
+using Content.Shared.Machines.Components;
 using Content.Shared.Singularity.Components;
 using Robust.Shared.Utility;
 using System.Diagnostics;
@@ -10,6 +11,8 @@ using Content.Shared.Power;
 using Robust.Shared.Audio;
 using Robust.Shared.Audio.Systems;
 using Robust.Shared.Player;
+using Content.Shared.ParticleAccelerator;
+using Content.Shared.Machines.Events;
 
 namespace Content.Server.ParticleAccelerator.EntitySystems;
 
@@ -20,12 +23,11 @@ public sealed partial class ParticleAcceleratorSystem
 
     private void InitializeControlBoxSystem()
     {
-        SubscribeLocalEvent<ParticleAcceleratorControlBoxComponent, ComponentStartup>(OnComponentStartup);
-        SubscribeLocalEvent<ParticleAcceleratorControlBoxComponent, ComponentShutdown>(OnComponentShutdown);
         SubscribeLocalEvent<ParticleAcceleratorControlBoxComponent, PowerChangedEvent>(OnControlBoxPowerChange);
         SubscribeLocalEvent<ParticleAcceleratorControlBoxComponent, ParticleAcceleratorSetEnableMessage>(OnUISetEnableMessage);
         SubscribeLocalEvent<ParticleAcceleratorControlBoxComponent, ParticleAcceleratorSetPowerStateMessage>(OnUISetPowerMessage);
         SubscribeLocalEvent<ParticleAcceleratorControlBoxComponent, ParticleAcceleratorRescanPartsMessage>(OnUIRescanMessage);
+        SubscribeLocalEvent<ParticleAcceleratorControlBoxComponent, MultipartMachineAssemblyStateChanged>(OnMachineAssembledChanged);
     }
 
     public override void Update(float frameTime)
@@ -40,14 +42,12 @@ public sealed partial class ParticleAcceleratorSystem
     }
 
     [Conditional("DEBUG")]
-    private void EverythingIsWellToFire(ParticleAcceleratorControlBoxComponent controller)
+    private void EverythingIsWellToFire(ParticleAcceleratorControlBoxComponent controller,
+        Entity<MultipartMachineComponent> machine)
     {
         DebugTools.Assert(controller.Powered);
         DebugTools.Assert(controller.SelectedStrength != ParticleAcceleratorPowerState.Standby);
-        DebugTools.Assert(controller.Assembled);
-        DebugTools.Assert(EntityManager.EntityExists(controller.PortEmitter));
-        DebugTools.Assert(EntityManager.EntityExists(controller.ForeEmitter));
-        DebugTools.Assert(EntityManager.EntityExists(controller.StarboardEmitter));
+        DebugTools.Assert(machine.Comp.IsAssembled);
     }
 
     public void Fire(EntityUid uid, TimeSpan curTime, ParticleAcceleratorControlBoxComponent? comp = null)
@@ -58,12 +58,17 @@ public sealed partial class ParticleAcceleratorSystem
         comp.LastFire = curTime;
         comp.NextFire = curTime + comp.ChargeTime;
 
-        EverythingIsWellToFire(comp);
+        if (!TryComp<MultipartMachineComponent>(uid, out var machineComp))
+            return;
+
+        var machine = (uid, machineComp);
+        EverythingIsWellToFire(comp, machine);
 
         var strength = comp.SelectedStrength;
-        FireEmitter(comp.PortEmitter!.Value, strength);
-        FireEmitter(comp.ForeEmitter!.Value, strength);
-        FireEmitter(comp.StarboardEmitter!.Value, strength);
+
+        FireEmitter(_multipartMachine.GetPartEntity(machine, AcceleratorParts.PortEmitter)!.Value, strength);
+        FireEmitter(_multipartMachine.GetPartEntity(machine, AcceleratorParts.ForeEmitter)!.Value, strength);
+        FireEmitter(_multipartMachine.GetPartEntity(machine, AcceleratorParts.StarboardEmitter)!.Value, strength);
     }
 
     public void SwitchOn(EntityUid uid, EntityUid? user = null, ParticleAcceleratorControlBoxComponent? comp = null)
@@ -71,7 +76,7 @@ public sealed partial class ParticleAcceleratorSystem
         if (!Resolve(uid, ref comp))
             return;
 
-        DebugTools.Assert(comp.Assembled);
+        DebugTools.Assert(_multipartMachine.IsAssembled((uid, null)));
 
         if (comp.Enabled || !comp.CanBeEnabled)
             return;
@@ -82,9 +87,11 @@ public sealed partial class ParticleAcceleratorSystem
         comp.Enabled = true;
         UpdatePowerDraw(uid, comp);
 
-        if (!TryComp<PowerConsumerComponent>(comp.PowerBox, out var powerConsumer)
-        || powerConsumer.ReceivedPower >= powerConsumer.DrawRate * ParticleAcceleratorControlBoxComponent.RequiredPowerRatio)
+        if (!TryComp<PowerConsumerComponent>(_multipartMachine.GetPartEntity(uid, AcceleratorParts.PowerBox), out var powerConsumer)
+            || powerConsumer.ReceivedPower >= powerConsumer.DrawRate * ParticleAcceleratorControlBoxComponent.RequiredPowerRatio)
+        {
             PowerOn(uid, comp);
+        }
 
         UpdateUI(uid, comp);
     }
@@ -112,7 +119,7 @@ public sealed partial class ParticleAcceleratorSystem
             return;
 
         DebugTools.Assert(comp.Enabled);
-        DebugTools.Assert(comp.Assembled);
+        DebugTools.Assert(_multipartMachine.IsAssembled((uid, null)));
 
         if (comp.Powered)
             return;
@@ -211,7 +218,10 @@ public sealed partial class ParticleAcceleratorSystem
             return;
         }
 
-        EverythingIsWellToFire(comp);
+        if (!TryComp<MultipartMachineComponent>(uid, out var machine))
+            return;
+
+        EverythingIsWellToFire(comp, (uid, machine));
 
         var curTime = _gameTiming.CurTime;
         comp.LastFire = curTime;
@@ -223,7 +233,8 @@ public sealed partial class ParticleAcceleratorSystem
     {
         if (!Resolve(uid, ref comp))
             return;
-        if (!TryComp<PowerConsumerComponent>(comp.PowerBox, out var powerConsumer))
+
+        if (!TryComp<PowerConsumerComponent>(_multipartMachine.GetPartEntity(uid, AcceleratorParts.PowerBox), out var powerConsumer))
             return;
 
         var powerDraw = comp.BasePowerDraw;
@@ -244,30 +255,35 @@ public sealed partial class ParticleAcceleratorSystem
         var draw = 0f;
         var receive = 0f;
 
-        if (TryComp<PowerConsumerComponent>(comp.PowerBox, out var powerConsumer))
+        if (TryComp<PowerConsumerComponent>(_multipartMachine.GetPartEntity(uid, AcceleratorParts.PowerBox), out var powerConsumer))
         {
             draw = powerConsumer.DrawRate;
             receive = powerConsumer.ReceivedPower;
         }
 
-        _uiSystem.SetUiState(uid,
-            ParticleAcceleratorControlBoxUiKey.Key,
-            new ParticleAcceleratorUIState(
-            comp.Assembled,
+        if (!TryComp<MultipartMachineComponent>(uid, out var machineComp))
+            return;
+
+        var machine = (uid, machineComp);
+
+        var uiState = new ParticleAcceleratorUIState(
+            machineComp.IsAssembled,
             comp.Enabled,
             comp.SelectedStrength,
-            (int) draw,
-            (int) receive,
-            comp.StarboardEmitter != null,
-            comp.ForeEmitter != null,
-            comp.PortEmitter != null,
-            comp.PowerBox != null,
-            comp.FuelChamber != null,
-            comp.EndCap != null,
+            (int)draw,
+            (int)receive,
+            _multipartMachine.HasPart(machine, AcceleratorParts.StarboardEmitter),
+            _multipartMachine.HasPart(machine, AcceleratorParts.ForeEmitter),
+            _multipartMachine.HasPart(machine, AcceleratorParts.PortEmitter),
+            _multipartMachine.HasPart(machine, AcceleratorParts.PowerBox),
+            _multipartMachine.HasPart(machine, AcceleratorParts.FuelChamber),
+            _multipartMachine.HasPart(machine, AcceleratorParts.EndCap),
             comp.InterfaceDisabled,
             comp.MaxStrength,
             comp.StrengthLocked
-        ));
+        );
+
+        _uiSystem.SetUiState(uid, ParticleAcceleratorControlBoxUiKey.Key, uiState);
     }
 
     private void UpdateAppearance(EntityUid uid, ParticleAcceleratorControlBoxComponent? comp = null, AppearanceComponent? appearance = null)
@@ -292,55 +308,58 @@ public sealed partial class ParticleAcceleratorSystem
 
         var state = controller.Powered ? (ParticleAcceleratorVisualState) controller.SelectedStrength : ParticleAcceleratorVisualState.Unpowered;
 
+        if (!TryComp<MultipartMachineComponent>(uid, out var machineComp))
+            return;
+
+        var machine = (uid, machineComp);
+
         // UpdatePartVisualState(ControlBox); (We are the control box)
-        if (controller.FuelChamber.HasValue)
-            _appearanceSystem.SetData(controller.FuelChamber!.Value, ParticleAcceleratorVisuals.VisualState, state);
-        if (controller.PowerBox.HasValue)
-            _appearanceSystem.SetData(controller.PowerBox!.Value, ParticleAcceleratorVisuals.VisualState, state);
-        if (controller.PortEmitter.HasValue)
-            _appearanceSystem.SetData(controller.PortEmitter!.Value, ParticleAcceleratorVisuals.VisualState, state);
-        if (controller.ForeEmitter.HasValue)
-            _appearanceSystem.SetData(controller.ForeEmitter!.Value, ParticleAcceleratorVisuals.VisualState, state);
-        if (controller.StarboardEmitter.HasValue)
-            _appearanceSystem.SetData(controller.StarboardEmitter!.Value, ParticleAcceleratorVisuals.VisualState, state);
+        if (_multipartMachine.TryGetPartEntity(machine, AcceleratorParts.FuelChamber, out var fuelChamber))
+            _appearanceSystem.SetData(fuelChamber.Value, ParticleAcceleratorVisuals.VisualState, state);
+        if (_multipartMachine.TryGetPartEntity(machine, AcceleratorParts.PowerBox, out var powerBox))
+            _appearanceSystem.SetData(powerBox.Value, ParticleAcceleratorVisuals.VisualState, state);
+        if (_multipartMachine.TryGetPartEntity(machine, AcceleratorParts.PortEmitter, out var portEmitter))
+            _appearanceSystem.SetData(portEmitter.Value, ParticleAcceleratorVisuals.VisualState, state);
+        if (_multipartMachine.TryGetPartEntity(machine, AcceleratorParts.ForeEmitter, out var foreEmitter))
+            _appearanceSystem.SetData(foreEmitter.Value, ParticleAcceleratorVisuals.VisualState, state);
+        if (_multipartMachine.TryGetPartEntity(machine, AcceleratorParts.StarboardEmitter, out var starboardEmitter))
+            _appearanceSystem.SetData(starboardEmitter.Value, ParticleAcceleratorVisuals.VisualState, state);
         //no endcap because it has no powerlevel-sprites
     }
 
-    private IEnumerable<EntityUid> AllParts(EntityUid uid, ParticleAcceleratorControlBoxComponent? comp = null)
+    /// <summary>
+    /// Handles when a multipart machine has had some assembled/disassembled state change, or had parts added/removed.
+    /// </summary>
+    /// <param name="ent">Multipart machine entity</param>
+    /// <param name="args">Args for this event</param>
+    private void OnMachineAssembledChanged(Entity<ParticleAcceleratorControlBoxComponent> ent, ref MultipartMachineAssemblyStateChanged args)
     {
-        if (Resolve(uid, ref comp))
+        if (args.IsAssembled)
         {
-            if (comp.FuelChamber.HasValue)
-                yield return comp.FuelChamber.Value;
-            if (comp.EndCap.HasValue)
-                yield return comp.EndCap.Value;
-            if (comp.PowerBox.HasValue)
-                yield return comp.PowerBox.Value;
-            if (comp.PortEmitter.HasValue)
-                yield return comp.PortEmitter.Value;
-            if (comp.ForeEmitter.HasValue)
-                yield return comp.ForeEmitter.Value;
-            if (comp.StarboardEmitter.HasValue)
-                yield return comp.StarboardEmitter.Value;
+            UpdatePowerDraw(ent, ent.Comp);
+            UpdateUI(ent, ent.Comp);
         }
-    }
-
-    private void OnComponentStartup(EntityUid uid, ParticleAcceleratorControlBoxComponent comp, ComponentStartup args)
-    {
-        if (TryComp<ParticleAcceleratorPartComponent>(uid, out var part))
-            part.Master = uid;
-    }
+        else
+        {
+            if (ent.Comp.Powered)
+            {
+                SwitchOff(ent, args.User, ent.Comp);
+            }
+            else
+            {
+                UpdateAppearance(ent, ent.Comp);
+                UpdateUI(ent, ent.Comp);
+            }
 
-    private void OnComponentShutdown(EntityUid uid, ParticleAcceleratorControlBoxComponent comp, ComponentShutdown args)
-    {
-        if (TryComp<ParticleAcceleratorPartComponent>(uid, out var partStatus))
-            partStatus.Master = null;
+            // Because the parts are already removed from the multipart machine, updating the visual appearance won't find any valid entities.
+            // We know which parts have been removed so we can update the visual state to unpowered in a more manual way here.
+            foreach (var (key, part) in args.PartsRemoved)
+            {
+                if (key is AcceleratorParts.EndCap)
+                    continue; // No endcap powerlevel-sprites
 
-        var partQuery = GetEntityQuery<ParticleAcceleratorPartComponent>();
-        foreach (var part in AllParts(uid, comp))
-        {
-            if (partQuery.TryGetComponent(part, out var partData))
-                partData.Master = null;
+                _appearanceSystem.SetData(part, ParticleAcceleratorVisuals.VisualState, ParticleAcceleratorVisualState.Unpowered);
+            }
         }
     }
 
@@ -365,7 +384,7 @@ public sealed partial class ParticleAcceleratorSystem
 
         if (msg.Enabled)
         {
-            if (comp.Assembled)
+            if (_multipartMachine.IsAssembled((uid, null)))
                 SwitchOn(uid, msg.Actor, comp);
         }
         else
@@ -397,9 +416,13 @@ public sealed partial class ParticleAcceleratorSystem
         if (TryComp<ApcPowerReceiverComponent>(uid, out var apcPower) && !apcPower.Powered)
             return;
 
-        RescanParts(uid, msg.Actor, comp);
+        if (!TryComp<MultipartMachineComponent>(uid, out var machineComp))
+            return;
 
-        UpdateUI(uid, comp);
+        // User has requested a manual rescan of the machine, if anything HAS changed that the multipart
+        // machine system has missed then a AssemblyStateChanged event will be raised at the machine.
+        var machine = new Entity<MultipartMachineComponent>(uid, machineComp);
+        _multipartMachine.Rescan(machine, msg.Actor);
     }
 
     public static int GetPANumericalLevel(ParticleAcceleratorPowerState state)
index 78aca15bca47a201d301fcb51feada2ffcd7a2e2..f8a5cc84305f7d6aee7e97329763bf4e3cc373d1 100644 (file)
@@ -1,5 +1,6 @@
 using Content.Server.ParticleAccelerator.Components;
 using Content.Server.Singularity.Components;
+using Content.Shared.ParticleAccelerator.Components;
 using Content.Shared.Projectiles;
 using Content.Shared.Singularity.Components;
 using Robust.Shared.Physics.Components;
diff --git a/Content.Server/ParticleAccelerator/EntitySystems/ParticleAcceleratorSystem.Parts.cs b/Content.Server/ParticleAccelerator/EntitySystems/ParticleAcceleratorSystem.Parts.cs
deleted file mode 100644 (file)
index 829de0a..0000000
+++ /dev/null
@@ -1,177 +0,0 @@
-using System.Diagnostics.CodeAnalysis;
-using Content.Server.ParticleAccelerator.Components;
-using JetBrains.Annotations;
-using Robust.Shared.Map.Components;
-using Robust.Shared.Physics.Events;
-
-namespace Content.Server.ParticleAccelerator.EntitySystems;
-
-[UsedImplicitly]
-public sealed partial class ParticleAcceleratorSystem
-{
-    private void InitializePartSystem()
-    {
-        SubscribeLocalEvent<ParticleAcceleratorPartComponent, ComponentShutdown>(OnComponentShutdown);
-        SubscribeLocalEvent<ParticleAcceleratorPartComponent, MoveEvent>(OnMoveEvent);
-        SubscribeLocalEvent<ParticleAcceleratorPartComponent, PhysicsBodyTypeChangedEvent>(BodyTypeChanged);
-    }
-
-    public void RescanParts(EntityUid uid, EntityUid? user = null, ParticleAcceleratorControlBoxComponent? controller = null)
-    {
-        if (!Resolve(uid, ref controller))
-            return;
-
-        if (controller.CurrentlyRescanning)
-            return;
-
-        var partQuery = GetEntityQuery<ParticleAcceleratorPartComponent>();
-        foreach (var part in AllParts(uid, controller))
-        {
-            if (partQuery.TryGetComponent(part, out var partState))
-                partState.Master = null;
-        }
-
-        controller.Assembled = false;
-        controller.FuelChamber = null;
-        controller.EndCap = null;
-        controller.PowerBox = null;
-        controller.PortEmitter = null;
-        controller.ForeEmitter = null;
-        controller.StarboardEmitter = null;
-
-        var xformQuery = GetEntityQuery<TransformComponent>();
-        if (!xformQuery.TryGetComponent(uid, out var xform) || !xform.Anchored)
-        {
-            SwitchOff(uid, user, controller);
-            return;
-        }
-
-        var gridUid = xform.GridUid;
-        if (gridUid == null || gridUid != xform.ParentUid || !TryComp<MapGridComponent>(gridUid, out var grid))
-        {
-            SwitchOff(uid, user, controller);
-            return;
-        }
-
-        // Find fuel chamber first by scanning cardinals.
-        var fuelQuery = GetEntityQuery<ParticleAcceleratorFuelChamberComponent>();
-        foreach (var adjacent in _mapSystem.GetCardinalNeighborCells(gridUid.Value, grid, xform.Coordinates))
-        {
-            if (fuelQuery.HasComponent(adjacent)
-                && partQuery.TryGetComponent(adjacent, out var partState)
-                && partState.Master == null)
-            {
-                controller.FuelChamber = adjacent;
-                break;
-            }
-        }
-
-        if (controller.FuelChamber == null)
-        {
-            SwitchOff(uid, user, controller);
-            return;
-        }
-
-        // When we call SetLocalRotation down there to rotate the control box,
-        // that ends up re-entrantly calling RescanParts() through the move event.
-        // You'll have to take my word for it that that breaks everything, yeah?
-        controller.CurrentlyRescanning = true;
-
-        // Automatically rotate the control box sprite to face the fuel chamber
-        var fuelXform = xformQuery.GetComponent(controller.FuelChamber!.Value);
-        var fuelDir = (fuelXform.LocalPosition - xform.LocalPosition).GetDir();
-        _transformSystem.SetLocalRotation(uid, fuelDir.ToAngle(), xform);
-
-        // Calculate offsets for each of the parts of the PA.
-        // These are all done relative to the fuel chamber BC that is basically the center of the machine.
-        var rotation = fuelXform.LocalRotation;
-        var offsetVect = rotation.GetCardinalDir().ToIntVec();
-        var orthoOffsetVect = new Vector2i(-offsetVect.Y, offsetVect.X);
-
-        var positionFuelChamber = _mapSystem.TileIndicesFor(gridUid!.Value, grid, fuelXform.Coordinates); //   n   // n: End Cap
-        var positionEndCap = positionFuelChamber - offsetVect;                                            //  CF   // C: Control Box, F: Fuel Chamber
-        var positionPowerBox = positionFuelChamber + offsetVect;                                          //   P   // P: Power Box
-        var positionPortEmitter = positionFuelChamber + offsetVect * 2 + orthoOffsetVect;                 //  EEE  // E: Emitter (Starboard, Fore, Port)
-        var positionForeEmitter = positionFuelChamber + offsetVect * 2;
-        var positionStarboardEmitter = positionFuelChamber + offsetVect * 2 - orthoOffsetVect;
-
-        ScanPart<ParticleAcceleratorEndCapComponent>(gridUid.Value, positionEndCap, rotation, out controller.EndCap, out _, grid);
-        ScanPart<ParticleAcceleratorPowerBoxComponent>(gridUid.Value, positionPowerBox, rotation, out controller.PowerBox, out _, grid);
-
-        if (!ScanPart<ParticleAcceleratorEmitterComponent>(gridUid.Value, positionPortEmitter, rotation, out controller.PortEmitter, out var portEmitter, grid)
-        || portEmitter.Type != ParticleAcceleratorEmitterType.Port)
-            controller.PortEmitter = null;
-
-        if (!ScanPart<ParticleAcceleratorEmitterComponent>(gridUid.Value, positionForeEmitter, rotation, out controller.ForeEmitter, out var foreEmitter, grid)
-        || foreEmitter.Type != ParticleAcceleratorEmitterType.Fore)
-            controller.ForeEmitter = null;
-
-        if (!ScanPart<ParticleAcceleratorEmitterComponent>(gridUid.Value, positionStarboardEmitter, rotation, out controller.StarboardEmitter, out var starboardEmitter, grid)
-        || starboardEmitter.Type != ParticleAcceleratorEmitterType.Starboard)
-            controller.StarboardEmitter = null;
-
-        controller.Assembled =
-            controller.FuelChamber.HasValue
-            && controller.EndCap.HasValue
-            && controller.PowerBox.HasValue
-            && controller.PortEmitter.HasValue
-            && controller.ForeEmitter.HasValue
-            && controller.StarboardEmitter.HasValue;
-
-        foreach (var part in AllParts(uid, controller))
-        {
-            if (partQuery.TryGetComponent(part, out var partState))
-                partState.Master = uid;
-        }
-
-        controller.CurrentlyRescanning = false;
-
-        UpdatePowerDraw(uid, controller);
-        UpdateUI(uid, controller);
-    }
-
-    private bool ScanPart<T>(EntityUid uid, Vector2i coordinates, Angle? rotation, [NotNullWhen(true)] out EntityUid? part, [NotNullWhen(true)] out T? comp, MapGridComponent? grid = null)
-        where T : IComponent
-    {
-        if (!Resolve(uid, ref grid))
-        {
-            part = null;
-            comp = default;
-            return false;
-        }
-
-        var compQuery = GetEntityQuery<T>();
-        foreach (var entity in _mapSystem.GetAnchoredEntities(uid, grid, coordinates))
-        {
-            if (compQuery.TryGetComponent(entity, out comp)
-            && TryComp<ParticleAcceleratorPartComponent>(entity, out var partState) && partState.Master == null
-            && (rotation == null || Transform(entity).LocalRotation.EqualsApprox(rotation!.Value.Theta)))
-            {
-                part = entity;
-                return true;
-            }
-        }
-
-        part = null;
-        comp = default;
-        return false;
-    }
-
-    private void OnComponentShutdown(EntityUid uid, ParticleAcceleratorPartComponent comp, ComponentShutdown args)
-    {
-        if (Exists(comp.Master))
-            RescanParts(comp.Master!.Value);
-    }
-
-    private void BodyTypeChanged(EntityUid uid, ParticleAcceleratorPartComponent comp, ref PhysicsBodyTypeChangedEvent args)
-    {
-        if (Exists(comp.Master))
-            RescanParts(comp.Master!.Value);
-    }
-
-    private void OnMoveEvent(EntityUid uid, ParticleAcceleratorPartComponent comp, ref MoveEvent args)
-    {
-        if (Exists(comp.Master))
-            RescanParts(comp.Master!.Value);
-    }
-}
index be961ba5fd8e016c89ce945bbb2058cda35cdb30..677228c15b6c91dd505b67f4a52758cd438bf5aa 100644 (file)
@@ -1,5 +1,7 @@
-using Content.Server.ParticleAccelerator.Components;
+using Content.Server.ParticleAccelerator.Components;
 using Content.Server.Power.EntitySystems;
+using Content.Shared.Machines.Components;
+using Content.Shared.ParticleAccelerator.Components;
 
 namespace Content.Server.ParticleAccelerator.EntitySystems;
 
@@ -12,7 +14,7 @@ public sealed partial class ParticleAcceleratorSystem
 
     private void PowerBoxReceivedChanged(EntityUid uid, ParticleAcceleratorPowerBoxComponent component, ref PowerConsumerReceivedChanged args)
     {
-        if (!TryComp<ParticleAcceleratorPartComponent>(uid, out var part))
+        if (!TryComp<MultipartMachinePartComponent>(uid, out var part))
             return;
         if (!TryComp<ParticleAcceleratorControlBoxComponent>(part.Master, out var controller))
             return;
index e9b62bc4a80a445459613d441dd1561f9ba0d414..d9fb84bad3bb6df3defb0caa173b02eccab84755 100644 (file)
@@ -1,6 +1,7 @@
 using Content.Server.Administration.Logs;
 using Content.Server.Chat.Managers;
 using Content.Server.Projectiles;
+using Content.Server.Machines.EntitySystems;
 using Robust.Shared.Physics.Systems;
 using Robust.Shared.Timing;
 using Robust.Server.GameObjects;
@@ -19,13 +20,12 @@ public sealed partial class ParticleAcceleratorSystem : EntitySystem
     [Dependency] private readonly SharedPhysicsSystem _physicsSystem = default!;
     [Dependency] private readonly SharedTransformSystem _transformSystem = default!;
     [Dependency] private readonly UserInterfaceSystem _uiSystem = default!;
-    [Dependency] private readonly MapSystem _mapSystem = default!;
+    [Dependency] private readonly MultipartMachineSystem _multipartMachine = default!;
 
     public override void Initialize()
     {
         base.Initialize();
         InitializeControlBoxSystem();
-        InitializePartSystem();
         InitializePowerBoxSystem();
     }
 }
index 40a15d2bc507d8c03379fd724daaba9517892e2b..98d2efa7f52c1f7485bdea2625f7810107d21bbb 100644 (file)
@@ -1,3 +1,4 @@
+using Content.Server.Machines.EntitySystems;
 using Content.Server.ParticleAccelerator.Components;
 using Content.Server.ParticleAccelerator.EntitySystems;
 using Content.Server.Wires;
@@ -38,10 +39,11 @@ public sealed partial class ParticleAcceleratorPowerWireAction : ComponentWireAc
     public override void Pulse(EntityUid user, Wire wire, ParticleAcceleratorControlBoxComponent controller)
     {
         var paSystem = EntityManager.System<ParticleAcceleratorSystem>();
+        var multipartMachine = EntityManager.System<MultipartMachineSystem>();
 
         if (controller.Enabled)
             paSystem.SwitchOff(wire.Owner, user, controller);
-        else if (controller.Assembled)
+        else if (multipartMachine.IsAssembled((wire.Owner, null)))
             paSystem.SwitchOn(wire.Owner, user, controller);
     }
 }
index e63a7467e0e34749158f1f8ac710ea509a186df5..a373c0da5ea63c29ec687ba7fd6f5854325da9ff 100644 (file)
@@ -1,9 +1,11 @@
 using Content.Server.Administration;
+using Content.Server.Machines.EntitySystems;
 using Content.Server.ParticleAccelerator.Components;
 using Content.Server.ParticleAccelerator.EntitySystems;
 using Content.Server.Singularity.Components;
 using Content.Server.Singularity.EntitySystems;
 using Content.Shared.Administration;
+using Content.Shared.Machines.Components;
 using Content.Shared.Singularity.Components;
 using Robust.Shared.Console;
 
@@ -45,12 +47,15 @@ namespace Content.Server.Singularity
             }
 
             // Setup PA
+            var multipartMachineManager = entitySystemManager.GetEntitySystem<MultipartMachineSystem>();
             var paSystem = entitySystemManager.GetEntitySystem<ParticleAcceleratorSystem>();
             var paQuery = entityManager.EntityQueryEnumerator<ParticleAcceleratorControlBoxComponent>();
             while (paQuery.MoveNext(out var paId, out var paControl))
             {
-                paSystem.RescanParts(paId, controller: paControl);
-                if (!paControl.Assembled)
+                if (!entityManager.TryGetComponent<MultipartMachineComponent>(paId, out var machine))
+                    continue;
+
+                if (!multipartMachineManager.Rescan((paId, machine)))
                     continue;
 
                 paSystem.SetStrength(paId, ParticleAcceleratorPowerState.Level0, comp: paControl);
diff --git a/Content.Shared/Machines/Components/MultipartMachineComponent.cs b/Content.Shared/Machines/Components/MultipartMachineComponent.cs
new file mode 100644 (file)
index 0000000..f7a118f
--- /dev/null
@@ -0,0 +1,103 @@
+using Content.Shared.Machines.EntitySystems;
+using Robust.Shared.GameStates;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
+using Robust.Shared.Utility;
+
+namespace Content.Shared.Machines.Components;
+
+/// <summary>
+/// Marks an entity as being the owner of a multipart machine.
+/// </summary>
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(raiseAfterAutoHandleState: true)]
+[Access(typeof(SharedMultipartMachineSystem))]
+public sealed partial class MultipartMachineComponent : Component
+{
+    /// <summary>
+    /// Dictionary of Enum values to specific parts of this machine.
+    /// Each key can be specified as 'enum.<EnumName>.<EnumValue>` in Yaml.
+    /// </summary>
+    [DataField, AutoNetworkedField]
+    public Dictionary<Enum, MachinePart> Parts = [];
+
+    /// <summary>
+    /// Whether this multipart machine is assembled or not.
+    /// Optional parts are not taken into account.
+    /// </summary>
+    [DataField, AutoNetworkedField]
+    public bool IsAssembled = false;
+
+    /// <summary>
+    /// Flag for whether the client side system is allowed to show
+    /// ghosts of missing machine parts.
+    /// Controlled/Used by the client side.
+    /// </summary>
+    public List<EntityUid> Ghosts = [];
+}
+
+[DataDefinition]
+[Serializable, NetSerializable]
+public sealed partial class MachinePart
+{
+    /// <summary>
+    /// Component type that is expected for this part to have
+    /// to be considered a "Part" of the machine.
+    /// </summary>
+    [DataField(required: true, customTypeSerializer: typeof(ComponentNameSerializer))]
+    public string Component = "";
+
+    /// <summary>
+    /// Expected offset to find this machine at.
+    /// </summary>
+    [DataField(required: true)]
+    public Vector2i Offset;
+
+    /// <summary>
+    /// Whether this part is required for the machine to be
+    /// considered "assembled", or is considered an optional extra.
+    /// </summary>
+    [DataField]
+    public bool Optional = false;
+
+    /// <summary>
+    /// ID of prototype, used to show sprite and description of part, when user examines the machine and there
+    /// is no matched entity. Can reference dummy entities to give more detailed descriptions.
+    /// </summary>
+    [DataField]
+    public EntProtoId? GhostProto = null;
+
+    /// <summary>
+    /// Expected rotation for this machine to have.
+    /// </summary>
+    [DataField]
+    public Angle Rotation = Angle.Zero;
+
+    /// <summary>
+    /// Network entity, used to inform clients and update their side of the component
+    /// locally.
+    /// Use the Entity attribute if you wish to get which entity is actually bound to this part.
+    /// </summary>
+    public NetEntity? NetEntity = null;
+
+    /// <summary>
+    /// Entity associated with this part.
+    /// Not null when an entity is successfully matched to the part and null otherwise.
+    /// </summary>
+    [DataField, NonSerialized]
+    public EntityUid? Entity = null;
+
+    /// <summary>
+    /// Expected graph for this part to use as part of its construction.
+    /// </summary>
+    [DataField]
+    public EntProtoId Graph;
+
+    /// <summary>
+    /// Expected node for this part to be in, on the graph.
+    /// Used to determine when a construct-able object has been
+    /// assembled or disassembled.
+    /// </summary>
+    [DataField]
+    public string ExpectedNode;
+}
diff --git a/Content.Shared/Machines/Components/MultipartMachinePartComponent.cs b/Content.Shared/Machines/Components/MultipartMachinePartComponent.cs
new file mode 100644 (file)
index 0000000..88eff5c
--- /dev/null
@@ -0,0 +1,17 @@
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Machines.Components;
+
+/// <summary>
+/// Component for marking entities as part of a multipart machine.
+/// </summary>
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
+public sealed partial class MultipartMachinePartComponent : Component
+{
+    /// <summary>
+    /// Links to the entity which holds the MultipartMachineComponent.
+    /// Useful so that entities that know which machine they are a part of.
+    /// </summary>
+    [DataField, AutoNetworkedField]
+    public EntityUid? Master = null;
+}
diff --git a/Content.Shared/Machines/EntitySystems/SharedMultipartMachineSystem.cs b/Content.Shared/Machines/EntitySystems/SharedMultipartMachineSystem.cs
new file mode 100644 (file)
index 0000000..7185d60
--- /dev/null
@@ -0,0 +1,115 @@
+using System.Diagnostics.CodeAnalysis;
+using Content.Shared.Machines.Components;
+
+namespace Content.Shared.Machines.EntitySystems;
+
+/// <summary>
+/// Shared handling of multipart machines.
+/// </summary>
+public abstract class SharedMultipartMachineSystem : EntitySystem
+{
+    protected EntityQuery<TransformComponent> XformQuery;
+
+    public override void Initialize()
+    {
+        base.Initialize();
+
+        XformQuery = GetEntityQuery<TransformComponent>();
+    }
+
+    /// <summary>
+    /// Returns whether each non-optional part of the machine has a matched entity
+    /// </summary>
+    /// <param name="ent">Entity to check the assembled state of.</param>
+    /// <returns>True if all non-optional parts have a matching entity, false otherwise.</returns>
+    public bool IsAssembled(Entity<MultipartMachineComponent?> ent)
+    {
+        if (!Resolve(ent, ref ent.Comp))
+            return false;
+
+        foreach (var part in ent.Comp.Parts.Values)
+        {
+            if (!part.Entity.HasValue && !part.Optional)
+                return false;
+        }
+
+        return true;
+    }
+
+    /// <summary>
+    /// Returns whether a machine has a specifed EntityUid bound to one of its parts.
+    /// </summary>
+    /// <param name="machine">Entity, which might have a multpart machine attached, to use for the query.</param>
+    /// <param name="entity">EntityUid to search for.</param>
+    /// <returns>True if any part has the specified EntityUid, false otherwise.</returns>
+    public bool HasPartEntity(Entity<MultipartMachineComponent?> machine, EntityUid entity)
+    {
+        if (!Resolve(machine, ref machine.Comp))
+            return false;
+
+        foreach (var part in machine.Comp.Parts.Values)
+        {
+            if (part.Entity.HasValue && part.Entity.Value == entity)
+                return true;
+        }
+
+        return false;
+    }
+
+    /// <summary>
+    /// Get the EntityUid for the entity bound to a specific part, if one exists.
+    /// </summary>
+    /// <param name="ent">Entity, which might have a multipart machine attached, to use for the query.</param>
+    /// <param name="part">Enum value for the part to find, must match the value specified in YAML.</param>
+    /// <returns>May contain the resolved EntityUid for the specified part, null otherwise.</returns>
+    public EntityUid? GetPartEntity(Entity<MultipartMachineComponent?> ent, Enum part)
+    {
+        if (!TryGetPartEntity(ent, part, out var entity))
+            return null;
+
+        return entity;
+    }
+
+    /// <summary>
+    /// Get the EntityUid for the entity bound to a specific part, if one exists.
+    /// </summary>
+    /// <param name="ent">Entity, which might have a multipart machine attached, to use for the query.</param>
+    /// <param name="part">Enum for the part to find, must match the value specified in YAML.</param>
+    /// <param name="entity">Out var which may contain the matched EntityUid for the specified part.</param>
+    /// <returns>True if the part is found and has a matched entity, false otherwise.</returns>
+    public bool TryGetPartEntity(
+        Entity<MultipartMachineComponent?> ent,
+        Enum part,
+        [NotNullWhen(true)] out EntityUid? entity
+    )
+    {
+        entity = null;
+        if (!Resolve(ent, ref ent.Comp))
+            return false;
+
+        if (ent.Comp.Parts.TryGetValue(part, out var value) && value.Entity.HasValue)
+        {
+            entity = value.Entity.Value;
+            return true;
+        }
+
+        return false;
+    }
+
+    /// <summary>
+    /// Check if a machine has an entity bound to a specific part
+    /// </summary>
+    /// <param name="ent">Entity, which might have a multipart machine attached, to use for the query.</param>
+    /// <param name="part">Enum for the part to find.</param>
+    /// <returns>True if the specific part has a entity bound to it, false otherwise.</returns>
+    public bool HasPart(Entity<MultipartMachineComponent?> ent, Enum part)
+    {
+        if (!Resolve(ent, ref ent.Comp))
+            return false;
+
+        if (!ent.Comp.Parts.TryGetValue(part, out var value))
+            return false;
+
+        return value.Entity != null;
+    }
+}
diff --git a/Content.Shared/Machines/Events/MultipartMachineEvents.cs b/Content.Shared/Machines/Events/MultipartMachineEvents.cs
new file mode 100644 (file)
index 0000000..8bf19e9
--- /dev/null
@@ -0,0 +1,21 @@
+namespace Content.Shared.Machines.Events;
+
+/// <summary>
+/// This event is raised when the assembled state of a Multipart machine changes.
+/// This includes when optional parts are found, parts become unanchored, or move
+/// within a construction graph.
+/// </summary>
+/// <param name="Entity">Entity that is bound to the multipart machine.</param>
+/// <param name="IsAssembled">Assembled state of the machine.</param>
+/// <param name="User">Optional user that may have caused the assembly state to change.</param>
+/// <param name="PartsAdded">Dictionary of keys to entities of parts that have been added to this machine.</param>
+/// <param name="PartsRemoved">Dictionary of keys to entities of parts that have been removed from this machine.</param>
+[ByRefEvent]
+public record struct MultipartMachineAssemblyStateChanged(
+    EntityUid Entity,
+    bool IsAssembled,
+    EntityUid? User,
+    Dictionary<Enum, EntityUid> PartsAdded,
+    Dictionary<Enum, EntityUid> PartsRemoved)
+{
+}
diff --git a/Content.Shared/ParticleAccelerator/AcceleratorParts.cs b/Content.Shared/ParticleAccelerator/AcceleratorParts.cs
new file mode 100644 (file)
index 0000000..2364891
--- /dev/null
@@ -0,0 +1,16 @@
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.ParticleAccelerator;
+
+[Serializable, NetSerializable]
+public enum AcceleratorParts : byte
+{
+    EndCap,
+    FuelChamber,
+    PowerBox,
+    PortEmitter,
+    ForeEmitter,
+    StarboardEmitter
+};
+
+
similarity index 90%
rename from Content.Server/ParticleAccelerator/Components/ParticleAcceleratorEmitterComponent.cs
rename to Content.Shared/ParticleAccelerator/Components/ParticleAcceleratorEmitterComponent.cs
index 7697644a3bce5f70d2915e14333cabab803cb993..9b52a73522bc9a0eb4642d9e71f088bcdc690794 100644 (file)
@@ -1,6 +1,6 @@
 using Robust.Shared.Prototypes;
 
-namespace Content.Server.ParticleAccelerator.Components;
+namespace Content.Shared.ParticleAccelerator.Components;
 
 [RegisterComponent]
 public sealed partial class ParticleAcceleratorEmitterComponent : Component
similarity index 62%
rename from Content.Server/ParticleAccelerator/Components/ParticleAcceleratorEndCapComponent.cs
rename to Content.Shared/ParticleAccelerator/Components/ParticleAcceleratorEndCapComponent.cs
index 9c111d1ea9709044700833804819269e2c8d6251..20615321d17b0028a4f07a0f394da2f1316b77d5 100644 (file)
@@ -1,4 +1,4 @@
-namespace Content.Server.ParticleAccelerator.Components;
+namespace Content.Shared.ParticleAccelerator.Components;
 
 [RegisterComponent]
 public sealed partial class ParticleAcceleratorEndCapComponent : Component
similarity index 63%
rename from Content.Server/ParticleAccelerator/Components/ParticleAcceleratorFuelChamberComponent.cs
rename to Content.Shared/ParticleAccelerator/Components/ParticleAcceleratorFuelChamberComponent.cs
index 9029e880d909bc2733ff50979d020e00f16c02e4..9e0da68b960dabe8449bb2aa30373bae6308d762 100644 (file)
@@ -1,4 +1,4 @@
-namespace Content.Server.ParticleAccelerator.Components;
+namespace Content.Shared.ParticleAccelerator.Components;
 
 [RegisterComponent]
 public sealed partial class ParticleAcceleratorFuelChamberComponent : Component
similarity index 62%
rename from Content.Server/ParticleAccelerator/Components/ParticleAcceleratorPowerBoxComponent.cs
rename to Content.Shared/ParticleAccelerator/Components/ParticleAcceleratorPowerBoxComponent.cs
index f8ad4ef5d2581ac5a7baf3d81c20378a3343598f..3da6b76f2f3b20fa8378e6cd555dde6411806510 100644 (file)
@@ -1,4 +1,4 @@
-namespace Content.Server.ParticleAccelerator.Components;
+namespace Content.Shared.ParticleAccelerator.Components;
 
 [RegisterComponent]
 public sealed partial class ParticleAcceleratorPowerBoxComponent : Component
diff --git a/Resources/Prototypes/Entities/Structures/Machines/multipart.yml b/Resources/Prototypes/Entities/Structures/Machines/multipart.yml
new file mode 100644 (file)
index 0000000..fecc4f2
--- /dev/null
@@ -0,0 +1,18 @@
+# Spawn on client side when users examine a multipart machine
+# If a sprite is given then the default component's value will be overriden
+- type: entity
+  id: MultipartMachineGhost
+  categories: [ HideSpawnMenu ]
+  components:
+    - type: MultipartMachineGhost
+    - type: Sprite
+      sprite: Markers/cross.rsi
+      layers:
+        - state: green
+      color: "#FFFFFF80"
+    - type: TimedDespawn
+      lifetime: 5
+    - type: Clickable
+    - type: Tag
+      tags:
+        - HideContextMenu
index d5b7e3667d740999f4fb2dc62a5e95810ba3be57..b19e2c8ebb3b187f54824b73b1f54e56da6a0156 100644 (file)
@@ -43,7 +43,6 @@
       map: [ "enum.ParticleAcceleratorVisualLayers.Unlit" ]
       shader: unshaded
       visible: false
-  - type: ParticleAcceleratorPart
   - type: ParticleAcceleratorPartVisuals
     stateBase: unlit
   - type: Construction
index ae3fc9677429f68b09f2dd64e72af0f277f6b166..7b3aa0c693459556437a1a557fb032fe8eecfee1 100644 (file)
     layoutId: ParticleAccelerator
   - type: AccessReader
     access: [["Engineering"]]
+  - type: MultipartMachine
+    parts:
+      enum.AcceleratorParts.EndCap:
+        component: ParticleAcceleratorEndCap
+        offset: 1, -1
+        rotation: -90
+        ghostProto: ParticleAcceleratorEndCap
+        graph: ParticleAcceleratorEndCap
+        expectedNode: completed
+      enum.AcceleratorParts.FuelChamber:
+        component: ParticleAcceleratorFuelChamber
+        offset: 0, -1
+        rotation: -90
+        ghostProto: ParticleAcceleratorFuelChamber
+        graph: ParticleAcceleratorFuelChamber
+        expectedNode: completed
+      enum.AcceleratorParts.PowerBox:
+        component: ParticleAcceleratorPowerBox
+        offset: -1, -1
+        rotation: -90
+        ghostProto: ParticleAcceleratorPowerBox
+        graph: ParticleAcceleratorPowerBox
+        expectedNode: completed
+      enum.AcceleratorParts.PortEmitter:
+        component: ParticleAcceleratorEmitter
+        offset: -2, -2
+        rotation: -90
+        ghostProto: ParticleAcceleratorEmitterPort
+        graph: ParticleAcceleratorEmitterPort
+        expectedNode: completed
+      enum.AcceleratorParts.ForeEmitter:
+        component: ParticleAcceleratorEmitter
+        offset: -2, -1
+        rotation: -90
+        ghostProto: ParticleAcceleratorEmitterFore
+        graph: ParticleAcceleratorEmitterFore
+        expectedNode: completed
+      enum.AcceleratorParts.StarboardEmitter:
+        component: ParticleAcceleratorEmitter
+        offset: -2, 0
+        rotation: -90
+        ghostProto: ParticleAcceleratorEmitterStarboard
+        graph: ParticleAcceleratorEmitterStarboard
+        expectedNode: completed
 
 # Unfinished
 
index 43b4d5f9efa604154851ad1ee6ba3e093ad6dd8c..38d0d0b6cdab52d2e89a96b513246659d504d0eb 100644 (file)
@@ -10,6 +10,7 @@
     emitterType: Port
   - type: Construction
     graph: ParticleAcceleratorEmitterPort
+  - type: MultipartMachinePart
 
 - type: entity
   parent: ParticleAcceleratorFinishedPart
@@ -23,6 +24,7 @@
     emitterType: Fore
   - type: Construction
     graph: ParticleAcceleratorEmitterFore
+  - type: MultipartMachinePart
 
 - type: entity
   parent: ParticleAcceleratorFinishedPart
@@ -36,6 +38,7 @@
     emitterType: Starboard
   - type: Construction
     graph: ParticleAcceleratorEmitterStarboard
+  - type: MultipartMachinePart
 
 # Unfinished
 
@@ -50,6 +53,7 @@
     sprite: Structures/Power/Generation/PA/emitter_port.rsi
   - type: Construction
     graph: ParticleAcceleratorEmitterPort
+  - type: MultipartMachinePart
 
 - type: entity
   parent: ParticleAcceleratorUnfinishedBase
@@ -62,6 +66,7 @@
     sprite: Structures/Power/Generation/PA/emitter_fore.rsi
   - type: Construction
     graph: ParticleAcceleratorEmitterFore
+  - type: MultipartMachinePart
 
 - type: entity
   parent: ParticleAcceleratorUnfinishedBase
@@ -74,3 +79,4 @@
     sprite: Structures/Power/Generation/PA/emitter_starboard.rsi
   - type: Construction
     graph: ParticleAcceleratorEmitterStarboard
+  - type: MultipartMachinePart
index 80b0240d89566356b2fb3d5a49603f45eb29bc5d..226ef915a73fe40f61358d9ec43446af5406baf3 100644 (file)
@@ -12,6 +12,7 @@
   - type: ParticleAcceleratorEndCap
   - type: Construction
     graph: ParticleAcceleratorEndCap
+  - type: MultipartMachinePart
 
 # Unfinished
 
@@ -26,3 +27,4 @@
     sprite: Structures/Power/Generation/PA/end_cap.rsi
   - type: Construction
     graph: ParticleAcceleratorEndCap
+  - type: MultipartMachinePart
index 868e9cf8f962f20ed0db130870146892dd34495c..37fba25212760bbc6a7c4ee296261435f0bc51f4 100644 (file)
@@ -9,6 +9,7 @@
   - type: ParticleAcceleratorFuelChamber
   - type: Construction
     graph: ParticleAcceleratorFuelChamber
+  - type: MultipartMachinePart
 
 # Unfinished
 
@@ -23,3 +24,4 @@
     sprite: Structures/Power/Generation/PA/fuel_chamber.rsi
   - type: Construction
     graph: ParticleAcceleratorFuelChamber
+  - type: MultipartMachinePart
index c9a25d62ffcc0b17b0975958cb79e5e968399136..9dbff7fa42925e4daf9ef58670bfec3fada61954 100644 (file)
@@ -17,6 +17,7 @@
         nodeGroupID: MVPower
   - type: Construction
     graph: ParticleAcceleratorPowerBox
+  - type: MultipartMachinePart
 
 - type: entity
   parent: ParticleAcceleratorUnfinishedBase
@@ -29,3 +30,4 @@
     sprite: Structures/Power/Generation/PA/power_box.rsi
   - type: Construction
     graph: ParticleAcceleratorPowerBox
+  - type: MultipartMachinePart