--- /dev/null
+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;
+}
--- /dev/null
+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);
+ }
+}
<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/>
private RSI? _rsi;
public string BaseState { get; set; } = "control_box";
+ public bool DefaultVisible { get; set; } = false;
public PASegmentControl()
{
_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)
{
_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);
+ }
+ }
}
// 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)
/// <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,
/// <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)
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;
}
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)
+ {
+ }
}
"InventorySlots",
"LightFade",
"HolidayRsiSwap",
- "OptionsVisualizer"
+ "OptionsVisualizer",
+ "MultipartMachineGhost"
};
}
}
--- /dev/null
+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;
+ }
+}
[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.
[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"/>.
[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>
+++ /dev/null
-namespace Content.Server.ParticleAccelerator.Components;
-
-[RegisterComponent]
-public sealed partial class ParticleAcceleratorPartComponent : Component
-{
- [ViewVariables]
- public EntityUid? Master;
-}
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;
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;
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)
}
[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)
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)
if (!Resolve(uid, ref comp))
return;
- DebugTools.Assert(comp.Assembled);
+ DebugTools.Assert(_multipartMachine.IsAssembled((uid, null)));
if (comp.Enabled || !comp.CanBeEnabled)
return;
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);
}
return;
DebugTools.Assert(comp.Enabled);
- DebugTools.Assert(comp.Assembled);
+ DebugTools.Assert(_multipartMachine.IsAssembled((uid, null)));
if (comp.Powered)
return;
return;
}
- EverythingIsWellToFire(comp);
+ if (!TryComp<MultipartMachineComponent>(uid, out var machine))
+ return;
+
+ EverythingIsWellToFire(comp, (uid, machine));
var curTime = _gameTiming.CurTime;
comp.LastFire = curTime;
{
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;
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)
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);
+ }
}
}
if (msg.Enabled)
{
- if (comp.Assembled)
+ if (_multipartMachine.IsAssembled((uid, null)))
SwitchOn(uid, msg.Actor, comp);
}
else
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)
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;
+++ /dev/null
-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);
- }
-}
-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;
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;
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;
[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();
}
}
+using Content.Server.Machines.EntitySystems;
using Content.Server.ParticleAccelerator.Components;
using Content.Server.ParticleAccelerator.EntitySystems;
using Content.Server.Wires;
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);
}
}
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;
}
// 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);
--- /dev/null
+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;
+}
--- /dev/null
+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;
+}
--- /dev/null
+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;
+ }
+}
--- /dev/null
+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)
+{
+}
--- /dev/null
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.ParticleAccelerator;
+
+[Serializable, NetSerializable]
+public enum AcceleratorParts : byte
+{
+ EndCap,
+ FuelChamber,
+ PowerBox,
+ PortEmitter,
+ ForeEmitter,
+ StarboardEmitter
+};
+
+
using Robust.Shared.Prototypes;
-namespace Content.Server.ParticleAccelerator.Components;
+namespace Content.Shared.ParticleAccelerator.Components;
[RegisterComponent]
public sealed partial class ParticleAcceleratorEmitterComponent : Component
-namespace Content.Server.ParticleAccelerator.Components;
+namespace Content.Shared.ParticleAccelerator.Components;
[RegisterComponent]
public sealed partial class ParticleAcceleratorEndCapComponent : Component
-namespace Content.Server.ParticleAccelerator.Components;
+namespace Content.Shared.ParticleAccelerator.Components;
[RegisterComponent]
public sealed partial class ParticleAcceleratorFuelChamberComponent : Component
-namespace Content.Server.ParticleAccelerator.Components;
+namespace Content.Shared.ParticleAccelerator.Components;
[RegisterComponent]
public sealed partial class ParticleAcceleratorPowerBoxComponent : Component
--- /dev/null
+# 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
map: [ "enum.ParticleAcceleratorVisualLayers.Unlit" ]
shader: unshaded
visible: false
- - type: ParticleAcceleratorPart
- type: ParticleAcceleratorPartVisuals
stateBase: unlit
- type: Construction
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
emitterType: Port
- type: Construction
graph: ParticleAcceleratorEmitterPort
+ - type: MultipartMachinePart
- type: entity
parent: ParticleAcceleratorFinishedPart
emitterType: Fore
- type: Construction
graph: ParticleAcceleratorEmitterFore
+ - type: MultipartMachinePart
- type: entity
parent: ParticleAcceleratorFinishedPart
emitterType: Starboard
- type: Construction
graph: ParticleAcceleratorEmitterStarboard
+ - type: MultipartMachinePart
# Unfinished
sprite: Structures/Power/Generation/PA/emitter_port.rsi
- type: Construction
graph: ParticleAcceleratorEmitterPort
+ - type: MultipartMachinePart
- type: entity
parent: ParticleAcceleratorUnfinishedBase
sprite: Structures/Power/Generation/PA/emitter_fore.rsi
- type: Construction
graph: ParticleAcceleratorEmitterFore
+ - type: MultipartMachinePart
- type: entity
parent: ParticleAcceleratorUnfinishedBase
sprite: Structures/Power/Generation/PA/emitter_starboard.rsi
- type: Construction
graph: ParticleAcceleratorEmitterStarboard
+ - type: MultipartMachinePart
- type: ParticleAcceleratorEndCap
- type: Construction
graph: ParticleAcceleratorEndCap
+ - type: MultipartMachinePart
# Unfinished
sprite: Structures/Power/Generation/PA/end_cap.rsi
- type: Construction
graph: ParticleAcceleratorEndCap
+ - type: MultipartMachinePart
- type: ParticleAcceleratorFuelChamber
- type: Construction
graph: ParticleAcceleratorFuelChamber
+ - type: MultipartMachinePart
# Unfinished
sprite: Structures/Power/Generation/PA/fuel_chamber.rsi
- type: Construction
graph: ParticleAcceleratorFuelChamber
+ - type: MultipartMachinePart
nodeGroupID: MVPower
- type: Construction
graph: ParticleAcceleratorPowerBox
+ - type: MultipartMachinePart
- type: entity
parent: ParticleAcceleratorUnfinishedBase
sprite: Structures/Power/Generation/PA/power_box.rsi
- type: Construction
graph: ParticleAcceleratorPowerBox
+ - type: MultipartMachinePart