Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement vital chef's hat functionality #25950

Merged
merged 7 commits into from
Jun 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions Content.Client/Clothing/Systems/PilotedByClothingSystem.cs
Original file line number Diff line number Diff line change
@@ -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<PilotedByClothingComponent, UpdateIsPredictedEvent>(OnUpdatePredicted);
}

private void OnUpdatePredicted(Entity<PilotedByClothingComponent> entity, ref UpdateIsPredictedEvent args)
{
args.BlockPrediction = true;
}
}
12 changes: 12 additions & 0 deletions Content.Shared/Clothing/Components/PilotedByClothingComponent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using Robust.Shared.GameStates;

namespace Content.Shared.Clothing.Components;

/// <summary>
/// Disables client-side physics prediction for this entity.
/// Without this, movement with <see cref="PilotedClothingSystem"/> is very rubberbandy.
/// </summary>
[RegisterComponent, NetworkedComponent]
public sealed partial class PilotedByClothingComponent : Component
{
}
38 changes: 38 additions & 0 deletions Content.Shared/Clothing/Components/PilotedClothingComponent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
using Content.Shared.Whitelist;
using Robust.Shared.GameStates;

namespace Content.Shared.Clothing.Components;

/// <summary>
/// Allows an entity stored in this clothing item to pass inputs to the entity wearing it.
/// </summary>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
public sealed partial class PilotedClothingComponent : Component
{
/// <summary>
/// Whitelist for entities that are allowed to act as pilots when inside this entity.
/// </summary>
[DataField]
public EntityWhitelist? PilotWhitelist;

/// <summary>
/// Should movement input be relayed from the pilot to the target?
/// </summary>
[DataField]
public bool RelayMovement = true;


/// <summary>
/// Reference to the entity contained in the clothing and acting as pilot.
/// </summary>
[DataField, AutoNetworkedField]
public EntityUid? Pilot;

/// <summary>
/// Reference to the entity wearing this clothing who will be controlled by the pilot.
/// </summary>
[DataField, AutoNetworkedField]
public EntityUid? Wearer;

public bool IsActive => Pilot != null && Wearer != null;
}
169 changes: 169 additions & 0 deletions Content.Shared/Clothing/EntitySystems/PilotedClothingSystem.cs
Original file line number Diff line number Diff line change
@@ -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<PilotedClothingComponent, EntInsertedIntoContainerMessage>(OnEntInserted);
SubscribeLocalEvent<PilotedClothingComponent, EntRemovedFromContainerMessage>(OnEntRemoved);
SubscribeLocalEvent<PilotedClothingComponent, GotEquippedEvent>(OnEquipped);
SubscribeLocalEvent<PilotedClothingComponent, GotUnequippedEvent>(OnUnequipped);
}

private void OnEntInserted(Entity<PilotedClothingComponent> 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<PilotedClothingComponent> 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<PilotedClothingComponent> 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<PilotedClothingComponent> entity, ref GotUnequippedEvent args)
{
StopPiloting(entity);

entity.Comp.Wearer = null;
Dirty(entity);
}

/// <summary>
/// Attempts to establish movement/interaction relay connection(s) from Pilot to Wearer.
/// If either is missing, fails and returns false.
/// </summary>
private bool StartPiloting(Entity<PilotedClothingComponent> 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<PilotedByClothingComponent>(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;
}

/// <summary>
/// Removes components from the Pilot and Wearer to stop the control relay.
/// Returns false if a connection does not already exist.
/// </summary>
private bool StopPiloting(Entity<PilotedClothingComponent> 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<RelayInputMoverComponent>(pilotEnt);

// Clean up components on the Wearer
var wearerEnt = entity.Comp.Wearer.Value;
RemCompDeferred<MovementRelayTargetComponent>(wearerEnt);
RemCompDeferred<PilotedByClothingComponent>(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;
}
}

/// <summary>
/// Raised on the Pilot when they gain control of the Wearer.
/// </summary>
[ByRefEvent]
public record struct StartedPilotingClothingEvent(EntityUid Clothing, EntityUid Wearer);

/// <summary>
/// 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.
/// </summary>
[ByRefEvent]
public record struct StoppedPilotingClothingEvent(EntityUid Clothing, EntityUid Wearer);

/// <summary>
/// Raised on the Wearer when the Pilot gains control of them.
/// </summary>
[ByRefEvent]
public record struct StartingBeingPilotedByClothing(EntityUid Clothing, EntityUid Pilot);

/// <summary>
/// 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.
/// </summary>
[ByRefEvent]
public record struct StoppedBeingPilotedByClothing(EntityUid Clothing, EntityUid Pilot);
4 changes: 4 additions & 0 deletions Resources/Prototypes/Entities/Clothing/Head/hats.yml
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,10 @@
- type: ContainerContainer
containers:
storagebase: !type:Container
- type: PilotedClothing
pilotWhitelist:
tags:
- ChefPilot
- type: Tag
tags:
- ClothMade
Expand Down
2 changes: 2 additions & 0 deletions Resources/Prototypes/Entities/Mobs/NPCs/animals.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1621,6 +1621,7 @@
tags:
- Trash
- VimPilot
- ChefPilot
- Mouse
- Meat
- type: Respirator
Expand Down Expand Up @@ -3173,6 +3174,7 @@
- type: Tag
tags:
- VimPilot
- ChefPilot
- Trash
- Hamster
- Meat
Expand Down
4 changes: 4 additions & 0 deletions Resources/Prototypes/tags.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Loading