diff --git a/Content.Client/Machines/Components/MultipartMachineGhostComponent.cs b/Content.Client/Machines/Components/MultipartMachineGhostComponent.cs
new file mode 100644
index 0000000000..8fe4f3d386
--- /dev/null
+++ b/Content.Client/Machines/Components/MultipartMachineGhostComponent.cs
@@ -0,0 +1,14 @@
+namespace Content.Client.Machines.Components;
+
+///
+/// Component attached to all multipart machine ghosts
+/// Intended for client side usage only, but used on prototypes.
+///
+[RegisterComponent]
+public sealed partial class MultipartMachineGhostComponent : Component
+{
+ ///
+ /// Machine this particular ghost is linked to.
+ ///
+ public EntityUid? LinkedMachine = null;
+}
diff --git a/Content.Client/Machines/EntitySystems/MultipartMachineSystem.cs b/Content.Client/Machines/EntitySystems/MultipartMachineSystem.cs
new file mode 100644
index 0000000000..4919a5e8f2
--- /dev/null
+++ b/Content.Client/Machines/EntitySystems/MultipartMachineSystem.cs
@@ -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;
+
+///
+/// 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.
+///
+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(OnMachineExamined);
+ SubscribeLocalEvent(OnHandleState);
+ SubscribeLocalEvent(OnGhostDespawned);
+ }
+
+ ///
+ /// 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.
+ ///
+ /// Entity/Component that has been inspected.
+ /// Args for the event.
+ private void OnMachineExamined(Entity 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(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(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 ent, ref AfterAutoHandleStateEvent args)
+ {
+ foreach (var part in ent.Comp.Parts.Values)
+ {
+ part.Entity = part.NetEntity.HasValue ? EnsureEntity(part.NetEntity.Value, ent) : null;
+ }
+ }
+
+ ///
+ /// 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.
+ ///
+ /// Ghost entity that has been despawned.
+ /// Args for the event.
+ private void OnGhostDespawned(Entity ent, ref TimedDespawnEvent args)
+ {
+ if (!TryComp(ent.Comp.LinkedMachine, out var machine))
+ return;
+
+ machine.Ghosts.Remove(ent);
+ }
+}
diff --git a/Content.Client/ParticleAccelerator/UI/ParticleAcceleratorControlMenu.xaml b/Content.Client/ParticleAccelerator/UI/ParticleAcceleratorControlMenu.xaml
index 7cef7d58b6..d05262f72d 100644
--- a/Content.Client/ParticleAccelerator/UI/ParticleAcceleratorControlMenu.xaml
+++ b/Content.Client/ParticleAccelerator/UI/ParticleAcceleratorControlMenu.xaml
@@ -127,7 +127,7 @@
-
+
diff --git a/Content.Client/ParticleAccelerator/UI/ParticleAcceleratorControlMenu.xaml.cs b/Content.Client/ParticleAccelerator/UI/ParticleAcceleratorControlMenu.xaml.cs
index 8b21e7d94b..cc5016c4a7 100644
--- a/Content.Client/ParticleAccelerator/UI/ParticleAcceleratorControlMenu.xaml.cs
+++ b/Content.Client/ParticleAccelerator/UI/ParticleAcceleratorControlMenu.xaml.cs
@@ -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().GetResource($"/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;
}
+
+ ///
+ /// Adds/Removes the shading to the part in the control menu based on the
+ /// input state.
+ ///
+ /// True if the part exists, false otherwise
+ private void SetVisible(bool state)
+ {
+ if (state)
+ {
+ _base.ShaderOverride = null;
+ _base.ModulateSelfOverride = null;
+ }
+ else
+ {
+ _base.ShaderOverride = _greyScaleShader;
+ _base.ModulateSelfOverride = new Color(127, 127, 127);
+ }
+ }
}
diff --git a/Content.Server/Construction/ConstructionSystem.Graph.cs b/Content.Server/Construction/ConstructionSystem.Graph.cs
index 0027b941f8..7d4dd6153d 100644
--- a/Content.Server/Construction/ConstructionSystem.Graph.cs
+++ b/Content.Server/Construction/ConstructionSystem.Graph.cs
@@ -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
/// An optional user entity, for actions.
/// The entity prototype identifier for the new entity.
/// The construction component of the target entity. Will be resolved if null.
+ /// The previous node, if any, this graph was on before changing entity.
/// The metadata component of the target entity. Will be resolved if null.
/// The transform component of the target entity. Will be resolved if null.
/// The container manager component of the target entity. Will be resolved if null,
@@ -288,6 +289,7 @@ namespace Content.Server.Construction
/// The new entity, or null if the method did not succeed.
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;
}
}
+
+ ///
+ /// 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.
+ ///
+ /// Construction graph for this entity.
+ /// New node that has become active.
+ /// Previous node that was active on the graph.
+ [ByRefEvent]
+ public record struct AfterConstructionChangeEntityEvent(string Graph, string CurrentNode, string? PreviousNode)
+ {
+ }
}
diff --git a/Content.Server/Entry/IgnoredComponents.cs b/Content.Server/Entry/IgnoredComponents.cs
index 04c4566227..58264e14ad 100644
--- a/Content.Server/Entry/IgnoredComponents.cs
+++ b/Content.Server/Entry/IgnoredComponents.cs
@@ -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
index 0000000000..04903241e3
--- /dev/null
+++ b/Content.Server/Machines/EntitySystems/MultipartMachineSystem.cs
@@ -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;
+
+///
+/// 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;
+ }
+}
diff --git a/Content.Server/ParticleAccelerator/Components/ParticleAcceleratorControlBoxComponent.cs b/Content.Server/ParticleAccelerator/Components/ParticleAcceleratorControlBoxComponent.cs
index b460e96acc..35d9dbde8f 100644
--- a/Content.Server/ParticleAccelerator/Components/ParticleAcceleratorControlBoxComponent.cs
+++ b/Content.Server/ParticleAccelerator/Components/ParticleAcceleratorControlBoxComponent.cs
@@ -13,12 +13,6 @@ namespace Content.Server.ParticleAccelerator.Components;
[RegisterComponent]
public sealed partial class ParticleAcceleratorControlBoxComponent : Component
{
- ///
- /// Whether the PA parts have been correctly arranged to make a functional device.
- ///
- [ViewVariables]
- public bool Assembled = false;
-
///
/// Whether the PA is currently set to fire at the console.
/// Requires to be true.
@@ -40,12 +34,6 @@ public sealed partial class ParticleAcceleratorControlBoxComponent : Component
[ViewVariables]
public bool Firing = false;
- ///
- /// Block re-entrant rescanning.
- ///
- [ViewVariables(VVAccess.ReadWrite)]
- public bool CurrentlyRescanning = false;
-
///
/// Whether the PA is currently firing or charging to fire.
/// Bounded by and .
@@ -61,48 +49,6 @@ public sealed partial class ParticleAcceleratorControlBoxComponent : Component
[ViewVariables]
public ParticleAcceleratorPowerState MaxStrength = ParticleAcceleratorPowerState.Level2;
- ///
- /// The power supply unit of the assembled particle accelerator.
- /// Implies the existance of a attached to this entity.
- ///
- [ViewVariables]
- public EntityUid? PowerBox;
-
- ///
- /// Whether the PA is currently firing or charging to fire.
- /// Implies the existance of a attached to this entity.
- ///
- [ViewVariables]
- public EntityUid? EndCap;
-
- ///
- /// Whether the PA is currently firing or charging to fire.
- /// Implies the existance of a attached to this entity.
- ///
- [ViewVariables]
- public EntityUid? FuelChamber;
-
- ///
- /// Whether the PA is currently firing or charging to fire.
- /// Implies the existance of a attached to this entity.
- ///
- [ViewVariables]
- public EntityUid? PortEmitter;
-
- ///
- /// Whether the PA is currently firing or charging to fire.
- /// Implies the existance of a attached to this entity.
- ///
- [ViewVariables]
- public EntityUid? ForeEmitter;
-
- ///
- /// Whether the PA is currently firing or charging to fire.
- /// Implies the existance of a attached to this entity.
- ///
- [ViewVariables]
- public EntityUid? StarboardEmitter;
-
///
/// The amount of power the particle accelerator must be provided with relative to the expected power draw to function.
///
diff --git a/Content.Server/ParticleAccelerator/Components/ParticleAcceleratorPartComponent.cs b/Content.Server/ParticleAccelerator/Components/ParticleAcceleratorPartComponent.cs
deleted file mode 100644
index 6d2b7b8960..0000000000
--- a/Content.Server/ParticleAccelerator/Components/ParticleAcceleratorPartComponent.cs
+++ /dev/null
@@ -1,8 +0,0 @@
-namespace Content.Server.ParticleAccelerator.Components;
-
-[RegisterComponent]
-public sealed partial class ParticleAcceleratorPartComponent : Component
-{
- [ViewVariables]
- public EntityUid? Master;
-}
diff --git a/Content.Server/ParticleAccelerator/EntitySystems/ParticleAcceleratorSystem.ControlBox.cs b/Content.Server/ParticleAccelerator/EntitySystems/ParticleAcceleratorSystem.ControlBox.cs
index 90a5cb2ea0..783c1b6174 100644
--- a/Content.Server/ParticleAccelerator/EntitySystems/ParticleAcceleratorSystem.ControlBox.cs
+++ b/Content.Server/ParticleAccelerator/EntitySystems/ParticleAcceleratorSystem.ControlBox.cs
@@ -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(OnComponentStartup);
- SubscribeLocalEvent(OnComponentShutdown);
SubscribeLocalEvent(OnControlBoxPowerChange);
SubscribeLocalEvent(OnUISetEnableMessage);
SubscribeLocalEvent(OnUISetPowerMessage);
SubscribeLocalEvent(OnUIRescanMessage);
+ SubscribeLocalEvent(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 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(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(comp.PowerBox, out var powerConsumer)
- || powerConsumer.ReceivedPower >= powerConsumer.DrawRate * ParticleAcceleratorControlBoxComponent.RequiredPowerRatio)
+ if (!TryComp(_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(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(comp.PowerBox, out var powerConsumer))
+
+ if (!TryComp(_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(comp.PowerBox, out var powerConsumer))
+ if (TryComp(_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(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(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 AllParts(EntityUid uid, ParticleAcceleratorControlBoxComponent? comp = null)
+ ///
+ /// Handles when a multipart machine has had some assembled/disassembled state change, or had parts added/removed.
+ ///
+ /// Multipart machine entity
+ /// Args for this event
+ private void OnMachineAssembledChanged(Entity 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(uid, out var part))
- part.Master = uid;
- }
-
- private void OnComponentShutdown(EntityUid uid, ParticleAcceleratorControlBoxComponent comp, ComponentShutdown args)
- {
- if (TryComp(uid, out var partStatus))
- partStatus.Master = null;
-
- var partQuery = GetEntityQuery();
- foreach (var part in AllParts(uid, comp))
+ else
{
- if (partQuery.TryGetComponent(part, out var partData))
- partData.Master = null;
+ if (ent.Comp.Powered)
+ {
+ SwitchOff(ent, args.User, ent.Comp);
+ }
+ else
+ {
+ UpdateAppearance(ent, ent.Comp);
+ UpdateUI(ent, ent.Comp);
+ }
+
+ // 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
+
+ _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(uid, out var apcPower) && !apcPower.Powered)
return;
- RescanParts(uid, msg.Actor, comp);
+ if (!TryComp(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(uid, machineComp);
+ _multipartMachine.Rescan(machine, msg.Actor);
}
public static int GetPANumericalLevel(ParticleAcceleratorPowerState state)
diff --git a/Content.Server/ParticleAccelerator/EntitySystems/ParticleAcceleratorSystem.Emitter.cs b/Content.Server/ParticleAccelerator/EntitySystems/ParticleAcceleratorSystem.Emitter.cs
index 78aca15bca..f8a5cc8430 100644
--- a/Content.Server/ParticleAccelerator/EntitySystems/ParticleAcceleratorSystem.Emitter.cs
+++ b/Content.Server/ParticleAccelerator/EntitySystems/ParticleAcceleratorSystem.Emitter.cs
@@ -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
index 829de0af47..0000000000
--- a/Content.Server/ParticleAccelerator/EntitySystems/ParticleAcceleratorSystem.Parts.cs
+++ /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(OnComponentShutdown);
- SubscribeLocalEvent(OnMoveEvent);
- SubscribeLocalEvent(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();
- 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();
- 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(gridUid, out var grid))
- {
- SwitchOff(uid, user, controller);
- return;
- }
-
- // Find fuel chamber first by scanning cardinals.
- var fuelQuery = GetEntityQuery();
- 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(gridUid.Value, positionEndCap, rotation, out controller.EndCap, out _, grid);
- ScanPart(gridUid.Value, positionPowerBox, rotation, out controller.PowerBox, out _, grid);
-
- if (!ScanPart(gridUid.Value, positionPortEmitter, rotation, out controller.PortEmitter, out var portEmitter, grid)
- || portEmitter.Type != ParticleAcceleratorEmitterType.Port)
- controller.PortEmitter = null;
-
- if (!ScanPart(gridUid.Value, positionForeEmitter, rotation, out controller.ForeEmitter, out var foreEmitter, grid)
- || foreEmitter.Type != ParticleAcceleratorEmitterType.Fore)
- controller.ForeEmitter = null;
-
- if (!ScanPart(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(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();
- foreach (var entity in _mapSystem.GetAnchoredEntities(uid, grid, coordinates))
- {
- if (compQuery.TryGetComponent(entity, out comp)
- && TryComp(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);
- }
-}
diff --git a/Content.Server/ParticleAccelerator/EntitySystems/ParticleAcceleratorSystem.PowerBox.cs b/Content.Server/ParticleAccelerator/EntitySystems/ParticleAcceleratorSystem.PowerBox.cs
index be961ba5fd..677228c15b 100644
--- a/Content.Server/ParticleAccelerator/EntitySystems/ParticleAcceleratorSystem.PowerBox.cs
+++ b/Content.Server/ParticleAccelerator/EntitySystems/ParticleAcceleratorSystem.PowerBox.cs
@@ -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(uid, out var part))
+ if (!TryComp(uid, out var part))
return;
if (!TryComp(part.Master, out var controller))
return;
diff --git a/Content.Server/ParticleAccelerator/EntitySystems/ParticleAcceleratorSystem.cs b/Content.Server/ParticleAccelerator/EntitySystems/ParticleAcceleratorSystem.cs
index e9b62bc4a8..d9fb84bad3 100644
--- a/Content.Server/ParticleAccelerator/EntitySystems/ParticleAcceleratorSystem.cs
+++ b/Content.Server/ParticleAccelerator/EntitySystems/ParticleAcceleratorSystem.cs
@@ -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();
}
}
diff --git a/Content.Server/ParticleAccelerator/Wires/ParticleAcceleratorToggleWireAction.cs b/Content.Server/ParticleAccelerator/Wires/ParticleAcceleratorToggleWireAction.cs
index 40a15d2bc5..98d2efa7f5 100644
--- a/Content.Server/ParticleAccelerator/Wires/ParticleAcceleratorToggleWireAction.cs
+++ b/Content.Server/ParticleAccelerator/Wires/ParticleAcceleratorToggleWireAction.cs
@@ -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();
+ var multipartMachine = EntityManager.System();
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);
}
}
diff --git a/Content.Server/Singularity/StartSingularityEngineCommand.cs b/Content.Server/Singularity/StartSingularityEngineCommand.cs
index e63a7467e0..a373c0da5e 100644
--- a/Content.Server/Singularity/StartSingularityEngineCommand.cs
+++ b/Content.Server/Singularity/StartSingularityEngineCommand.cs
@@ -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();
var paSystem = entitySystemManager.GetEntitySystem();
var paQuery = entityManager.EntityQueryEnumerator();
while (paQuery.MoveNext(out var paId, out var paControl))
{
- paSystem.RescanParts(paId, controller: paControl);
- if (!paControl.Assembled)
+ if (!entityManager.TryGetComponent(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
index 0000000000..f7a118ff54
--- /dev/null
+++ b/Content.Shared/Machines/Components/MultipartMachineComponent.cs
@@ -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;
+
+///
+/// Marks an entity as being the owner of a multipart machine.
+///
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(raiseAfterAutoHandleState: true)]
+[Access(typeof(SharedMultipartMachineSystem))]
+public sealed partial class MultipartMachineComponent : Component
+{
+ ///
+ /// Dictionary of Enum values to specific parts of this machine.
+ /// Each key can be specified as 'enum..` in Yaml.
+ ///
+ [DataField, AutoNetworkedField]
+ public Dictionary Parts = [];
+
+ ///
+ /// Whether this multipart machine is assembled or not.
+ /// Optional parts are not taken into account.
+ ///
+ [DataField, AutoNetworkedField]
+ public bool IsAssembled = false;
+
+ ///
+ /// Flag for whether the client side system is allowed to show
+ /// ghosts of missing machine parts.
+ /// Controlled/Used by the client side.
+ ///
+ public List Ghosts = [];
+}
+
+[DataDefinition]
+[Serializable, NetSerializable]
+public sealed partial class MachinePart
+{
+ ///
+ /// Component type that is expected for this part to have
+ /// to be considered a "Part" of the machine.
+ ///
+ [DataField(required: true, customTypeSerializer: typeof(ComponentNameSerializer))]
+ public string Component = "";
+
+ ///
+ /// Expected offset to find this machine at.
+ ///
+ [DataField(required: true)]
+ public Vector2i Offset;
+
+ ///
+ /// Whether this part is required for the machine to be
+ /// considered "assembled", or is considered an optional extra.
+ ///
+ [DataField]
+ public bool Optional = false;
+
+ ///
+ /// 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.
+ ///
+ [DataField]
+ public EntProtoId? GhostProto = null;
+
+ ///
+ /// Expected rotation for this machine to have.
+ ///
+ [DataField]
+ public Angle Rotation = Angle.Zero;
+
+ ///
+ /// 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.
+ ///
+ public NetEntity? NetEntity = null;
+
+ ///
+ /// Entity associated with this part.
+ /// Not null when an entity is successfully matched to the part and null otherwise.
+ ///
+ [DataField, NonSerialized]
+ public EntityUid? Entity = null;
+
+ ///
+ /// Expected graph for this part to use as part of its construction.
+ ///
+ [DataField]
+ public EntProtoId Graph;
+
+ ///
+ /// Expected node for this part to be in, on the graph.
+ /// Used to determine when a construct-able object has been
+ /// assembled or disassembled.
+ ///
+ [DataField]
+ public string ExpectedNode;
+}
diff --git a/Content.Shared/Machines/Components/MultipartMachinePartComponent.cs b/Content.Shared/Machines/Components/MultipartMachinePartComponent.cs
new file mode 100644
index 0000000000..88eff5ce53
--- /dev/null
+++ b/Content.Shared/Machines/Components/MultipartMachinePartComponent.cs
@@ -0,0 +1,17 @@
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Machines.Components;
+
+///
+/// Component for marking entities as part of a multipart machine.
+///
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
+public sealed partial class MultipartMachinePartComponent : Component
+{
+ ///
+ /// Links to the entity which holds the MultipartMachineComponent.
+ /// Useful so that entities that know which machine they are a part of.
+ ///
+ [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
index 0000000000..7185d60305
--- /dev/null
+++ b/Content.Shared/Machines/EntitySystems/SharedMultipartMachineSystem.cs
@@ -0,0 +1,115 @@
+using System.Diagnostics.CodeAnalysis;
+using Content.Shared.Machines.Components;
+
+namespace Content.Shared.Machines.EntitySystems;
+
+///
+/// Shared handling of multipart machines.
+///
+public abstract class SharedMultipartMachineSystem : EntitySystem
+{
+ protected EntityQuery XformQuery;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ XformQuery = GetEntityQuery();
+ }
+
+ ///
+ /// Returns whether each non-optional part of the machine has a matched entity
+ ///
+ /// Entity to check the assembled state of.
+ /// True if all non-optional parts have a matching entity, false otherwise.
+ public bool IsAssembled(Entity 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;
+ }
+
+ ///
+ /// Returns whether a machine has a specifed EntityUid bound to one of its parts.
+ ///
+ /// Entity, which might have a multpart machine attached, to use for the query.
+ /// EntityUid to search for.
+ /// True if any part has the specified EntityUid, false otherwise.
+ public bool HasPartEntity(Entity 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;
+ }
+
+ ///
+ /// Get the EntityUid for the entity bound to a specific part, if one exists.
+ ///
+ /// Entity, which might have a multipart machine attached, to use for the query.
+ /// Enum value for the part to find, must match the value specified in YAML.
+ /// May contain the resolved EntityUid for the specified part, null otherwise.
+ public EntityUid? GetPartEntity(Entity ent, Enum part)
+ {
+ if (!TryGetPartEntity(ent, part, out var entity))
+ return null;
+
+ return entity;
+ }
+
+ ///
+ /// Get the EntityUid for the entity bound to a specific part, if one exists.
+ ///
+ /// Entity, which might have a multipart machine attached, to use for the query.
+ /// Enum for the part to find, must match the value specified in YAML.
+ /// Out var which may contain the matched EntityUid for the specified part.
+ /// True if the part is found and has a matched entity, false otherwise.
+ public bool TryGetPartEntity(
+ Entity 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;
+ }
+
+ ///
+ /// Check if a machine has an entity bound to a specific part
+ ///
+ /// Entity, which might have a multipart machine attached, to use for the query.
+ /// Enum for the part to find.
+ /// True if the specific part has a entity bound to it, false otherwise.
+ public bool HasPart(Entity 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
index 0000000000..8bf19e9d6b
--- /dev/null
+++ b/Content.Shared/Machines/Events/MultipartMachineEvents.cs
@@ -0,0 +1,21 @@
+namespace Content.Shared.Machines.Events;
+
+///
+/// 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.
+///
+/// Entity that is bound to the multipart machine.
+/// Assembled state of the machine.
+/// Optional user that may have caused the assembly state to change.
+/// Dictionary of keys to entities of parts that have been added to this machine.
+/// Dictionary of keys to entities of parts that have been removed from this machine.
+[ByRefEvent]
+public record struct MultipartMachineAssemblyStateChanged(
+ EntityUid Entity,
+ bool IsAssembled,
+ EntityUid? User,
+ Dictionary PartsAdded,
+ Dictionary PartsRemoved)
+{
+}
diff --git a/Content.Shared/ParticleAccelerator/AcceleratorParts.cs b/Content.Shared/ParticleAccelerator/AcceleratorParts.cs
new file mode 100644
index 0000000000..23648916e3
--- /dev/null
+++ b/Content.Shared/ParticleAccelerator/AcceleratorParts.cs
@@ -0,0 +1,16 @@
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.ParticleAccelerator;
+
+[Serializable, NetSerializable]
+public enum AcceleratorParts : byte
+{
+ EndCap,
+ FuelChamber,
+ PowerBox,
+ PortEmitter,
+ ForeEmitter,
+ StarboardEmitter
+};
+
+
diff --git a/Content.Server/ParticleAccelerator/Components/ParticleAcceleratorEmitterComponent.cs b/Content.Shared/ParticleAccelerator/Components/ParticleAcceleratorEmitterComponent.cs
similarity index 90%
rename from Content.Server/ParticleAccelerator/Components/ParticleAcceleratorEmitterComponent.cs
rename to Content.Shared/ParticleAccelerator/Components/ParticleAcceleratorEmitterComponent.cs
index 7697644a3b..9b52a73522 100644
--- a/Content.Server/ParticleAccelerator/Components/ParticleAcceleratorEmitterComponent.cs
+++ b/Content.Shared/ParticleAccelerator/Components/ParticleAcceleratorEmitterComponent.cs
@@ -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
diff --git a/Content.Server/ParticleAccelerator/Components/ParticleAcceleratorEndCapComponent.cs b/Content.Shared/ParticleAccelerator/Components/ParticleAcceleratorEndCapComponent.cs
similarity index 62%
rename from Content.Server/ParticleAccelerator/Components/ParticleAcceleratorEndCapComponent.cs
rename to Content.Shared/ParticleAccelerator/Components/ParticleAcceleratorEndCapComponent.cs
index 9c111d1ea9..20615321d1 100644
--- a/Content.Server/ParticleAccelerator/Components/ParticleAcceleratorEndCapComponent.cs
+++ b/Content.Shared/ParticleAccelerator/Components/ParticleAcceleratorEndCapComponent.cs
@@ -1,4 +1,4 @@
-namespace Content.Server.ParticleAccelerator.Components;
+namespace Content.Shared.ParticleAccelerator.Components;
[RegisterComponent]
public sealed partial class ParticleAcceleratorEndCapComponent : Component
diff --git a/Content.Server/ParticleAccelerator/Components/ParticleAcceleratorFuelChamberComponent.cs b/Content.Shared/ParticleAccelerator/Components/ParticleAcceleratorFuelChamberComponent.cs
similarity index 63%
rename from Content.Server/ParticleAccelerator/Components/ParticleAcceleratorFuelChamberComponent.cs
rename to Content.Shared/ParticleAccelerator/Components/ParticleAcceleratorFuelChamberComponent.cs
index 9029e880d9..9e0da68b96 100644
--- a/Content.Server/ParticleAccelerator/Components/ParticleAcceleratorFuelChamberComponent.cs
+++ b/Content.Shared/ParticleAccelerator/Components/ParticleAcceleratorFuelChamberComponent.cs
@@ -1,4 +1,4 @@
-namespace Content.Server.ParticleAccelerator.Components;
+namespace Content.Shared.ParticleAccelerator.Components;
[RegisterComponent]
public sealed partial class ParticleAcceleratorFuelChamberComponent : Component
diff --git a/Content.Server/ParticleAccelerator/Components/ParticleAcceleratorPowerBoxComponent.cs b/Content.Shared/ParticleAccelerator/Components/ParticleAcceleratorPowerBoxComponent.cs
similarity index 62%
rename from Content.Server/ParticleAccelerator/Components/ParticleAcceleratorPowerBoxComponent.cs
rename to Content.Shared/ParticleAccelerator/Components/ParticleAcceleratorPowerBoxComponent.cs
index f8ad4ef5d2..3da6b76f2f 100644
--- a/Content.Server/ParticleAccelerator/Components/ParticleAcceleratorPowerBoxComponent.cs
+++ b/Content.Shared/ParticleAccelerator/Components/ParticleAcceleratorPowerBoxComponent.cs
@@ -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
index 0000000000..fecc4f2bc7
--- /dev/null
+++ b/Resources/Prototypes/Entities/Structures/Machines/multipart.yml
@@ -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
diff --git a/Resources/Prototypes/Entities/Structures/Power/Generation/PA/base_particleaccelerator.yml b/Resources/Prototypes/Entities/Structures/Power/Generation/PA/base_particleaccelerator.yml
index d5b7e3667d..b19e2c8ebb 100644
--- a/Resources/Prototypes/Entities/Structures/Power/Generation/PA/base_particleaccelerator.yml
+++ b/Resources/Prototypes/Entities/Structures/Power/Generation/PA/base_particleaccelerator.yml
@@ -43,7 +43,6 @@
map: [ "enum.ParticleAcceleratorVisualLayers.Unlit" ]
shader: unshaded
visible: false
- - type: ParticleAcceleratorPart
- type: ParticleAcceleratorPartVisuals
stateBase: unlit
- type: Construction
diff --git a/Resources/Prototypes/Entities/Structures/Power/Generation/PA/control_box.yml b/Resources/Prototypes/Entities/Structures/Power/Generation/PA/control_box.yml
index ae3fc96774..7b3aa0c693 100644
--- a/Resources/Prototypes/Entities/Structures/Power/Generation/PA/control_box.yml
+++ b/Resources/Prototypes/Entities/Structures/Power/Generation/PA/control_box.yml
@@ -27,6 +27,50 @@
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
diff --git a/Resources/Prototypes/Entities/Structures/Power/Generation/PA/emitter.yml b/Resources/Prototypes/Entities/Structures/Power/Generation/PA/emitter.yml
index 43b4d5f9ef..38d0d0b6cd 100644
--- a/Resources/Prototypes/Entities/Structures/Power/Generation/PA/emitter.yml
+++ b/Resources/Prototypes/Entities/Structures/Power/Generation/PA/emitter.yml
@@ -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
diff --git a/Resources/Prototypes/Entities/Structures/Power/Generation/PA/end_cap.yml b/Resources/Prototypes/Entities/Structures/Power/Generation/PA/end_cap.yml
index 80b0240d89..226ef915a7 100644
--- a/Resources/Prototypes/Entities/Structures/Power/Generation/PA/end_cap.yml
+++ b/Resources/Prototypes/Entities/Structures/Power/Generation/PA/end_cap.yml
@@ -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
diff --git a/Resources/Prototypes/Entities/Structures/Power/Generation/PA/fuel_chamber.yml b/Resources/Prototypes/Entities/Structures/Power/Generation/PA/fuel_chamber.yml
index 868e9cf8f9..37fba25212 100644
--- a/Resources/Prototypes/Entities/Structures/Power/Generation/PA/fuel_chamber.yml
+++ b/Resources/Prototypes/Entities/Structures/Power/Generation/PA/fuel_chamber.yml
@@ -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
diff --git a/Resources/Prototypes/Entities/Structures/Power/Generation/PA/power_box.yml b/Resources/Prototypes/Entities/Structures/Power/Generation/PA/power_box.yml
index c9a25d62ff..9dbff7fa42 100644
--- a/Resources/Prototypes/Entities/Structures/Power/Generation/PA/power_box.yml
+++ b/Resources/Prototypes/Entities/Structures/Power/Generation/PA/power_box.yml
@@ -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