using Content.Shared.Audio; using Content.Shared.Hands.Components; using Content.Shared.Rotation; using Robust.Shared.Audio; using Robust.Shared.Player; using Robust.Shared.Timing; using Robust.Shared.Physics; using Content.Shared.Physics; using Robust.Shared.GameStates; using Robust.Shared.Serialization; using Robust.Shared.Network; namespace Content.Shared.Standing { public sealed class StandingStateSystem : EntitySystem { [Dependency] private readonly IGameTiming _gameTiming = default!; [Dependency] private readonly INetManager _netMan = default!; // If StandingCollisionLayer value is ever changed to more than one layer, the logic needs to be edited. private const int StandingCollisionLayer = (int) CollisionGroup.MidImpassable; public override void Initialize() { SubscribeLocalEvent(OnGetState); SubscribeLocalEvent(OnHandleState); } private void OnHandleState(EntityUid uid, StandingStateComponent component, ref ComponentHandleState args) { if (args.Current is not StandingComponentState state) return; component.Standing = state.Standing; component.ChangedFixtures = new(state.ChangedFixtures); } private void OnGetState(EntityUid uid, StandingStateComponent component, ref ComponentGetState args) { args.State = new StandingComponentState(component.Standing, component.ChangedFixtures); } 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, StandingStateComponent? standingState = null, AppearanceComponent? appearance = null, SharedHandsComponent? 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) { RaiseLocalEvent(uid, new DropHandItemsEvent(), false); } var msg = new DownAttemptEvent(); RaiseLocalEvent(uid, msg, false); if (msg.Cancelled) return false; standingState.Standing = false; Dirty(standingState); RaiseLocalEvent(uid, new DownedEvent(), false); // Seemed like the best place to put it appearance?.SetData(RotationVisuals.RotationState, RotationState.Horizontal); // 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); fixture.CollisionMask &= ~StandingCollisionLayer; } } if (!_gameTiming.IsFirstTimePredicted) return true; // TODO audio prediction if (playSound && _netMan.IsServer) { SoundSystem.Play(standingState.DownSound.GetSound(), Filter.Pvs(uid), uid, AudioHelpers.WithVariation(0.25f)); } return true; } public bool Stand(EntityUid uid, StandingStateComponent? standingState = null, AppearanceComponent? appearance = null) { // 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; var msg = new StandAttemptEvent(); RaiseLocalEvent(uid, msg, false); if (msg.Cancelled) return false; standingState.Standing = true; Dirty(standingState); RaiseLocalEvent(uid, new StoodEvent(), false); appearance?.SetData(RotationVisuals.RotationState, RotationState.Vertical); if (TryComp(uid, out FixturesComponent? fixtureComponent)) { foreach (var key in standingState.ChangedFixtures) { if (fixtureComponent.Fixtures.TryGetValue(key, out var fixture)) fixture.CollisionMask |= StandingCollisionLayer; } } standingState.ChangedFixtures.Clear(); return true; } // I'm not calling it StandingStateComponentState [Serializable, NetSerializable] private sealed class StandingComponentState : ComponentState { public bool Standing { get; } public List ChangedFixtures { get; } public StandingComponentState(bool standing, List changedFixtures) { Standing = standing; ChangedFixtures = changedFixtures; } } } public sealed class DropHandItemsEvent : EventArgs { } /// /// 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 { } }