using Content.Shared.Alert; using Content.Shared.Inventory; using Content.Shared.Throwing; using Content.Shared.Weapons.Ranged.Systems; using Robust.Shared.Map; using Robust.Shared.Physics; using Robust.Shared.Physics.Components; using Robust.Shared.Physics.Events; using Robust.Shared.Prototypes; using Robust.Shared.Serialization; using Robust.Shared.Timing; namespace Content.Shared.Gravity; public abstract partial class SharedGravitySystem : EntitySystem { [Dependency] protected readonly IGameTiming Timing = default!; [Dependency] private readonly AlertsSystem _alerts = default!; public static readonly ProtoId WeightlessAlert = "Weightless"; protected EntityQuery GravityQuery; private EntityQuery _weightlessQuery; private EntityQuery _physicsQuery; public override void Initialize() { base.Initialize(); // Grid Gravity SubscribeLocalEvent(OnGridInit); SubscribeLocalEvent(OnGravityChange); // Weightlessness SubscribeLocalEvent(OnMapInit); SubscribeLocalEvent(OnEntParentChanged); SubscribeLocalEvent(OnBodyTypeChanged); // Alerts SubscribeLocalEvent(OnAlertsSync); SubscribeLocalEvent(OnWeightlessnessChanged); SubscribeLocalEvent(OnAlertsParentChange); // Impulse SubscribeLocalEvent(OnShooterImpulse); SubscribeLocalEvent(OnThrowerImpulse); GravityQuery = GetEntityQuery(); _weightlessQuery = GetEntityQuery(); _physicsQuery = GetEntityQuery(); } public override void Update(float frameTime) { base.Update(frameTime); UpdateShake(); } public bool IsWeightless(Entity entity) { // If we can be weightless and are weightless, return true, otherwise return false return _weightlessQuery.Resolve(entity, ref entity.Comp, false) && entity.Comp.Weightless; } private bool GetWeightless(Entity entity) { if (!_physicsQuery.Resolve(entity, ref entity.Comp2, false)) return false; if (entity.Comp2.BodyType is BodyType.Static or BodyType.Kinematic) return false; // Check if something other than the grid or map is overriding our gravity var ev = new IsWeightlessEvent(); RaiseLocalEvent(entity, ref ev); if (ev.Handled) return ev.IsWeightless; return !EntityGridOrMapHaveGravity(entity.Owner); } /// /// Refreshes weightlessness status, needs to be called anytime it would change. /// /// The entity we are updating the weightless status of public void RefreshWeightless(Entity entity) { if (!_weightlessQuery.Resolve(entity, ref entity.Comp)) return; UpdateWeightless(entity!); } /// /// Overload of which also takes a bool for the weightlessness value we want to change to. /// This method should only be called if there is no chance something can override the weightless value you're trying to change to. /// This is really only the case if you're applying a weightless value that overrides non-conditionally from events or are a grid with the gravity component. /// /// The entity we are updating the weightless status of /// The weightless value we are trying to change to, helps avoid needless networking public void RefreshWeightless(Entity entity, bool weightless) { if (!_weightlessQuery.Resolve(entity, ref entity.Comp)) return; // Only update if we're changing our weightless status if (entity.Comp.Weightless == weightless) return; UpdateWeightless(entity!); } private void UpdateWeightless(Entity entity) { var newWeightless = GetWeightless(entity); // Don't network or raise events if it's not changing if (newWeightless == entity.Comp.Weightless) return; entity.Comp.Weightless = newWeightless; Dirty(entity); var ev = new WeightlessnessChangedEvent(entity.Comp.Weightless); RaiseLocalEvent(entity, ref ev); } private void OnMapInit(Entity entity, ref MapInitEvent args) { RefreshWeightless((entity.Owner, entity.Comp)); } private void OnWeightlessnessChanged(Entity entity, ref WeightlessnessChangedEvent args) { if (args.Weightless) _alerts.ShowAlert(entity.AsNullable(), WeightlessAlert); else _alerts.ClearAlert(entity.AsNullable(), WeightlessAlert); } private void OnEntParentChanged(Entity entity, ref EntParentChangedMessage args) { // If we've moved but are still on the same grid, then don't do anything. if (args.OldParent == args.Transform.GridUid) return; RefreshWeightless((entity.Owner, entity.Comp)); } private void OnBodyTypeChanged(Entity entity, ref PhysicsBodyTypeChangedEvent args) { // No need to update weightlessness if we're not weightless and we're a body type that can't be weightless if (args.New is BodyType.Static or BodyType.Kinematic && entity.Comp.Weightless == false) return; RefreshWeightless((entity.Owner, entity.Comp)); } /// /// Checks if a given entity is currently standing on a grid or map that supports having gravity at all. /// public bool EntityOnGravitySupportingGridOrMap(Entity entity) { entity.Comp ??= Transform(entity); return GravityQuery.HasComp(entity.Comp.GridUid) || GravityQuery.HasComp(entity.Comp.MapUid); } /// /// Checks if a given entity is currently standing on a grid or map that has gravity of some kind. /// public bool EntityGridOrMapHaveGravity(Entity entity) { entity.Comp ??= Transform(entity); // DO NOT SET TO WEIGHTLESS IF THEY'RE IN NULL-SPACE // TODO: If entities actually properly pause when leaving PVS rather than entering null-space this can probably go. if (entity.Comp.MapID == MapId.Nullspace) return true; return GravityQuery.TryComp(entity.Comp.GridUid, out var gravity) && gravity.Enabled || GravityQuery.TryComp(entity.Comp.MapUid, out var mapGravity) && mapGravity.Enabled; } private void OnGravityChange(ref GravityChangedEvent args) { var gravity = AllEntityQuery(); while(gravity.MoveNext(out var uid, out var weightless, out var xform)) { if (xform.GridUid != args.ChangedGridIndex) continue; RefreshWeightless((uid, weightless), !args.HasGravity); } } private void OnAlertsSync(AlertSyncEvent ev) { if (IsWeightless(ev.Euid)) _alerts.ShowAlert(ev.Euid, WeightlessAlert); else _alerts.ClearAlert(ev.Euid, WeightlessAlert); } private void OnAlertsParentChange(Entity entity, ref EntParentChangedMessage args) { if (IsWeightless(entity.Owner)) _alerts.ShowAlert(entity.AsNullable(), WeightlessAlert); else _alerts.ClearAlert(entity.AsNullable(), WeightlessAlert); } private void OnGridInit(GridInitializeEvent ev) { EnsureComp(ev.EntityUid); } [Serializable, NetSerializable] private sealed class GravityComponentState : ComponentState { public bool Enabled { get; } public GravityComponentState(bool enabled) { Enabled = enabled; } } private void OnThrowerImpulse(Entity entity, ref ThrowerImpulseEvent args) { args.Push = true; } private void OnShooterImpulse(Entity entity, ref ShooterImpulseEvent args) { args.Push = true; } } /// /// Raised to determine if an entity's weightlessness is being overwritten by a component or item with a component. /// /// Whether we should be weightless /// Whether something is trying to override our weightlessness [ByRefEvent] public record struct IsWeightlessEvent(bool IsWeightless = false, bool Handled = false) : IInventoryRelayEvent { SlotFlags IInventoryRelayEvent.TargetSlots => ~SlotFlags.POCKET; } /// /// Raised on an entity when their weightless status changes. /// [ByRefEvent] public readonly record struct WeightlessnessChangedEvent(bool Weightless);