using Content.Shared.Examine; using Content.Shared.Stealth.Components; using Robust.Shared.GameStates; using Robust.Shared.Timing; namespace Content.Shared.Stealth; public abstract class SharedStealthSystem : EntitySystem { [Dependency] private readonly IGameTiming _timing = default!; public override void Initialize() { base.Initialize(); SubscribeLocalEvent(OnStealthGetState); SubscribeLocalEvent(OnStealthHandleState); SubscribeLocalEvent(OnMove); SubscribeLocalEvent(OnGetVisibilityModifiers); SubscribeLocalEvent(OnPaused); SubscribeLocalEvent(OnInit); SubscribeLocalEvent(OnExamine); } private void OnExamine(EntityUid uid, StealthComponent component, ExamineAttemptEvent args) { if (!component.Enabled || GetVisibility(uid, component) > component.ExamineThreshold) return; // Don't block examine for owner or children of the cloaked entity. // Containers and the like should already block examining, so not bothering to check for occluding containers. var source = args.Examiner; do { if (source == uid) return; source = Transform(source).ParentUid; } while (source.IsValid()); args.Cancel(); } public virtual void SetEnabled(EntityUid uid, bool value, StealthComponent? component = null) { if (!Resolve(uid, ref component, false) || component.Enabled == value) return; component.Enabled = value; Dirty(component); } private void OnPaused(EntityUid uid, StealthComponent component, EntityPausedEvent args) { if (args.Paused) { component.LastVisibility = GetVisibility(uid, component); component.LastUpdated = null; } else { component.LastUpdated = _timing.CurTime; } Dirty(component); } protected virtual void OnInit(EntityUid uid, StealthComponent component, ComponentInit args) { if (component.LastUpdated != null || Paused(uid)) return; component.LastUpdated = _timing.CurTime; } private void OnStealthGetState(EntityUid uid, StealthComponent component, ref ComponentGetState args) { args.State = new StealthComponentState(component.LastVisibility, component.LastUpdated, component.Enabled); } private void OnStealthHandleState(EntityUid uid, StealthComponent component, ref ComponentHandleState args) { if (args.Current is not StealthComponentState cast) return; SetEnabled(uid, cast.Enabled, component); component.LastVisibility = cast.Visibility; component.LastUpdated = cast.LastUpdated; } private void OnMove(EntityUid uid, StealthOnMoveComponent component, ref MoveEvent args) { if (args.FromStateHandling) return; if (args.NewPosition.EntityId != args.OldPosition.EntityId) return; var delta = component.MovementVisibilityRate * (args.NewPosition.Position - args.OldPosition.Position).Length; ModifyVisibility(uid, delta); } private void OnGetVisibilityModifiers(EntityUid uid, StealthOnMoveComponent component, GetVisibilityModifiersEvent args) { var mod = args.SecondsSinceUpdate * component.PassiveVisibilityRate; args.FlatModifier += mod; } /// /// Modifies the visibility based on the delta provided. /// /// The delta to be used in visibility calculation. public void ModifyVisibility(EntityUid uid, float delta, StealthComponent? component = null) { if (delta == 0 || !Resolve(uid, ref component)) return; if (component.LastUpdated != null) { component.LastVisibility = GetVisibility(uid, component); component.LastUpdated = _timing.CurTime; } component.LastVisibility = Math.Clamp(component.LastVisibility + delta, component.MinVisibility, component.MaxVisibility); Dirty(component); } /// /// Sets the visibility directly with no modifications /// /// The value to set the visibility to. -1 is fully invisible, 1 is fully visible public void SetVisibility(EntityUid uid, float value, StealthComponent? component = null) { if (!Resolve(uid, ref component)) return; component.LastVisibility = Math.Clamp(value, component.MinVisibility, component.MaxVisibility); if (component.LastUpdated != null) component.LastUpdated = _timing.CurTime; Dirty(component); } /// /// Gets the current visibility from the /// Use this instead of getting LastVisibility from the component directly. /// /// Returns a calculation that accounts for any stealth change that happened since last update, otherwise /// returns based on if it can resolve the component. Note that the returned value may be larger than the components /// maximum stealth value if it is currently disabled. public float GetVisibility(EntityUid uid, StealthComponent? component = null) { if (!Resolve(uid, ref component) || !component.Enabled) return 1; if (component.LastUpdated == null) return component.LastVisibility; var deltaTime = _timing.CurTime - component.LastUpdated.Value; var ev = new GetVisibilityModifiersEvent(uid, component, (float) deltaTime.TotalSeconds, 0f); RaiseLocalEvent(uid, ev, false); return Math.Clamp(component.LastVisibility + ev.FlatModifier, component.MinVisibility, component.MaxVisibility); } /// /// Used to run through any stealth effecting components on the entity. /// private sealed class GetVisibilityModifiersEvent : EntityEventArgs { public readonly StealthComponent Stealth = default!; public readonly float SecondsSinceUpdate; /// /// Calculate this and add to it. Do not divide, multiply, or overwrite. /// The sum will be added to the stealth component's visibility. /// public float FlatModifier; public GetVisibilityModifiersEvent(EntityUid uid, StealthComponent stealth, float secondsSinceUpdate, float flatModifier) { Stealth = stealth; SecondsSinceUpdate = secondsSinceUpdate; FlatModifier = flatModifier; } } }