mirror of
https://github.com/space-syndicate/space-station-14.git
synced 2026-10-09 10:21:18 +03:00
network HandsComponent, fix changeling not updating hand displacements on transform (#43545)
* network handscomp * fix * small clean * only clone displacements * TODO --------- Co-authored-by: ScarKy0 <scarky0@onet.eu>
This commit is contained in:
parent
295d73317c
commit
d1a53fcc54
10 changed files with 204 additions and 38 deletions
|
|
@ -57,6 +57,13 @@ namespace Content.Client.Hands.Systems
|
|||
#region StateHandling
|
||||
private void HandleComponentState(Entity<HandsComponent> ent, ref ComponentHandleState args)
|
||||
{
|
||||
// No need to update everything if we are only switching hands.
|
||||
if (args.Current is HandsComponentActiveHandDeltaState activeHandState)
|
||||
{
|
||||
SetActiveHand(ent.AsNullable(), activeHandState.ActiveHandId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.Current is not HandsComponentState state)
|
||||
return;
|
||||
|
||||
|
|
@ -72,10 +79,18 @@ namespace Content.Client.Hands.Systems
|
|||
{
|
||||
AddHand(ent.AsNullable(), handId, state.Hands[handId]);
|
||||
}
|
||||
ent.Comp.SortedHands = new (state.SortedHands);
|
||||
ent.Comp.SortedHands = new(state.SortedHands);
|
||||
|
||||
SetActiveHand(ent.AsNullable(), state.ActiveHandId);
|
||||
|
||||
ent.Comp.ShowInHands = state.ShowInHands;
|
||||
ent.Comp.HandDisplacement = state.HandDisplacement;
|
||||
ent.Comp.LeftHandDisplacement = state.LeftHandDisplacement;
|
||||
ent.Comp.RightHandDisplacement = state.RightHandDisplacement;
|
||||
ent.Comp.CanBeStripped = state.CanBeStripped;
|
||||
|
||||
// TODO: Ideally this would only update if the displacement data actually changed, but there is no way to compare it since the type is not equatable.
|
||||
UpdateAllHandVisuals((ent.Owner, ent.Comp));
|
||||
_stripSys.UpdateUi(ent);
|
||||
}
|
||||
#endregion
|
||||
|
|
@ -243,6 +258,20 @@ namespace Content.Client.Hands.Systems
|
|||
OnPlayerHandUnblocked?.Invoke(args.Container.ID);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update the players sprite with new in-hand visuals for all held items.
|
||||
/// </summary>
|
||||
private void UpdateAllHandVisuals(Entity<HandsComponent?> ent)
|
||||
{
|
||||
foreach (var handId in EnumerateHands(ent))
|
||||
{
|
||||
if (!TryGetHeldItem(ent, handId, out var held))
|
||||
continue;
|
||||
|
||||
UpdateHandVisuals(ent, held.Value, handId);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update the players sprite with new in-hand visuals.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -67,7 +67,28 @@ namespace Content.Server.Hands.Systems
|
|||
|
||||
private void GetComponentState(EntityUid uid, HandsComponent hands, ref ComponentGetState args)
|
||||
{
|
||||
args.State = new HandsComponentState(hands);
|
||||
// If we only switch hands don't send a full state.
|
||||
if (args.FromTick > hands.CreationTick && hands.LastUnclassifiedDirty >= args.FromTick)
|
||||
{
|
||||
var aspects = EntityManager.GetModifiedAspects(hands, args.FromTick);
|
||||
|
||||
if (aspects == ActiveHandIdIndex)
|
||||
{
|
||||
args.State = new HandsComponentActiveHandDeltaState(hands.ActiveHandId);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Get full state.
|
||||
args.State = new HandsComponentState(
|
||||
hands.ActiveHandId,
|
||||
hands.Hands,
|
||||
hands.SortedHands,
|
||||
hands.ShowInHands,
|
||||
hands.HandDisplacement,
|
||||
hands.LeftHandDisplacement,
|
||||
hands.RightHandDisplacement,
|
||||
hands.CanBeStripped);
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -16,4 +16,12 @@ public sealed partial class DisplacementData
|
|||
|
||||
[DataField]
|
||||
public string ShaderOverrideUnshaded = "DisplacedDrawUnshaded";
|
||||
|
||||
// Shallow copy for networking purposes.
|
||||
public DisplacementData(DisplacementData other)
|
||||
{
|
||||
SizeMaps = new(other.SizeMaps);
|
||||
ShaderOverride = other.ShaderOverride;
|
||||
ShaderOverrideUnshaded = other.ShaderOverrideUnshaded;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,21 +4,39 @@ using Content.Shared.Whitelist;
|
|||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Shared.Hands.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Allows this entity to have hands so that it can interact with items.
|
||||
/// </summary>
|
||||
[RegisterComponent, NetworkedComponent, AutoGenerateComponentPause]
|
||||
[Access(typeof(SharedHandsSystem))]
|
||||
public sealed partial class HandsComponent : Component
|
||||
public sealed partial class HandsComponent : Component, IComponentDelta
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public GameTick[] LastModifiedFields { get; set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public GameTick LastUnclassifiedDirty { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The currently active hand.
|
||||
/// The currently active hand.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public string? ActiveHandId;
|
||||
|
||||
/// <summary>
|
||||
/// Intrinsic hands to be added on map init.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public Dictionary<string, Hand> StartingHands = new();
|
||||
|
||||
/// <summary>
|
||||
/// Contains all hands this entity currently has.
|
||||
/// Dictionary relating a unique hand ID corresponding to a container slot on the attached entity to a class containing information about the Hand itself.
|
||||
/// Do not set this in yaml if you want to add intrinsic hands. Use <see cref="StartingHands"/> instead.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public Dictionary<string, Hand> Hands = new();
|
||||
|
|
@ -30,68 +48,68 @@ public sealed partial class HandsComponent : Component
|
|||
public int Count => Hands.Count;
|
||||
|
||||
/// <summary>
|
||||
/// List of hand-names. These are keys for <see cref="Hands"/>. The order of this list determines the order in which hands are iterated over.
|
||||
/// List of hand-names. These are keys for <see cref="Hands"/>. The order of this list determines the order in which hands are iterated over.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public List<string> SortedHands = new();
|
||||
|
||||
/// <summary>
|
||||
/// If true, the items in the hands won't be affected by explosions.
|
||||
/// If true, the items in the hands won't be affected by explosions.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public bool DisableExplosionRecursion;
|
||||
|
||||
/// <summary>
|
||||
/// Modifies the speed at which items are thrown.
|
||||
/// Modifies the speed at which items are thrown.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public float BaseThrowspeed = 11f;
|
||||
|
||||
/// <summary>
|
||||
/// Distance after which longer throw targets stop increasing throw impulse.
|
||||
/// Distance after which longer throw targets stop increasing throw impulse.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public float ThrowRange = 8f;
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not to add in-hand sprites for held items. Some entities (e.g., drones) don't want these.
|
||||
/// Used by the client.
|
||||
/// Whether or not to add in-hand sprites for held items. Some entities (e.g., drones) don't want these.
|
||||
/// Used by the client.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public bool ShowInHands = true;
|
||||
|
||||
/// <summary>
|
||||
/// Data about the current sprite layers that the hand is contributing to the owner entity. Used for sprite in-hands.
|
||||
/// Used by the client.
|
||||
/// Data about the current sprite layers that the hand is contributing to the owner entity. Used for sprite in-hands.
|
||||
/// Used by the client.
|
||||
/// </summary>
|
||||
public readonly Dictionary<HandLocation, HashSet<string>> RevealedLayers = new();
|
||||
|
||||
/// <summary>
|
||||
/// The time at which throws will be allowed again.
|
||||
/// The time at which throws will be allowed again.
|
||||
/// </summary>
|
||||
[DataField, AutoPausedField]
|
||||
public TimeSpan NextThrowTime;
|
||||
|
||||
/// <summary>
|
||||
/// The minimum time inbetween throws.
|
||||
/// The minimum time inbetween throws.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public TimeSpan ThrowCooldown = TimeSpan.FromSeconds(0.5f);
|
||||
|
||||
/// <summary>
|
||||
/// Fallback displacement map applied to all sprites in the hand, unless otherwise specified
|
||||
/// Fallback displacement map applied to all sprites in the hand, unless otherwise specified
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public DisplacementData? HandDisplacement;
|
||||
|
||||
/// <summary>
|
||||
/// If defined, applies to all sprites in the left hand, ignoring <see cref="HandDisplacement"/>
|
||||
/// If defined, applies to all sprites in the left hand, ignoring <see cref="HandDisplacement"/>
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public DisplacementData? LeftHandDisplacement;
|
||||
|
||||
/// <summary>
|
||||
/// If defined, applies to all sprites in the right hand, ignoring <see cref="HandDisplacement"/>
|
||||
/// If defined, applies to all sprites in the right hand, ignoring <see cref="HandDisplacement"/>
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public DisplacementData? RightHandDisplacement;
|
||||
|
|
@ -103,6 +121,9 @@ public sealed partial class HandsComponent : Component
|
|||
public bool CanBeStripped = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parameters for a single hand.
|
||||
/// </summary>
|
||||
[DataDefinition]
|
||||
[Serializable, NetSerializable]
|
||||
public partial record struct Hand
|
||||
|
|
@ -150,24 +171,63 @@ public partial record struct Hand
|
|||
}
|
||||
}
|
||||
|
||||
// If you add more fields make sure to also add them to the RegisterFields call in SharedHandsSystem!
|
||||
// This is needed for delta states.
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class HandsComponentState : ComponentState
|
||||
public sealed class HandsComponentState(
|
||||
string? activeHandId,
|
||||
Dictionary<string, Hand> hands,
|
||||
List<string> sortedHands,
|
||||
bool showInHands,
|
||||
DisplacementData? handDisplacement,
|
||||
DisplacementData? leftHandDisplacement,
|
||||
DisplacementData? rightHandDisplacement,
|
||||
bool canBeStripped) : ComponentState
|
||||
{
|
||||
public readonly Dictionary<string, Hand> Hands;
|
||||
public readonly List<string> SortedHands;
|
||||
public readonly string? ActiveHandId;
|
||||
public string? ActiveHandId = activeHandId;
|
||||
public readonly Dictionary<string, Hand> Hands = new(hands);
|
||||
public readonly List<string> SortedHands = new(sortedHands);
|
||||
public readonly bool ShowInHands = showInHands;
|
||||
public readonly DisplacementData? HandDisplacement = handDisplacement == null ? null : new(handDisplacement);
|
||||
public readonly DisplacementData? LeftHandDisplacement = leftHandDisplacement == null ? null : new(leftHandDisplacement);
|
||||
public readonly DisplacementData? RightHandDisplacement = rightHandDisplacement == null ? null : new(rightHandDisplacement);
|
||||
public readonly bool CanBeStripped = canBeStripped;
|
||||
}
|
||||
|
||||
public HandsComponentState(HandsComponent handComp)
|
||||
/// <summary>
|
||||
/// Delta state for the active hand so that we don't have to network
|
||||
/// the entire component inluding displacements each time we switch hands.
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class HandsComponentActiveHandDeltaState(string? activeHandId) : IComponentDeltaState<HandsComponentState>
|
||||
{
|
||||
public string? ActiveHandId = activeHandId;
|
||||
|
||||
public void ApplyToFullState(HandsComponentState fullState)
|
||||
{
|
||||
// cloning lists because of test networking.
|
||||
Hands = new(handComp.Hands);
|
||||
SortedHands = new(handComp.SortedHands);
|
||||
ActiveHandId = handComp.ActiveHandId;
|
||||
fullState.ActiveHandId = ActiveHandId;
|
||||
}
|
||||
|
||||
public HandsComponentState CreateNewFullState(HandsComponentState fullState)
|
||||
{
|
||||
var newState = new HandsComponentState(
|
||||
fullState.ActiveHandId,
|
||||
fullState.Hands,
|
||||
fullState.SortedHands,
|
||||
fullState.ShowInHands,
|
||||
fullState.HandDisplacement,
|
||||
fullState.LeftHandDisplacement,
|
||||
fullState.RightHandDisplacement,
|
||||
fullState.CanBeStripped)
|
||||
{
|
||||
ActiveHandId = fullState.ActiveHandId,
|
||||
};
|
||||
return newState;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// What side of the body this hand is on.
|
||||
/// What side of the body this hand is on.
|
||||
/// </summary>
|
||||
public enum HandLocation : byte
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
using Content.Shared.Hands.Components;
|
||||
using Content.Shared.Inventory.Events;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Shared.Hands.EntitySystems;
|
||||
|
||||
public sealed partial class ExtraHandsEquipmentSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private SharedHandsSystem _hands = default!;
|
||||
[Dependency] private IGameTiming _timing = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
|
|
@ -17,6 +19,9 @@ public sealed partial class ExtraHandsEquipmentSystem : EntitySystem
|
|||
|
||||
private void OnEquipped(Entity<ExtraHandsEquipmentComponent> ent, ref GotEquippedEvent args)
|
||||
{
|
||||
if (_timing.ApplyingState)
|
||||
return; // The changes are already networked as part of the same game state.
|
||||
|
||||
if (!TryComp<HandsComponent>(args.EquipTarget, out var handsComp))
|
||||
return;
|
||||
|
||||
|
|
@ -30,6 +35,9 @@ public sealed partial class ExtraHandsEquipmentSystem : EntitySystem
|
|||
|
||||
private void OnUnequipped(Entity<ExtraHandsEquipmentComponent> ent, ref GotUnequippedEvent args)
|
||||
{
|
||||
if (_timing.ApplyingState)
|
||||
return; // The changes are already networked as part of the same game state.
|
||||
|
||||
if (!TryComp<HandsComponent>(args.EquipTarget, out var handsComp))
|
||||
return;
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ using System.Diagnostics.CodeAnalysis;
|
|||
using System.Linq;
|
||||
using Content.Shared.ActionBlocker;
|
||||
using Content.Shared.Administration.Logs;
|
||||
using Content.Shared.Cloning.Events;
|
||||
using Content.Shared.Hands.Components;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Inventory;
|
||||
|
|
@ -30,6 +31,7 @@ public abstract partial class SharedHandsSystem
|
|||
public event Action<Entity<HandsComponent>, string, HandLocation>? OnPlayerAddHand;
|
||||
public event Action<Entity<HandsComponent>, string>? OnPlayerRemoveHand;
|
||||
protected event Action<Entity<HandsComponent>?>? OnHandSetActive;
|
||||
protected byte ActiveHandIdIndex;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
|
|
@ -43,6 +45,20 @@ public abstract partial class SharedHandsSystem
|
|||
|
||||
SubscribeLocalEvent<HandsComponent, ComponentInit>(OnInit);
|
||||
SubscribeLocalEvent<HandsComponent, MapInitEvent>(OnMapInit);
|
||||
SubscribeLocalEvent<HandsComponent, CloningEvent>(OnClone);
|
||||
|
||||
// Needed for manual delta states.
|
||||
EntityManager.ComponentFactory.RegisterNetworkedFields<HandsComponent>(
|
||||
nameof(HandsComponent.ActiveHandId),
|
||||
nameof(HandsComponent.Hands),
|
||||
nameof(HandsComponent.SortedHands),
|
||||
nameof(HandsComponent.ShowInHands),
|
||||
nameof(HandsComponent.HandDisplacement),
|
||||
nameof(HandsComponent.LeftHandDisplacement),
|
||||
nameof(HandsComponent.RightHandDisplacement),
|
||||
nameof(HandsComponent.CanBeStripped));
|
||||
|
||||
ActiveHandIdIndex = 0; // Corresponds to HandsComponentActiveHandDeltaState
|
||||
}
|
||||
|
||||
public override void Shutdown()
|
||||
|
|
@ -63,10 +79,38 @@ public abstract partial class SharedHandsSystem
|
|||
|
||||
private void OnMapInit(Entity<HandsComponent> ent, ref MapInitEvent args)
|
||||
{
|
||||
foreach (var (handId, hand) in ent.Comp.StartingHands)
|
||||
AddHand(ent.AsNullable(), handId, hand);
|
||||
|
||||
if (ent.Comp.ActiveHandId == null)
|
||||
SetActiveHand(ent.AsNullable(), ent.Comp.SortedHands.FirstOrDefault());
|
||||
}
|
||||
|
||||
private void OnClone(Entity<HandsComponent> ent, ref CloningEvent args)
|
||||
{
|
||||
if (!args.Settings.EventComponents.Contains(Factory.GetRegistration(ent.Comp.GetType()).Name))
|
||||
return;
|
||||
|
||||
var targetComp = EnsureComp<HandsComponent>(args.CloneUid);
|
||||
// Don't copy the Hands or SortedHands datafields since those are dynamically added and removed on map init or through other components
|
||||
// targetComp.StartingHands = ent.Comp.StartingHands;
|
||||
// A lot of hand related stuff is done via organs.
|
||||
// Because of that we cannot sanely clone hands, but we can still clone over displacements etc.
|
||||
// Organ cloning logic cannot be worked on until body system work progresses enough to allow us to do that.
|
||||
// TODO: Clone hands properly once we can do organ cloning.
|
||||
targetComp.DisableExplosionRecursion = ent.Comp.DisableExplosionRecursion;
|
||||
targetComp.BaseThrowspeed = ent.Comp.BaseThrowspeed;
|
||||
targetComp.ThrowRange = ent.Comp.ThrowRange;
|
||||
targetComp.ShowInHands = ent.Comp.ShowInHands;
|
||||
targetComp.ThrowCooldown = ent.Comp.ThrowCooldown;
|
||||
targetComp.HandDisplacement = ent.Comp.HandDisplacement;
|
||||
targetComp.LeftHandDisplacement = ent.Comp.LeftHandDisplacement;
|
||||
targetComp.RightHandDisplacement = ent.Comp.RightHandDisplacement;
|
||||
targetComp.CanBeStripped = ent.Comp.CanBeStripped;
|
||||
|
||||
Dirty(args.CloneUid, targetComp);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a hand with the given container id and supplied location to the specified entity.
|
||||
/// </summary>
|
||||
|
|
@ -338,7 +382,7 @@ public abstract partial class SharedHandsSystem
|
|||
if (TryGetHeldItem(ent, handId, out var newHeld))
|
||||
RaiseLocalEvent(newHeld.Value, new HandSelectedEvent(ent));
|
||||
|
||||
Dirty(ent);
|
||||
DirtyField(ent, nameof(HandsComponent.ActiveHandId));
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1665,9 +1665,8 @@
|
|||
- map: [ "mask" ]
|
||||
- map: [ "head" ]
|
||||
- type: Hands
|
||||
activeHandId: Hand
|
||||
hands:
|
||||
Hand:
|
||||
startingHands:
|
||||
hand_left:
|
||||
location: Left
|
||||
- type: ComplexInteraction
|
||||
- type: CreamPied
|
||||
|
|
|
|||
|
|
@ -26,14 +26,11 @@
|
|||
canInteract: true
|
||||
- type: GhostHearing
|
||||
- type: Hands
|
||||
hands:
|
||||
startingHands:
|
||||
hand_right:
|
||||
location: Right
|
||||
hand_left:
|
||||
location: Left
|
||||
sortedHands:
|
||||
- hand_right
|
||||
- hand_left
|
||||
- type: ComplexInteraction
|
||||
- type: Puller
|
||||
needsHands: false
|
||||
|
|
|
|||
|
|
@ -73,6 +73,7 @@
|
|||
- SpanishAccent
|
||||
- StutteringAccent
|
||||
eventComponents:
|
||||
- Hands # displacements
|
||||
- Puller
|
||||
- Vocal # voice sounds
|
||||
|
||||
|
|
|
|||
|
|
@ -232,9 +232,8 @@
|
|||
- type: Inventory
|
||||
templateId: holoclown
|
||||
- type: Hands
|
||||
activeHandId: Hand
|
||||
hands:
|
||||
Hand:
|
||||
startingHands:
|
||||
hand_left:
|
||||
location: Left
|
||||
- type: ComplexInteraction
|
||||
- type: MeleeWeapon
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue