diff --git a/Content.Server/Construction/ConstructionSystem.Graph.cs b/Content.Server/Construction/ConstructionSystem.Graph.cs index 8b5b783d7a..835dcf058e 100644 --- a/Content.Server/Construction/ConstructionSystem.Graph.cs +++ b/Content.Server/Construction/ConstructionSystem.Graph.cs @@ -1,4 +1,5 @@ using Content.Server.Construction.Components; +using Content.Server.Containers; using Content.Shared.Construction; using Content.Shared.Construction.Prototypes; using Content.Shared.Construction.Steps; @@ -281,20 +282,33 @@ namespace Content.Server.Construction Resolve(uid, ref containerManager, false); // We create the new entity. - var newUid = EntityManager.SpawnEntity(newEntity, transform.Coordinates); + var newUid = EntityManager.CreateEntityUninitialized(newEntity, transform.Coordinates); // Construction transferring. var newConstruction = EntityManager.EnsureComponent(newUid); - // We set the graph and node accordingly... Then we append our containers to theirs. + // Transfer all construction-owned containers. + newConstruction.Containers.UnionWith(construction.Containers); + + // Prevent MapInitEvent spawned entities from spawning into the containers. + // Containers created by ChangeNode() actions do not exist until after this function is complete, + // but this should be fine, as long as the target entity properly declared its managed containers. + if (TryComp(newUid, out ContainerFillComponent? containerFill) && containerFill.IgnoreConstructionSpawn) + { + foreach (var id in newConstruction.Containers) + { + containerFill.Containers.Remove(id); + } + } + + EntityManager.InitializeAndStartEntity(newUid); + + // We set the graph and node accordingly. ChangeGraph(newUid, userUid, construction.Graph, construction.Node, false, newConstruction); if (construction.TargetNode is {} targetNode) SetPathfindingTarget(newUid, targetNode, newConstruction); - // Transfer all construction-owned containers. - newConstruction.Containers.UnionWith(construction.Containers); - // Transfer all pending interaction events too. while (construction.InteractionQueue.TryDequeue(out var ev)) { diff --git a/Content.Server/Containers/ContainerFillComponent.cs b/Content.Server/Containers/ContainerFillComponent.cs new file mode 100644 index 0000000000..fc69a89727 --- /dev/null +++ b/Content.Server/Containers/ContainerFillComponent.cs @@ -0,0 +1,95 @@ +using Content.Server.Storage.Components; +using Content.Shared.Storage; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization.Manager; +using Robust.Shared.Serialization.Markdown; +using Robust.Shared.Serialization.Markdown.Mapping; +using Robust.Shared.Serialization.Markdown.Sequence; +using Robust.Shared.Serialization.Markdown.Validation; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List; +using Robust.Shared.Serialization.TypeSerializers.Interfaces; + +namespace Content.Server.Containers; + +/// +/// Component for spawning entity prototypes into containers on map init. +/// +/// +/// Unlike this is deterministic and supports arbitrary containers. While this +/// could maybe be merged with that component, it would require significant changes to , which is also used by several other systems. +/// +[RegisterComponent] +public sealed class ContainerFillComponent : Component +{ + [DataField("containers", customTypeSerializer:typeof(ContainerFillSerializer))] + public readonly Dictionary> Containers = new(); + + /// + /// If true, entities spawned via the construction system will not have entities spawned into containers managed + /// by the construction system. + /// + [DataField("ignoreConstructionSpawn")] + public bool IgnoreConstructionSpawn = true; +} + +// all of this exists just to validate prototype ids. +// it would be nice if you could specify only a type validator and not have to re-implement everything else. +// or a dictionary serializer that accepts a custom type serializer for the dictionary values +public sealed class ContainerFillSerializer : ITypeSerializer>, MappingDataNode> +{ + private static PrototypeIdListSerializer ListSerializer => new(); + + public ValidationNode Validate( + ISerializationManager serializationManager, + MappingDataNode node, + IDependencyCollection dependencies, + ISerializationContext? context = null) + { + var mapping = new Dictionary(); + + foreach (var (key, val) in node.Children) + { + var keyVal = serializationManager.ValidateNode(key, context); + + var listVal = (val is SequenceDataNode seq) + ? ListSerializer.Validate(serializationManager, seq, dependencies, context) + : new ErrorNode(val, "ContainerFillComponent prototypes must be a sequence/list"); + + mapping.Add(keyVal, listVal); + } + + return new ValidatedMappingNode(mapping); + } + + public Dictionary> Copy( + ISerializationManager serializationManager, + Dictionary> source, + Dictionary> target, + bool skipHook, + ISerializationContext? context = null) + { + serializationManager.Copy(source, ref target, context, skipHook); + return target; + } + + public Dictionary> Read( + ISerializationManager serializationManager, + MappingDataNode node, + IDependencyCollection dependencies, + bool skipHook, + ISerializationContext? context = null, + Dictionary>? value = null) + { + return serializationManager.Read(node, context, skipHook, value); + } + + public DataNode Write(ISerializationManager serializationManager, + Dictionary> value, + IDependencyCollection dependencies, + bool alwaysWrite = false, + ISerializationContext? context = null) + { + return serializationManager.WriteValue(value, alwaysWrite, context); + } +} diff --git a/Content.Server/Containers/ContainerFillSystem.cs b/Content.Server/Containers/ContainerFillSystem.cs new file mode 100644 index 0000000000..4ab1ca899a --- /dev/null +++ b/Content.Server/Containers/ContainerFillSystem.cs @@ -0,0 +1,47 @@ +using Robust.Shared.Containers; +using Robust.Shared.Map; + +namespace Content.Server.Containers; + +public sealed class ContainerFillSystem : EntitySystem +{ + [Dependency] private readonly SharedContainerSystem _containerSystem = default!; + + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent(OnMapInit); + } + + private void OnMapInit(EntityUid uid, ContainerFillComponent component, MapInitEvent args) + { + if (!TryComp(uid, out ContainerManagerComponent? containerComp)) + { + Logger.Error($"Entity {ToPrettyString(uid)} with a {nameof(ContainerFillComponent)} has no {nameof(ContainerManagerComponent)}."); + return; + } + + var xform = Transform(uid); + var coords = new EntityCoordinates(uid, Vector2.Zero); + + foreach (var (contaienrId, prototypes) in component.Containers) + { + if (!_containerSystem.TryGetContainer(uid, contaienrId, out var container, containerComp)) + { + Logger.Error($"Entity {ToPrettyString(uid)} with a {nameof(ContainerFillComponent)} is missing a container ({contaienrId})."); + continue; + } + + foreach (var proto in prototypes) + { + var ent = Spawn(proto, coords); + if (!container.Insert(ent, EntityManager, null, xform)) + { + Logger.Error($"Entity {ToPrettyString(uid)} with a {nameof(ContainerFillComponent)} failed to insert an entity: {ToPrettyString(ent)}."); + Transform(ent).AttachToGridOrMap(); + break; + } + } + } + } +} diff --git a/Content.Server/Doors/Systems/DoorSystem.cs b/Content.Server/Doors/Systems/DoorSystem.cs index 62a25566f9..5beae0323c 100644 --- a/Content.Server/Doors/Systems/DoorSystem.cs +++ b/Content.Server/Doors/Systems/DoorSystem.cs @@ -18,7 +18,6 @@ using Content.Shared.Tools.Components; using Content.Shared.Verbs; using Robust.Shared.Audio; using Robust.Shared.Containers; -using Robust.Shared.Physics.Dynamics; using Robust.Shared.Player; using System.Linq; using Robust.Shared.Physics.Components; @@ -38,7 +37,6 @@ public sealed class DoorSystem : SharedDoorSystem { base.Initialize(); - SubscribeLocalEvent(OnMapInit); SubscribeLocalEvent(OnInteractUsing, after: new[] { typeof(ConstructionSystem) }); // Mob prying doors @@ -274,30 +272,6 @@ public sealed class DoorSystem : SharedDoorSystem TryOpen(uid, door, otherUid); } - private void OnMapInit(EntityUid uid, DoorComponent door, MapInitEvent args) - { - // Ensure that the construction component is aware of the board container. - if (TryComp(uid, out ConstructionComponent? construction)) - _constructionSystem.AddContainer(uid, "board", construction); - - // We don't do anything if this is null or empty. - if (string.IsNullOrEmpty(door.BoardPrototype)) - return; - - var container = _containerSystem.EnsureContainer(uid, "board", out var existed); - - if (existed && container.ContainedEntities.Count != 0) - { - // We already contain a board. Note: We don't check if it's the right one! - return; - } - - var board = EntityManager.SpawnEntity(door.BoardPrototype, Transform(uid).Coordinates); - - if(!container.Insert(board)) - Logger.Warning($"Couldn't insert board {ToPrettyString(board)} into door {ToPrettyString(uid)}!"); - } - private void OnEmagged(EntityUid uid, DoorComponent door, GotEmaggedEvent args) { if(TryComp(uid, out var airlockComponent)) diff --git a/Content.Shared/Doors/Components/DoorComponent.cs b/Content.Shared/Doors/Components/DoorComponent.cs index b2f8a6b585..a67f57731e 100644 --- a/Content.Shared/Doors/Components/DoorComponent.cs +++ b/Content.Shared/Doors/Components/DoorComponent.cs @@ -169,9 +169,6 @@ public sealed class DoorComponent : Component, ISerializationHooks } #endregion - [DataField("board", customTypeSerializer: typeof(PrototypeIdSerializer))] - public string? BoardPrototype; - [DataField("pryingQuality", customTypeSerializer: typeof(PrototypeIdSerializer))] public string PryingQuality = "Prying"; diff --git a/Resources/Prototypes/Entities/Structures/Doors/Airlocks/base_structureairlocks.yml b/Resources/Prototypes/Entities/Structures/Doors/Airlocks/base_structureairlocks.yml index e5088b1ddc..34c51c7272 100644 --- a/Resources/Prototypes/Entities/Structures/Doors/Airlocks/base_structureairlocks.yml +++ b/Resources/Prototypes/Entities/Structures/Doors/Airlocks/base_structureairlocks.yml @@ -41,8 +41,10 @@ - FullTileMask layer: - AirlockLayer + - type: ContainerFill + containers: + board: [ DoorElectronics ] - type: Door - board: DoorElectronics crushDamage: types: Blunt: 15 @@ -96,6 +98,8 @@ - type: Construction graph: Airlock node: airlock + containers: + - board - type: IconSmooth key: walls mode: NoSprite diff --git a/Resources/Prototypes/Entities/Structures/Doors/Airlocks/highsec.yml b/Resources/Prototypes/Entities/Structures/Doors/Airlocks/highsec.yml index 597e7da0a4..5c0259437c 100644 --- a/Resources/Prototypes/Entities/Structures/Doors/Airlocks/highsec.yml +++ b/Resources/Prototypes/Entities/Structures/Doors/Airlocks/highsec.yml @@ -35,8 +35,13 @@ - FullTileMask layer: - WallLayer + - type: ContainerFill + containers: + board: [ DoorElectronics ] + - type: ContainerContainer + containers: + board: !type:Container - type: Door - board: DoorElectronics crushDamage: types: Blunt: 50 diff --git a/Resources/Prototypes/Entities/Structures/Doors/Airlocks/shuttle.yml b/Resources/Prototypes/Entities/Structures/Doors/Airlocks/shuttle.yml index 4de40749ab..117329cdd9 100644 --- a/Resources/Prototypes/Entities/Structures/Doors/Airlocks/shuttle.yml +++ b/Resources/Prototypes/Entities/Structures/Doors/Airlocks/shuttle.yml @@ -32,7 +32,6 @@ bumpOpen: false closeTimeTwo: 0.4 openTimeTwo: 0.4 - board: DoorElectronics crushDamage: types: Blunt: 15 diff --git a/Resources/Prototypes/Entities/Structures/Doors/Firelocks/firelock.yml b/Resources/Prototypes/Entities/Structures/Doors/Firelocks/firelock.yml index cea5d73774..7f7ac961ab 100644 --- a/Resources/Prototypes/Entities/Structures/Doors/Firelocks/firelock.yml +++ b/Resources/Prototypes/Entities/Structures/Doors/Firelocks/firelock.yml @@ -1,5 +1,6 @@ - type: entity - id: Firelock + id: BaseFirelock + abstract: true parent: BaseStructure name: firelock description: Apply crowbar. @@ -97,14 +98,27 @@ enabled: false - type: Occluder enabled: false - - type: Construction - graph: Firelock - node: Firelock - type: WallMount arc: 360 - type: StaticPrice price: 150 +- type: entity + id: Firelock + parent: BaseFirelock + components: + - type: ContainerContainer + containers: + board: !type:Container + - type: Construction + graph: Firelock + node: Firelock + containers: + - board + - type: ContainerFill + containers: + board: [ FirelockElectronics ] + - type: entity id: FirelockGlass parent: Firelock @@ -126,10 +140,12 @@ - GlassAirlockLayer - type: Door occludes: false + - type: Construction + node: FirelockGlass - type: entity id: FirelockEdge - parent: Firelock + parent: BaseFirelock name: firelock components: - type: Sprite diff --git a/Resources/Prototypes/Entities/Structures/Doors/Shutter/shutters.yml b/Resources/Prototypes/Entities/Structures/Doors/Shutter/shutters.yml index 55fa9b08ed..9c7db8cd18 100644 --- a/Resources/Prototypes/Entities/Structures/Doors/Shutter/shutters.yml +++ b/Resources/Prototypes/Entities/Structures/Doors/Shutter/shutters.yml @@ -4,6 +4,8 @@ name: shutter abstract: true description: One shudders to think about what might be behind this shutter. + placement: + mode: SnapgridCenter components: - type: Sprite netsync: false @@ -22,8 +24,13 @@ - FullTileMask layer: - AirlockLayer + - type: ContainerFill + containers: + board: [ DoorElectronics ] + - type: ContainerContainer + containers: + board: !type:Container - type: Door - board: DoorElectronics bumpOpen: false clickOpen: false closeTimeOne: 0.2 @@ -78,8 +85,6 @@ messagePerceivedByOthers: comp-window-knock interactSuccessSound: path: /Audio/Effects/glass_knock.ogg - placement: - mode: SnapgridCenter - type: entity id: ShuttersNormal @@ -89,6 +94,8 @@ - type: Construction graph: Shutters node: Shutters + containers: + - board - type: entity id: ShuttersNormalOpen @@ -105,9 +112,6 @@ airBlocked: false - type: RadiationBlocker enabled: false - - type: Construction - graph: Shutters - node: Shutters - type: entity id: ShuttersRadiation @@ -125,6 +129,8 @@ - type: Construction graph: Shutters node: ShuttersRadiation + containers: + - board - type: entity id: ShuttersRadiationOpen @@ -133,9 +139,6 @@ components: - type: Door state: Open - - type: Construction - graph: Shutters - node: ShuttersRadiation - type: Occluder enabled: false - type: Physics @@ -160,6 +163,8 @@ - type: Construction graph: Shutters node: ShuttersWindow + containers: + - board - type: entity id: ShuttersWindowOpen @@ -168,9 +173,6 @@ components: - type: Door state: Open - - type: Construction - graph: Shutters - node: ShuttersWindow - type: Physics canCollide: false - type: Airtight @@ -192,6 +194,8 @@ - type: Construction graph: Shutters node: frame1 + containers: + - board - type: InteractionOutline - type: Damageable damageContainer: Inorganic diff --git a/Resources/Prototypes/Entities/Structures/Doors/Windoors/base_structurewindoors.yml b/Resources/Prototypes/Entities/Structures/Doors/Windoors/base_structurewindoors.yml index 9b98fb9b6d..3bc3d5b407 100644 --- a/Resources/Prototypes/Entities/Structures/Doors/Windoors/base_structurewindoors.yml +++ b/Resources/Prototypes/Entities/Structures/Doors/Windoors/base_structurewindoors.yml @@ -76,9 +76,14 @@ openPanelVisible: true # needed so that windoors will close regardless of whether there are people in it; it doesn't crush after all safety: false + - type: ContainerFill + containers: + board: [ DoorElectronics ] + - type: ContainerContainer + containers: + board: !type:Container - type: Door canCrush: false - board: DoorElectronics openSound: path: /Audio/Machines/windoor_open.ogg closeSound: @@ -111,6 +116,8 @@ - type: Construction graph: Windoor node: windoor + containers: + - board - type: StaticPrice price: 100 diff --git a/Resources/Prototypes/Recipes/Construction/Graphs/structures/firelock.yml b/Resources/Prototypes/Recipes/Construction/Graphs/structures/firelock.yml index 647c89db02..40f09e947a 100644 --- a/Resources/Prototypes/Recipes/Construction/Graphs/structures/firelock.yml +++ b/Resources/Prototypes/Recipes/Construction/Graphs/structures/firelock.yml @@ -148,7 +148,7 @@ prototype: SheetGlass1 amount: 2 steps: - - tool: Screwing + - tool: Prying doAfter: 1 diff --git a/Resources/Prototypes/Recipes/Construction/structures.yml b/Resources/Prototypes/Recipes/Construction/structures.yml index 5e0fb7ab23..c9b71daa07 100644 --- a/Resources/Prototypes/Recipes/Construction/structures.yml +++ b/Resources/Prototypes/Recipes/Construction/structures.yml @@ -379,6 +379,23 @@ conditions: - !type:TileNotBlocked +- type: construction + name: glass firelock + id: FirelockGlass + graph: Firelock + startNode: start + targetNode: FirelockGlass + category: construction-category-structures + description: This is a firelock - it locks an area when a fire alarm in the area is triggered. Don't get squished! + icon: + sprite: Structures/Doors/Airlocks/Glass/firelock.rsi + state: closed + objectType: Structure + placementMode: SnapgridCenter + canBuildInImpassable: false + conditions: + - !type:TileNotBlocked + - type: construction name: shutter id: Shutters