Add multipart machines system (#35969)

This commit is contained in:
BarryNorfolk
2025-06-02 16:02:41 +02:00
committed by GitHub
parent 9a38d66df2
commit b2d0f7ed28
32 changed files with 987 additions and 331 deletions

View File

@@ -0,0 +1,14 @@
namespace Content.Client.Machines.Components;
/// <summary>
/// Component attached to all multipart machine ghosts
/// Intended for client side usage only, but used on prototypes.
/// </summary>
[RegisterComponent]
public sealed partial class MultipartMachineGhostComponent : Component
{
/// <summary>
/// Machine this particular ghost is linked to.
/// </summary>
public EntityUid? LinkedMachine = null;
}

View File

@@ -0,0 +1,109 @@
using Content.Client.Examine;
using Content.Client.Machines.Components;
using Content.Shared.Machines.Components;
using Content.Shared.Machines.EntitySystems;
using Robust.Client.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.Manager;
using Robust.Shared.Spawners;
namespace Content.Client.Machines.EntitySystems;
/// <summary>
/// Client side handling of multipart machines.
/// Handles client side examination events to show the expected layout of the machine
/// based on the origin of the main entity.
/// </summary>
public sealed class MultipartMachineSystem : SharedMultipartMachineSystem
{
private readonly EntProtoId _ghostPrototype = "MultipartMachineGhost";
private readonly Color _partiallyTransparent = new Color(255, 255, 255, 180);
[Dependency] private readonly SpriteSystem _sprite = default!;
[Dependency] private readonly IPrototypeManager _prototype = default!;
[Dependency] private readonly MetaDataSystem _metaData = default!;
[Dependency] private readonly ISerializationManager _serialization= default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<MultipartMachineComponent, ClientExaminedEvent>(OnMachineExamined);
SubscribeLocalEvent<MultipartMachineComponent, AfterAutoHandleStateEvent>(OnHandleState);
SubscribeLocalEvent<MultipartMachineGhostComponent, TimedDespawnEvent>(OnGhostDespawned);
}
/// <summary>
/// Handles spawning several ghost sprites to show where the different parts of the machine
/// should go and the rotations they're expected to have.
/// Can only show one set of ghost parts at a time and their location depends on the current map/grid
/// location of the origin machine.
/// </summary>
/// <param name="ent">Entity/Component that has been inspected.</param>
/// <param name="args">Args for the event.</param>
private void OnMachineExamined(Entity<MultipartMachineComponent> ent, ref ClientExaminedEvent args)
{
if (ent.Comp.Ghosts.Count != 0)
{
// Already showing some part ghosts
return;
}
foreach (var part in ent.Comp.Parts.Values)
{
if (part.Entity.HasValue)
continue;
var entityCoords = new EntityCoordinates(ent.Owner, part.Offset);
var ghostEnt = Spawn(_ghostPrototype, entityCoords);
if (!XformQuery.TryGetComponent(ghostEnt, out var xform))
break;
xform.LocalRotation = part.Rotation;
Comp<MultipartMachineGhostComponent>(ghostEnt).LinkedMachine = ent;
ent.Comp.Ghosts.Add(ghostEnt);
if (part.GhostProto == null)
continue;
var entProto = _prototype.Index(part.GhostProto.Value);
if (!entProto.Components.TryGetComponent("Sprite", out var s) || s is not SpriteComponent protoSprite)
return;
var ghostSprite = EnsureComp<SpriteComponent>(ghostEnt);
_serialization.CopyTo(protoSprite, ref ghostSprite, notNullableOverride: true);
_sprite.SetColor((ghostEnt, ghostSprite), _partiallyTransparent);
_metaData.SetEntityName(ghostEnt, entProto.Name);
_metaData.SetEntityDescription(ghostEnt, entProto.Description);
}
}
private void OnHandleState(Entity<MultipartMachineComponent> ent, ref AfterAutoHandleStateEvent args)
{
foreach (var part in ent.Comp.Parts.Values)
{
part.Entity = part.NetEntity.HasValue ? EnsureEntity<MultipartMachinePartComponent>(part.NetEntity.Value, ent) : null;
}
}
/// <summary>
/// Handles when a ghost part despawns after its short lifetime.
/// Will attempt to remove itself from the list of known ghost entities in the main multipart
/// machine component.
/// </summary>
/// <param name="ent">Ghost entity that has been despawned.</param>
/// <param name="args">Args for the event.</param>
private void OnGhostDespawned(Entity<MultipartMachineGhostComponent> ent, ref TimedDespawnEvent args)
{
if (!TryComp<MultipartMachineComponent>(ent.Comp.LinkedMachine, out var machine))
return;
machine.Ghosts.Remove(ent);
}
}

View File

@@ -127,7 +127,7 @@
<Control/> <Control/>
<ui:PASegmentControl Name="EndCapTexture" BaseState="end_cap"/> <ui:PASegmentControl Name="EndCapTexture" BaseState="end_cap"/>
<Control/> <Control/>
<ui:PASegmentControl Name="ControlBoxTexture" BaseState="control_box"/> <ui:PASegmentControl Name="ControlBoxTexture" BaseState="control_box" DefaultVisible="True"/>
<ui:PASegmentControl Name="FuelChamberTexture" BaseState="fuel_chamber"/> <ui:PASegmentControl Name="FuelChamberTexture" BaseState="fuel_chamber"/>
<Control/> <Control/>
<Control/> <Control/>

View File

@@ -268,6 +268,7 @@ public sealed class PASegmentControl : Control
private RSI? _rsi; private RSI? _rsi;
public string BaseState { get; set; } = "control_box"; public string BaseState { get; set; } = "control_box";
public bool DefaultVisible { get; set; } = false;
public PASegmentControl() public PASegmentControl()
{ {
@@ -283,12 +284,14 @@ public sealed class PASegmentControl : Control
_rsi = IoCManager.Resolve<IResourceCache>().GetResource<RSIResource>($"/Textures/Structures/Power/Generation/PA/{BaseState}.rsi").RSI; _rsi = IoCManager.Resolve<IResourceCache>().GetResource<RSIResource>($"/Textures/Structures/Power/Generation/PA/{BaseState}.rsi").RSI;
MinSize = _rsi.Size; MinSize = _rsi.Size;
_base.Texture = _rsi["completed"].Frame0; _base.Texture = _rsi["completed"].Frame0;
SetVisible(DefaultVisible);
_unlit.Visible = DefaultVisible;
} }
public void SetPowerState(ParticleAcceleratorUIState state, bool exists) public void SetPowerState(ParticleAcceleratorUIState state, bool exists)
{ {
_base.ShaderOverride = exists ? null : _greyScaleShader; SetVisible(exists);
_base.ModulateSelfOverride = exists ? null : new Color(127, 127, 127);
if (!state.Enabled || !exists) if (!state.Enabled || !exists)
{ {
@@ -319,4 +322,23 @@ public sealed class PASegmentControl : Control
_unlit.Texture = rState.Frame0; _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);
}
}
} }

View File

@@ -258,7 +258,7 @@ namespace Content.Server.Construction
// ChangeEntity will handle the pathfinding update. // ChangeEntity will handle the pathfinding update.
if (node.Entity.GetId(uid, userUid, new(EntityManager)) is { } newEntity 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; return true;
if (performActions) if (performActions)
@@ -281,6 +281,7 @@ namespace Content.Server.Construction
/// <param name="userUid">An optional user entity, for actions.</param> /// <param name="userUid">An optional user entity, for actions.</param>
/// <param name="newEntity">The entity prototype identifier for the new entity.</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="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="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="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, /// <param name="containerManager">The container manager component of the target entity. Will be resolved if null,
@@ -288,6 +289,7 @@ namespace Content.Server.Construction
/// <returns>The new entity, or null if the method did not succeed.</returns> /// <returns>The new entity, or null if the method did not succeed.</returns>
private EntityUid? ChangeEntity(EntityUid uid, EntityUid? userUid, string newEntity, private EntityUid? ChangeEntity(EntityUid uid, EntityUid? userUid, string newEntity,
ConstructionComponent? construction = null, ConstructionComponent? construction = null,
string? previousNode = null,
MetaDataComponent? metaData = null, MetaDataComponent? metaData = null,
TransformComponent? transform = null, TransformComponent? transform = null,
ContainerManagerComponent? containerManager = null) ContainerManagerComponent? containerManager = null)
@@ -407,6 +409,11 @@ namespace Content.Server.Construction
QueueDel(uid); 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; return newUid;
} }
@@ -453,4 +460,16 @@ namespace Content.Server.Construction
Old = oldUid; 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)
{
}
} }

View File

@@ -19,7 +19,8 @@ namespace Content.Server.Entry
"InventorySlots", "InventorySlots",
"LightFade", "LightFade",
"HolidayRsiSwap", "HolidayRsiSwap",
"OptionsVisualizer" "OptionsVisualizer",
"MultipartMachineGhost"
}; };
} }
} }

View File

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

View File

@@ -13,12 +13,6 @@ namespace Content.Server.ParticleAccelerator.Components;
[RegisterComponent] [RegisterComponent]
public sealed partial class ParticleAcceleratorControlBoxComponent : Component 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> /// <summary>
/// Whether the PA is currently set to fire at the console. /// Whether the PA is currently set to fire at the console.
/// Requires <see cref="Assembled"/> to be true. /// Requires <see cref="Assembled"/> to be true.
@@ -40,12 +34,6 @@ public sealed partial class ParticleAcceleratorControlBoxComponent : Component
[ViewVariables] [ViewVariables]
public bool Firing = false; public bool Firing = false;
/// <summary>
/// Block re-entrant rescanning.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public bool CurrentlyRescanning = false;
/// <summary> /// <summary>
/// Whether the PA is currently firing or charging to fire. /// Whether the PA is currently firing or charging to fire.
/// Bounded by <see cref="ParticleAcceleratorPowerState.Standby"/> and <see cref="MaxStrength"/>. /// Bounded by <see cref="ParticleAcceleratorPowerState.Standby"/> and <see cref="MaxStrength"/>.
@@ -61,48 +49,6 @@ public sealed partial class ParticleAcceleratorControlBoxComponent : Component
[ViewVariables] [ViewVariables]
public ParticleAcceleratorPowerState MaxStrength = ParticleAcceleratorPowerState.Level2; 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> /// <summary>
/// The amount of power the particle accelerator must be provided with relative to the expected power draw to function. /// The amount of power the particle accelerator must be provided with relative to the expected power draw to function.
/// </summary> /// </summary>

View File

@@ -1,8 +0,0 @@
namespace Content.Server.ParticleAccelerator.Components;
[RegisterComponent]
public sealed partial class ParticleAcceleratorPartComponent : Component
{
[ViewVariables]
public EntityUid? Master;
}

View File

@@ -1,6 +1,7 @@
using Content.Server.ParticleAccelerator.Components; using Content.Server.ParticleAccelerator.Components;
using Content.Server.Power.Components; using Content.Server.Power.Components;
using Content.Shared.Database; using Content.Shared.Database;
using Content.Shared.Machines.Components;
using Content.Shared.Singularity.Components; using Content.Shared.Singularity.Components;
using Robust.Shared.Utility; using Robust.Shared.Utility;
using System.Diagnostics; using System.Diagnostics;
@@ -10,6 +11,8 @@ using Content.Shared.Power;
using Robust.Shared.Audio; using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems; using Robust.Shared.Audio.Systems;
using Robust.Shared.Player; using Robust.Shared.Player;
using Content.Shared.ParticleAccelerator;
using Content.Shared.Machines.Events;
namespace Content.Server.ParticleAccelerator.EntitySystems; namespace Content.Server.ParticleAccelerator.EntitySystems;
@@ -20,12 +23,11 @@ public sealed partial class ParticleAcceleratorSystem
private void InitializeControlBoxSystem() private void InitializeControlBoxSystem()
{ {
SubscribeLocalEvent<ParticleAcceleratorControlBoxComponent, ComponentStartup>(OnComponentStartup);
SubscribeLocalEvent<ParticleAcceleratorControlBoxComponent, ComponentShutdown>(OnComponentShutdown);
SubscribeLocalEvent<ParticleAcceleratorControlBoxComponent, PowerChangedEvent>(OnControlBoxPowerChange); SubscribeLocalEvent<ParticleAcceleratorControlBoxComponent, PowerChangedEvent>(OnControlBoxPowerChange);
SubscribeLocalEvent<ParticleAcceleratorControlBoxComponent, ParticleAcceleratorSetEnableMessage>(OnUISetEnableMessage); SubscribeLocalEvent<ParticleAcceleratorControlBoxComponent, ParticleAcceleratorSetEnableMessage>(OnUISetEnableMessage);
SubscribeLocalEvent<ParticleAcceleratorControlBoxComponent, ParticleAcceleratorSetPowerStateMessage>(OnUISetPowerMessage); SubscribeLocalEvent<ParticleAcceleratorControlBoxComponent, ParticleAcceleratorSetPowerStateMessage>(OnUISetPowerMessage);
SubscribeLocalEvent<ParticleAcceleratorControlBoxComponent, ParticleAcceleratorRescanPartsMessage>(OnUIRescanMessage); SubscribeLocalEvent<ParticleAcceleratorControlBoxComponent, ParticleAcceleratorRescanPartsMessage>(OnUIRescanMessage);
SubscribeLocalEvent<ParticleAcceleratorControlBoxComponent, MultipartMachineAssemblyStateChanged>(OnMachineAssembledChanged);
} }
public override void Update(float frameTime) public override void Update(float frameTime)
@@ -40,14 +42,12 @@ public sealed partial class ParticleAcceleratorSystem
} }
[Conditional("DEBUG")] [Conditional("DEBUG")]
private void EverythingIsWellToFire(ParticleAcceleratorControlBoxComponent controller) private void EverythingIsWellToFire(ParticleAcceleratorControlBoxComponent controller,
Entity<MultipartMachineComponent> machine)
{ {
DebugTools.Assert(controller.Powered); DebugTools.Assert(controller.Powered);
DebugTools.Assert(controller.SelectedStrength != ParticleAcceleratorPowerState.Standby); DebugTools.Assert(controller.SelectedStrength != ParticleAcceleratorPowerState.Standby);
DebugTools.Assert(controller.Assembled); DebugTools.Assert(machine.Comp.IsAssembled);
DebugTools.Assert(EntityManager.EntityExists(controller.PortEmitter));
DebugTools.Assert(EntityManager.EntityExists(controller.ForeEmitter));
DebugTools.Assert(EntityManager.EntityExists(controller.StarboardEmitter));
} }
public void Fire(EntityUid uid, TimeSpan curTime, ParticleAcceleratorControlBoxComponent? comp = null) public void Fire(EntityUid uid, TimeSpan curTime, ParticleAcceleratorControlBoxComponent? comp = null)
@@ -58,12 +58,17 @@ public sealed partial class ParticleAcceleratorSystem
comp.LastFire = curTime; comp.LastFire = curTime;
comp.NextFire = curTime + comp.ChargeTime; 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; var strength = comp.SelectedStrength;
FireEmitter(comp.PortEmitter!.Value, strength);
FireEmitter(comp.ForeEmitter!.Value, strength); FireEmitter(_multipartMachine.GetPartEntity(machine, AcceleratorParts.PortEmitter)!.Value, strength);
FireEmitter(comp.StarboardEmitter!.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) 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)) if (!Resolve(uid, ref comp))
return; return;
DebugTools.Assert(comp.Assembled); DebugTools.Assert(_multipartMachine.IsAssembled((uid, null)));
if (comp.Enabled || !comp.CanBeEnabled) if (comp.Enabled || !comp.CanBeEnabled)
return; return;
@@ -82,9 +87,11 @@ public sealed partial class ParticleAcceleratorSystem
comp.Enabled = true; comp.Enabled = true;
UpdatePowerDraw(uid, comp); UpdatePowerDraw(uid, comp);
if (!TryComp<PowerConsumerComponent>(comp.PowerBox, out var powerConsumer) if (!TryComp<PowerConsumerComponent>(_multipartMachine.GetPartEntity(uid, AcceleratorParts.PowerBox), out var powerConsumer)
|| powerConsumer.ReceivedPower >= powerConsumer.DrawRate * ParticleAcceleratorControlBoxComponent.RequiredPowerRatio) || powerConsumer.ReceivedPower >= powerConsumer.DrawRate * ParticleAcceleratorControlBoxComponent.RequiredPowerRatio)
{
PowerOn(uid, comp); PowerOn(uid, comp);
}
UpdateUI(uid, comp); UpdateUI(uid, comp);
} }
@@ -112,7 +119,7 @@ public sealed partial class ParticleAcceleratorSystem
return; return;
DebugTools.Assert(comp.Enabled); DebugTools.Assert(comp.Enabled);
DebugTools.Assert(comp.Assembled); DebugTools.Assert(_multipartMachine.IsAssembled((uid, null)));
if (comp.Powered) if (comp.Powered)
return; return;
@@ -211,7 +218,10 @@ public sealed partial class ParticleAcceleratorSystem
return; return;
} }
EverythingIsWellToFire(comp); if (!TryComp<MultipartMachineComponent>(uid, out var machine))
return;
EverythingIsWellToFire(comp, (uid, machine));
var curTime = _gameTiming.CurTime; var curTime = _gameTiming.CurTime;
comp.LastFire = curTime; comp.LastFire = curTime;
@@ -223,7 +233,8 @@ public sealed partial class ParticleAcceleratorSystem
{ {
if (!Resolve(uid, ref comp)) if (!Resolve(uid, ref comp))
return; return;
if (!TryComp<PowerConsumerComponent>(comp.PowerBox, out var powerConsumer))
if (!TryComp<PowerConsumerComponent>(_multipartMachine.GetPartEntity(uid, AcceleratorParts.PowerBox), out var powerConsumer))
return; return;
var powerDraw = comp.BasePowerDraw; var powerDraw = comp.BasePowerDraw;
@@ -244,30 +255,35 @@ public sealed partial class ParticleAcceleratorSystem
var draw = 0f; var draw = 0f;
var receive = 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; draw = powerConsumer.DrawRate;
receive = powerConsumer.ReceivedPower; receive = powerConsumer.ReceivedPower;
} }
_uiSystem.SetUiState(uid, if (!TryComp<MultipartMachineComponent>(uid, out var machineComp))
ParticleAcceleratorControlBoxUiKey.Key, return;
new ParticleAcceleratorUIState(
comp.Assembled, var machine = (uid, machineComp);
var uiState = new ParticleAcceleratorUIState(
machineComp.IsAssembled,
comp.Enabled, comp.Enabled,
comp.SelectedStrength, comp.SelectedStrength,
(int) draw, (int)draw,
(int) receive, (int)receive,
comp.StarboardEmitter != null, _multipartMachine.HasPart(machine, AcceleratorParts.StarboardEmitter),
comp.ForeEmitter != null, _multipartMachine.HasPart(machine, AcceleratorParts.ForeEmitter),
comp.PortEmitter != null, _multipartMachine.HasPart(machine, AcceleratorParts.PortEmitter),
comp.PowerBox != null, _multipartMachine.HasPart(machine, AcceleratorParts.PowerBox),
comp.FuelChamber != null, _multipartMachine.HasPart(machine, AcceleratorParts.FuelChamber),
comp.EndCap != null, _multipartMachine.HasPart(machine, AcceleratorParts.EndCap),
comp.InterfaceDisabled, comp.InterfaceDisabled,
comp.MaxStrength, comp.MaxStrength,
comp.StrengthLocked comp.StrengthLocked
)); );
_uiSystem.SetUiState(uid, ParticleAcceleratorControlBoxUiKey.Key, uiState);
} }
private void UpdateAppearance(EntityUid uid, ParticleAcceleratorControlBoxComponent? comp = null, AppearanceComponent? appearance = null) 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; 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) // UpdatePartVisualState(ControlBox); (We are the control box)
if (controller.FuelChamber.HasValue) if (_multipartMachine.TryGetPartEntity(machine, AcceleratorParts.FuelChamber, out var fuelChamber))
_appearanceSystem.SetData(controller.FuelChamber!.Value, ParticleAcceleratorVisuals.VisualState, state); _appearanceSystem.SetData(fuelChamber.Value, ParticleAcceleratorVisuals.VisualState, state);
if (controller.PowerBox.HasValue) if (_multipartMachine.TryGetPartEntity(machine, AcceleratorParts.PowerBox, out var powerBox))
_appearanceSystem.SetData(controller.PowerBox!.Value, ParticleAcceleratorVisuals.VisualState, state); _appearanceSystem.SetData(powerBox.Value, ParticleAcceleratorVisuals.VisualState, state);
if (controller.PortEmitter.HasValue) if (_multipartMachine.TryGetPartEntity(machine, AcceleratorParts.PortEmitter, out var portEmitter))
_appearanceSystem.SetData(controller.PortEmitter!.Value, ParticleAcceleratorVisuals.VisualState, state); _appearanceSystem.SetData(portEmitter.Value, ParticleAcceleratorVisuals.VisualState, state);
if (controller.ForeEmitter.HasValue) if (_multipartMachine.TryGetPartEntity(machine, AcceleratorParts.ForeEmitter, out var foreEmitter))
_appearanceSystem.SetData(controller.ForeEmitter!.Value, ParticleAcceleratorVisuals.VisualState, state); _appearanceSystem.SetData(foreEmitter.Value, ParticleAcceleratorVisuals.VisualState, state);
if (controller.StarboardEmitter.HasValue) if (_multipartMachine.TryGetPartEntity(machine, AcceleratorParts.StarboardEmitter, out var starboardEmitter))
_appearanceSystem.SetData(controller.StarboardEmitter!.Value, ParticleAcceleratorVisuals.VisualState, state); _appearanceSystem.SetData(starboardEmitter.Value, ParticleAcceleratorVisuals.VisualState, state);
//no endcap because it has no powerlevel-sprites //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) UpdatePowerDraw(ent, ent.Comp);
yield return comp.FuelChamber.Value; UpdateUI(ent, ent.Comp);
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;
} }
} else
private void OnComponentStartup(EntityUid uid, ParticleAcceleratorControlBoxComponent comp, ComponentStartup args)
{
if (TryComp<ParticleAcceleratorPartComponent>(uid, out var part))
part.Master = uid;
}
private void OnComponentShutdown(EntityUid uid, ParticleAcceleratorControlBoxComponent comp, ComponentShutdown args)
{
if (TryComp<ParticleAcceleratorPartComponent>(uid, out var partStatus))
partStatus.Master = null;
var partQuery = GetEntityQuery<ParticleAcceleratorPartComponent>();
foreach (var part in AllParts(uid, comp))
{ {
if (partQuery.TryGetComponent(part, out var partData)) if (ent.Comp.Powered)
partData.Master = null; {
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 (msg.Enabled)
{ {
if (comp.Assembled) if (_multipartMachine.IsAssembled((uid, null)))
SwitchOn(uid, msg.Actor, comp); SwitchOn(uid, msg.Actor, comp);
} }
else else
@@ -397,9 +416,13 @@ public sealed partial class ParticleAcceleratorSystem
if (TryComp<ApcPowerReceiverComponent>(uid, out var apcPower) && !apcPower.Powered) if (TryComp<ApcPowerReceiverComponent>(uid, out var apcPower) && !apcPower.Powered)
return; 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) public static int GetPANumericalLevel(ParticleAcceleratorPowerState state)

View File

@@ -1,5 +1,6 @@
using Content.Server.ParticleAccelerator.Components; using Content.Server.ParticleAccelerator.Components;
using Content.Server.Singularity.Components; using Content.Server.Singularity.Components;
using Content.Shared.ParticleAccelerator.Components;
using Content.Shared.Projectiles; using Content.Shared.Projectiles;
using Content.Shared.Singularity.Components; using Content.Shared.Singularity.Components;
using Robust.Shared.Physics.Components; using Robust.Shared.Physics.Components;

View File

@@ -1,177 +0,0 @@
using System.Diagnostics.CodeAnalysis;
using Content.Server.ParticleAccelerator.Components;
using JetBrains.Annotations;
using Robust.Shared.Map.Components;
using Robust.Shared.Physics.Events;
namespace Content.Server.ParticleAccelerator.EntitySystems;
[UsedImplicitly]
public sealed partial class ParticleAcceleratorSystem
{
private void InitializePartSystem()
{
SubscribeLocalEvent<ParticleAcceleratorPartComponent, ComponentShutdown>(OnComponentShutdown);
SubscribeLocalEvent<ParticleAcceleratorPartComponent, MoveEvent>(OnMoveEvent);
SubscribeLocalEvent<ParticleAcceleratorPartComponent, PhysicsBodyTypeChangedEvent>(BodyTypeChanged);
}
public void RescanParts(EntityUid uid, EntityUid? user = null, ParticleAcceleratorControlBoxComponent? controller = null)
{
if (!Resolve(uid, ref controller))
return;
if (controller.CurrentlyRescanning)
return;
var partQuery = GetEntityQuery<ParticleAcceleratorPartComponent>();
foreach (var part in AllParts(uid, controller))
{
if (partQuery.TryGetComponent(part, out var partState))
partState.Master = null;
}
controller.Assembled = false;
controller.FuelChamber = null;
controller.EndCap = null;
controller.PowerBox = null;
controller.PortEmitter = null;
controller.ForeEmitter = null;
controller.StarboardEmitter = null;
var xformQuery = GetEntityQuery<TransformComponent>();
if (!xformQuery.TryGetComponent(uid, out var xform) || !xform.Anchored)
{
SwitchOff(uid, user, controller);
return;
}
var gridUid = xform.GridUid;
if (gridUid == null || gridUid != xform.ParentUid || !TryComp<MapGridComponent>(gridUid, out var grid))
{
SwitchOff(uid, user, controller);
return;
}
// Find fuel chamber first by scanning cardinals.
var fuelQuery = GetEntityQuery<ParticleAcceleratorFuelChamberComponent>();
foreach (var adjacent in _mapSystem.GetCardinalNeighborCells(gridUid.Value, grid, xform.Coordinates))
{
if (fuelQuery.HasComponent(adjacent)
&& partQuery.TryGetComponent(adjacent, out var partState)
&& partState.Master == null)
{
controller.FuelChamber = adjacent;
break;
}
}
if (controller.FuelChamber == null)
{
SwitchOff(uid, user, controller);
return;
}
// When we call SetLocalRotation down there to rotate the control box,
// that ends up re-entrantly calling RescanParts() through the move event.
// You'll have to take my word for it that that breaks everything, yeah?
controller.CurrentlyRescanning = true;
// Automatically rotate the control box sprite to face the fuel chamber
var fuelXform = xformQuery.GetComponent(controller.FuelChamber!.Value);
var fuelDir = (fuelXform.LocalPosition - xform.LocalPosition).GetDir();
_transformSystem.SetLocalRotation(uid, fuelDir.ToAngle(), xform);
// Calculate offsets for each of the parts of the PA.
// These are all done relative to the fuel chamber BC that is basically the center of the machine.
var rotation = fuelXform.LocalRotation;
var offsetVect = rotation.GetCardinalDir().ToIntVec();
var orthoOffsetVect = new Vector2i(-offsetVect.Y, offsetVect.X);
var positionFuelChamber = _mapSystem.TileIndicesFor(gridUid!.Value, grid, fuelXform.Coordinates); // n // n: End Cap
var positionEndCap = positionFuelChamber - offsetVect; // CF // C: Control Box, F: Fuel Chamber
var positionPowerBox = positionFuelChamber + offsetVect; // P // P: Power Box
var positionPortEmitter = positionFuelChamber + offsetVect * 2 + orthoOffsetVect; // EEE // E: Emitter (Starboard, Fore, Port)
var positionForeEmitter = positionFuelChamber + offsetVect * 2;
var positionStarboardEmitter = positionFuelChamber + offsetVect * 2 - orthoOffsetVect;
ScanPart<ParticleAcceleratorEndCapComponent>(gridUid.Value, positionEndCap, rotation, out controller.EndCap, out _, grid);
ScanPart<ParticleAcceleratorPowerBoxComponent>(gridUid.Value, positionPowerBox, rotation, out controller.PowerBox, out _, grid);
if (!ScanPart<ParticleAcceleratorEmitterComponent>(gridUid.Value, positionPortEmitter, rotation, out controller.PortEmitter, out var portEmitter, grid)
|| portEmitter.Type != ParticleAcceleratorEmitterType.Port)
controller.PortEmitter = null;
if (!ScanPart<ParticleAcceleratorEmitterComponent>(gridUid.Value, positionForeEmitter, rotation, out controller.ForeEmitter, out var foreEmitter, grid)
|| foreEmitter.Type != ParticleAcceleratorEmitterType.Fore)
controller.ForeEmitter = null;
if (!ScanPart<ParticleAcceleratorEmitterComponent>(gridUid.Value, positionStarboardEmitter, rotation, out controller.StarboardEmitter, out var starboardEmitter, grid)
|| starboardEmitter.Type != ParticleAcceleratorEmitterType.Starboard)
controller.StarboardEmitter = null;
controller.Assembled =
controller.FuelChamber.HasValue
&& controller.EndCap.HasValue
&& controller.PowerBox.HasValue
&& controller.PortEmitter.HasValue
&& controller.ForeEmitter.HasValue
&& controller.StarboardEmitter.HasValue;
foreach (var part in AllParts(uid, controller))
{
if (partQuery.TryGetComponent(part, out var partState))
partState.Master = uid;
}
controller.CurrentlyRescanning = false;
UpdatePowerDraw(uid, controller);
UpdateUI(uid, controller);
}
private bool ScanPart<T>(EntityUid uid, Vector2i coordinates, Angle? rotation, [NotNullWhen(true)] out EntityUid? part, [NotNullWhen(true)] out T? comp, MapGridComponent? grid = null)
where T : IComponent
{
if (!Resolve(uid, ref grid))
{
part = null;
comp = default;
return false;
}
var compQuery = GetEntityQuery<T>();
foreach (var entity in _mapSystem.GetAnchoredEntities(uid, grid, coordinates))
{
if (compQuery.TryGetComponent(entity, out comp)
&& TryComp<ParticleAcceleratorPartComponent>(entity, out var partState) && partState.Master == null
&& (rotation == null || Transform(entity).LocalRotation.EqualsApprox(rotation!.Value.Theta)))
{
part = entity;
return true;
}
}
part = null;
comp = default;
return false;
}
private void OnComponentShutdown(EntityUid uid, ParticleAcceleratorPartComponent comp, ComponentShutdown args)
{
if (Exists(comp.Master))
RescanParts(comp.Master!.Value);
}
private void BodyTypeChanged(EntityUid uid, ParticleAcceleratorPartComponent comp, ref PhysicsBodyTypeChangedEvent args)
{
if (Exists(comp.Master))
RescanParts(comp.Master!.Value);
}
private void OnMoveEvent(EntityUid uid, ParticleAcceleratorPartComponent comp, ref MoveEvent args)
{
if (Exists(comp.Master))
RescanParts(comp.Master!.Value);
}
}

View File

@@ -1,5 +1,7 @@
using Content.Server.ParticleAccelerator.Components; using Content.Server.ParticleAccelerator.Components;
using Content.Server.Power.EntitySystems; using Content.Server.Power.EntitySystems;
using Content.Shared.Machines.Components;
using Content.Shared.ParticleAccelerator.Components;
namespace Content.Server.ParticleAccelerator.EntitySystems; namespace Content.Server.ParticleAccelerator.EntitySystems;
@@ -12,7 +14,7 @@ public sealed partial class ParticleAcceleratorSystem
private void PowerBoxReceivedChanged(EntityUid uid, ParticleAcceleratorPowerBoxComponent component, ref PowerConsumerReceivedChanged args) 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; return;
if (!TryComp<ParticleAcceleratorControlBoxComponent>(part.Master, out var controller)) if (!TryComp<ParticleAcceleratorControlBoxComponent>(part.Master, out var controller))
return; return;

View File

@@ -1,6 +1,7 @@
using Content.Server.Administration.Logs; using Content.Server.Administration.Logs;
using Content.Server.Chat.Managers; using Content.Server.Chat.Managers;
using Content.Server.Projectiles; using Content.Server.Projectiles;
using Content.Server.Machines.EntitySystems;
using Robust.Shared.Physics.Systems; using Robust.Shared.Physics.Systems;
using Robust.Shared.Timing; using Robust.Shared.Timing;
using Robust.Server.GameObjects; using Robust.Server.GameObjects;
@@ -19,13 +20,12 @@ public sealed partial class ParticleAcceleratorSystem : EntitySystem
[Dependency] private readonly SharedPhysicsSystem _physicsSystem = default!; [Dependency] private readonly SharedPhysicsSystem _physicsSystem = default!;
[Dependency] private readonly SharedTransformSystem _transformSystem = default!; [Dependency] private readonly SharedTransformSystem _transformSystem = default!;
[Dependency] private readonly UserInterfaceSystem _uiSystem = default!; [Dependency] private readonly UserInterfaceSystem _uiSystem = default!;
[Dependency] private readonly MapSystem _mapSystem = default!; [Dependency] private readonly MultipartMachineSystem _multipartMachine = default!;
public override void Initialize() public override void Initialize()
{ {
base.Initialize(); base.Initialize();
InitializeControlBoxSystem(); InitializeControlBoxSystem();
InitializePartSystem();
InitializePowerBoxSystem(); InitializePowerBoxSystem();
} }
} }

View File

@@ -1,3 +1,4 @@
using Content.Server.Machines.EntitySystems;
using Content.Server.ParticleAccelerator.Components; using Content.Server.ParticleAccelerator.Components;
using Content.Server.ParticleAccelerator.EntitySystems; using Content.Server.ParticleAccelerator.EntitySystems;
using Content.Server.Wires; using Content.Server.Wires;
@@ -38,10 +39,11 @@ public sealed partial class ParticleAcceleratorPowerWireAction : ComponentWireAc
public override void Pulse(EntityUid user, Wire wire, ParticleAcceleratorControlBoxComponent controller) public override void Pulse(EntityUid user, Wire wire, ParticleAcceleratorControlBoxComponent controller)
{ {
var paSystem = EntityManager.System<ParticleAcceleratorSystem>(); var paSystem = EntityManager.System<ParticleAcceleratorSystem>();
var multipartMachine = EntityManager.System<MultipartMachineSystem>();
if (controller.Enabled) if (controller.Enabled)
paSystem.SwitchOff(wire.Owner, user, controller); paSystem.SwitchOff(wire.Owner, user, controller);
else if (controller.Assembled) else if (multipartMachine.IsAssembled((wire.Owner, null)))
paSystem.SwitchOn(wire.Owner, user, controller); paSystem.SwitchOn(wire.Owner, user, controller);
} }
} }

View File

@@ -1,9 +1,11 @@
using Content.Server.Administration; using Content.Server.Administration;
using Content.Server.Machines.EntitySystems;
using Content.Server.ParticleAccelerator.Components; using Content.Server.ParticleAccelerator.Components;
using Content.Server.ParticleAccelerator.EntitySystems; using Content.Server.ParticleAccelerator.EntitySystems;
using Content.Server.Singularity.Components; using Content.Server.Singularity.Components;
using Content.Server.Singularity.EntitySystems; using Content.Server.Singularity.EntitySystems;
using Content.Shared.Administration; using Content.Shared.Administration;
using Content.Shared.Machines.Components;
using Content.Shared.Singularity.Components; using Content.Shared.Singularity.Components;
using Robust.Shared.Console; using Robust.Shared.Console;
@@ -45,12 +47,15 @@ namespace Content.Server.Singularity
} }
// Setup PA // Setup PA
var multipartMachineManager = entitySystemManager.GetEntitySystem<MultipartMachineSystem>();
var paSystem = entitySystemManager.GetEntitySystem<ParticleAcceleratorSystem>(); var paSystem = entitySystemManager.GetEntitySystem<ParticleAcceleratorSystem>();
var paQuery = entityManager.EntityQueryEnumerator<ParticleAcceleratorControlBoxComponent>(); var paQuery = entityManager.EntityQueryEnumerator<ParticleAcceleratorControlBoxComponent>();
while (paQuery.MoveNext(out var paId, out var paControl)) while (paQuery.MoveNext(out var paId, out var paControl))
{ {
paSystem.RescanParts(paId, controller: paControl); if (!entityManager.TryGetComponent<MultipartMachineComponent>(paId, out var machine))
if (!paControl.Assembled) continue;
if (!multipartMachineManager.Rescan((paId, machine)))
continue; continue;
paSystem.SetStrength(paId, ParticleAcceleratorPowerState.Level0, comp: paControl); paSystem.SetStrength(paId, ParticleAcceleratorPowerState.Level0, comp: paControl);

View File

@@ -0,0 +1,103 @@
using Content.Shared.Machines.EntitySystems;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
using Robust.Shared.Utility;
namespace Content.Shared.Machines.Components;
/// <summary>
/// Marks an entity as being the owner of a multipart machine.
/// </summary>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(raiseAfterAutoHandleState: true)]
[Access(typeof(SharedMultipartMachineSystem))]
public sealed partial class MultipartMachineComponent : Component
{
/// <summary>
/// Dictionary of Enum values to specific parts of this machine.
/// Each key can be specified as 'enum.<EnumName>.<EnumValue>` in Yaml.
/// </summary>
[DataField, AutoNetworkedField]
public Dictionary<Enum, MachinePart> Parts = [];
/// <summary>
/// Whether this multipart machine is assembled or not.
/// Optional parts are not taken into account.
/// </summary>
[DataField, AutoNetworkedField]
public bool IsAssembled = false;
/// <summary>
/// Flag for whether the client side system is allowed to show
/// ghosts of missing machine parts.
/// Controlled/Used by the client side.
/// </summary>
public List<EntityUid> Ghosts = [];
}
[DataDefinition]
[Serializable, NetSerializable]
public sealed partial class MachinePart
{
/// <summary>
/// Component type that is expected for this part to have
/// to be considered a "Part" of the machine.
/// </summary>
[DataField(required: true, customTypeSerializer: typeof(ComponentNameSerializer))]
public string Component = "";
/// <summary>
/// Expected offset to find this machine at.
/// </summary>
[DataField(required: true)]
public Vector2i Offset;
/// <summary>
/// Whether this part is required for the machine to be
/// considered "assembled", or is considered an optional extra.
/// </summary>
[DataField]
public bool Optional = false;
/// <summary>
/// ID of prototype, used to show sprite and description of part, when user examines the machine and there
/// is no matched entity. Can reference dummy entities to give more detailed descriptions.
/// </summary>
[DataField]
public EntProtoId? GhostProto = null;
/// <summary>
/// Expected rotation for this machine to have.
/// </summary>
[DataField]
public Angle Rotation = Angle.Zero;
/// <summary>
/// Network entity, used to inform clients and update their side of the component
/// locally.
/// Use the Entity attribute if you wish to get which entity is actually bound to this part.
/// </summary>
public NetEntity? NetEntity = null;
/// <summary>
/// Entity associated with this part.
/// Not null when an entity is successfully matched to the part and null otherwise.
/// </summary>
[DataField, NonSerialized]
public EntityUid? Entity = null;
/// <summary>
/// Expected graph for this part to use as part of its construction.
/// </summary>
[DataField]
public EntProtoId Graph;
/// <summary>
/// Expected node for this part to be in, on the graph.
/// Used to determine when a construct-able object has been
/// assembled or disassembled.
/// </summary>
[DataField]
public string ExpectedNode;
}

View File

@@ -0,0 +1,17 @@
using Robust.Shared.GameStates;
namespace Content.Shared.Machines.Components;
/// <summary>
/// Component for marking entities as part of a multipart machine.
/// </summary>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
public sealed partial class MultipartMachinePartComponent : Component
{
/// <summary>
/// Links to the entity which holds the MultipartMachineComponent.
/// Useful so that entities that know which machine they are a part of.
/// </summary>
[DataField, AutoNetworkedField]
public EntityUid? Master = null;
}

View File

@@ -0,0 +1,115 @@
using System.Diagnostics.CodeAnalysis;
using Content.Shared.Machines.Components;
namespace Content.Shared.Machines.EntitySystems;
/// <summary>
/// Shared handling of multipart machines.
/// </summary>
public abstract class SharedMultipartMachineSystem : EntitySystem
{
protected EntityQuery<TransformComponent> XformQuery;
public override void Initialize()
{
base.Initialize();
XformQuery = GetEntityQuery<TransformComponent>();
}
/// <summary>
/// Returns whether each non-optional part of the machine has a matched entity
/// </summary>
/// <param name="ent">Entity to check the assembled state of.</param>
/// <returns>True if all non-optional parts have a matching entity, false otherwise.</returns>
public bool IsAssembled(Entity<MultipartMachineComponent?> ent)
{
if (!Resolve(ent, ref ent.Comp))
return false;
foreach (var part in ent.Comp.Parts.Values)
{
if (!part.Entity.HasValue && !part.Optional)
return false;
}
return true;
}
/// <summary>
/// Returns whether a machine has a specifed EntityUid bound to one of its parts.
/// </summary>
/// <param name="machine">Entity, which might have a multpart machine attached, to use for the query.</param>
/// <param name="entity">EntityUid to search for.</param>
/// <returns>True if any part has the specified EntityUid, false otherwise.</returns>
public bool HasPartEntity(Entity<MultipartMachineComponent?> machine, EntityUid entity)
{
if (!Resolve(machine, ref machine.Comp))
return false;
foreach (var part in machine.Comp.Parts.Values)
{
if (part.Entity.HasValue && part.Entity.Value == entity)
return true;
}
return false;
}
/// <summary>
/// Get the EntityUid for the entity bound to a specific part, if one exists.
/// </summary>
/// <param name="ent">Entity, which might have a multipart machine attached, to use for the query.</param>
/// <param name="part">Enum value for the part to find, must match the value specified in YAML.</param>
/// <returns>May contain the resolved EntityUid for the specified part, null otherwise.</returns>
public EntityUid? GetPartEntity(Entity<MultipartMachineComponent?> ent, Enum part)
{
if (!TryGetPartEntity(ent, part, out var entity))
return null;
return entity;
}
/// <summary>
/// Get the EntityUid for the entity bound to a specific part, if one exists.
/// </summary>
/// <param name="ent">Entity, which might have a multipart machine attached, to use for the query.</param>
/// <param name="part">Enum for the part to find, must match the value specified in YAML.</param>
/// <param name="entity">Out var which may contain the matched EntityUid for the specified part.</param>
/// <returns>True if the part is found and has a matched entity, false otherwise.</returns>
public bool TryGetPartEntity(
Entity<MultipartMachineComponent?> ent,
Enum part,
[NotNullWhen(true)] out EntityUid? entity
)
{
entity = null;
if (!Resolve(ent, ref ent.Comp))
return false;
if (ent.Comp.Parts.TryGetValue(part, out var value) && value.Entity.HasValue)
{
entity = value.Entity.Value;
return true;
}
return false;
}
/// <summary>
/// Check if a machine has an entity bound to a specific part
/// </summary>
/// <param name="ent">Entity, which might have a multipart machine attached, to use for the query.</param>
/// <param name="part">Enum for the part to find.</param>
/// <returns>True if the specific part has a entity bound to it, false otherwise.</returns>
public bool HasPart(Entity<MultipartMachineComponent?> ent, Enum part)
{
if (!Resolve(ent, ref ent.Comp))
return false;
if (!ent.Comp.Parts.TryGetValue(part, out var value))
return false;
return value.Entity != null;
}
}

View File

@@ -0,0 +1,21 @@
namespace Content.Shared.Machines.Events;
/// <summary>
/// This event is raised when the assembled state of a Multipart machine changes.
/// This includes when optional parts are found, parts become unanchored, or move
/// within a construction graph.
/// </summary>
/// <param name="Entity">Entity that is bound to the multipart machine.</param>
/// <param name="IsAssembled">Assembled state of the machine.</param>
/// <param name="User">Optional user that may have caused the assembly state to change.</param>
/// <param name="PartsAdded">Dictionary of keys to entities of parts that have been added to this machine.</param>
/// <param name="PartsRemoved">Dictionary of keys to entities of parts that have been removed from this machine.</param>
[ByRefEvent]
public record struct MultipartMachineAssemblyStateChanged(
EntityUid Entity,
bool IsAssembled,
EntityUid? User,
Dictionary<Enum, EntityUid> PartsAdded,
Dictionary<Enum, EntityUid> PartsRemoved)
{
}

View File

@@ -0,0 +1,16 @@
using Robust.Shared.Serialization;
namespace Content.Shared.ParticleAccelerator;
[Serializable, NetSerializable]
public enum AcceleratorParts : byte
{
EndCap,
FuelChamber,
PowerBox,
PortEmitter,
ForeEmitter,
StarboardEmitter
};

View File

@@ -1,6 +1,6 @@
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
namespace Content.Server.ParticleAccelerator.Components; namespace Content.Shared.ParticleAccelerator.Components;
[RegisterComponent] [RegisterComponent]
public sealed partial class ParticleAcceleratorEmitterComponent : Component public sealed partial class ParticleAcceleratorEmitterComponent : Component

View File

@@ -1,4 +1,4 @@
namespace Content.Server.ParticleAccelerator.Components; namespace Content.Shared.ParticleAccelerator.Components;
[RegisterComponent] [RegisterComponent]
public sealed partial class ParticleAcceleratorEndCapComponent : Component public sealed partial class ParticleAcceleratorEndCapComponent : Component

View File

@@ -1,4 +1,4 @@
namespace Content.Server.ParticleAccelerator.Components; namespace Content.Shared.ParticleAccelerator.Components;
[RegisterComponent] [RegisterComponent]
public sealed partial class ParticleAcceleratorFuelChamberComponent : Component public sealed partial class ParticleAcceleratorFuelChamberComponent : Component

View File

@@ -1,4 +1,4 @@
namespace Content.Server.ParticleAccelerator.Components; namespace Content.Shared.ParticleAccelerator.Components;
[RegisterComponent] [RegisterComponent]
public sealed partial class ParticleAcceleratorPowerBoxComponent : Component public sealed partial class ParticleAcceleratorPowerBoxComponent : Component

View File

@@ -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

View File

@@ -43,7 +43,6 @@
map: [ "enum.ParticleAcceleratorVisualLayers.Unlit" ] map: [ "enum.ParticleAcceleratorVisualLayers.Unlit" ]
shader: unshaded shader: unshaded
visible: false visible: false
- type: ParticleAcceleratorPart
- type: ParticleAcceleratorPartVisuals - type: ParticleAcceleratorPartVisuals
stateBase: unlit stateBase: unlit
- type: Construction - type: Construction

View File

@@ -27,6 +27,50 @@
layoutId: ParticleAccelerator layoutId: ParticleAccelerator
- type: AccessReader - type: AccessReader
access: [["Engineering"]] 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 # Unfinished

View File

@@ -10,6 +10,7 @@
emitterType: Port emitterType: Port
- type: Construction - type: Construction
graph: ParticleAcceleratorEmitterPort graph: ParticleAcceleratorEmitterPort
- type: MultipartMachinePart
- type: entity - type: entity
parent: ParticleAcceleratorFinishedPart parent: ParticleAcceleratorFinishedPart
@@ -23,6 +24,7 @@
emitterType: Fore emitterType: Fore
- type: Construction - type: Construction
graph: ParticleAcceleratorEmitterFore graph: ParticleAcceleratorEmitterFore
- type: MultipartMachinePart
- type: entity - type: entity
parent: ParticleAcceleratorFinishedPart parent: ParticleAcceleratorFinishedPart
@@ -36,6 +38,7 @@
emitterType: Starboard emitterType: Starboard
- type: Construction - type: Construction
graph: ParticleAcceleratorEmitterStarboard graph: ParticleAcceleratorEmitterStarboard
- type: MultipartMachinePart
# Unfinished # Unfinished
@@ -50,6 +53,7 @@
sprite: Structures/Power/Generation/PA/emitter_port.rsi sprite: Structures/Power/Generation/PA/emitter_port.rsi
- type: Construction - type: Construction
graph: ParticleAcceleratorEmitterPort graph: ParticleAcceleratorEmitterPort
- type: MultipartMachinePart
- type: entity - type: entity
parent: ParticleAcceleratorUnfinishedBase parent: ParticleAcceleratorUnfinishedBase
@@ -62,6 +66,7 @@
sprite: Structures/Power/Generation/PA/emitter_fore.rsi sprite: Structures/Power/Generation/PA/emitter_fore.rsi
- type: Construction - type: Construction
graph: ParticleAcceleratorEmitterFore graph: ParticleAcceleratorEmitterFore
- type: MultipartMachinePart
- type: entity - type: entity
parent: ParticleAcceleratorUnfinishedBase parent: ParticleAcceleratorUnfinishedBase
@@ -74,3 +79,4 @@
sprite: Structures/Power/Generation/PA/emitter_starboard.rsi sprite: Structures/Power/Generation/PA/emitter_starboard.rsi
- type: Construction - type: Construction
graph: ParticleAcceleratorEmitterStarboard graph: ParticleAcceleratorEmitterStarboard
- type: MultipartMachinePart

View File

@@ -12,6 +12,7 @@
- type: ParticleAcceleratorEndCap - type: ParticleAcceleratorEndCap
- type: Construction - type: Construction
graph: ParticleAcceleratorEndCap graph: ParticleAcceleratorEndCap
- type: MultipartMachinePart
# Unfinished # Unfinished
@@ -26,3 +27,4 @@
sprite: Structures/Power/Generation/PA/end_cap.rsi sprite: Structures/Power/Generation/PA/end_cap.rsi
- type: Construction - type: Construction
graph: ParticleAcceleratorEndCap graph: ParticleAcceleratorEndCap
- type: MultipartMachinePart

View File

@@ -9,6 +9,7 @@
- type: ParticleAcceleratorFuelChamber - type: ParticleAcceleratorFuelChamber
- type: Construction - type: Construction
graph: ParticleAcceleratorFuelChamber graph: ParticleAcceleratorFuelChamber
- type: MultipartMachinePart
# Unfinished # Unfinished
@@ -23,3 +24,4 @@
sprite: Structures/Power/Generation/PA/fuel_chamber.rsi sprite: Structures/Power/Generation/PA/fuel_chamber.rsi
- type: Construction - type: Construction
graph: ParticleAcceleratorFuelChamber graph: ParticleAcceleratorFuelChamber
- type: MultipartMachinePart

View File

@@ -17,6 +17,7 @@
nodeGroupID: MVPower nodeGroupID: MVPower
- type: Construction - type: Construction
graph: ParticleAcceleratorPowerBox graph: ParticleAcceleratorPowerBox
- type: MultipartMachinePart
- type: entity - type: entity
parent: ParticleAcceleratorUnfinishedBase parent: ParticleAcceleratorUnfinishedBase
@@ -29,3 +30,4 @@
sprite: Structures/Power/Generation/PA/power_box.rsi sprite: Structures/Power/Generation/PA/power_box.rsi
- type: Construction - type: Construction
graph: ParticleAcceleratorPowerBox graph: ParticleAcceleratorPowerBox
- type: MultipartMachinePart