diff --git a/Content.Client/Machines/Components/MultipartMachineGhostComponent.cs b/Content.Client/Machines/Components/MultipartMachineGhostComponent.cs new file mode 100644 index 0000000000..8fe4f3d386 --- /dev/null +++ b/Content.Client/Machines/Components/MultipartMachineGhostComponent.cs @@ -0,0 +1,14 @@ +namespace Content.Client.Machines.Components; + +/// +/// Component attached to all multipart machine ghosts +/// Intended for client side usage only, but used on prototypes. +/// +[RegisterComponent] +public sealed partial class MultipartMachineGhostComponent : Component +{ + /// + /// Machine this particular ghost is linked to. + /// + public EntityUid? LinkedMachine = null; +} diff --git a/Content.Client/Machines/EntitySystems/MultipartMachineSystem.cs b/Content.Client/Machines/EntitySystems/MultipartMachineSystem.cs new file mode 100644 index 0000000000..4919a5e8f2 --- /dev/null +++ b/Content.Client/Machines/EntitySystems/MultipartMachineSystem.cs @@ -0,0 +1,109 @@ +using Content.Client.Examine; +using Content.Client.Machines.Components; +using Content.Shared.Machines.Components; +using Content.Shared.Machines.EntitySystems; +using Robust.Client.GameObjects; +using Robust.Shared.Map; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization.Manager; +using Robust.Shared.Spawners; + +namespace Content.Client.Machines.EntitySystems; + +/// +/// Client side handling of multipart machines. +/// Handles client side examination events to show the expected layout of the machine +/// based on the origin of the main entity. +/// +public sealed class MultipartMachineSystem : SharedMultipartMachineSystem +{ + private readonly EntProtoId _ghostPrototype = "MultipartMachineGhost"; + private readonly Color _partiallyTransparent = new Color(255, 255, 255, 180); + + [Dependency] private readonly SpriteSystem _sprite = default!; + [Dependency] private readonly IPrototypeManager _prototype = default!; + [Dependency] private readonly MetaDataSystem _metaData = default!; + [Dependency] private readonly ISerializationManager _serialization= default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnMachineExamined); + SubscribeLocalEvent(OnHandleState); + SubscribeLocalEvent(OnGhostDespawned); + } + + /// + /// Handles spawning several ghost sprites to show where the different parts of the machine + /// should go and the rotations they're expected to have. + /// Can only show one set of ghost parts at a time and their location depends on the current map/grid + /// location of the origin machine. + /// + /// Entity/Component that has been inspected. + /// Args for the event. + private void OnMachineExamined(Entity ent, ref ClientExaminedEvent args) + { + if (ent.Comp.Ghosts.Count != 0) + { + // Already showing some part ghosts + return; + } + + foreach (var part in ent.Comp.Parts.Values) + { + if (part.Entity.HasValue) + continue; + + var entityCoords = new EntityCoordinates(ent.Owner, part.Offset); + var ghostEnt = Spawn(_ghostPrototype, entityCoords); + + if (!XformQuery.TryGetComponent(ghostEnt, out var xform)) + break; + + xform.LocalRotation = part.Rotation; + + Comp(ghostEnt).LinkedMachine = ent; + + ent.Comp.Ghosts.Add(ghostEnt); + + if (part.GhostProto == null) + continue; + + var entProto = _prototype.Index(part.GhostProto.Value); + if (!entProto.Components.TryGetComponent("Sprite", out var s) || s is not SpriteComponent protoSprite) + return; + + var ghostSprite = EnsureComp(ghostEnt); + _serialization.CopyTo(protoSprite, ref ghostSprite, notNullableOverride: true); + + _sprite.SetColor((ghostEnt, ghostSprite), _partiallyTransparent); + + _metaData.SetEntityName(ghostEnt, entProto.Name); + _metaData.SetEntityDescription(ghostEnt, entProto.Description); + } + } + + private void OnHandleState(Entity ent, ref AfterAutoHandleStateEvent args) + { + foreach (var part in ent.Comp.Parts.Values) + { + part.Entity = part.NetEntity.HasValue ? EnsureEntity(part.NetEntity.Value, ent) : null; + } + } + + /// + /// Handles when a ghost part despawns after its short lifetime. + /// Will attempt to remove itself from the list of known ghost entities in the main multipart + /// machine component. + /// + /// Ghost entity that has been despawned. + /// Args for the event. + private void OnGhostDespawned(Entity ent, ref TimedDespawnEvent args) + { + if (!TryComp(ent.Comp.LinkedMachine, out var machine)) + return; + + machine.Ghosts.Remove(ent); + } +} diff --git a/Content.Client/ParticleAccelerator/UI/ParticleAcceleratorControlMenu.xaml b/Content.Client/ParticleAccelerator/UI/ParticleAcceleratorControlMenu.xaml index 7cef7d58b6..d05262f72d 100644 --- a/Content.Client/ParticleAccelerator/UI/ParticleAcceleratorControlMenu.xaml +++ b/Content.Client/ParticleAccelerator/UI/ParticleAcceleratorControlMenu.xaml @@ -127,7 +127,7 @@ - + diff --git a/Content.Client/ParticleAccelerator/UI/ParticleAcceleratorControlMenu.xaml.cs b/Content.Client/ParticleAccelerator/UI/ParticleAcceleratorControlMenu.xaml.cs index 8b21e7d94b..cc5016c4a7 100644 --- a/Content.Client/ParticleAccelerator/UI/ParticleAcceleratorControlMenu.xaml.cs +++ b/Content.Client/ParticleAccelerator/UI/ParticleAcceleratorControlMenu.xaml.cs @@ -268,6 +268,7 @@ public sealed class PASegmentControl : Control private RSI? _rsi; public string BaseState { get; set; } = "control_box"; + public bool DefaultVisible { get; set; } = false; public PASegmentControl() { @@ -283,12 +284,14 @@ public sealed class PASegmentControl : Control _rsi = IoCManager.Resolve().GetResource($"/Textures/Structures/Power/Generation/PA/{BaseState}.rsi").RSI; MinSize = _rsi.Size; _base.Texture = _rsi["completed"].Frame0; + + SetVisible(DefaultVisible); + _unlit.Visible = DefaultVisible; } public void SetPowerState(ParticleAcceleratorUIState state, bool exists) { - _base.ShaderOverride = exists ? null : _greyScaleShader; - _base.ModulateSelfOverride = exists ? null : new Color(127, 127, 127); + SetVisible(exists); if (!state.Enabled || !exists) { @@ -319,4 +322,23 @@ public sealed class PASegmentControl : Control _unlit.Texture = rState.Frame0; } + + /// + /// Adds/Removes the shading to the part in the control menu based on the + /// input state. + /// + /// True if the part exists, false otherwise + private void SetVisible(bool state) + { + if (state) + { + _base.ShaderOverride = null; + _base.ModulateSelfOverride = null; + } + else + { + _base.ShaderOverride = _greyScaleShader; + _base.ModulateSelfOverride = new Color(127, 127, 127); + } + } } diff --git a/Content.Server/Construction/ConstructionSystem.Graph.cs b/Content.Server/Construction/ConstructionSystem.Graph.cs index 0027b941f8..7d4dd6153d 100644 --- a/Content.Server/Construction/ConstructionSystem.Graph.cs +++ b/Content.Server/Construction/ConstructionSystem.Graph.cs @@ -258,7 +258,7 @@ namespace Content.Server.Construction // ChangeEntity will handle the pathfinding update. if (node.Entity.GetId(uid, userUid, new(EntityManager)) is { } newEntity - && ChangeEntity(uid, userUid, newEntity, construction) != null) + && ChangeEntity(uid, userUid, newEntity, construction, oldNode) != null) return true; if (performActions) @@ -281,6 +281,7 @@ namespace Content.Server.Construction /// An optional user entity, for actions. /// The entity prototype identifier for the new entity. /// The construction component of the target entity. Will be resolved if null. + /// The previous node, if any, this graph was on before changing entity. /// The metadata component of the target entity. Will be resolved if null. /// The transform component of the target entity. Will be resolved if null. /// The container manager component of the target entity. Will be resolved if null, @@ -288,6 +289,7 @@ namespace Content.Server.Construction /// The new entity, or null if the method did not succeed. private EntityUid? ChangeEntity(EntityUid uid, EntityUid? userUid, string newEntity, ConstructionComponent? construction = null, + string? previousNode = null, MetaDataComponent? metaData = null, TransformComponent? transform = null, ContainerManagerComponent? containerManager = null) @@ -407,6 +409,11 @@ namespace Content.Server.Construction QueueDel(uid); + // If ChangeEntity has ran, then the entity uid has changed and the + // new entity should be initialized by this point. + var afterChangeEv = new AfterConstructionChangeEntityEvent(construction.Graph, construction.Node, previousNode); + RaiseLocalEvent(newUid, ref afterChangeEv); + return newUid; } @@ -453,4 +460,16 @@ namespace Content.Server.Construction Old = oldUid; } } + + /// + /// This event is raised after an entity changes prototype/uid during construction. + /// This is only raised at the new entity, after it has been initialized. + /// + /// Construction graph for this entity. + /// New node that has become active. + /// Previous node that was active on the graph. + [ByRefEvent] + public record struct AfterConstructionChangeEntityEvent(string Graph, string CurrentNode, string? PreviousNode) + { + } } diff --git a/Content.Server/Entry/IgnoredComponents.cs b/Content.Server/Entry/IgnoredComponents.cs index 04c4566227..58264e14ad 100644 --- a/Content.Server/Entry/IgnoredComponents.cs +++ b/Content.Server/Entry/IgnoredComponents.cs @@ -19,7 +19,8 @@ namespace Content.Server.Entry "InventorySlots", "LightFade", "HolidayRsiSwap", - "OptionsVisualizer" + "OptionsVisualizer", + "MultipartMachineGhost" }; } } diff --git a/Content.Server/Machines/EntitySystems/MultipartMachineSystem.cs b/Content.Server/Machines/EntitySystems/MultipartMachineSystem.cs new file mode 100644 index 0000000000..04903241e3 --- /dev/null +++ b/Content.Server/Machines/EntitySystems/MultipartMachineSystem.cs @@ -0,0 +1,352 @@ +using Content.Server.Construction; +using Content.Server.Construction.Components; +using Content.Shared.Machines.Components; +using Content.Shared.Machines.EntitySystems; +using Content.Shared.Machines.Events; +using Robust.Server.GameObjects; +using Robust.Shared.Map.Components; + +namespace Content.Server.Machines.EntitySystems; + +/// +/// Server side handling of multipart machines. +/// When requested, performs scans of the map area around the specified entity +/// to find and match parts of the machine. +/// +public sealed class MultipartMachineSystem : SharedMultipartMachineSystem +{ + [Dependency] private readonly IComponentFactory _factory = default!; + [Dependency] private readonly MapSystem _mapSystem = default!; + [Dependency] private readonly EntityLookupSystem _lookupSystem = default!; + + // The largest size ANY machine can theoretically have. + // Used to aid search for machines in range of parts that have been anchored/constructed. + private const float MaximumRange = 30; + private readonly HashSet> _entitiesInRange = []; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnComponentStartup); + + SubscribeLocalEvent(OnMachineAnchorChanged); + + SubscribeLocalEvent(OnPartConstructionNodeChanged); + SubscribeLocalEvent(OnPartAnchorChanged); + } + + /// + /// Clears the matched entity from the specified part + /// + /// Entity to clear the part for. + /// Enum value for the part to clear. + public void ClearPartEntity(Entity ent, Enum part) + { + if (!Resolve(ent, ref ent.Comp)) + return; + + if (!ent.Comp.Parts.TryGetValue(part, out var value)) + return; + + if (!value.Entity.HasValue) + return; + + var partEnt = value.Entity.Value; + var partComp = EnsureComp(partEnt); + + if (partComp.Master.HasValue) + { + partComp.Master = null; + Dirty(partEnt, partComp); + } + + value.Entity = null; + Dirty(ent); + } + + /// + /// Performs a rescan of all parts of the machine to confirm they exist and match + /// the specified requirements for offset, rotation, and components. + /// + /// Entity to rescan for. + /// Optional user entity which has caused this rescan. + /// True the state of the machine's assembly has changed, false otherwise. + public bool Rescan(Entity ent, EntityUid? user = null) + { + // Get all required transform information to start looking for the other parts based on their offset + if (!XformQuery.TryGetComponent(ent.Owner, out var xform) || !xform.Anchored) + return false; + + var gridUid = xform.GridUid; + if (gridUid == null || gridUid != xform.ParentUid || !TryComp(gridUid, out var grid)) + return false; + + // Whichever component has the MultipartMachine component should be treated as the origin + var machineOrigin = _mapSystem.TileIndicesFor(gridUid.Value, grid, xform.Coordinates); + + // Set to true if any of the parts' state changes + var stateHasChanged = false; + + // Keep a track of what parts were added or removed so we can inform listeners + Dictionary partsAdded = []; + Dictionary partsRemoved = []; + + var missingParts = false; + var machineRotation = xform.LocalRotation.GetCardinalDir().ToAngle(); + foreach (var (key, part) in ent.Comp.Parts) + { + var originalPart = part.Entity; + part.Entity = null; + + if (!_factory.TryGetRegistration(part.Component, out var registration)) + break; + + var query = EntityManager.GetEntityQuery(registration.Type); + + ScanPart(machineOrigin, machineRotation, query, gridUid.Value, grid, part); + + if (!part.Entity.HasValue && !part.Optional) + missingParts = true; + + if (part.Entity == originalPart) + continue; // Nothing has changed here + + stateHasChanged = true; + + MultipartMachinePartComponent comp; + EntityUid partEnt; + if (part.Entity.HasValue) + { + // This part gained an entity, add the Part component so it can find out which machine + // it's a part of + partEnt = part.Entity.Value; + comp = EnsureComp(partEnt); + comp.Master = ent; + partsAdded.Add(key, partEnt); + } + else + { + // This part lost its entity, ensure we clean up the old entity so it's no longer marked + // as something we care about. + partEnt = originalPart!.Value; + comp = EnsureComp(partEnt); + comp.Master = null; + partsRemoved.Add(key, partEnt); + } + + Dirty(partEnt, comp); + } + + ent.Comp.IsAssembled = !missingParts; + if (stateHasChanged) + { + var ev = new MultipartMachineAssemblyStateChanged( + ent, + ent.Comp.IsAssembled, + user, + partsAdded, + partsRemoved + ); + RaiseLocalEvent(ent, ref ev); + + Dirty(ent); + } + + return stateHasChanged; + } + + /// + /// Clears all entities bound to parts for a specified machine. + /// Will also raise the assembly state change and dirty event for it. + /// + /// Machine to completely clear the parts of. + private void ClearAllParts(Entity ent) + { + var stateHasChanged = false; + + Dictionary clearedParts = []; + foreach (var (key, part) in ent.Comp.Parts) + { + if (!part.Entity.HasValue) + continue; + + stateHasChanged = true; + var partEntity = part.Entity.Value; + var partComp = EnsureComp(partEntity); + + clearedParts.Add(key, partEntity); + + part.Entity = null; + part.NetEntity = null; + + partComp.Master = null; + Dirty(partEntity, partComp); + } + + ent.Comp.IsAssembled = false; + + if (stateHasChanged) + { + var ev = new MultipartMachineAssemblyStateChanged( + ent, + ent.Comp.IsAssembled, + null, + [], + clearedParts + ); + RaiseLocalEvent(ent, ref ev); + + Dirty(ent); + } + } + + /// + /// Handles any additional setup of the MultipartMachine component. + /// + /// Entity/Component that just started. + /// Args for the startup. + private void OnComponentStartup(Entity ent, ref ComponentStartup args) + { + // If anchored, perform a rescan of this machine when the component starts so we can immediately + // jump to an assembled state if needed. + if (XformQuery.TryGetComponent(ent.Owner, out var xform) && xform.Anchored) + Rescan(ent); + } + + /// + /// Handles when a main machine entity has been anchored or unanchored by a user. + /// Rescanning is then required in order to check whether parts are still in the right places, + /// and raise a AfterConstructionChangeEntityEvent. + /// + /// Machine entity that has been anchored or unanchored. + /// Args for this event. + private void OnMachineAnchorChanged(Entity ent, + ref AnchorStateChangedEvent args) + { + if (args.Anchored) + Rescan(ent); + else + ClearAllParts(ent); + } + + /// + /// Handles when a machine part entity has been created due to a move in a construction graph. + /// Rescans all known multipart machines within range that have a part which matches that specific graph + /// and node IDs. + /// + /// Machine part entity that has moved in a graph. + /// Args for this event. + private void OnPartConstructionNodeChanged(Entity ent, + ref AfterConstructionChangeEntityEvent args) + { + if (!XformQuery.TryGetComponent(ent.Owner, out var constructXform)) + return; + + _lookupSystem.GetEntitiesInRange(constructXform.Coordinates, MaximumRange, _entitiesInRange); + foreach (var machine in _entitiesInRange) + { + foreach (var part in machine.Comp.Parts.Values) + { + if (args.Graph == part.Graph && + (args.PreviousNode == part.ExpectedNode || args.CurrentNode == part.ExpectedNode)) + { + Rescan(machine); + break; // No need to scan the same machine again + } + } + } + } + + /// + /// Handles when a machine part entity has been anchored or unanchored by a user. + /// We might be able to link an unanchored part to a machine, but anchoring a constructable entity, + /// which machine parts are, will require a rescan of all machines within range as we have no idea + /// what machine it might be a part of. + /// + /// Machine part entity that has been anchored or unanchored. + /// Args for this event, notably the anchor status. + private void OnPartAnchorChanged(Entity ent, ref AnchorStateChangedEvent args) + { + if (!args.Anchored) + { + if (!TryComp(ent.Owner, out var part) || !part.Master.HasValue) + return; // This is not an entity we care about + + // This is a machine part that is being unanchored, rescan its machine + if (!TryComp(part.Master, out var machine)) + return; + + Rescan((part.Master.Value, machine)); + return; + } + + // We're anchoring some construction, we have no idea which machine this might be for + // so we have to just check everyone in range and perform a rescan. + if (!XformQuery.TryGetComponent(ent.Owner, out var constructXform)) + return; + + _lookupSystem.GetEntitiesInRange(constructXform.Coordinates, MaximumRange, _entitiesInRange); + foreach (var machine in _entitiesInRange) + { + if (Rescan(machine) && HasPartEntity(machine.AsNullable(), ent.Owner)) + return; // This machine is using this entity so we don't need to go any further + } + } + + /// + /// Scans the specified coordinates for any anchored entities that might match the given + /// component and rotation requirements. + /// + /// Origin coordinates for the machine. + /// Rotation of the master entity to use when searching for this part. + /// Entity query for the specific component the entity must have. + /// EntityUID of the grid to use for the lookup. + /// Grid to use for the lookup. + /// Part we're searching for. + /// True when part is found and matches requirements, false otherwise. + private bool ScanPart( + Vector2i machineOrigin, + Angle rotation, + EntityQuery query, + EntityUid gridUid, + MapGridComponent grid, + MachinePart part) + { + // Safety first, nuke any existing data + part.Entity = null; + + var expectedLocation = machineOrigin + part.Offset.Rotate(rotation); + var expectedRotation = part.Rotation + rotation; + + foreach (var entity in _mapSystem.GetAnchoredEntities(gridUid, grid, expectedLocation)) + { + if (TerminatingOrDeleted(entity)) + { + // Ignore entities which are in the process of being deleted + continue; + } + + if (!query.TryGetComponent(entity, out var comp) || + !Transform(entity).LocalRotation.EqualsApprox(expectedRotation.Theta)) + { + // Either has no transform, or doesn't match the rotation + continue; + } + + if (!TryComp(entity, out var construction) || + construction.Graph != part.Graph || + construction.Node != part.ExpectedNode) + { + // This constructable doesn't match the right graph we expect + continue; + } + + part.Entity = entity; + part.NetEntity = GetNetEntity(entity); + return true; + } + + return false; + } +} diff --git a/Content.Server/ParticleAccelerator/Components/ParticleAcceleratorControlBoxComponent.cs b/Content.Server/ParticleAccelerator/Components/ParticleAcceleratorControlBoxComponent.cs index b460e96acc..35d9dbde8f 100644 --- a/Content.Server/ParticleAccelerator/Components/ParticleAcceleratorControlBoxComponent.cs +++ b/Content.Server/ParticleAccelerator/Components/ParticleAcceleratorControlBoxComponent.cs @@ -13,12 +13,6 @@ namespace Content.Server.ParticleAccelerator.Components; [RegisterComponent] public sealed partial class ParticleAcceleratorControlBoxComponent : Component { - /// - /// Whether the PA parts have been correctly arranged to make a functional device. - /// - [ViewVariables] - public bool Assembled = false; - /// /// Whether the PA is currently set to fire at the console. /// Requires to be true. @@ -40,12 +34,6 @@ public sealed partial class ParticleAcceleratorControlBoxComponent : Component [ViewVariables] public bool Firing = false; - /// - /// Block re-entrant rescanning. - /// - [ViewVariables(VVAccess.ReadWrite)] - public bool CurrentlyRescanning = false; - /// /// Whether the PA is currently firing or charging to fire. /// Bounded by and . @@ -61,48 +49,6 @@ public sealed partial class ParticleAcceleratorControlBoxComponent : Component [ViewVariables] public ParticleAcceleratorPowerState MaxStrength = ParticleAcceleratorPowerState.Level2; - /// - /// The power supply unit of the assembled particle accelerator. - /// Implies the existance of a attached to this entity. - /// - [ViewVariables] - public EntityUid? PowerBox; - - /// - /// Whether the PA is currently firing or charging to fire. - /// Implies the existance of a attached to this entity. - /// - [ViewVariables] - public EntityUid? EndCap; - - /// - /// Whether the PA is currently firing or charging to fire. - /// Implies the existance of a attached to this entity. - /// - [ViewVariables] - public EntityUid? FuelChamber; - - /// - /// Whether the PA is currently firing or charging to fire. - /// Implies the existance of a attached to this entity. - /// - [ViewVariables] - public EntityUid? PortEmitter; - - /// - /// Whether the PA is currently firing or charging to fire. - /// Implies the existance of a attached to this entity. - /// - [ViewVariables] - public EntityUid? ForeEmitter; - - /// - /// Whether the PA is currently firing or charging to fire. - /// Implies the existance of a attached to this entity. - /// - [ViewVariables] - public EntityUid? StarboardEmitter; - /// /// The amount of power the particle accelerator must be provided with relative to the expected power draw to function. /// diff --git a/Content.Server/ParticleAccelerator/Components/ParticleAcceleratorPartComponent.cs b/Content.Server/ParticleAccelerator/Components/ParticleAcceleratorPartComponent.cs deleted file mode 100644 index 6d2b7b8960..0000000000 --- a/Content.Server/ParticleAccelerator/Components/ParticleAcceleratorPartComponent.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Content.Server.ParticleAccelerator.Components; - -[RegisterComponent] -public sealed partial class ParticleAcceleratorPartComponent : Component -{ - [ViewVariables] - public EntityUid? Master; -} diff --git a/Content.Server/ParticleAccelerator/EntitySystems/ParticleAcceleratorSystem.ControlBox.cs b/Content.Server/ParticleAccelerator/EntitySystems/ParticleAcceleratorSystem.ControlBox.cs index 90a5cb2ea0..783c1b6174 100644 --- a/Content.Server/ParticleAccelerator/EntitySystems/ParticleAcceleratorSystem.ControlBox.cs +++ b/Content.Server/ParticleAccelerator/EntitySystems/ParticleAcceleratorSystem.ControlBox.cs @@ -1,6 +1,7 @@ using Content.Server.ParticleAccelerator.Components; using Content.Server.Power.Components; using Content.Shared.Database; +using Content.Shared.Machines.Components; using Content.Shared.Singularity.Components; using Robust.Shared.Utility; using System.Diagnostics; @@ -10,6 +11,8 @@ using Content.Shared.Power; using Robust.Shared.Audio; using Robust.Shared.Audio.Systems; using Robust.Shared.Player; +using Content.Shared.ParticleAccelerator; +using Content.Shared.Machines.Events; namespace Content.Server.ParticleAccelerator.EntitySystems; @@ -20,12 +23,11 @@ public sealed partial class ParticleAcceleratorSystem private void InitializeControlBoxSystem() { - SubscribeLocalEvent(OnComponentStartup); - SubscribeLocalEvent(OnComponentShutdown); SubscribeLocalEvent(OnControlBoxPowerChange); SubscribeLocalEvent(OnUISetEnableMessage); SubscribeLocalEvent(OnUISetPowerMessage); SubscribeLocalEvent(OnUIRescanMessage); + SubscribeLocalEvent(OnMachineAssembledChanged); } public override void Update(float frameTime) @@ -40,14 +42,12 @@ public sealed partial class ParticleAcceleratorSystem } [Conditional("DEBUG")] - private void EverythingIsWellToFire(ParticleAcceleratorControlBoxComponent controller) + private void EverythingIsWellToFire(ParticleAcceleratorControlBoxComponent controller, + Entity machine) { DebugTools.Assert(controller.Powered); DebugTools.Assert(controller.SelectedStrength != ParticleAcceleratorPowerState.Standby); - DebugTools.Assert(controller.Assembled); - DebugTools.Assert(EntityManager.EntityExists(controller.PortEmitter)); - DebugTools.Assert(EntityManager.EntityExists(controller.ForeEmitter)); - DebugTools.Assert(EntityManager.EntityExists(controller.StarboardEmitter)); + DebugTools.Assert(machine.Comp.IsAssembled); } public void Fire(EntityUid uid, TimeSpan curTime, ParticleAcceleratorControlBoxComponent? comp = null) @@ -58,12 +58,17 @@ public sealed partial class ParticleAcceleratorSystem comp.LastFire = curTime; comp.NextFire = curTime + comp.ChargeTime; - EverythingIsWellToFire(comp); + if (!TryComp(uid, out var machineComp)) + return; + + var machine = (uid, machineComp); + EverythingIsWellToFire(comp, machine); var strength = comp.SelectedStrength; - FireEmitter(comp.PortEmitter!.Value, strength); - FireEmitter(comp.ForeEmitter!.Value, strength); - FireEmitter(comp.StarboardEmitter!.Value, strength); + + FireEmitter(_multipartMachine.GetPartEntity(machine, AcceleratorParts.PortEmitter)!.Value, strength); + FireEmitter(_multipartMachine.GetPartEntity(machine, AcceleratorParts.ForeEmitter)!.Value, strength); + FireEmitter(_multipartMachine.GetPartEntity(machine, AcceleratorParts.StarboardEmitter)!.Value, strength); } public void SwitchOn(EntityUid uid, EntityUid? user = null, ParticleAcceleratorControlBoxComponent? comp = null) @@ -71,7 +76,7 @@ public sealed partial class ParticleAcceleratorSystem if (!Resolve(uid, ref comp)) return; - DebugTools.Assert(comp.Assembled); + DebugTools.Assert(_multipartMachine.IsAssembled((uid, null))); if (comp.Enabled || !comp.CanBeEnabled) return; @@ -82,9 +87,11 @@ public sealed partial class ParticleAcceleratorSystem comp.Enabled = true; UpdatePowerDraw(uid, comp); - if (!TryComp(comp.PowerBox, out var powerConsumer) - || powerConsumer.ReceivedPower >= powerConsumer.DrawRate * ParticleAcceleratorControlBoxComponent.RequiredPowerRatio) + if (!TryComp(_multipartMachine.GetPartEntity(uid, AcceleratorParts.PowerBox), out var powerConsumer) + || powerConsumer.ReceivedPower >= powerConsumer.DrawRate * ParticleAcceleratorControlBoxComponent.RequiredPowerRatio) + { PowerOn(uid, comp); + } UpdateUI(uid, comp); } @@ -112,7 +119,7 @@ public sealed partial class ParticleAcceleratorSystem return; DebugTools.Assert(comp.Enabled); - DebugTools.Assert(comp.Assembled); + DebugTools.Assert(_multipartMachine.IsAssembled((uid, null))); if (comp.Powered) return; @@ -211,7 +218,10 @@ public sealed partial class ParticleAcceleratorSystem return; } - EverythingIsWellToFire(comp); + if (!TryComp(uid, out var machine)) + return; + + EverythingIsWellToFire(comp, (uid, machine)); var curTime = _gameTiming.CurTime; comp.LastFire = curTime; @@ -223,7 +233,8 @@ public sealed partial class ParticleAcceleratorSystem { if (!Resolve(uid, ref comp)) return; - if (!TryComp(comp.PowerBox, out var powerConsumer)) + + if (!TryComp(_multipartMachine.GetPartEntity(uid, AcceleratorParts.PowerBox), out var powerConsumer)) return; var powerDraw = comp.BasePowerDraw; @@ -244,30 +255,35 @@ public sealed partial class ParticleAcceleratorSystem var draw = 0f; var receive = 0f; - if (TryComp(comp.PowerBox, out var powerConsumer)) + if (TryComp(_multipartMachine.GetPartEntity(uid, AcceleratorParts.PowerBox), out var powerConsumer)) { draw = powerConsumer.DrawRate; receive = powerConsumer.ReceivedPower; } - _uiSystem.SetUiState(uid, - ParticleAcceleratorControlBoxUiKey.Key, - new ParticleAcceleratorUIState( - comp.Assembled, + if (!TryComp(uid, out var machineComp)) + return; + + var machine = (uid, machineComp); + + var uiState = new ParticleAcceleratorUIState( + machineComp.IsAssembled, comp.Enabled, comp.SelectedStrength, - (int) draw, - (int) receive, - comp.StarboardEmitter != null, - comp.ForeEmitter != null, - comp.PortEmitter != null, - comp.PowerBox != null, - comp.FuelChamber != null, - comp.EndCap != null, + (int)draw, + (int)receive, + _multipartMachine.HasPart(machine, AcceleratorParts.StarboardEmitter), + _multipartMachine.HasPart(machine, AcceleratorParts.ForeEmitter), + _multipartMachine.HasPart(machine, AcceleratorParts.PortEmitter), + _multipartMachine.HasPart(machine, AcceleratorParts.PowerBox), + _multipartMachine.HasPart(machine, AcceleratorParts.FuelChamber), + _multipartMachine.HasPart(machine, AcceleratorParts.EndCap), comp.InterfaceDisabled, comp.MaxStrength, comp.StrengthLocked - )); + ); + + _uiSystem.SetUiState(uid, ParticleAcceleratorControlBoxUiKey.Key, uiState); } private void UpdateAppearance(EntityUid uid, ParticleAcceleratorControlBoxComponent? comp = null, AppearanceComponent? appearance = null) @@ -292,55 +308,58 @@ public sealed partial class ParticleAcceleratorSystem var state = controller.Powered ? (ParticleAcceleratorVisualState) controller.SelectedStrength : ParticleAcceleratorVisualState.Unpowered; + if (!TryComp(uid, out var machineComp)) + return; + + var machine = (uid, machineComp); + // UpdatePartVisualState(ControlBox); (We are the control box) - if (controller.FuelChamber.HasValue) - _appearanceSystem.SetData(controller.FuelChamber!.Value, ParticleAcceleratorVisuals.VisualState, state); - if (controller.PowerBox.HasValue) - _appearanceSystem.SetData(controller.PowerBox!.Value, ParticleAcceleratorVisuals.VisualState, state); - if (controller.PortEmitter.HasValue) - _appearanceSystem.SetData(controller.PortEmitter!.Value, ParticleAcceleratorVisuals.VisualState, state); - if (controller.ForeEmitter.HasValue) - _appearanceSystem.SetData(controller.ForeEmitter!.Value, ParticleAcceleratorVisuals.VisualState, state); - if (controller.StarboardEmitter.HasValue) - _appearanceSystem.SetData(controller.StarboardEmitter!.Value, ParticleAcceleratorVisuals.VisualState, state); + if (_multipartMachine.TryGetPartEntity(machine, AcceleratorParts.FuelChamber, out var fuelChamber)) + _appearanceSystem.SetData(fuelChamber.Value, ParticleAcceleratorVisuals.VisualState, state); + if (_multipartMachine.TryGetPartEntity(machine, AcceleratorParts.PowerBox, out var powerBox)) + _appearanceSystem.SetData(powerBox.Value, ParticleAcceleratorVisuals.VisualState, state); + if (_multipartMachine.TryGetPartEntity(machine, AcceleratorParts.PortEmitter, out var portEmitter)) + _appearanceSystem.SetData(portEmitter.Value, ParticleAcceleratorVisuals.VisualState, state); + if (_multipartMachine.TryGetPartEntity(machine, AcceleratorParts.ForeEmitter, out var foreEmitter)) + _appearanceSystem.SetData(foreEmitter.Value, ParticleAcceleratorVisuals.VisualState, state); + if (_multipartMachine.TryGetPartEntity(machine, AcceleratorParts.StarboardEmitter, out var starboardEmitter)) + _appearanceSystem.SetData(starboardEmitter.Value, ParticleAcceleratorVisuals.VisualState, state); //no endcap because it has no powerlevel-sprites } - private IEnumerable AllParts(EntityUid uid, ParticleAcceleratorControlBoxComponent? comp = null) + /// + /// Handles when a multipart machine has had some assembled/disassembled state change, or had parts added/removed. + /// + /// Multipart machine entity + /// Args for this event + private void OnMachineAssembledChanged(Entity ent, ref MultipartMachineAssemblyStateChanged args) { - if (Resolve(uid, ref comp)) + if (args.IsAssembled) { - if (comp.FuelChamber.HasValue) - yield return comp.FuelChamber.Value; - if (comp.EndCap.HasValue) - yield return comp.EndCap.Value; - if (comp.PowerBox.HasValue) - yield return comp.PowerBox.Value; - if (comp.PortEmitter.HasValue) - yield return comp.PortEmitter.Value; - if (comp.ForeEmitter.HasValue) - yield return comp.ForeEmitter.Value; - if (comp.StarboardEmitter.HasValue) - yield return comp.StarboardEmitter.Value; + UpdatePowerDraw(ent, ent.Comp); + UpdateUI(ent, ent.Comp); } - } - - private void OnComponentStartup(EntityUid uid, ParticleAcceleratorControlBoxComponent comp, ComponentStartup args) - { - if (TryComp(uid, out var part)) - part.Master = uid; - } - - private void OnComponentShutdown(EntityUid uid, ParticleAcceleratorControlBoxComponent comp, ComponentShutdown args) - { - if (TryComp(uid, out var partStatus)) - partStatus.Master = null; - - var partQuery = GetEntityQuery(); - foreach (var part in AllParts(uid, comp)) + else { - if (partQuery.TryGetComponent(part, out var partData)) - partData.Master = null; + if (ent.Comp.Powered) + { + SwitchOff(ent, args.User, ent.Comp); + } + else + { + UpdateAppearance(ent, ent.Comp); + UpdateUI(ent, ent.Comp); + } + + // Because the parts are already removed from the multipart machine, updating the visual appearance won't find any valid entities. + // We know which parts have been removed so we can update the visual state to unpowered in a more manual way here. + foreach (var (key, part) in args.PartsRemoved) + { + if (key is AcceleratorParts.EndCap) + continue; // No endcap powerlevel-sprites + + _appearanceSystem.SetData(part, ParticleAcceleratorVisuals.VisualState, ParticleAcceleratorVisualState.Unpowered); + } } } @@ -365,7 +384,7 @@ public sealed partial class ParticleAcceleratorSystem if (msg.Enabled) { - if (comp.Assembled) + if (_multipartMachine.IsAssembled((uid, null))) SwitchOn(uid, msg.Actor, comp); } else @@ -397,9 +416,13 @@ public sealed partial class ParticleAcceleratorSystem if (TryComp(uid, out var apcPower) && !apcPower.Powered) return; - RescanParts(uid, msg.Actor, comp); + if (!TryComp(uid, out var machineComp)) + return; - UpdateUI(uid, comp); + // User has requested a manual rescan of the machine, if anything HAS changed that the multipart + // machine system has missed then a AssemblyStateChanged event will be raised at the machine. + var machine = new Entity(uid, machineComp); + _multipartMachine.Rescan(machine, msg.Actor); } public static int GetPANumericalLevel(ParticleAcceleratorPowerState state) diff --git a/Content.Server/ParticleAccelerator/EntitySystems/ParticleAcceleratorSystem.Emitter.cs b/Content.Server/ParticleAccelerator/EntitySystems/ParticleAcceleratorSystem.Emitter.cs index 78aca15bca..f8a5cc8430 100644 --- a/Content.Server/ParticleAccelerator/EntitySystems/ParticleAcceleratorSystem.Emitter.cs +++ b/Content.Server/ParticleAccelerator/EntitySystems/ParticleAcceleratorSystem.Emitter.cs @@ -1,5 +1,6 @@ using Content.Server.ParticleAccelerator.Components; using Content.Server.Singularity.Components; +using Content.Shared.ParticleAccelerator.Components; using Content.Shared.Projectiles; using Content.Shared.Singularity.Components; using Robust.Shared.Physics.Components; diff --git a/Content.Server/ParticleAccelerator/EntitySystems/ParticleAcceleratorSystem.Parts.cs b/Content.Server/ParticleAccelerator/EntitySystems/ParticleAcceleratorSystem.Parts.cs deleted file mode 100644 index 829de0af47..0000000000 --- a/Content.Server/ParticleAccelerator/EntitySystems/ParticleAcceleratorSystem.Parts.cs +++ /dev/null @@ -1,177 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using Content.Server.ParticleAccelerator.Components; -using JetBrains.Annotations; -using Robust.Shared.Map.Components; -using Robust.Shared.Physics.Events; - -namespace Content.Server.ParticleAccelerator.EntitySystems; - -[UsedImplicitly] -public sealed partial class ParticleAcceleratorSystem -{ - private void InitializePartSystem() - { - SubscribeLocalEvent(OnComponentShutdown); - SubscribeLocalEvent(OnMoveEvent); - SubscribeLocalEvent(BodyTypeChanged); - } - - public void RescanParts(EntityUid uid, EntityUid? user = null, ParticleAcceleratorControlBoxComponent? controller = null) - { - if (!Resolve(uid, ref controller)) - return; - - if (controller.CurrentlyRescanning) - return; - - var partQuery = GetEntityQuery(); - foreach (var part in AllParts(uid, controller)) - { - if (partQuery.TryGetComponent(part, out var partState)) - partState.Master = null; - } - - controller.Assembled = false; - controller.FuelChamber = null; - controller.EndCap = null; - controller.PowerBox = null; - controller.PortEmitter = null; - controller.ForeEmitter = null; - controller.StarboardEmitter = null; - - var xformQuery = GetEntityQuery(); - if (!xformQuery.TryGetComponent(uid, out var xform) || !xform.Anchored) - { - SwitchOff(uid, user, controller); - return; - } - - var gridUid = xform.GridUid; - if (gridUid == null || gridUid != xform.ParentUid || !TryComp(gridUid, out var grid)) - { - SwitchOff(uid, user, controller); - return; - } - - // Find fuel chamber first by scanning cardinals. - var fuelQuery = GetEntityQuery(); - foreach (var adjacent in _mapSystem.GetCardinalNeighborCells(gridUid.Value, grid, xform.Coordinates)) - { - if (fuelQuery.HasComponent(adjacent) - && partQuery.TryGetComponent(adjacent, out var partState) - && partState.Master == null) - { - controller.FuelChamber = adjacent; - break; - } - } - - if (controller.FuelChamber == null) - { - SwitchOff(uid, user, controller); - return; - } - - // When we call SetLocalRotation down there to rotate the control box, - // that ends up re-entrantly calling RescanParts() through the move event. - // You'll have to take my word for it that that breaks everything, yeah? - controller.CurrentlyRescanning = true; - - // Automatically rotate the control box sprite to face the fuel chamber - var fuelXform = xformQuery.GetComponent(controller.FuelChamber!.Value); - var fuelDir = (fuelXform.LocalPosition - xform.LocalPosition).GetDir(); - _transformSystem.SetLocalRotation(uid, fuelDir.ToAngle(), xform); - - // Calculate offsets for each of the parts of the PA. - // These are all done relative to the fuel chamber BC that is basically the center of the machine. - var rotation = fuelXform.LocalRotation; - var offsetVect = rotation.GetCardinalDir().ToIntVec(); - var orthoOffsetVect = new Vector2i(-offsetVect.Y, offsetVect.X); - - var positionFuelChamber = _mapSystem.TileIndicesFor(gridUid!.Value, grid, fuelXform.Coordinates); // n // n: End Cap - var positionEndCap = positionFuelChamber - offsetVect; // CF // C: Control Box, F: Fuel Chamber - var positionPowerBox = positionFuelChamber + offsetVect; // P // P: Power Box - var positionPortEmitter = positionFuelChamber + offsetVect * 2 + orthoOffsetVect; // EEE // E: Emitter (Starboard, Fore, Port) - var positionForeEmitter = positionFuelChamber + offsetVect * 2; - var positionStarboardEmitter = positionFuelChamber + offsetVect * 2 - orthoOffsetVect; - - ScanPart(gridUid.Value, positionEndCap, rotation, out controller.EndCap, out _, grid); - ScanPart(gridUid.Value, positionPowerBox, rotation, out controller.PowerBox, out _, grid); - - if (!ScanPart(gridUid.Value, positionPortEmitter, rotation, out controller.PortEmitter, out var portEmitter, grid) - || portEmitter.Type != ParticleAcceleratorEmitterType.Port) - controller.PortEmitter = null; - - if (!ScanPart(gridUid.Value, positionForeEmitter, rotation, out controller.ForeEmitter, out var foreEmitter, grid) - || foreEmitter.Type != ParticleAcceleratorEmitterType.Fore) - controller.ForeEmitter = null; - - if (!ScanPart(gridUid.Value, positionStarboardEmitter, rotation, out controller.StarboardEmitter, out var starboardEmitter, grid) - || starboardEmitter.Type != ParticleAcceleratorEmitterType.Starboard) - controller.StarboardEmitter = null; - - controller.Assembled = - controller.FuelChamber.HasValue - && controller.EndCap.HasValue - && controller.PowerBox.HasValue - && controller.PortEmitter.HasValue - && controller.ForeEmitter.HasValue - && controller.StarboardEmitter.HasValue; - - foreach (var part in AllParts(uid, controller)) - { - if (partQuery.TryGetComponent(part, out var partState)) - partState.Master = uid; - } - - controller.CurrentlyRescanning = false; - - UpdatePowerDraw(uid, controller); - UpdateUI(uid, controller); - } - - private bool ScanPart(EntityUid uid, Vector2i coordinates, Angle? rotation, [NotNullWhen(true)] out EntityUid? part, [NotNullWhen(true)] out T? comp, MapGridComponent? grid = null) - where T : IComponent - { - if (!Resolve(uid, ref grid)) - { - part = null; - comp = default; - return false; - } - - var compQuery = GetEntityQuery(); - foreach (var entity in _mapSystem.GetAnchoredEntities(uid, grid, coordinates)) - { - if (compQuery.TryGetComponent(entity, out comp) - && TryComp(entity, out var partState) && partState.Master == null - && (rotation == null || Transform(entity).LocalRotation.EqualsApprox(rotation!.Value.Theta))) - { - part = entity; - return true; - } - } - - part = null; - comp = default; - return false; - } - - private void OnComponentShutdown(EntityUid uid, ParticleAcceleratorPartComponent comp, ComponentShutdown args) - { - if (Exists(comp.Master)) - RescanParts(comp.Master!.Value); - } - - private void BodyTypeChanged(EntityUid uid, ParticleAcceleratorPartComponent comp, ref PhysicsBodyTypeChangedEvent args) - { - if (Exists(comp.Master)) - RescanParts(comp.Master!.Value); - } - - private void OnMoveEvent(EntityUid uid, ParticleAcceleratorPartComponent comp, ref MoveEvent args) - { - if (Exists(comp.Master)) - RescanParts(comp.Master!.Value); - } -} diff --git a/Content.Server/ParticleAccelerator/EntitySystems/ParticleAcceleratorSystem.PowerBox.cs b/Content.Server/ParticleAccelerator/EntitySystems/ParticleAcceleratorSystem.PowerBox.cs index be961ba5fd..677228c15b 100644 --- a/Content.Server/ParticleAccelerator/EntitySystems/ParticleAcceleratorSystem.PowerBox.cs +++ b/Content.Server/ParticleAccelerator/EntitySystems/ParticleAcceleratorSystem.PowerBox.cs @@ -1,5 +1,7 @@ -using Content.Server.ParticleAccelerator.Components; +using Content.Server.ParticleAccelerator.Components; using Content.Server.Power.EntitySystems; +using Content.Shared.Machines.Components; +using Content.Shared.ParticleAccelerator.Components; namespace Content.Server.ParticleAccelerator.EntitySystems; @@ -12,7 +14,7 @@ public sealed partial class ParticleAcceleratorSystem private void PowerBoxReceivedChanged(EntityUid uid, ParticleAcceleratorPowerBoxComponent component, ref PowerConsumerReceivedChanged args) { - if (!TryComp(uid, out var part)) + if (!TryComp(uid, out var part)) return; if (!TryComp(part.Master, out var controller)) return; diff --git a/Content.Server/ParticleAccelerator/EntitySystems/ParticleAcceleratorSystem.cs b/Content.Server/ParticleAccelerator/EntitySystems/ParticleAcceleratorSystem.cs index e9b62bc4a8..d9fb84bad3 100644 --- a/Content.Server/ParticleAccelerator/EntitySystems/ParticleAcceleratorSystem.cs +++ b/Content.Server/ParticleAccelerator/EntitySystems/ParticleAcceleratorSystem.cs @@ -1,6 +1,7 @@ using Content.Server.Administration.Logs; using Content.Server.Chat.Managers; using Content.Server.Projectiles; +using Content.Server.Machines.EntitySystems; using Robust.Shared.Physics.Systems; using Robust.Shared.Timing; using Robust.Server.GameObjects; @@ -19,13 +20,12 @@ public sealed partial class ParticleAcceleratorSystem : EntitySystem [Dependency] private readonly SharedPhysicsSystem _physicsSystem = default!; [Dependency] private readonly SharedTransformSystem _transformSystem = default!; [Dependency] private readonly UserInterfaceSystem _uiSystem = default!; - [Dependency] private readonly MapSystem _mapSystem = default!; + [Dependency] private readonly MultipartMachineSystem _multipartMachine = default!; public override void Initialize() { base.Initialize(); InitializeControlBoxSystem(); - InitializePartSystem(); InitializePowerBoxSystem(); } } diff --git a/Content.Server/ParticleAccelerator/Wires/ParticleAcceleratorToggleWireAction.cs b/Content.Server/ParticleAccelerator/Wires/ParticleAcceleratorToggleWireAction.cs index 40a15d2bc5..98d2efa7f5 100644 --- a/Content.Server/ParticleAccelerator/Wires/ParticleAcceleratorToggleWireAction.cs +++ b/Content.Server/ParticleAccelerator/Wires/ParticleAcceleratorToggleWireAction.cs @@ -1,3 +1,4 @@ +using Content.Server.Machines.EntitySystems; using Content.Server.ParticleAccelerator.Components; using Content.Server.ParticleAccelerator.EntitySystems; using Content.Server.Wires; @@ -38,10 +39,11 @@ public sealed partial class ParticleAcceleratorPowerWireAction : ComponentWireAc public override void Pulse(EntityUid user, Wire wire, ParticleAcceleratorControlBoxComponent controller) { var paSystem = EntityManager.System(); + var multipartMachine = EntityManager.System(); if (controller.Enabled) paSystem.SwitchOff(wire.Owner, user, controller); - else if (controller.Assembled) + else if (multipartMachine.IsAssembled((wire.Owner, null))) paSystem.SwitchOn(wire.Owner, user, controller); } } diff --git a/Content.Server/Singularity/StartSingularityEngineCommand.cs b/Content.Server/Singularity/StartSingularityEngineCommand.cs index e63a7467e0..a373c0da5e 100644 --- a/Content.Server/Singularity/StartSingularityEngineCommand.cs +++ b/Content.Server/Singularity/StartSingularityEngineCommand.cs @@ -1,9 +1,11 @@ using Content.Server.Administration; +using Content.Server.Machines.EntitySystems; using Content.Server.ParticleAccelerator.Components; using Content.Server.ParticleAccelerator.EntitySystems; using Content.Server.Singularity.Components; using Content.Server.Singularity.EntitySystems; using Content.Shared.Administration; +using Content.Shared.Machines.Components; using Content.Shared.Singularity.Components; using Robust.Shared.Console; @@ -45,12 +47,15 @@ namespace Content.Server.Singularity } // Setup PA + var multipartMachineManager = entitySystemManager.GetEntitySystem(); var paSystem = entitySystemManager.GetEntitySystem(); var paQuery = entityManager.EntityQueryEnumerator(); while (paQuery.MoveNext(out var paId, out var paControl)) { - paSystem.RescanParts(paId, controller: paControl); - if (!paControl.Assembled) + if (!entityManager.TryGetComponent(paId, out var machine)) + continue; + + if (!multipartMachineManager.Rescan((paId, machine))) continue; paSystem.SetStrength(paId, ParticleAcceleratorPowerState.Level0, comp: paControl); diff --git a/Content.Shared/Machines/Components/MultipartMachineComponent.cs b/Content.Shared/Machines/Components/MultipartMachineComponent.cs new file mode 100644 index 0000000000..f7a118ff54 --- /dev/null +++ b/Content.Shared/Machines/Components/MultipartMachineComponent.cs @@ -0,0 +1,103 @@ +using Content.Shared.Machines.EntitySystems; +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; +using Robust.Shared.Utility; + +namespace Content.Shared.Machines.Components; + +/// +/// Marks an entity as being the owner of a multipart machine. +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(raiseAfterAutoHandleState: true)] +[Access(typeof(SharedMultipartMachineSystem))] +public sealed partial class MultipartMachineComponent : Component +{ + /// + /// Dictionary of Enum values to specific parts of this machine. + /// Each key can be specified as 'enum..` in Yaml. + /// + [DataField, AutoNetworkedField] + public Dictionary Parts = []; + + /// + /// Whether this multipart machine is assembled or not. + /// Optional parts are not taken into account. + /// + [DataField, AutoNetworkedField] + public bool IsAssembled = false; + + /// + /// Flag for whether the client side system is allowed to show + /// ghosts of missing machine parts. + /// Controlled/Used by the client side. + /// + public List Ghosts = []; +} + +[DataDefinition] +[Serializable, NetSerializable] +public sealed partial class MachinePart +{ + /// + /// Component type that is expected for this part to have + /// to be considered a "Part" of the machine. + /// + [DataField(required: true, customTypeSerializer: typeof(ComponentNameSerializer))] + public string Component = ""; + + /// + /// Expected offset to find this machine at. + /// + [DataField(required: true)] + public Vector2i Offset; + + /// + /// Whether this part is required for the machine to be + /// considered "assembled", or is considered an optional extra. + /// + [DataField] + public bool Optional = false; + + /// + /// ID of prototype, used to show sprite and description of part, when user examines the machine and there + /// is no matched entity. Can reference dummy entities to give more detailed descriptions. + /// + [DataField] + public EntProtoId? GhostProto = null; + + /// + /// Expected rotation for this machine to have. + /// + [DataField] + public Angle Rotation = Angle.Zero; + + /// + /// Network entity, used to inform clients and update their side of the component + /// locally. + /// Use the Entity attribute if you wish to get which entity is actually bound to this part. + /// + public NetEntity? NetEntity = null; + + /// + /// Entity associated with this part. + /// Not null when an entity is successfully matched to the part and null otherwise. + /// + [DataField, NonSerialized] + public EntityUid? Entity = null; + + /// + /// Expected graph for this part to use as part of its construction. + /// + [DataField] + public EntProtoId Graph; + + /// + /// Expected node for this part to be in, on the graph. + /// Used to determine when a construct-able object has been + /// assembled or disassembled. + /// + [DataField] + public string ExpectedNode; +} diff --git a/Content.Shared/Machines/Components/MultipartMachinePartComponent.cs b/Content.Shared/Machines/Components/MultipartMachinePartComponent.cs new file mode 100644 index 0000000000..88eff5ce53 --- /dev/null +++ b/Content.Shared/Machines/Components/MultipartMachinePartComponent.cs @@ -0,0 +1,17 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Machines.Components; + +/// +/// Component for marking entities as part of a multipart machine. +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +public sealed partial class MultipartMachinePartComponent : Component +{ + /// + /// Links to the entity which holds the MultipartMachineComponent. + /// Useful so that entities that know which machine they are a part of. + /// + [DataField, AutoNetworkedField] + public EntityUid? Master = null; +} diff --git a/Content.Shared/Machines/EntitySystems/SharedMultipartMachineSystem.cs b/Content.Shared/Machines/EntitySystems/SharedMultipartMachineSystem.cs new file mode 100644 index 0000000000..7185d60305 --- /dev/null +++ b/Content.Shared/Machines/EntitySystems/SharedMultipartMachineSystem.cs @@ -0,0 +1,115 @@ +using System.Diagnostics.CodeAnalysis; +using Content.Shared.Machines.Components; + +namespace Content.Shared.Machines.EntitySystems; + +/// +/// Shared handling of multipart machines. +/// +public abstract class SharedMultipartMachineSystem : EntitySystem +{ + protected EntityQuery XformQuery; + + public override void Initialize() + { + base.Initialize(); + + XformQuery = GetEntityQuery(); + } + + /// + /// Returns whether each non-optional part of the machine has a matched entity + /// + /// Entity to check the assembled state of. + /// True if all non-optional parts have a matching entity, false otherwise. + public bool IsAssembled(Entity ent) + { + if (!Resolve(ent, ref ent.Comp)) + return false; + + foreach (var part in ent.Comp.Parts.Values) + { + if (!part.Entity.HasValue && !part.Optional) + return false; + } + + return true; + } + + /// + /// Returns whether a machine has a specifed EntityUid bound to one of its parts. + /// + /// Entity, which might have a multpart machine attached, to use for the query. + /// EntityUid to search for. + /// True if any part has the specified EntityUid, false otherwise. + public bool HasPartEntity(Entity machine, EntityUid entity) + { + if (!Resolve(machine, ref machine.Comp)) + return false; + + foreach (var part in machine.Comp.Parts.Values) + { + if (part.Entity.HasValue && part.Entity.Value == entity) + return true; + } + + return false; + } + + /// + /// Get the EntityUid for the entity bound to a specific part, if one exists. + /// + /// Entity, which might have a multipart machine attached, to use for the query. + /// Enum value for the part to find, must match the value specified in YAML. + /// May contain the resolved EntityUid for the specified part, null otherwise. + public EntityUid? GetPartEntity(Entity ent, Enum part) + { + if (!TryGetPartEntity(ent, part, out var entity)) + return null; + + return entity; + } + + /// + /// Get the EntityUid for the entity bound to a specific part, if one exists. + /// + /// Entity, which might have a multipart machine attached, to use for the query. + /// Enum for the part to find, must match the value specified in YAML. + /// Out var which may contain the matched EntityUid for the specified part. + /// True if the part is found and has a matched entity, false otherwise. + public bool TryGetPartEntity( + Entity ent, + Enum part, + [NotNullWhen(true)] out EntityUid? entity + ) + { + entity = null; + if (!Resolve(ent, ref ent.Comp)) + return false; + + if (ent.Comp.Parts.TryGetValue(part, out var value) && value.Entity.HasValue) + { + entity = value.Entity.Value; + return true; + } + + return false; + } + + /// + /// Check if a machine has an entity bound to a specific part + /// + /// Entity, which might have a multipart machine attached, to use for the query. + /// Enum for the part to find. + /// True if the specific part has a entity bound to it, false otherwise. + public bool HasPart(Entity ent, Enum part) + { + if (!Resolve(ent, ref ent.Comp)) + return false; + + if (!ent.Comp.Parts.TryGetValue(part, out var value)) + return false; + + return value.Entity != null; + } +} diff --git a/Content.Shared/Machines/Events/MultipartMachineEvents.cs b/Content.Shared/Machines/Events/MultipartMachineEvents.cs new file mode 100644 index 0000000000..8bf19e9d6b --- /dev/null +++ b/Content.Shared/Machines/Events/MultipartMachineEvents.cs @@ -0,0 +1,21 @@ +namespace Content.Shared.Machines.Events; + +/// +/// This event is raised when the assembled state of a Multipart machine changes. +/// This includes when optional parts are found, parts become unanchored, or move +/// within a construction graph. +/// +/// Entity that is bound to the multipart machine. +/// Assembled state of the machine. +/// Optional user that may have caused the assembly state to change. +/// Dictionary of keys to entities of parts that have been added to this machine. +/// Dictionary of keys to entities of parts that have been removed from this machine. +[ByRefEvent] +public record struct MultipartMachineAssemblyStateChanged( + EntityUid Entity, + bool IsAssembled, + EntityUid? User, + Dictionary PartsAdded, + Dictionary PartsRemoved) +{ +} diff --git a/Content.Shared/ParticleAccelerator/AcceleratorParts.cs b/Content.Shared/ParticleAccelerator/AcceleratorParts.cs new file mode 100644 index 0000000000..23648916e3 --- /dev/null +++ b/Content.Shared/ParticleAccelerator/AcceleratorParts.cs @@ -0,0 +1,16 @@ +using Robust.Shared.Serialization; + +namespace Content.Shared.ParticleAccelerator; + +[Serializable, NetSerializable] +public enum AcceleratorParts : byte +{ + EndCap, + FuelChamber, + PowerBox, + PortEmitter, + ForeEmitter, + StarboardEmitter +}; + + diff --git a/Content.Server/ParticleAccelerator/Components/ParticleAcceleratorEmitterComponent.cs b/Content.Shared/ParticleAccelerator/Components/ParticleAcceleratorEmitterComponent.cs similarity index 90% rename from Content.Server/ParticleAccelerator/Components/ParticleAcceleratorEmitterComponent.cs rename to Content.Shared/ParticleAccelerator/Components/ParticleAcceleratorEmitterComponent.cs index 7697644a3b..9b52a73522 100644 --- a/Content.Server/ParticleAccelerator/Components/ParticleAcceleratorEmitterComponent.cs +++ b/Content.Shared/ParticleAccelerator/Components/ParticleAcceleratorEmitterComponent.cs @@ -1,6 +1,6 @@ using Robust.Shared.Prototypes; -namespace Content.Server.ParticleAccelerator.Components; +namespace Content.Shared.ParticleAccelerator.Components; [RegisterComponent] public sealed partial class ParticleAcceleratorEmitterComponent : Component diff --git a/Content.Server/ParticleAccelerator/Components/ParticleAcceleratorEndCapComponent.cs b/Content.Shared/ParticleAccelerator/Components/ParticleAcceleratorEndCapComponent.cs similarity index 62% rename from Content.Server/ParticleAccelerator/Components/ParticleAcceleratorEndCapComponent.cs rename to Content.Shared/ParticleAccelerator/Components/ParticleAcceleratorEndCapComponent.cs index 9c111d1ea9..20615321d1 100644 --- a/Content.Server/ParticleAccelerator/Components/ParticleAcceleratorEndCapComponent.cs +++ b/Content.Shared/ParticleAccelerator/Components/ParticleAcceleratorEndCapComponent.cs @@ -1,4 +1,4 @@ -namespace Content.Server.ParticleAccelerator.Components; +namespace Content.Shared.ParticleAccelerator.Components; [RegisterComponent] public sealed partial class ParticleAcceleratorEndCapComponent : Component diff --git a/Content.Server/ParticleAccelerator/Components/ParticleAcceleratorFuelChamberComponent.cs b/Content.Shared/ParticleAccelerator/Components/ParticleAcceleratorFuelChamberComponent.cs similarity index 63% rename from Content.Server/ParticleAccelerator/Components/ParticleAcceleratorFuelChamberComponent.cs rename to Content.Shared/ParticleAccelerator/Components/ParticleAcceleratorFuelChamberComponent.cs index 9029e880d9..9e0da68b96 100644 --- a/Content.Server/ParticleAccelerator/Components/ParticleAcceleratorFuelChamberComponent.cs +++ b/Content.Shared/ParticleAccelerator/Components/ParticleAcceleratorFuelChamberComponent.cs @@ -1,4 +1,4 @@ -namespace Content.Server.ParticleAccelerator.Components; +namespace Content.Shared.ParticleAccelerator.Components; [RegisterComponent] public sealed partial class ParticleAcceleratorFuelChamberComponent : Component diff --git a/Content.Server/ParticleAccelerator/Components/ParticleAcceleratorPowerBoxComponent.cs b/Content.Shared/ParticleAccelerator/Components/ParticleAcceleratorPowerBoxComponent.cs similarity index 62% rename from Content.Server/ParticleAccelerator/Components/ParticleAcceleratorPowerBoxComponent.cs rename to Content.Shared/ParticleAccelerator/Components/ParticleAcceleratorPowerBoxComponent.cs index f8ad4ef5d2..3da6b76f2f 100644 --- a/Content.Server/ParticleAccelerator/Components/ParticleAcceleratorPowerBoxComponent.cs +++ b/Content.Shared/ParticleAccelerator/Components/ParticleAcceleratorPowerBoxComponent.cs @@ -1,4 +1,4 @@ -namespace Content.Server.ParticleAccelerator.Components; +namespace Content.Shared.ParticleAccelerator.Components; [RegisterComponent] public sealed partial class ParticleAcceleratorPowerBoxComponent : Component diff --git a/Resources/Prototypes/Entities/Structures/Machines/multipart.yml b/Resources/Prototypes/Entities/Structures/Machines/multipart.yml new file mode 100644 index 0000000000..fecc4f2bc7 --- /dev/null +++ b/Resources/Prototypes/Entities/Structures/Machines/multipart.yml @@ -0,0 +1,18 @@ +# Spawn on client side when users examine a multipart machine +# If a sprite is given then the default component's value will be overriden +- type: entity + id: MultipartMachineGhost + categories: [ HideSpawnMenu ] + components: + - type: MultipartMachineGhost + - type: Sprite + sprite: Markers/cross.rsi + layers: + - state: green + color: "#FFFFFF80" + - type: TimedDespawn + lifetime: 5 + - type: Clickable + - type: Tag + tags: + - HideContextMenu diff --git a/Resources/Prototypes/Entities/Structures/Power/Generation/PA/base_particleaccelerator.yml b/Resources/Prototypes/Entities/Structures/Power/Generation/PA/base_particleaccelerator.yml index d5b7e3667d..b19e2c8ebb 100644 --- a/Resources/Prototypes/Entities/Structures/Power/Generation/PA/base_particleaccelerator.yml +++ b/Resources/Prototypes/Entities/Structures/Power/Generation/PA/base_particleaccelerator.yml @@ -43,7 +43,6 @@ map: [ "enum.ParticleAcceleratorVisualLayers.Unlit" ] shader: unshaded visible: false - - type: ParticleAcceleratorPart - type: ParticleAcceleratorPartVisuals stateBase: unlit - type: Construction diff --git a/Resources/Prototypes/Entities/Structures/Power/Generation/PA/control_box.yml b/Resources/Prototypes/Entities/Structures/Power/Generation/PA/control_box.yml index ae3fc96774..7b3aa0c693 100644 --- a/Resources/Prototypes/Entities/Structures/Power/Generation/PA/control_box.yml +++ b/Resources/Prototypes/Entities/Structures/Power/Generation/PA/control_box.yml @@ -27,6 +27,50 @@ layoutId: ParticleAccelerator - type: AccessReader access: [["Engineering"]] + - type: MultipartMachine + parts: + enum.AcceleratorParts.EndCap: + component: ParticleAcceleratorEndCap + offset: 1, -1 + rotation: -90 + ghostProto: ParticleAcceleratorEndCap + graph: ParticleAcceleratorEndCap + expectedNode: completed + enum.AcceleratorParts.FuelChamber: + component: ParticleAcceleratorFuelChamber + offset: 0, -1 + rotation: -90 + ghostProto: ParticleAcceleratorFuelChamber + graph: ParticleAcceleratorFuelChamber + expectedNode: completed + enum.AcceleratorParts.PowerBox: + component: ParticleAcceleratorPowerBox + offset: -1, -1 + rotation: -90 + ghostProto: ParticleAcceleratorPowerBox + graph: ParticleAcceleratorPowerBox + expectedNode: completed + enum.AcceleratorParts.PortEmitter: + component: ParticleAcceleratorEmitter + offset: -2, -2 + rotation: -90 + ghostProto: ParticleAcceleratorEmitterPort + graph: ParticleAcceleratorEmitterPort + expectedNode: completed + enum.AcceleratorParts.ForeEmitter: + component: ParticleAcceleratorEmitter + offset: -2, -1 + rotation: -90 + ghostProto: ParticleAcceleratorEmitterFore + graph: ParticleAcceleratorEmitterFore + expectedNode: completed + enum.AcceleratorParts.StarboardEmitter: + component: ParticleAcceleratorEmitter + offset: -2, 0 + rotation: -90 + ghostProto: ParticleAcceleratorEmitterStarboard + graph: ParticleAcceleratorEmitterStarboard + expectedNode: completed # Unfinished diff --git a/Resources/Prototypes/Entities/Structures/Power/Generation/PA/emitter.yml b/Resources/Prototypes/Entities/Structures/Power/Generation/PA/emitter.yml index 43b4d5f9ef..38d0d0b6cd 100644 --- a/Resources/Prototypes/Entities/Structures/Power/Generation/PA/emitter.yml +++ b/Resources/Prototypes/Entities/Structures/Power/Generation/PA/emitter.yml @@ -10,6 +10,7 @@ emitterType: Port - type: Construction graph: ParticleAcceleratorEmitterPort + - type: MultipartMachinePart - type: entity parent: ParticleAcceleratorFinishedPart @@ -23,6 +24,7 @@ emitterType: Fore - type: Construction graph: ParticleAcceleratorEmitterFore + - type: MultipartMachinePart - type: entity parent: ParticleAcceleratorFinishedPart @@ -36,6 +38,7 @@ emitterType: Starboard - type: Construction graph: ParticleAcceleratorEmitterStarboard + - type: MultipartMachinePart # Unfinished @@ -50,6 +53,7 @@ sprite: Structures/Power/Generation/PA/emitter_port.rsi - type: Construction graph: ParticleAcceleratorEmitterPort + - type: MultipartMachinePart - type: entity parent: ParticleAcceleratorUnfinishedBase @@ -62,6 +66,7 @@ sprite: Structures/Power/Generation/PA/emitter_fore.rsi - type: Construction graph: ParticleAcceleratorEmitterFore + - type: MultipartMachinePart - type: entity parent: ParticleAcceleratorUnfinishedBase @@ -74,3 +79,4 @@ sprite: Structures/Power/Generation/PA/emitter_starboard.rsi - type: Construction graph: ParticleAcceleratorEmitterStarboard + - type: MultipartMachinePart diff --git a/Resources/Prototypes/Entities/Structures/Power/Generation/PA/end_cap.yml b/Resources/Prototypes/Entities/Structures/Power/Generation/PA/end_cap.yml index 80b0240d89..226ef915a7 100644 --- a/Resources/Prototypes/Entities/Structures/Power/Generation/PA/end_cap.yml +++ b/Resources/Prototypes/Entities/Structures/Power/Generation/PA/end_cap.yml @@ -12,6 +12,7 @@ - type: ParticleAcceleratorEndCap - type: Construction graph: ParticleAcceleratorEndCap + - type: MultipartMachinePart # Unfinished @@ -26,3 +27,4 @@ sprite: Structures/Power/Generation/PA/end_cap.rsi - type: Construction graph: ParticleAcceleratorEndCap + - type: MultipartMachinePart diff --git a/Resources/Prototypes/Entities/Structures/Power/Generation/PA/fuel_chamber.yml b/Resources/Prototypes/Entities/Structures/Power/Generation/PA/fuel_chamber.yml index 868e9cf8f9..37fba25212 100644 --- a/Resources/Prototypes/Entities/Structures/Power/Generation/PA/fuel_chamber.yml +++ b/Resources/Prototypes/Entities/Structures/Power/Generation/PA/fuel_chamber.yml @@ -9,6 +9,7 @@ - type: ParticleAcceleratorFuelChamber - type: Construction graph: ParticleAcceleratorFuelChamber + - type: MultipartMachinePart # Unfinished @@ -23,3 +24,4 @@ sprite: Structures/Power/Generation/PA/fuel_chamber.rsi - type: Construction graph: ParticleAcceleratorFuelChamber + - type: MultipartMachinePart diff --git a/Resources/Prototypes/Entities/Structures/Power/Generation/PA/power_box.yml b/Resources/Prototypes/Entities/Structures/Power/Generation/PA/power_box.yml index c9a25d62ff..9dbff7fa42 100644 --- a/Resources/Prototypes/Entities/Structures/Power/Generation/PA/power_box.yml +++ b/Resources/Prototypes/Entities/Structures/Power/Generation/PA/power_box.yml @@ -17,6 +17,7 @@ nodeGroupID: MVPower - type: Construction graph: ParticleAcceleratorPowerBox + - type: MultipartMachinePart - type: entity parent: ParticleAcceleratorUnfinishedBase @@ -29,3 +30,4 @@ sprite: Structures/Power/Generation/PA/power_box.rsi - type: Construction graph: ParticleAcceleratorPowerBox + - type: MultipartMachinePart