diff --git a/Content.Client/Clothing/Systems/PilotedByClothingSystem.cs b/Content.Client/Clothing/Systems/PilotedByClothingSystem.cs new file mode 100644 index 00000000000000..c04cf0a60bacc9 --- /dev/null +++ b/Content.Client/Clothing/Systems/PilotedByClothingSystem.cs @@ -0,0 +1,19 @@ +using Content.Shared.Clothing.Components; +using Robust.Client.Physics; + +namespace Content.Client.Clothing.Systems; + +public sealed partial class PilotedByClothingSystem : EntitySystem +{ + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnUpdatePredicted); + } + + private void OnUpdatePredicted(Entity entity, ref UpdateIsPredictedEvent args) + { + args.BlockPrediction = true; + } +} diff --git a/Content.Shared/Clothing/Components/PilotedByClothingComponent.cs b/Content.Shared/Clothing/Components/PilotedByClothingComponent.cs new file mode 100644 index 00000000000000..cd4d0d62030538 --- /dev/null +++ b/Content.Shared/Clothing/Components/PilotedByClothingComponent.cs @@ -0,0 +1,12 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Clothing.Components; + +/// +/// Disables client-side physics prediction for this entity. +/// Without this, movement with is very rubberbandy. +/// +[RegisterComponent, NetworkedComponent] +public sealed partial class PilotedByClothingComponent : Component +{ +} diff --git a/Content.Shared/Clothing/Components/PilotedClothingComponent.cs b/Content.Shared/Clothing/Components/PilotedClothingComponent.cs new file mode 100644 index 00000000000000..a349e4e485ee13 --- /dev/null +++ b/Content.Shared/Clothing/Components/PilotedClothingComponent.cs @@ -0,0 +1,38 @@ +using Content.Shared.Whitelist; +using Robust.Shared.GameStates; + +namespace Content.Shared.Clothing.Components; + +/// +/// Allows an entity stored in this clothing item to pass inputs to the entity wearing it. +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +public sealed partial class PilotedClothingComponent : Component +{ + /// + /// Whitelist for entities that are allowed to act as pilots when inside this entity. + /// + [DataField] + public EntityWhitelist? PilotWhitelist; + + /// + /// Should movement input be relayed from the pilot to the target? + /// + [DataField] + public bool RelayMovement = true; + + + /// + /// Reference to the entity contained in the clothing and acting as pilot. + /// + [DataField, AutoNetworkedField] + public EntityUid? Pilot; + + /// + /// Reference to the entity wearing this clothing who will be controlled by the pilot. + /// + [DataField, AutoNetworkedField] + public EntityUid? Wearer; + + public bool IsActive => Pilot != null && Wearer != null; +} diff --git a/Content.Shared/Clothing/EntitySystems/PilotedClothingSystem.cs b/Content.Shared/Clothing/EntitySystems/PilotedClothingSystem.cs new file mode 100644 index 00000000000000..49df7aee9430f8 --- /dev/null +++ b/Content.Shared/Clothing/EntitySystems/PilotedClothingSystem.cs @@ -0,0 +1,169 @@ +using Content.Shared.Clothing.Components; +using Content.Shared.Inventory.Events; +using Content.Shared.Movement.Components; +using Content.Shared.Movement.Systems; +using Content.Shared.Storage; +using Content.Shared.Whitelist; +using Robust.Shared.Containers; +using Robust.Shared.Timing; + +namespace Content.Shared.Clothing.EntitySystems; + +public sealed partial class PilotedClothingSystem : EntitySystem +{ + [Dependency] private readonly IGameTiming _timing = default!; + [Dependency] private readonly SharedMoverController _moverController = default!; + [Dependency] private readonly EntityWhitelistSystem _whitelist = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnEntInserted); + SubscribeLocalEvent(OnEntRemoved); + SubscribeLocalEvent(OnEquipped); + SubscribeLocalEvent(OnUnequipped); + } + + private void OnEntInserted(Entity entity, ref EntInsertedIntoContainerMessage args) + { + // Make sure the entity was actually inserted into storage and not a different container. + if (!TryComp(entity, out StorageComponent? storage) || args.Container != storage.Container) + return; + + // Check potential pilot against whitelist, if one exists. + if (_whitelist.IsWhitelistFail(entity.Comp.PilotWhitelist, args.Entity)) + return; + + entity.Comp.Pilot = args.Entity; + Dirty(entity); + + // Attempt to setup control link, if Pilot and Wearer are both present. + StartPiloting(entity); + } + + private void OnEntRemoved(Entity entity, ref EntRemovedFromContainerMessage args) + { + // Make sure the removed entity is actually the pilot. + if (args.Entity != entity.Comp.Pilot) + return; + + StopPiloting(entity); + entity.Comp.Pilot = null; + Dirty(entity); + } + + private void OnEquipped(Entity entity, ref GotEquippedEvent args) + { + if (!TryComp(entity, out ClothingComponent? clothing)) + return; + + // Make sure the clothing item was equipped to the right slot, and not just held in a hand. + var isCorrectSlot = (clothing.Slots & args.SlotFlags) != Inventory.SlotFlags.NONE; + if (!isCorrectSlot) + return; + + entity.Comp.Wearer = args.Equipee; + Dirty(entity); + + // Attempt to setup control link, if Pilot and Wearer are both present. + StartPiloting(entity); + } + + private void OnUnequipped(Entity entity, ref GotUnequippedEvent args) + { + StopPiloting(entity); + + entity.Comp.Wearer = null; + Dirty(entity); + } + + /// + /// Attempts to establish movement/interaction relay connection(s) from Pilot to Wearer. + /// If either is missing, fails and returns false. + /// + private bool StartPiloting(Entity entity) + { + // Make sure we have both a Pilot and a Wearer + if (entity.Comp.Pilot == null || entity.Comp.Wearer == null) + return false; + + if (!_timing.IsFirstTimePredicted) + return false; + + var pilotEnt = entity.Comp.Pilot.Value; + var wearerEnt = entity.Comp.Wearer.Value; + + // Add component to block prediction of wearer + EnsureComp(wearerEnt); + + if (entity.Comp.RelayMovement) + { + // Establish movement input relay. + _moverController.SetRelay(pilotEnt, wearerEnt); + } + + var pilotEv = new StartedPilotingClothingEvent(entity, wearerEnt); + RaiseLocalEvent(pilotEnt, ref pilotEv); + + var wearerEv = new StartingBeingPilotedByClothing(entity, pilotEnt); + RaiseLocalEvent(wearerEnt, ref wearerEv); + + return true; + } + + /// + /// Removes components from the Pilot and Wearer to stop the control relay. + /// Returns false if a connection does not already exist. + /// + private bool StopPiloting(Entity entity) + { + if (entity.Comp.Pilot == null || entity.Comp.Wearer == null) + return false; + + // Clean up components on the Pilot + var pilotEnt = entity.Comp.Pilot.Value; + RemCompDeferred(pilotEnt); + + // Clean up components on the Wearer + var wearerEnt = entity.Comp.Wearer.Value; + RemCompDeferred(wearerEnt); + RemCompDeferred(wearerEnt); + + // Raise an event on the Pilot + var pilotEv = new StoppedPilotingClothingEvent(entity, wearerEnt); + RaiseLocalEvent(pilotEnt, ref pilotEv); + + // Raise an event on the Wearer + var wearerEv = new StoppedBeingPilotedByClothing(entity, pilotEnt); + RaiseLocalEvent(wearerEnt, ref wearerEv); + + return true; + } +} + +/// +/// Raised on the Pilot when they gain control of the Wearer. +/// +[ByRefEvent] +public record struct StartedPilotingClothingEvent(EntityUid Clothing, EntityUid Wearer); + +/// +/// Raised on the Pilot when they lose control of the Wearer, +/// due to the Pilot exiting the clothing or the clothing being unequipped by the Wearer. +/// +[ByRefEvent] +public record struct StoppedPilotingClothingEvent(EntityUid Clothing, EntityUid Wearer); + +/// +/// Raised on the Wearer when the Pilot gains control of them. +/// +[ByRefEvent] +public record struct StartingBeingPilotedByClothing(EntityUid Clothing, EntityUid Pilot); + +/// +/// Raised on the Wearer when the Pilot loses control of them +/// due to the Pilot exiting the clothing or the clothing being unequipped by the Wearer. +/// +[ByRefEvent] +public record struct StoppedBeingPilotedByClothing(EntityUid Clothing, EntityUid Pilot); diff --git a/Resources/Prototypes/Entities/Clothing/Head/hats.yml b/Resources/Prototypes/Entities/Clothing/Head/hats.yml index 2d2b2a353ef39a..b58727694a9fa8 100644 --- a/Resources/Prototypes/Entities/Clothing/Head/hats.yml +++ b/Resources/Prototypes/Entities/Clothing/Head/hats.yml @@ -250,6 +250,10 @@ - type: ContainerContainer containers: storagebase: !type:Container + - type: PilotedClothing + pilotWhitelist: + tags: + - ChefPilot - type: Tag tags: - ClothMade diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml b/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml index 971d05823a5271..fe8293a7b97a5f 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml @@ -1621,6 +1621,7 @@ tags: - Trash - VimPilot + - ChefPilot - Mouse - Meat - type: Respirator @@ -3173,6 +3174,7 @@ - type: Tag tags: - VimPilot + - ChefPilot - Trash - Hamster - Meat diff --git a/Resources/Prototypes/tags.yml b/Resources/Prototypes/tags.yml index d60b6329865b94..5880203773e7ba 100644 --- a/Resources/Prototypes/tags.yml +++ b/Resources/Prototypes/tags.yml @@ -364,6 +364,10 @@ - type: Tag id: Chicken +# Allowed to control someone wearing a Chef's hat if inside their hat. +- type: Tag + id: ChefPilot + - type: Tag id: ChemDispensable # container that can go into the chem dispenser