Merge remote-tracking branch 'upstream/master' into ed-21-06-2024-upstream-sync
# Conflicts: # Resources/Prototypes/Maps/europa.yml # Resources/Prototypes/Maps/train.yml
This commit is contained in:
@@ -70,7 +70,7 @@ namespace Content.Shared.ActionBlocker
|
||||
return false;
|
||||
|
||||
var ev = new InteractionAttemptEvent(user, target);
|
||||
RaiseLocalEvent(user, ev);
|
||||
RaiseLocalEvent(user, ref ev);
|
||||
|
||||
if (ev.Cancelled)
|
||||
return false;
|
||||
@@ -79,7 +79,7 @@ namespace Content.Shared.ActionBlocker
|
||||
return true;
|
||||
|
||||
var targetEv = new GettingInteractedWithAttemptEvent(user, target);
|
||||
RaiseLocalEvent(target.Value, targetEv);
|
||||
RaiseLocalEvent(target.Value, ref targetEv);
|
||||
|
||||
return !targetEv.Cancelled;
|
||||
}
|
||||
@@ -110,7 +110,7 @@ namespace Content.Shared.ActionBlocker
|
||||
public bool CanConsciouslyPerformAction(EntityUid user)
|
||||
{
|
||||
var ev = new ConsciousAttemptEvent(user);
|
||||
RaiseLocalEvent(user, ev);
|
||||
RaiseLocalEvent(user, ref ev);
|
||||
|
||||
return !ev.Cancelled;
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ using Content.Shared.Throwing;
|
||||
|
||||
namespace Content.Shared.Administration;
|
||||
|
||||
// TODO deduplicate with BlockMovementComponent
|
||||
public abstract class SharedAdminFrozenSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly ActionBlockerSystem _blocker = default!;
|
||||
@@ -23,7 +24,7 @@ public abstract class SharedAdminFrozenSystem : EntitySystem
|
||||
SubscribeLocalEvent<AdminFrozenComponent, UseAttemptEvent>(OnAttempt);
|
||||
SubscribeLocalEvent<AdminFrozenComponent, PickupAttemptEvent>(OnAttempt);
|
||||
SubscribeLocalEvent<AdminFrozenComponent, ThrowAttemptEvent>(OnAttempt);
|
||||
SubscribeLocalEvent<AdminFrozenComponent, InteractionAttemptEvent>(OnAttempt);
|
||||
SubscribeLocalEvent<AdminFrozenComponent, InteractionAttemptEvent>(OnInteractAttempt);
|
||||
SubscribeLocalEvent<AdminFrozenComponent, ComponentStartup>(OnStartup);
|
||||
SubscribeLocalEvent<AdminFrozenComponent, ComponentShutdown>(UpdateCanMove);
|
||||
SubscribeLocalEvent<AdminFrozenComponent, UpdateCanMoveEvent>(OnUpdateCanMove);
|
||||
@@ -34,6 +35,11 @@ public abstract class SharedAdminFrozenSystem : EntitySystem
|
||||
SubscribeLocalEvent<AdminFrozenComponent, SpeakAttemptEvent>(OnSpeakAttempt);
|
||||
}
|
||||
|
||||
private void OnInteractAttempt(Entity<AdminFrozenComponent> ent, ref InteractionAttemptEvent args)
|
||||
{
|
||||
args.Cancelled = true;
|
||||
}
|
||||
|
||||
private void OnSpeakAttempt(EntityUid uid, AdminFrozenComponent component, SpeakAttemptEvent args)
|
||||
{
|
||||
if (!component.Muted)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Content.Shared.Actions;
|
||||
using Content.Shared.Buckle.Components;
|
||||
using Content.Shared.Damage;
|
||||
using Content.Shared.Damage.ForceSay;
|
||||
using Content.Shared.Examine;
|
||||
@@ -59,6 +60,15 @@ public sealed partial class SleepingSystem : EntitySystem
|
||||
SubscribeLocalEvent<SleepingComponent, InteractHandEvent>(OnInteractHand);
|
||||
|
||||
SubscribeLocalEvent<ForcedSleepingComponent, ComponentInit>(OnInit);
|
||||
SubscribeLocalEvent<SleepingComponent, UnbuckleAttemptEvent>(OnUnbuckleAttempt);
|
||||
}
|
||||
|
||||
private void OnUnbuckleAttempt(Entity<SleepingComponent> ent, ref UnbuckleAttemptEvent args)
|
||||
{
|
||||
// TODO is this necessary?
|
||||
// Shouldn't the interaction have already been blocked by a general interaction check?
|
||||
if (ent.Owner == args.User)
|
||||
args.Cancelled = true;
|
||||
}
|
||||
|
||||
private void OnBedSleepAction(Entity<ActionsContainerComponent> ent, ref SleepActionEvent args)
|
||||
@@ -153,7 +163,7 @@ public sealed partial class SleepingSystem : EntitySystem
|
||||
|
||||
private void OnConsciousAttempt(Entity<SleepingComponent> ent, ref ConsciousAttemptEvent args)
|
||||
{
|
||||
args.Cancel();
|
||||
args.Cancelled = true;
|
||||
}
|
||||
|
||||
private void OnExamined(Entity<SleepingComponent> ent, ref ExaminedEvent args)
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Content.Shared.Interaction;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
|
||||
|
||||
namespace Content.Shared.Buckle.Components;
|
||||
|
||||
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(true)]
|
||||
/// <summary>
|
||||
/// This component allows an entity to be buckled to an entity with a <see cref="StrapComponent"/>.
|
||||
/// </summary>
|
||||
[RegisterComponent, NetworkedComponent]
|
||||
[Access(typeof(SharedBuckleSystem))]
|
||||
public sealed partial class BuckleComponent : Component
|
||||
{
|
||||
@@ -14,31 +19,23 @@ public sealed partial class BuckleComponent : Component
|
||||
/// across a table two tiles away" problem.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public float Range = SharedInteractionSystem.InteractionRange / 1.4f;
|
||||
|
||||
/// <summary>
|
||||
/// True if the entity is buckled, false otherwise.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[AutoNetworkedField]
|
||||
public bool Buckled;
|
||||
|
||||
[ViewVariables]
|
||||
[AutoNetworkedField]
|
||||
public EntityUid? LastEntityBuckledTo;
|
||||
[MemberNotNullWhen(true, nameof(BuckledTo))]
|
||||
public bool Buckled => BuckledTo != null;
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not collisions should be possible with the entity we are strapped to
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField, AutoNetworkedField]
|
||||
[DataField]
|
||||
public bool DontCollide;
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not we should be allowed to pull the entity we are strapped to
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField]
|
||||
public bool PullStrap;
|
||||
|
||||
@@ -47,20 +44,18 @@ public sealed partial class BuckleComponent : Component
|
||||
/// be able to unbuckle after recently buckling.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public TimeSpan Delay = TimeSpan.FromSeconds(0.25f);
|
||||
|
||||
/// <summary>
|
||||
/// The time that this entity buckled at.
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public TimeSpan BuckleTime;
|
||||
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
|
||||
public TimeSpan? BuckleTime;
|
||||
|
||||
/// <summary>
|
||||
/// The strap that this component is buckled to.
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
[AutoNetworkedField]
|
||||
[DataField]
|
||||
public EntityUid? BuckledTo;
|
||||
|
||||
/// <summary>
|
||||
@@ -68,7 +63,6 @@ public sealed partial class BuckleComponent : Component
|
||||
/// <see cref="StrapComponent"/>.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public int Size = 100;
|
||||
|
||||
/// <summary>
|
||||
@@ -77,11 +71,90 @@ public sealed partial class BuckleComponent : Component
|
||||
[ViewVariables] public int? OriginalDrawDepth;
|
||||
}
|
||||
|
||||
[ByRefEvent]
|
||||
public record struct BuckleAttemptEvent(EntityUid StrapEntity, EntityUid BuckledEntity, EntityUid UserEntity, bool Buckling, bool Cancelled = false);
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class BuckleState(NetEntity? buckledTo, bool dontCollide, TimeSpan? buckleTime) : ComponentState
|
||||
{
|
||||
public readonly NetEntity? BuckledTo = buckledTo;
|
||||
public readonly bool DontCollide = dontCollide;
|
||||
public readonly TimeSpan? BuckleTime = buckleTime;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Event raised directed at a strap entity before some entity gets buckled to it.
|
||||
/// </summary>
|
||||
[ByRefEvent]
|
||||
public readonly record struct BuckleChangeEvent(EntityUid StrapEntity, EntityUid BuckledEntity, bool Buckling);
|
||||
public record struct StrapAttemptEvent(
|
||||
Entity<StrapComponent> Strap,
|
||||
Entity<BuckleComponent> Buckle,
|
||||
EntityUid? User,
|
||||
bool Popup)
|
||||
{
|
||||
public bool Cancelled;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Event raised directed at a buckle entity before it gets buckled to some strap entity.
|
||||
/// </summary>
|
||||
[ByRefEvent]
|
||||
public record struct BuckleAttemptEvent(
|
||||
Entity<StrapComponent> Strap,
|
||||
Entity<BuckleComponent> Buckle,
|
||||
EntityUid? User,
|
||||
bool Popup)
|
||||
{
|
||||
public bool Cancelled;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Event raised directed at a strap entity before some entity gets unbuckled from it.
|
||||
/// </summary>
|
||||
[ByRefEvent]
|
||||
public record struct UnstrapAttemptEvent(
|
||||
Entity<StrapComponent> Strap,
|
||||
Entity<BuckleComponent> Buckle,
|
||||
EntityUid? User,
|
||||
bool Popup)
|
||||
{
|
||||
public bool Cancelled;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Event raised directed at a buckle entity before it gets unbuckled.
|
||||
/// </summary>
|
||||
[ByRefEvent]
|
||||
public record struct UnbuckleAttemptEvent(
|
||||
Entity<StrapComponent> Strap,
|
||||
Entity<BuckleComponent> Buckle,
|
||||
EntityUid? User,
|
||||
bool Popup)
|
||||
{
|
||||
public bool Cancelled;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Event raised directed at a strap entity after something has been buckled to it.
|
||||
/// </summary>
|
||||
[ByRefEvent]
|
||||
public readonly record struct StrappedEvent(Entity<StrapComponent> Strap, Entity<BuckleComponent> Buckle);
|
||||
|
||||
/// <summary>
|
||||
/// Event raised directed at a buckle entity after it has been buckled.
|
||||
/// </summary>
|
||||
[ByRefEvent]
|
||||
public readonly record struct BuckledEvent(Entity<StrapComponent> Strap, Entity<BuckleComponent> Buckle);
|
||||
|
||||
/// <summary>
|
||||
/// Event raised directed at a strap entity after something has been unbuckled from it.
|
||||
/// </summary>
|
||||
[ByRefEvent]
|
||||
public readonly record struct UnstrappedEvent(Entity<StrapComponent> Strap, Entity<BuckleComponent> Buckle);
|
||||
|
||||
/// <summary>
|
||||
/// Event raised directed at a buckle entity after it has been unbuckled from some strap entity.
|
||||
/// </summary>
|
||||
[ByRefEvent]
|
||||
public readonly record struct UnbuckledEvent(Entity<StrapComponent> Strap, Entity<BuckleComponent> Buckle);
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public enum BuckleVisuals
|
||||
|
||||
@@ -13,117 +13,77 @@ namespace Content.Shared.Buckle.Components;
|
||||
public sealed partial class StrapComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// The entities that are currently buckled
|
||||
/// The entities that are currently buckled to this strap.
|
||||
/// </summary>
|
||||
[AutoNetworkedField]
|
||||
[ViewVariables] // TODO serialization
|
||||
[ViewVariables]
|
||||
public HashSet<EntityUid> BuckledEntities = new();
|
||||
|
||||
/// <summary>
|
||||
/// Entities that this strap accepts and can buckle
|
||||
/// If null it accepts any entity
|
||||
/// </summary>
|
||||
[DataField, ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField]
|
||||
public EntityWhitelist? Whitelist;
|
||||
|
||||
/// <summary>
|
||||
/// Entities that this strap does not accept and cannot buckle.
|
||||
/// </summary>
|
||||
[DataField, ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField]
|
||||
public EntityWhitelist? Blacklist;
|
||||
|
||||
/// <summary>
|
||||
/// The change in position to the strapped mob
|
||||
/// </summary>
|
||||
[DataField, AutoNetworkedField]
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public StrapPosition Position = StrapPosition.None;
|
||||
|
||||
/// <summary>
|
||||
/// The distance above which a buckled entity will be automatically unbuckled.
|
||||
/// Don't change it unless you really have to
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Dont set this below 0.2 because that causes audio issues with <see cref="SharedBuckleSystem.OnBuckleMove"/>
|
||||
/// My guess after testing is that the client sets BuckledTo to the strap in *some* ticks for some reason
|
||||
/// whereas the server doesnt, thus the client tries to unbuckle like 15 times because it passes the strap null check
|
||||
/// This is why this needs to be above 0.1 to make the InRange check fail in both client and server.
|
||||
/// </remarks>
|
||||
[DataField, AutoNetworkedField]
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public float MaxBuckleDistance = 0.2f;
|
||||
|
||||
/// <summary>
|
||||
/// Gets and clamps the buckle offset to MaxBuckleDistance
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public Vector2 BuckleOffsetClamped => Vector2.Clamp(
|
||||
BuckleOffset,
|
||||
Vector2.One * -MaxBuckleDistance,
|
||||
Vector2.One * MaxBuckleDistance);
|
||||
|
||||
/// <summary>
|
||||
/// The buckled entity will be offset by this amount from the center of the strap object.
|
||||
/// If this offset it too big, it will be clamped to <see cref="MaxBuckleDistance"/>
|
||||
/// </summary>
|
||||
[DataField, AutoNetworkedField]
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public Vector2 BuckleOffset = Vector2.Zero;
|
||||
|
||||
/// <summary>
|
||||
/// The angle to rotate the player by when they get strapped
|
||||
/// </summary>
|
||||
[DataField]
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public Angle Rotation;
|
||||
|
||||
/// <summary>
|
||||
/// The size of the strap which is compared against when buckling entities
|
||||
/// </summary>
|
||||
[DataField]
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public int Size = 100;
|
||||
|
||||
/// <summary>
|
||||
/// If disabled, nothing can be buckled on this object, and it will unbuckle anything that's already buckled
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
[DataField, AutoNetworkedField]
|
||||
public bool Enabled = true;
|
||||
|
||||
/// <summary>
|
||||
/// You can specify the offset the entity will have after unbuckling.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public Vector2 UnbuckleOffset = Vector2.Zero;
|
||||
|
||||
/// <summary>
|
||||
/// The sound to be played when a mob is buckled
|
||||
/// </summary>
|
||||
[DataField]
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public SoundSpecifier BuckleSound = new SoundPathSpecifier("/Audio/Effects/buckle.ogg");
|
||||
|
||||
/// <summary>
|
||||
/// The sound to be played when a mob is unbuckled
|
||||
/// </summary>
|
||||
[DataField]
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public SoundSpecifier UnbuckleSound = new SoundPathSpecifier("/Audio/Effects/unbuckle.ogg");
|
||||
|
||||
/// <summary>
|
||||
/// ID of the alert to show when buckled
|
||||
/// </summary>
|
||||
[DataField]
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public ProtoId<AlertPrototype> BuckledAlertType = "Buckled";
|
||||
|
||||
/// <summary>
|
||||
/// The sum of the sizes of all the buckled entities in this strap
|
||||
/// </summary>
|
||||
[AutoNetworkedField]
|
||||
[ViewVariables]
|
||||
public int OccupiedSize;
|
||||
}
|
||||
|
||||
public enum StrapPosition
|
||||
|
||||
@@ -1,39 +1,46 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Numerics;
|
||||
using Content.Shared.Alert;
|
||||
using Content.Shared.Bed.Sleep;
|
||||
using Content.Shared.Buckle.Components;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.Hands.Components;
|
||||
using Content.Shared.IdentityManagement;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Mobs.Components;
|
||||
using Content.Shared.Movement.Events;
|
||||
using Content.Shared.Movement.Pulling.Events;
|
||||
using Content.Shared.Popups;
|
||||
using Content.Shared.Pulling.Events;
|
||||
using Content.Shared.Standing;
|
||||
using Content.Shared.Storage.Components;
|
||||
using Content.Shared.Stunnable;
|
||||
using Content.Shared.Throwing;
|
||||
using Content.Shared.Verbs;
|
||||
using Content.Shared.Whitelist;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Physics.Components;
|
||||
using Robust.Shared.Physics.Events;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Utility;
|
||||
using PullableComponent = Content.Shared.Movement.Pulling.Components.PullableComponent;
|
||||
|
||||
namespace Content.Shared.Buckle;
|
||||
|
||||
public abstract partial class SharedBuckleSystem
|
||||
{
|
||||
public static ProtoId<AlertCategoryPrototype> BuckledAlertCategory = "Buckled";
|
||||
|
||||
[Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!;
|
||||
|
||||
private void InitializeBuckle()
|
||||
{
|
||||
SubscribeLocalEvent<BuckleComponent, ComponentStartup>(OnBuckleComponentStartup);
|
||||
SubscribeLocalEvent<BuckleComponent, ComponentShutdown>(OnBuckleComponentShutdown);
|
||||
SubscribeLocalEvent<BuckleComponent, MoveEvent>(OnBuckleMove);
|
||||
SubscribeLocalEvent<BuckleComponent, InteractHandEvent>(OnBuckleInteractHand);
|
||||
SubscribeLocalEvent<BuckleComponent, GetVerbsEvent<InteractionVerb>>(AddUnbuckleVerb);
|
||||
SubscribeLocalEvent<BuckleComponent, EntParentChangedMessage>(OnParentChanged);
|
||||
SubscribeLocalEvent<BuckleComponent, EntGotInsertedIntoContainerMessage>(OnInserted);
|
||||
|
||||
SubscribeLocalEvent<BuckleComponent, StartPullAttemptEvent>(OnPullAttempt);
|
||||
SubscribeLocalEvent<BuckleComponent, BeingPulledAttemptEvent>(OnBeingPulledAttempt);
|
||||
SubscribeLocalEvent<BuckleComponent, PullStartedMessage>(OnPullStarted);
|
||||
|
||||
SubscribeLocalEvent<BuckleComponent, InsertIntoEntityStorageAttemptEvent>(OnBuckleInsertIntoEntityStorageAttempt);
|
||||
|
||||
SubscribeLocalEvent<BuckleComponent, PreventCollideEvent>(OnBucklePreventCollide);
|
||||
@@ -41,69 +48,93 @@ public abstract partial class SharedBuckleSystem
|
||||
SubscribeLocalEvent<BuckleComponent, StandAttemptEvent>(OnBuckleStandAttempt);
|
||||
SubscribeLocalEvent<BuckleComponent, ThrowPushbackAttemptEvent>(OnBuckleThrowPushbackAttempt);
|
||||
SubscribeLocalEvent<BuckleComponent, UpdateCanMoveEvent>(OnBuckleUpdateCanMove);
|
||||
|
||||
SubscribeLocalEvent<BuckleComponent, ComponentGetState>(OnGetState);
|
||||
}
|
||||
|
||||
[ValidatePrototypeId<AlertCategoryPrototype>]
|
||||
public const string BuckledAlertCategory = "Buckled";
|
||||
|
||||
private void OnBuckleComponentStartup(EntityUid uid, BuckleComponent component, ComponentStartup args)
|
||||
private void OnGetState(Entity<BuckleComponent> ent, ref ComponentGetState args)
|
||||
{
|
||||
UpdateBuckleStatus(uid, component);
|
||||
args.State = new BuckleState(GetNetEntity(ent.Comp.BuckledTo), ent.Comp.DontCollide, ent.Comp.BuckleTime);
|
||||
}
|
||||
|
||||
private void OnBuckleComponentShutdown(EntityUid uid, BuckleComponent component, ComponentShutdown args)
|
||||
private void OnBuckleComponentShutdown(Entity<BuckleComponent> ent, ref ComponentShutdown args)
|
||||
{
|
||||
TryUnbuckle(uid, uid, true, component);
|
||||
|
||||
component.BuckleTime = default;
|
||||
Unbuckle(ent!, null);
|
||||
}
|
||||
|
||||
private void OnBuckleMove(EntityUid uid, BuckleComponent component, ref MoveEvent ev)
|
||||
#region Pulling
|
||||
|
||||
private void OnPullAttempt(Entity<BuckleComponent> ent, ref StartPullAttemptEvent args)
|
||||
{
|
||||
if (component.BuckledTo is not { } strapUid)
|
||||
// Prevent people pulling the chair they're on, etc.
|
||||
if (ent.Comp.BuckledTo == args.Pulled && !ent.Comp.PullStrap)
|
||||
args.Cancel();
|
||||
}
|
||||
|
||||
private void OnBeingPulledAttempt(Entity<BuckleComponent> ent, ref BeingPulledAttemptEvent args)
|
||||
{
|
||||
if (args.Cancelled || !ent.Comp.Buckled)
|
||||
return;
|
||||
|
||||
if (!CanUnbuckle(ent!, args.Puller, false))
|
||||
args.Cancel();
|
||||
}
|
||||
|
||||
private void OnPullStarted(Entity<BuckleComponent> ent, ref PullStartedMessage args)
|
||||
{
|
||||
Unbuckle(ent!, args.PullerUid);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Transform
|
||||
|
||||
private void OnParentChanged(Entity<BuckleComponent> ent, ref EntParentChangedMessage args)
|
||||
{
|
||||
BuckleTransformCheck(ent, args.Transform);
|
||||
}
|
||||
|
||||
private void OnInserted(Entity<BuckleComponent> ent, ref EntGotInsertedIntoContainerMessage args)
|
||||
{
|
||||
BuckleTransformCheck(ent, Transform(ent));
|
||||
}
|
||||
|
||||
private void OnBuckleMove(Entity<BuckleComponent> ent, ref MoveEvent ev)
|
||||
{
|
||||
BuckleTransformCheck(ent, ev.Component);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if the entity should get unbuckled as a result of transform or container changes.
|
||||
/// </summary>
|
||||
private void BuckleTransformCheck(Entity<BuckleComponent> buckle, TransformComponent xform)
|
||||
{
|
||||
if (_gameTiming.ApplyingState)
|
||||
return;
|
||||
|
||||
if (buckle.Comp.BuckledTo is not { } strapUid)
|
||||
return;
|
||||
|
||||
if (!TryComp<StrapComponent>(strapUid, out var strapComp))
|
||||
return;
|
||||
|
||||
var strapPosition = Transform(strapUid).Coordinates;
|
||||
if (ev.NewPosition.EntityId.IsValid() && ev.NewPosition.InRange(EntityManager, _transform, strapPosition, strapComp.MaxBuckleDistance))
|
||||
return;
|
||||
|
||||
TryUnbuckle(uid, uid, true, component);
|
||||
}
|
||||
|
||||
private void OnBuckleInteractHand(EntityUid uid, BuckleComponent component, InteractHandEvent args)
|
||||
{
|
||||
if (!component.Buckled)
|
||||
return;
|
||||
|
||||
if (TryUnbuckle(uid, args.User, buckleComp: component))
|
||||
args.Handled = true;
|
||||
}
|
||||
|
||||
private void AddUnbuckleVerb(EntityUid uid, BuckleComponent component, GetVerbsEvent<InteractionVerb> args)
|
||||
{
|
||||
if (!args.CanAccess || !args.CanInteract || !component.Buckled)
|
||||
return;
|
||||
|
||||
InteractionVerb verb = new()
|
||||
{
|
||||
Act = () => TryUnbuckle(uid, args.User, buckleComp: component),
|
||||
Text = Loc.GetString("verb-categories-unbuckle"),
|
||||
Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/unbuckle.svg.192dpi.png"))
|
||||
};
|
||||
|
||||
if (args.Target == args.User && args.Using == null)
|
||||
{
|
||||
// A user is left clicking themselves with an empty hand, while buckled.
|
||||
// It is very likely they are trying to unbuckle themselves.
|
||||
verb.Priority = 1;
|
||||
Log.Error($"Encountered buckle entity {ToPrettyString(buckle)} without a valid strap entity {ToPrettyString(strapUid)}");
|
||||
SetBuckledTo(buckle, null);
|
||||
return;
|
||||
}
|
||||
|
||||
args.Verbs.Add(verb);
|
||||
if (xform.ParentUid != strapUid || _container.IsEntityInContainer(buckle))
|
||||
{
|
||||
Unbuckle(buckle, (strapUid, strapComp), null);
|
||||
return;
|
||||
}
|
||||
|
||||
var delta = (xform.LocalPosition - strapComp.BuckleOffset).LengthSquared();
|
||||
if (delta > 1e-5)
|
||||
Unbuckle(buckle, (strapUid, strapComp), null);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private void OnBuckleInsertIntoEntityStorageAttempt(EntityUid uid, BuckleComponent component, ref InsertIntoEntityStorageAttemptEvent args)
|
||||
{
|
||||
if (component.Buckled)
|
||||
@@ -112,10 +143,7 @@ public abstract partial class SharedBuckleSystem
|
||||
|
||||
private void OnBucklePreventCollide(EntityUid uid, BuckleComponent component, ref PreventCollideEvent args)
|
||||
{
|
||||
if (args.OtherEntity != component.BuckledTo)
|
||||
return;
|
||||
|
||||
if (component.Buckled || component.DontCollide)
|
||||
if (args.OtherEntity == component.BuckledTo && component.DontCollide)
|
||||
args.Cancelled = true;
|
||||
}
|
||||
|
||||
@@ -139,10 +167,7 @@ public abstract partial class SharedBuckleSystem
|
||||
|
||||
private void OnBuckleUpdateCanMove(EntityUid uid, BuckleComponent component, UpdateCanMoveEvent args)
|
||||
{
|
||||
if (component.LifeStage > ComponentLifeStage.Running)
|
||||
return;
|
||||
|
||||
if (component.Buckled) // buckle shitcode
|
||||
if (component.Buckled)
|
||||
args.Cancel();
|
||||
}
|
||||
|
||||
@@ -151,162 +176,139 @@ public abstract partial class SharedBuckleSystem
|
||||
return Resolve(uid, ref component, false) && component.Buckled;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shows or hides the buckled status effect depending on if the
|
||||
/// entity is buckled or not.
|
||||
/// </summary>
|
||||
/// <param name="uid"> Entity that we want to show the alert </param>
|
||||
/// <param name="buckleComp"> buckle component of the entity </param>
|
||||
/// <param name="strapComp"> strap component of the thing we are strapping to </param>
|
||||
private void UpdateBuckleStatus(EntityUid uid, BuckleComponent buckleComp, StrapComponent? strapComp = null)
|
||||
protected void SetBuckledTo(Entity<BuckleComponent> buckle, Entity<StrapComponent?>? strap)
|
||||
{
|
||||
Appearance.SetData(uid, StrapVisuals.State, buckleComp.Buckled);
|
||||
if (buckleComp.BuckledTo != null)
|
||||
{
|
||||
if (!Resolve(buckleComp.BuckledTo.Value, ref strapComp))
|
||||
return;
|
||||
if (TryComp(buckle.Comp.BuckledTo, out StrapComponent? old))
|
||||
old.BuckledEntities.Remove(buckle);
|
||||
|
||||
var alertType = strapComp.BuckledAlertType;
|
||||
_alerts.ShowAlert(uid, alertType);
|
||||
if (strap is {} strapEnt && Resolve(strapEnt.Owner, ref strapEnt.Comp))
|
||||
{
|
||||
strapEnt.Comp.BuckledEntities.Add(buckle);
|
||||
_alerts.ShowAlert(buckle, strapEnt.Comp.BuckledAlertType);
|
||||
}
|
||||
else
|
||||
{
|
||||
_alerts.ClearAlertCategory(uid, BuckledAlertCategory);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the <see cref="BuckleComponent.BuckledTo"/> field in the component to a value
|
||||
/// </summary>
|
||||
/// <param name="strapUid"> Value tat with be assigned to the field </param>
|
||||
private void SetBuckledTo(EntityUid buckleUid, EntityUid? strapUid, StrapComponent? strapComp, BuckleComponent buckleComp)
|
||||
{
|
||||
buckleComp.BuckledTo = strapUid;
|
||||
|
||||
if (strapUid == null)
|
||||
{
|
||||
buckleComp.Buckled = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
buckleComp.LastEntityBuckledTo = strapUid;
|
||||
buckleComp.DontCollide = true;
|
||||
buckleComp.Buckled = true;
|
||||
buckleComp.BuckleTime = _gameTiming.CurTime;
|
||||
_alerts.ClearAlertCategory(buckle, BuckledAlertCategory);
|
||||
}
|
||||
|
||||
ActionBlocker.UpdateCanMove(buckleUid);
|
||||
UpdateBuckleStatus(buckleUid, buckleComp, strapComp);
|
||||
Dirty(buckleUid, buckleComp);
|
||||
buckle.Comp.BuckledTo = strap;
|
||||
buckle.Comp.BuckleTime = _gameTiming.CurTime;
|
||||
ActionBlocker.UpdateCanMove(buckle);
|
||||
Appearance.SetData(buckle, StrapVisuals.State, buckle.Comp.Buckled);
|
||||
Dirty(buckle);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether or not buckling is possible
|
||||
/// </summary>
|
||||
/// <param name="buckleUid"> Uid of the owner of BuckleComponent </param>
|
||||
/// <param name="userUid">
|
||||
/// Uid of a third party entity,
|
||||
/// i.e, the uid of someone else you are dragging to a chair.
|
||||
/// Can equal buckleUid sometimes
|
||||
/// <param name="user">
|
||||
/// Uid of a third party entity,
|
||||
/// i.e, the uid of someone else you are dragging to a chair.
|
||||
/// Can equal buckleUid sometimes
|
||||
/// </param>
|
||||
/// <param name="strapUid"> Uid of the owner of strap component </param>
|
||||
private bool CanBuckle(
|
||||
EntityUid buckleUid,
|
||||
EntityUid userUid,
|
||||
/// <param name="strapComp"></param>
|
||||
/// <param name="buckleComp"></param>
|
||||
private bool CanBuckle(EntityUid buckleUid,
|
||||
EntityUid? user,
|
||||
EntityUid strapUid,
|
||||
bool popup,
|
||||
[NotNullWhen(true)] out StrapComponent? strapComp,
|
||||
BuckleComponent? buckleComp = null)
|
||||
BuckleComponent buckleComp)
|
||||
{
|
||||
strapComp = null;
|
||||
|
||||
if (userUid == strapUid ||
|
||||
!Resolve(buckleUid, ref buckleComp, false) ||
|
||||
!Resolve(strapUid, ref strapComp, false))
|
||||
{
|
||||
if (!Resolve(strapUid, ref strapComp, false))
|
||||
return false;
|
||||
}
|
||||
|
||||
// Does it pass the Whitelist
|
||||
if (_whitelistSystem.IsWhitelistFail(strapComp.Whitelist, buckleUid) ||
|
||||
_whitelistSystem.IsBlacklistPass(strapComp.Blacklist, buckleUid))
|
||||
{
|
||||
if (_netManager.IsServer)
|
||||
_popup.PopupEntity(Loc.GetString("buckle-component-cannot-fit-message"), userUid, buckleUid, PopupType.Medium);
|
||||
if (_netManager.IsServer && popup && user != null)
|
||||
_popup.PopupEntity(Loc.GetString("buckle-component-cannot-fit-message"), user.Value, user.Value, PopupType.Medium);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Is it within range
|
||||
bool Ignored(EntityUid entity) => entity == buckleUid || entity == userUid || entity == strapUid;
|
||||
|
||||
if (!_interaction.InRangeUnobstructed(buckleUid, strapUid, buckleComp.Range, predicate: Ignored,
|
||||
if (!_interaction.InRangeUnobstructed(buckleUid,
|
||||
strapUid,
|
||||
buckleComp.Range,
|
||||
predicate: entity => entity == buckleUid || entity == user || entity == strapUid,
|
||||
popup: true))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// If in a container
|
||||
if (_container.TryGetContainingContainer(buckleUid, out var ownerContainer))
|
||||
{
|
||||
// And not in the same container as the strap
|
||||
if (!_container.TryGetContainingContainer(strapUid, out var strapContainer) ||
|
||||
ownerContainer != strapContainer)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (!_container.IsInSameOrNoContainer((buckleUid, null, null), (strapUid, null, null)))
|
||||
return false;
|
||||
|
||||
if (!HasComp<HandsComponent>(userUid))
|
||||
if (user != null && !HasComp<HandsComponent>(user))
|
||||
{
|
||||
// PopupPredicted when
|
||||
if (_netManager.IsServer)
|
||||
_popup.PopupEntity(Loc.GetString("buckle-component-no-hands-message"), userUid, userUid);
|
||||
if (_netManager.IsServer && popup)
|
||||
_popup.PopupEntity(Loc.GetString("buckle-component-no-hands-message"), user.Value, user.Value);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (buckleComp.Buckled)
|
||||
{
|
||||
var message = Loc.GetString(buckleUid == userUid
|
||||
if (_netManager.IsClient || popup || user == null)
|
||||
return false;
|
||||
|
||||
var message = Loc.GetString(buckleUid == user
|
||||
? "buckle-component-already-buckled-message"
|
||||
: "buckle-component-other-already-buckled-message",
|
||||
("owner", Identity.Entity(buckleUid, EntityManager)));
|
||||
if (_netManager.IsServer)
|
||||
_popup.PopupEntity(message, userUid, userUid);
|
||||
|
||||
_popup.PopupEntity(message, user.Value, user.Value);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check whether someone is attempting to buckle something to their own child
|
||||
var parent = Transform(strapUid).ParentUid;
|
||||
while (parent.IsValid())
|
||||
{
|
||||
if (parent == userUid)
|
||||
if (parent != buckleUid)
|
||||
{
|
||||
var message = Loc.GetString(buckleUid == userUid
|
||||
? "buckle-component-cannot-buckle-message"
|
||||
: "buckle-component-other-cannot-buckle-message", ("owner", Identity.Entity(buckleUid, EntityManager)));
|
||||
if (_netManager.IsServer)
|
||||
_popup.PopupEntity(message, userUid, userUid);
|
||||
|
||||
return false;
|
||||
parent = Transform(parent).ParentUid;
|
||||
continue;
|
||||
}
|
||||
|
||||
parent = Transform(parent).ParentUid;
|
||||
if (_netManager.IsClient || popup || user == null)
|
||||
return false;
|
||||
|
||||
var message = Loc.GetString(buckleUid == user
|
||||
? "buckle-component-cannot-buckle-message"
|
||||
: "buckle-component-other-cannot-buckle-message",
|
||||
("owner", Identity.Entity(buckleUid, EntityManager)));
|
||||
|
||||
_popup.PopupEntity(message, user.Value, user.Value);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!StrapHasSpace(strapUid, buckleComp, strapComp))
|
||||
{
|
||||
var message = Loc.GetString(buckleUid == userUid
|
||||
? "buckle-component-cannot-fit-message"
|
||||
: "buckle-component-other-cannot-fit-message", ("owner", Identity.Entity(buckleUid, EntityManager)));
|
||||
if (_netManager.IsServer)
|
||||
_popup.PopupEntity(message, userUid, userUid);
|
||||
if (_netManager.IsClient || popup || user == null)
|
||||
return false;
|
||||
|
||||
var message = Loc.GetString(buckleUid == user
|
||||
? "buckle-component-cannot-fit-message"
|
||||
: "buckle-component-other-cannot-fit-message",
|
||||
("owner", Identity.Entity(buckleUid, EntityManager)));
|
||||
|
||||
_popup.PopupEntity(message, user.Value, user.Value);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
var attemptEvent = new BuckleAttemptEvent(strapUid, buckleUid, userUid, true);
|
||||
RaiseLocalEvent(attemptEvent.BuckledEntity, ref attemptEvent);
|
||||
RaiseLocalEvent(attemptEvent.StrapEntity, ref attemptEvent);
|
||||
if (attemptEvent.Cancelled)
|
||||
var buckleAttempt = new BuckleAttemptEvent((strapUid, strapComp), (buckleUid, buckleComp), user, popup);
|
||||
RaiseLocalEvent(buckleUid, ref buckleAttempt);
|
||||
if (buckleAttempt.Cancelled)
|
||||
return false;
|
||||
|
||||
var strapAttempt = new StrapAttemptEvent((strapUid, strapComp), (buckleUid, buckleComp), user, popup);
|
||||
RaiseLocalEvent(strapUid, ref strapAttempt);
|
||||
if (strapAttempt.Cancelled)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
@@ -315,216 +317,194 @@ public abstract partial class SharedBuckleSystem
|
||||
/// <summary>
|
||||
/// Attempts to buckle an entity to a strap
|
||||
/// </summary>
|
||||
/// <param name="buckleUid"> Uid of the owner of BuckleComponent </param>
|
||||
/// <param name="userUid">
|
||||
/// <param name="buckle"> Uid of the owner of BuckleComponent </param>
|
||||
/// <param name="user">
|
||||
/// Uid of a third party entity,
|
||||
/// i.e, the uid of someone else you are dragging to a chair.
|
||||
/// Can equal buckleUid sometimes
|
||||
/// </param>
|
||||
/// <param name="strapUid"> Uid of the owner of strap component </param>
|
||||
public bool TryBuckle(EntityUid buckleUid, EntityUid userUid, EntityUid strapUid, BuckleComponent? buckleComp = null)
|
||||
/// <param name="strap"> Uid of the owner of strap component </param>
|
||||
public bool TryBuckle(EntityUid buckle, EntityUid? user, EntityUid strap, BuckleComponent? buckleComp = null, bool popup = true)
|
||||
{
|
||||
if (!Resolve(buckleUid, ref buckleComp, false))
|
||||
if (!Resolve(buckle, ref buckleComp, false))
|
||||
return false;
|
||||
|
||||
if (!CanBuckle(buckleUid, userUid, strapUid, out var strapComp, buckleComp))
|
||||
if (!CanBuckle(buckle, user, strap, popup, out var strapComp, buckleComp))
|
||||
return false;
|
||||
|
||||
if (!StrapTryAdd(strapUid, buckleUid, buckleComp, false, strapComp))
|
||||
{
|
||||
var message = Loc.GetString(buckleUid == userUid
|
||||
? "buckle-component-cannot-buckle-message"
|
||||
: "buckle-component-other-cannot-buckle-message", ("owner", Identity.Entity(buckleUid, EntityManager)));
|
||||
if (_netManager.IsServer)
|
||||
_popup.PopupEntity(message, userUid, userUid);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (TryComp<AppearanceComponent>(buckleUid, out var appearance))
|
||||
Appearance.SetData(buckleUid, BuckleVisuals.Buckled, true, appearance);
|
||||
|
||||
_rotationVisuals.SetHorizontalAngle(buckleUid, strapComp.Rotation);
|
||||
|
||||
ReAttach(buckleUid, strapUid, buckleComp, strapComp);
|
||||
SetBuckledTo(buckleUid, strapUid, strapComp, buckleComp);
|
||||
// TODO user is currently set to null because if it isn't the sound fails to play in some situations, fix that
|
||||
_audio.PlayPredicted(strapComp.BuckleSound, strapUid, userUid);
|
||||
|
||||
var ev = new BuckleChangeEvent(strapUid, buckleUid, true);
|
||||
RaiseLocalEvent(ev.BuckledEntity, ref ev);
|
||||
RaiseLocalEvent(ev.StrapEntity, ref ev);
|
||||
|
||||
if (TryComp<PullableComponent>(buckleUid, out var ownerPullable))
|
||||
{
|
||||
if (ownerPullable.Puller != null)
|
||||
{
|
||||
_pulling.TryStopPull(buckleUid, ownerPullable);
|
||||
}
|
||||
}
|
||||
|
||||
if (TryComp<PhysicsComponent>(buckleUid, out var physics))
|
||||
{
|
||||
_physics.ResetDynamics(buckleUid, physics);
|
||||
}
|
||||
|
||||
if (!buckleComp.PullStrap && TryComp<PullableComponent>(strapUid, out var toPullable))
|
||||
{
|
||||
if (toPullable.Puller == buckleUid)
|
||||
{
|
||||
// can't pull it and buckle to it at the same time
|
||||
_pulling.TryStopPull(strapUid, toPullable);
|
||||
}
|
||||
}
|
||||
|
||||
// Logging
|
||||
if (userUid != buckleUid)
|
||||
_adminLogger.Add(LogType.Action, LogImpact.Low, $"{ToPrettyString(userUid):player} buckled {ToPrettyString(buckleUid)} to {ToPrettyString(strapUid)}");
|
||||
else
|
||||
_adminLogger.Add(LogType.Action, LogImpact.Low, $"{ToPrettyString(userUid):player} buckled themselves to {ToPrettyString(strapUid)}");
|
||||
|
||||
Buckle((buckle, buckleComp), (strap, strapComp), user);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void Buckle(Entity<BuckleComponent> buckle, Entity<StrapComponent> strap, EntityUid? user)
|
||||
{
|
||||
if (user == buckle.Owner)
|
||||
_adminLogger.Add(LogType.Action, LogImpact.Low, $"{ToPrettyString(user):player} buckled themselves to {ToPrettyString(strap)}");
|
||||
else if (user != null)
|
||||
_adminLogger.Add(LogType.Action, LogImpact.Low, $"{ToPrettyString(user):player} buckled {ToPrettyString(buckle)} to {ToPrettyString(strap)}");
|
||||
|
||||
_audio.PlayPredicted(strap.Comp.BuckleSound, strap, user);
|
||||
|
||||
SetBuckledTo(buckle, strap!);
|
||||
Appearance.SetData(strap, StrapVisuals.State, true);
|
||||
Appearance.SetData(buckle, BuckleVisuals.Buckled, true);
|
||||
|
||||
_rotationVisuals.SetHorizontalAngle(buckle.Owner, strap.Comp.Rotation);
|
||||
|
||||
var xform = Transform(buckle);
|
||||
var coords = new EntityCoordinates(strap, strap.Comp.BuckleOffset);
|
||||
_transform.SetCoordinates(buckle, xform, coords, rotation: Angle.Zero);
|
||||
|
||||
_joints.SetRelay(buckle, strap);
|
||||
|
||||
switch (strap.Comp.Position)
|
||||
{
|
||||
case StrapPosition.Stand:
|
||||
_standing.Stand(buckle);
|
||||
break;
|
||||
case StrapPosition.Down:
|
||||
_standing.Down(buckle, false, false);
|
||||
break;
|
||||
}
|
||||
|
||||
var ev = new StrappedEvent(strap, buckle);
|
||||
RaiseLocalEvent(strap, ref ev);
|
||||
|
||||
var gotEv = new BuckledEvent(strap, buckle);
|
||||
RaiseLocalEvent(buckle, ref gotEv);
|
||||
|
||||
if (TryComp<PhysicsComponent>(buckle, out var physics))
|
||||
_physics.ResetDynamics(buckle, physics);
|
||||
|
||||
DebugTools.AssertEqual(xform.ParentUid, strap.Owner);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to unbuckle the Owner of this component from its current strap.
|
||||
/// </summary>
|
||||
/// <param name="buckleUid">The entity to unbuckle.</param>
|
||||
/// <param name="userUid">The entity doing the unbuckling.</param>
|
||||
/// <param name="force">
|
||||
/// Whether to force the unbuckling or not. Does not guarantee true to
|
||||
/// be returned, but guarantees the owner to be unbuckled afterwards.
|
||||
/// </param>
|
||||
/// <param name="user">The entity doing the unbuckling.</param>
|
||||
/// <param name="buckleComp">The buckle component of the entity to unbuckle.</param>
|
||||
/// <returns>
|
||||
/// true if the owner was unbuckled, otherwise false even if the owner
|
||||
/// was previously already unbuckled.
|
||||
/// </returns>
|
||||
public bool TryUnbuckle(EntityUid buckleUid, EntityUid userUid, bool force = false, BuckleComponent? buckleComp = null)
|
||||
public bool TryUnbuckle(EntityUid buckleUid,
|
||||
EntityUid? user,
|
||||
BuckleComponent? buckleComp = null,
|
||||
bool popup = true)
|
||||
{
|
||||
if (!Resolve(buckleUid, ref buckleComp, false) ||
|
||||
buckleComp.BuckledTo is not { } strapUid)
|
||||
return TryUnbuckle((buckleUid, buckleComp), user, popup);
|
||||
}
|
||||
|
||||
public bool TryUnbuckle(Entity<BuckleComponent?> buckle, EntityUid? user, bool popup)
|
||||
{
|
||||
if (!Resolve(buckle.Owner, ref buckle.Comp))
|
||||
return false;
|
||||
|
||||
if (!force)
|
||||
{
|
||||
var attemptEvent = new BuckleAttemptEvent(strapUid, buckleUid, userUid, false);
|
||||
RaiseLocalEvent(attemptEvent.BuckledEntity, ref attemptEvent);
|
||||
RaiseLocalEvent(attemptEvent.StrapEntity, ref attemptEvent);
|
||||
if (attemptEvent.Cancelled)
|
||||
return false;
|
||||
|
||||
if (_gameTiming.CurTime < buckleComp.BuckleTime + buckleComp.Delay)
|
||||
return false;
|
||||
|
||||
if (!_interaction.InRangeUnobstructed(userUid, strapUid, buckleComp.Range, popup: true))
|
||||
return false;
|
||||
|
||||
if (HasComp<SleepingComponent>(buckleUid) && buckleUid == userUid)
|
||||
return false;
|
||||
|
||||
// If the person is crit or dead in any kind of strap, return. This prevents people from unbuckling themselves while incapacitated.
|
||||
if (_mobState.IsIncapacitated(buckleUid) && userUid == buckleUid)
|
||||
return false;
|
||||
}
|
||||
|
||||
// Logging
|
||||
if (userUid != buckleUid)
|
||||
_adminLogger.Add(LogType.Action, LogImpact.Low, $"{ToPrettyString(userUid):player} unbuckled {ToPrettyString(buckleUid)} from {ToPrettyString(strapUid)}");
|
||||
else
|
||||
_adminLogger.Add(LogType.Action, LogImpact.Low, $"{ToPrettyString(userUid):player} unbuckled themselves from {ToPrettyString(strapUid)}");
|
||||
|
||||
SetBuckledTo(buckleUid, null, null, buckleComp);
|
||||
|
||||
if (!TryComp<StrapComponent>(strapUid, out var strapComp))
|
||||
if (!CanUnbuckle(buckle, user, popup, out var strap))
|
||||
return false;
|
||||
|
||||
var buckleXform = Transform(buckleUid);
|
||||
var oldBuckledXform = Transform(strapUid);
|
||||
|
||||
if (buckleXform.ParentUid == strapUid && !Terminating(buckleXform.ParentUid))
|
||||
{
|
||||
_container.AttachParentToContainerOrGrid((buckleUid, buckleXform));
|
||||
|
||||
var oldBuckledToWorldRot = _transform.GetWorldRotation(strapUid);
|
||||
_transform.SetWorldRotation(buckleXform, oldBuckledToWorldRot);
|
||||
|
||||
if (strapComp.UnbuckleOffset != Vector2.Zero)
|
||||
buckleXform.Coordinates = oldBuckledXform.Coordinates.Offset(strapComp.UnbuckleOffset);
|
||||
}
|
||||
|
||||
if (TryComp(buckleUid, out AppearanceComponent? appearance))
|
||||
Appearance.SetData(buckleUid, BuckleVisuals.Buckled, false, appearance);
|
||||
_rotationVisuals.ResetHorizontalAngle(buckleUid);
|
||||
|
||||
if (TryComp<MobStateComponent>(buckleUid, out var mobState)
|
||||
&& _mobState.IsIncapacitated(buckleUid, mobState)
|
||||
|| HasComp<KnockedDownComponent>(buckleUid))
|
||||
{
|
||||
_standing.Down(buckleUid);
|
||||
}
|
||||
else
|
||||
{
|
||||
_standing.Stand(buckleUid);
|
||||
}
|
||||
|
||||
if (_mobState.IsIncapacitated(buckleUid, mobState))
|
||||
{
|
||||
_standing.Down(buckleUid);
|
||||
}
|
||||
if (strapComp.BuckledEntities.Remove(buckleUid))
|
||||
{
|
||||
strapComp.OccupiedSize -= buckleComp.Size;
|
||||
Dirty(strapUid, strapComp);
|
||||
}
|
||||
|
||||
_joints.RefreshRelay(buckleUid);
|
||||
Appearance.SetData(strapUid, StrapVisuals.State, strapComp.BuckledEntities.Count != 0);
|
||||
|
||||
// TODO: Buckle listening to moveevents is sussy anyway.
|
||||
if (!TerminatingOrDeleted(strapUid))
|
||||
_audio.PlayPredicted(strapComp.UnbuckleSound, strapUid, userUid);
|
||||
|
||||
var ev = new BuckleChangeEvent(strapUid, buckleUid, false);
|
||||
RaiseLocalEvent(buckleUid, ref ev);
|
||||
RaiseLocalEvent(strapUid, ref ev);
|
||||
|
||||
Unbuckle(buckle!, strap, user);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Makes an entity toggle the buckling status of the owner to a
|
||||
/// specific entity.
|
||||
/// </summary>
|
||||
/// <param name="buckleUid">The entity to buckle/unbuckle from <see cref="to"/>.</param>
|
||||
/// <param name="userUid">The entity doing the buckling/unbuckling.</param>
|
||||
/// <param name="strapUid">
|
||||
/// The entity to toggle the buckle status of the owner to.
|
||||
/// </param>
|
||||
/// <param name="force">
|
||||
/// Whether to force the unbuckling or not, if it happens. Does not
|
||||
/// guarantee true to be returned, but guarantees the owner to be
|
||||
/// unbuckled afterwards.
|
||||
/// </param>
|
||||
/// <param name="buckle">The buckle component of the entity to buckle/unbuckle from <see cref="to"/>.</param>
|
||||
/// <returns>true if the buckling status was changed, false otherwise.</returns>
|
||||
public bool ToggleBuckle(
|
||||
EntityUid buckleUid,
|
||||
EntityUid userUid,
|
||||
EntityUid strapUid,
|
||||
bool force = false,
|
||||
BuckleComponent? buckle = null)
|
||||
public void Unbuckle(Entity<BuckleComponent?> buckle, EntityUid? user)
|
||||
{
|
||||
if (!Resolve(buckleUid, ref buckle, false))
|
||||
if (!Resolve(buckle.Owner, ref buckle.Comp, false))
|
||||
return;
|
||||
|
||||
if (buckle.Comp.BuckledTo is not { } strap)
|
||||
return;
|
||||
|
||||
if (!TryComp(strap, out StrapComponent? strapComp))
|
||||
{
|
||||
Log.Error($"Encountered buckle {ToPrettyString(buckle.Owner)} with invalid strap entity {ToPrettyString(strap)}");
|
||||
SetBuckledTo(buckle!, null);
|
||||
return;
|
||||
}
|
||||
|
||||
Unbuckle(buckle!, (strap, strapComp), user);
|
||||
}
|
||||
|
||||
private void Unbuckle(Entity<BuckleComponent> buckle, Entity<StrapComponent> strap, EntityUid? user)
|
||||
{
|
||||
if (user == buckle.Owner)
|
||||
_adminLogger.Add(LogType.Action, LogImpact.Low, $"{user} unbuckled themselves from {strap}");
|
||||
else if (user != null)
|
||||
_adminLogger.Add(LogType.Action, LogImpact.Low, $"{user} unbuckled {buckle} from {strap}");
|
||||
|
||||
_audio.PlayPredicted(strap.Comp.UnbuckleSound, strap, user);
|
||||
|
||||
SetBuckledTo(buckle, null);
|
||||
|
||||
var buckleXform = Transform(buckle);
|
||||
var oldBuckledXform = Transform(strap);
|
||||
|
||||
if (buckleXform.ParentUid == strap.Owner && !Terminating(buckleXform.ParentUid))
|
||||
{
|
||||
_container.AttachParentToContainerOrGrid((buckle, buckleXform));
|
||||
|
||||
var oldBuckledToWorldRot = _transform.GetWorldRotation(strap);
|
||||
_transform.SetWorldRotation(buckleXform, oldBuckledToWorldRot);
|
||||
|
||||
if (strap.Comp.UnbuckleOffset != Vector2.Zero)
|
||||
buckleXform.Coordinates = oldBuckledXform.Coordinates.Offset(strap.Comp.UnbuckleOffset);
|
||||
}
|
||||
|
||||
_rotationVisuals.ResetHorizontalAngle(buckle.Owner);
|
||||
Appearance.SetData(strap, StrapVisuals.State, strap.Comp.BuckledEntities.Count != 0);
|
||||
Appearance.SetData(buckle, BuckleVisuals.Buckled, false);
|
||||
|
||||
if (HasComp<KnockedDownComponent>(buckle) || _mobState.IsIncapacitated(buckle))
|
||||
_standing.Down(buckle);
|
||||
else
|
||||
_standing.Stand(buckle);
|
||||
|
||||
_joints.RefreshRelay(buckle);
|
||||
|
||||
var buckleEv = new UnbuckledEvent(strap, buckle);
|
||||
RaiseLocalEvent(buckle, ref buckleEv);
|
||||
|
||||
var strapEv = new UnstrappedEvent(strap, buckle);
|
||||
RaiseLocalEvent(strap, ref strapEv);
|
||||
}
|
||||
|
||||
public bool CanUnbuckle(Entity<BuckleComponent?> buckle, EntityUid user, bool popup)
|
||||
{
|
||||
return CanUnbuckle(buckle, user, popup, out _);
|
||||
}
|
||||
|
||||
private bool CanUnbuckle(Entity<BuckleComponent?> buckle, EntityUid? user, bool popup, out Entity<StrapComponent> strap)
|
||||
{
|
||||
strap = default;
|
||||
if (!Resolve(buckle.Owner, ref buckle.Comp))
|
||||
return false;
|
||||
|
||||
if (!buckle.Buckled)
|
||||
if (buckle.Comp.BuckledTo is not { } strapUid)
|
||||
return false;
|
||||
|
||||
if (!TryComp(strapUid, out StrapComponent? strapComp))
|
||||
{
|
||||
return TryBuckle(buckleUid, userUid, strapUid, buckle);
|
||||
}
|
||||
else
|
||||
{
|
||||
return TryUnbuckle(buckleUid, userUid, force, buckle);
|
||||
Log.Error($"Encountered buckle {ToPrettyString(buckle.Owner)} with invalid strap entity {ToPrettyString(strap)}");
|
||||
SetBuckledTo(buckle!, null);
|
||||
return false;
|
||||
}
|
||||
|
||||
strap = (strapUid, strapComp);
|
||||
if (_gameTiming.CurTime < buckle.Comp.BuckleTime + buckle.Comp.Delay)
|
||||
return false;
|
||||
|
||||
if (user != null && !_interaction.InRangeUnobstructed(user.Value, strap.Owner, buckle.Comp.Range, popup: popup))
|
||||
return false;
|
||||
|
||||
var unbuckleAttempt = new UnbuckleAttemptEvent(strap, buckle!, user, popup);
|
||||
RaiseLocalEvent(buckle, ref unbuckleAttempt);
|
||||
if (unbuckleAttempt.Cancelled)
|
||||
return false;
|
||||
|
||||
var unstrapAttempt = new UnstrapAttemptEvent(strap, buckle!, user, popup);
|
||||
RaiseLocalEvent(strap, ref unstrapAttempt);
|
||||
return !unstrapAttempt.Cancelled;
|
||||
}
|
||||
}
|
||||
|
||||
188
Content.Shared/Buckle/SharedBuckleSystem.Interaction.cs
Normal file
188
Content.Shared/Buckle/SharedBuckleSystem.Interaction.cs
Normal file
@@ -0,0 +1,188 @@
|
||||
using Content.Shared.Buckle.Components;
|
||||
using Content.Shared.DragDrop;
|
||||
using Content.Shared.IdentityManagement;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Verbs;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Shared.Buckle;
|
||||
|
||||
// Partial class containing interaction & verb event handlers
|
||||
public abstract partial class SharedBuckleSystem
|
||||
{
|
||||
private void InitializeInteraction()
|
||||
{
|
||||
SubscribeLocalEvent<StrapComponent, GetVerbsEvent<InteractionVerb>>(AddStrapVerbs);
|
||||
SubscribeLocalEvent<StrapComponent, InteractHandEvent>(OnStrapInteractHand, after: [typeof(InteractionPopupSystem)]);
|
||||
SubscribeLocalEvent<StrapComponent, DragDropTargetEvent>(OnStrapDragDropTarget);
|
||||
SubscribeLocalEvent<StrapComponent, CanDropTargetEvent>(OnCanDropTarget);
|
||||
|
||||
SubscribeLocalEvent<BuckleComponent, InteractHandEvent>(OnBuckleInteractHand, after: [typeof(InteractionPopupSystem)]);
|
||||
SubscribeLocalEvent<BuckleComponent, GetVerbsEvent<InteractionVerb>>(AddUnbuckleVerb);
|
||||
}
|
||||
|
||||
private void OnCanDropTarget(EntityUid uid, StrapComponent component, ref CanDropTargetEvent args)
|
||||
{
|
||||
args.CanDrop = StrapCanDragDropOn(uid, args.User, uid, args.Dragged, component);
|
||||
args.Handled = true;
|
||||
}
|
||||
|
||||
private void OnStrapDragDropTarget(EntityUid uid, StrapComponent component, ref DragDropTargetEvent args)
|
||||
{
|
||||
if (!StrapCanDragDropOn(uid, args.User, uid, args.Dragged, component))
|
||||
return;
|
||||
|
||||
args.Handled = TryBuckle(args.Dragged, args.User, uid, popup: false);
|
||||
}
|
||||
|
||||
private bool StrapCanDragDropOn(
|
||||
EntityUid strapUid,
|
||||
EntityUid userUid,
|
||||
EntityUid targetUid,
|
||||
EntityUid buckleUid,
|
||||
StrapComponent? strapComp = null,
|
||||
BuckleComponent? buckleComp = null)
|
||||
{
|
||||
if (!Resolve(strapUid, ref strapComp, false) ||
|
||||
!Resolve(buckleUid, ref buckleComp, false))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Ignored(EntityUid entity) => entity == userUid || entity == buckleUid || entity == targetUid;
|
||||
|
||||
return _interaction.InRangeUnobstructed(targetUid, buckleUid, buckleComp.Range, predicate: Ignored);
|
||||
}
|
||||
|
||||
private void OnStrapInteractHand(EntityUid uid, StrapComponent component, InteractHandEvent args)
|
||||
{
|
||||
if (args.Handled)
|
||||
return;
|
||||
|
||||
if (!component.Enabled)
|
||||
return;
|
||||
|
||||
if (!TryComp(args.User, out BuckleComponent? buckle))
|
||||
return;
|
||||
|
||||
if (buckle.BuckledTo == null)
|
||||
TryBuckle(args.User, args.User, uid, buckle, popup: true);
|
||||
else if (buckle.BuckledTo == uid)
|
||||
TryUnbuckle(args.User, args.User, buckle, popup: true);
|
||||
else
|
||||
return;
|
||||
|
||||
// TODO BUCKLE add out bool for whether a pop-up was generated or not.
|
||||
args.Handled = true;
|
||||
}
|
||||
|
||||
private void OnBuckleInteractHand(Entity<BuckleComponent> ent, ref InteractHandEvent args)
|
||||
{
|
||||
if (args.Handled)
|
||||
return;
|
||||
|
||||
if (ent.Comp.BuckledTo != null)
|
||||
TryUnbuckle(ent!, args.User, popup: true);
|
||||
|
||||
// TODO BUCKLE add out bool for whether a pop-up was generated or not.
|
||||
args.Handled = true;
|
||||
}
|
||||
|
||||
private void AddStrapVerbs(EntityUid uid, StrapComponent component, GetVerbsEvent<InteractionVerb> args)
|
||||
{
|
||||
if (args.Hands == null || !args.CanAccess || !args.CanInteract || !component.Enabled)
|
||||
return;
|
||||
|
||||
// Note that for whatever bloody reason, buckle component has its own interaction range. Additionally, this
|
||||
// range can be set per-component, so we have to check a modified InRangeUnobstructed for every verb.
|
||||
|
||||
// Add unstrap verbs for every strapped entity.
|
||||
foreach (var entity in component.BuckledEntities)
|
||||
{
|
||||
var buckledComp = Comp<BuckleComponent>(entity);
|
||||
|
||||
if (!_interaction.InRangeUnobstructed(args.User, args.Target, range: buckledComp.Range))
|
||||
continue;
|
||||
|
||||
var verb = new InteractionVerb()
|
||||
{
|
||||
Act = () => TryUnbuckle(entity, args.User, buckleComp: buckledComp),
|
||||
Category = VerbCategory.Unbuckle,
|
||||
Text = entity == args.User
|
||||
? Loc.GetString("verb-self-target-pronoun")
|
||||
: Identity.Name(entity, EntityManager)
|
||||
};
|
||||
|
||||
// In the event that you have more than once entity with the same name strapped to the same object,
|
||||
// these two verbs will be identical according to Verb.CompareTo, and only one with actually be added to
|
||||
// the verb list. However this should rarely ever be a problem. If it ever is, it could be fixed by
|
||||
// appending an integer to verb.Text to distinguish the verbs.
|
||||
|
||||
args.Verbs.Add(verb);
|
||||
}
|
||||
|
||||
// Add a verb to buckle the user.
|
||||
if (TryComp<BuckleComponent>(args.User, out var buckle) &&
|
||||
buckle.BuckledTo != uid &&
|
||||
args.User != uid &&
|
||||
StrapHasSpace(uid, buckle, component) &&
|
||||
_interaction.InRangeUnobstructed(args.User, args.Target, range: buckle.Range))
|
||||
{
|
||||
InteractionVerb verb = new()
|
||||
{
|
||||
Act = () => TryBuckle(args.User, args.User, args.Target, buckle),
|
||||
Category = VerbCategory.Buckle,
|
||||
Text = Loc.GetString("verb-self-target-pronoun")
|
||||
};
|
||||
args.Verbs.Add(verb);
|
||||
}
|
||||
|
||||
// If the user is currently holding/pulling an entity that can be buckled, add a verb for that.
|
||||
if (args.Using is { Valid: true } @using &&
|
||||
TryComp<BuckleComponent>(@using, out var usingBuckle) &&
|
||||
StrapHasSpace(uid, usingBuckle, component) &&
|
||||
_interaction.InRangeUnobstructed(@using, args.Target, range: usingBuckle.Range))
|
||||
{
|
||||
// Check that the entity is unobstructed from the target (ignoring the user).
|
||||
bool Ignored(EntityUid entity) => entity == args.User || entity == args.Target || entity == @using;
|
||||
if (!_interaction.InRangeUnobstructed(@using, args.Target, usingBuckle.Range, predicate: Ignored))
|
||||
return;
|
||||
|
||||
var isPlayer = _playerManager.TryGetSessionByEntity(@using, out var _);
|
||||
InteractionVerb verb = new()
|
||||
{
|
||||
Act = () => TryBuckle(@using, args.User, args.Target, usingBuckle),
|
||||
Category = VerbCategory.Buckle,
|
||||
Text = Identity.Name(@using, EntityManager),
|
||||
// just a held object, the user is probably just trying to sit down.
|
||||
// If the used entity is a person being pulled, prioritize this verb. Conversely, if it is
|
||||
Priority = isPlayer ? 1 : -1
|
||||
};
|
||||
|
||||
args.Verbs.Add(verb);
|
||||
}
|
||||
}
|
||||
|
||||
private void AddUnbuckleVerb(EntityUid uid, BuckleComponent component, GetVerbsEvent<InteractionVerb> args)
|
||||
{
|
||||
if (!args.CanAccess || !args.CanInteract || !component.Buckled)
|
||||
return;
|
||||
|
||||
InteractionVerb verb = new()
|
||||
{
|
||||
Act = () => TryUnbuckle(uid, args.User, buckleComp: component),
|
||||
Text = Loc.GetString("verb-categories-unbuckle"),
|
||||
Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/unbuckle.svg.192dpi.png"))
|
||||
};
|
||||
|
||||
if (args.Target == args.User && args.Using == null)
|
||||
{
|
||||
// A user is left clicking themselves with an empty hand, while buckled.
|
||||
// It is very likely they are trying to unbuckle themselves.
|
||||
verb.Priority = 1;
|
||||
}
|
||||
|
||||
args.Verbs.Add(verb);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -2,39 +2,25 @@
|
||||
using Content.Shared.Buckle.Components;
|
||||
using Content.Shared.Construction;
|
||||
using Content.Shared.Destructible;
|
||||
using Content.Shared.DragDrop;
|
||||
using Content.Shared.Foldable;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Rotation;
|
||||
using Content.Shared.Storage;
|
||||
using Content.Shared.Verbs;
|
||||
using Robust.Shared.Containers;
|
||||
|
||||
namespace Content.Shared.Buckle;
|
||||
|
||||
public abstract partial class SharedBuckleSystem
|
||||
{
|
||||
[Dependency] private readonly SharedRotationVisualsSystem _rotationVisuals = default!;
|
||||
|
||||
private void InitializeStrap()
|
||||
{
|
||||
SubscribeLocalEvent<StrapComponent, ComponentStartup>(OnStrapStartup);
|
||||
SubscribeLocalEvent<StrapComponent, ComponentShutdown>(OnStrapShutdown);
|
||||
SubscribeLocalEvent<StrapComponent, ComponentRemove>((e, c, _) => StrapRemoveAll(e, c));
|
||||
|
||||
SubscribeLocalEvent<StrapComponent, EntInsertedIntoContainerMessage>(OnStrapEntModifiedFromContainer);
|
||||
SubscribeLocalEvent<StrapComponent, EntRemovedFromContainerMessage>(OnStrapEntModifiedFromContainer);
|
||||
SubscribeLocalEvent<StrapComponent, GetVerbsEvent<InteractionVerb>>(AddStrapVerbs);
|
||||
SubscribeLocalEvent<StrapComponent, ContainerGettingInsertedAttemptEvent>(OnStrapContainerGettingInsertedAttempt);
|
||||
SubscribeLocalEvent<StrapComponent, InteractHandEvent>(OnStrapInteractHand);
|
||||
SubscribeLocalEvent<StrapComponent, DestructionEventArgs>((e, c, _) => StrapRemoveAll(e, c));
|
||||
SubscribeLocalEvent<StrapComponent, BreakageEventArgs>((e, c, _) => StrapRemoveAll(e, c));
|
||||
|
||||
SubscribeLocalEvent<StrapComponent, DragDropTargetEvent>(OnStrapDragDropTarget);
|
||||
SubscribeLocalEvent<StrapComponent, CanDropTargetEvent>(OnCanDropTarget);
|
||||
SubscribeLocalEvent<StrapComponent, FoldAttemptEvent>(OnAttemptFold);
|
||||
|
||||
SubscribeLocalEvent<StrapComponent, MoveEvent>(OnStrapMoveEvent);
|
||||
SubscribeLocalEvent<StrapComponent, MachineDeconstructedEvent>((e, c, _) => StrapRemoveAll(e, c));
|
||||
}
|
||||
|
||||
@@ -45,145 +31,17 @@ public abstract partial class SharedBuckleSystem
|
||||
|
||||
private void OnStrapShutdown(EntityUid uid, StrapComponent component, ComponentShutdown args)
|
||||
{
|
||||
if (LifeStage(uid) > EntityLifeStage.MapInitialized)
|
||||
return;
|
||||
|
||||
StrapRemoveAll(uid, component);
|
||||
}
|
||||
|
||||
private void OnStrapEntModifiedFromContainer(EntityUid uid, StrapComponent component, ContainerModifiedMessage message)
|
||||
{
|
||||
if (_gameTiming.ApplyingState)
|
||||
return;
|
||||
|
||||
foreach (var buckledEntity in component.BuckledEntities)
|
||||
{
|
||||
if (!TryComp<BuckleComponent>(buckledEntity, out var buckleComp))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
ContainerModifiedReAttach(buckledEntity, uid, buckleComp, component);
|
||||
}
|
||||
}
|
||||
|
||||
private void ContainerModifiedReAttach(EntityUid buckleUid, EntityUid strapUid, BuckleComponent? buckleComp = null, StrapComponent? strapComp = null)
|
||||
{
|
||||
if (!Resolve(buckleUid, ref buckleComp, false) ||
|
||||
!Resolve(strapUid, ref strapComp, false))
|
||||
return;
|
||||
|
||||
var contained = _container.TryGetContainingContainer(buckleUid, out var ownContainer);
|
||||
var strapContained = _container.TryGetContainingContainer(strapUid, out var strapContainer);
|
||||
|
||||
if (contained != strapContained || ownContainer != strapContainer)
|
||||
{
|
||||
TryUnbuckle(buckleUid, buckleUid, true, buckleComp);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!contained)
|
||||
{
|
||||
ReAttach(buckleUid, strapUid, buckleComp, strapComp);
|
||||
}
|
||||
if (!TerminatingOrDeleted(uid))
|
||||
StrapRemoveAll(uid, component);
|
||||
}
|
||||
|
||||
private void OnStrapContainerGettingInsertedAttempt(EntityUid uid, StrapComponent component, ContainerGettingInsertedAttemptEvent args)
|
||||
{
|
||||
// If someone is attempting to put this item inside of a backpack, ensure that it has no entities strapped to it.
|
||||
if (HasComp<StorageComponent>(args.Container.Owner) && component.BuckledEntities.Count != 0)
|
||||
if (args.Container.ID == StorageComponent.ContainerId && component.BuckledEntities.Count != 0)
|
||||
args.Cancel();
|
||||
}
|
||||
|
||||
private void OnStrapInteractHand(EntityUid uid, StrapComponent component, InteractHandEvent args)
|
||||
{
|
||||
if (args.Handled)
|
||||
return;
|
||||
|
||||
args.Handled = ToggleBuckle(args.User, args.User, uid);
|
||||
}
|
||||
|
||||
private void AddStrapVerbs(EntityUid uid, StrapComponent component, GetVerbsEvent<InteractionVerb> args)
|
||||
{
|
||||
if (args.Hands == null || !args.CanAccess || !args.CanInteract || !component.Enabled)
|
||||
return;
|
||||
|
||||
// Note that for whatever bloody reason, buckle component has its own interaction range. Additionally, this
|
||||
// range can be set per-component, so we have to check a modified InRangeUnobstructed for every verb.
|
||||
|
||||
// Add unstrap verbs for every strapped entity.
|
||||
foreach (var entity in component.BuckledEntities)
|
||||
{
|
||||
var buckledComp = Comp<BuckleComponent>(entity);
|
||||
|
||||
if (!_interaction.InRangeUnobstructed(args.User, args.Target, range: buckledComp.Range))
|
||||
continue;
|
||||
|
||||
var verb = new InteractionVerb()
|
||||
{
|
||||
Act = () => TryUnbuckle(entity, args.User, buckleComp: buckledComp),
|
||||
Category = VerbCategory.Unbuckle,
|
||||
Text = entity == args.User
|
||||
? Loc.GetString("verb-self-target-pronoun")
|
||||
: Comp<MetaDataComponent>(entity).EntityName
|
||||
};
|
||||
|
||||
// In the event that you have more than once entity with the same name strapped to the same object,
|
||||
// these two verbs will be identical according to Verb.CompareTo, and only one with actually be added to
|
||||
// the verb list. However this should rarely ever be a problem. If it ever is, it could be fixed by
|
||||
// appending an integer to verb.Text to distinguish the verbs.
|
||||
|
||||
args.Verbs.Add(verb);
|
||||
}
|
||||
|
||||
// Add a verb to buckle the user.
|
||||
if (TryComp<BuckleComponent>(args.User, out var buckle) &&
|
||||
buckle.BuckledTo != uid &&
|
||||
args.User != uid &&
|
||||
StrapHasSpace(uid, buckle, component) &&
|
||||
_interaction.InRangeUnobstructed(args.User, args.Target, range: buckle.Range))
|
||||
{
|
||||
InteractionVerb verb = new()
|
||||
{
|
||||
Act = () => TryBuckle(args.User, args.User, args.Target, buckle),
|
||||
Category = VerbCategory.Buckle,
|
||||
Text = Loc.GetString("verb-self-target-pronoun")
|
||||
};
|
||||
args.Verbs.Add(verb);
|
||||
}
|
||||
|
||||
// If the user is currently holding/pulling an entity that can be buckled, add a verb for that.
|
||||
if (args.Using is { Valid: true } @using &&
|
||||
TryComp<BuckleComponent>(@using, out var usingBuckle) &&
|
||||
StrapHasSpace(uid, usingBuckle, component) &&
|
||||
_interaction.InRangeUnobstructed(@using, args.Target, range: usingBuckle.Range))
|
||||
{
|
||||
// Check that the entity is unobstructed from the target (ignoring the user).
|
||||
bool Ignored(EntityUid entity) => entity == args.User || entity == args.Target || entity == @using;
|
||||
if (!_interaction.InRangeUnobstructed(@using, args.Target, usingBuckle.Range, predicate: Ignored))
|
||||
return;
|
||||
|
||||
var isPlayer = _playerManager.TryGetSessionByEntity(@using, out var _);
|
||||
InteractionVerb verb = new()
|
||||
{
|
||||
Act = () => TryBuckle(@using, args.User, args.Target, usingBuckle),
|
||||
Category = VerbCategory.Buckle,
|
||||
Text = Comp<MetaDataComponent>(@using).EntityName,
|
||||
// just a held object, the user is probably just trying to sit down.
|
||||
// If the used entity is a person being pulled, prioritize this verb. Conversely, if it is
|
||||
Priority = isPlayer ? 1 : -1
|
||||
};
|
||||
|
||||
args.Verbs.Add(verb);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnCanDropTarget(EntityUid uid, StrapComponent component, ref CanDropTargetEvent args)
|
||||
{
|
||||
args.CanDrop = StrapCanDragDropOn(uid, args.User, uid, args.Dragged, component);
|
||||
args.Handled = true;
|
||||
}
|
||||
|
||||
private void OnAttemptFold(EntityUid uid, StrapComponent component, ref FoldAttemptEvent args)
|
||||
{
|
||||
if (args.Cancelled)
|
||||
@@ -192,69 +50,6 @@ public abstract partial class SharedBuckleSystem
|
||||
args.Cancelled = component.BuckledEntities.Count != 0;
|
||||
}
|
||||
|
||||
private void OnStrapDragDropTarget(EntityUid uid, StrapComponent component, ref DragDropTargetEvent args)
|
||||
{
|
||||
if (!StrapCanDragDropOn(uid, args.User, uid, args.Dragged, component))
|
||||
return;
|
||||
|
||||
args.Handled = TryBuckle(args.Dragged, args.User, uid);
|
||||
}
|
||||
|
||||
private void OnStrapMoveEvent(EntityUid uid, StrapComponent component, ref MoveEvent args)
|
||||
{
|
||||
// TODO: This looks dirty af.
|
||||
// On rotation of a strap, reattach all buckled entities.
|
||||
// This fixes buckle offsets and draw depths.
|
||||
// This is mega cursed. Please somebody save me from Mr Buckle's wild ride.
|
||||
// Oh god I'm back here again. Send help.
|
||||
|
||||
// Consider a chair that has a player strapped to it. Then the client receives a new server state, showing
|
||||
// that the player entity has moved elsewhere, and the chair has rotated. If the client applies the player
|
||||
// state, then the chairs transform comp state, and then the buckle state. The transform state will
|
||||
// forcefully teleport the player back to the chair (client-side only). This causes even more issues if the
|
||||
// chair was teleporting in from nullspace after having left PVS.
|
||||
//
|
||||
// One option is to just never trigger re-buckles during state application.
|
||||
// another is to.. just not do this? Like wtf is this code. But I CBF with buckle atm.
|
||||
|
||||
if (_gameTiming.ApplyingState || args.NewRotation == args.OldRotation)
|
||||
return;
|
||||
|
||||
foreach (var buckledEntity in component.BuckledEntities)
|
||||
{
|
||||
if (!TryComp<BuckleComponent>(buckledEntity, out var buckled))
|
||||
continue;
|
||||
|
||||
if (!buckled.Buckled || buckled.LastEntityBuckledTo != uid)
|
||||
{
|
||||
Log.Error($"A moving strap entity {ToPrettyString(uid)} attempted to re-parent an entity that does not 'belong' to it {ToPrettyString(buckledEntity)}");
|
||||
continue;
|
||||
}
|
||||
|
||||
ReAttach(buckledEntity, uid, buckled, component);
|
||||
Dirty(buckledEntity, buckled);
|
||||
}
|
||||
}
|
||||
|
||||
private bool StrapCanDragDropOn(
|
||||
EntityUid strapUid,
|
||||
EntityUid userUid,
|
||||
EntityUid targetUid,
|
||||
EntityUid buckleUid,
|
||||
StrapComponent? strapComp = null,
|
||||
BuckleComponent? buckleComp = null)
|
||||
{
|
||||
if (!Resolve(strapUid, ref strapComp, false) ||
|
||||
!Resolve(buckleUid, ref buckleComp, false))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Ignored(EntityUid entity) => entity == userUid || entity == buckleUid || entity == targetUid;
|
||||
|
||||
return _interaction.InRangeUnobstructed(targetUid, buckleUid, buckleComp.Range, predicate: Ignored);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remove everything attached to the strap
|
||||
/// </summary>
|
||||
@@ -264,10 +59,6 @@ public abstract partial class SharedBuckleSystem
|
||||
{
|
||||
TryUnbuckle(entity, entity, true);
|
||||
}
|
||||
|
||||
strapComp.BuckledEntities.Clear();
|
||||
strapComp.OccupiedSize = 0;
|
||||
Dirty(uid, strapComp);
|
||||
}
|
||||
|
||||
private bool StrapHasSpace(EntityUid strapUid, BuckleComponent buckleComp, StrapComponent? strapComp = null)
|
||||
@@ -275,30 +66,13 @@ public abstract partial class SharedBuckleSystem
|
||||
if (!Resolve(strapUid, ref strapComp, false))
|
||||
return false;
|
||||
|
||||
return strapComp.OccupiedSize + buckleComp.Size <= strapComp.Size;
|
||||
}
|
||||
var avail = strapComp.Size;
|
||||
foreach (var buckle in strapComp.BuckledEntities)
|
||||
{
|
||||
avail -= CompOrNull<BuckleComponent>(buckle)?.Size ?? 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Try to add an entity to the strap
|
||||
/// </summary>
|
||||
private bool StrapTryAdd(EntityUid strapUid, EntityUid buckleUid, BuckleComponent buckleComp, bool force = false, StrapComponent? strapComp = null)
|
||||
{
|
||||
if (!Resolve(strapUid, ref strapComp, false) ||
|
||||
!strapComp.Enabled)
|
||||
return false;
|
||||
|
||||
if (!force && !StrapHasSpace(strapUid, buckleComp, strapComp))
|
||||
return false;
|
||||
|
||||
if (!strapComp.BuckledEntities.Add(buckleUid))
|
||||
return false;
|
||||
|
||||
strapComp.OccupiedSize += buckleComp.Size;
|
||||
|
||||
Appearance.SetData(strapUid, StrapVisuals.State, true);
|
||||
|
||||
Dirty(strapUid, strapComp);
|
||||
return true;
|
||||
return avail >= buckleComp.Size;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -311,6 +85,7 @@ public abstract partial class SharedBuckleSystem
|
||||
return;
|
||||
|
||||
strapComp.Enabled = enabled;
|
||||
Dirty(strapUid, strapComp);
|
||||
|
||||
if (!enabled)
|
||||
StrapRemoveAll(strapUid, strapComp);
|
||||
|
||||
@@ -1,21 +1,17 @@
|
||||
using Content.Shared.ActionBlocker;
|
||||
using Content.Shared.Administration.Logs;
|
||||
using Content.Shared.Alert;
|
||||
using Content.Shared.Buckle.Components;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Mobs.Systems;
|
||||
using Content.Shared.Popups;
|
||||
using Content.Shared.Pulling;
|
||||
using Content.Shared.Rotation;
|
||||
using Content.Shared.Standing;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Audio.Systems;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Physics.Systems;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Timing;
|
||||
using PullingSystem = Content.Shared.Movement.Pulling.Systems.PullingSystem;
|
||||
|
||||
namespace Content.Shared.Buckle;
|
||||
|
||||
@@ -36,10 +32,10 @@ public abstract partial class SharedBuckleSystem : EntitySystem
|
||||
[Dependency] private readonly SharedInteractionSystem _interaction = default!;
|
||||
[Dependency] private readonly SharedJointSystem _joints = default!;
|
||||
[Dependency] private readonly SharedPopupSystem _popup = default!;
|
||||
[Dependency] private readonly PullingSystem _pulling = default!;
|
||||
[Dependency] private readonly SharedTransformSystem _transform = default!;
|
||||
[Dependency] private readonly StandingStateSystem _standing = default!;
|
||||
[Dependency] private readonly SharedPhysicsSystem _physics = default!;
|
||||
[Dependency] private readonly SharedRotationVisualsSystem _rotationVisuals = default!;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Initialize()
|
||||
@@ -51,45 +47,6 @@ public abstract partial class SharedBuckleSystem : EntitySystem
|
||||
|
||||
InitializeBuckle();
|
||||
InitializeStrap();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reattaches this entity to the strap, modifying its position and rotation.
|
||||
/// </summary>
|
||||
/// <param name="buckleUid">The entity to reattach.</param>
|
||||
/// <param name="strapUid">The entity to reattach the buckleUid entity to.</param>
|
||||
private void ReAttach(
|
||||
EntityUid buckleUid,
|
||||
EntityUid strapUid,
|
||||
BuckleComponent? buckleComp = null,
|
||||
StrapComponent? strapComp = null)
|
||||
{
|
||||
if (!Resolve(strapUid, ref strapComp, false)
|
||||
|| !Resolve(buckleUid, ref buckleComp, false))
|
||||
return;
|
||||
|
||||
_transform.SetCoordinates(buckleUid, new EntityCoordinates(strapUid, strapComp.BuckleOffsetClamped));
|
||||
|
||||
var buckleTransform = Transform(buckleUid);
|
||||
|
||||
// Buckle subscribes to move for <reasons> so this might fail.
|
||||
// TODO: Make buckle not do that.
|
||||
if (buckleTransform.ParentUid != strapUid)
|
||||
return;
|
||||
|
||||
_transform.SetLocalRotation(buckleUid, Angle.Zero, buckleTransform);
|
||||
_joints.SetRelay(buckleUid, strapUid);
|
||||
|
||||
switch (strapComp.Position)
|
||||
{
|
||||
case StrapPosition.None:
|
||||
break;
|
||||
case StrapPosition.Stand:
|
||||
_standing.Stand(buckleUid);
|
||||
break;
|
||||
case StrapPosition.Down:
|
||||
_standing.Down(buckleUid, false, false);
|
||||
break;
|
||||
}
|
||||
InitializeInteraction();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,13 +128,13 @@ namespace Content.Shared.CCVar
|
||||
/// Minimum time between meteor swarms in minutes.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<float>
|
||||
MeteorSwarmMinTime = CVarDef.Create("events.meteor_swarm_min_time", 7.5f, CVar.ARCHIVE | CVar.SERVERONLY);
|
||||
MeteorSwarmMinTime = CVarDef.Create("events.meteor_swarm_min_time", 12.5f, CVar.ARCHIVE | CVar.SERVERONLY);
|
||||
|
||||
/// <summary>
|
||||
/// Maximum time between meteor swarms in minutes.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<float>
|
||||
MeteorSwarmMaxTime = CVarDef.Create("events.meteor_swarm_max_time", 12.5f, CVar.ARCHIVE | CVar.SERVERONLY);
|
||||
MeteorSwarmMaxTime = CVarDef.Create("events.meteor_swarm_max_time", 17.5f, CVar.ARCHIVE | CVar.SERVERONLY);
|
||||
|
||||
/*
|
||||
* Game
|
||||
@@ -432,6 +432,14 @@ namespace Content.Shared.CCVar
|
||||
public static readonly CVarDef<string> RoundEndSoundCollection =
|
||||
CVarDef.Create("game.round_end_sound_collection", "RoundEnd", CVar.SERVERONLY);
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not to add every player as a global override to PVS at round end.
|
||||
/// This will allow all players to see their clothing in the round screen player list screen,
|
||||
/// but may cause lag during round end with very high player counts.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<bool> RoundEndPVSOverrides =
|
||||
CVarDef.Create("game.round_end_pvs_overrides", true, CVar.SERVERONLY);
|
||||
|
||||
/*
|
||||
* Discord
|
||||
*/
|
||||
@@ -878,6 +886,25 @@ namespace Content.Shared.CCVar
|
||||
public static readonly CVarDef<bool> AdminBypassMaxPlayers =
|
||||
CVarDef.Create("admin.bypass_max_players", true, CVar.SERVERONLY);
|
||||
|
||||
/*
|
||||
* AHELP
|
||||
*/
|
||||
|
||||
/// <summary>
|
||||
/// Ahelp rate limit values are accounted in periods of this size (seconds).
|
||||
/// After the period has passed, the count resets.
|
||||
/// </summary>
|
||||
/// <seealso cref="AhelpRateLimitCount"/>
|
||||
public static readonly CVarDef<int> AhelpRateLimitPeriod =
|
||||
CVarDef.Create("ahelp.rate_limit_period", 2, CVar.SERVERONLY);
|
||||
|
||||
/// <summary>
|
||||
/// How many ahelp messages are allowed in a single rate limit period.
|
||||
/// </summary>
|
||||
/// <seealso cref="AhelpRateLimitPeriod"/>
|
||||
public static readonly CVarDef<int> AhelpRateLimitCount =
|
||||
CVarDef.Create("ahelp.rate_limit_count", 10, CVar.SERVERONLY);
|
||||
|
||||
/*
|
||||
* Explosions
|
||||
*/
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
using Robust.Shared.GameStates;
|
||||
|
||||
namespace Content.Shared.Chemistry.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Blocks all attempts to access solutions contained by this entity.
|
||||
/// </summary>
|
||||
[RegisterComponent, NetworkedComponent]
|
||||
public sealed partial class BlockSolutionAccessComponent : Component
|
||||
{
|
||||
}
|
||||
@@ -48,6 +48,12 @@ public partial record struct SolutionOverflowEvent(Entity<SolutionComponent> Sol
|
||||
public bool Handled = false;
|
||||
}
|
||||
|
||||
[ByRefEvent]
|
||||
public partial record struct SolutionAccessAttemptEvent(string SolutionName)
|
||||
{
|
||||
public bool Cancelled;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Part of Chemistry system deal with SolutionContainers
|
||||
/// </summary>
|
||||
@@ -156,12 +162,6 @@ public abstract partial class SharedSolutionContainerSystem : EntitySystem
|
||||
[NotNullWhen(true)] out Entity<SolutionComponent>? entity,
|
||||
bool errorOnMissing = false)
|
||||
{
|
||||
if (TryComp(container, out BlockSolutionAccessComponent? blocker))
|
||||
{
|
||||
entity = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
EntityUid uid;
|
||||
if (name is null)
|
||||
uid = container;
|
||||
@@ -170,7 +170,18 @@ public abstract partial class SharedSolutionContainerSystem : EntitySystem
|
||||
solutionContainer is ContainerSlot solutionSlot &&
|
||||
solutionSlot.ContainedEntity is { } containedSolution
|
||||
)
|
||||
{
|
||||
var attemptEv = new SolutionAccessAttemptEvent(name);
|
||||
RaiseLocalEvent(container, ref attemptEv);
|
||||
|
||||
if (attemptEv.Cancelled)
|
||||
{
|
||||
entity = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
uid = containedSolution;
|
||||
}
|
||||
else
|
||||
{
|
||||
entity = null;
|
||||
@@ -218,11 +229,14 @@ public abstract partial class SharedSolutionContainerSystem : EntitySystem
|
||||
if (!Resolve(container, ref container.Comp, logMissing: false))
|
||||
yield break;
|
||||
|
||||
if (HasComp<BlockSolutionAccessComponent>(container))
|
||||
yield break;
|
||||
|
||||
foreach (var name in container.Comp.Containers)
|
||||
{
|
||||
var attemptEv = new SolutionAccessAttemptEvent(name);
|
||||
RaiseLocalEvent(container, ref attemptEv);
|
||||
|
||||
if (attemptEv.Cancelled)
|
||||
continue;
|
||||
|
||||
if (ContainerSystem.GetContainer(container, $"solution@{name}") is ContainerSlot slot && slot.ContainedEntity is { } solutionId)
|
||||
yield return (name, (solutionId, Comp<SolutionComponent>(solutionId)));
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ public sealed partial class ClimbSystem : VirtualController
|
||||
SubscribeLocalEvent<ClimbingComponent, EntParentChangedMessage>(OnParentChange);
|
||||
SubscribeLocalEvent<ClimbingComponent, ClimbDoAfterEvent>(OnDoAfter);
|
||||
SubscribeLocalEvent<ClimbingComponent, EndCollideEvent>(OnClimbEndCollide);
|
||||
SubscribeLocalEvent<ClimbingComponent, BuckleChangeEvent>(OnBuckleChange);
|
||||
SubscribeLocalEvent<ClimbingComponent, BuckledEvent>(OnBuckled);
|
||||
|
||||
SubscribeLocalEvent<ClimbableComponent, CanDropTargetEvent>(OnCanDragDropOn);
|
||||
SubscribeLocalEvent<ClimbableComponent, GetVerbsEvent<AlternativeVerb>>(AddClimbableVerb);
|
||||
@@ -468,10 +468,8 @@ public sealed partial class ClimbSystem : VirtualController
|
||||
Climb(uid, uid, climbable, true, component);
|
||||
}
|
||||
|
||||
private void OnBuckleChange(EntityUid uid, ClimbingComponent component, ref BuckleChangeEvent args)
|
||||
private void OnBuckled(EntityUid uid, ClimbingComponent component, ref BuckledEvent args)
|
||||
{
|
||||
if (!args.Buckling)
|
||||
return;
|
||||
StopClimb(uid, component);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
{
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -87,7 +87,7 @@ public abstract class ClothingSystem : EntitySystem
|
||||
foreach (HumanoidVisualLayers layer in layers)
|
||||
{
|
||||
if (!appearanceLayers.Contains(layer))
|
||||
break;
|
||||
continue;
|
||||
|
||||
InventorySystem.InventorySlotEnumerator enumerator = _invSystem.GetSlotEnumerator(equipee);
|
||||
|
||||
|
||||
169
Content.Shared/Clothing/EntitySystems/PilotedClothingSystem.cs
Normal file
169
Content.Shared/Clothing/EntitySystems/PilotedClothingSystem.cs
Normal file
@@ -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);
|
||||
@@ -1,12 +0,0 @@
|
||||
using Robust.Shared.GameStates;
|
||||
|
||||
namespace Content.Shared.Conveyor;
|
||||
|
||||
/// <summary>
|
||||
/// Used to track which conveyors are relevant in case there's a lot of them.
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public sealed partial class ActiveConveyorComponent : Component
|
||||
{
|
||||
|
||||
}
|
||||
13
Content.Shared/Conveyor/ConveyedComponent.cs
Normal file
13
Content.Shared/Conveyor/ConveyedComponent.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using Robust.Shared.GameStates;
|
||||
|
||||
namespace Content.Shared.Conveyor;
|
||||
|
||||
/// <summary>
|
||||
/// Indicates this entity is currently being conveyed.
|
||||
/// </summary>
|
||||
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
|
||||
public sealed partial class ConveyedComponent : Component
|
||||
{
|
||||
[ViewVariables, AutoNetworkedField]
|
||||
public List<EntityUid> Colliding = new();
|
||||
}
|
||||
@@ -39,9 +39,6 @@ public sealed partial class ConveyorComponent : Component
|
||||
|
||||
[DataField]
|
||||
public ProtoId<SinkPortPrototype> OffPort = "Off";
|
||||
|
||||
[ViewVariables]
|
||||
public readonly HashSet<EntityUid> Intersecting = new();
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
|
||||
@@ -71,6 +71,7 @@ namespace Content.Shared.Cuffs
|
||||
SubscribeLocalEvent<CuffableComponent, IsUnequippingAttemptEvent>(OnUnequipAttempt);
|
||||
SubscribeLocalEvent<CuffableComponent, BeingPulledAttemptEvent>(OnBeingPulledAttempt);
|
||||
SubscribeLocalEvent<CuffableComponent, BuckleAttemptEvent>(OnBuckleAttemptEvent);
|
||||
SubscribeLocalEvent<CuffableComponent, UnbuckleAttemptEvent>(OnUnbuckleAttemptEvent);
|
||||
SubscribeLocalEvent<CuffableComponent, GetVerbsEvent<Verb>>(AddUncuffVerb);
|
||||
SubscribeLocalEvent<CuffableComponent, UnCuffDoAfterEvent>(OnCuffableDoAfter);
|
||||
SubscribeLocalEvent<CuffableComponent, PullStartedMessage>(OnPull);
|
||||
@@ -79,7 +80,7 @@ namespace Content.Shared.Cuffs
|
||||
SubscribeLocalEvent<CuffableComponent, PickupAttemptEvent>(CheckAct);
|
||||
SubscribeLocalEvent<CuffableComponent, AttackAttemptEvent>(CheckAct);
|
||||
SubscribeLocalEvent<CuffableComponent, UseAttemptEvent>(CheckAct);
|
||||
SubscribeLocalEvent<CuffableComponent, InteractionAttemptEvent>(CheckAct);
|
||||
SubscribeLocalEvent<CuffableComponent, InteractionAttemptEvent>(CheckInteract);
|
||||
|
||||
SubscribeLocalEvent<HandcuffComponent, AfterInteractEvent>(OnCuffAfterInteract);
|
||||
SubscribeLocalEvent<HandcuffComponent, MeleeHitEvent>(OnCuffMeleeHit);
|
||||
@@ -87,6 +88,12 @@ namespace Content.Shared.Cuffs
|
||||
SubscribeLocalEvent<HandcuffComponent, VirtualItemDeletedEvent>(OnCuffVirtualItemDeleted);
|
||||
}
|
||||
|
||||
private void CheckInteract(Entity<CuffableComponent> ent, ref InteractionAttemptEvent args)
|
||||
{
|
||||
if (!ent.Comp.CanStillInteract)
|
||||
args.Cancelled = true;
|
||||
}
|
||||
|
||||
private void OnUncuffAttempt(ref UncuffAttemptEvent args)
|
||||
{
|
||||
if (args.Cancelled)
|
||||
@@ -189,21 +196,33 @@ namespace Content.Shared.Cuffs
|
||||
args.Cancel();
|
||||
}
|
||||
|
||||
private void OnBuckleAttemptEvent(EntityUid uid, CuffableComponent component, ref BuckleAttemptEvent args)
|
||||
private void OnBuckleAttempt(Entity<CuffableComponent> ent, EntityUid? user, ref bool cancelled, bool buckling, bool popup)
|
||||
{
|
||||
// if someone else is doing it, let it pass.
|
||||
if (args.UserEntity != uid)
|
||||
if (cancelled || user != ent.Owner)
|
||||
return;
|
||||
|
||||
if (!TryComp<HandsComponent>(uid, out var hands) || component.CuffedHandCount != hands.Count)
|
||||
if (!TryComp<HandsComponent>(ent, out var hands) || ent.Comp.CuffedHandCount != hands.Count)
|
||||
return;
|
||||
|
||||
args.Cancelled = true;
|
||||
var message = args.Buckling
|
||||
cancelled = true;
|
||||
if (!popup)
|
||||
return;
|
||||
|
||||
var message = buckling
|
||||
? Loc.GetString("handcuff-component-cuff-interrupt-buckled-message")
|
||||
: Loc.GetString("handcuff-component-cuff-interrupt-unbuckled-message");
|
||||
|
||||
_popup.PopupClient(message, uid, args.UserEntity);
|
||||
_popup.PopupClient(message, ent, user);
|
||||
}
|
||||
|
||||
private void OnBuckleAttemptEvent(Entity<CuffableComponent> ent, ref BuckleAttemptEvent args)
|
||||
{
|
||||
OnBuckleAttempt(ent, args.User, ref args.Cancelled, true, args.Popup);
|
||||
}
|
||||
|
||||
private void OnUnbuckleAttemptEvent(Entity<CuffableComponent> ent, ref UnbuckleAttemptEvent args)
|
||||
{
|
||||
OnBuckleAttempt(ent, args.User, ref args.Cancelled, false, args.Popup);
|
||||
}
|
||||
|
||||
private void OnPull(EntityUid uid, CuffableComponent component, PullMessage args)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Set;
|
||||
|
||||
namespace Content.Shared.DeviceLinking;
|
||||
@@ -11,13 +12,14 @@ public sealed partial class DeviceLinkSinkComponent : Component
|
||||
/// <summary>
|
||||
/// The ports this sink has
|
||||
/// </summary>
|
||||
[DataField("ports", customTypeSerializer: typeof(PrototypeIdHashSetSerializer<SinkPortPrototype>))]
|
||||
public HashSet<string>? Ports;
|
||||
[DataField]
|
||||
public HashSet<ProtoId<SinkPortPrototype>> Ports = new();
|
||||
|
||||
/// <summary>
|
||||
/// Used for removing a sink from all linked sources when it gets removed
|
||||
/// Used for removing a sink from all linked sources when this component gets removed.
|
||||
/// This is not serialized to yaml as it can be inferred from source components.
|
||||
/// </summary>
|
||||
[DataField("links")]
|
||||
[ViewVariables]
|
||||
public HashSet<EntityUid> LinkedSources = new();
|
||||
|
||||
/// <summary>
|
||||
@@ -25,14 +27,13 @@ public sealed partial class DeviceLinkSinkComponent : Component
|
||||
/// The counter is counted down by one every tick if it's higher than 0
|
||||
/// This is for preventing infinite loops
|
||||
/// </summary>
|
||||
[DataField("invokeCounter")]
|
||||
[DataField]
|
||||
public int InvokeCounter;
|
||||
|
||||
/// <summary>
|
||||
/// How high the invoke counter is allowed to get before the links to the sink are removed and the DeviceLinkOverloadedEvent gets raised
|
||||
/// If the invoke limit is smaller than 1 the sink can't overload
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField("invokeLimit")]
|
||||
[DataField]
|
||||
public int InvokeLimit = 10;
|
||||
}
|
||||
|
||||
@@ -12,12 +12,12 @@ public sealed partial class DeviceLinkSourceComponent : Component
|
||||
/// The ports the device link source sends signals from
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public HashSet<ProtoId<SourcePortPrototype>>? Ports;
|
||||
public HashSet<ProtoId<SourcePortPrototype>> Ports = new();
|
||||
|
||||
/// <summary>
|
||||
/// A list of sink uids that got linked for each port
|
||||
/// Dictionary mapping each port to a set of linked sink entities.
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
[ViewVariables] // This is not serialized as it can be constructed from LinkedPorts
|
||||
public Dictionary<ProtoId<SourcePortPrototype>, HashSet<EntityUid>> Outputs = new();
|
||||
|
||||
/// <summary>
|
||||
@@ -32,7 +32,7 @@ public sealed partial class DeviceLinkSourceComponent : Component
|
||||
/// The list of source to sink ports for each linked sink entity for easier managing of links
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public Dictionary<EntityUid, HashSet<(ProtoId<SourcePortPrototype> source, ProtoId<SinkPortPrototype> sink)>> LinkedPorts = new();
|
||||
public Dictionary<EntityUid, HashSet<(ProtoId<SourcePortPrototype> Source, ProtoId<SinkPortPrototype> Sink)>> LinkedPorts = new();
|
||||
|
||||
/// <summary>
|
||||
/// Limits the range devices can be linked across.
|
||||
|
||||
@@ -20,98 +20,56 @@ public abstract class SharedDeviceLinkSystem : EntitySystem
|
||||
/// <inheritdoc/>
|
||||
public override void Initialize()
|
||||
{
|
||||
SubscribeLocalEvent<DeviceLinkSourceComponent, ComponentInit>(OnInit);
|
||||
SubscribeLocalEvent<DeviceLinkSourceComponent, ComponentStartup>(OnSourceStartup);
|
||||
SubscribeLocalEvent<DeviceLinkSinkComponent, ComponentStartup>(OnSinkStartup);
|
||||
SubscribeLocalEvent<DeviceLinkSourceComponent, ComponentRemove>(OnSourceRemoved);
|
||||
SubscribeLocalEvent<DeviceLinkSinkComponent, ComponentRemove>(OnSinkRemoved);
|
||||
}
|
||||
|
||||
#region Link Validation
|
||||
|
||||
private void OnInit(EntityUid uid, DeviceLinkSourceComponent component, ComponentInit args)
|
||||
{
|
||||
// Populate the output dictionary.
|
||||
foreach (var (sinkUid, links) in component.LinkedPorts)
|
||||
{
|
||||
foreach (var link in links)
|
||||
{
|
||||
component.Outputs.GetOrNew(link.source).Add(sinkUid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes invalid links where the saved sink doesn't exist/have a sink component for example
|
||||
/// </summary>
|
||||
private void OnSourceStartup(EntityUid sourceUid, DeviceLinkSourceComponent sourceComponent, ComponentStartup args)
|
||||
private void OnSourceStartup(Entity<DeviceLinkSourceComponent> source, ref ComponentStartup args)
|
||||
{
|
||||
List<EntityUid> invalidSinks = new();
|
||||
foreach (var sinkUid in sourceComponent.LinkedPorts.Keys)
|
||||
List<(string, string)> invalidLinks = new();
|
||||
foreach (var (sink, links) in source.Comp.LinkedPorts)
|
||||
{
|
||||
if (!TryComp<DeviceLinkSinkComponent>(sinkUid, out var sinkComponent))
|
||||
if (!TryComp(sink, out DeviceLinkSinkComponent? sinkComponent))
|
||||
{
|
||||
invalidSinks.Add(sinkUid);
|
||||
foreach (var savedSinks in sourceComponent.Outputs.Values)
|
||||
{
|
||||
savedSinks.Remove(sinkUid);
|
||||
}
|
||||
|
||||
invalidSinks.Add(sink);
|
||||
continue;
|
||||
}
|
||||
|
||||
sinkComponent.LinkedSources.Add(sourceUid);
|
||||
}
|
||||
|
||||
foreach (var invalidSink in invalidSinks)
|
||||
{
|
||||
sourceComponent.LinkedPorts.Remove(invalidSink);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Same with <see cref="OnSourceStartup"/> but also checks that the saved ports are present on the sink
|
||||
/// </summary>
|
||||
private void OnSinkStartup(EntityUid sinkUid, DeviceLinkSinkComponent sinkComponent, ComponentStartup args)
|
||||
{
|
||||
List<EntityUid> invalidSources = new();
|
||||
foreach (var sourceUid in sinkComponent.LinkedSources)
|
||||
{
|
||||
if (!TryComp<DeviceLinkSourceComponent>(sourceUid, out var sourceComponent))
|
||||
foreach (var link in links)
|
||||
{
|
||||
invalidSources.Add(sourceUid);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!sourceComponent.LinkedPorts.TryGetValue(sinkUid, out var linkedPorts))
|
||||
{
|
||||
foreach (var savedSinks in sourceComponent.Outputs.Values)
|
||||
{
|
||||
savedSinks.Remove(sinkUid);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (sinkComponent.Ports == null)
|
||||
continue;
|
||||
|
||||
List<(string, string)> invalidLinks = new();
|
||||
foreach (var link in linkedPorts)
|
||||
{
|
||||
if (!sinkComponent.Ports.Contains(link.sink))
|
||||
if (sinkComponent.Ports.Contains(link.Sink) && source.Comp.Ports.Contains(link.Source))
|
||||
source.Comp.Outputs.GetOrNew(link.Source).Add(sink);
|
||||
else
|
||||
invalidLinks.Add(link);
|
||||
}
|
||||
|
||||
foreach (var invalidLink in invalidLinks)
|
||||
foreach (var link in invalidLinks)
|
||||
{
|
||||
linkedPorts.Remove(invalidLink);
|
||||
sourceComponent.Outputs.GetValueOrDefault(invalidLink.Item1)?.Remove(sinkUid);
|
||||
Log.Warning($"Device source {ToPrettyString(source)} contains invalid links to entity {ToPrettyString(sink)}: {link.Item1}->{link.Item2}");
|
||||
links.Remove(link);
|
||||
}
|
||||
|
||||
if (links.Count == 0)
|
||||
{
|
||||
invalidSinks.Add(sink);
|
||||
continue;
|
||||
}
|
||||
|
||||
invalidLinks.Clear();
|
||||
sinkComponent.LinkedSources.Add(source.Owner);
|
||||
}
|
||||
|
||||
foreach (var invalidSource in invalidSources)
|
||||
foreach (var sink in invalidSinks)
|
||||
{
|
||||
sinkComponent.LinkedSources.Remove(invalidSource);
|
||||
source.Comp.LinkedPorts.Remove(sink);
|
||||
Log.Warning($"Device source {ToPrettyString(source)} contains invalid sink: {ToPrettyString(sink)}");
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
@@ -119,26 +77,29 @@ public abstract class SharedDeviceLinkSystem : EntitySystem
|
||||
/// <summary>
|
||||
/// Ensures that its links get deleted when a source gets removed
|
||||
/// </summary>
|
||||
private void OnSourceRemoved(EntityUid uid, DeviceLinkSourceComponent component, ComponentRemove args)
|
||||
private void OnSourceRemoved(Entity<DeviceLinkSourceComponent> source, ref ComponentRemove args)
|
||||
{
|
||||
var query = GetEntityQuery<DeviceLinkSinkComponent>();
|
||||
foreach (var sinkUid in component.LinkedPorts.Keys)
|
||||
foreach (var sinkUid in source.Comp.LinkedPorts.Keys)
|
||||
{
|
||||
if (query.TryGetComponent(sinkUid, out var sink))
|
||||
RemoveSinkFromSourceInternal(uid, sinkUid, component, sink);
|
||||
RemoveSinkFromSourceInternal(source, sinkUid, source, sink);
|
||||
else
|
||||
Log.Error($"Device source {ToPrettyString(source)} links to invalid entity: {ToPrettyString(sinkUid)}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ensures that its links get deleted when a sink gets removed
|
||||
/// </summary>
|
||||
private void OnSinkRemoved(EntityUid sinkUid, DeviceLinkSinkComponent sinkComponent, ComponentRemove args)
|
||||
private void OnSinkRemoved(Entity<DeviceLinkSinkComponent> sink, ref ComponentRemove args)
|
||||
{
|
||||
var query = GetEntityQuery<DeviceLinkSourceComponent>();
|
||||
foreach (var linkedSource in sinkComponent.LinkedSources)
|
||||
foreach (var sourceUid in sink.Comp.LinkedSources)
|
||||
{
|
||||
if (query.TryGetComponent(sinkUid, out var source))
|
||||
RemoveSinkFromSourceInternal(linkedSource, sinkUid, source, sinkComponent);
|
||||
if (TryComp(sourceUid, out DeviceLinkSourceComponent? source))
|
||||
RemoveSinkFromSourceInternal(sourceUid, sink, source, sink);
|
||||
else
|
||||
Log.Error($"Device sink {ToPrettyString(sink)} source list contains invalid entity: {ToPrettyString(sourceUid)}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,36 +107,36 @@ public abstract class SharedDeviceLinkSystem : EntitySystem
|
||||
/// <summary>
|
||||
/// Convenience function to add several ports to an entity
|
||||
/// </summary>
|
||||
public void EnsureSourcePorts(EntityUid uid, params string[] ports)
|
||||
public void EnsureSourcePorts(EntityUid uid, params ProtoId<SourcePortPrototype>[] ports)
|
||||
{
|
||||
if (ports.Length == 0)
|
||||
return;
|
||||
|
||||
var comp = EnsureComp<DeviceLinkSourceComponent>(uid);
|
||||
comp.Ports ??= new HashSet<ProtoId<SourcePortPrototype>>();
|
||||
|
||||
foreach (var port in ports)
|
||||
{
|
||||
DebugTools.Assert(_prototypeManager.HasIndex<SourcePortPrototype>(port));
|
||||
comp.Ports?.Add(port);
|
||||
if (!_prototypeManager.HasIndex(port))
|
||||
Log.Error($"Attempted to add invalid port {port} to {ToPrettyString(uid)}");
|
||||
else
|
||||
comp.Ports.Add(port);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convenience function to add several ports to an entity.
|
||||
/// </summary>
|
||||
public void EnsureSinkPorts(EntityUid uid, params string[] ports)
|
||||
public void EnsureSinkPorts(EntityUid uid, params ProtoId<SinkPortPrototype>[] ports)
|
||||
{
|
||||
if (ports.Length == 0)
|
||||
return;
|
||||
|
||||
var comp = EnsureComp<DeviceLinkSinkComponent>(uid);
|
||||
comp.Ports ??= new HashSet<string>();
|
||||
|
||||
foreach (var port in ports)
|
||||
{
|
||||
DebugTools.Assert(_prototypeManager.HasIndex<SinkPortPrototype>(port));
|
||||
comp.Ports?.Add(port);
|
||||
if (!_prototypeManager.HasIndex(port))
|
||||
Log.Error($"Attempted to add invalid port {port} to {ToPrettyString(uid)}");
|
||||
else
|
||||
comp.Ports.Add(port);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,13 +146,13 @@ public abstract class SharedDeviceLinkSystem : EntitySystem
|
||||
/// <returns>A list of source port prototypes</returns>
|
||||
public List<SourcePortPrototype> GetSourcePorts(EntityUid sourceUid, DeviceLinkSourceComponent? sourceComponent = null)
|
||||
{
|
||||
if (!Resolve(sourceUid, ref sourceComponent) || sourceComponent.Ports == null)
|
||||
if (!Resolve(sourceUid, ref sourceComponent))
|
||||
return new List<SourcePortPrototype>();
|
||||
|
||||
var sourcePorts = new List<SourcePortPrototype>();
|
||||
foreach (var port in sourceComponent.Ports)
|
||||
{
|
||||
sourcePorts.Add(_prototypeManager.Index<SourcePortPrototype>(port));
|
||||
sourcePorts.Add(_prototypeManager.Index(port));
|
||||
}
|
||||
|
||||
return sourcePorts;
|
||||
@@ -203,13 +164,13 @@ public abstract class SharedDeviceLinkSystem : EntitySystem
|
||||
/// <returns>A list of sink port prototypes</returns>
|
||||
public List<SinkPortPrototype> GetSinkPorts(EntityUid sinkUid, DeviceLinkSinkComponent? sinkComponent = null)
|
||||
{
|
||||
if (!Resolve(sinkUid, ref sinkComponent) || sinkComponent.Ports == null)
|
||||
if (!Resolve(sinkUid, ref sinkComponent))
|
||||
return new List<SinkPortPrototype>();
|
||||
|
||||
var sinkPorts = new List<SinkPortPrototype>();
|
||||
foreach (var port in sinkComponent.Ports)
|
||||
{
|
||||
sinkPorts.Add(_prototypeManager.Index<SinkPortPrototype>(port));
|
||||
sinkPorts.Add(_prototypeManager.Index(port));
|
||||
}
|
||||
|
||||
return sinkPorts;
|
||||
@@ -315,9 +276,6 @@ public abstract class SharedDeviceLinkSystem : EntitySystem
|
||||
if (!Resolve(sourceUid, ref sourceComponent) || !Resolve(sinkUid, ref sinkComponent))
|
||||
return;
|
||||
|
||||
if (sourceComponent.Ports == null || sinkComponent.Ports == null)
|
||||
return;
|
||||
|
||||
if (!InRange(sourceUid, sinkUid, sourceComponent.Range))
|
||||
{
|
||||
if (userId != null)
|
||||
@@ -391,7 +349,7 @@ public abstract class SharedDeviceLinkSystem : EntitySystem
|
||||
else
|
||||
{
|
||||
Log.Error($"Attempted to remove link between {ToPrettyString(sourceUid)} and {ToPrettyString(sinkUid)}, but the sink component was missing.");
|
||||
sourceComponent.LinkedPorts.Remove(sourceUid);
|
||||
sourceComponent.LinkedPorts.Remove(sinkUid);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -414,12 +372,10 @@ public abstract class SharedDeviceLinkSystem : EntitySystem
|
||||
|
||||
sinkComponent.LinkedSources.Remove(sourceUid);
|
||||
sourceComponent.LinkedPorts.Remove(sinkUid);
|
||||
var outputLists = sourceComponent.Outputs.Values;
|
||||
foreach (var outputList in outputLists)
|
||||
foreach (var outputList in sourceComponent.Outputs.Values)
|
||||
{
|
||||
outputList.Remove(sinkUid);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -438,9 +394,6 @@ public abstract class SharedDeviceLinkSystem : EntitySystem
|
||||
if (!Resolve(sourceUid, ref sourceComponent) || !Resolve(sinkUid, ref sinkComponent))
|
||||
return false;
|
||||
|
||||
if (sourceComponent.Ports == null || sinkComponent.Ports == null)
|
||||
return false;
|
||||
|
||||
var outputs = sourceComponent.Outputs.GetOrNew(source);
|
||||
var linkedPorts = sourceComponent.LinkedPorts.GetOrNew(sinkUid);
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using Content.Shared.DeviceNetwork.Systems;
|
||||
using Robust.Shared.GameStates;
|
||||
|
||||
namespace Content.Shared.DeviceNetwork.Components;
|
||||
@@ -6,6 +7,7 @@ namespace Content.Shared.DeviceNetwork.Components;
|
||||
/// Allow entities to jam DeviceNetwork packets.
|
||||
/// </summary>
|
||||
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
|
||||
[Access(typeof(SharedDeviceNetworkJammerSystem))]
|
||||
public sealed partial class DeviceNetworkJammerComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
using Content.Shared.DeviceNetwork.Components;
|
||||
|
||||
namespace Content.Shared.DeviceNetwork.Systems;
|
||||
|
||||
/// <inheritdoc cref="DeviceNetworkJammerComponent"/>
|
||||
public abstract class SharedDeviceNetworkJammerSystem : EntitySystem
|
||||
{
|
||||
/// <summary>
|
||||
/// Sets the range of the jamming effect.
|
||||
/// </summary>
|
||||
public void SetRange(Entity<DeviceNetworkJammerComponent> ent, float value)
|
||||
{
|
||||
ent.Comp.Range = value;
|
||||
Dirty(ent);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="SetRange"/>
|
||||
public bool TrySetRange(Entity<DeviceNetworkJammerComponent?> ent, float value)
|
||||
{
|
||||
if (!Resolve(ent, ref ent.Comp, logMissing: false))
|
||||
return false;
|
||||
|
||||
SetRange((ent, ent.Comp), value);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the set of networks that this entity can jam.
|
||||
public IReadOnlySet<string> GetJammableNetworks(Entity<DeviceNetworkJammerComponent> ent)
|
||||
{
|
||||
return ent.Comp.JammableNetworks;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enables this entity to jam packets on the specified network.
|
||||
/// </summary>
|
||||
public void AddJammableNetwork(Entity<DeviceNetworkJammerComponent> ent, string networkId)
|
||||
{
|
||||
if (ent.Comp.JammableNetworks.Add(networkId))
|
||||
Dirty(ent);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stops this entity from jamming packets on the specified network.
|
||||
/// </summary>
|
||||
public void RemoveJammableNetwork(Entity<DeviceNetworkJammerComponent> ent, string networkId)
|
||||
{
|
||||
if (ent.Comp.JammableNetworks.Remove(networkId))
|
||||
Dirty(ent);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stops this entity from jamming packets on any networks.
|
||||
/// </summary>
|
||||
public void ClearJammableNetworks(Entity<DeviceNetworkJammerComponent> ent)
|
||||
{
|
||||
if (ent.Comp.JammableNetworks.Count == 0)
|
||||
return;
|
||||
|
||||
ent.Comp.JammableNetworks.Clear();
|
||||
Dirty(ent);
|
||||
}
|
||||
}
|
||||
@@ -34,6 +34,14 @@ public abstract partial class DoAfterEvent : HandledEntityEventArgs
|
||||
public EntityUid? Used => DoAfter.Args.Used;
|
||||
public DoAfterArgs Args => DoAfter.Args;
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Check whether this event is "the same" as another event for duplicate checking.
|
||||
/// </summary>
|
||||
public virtual bool IsDuplicate(DoAfterEvent other)
|
||||
{
|
||||
return GetType() == other.GetType();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -310,7 +310,7 @@ public abstract partial class SharedDoAfterSystem : EntitySystem
|
||||
}
|
||||
|
||||
if ((conditions & DuplicateConditions.SameEvent) != 0
|
||||
&& args.Event.GetType() != otherArgs.Event.GetType())
|
||||
&& !args.Event.IsDuplicate(otherArgs.Event))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
24
Content.Shared/Fluids/Components/SpillWhenWornComponent.cs
Normal file
24
Content.Shared/Fluids/Components/SpillWhenWornComponent.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using Robust.Shared.GameStates;
|
||||
|
||||
namespace Content.Shared.Fluids.Components;
|
||||
|
||||
/// <summary>
|
||||
/// This entity will spill its contained solution onto the wearer when worn, and its
|
||||
/// (empty) contents will be inaccessible while still worn.
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
[NetworkedComponent, AutoGenerateComponentState]
|
||||
public sealed partial class SpillWhenWornComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// Name of the solution to spill.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public string Solution = "default";
|
||||
|
||||
/// <summary>
|
||||
/// Tracks if this item is currently being worn.
|
||||
/// </summary>
|
||||
[DataField, AutoNetworkedField]
|
||||
public bool IsWorn;
|
||||
}
|
||||
@@ -14,13 +14,6 @@ public sealed partial class SpillableComponent : Component
|
||||
[DataField("solution")]
|
||||
public string SolutionName = "puddle";
|
||||
|
||||
/// <summary>
|
||||
/// Should this item be spilled when worn as clothing?
|
||||
/// Doesn't count for pockets or hands.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public bool SpillWorn = true;
|
||||
|
||||
[DataField]
|
||||
public float? SpillDelay;
|
||||
|
||||
|
||||
55
Content.Shared/Fluids/EntitySystems/SpillWhenWornSystem.cs
Normal file
55
Content.Shared/Fluids/EntitySystems/SpillWhenWornSystem.cs
Normal file
@@ -0,0 +1,55 @@
|
||||
using Content.Shared.Chemistry.EntitySystems;
|
||||
using Content.Shared.Clothing;
|
||||
using Content.Shared.Fluids.Components;
|
||||
|
||||
namespace Content.Shared.Fluids.EntitySystems;
|
||||
|
||||
/// <inheritdoc cref="SpillWhenWornComponent"/>
|
||||
public sealed class SpillWhenWornSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly SharedSolutionContainerSystem _solutionContainer = default!;
|
||||
[Dependency] private readonly SharedPuddleSystem _puddle = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<SpillWhenWornComponent, ClothingGotEquippedEvent>(OnGotEquipped);
|
||||
SubscribeLocalEvent<SpillWhenWornComponent, ClothingGotUnequippedEvent>(OnGotUnequipped);
|
||||
SubscribeLocalEvent<SpillWhenWornComponent, SolutionAccessAttemptEvent>(OnSolutionAccessAttempt);
|
||||
}
|
||||
|
||||
private void OnGotEquipped(Entity<SpillWhenWornComponent> ent, ref ClothingGotEquippedEvent args)
|
||||
{
|
||||
if (_solutionContainer.TryGetSolution(ent.Owner, ent.Comp.Solution, out var soln, out var solution)
|
||||
&& solution.Volume > 0)
|
||||
{
|
||||
// Spill all solution on the player
|
||||
var drainedSolution = _solutionContainer.Drain(ent.Owner, soln.Value, solution.Volume);
|
||||
_puddle.TrySplashSpillAt(ent.Owner, Transform(args.Wearer).Coordinates, drainedSolution, out _);
|
||||
}
|
||||
|
||||
// Flag as worn after draining, otherwise we'll block ourself from accessing!
|
||||
ent.Comp.IsWorn = true;
|
||||
Dirty(ent);
|
||||
}
|
||||
|
||||
private void OnGotUnequipped(Entity<SpillWhenWornComponent> ent, ref ClothingGotUnequippedEvent args)
|
||||
{
|
||||
ent.Comp.IsWorn = false;
|
||||
Dirty(ent);
|
||||
}
|
||||
|
||||
private void OnSolutionAccessAttempt(Entity<SpillWhenWornComponent> ent, ref SolutionAccessAttemptEvent args)
|
||||
{
|
||||
// If we're not being worn right now, we don't care
|
||||
if (!ent.Comp.IsWorn)
|
||||
return;
|
||||
|
||||
// Make sure it's the right solution
|
||||
if (ent.Comp.Solution != args.SolutionName)
|
||||
return;
|
||||
|
||||
args.Cancelled = true;
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,7 @@ public sealed class FoldableSystem : EntitySystem
|
||||
SubscribeLocalEvent<FoldableComponent, StoreMobInItemContainerAttemptEvent>(OnStoreThisAttempt);
|
||||
SubscribeLocalEvent<FoldableComponent, StorageOpenAttemptEvent>(OnFoldableOpenAttempt);
|
||||
|
||||
SubscribeLocalEvent<FoldableComponent, BuckleAttemptEvent>(OnBuckleAttempt);
|
||||
SubscribeLocalEvent<FoldableComponent, StrapAttemptEvent>(OnStrapAttempt);
|
||||
}
|
||||
|
||||
private void OnHandleState(EntityUid uid, FoldableComponent component, ref AfterAutoHandleStateEvent args)
|
||||
@@ -53,9 +53,9 @@ public sealed class FoldableSystem : EntitySystem
|
||||
args.Cancelled = true;
|
||||
}
|
||||
|
||||
public void OnBuckleAttempt(EntityUid uid, FoldableComponent comp, ref BuckleAttemptEvent args)
|
||||
public void OnStrapAttempt(EntityUid uid, FoldableComponent comp, ref StrapAttemptEvent args)
|
||||
{
|
||||
if (args.Buckling && comp.IsFolded)
|
||||
if (comp.IsFolded)
|
||||
args.Cancelled = true;
|
||||
}
|
||||
|
||||
|
||||
@@ -19,12 +19,18 @@ namespace Content.Shared.Ghost
|
||||
{
|
||||
base.Initialize();
|
||||
SubscribeLocalEvent<GhostComponent, UseAttemptEvent>(OnAttempt);
|
||||
SubscribeLocalEvent<GhostComponent, InteractionAttemptEvent>(OnAttempt);
|
||||
SubscribeLocalEvent<GhostComponent, InteractionAttemptEvent>(OnAttemptInteract);
|
||||
SubscribeLocalEvent<GhostComponent, EmoteAttemptEvent>(OnAttempt);
|
||||
SubscribeLocalEvent<GhostComponent, DropAttemptEvent>(OnAttempt);
|
||||
SubscribeLocalEvent<GhostComponent, PickupAttemptEvent>(OnAttempt);
|
||||
}
|
||||
|
||||
private void OnAttemptInteract(Entity<GhostComponent> ent, ref InteractionAttemptEvent args)
|
||||
{
|
||||
if (!ent.Comp.CanGhostInteract)
|
||||
args.Cancelled = true;
|
||||
}
|
||||
|
||||
private void OnAttempt(EntityUid uid, GhostComponent component, CancellableEntityEventArgs args)
|
||||
{
|
||||
if (!component.CanGhostInteract)
|
||||
|
||||
@@ -15,6 +15,9 @@ public static class Identity
|
||||
/// </summary>
|
||||
public static string Name(EntityUid uid, IEntityManager ent, EntityUid? viewer=null)
|
||||
{
|
||||
if (!uid.IsValid())
|
||||
return string.Empty;
|
||||
|
||||
var meta = ent.GetComponent<MetaDataComponent>(uid);
|
||||
if (meta.EntityLifeStage <= EntityLifeStage.Initializing)
|
||||
return meta.EntityName; // Identity component and such will not yet have initialized and may throw NREs
|
||||
|
||||
@@ -3,39 +3,33 @@
|
||||
/// <summary>
|
||||
/// Event raised directed at a user to see if they can perform a generic interaction.
|
||||
/// </summary>
|
||||
public sealed class InteractionAttemptEvent : CancellableEntityEventArgs
|
||||
[ByRefEvent]
|
||||
public struct InteractionAttemptEvent(EntityUid uid, EntityUid? target)
|
||||
{
|
||||
public InteractionAttemptEvent(EntityUid uid, EntityUid? target)
|
||||
{
|
||||
Uid = uid;
|
||||
Target = target;
|
||||
}
|
||||
|
||||
public EntityUid Uid { get; }
|
||||
public EntityUid? Target { get; }
|
||||
public bool Cancelled;
|
||||
public readonly EntityUid Uid = uid;
|
||||
public readonly EntityUid? Target = target;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raised to determine whether an entity is conscious to perform an action.
|
||||
/// </summary>
|
||||
public sealed class ConsciousAttemptEvent(EntityUid Uid) : CancellableEntityEventArgs
|
||||
[ByRefEvent]
|
||||
public struct ConsciousAttemptEvent(EntityUid uid)
|
||||
{
|
||||
public EntityUid Uid { get; } = Uid;
|
||||
public bool Cancelled;
|
||||
public readonly EntityUid Uid = uid;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Event raised directed at the target entity of an interaction to see if the user is allowed to perform some
|
||||
/// generic interaction.
|
||||
/// </summary>
|
||||
public sealed class GettingInteractedWithAttemptEvent : CancellableEntityEventArgs
|
||||
[ByRefEvent]
|
||||
public struct GettingInteractedWithAttemptEvent(EntityUid uid, EntityUid? target)
|
||||
{
|
||||
public GettingInteractedWithAttemptEvent(EntityUid uid, EntityUid? target)
|
||||
{
|
||||
Uid = uid;
|
||||
Target = target;
|
||||
}
|
||||
|
||||
public EntityUid Uid { get; }
|
||||
public EntityUid? Target { get; }
|
||||
public bool Cancelled;
|
||||
public readonly EntityUid Uid = uid;
|
||||
public readonly EntityUid? Target = target;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
using System.Numerics;
|
||||
using Content.Shared.ActionBlocker;
|
||||
using Content.Shared.Buckle.Components;
|
||||
using Content.Shared.Mobs.Systems;
|
||||
using Content.Shared.Rotatable;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
@@ -83,24 +82,21 @@ namespace Content.Shared.Interaction
|
||||
if (!_actionBlockerSystem.CanChangeDirection(user))
|
||||
return false;
|
||||
|
||||
if (EntityManager.TryGetComponent(user, out BuckleComponent? buckle) && buckle.Buckled)
|
||||
if (TryComp(user, out BuckleComponent? buckle) && buckle.BuckledTo is {} strap)
|
||||
{
|
||||
var suid = buckle.LastEntityBuckledTo;
|
||||
if (suid != null)
|
||||
{
|
||||
// We're buckled to another object. Is that object rotatable?
|
||||
if (TryComp<RotatableComponent>(suid.Value, out var rotatable) && rotatable.RotateWhileAnchored)
|
||||
{
|
||||
// Note the assumption that even if unanchored, user can only do spinnychair with an "independent wheel".
|
||||
// (Since the user being buckled to it holds it down with their weight.)
|
||||
// This is logically equivalent to RotateWhileAnchored.
|
||||
// Barstools and office chairs have independent wheels, while regular chairs don't.
|
||||
_transform.SetWorldRotation(Transform(suid.Value), diffAngle);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// What if a person is strapped to a borg?
|
||||
// I'm pretty sure this would allow them to be partially ratatouille'd
|
||||
|
||||
return false;
|
||||
// We're buckled to another object. Is that object rotatable?
|
||||
if (!TryComp<RotatableComponent>(strap, out var rotatable) || !rotatable.RotateWhileAnchored)
|
||||
return false;
|
||||
|
||||
// Note the assumption that even if unanchored, user can only do spinnychair with an "independent wheel".
|
||||
// (Since the user being buckled to it holds it down with their weight.)
|
||||
// This is logically equivalent to RotateWhileAnchored.
|
||||
// Barstools and office chairs have independent wheels, while regular chairs don't.
|
||||
_transform.SetWorldRotation(Transform(strap), diffAngle);
|
||||
return true;
|
||||
}
|
||||
|
||||
// user is not buckled in; apply to their transform
|
||||
|
||||
@@ -6,6 +6,7 @@ using Content.Shared.Movement.Events;
|
||||
|
||||
namespace Content.Shared.Interaction;
|
||||
|
||||
// TODO deduplicate with AdminFrozenComponent
|
||||
/// <summary>
|
||||
/// Handles <see cref="BlockMovementComponent"/>, which prevents various
|
||||
/// kinds of movement and interactions when attached to an entity.
|
||||
@@ -16,7 +17,7 @@ public partial class SharedInteractionSystem
|
||||
{
|
||||
SubscribeLocalEvent<BlockMovementComponent, UpdateCanMoveEvent>(OnMoveAttempt);
|
||||
SubscribeLocalEvent<BlockMovementComponent, UseAttemptEvent>(CancelEvent);
|
||||
SubscribeLocalEvent<BlockMovementComponent, InteractionAttemptEvent>(CancelEvent);
|
||||
SubscribeLocalEvent<BlockMovementComponent, InteractionAttemptEvent>(CancelInteractEvent);
|
||||
SubscribeLocalEvent<BlockMovementComponent, DropAttemptEvent>(CancelEvent);
|
||||
SubscribeLocalEvent<BlockMovementComponent, PickupAttemptEvent>(CancelEvent);
|
||||
SubscribeLocalEvent<BlockMovementComponent, ChangeDirectionAttemptEvent>(CancelEvent);
|
||||
@@ -25,6 +26,11 @@ public partial class SharedInteractionSystem
|
||||
SubscribeLocalEvent<BlockMovementComponent, ComponentShutdown>(OnBlockingShutdown);
|
||||
}
|
||||
|
||||
private void CancelInteractEvent(Entity<BlockMovementComponent> ent, ref InteractionAttemptEvent args)
|
||||
{
|
||||
args.Cancelled = true;
|
||||
}
|
||||
|
||||
private void OnMoveAttempt(EntityUid uid, BlockMovementComponent component, UpdateCanMoveEvent args)
|
||||
{
|
||||
if (component.LifeStage > ComponentLifeStage.Running)
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
using System.Linq;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Interaction.Components;
|
||||
using Content.Shared.Mobs;
|
||||
using Content.Shared.Stacks;
|
||||
using Content.Shared.Whitelist;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Shared.Materials;
|
||||
|
||||
@@ -166,8 +168,14 @@ public abstract class SharedMaterialStorageSystem : EntitySystem
|
||||
return false;
|
||||
if (!CanChangeMaterialAmount(uid, materialId, volume, component))
|
||||
return false;
|
||||
component.Storage.TryAdd(materialId, 0);
|
||||
component.Storage[materialId] += volume;
|
||||
|
||||
var existing = component.Storage.GetOrNew(materialId);
|
||||
existing += volume;
|
||||
|
||||
if (existing == 0)
|
||||
component.Storage.Remove(materialId);
|
||||
else
|
||||
component.Storage[materialId] = existing;
|
||||
|
||||
var ev = new MaterialAmountChangedEvent();
|
||||
RaiseLocalEvent(uid, ref ev);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Content.Shared.Bed.Sleep;
|
||||
using Content.Shared.Buckle.Components;
|
||||
using Content.Shared.CombatMode.Pacification;
|
||||
using Content.Shared.Damage.ForceSay;
|
||||
using Content.Shared.Emoting;
|
||||
@@ -10,15 +11,12 @@ using Content.Shared.Item;
|
||||
using Content.Shared.Mobs.Components;
|
||||
using Content.Shared.Movement.Events;
|
||||
using Content.Shared.Pointing;
|
||||
using Content.Shared.Projectiles;
|
||||
using Content.Shared.Pulling.Events;
|
||||
using Content.Shared.Speech;
|
||||
using Content.Shared.Standing;
|
||||
using Content.Shared.Strip.Components;
|
||||
using Content.Shared.Throwing;
|
||||
using Content.Shared.Weapons.Ranged.Components;
|
||||
using Robust.Shared.Physics.Components;
|
||||
using Robust.Shared.Physics.Events;
|
||||
|
||||
namespace Content.Shared.Mobs.Systems;
|
||||
|
||||
@@ -31,7 +29,7 @@ public partial class MobStateSystem
|
||||
SubscribeLocalEvent<MobStateComponent, ChangeDirectionAttemptEvent>(CheckAct);
|
||||
SubscribeLocalEvent<MobStateComponent, UseAttemptEvent>(CheckAct);
|
||||
SubscribeLocalEvent<MobStateComponent, AttackAttemptEvent>(CheckAct);
|
||||
SubscribeLocalEvent<MobStateComponent, ConsciousAttemptEvent>(CheckAct);
|
||||
SubscribeLocalEvent<MobStateComponent, ConsciousAttemptEvent>(CheckConcious);
|
||||
SubscribeLocalEvent<MobStateComponent, ThrowAttemptEvent>(CheckAct);
|
||||
SubscribeLocalEvent<MobStateComponent, SpeakAttemptEvent>(OnSpeakAttempt);
|
||||
SubscribeLocalEvent<MobStateComponent, IsEquippingAttemptEvent>(OnEquipAttempt);
|
||||
@@ -46,6 +44,27 @@ public partial class MobStateSystem
|
||||
SubscribeLocalEvent<MobStateComponent, TryingToSleepEvent>(OnSleepAttempt);
|
||||
SubscribeLocalEvent<MobStateComponent, CombatModeShouldHandInteractEvent>(OnCombatModeShouldHandInteract);
|
||||
SubscribeLocalEvent<MobStateComponent, AttemptPacifiedAttackEvent>(OnAttemptPacifiedAttack);
|
||||
|
||||
SubscribeLocalEvent<MobStateComponent, UnbuckleAttemptEvent>(OnUnbuckleAttempt);
|
||||
}
|
||||
|
||||
private void OnUnbuckleAttempt(Entity<MobStateComponent> ent, ref UnbuckleAttemptEvent args)
|
||||
{
|
||||
// TODO is this necessary?
|
||||
// Shouldn't the interaction have already been blocked by a general interaction check?
|
||||
if (args.User == ent.Owner && IsIncapacitated(ent))
|
||||
args.Cancelled = true;
|
||||
}
|
||||
|
||||
private void CheckConcious(Entity<MobStateComponent> ent, ref ConsciousAttemptEvent args)
|
||||
{
|
||||
switch (ent.Comp.CurrentState)
|
||||
{
|
||||
case MobState.Dead:
|
||||
case MobState.Critical:
|
||||
args.Cancelled = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnStateExitSubscribers(EntityUid target, MobStateComponent component, MobState state)
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
namespace Content.Shared.Movement.Pulling.Events;
|
||||
|
||||
public sealed class PullStartedMessage : PullMessage
|
||||
{
|
||||
public PullStartedMessage(EntityUid pullerUid, EntityUid pullableUid) :
|
||||
base(pullerUid, pullableUid)
|
||||
{
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// Event raised directed BOTH at the puller and pulled entity when a pull starts.
|
||||
/// </summary>
|
||||
public sealed class PullStartedMessage(EntityUid pullerUid, EntityUid pullableUid) : PullMessage(pullerUid, pullableUid);
|
||||
|
||||
@@ -1,13 +1,6 @@
|
||||
using Robust.Shared.Physics.Components;
|
||||
|
||||
namespace Content.Shared.Movement.Pulling.Events;
|
||||
namespace Content.Shared.Movement.Pulling.Events;
|
||||
|
||||
/// <summary>
|
||||
/// Raised directed on both puller and pullable.
|
||||
/// Event raised directed BOTH at the puller and pulled entity when a pull starts.
|
||||
/// </summary>
|
||||
public sealed class PullStoppedMessage : PullMessage
|
||||
{
|
||||
public PullStoppedMessage(EntityUid pullerUid, EntityUid pulledUid) : base(pullerUid, pulledUid)
|
||||
{
|
||||
}
|
||||
}
|
||||
public sealed class PullStoppedMessage(EntityUid pullerUid, EntityUid pulledUid) : PullMessage(pullerUid, pulledUid);
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
using System.Numerics;
|
||||
using Content.Shared.ActionBlocker;
|
||||
using Content.Shared.Administration.Logs;
|
||||
using Content.Shared.Alert;
|
||||
@@ -16,11 +15,9 @@ using Content.Shared.Movement.Systems;
|
||||
using Content.Shared.Popups;
|
||||
using Content.Shared.Pulling.Events;
|
||||
using Content.Shared.Standing;
|
||||
using Content.Shared.Throwing;
|
||||
using Content.Shared.Verbs;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.Input.Binding;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Physics;
|
||||
using Robust.Shared.Physics.Components;
|
||||
using Robust.Shared.Physics.Events;
|
||||
@@ -68,11 +65,26 @@ public sealed class PullingSystem : EntitySystem
|
||||
SubscribeLocalEvent<PullerComponent, RefreshMovementSpeedModifiersEvent>(OnRefreshMovespeed);
|
||||
SubscribeLocalEvent<PullerComponent, DropHandItemsEvent>(OnDropHandItems);
|
||||
|
||||
SubscribeLocalEvent<PullableComponent, StrappedEvent>(OnBuckled);
|
||||
SubscribeLocalEvent<PullableComponent, BuckledEvent>(OnGotBuckled);
|
||||
|
||||
CommandBinds.Builder
|
||||
.Bind(ContentKeyFunctions.ReleasePulledObject, InputCmdHandler.FromDelegate(OnReleasePulledObject, handle: false))
|
||||
.Register<PullingSystem>();
|
||||
}
|
||||
|
||||
private void OnBuckled(Entity<PullableComponent> ent, ref StrappedEvent args)
|
||||
{
|
||||
// Prevent people from pulling the entity they are buckled to
|
||||
if (ent.Comp.Puller == args.Buckle.Owner && !args.Buckle.Comp.PullStrap)
|
||||
StopPulling(ent, ent);
|
||||
}
|
||||
|
||||
private void OnGotBuckled(Entity<PullableComponent> ent, ref BuckledEvent args)
|
||||
{
|
||||
StopPulling(ent, ent);
|
||||
}
|
||||
|
||||
private void OnAfterState(Entity<PullerComponent> ent, ref AfterAutoHandleStateEvent args)
|
||||
{
|
||||
if (ent.Comp.Pulling == null)
|
||||
@@ -94,7 +106,8 @@ public sealed class PullingSystem : EntitySystem
|
||||
|
||||
private void OnPullerContainerInsert(Entity<PullerComponent> ent, ref EntGotInsertedIntoContainerMessage args)
|
||||
{
|
||||
if (ent.Comp.Pulling == null) return;
|
||||
if (ent.Comp.Pulling == null)
|
||||
return;
|
||||
|
||||
if (!TryComp(ent.Comp.Pulling.Value, out PullableComponent? pulling))
|
||||
return;
|
||||
@@ -228,8 +241,18 @@ public sealed class PullingSystem : EntitySystem
|
||||
/// </summary>
|
||||
private void StopPulling(EntityUid pullableUid, PullableComponent pullableComp)
|
||||
{
|
||||
if (pullableComp.Puller == null)
|
||||
return;
|
||||
|
||||
if (!_timing.ApplyingState)
|
||||
{
|
||||
// Joint shutdown
|
||||
if (pullableComp.PullJointId != null)
|
||||
{
|
||||
_joints.RemoveJoint(pullableUid, pullableComp.PullJointId);
|
||||
pullableComp.PullJointId = null;
|
||||
}
|
||||
|
||||
if (TryComp<PhysicsComponent>(pullableUid, out var pullablePhysics))
|
||||
{
|
||||
_physics.SetFixedRotation(pullableUid, pullableComp.PrevFixedRotation, body: pullablePhysics);
|
||||
@@ -330,15 +353,6 @@ public sealed class PullingSystem : EntitySystem
|
||||
return false;
|
||||
}
|
||||
|
||||
if (EntityManager.TryGetComponent(puller, out BuckleComponent? buckle))
|
||||
{
|
||||
// Prevent people pulling the chair they're on, etc.
|
||||
if (buckle is { PullStrap: false, Buckled: true } && (buckle.LastEntityBuckledTo == pullableUid))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
var getPulled = new BeingPulledAttemptEvent(puller, pullableUid);
|
||||
RaiseLocalEvent(pullableUid, getPulled, true);
|
||||
var startPull = new StartPullAttemptEvent(puller, pullableUid);
|
||||
@@ -382,11 +396,8 @@ public sealed class PullingSystem : EntitySystem
|
||||
if (!CanPull(pullerUid, pullableUid))
|
||||
return false;
|
||||
|
||||
if (!EntityManager.TryGetComponent<PhysicsComponent>(pullerUid, out var pullerPhysics) ||
|
||||
!EntityManager.TryGetComponent<PhysicsComponent>(pullableUid, out var pullablePhysics))
|
||||
{
|
||||
if (!HasComp<PhysicsComponent>(pullerUid) || !TryComp(pullableUid, out PhysicsComponent? pullablePhysics))
|
||||
return false;
|
||||
}
|
||||
|
||||
// Ensure that the puller is not currently pulling anything.
|
||||
if (TryComp<PullableComponent>(pullerComp.Pulling, out var oldPullable)
|
||||
@@ -431,7 +442,7 @@ public sealed class PullingSystem : EntitySystem
|
||||
{
|
||||
// Joint startup
|
||||
var union = _physics.GetHardAABB(pullerUid).Union(_physics.GetHardAABB(pullableUid, body: pullablePhysics));
|
||||
var length = Math.Max((float) union.Size.X, (float) union.Size.Y) * 0.75f;
|
||||
var length = Math.Max(union.Size.X, union.Size.Y) * 0.75f;
|
||||
|
||||
var joint = _joints.CreateDistanceJoint(pullableUid, pullerUid, id: pullableComp.PullJointId);
|
||||
joint.CollideConnected = false;
|
||||
@@ -475,17 +486,6 @@ public sealed class PullingSystem : EntitySystem
|
||||
if (msg.Cancelled)
|
||||
return false;
|
||||
|
||||
// Stop pulling confirmed!
|
||||
if (!_timing.ApplyingState)
|
||||
{
|
||||
// Joint shutdown
|
||||
if (pullable.PullJointId != null)
|
||||
{
|
||||
_joints.RemoveJoint(pullableUid, pullable.PullJointId);
|
||||
pullable.PullJointId = null;
|
||||
}
|
||||
}
|
||||
|
||||
StopPulling(pullableUid, pullable);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ public abstract class SharedWaddleAnimationSystem : EntitySystem
|
||||
// Stop moving possibilities
|
||||
SubscribeLocalEvent((Entity<WaddleAnimationComponent> ent, ref StunnedEvent _) => StopWaddling(ent));
|
||||
SubscribeLocalEvent((Entity<WaddleAnimationComponent> ent, ref DownedEvent _) => StopWaddling(ent));
|
||||
SubscribeLocalEvent((Entity<WaddleAnimationComponent> ent, ref BuckleChangeEvent _) => StopWaddling(ent));
|
||||
SubscribeLocalEvent((Entity<WaddleAnimationComponent> ent, ref BuckledEvent _) => StopWaddling(ent));
|
||||
SubscribeLocalEvent<WaddleAnimationComponent, GravityChangedEvent>(OnGravityChanged);
|
||||
}
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ public sealed partial class StampComponent : Component
|
||||
public string StampedName { get; set; } = "stamp-component-stamped-name-default";
|
||||
|
||||
/// <summary>
|
||||
/// Tne sprite state of the stamp to display on the paper from paper Sprite path.
|
||||
/// The sprite state of the stamp to display on the paper from paper Sprite path.
|
||||
/// </summary>
|
||||
[DataField("stampState")]
|
||||
public string StampState { get; set; } = "paper_stamp-generic";
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
using System.Numerics;
|
||||
using Content.Shared.Conveyor;
|
||||
using Content.Shared.Gravity;
|
||||
using Content.Shared.Magic;
|
||||
using Content.Shared.Movement.Systems;
|
||||
using Robust.Shared.Collections;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Map.Components;
|
||||
using Robust.Shared.Physics;
|
||||
@@ -9,6 +11,7 @@ using Robust.Shared.Physics.Components;
|
||||
using Robust.Shared.Physics.Controllers;
|
||||
using Robust.Shared.Physics.Events;
|
||||
using Robust.Shared.Physics.Systems;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Shared.Physics.Controllers;
|
||||
|
||||
@@ -16,15 +19,23 @@ public abstract class SharedConveyorController : VirtualController
|
||||
{
|
||||
[Dependency] protected readonly IMapManager MapManager = default!;
|
||||
[Dependency] protected readonly EntityLookupSystem Lookup = default!;
|
||||
[Dependency] private readonly SharedMapSystem _maps = default!;
|
||||
[Dependency] protected readonly SharedPhysicsSystem Physics = default!;
|
||||
[Dependency] private readonly SharedGravitySystem _gravity = default!;
|
||||
|
||||
protected const string ConveyorFixture = "conveyor";
|
||||
|
||||
private static readonly Vector2 _expansion = new Vector2(0.1f, 0.1f);
|
||||
private EntityQuery<MapGridComponent> _gridQuery;
|
||||
private EntityQuery<TransformComponent> _xformQuery;
|
||||
|
||||
private ValueList<EntityUid> _ents = new();
|
||||
private HashSet<Entity<ConveyorComponent>> _conveyors = new();
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
_gridQuery = GetEntityQuery<MapGridComponent>();
|
||||
_xformQuery = GetEntityQuery<TransformComponent>();
|
||||
|
||||
UpdatesAfter.Add(typeof(SharedMoverController));
|
||||
|
||||
SubscribeLocalEvent<ConveyorComponent, StartCollideEvent>(OnConveyorStartCollide);
|
||||
@@ -37,74 +48,125 @@ public abstract class SharedConveyorController : VirtualController
|
||||
{
|
||||
var otherUid = args.OtherEntity;
|
||||
|
||||
if (args.OtherBody.BodyType == BodyType.Static || component.State == ConveyorState.Off)
|
||||
if (!args.OtherFixture.Hard || args.OtherBody.BodyType == BodyType.Static || component.State == ConveyorState.Off)
|
||||
return;
|
||||
|
||||
component.Intersecting.Add(otherUid);
|
||||
EnsureComp<ActiveConveyorComponent>(uid);
|
||||
var conveyed = EnsureComp<ConveyedComponent>(otherUid);
|
||||
|
||||
if (conveyed.Colliding.Contains(uid))
|
||||
return;
|
||||
|
||||
conveyed.Colliding.Add(uid);
|
||||
Dirty(otherUid, conveyed);
|
||||
}
|
||||
|
||||
private void OnConveyorEndCollide(EntityUid uid, ConveyorComponent component, ref EndCollideEvent args)
|
||||
private void OnConveyorEndCollide(Entity<ConveyorComponent> ent, ref EndCollideEvent args)
|
||||
{
|
||||
component.Intersecting.Remove(args.OtherEntity);
|
||||
if (!TryComp(args.OtherEntity, out ConveyedComponent? conveyed))
|
||||
return;
|
||||
|
||||
if (component.Intersecting.Count == 0)
|
||||
RemComp<ActiveConveyorComponent>(uid);
|
||||
if (!conveyed.Colliding.Remove(ent.Owner))
|
||||
return;
|
||||
|
||||
Dirty(args.OtherEntity, conveyed);
|
||||
}
|
||||
|
||||
public override void UpdateBeforeSolve(bool prediction, float frameTime)
|
||||
{
|
||||
base.UpdateBeforeSolve(prediction, frameTime);
|
||||
|
||||
var conveyed = new HashSet<EntityUid>();
|
||||
// Don't use it directly in EntityQuery because we may be able to save getcomponents.
|
||||
var xformQuery = GetEntityQuery<TransformComponent>();
|
||||
var bodyQuery = GetEntityQuery<PhysicsComponent>();
|
||||
var query = EntityQueryEnumerator<ActiveConveyorComponent, ConveyorComponent>();
|
||||
var query = EntityQueryEnumerator<ConveyedComponent, TransformComponent, PhysicsComponent>();
|
||||
_ents.Clear();
|
||||
|
||||
while (query.MoveNext(out var uid, out var _, out var comp))
|
||||
while (query.MoveNext(out var uid, out var comp, out var xform, out var physics))
|
||||
{
|
||||
Convey(uid, comp, xformQuery, bodyQuery, conveyed, frameTime, prediction);
|
||||
if (TryConvey((uid, comp, physics, xform), prediction, frameTime))
|
||||
continue;
|
||||
|
||||
_ents.Add(uid);
|
||||
}
|
||||
|
||||
foreach (var ent in _ents)
|
||||
{
|
||||
RemComp<ConveyedComponent>(ent);
|
||||
}
|
||||
}
|
||||
|
||||
private void Convey(EntityUid uid, ConveyorComponent comp, EntityQuery<TransformComponent> xformQuery, EntityQuery<PhysicsComponent> bodyQuery, HashSet<EntityUid> conveyed, float frameTime, bool prediction)
|
||||
private bool TryConvey(Entity<ConveyedComponent, PhysicsComponent, TransformComponent> entity, bool prediction, float frameTime)
|
||||
{
|
||||
// Use an event for conveyors to know what needs to run
|
||||
if (!CanRun(comp))
|
||||
return;
|
||||
var physics = entity.Comp2;
|
||||
var xform = entity.Comp3;
|
||||
var contacting = entity.Comp1.Colliding.Count > 0;
|
||||
|
||||
var speed = comp.Speed;
|
||||
if (!contacting)
|
||||
return false;
|
||||
|
||||
if (speed <= 0f || !xformQuery.TryGetComponent(uid, out var xform) || xform.GridUid == null)
|
||||
return;
|
||||
// Client moment
|
||||
if (!physics.Predict && prediction)
|
||||
return true;
|
||||
|
||||
var conveyorPos = xform.LocalPosition;
|
||||
var conveyorRot = xform.LocalRotation;
|
||||
if (physics.BodyType == BodyType.Static)
|
||||
return false;
|
||||
|
||||
conveyorRot += comp.Angle;
|
||||
if (!_gridQuery.TryComp(xform.GridUid, out var grid))
|
||||
return true;
|
||||
|
||||
var gridTile = _maps.TileIndicesFor(xform.GridUid.Value, grid, xform.Coordinates);
|
||||
_conveyors.Clear();
|
||||
|
||||
// Check for any conveyors on the attached tile.
|
||||
Lookup.GetLocalEntitiesIntersecting(xform.GridUid.Value, gridTile, _conveyors);
|
||||
DebugTools.Assert(_conveyors.Count <= 1);
|
||||
|
||||
// No more conveyors.
|
||||
if (_conveyors.Count == 0)
|
||||
return true;
|
||||
|
||||
if (physics.BodyStatus == BodyStatus.InAir ||
|
||||
_gravity.IsWeightless(entity, physics, xform))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
Entity<ConveyorComponent> bestConveyor = default;
|
||||
var bestSpeed = 0f;
|
||||
|
||||
foreach (var conveyor in _conveyors)
|
||||
{
|
||||
if (conveyor.Comp.Speed > bestSpeed && CanRun(conveyor))
|
||||
{
|
||||
bestSpeed = conveyor.Comp.Speed;
|
||||
bestConveyor = conveyor;
|
||||
}
|
||||
}
|
||||
|
||||
if (bestSpeed == 0f || bestConveyor == default)
|
||||
return true;
|
||||
|
||||
var comp = bestConveyor.Comp!;
|
||||
var conveyorXform = _xformQuery.GetComponent(bestConveyor.Owner);
|
||||
var conveyorPos = conveyorXform.LocalPosition;
|
||||
var conveyorRot = conveyorXform.LocalRotation;
|
||||
|
||||
conveyorRot += bestConveyor.Comp!.Angle;
|
||||
|
||||
if (comp.State == ConveyorState.Reverse)
|
||||
conveyorRot += MathF.PI;
|
||||
|
||||
var direction = conveyorRot.ToWorldVec();
|
||||
|
||||
foreach (var (entity, transform, body) in GetEntitiesToMove(comp, xform, xformQuery, bodyQuery))
|
||||
{
|
||||
if (!conveyed.Add(entity) || prediction && !body.Predict)
|
||||
continue;
|
||||
var localPos = xform.LocalPosition;
|
||||
var itemRelative = conveyorPos - localPos;
|
||||
|
||||
var localPos = transform.LocalPosition;
|
||||
var itemRelative = conveyorPos - localPos;
|
||||
localPos += Convey(direction, bestSpeed, frameTime, itemRelative);
|
||||
|
||||
localPos += Convey(direction, speed, frameTime, itemRelative);
|
||||
transform.LocalPosition = localPos;
|
||||
TransformSystem.SetLocalPosition(entity, localPos, xform);
|
||||
|
||||
// Force it awake for collisionwake reasons.
|
||||
Physics.SetAwake((entity, body), true);
|
||||
Physics.SetSleepTime(body, 0f);
|
||||
}
|
||||
Dirty(uid, comp);
|
||||
// Force it awake for collisionwake reasons.
|
||||
Physics.SetAwake((entity, physics), true);
|
||||
Physics.SetSleepTime(physics, 0f);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static Vector2 Convey(Vector2 direction, float speed, float frameTime, Vector2 itemRelative)
|
||||
@@ -140,36 +202,6 @@ public abstract class SharedConveyorController : VirtualController
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerable<(EntityUid, TransformComponent, PhysicsComponent)> GetEntitiesToMove(
|
||||
ConveyorComponent comp,
|
||||
TransformComponent xform,
|
||||
EntityQuery<TransformComponent> xformQuery,
|
||||
EntityQuery<PhysicsComponent> bodyQuery)
|
||||
{
|
||||
// Check if the thing's centre overlaps the grid tile.
|
||||
var grid = Comp<MapGridComponent>(xform.GridUid!.Value);
|
||||
var tile = grid.GetTileRef(xform.Coordinates);
|
||||
var conveyorBounds = Lookup.GetLocalBounds(tile, grid.TileSize);
|
||||
|
||||
foreach (var entity in comp.Intersecting)
|
||||
{
|
||||
if (!xformQuery.TryGetComponent(entity, out var entityXform) || entityXform.ParentUid != xform.GridUid!.Value)
|
||||
continue;
|
||||
|
||||
if (!bodyQuery.TryGetComponent(entity, out var physics) || physics.BodyType == BodyType.Static || physics.BodyStatus == BodyStatus.InAir || _gravity.IsWeightless(entity, physics, entityXform))
|
||||
continue;
|
||||
|
||||
// Yes there's still going to be the occasional rounding issue where it stops getting conveyed
|
||||
// When you fix the corner issue that will fix this anyway.
|
||||
var gridAABB = new Box2(entityXform.LocalPosition - _expansion, entityXform.LocalPosition + _expansion);
|
||||
|
||||
if (!conveyorBounds.Intersects(gridAABB))
|
||||
continue;
|
||||
|
||||
yield return (entity, entityXform, physics);
|
||||
}
|
||||
}
|
||||
|
||||
public bool CanRun(ConveyorComponent component)
|
||||
{
|
||||
return component.State != ConveyorState.Off && component.Powered;
|
||||
|
||||
@@ -29,6 +29,12 @@ public sealed partial class LoadoutGroupPrototype : IPrototype
|
||||
[DataField]
|
||||
public int MaxLimit = 1;
|
||||
|
||||
/// <summary>
|
||||
/// Hides the loadout group from the player.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public bool Hidden;
|
||||
|
||||
[DataField(required: true)]
|
||||
public List<ProtoId<LoadoutPrototype>> Loadouts = new();
|
||||
}
|
||||
|
||||
@@ -193,9 +193,14 @@ public sealed partial class RoleLoadout : IEquatable<RoleLoadout>
|
||||
if (groupProto.MinLimit > 0)
|
||||
{
|
||||
// Apply any loadouts we can.
|
||||
for (var j = 0; j < Math.Min(groupProto.MinLimit, groupProto.Loadouts.Count); j++)
|
||||
var addedCount = 0;
|
||||
foreach (var protoId in groupProto.Loadouts)
|
||||
{
|
||||
if (!protoManager.TryIndex(groupProto.Loadouts[j], out var loadoutProto))
|
||||
// Reached the limit, time to stop
|
||||
if (addedCount >= groupProto.MinLimit)
|
||||
break;
|
||||
|
||||
if (!protoManager.TryIndex(protoId, out var loadoutProto))
|
||||
continue;
|
||||
|
||||
var defaultLoadout = new Loadout()
|
||||
@@ -209,6 +214,7 @@ public sealed partial class RoleLoadout : IEquatable<RoleLoadout>
|
||||
|
||||
loadouts.Add(defaultLoadout);
|
||||
Apply(loadoutProto);
|
||||
addedCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,32 +9,32 @@ public sealed partial class PryingComponent : Component
|
||||
/// <summary>
|
||||
/// Whether the entity can pry open powered doors
|
||||
/// </summary>
|
||||
[DataField("pryPowered")]
|
||||
public bool PryPowered = false;
|
||||
[DataField]
|
||||
public bool PryPowered;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the tool can bypass certain restrictions when prying.
|
||||
/// For example door bolts.
|
||||
/// </summary>
|
||||
[DataField("force")]
|
||||
public bool Force = false;
|
||||
[DataField]
|
||||
public bool Force;
|
||||
/// <summary>
|
||||
/// Modifier on the prying time.
|
||||
/// Lower values result in more time.
|
||||
/// </summary>
|
||||
[DataField("speedModifier")]
|
||||
[DataField]
|
||||
public float SpeedModifier = 1.0f;
|
||||
|
||||
/// <summary>
|
||||
/// What sound to play when prying is finished.
|
||||
/// </summary>
|
||||
[DataField("useSound")]
|
||||
[DataField]
|
||||
public SoundSpecifier UseSound = new SoundPathSpecifier("/Audio/Items/crowbar.ogg");
|
||||
|
||||
/// <summary>
|
||||
/// Whether the entity can currently pry things.
|
||||
/// </summary>
|
||||
[DataField("enabled")]
|
||||
[DataField]
|
||||
public bool Enabled = true;
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ public sealed class PryingSystem : EntitySystem
|
||||
[Dependency] private readonly ISharedAdminLogManager _adminLog = default!;
|
||||
[Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!;
|
||||
[Dependency] private readonly SharedAudioSystem _audioSystem = default!;
|
||||
[Dependency] private readonly SharedPopupSystem Popup = default!;
|
||||
[Dependency] private readonly SharedPopupSystem _popup = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
@@ -46,7 +46,7 @@ public sealed class PryingSystem : EntitySystem
|
||||
if (!args.CanInteract || !args.CanAccess)
|
||||
return;
|
||||
|
||||
if (!TryComp<PryingComponent>(args.User, out var tool))
|
||||
if (!TryComp<PryingComponent>(args.User, out _))
|
||||
return;
|
||||
|
||||
args.Verbs.Add(new AlternativeVerb()
|
||||
@@ -74,7 +74,7 @@ public sealed class PryingSystem : EntitySystem
|
||||
if (!CanPry(target, user, out var message, comp))
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(message))
|
||||
Popup.PopupClient(Loc.GetString(message), target, user);
|
||||
_popup.PopupClient(Loc.GetString(message), target, user);
|
||||
// If we have reached this point we want the event that caused this
|
||||
// to be marked as handled.
|
||||
return true;
|
||||
@@ -138,9 +138,10 @@ public sealed class PryingSystem : EntitySystem
|
||||
{
|
||||
BreakOnDamage = true,
|
||||
BreakOnMove = true,
|
||||
NeedHand = tool != user,
|
||||
};
|
||||
|
||||
if (tool != null)
|
||||
if (tool != user && tool != null)
|
||||
{
|
||||
_adminLog.Add(LogType.Action, LogImpact.Low, $"{ToPrettyString(user)} is using {ToPrettyString(tool.Value)} to pry {ToPrettyString(target)}");
|
||||
}
|
||||
@@ -163,7 +164,7 @@ public sealed class PryingSystem : EntitySystem
|
||||
if (!CanPry(uid, args.User, out var message, comp))
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(message))
|
||||
Popup.PopupClient(Loc.GetString(message), uid, args.User);
|
||||
_popup.PopupClient(Loc.GetString(message), uid, args.User);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -178,6 +179,4 @@ public sealed class PryingSystem : EntitySystem
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public sealed partial class DoorPryDoAfterEvent : SimpleDoAfterEvent
|
||||
{
|
||||
}
|
||||
public sealed partial class DoorPryDoAfterEvent : SimpleDoAfterEvent;
|
||||
|
||||
@@ -7,6 +7,7 @@ using Content.Shared.Movement.Events;
|
||||
|
||||
namespace Content.Shared.Puppet;
|
||||
|
||||
// TODO deduplicate with BlockMovementComponent
|
||||
public abstract class SharedVentriloquistPuppetSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly ActionBlockerSystem _blocker = default!;
|
||||
@@ -15,7 +16,7 @@ public abstract class SharedVentriloquistPuppetSystem : EntitySystem
|
||||
{
|
||||
base.Initialize();
|
||||
SubscribeLocalEvent<VentriloquistPuppetComponent, UseAttemptEvent>(Cancel);
|
||||
SubscribeLocalEvent<VentriloquistPuppetComponent, InteractionAttemptEvent>(Cancel);
|
||||
SubscribeLocalEvent<VentriloquistPuppetComponent, InteractionAttemptEvent>(CancelInteract);
|
||||
SubscribeLocalEvent<VentriloquistPuppetComponent, DropAttemptEvent>(Cancel);
|
||||
SubscribeLocalEvent<VentriloquistPuppetComponent, PickupAttemptEvent>(Cancel);
|
||||
SubscribeLocalEvent<VentriloquistPuppetComponent, UpdateCanMoveEvent>(Cancel);
|
||||
@@ -24,6 +25,11 @@ public abstract class SharedVentriloquistPuppetSystem : EntitySystem
|
||||
SubscribeLocalEvent<VentriloquistPuppetComponent, ComponentStartup>(OnStartup);
|
||||
}
|
||||
|
||||
private void CancelInteract(Entity<VentriloquistPuppetComponent> ent, ref InteractionAttemptEvent args)
|
||||
{
|
||||
args.Cancelled = true;
|
||||
}
|
||||
|
||||
private void OnStartup(EntityUid uid, VentriloquistPuppetComponent component, ComponentStartup args)
|
||||
{
|
||||
_blocker.UpdateCanMove(uid);
|
||||
@@ -33,4 +39,4 @@ public abstract class SharedVentriloquistPuppetSystem : EntitySystem
|
||||
{
|
||||
args.Cancel();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
using Content.Shared.Radio.EntitySystems;
|
||||
using Robust.Shared.GameStates;
|
||||
|
||||
namespace Content.Shared.Radio.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Prevents all radio in range from sending messages
|
||||
/// </summary>
|
||||
[RegisterComponent, NetworkedComponent]
|
||||
[Access(typeof(SharedJammerSystem))]
|
||||
public sealed partial class ActiveRadioJammerComponent : Component
|
||||
{
|
||||
}
|
||||
@@ -1,13 +1,14 @@
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.GameStates;
|
||||
|
||||
namespace Content.Shared.RadioJammer;
|
||||
namespace Content.Shared.Radio.Components;
|
||||
|
||||
/// <summary>
|
||||
/// When activated (<see cref="ActiveRadioJammerComponent"/>) prevents from sending messages in range
|
||||
/// Suit sensors will also stop working.
|
||||
/// </summary>
|
||||
[NetworkedComponent, RegisterComponent]
|
||||
[AutoGenerateComponentState]
|
||||
public sealed partial class RadioJammerComponent : Component
|
||||
{
|
||||
[DataDefinition]
|
||||
@@ -26,7 +27,7 @@ public sealed partial class RadioJammerComponent : Component
|
||||
public float Range;
|
||||
|
||||
/// <summary>
|
||||
/// The message that is displayed when switched
|
||||
/// The message that is displayed when switched.
|
||||
/// to this setting.
|
||||
/// </summary>
|
||||
[DataField(required: true)]
|
||||
@@ -49,6 +50,7 @@ public sealed partial class RadioJammerComponent : Component
|
||||
/// Index of the currently selected setting.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
[AutoNetworkedField]
|
||||
public int SelectedPowerLevel = 1;
|
||||
}
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
using Content.Shared.Popups;
|
||||
using Content.Shared.DeviceNetwork.Components;
|
||||
using Content.Shared.Verbs;
|
||||
using Content.Shared.RadioJammer;
|
||||
using Content.Shared.Examine;
|
||||
using Content.Shared.Radio.Components;
|
||||
using Content.Shared.DeviceNetwork.Systems;
|
||||
|
||||
namespace Content.Shared.Radio.EntitySystems;
|
||||
|
||||
public abstract class SharedJammerSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
|
||||
[Dependency] private readonly SharedDeviceNetworkJammerSystem _jammer = default!;
|
||||
[Dependency] protected readonly SharedPopupSystem Popup = default!;
|
||||
|
||||
public override void Initialize()
|
||||
@@ -15,6 +17,7 @@ public abstract class SharedJammerSystem : EntitySystem
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<RadioJammerComponent, GetVerbsEvent<Verb>>(OnGetVerb);
|
||||
SubscribeLocalEvent<RadioJammerComponent, ExaminedEvent>(OnExamine);
|
||||
}
|
||||
|
||||
private void OnGetVerb(Entity<RadioJammerComponent> entity, ref GetVerbsEvent<Verb> args)
|
||||
@@ -38,13 +41,11 @@ public abstract class SharedJammerSystem : EntitySystem
|
||||
Act = () =>
|
||||
{
|
||||
entity.Comp.SelectedPowerLevel = currIndex;
|
||||
if (TryComp<DeviceNetworkJammerComponent>(entity.Owner, out var jammerComp))
|
||||
Dirty(entity);
|
||||
if (_jammer.TrySetRange(entity.Owner, GetCurrentRange(entity)))
|
||||
{
|
||||
// This is a little sketcy but only way to do it.
|
||||
jammerComp.Range = GetCurrentRange(entity.Comp);
|
||||
Dirty(entity.Owner, jammerComp);
|
||||
Popup.PopupPredicted(Loc.GetString(setting.Message), user, user);
|
||||
}
|
||||
Popup.PopupPredicted(Loc.GetString(setting.Message), user, user);
|
||||
},
|
||||
Text = Loc.GetString(setting.Name),
|
||||
};
|
||||
@@ -53,26 +54,39 @@ public abstract class SharedJammerSystem : EntitySystem
|
||||
}
|
||||
}
|
||||
|
||||
public float GetCurrentWattage(RadioJammerComponent jammer)
|
||||
private void OnExamine(Entity<RadioJammerComponent> ent, ref ExaminedEvent args)
|
||||
{
|
||||
return jammer.Settings[jammer.SelectedPowerLevel].Wattage;
|
||||
if (args.IsInDetailsRange)
|
||||
{
|
||||
var powerIndicator = HasComp<ActiveRadioJammerComponent>(ent)
|
||||
? Loc.GetString("radio-jammer-component-examine-on-state")
|
||||
: Loc.GetString("radio-jammer-component-examine-off-state");
|
||||
args.PushMarkup(powerIndicator);
|
||||
|
||||
var powerLevel = Loc.GetString(ent.Comp.Settings[ent.Comp.SelectedPowerLevel].Name);
|
||||
var switchIndicator = Loc.GetString("radio-jammer-component-switch-setting", ("powerLevel", powerLevel));
|
||||
args.PushMarkup(switchIndicator);
|
||||
}
|
||||
}
|
||||
|
||||
public float GetCurrentRange(RadioJammerComponent jammer)
|
||||
public float GetCurrentWattage(Entity<RadioJammerComponent> jammer)
|
||||
{
|
||||
return jammer.Settings[jammer.SelectedPowerLevel].Range;
|
||||
return jammer.Comp.Settings[jammer.Comp.SelectedPowerLevel].Wattage;
|
||||
}
|
||||
|
||||
protected void ChangeLEDState(bool isLEDOn, EntityUid uid,
|
||||
AppearanceComponent? appearance = null)
|
||||
public float GetCurrentRange(Entity<RadioJammerComponent> jammer)
|
||||
{
|
||||
_appearance.SetData(uid, RadioJammerVisuals.LEDOn, isLEDOn, appearance);
|
||||
return jammer.Comp.Settings[jammer.Comp.SelectedPowerLevel].Range;
|
||||
}
|
||||
|
||||
protected void ChangeChargeLevel(RadioJammerChargeLevel chargeLevel, EntityUid uid,
|
||||
AppearanceComponent? appearance = null)
|
||||
protected void ChangeLEDState(Entity<AppearanceComponent?> ent, bool isLEDOn)
|
||||
{
|
||||
_appearance.SetData(uid, RadioJammerVisuals.ChargeLevel, chargeLevel, appearance);
|
||||
_appearance.SetData(ent, RadioJammerVisuals.LEDOn, isLEDOn, ent.Comp);
|
||||
}
|
||||
|
||||
protected void ChangeChargeLevel(Entity<AppearanceComponent?> ent, RadioJammerChargeLevel chargeLevel)
|
||||
{
|
||||
_appearance.SetData(ent, RadioJammerVisuals.ChargeLevel, chargeLevel, ent.Comp);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -280,4 +280,23 @@ public abstract class SharedResearchSystem : EntitySystem
|
||||
comp.UnlockedTechnologies.Clear();
|
||||
Dirty(uid, comp);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a lathe recipe to the specified technology database
|
||||
/// without checking if it can be unlocked.
|
||||
/// </summary>
|
||||
public void AddLatheRecipe(EntityUid uid, string recipe, TechnologyDatabaseComponent? component = null)
|
||||
{
|
||||
if (!Resolve(uid, ref component))
|
||||
return;
|
||||
|
||||
if (component.UnlockedRecipes.Contains(recipe))
|
||||
return;
|
||||
|
||||
component.UnlockedRecipes.Add(recipe);
|
||||
Dirty(uid, component);
|
||||
|
||||
var ev = new TechnologyDatabaseModifiedEvent();
|
||||
RaiseLocalEvent(uid, ref ev);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
using Content.Shared.Random;
|
||||
using Content.Shared.Research.Prototypes;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Shared.Research.TechnologyDisk.Components;
|
||||
|
||||
[RegisterComponent, NetworkedComponent]
|
||||
[AutoGenerateComponentState]
|
||||
public sealed partial class TechnologyDiskComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// The recipe that will be added. If null, one will be randomly generated
|
||||
/// </summary>
|
||||
[DataField]
|
||||
[AutoNetworkedField]
|
||||
public List<ProtoId<LatheRecipePrototype>>? Recipes;
|
||||
|
||||
/// <summary>
|
||||
/// A weighted random prototype for how rare each tier should be.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public ProtoId<WeightedRandomPrototype> TierWeightPrototype = "TechDiskTierWeights";
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
using Content.Shared.Examine;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Popups;
|
||||
using Content.Shared.Random.Helpers;
|
||||
using Content.Shared.Research.Components;
|
||||
using Content.Shared.Research.Prototypes;
|
||||
using Content.Shared.Research.Systems;
|
||||
using Content.Shared.Research.TechnologyDisk.Components;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Random;
|
||||
|
||||
namespace Content.Shared.Research.TechnologyDisk.Systems;
|
||||
|
||||
public sealed class TechnologyDiskSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly INetManager _net = default!;
|
||||
[Dependency] private readonly IPrototypeManager _protoMan = default!;
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
[Dependency] private readonly SharedPopupSystem _popup = default!;
|
||||
[Dependency] private readonly SharedResearchSystem _research = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<TechnologyDiskComponent, MapInitEvent>(OnMapInit);
|
||||
SubscribeLocalEvent<TechnologyDiskComponent, AfterInteractEvent>(OnAfterInteract);
|
||||
SubscribeLocalEvent<TechnologyDiskComponent, ExaminedEvent>(OnExamine);
|
||||
}
|
||||
|
||||
private void OnMapInit(Entity<TechnologyDiskComponent> ent, ref MapInitEvent args)
|
||||
{
|
||||
if (ent.Comp.Recipes != null)
|
||||
return;
|
||||
|
||||
var weightedRandom = _protoMan.Index(ent.Comp.TierWeightPrototype);
|
||||
var tier = int.Parse(weightedRandom.Pick(_random));
|
||||
|
||||
//get a list of every distinct recipe in all the technologies.
|
||||
var techs = new HashSet<ProtoId<LatheRecipePrototype>>();
|
||||
foreach (var tech in _protoMan.EnumeratePrototypes<TechnologyPrototype>())
|
||||
{
|
||||
if (tech.Tier != tier)
|
||||
continue;
|
||||
|
||||
techs.UnionWith(tech.RecipeUnlocks);
|
||||
}
|
||||
|
||||
if (techs.Count == 0)
|
||||
return;
|
||||
|
||||
//pick one
|
||||
ent.Comp.Recipes = [];
|
||||
ent.Comp.Recipes.Add(_random.Pick(techs));
|
||||
Dirty(ent);
|
||||
}
|
||||
|
||||
private void OnAfterInteract(Entity<TechnologyDiskComponent> ent, ref AfterInteractEvent args)
|
||||
{
|
||||
if (args.Handled || !args.CanReach || args.Target is not { } target)
|
||||
return;
|
||||
|
||||
if (!HasComp<ResearchServerComponent>(target) || !TryComp<TechnologyDatabaseComponent>(target, out var database))
|
||||
return;
|
||||
|
||||
if (ent.Comp.Recipes != null)
|
||||
{
|
||||
foreach (var recipe in ent.Comp.Recipes)
|
||||
{
|
||||
_research.AddLatheRecipe(target, recipe, database);
|
||||
}
|
||||
}
|
||||
_popup.PopupClient(Loc.GetString("tech-disk-inserted"), target, args.User);
|
||||
if (_net.IsServer)
|
||||
QueueDel(ent);
|
||||
args.Handled = true;
|
||||
}
|
||||
|
||||
private void OnExamine(Entity<TechnologyDiskComponent> ent, ref ExaminedEvent args)
|
||||
{
|
||||
var message = Loc.GetString("tech-disk-examine-none");
|
||||
if (ent.Comp.Recipes != null && ent.Comp.Recipes.Count > 0)
|
||||
{
|
||||
var prototype = _protoMan.Index(ent.Comp.Recipes[0]);
|
||||
var resultPrototype = _protoMan.Index<EntityPrototype>(prototype.Result);
|
||||
message = Loc.GetString("tech-disk-examine", ("result", resultPrototype.Name));
|
||||
|
||||
if (ent.Comp.Recipes.Count > 1) //idk how to do this well. sue me.
|
||||
message += " " + Loc.GetString("tech-disk-examine-more");
|
||||
}
|
||||
args.PushMarkup(message);
|
||||
}
|
||||
}
|
||||
@@ -69,6 +69,13 @@ public sealed partial class ContainmentFieldGeneratorComponent : Component
|
||||
[DataField("idTag", customTypeSerializer: typeof(PrototypeIdSerializer<TagPrototype>))]
|
||||
public string IDTag = "EmitterBolt";
|
||||
|
||||
/// <summary>
|
||||
/// Which fixture ID should test collision with from the entity that powers the generator?
|
||||
/// Prevents the generator from being powered by fly-by fixtures.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public string SourceFixtureId = "projectile";
|
||||
|
||||
/// <summary>
|
||||
/// Is the generator toggled on?
|
||||
/// </summary>
|
||||
|
||||
@@ -1391,7 +1391,12 @@ public abstract class SharedStorageSystem : EntitySystem
|
||||
|
||||
// If we specify a max item size, use that
|
||||
if (uid.Comp.MaxItemSize != null)
|
||||
return _prototype.Index(uid.Comp.MaxItemSize.Value);
|
||||
{
|
||||
if (_prototype.TryIndex(uid.Comp.MaxItemSize.Value, out var proto))
|
||||
return proto;
|
||||
|
||||
Log.Error($"{ToPrettyString(uid.Owner)} tried to get invalid item size prototype: {uid.Comp.MaxItemSize.Value}. Stack trace:\\n{Environment.StackTrace}");
|
||||
}
|
||||
|
||||
if (!_itemQuery.TryGetComponent(uid, out var item))
|
||||
return _defaultStorageMaxItemSize;
|
||||
|
||||
@@ -54,7 +54,7 @@ public abstract class SharedStunSystem : EntitySystem
|
||||
// Attempt event subscriptions.
|
||||
SubscribeLocalEvent<StunnedComponent, ChangeDirectionAttemptEvent>(OnAttempt);
|
||||
SubscribeLocalEvent<StunnedComponent, UpdateCanMoveEvent>(OnMoveAttempt);
|
||||
SubscribeLocalEvent<StunnedComponent, InteractionAttemptEvent>(OnAttempt);
|
||||
SubscribeLocalEvent<StunnedComponent, InteractionAttemptEvent>(OnAttemptInteract);
|
||||
SubscribeLocalEvent<StunnedComponent, UseAttemptEvent>(OnAttempt);
|
||||
SubscribeLocalEvent<StunnedComponent, ThrowAttemptEvent>(OnAttempt);
|
||||
SubscribeLocalEvent<StunnedComponent, DropAttemptEvent>(OnAttempt);
|
||||
@@ -65,7 +65,10 @@ public abstract class SharedStunSystem : EntitySystem
|
||||
SubscribeLocalEvent<MobStateComponent, MobStateChangedEvent>(OnMobStateChanged);
|
||||
}
|
||||
|
||||
|
||||
private void OnAttemptInteract(Entity<StunnedComponent> ent, ref InteractionAttemptEvent args)
|
||||
{
|
||||
args.Cancelled = true;
|
||||
}
|
||||
|
||||
private void OnMobStateChanged(EntityUid uid, MobStateComponent component, MobStateChangedEvent args)
|
||||
{
|
||||
|
||||
@@ -45,11 +45,11 @@ namespace Content.Shared.SubFloor
|
||||
args.Cancelled = true;
|
||||
}
|
||||
|
||||
private void OnInteractionAttempt(EntityUid uid, SubFloorHideComponent component, GettingInteractedWithAttemptEvent args)
|
||||
private void OnInteractionAttempt(EntityUid uid, SubFloorHideComponent component, ref GettingInteractedWithAttemptEvent args)
|
||||
{
|
||||
// No interactions with entities hidden under floor tiles.
|
||||
if (component.BlockInteractions && component.IsUnderCover)
|
||||
args.Cancel();
|
||||
args.Cancelled = true;
|
||||
}
|
||||
|
||||
private void OnSubFloorStarted(EntityUid uid, SubFloorHideComponent component, ComponentStartup _)
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
namespace Content.Shared.Tools.Components
|
||||
{
|
||||
/// <summary>
|
||||
/// Tag component to let a tool ignore restrictions on whether devices are powered
|
||||
/// or not to work.
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public sealed partial class ToolForcePoweredComponent : Component
|
||||
{}
|
||||
}
|
||||
@@ -30,15 +30,24 @@ public sealed partial class ToolTileCompatibleComponent : Component
|
||||
[Serializable, NetSerializable]
|
||||
public sealed partial class TileToolDoAfterEvent : DoAfterEvent
|
||||
{
|
||||
public NetCoordinates Coordinates;
|
||||
public NetEntity Grid;
|
||||
public Vector2i GridTile;
|
||||
|
||||
public TileToolDoAfterEvent(NetCoordinates coordinates)
|
||||
public TileToolDoAfterEvent(NetEntity grid, Vector2i gridTile)
|
||||
{
|
||||
Coordinates = coordinates;
|
||||
Grid = grid;
|
||||
GridTile = gridTile;
|
||||
}
|
||||
|
||||
public override DoAfterEvent Clone()
|
||||
{
|
||||
return this;
|
||||
}
|
||||
|
||||
public override bool IsDuplicate(DoAfterEvent other)
|
||||
{
|
||||
return other is TileToolDoAfterEvent otherTile
|
||||
&& Grid == otherTile.Grid
|
||||
&& GridTile == otherTile.GridTile;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,16 +70,9 @@ public abstract partial class SharedToolSystem
|
||||
tool.Qualities = current.Behavior;
|
||||
|
||||
// TODO: Replace this with a better solution later
|
||||
if (TryComp<PryingComponent>(uid, out var pcomp))
|
||||
if (TryComp<PryingComponent>(uid, out var pryComp))
|
||||
{
|
||||
if (current.Behavior.Contains("Prying"))
|
||||
{
|
||||
pcomp.Enabled = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
pcomp.Enabled = false;
|
||||
}
|
||||
pryComp.Enabled = current.Behavior.Contains("Prying");
|
||||
}
|
||||
|
||||
if (playSound && current.ChangeSound != null)
|
||||
|
||||
@@ -37,24 +37,26 @@ public abstract partial class SharedToolSystem
|
||||
|
||||
if (!TryComp<ToolComponent>(ent, out var tool))
|
||||
return;
|
||||
var coordinates = GetCoordinates(args.Coordinates);
|
||||
|
||||
var gridUid = coordinates.GetGridUid(EntityManager);
|
||||
var gridUid = GetEntity(args.Grid);
|
||||
if (!TryComp<MapGridComponent>(gridUid, out var grid))
|
||||
{
|
||||
Log.Error("Attempted use tool on a non-existent grid?");
|
||||
return;
|
||||
}
|
||||
|
||||
var tileRef = _maps.GetTileRef(gridUid.Value, grid, coordinates);
|
||||
if (comp.RequiresUnobstructed && _turfs.IsTileBlocked(gridUid.Value, tileRef.GridIndices, CollisionGroup.MobMask))
|
||||
var tileRef = _maps.GetTileRef(gridUid, grid, args.GridTile);
|
||||
var coords = _maps.ToCoordinates(tileRef, grid);
|
||||
if (comp.RequiresUnobstructed && _turfs.IsTileBlocked(gridUid, tileRef.GridIndices, CollisionGroup.MobMask))
|
||||
return;
|
||||
|
||||
if (!TryDeconstructWithToolQualities(tileRef, tool.Qualities))
|
||||
return;
|
||||
|
||||
AdminLogger.Add(LogType.LatticeCut, LogImpact.Medium,
|
||||
$"{ToPrettyString(args.User):player} used {ToPrettyString(ent)} to edit the tile at {args.Coordinates}");
|
||||
AdminLogger.Add(
|
||||
LogType.LatticeCut,
|
||||
LogImpact.Medium,
|
||||
$"{ToPrettyString(args.User):player} used {ToPrettyString(ent)} to edit the tile at {coords}");
|
||||
args.Handled = true;
|
||||
}
|
||||
|
||||
@@ -66,7 +68,7 @@ public abstract partial class SharedToolSystem
|
||||
var comp = ent.Comp1!;
|
||||
var tool = ent.Comp2!;
|
||||
|
||||
if (!_mapManager.TryFindGridAt(clickLocation.ToMap(EntityManager, _transformSystem), out var gridUid, out var mapGrid))
|
||||
if (!_mapManager.TryFindGridAt(_transformSystem.ToMapCoordinates(clickLocation), out var gridUid, out var mapGrid))
|
||||
return false;
|
||||
|
||||
var tileRef = _maps.GetTileRef(gridUid, mapGrid, clickLocation);
|
||||
@@ -85,7 +87,7 @@ public abstract partial class SharedToolSystem
|
||||
if (!InteractionSystem.InRangeUnobstructed(user, coordinates, popup: false))
|
||||
return false;
|
||||
|
||||
var args = new TileToolDoAfterEvent(GetNetCoordinates(coordinates));
|
||||
var args = new TileToolDoAfterEvent(GetNetEntity(gridUid), tileRef.GridIndices);
|
||||
UseTool(ent, user, ent, comp.Delay, tool.Qualities, args, out _, toolComponent: tool);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -271,6 +271,11 @@ public abstract partial class SharedToolSystem : EntitySystem
|
||||
|
||||
return new ToolDoAfterEvent(Fuel, evClone, OriginalTarget);
|
||||
}
|
||||
|
||||
public override bool IsDuplicate(DoAfterEvent other)
|
||||
{
|
||||
return other is ToolDoAfterEvent toolDoAfter && WrappedEvent.IsDuplicate(toolDoAfter.WrappedEvent);
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
|
||||
@@ -17,7 +17,8 @@ public sealed class LegsParalyzedSystem : EntitySystem
|
||||
{
|
||||
SubscribeLocalEvent<LegsParalyzedComponent, ComponentStartup>(OnStartup);
|
||||
SubscribeLocalEvent<LegsParalyzedComponent, ComponentShutdown>(OnShutdown);
|
||||
SubscribeLocalEvent<LegsParalyzedComponent, BuckleChangeEvent>(OnBuckleChange);
|
||||
SubscribeLocalEvent<LegsParalyzedComponent, BuckledEvent>(OnBuckled);
|
||||
SubscribeLocalEvent<LegsParalyzedComponent, UnbuckledEvent>(OnUnbuckled);
|
||||
SubscribeLocalEvent<LegsParalyzedComponent, ThrowPushbackAttemptEvent>(OnThrowPushbackAttempt);
|
||||
SubscribeLocalEvent<LegsParalyzedComponent, UpdateCanMoveEvent>(OnUpdateCanMoveEvent);
|
||||
}
|
||||
@@ -34,16 +35,14 @@ public sealed class LegsParalyzedSystem : EntitySystem
|
||||
_bodySystem.UpdateMovementSpeed(uid);
|
||||
}
|
||||
|
||||
private void OnBuckleChange(EntityUid uid, LegsParalyzedComponent component, ref BuckleChangeEvent args)
|
||||
private void OnBuckled(EntityUid uid, LegsParalyzedComponent component, ref BuckledEvent args)
|
||||
{
|
||||
if (args.Buckling)
|
||||
{
|
||||
_standingSystem.Stand(args.BuckledEntity);
|
||||
}
|
||||
else
|
||||
{
|
||||
_standingSystem.Down(args.BuckledEntity);
|
||||
}
|
||||
_standingSystem.Stand(uid);
|
||||
}
|
||||
|
||||
private void OnUnbuckled(EntityUid uid, LegsParalyzedComponent component, ref UnbuckledEvent args)
|
||||
{
|
||||
_standingSystem.Down(uid);
|
||||
}
|
||||
|
||||
private void OnUpdateCanMoveEvent(EntityUid uid, LegsParalyzedComponent component, UpdateCanMoveEvent args)
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
using Content.Shared.Maps;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Audio.Systems;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Map.Components;
|
||||
using Robust.Shared.Physics.Components;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Timing;
|
||||
@@ -18,15 +16,14 @@ public abstract class SharedWeatherSystem : EntitySystem
|
||||
[Dependency] private readonly ITileDefinitionManager _tileDefManager = default!;
|
||||
[Dependency] private readonly MetaDataSystem _metadata = default!;
|
||||
[Dependency] private readonly SharedAudioSystem _audio = default!;
|
||||
[Dependency] private readonly SharedMapSystem _mapSystem = default!;
|
||||
|
||||
private EntityQuery<BlockWeatherComponent> _blockQuery;
|
||||
private EntityQuery<PhysicsComponent> _physicsQuery;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
_blockQuery = GetEntityQuery<BlockWeatherComponent>();
|
||||
_physicsQuery = GetEntityQuery<PhysicsComponent>();
|
||||
SubscribeLocalEvent<WeatherComponent, EntityUnpausedEvent>(OnWeatherUnpaused);
|
||||
}
|
||||
|
||||
@@ -41,9 +38,7 @@ public abstract class SharedWeatherSystem : EntitySystem
|
||||
}
|
||||
}
|
||||
|
||||
public bool CanWeatherAffect(
|
||||
MapGridComponent grid,
|
||||
TileRef tileRef)
|
||||
public bool CanWeatherAffect(EntityUid uid, MapGridComponent grid, TileRef tileRef)
|
||||
{
|
||||
if (tileRef.Tile.IsEmpty)
|
||||
return true;
|
||||
@@ -53,9 +48,9 @@ public abstract class SharedWeatherSystem : EntitySystem
|
||||
if (!tileDef.Weather)
|
||||
return false;
|
||||
|
||||
var anchoredEnts = grid.GetAnchoredEntitiesEnumerator(tileRef.GridIndices);
|
||||
var anchoredEntities = _mapSystem.GetAnchoredEntitiesEnumerator(uid, grid, tileRef.GridIndices);
|
||||
|
||||
while (anchoredEnts.MoveNext(out var ent))
|
||||
while (anchoredEntities.MoveNext(out var ent))
|
||||
{
|
||||
if (_blockQuery.HasComponent(ent.Value))
|
||||
return false;
|
||||
@@ -154,20 +149,22 @@ public abstract class SharedWeatherSystem : EntitySystem
|
||||
/// </summary>
|
||||
public void SetWeather(MapId mapId, WeatherPrototype? proto, TimeSpan? endTime)
|
||||
{
|
||||
var mapUid = MapManager.GetMapEntityId(mapId);
|
||||
var weatherComp = EnsureComp<WeatherComponent>(mapUid);
|
||||
if (!_mapSystem.TryGetMap(mapId, out var mapUid))
|
||||
return;
|
||||
|
||||
var weatherComp = EnsureComp<WeatherComponent>(mapUid.Value);
|
||||
|
||||
foreach (var (eProto, weather) in weatherComp.Weather)
|
||||
{
|
||||
// Reset cooldown if it's an existing one.
|
||||
if (eProto == proto?.ID)
|
||||
if (proto == null || eProto == proto.ID)
|
||||
{
|
||||
weather.EndTime = endTime;
|
||||
|
||||
if (weather.State == WeatherState.Ending)
|
||||
weather.State = WeatherState.Running;
|
||||
|
||||
Dirty(mapUid, weatherComp);
|
||||
Dirty(mapUid.Value, weatherComp);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -177,12 +174,12 @@ public abstract class SharedWeatherSystem : EntitySystem
|
||||
if (weather.EndTime == null || weather.EndTime > end)
|
||||
{
|
||||
weather.EndTime = end;
|
||||
Dirty(mapUid, weatherComp);
|
||||
Dirty(mapUid.Value, weatherComp);
|
||||
}
|
||||
}
|
||||
|
||||
if (proto != null)
|
||||
StartWeather(mapUid, weatherComp, proto, endTime);
|
||||
StartWeather(mapUid.Value, weatherComp, proto, endTime);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -229,9 +226,9 @@ public abstract class SharedWeatherSystem : EntitySystem
|
||||
[Serializable, NetSerializable]
|
||||
protected sealed class WeatherComponentState : ComponentState
|
||||
{
|
||||
public Dictionary<string, WeatherData> Weather;
|
||||
public Dictionary<ProtoId<WeatherPrototype>, WeatherData> Weather;
|
||||
|
||||
public WeatherComponentState(Dictionary<string, WeatherData> weather)
|
||||
public WeatherComponentState(Dictionary<ProtoId<WeatherPrototype>, WeatherData> weather)
|
||||
{
|
||||
Weather = weather;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Dictionary;
|
||||
|
||||
namespace Content.Shared.Weather;
|
||||
|
||||
@@ -12,8 +11,8 @@ public sealed partial class WeatherComponent : Component
|
||||
/// <summary>
|
||||
/// Currently running weathers
|
||||
/// </summary>
|
||||
[ViewVariables, DataField("weather", customTypeSerializer:typeof(PrototypeIdDictionarySerializer<WeatherData, WeatherPrototype>))]
|
||||
public Dictionary<string, WeatherData> Weather = new();
|
||||
[DataField]
|
||||
public Dictionary<ProtoId<WeatherPrototype>, WeatherData> Weather = new();
|
||||
|
||||
public static readonly TimeSpan StartupTime = TimeSpan.FromSeconds(15);
|
||||
public static readonly TimeSpan ShutdownTime = TimeSpan.FromSeconds(15);
|
||||
@@ -29,19 +28,19 @@ public sealed partial class WeatherData
|
||||
/// <summary>
|
||||
/// When the weather started if relevant.
|
||||
/// </summary>
|
||||
[ViewVariables, DataField("startTime", customTypeSerializer: typeof(TimeOffsetSerializer))]
|
||||
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer))] //TODO: Remove Custom serializer
|
||||
public TimeSpan StartTime = TimeSpan.Zero;
|
||||
|
||||
/// <summary>
|
||||
/// When the applied weather will end.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite), DataField("endTime", customTypeSerializer: typeof(TimeOffsetSerializer))]
|
||||
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer))] //TODO: Remove Custom serializer
|
||||
public TimeSpan? EndTime;
|
||||
|
||||
[ViewVariables]
|
||||
public TimeSpan Duration => EndTime == null ? TimeSpan.MaxValue : EndTime.Value - StartTime;
|
||||
|
||||
[DataField("state")]
|
||||
[DataField]
|
||||
public WeatherState State = WeatherState.Invalid;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user