using Content.Shared.Hands.Components; using Content.Shared.Movement.Events; using Content.Shared.Movement.Systems; using Content.Shared.Physics; using Content.Shared.Rotation; using Robust.Shared.Audio.Systems; using Robust.Shared.Physics; using Robust.Shared.Physics.Systems; namespace Content.Shared.Standing; public sealed class StandingStateSystem : EntitySystem { [Dependency] private readonly SharedAppearanceSystem _appearance = default!; [Dependency] private readonly SharedAudioSystem _audio = default!; [Dependency] private readonly SharedPhysicsSystem _physics = default!; // If StandingCollisionLayer value is ever changed to more than one layer, the logic needs to be edited. public const int StandingCollisionLayer = (int) CollisionGroup.MidImpassable; public override void Initialize() { base.Initialize(); SubscribeLocalEvent(OnMobCollide); SubscribeLocalEvent(OnMobTargetCollide); SubscribeLocalEvent(OnRefreshFrictionModifiers); SubscribeLocalEvent(OnTileFriction); } private void OnMobTargetCollide(Entity ent, ref AttemptMobTargetCollideEvent args) { if (!ent.Comp.Standing) { args.Cancelled = true; } } private void OnMobCollide(Entity ent, ref AttemptMobCollideEvent args) { if (!ent.Comp.Standing) { args.Cancelled = true; } } private void OnRefreshFrictionModifiers(Entity entity, ref RefreshFrictionModifiersEvent args) { if (entity.Comp.Standing) return; args.ModifyFriction(entity.Comp.DownFrictionMod); args.ModifyAcceleration(entity.Comp.DownFrictionMod); } private void OnTileFriction(Entity entity, ref TileFrictionEvent args) { if (!entity.Comp.Standing) args.Modifier *= entity.Comp.DownFrictionMod; } public bool IsDown(EntityUid uid, StandingStateComponent? standingState = null) { if (!Resolve(uid, ref standingState, false)) return false; return !standingState.Standing; } public bool Down(EntityUid uid, bool playSound = true, bool dropHeldItems = true, bool force = false, StandingStateComponent? standingState = null, AppearanceComponent? appearance = null, HandsComponent? hands = null) { // TODO: This should actually log missing comps... if (!Resolve(uid, ref standingState, false)) return false; // Optional component. Resolve(uid, ref appearance, ref hands, false); if (!standingState.Standing) return true; // This is just to avoid most callers doing this manually saving boilerplate // 99% of the time you'll want to drop items but in some scenarios (e.g. buckling) you don't want to. // We do this BEFORE downing because something like buckle may be blocking downing but we want to drop hand items anyway // and ultimately this is just to avoid boilerplate in Down callers + keep their behavior consistent. if (dropHeldItems && hands != null) { var ev = new DropHandItemsEvent(); RaiseLocalEvent(uid, ref ev, false); } if (!force) { var msg = new DownAttemptEvent(); RaiseLocalEvent(uid, msg, false); if (msg.Cancelled) return false; } standingState.Standing = false; Dirty(uid, standingState); RaiseLocalEvent(uid, new DownedEvent(), false); // Seemed like the best place to put it _appearance.SetData(uid, RotationVisuals.RotationState, RotationState.Horizontal, appearance); // Change collision masks to allow going under certain entities like flaps and tables if (TryComp(uid, out FixturesComponent? fixtureComponent)) { foreach (var (key, fixture) in fixtureComponent.Fixtures) { if ((fixture.CollisionMask & StandingCollisionLayer) == 0) continue; standingState.ChangedFixtures.Add(key); _physics.SetCollisionMask(uid, key, fixture, fixture.CollisionMask & ~StandingCollisionLayer, manager: fixtureComponent); } } // check if component was just added or streamed to client // if true, no need to play sound - mob was down before player could seen that if (standingState.LifeStage <= ComponentLifeStage.Starting) return true; if (playSound) { _audio.PlayPredicted(standingState.DownSound, uid, uid); } return true; } public bool Stand(EntityUid uid, StandingStateComponent? standingState = null, AppearanceComponent? appearance = null, bool force = false) { // TODO: This should actually log missing comps... if (!Resolve(uid, ref standingState, false)) return false; // Optional component. Resolve(uid, ref appearance, false); if (standingState.Standing) return true; if (!force) { var msg = new StandAttemptEvent(); RaiseLocalEvent(uid, msg, false); if (msg.Cancelled) return false; } standingState.Standing = true; Dirty(uid, standingState); RaiseLocalEvent(uid, new StoodEvent(), false); _appearance.SetData(uid, RotationVisuals.RotationState, RotationState.Vertical, appearance); if (TryComp(uid, out FixturesComponent? fixtureComponent)) { foreach (var key in standingState.ChangedFixtures) { if (fixtureComponent.Fixtures.TryGetValue(key, out var fixture)) _physics.SetCollisionMask(uid, key, fixture, fixture.CollisionMask | StandingCollisionLayer, fixtureComponent); } } standingState.ChangedFixtures.Clear(); return true; } } [ByRefEvent] public record struct DropHandItemsEvent(); /// /// Subscribe if you can potentially block a down attempt. /// public sealed class DownAttemptEvent : CancellableEntityEventArgs { } /// /// Subscribe if you can potentially block a stand attempt. /// public sealed class StandAttemptEvent : CancellableEntityEventArgs { } /// /// Raised when an entity becomes standing /// public sealed class StoodEvent : EntityEventArgs { } /// /// Raised when an entity is not standing /// public sealed class DownedEvent : EntityEventArgs { } /// /// Raised after an entity falls down. /// public sealed class FellDownEvent : EntityEventArgs { public EntityUid Uid { get; } public FellDownEvent(EntityUid uid) { Uid = uid; } } /// /// Raised on the entity being thrown due to the holder falling down. /// [ByRefEvent] public record struct FellDownThrowAttemptEvent(EntityUid Thrower, bool Cancelled = false);