From 1a8bf1ad165d2d184db4759667a3f538816f0cfe Mon Sep 17 00:00:00 2001 From: drdth Date: Mon, 16 Mar 2026 03:11:40 +0300 Subject: [PATCH 01/17] refactor: optimize event sending, add auto-scaling SeeRange --- Content.Shared/_Scp/Watching/EyeWatching.cs | 85 +++++++++---------- .../_Scp/Watching/EyeWatchingSystem.Events.cs | 32 +++++++ .../_Scp/Watching/WatchingTargetComponent.cs | 23 ++++- .../Entities/Mobs/Player/Scp/Main/scp096.yml | 2 + 4 files changed, 97 insertions(+), 45 deletions(-) create mode 100644 Content.Shared/_Scp/Watching/EyeWatchingSystem.Events.cs diff --git a/Content.Shared/_Scp/Watching/EyeWatching.cs b/Content.Shared/_Scp/Watching/EyeWatching.cs index b44ed214c1f..19ff8b2ab93 100644 --- a/Content.Shared/_Scp/Watching/EyeWatching.cs +++ b/Content.Shared/_Scp/Watching/EyeWatching.cs @@ -14,21 +14,25 @@ public sealed partial class EyeWatchingSystem : EntitySystem [Dependency] private readonly ProximitySystem _proximity = default!; [Dependency] private readonly IGameTiming _timing = default!; - private static readonly TimeSpan WatchingCheckInterval = TimeSpan.FromSeconds(0.3f); - - public const float SeeRange = 16f; + /// + /// Радиус, в котором сущности могут увидеть друг друга. + /// + [ViewVariables] + public float SeeRange = 16f; public override void Initialize() { - SubscribeLocalEvent(OnMapInit); + InitializeEvents(); _mobStateQuery = GetEntityQuery(); _insideStorageQuery = GetEntityQuery(); } - private void OnMapInit(Entity ent, ref MapInitEvent args) + public override void Shutdown() { - SetNextTime(ent); + base.Shutdown(); + + ShutdownEvents(); } /// @@ -39,9 +43,6 @@ public override void Update(float frameTime) { base.Update(frameTime); - if (!_timing.IsFirstTimePredicted) - return; - var query = EntityQueryEnumerator(); while (query.MoveNext(out var uid, out var watchingComponent)) { @@ -55,11 +56,18 @@ public override void Update(float frameTime) // Полезно в коде, который уже использует подобные проверки или не требует этого foreach (var potentialViewer in potentialViewers) { + var simpleViewerEvent = new SimpleEntityLookedAtEvent((uid, watchingComponent)); + var simpleTargetEvent = new SimpleEntitySeenEvent(potentialViewer); + // За подробностями какой ивент для чего навести мышку на название ивента - RaiseLocalEvent(potentialViewer, new SimpleEntityLookedAtEvent((uid, watchingComponent))); - RaiseLocalEvent(uid, new SimpleEntitySeenEvent(potentialViewer)); + RaiseLocalEvent(potentialViewer, ref simpleViewerEvent); + RaiseLocalEvent(uid, ref simpleTargetEvent); } + // Если требуются только Simple ивенты, то нет смысла делать дальнейшие действия. + if (watchingComponent.SimpleMode) + continue; + // Проверяет всех потенциальных смотрящих на то, действительно ли они видят цель. // Каждый потенциально смотрящий проходит полный комплекс проверок. // Выдает полный список всех сущностей, кто действительно видит цель @@ -78,67 +86,58 @@ public override void Update(float frameTime) watchingComponent.AlreadyLookedAt[netViewer] = TimeSpan.Zero; // За подробностями какой ивент для чего навести мышку на название ивента - RaiseLocalEvent(viewer, new EntityLookedAtEvent((uid, watchingComponent), firstTime, blockerLevel)); - RaiseLocalEvent(uid, new EntitySeenEvent(viewer, firstTime, blockerLevel)); + var viewerEvent = new EntityLookedAtEvent((uid, watchingComponent), firstTime, blockerLevel); + var targetEvent = new EntitySeenEvent(viewer, firstTime, blockerLevel); + + RaiseLocalEvent(viewer, ref viewerEvent); + RaiseLocalEvent(uid, ref targetEvent); // Добавляет смотрящего в список уже смотревших, чтобы позволить системам манипулировать этим // И предотвращать эффект, если игрок смотрит не первый раз или не так давно watchingComponent.AlreadyLookedAt[netViewer] = _timing.CurTime; } - Dirty(uid, watchingComponent); SetNextTime(watchingComponent); + Dirty(uid, watchingComponent); } } private void SetNextTime(WatchingTargetComponent component) { - component.NextTimeWatchedCheck = _timing.CurTime + WatchingCheckInterval; + component.NextTimeWatchedCheck = _timing.CurTime + component.WatchingCheckInterval; } } /// /// Ивент вызываемый на смотрящем, передающий информации, что он посмотрел на кого-то /// -/// Цель, на которую посмотрели -/// Видим ли мы цель в первый раз -/// Линия видимости между смотрящим и целью, подробнее -public sealed class EntityLookedAtEvent(Entity target, bool firstTime, LineOfSightBlockerLevel blockerLevel) : EntityEventArgs -{ - public readonly Entity Target = target; - public readonly bool IsSeenFirstTime = firstTime; - public readonly LineOfSightBlockerLevel BlockerLevel = blockerLevel; -} +/// Цель, на которую посмотрели +/// Видим ли мы цель в первый раз +/// Линия видимости между смотрящим и целью, подробнее +[ByRefEvent] +public readonly record struct EntityLookedAtEvent(Entity Target, bool FirstTime, LineOfSightBlockerLevel BlockerLevel); /// /// Ивент вызываемый на цели, передающий информации, что на нее кто-то посмотрел /// -/// Смотрящий, который увидел цель -/// Видим ли мы цель в первый раз -/// Линия видимости между смотрящим и целью, подробнее -public sealed class EntitySeenEvent(EntityUid viewer, bool firstTime, LineOfSightBlockerLevel blockerLevel) : EntityEventArgs -{ - public readonly EntityUid Viewer = viewer; - public readonly bool IsSeenFirstTime = firstTime; - public readonly LineOfSightBlockerLevel BlockerLevel = blockerLevel; -} +/// Смотрящий, который увидел цель +/// Видим ли мы цель в первый раз +/// Линия видимости между смотрящим и целью, подробнее +[ByRefEvent] +public readonly record struct EntitySeenEvent(EntityUid Viewer, bool FirstTime, LineOfSightBlockerLevel BlockerLevel); /// /// Простой ивент, говорящий, что смотрящий посмотрел на цель. /// Вызывается до прохождения различных проверок на смотрящем. Если вдруг требуются собственная ручная проверка /// -/// Цель, на которую посмотри -public sealed class SimpleEntityLookedAtEvent(Entity target) : EntityEventArgs -{ - public readonly Entity Target = target; -} +/// Цель, на которую посмотри +[ByRefEvent] +public readonly record struct SimpleEntityLookedAtEvent(Entity Target); /// /// Простой ивент, говорящий, что на цель кто-то посмотрел. /// Вызывается до прохождения различных проверок на цели. Если вдруг требуются собственная ручная проверка /// -/// Смотрящий -public sealed class SimpleEntitySeenEvent(EntityUid viewer) : EntityEventArgs -{ - public readonly EntityUid Viewer = viewer; -} +/// Смотрящий +[ByRefEvent] +public readonly record struct SimpleEntitySeenEvent(EntityUid Viewer); diff --git a/Content.Shared/_Scp/Watching/EyeWatchingSystem.Events.cs b/Content.Shared/_Scp/Watching/EyeWatchingSystem.Events.cs new file mode 100644 index 00000000000..217df44ed0e --- /dev/null +++ b/Content.Shared/_Scp/Watching/EyeWatchingSystem.Events.cs @@ -0,0 +1,32 @@ +using Robust.Shared; +using Robust.Shared.Configuration; + +namespace Content.Shared._Scp.Watching; + +public sealed partial class EyeWatchingSystem +{ + [Dependency] private readonly IConfigurationManager _cfg = default!; + + private void InitializeEvents() + { + SubscribeLocalEvent(OnMapInit); + + _cfg.OnValueChanged(CVars.NetMaxUpdateRange, OnPvsRageChanged, true); + } + + private void ShutdownEvents() + { + _cfg.UnsubValueChanged(CVars.NetMaxUpdateRange, OnPvsRageChanged); + } + + private void OnMapInit(Entity ent, ref MapInitEvent args) + { + SetNextTime(ent); + } + + private void OnPvsRageChanged(float newRange) + { + // Потому что игрок в середине экрана, а SeeRange работает как радиус. + SeeRange = newRange / 2f; + } +} diff --git a/Content.Shared/_Scp/Watching/WatchingTargetComponent.cs b/Content.Shared/_Scp/Watching/WatchingTargetComponent.cs index 21f3fe85c85..cba3cedeee3 100644 --- a/Content.Shared/_Scp/Watching/WatchingTargetComponent.cs +++ b/Content.Shared/_Scp/Watching/WatchingTargetComponent.cs @@ -6,7 +6,7 @@ namespace Content.Shared._Scp.Watching; /// Компонент-маркер, который позволяет системе смотрения включить владельца в обработку /// Это позволит вызывать ивенты на владельце, когда на него кто-то посмотрит /// -[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, AutoGenerateComponentPause] public sealed partial class WatchingTargetComponent : Component { /// @@ -16,5 +16,24 @@ public sealed partial class WatchingTargetComponent : Component [AutoNetworkedField] public Dictionary AlreadyLookedAt = new(); - public TimeSpan NextTimeWatchedCheck; + /// + /// Время между проверками зрения + /// + [DataField] + public TimeSpan WatchingCheckInterval = TimeSpan.FromSeconds(0.3f); + + /// + /// Время следующей проверки зрения + /// + [AutoNetworkedField, AutoPausedField, ViewVariables] + public TimeSpan? NextTimeWatchedCheck; + + /// + /// Будет ли система обрабатывать только простые ивенты, исключая комплексные проверки и ивенты после них? + /// + /// + /// Используется, когда другие системы вручную обрабатывают данные, чтобы исключить двойную работу + /// + [DataField] + public bool SimpleMode; } diff --git a/Resources/Prototypes/_Scp/Entities/Mobs/Player/Scp/Main/scp096.yml b/Resources/Prototypes/_Scp/Entities/Mobs/Player/Scp/Main/scp096.yml index a8e73c7f29c..5945c4142b1 100644 --- a/Resources/Prototypes/_Scp/Entities/Mobs/Player/Scp/Main/scp096.yml +++ b/Resources/Prototypes/_Scp/Entities/Mobs/Player/Scp/Main/scp096.yml @@ -71,6 +71,8 @@ canStandingState: true - type: FearSource uponSeenState: Fear + - type: WatchingTarget + simpleMode: true - type: ShowBlinkable - type: ScpAnnounceOnSpawn text: scp096-announce-on-spawn From 0d993b9445b73b7c028ad0075e1adce2432a2b84 Mon Sep 17 00:00:00 2001 From: drdth Date: Mon, 16 Mar 2026 05:25:03 +0300 Subject: [PATCH 02/17] refactor: huge eye watching optimizations --- Content.Server/_Scp/Fear/FearSystem.Fears.cs | 11 +- Content.Server/_Scp/Fear/FearSystem.cs | 11 +- .../Madness/ArtifactScp096MadnessComponent.cs | 2 +- .../Madness/ArtifactScp096MadnessSystem.cs | 22 +- Content.Server/_Scp/Scp173/Scp173System.cs | 6 +- .../_Scp/Blinking/SharedBlinkingSystem.cs | 12 +- .../Fear/Systems/SharedFearSystem.Helpers.cs | 15 +- .../_Scp/Fear/Systems/SharedFearSystem.cs | 2 +- Content.Shared/_Scp/Helpers/ScpHelpers.cs | 32 +-- .../Systems/SharedScp106System.Phantom.cs | 2 +- .../_Scp/Scp173/SharedScp173System.cs | 62 ++++-- .../_Scp/Watching/EyeWatching.Pools.cs | 57 +++++ Content.Shared/_Scp/Watching/EyeWatching.cs | 22 +- .../_Scp/Watching/EyeWatchingSystem.API.cs | 202 +++++++++++------- 14 files changed, 309 insertions(+), 149 deletions(-) create mode 100644 Content.Shared/_Scp/Watching/EyeWatching.Pools.cs diff --git a/Content.Server/_Scp/Fear/FearSystem.Fears.cs b/Content.Server/_Scp/Fear/FearSystem.Fears.cs index 1e99ee4e4e2..71474ff095c 100644 --- a/Content.Server/_Scp/Fear/FearSystem.Fears.cs +++ b/Content.Server/_Scp/Fear/FearSystem.Fears.cs @@ -31,6 +31,9 @@ public sealed partial class FearSystem private readonly List _hemophobiaBloodList = []; + private readonly List> _moodList = []; + private readonly HashSet> _moodSearchList = []; + private void InitializeFears() { SubscribeLocalEvent(OnMobStateChanged); @@ -55,9 +58,11 @@ private void OnMobStateChanged(MobStateChangedEvent ev) if (!activated) return; - var whoSaw = _watching.GetAllEntitiesVisibleTo(ev.Target); + _moodList.Clear(); + if (!_watching.TryGetAllEntitiesVisibleTo(ev.Target, _moodList, _moodSearchList, flags: LookupFlags.Dynamic)) + return; - foreach (var uid in whoSaw) + foreach (var uid in _moodList) { AddNegativeMoodEffect(uid, MoodSomeoneDiedOnMyEyes); } @@ -80,7 +85,7 @@ private void UpdateHemophobia() continue; _hemophobiaBloodList.Clear(); - var bloodAmount = _helpers.GetAroundSolutionVolume(uid, hemophobia.Reagent, in _hemophobiaBloodList); + var bloodAmount = _helpers.GetAroundSolutionVolume(uid, hemophobia.Reagent, _hemophobiaBloodList); var requiredBloodAmount = hemophobia.BloodRequiredPerState[fear.State]; if (bloodAmount <= requiredBloodAmount) diff --git a/Content.Server/_Scp/Fear/FearSystem.cs b/Content.Server/_Scp/Fear/FearSystem.cs index afe71f1ba63..9dddfd886ed 100644 --- a/Content.Server/_Scp/Fear/FearSystem.cs +++ b/Content.Server/_Scp/Fear/FearSystem.cs @@ -1,5 +1,4 @@ -using System.Linq; -using Content.Shared._Scp.Fear; +using Content.Shared._Scp.Fear; using Content.Shared._Scp.Fear.Components; using Content.Shared._Scp.Fear.Systems; using Content.Shared._Sunrise.Mood; @@ -18,6 +17,9 @@ public sealed partial class FearSystem : SharedFearSystem private static readonly TimeSpan CalmDownCheckCooldown = TimeSpan.FromSeconds(1f); private TimeSpan _nextCalmDownCheck = TimeSpan.Zero; + private readonly List> _fearSourceList = []; + private readonly HashSet> _fearSourceSearchList = []; + public override void Initialize() { base.Initialize(); @@ -80,11 +82,10 @@ public bool TryCalmDown(Entity ent) if (_activeFearEffects.HasComp(ent)) return false; - var visibleFearSources = _watching.GetAllVisibleTo(ent.Owner, ent.Comp.SeenBlockerLevel); - // Проверка на то, что мы в данный момент не смотрим на какую-то страшную сущность. // Нельзя успокоиться, когда мы смотрим на источник страха. - if (visibleFearSources.Any()) + _fearSourceList.Clear(); + if (!_watching.TryGetAllEntitiesVisibleTo(ent.Owner, _fearSourceList, _fearSourceSearchList, ent.Comp.SeenBlockerLevel)) return false; var newFearState = GetDecreasedLevel(ent.Comp.State); diff --git a/Content.Server/_Scp/Research/Artifacts/Effects/_ScpSpecific/Scp096/Madness/ArtifactScp096MadnessComponent.cs b/Content.Server/_Scp/Research/Artifacts/Effects/_ScpSpecific/Scp096/Madness/ArtifactScp096MadnessComponent.cs index ae57cfcc523..f4dec26d28f 100644 --- a/Content.Server/_Scp/Research/Artifacts/Effects/_ScpSpecific/Scp096/Madness/ArtifactScp096MadnessComponent.cs +++ b/Content.Server/_Scp/Research/Artifacts/Effects/_ScpSpecific/Scp096/Madness/ArtifactScp096MadnessComponent.cs @@ -7,5 +7,5 @@ public sealed partial class ArtifactScp096MadnessComponent : Component public float Radius = 12f; [DataField] - public float Percent = 70f; + public float Percent = 0.7f; } diff --git a/Content.Server/_Scp/Research/Artifacts/Effects/_ScpSpecific/Scp096/Madness/ArtifactScp096MadnessSystem.cs b/Content.Server/_Scp/Research/Artifacts/Effects/_ScpSpecific/Scp096/Madness/ArtifactScp096MadnessSystem.cs index e432cbcfd91..12b657fad3c 100644 --- a/Content.Server/_Scp/Research/Artifacts/Effects/_ScpSpecific/Scp096/Madness/ArtifactScp096MadnessSystem.cs +++ b/Content.Server/_Scp/Research/Artifacts/Effects/_ScpSpecific/Scp096/Madness/ArtifactScp096MadnessSystem.cs @@ -1,6 +1,7 @@ -using System.Linq; -using Content.Server._Scp.Scp096; +using Content.Server._Scp.Scp096; using Content.Server._Sunrise.Helpers; +using Content.Shared._Scp.Blinking; +using Content.Shared._Scp.Proximity; using Content.Shared._Scp.Scp096.Main.Components; using Content.Shared._Scp.ScpMask; using Content.Shared._Scp.Watching; @@ -19,17 +20,22 @@ public sealed class ArtifactScp096MadnessSystem : BaseXAESystem> _list = []; + protected override void OnActivated(Entity ent, ref XenoArtifactNodeActivatedEvent args) { if (!_helpers.TryGetFirst(out var scp096)) return; - var targets = _watching.GetWatchers(ent.Owner) - .ToList() - .ShuffleRobust(_random) - .TakePercentage(ent.Comp.Percent); + _list.Clear(); + if (!_watching.TryGetAllEntitiesVisibleTo(scp096.Value.Owner, + _list, + LineOfSightBlockerLevel.Solid, + LookupFlags.Dynamic)) + return; - foreach (var target in targets) + _list.ShuffleRobust(_random).TakePercentage(ent.Comp.Percent); + foreach (var target in _list) { if (!_scp096.TryAddTarget(scp096.Value, target, true, true)) continue; @@ -37,5 +43,7 @@ protected override void OnActivated(Entity ent, // TODO: Пофиксить разрыв маски много раз _scpMask.TryTear(scp096.Value); } + + // TODO: Звук } } diff --git a/Content.Server/_Scp/Scp173/Scp173System.cs b/Content.Server/_Scp/Scp173/Scp173System.cs index c070cb8ca0a..519c88f9587 100644 --- a/Content.Server/_Scp/Scp173/Scp173System.cs +++ b/Content.Server/_Scp/Scp173/Scp173System.cs @@ -103,7 +103,7 @@ private void OnStructureDamage(Entity uid, ref Scp173DamageStru return; } - if (Watching.IsWatched(uid.Owner)) + if (Watching.IsWatched(uid)) { var message = Loc.GetString("scp173-fast-movement-too-many-watchers"); _popup.PopupEntity(message, uid, uid, PopupType.LargeCaution); @@ -185,7 +185,7 @@ private void OnClog(Entity ent, ref Scp173ClogAction args) return; } - if (Watching.IsWatched(ent.Owner)) + if (Watching.IsWatched(ent)) { var message = Loc.GetString("scp173-fast-movement-too-many-watchers"); _popup.PopupEntity(message, ent, ent, PopupType.LargeCaution); @@ -253,7 +253,7 @@ private void OnFastMovement(Entity ent, ref Scp173FastMovementA return; } - if (Watching.IsWatched(ent.Owner, out var watchersCount) && watchersCount > ent.Comp.MaxWatchers) + if (Watching.IsWatched(ent, out var watchersCount) && watchersCount > ent.Comp.MaxWatchers) { var message = Loc.GetString("scp173-fast-movement-too-many-watchers"); _popup.PopupEntity(message, ent, ent, PopupType.LargeCaution); diff --git a/Content.Shared/_Scp/Blinking/SharedBlinkingSystem.cs b/Content.Shared/_Scp/Blinking/SharedBlinkingSystem.cs index 9d0b0ec5563..c5c1ac0cf05 100644 --- a/Content.Shared/_Scp/Blinking/SharedBlinkingSystem.cs +++ b/Content.Shared/_Scp/Blinking/SharedBlinkingSystem.cs @@ -1,5 +1,4 @@ using System.Linq; -using Content.Shared._Scp.Scp096.Main.Components; using Content.Shared._Scp.Scp173; using Content.Shared._Scp.Watching; using Content.Shared._Sunrise.Random; @@ -25,6 +24,9 @@ public abstract partial class SharedBlinkingSystem : EntitySystem [Dependency] private readonly IGameTiming _timing = default!; [Dependency] private readonly INetManager _net = default!; + private readonly HashSet> _scp173SearchList = []; + private readonly List> _scp173List = []; + public override void Initialize() { base.Initialize(); @@ -255,11 +257,11 @@ protected void UpdateAlert(Entity ent) protected bool IsScpNearby(EntityUid player) { // Получаем всех Scp с механиками зрения, которые видят игрока - var allScp173InView = _watching.GetAllVisibleTo(player); - var allScp096InView = _watching.GetAllVisibleTo(player); + _scp173List.Clear(); + if (!_watching.TryGetAllEntitiesVisibleTo(player, _scp173List, _scp173SearchList)) + return false; - return allScp173InView.Any(e => _watching.CanBeWatched(player, e)) - || allScp096InView.Any(e => _watching.CanBeWatched(player, e)); + return _scp173List.Any(e => _watching.CanBeWatched(player, e)); } } diff --git a/Content.Shared/_Scp/Fear/Systems/SharedFearSystem.Helpers.cs b/Content.Shared/_Scp/Fear/Systems/SharedFearSystem.Helpers.cs index 5dbb0d18911..15b630acc76 100644 --- a/Content.Shared/_Scp/Fear/Systems/SharedFearSystem.Helpers.cs +++ b/Content.Shared/_Scp/Fear/Systems/SharedFearSystem.Helpers.cs @@ -15,16 +15,23 @@ public abstract partial class SharedFearSystem private const int GenericFearBasedScaleModifier = 2; + private readonly HashSet> _fearSourceSearchList = []; + private readonly List> _fearSourceList = []; + /// /// Подсвечивает все сущности, которые вызывают страх. /// Сущности, чей уровень страха при видимости отличается от текущего уровня страха не будет подсвечены. /// private void HighLightAllVisibleFears(Entity ent) { - var visibleFearSources = - _watching.GetAllEntitiesVisibleTo(ent.Owner, ent.Comp.SeenBlockerLevel); - - foreach (var source in visibleFearSources) + _fearSourceList.Clear(); + if (!_watching.TryGetAllEntitiesVisibleTo(ent.Owner, + _fearSourceList, + _fearSourceSearchList, + ent.Comp.SeenBlockerLevel)) + return; + + foreach (var source in _fearSourceList) { if (source.Comp.UponSeenState != ent.Comp.State) continue; diff --git a/Content.Shared/_Scp/Fear/Systems/SharedFearSystem.cs b/Content.Shared/_Scp/Fear/Systems/SharedFearSystem.cs index ecfee58a2ea..8d600bbf8cd 100644 --- a/Content.Shared/_Scp/Fear/Systems/SharedFearSystem.cs +++ b/Content.Shared/_Scp/Fear/Systems/SharedFearSystem.cs @@ -139,7 +139,7 @@ private void OnProximityInRange(Entity ent, ref ProximityInRangeT return; // Проверка на зрение, чтобы можно было закрыть глазки и было не страшно - if (!_watching.SimpleIsWatchedBy(args.Receiver, [ent])) + if (!_watching.SimpleIsWatchedBy(args.Receiver, ent)) return; // Если текущий уровень страха выше, чем тот, что мы хотим поставить, diff --git a/Content.Shared/_Scp/Helpers/ScpHelpers.cs b/Content.Shared/_Scp/Helpers/ScpHelpers.cs index 9cfdff50daf..197682cffa1 100644 --- a/Content.Shared/_Scp/Helpers/ScpHelpers.cs +++ b/Content.Shared/_Scp/Helpers/ScpHelpers.cs @@ -7,31 +7,33 @@ namespace Content.Shared._Scp.Helpers; -// TODO: Использовать оптимизации GC после внедрения их в EyeWatchingSystem - public sealed class ScpHelpers : EntitySystem { [Dependency] private readonly EyeWatchingSystem _watching = default!; + private readonly HashSet> _puddleSearchList = []; + private readonly List> _puddleList = []; + /// /// Получает суммарное количество реагента в зоне видимости сущности. /// Возвращает количество реагентов. /// public FixedPoint2 GetAroundSolutionVolume(EntityUid uid, ProtoId reagent, - in List puddleList, + List puddleList, LineOfSightBlockerLevel lineOfSight = LineOfSightBlockerLevel.Transparent) { - FixedPoint2 total = 0; - var puddles = _watching.GetAllEntitiesVisibleTo(uid, lineOfSight); + _puddleList.Clear(); + if (!_watching.TryGetAllEntitiesVisibleTo(uid, _puddleList, _puddleSearchList, lineOfSight, LookupFlags.Static)) + return FixedPoint2.Zero; - foreach (var puddle in puddles) + FixedPoint2 total = 0; + foreach (var puddle in _puddleList) { if (!puddle.Comp.Solution.HasValue) continue; var solution = puddle.Comp.Solution.Value.Comp.Solution; - foreach (var (reagentId, quantity) in solution.Contents) { if (reagentId.Prototype != reagent) @@ -53,10 +55,12 @@ public FixedPoint2 GetAroundSolutionVolume(EntityUid uid, ProtoId reagent, LineOfSightBlockerLevel lineOfSight = LineOfSightBlockerLevel.Transparent) { - FixedPoint2 total = 0; - var puddles = _watching.GetAllEntitiesVisibleTo(uid, lineOfSight); + _puddleList.Clear(); + if (!_watching.TryGetAllEntitiesVisibleTo(uid, _puddleList, _puddleSearchList, lineOfSight, LookupFlags.Static)) + return FixedPoint2.Zero; - foreach (var puddle in puddles) + FixedPoint2 total = 0; + foreach (var puddle in _puddleList) { if (!puddle.Comp.Solution.HasValue) continue; @@ -80,10 +84,12 @@ public bool IsAroundSolutionVolumeGreaterThan(EntityUid uid, FixedPoint2 required, LineOfSightBlockerLevel lineOfSight = LineOfSightBlockerLevel.Transparent) { - FixedPoint2 total = 0; - var puddles = _watching.GetAllEntitiesVisibleTo(uid, lineOfSight); + _puddleList.Clear(); + if (!_watching.TryGetAllEntitiesVisibleTo(uid, _puddleList, _puddleSearchList, lineOfSight, LookupFlags.Static)) + return false; - foreach (var puddle in puddles) + FixedPoint2 total = 0; + foreach (var puddle in _puddleList) { if (!puddle.Comp.Solution.HasValue) continue; diff --git a/Content.Shared/_Scp/Scp106/Systems/SharedScp106System.Phantom.cs b/Content.Shared/_Scp/Scp106/Systems/SharedScp106System.Phantom.cs index 0210cdc4edd..fae99922bc9 100644 --- a/Content.Shared/_Scp/Scp106/Systems/SharedScp106System.Phantom.cs +++ b/Content.Shared/_Scp/Scp106/Systems/SharedScp106System.Phantom.cs @@ -101,7 +101,7 @@ private void OnExamined(Entity ent, ref ExaminedEvent ar if (!_mob.IsAlive(args.Examiner)) return; - if (!_watching.SimpleIsWatchedBy(ent.Owner, [args.Examiner])) + if (!_watching.SimpleIsWatchedBy(ent.Owner, args.Examiner)) return; // Ликвидируйся diff --git a/Content.Shared/_Scp/Scp173/SharedScp173System.cs b/Content.Shared/_Scp/Scp173/SharedScp173System.cs index 8d9bd65a644..5b67e70653e 100644 --- a/Content.Shared/_Scp/Scp173/SharedScp173System.cs +++ b/Content.Shared/_Scp/Scp173/SharedScp173System.cs @@ -18,6 +18,7 @@ namespace Content.Shared._Scp.Scp173; +// TODO: Выделить логику блокировки движения при смотрении в отдельную систему со своим компонентом. public abstract class SharedScp173System : EntitySystem { [Dependency] protected readonly IGameTiming Timing = default!; @@ -29,19 +30,17 @@ public abstract class SharedScp173System : EntitySystem [Dependency] private readonly SharedDoAfterSystem _doAfter = default!; [Dependency] private readonly SharedPopupSystem _popup = default!; - protected static readonly TimeSpan ReagentCheckInterval = TimeSpan.FromSeconds(1); + protected static readonly TimeSpan ReagentCheckInterval = TimeSpan.FromSeconds(1f); public const float ContainmentRoomSearchRadius = 8f; + private readonly List> _blinkableList = []; + public override void Initialize() { base.Initialize(); - SubscribeLocalEvent((uid, _, args) => - { - if (Watching.IsWatched(uid)) - args.Cancel(); - }); + SubscribeLocalEvent(OnAttackAttempt); SubscribeLocalEvent(OnDirectionAttempt); SubscribeLocalEvent(OnMoveAttempt); @@ -54,16 +53,37 @@ public override void Initialize() #region Movement + private void OnAttackAttempt(Entity ent, ref AttackAttemptEvent args) + { + if (IsInScpCage(ent, out _)) + { + args.Cancel(); + return; + } + + if (Watching.IsWatched(ent.Owner)) + { + args.Cancel(); + return; + } + } + private void OnDirectionAttempt(Entity ent, ref ChangeDirectionAttemptEvent args) { if (Watching.IsWatched(ent.Owner) && !IsInScpCage(ent, out _)) + { args.Cancel(); + return; + } } private void OnMoveAttempt(Entity ent, ref UpdateCanMoveEvent args) { if (Watching.IsWatched(ent.Owner) && !IsInScpCage(ent, out _)) + { args.Cancel(); + return; + } } private void OnMoveInput(Entity ent, ref MoveInputEvent args) @@ -123,11 +143,13 @@ private void OnBlind(Entity ent, ref Scp173StartBlind args) public void BlindEveryoneInRange(EntityUid scp, TimeSpan time, bool predicted = true) { - var eyes = Watching.GetWatchers(scp); + _blinkableList.Clear(); + if (!Watching.TryGetAllEntitiesVisibleTo(scp, _blinkableList)) + return; - foreach (var eye in eyes) + foreach (var eye in _blinkableList) { - _blinking.ForceBlind(eye, time, predicted); + _blinking.ForceBlind(eye.AsNullable(), time, predicted); } // TODO: Add sound. @@ -186,7 +208,7 @@ private bool CanBlind(EntityUid uid, bool showPopups = true) return false; } - if (watchers.Count <= 3) + if (watchers <= 3) { if (showPopups) _popup.PopupClient(Loc.GetString("scp173-blind-failed-too-few-watchers"), uid, uid); @@ -197,15 +219,23 @@ private bool CanBlind(EntityUid uid, bool showPopups = true) return true; } - public bool IsWatched(EntityUid target, out HashSet viewers) + public bool IsWatched(EntityUid scp, out int viewersCount) { - var watchers = Watching.GetWatchers(target); + viewersCount = 0; + + _blinkableList.Clear(); + if (!Watching.TryGetAllEntitiesVisibleTo(scp, _blinkableList)) + return false; - viewers = watchers - .Where(eye => Watching.CanBeWatched(eye, target)) - .ToHashSet(); + foreach (var viewer in _blinkableList) + { + if (!Watching.CanBeWatched(viewer.AsNullable(), scp)) + continue; + + viewersCount++; + } - return viewers.Count != 0; + return viewersCount != 0; } #endregion diff --git a/Content.Shared/_Scp/Watching/EyeWatching.Pools.cs b/Content.Shared/_Scp/Watching/EyeWatching.Pools.cs new file mode 100644 index 00000000000..00e689ff588 --- /dev/null +++ b/Content.Shared/_Scp/Watching/EyeWatching.Pools.cs @@ -0,0 +1,57 @@ +using Content.Shared._Scp.Blinking; + +namespace Content.Shared._Scp.Watching; + +public sealed partial class EyeWatchingSystem +{ + private readonly Stack> _uidListPool = new(); + private readonly Stack>> _blinkableListPool = new(); + private readonly Stack>> _blinkableSetPool = new(); + + #region Rent + + private List RentUidList() + { + return _uidListPool.TryPop(out var list) + ? list + : []; + } + + private List> RentBlinkableList() + { + return _blinkableListPool.TryPop(out var list) + ? list + : []; + } + + private HashSet> RentBlinkableSet() + { + return _blinkableSetPool.TryPop(out var set) + ? set + : []; + } + + #endregion + + #region Return + + private void ReturnUidList(List list) + { + list.Clear(); + _uidListPool.Push(list); + } + + private void ReturnBlinkableList(List> list) + { + list.Clear(); + _blinkableListPool.Push(list); + } + + private void ReturnBlinkableSet(HashSet> set) + { + set.Clear(); + _blinkableSetPool.Push(set); + } + + #endregion +} diff --git a/Content.Shared/_Scp/Watching/EyeWatching.cs b/Content.Shared/_Scp/Watching/EyeWatching.cs index 19ff8b2ab93..3cd79621a8f 100644 --- a/Content.Shared/_Scp/Watching/EyeWatching.cs +++ b/Content.Shared/_Scp/Watching/EyeWatching.cs @@ -1,6 +1,5 @@ -using Content.Shared._Scp.Proximity; -using Content.Shared.Mobs.Components; -using Content.Shared.Storage.Components; +using Content.Shared._Scp.Blinking; +using Content.Shared._Scp.Proximity; using Robust.Shared.Timing; namespace Content.Shared._Scp.Watching; @@ -14,18 +13,18 @@ public sealed partial class EyeWatchingSystem : EntitySystem [Dependency] private readonly ProximitySystem _proximity = default!; [Dependency] private readonly IGameTiming _timing = default!; + private readonly HashSet> _searchList = []; + /// /// Радиус, в котором сущности могут увидеть друг друга. /// [ViewVariables] - public float SeeRange = 16f; + public float SeeRange { get; private set; } = 16f; public override void Initialize() { + InitializeApi(); InitializeEvents(); - - _mobStateQuery = GetEntityQuery(); - _insideStorageQuery = GetEntityQuery(); } public override void Shutdown() @@ -50,7 +49,9 @@ public override void Update(float frameTime) continue; // Все потенциально возможные смотрящие. Среди них те, что прошли фаст-чек из самых простых проверок - var potentialViewers = GetWatchers(uid); + var potentialViewers = new List> (); + if (!TryGetAllEntitiesVisibleTo(uid, potentialViewers, _searchList)) + continue; // Вызываем ивенты на потенциально смотрящих. Без особых проверок // Полезно в коде, который уже использует подобные проверки или не требует этого @@ -71,11 +72,12 @@ public override void Update(float frameTime) // Проверяет всех потенциальных смотрящих на то, действительно ли они видят цель. // Каждый потенциально смотрящий проходит полный комплекс проверок. // Выдает полный список всех сущностей, кто действительно видит цель - if (!IsWatchedBy(uid, potentialViewers, viewers: out var viewers)) + var realViewers = new List> (); + if (!IsWatchedBy(uid, potentialViewers, realViewers)) continue; // Вызываем ивент на смотрящем, говорящие, что он действительно видит цель - foreach (var viewer in viewers) + foreach (var viewer in realViewers) { var netViewer = GetNetEntity(viewer); var firstTime = !watchingComponent.AlreadyLookedAt.ContainsKey(netViewer); diff --git a/Content.Shared/_Scp/Watching/EyeWatchingSystem.API.cs b/Content.Shared/_Scp/Watching/EyeWatchingSystem.API.cs index 9ebfc9766c9..0aee815745e 100644 --- a/Content.Shared/_Scp/Watching/EyeWatchingSystem.API.cs +++ b/Content.Shared/_Scp/Watching/EyeWatchingSystem.API.cs @@ -1,5 +1,4 @@ -using System.Diagnostics.CodeAnalysis; -using System.Linq; +using System.Linq; using Content.Shared._Scp.Blinking; using Content.Shared._Scp.Proximity; using Content.Shared._Scp.Watching.FOV; @@ -10,9 +9,6 @@ namespace Content.Shared._Scp.Watching; -// TODO: Оптимизации Garbage Collector посредством возможности передать заранее готовый список сущностей. -// Вместо создания каждый раз нового система будет использовать список, который заготовлен заранее и будет очищаться перед повторным использованием. - public sealed partial class EyeWatchingSystem { [Dependency] private readonly SharedBlinkingSystem _blinking = default!; @@ -23,18 +19,32 @@ public sealed partial class EyeWatchingSystem private EntityQuery _mobStateQuery; private EntityQuery _insideStorageQuery; - /// - /// Проверяет, смотрит ли кто-то на указанную цель - /// - /// Цель, которую проверяем - /// Нужно ли проверять поле зрения - /// Если нужно использовать другой угол обзора, отличный от стандартного - /// Смотрит ли на цель хоть кто-то - public bool IsWatched(Entity ent, bool useFov = true, float? fovOverride = null) + private void InitializeApi() + { + _mobStateQuery = GetEntityQuery(); + _insideStorageQuery = GetEntityQuery(); + } + + public bool IsWatched(EntityUid ent, bool useFov = true, float? fovOverride = null) { return IsWatched(ent, out _, useFov, fovOverride); } + public bool IsWatched(EntityUid ent, out int watchersCount, bool useFov = true, float? fovOverride = null) + { + watchersCount = 0; + var potentialWatchers = RentBlinkableList(); + var searchSet = RentBlinkableSet(); + + var result = IsWatched(ent, potentialWatchers, searchSet, useFov , fovOverride); + watchersCount = potentialWatchers.Count; + + ReturnBlinkableList(potentialWatchers); + ReturnBlinkableSet(searchSet); + + return result; + } + /// /// Проверяет, смотрит ли кто-то на указанную цель /// @@ -43,45 +53,29 @@ public bool IsWatched(Entity ent, bool useFov = true, float /// Нужно ли проверять поле зрения /// Если нужно использовать другой угол обзора, отличный от стандартного /// Смотрит ли на цель хоть кто-то - public bool IsWatched(EntityUid ent, [NotNullWhen(true)] out int? watchersCount, bool useFov = true, float? fovOverride = null) + public bool IsWatched(EntityUid ent, + List> potentialWatchers, + HashSet> searchSet, + bool useFov = true, + float? fovOverride = null) { - var eyes = GetWatchers(ent); - - var isWatched = IsWatchedBy(ent, eyes, out int count , useFov, fovOverride); - watchersCount = count; + if (!TryGetAllEntitiesVisibleTo(ent, potentialWatchers, searchSet)) + return false; - return isWatched; + return IsWatchedBy(ent, potentialWatchers , useFov, fovOverride); } - /// - /// Получает и возвращает всех потенциально смотрящих на указанную цель. - /// - /// - /// В методе нет проверок на дополнительные состояния, такие как моргание/закрыты ли глаза/поле зрения т.п. - /// Единственная проверка - можно ли физически увидеть цель(т.е. не закрыта ли она стеной и т.п.) - /// - /// Цель, для которой ищем потенциальных смотрящих - /// Список всех, кто потенциально видит цель - public IEnumerable GetWatchers(Entity ent) + public bool TryGetAllEntitiesVisibleTo( + Entity ent, + List> potentialWatchers, + LineOfSightBlockerLevel type = LineOfSightBlockerLevel.Transparent, + LookupFlags flags = LookupFlags.All) { - return GetAllVisibleTo(ent); - } + var searchSet = RentBlinkableSet(); + var result = TryGetAllEntitiesVisibleTo(ent, potentialWatchers, searchSet, type, flags); + ReturnBlinkableSet(searchSet); - /// - /// Получает и возвращает всех потенциально смотрящих на указанную цель. - /// - /// - /// В методе нет проверок на дополнительные состояния, такие как моргание/закрыты ли глаза/поле зрения т.п. - /// Единственная проверка - можно ли физически увидеть цель(т.е. не закрыта ли она стеной и т.п.) - /// - /// Цель, для которой ищем потенциальных смотрящих\ - /// Требуемая прозрачность линии видимости. - /// Список всех, кто потенциально видит цель - public IEnumerable GetAllVisibleTo(Entity ent, LineOfSightBlockerLevel type = LineOfSightBlockerLevel.Transparent) - where T : IComponent - { - return GetAllEntitiesVisibleTo(ent, type) - .Select(e => e.Owner); + return result; } /// @@ -91,53 +85,86 @@ public IEnumerable GetAllVisibleTo(Entity ent /// В методе нет проверок на дополнительные состояния, такие как моргание/закрыты ли глаза/поле зрения т.п. /// Единственная проверка - можно ли физически увидеть цель(т.е. не закрыта ли она стеной и т.п.) /// - /// Цель, для которой ищем потенциальных смотрящих\ + /// Цель, для которой ищем потенциальных смотрящих + /// Список всех, кто потенциально видит цель /// Требуемая прозрачность линии видимости. - /// Список всех, кто потенциально видит цель - public IEnumerable> GetAllEntitiesVisibleTo(Entity ent, LineOfSightBlockerLevel type = LineOfSightBlockerLevel.Transparent) + /// Заранее заготовленный список, который будет использоваться в + /// Список флагов для поиска целей в + /// Удалось ли найти хоть кого-то + public bool TryGetAllEntitiesVisibleTo( + Entity ent, + List> potentialWatchers, + HashSet> searchSet, + LineOfSightBlockerLevel type = LineOfSightBlockerLevel.Transparent, + LookupFlags flags = LookupFlags.All) where T : IComponent { if (!Resolve(ent.Owner, ref ent.Comp)) - return []; + return false; + + searchSet.Clear(); + _lookup.GetEntitiesInRange(ent.Comp.Coordinates, SeeRange, searchSet, flags); + + foreach (var target in searchSet) + { + if (target.Owner == ent.Owner) + continue; + + if (!_proximity.IsRightType(ent, target, type, out _)) + continue; + + potentialWatchers.Add(target); + } - return _lookup.GetEntitiesInRange(ent.Comp.Coordinates, SeeRange) - .Where(eye => _proximity.IsRightType(ent, eye, type, out _)) - .Where(e => e.Owner != ent.Owner); + return potentialWatchers.Count != 0; } /// - /// Проверяет, смотрят ли переданные сущности на указанную цель + /// Проверяет, смотрят ли переданные сущности на указанную цель. Передает список всех сущностей, что действительно смотрят на цель /// /// Цель - /// Список сущностей для проверки - /// Количество смотрящих + /// Список сущностей для проверки + /// Список всех сущностей, что действительно смотрят на цель /// Нужно ли проверять, находится ли цель в поле зрения сущности /// Если нужно перезаписать угол поля зрения /// Смотрит ли хоть кто-то на цель - public bool IsWatchedBy(EntityUid target, IEnumerable watchers, out int watchersCount, bool useFov = true, float? fovOverride = null) + public bool IsWatchedBy(EntityUid target, + List> potentialViewers, + List> realViewers, + bool useFov = true, + float? fovOverride = null) { - var isWatched = IsWatchedBy(target, watchers, out IEnumerable viewers, useFov, fovOverride); - watchersCount = viewers.Count(); + foreach (var viewer in potentialViewers) + { + if (!CanBeWatched(viewer.AsNullable(), target)) + continue; + + if (IsEyeBlinded(viewer.AsNullable(), target, useFov, fovOverride)) + continue; - return isWatched; + realViewers.Add(viewer); + } + + return realViewers.Any(); } - /// - /// Проверяет, смотрят ли переданные сущности на указанную цель. Передает список всех сущностей, что действительно смотрят на цель - /// - /// Цель - /// Список сущностей для проверки - /// Список всех сущностей, что действительно смотрят на цель - /// Нужно ли проверять, находится ли цель в поле зрения сущности - /// Если нужно перезаписать угол поля зрения - /// Смотрит ли хоть кто-то на цель - public bool IsWatchedBy(EntityUid target, IEnumerable watchers, out IEnumerable viewers, bool useFov = true, float? fovOverride = null) + public bool IsWatchedBy(EntityUid target, + List> potentialViewers, + bool useFov = true, + float? fovOverride = null) { - viewers = watchers - .Where(eye => CanBeWatched(eye, target)) - .Where(eye => !IsEyeBlinded(eye, target, useFov, fovOverride)); + foreach (var viewer in potentialViewers) + { + if (!CanBeWatched(viewer.AsNullable(), target)) + continue; + + if (IsEyeBlinded(viewer.AsNullable(), target, useFov, fovOverride)) + continue; + + return true; + } - return viewers.Any(); + return false; } /// @@ -145,15 +172,30 @@ public bool IsWatchedBy(EntityUid target, IEnumerable watchers, out I /// Вместо проверки на интервальное моргание используется проверка на мануальное закрытие глаз. /// /// Сущность, на которую смотрят - /// Смотрящие + /// Смотрящие /// Смотри ли хоть кто-нибудь из переданных - public bool SimpleIsWatchedBy(EntityUid target, IEnumerable watchers) + public bool SimpleIsWatchedBy(EntityUid target, List potentialViewers) { - var viewers = watchers - .Where(eye => CanBeWatched(eye, target)) - .Where(eye => !_blinking.AreEyesClosedManually(eye)); + foreach (var viewer in potentialViewers) + { + if (!SimpleIsWatchedBy(target, viewer)) + continue; + + return true; + } - return viewers.Any(); + return false; + } + + public bool SimpleIsWatchedBy(EntityUid target, EntityUid potentialViewer) + { + if (!CanBeWatched(potentialViewer, target)) + return false; + + if (_blinking.AreEyesClosedManually(potentialViewer)) + return false; + + return true; } /// From 6ecfbbc065f35b06b0a6c4059e25c7f11df3d0cc Mon Sep 17 00:00:00 2001 From: drdth Date: Tue, 17 Mar 2026 04:48:33 +0300 Subject: [PATCH 03/17] refactor: optimize eye watching and related systems by list/hashset pooling. --- Content.Server/_Scp/Fear/FearSystem.Fears.cs | 19 +- Content.Server/_Scp/Fear/FearSystem.cs | 9 +- .../Madness/ArtifactScp096MadnessSystem.cs | 14 +- .../_Scp/Blinking/SharedBlinkingSystem.cs | 10 +- .../Fear/Systems/SharedFearSystem.Helpers.cs | 12 +- Content.Shared/_Scp/Helpers/CollectionPool.cs | 187 ++++++++++++++++++ Content.Shared/_Scp/Helpers/ScpHelpers.cs | 21 +- .../_Scp/Scp173/SharedScp173System.cs | 15 +- .../_Scp/Watching/EyeWatching.Pools.cs | 57 ------ Content.Shared/_Scp/Watching/EyeWatching.cs | 17 +- .../_Scp/Watching/EyeWatchingSystem.API.cs | 56 +++--- 11 files changed, 278 insertions(+), 139 deletions(-) create mode 100644 Content.Shared/_Scp/Helpers/CollectionPool.cs delete mode 100644 Content.Shared/_Scp/Watching/EyeWatching.Pools.cs diff --git a/Content.Server/_Scp/Fear/FearSystem.Fears.cs b/Content.Server/_Scp/Fear/FearSystem.Fears.cs index 71474ff095c..ee3e872ce63 100644 --- a/Content.Server/_Scp/Fear/FearSystem.Fears.cs +++ b/Content.Server/_Scp/Fear/FearSystem.Fears.cs @@ -31,9 +31,6 @@ public sealed partial class FearSystem private readonly List _hemophobiaBloodList = []; - private readonly List> _moodList = []; - private readonly HashSet> _moodSearchList = []; - private void InitializeFears() { SubscribeLocalEvent(OnMobStateChanged); @@ -55,15 +52,25 @@ private void OnMobStateChanged(MobStateChangedEvent ev) var toggleUsed = new ItemToggledEvent(false, activated, null); RaiseLocalEvent(ev.Target, ref toggleUsed); + // Если activated = true, значит человек умер. + // Поэтому код ниже требует true, так как реализует логику для смерти. if (!activated) return; - _moodList.Clear(); - if (!_watching.TryGetAllEntitiesVisibleTo(ev.Target, _moodList, _moodSearchList, flags: LookupFlags.Dynamic)) + // TODO: Создать проверки "сколько сущностей с компонентом X видит сущность способная к зрению" + using var potentialViewer = ListPoolEntity.Rent(); + if (!_watching.TryGetAllEntitiesVisibleTo(ev.Target, potentialViewer.Value, flags: LookupFlags.Dynamic)) return; - foreach (var uid in _moodList) + foreach (var uid in potentialViewer.Value) { + // Убийца не будет печалиться смерти убитого + if (uid.Owner == ev.Origin) + continue; + + if (!_watching.IsWatchedBy(uid, ev.Target)) + continue; + AddNegativeMoodEffect(uid, MoodSomeoneDiedOnMyEyes); } } diff --git a/Content.Server/_Scp/Fear/FearSystem.cs b/Content.Server/_Scp/Fear/FearSystem.cs index 9dddfd886ed..b7eb1de1f10 100644 --- a/Content.Server/_Scp/Fear/FearSystem.cs +++ b/Content.Server/_Scp/Fear/FearSystem.cs @@ -1,6 +1,7 @@ using Content.Shared._Scp.Fear; using Content.Shared._Scp.Fear.Components; using Content.Shared._Scp.Fear.Systems; +using Content.Shared._Scp.Helpers; using Content.Shared._Sunrise.Mood; using Content.Shared.Mobs.Components; using Content.Shared.Rejuvenate; @@ -17,9 +18,6 @@ public sealed partial class FearSystem : SharedFearSystem private static readonly TimeSpan CalmDownCheckCooldown = TimeSpan.FromSeconds(1f); private TimeSpan _nextCalmDownCheck = TimeSpan.Zero; - private readonly List> _fearSourceList = []; - private readonly HashSet> _fearSourceSearchList = []; - public override void Initialize() { base.Initialize(); @@ -84,8 +82,9 @@ public bool TryCalmDown(Entity ent) // Проверка на то, что мы в данный момент не смотрим на какую-то страшную сущность. // Нельзя успокоиться, когда мы смотрим на источник страха. - _fearSourceList.Clear(); - if (!_watching.TryGetAllEntitiesVisibleTo(ent.Owner, _fearSourceList, _fearSourceSearchList, ent.Comp.SeenBlockerLevel)) + // TODO: Создать проверки "сколько сущностей с компонентом X видит сущность способная к зрению" + using var fearSources = ListPoolEntity.Rent(); + if (_watching.TryGetAllEntitiesVisibleTo(ent.Owner, fearSources.Value, ent.Comp.SeenBlockerLevel)) return false; var newFearState = GetDecreasedLevel(ent.Comp.State); diff --git a/Content.Server/_Scp/Research/Artifacts/Effects/_ScpSpecific/Scp096/Madness/ArtifactScp096MadnessSystem.cs b/Content.Server/_Scp/Research/Artifacts/Effects/_ScpSpecific/Scp096/Madness/ArtifactScp096MadnessSystem.cs index 12b657fad3c..eac5fab5143 100644 --- a/Content.Server/_Scp/Research/Artifacts/Effects/_ScpSpecific/Scp096/Madness/ArtifactScp096MadnessSystem.cs +++ b/Content.Server/_Scp/Research/Artifacts/Effects/_ScpSpecific/Scp096/Madness/ArtifactScp096MadnessSystem.cs @@ -1,6 +1,7 @@ using Content.Server._Scp.Scp096; using Content.Server._Sunrise.Helpers; using Content.Shared._Scp.Blinking; +using Content.Shared._Scp.Helpers; using Content.Shared._Scp.Proximity; using Content.Shared._Scp.Scp096.Main.Components; using Content.Shared._Scp.ScpMask; @@ -20,22 +21,23 @@ public sealed class ArtifactScp096MadnessSystem : BaseXAESystem> _list = []; - protected override void OnActivated(Entity ent, ref XenoArtifactNodeActivatedEvent args) { if (!_helpers.TryGetFirst(out var scp096)) return; - _list.Clear(); + using var targets = ListPoolEntity.Rent(); if (!_watching.TryGetAllEntitiesVisibleTo(scp096.Value.Owner, - _list, + targets.Value, LineOfSightBlockerLevel.Solid, LookupFlags.Dynamic)) return; - _list.ShuffleRobust(_random).TakePercentage(ent.Comp.Percent); - foreach (var target in _list) + targets.Value + .ShuffleRobust(_random) + .TakePercentage(ent.Comp.Percent); + + foreach (var target in targets.Value) { if (!_scp096.TryAddTarget(scp096.Value, target, true, true)) continue; diff --git a/Content.Shared/_Scp/Blinking/SharedBlinkingSystem.cs b/Content.Shared/_Scp/Blinking/SharedBlinkingSystem.cs index c5c1ac0cf05..a779dc19fad 100644 --- a/Content.Shared/_Scp/Blinking/SharedBlinkingSystem.cs +++ b/Content.Shared/_Scp/Blinking/SharedBlinkingSystem.cs @@ -1,4 +1,5 @@ using System.Linq; +using Content.Shared._Scp.Helpers; using Content.Shared._Scp.Scp173; using Content.Shared._Scp.Watching; using Content.Shared._Sunrise.Random; @@ -24,9 +25,6 @@ public abstract partial class SharedBlinkingSystem : EntitySystem [Dependency] private readonly IGameTiming _timing = default!; [Dependency] private readonly INetManager _net = default!; - private readonly HashSet> _scp173SearchList = []; - private readonly List> _scp173List = []; - public override void Initialize() { base.Initialize(); @@ -257,11 +255,11 @@ protected void UpdateAlert(Entity ent) protected bool IsScpNearby(EntityUid player) { // Получаем всех Scp с механиками зрения, которые видят игрока - _scp173List.Clear(); - if (!_watching.TryGetAllEntitiesVisibleTo(player, _scp173List, _scp173SearchList)) + using var scp173List = ListPoolEntity.Rent(); + if (!_watching.TryGetAllEntitiesVisibleTo(player, scp173List.Value)) return false; - return _scp173List.Any(e => _watching.CanBeWatched(player, e)); + return scp173List.Value.Any(e => _watching.CanBeWatched(player, e)); } } diff --git a/Content.Shared/_Scp/Fear/Systems/SharedFearSystem.Helpers.cs b/Content.Shared/_Scp/Fear/Systems/SharedFearSystem.Helpers.cs index 15b630acc76..30711ea464a 100644 --- a/Content.Shared/_Scp/Fear/Systems/SharedFearSystem.Helpers.cs +++ b/Content.Shared/_Scp/Fear/Systems/SharedFearSystem.Helpers.cs @@ -15,23 +15,17 @@ public abstract partial class SharedFearSystem private const int GenericFearBasedScaleModifier = 2; - private readonly HashSet> _fearSourceSearchList = []; - private readonly List> _fearSourceList = []; - /// /// Подсвечивает все сущности, которые вызывают страх. /// Сущности, чей уровень страха при видимости отличается от текущего уровня страха не будет подсвечены. /// private void HighLightAllVisibleFears(Entity ent) { - _fearSourceList.Clear(); - if (!_watching.TryGetAllEntitiesVisibleTo(ent.Owner, - _fearSourceList, - _fearSourceSearchList, - ent.Comp.SeenBlockerLevel)) + using var fearSources = ListPoolEntity.Rent(); + if (!_watching.TryGetAllEntitiesVisibleTo(ent.Owner, fearSources.Value, ent.Comp.SeenBlockerLevel)) return; - foreach (var source in _fearSourceList) + foreach (var source in fearSources.Value) { if (source.Comp.UponSeenState != ent.Comp.State) continue; diff --git a/Content.Shared/_Scp/Helpers/CollectionPool.cs b/Content.Shared/_Scp/Helpers/CollectionPool.cs new file mode 100644 index 00000000000..b62af7fb147 --- /dev/null +++ b/Content.Shared/_Scp/Helpers/CollectionPool.cs @@ -0,0 +1,187 @@ +namespace Content.Shared._Scp.Helpers; + +/// +/// Provides a static object pool for collections to minimize garbage collection allocations. +/// +/// The type of the collection being pooled. Must implement . +/// The type of the elements contained in the collection. +public static class CollectionPool + where TCollection : class, ICollection +{ + private static readonly Stack Pool = new(); + private static Func? _factory; + + /// + /// Configures the factory function used to instantiate new collections when the pool is empty. + /// + /// The delegate used to create new instances of . + /// Thrown when the provided is null. + public static void Configure(Func factory) + { + _factory = factory ?? throw new InvalidOperationException("Factory cannot be null"); + } + + /// + /// Creates a new collection using the configured factory. + /// + /// A new instance of . + /// Thrown if the pool has not been configured via . + private static TCollection Create() + { + if (_factory is null) + { + throw new InvalidOperationException( + $"CollectionPool<{typeof(TCollection).Name}, {typeof(T).Name}> " + + $"is not configured. Call Configure(factory) before use."); + } + + return _factory(); + } + + /// + /// Rents a collection from the pool. If the pool is empty, a new collection is created. + /// + /// A disposable wrapper. Use within a statement to automatically return the collection to the pool. + public static PooledCollection Rent() + { + return Pool.TryPop(out var collection) + ? new PooledCollection(collection) + : new PooledCollection(Create()); + } + + /// + /// Returns a collection to the pool. Clears the collection before storing it. + /// Collections will be dropped and garbage collected if the pool is full (>= 512 items) or if the collection capacity exceeds 1024. + /// + /// The collection to return to the pool. + internal static void Return(TCollection collection) + { + if (Pool.Count >= 512) + { + Logger.Warning("Pool bloated, new collections will not be generated"); + return; + } + + if (collection is List list && list.Capacity > 1024) + return; + + if (collection is HashSet hashSet && hashSet.Capacity > 1024) + return; + + collection.Clear(); + Pool.Push(collection); + } + + /// + /// An allocation-free disposable wrapper around a rented collection. + /// + public struct PooledCollection : IDisposable + { + private TCollection? _value; + private bool _disposed; + + /// + /// Gets the underlying rented collection. + /// + /// Thrown if accessed after the collection has been returned to the pool. + public TCollection Value + { + get + { + if (_disposed) + throw new InvalidOperationException("The collection has already been returned to the pool."); + + return _value!; + } + } + + internal PooledCollection(TCollection value) + { + _value = value; + _disposed = false; + } + + /// + /// Returns the underlying collection to the pool and marks this wrapper as disposed. + /// + public void Dispose() + { + if (_disposed) + return; + + _disposed = true; + + if (_value is not null) + { + Return(_value); + _value = default; + } + } + } +} + +/// +/// A pre-configured pool specifically for instances. +/// +/// The type of elements in the list. +public static class ListPool +{ + static ListPool() + { + CollectionPool, T>.Configure(() => new List()); + } + + /// + /// Rents a list from the pool. + /// + /// A disposable wrapper containing the rented list. + public static CollectionPool, T>.PooledCollection Rent() + => CollectionPool, T>.Rent(); +} + +/// +/// A helper pool for renting lists of entities with specific components. +/// +/// The component type associated with the entity. +public static class ListPoolEntity where T : IComponent +{ + /// + /// Rents a list of from the pool. + /// + /// A disposable wrapper containing the rented entity list. + public static CollectionPool>, Entity>.PooledCollection Rent() + => ListPool>.Rent(); +} + +/// +/// A pre-configured pool specifically for instances. +/// +/// The type of elements in the hash set. +public static class HashSetPool +{ + static HashSetPool() + { + CollectionPool, T>.Configure(() => new HashSet()); + } + + /// + /// Rents a hash set from the pool. + /// + /// A disposable wrapper containing the rented hash set. + public static CollectionPool, T>.PooledCollection Rent() + => CollectionPool, T>.Rent(); +} + +/// +/// A helper pool for renting hash sets of entities with specific components. +/// +/// The component type associated with the entity. +public static class HashSetPoolEntity where T : IComponent +{ + /// + /// Rents a hash set of from the pool. + /// + /// A disposable wrapper containing the rented entity hash set. + public static CollectionPool>, Entity>.PooledCollection Rent() + => HashSetPool>.Rent(); +} diff --git a/Content.Shared/_Scp/Helpers/ScpHelpers.cs b/Content.Shared/_Scp/Helpers/ScpHelpers.cs index 197682cffa1..5adf758d50b 100644 --- a/Content.Shared/_Scp/Helpers/ScpHelpers.cs +++ b/Content.Shared/_Scp/Helpers/ScpHelpers.cs @@ -11,9 +11,6 @@ public sealed class ScpHelpers : EntitySystem { [Dependency] private readonly EyeWatchingSystem _watching = default!; - private readonly HashSet> _puddleSearchList = []; - private readonly List> _puddleList = []; - /// /// Получает суммарное количество реагента в зоне видимости сущности. /// Возвращает количество реагентов. @@ -23,12 +20,12 @@ public FixedPoint2 GetAroundSolutionVolume(EntityUid uid, List puddleList, LineOfSightBlockerLevel lineOfSight = LineOfSightBlockerLevel.Transparent) { - _puddleList.Clear(); - if (!_watching.TryGetAllEntitiesVisibleTo(uid, _puddleList, _puddleSearchList, lineOfSight, LookupFlags.Static)) + using var puddles = ListPoolEntity.Rent(); + if (!_watching.TryGetAllEntitiesVisibleTo(uid, puddles.Value, lineOfSight, LookupFlags.Static)) return FixedPoint2.Zero; FixedPoint2 total = 0; - foreach (var puddle in _puddleList) + foreach (var puddle in puddles.Value) { if (!puddle.Comp.Solution.HasValue) continue; @@ -55,12 +52,12 @@ public FixedPoint2 GetAroundSolutionVolume(EntityUid uid, ProtoId reagent, LineOfSightBlockerLevel lineOfSight = LineOfSightBlockerLevel.Transparent) { - _puddleList.Clear(); - if (!_watching.TryGetAllEntitiesVisibleTo(uid, _puddleList, _puddleSearchList, lineOfSight, LookupFlags.Static)) + using var puddles = ListPoolEntity.Rent(); + if (!_watching.TryGetAllEntitiesVisibleTo(uid, puddles.Value, lineOfSight, LookupFlags.Static)) return FixedPoint2.Zero; FixedPoint2 total = 0; - foreach (var puddle in _puddleList) + foreach (var puddle in puddles.Value) { if (!puddle.Comp.Solution.HasValue) continue; @@ -84,12 +81,12 @@ public bool IsAroundSolutionVolumeGreaterThan(EntityUid uid, FixedPoint2 required, LineOfSightBlockerLevel lineOfSight = LineOfSightBlockerLevel.Transparent) { - _puddleList.Clear(); - if (!_watching.TryGetAllEntitiesVisibleTo(uid, _puddleList, _puddleSearchList, lineOfSight, LookupFlags.Static)) + using var puddles = ListPoolEntity.Rent(); + if (!_watching.TryGetAllEntitiesVisibleTo(uid, puddles.Value, lineOfSight, LookupFlags.Static)) return false; FixedPoint2 total = 0; - foreach (var puddle in _puddleList) + foreach (var puddle in puddles.Value) { if (!puddle.Comp.Solution.HasValue) continue; diff --git a/Content.Shared/_Scp/Scp173/SharedScp173System.cs b/Content.Shared/_Scp/Scp173/SharedScp173System.cs index 5b67e70653e..e888765319f 100644 --- a/Content.Shared/_Scp/Scp173/SharedScp173System.cs +++ b/Content.Shared/_Scp/Scp173/SharedScp173System.cs @@ -3,6 +3,7 @@ using System.Numerics; using Content.Shared._Scp.Blinking; using Content.Shared._Scp.Containment.Cage; +using Content.Shared._Scp.Helpers; using Content.Shared._Scp.Watching; using Content.Shared.ActionBlocker; using Content.Shared.DoAfter; @@ -34,8 +35,6 @@ public abstract class SharedScp173System : EntitySystem public const float ContainmentRoomSearchRadius = 8f; - private readonly List> _blinkableList = []; - public override void Initialize() { base.Initialize(); @@ -143,11 +142,11 @@ private void OnBlind(Entity ent, ref Scp173StartBlind args) public void BlindEveryoneInRange(EntityUid scp, TimeSpan time, bool predicted = true) { - _blinkableList.Clear(); - if (!Watching.TryGetAllEntitiesVisibleTo(scp, _blinkableList)) + using var blinkableList = ListPoolEntity.Rent(); + if (!Watching.TryGetAllEntitiesVisibleTo(scp, blinkableList.Value)) return; - foreach (var eye in _blinkableList) + foreach (var eye in blinkableList.Value) { _blinking.ForceBlind(eye.AsNullable(), time, predicted); } @@ -223,11 +222,11 @@ public bool IsWatched(EntityUid scp, out int viewersCount) { viewersCount = 0; - _blinkableList.Clear(); - if (!Watching.TryGetAllEntitiesVisibleTo(scp, _blinkableList)) + using var blinkableList = ListPoolEntity.Rent(); + if (!Watching.TryGetAllEntitiesVisibleTo(scp, blinkableList.Value)) return false; - foreach (var viewer in _blinkableList) + foreach (var viewer in blinkableList.Value) { if (!Watching.CanBeWatched(viewer.AsNullable(), scp)) continue; diff --git a/Content.Shared/_Scp/Watching/EyeWatching.Pools.cs b/Content.Shared/_Scp/Watching/EyeWatching.Pools.cs deleted file mode 100644 index 00e689ff588..00000000000 --- a/Content.Shared/_Scp/Watching/EyeWatching.Pools.cs +++ /dev/null @@ -1,57 +0,0 @@ -using Content.Shared._Scp.Blinking; - -namespace Content.Shared._Scp.Watching; - -public sealed partial class EyeWatchingSystem -{ - private readonly Stack> _uidListPool = new(); - private readonly Stack>> _blinkableListPool = new(); - private readonly Stack>> _blinkableSetPool = new(); - - #region Rent - - private List RentUidList() - { - return _uidListPool.TryPop(out var list) - ? list - : []; - } - - private List> RentBlinkableList() - { - return _blinkableListPool.TryPop(out var list) - ? list - : []; - } - - private HashSet> RentBlinkableSet() - { - return _blinkableSetPool.TryPop(out var set) - ? set - : []; - } - - #endregion - - #region Return - - private void ReturnUidList(List list) - { - list.Clear(); - _uidListPool.Push(list); - } - - private void ReturnBlinkableList(List> list) - { - list.Clear(); - _blinkableListPool.Push(list); - } - - private void ReturnBlinkableSet(HashSet> set) - { - set.Clear(); - _blinkableSetPool.Push(set); - } - - #endregion -} diff --git a/Content.Shared/_Scp/Watching/EyeWatching.cs b/Content.Shared/_Scp/Watching/EyeWatching.cs index 3cd79621a8f..c0717950bd5 100644 --- a/Content.Shared/_Scp/Watching/EyeWatching.cs +++ b/Content.Shared/_Scp/Watching/EyeWatching.cs @@ -1,4 +1,5 @@ using Content.Shared._Scp.Blinking; +using Content.Shared._Scp.Helpers; using Content.Shared._Scp.Proximity; using Robust.Shared.Timing; @@ -13,8 +14,6 @@ public sealed partial class EyeWatchingSystem : EntitySystem [Dependency] private readonly ProximitySystem _proximity = default!; [Dependency] private readonly IGameTiming _timing = default!; - private readonly HashSet> _searchList = []; - /// /// Радиус, в котором сущности могут увидеть друг друга. /// @@ -49,13 +48,15 @@ public override void Update(float frameTime) continue; // Все потенциально возможные смотрящие. Среди них те, что прошли фаст-чек из самых простых проверок - var potentialViewers = new List> (); - if (!TryGetAllEntitiesVisibleTo(uid, potentialViewers, _searchList)) + using var potentialViewers = ListPoolEntity.Rent(); + using var searchList = HashSetPoolEntity.Rent(); + + if (!TryGetAllEntitiesVisibleTo(uid, potentialViewers.Value, searchList.Value)) continue; // Вызываем ивенты на потенциально смотрящих. Без особых проверок // Полезно в коде, который уже использует подобные проверки или не требует этого - foreach (var potentialViewer in potentialViewers) + foreach (var potentialViewer in potentialViewers.Value) { var simpleViewerEvent = new SimpleEntityLookedAtEvent((uid, watchingComponent)); var simpleTargetEvent = new SimpleEntitySeenEvent(potentialViewer); @@ -72,12 +73,12 @@ public override void Update(float frameTime) // Проверяет всех потенциальных смотрящих на то, действительно ли они видят цель. // Каждый потенциально смотрящий проходит полный комплекс проверок. // Выдает полный список всех сущностей, кто действительно видит цель - var realViewers = new List> (); - if (!IsWatchedBy(uid, potentialViewers, realViewers)) + using var realViewers = ListPoolEntity.Rent(); + if (!TryGetWatchers(uid, potentialViewers.Value, realViewers.Value)) continue; // Вызываем ивент на смотрящем, говорящие, что он действительно видит цель - foreach (var viewer in realViewers) + foreach (var viewer in realViewers.Value) { var netViewer = GetNetEntity(viewer); var firstTime = !watchingComponent.AlreadyLookedAt.ContainsKey(netViewer); diff --git a/Content.Shared/_Scp/Watching/EyeWatchingSystem.API.cs b/Content.Shared/_Scp/Watching/EyeWatchingSystem.API.cs index 0aee815745e..2431a0ab828 100644 --- a/Content.Shared/_Scp/Watching/EyeWatchingSystem.API.cs +++ b/Content.Shared/_Scp/Watching/EyeWatchingSystem.API.cs @@ -1,5 +1,6 @@ using System.Linq; using Content.Shared._Scp.Blinking; +using Content.Shared._Scp.Helpers; using Content.Shared._Scp.Proximity; using Content.Shared._Scp.Watching.FOV; using Content.Shared.Eye.Blinding.Systems; @@ -32,15 +33,11 @@ public bool IsWatched(EntityUid ent, bool useFov = true, float? fovOverride = nu public bool IsWatched(EntityUid ent, out int watchersCount, bool useFov = true, float? fovOverride = null) { - watchersCount = 0; - var potentialWatchers = RentBlinkableList(); - var searchSet = RentBlinkableSet(); + using var potentialWatchers = ListPoolEntity.Rent(); + using var searchSet = HashSetPoolEntity.Rent(); - var result = IsWatched(ent, potentialWatchers, searchSet, useFov , fovOverride); - watchersCount = potentialWatchers.Count; - - ReturnBlinkableList(potentialWatchers); - ReturnBlinkableSet(searchSet); + var result = IsWatched(ent, potentialWatchers.Value, searchSet.Value, useFov, fovOverride); + watchersCount = potentialWatchers.Value.Count; return result; } @@ -62,7 +59,7 @@ public bool IsWatched(EntityUid ent, if (!TryGetAllEntitiesVisibleTo(ent, potentialWatchers, searchSet)) return false; - return IsWatchedBy(ent, potentialWatchers , useFov, fovOverride); + return IsWatchedByAny(ent, potentialWatchers , useFov, fovOverride); } public bool TryGetAllEntitiesVisibleTo( @@ -71,13 +68,23 @@ public bool TryGetAllEntitiesVisibleTo( LineOfSightBlockerLevel type = LineOfSightBlockerLevel.Transparent, LookupFlags flags = LookupFlags.All) { - var searchSet = RentBlinkableSet(); - var result = TryGetAllEntitiesVisibleTo(ent, potentialWatchers, searchSet, type, flags); - ReturnBlinkableSet(searchSet); + using var searchSet = HashSetPoolEntity.Rent(); + var result = TryGetAllEntitiesVisibleTo(ent, potentialWatchers, searchSet.Value, type, flags); return result; } + public bool TryGetAllEntitiesVisibleTo( + Entity ent, + List> potentialWatchers, + LineOfSightBlockerLevel type = LineOfSightBlockerLevel.Transparent, + LookupFlags flags = LookupFlags.All) + where T : IComponent + { + using var searchSet = HashSetPoolEntity.Rent(); + return TryGetAllEntitiesVisibleTo(ent, potentialWatchers, searchSet.Value, type, flags); + } + /// /// Получает и возвращает всех потенциально смотрящих на указанную цель. /// @@ -119,6 +126,17 @@ public bool TryGetAllEntitiesVisibleTo( return potentialWatchers.Count != 0; } + public bool IsWatchedBy(EntityUid target, EntityUid potentialViewer, bool useFov = true, float? fovOverride = null) + { + if (!CanBeWatched(potentialViewer, target)) + return false; + + if (IsEyeBlinded(potentialViewer, target, useFov, fovOverride)) + return false; + + return true; + } + /// /// Проверяет, смотрят ли переданные сущности на указанную цель. Передает список всех сущностей, что действительно смотрят на цель /// @@ -128,7 +146,7 @@ public bool TryGetAllEntitiesVisibleTo( /// Нужно ли проверять, находится ли цель в поле зрения сущности /// Если нужно перезаписать угол поля зрения /// Смотрит ли хоть кто-то на цель - public bool IsWatchedBy(EntityUid target, + public bool TryGetWatchers(EntityUid target, List> potentialViewers, List> realViewers, bool useFov = true, @@ -136,10 +154,7 @@ public bool IsWatchedBy(EntityUid target, { foreach (var viewer in potentialViewers) { - if (!CanBeWatched(viewer.AsNullable(), target)) - continue; - - if (IsEyeBlinded(viewer.AsNullable(), target, useFov, fovOverride)) + if (!IsWatchedBy(target, viewer, useFov, fovOverride)) continue; realViewers.Add(viewer); @@ -148,17 +163,14 @@ public bool IsWatchedBy(EntityUid target, return realViewers.Any(); } - public bool IsWatchedBy(EntityUid target, + public bool IsWatchedByAny(EntityUid target, List> potentialViewers, bool useFov = true, float? fovOverride = null) { foreach (var viewer in potentialViewers) { - if (!CanBeWatched(viewer.AsNullable(), target)) - continue; - - if (IsEyeBlinded(viewer.AsNullable(), target, useFov, fovOverride)) + if (!IsWatchedBy(target, viewer, useFov, fovOverride)) continue; return true; From f92bab758109db333fa229bf6d0e5924753a537c Mon Sep 17 00:00:00 2001 From: drdth Date: Tue, 17 Mar 2026 08:13:46 +0300 Subject: [PATCH 04/17] refactor: split eye watching API into 3 parts, optimize API usings, optimize lookup flags --- Content.Server/_Scp/Fear/FearSystem.Fears.cs | 12 +- Content.Server/_Scp/Fear/FearSystem.cs | 5 +- .../Madness/ArtifactScp096MadnessSystem.cs | 20 +- Content.Server/_Scp/Scp173/Scp173System.cs | 6 +- .../Fear/Systems/SharedFearSystem.Helpers.cs | 2 +- .../_Scp/Fear/Systems/SharedFearSystem.cs | 9 +- Content.Shared/_Scp/Helpers/CollectionPool.cs | 18 +- Content.Shared/_Scp/Helpers/ScpHelpers.cs | 6 +- .../Main/Systems/SharedScp096System.Target.cs | 2 +- .../Systems/SharedScp106System.Phantom.cs | 2 +- .../_Scp/Scp173/SharedScp173System.cs | 68 ++--- Content.Shared/_Scp/Watching/EyeWatching.cs | 7 +- .../Watching/EyeWatchingSystem.API.Base.cs | 177 ++++++++++++ .../Watching/EyeWatchingSystem.API.Watched.cs | 134 +++++++++ .../EyeWatchingSystem.API.Watching.cs | 49 ++++ .../_Scp/Watching/EyeWatchingSystem.API.cs | 267 ------------------ 16 files changed, 434 insertions(+), 350 deletions(-) create mode 100644 Content.Shared/_Scp/Watching/EyeWatchingSystem.API.Base.cs create mode 100644 Content.Shared/_Scp/Watching/EyeWatchingSystem.API.Watched.cs create mode 100644 Content.Shared/_Scp/Watching/EyeWatchingSystem.API.Watching.cs delete mode 100644 Content.Shared/_Scp/Watching/EyeWatchingSystem.API.cs diff --git a/Content.Server/_Scp/Fear/FearSystem.Fears.cs b/Content.Server/_Scp/Fear/FearSystem.Fears.cs index ee3e872ce63..44172aea89c 100644 --- a/Content.Server/_Scp/Fear/FearSystem.Fears.cs +++ b/Content.Server/_Scp/Fear/FearSystem.Fears.cs @@ -1,6 +1,6 @@ using System.Runtime.InteropServices; using Content.Server._Scp.Shaders.Highlighting; -using Content.Server._Sunrise.Mood; +using Content.Shared._Scp.Blinking; using Content.Shared._Scp.Fear; using Content.Shared._Scp.Fear.Components; using Content.Shared._Scp.Fear.Components.Fears; @@ -57,20 +57,16 @@ private void OnMobStateChanged(MobStateChangedEvent ev) if (!activated) return; - // TODO: Создать проверки "сколько сущностей с компонентом X видит сущность способная к зрению" - using var potentialViewer = ListPoolEntity.Rent(); - if (!_watching.TryGetAllEntitiesVisibleTo(ev.Target, potentialViewer.Value, flags: LookupFlags.Dynamic)) + using var realWatchers = ListPoolEntity.Rent(); + if (!_watching.TryGetWatchers(ev.Target, realWatchers.Value, flags: LookupFlags.Dynamic)) return; - foreach (var uid in potentialViewer.Value) + foreach (var uid in realWatchers.Value) { // Убийца не будет печалиться смерти убитого if (uid.Owner == ev.Origin) continue; - if (!_watching.IsWatchedBy(uid, ev.Target)) - continue; - AddNegativeMoodEffect(uid, MoodSomeoneDiedOnMyEyes); } } diff --git a/Content.Server/_Scp/Fear/FearSystem.cs b/Content.Server/_Scp/Fear/FearSystem.cs index b7eb1de1f10..2db1af27b41 100644 --- a/Content.Server/_Scp/Fear/FearSystem.cs +++ b/Content.Server/_Scp/Fear/FearSystem.cs @@ -1,7 +1,6 @@ using Content.Shared._Scp.Fear; using Content.Shared._Scp.Fear.Components; using Content.Shared._Scp.Fear.Systems; -using Content.Shared._Scp.Helpers; using Content.Shared._Sunrise.Mood; using Content.Shared.Mobs.Components; using Content.Shared.Rejuvenate; @@ -82,9 +81,7 @@ public bool TryCalmDown(Entity ent) // Проверка на то, что мы в данный момент не смотрим на какую-то страшную сущность. // Нельзя успокоиться, когда мы смотрим на источник страха. - // TODO: Создать проверки "сколько сущностей с компонентом X видит сущность способная к зрению" - using var fearSources = ListPoolEntity.Rent(); - if (_watching.TryGetAllEntitiesVisibleTo(ent.Owner, fearSources.Value, ent.Comp.SeenBlockerLevel)) + if (_watching.TryGetAnyEntitiesVisibleTo(ent.Owner, ent.Comp.SeenBlockerLevel)) return false; var newFearState = GetDecreasedLevel(ent.Comp.State); diff --git a/Content.Server/_Scp/Research/Artifacts/Effects/_ScpSpecific/Scp096/Madness/ArtifactScp096MadnessSystem.cs b/Content.Server/_Scp/Research/Artifacts/Effects/_ScpSpecific/Scp096/Madness/ArtifactScp096MadnessSystem.cs index eac5fab5143..e073a27543f 100644 --- a/Content.Server/_Scp/Research/Artifacts/Effects/_ScpSpecific/Scp096/Madness/ArtifactScp096MadnessSystem.cs +++ b/Content.Server/_Scp/Research/Artifacts/Effects/_ScpSpecific/Scp096/Madness/ArtifactScp096MadnessSystem.cs @@ -1,11 +1,9 @@ -using Content.Server._Scp.Scp096; +using System.Linq; +using Content.Server._Scp.Scp096; using Content.Server._Sunrise.Helpers; using Content.Shared._Scp.Blinking; -using Content.Shared._Scp.Helpers; -using Content.Shared._Scp.Proximity; using Content.Shared._Scp.Scp096.Main.Components; using Content.Shared._Scp.ScpMask; -using Content.Shared._Scp.Watching; using Content.Shared._Sunrise.Helpers; using Content.Shared.Xenoarchaeology.Artifact; using Content.Shared.Xenoarchaeology.Artifact.XAE; @@ -17,7 +15,7 @@ public sealed class ArtifactScp096MadnessSystem : BaseXAESystem ent, if (!_helpers.TryGetFirst(out var scp096)) return; - using var targets = ListPoolEntity.Rent(); - if (!_watching.TryGetAllEntitiesVisibleTo(scp096.Value.Owner, - targets.Value, - LineOfSightBlockerLevel.Solid, - LookupFlags.Dynamic)) - return; - - targets.Value + var targets = _lookup.GetEntitiesInRange(Transform(scp096.Value).Coordinates, ent.Comp.Radius, LookupFlags.Dynamic) + .ToList() .ShuffleRobust(_random) .TakePercentage(ent.Comp.Percent); - foreach (var target in targets.Value) + foreach (var target in targets) { if (!_scp096.TryAddTarget(scp096.Value, target, true, true)) continue; diff --git a/Content.Server/_Scp/Scp173/Scp173System.cs b/Content.Server/_Scp/Scp173/Scp173System.cs index 519c88f9587..f595135cf6a 100644 --- a/Content.Server/_Scp/Scp173/Scp173System.cs +++ b/Content.Server/_Scp/Scp173/Scp173System.cs @@ -103,7 +103,7 @@ private void OnStructureDamage(Entity uid, ref Scp173DamageStru return; } - if (Watching.IsWatched(uid)) + if (Watching.IsWatchedByAny(uid)) { var message = Loc.GetString("scp173-fast-movement-too-many-watchers"); _popup.PopupEntity(message, uid, uid, PopupType.LargeCaution); @@ -185,7 +185,7 @@ private void OnClog(Entity ent, ref Scp173ClogAction args) return; } - if (Watching.IsWatched(ent)) + if (Watching.IsWatchedByAny(ent)) { var message = Loc.GetString("scp173-fast-movement-too-many-watchers"); _popup.PopupEntity(message, ent, ent, PopupType.LargeCaution); @@ -253,7 +253,7 @@ private void OnFastMovement(Entity ent, ref Scp173FastMovementA return; } - if (Watching.IsWatched(ent, out var watchersCount) && watchersCount > ent.Comp.MaxWatchers) + if (Watching.TryGetWatchers(ent, out var watchersCount) && watchersCount > ent.Comp.MaxWatchers) { var message = Loc.GetString("scp173-fast-movement-too-many-watchers"); _popup.PopupEntity(message, ent, ent, PopupType.LargeCaution); diff --git a/Content.Shared/_Scp/Fear/Systems/SharedFearSystem.Helpers.cs b/Content.Shared/_Scp/Fear/Systems/SharedFearSystem.Helpers.cs index 30711ea464a..b9b5e641c19 100644 --- a/Content.Shared/_Scp/Fear/Systems/SharedFearSystem.Helpers.cs +++ b/Content.Shared/_Scp/Fear/Systems/SharedFearSystem.Helpers.cs @@ -22,7 +22,7 @@ public abstract partial class SharedFearSystem private void HighLightAllVisibleFears(Entity ent) { using var fearSources = ListPoolEntity.Rent(); - if (!_watching.TryGetAllEntitiesVisibleTo(ent.Owner, fearSources.Value, ent.Comp.SeenBlockerLevel)) + if (!_watching.TryGetWatchingTargets(ent.Owner, fearSources.Value, ent.Comp.SeenBlockerLevel)) return; foreach (var source in fearSources.Value) diff --git a/Content.Shared/_Scp/Fear/Systems/SharedFearSystem.cs b/Content.Shared/_Scp/Fear/Systems/SharedFearSystem.cs index 8d600bbf8cd..b8ac7498005 100644 --- a/Content.Shared/_Scp/Fear/Systems/SharedFearSystem.cs +++ b/Content.Shared/_Scp/Fear/Systems/SharedFearSystem.cs @@ -1,4 +1,5 @@ -using Content.Shared._Scp.Fear.Components; +using Content.Shared._Scp.Blinking; +using Content.Shared._Scp.Fear.Components; using Content.Shared._Scp.Helpers; using Content.Shared._Scp.Proximity; using Content.Shared._Scp.Shaders; @@ -27,6 +28,7 @@ namespace Content.Shared._Scp.Fear.Systems; public abstract partial class SharedFearSystem : EntitySystem { [Dependency] private readonly SharedHighlightSystem _highlight = default!; + [Dependency] private readonly SharedBlinkingSystem _blinking = default!; [Dependency] private readonly EyeWatchingSystem _watching = default!; [Dependency] private readonly MobStateSystem _mobState = default!; [Dependency] private readonly SharedShaderStrengthSystem _shaderStrength = default!; @@ -139,7 +141,10 @@ private void OnProximityInRange(Entity ent, ref ProximityInRangeT return; // Проверка на зрение, чтобы можно было закрыть глазки и было не страшно - if (!_watching.SimpleIsWatchedBy(args.Receiver, ent)) + if (_blinking.AreEyesClosedManually(ent.Owner)) + return; + + if (!_watching.IsWatchedBy(args.Receiver, ent, checkProximity: false)) return; // Если текущий уровень страха выше, чем тот, что мы хотим поставить, diff --git a/Content.Shared/_Scp/Helpers/CollectionPool.cs b/Content.Shared/_Scp/Helpers/CollectionPool.cs index b62af7fb147..3fd1314e0e8 100644 --- a/Content.Shared/_Scp/Helpers/CollectionPool.cs +++ b/Content.Shared/_Scp/Helpers/CollectionPool.cs @@ -1,4 +1,6 @@ -namespace Content.Shared._Scp.Helpers; +using System.Runtime.CompilerServices; + +namespace Content.Shared._Scp.Helpers; /// /// Provides a static object pool for collections to minimize garbage collection allocations. @@ -15,7 +17,7 @@ public static class CollectionPool /// Configures the factory function used to instantiate new collections when the pool is empty. /// /// The delegate used to create new instances of . - /// Thrown when the provided is null. + /// Thrown when the provided is null. public static void Configure(Func factory) { _factory = factory ?? throw new InvalidOperationException("Factory cannot be null"); @@ -58,14 +60,14 @@ internal static void Return(TCollection collection) { if (Pool.Count >= 512) { - Logger.Warning("Pool bloated, new collections will not be generated"); + Logger.Warning("Pool bloated, new collections will not be stored"); return; } - if (collection is List list && list.Capacity > 1024) + if (collection is List list && list.Capacity > 2048) return; - if (collection is HashSet hashSet && hashSet.Capacity > 1024) + if (collection is HashSet hashSet && hashSet.Capacity > 2048) return; collection.Clear(); @@ -83,7 +85,7 @@ public struct PooledCollection : IDisposable /// /// Gets the underlying rented collection. /// - /// Thrown if accessed after the collection has been returned to the pool. + /// Thrown if accessed after the collection has been returned to the pool. public TCollection Value { get @@ -135,6 +137,7 @@ static ListPool() /// Rents a list from the pool. /// /// A disposable wrapper containing the rented list. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static CollectionPool, T>.PooledCollection Rent() => CollectionPool, T>.Rent(); } @@ -149,6 +152,7 @@ public static class ListPoolEntity where T : IComponent /// Rents a list of from the pool. /// /// A disposable wrapper containing the rented entity list. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static CollectionPool>, Entity>.PooledCollection Rent() => ListPool>.Rent(); } @@ -168,6 +172,7 @@ static HashSetPool() /// Rents a hash set from the pool. /// /// A disposable wrapper containing the rented hash set. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static CollectionPool, T>.PooledCollection Rent() => CollectionPool, T>.Rent(); } @@ -182,6 +187,7 @@ public static class HashSetPoolEntity where T : IComponent /// Rents a hash set of from the pool. /// /// A disposable wrapper containing the rented entity hash set. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static CollectionPool>, Entity>.PooledCollection Rent() => HashSetPool>.Rent(); } diff --git a/Content.Shared/_Scp/Helpers/ScpHelpers.cs b/Content.Shared/_Scp/Helpers/ScpHelpers.cs index 5adf758d50b..56acb1dddfa 100644 --- a/Content.Shared/_Scp/Helpers/ScpHelpers.cs +++ b/Content.Shared/_Scp/Helpers/ScpHelpers.cs @@ -21,7 +21,7 @@ public FixedPoint2 GetAroundSolutionVolume(EntityUid uid, LineOfSightBlockerLevel lineOfSight = LineOfSightBlockerLevel.Transparent) { using var puddles = ListPoolEntity.Rent(); - if (!_watching.TryGetAllEntitiesVisibleTo(uid, puddles.Value, lineOfSight, LookupFlags.Static)) + if (!_watching.TryGetAllEntitiesVisibleTo(uid, puddles.Value, lineOfSight, LookupFlags.Static | LookupFlags.Approximate)) return FixedPoint2.Zero; FixedPoint2 total = 0; @@ -53,7 +53,7 @@ public FixedPoint2 GetAroundSolutionVolume(EntityUid uid, LineOfSightBlockerLevel lineOfSight = LineOfSightBlockerLevel.Transparent) { using var puddles = ListPoolEntity.Rent(); - if (!_watching.TryGetAllEntitiesVisibleTo(uid, puddles.Value, lineOfSight, LookupFlags.Static)) + if (!_watching.TryGetAllEntitiesVisibleTo(uid, puddles.Value, lineOfSight, LookupFlags.Static | LookupFlags.Approximate)) return FixedPoint2.Zero; FixedPoint2 total = 0; @@ -82,7 +82,7 @@ public bool IsAroundSolutionVolumeGreaterThan(EntityUid uid, LineOfSightBlockerLevel lineOfSight = LineOfSightBlockerLevel.Transparent) { using var puddles = ListPoolEntity.Rent(); - if (!_watching.TryGetAllEntitiesVisibleTo(uid, puddles.Value, lineOfSight, LookupFlags.Static)) + if (!_watching.TryGetAllEntitiesVisibleTo(uid, puddles.Value, lineOfSight, LookupFlags.Static | LookupFlags.Approximate)) return false; FixedPoint2 total = 0; diff --git a/Content.Shared/_Scp/Scp096/Main/Systems/SharedScp096System.Target.cs b/Content.Shared/_Scp/Scp096/Main/Systems/SharedScp096System.Target.cs index 41674f9d19c..bf31f98027e 100644 --- a/Content.Shared/_Scp/Scp096/Main/Systems/SharedScp096System.Target.cs +++ b/Content.Shared/_Scp/Scp096/Main/Systems/SharedScp096System.Target.cs @@ -190,7 +190,7 @@ private bool IsTargetSeeScp096(EntityUid viewer, Entity scp, bo return false; // Проверяет, не слеп ли персонаж - if (_watching.IsEyeBlinded(viewer, scp, false) && !ignoreBlinded) + if (!_watching.CanSee(viewer, scp, false) && !ignoreBlinded) return false; // Если игнорируем угол, то считаем, что смотрящий видит 096 diff --git a/Content.Shared/_Scp/Scp106/Systems/SharedScp106System.Phantom.cs b/Content.Shared/_Scp/Scp106/Systems/SharedScp106System.Phantom.cs index fae99922bc9..8a6fe663694 100644 --- a/Content.Shared/_Scp/Scp106/Systems/SharedScp106System.Phantom.cs +++ b/Content.Shared/_Scp/Scp106/Systems/SharedScp106System.Phantom.cs @@ -101,7 +101,7 @@ private void OnExamined(Entity ent, ref ExaminedEvent ar if (!_mob.IsAlive(args.Examiner)) return; - if (!_watching.SimpleIsWatchedBy(ent.Owner, args.Examiner)) + if (!_watching.IsWatchedBy(ent.Owner, args.Examiner)) return; // Ликвидируйся diff --git a/Content.Shared/_Scp/Scp173/SharedScp173System.cs b/Content.Shared/_Scp/Scp173/SharedScp173System.cs index e888765319f..1f0cd4f22aa 100644 --- a/Content.Shared/_Scp/Scp173/SharedScp173System.cs +++ b/Content.Shared/_Scp/Scp173/SharedScp173System.cs @@ -1,13 +1,12 @@ using System.Diagnostics.CodeAnalysis; -using System.Linq; using System.Numerics; using Content.Shared._Scp.Blinking; using Content.Shared._Scp.Containment.Cage; using Content.Shared._Scp.Helpers; +using Content.Shared._Scp.Proximity; using Content.Shared._Scp.Watching; using Content.Shared.ActionBlocker; using Content.Shared.DoAfter; -using Content.Shared.Interaction; using Content.Shared.Interaction.Events; using Content.Shared.Movement.Events; using Content.Shared.Physics; @@ -26,8 +25,6 @@ public abstract class SharedScp173System : EntitySystem [Dependency] private readonly SharedBlinkingSystem _blinking = default!; [Dependency] private readonly ActionBlockerSystem _blocker = default!; [Dependency] protected readonly EyeWatchingSystem Watching = default!; - [Dependency] private readonly EntityLookupSystem _lookup = default!; - [Dependency] private readonly SharedInteractionSystem _interaction = default!; [Dependency] private readonly SharedDoAfterSystem _doAfter = default!; [Dependency] private readonly SharedPopupSystem _popup = default!; @@ -35,6 +32,9 @@ public abstract class SharedScp173System : EntitySystem public const float ContainmentRoomSearchRadius = 8f; + private EntityQuery _insideQuery; + private EntityQuery _scpCageQuery; + public override void Initialize() { base.Initialize(); @@ -48,6 +48,9 @@ public override void Initialize() SubscribeLocalEvent(OnStartedBlind); SubscribeLocalEvent(OnBlind); + + _insideQuery = GetEntityQuery(); + _scpCageQuery = GetEntityQuery(); } #region Movement @@ -60,7 +63,7 @@ private void OnAttackAttempt(Entity ent, ref AttackAttemptEvent return; } - if (Watching.IsWatched(ent.Owner)) + if (Watching.IsWatchedByAny(ent)) { args.Cancel(); return; @@ -69,20 +72,26 @@ private void OnAttackAttempt(Entity ent, ref AttackAttemptEvent private void OnDirectionAttempt(Entity ent, ref ChangeDirectionAttemptEvent args) { - if (Watching.IsWatched(ent.Owner) && !IsInScpCage(ent, out _)) - { - args.Cancel(); + // В клетке можно двигаться + if (IsInScpCage(ent, out _)) return; - } + + if (Watching.IsWatchedByAny(ent)) + return; + + args.Cancel(); } private void OnMoveAttempt(Entity ent, ref UpdateCanMoveEvent args) { - if (Watching.IsWatched(ent.Owner) && !IsInScpCage(ent, out _)) - { - args.Cancel(); + // В клетке можно двигаться + if (IsInScpCage(ent, out _)) return; - } + + if (!Watching.IsWatchedByAny(ent)) + return; + + args.Cancel(); } private void OnMoveInput(Entity ent, ref MoveInputEvent args) @@ -143,7 +152,7 @@ private void OnBlind(Entity ent, ref Scp173StartBlind args) public void BlindEveryoneInRange(EntityUid scp, TimeSpan time, bool predicted = true) { using var blinkableList = ListPoolEntity.Rent(); - if (!Watching.TryGetAllEntitiesVisibleTo(scp, blinkableList.Value)) + if (!Watching.TryGetAllEntitiesVisibleTo(scp, blinkableList.Value, flags: LookupFlags.Dynamic | LookupFlags.Approximate)) return; foreach (var eye in blinkableList.Value) @@ -161,8 +170,8 @@ public bool IsInScpCage(EntityUid uid, [NotNullWhen(true)] out EntityUid? storag { storage = null; - if (TryComp(uid, out var insideEntityStorageComponent) && - HasComp(insideEntityStorageComponent.Storage)) + if (_insideQuery.TryComp(uid, out var insideEntityStorageComponent) && + _scpCageQuery.HasComp(insideEntityStorageComponent.Storage)) { storage = insideEntityStorageComponent.Storage; return true; @@ -174,11 +183,11 @@ public bool IsInScpCage(EntityUid uid, [NotNullWhen(true)] out EntityUid? storag /// /// Находится ли 173 в своей камере. Проверяется по наличию рядом спавнера работы /// - /// TODO: Оптимизировать, использовав EntityQueryEnumerator и ранний выход public bool IsContained(EntityUid uid) { - return _lookup.GetEntitiesInRange(Transform(uid).Coordinates, ContainmentRoomSearchRadius) - .Any(entity => _interaction.InRangeUnobstructed(uid, entity.Owner, ContainmentRoomSearchRadius)); + return Watching.TryGetAnyEntitiesVisibleTo(uid, + LineOfSightBlockerLevel.None, + LookupFlags.Sensors | LookupFlags.Sundries); } private bool CanBlind(EntityUid uid, bool showPopups = true) @@ -199,7 +208,7 @@ private bool CanBlind(EntityUid uid, bool showPopups = true) return false; } - if (!IsWatched(uid, out var watchers)) + if (!Watching.TryGetWatchers(uid, out var watchers)) { if (showPopups) _popup.PopupClient(Loc.GetString("scp173-blind-failed-too-few-watchers"), uid, uid); @@ -218,25 +227,6 @@ private bool CanBlind(EntityUid uid, bool showPopups = true) return true; } - public bool IsWatched(EntityUid scp, out int viewersCount) - { - viewersCount = 0; - - using var blinkableList = ListPoolEntity.Rent(); - if (!Watching.TryGetAllEntitiesVisibleTo(scp, blinkableList.Value)) - return false; - - foreach (var viewer in blinkableList.Value) - { - if (!Watching.CanBeWatched(viewer.AsNullable(), scp)) - continue; - - viewersCount++; - } - - return viewersCount != 0; - } - #endregion #region Jump Helpers diff --git a/Content.Shared/_Scp/Watching/EyeWatching.cs b/Content.Shared/_Scp/Watching/EyeWatching.cs index c0717950bd5..02c9c565303 100644 --- a/Content.Shared/_Scp/Watching/EyeWatching.cs +++ b/Content.Shared/_Scp/Watching/EyeWatching.cs @@ -68,13 +68,18 @@ public override void Update(float frameTime) // Если требуются только Simple ивенты, то нет смысла делать дальнейшие действия. if (watchingComponent.SimpleMode) + { + SetNextTime(watchingComponent); + Dirty(uid, watchingComponent); + continue; + } // Проверяет всех потенциальных смотрящих на то, действительно ли они видят цель. // Каждый потенциально смотрящий проходит полный комплекс проверок. // Выдает полный список всех сущностей, кто действительно видит цель using var realViewers = ListPoolEntity.Rent(); - if (!TryGetWatchers(uid, potentialViewers.Value, realViewers.Value)) + if (!TryGetWatchersFrom(uid, realViewers.Value, potentialViewers.Value)) continue; // Вызываем ивент на смотрящем, говорящие, что он действительно видит цель diff --git a/Content.Shared/_Scp/Watching/EyeWatchingSystem.API.Base.cs b/Content.Shared/_Scp/Watching/EyeWatchingSystem.API.Base.cs new file mode 100644 index 00000000000..43876726c8e --- /dev/null +++ b/Content.Shared/_Scp/Watching/EyeWatchingSystem.API.Base.cs @@ -0,0 +1,177 @@ +using System.Diagnostics.CodeAnalysis; +using Content.Shared._Scp.Blinking; +using Content.Shared._Scp.Helpers; +using Content.Shared._Scp.Proximity; +using Content.Shared._Scp.Watching.FOV; +using Content.Shared.Eye.Blinding.Systems; +using Content.Shared.Mobs.Components; +using Content.Shared.Mobs.Systems; +using Content.Shared.Storage.Components; + +namespace Content.Shared._Scp.Watching; + +public sealed partial class EyeWatchingSystem +{ + [Dependency] private readonly SharedBlinkingSystem _blinking = default!; + [Dependency] private readonly EntityLookupSystem _lookup = default!; + [Dependency] private readonly MobStateSystem _mobState = default!; + [Dependency] private readonly FieldOfViewSystem _fov = default!; + + private EntityQuery _mobStateQuery; + private EntityQuery _insideStorageQuery; + private EntityQuery _blinkableQuery; + + private void InitializeApi() + { + _mobStateQuery = GetEntityQuery(); + _insideStorageQuery = GetEntityQuery(); + _blinkableQuery = GetEntityQuery(); + } + + public bool TryGetAllEntitiesVisibleTo( + Entity ent, + List> potentialWatchers, + LineOfSightBlockerLevel type = LineOfSightBlockerLevel.Transparent, + LookupFlags flags = LookupFlags.Uncontained | LookupFlags.Approximate) + where T : IComponent + { + using var searchSet = HashSetPoolEntity.Rent(); + return TryGetAllEntitiesVisibleTo(ent, potentialWatchers, searchSet.Value, type, flags); + } + + /// + /// Получает и возвращает всех потенциально смотрящих на указанную цель. + /// + /// + /// В методе нет проверок на дополнительные состояния, такие как моргание/закрыты ли глаза/поле зрения т.п. + /// Единственная проверка - можно ли физически увидеть цель(т.е. не закрыта ли она стеной и т.п.) + /// + /// Цель, для которой ищем потенциальных смотрящих + /// Список всех, кто потенциально видит цель + /// Требуемая прозрачность линии видимости. + /// Заранее заготовленный список, который будет использоваться в + /// Список флагов для поиска целей в + /// Удалось ли найти хоть кого-то + private bool TryGetAllEntitiesVisibleTo( + Entity ent, + List> potentialWatchers, + HashSet> searchSet, + LineOfSightBlockerLevel type = LineOfSightBlockerLevel.Transparent, + LookupFlags flags = LookupFlags.Uncontained | LookupFlags.Approximate) + where T : IComponent + { + if (!Resolve(ent.Owner, ref ent.Comp)) + return false; + + searchSet.Clear(); + _lookup.GetEntitiesInRange(ent.Comp.Coordinates, SeeRange, searchSet, flags); + + foreach (var target in searchSet) + { + if (!IsInProximity(ent, target, type)) + continue; + + potentialWatchers.Add(target); + } + + return potentialWatchers.Count != 0; + } + + public bool TryGetAnyEntitiesVisibleTo( + Entity viewer, + LineOfSightBlockerLevel type = LineOfSightBlockerLevel.Transparent, + LookupFlags flags = LookupFlags.Uncontained | LookupFlags.Approximate) + where T : IComponent + { + using var searchSet = HashSetPoolEntity.Rent(); + if (!TryGetAnyEntitiesVisibleTo(viewer, out _, searchSet.Value, type, flags)) + return false; + + return true; + } + + public bool TryGetAnyEntitiesVisibleTo( + Entity viewer, + [NotNullWhen(true)] out Entity? firstVisible, + LineOfSightBlockerLevel type = LineOfSightBlockerLevel.Transparent, + LookupFlags flags = LookupFlags.Uncontained | LookupFlags.Approximate) + where T : IComponent + { + firstVisible = null; + + using var searchSet = HashSetPoolEntity.Rent(); + if (!TryGetAnyEntitiesVisibleTo(viewer, out var first, searchSet.Value, type, flags)) + return false; + + firstVisible = first; + return true; + } + + private bool TryGetAnyEntitiesVisibleTo( + Entity viewer, + [NotNullWhen(true)] out Entity? firstVisible, + HashSet> searchSet, + LineOfSightBlockerLevel type = LineOfSightBlockerLevel.Transparent, + LookupFlags flags = LookupFlags.Uncontained | LookupFlags.Approximate) + where T : IComponent + { + firstVisible = null; + + if (!Resolve(viewer.Owner, ref viewer.Comp)) + return false; + + searchSet.Clear(); + _lookup.GetEntitiesInRange(viewer.Comp.Coordinates, SeeRange, searchSet, flags); + + foreach (var target in searchSet) + { + if (!IsInProximity(viewer, target, type)) + continue; + + firstVisible = target; + return true; + } + + return false; + } + + private bool IsInProximity(EntityUid ent, EntityUid target, LineOfSightBlockerLevel type) + { + if (target == ent) + return false; + + if (!_proximity.IsRightType(ent, target, type, out _)) + return false; + + return true; + } + + /// + /// Проверка на то, может ли смотрящий видеть цель + /// + /// Смотрящий + /// Цель, которую проверяем + /// Применять ли проверку на поле зрения? + /// Если нужно использовать другой угол поля зрения + /// Видит ли смотрящий цель + public bool CanSee(Entity viewer, EntityUid target, bool useFov = true, float? fovOverride = null) + { + if (_mobState.IsIncapacitated(viewer)) + return false; + + // Проверяем, видит ли смотрящий цель + if (useFov && !_fov.IsInFov(viewer.Owner, target, fovOverride)) + return false; // Если не видит, то не считаем его как смотрящего + + if (_blinking.IsBlind(viewer, true)) + return false; + + var canSeeAttempt = new CanSeeAttemptEvent(); + RaiseLocalEvent(viewer, canSeeAttempt); + + if (canSeeAttempt.Blind) + return false; + + return true; + } +} diff --git a/Content.Shared/_Scp/Watching/EyeWatchingSystem.API.Watched.cs b/Content.Shared/_Scp/Watching/EyeWatchingSystem.API.Watched.cs new file mode 100644 index 00000000000..07385e8c5e3 --- /dev/null +++ b/Content.Shared/_Scp/Watching/EyeWatchingSystem.API.Watched.cs @@ -0,0 +1,134 @@ +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using Content.Shared._Scp.Blinking; +using Content.Shared._Scp.Helpers; +using Content.Shared._Scp.Proximity; + +namespace Content.Shared._Scp.Watching; + +// TODO: Унифицировать название переменных realWatchers/realViewers + potentialWatchers/potentialViewers +public sealed partial class EyeWatchingSystem +{ + public bool TryGetWatchers(EntityUid target, + [NotNullWhen(true)] out int? watchers, + LineOfSightBlockerLevel type = LineOfSightBlockerLevel.Transparent, + LookupFlags flags = LookupFlags.Uncontained | LookupFlags.Approximate, + bool checkProximity = true, + bool useFov = true, + float? fovOverride = null) + { + watchers = null; + + using var realWatchers = ListPoolEntity.Rent(); + if (!TryGetWatchers(target, realWatchers.Value, type, flags, useFov, checkProximity, fovOverride)) + return false; + + watchers = realWatchers.Value.Count; + return true; + } + + public bool TryGetWatchers(EntityUid target, + List> realWatchers, + LineOfSightBlockerLevel type = LineOfSightBlockerLevel.Transparent, + LookupFlags flags = LookupFlags.Uncontained | LookupFlags.Approximate, + bool checkProximity = true, + bool useFov = true, + float? fovOverride = null) + { + using var potentialWatchers = HashSetPoolEntity.Rent(); + _lookup.GetEntitiesInRange(Transform(target).Coordinates, SeeRange, potentialWatchers.Value, flags); + + return TryGetWatchersFrom(target, + realWatchers, + potentialWatchers.Value, + type, + checkProximity, + useFov, + fovOverride); + } + + public bool TryGetWatchersFrom(EntityUid target, + List> realWatchers, + ICollection> potentialWatchers, + LineOfSightBlockerLevel type = LineOfSightBlockerLevel.Transparent, + bool checkProximity = true, + bool useFov = true, + float? fovOverride = null) + { + foreach (var viewer in potentialWatchers) + { + if (!IsWatchedBy(target, viewer, type, useFov, checkProximity, fovOverride)) + continue; + + realWatchers.Add(viewer); + } + + return realWatchers.Any(); + } + + public bool IsWatchedByAny(EntityUid target, + LineOfSightBlockerLevel type = LineOfSightBlockerLevel.Transparent, + LookupFlags flags = LookupFlags.Uncontained | LookupFlags.Approximate, + bool checkProximity = true, + bool useFov = true, + float? fovOverride = null) + { + using var potentialWatchers = HashSetPoolEntity.Rent(); + _lookup.GetEntitiesInRange(Transform(target).Coordinates, SeeRange, potentialWatchers.Value, flags); + + foreach (var viewer in potentialWatchers.Value) + { + if (!IsWatchedBy(target, viewer, type, useFov, checkProximity, fovOverride)) + continue; + + return true; + } + + return false; + } + + public bool IsWatchedBy(EntityUid target, + EntityUid potentialViewer, + LineOfSightBlockerLevel type = LineOfSightBlockerLevel.Transparent, + bool checkProximity = true, + bool useFov = true, + float? fovOverride = null) + { + if (!CanBeWatched(potentialViewer, target)) + return false; + + if (checkProximity && !IsInProximity(potentialViewer, target, type)) + return false; + + if (!CanSee(potentialViewer, target, useFov, fovOverride)) + return false; + + return true; + } + + /// + /// Проверяет, может ли цель вообще быть увидена смотрящим + /// + /// + /// Проверка заключается в поиске базовых компонентов, без которых Watching система не будет работать + /// + /// Смотрящий, который в теории может увидеть цель + /// Цель, которую мы проверяем на возможность быть увиденной смотрящим + /// Да/нет + public bool CanBeWatched(Entity viewer, EntityUid target) + { + if (!_blinkableQuery.Resolve(viewer.Owner, ref viewer.Comp, false)) + return false; + + if (viewer.Owner == target) + return false; + + if (_insideStorageQuery.HasComp(viewer)) + return false; + + if (_mobStateQuery.TryComp(viewer, out var mobState) && _mobState.IsIncapacitated(viewer, mobState)) + return false; + + return true; + } +} diff --git a/Content.Shared/_Scp/Watching/EyeWatchingSystem.API.Watching.cs b/Content.Shared/_Scp/Watching/EyeWatchingSystem.API.Watching.cs new file mode 100644 index 00000000000..b9f9c5080f4 --- /dev/null +++ b/Content.Shared/_Scp/Watching/EyeWatchingSystem.API.Watching.cs @@ -0,0 +1,49 @@ +using System.Linq; +using Content.Shared._Scp.Helpers; +using Content.Shared._Scp.Proximity; + +namespace Content.Shared._Scp.Watching; + +public sealed partial class EyeWatchingSystem +{ + public bool TryGetWatchingTargets(EntityUid watcher, + List> targets, + LineOfSightBlockerLevel type = LineOfSightBlockerLevel.Transparent, + LookupFlags flags = LookupFlags.Uncontained | LookupFlags.Approximate, + bool checkProximity = true, + bool useFov = true, + float? fovOverride = null) + where T : IComponent + { + using var potentialTargets = HashSetPoolEntity.Rent(); + _lookup.GetEntitiesInRange(Transform(watcher).Coordinates, SeeRange, potentialTargets.Value, flags); + + return TryGetWatchingTargetsFrom(watcher, + targets, + potentialTargets.Value, + type, + checkProximity, + useFov, + fovOverride); + } + + public bool TryGetWatchingTargetsFrom(EntityUid watcher, + List> targets, + ICollection> potentialTargets, + LineOfSightBlockerLevel type = LineOfSightBlockerLevel.Transparent, + bool checkProximity = true, + bool useFov = true, + float? fovOverride = null) + where T : IComponent + { + foreach (var target in potentialTargets) + { + if (!IsWatchedBy(target, watcher, type, useFov, checkProximity, fovOverride)) + continue; + + targets.Add(target); + } + + return targets.Any(); + } +} diff --git a/Content.Shared/_Scp/Watching/EyeWatchingSystem.API.cs b/Content.Shared/_Scp/Watching/EyeWatchingSystem.API.cs deleted file mode 100644 index 2431a0ab828..00000000000 --- a/Content.Shared/_Scp/Watching/EyeWatchingSystem.API.cs +++ /dev/null @@ -1,267 +0,0 @@ -using System.Linq; -using Content.Shared._Scp.Blinking; -using Content.Shared._Scp.Helpers; -using Content.Shared._Scp.Proximity; -using Content.Shared._Scp.Watching.FOV; -using Content.Shared.Eye.Blinding.Systems; -using Content.Shared.Mobs.Components; -using Content.Shared.Mobs.Systems; -using Content.Shared.Storage.Components; - -namespace Content.Shared._Scp.Watching; - -public sealed partial class EyeWatchingSystem -{ - [Dependency] private readonly SharedBlinkingSystem _blinking = default!; - [Dependency] private readonly EntityLookupSystem _lookup = default!; - [Dependency] private readonly MobStateSystem _mobState = default!; - [Dependency] private readonly FieldOfViewSystem _fov = default!; - - private EntityQuery _mobStateQuery; - private EntityQuery _insideStorageQuery; - - private void InitializeApi() - { - _mobStateQuery = GetEntityQuery(); - _insideStorageQuery = GetEntityQuery(); - } - - public bool IsWatched(EntityUid ent, bool useFov = true, float? fovOverride = null) - { - return IsWatched(ent, out _, useFov, fovOverride); - } - - public bool IsWatched(EntityUid ent, out int watchersCount, bool useFov = true, float? fovOverride = null) - { - using var potentialWatchers = ListPoolEntity.Rent(); - using var searchSet = HashSetPoolEntity.Rent(); - - var result = IsWatched(ent, potentialWatchers.Value, searchSet.Value, useFov, fovOverride); - watchersCount = potentialWatchers.Value.Count; - - return result; - } - - /// - /// Проверяет, смотрит ли кто-то на указанную цель - /// - /// Цель, которую проверяем - /// Количество смотрящих - /// Нужно ли проверять поле зрения - /// Если нужно использовать другой угол обзора, отличный от стандартного - /// Смотрит ли на цель хоть кто-то - public bool IsWatched(EntityUid ent, - List> potentialWatchers, - HashSet> searchSet, - bool useFov = true, - float? fovOverride = null) - { - if (!TryGetAllEntitiesVisibleTo(ent, potentialWatchers, searchSet)) - return false; - - return IsWatchedByAny(ent, potentialWatchers , useFov, fovOverride); - } - - public bool TryGetAllEntitiesVisibleTo( - Entity ent, - List> potentialWatchers, - LineOfSightBlockerLevel type = LineOfSightBlockerLevel.Transparent, - LookupFlags flags = LookupFlags.All) - { - using var searchSet = HashSetPoolEntity.Rent(); - var result = TryGetAllEntitiesVisibleTo(ent, potentialWatchers, searchSet.Value, type, flags); - - return result; - } - - public bool TryGetAllEntitiesVisibleTo( - Entity ent, - List> potentialWatchers, - LineOfSightBlockerLevel type = LineOfSightBlockerLevel.Transparent, - LookupFlags flags = LookupFlags.All) - where T : IComponent - { - using var searchSet = HashSetPoolEntity.Rent(); - return TryGetAllEntitiesVisibleTo(ent, potentialWatchers, searchSet.Value, type, flags); - } - - /// - /// Получает и возвращает всех потенциально смотрящих на указанную цель. - /// - /// - /// В методе нет проверок на дополнительные состояния, такие как моргание/закрыты ли глаза/поле зрения т.п. - /// Единственная проверка - можно ли физически увидеть цель(т.е. не закрыта ли она стеной и т.п.) - /// - /// Цель, для которой ищем потенциальных смотрящих - /// Список всех, кто потенциально видит цель - /// Требуемая прозрачность линии видимости. - /// Заранее заготовленный список, который будет использоваться в - /// Список флагов для поиска целей в - /// Удалось ли найти хоть кого-то - public bool TryGetAllEntitiesVisibleTo( - Entity ent, - List> potentialWatchers, - HashSet> searchSet, - LineOfSightBlockerLevel type = LineOfSightBlockerLevel.Transparent, - LookupFlags flags = LookupFlags.All) - where T : IComponent - { - if (!Resolve(ent.Owner, ref ent.Comp)) - return false; - - searchSet.Clear(); - _lookup.GetEntitiesInRange(ent.Comp.Coordinates, SeeRange, searchSet, flags); - - foreach (var target in searchSet) - { - if (target.Owner == ent.Owner) - continue; - - if (!_proximity.IsRightType(ent, target, type, out _)) - continue; - - potentialWatchers.Add(target); - } - - return potentialWatchers.Count != 0; - } - - public bool IsWatchedBy(EntityUid target, EntityUid potentialViewer, bool useFov = true, float? fovOverride = null) - { - if (!CanBeWatched(potentialViewer, target)) - return false; - - if (IsEyeBlinded(potentialViewer, target, useFov, fovOverride)) - return false; - - return true; - } - - /// - /// Проверяет, смотрят ли переданные сущности на указанную цель. Передает список всех сущностей, что действительно смотрят на цель - /// - /// Цель - /// Список сущностей для проверки - /// Список всех сущностей, что действительно смотрят на цель - /// Нужно ли проверять, находится ли цель в поле зрения сущности - /// Если нужно перезаписать угол поля зрения - /// Смотрит ли хоть кто-то на цель - public bool TryGetWatchers(EntityUid target, - List> potentialViewers, - List> realViewers, - bool useFov = true, - float? fovOverride = null) - { - foreach (var viewer in potentialViewers) - { - if (!IsWatchedBy(target, viewer, useFov, fovOverride)) - continue; - - realViewers.Add(viewer); - } - - return realViewers.Any(); - } - - public bool IsWatchedByAny(EntityUid target, - List> potentialViewers, - bool useFov = true, - float? fovOverride = null) - { - foreach (var viewer in potentialViewers) - { - if (!IsWatchedBy(target, viewer, useFov, fovOverride)) - continue; - - return true; - } - - return false; - } - - /// - /// Простая проверка на то, видят ли переданную сущность другие сущности. - /// Вместо проверки на интервальное моргание используется проверка на мануальное закрытие глаз. - /// - /// Сущность, на которую смотрят - /// Смотрящие - /// Смотри ли хоть кто-нибудь из переданных - public bool SimpleIsWatchedBy(EntityUid target, List potentialViewers) - { - foreach (var viewer in potentialViewers) - { - if (!SimpleIsWatchedBy(target, viewer)) - continue; - - return true; - } - - return false; - } - - public bool SimpleIsWatchedBy(EntityUid target, EntityUid potentialViewer) - { - if (!CanBeWatched(potentialViewer, target)) - return false; - - if (_blinking.AreEyesClosedManually(potentialViewer)) - return false; - - return true; - } - - /// - /// Проверяет, может ли цель вообще быть увидена смотрящим - /// - /// - /// Проверка заключается в поиске базовых компонентов, без которых Watching система не будет работать - /// - /// Смотрящий, который в теории может увидеть цель - /// Цель, которую мы проверяем на возможность быть увиденной смотрящим - /// Да/нет - public bool CanBeWatched(Entity viewer, EntityUid target) - { - if (!Resolve(viewer.Owner, ref viewer.Comp, false)) - return false; - - if (viewer.Owner == target) - return false; - - if (_insideStorageQuery.HasComp(viewer)) - return false; - - if (_mobStateQuery.TryComp(viewer, out var mobState) && _mobState.IsIncapacitated(viewer, mobState)) - return false; - - return true; - } - - /// - /// Проверка на то, может ли смотрящий видеть цель - /// - /// Смотрящий - /// Цель, которую проверяем - /// Применять ли проверку на поле зрения? - /// Если нужно использовать другой угол поля зрения - /// Видит ли смотрящий цель - public bool IsEyeBlinded(Entity viewer, EntityUid target, bool useFov = true, float? fovOverride = null) - { - if (_mobState.IsIncapacitated(viewer)) - return true; - - // Проверяем, видит ли смотрящий цель - if (useFov && !_fov.IsInFov(viewer.Owner, target, fovOverride)) - return true; // Если не видит, то не считаем его как смотрящего - - if (_blinking.IsBlind(viewer, true)) - return true; - - var canSeeAttempt = new CanSeeAttemptEvent(); - RaiseLocalEvent(viewer, canSeeAttempt); - - if (canSeeAttempt.Blind) - return true; - - return false; - } -} From 619a79a6ef27fd518786864e748dbb88b6f7925b Mon Sep 17 00:00:00 2001 From: drdth Date: Tue, 17 Mar 2026 08:39:56 +0300 Subject: [PATCH 05/17] refactor: micro optimize proximity --- .../Proximity/ProximityReceiverComponent.cs | 49 +++++++++---------- .../_Scp/Proximity/ProximitySystem.cs | 6 +-- 2 files changed, 27 insertions(+), 28 deletions(-) diff --git a/Content.Shared/_Scp/Proximity/ProximityReceiverComponent.cs b/Content.Shared/_Scp/Proximity/ProximityReceiverComponent.cs index c5c3326c74c..c573962c1f7 100644 --- a/Content.Shared/_Scp/Proximity/ProximityReceiverComponent.cs +++ b/Content.Shared/_Scp/Proximity/ProximityReceiverComponent.cs @@ -12,12 +12,15 @@ public sealed partial class ProximityReceiverComponent : Component /// /// На каком расстоянии будут вызываться ивент? /// - [DataField, AutoNetworkedField, ViewVariables] + [DataField, AutoNetworkedField] public float CloseRange = 3f; /// - [DataField, ViewVariables, AutoNetworkedField] + [DataField, AutoNetworkedField] public LineOfSightBlockerLevel RequiredLineOfSight = LineOfSightBlockerLevel.Transparent; + + [DataField] + public LookupFlags Flags = LookupFlags.Uncontained | LookupFlags.Approximate; } /// @@ -51,33 +54,29 @@ public enum LineOfSightBlockerLevel /// Ивент, вызываемый при приближении к . /// Вызывается на сущности, к которой приблизились. /// -/// Цель, которая приблизилась -/// Текущее расстояние сущности до цели -/// Расстояние, на котором начинается триггер ивента -/// Фактический уровень видимости -public sealed class ProximityInRangeReceiverEvent(EntityUid target, float range, float closeRange, LineOfSightBlockerLevel type) : EntityEventArgs -{ - public readonly EntityUid Target = target; - public readonly float Range = range; - public readonly float CloseRange = closeRange; - public readonly LineOfSightBlockerLevel Type = type; -} +/// Цель, которая приблизилась +/// Текущее расстояние сущности до цели +/// Расстояние, на котором начинается триггер ивента +/// Фактический уровень видимости +[ByRefEvent] +public readonly record struct ProximityInRangeReceiverEvent(EntityUid Target, + float Range, + float CloseRange, + LineOfSightBlockerLevel Type); /// /// Ивент, вызываемый при приближении к . /// Вызывается на цели, которая приблизилась. /// -/// Сущность, к которой приблизились -/// Текущее расстояние цели до сущности -/// Расстояние, на котором начинается триггер ивента -/// Фактический уровень видимости -public sealed class ProximityInRangeTargetEvent(EntityUid receiver, float range, float closeRange, LineOfSightBlockerLevel type) : EntityEventArgs -{ - public readonly EntityUid Receiver = receiver; - public readonly float Range = range; - public readonly float CloseRange = closeRange; - public readonly LineOfSightBlockerLevel Type = type; -} +/// Сущность, к которой приблизились +/// Текущее расстояние цели до сущности +/// Расстояние, на котором начинается триггер ивента +/// Фактический уровень видимости +[ByRefEvent] +public readonly record struct ProximityInRangeTargetEvent(EntityUid Receiver, + float Range, + float CloseRange, + LineOfSightBlockerLevel Type); /// /// Ивент, вызываемый, когда сущность отсутствует рядом с любым . @@ -85,4 +84,4 @@ public sealed class ProximityInRangeTargetEvent(EntityUid receiver, float range, /// Служит, чтобы убирать какие-то эффекты, вызванные ивента приближения. /// [ByRefEvent] -public record struct ProximityNotInRangeTargetEvent; +public readonly record struct ProximityNotInRangeTargetEvent; diff --git a/Content.Shared/_Scp/Proximity/ProximitySystem.cs b/Content.Shared/_Scp/Proximity/ProximitySystem.cs index 508be5d62cf..f4771c3f98a 100644 --- a/Content.Shared/_Scp/Proximity/ProximitySystem.cs +++ b/Content.Shared/_Scp/Proximity/ProximitySystem.cs @@ -100,7 +100,7 @@ public override void Update(float frameTime) while (query.MoveNext(out var uid, out var receiver, out var xform)) { Targets.Clear(); - _lookup.GetEntitiesInRange(xform.Coordinates, receiver.CloseRange, Targets); + _lookup.GetEntitiesInRange(xform.Coordinates, receiver.CloseRange, Targets, receiver.Flags); foreach (var target in Targets) { @@ -113,10 +113,10 @@ public override void Update(float frameTime) continue; var receiverEvent = new ProximityInRangeReceiverEvent(target, range, receiver.CloseRange, lightOfSightBlockerLevel); - RaiseLocalEvent(uid, receiverEvent); + RaiseLocalEvent(uid, ref receiverEvent); var targetEvent = new ProximityInRangeTargetEvent(uid, range, receiver.CloseRange, lightOfSightBlockerLevel); - RaiseLocalEvent(target, targetEvent); + RaiseLocalEvent(target, ref targetEvent); PossibleNotInRange.Remove(target); } From 7e9042c7b6e7e21f5ad40ef5feb4aa07a88a1256 Mon Sep 17 00:00:00 2001 From: drdth Date: Tue, 17 Mar 2026 08:49:12 +0300 Subject: [PATCH 06/17] refactor: little more optimizations for blinking system --- .../_Scp/Blinking/BlinkingSystem.cs | 2 +- .../SharedBlinkingSystem.EyeClosing.cs | 24 +++++++---- .../_Scp/Blinking/SharedBlinkingSystem.cs | 40 ++++++++++--------- 3 files changed, 38 insertions(+), 28 deletions(-) diff --git a/Content.Client/_Scp/Blinking/BlinkingSystem.cs b/Content.Client/_Scp/Blinking/BlinkingSystem.cs index c06e3f6fb96..31ed601a2ec 100644 --- a/Content.Client/_Scp/Blinking/BlinkingSystem.cs +++ b/Content.Client/_Scp/Blinking/BlinkingSystem.cs @@ -84,7 +84,7 @@ private void OnEyesStateChanged(EntityEyesStateChanged ev) var ent = GetEntity(ev.NetEntity); - if (!TryComp(ent, out var blinkable)) + if (!BlinkableQuery.TryComp(ent, out var blinkable)) return; if (ev.NewState == EyesState.Closed) diff --git a/Content.Shared/_Scp/Blinking/SharedBlinkingSystem.EyeClosing.cs b/Content.Shared/_Scp/Blinking/SharedBlinkingSystem.EyeClosing.cs index 66ee5110936..e88caf38734 100644 --- a/Content.Shared/_Scp/Blinking/SharedBlinkingSystem.EyeClosing.cs +++ b/Content.Shared/_Scp/Blinking/SharedBlinkingSystem.EyeClosing.cs @@ -30,7 +30,7 @@ private void InitializeEyeClosing() #region Event handlers - private void OnShutdown(Entity ent, ref ComponentShutdown _) + private void OnShutdown(Entity ent, ref ComponentShutdown args) { _actions.RemoveAction(ent.Owner, ent.Comp.EyeToggleActionEntity); @@ -101,7 +101,7 @@ private void OnTryingSleep(Entity ent, ref TryingToSleepEven /// private void OnHumanoidClosedEyes(Entity ent, ref EntityClosedEyesEvent args) { - if (!TryComp(ent, out var blinkableComponent)) + if (!BlinkableQuery.TryComp(ent, out var blinkableComponent)) return; blinkableComponent.CachedEyesColor = ent.Comp.EyeColor; @@ -114,7 +114,7 @@ private void OnHumanoidClosedEyes(Entity ent, ref E /// private void OnHumanoidOpenedEyes(Entity ent, ref EntityOpenedEyesEvent args) { - if (!TryComp(ent, out var blinkableComponent)) + if (!BlinkableQuery.TryComp(ent, out var blinkableComponent)) return; if (blinkableComponent.CachedEyesColor == null) @@ -152,7 +152,7 @@ public bool TrySetEyelids(Entity ent, bool useEffects = false, TimeSpan? customBlinkDuration = null) { - if (!Resolve(ent, ref ent.Comp)) + if (!BlinkableQuery.Resolve(ent, ref ent.Comp)) return false; if (ent.Comp.State == newState) @@ -192,7 +192,7 @@ public bool CanToggleEyes(Entity ent, EyesState newState) /// True -> закрыты, False -> открыты public bool AreEyesClosed(Entity ent) { - if (!Resolve(ent.Owner, ref ent.Comp, false)) + if (!BlinkableQuery.Resolve(ent.Owner, ref ent.Comp, false)) return false; // Мб одна из проверок лишняя, но ладно пусть будет. Не сильная потеря производительности @@ -207,7 +207,7 @@ public bool AreEyesClosed(Entity ent) /// public bool AreEyesClosedManually(Entity ent) { - if (!Resolve(ent.Owner, ref ent.Comp, false)) + if (!BlinkableQuery.Resolve(ent.Owner, ref ent.Comp, false)) return false; if (ent.Comp.State == EyesState.Opened) @@ -265,9 +265,17 @@ private void SetEyelids(Entity ent, nameof(BlinkableComponent.NextOpenEyesRequiresEffects)); if (newState == EyesState.Closed) - RaiseLocalEvent(ent, new EntityClosedEyesEvent(manual, useEffects, customBlinkDuration)); + { + var closedEvent = new EntityClosedEyesEvent(manual, useEffects, customBlinkDuration); + RaiseLocalEvent(ent, ref closedEvent); + } + else - RaiseLocalEvent(ent, new EntityOpenedEyesEvent(manual, useEffects || openEyesRequiresEffects, customBlinkDuration)); + { + var openedEvent = + new EntityOpenedEyesEvent(manual, useEffects || openEyesRequiresEffects, customBlinkDuration); + RaiseLocalEvent(ent, ref openedEvent); + } if (predicted) RaiseLocalEvent(ent, new EntityEyesStateChanged(oldState, newState, manual)); diff --git a/Content.Shared/_Scp/Blinking/SharedBlinkingSystem.cs b/Content.Shared/_Scp/Blinking/SharedBlinkingSystem.cs index a779dc19fad..51346b465ee 100644 --- a/Content.Shared/_Scp/Blinking/SharedBlinkingSystem.cs +++ b/Content.Shared/_Scp/Blinking/SharedBlinkingSystem.cs @@ -25,6 +25,8 @@ public abstract partial class SharedBlinkingSystem : EntitySystem [Dependency] private readonly IGameTiming _timing = default!; [Dependency] private readonly INetManager _net = default!; + protected EntityQuery BlinkableQuery; + public override void Initialize() { base.Initialize(); @@ -35,6 +37,8 @@ public override void Initialize() SubscribeLocalEvent(OnMobStateChanged); InitializeEyeClosing(); + + BlinkableQuery = GetEntityQuery(); } #region Event handlers @@ -120,7 +124,7 @@ public override void Update(float frameTime) private bool TryBlink(Entity ent, TimeSpan? customDuration = null) { - if (!Resolve(ent.Owner, ref ent.Comp)) + if (!BlinkableQuery.Resolve(ent.Owner, ref ent.Comp)) return false; if (_timing.CurTime < ent.Comp.NextBlink) @@ -142,7 +146,7 @@ private bool TryBlink(Entity ent, TimeSpan? customDuration /// Предугадывается ли клиентом этот вызов метода? Если нет, отправляет клиенту стейт с сервера. public void SetNextBlink(Entity ent, TimeSpan interval, TimeSpan? variance = null, bool predicted = true) { - if (!Resolve(ent, ref ent.Comp)) + if (!BlinkableQuery.Resolve(ent, ref ent.Comp)) return; if (!variance.HasValue) @@ -162,7 +166,7 @@ public void SetNextBlink(Entity ent, TimeSpan interval, Tim public void ResetBlink(Entity ent, bool useVariance = true, bool predicted = true) { - if (!Resolve(ent, ref ent.Comp)) + if (!BlinkableQuery.Resolve(ent, ref ent.Comp)) return; // Если useVariance == false, то variance = 0 @@ -183,7 +187,7 @@ public void ResetBlink(Entity ent, bool useVariance = true, /// public bool IsBlind(Entity ent, bool useTimeCompensation = false) { - if (!Resolve(ent, ref ent.Comp, false)) + if (!BlinkableQuery.Resolve(ent, ref ent.Comp, false)) return false; // Специально для сцп173. Он должен начинать остановку незадолго до того, как у людей откроются глаза @@ -200,7 +204,7 @@ public void ForceBlind(Entity ent, TimeSpan duration, bool if (_mobState.IsIncapacitated(ent)) return; - if (!Resolve(ent, ref ent.Comp)) + if (!BlinkableQuery.Resolve(ent, ref ent.Comp)) return; // Если у персонажа уже закрыты глаза, то обновляем время @@ -256,26 +260,24 @@ protected bool IsScpNearby(EntityUid player) { // Получаем всех Scp с механиками зрения, которые видят игрока using var scp173List = ListPoolEntity.Rent(); - if (!_watching.TryGetAllEntitiesVisibleTo(player, scp173List.Value)) + if (!_watching.TryGetAllEntitiesVisibleTo(player, scp173List.Value, flags: LookupFlags.Dynamic | LookupFlags.Approximate)) return false; return scp173List.Value.Any(e => _watching.CanBeWatched(player, e)); } } -public sealed class EntityOpenedEyesEvent(bool manual = false, bool useEffects = false, TimeSpan? customNextTimeBlinkInterval = null) : EntityEventArgs -{ - public readonly bool Manual = manual; - public readonly bool UseEffects = useEffects; - public readonly TimeSpan? CustomNextTimeBlinkInterval = customNextTimeBlinkInterval; -} - -public sealed class EntityClosedEyesEvent(bool manual = false, bool useEffects = false, TimeSpan? customBlinkDuration = null) : EntityEventArgs -{ - public readonly bool Manual = manual; - public readonly bool UseEffects = useEffects; - public readonly TimeSpan? CustomBlinkDuration = customBlinkDuration; -} +[ByRefEvent] +public readonly record struct EntityOpenedEyesEvent( + bool Manual = false, + bool UseEffects = false, + TimeSpan? CustomNextTimeBlinkInterval = null); + +[ByRefEvent] +public readonly record struct EntityClosedEyesEvent( + bool Manual = false, + bool UseEffects = false, + TimeSpan? CustomBlinkDuration = null); [Serializable, NetSerializable] public sealed class EntityEyesStateChanged(EyesState oldState, EyesState newState, bool manual = false, bool useEffects = false, NetEntity? netEntity = null) : EntityEventArgs From 21612864f9eb09247e16109eba29dfff5d650157 Mon Sep 17 00:00:00 2001 From: drdth Date: Tue, 17 Mar 2026 09:51:53 +0300 Subject: [PATCH 07/17] refactor: remove duplicate proximity checks from EyeWatchingSystem Update loop. Decrease WatchingCheckInterval from 0.3 to 0.1 --- Content.Shared/_Scp/Helpers/CollectionPool.cs | 3 - .../EyeWatchingSystem.API.Internal.cs | 60 +++++++++++++++++++ .../{EyeWatching.cs => EyeWatchingSystem.cs} | 23 ++++--- .../_Scp/Watching/WatchingTargetComponent.cs | 2 +- 4 files changed, 71 insertions(+), 17 deletions(-) create mode 100644 Content.Shared/_Scp/Watching/EyeWatchingSystem.API.Internal.cs rename Content.Shared/_Scp/Watching/{EyeWatching.cs => EyeWatchingSystem.cs} (90%) diff --git a/Content.Shared/_Scp/Helpers/CollectionPool.cs b/Content.Shared/_Scp/Helpers/CollectionPool.cs index 3fd1314e0e8..d75eec96bcf 100644 --- a/Content.Shared/_Scp/Helpers/CollectionPool.cs +++ b/Content.Shared/_Scp/Helpers/CollectionPool.cs @@ -59,10 +59,7 @@ public static PooledCollection Rent() internal static void Return(TCollection collection) { if (Pool.Count >= 512) - { - Logger.Warning("Pool bloated, new collections will not be stored"); return; - } if (collection is List list && list.Capacity > 2048) return; diff --git a/Content.Shared/_Scp/Watching/EyeWatchingSystem.API.Internal.cs b/Content.Shared/_Scp/Watching/EyeWatchingSystem.API.Internal.cs new file mode 100644 index 00000000000..27c1420053a --- /dev/null +++ b/Content.Shared/_Scp/Watching/EyeWatchingSystem.API.Internal.cs @@ -0,0 +1,60 @@ +using Content.Shared._Scp.Blinking; +using Content.Shared._Scp.Helpers; +using Content.Shared._Scp.Proximity; + +namespace Content.Shared._Scp.Watching; + +public sealed partial class EyeWatchingSystem +{ + /* + * Внутренние методы для работы внутри Update() цикла. + * Нужны, чтобы исключить дублирование проверки Proximity(), + * которая происходит из-за логики Simple и Full проверок в цикле. + * По факту являются чуть исправленной копией публичных методов, + * но с использованием WatchCandidate для переноса просчитанного BlockerLevel + */ + + private bool _TryGetAllEntitiesVisibleTo( + Entity ent, + List potentialWatchers, + LineOfSightBlockerLevel type = LineOfSightBlockerLevel.Transparent, + LookupFlags flags = LookupFlags.Uncontained | LookupFlags.Approximate) + { + if (!Resolve(ent.Owner, ref ent.Comp)) + return false; + + using var searchSet = HashSetPoolEntity.Rent(); + _lookup.GetEntitiesInRange(ent.Comp.Coordinates, SeeRange, searchSet.Value, flags); + + foreach (var target in searchSet.Value) + { + if (!_proximity.IsRightType(ent, target, type, out var blockerLevel)) + continue; + + potentialWatchers.Add(new WatchCandidate(target, blockerLevel)); + } + + return potentialWatchers.Count != 0; + } + + private bool _TryGetWatchersFrom(EntityUid target, + List realWatchers, + ICollection potentialWatchers, + LineOfSightBlockerLevel type = LineOfSightBlockerLevel.Transparent, + bool checkProximity = true, + bool useFov = true, + float? fovOverride = null) + { + foreach (var viewer in potentialWatchers) + { + if (!IsWatchedBy(target, viewer.Viewer, type, useFov, checkProximity, fovOverride)) + continue; + + realWatchers.Add(viewer); + } + + return realWatchers.Count != 0; + } + + private readonly record struct WatchCandidate(Entity Viewer, LineOfSightBlockerLevel BlockerLevel); +} diff --git a/Content.Shared/_Scp/Watching/EyeWatching.cs b/Content.Shared/_Scp/Watching/EyeWatchingSystem.cs similarity index 90% rename from Content.Shared/_Scp/Watching/EyeWatching.cs rename to Content.Shared/_Scp/Watching/EyeWatchingSystem.cs index 02c9c565303..7212c0a9c66 100644 --- a/Content.Shared/_Scp/Watching/EyeWatching.cs +++ b/Content.Shared/_Scp/Watching/EyeWatchingSystem.cs @@ -48,10 +48,8 @@ public override void Update(float frameTime) continue; // Все потенциально возможные смотрящие. Среди них те, что прошли фаст-чек из самых простых проверок - using var potentialViewers = ListPoolEntity.Rent(); - using var searchList = HashSetPoolEntity.Rent(); - - if (!TryGetAllEntitiesVisibleTo(uid, potentialViewers.Value, searchList.Value)) + using var potentialViewers = ListPool.Rent(); + if (!_TryGetAllEntitiesVisibleTo(uid, potentialViewers.Value)) continue; // Вызываем ивенты на потенциально смотрящих. Без особых проверок @@ -59,10 +57,10 @@ public override void Update(float frameTime) foreach (var potentialViewer in potentialViewers.Value) { var simpleViewerEvent = new SimpleEntityLookedAtEvent((uid, watchingComponent)); - var simpleTargetEvent = new SimpleEntitySeenEvent(potentialViewer); + var simpleTargetEvent = new SimpleEntitySeenEvent(potentialViewer.Viewer); // За подробностями какой ивент для чего навести мышку на название ивента - RaiseLocalEvent(potentialViewer, ref simpleViewerEvent); + RaiseLocalEvent(potentialViewer.Viewer, ref simpleViewerEvent); RaiseLocalEvent(uid, ref simpleTargetEvent); } @@ -78,26 +76,25 @@ public override void Update(float frameTime) // Проверяет всех потенциальных смотрящих на то, действительно ли они видят цель. // Каждый потенциально смотрящий проходит полный комплекс проверок. // Выдает полный список всех сущностей, кто действительно видит цель - using var realViewers = ListPoolEntity.Rent(); - if (!TryGetWatchersFrom(uid, realViewers.Value, potentialViewers.Value)) + using var realViewers = ListPool.Rent(); + if (!_TryGetWatchersFrom(uid, realViewers.Value, potentialViewers.Value, checkProximity: false)) continue; // Вызываем ивент на смотрящем, говорящие, что он действительно видит цель foreach (var viewer in realViewers.Value) { - var netViewer = GetNetEntity(viewer); + var netViewer = GetNetEntity(viewer.Viewer); var firstTime = !watchingComponent.AlreadyLookedAt.ContainsKey(netViewer); - var blockerLevel = _proximity.GetLightOfSightBlockerLevel(viewer, uid); // Небольшая заглушка для удобства работы с ивентами. // Использовать firstTime не очень удобно, поэтому в качестве дополнительного способа определения будет TimeSpan.Zero watchingComponent.AlreadyLookedAt[netViewer] = TimeSpan.Zero; // За подробностями какой ивент для чего навести мышку на название ивента - var viewerEvent = new EntityLookedAtEvent((uid, watchingComponent), firstTime, blockerLevel); - var targetEvent = new EntitySeenEvent(viewer, firstTime, blockerLevel); + var viewerEvent = new EntityLookedAtEvent((uid, watchingComponent), firstTime, viewer.BlockerLevel); + var targetEvent = new EntitySeenEvent(viewer.Viewer, firstTime, viewer.BlockerLevel); - RaiseLocalEvent(viewer, ref viewerEvent); + RaiseLocalEvent(viewer.Viewer, ref viewerEvent); RaiseLocalEvent(uid, ref targetEvent); // Добавляет смотрящего в список уже смотревших, чтобы позволить системам манипулировать этим diff --git a/Content.Shared/_Scp/Watching/WatchingTargetComponent.cs b/Content.Shared/_Scp/Watching/WatchingTargetComponent.cs index cba3cedeee3..4d65ddb34a1 100644 --- a/Content.Shared/_Scp/Watching/WatchingTargetComponent.cs +++ b/Content.Shared/_Scp/Watching/WatchingTargetComponent.cs @@ -20,7 +20,7 @@ public sealed partial class WatchingTargetComponent : Component /// Время между проверками зрения /// [DataField] - public TimeSpan WatchingCheckInterval = TimeSpan.FromSeconds(0.3f); + public TimeSpan WatchingCheckInterval = TimeSpan.FromSeconds(0.1f); /// /// Время следующей проверки зрения From c95fec54fbc7dce0d220b8bfd77e93433e1ad202 Mon Sep 17 00:00:00 2001 From: drdth Date: Fri, 20 Mar 2026 17:29:45 +0300 Subject: [PATCH 08/17] add: docs and little performance improvement --- .../_Scp/Fear/Systems/SharedFearSystem.cs | 3 +- .../Watching/EyeWatchingSystem.API.Base.cs | 76 +++++++++++++++++-- .../EyeWatchingSystem.API.Internal.cs | 60 --------------- .../Watching/EyeWatchingSystem.API.Watched.cs | 66 ++++++++++++++-- .../EyeWatchingSystem.API.Watching.cs | 30 +++++++- .../_Scp/Watching/EyeWatchingSystem.cs | 33 ++++---- .../_Scp/Watching/WatchingTargetComponent.cs | 2 +- 7 files changed, 177 insertions(+), 93 deletions(-) delete mode 100644 Content.Shared/_Scp/Watching/EyeWatchingSystem.API.Internal.cs diff --git a/Content.Shared/_Scp/Fear/Systems/SharedFearSystem.cs b/Content.Shared/_Scp/Fear/Systems/SharedFearSystem.cs index b8ac7498005..12d1e05792a 100644 --- a/Content.Shared/_Scp/Fear/Systems/SharedFearSystem.cs +++ b/Content.Shared/_Scp/Fear/Systems/SharedFearSystem.cs @@ -32,6 +32,7 @@ public abstract partial class SharedFearSystem : EntitySystem [Dependency] private readonly EyeWatchingSystem _watching = default!; [Dependency] private readonly MobStateSystem _mobState = default!; [Dependency] private readonly SharedShaderStrengthSystem _shaderStrength = default!; + [Dependency] private readonly ProximitySystem _proximity = default!; [Dependency] private readonly IGameTiming _timing = default!; private const string MoodSourceClose = "FearSourceClose"; @@ -78,7 +79,7 @@ private void OnEntityLookedAt(Entity ent, ref EntityLookedAtEvent // Проверка на видимость. // Это нужно, чтобы можно было не пугаться через стекло, например. // Это будет использовано, например, у ученых, которые 100 лет видели сцп через стекла и не должны пугаться. - if (args.BlockerLevel > ent.Comp.SeenBlockerLevel) + if (!_proximity.IsRightType(args.Target, ent, ent.Comp.SeenBlockerLevel)) return; if (!args.Target.Comp.AlreadyLookedAt.TryGetValue(GetNetEntity(ent), out var lastSeenTime)) diff --git a/Content.Shared/_Scp/Watching/EyeWatchingSystem.API.Base.cs b/Content.Shared/_Scp/Watching/EyeWatchingSystem.API.Base.cs index 43876726c8e..aea504a47ce 100644 --- a/Content.Shared/_Scp/Watching/EyeWatchingSystem.API.Base.cs +++ b/Content.Shared/_Scp/Watching/EyeWatchingSystem.API.Base.cs @@ -28,6 +28,21 @@ private void InitializeApi() _blinkableQuery = GetEntityQuery(); } + /// + /// Получает и возвращает всех сущности в радиусе видимости для цели. + /// По сути является аналогом , но использует проверку на линию видимости. + /// + /// + /// В методе нет проверок на дополнительные состояния, такие как моргание/закрыты ли глаза/поле зрения т.п. + /// Единственная проверка - можно ли физически увидеть цель(т.е. не закрыта ли она стеной и т.п.). + /// + /// + /// Цель, для которой ищем сущности в радиусе видимости + /// Список всех, кто находится в радиусе видимости + /// Требуемая прозрачность линии видимости. + /// Список флагов для поиска целей в + /// Компонент, который должны иметь все сущности в радиусе видимости + /// Удалось ли найти хоть кого-то public bool TryGetAllEntitiesVisibleTo( Entity ent, List> potentialWatchers, @@ -40,17 +55,20 @@ public bool TryGetAllEntitiesVisibleTo( } /// - /// Получает и возвращает всех потенциально смотрящих на указанную цель. + /// Получает и возвращает всех сущности в радиусе видимости для цели. + /// По сути является аналогом , но использует проверку на линию видимости. /// /// /// В методе нет проверок на дополнительные состояния, такие как моргание/закрыты ли глаза/поле зрения т.п. - /// Единственная проверка - можно ли физически увидеть цель(т.е. не закрыта ли она стеной и т.п.) + /// Единственная проверка - можно ли физически увидеть цель(т.е. не закрыта ли она стеной и т.п.). + /// /// - /// Цель, для которой ищем потенциальных смотрящих - /// Список всех, кто потенциально видит цель + /// Цель, для которой ищем сущности в радиусе видимости + /// Список всех, кто находится в радиусе видимости + /// Заранее заготовленный список, который будет использоваться в /// Требуемая прозрачность линии видимости. - /// Заранее заготовленный список, который будет использоваться в /// Список флагов для поиска целей в + /// Компонент, который должны иметь все сущности в радиусе видимости /// Удалось ли найти хоть кого-то private bool TryGetAllEntitiesVisibleTo( Entity ent, @@ -77,6 +95,20 @@ private bool TryGetAllEntitiesVisibleTo( return potentialWatchers.Count != 0; } + /// + /// Проверяет, есть ли хоть одна сущность в радиусе видимости цели. + /// По сути является аналогом , но использует проверку на линию видимости. + /// + /// + /// В методе нет проверок на дополнительные состояния, такие как моргание/закрыты ли глаза/поле зрения т.п. + /// Единственная проверка - можно ли физически увидеть цель(т.е. не закрыта ли она стеной и т.п.). + /// + /// + /// Цель, для которой ищем сущности в радиусе видимости + /// Требуемая прозрачность линии видимости. + /// Список флагов для поиска целей в + /// Компонент, который должны иметь все сущности в радиусе видимости + /// Удалось ли найти хоть кого-то public bool TryGetAnyEntitiesVisibleTo( Entity viewer, LineOfSightBlockerLevel type = LineOfSightBlockerLevel.Transparent, @@ -90,6 +122,21 @@ public bool TryGetAnyEntitiesVisibleTo( return true; } + /// + /// Получает и возвращает первую сущность в радиусе видимости цели. + /// По сути является аналогом , но использует проверку на линию видимости. + /// + /// + /// В методе нет проверок на дополнительные состояния, такие как моргание/закрыты ли глаза/поле зрения т.п. + /// Единственная проверка - можно ли физически увидеть цель(т.е. не закрыта ли она стеной и т.п.). + /// + /// + /// Цель, для которой ищем сущности в радиусе видимости + /// Первая попавшаяся сущность в радиусе видимости цели + /// Требуемая прозрачность линии видимости. + /// Список флагов для поиска целей в + /// Компонент, который должны иметь все сущности в радиусе видимости + /// Удалось ли найти хоть кого-то public bool TryGetAnyEntitiesVisibleTo( Entity viewer, [NotNullWhen(true)] out Entity? firstVisible, @@ -107,6 +154,22 @@ public bool TryGetAnyEntitiesVisibleTo( return true; } + /// + /// Получает и возвращает первую сущность в радиусе видимости цели. + /// По сути является аналогом , но использует проверку на линию видимости. + /// + /// + /// В методе нет проверок на дополнительные состояния, такие как моргание/закрыты ли глаза/поле зрения т.п. + /// Единственная проверка - можно ли физически увидеть цель(т.е. не закрыта ли она стеной и т.п.). + /// + /// + /// Цель, для которой ищем сущности в радиусе видимости + /// Первая попавшаяся сущность в радиусе видимости цели + /// Заранее заготовленный список, который будет использоваться в + /// Требуемая прозрачность линии видимости. + /// Список флагов для поиска целей в + /// Компонент, который должны иметь все сущности в радиусе видимости + /// Удалось ли найти хоть кого-то private bool TryGetAnyEntitiesVisibleTo( Entity viewer, [NotNullWhen(true)] out Entity? firstVisible, @@ -135,6 +198,9 @@ private bool TryGetAnyEntitiesVisibleTo( return false; } + /// + /// Проверяет, правильный ли между сущностями тип видимости. + /// private bool IsInProximity(EntityUid ent, EntityUid target, LineOfSightBlockerLevel type) { if (target == ent) diff --git a/Content.Shared/_Scp/Watching/EyeWatchingSystem.API.Internal.cs b/Content.Shared/_Scp/Watching/EyeWatchingSystem.API.Internal.cs deleted file mode 100644 index 27c1420053a..00000000000 --- a/Content.Shared/_Scp/Watching/EyeWatchingSystem.API.Internal.cs +++ /dev/null @@ -1,60 +0,0 @@ -using Content.Shared._Scp.Blinking; -using Content.Shared._Scp.Helpers; -using Content.Shared._Scp.Proximity; - -namespace Content.Shared._Scp.Watching; - -public sealed partial class EyeWatchingSystem -{ - /* - * Внутренние методы для работы внутри Update() цикла. - * Нужны, чтобы исключить дублирование проверки Proximity(), - * которая происходит из-за логики Simple и Full проверок в цикле. - * По факту являются чуть исправленной копией публичных методов, - * но с использованием WatchCandidate для переноса просчитанного BlockerLevel - */ - - private bool _TryGetAllEntitiesVisibleTo( - Entity ent, - List potentialWatchers, - LineOfSightBlockerLevel type = LineOfSightBlockerLevel.Transparent, - LookupFlags flags = LookupFlags.Uncontained | LookupFlags.Approximate) - { - if (!Resolve(ent.Owner, ref ent.Comp)) - return false; - - using var searchSet = HashSetPoolEntity.Rent(); - _lookup.GetEntitiesInRange(ent.Comp.Coordinates, SeeRange, searchSet.Value, flags); - - foreach (var target in searchSet.Value) - { - if (!_proximity.IsRightType(ent, target, type, out var blockerLevel)) - continue; - - potentialWatchers.Add(new WatchCandidate(target, blockerLevel)); - } - - return potentialWatchers.Count != 0; - } - - private bool _TryGetWatchersFrom(EntityUid target, - List realWatchers, - ICollection potentialWatchers, - LineOfSightBlockerLevel type = LineOfSightBlockerLevel.Transparent, - bool checkProximity = true, - bool useFov = true, - float? fovOverride = null) - { - foreach (var viewer in potentialWatchers) - { - if (!IsWatchedBy(target, viewer.Viewer, type, useFov, checkProximity, fovOverride)) - continue; - - realWatchers.Add(viewer); - } - - return realWatchers.Count != 0; - } - - private readonly record struct WatchCandidate(Entity Viewer, LineOfSightBlockerLevel BlockerLevel); -} diff --git a/Content.Shared/_Scp/Watching/EyeWatchingSystem.API.Watched.cs b/Content.Shared/_Scp/Watching/EyeWatchingSystem.API.Watched.cs index 07385e8c5e3..2a1a4335f60 100644 --- a/Content.Shared/_Scp/Watching/EyeWatchingSystem.API.Watched.cs +++ b/Content.Shared/_Scp/Watching/EyeWatchingSystem.API.Watched.cs @@ -1,14 +1,23 @@ using System.Diagnostics.CodeAnalysis; -using System.Linq; using Content.Shared._Scp.Blinking; using Content.Shared._Scp.Helpers; using Content.Shared._Scp.Proximity; namespace Content.Shared._Scp.Watching; -// TODO: Унифицировать название переменных realWatchers/realViewers + potentialWatchers/potentialViewers public sealed partial class EyeWatchingSystem { + /// + /// Получает всех зрителей для конкретной сущности, которые подходят заданным условиям. + /// + /// Цель, для которой ищутся зрители + /// Количество зрителей + /// Требуемый тип линии видимости + /// Флаги для поиска зрителей + /// Будет ли проверять тип линии видимости + /// Будет ли проверять FOV зрителя + /// Если нужно использовать другой угол для FOV зрителя + /// Найден ли хоть один зритель public bool TryGetWatchers(EntityUid target, [NotNullWhen(true)] out int? watchers, LineOfSightBlockerLevel type = LineOfSightBlockerLevel.Transparent, @@ -20,13 +29,24 @@ public bool TryGetWatchers(EntityUid target, watchers = null; using var realWatchers = ListPoolEntity.Rent(); - if (!TryGetWatchers(target, realWatchers.Value, type, flags, useFov, checkProximity, fovOverride)) + if (!TryGetWatchers(target, realWatchers.Value, type, flags, checkProximity, useFov, fovOverride)) return false; watchers = realWatchers.Value.Count; return true; } + /// + /// Получает всех зрителей для конкретной сущности, которые подходят заданным условиям. + /// + /// Цель, для которой ищутся зрители + /// Список зрителей, который будет наполнен методом + /// Требуемый тип линии видимости + /// Флаги для поиска зрителей + /// Будет ли проверять тип линии видимости + /// Будет ли проверять FOV зрителя + /// Если нужно использовать другой угол для FOV зрителя + /// Найден ли хоть один зритель public bool TryGetWatchers(EntityUid target, List> realWatchers, LineOfSightBlockerLevel type = LineOfSightBlockerLevel.Transparent, @@ -47,6 +67,17 @@ public bool TryGetWatchers(EntityUid target, fovOverride); } + /// + /// Получает всех зрителей для конкретной сущности из заранее заготовленного списка потенциальных зрителей, которые подходят заданным условиям. + /// + /// Цель, для которой ищутся зрители + /// Список зрителей, который будет наполнен методом + /// Заранее заготовленный список потенциальных зрителей, среди которых будет поиск + /// Требуемый тип линии видимости + /// Будет ли проверять тип линии видимости + /// Будет ли проверять FOV зрителя + /// Если нужно использовать другой угол для FOV зрителя + /// Найден ли хоть один зритель public bool TryGetWatchersFrom(EntityUid target, List> realWatchers, ICollection> potentialWatchers, @@ -63,9 +94,20 @@ public bool TryGetWatchersFrom(EntityUid target, realWatchers.Add(viewer); } - return realWatchers.Any(); + return realWatchers.Count != 0; } + /// + /// Проверяет, есть ли хоть один зритель для целевой сущности. + /// Более оптимизированный вариант, который прерывает свое выполнение при найденном результате + /// + /// Цель для поиска зрителей + /// Требуемый тип линии видимости + /// Флаги для поиска зрителей в радиусе видимости + /// Будет ли проверяться тип линии видимости? + /// Будет ли проверять FOV зрителя? + /// Если нужно использовать другой угол FOV зрителя + /// Найден ли хоть один зритель для цели public bool IsWatchedByAny(EntityUid target, LineOfSightBlockerLevel type = LineOfSightBlockerLevel.Transparent, LookupFlags flags = LookupFlags.Uncontained | LookupFlags.Approximate, @@ -87,6 +129,16 @@ public bool IsWatchedByAny(EntityUid target, return false; } + /// + /// Проверяет, смотри ли потенциальный зритель на цель. + /// + /// Цель для проверки + /// Потенциальный зритель, который проверяется + /// Требуемый тип линии видимости + /// Будет ли проверяться тип линии видимости + /// Будет ли проверять FOV зрителя + /// Если нужно задать другой угол FOV зрителя + /// Смотрит ли потенциальный зритель на цель. public bool IsWatchedBy(EntityUid target, EntityUid potentialViewer, LineOfSightBlockerLevel type = LineOfSightBlockerLevel.Transparent, @@ -115,12 +167,12 @@ public bool IsWatchedBy(EntityUid target, /// Смотрящий, который в теории может увидеть цель /// Цель, которую мы проверяем на возможность быть увиденной смотрящим /// Да/нет - public bool CanBeWatched(Entity viewer, EntityUid target) + public bool CanBeWatched(EntityUid viewer, EntityUid target) { - if (!_blinkableQuery.Resolve(viewer.Owner, ref viewer.Comp, false)) + if (!_blinkableQuery.HasComp(viewer)) return false; - if (viewer.Owner == target) + if (viewer == target) return false; if (_insideStorageQuery.HasComp(viewer)) diff --git a/Content.Shared/_Scp/Watching/EyeWatchingSystem.API.Watching.cs b/Content.Shared/_Scp/Watching/EyeWatchingSystem.API.Watching.cs index b9f9c5080f4..31942c5d046 100644 --- a/Content.Shared/_Scp/Watching/EyeWatchingSystem.API.Watching.cs +++ b/Content.Shared/_Scp/Watching/EyeWatchingSystem.API.Watching.cs @@ -1,11 +1,22 @@ -using System.Linq; -using Content.Shared._Scp.Helpers; +using Content.Shared._Scp.Helpers; using Content.Shared._Scp.Proximity; namespace Content.Shared._Scp.Watching; public sealed partial class EyeWatchingSystem { + /// + /// Получает и возвращает на кого в данный момент смотрит зритель. + /// + /// Зритель, для которого идут проверки + /// Список целей, в который метод занесет всех, на кого смотрит зритель + /// Требуемый тип линии видимости + /// Флаги для поиска целей + /// Будет ли проверяться тип линии видимости + /// Будет ли проверяться FOV зрителя + /// Если нужно поставить другой угол FOV зрителя + /// Компонент, который должен быть у целей + /// Найдена ли хоть одна цель public bool TryGetWatchingTargets(EntityUid watcher, List> targets, LineOfSightBlockerLevel type = LineOfSightBlockerLevel.Transparent, @@ -27,6 +38,19 @@ public bool TryGetWatchingTargets(EntityUid watcher, fovOverride); } + /// + /// Получает и возвращает на кого в данный момент смотрит зритель. + /// Использует заранее заготовленный список целей для поиска реальных целей + /// + /// Зритель, для которого идут проверки + /// Список целей, в который метод занесет всех, на кого смотрит зритель + /// Требуемый тип линии видимости + /// Заранее заготовленный список целей из которых будет производиться поиск. + /// Будет ли проверяться тип линии видимости + /// Будет ли проверяться FOV зрителя + /// Если нужно поставить другой угол FOV зрителя + /// Компонент, который должен быть у целей + /// Найдена ли хоть одна цель public bool TryGetWatchingTargetsFrom(EntityUid watcher, List> targets, ICollection> potentialTargets, @@ -44,6 +68,6 @@ public bool TryGetWatchingTargetsFrom(EntityUid watcher, targets.Add(target); } - return targets.Any(); + return targets.Count != 0; } } diff --git a/Content.Shared/_Scp/Watching/EyeWatchingSystem.cs b/Content.Shared/_Scp/Watching/EyeWatchingSystem.cs index 7212c0a9c66..14ba83fe092 100644 --- a/Content.Shared/_Scp/Watching/EyeWatchingSystem.cs +++ b/Content.Shared/_Scp/Watching/EyeWatchingSystem.cs @@ -48,19 +48,19 @@ public override void Update(float frameTime) continue; // Все потенциально возможные смотрящие. Среди них те, что прошли фаст-чек из самых простых проверок - using var potentialViewers = ListPool.Rent(); - if (!_TryGetAllEntitiesVisibleTo(uid, potentialViewers.Value)) + using var potentialWatchers = ListPoolEntity.Rent(); + if (!TryGetAllEntitiesVisibleTo(uid, potentialWatchers.Value)) continue; // Вызываем ивенты на потенциально смотрящих. Без особых проверок // Полезно в коде, который уже использует подобные проверки или не требует этого - foreach (var potentialViewer in potentialViewers.Value) + foreach (var potentialViewer in potentialWatchers.Value) { var simpleViewerEvent = new SimpleEntityLookedAtEvent((uid, watchingComponent)); - var simpleTargetEvent = new SimpleEntitySeenEvent(potentialViewer.Viewer); + var simpleTargetEvent = new SimpleEntitySeenEvent(potentialViewer); // За подробностями какой ивент для чего навести мышку на название ивента - RaiseLocalEvent(potentialViewer.Viewer, ref simpleViewerEvent); + RaiseLocalEvent(potentialViewer, ref simpleViewerEvent); RaiseLocalEvent(uid, ref simpleTargetEvent); } @@ -76,14 +76,14 @@ public override void Update(float frameTime) // Проверяет всех потенциальных смотрящих на то, действительно ли они видят цель. // Каждый потенциально смотрящий проходит полный комплекс проверок. // Выдает полный список всех сущностей, кто действительно видит цель - using var realViewers = ListPool.Rent(); - if (!_TryGetWatchersFrom(uid, realViewers.Value, potentialViewers.Value, checkProximity: false)) + using var realWatchers = ListPoolEntity.Rent(); + if (!TryGetWatchersFrom(uid, realWatchers.Value, potentialWatchers.Value, checkProximity: false)) continue; // Вызываем ивент на смотрящем, говорящие, что он действительно видит цель - foreach (var viewer in realViewers.Value) + foreach (var viewer in realWatchers.Value) { - var netViewer = GetNetEntity(viewer.Viewer); + var netViewer = GetNetEntity(viewer); var firstTime = !watchingComponent.AlreadyLookedAt.ContainsKey(netViewer); // Небольшая заглушка для удобства работы с ивентами. @@ -91,10 +91,10 @@ public override void Update(float frameTime) watchingComponent.AlreadyLookedAt[netViewer] = TimeSpan.Zero; // За подробностями какой ивент для чего навести мышку на название ивента - var viewerEvent = new EntityLookedAtEvent((uid, watchingComponent), firstTime, viewer.BlockerLevel); - var targetEvent = new EntitySeenEvent(viewer.Viewer, firstTime, viewer.BlockerLevel); + var viewerEvent = new EntityLookedAtEvent((uid, watchingComponent), firstTime); + var targetEvent = new EntitySeenEvent(viewer, firstTime); - RaiseLocalEvent(viewer.Viewer, ref viewerEvent); + RaiseLocalEvent(viewer, ref viewerEvent); RaiseLocalEvent(uid, ref targetEvent); // Добавляет смотрящего в список уже смотревших, чтобы позволить системам манипулировать этим @@ -107,6 +107,9 @@ public override void Update(float frameTime) } } + /// + /// Устанавливает время следующей проверки видимости + /// private void SetNextTime(WatchingTargetComponent component) { component.NextTimeWatchedCheck = _timing.CurTime + component.WatchingCheckInterval; @@ -118,18 +121,16 @@ private void SetNextTime(WatchingTargetComponent component) /// /// Цель, на которую посмотрели /// Видим ли мы цель в первый раз -/// Линия видимости между смотрящим и целью, подробнее [ByRefEvent] -public readonly record struct EntityLookedAtEvent(Entity Target, bool FirstTime, LineOfSightBlockerLevel BlockerLevel); +public readonly record struct EntityLookedAtEvent(Entity Target, bool FirstTime); /// /// Ивент вызываемый на цели, передающий информации, что на нее кто-то посмотрел /// /// Смотрящий, который увидел цель /// Видим ли мы цель в первый раз -/// Линия видимости между смотрящим и целью, подробнее [ByRefEvent] -public readonly record struct EntitySeenEvent(EntityUid Viewer, bool FirstTime, LineOfSightBlockerLevel BlockerLevel); +public readonly record struct EntitySeenEvent(EntityUid Viewer, bool FirstTime); /// /// Простой ивент, говорящий, что смотрящий посмотрел на цель. diff --git a/Content.Shared/_Scp/Watching/WatchingTargetComponent.cs b/Content.Shared/_Scp/Watching/WatchingTargetComponent.cs index 4d65ddb34a1..da92a6db574 100644 --- a/Content.Shared/_Scp/Watching/WatchingTargetComponent.cs +++ b/Content.Shared/_Scp/Watching/WatchingTargetComponent.cs @@ -20,7 +20,7 @@ public sealed partial class WatchingTargetComponent : Component /// Время между проверками зрения /// [DataField] - public TimeSpan WatchingCheckInterval = TimeSpan.FromSeconds(0.1f); + public TimeSpan WatchingCheckInterval = TimeSpan.FromSeconds(0.2f); /// /// Время следующей проверки зрения From f8474858d484ddec7b83a4f42a22478f0ab187e4 Mon Sep 17 00:00:00 2001 From: drdth Date: Fri, 20 Mar 2026 21:04:35 +0300 Subject: [PATCH 09/17] refactor: big fear refactor and optimizations --- Content.Client/_Scp/Fear/FearSystem.cs | 3 +- .../Common/Grain/GrainOverlaySystem.cs | 4 +- .../Common/Vignette/VignetteOverlaySystem.cs | 4 +- .../_Scp/Fear/FearSystem.SoundEffects.cs | 4 +- .../Components/ActiveCloseFearComponent.cs | 15 ++ .../FearActiveSoundEffectsComponent.cs | 1 - .../Systems/SharedFearSystem.CloseFear.cs | 144 +++++++++++++++++ .../Systems/SharedFearSystem.SoundEffects.cs | 34 +++- .../_Scp/Fear/Systems/SharedFearSystem.cs | 131 ++++----------- .../ActiveProximityTargetComponent.cs | 22 +++ .../Proximity/ProximityReceiverComponent.cs | 27 ++++ .../_Scp/Proximity/ProximitySystem.cs | 151 ++++++++++++++---- .../_Scp/Scp173/SharedScp173System.cs | 6 +- .../Shaders/Grain/GrainOverlayComponent.cs | 2 +- .../Shaders/SharedShaderStrengthSystem.cs | 16 +- .../Vignette/VignetteOverlayComponent.cs | 2 +- .../Watching/EyeWatchingSystem.API.Base.cs | 14 +- .../Watching/EyeWatchingSystem.API.Watched.cs | 30 +++- .../EyeWatchingSystem.API.Watching.cs | 19 ++- 19 files changed, 454 insertions(+), 175 deletions(-) create mode 100644 Content.Shared/_Scp/Fear/Components/ActiveCloseFearComponent.cs create mode 100644 Content.Shared/_Scp/Fear/Systems/SharedFearSystem.CloseFear.cs create mode 100644 Content.Shared/_Scp/Proximity/ActiveProximityTargetComponent.cs diff --git a/Content.Client/_Scp/Fear/FearSystem.cs b/Content.Client/_Scp/Fear/FearSystem.cs index f043a213250..47f413b31a3 100644 --- a/Content.Client/_Scp/Fear/FearSystem.cs +++ b/Content.Client/_Scp/Fear/FearSystem.cs @@ -1,5 +1,4 @@ -using Content.Shared._Scp.Fear; -using Content.Shared._Scp.Fear.Components; +using Content.Shared._Scp.Fear.Components; using Content.Shared._Scp.Fear.Systems; using Content.Shared._Sunrise.Heartbeat; using Robust.Client.Audio; diff --git a/Content.Client/_Scp/Shaders/Common/Grain/GrainOverlaySystem.cs b/Content.Client/_Scp/Shaders/Common/Grain/GrainOverlaySystem.cs index 9e6fb4b11eb..a2f8dd79780 100644 --- a/Content.Client/_Scp/Shaders/Common/Grain/GrainOverlaySystem.cs +++ b/Content.Client/_Scp/Shaders/Common/Grain/GrainOverlaySystem.cs @@ -20,7 +20,7 @@ public override void Initialize() Overlay = new GrainOverlay(); - SubscribeLocalEvent(OnAdditionalStrengthChanged); + SubscribeLocalEvent(OnAdditionalStrengthChanged); _cfg.OnValueChanged(ScpCCVars.GrainToggleOverlay, ToggleGrainOverlay); _cfg.OnValueChanged(ScpCCVars.GrainStrength, SetBaseStrength); @@ -44,7 +44,7 @@ protected override void OnPlayerAttached(Entity ent, ref SetBaseStrength(_cfg.GetCVar(ScpCCVars.GrainStrength)); } - private void OnAdditionalStrengthChanged(Entity ent, ref ShaderAdditionalStrengthChanged args) + private void OnAdditionalStrengthChanged(Entity ent, ref AfterAutoHandleStateEvent args) { if (_player.LocalEntity != ent) return; diff --git a/Content.Client/_Scp/Shaders/Common/Vignette/VignetteOverlaySystem.cs b/Content.Client/_Scp/Shaders/Common/Vignette/VignetteOverlaySystem.cs index 3cabadc1d1b..51f78a9539c 100644 --- a/Content.Client/_Scp/Shaders/Common/Vignette/VignetteOverlaySystem.cs +++ b/Content.Client/_Scp/Shaders/Common/Vignette/VignetteOverlaySystem.cs @@ -17,11 +17,11 @@ public override void Initialize() DisableOnCompatibilityMode = false; Overlay = new VignetteOverlay(); - SubscribeLocalEvent(OnAdditionalStrengthChanged); + SubscribeLocalEvent(OnAdditionalStrengthChanged); } private void OnAdditionalStrengthChanged(Entity ent, - ref ShaderAdditionalStrengthChanged args) + ref AfterAutoHandleStateEvent args) { if (_player.LocalEntity != ent) return; diff --git a/Content.Server/_Scp/Fear/FearSystem.SoundEffects.cs b/Content.Server/_Scp/Fear/FearSystem.SoundEffects.cs index 2264e26207a..ac16cb139cb 100644 --- a/Content.Server/_Scp/Fear/FearSystem.SoundEffects.cs +++ b/Content.Server/_Scp/Fear/FearSystem.SoundEffects.cs @@ -77,10 +77,12 @@ protected override void StartBreathing(Entity e var audio = _audio.PlayGlobal(BreathingSound, ent, audioParams); ent.Comp.BreathingAudioStream = audio?.Entity; + Dirty(ent); } - private void StopBreathing(Entity ent) + protected override void StopBreathing(Entity ent) { ent.Comp.BreathingAudioStream = _audio.Stop(ent.Comp.BreathingAudioStream); + Dirty(ent); } } diff --git a/Content.Shared/_Scp/Fear/Components/ActiveCloseFearComponent.cs b/Content.Shared/_Scp/Fear/Components/ActiveCloseFearComponent.cs new file mode 100644 index 00000000000..3b47b0502d2 --- /dev/null +++ b/Content.Shared/_Scp/Fear/Components/ActiveCloseFearComponent.cs @@ -0,0 +1,15 @@ +namespace Content.Shared._Scp.Fear.Components; + +/// +/// Runtime-компонент активного "страха от близости". +/// Существует только пока у сущности реально активны close-fear эффекты. +/// +[RegisterComponent] +public sealed partial class ActiveCloseFearComponent : Component +{ + /// + /// Источник страха, который сейчас применяется к сущности. + /// + [ViewVariables] + public EntityUid Source; +} diff --git a/Content.Shared/_Scp/Fear/Components/FearActiveSoundEffectsComponent.cs b/Content.Shared/_Scp/Fear/Components/FearActiveSoundEffectsComponent.cs index f2cdf368bb9..60aadccbfe0 100644 --- a/Content.Shared/_Scp/Fear/Components/FearActiveSoundEffectsComponent.cs +++ b/Content.Shared/_Scp/Fear/Components/FearActiveSoundEffectsComponent.cs @@ -61,5 +61,4 @@ public sealed partial class FearActiveSoundEffectsComponent : Component public EntityUid? BreathingAudioStream; #endregion - } diff --git a/Content.Shared/_Scp/Fear/Systems/SharedFearSystem.CloseFear.cs b/Content.Shared/_Scp/Fear/Systems/SharedFearSystem.CloseFear.cs new file mode 100644 index 00000000000..71329b86ee5 --- /dev/null +++ b/Content.Shared/_Scp/Fear/Systems/SharedFearSystem.CloseFear.cs @@ -0,0 +1,144 @@ +using Content.Shared._Scp.Fear.Components; +using Content.Shared._Scp.Proximity; +using Content.Shared._Scp.Shaders.Grain; +using Content.Shared._Scp.Shaders.Vignette; + +namespace Content.Shared._Scp.Fear.Systems; + +public abstract partial class SharedFearSystem +{ + private EntityQuery _activeCloseFearQuery; + + private void InitializeCloseFear() + { + SubscribeLocalEvent(OnActiveProximityShutdown); + + _activeCloseFearQuery = GetEntityQuery(); + } + + public override void Update(float frameTime) + { + base.Update(frameTime); + + UpdateCloseFear(); + } + + private void UpdateCloseFear() + { + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var proximity, out var fear)) + { + SyncCloseFear((uid, fear), (uid, proximity)); + } + } + + private void SyncCloseFear(Entity ent, Entity proximity) + { + if (!_mobState.IsAlive(ent)) + { + ClearCloseFear(ent); + return; + } + + if (!_fears.TryComp(proximity.Comp.Receiver, out var source)) + { + ClearCloseFear(ent); + return; + } + + if (source.UponComeCloser == FearState.None) + { + ClearCloseFear(ent); + return; + } + + if (source.PhobiaType.HasValue && !ent.Comp.Phobias.Contains(source.PhobiaType.Value)) + { + ClearCloseFear(ent); + return; + } + + var blockerLevel = _proximity.GetLightOfSightBlockerLevel(proximity.Comp.Receiver, ent); + if (blockerLevel > ent.Comp.ProximityBlockerLevel) + { + ClearCloseFear(ent); + return; + } + + if (!_watching.IsWatchedBy(proximity.Comp.Receiver, ent, checkProximity: false, useFov: false, checkBlinking: false)) + { + ClearCloseFear(ent); + return; + } + + var sourceCoords = Transform(proximity.Comp.Receiver).Coordinates; + var targetCoords = Transform(ent).Coordinates; + if (!sourceCoords.TryDistance(EntityManager, targetCoords, out var range)) + { + ClearCloseFear(ent); + return; + } + + if (range > proximity.Comp.CloseRange) + { + ClearCloseFear(ent); + return; + } + + var hadActive = _activeCloseFearQuery.TryComp(ent, out var activeCloseFear); + activeCloseFear ??= EnsureComp(ent); + var sourceChanged = hadActive && activeCloseFear.Source != proximity.Comp.Receiver; + + if (!hadActive) + { + AddNegativeMoodEffect(ent, MoodSourceClose); + } + else if (sourceChanged) + { + RemoveSoundEffects(ent.Owner); + } + + activeCloseFear.Source = proximity.Comp.Receiver; + + StartEffects(ent, source.PlayHeartbeatSound, source.PlayBreathingSound); + + if (ent.Comp.State < source.UponComeCloser) + TrySetFearLevel(ent.AsNullable(), source.UponComeCloser); + + RecalculateEffectsStrength(ent.Owner, range, proximity.Comp.CloseRange); + + SetRangeBasedShaderStrength( + ent.Owner, + range, + proximity.Comp.CloseRange, + source.GrainShaderStrength, + blockerLevel, + ent.Comp); + + SetRangeBasedShaderStrength( + ent.Owner, + range, + proximity.Comp.CloseRange, + source.VignetteShaderStrength, + blockerLevel, + ent.Comp); + } + + private void OnActiveProximityShutdown(Entity ent, ref ComponentShutdown args) + { + if (!TryComp(ent, out var fear)) + return; + + ClearCloseFear((ent.Owner, fear)); + } + + private void ClearCloseFear(Entity ent) + { + RemComp(ent); + + SetFearBasedShaderStrength(ent); + + RemoveSoundEffects(ent.Owner); + RemoveCloseFearMood(ent.Owner); + } +} diff --git a/Content.Shared/_Scp/Fear/Systems/SharedFearSystem.SoundEffects.cs b/Content.Shared/_Scp/Fear/Systems/SharedFearSystem.SoundEffects.cs index 6bb94e495fb..936a6914322 100644 --- a/Content.Shared/_Scp/Fear/Systems/SharedFearSystem.SoundEffects.cs +++ b/Content.Shared/_Scp/Fear/Systems/SharedFearSystem.SoundEffects.cs @@ -15,6 +15,7 @@ public abstract partial class SharedFearSystem public const float MaximumAdditionalVolume = 16f; protected virtual void StartBreathing(Entity ent) {} + protected virtual void StopBreathing(Entity ent) {} protected virtual void StartHeartBeat(Entity ent) {} @@ -32,17 +33,26 @@ protected virtual void PlayFearStateSound(Entity ent, FearState o /// Проигрывать звук дыхания? private void StartEffects(EntityUid uid, bool playHeartbeatSound, bool playBreathingSound) { - if (HasComp(uid)) - return; + var existed = TryComp(uid, out var effects); + effects ??= EnsureComp(uid); + + var heartbeatChanged = effects.PlayHeartbeatSound != playHeartbeatSound; + var breathingChanged = effects.PlayBreathingSound != playBreathingSound; - var effects = EnsureComp(uid); effects.PlayHeartbeatSound = playHeartbeatSound; effects.PlayBreathingSound = playBreathingSound; - Dirty(uid, effects); + if (!existed || heartbeatChanged || breathingChanged) + Dirty(uid, effects); + + if (!existed || (heartbeatChanged && playHeartbeatSound)) + StartHeartBeat((uid, effects)); - StartBreathing((uid, effects)); - StartHeartBeat((uid, effects)); + if (!existed || (breathingChanged && playBreathingSound)) + StartBreathing((uid, effects)); + + if (existed && breathingChanged && !playBreathingSound) + StopBreathing((uid, effects)); } /// @@ -57,10 +67,18 @@ private void RecalculateEffectsStrength(Entity var cooldown = CalculateStrength(currentRange, maxRange, HeartBeatMinimumCooldown, HeartBeatMaximumCooldown); var currentPitch = CalculateStrength(currentRange, maxRange, HeartBeatMinimumPitch, HeartBeatMaximumPitch); + var currentCooldown = TimeSpan.FromSeconds(cooldown); + + if (MathF.Abs(ent.Comp.AdditionalVolume - volume) < 0.05f + && MathF.Abs(ent.Comp.Pitch - currentPitch) < 0.01f + && Math.Abs((ent.Comp.NextHeartbeatCooldown - currentCooldown).TotalMilliseconds) < 25) + { + return; + } ent.Comp.AdditionalVolume = volume; ent.Comp.Pitch = currentPitch; - ent.Comp.NextHeartbeatCooldown = TimeSpan.FromSeconds(cooldown); + ent.Comp.NextHeartbeatCooldown = currentCooldown; Dirty(ent); } @@ -68,7 +86,7 @@ private void RecalculateEffectsStrength(Entity /// /// Убирает все звуковые эффекты. /// - private void RemoveEffects(EntityUid uid) + private void RemoveSoundEffects(EntityUid uid) { RemComp(uid); } diff --git a/Content.Shared/_Scp/Fear/Systems/SharedFearSystem.cs b/Content.Shared/_Scp/Fear/Systems/SharedFearSystem.cs index 12d1e05792a..53627b36bf2 100644 --- a/Content.Shared/_Scp/Fear/Systems/SharedFearSystem.cs +++ b/Content.Shared/_Scp/Fear/Systems/SharedFearSystem.cs @@ -1,5 +1,4 @@ -using Content.Shared._Scp.Blinking; -using Content.Shared._Scp.Fear.Components; +using Content.Shared._Scp.Fear.Components; using Content.Shared._Scp.Helpers; using Content.Shared._Scp.Proximity; using Content.Shared._Scp.Shaders; @@ -28,7 +27,6 @@ namespace Content.Shared._Scp.Fear.Systems; public abstract partial class SharedFearSystem : EntitySystem { [Dependency] private readonly SharedHighlightSystem _highlight = default!; - [Dependency] private readonly SharedBlinkingSystem _blinking = default!; [Dependency] private readonly EyeWatchingSystem _watching = default!; [Dependency] private readonly MobStateSystem _mobState = default!; [Dependency] private readonly SharedShaderStrengthSystem _shaderStrength = default!; @@ -45,8 +43,6 @@ public override void Initialize() base.Initialize(); SubscribeLocalEvent(OnEntityLookedAt); - SubscribeLocalEvent(OnProximityInRange); - SubscribeLocalEvent(OnProximityNotInRange); SubscribeLocalEvent(OnFearStateChanged); @@ -60,6 +56,7 @@ public override void Initialize() InitializeFears(); InitializeGameplay(); InitializeTraits(); + InitializeCloseFear(); _fears = GetEntityQuery(); } @@ -70,9 +67,6 @@ public override void Initialize() /// private void OnEntityLookedAt(Entity ent, ref EntityLookedAtEvent args) { - if (!_timing.IsFirstTimePredicted) - return; - if (!_mobState.IsAlive(ent)) return; @@ -113,91 +107,6 @@ private void OnEntityLookedAt(Entity ent, ref EntityLookedAtEvent HighLightAllVisibleFears(ent); } - /// - /// Обрабатывает событие, когда игрок находится вблизи с источником страха. - /// Нужен, чтобы включить разные страшные эффекты. - /// - private void OnProximityInRange(Entity ent, ref ProximityInRangeTargetEvent args) - { - if (!_timing.IsFirstTimePredicted) - return; - - if (!_mobState.IsAlive(ent)) - return; - - // Проверка на видимость. - // Это нужно, чтобы можно было не пугаться через стекло, например. - // Это будет использовано, например, у ученых, которые 100 лет видели сцп через стекла и не должны пугаться. - if (args.Type > ent.Comp.ProximityBlockerLevel) - return; - - if (!_fears.TryComp(args.Receiver, out var source)) - return; - - if (source.UponComeCloser == FearState.None) - return; - - // Если в списке фобий персонажа нет фобии, которой является источник, то мы ее не боимся - if (source.PhobiaType.HasValue && !ent.Comp.Phobias.Contains(source.PhobiaType.Value)) - return; - - // Проверка на зрение, чтобы можно было закрыть глазки и было не страшно - if (_blinking.AreEyesClosedManually(ent.Owner)) - return; - - if (!_watching.IsWatchedBy(args.Receiver, ent, checkProximity: false)) - return; - - // Если текущий уровень страха выше, чем тот, что мы хотим поставить, - // то мы не должны его ставить. - if (ent.Comp.State < source.UponComeCloser) - TrySetFearLevel(ent.AsNullable(), source.UponComeCloser); - - StartEffects(ent, source.PlayHeartbeatSound, source.PlayBreathingSound); - RecalculateEffectsStrength(ent.Owner, args.Range, args.CloseRange); - - SetRangeBasedShaderStrength(ent.Owner, - args.Range, - args.CloseRange, - source.GrainShaderStrength, - args.Type, - ent.Comp); - - SetRangeBasedShaderStrength(ent.Owner, - args.Range, - args.CloseRange, - source.VignetteShaderStrength, - args.Type, - ent.Comp); - - AddNegativeMoodEffect(ent, MoodSourceClose); - } - - /// - /// Обрабатывает событие, когда сущность НЕ находится рядом с источником страха. - /// Нужен, чтобы выключить эффекты от источника страха. - /// - private void OnProximityNotInRange(Entity ent, ref ProximityNotInRangeTargetEvent args) - { - // Оказывается этот метод фиксит серверные проблемы, - // из-за которых изменение уровня страха на сервере не меняет параметры оверлеев на клиенте. - // Благодаря тому, что оно вызывается постоянно, это создает костыль, который закрывает проблему. Вот оно как. - // TODO: Избавиться от этого костыля когда-нибудь и реализовать Net версию изменения уровня страха - if (!_timing.IsFirstTimePredicted) - return; - - if (!_mobState.IsAlive(ent)) - return; - - // Как только игрок отходит от источника страха он должен перестать бояться - // Но значения шейдера от уровня страха должны продолжать действовать, что и учитывает метод - SetShaderStrength(ent.Owner, ent.Comp, 0f); - SetShaderStrength(ent.Owner, ent.Comp, 0f); - - RemoveEffects(ent); - RaiseLocalEvent(ent, new MoodRemoveEffectEvent(MoodSourceClose)); - } - /// /// Обрабатывает событие изменения уровня страха у персонажа. /// @@ -224,20 +133,12 @@ private void OnFearStateChanged(Entity ent, ref FearStateChangedE protected virtual void OnShutdown(Entity ent, ref ComponentShutdown args) { - _shaderStrength.TrySetAdditionalStrength(ent.Owner, 0f); - _shaderStrength.TrySetAdditionalStrength(ent.Owner, 0f); - - RemoveEffects(ent.Owner); - - RaiseLocalEvent(ent, new MoodRemoveEffectEvent(MoodFearSourceSeen)); - RaiseLocalEvent(ent, new MoodRemoveEffectEvent(MoodSourceClose)); + CleanupFear(ent); } protected virtual void OnRejuvenate(Entity ent, ref RejuvenateEvent args) { - TrySetFearLevel(ent.AsNullable(), FearState.None); - - RaiseLocalEvent(ent, new MoodRemoveEffectEvent(MoodFearSourceSeen)); + ResetFear(ent); } /// @@ -303,6 +204,30 @@ private void SetFearBasedShaderStrength(Entity ent) Dirty(ent); } + private void ResetFear(Entity ent) + { + TrySetFearLevel(ent.AsNullable(), FearState.None); + CleanupFear(ent); + } + + private void CleanupFear(Entity ent) + { + ClearCloseFear(ent); + + RemoveSeenFearMood(ent.Owner); + WipeMood(ent.Owner); + } + + private void RemoveSeenFearMood(EntityUid uid) + { + RaiseLocalEvent(uid, new MoodRemoveEffectEvent(MoodFearSourceSeen)); + } + + private void RemoveCloseFearMood(EntityUid uid) + { + RaiseLocalEvent(uid, new MoodRemoveEffectEvent(MoodSourceClose)); + } + /// /// Устанавливает для шейдера параметры силы. /// Сила зависит от расстояния до источника страха и параметров самого источника. diff --git a/Content.Shared/_Scp/Proximity/ActiveProximityTargetComponent.cs b/Content.Shared/_Scp/Proximity/ActiveProximityTargetComponent.cs new file mode 100644 index 00000000000..10c8938d5fd --- /dev/null +++ b/Content.Shared/_Scp/Proximity/ActiveProximityTargetComponent.cs @@ -0,0 +1,22 @@ +namespace Content.Shared._Scp.Proximity; + +/// +/// Runtime-компонент, который существует только пока цель находится рядом +/// хотя бы с одним . +/// Хранит выбранный dominant receiver для дальнейшей логики. +/// +[RegisterComponent] +public sealed partial class ActiveProximityTargetComponent : Component +{ + /// + /// Текущий dominant receiver, рядом с которым находится цель. + /// + [ViewVariables] + public EntityUid Receiver; + + /// + /// Эффективный радиус receiver, внутри которого находится цель. + /// + [ViewVariables] + public float CloseRange; +} diff --git a/Content.Shared/_Scp/Proximity/ProximityReceiverComponent.cs b/Content.Shared/_Scp/Proximity/ProximityReceiverComponent.cs index c573962c1f7..038fccf6de0 100644 --- a/Content.Shared/_Scp/Proximity/ProximityReceiverComponent.cs +++ b/Content.Shared/_Scp/Proximity/ProximityReceiverComponent.cs @@ -85,3 +85,30 @@ public readonly record struct ProximityInRangeTargetEvent(EntityUid Receiver, /// [ByRefEvent] public readonly record struct ProximityNotInRangeTargetEvent; + +/// +/// Ивент, вызываемый на цели, когда рядом впервые появляется dominant proximity receiver. +/// +[ByRefEvent] +public readonly record struct ProximityTargetEnteredEvent( + EntityUid Receiver, + float Range, + float CloseRange, + LineOfSightBlockerLevel Type); + +/// +/// Ивент, вызываемый на цели, когда она перестает находиться рядом с dominant proximity receiver. +/// +[ByRefEvent] +public readonly record struct ProximityTargetExitedEvent(EntityUid Receiver); + +/// +/// Ивент, вызываемый на цели, когда ее dominant proximity receiver сменился. +/// +[ByRefEvent] +public readonly record struct ProximityTargetReceiverChangedEvent( + EntityUid OldReceiver, + EntityUid NewReceiver, + float Range, + float CloseRange, + LineOfSightBlockerLevel Type); diff --git a/Content.Shared/_Scp/Proximity/ProximitySystem.cs b/Content.Shared/_Scp/Proximity/ProximitySystem.cs index f4771c3f98a..9a55f9d4dad 100644 --- a/Content.Shared/_Scp/Proximity/ProximitySystem.cs +++ b/Content.Shared/_Scp/Proximity/ProximitySystem.cs @@ -28,12 +28,12 @@ public sealed class ProximitySystem : EntitySystem [Dependency] private readonly IGameTiming _timing = default!; private static readonly TimeSpan ProximitySearchCooldown = TimeSpan.FromSeconds(0.05f); - private static TimeSpan _nextSearchTime = TimeSpan.Zero; + private TimeSpan _nextSearchTime = TimeSpan.Zero; // Оптимизации аллокации памяти - private static readonly HashSet> Targets = []; - private static readonly HashSet PossibleNotInRange = []; - private static readonly HashSet AllTargets = []; + private readonly HashSet> _targets = []; + private readonly Dictionary _currentMatches = []; + private readonly Dictionary _nextMatches = []; private const float JustUselessNumber = 30f; @@ -54,33 +54,33 @@ public sealed class ProximitySystem : EntitySystem "SecureUraniumWindoor", ]; + private EntityQuery _activeProximityQuery; private EntityQuery _insideQuery; + private EntityQuery _xformQuery; public override void Initialize() { base.Initialize(); - SubscribeLocalEvent(_ => Clean()); - - SubscribeLocalEvent(AddToTargets); - SubscribeLocalEvent(RemoveFromTargets); - SubscribeLocalEvent(RemoveFromTargets); + SubscribeLocalEvent(OnRoundRestartCleanup); + _activeProximityQuery = GetEntityQuery(); _insideQuery = GetEntityQuery(); + _xformQuery = GetEntityQuery(); } - private static void Clean() + private void OnRoundRestartCleanup(RoundRestartCleanupEvent args) { _nextSearchTime = TimeSpan.Zero; - AllTargets.Clear(); - } - - #region All targets population + _currentMatches.Clear(); + _nextMatches.Clear(); - private static void AddToTargets(Entity ent, ref T args) => AllTargets.Add(ent); - private static void RemoveFromTargets(Entity ent, ref T args) => AllTargets.Remove(ent); - - #endregion + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out _)) + { + RemCompDeferred(uid); + } + } public override void Update(float frameTime) { @@ -93,42 +93,117 @@ public override void Update(float frameTime) if (_timing.CurTime < _nextSearchTime) return; - PossibleNotInRange.Clear(); - PossibleNotInRange.UnionWith(AllTargets); + _nextMatches.Clear(); var query = EntityQueryEnumerator(); while (query.MoveNext(out var uid, out var receiver, out var xform)) { - Targets.Clear(); - _lookup.GetEntitiesInRange(xform.Coordinates, receiver.CloseRange, Targets, receiver.Flags); + _targets.Clear(); + _lookup.GetEntitiesInRange(xform.Coordinates, receiver.CloseRange, _targets, receiver.Flags); - foreach (var target in Targets) + foreach (var target in _targets) { - if (!IsRightType(uid, target, receiver.RequiredLineOfSight, out var lightOfSightBlockerLevel)) + if (!_xformQuery.TryComp(target, out var targetXform)) continue; - var targetCoords = Transform(target).Coordinates; + var targetCoords = targetXform.Coordinates; if (!xform.Coordinates.TryDistance(EntityManager, _transform, targetCoords, out var range)) continue; - var receiverEvent = new ProximityInRangeReceiverEvent(target, range, receiver.CloseRange, lightOfSightBlockerLevel); - RaiseLocalEvent(uid, ref receiverEvent); + if (range > receiver.CloseRange) + continue; + + if (!IsRightType(uid, target, receiver.RequiredLineOfSight, out var lightOfSightBlockerLevel)) + continue; - var targetEvent = new ProximityInRangeTargetEvent(uid, range, receiver.CloseRange, lightOfSightBlockerLevel); - RaiseLocalEvent(target, ref targetEvent); + var candidate = new ProximityMatch(uid, receiver.CloseRange, range, lightOfSightBlockerLevel); - PossibleNotInRange.Remove(target); + if (_nextMatches.TryGetValue(target, out var current) && !IsBetter(candidate, current)) + continue; + + _nextMatches[target] = candidate; } } - foreach (var target in PossibleNotInRange) + ApplyDelta(); + + _nextSearchTime = _timing.CurTime + ProximitySearchCooldown; + } + + private void ApplyDelta() + { + foreach (var (target, current) in _currentMatches) { - var notInRangeEvent = new ProximityNotInRangeTargetEvent(); - RaiseLocalEvent(target, ref notInRangeEvent); + if (_nextMatches.ContainsKey(target)) + continue; + + if (Deleted(target)) + continue; + + if (_activeProximityQuery.HasComp(target)) + RemCompDeferred(target); + + var exited = new ProximityTargetExitedEvent(current.Receiver); + RaiseLocalEvent(target, ref exited); } - _nextSearchTime = _timing.CurTime + ProximitySearchCooldown; + foreach (var (target, next) in _nextMatches) + { + if (Deleted(target)) + continue; + + var hadCurrent = _currentMatches.TryGetValue(target, out var current); + var proximity = EnsureComp(target); + + proximity.Receiver = next.Receiver; + proximity.CloseRange = next.CloseRange; + + if (!hadCurrent) + { + var entered = new ProximityTargetEnteredEvent(next.Receiver, next.Range, next.CloseRange, next.BlockerLevel); + RaiseLocalEvent(target, ref entered); + continue; + } + + if (current.Receiver != next.Receiver) + { + var changed = new ProximityTargetReceiverChangedEvent( + current.Receiver, + next.Receiver, + next.Range, + next.CloseRange, + next.BlockerLevel); + + RaiseLocalEvent(target, ref changed); + } + } + + _currentMatches.Clear(); + foreach (var (target, next) in _nextMatches) + { + _currentMatches[target] = next; + } + } + + private static bool IsBetter(ProximityMatch candidate, ProximityMatch current) + { + var candidateNormalized = candidate.Range / MathF.Max(candidate.CloseRange, float.Epsilon); + var currentNormalized = current.Range / MathF.Max(current.CloseRange, float.Epsilon); + + if (candidateNormalized < currentNormalized) + return true; + + if (candidateNormalized > currentNormalized) + return false; + + if (candidate.BlockerLevel < current.BlockerLevel) + return true; + + if (candidate.BlockerLevel > current.BlockerLevel) + return false; + + return candidate.Receiver.CompareTo(current.Receiver) < 0; } @@ -195,7 +270,7 @@ private bool InRangeUnobstructed(Entity first, EntityИмеется ли рядом такая сущность или нет public bool IsNearby(EntityUid uid, float range, LineOfSightBlockerLevel level = LineOfSightBlockerLevel.None) where T : IComponent { - return _lookup.GetEntitiesInRange(Transform(uid).Coordinates, range) + return _lookup.GetEntitiesInRange(Transform(uid).Coordinates, range, LookupFlags.Uncontained | LookupFlags.Approximate) .Any(e => IsRightType(uid, e, level, out _)); } @@ -214,4 +289,10 @@ public bool IsNearby(EntityUid uid, float range, HashSet> buffer, L return buffer.Any(e => IsRightType(uid, e, level, out _)); } + + private readonly record struct ProximityMatch( + EntityUid Receiver, + float CloseRange, + float Range, + LineOfSightBlockerLevel BlockerLevel); } diff --git a/Content.Shared/_Scp/Scp173/SharedScp173System.cs b/Content.Shared/_Scp/Scp173/SharedScp173System.cs index 1f0cd4f22aa..b208125f186 100644 --- a/Content.Shared/_Scp/Scp173/SharedScp173System.cs +++ b/Content.Shared/_Scp/Scp173/SharedScp173System.cs @@ -63,7 +63,7 @@ private void OnAttackAttempt(Entity ent, ref AttackAttemptEvent return; } - if (Watching.IsWatchedByAny(ent)) + if (Watching.IsWatchedByAny(ent, useTimeCompensation: true)) { args.Cancel(); return; @@ -76,7 +76,7 @@ private void OnDirectionAttempt(Entity ent, ref ChangeDirection if (IsInScpCage(ent, out _)) return; - if (Watching.IsWatchedByAny(ent)) + if (Watching.IsWatchedByAny(ent, useTimeCompensation: true)) return; args.Cancel(); @@ -88,7 +88,7 @@ private void OnMoveAttempt(Entity ent, ref UpdateCanMoveEvent a if (IsInScpCage(ent, out _)) return; - if (!Watching.IsWatchedByAny(ent)) + if (!Watching.IsWatchedByAny(ent, useTimeCompensation: true)) return; args.Cancel(); diff --git a/Content.Shared/_Scp/Shaders/Grain/GrainOverlayComponent.cs b/Content.Shared/_Scp/Shaders/Grain/GrainOverlayComponent.cs index 082cea2e7e7..4380f74c6e9 100644 --- a/Content.Shared/_Scp/Shaders/Grain/GrainOverlayComponent.cs +++ b/Content.Shared/_Scp/Shaders/Grain/GrainOverlayComponent.cs @@ -7,7 +7,7 @@ namespace Content.Shared._Scp.Shaders.Grain; /// Компонент, отвечающий за параметры шейдера зернистости. /// Наличие компонента необходимо для работы шейдера. /// -[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(true, true)] +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(true)] public sealed partial class GrainOverlayComponent : Component, IShaderStrength { /// diff --git a/Content.Shared/_Scp/Shaders/SharedShaderStrengthSystem.cs b/Content.Shared/_Scp/Shaders/SharedShaderStrengthSystem.cs index b01ba15f9ae..13832775298 100644 --- a/Content.Shared/_Scp/Shaders/SharedShaderStrengthSystem.cs +++ b/Content.Shared/_Scp/Shaders/SharedShaderStrengthSystem.cs @@ -13,7 +13,8 @@ public sealed class SharedShaderStrengthSystem : EntitySystem /// Сущность, к которой будет применено значение /// Значение, которое будет установлено /// Получилось/Не получилось - public bool TrySetBaseStrength(Entity ent, float value) where T : IComponent, IShaderStrength + public bool TrySetBaseStrength(Entity ent, float value) + where T : IComponent, IShaderStrength { // Базовая сила это чисто клиентский параметр // Поэтому ее задавать только на клиенте @@ -35,7 +36,8 @@ public bool TrySetBaseStrength(Entity ent, float value) where T : ICompon /// Значение, которое будет установлено /// Возвращаемый компонент с параметрами /// Получилось/Не получилось - public bool TrySetBaseStrength(Entity ent, float value, [NotNullWhen(true)] out T? component) where T : Component, IShaderStrength + public bool TrySetBaseStrength(Entity ent, float value, [NotNullWhen(true)] out T? component) + where T : Component, IShaderStrength { component = null; @@ -59,19 +61,15 @@ public bool TrySetBaseStrength(Entity ent, float value, [NotNullWhen(true /// Сущность, к которой будет применено значение /// Значение, которое будет установлено /// Получилось/Не получилось - public bool TrySetAdditionalStrength(Entity ent, float value) where T : IComponent, IShaderStrength + public bool TrySetAdditionalStrength(Entity ent, float value) + where T : IComponent, IShaderStrength { if (!Resolve(ent, ref ent.Comp)) return false; ent.Comp.AdditionalStrength = value; - - var ev = new ShaderAdditionalStrengthChanged(); - RaiseLocalEvent(ent, ref ev); + Dirty(ent); return true; } } - -[ByRefEvent] -public record struct ShaderAdditionalStrengthChanged(); diff --git a/Content.Shared/_Scp/Shaders/Vignette/VignetteOverlayComponent.cs b/Content.Shared/_Scp/Shaders/Vignette/VignetteOverlayComponent.cs index 830cd000aef..5e4bc739dd4 100644 --- a/Content.Shared/_Scp/Shaders/Vignette/VignetteOverlayComponent.cs +++ b/Content.Shared/_Scp/Shaders/Vignette/VignetteOverlayComponent.cs @@ -2,7 +2,7 @@ namespace Content.Shared._Scp.Shaders.Vignette; -[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(true, true)] +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(true)] public sealed partial class VignetteOverlayComponent : Component, IShaderStrength { /// diff --git a/Content.Shared/_Scp/Watching/EyeWatchingSystem.API.Base.cs b/Content.Shared/_Scp/Watching/EyeWatchingSystem.API.Base.cs index aea504a47ce..e2731ab1df4 100644 --- a/Content.Shared/_Scp/Watching/EyeWatchingSystem.API.Base.cs +++ b/Content.Shared/_Scp/Watching/EyeWatchingSystem.API.Base.cs @@ -218,9 +218,16 @@ private bool IsInProximity(EntityUid ent, EntityUid target, LineOfSightBlockerLe /// Смотрящий /// Цель, которую проверяем /// Применять ли проверку на поле зрения? + /// Будет ли использоваться компенсация времени? Нужно для передвижения SCP-173 + /// Будет ли проводиться проверка на моргание? /// Если нужно использовать другой угол поля зрения /// Видит ли смотрящий цель - public bool CanSee(Entity viewer, EntityUid target, bool useFov = true, float? fovOverride = null) + public bool CanSee(Entity viewer, + EntityUid target, + bool useFov = true, + bool useTimeCompensation = false, + bool checkBlinking = true, + float? fovOverride = null) { if (_mobState.IsIncapacitated(viewer)) return false; @@ -229,7 +236,10 @@ public bool CanSee(Entity viewer, EntityUid target, bool us if (useFov && !_fov.IsInFov(viewer.Owner, target, fovOverride)) return false; // Если не видит, то не считаем его как смотрящего - if (_blinking.IsBlind(viewer, true)) + if (checkBlinking && _blinking.IsBlind(viewer, useTimeCompensation)) + return false; + + if (!checkBlinking && _blinking.AreEyesClosedManually(viewer)) return false; var canSeeAttempt = new CanSeeAttemptEvent(); diff --git a/Content.Shared/_Scp/Watching/EyeWatchingSystem.API.Watched.cs b/Content.Shared/_Scp/Watching/EyeWatchingSystem.API.Watched.cs index 2a1a4335f60..3abe349b67b 100644 --- a/Content.Shared/_Scp/Watching/EyeWatchingSystem.API.Watched.cs +++ b/Content.Shared/_Scp/Watching/EyeWatchingSystem.API.Watched.cs @@ -16,6 +16,8 @@ public sealed partial class EyeWatchingSystem /// Флаги для поиска зрителей /// Будет ли проверять тип линии видимости /// Будет ли проверять FOV зрителя + /// Будет ли использоваться компенсация времени? Нужно для передвижения SCP-173 + /// Будет ли проводиться проверка на моргание? /// Если нужно использовать другой угол для FOV зрителя /// Найден ли хоть один зритель public bool TryGetWatchers(EntityUid target, @@ -24,12 +26,14 @@ public bool TryGetWatchers(EntityUid target, LookupFlags flags = LookupFlags.Uncontained | LookupFlags.Approximate, bool checkProximity = true, bool useFov = true, + bool useTimeCompensation = false, + bool checkBlinking = true, float? fovOverride = null) { watchers = null; using var realWatchers = ListPoolEntity.Rent(); - if (!TryGetWatchers(target, realWatchers.Value, type, flags, checkProximity, useFov, fovOverride)) + if (!TryGetWatchers(target, realWatchers.Value, type, flags, checkProximity, useFov, useTimeCompensation, checkBlinking, fovOverride)) return false; watchers = realWatchers.Value.Count; @@ -45,6 +49,8 @@ public bool TryGetWatchers(EntityUid target, /// Флаги для поиска зрителей /// Будет ли проверять тип линии видимости /// Будет ли проверять FOV зрителя + /// Будет ли использоваться компенсация времени? Нужно для передвижения SCP-173 + /// Будет ли проводиться проверка на моргание? /// Если нужно использовать другой угол для FOV зрителя /// Найден ли хоть один зритель public bool TryGetWatchers(EntityUid target, @@ -53,6 +59,8 @@ public bool TryGetWatchers(EntityUid target, LookupFlags flags = LookupFlags.Uncontained | LookupFlags.Approximate, bool checkProximity = true, bool useFov = true, + bool useTimeCompensation = false, + bool checkBlinking = true, float? fovOverride = null) { using var potentialWatchers = HashSetPoolEntity.Rent(); @@ -64,6 +72,8 @@ public bool TryGetWatchers(EntityUid target, type, checkProximity, useFov, + useTimeCompensation, + checkBlinking, fovOverride); } @@ -76,6 +86,8 @@ public bool TryGetWatchers(EntityUid target, /// Требуемый тип линии видимости /// Будет ли проверять тип линии видимости /// Будет ли проверять FOV зрителя + /// Будет ли использоваться компенсация времени? Нужно для передвижения SCP-173 + /// Будет ли проводиться проверка на моргание? /// Если нужно использовать другой угол для FOV зрителя /// Найден ли хоть один зритель public bool TryGetWatchersFrom(EntityUid target, @@ -84,11 +96,13 @@ public bool TryGetWatchersFrom(EntityUid target, LineOfSightBlockerLevel type = LineOfSightBlockerLevel.Transparent, bool checkProximity = true, bool useFov = true, + bool useTimeCompensation = false, + bool checkBlinking = true, float? fovOverride = null) { foreach (var viewer in potentialWatchers) { - if (!IsWatchedBy(target, viewer, type, useFov, checkProximity, fovOverride)) + if (!IsWatchedBy(target, viewer, type, checkProximity, useFov, useTimeCompensation, checkBlinking, fovOverride)) continue; realWatchers.Add(viewer); @@ -106,6 +120,8 @@ public bool TryGetWatchersFrom(EntityUid target, /// Флаги для поиска зрителей в радиусе видимости /// Будет ли проверяться тип линии видимости? /// Будет ли проверять FOV зрителя? + /// Будет ли использоваться компенсация времени? Нужно для передвижения SCP-173 + /// Будет ли проводиться проверка на моргание? /// Если нужно использовать другой угол FOV зрителя /// Найден ли хоть один зритель для цели public bool IsWatchedByAny(EntityUid target, @@ -113,6 +129,8 @@ public bool IsWatchedByAny(EntityUid target, LookupFlags flags = LookupFlags.Uncontained | LookupFlags.Approximate, bool checkProximity = true, bool useFov = true, + bool useTimeCompensation = false, + bool checkBlinking = true, float? fovOverride = null) { using var potentialWatchers = HashSetPoolEntity.Rent(); @@ -120,7 +138,7 @@ public bool IsWatchedByAny(EntityUid target, foreach (var viewer in potentialWatchers.Value) { - if (!IsWatchedBy(target, viewer, type, useFov, checkProximity, fovOverride)) + if (!IsWatchedBy(target, viewer, type, useFov, checkProximity, useTimeCompensation, checkBlinking, fovOverride)) continue; return true; @@ -137,6 +155,8 @@ public bool IsWatchedByAny(EntityUid target, /// Требуемый тип линии видимости /// Будет ли проверяться тип линии видимости /// Будет ли проверять FOV зрителя + /// Будет ли использоваться компенсация времени? Нужно для передвижения SCP-173 + /// Будет ли проводиться проверка на моргание? /// Если нужно задать другой угол FOV зрителя /// Смотрит ли потенциальный зритель на цель. public bool IsWatchedBy(EntityUid target, @@ -144,6 +164,8 @@ public bool IsWatchedBy(EntityUid target, LineOfSightBlockerLevel type = LineOfSightBlockerLevel.Transparent, bool checkProximity = true, bool useFov = true, + bool useTimeCompensation = false, + bool checkBlinking = true, float? fovOverride = null) { if (!CanBeWatched(potentialViewer, target)) @@ -152,7 +174,7 @@ public bool IsWatchedBy(EntityUid target, if (checkProximity && !IsInProximity(potentialViewer, target, type)) return false; - if (!CanSee(potentialViewer, target, useFov, fovOverride)) + if (!CanSee(potentialViewer, target, useFov, useTimeCompensation, checkBlinking, fovOverride)) return false; return true; diff --git a/Content.Shared/_Scp/Watching/EyeWatchingSystem.API.Watching.cs b/Content.Shared/_Scp/Watching/EyeWatchingSystem.API.Watching.cs index 31942c5d046..6e13bac8e8b 100644 --- a/Content.Shared/_Scp/Watching/EyeWatchingSystem.API.Watching.cs +++ b/Content.Shared/_Scp/Watching/EyeWatchingSystem.API.Watching.cs @@ -14,6 +14,8 @@ public sealed partial class EyeWatchingSystem /// Флаги для поиска целей /// Будет ли проверяться тип линии видимости /// Будет ли проверяться FOV зрителя + /// Будет ли использоваться компенсация времени? Нужно для передвижения SCP-173 + /// Будет ли проводиться проверка на моргание? /// Если нужно поставить другой угол FOV зрителя /// Компонент, который должен быть у целей /// Найдена ли хоть одна цель @@ -23,6 +25,8 @@ public bool TryGetWatchingTargets(EntityUid watcher, LookupFlags flags = LookupFlags.Uncontained | LookupFlags.Approximate, bool checkProximity = true, bool useFov = true, + bool useTimeCompensation = false, + bool checkBlinking = true, float? fovOverride = null) where T : IComponent { @@ -35,6 +39,8 @@ public bool TryGetWatchingTargets(EntityUid watcher, type, checkProximity, useFov, + useTimeCompensation, + checkBlinking, fovOverride); } @@ -48,6 +54,8 @@ public bool TryGetWatchingTargets(EntityUid watcher, /// Заранее заготовленный список целей из которых будет производиться поиск. /// Будет ли проверяться тип линии видимости /// Будет ли проверяться FOV зрителя + /// Будет ли использоваться компенсация времени? Нужно для передвижения SCP-173 + /// Будет ли проводиться проверка на моргание? /// Если нужно поставить другой угол FOV зрителя /// Компонент, который должен быть у целей /// Найдена ли хоть одна цель @@ -57,12 +65,21 @@ public bool TryGetWatchingTargetsFrom(EntityUid watcher, LineOfSightBlockerLevel type = LineOfSightBlockerLevel.Transparent, bool checkProximity = true, bool useFov = true, + bool useTimeCompensation = false, + bool checkBlinking = true, float? fovOverride = null) where T : IComponent { foreach (var target in potentialTargets) { - if (!IsWatchedBy(target, watcher, type, useFov, checkProximity, fovOverride)) + if (!IsWatchedBy(target, + watcher, + type, + checkProximity, + useFov, + useTimeCompensation, + checkBlinking, + fovOverride)) continue; targets.Add(target); From b566e011a3bfa44e38318deb7cbabbd25a26eb94 Mon Sep 17 00:00:00 2001 From: drdth Date: Fri, 20 Mar 2026 21:23:24 +0300 Subject: [PATCH 10/17] add: dirty fields for fear component --- .../_Scp/Fear/FearSystem.EntityEffects.cs | 1 - .../_Scp/Fear/FearSystem.SoundEffects.cs | 4 ++-- .../FearActiveSoundEffectsComponent.cs | 2 +- .../_Scp/Fear/Components/FearComponent.cs | 2 +- .../Fear/Components/FearSourceComponent.cs | 2 +- .../Components/Fears/HemophobiaComponent.cs | 2 +- .../Fear/Systems/SharedFearSystem.Fears.cs | 4 ++-- .../Fear/Systems/SharedFearSystem.Helpers.cs | 23 +------------------ .../Systems/SharedFearSystem.SoundEffects.cs | 15 ++++++++++-- .../_Scp/Fear/Systems/SharedFearSystem.cs | 7 ++---- 10 files changed, 24 insertions(+), 38 deletions(-) diff --git a/Content.Server/_Scp/Fear/FearSystem.EntityEffects.cs b/Content.Server/_Scp/Fear/FearSystem.EntityEffects.cs index b6437dbd826..e011994bc14 100644 --- a/Content.Server/_Scp/Fear/FearSystem.EntityEffects.cs +++ b/Content.Server/_Scp/Fear/FearSystem.EntityEffects.cs @@ -14,6 +14,5 @@ private void InitializeEntityEffects() private void OnExecuteCalmDown(Entity ent, ref EntityEffectEvent args) { ent.Comp.NextTimeDecreaseFearLevel -= args.Effect.SpeedUpBy; - Dirty(ent); } } diff --git a/Content.Server/_Scp/Fear/FearSystem.SoundEffects.cs b/Content.Server/_Scp/Fear/FearSystem.SoundEffects.cs index ac16cb139cb..82e0f24cb61 100644 --- a/Content.Server/_Scp/Fear/FearSystem.SoundEffects.cs +++ b/Content.Server/_Scp/Fear/FearSystem.SoundEffects.cs @@ -77,12 +77,12 @@ protected override void StartBreathing(Entity e var audio = _audio.PlayGlobal(BreathingSound, ent, audioParams); ent.Comp.BreathingAudioStream = audio?.Entity; - Dirty(ent); + DirtyField(ent, ent.Comp, nameof(FearActiveSoundEffectsComponent.BreathingAudioStream)); } protected override void StopBreathing(Entity ent) { ent.Comp.BreathingAudioStream = _audio.Stop(ent.Comp.BreathingAudioStream); - Dirty(ent); + DirtyField(ent, ent.Comp, nameof(FearActiveSoundEffectsComponent.BreathingAudioStream)); } } diff --git a/Content.Shared/_Scp/Fear/Components/FearActiveSoundEffectsComponent.cs b/Content.Shared/_Scp/Fear/Components/FearActiveSoundEffectsComponent.cs index 60aadccbfe0..a14c145a34e 100644 --- a/Content.Shared/_Scp/Fear/Components/FearActiveSoundEffectsComponent.cs +++ b/Content.Shared/_Scp/Fear/Components/FearActiveSoundEffectsComponent.cs @@ -6,7 +6,7 @@ namespace Content.Shared._Scp.Fear.Components; /// /// Компонент, отвечающий за звуковые эффекты страха при приближении к источнику. /// -[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(fieldDeltas: true)] public sealed partial class FearActiveSoundEffectsComponent : Component { /// diff --git a/Content.Shared/_Scp/Fear/Components/FearComponent.cs b/Content.Shared/_Scp/Fear/Components/FearComponent.cs index 345600baf20..29cb7121d73 100644 --- a/Content.Shared/_Scp/Fear/Components/FearComponent.cs +++ b/Content.Shared/_Scp/Fear/Components/FearComponent.cs @@ -8,7 +8,7 @@ namespace Content.Shared._Scp.Fear.Components; /// Компонент, отвечающий за возможность пугаться. /// Обрабатывает уровни страха и хранит текущий. /// -[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(fieldDeltas: true)] public sealed partial class FearComponent : Component { /// diff --git a/Content.Shared/_Scp/Fear/Components/FearSourceComponent.cs b/Content.Shared/_Scp/Fear/Components/FearSourceComponent.cs index dba3e333c57..192a75423d1 100644 --- a/Content.Shared/_Scp/Fear/Components/FearSourceComponent.cs +++ b/Content.Shared/_Scp/Fear/Components/FearSourceComponent.cs @@ -9,7 +9,7 @@ namespace Content.Shared._Scp.Fear.Components; /// Сущность с этим компонентом будет являться источников страха для игрока. /// Здесь настраивается, какой уровень страха будет вызван у игрока при разных обстоятельствах. /// -[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(fieldDeltas: true)] public sealed partial class FearSourceComponent : Component { [DataField, ViewVariables(VVAccess.ReadWrite), AutoNetworkedField] diff --git a/Content.Shared/_Scp/Fear/Components/Fears/HemophobiaComponent.cs b/Content.Shared/_Scp/Fear/Components/Fears/HemophobiaComponent.cs index d5ef761bc87..1b0b65f10b2 100644 --- a/Content.Shared/_Scp/Fear/Components/Fears/HemophobiaComponent.cs +++ b/Content.Shared/_Scp/Fear/Components/Fears/HemophobiaComponent.cs @@ -8,7 +8,7 @@ namespace Content.Shared._Scp.Fear.Components.Fears; /// /// Компонент, отвечающий за страх перед кровью /// -[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(fieldDeltas: true)] public sealed partial class HemophobiaComponent : Component { /// diff --git a/Content.Shared/_Scp/Fear/Systems/SharedFearSystem.Fears.cs b/Content.Shared/_Scp/Fear/Systems/SharedFearSystem.Fears.cs index 0de7aee9aae..5d56ac523b7 100644 --- a/Content.Shared/_Scp/Fear/Systems/SharedFearSystem.Fears.cs +++ b/Content.Shared/_Scp/Fear/Systems/SharedFearSystem.Fears.cs @@ -26,7 +26,7 @@ private void OnHemophobiaInit(Entity ent, ref ComponentStar return; fearComponent.Phobias.Add(ent.Comp.Phobia); - Dirty(ent, fearComponent); + DirtyField(ent, fearComponent, nameof(FearComponent.Phobias)); } /// @@ -42,6 +42,6 @@ private void OnHemophobiaShutdown(Entity ent, ref Component return; fearComponent.Phobias.Remove(ent.Comp.Phobia); - Dirty(ent, fearComponent); + DirtyField(ent, fearComponent, nameof(FearComponent.Phobias)); } } diff --git a/Content.Shared/_Scp/Fear/Systems/SharedFearSystem.Helpers.cs b/Content.Shared/_Scp/Fear/Systems/SharedFearSystem.Helpers.cs index b9b5e641c19..f8788180598 100644 --- a/Content.Shared/_Scp/Fear/Systems/SharedFearSystem.Helpers.cs +++ b/Content.Shared/_Scp/Fear/Systems/SharedFearSystem.Helpers.cs @@ -1,15 +1,11 @@ -using System.Threading; -using Content.Shared._Scp.Fear.Components; +using Content.Shared._Scp.Fear.Components; using Content.Shared._Scp.Helpers; using Content.Shared._Scp.Shaders; -using Timer = Robust.Shared.Timing.Timer; namespace Content.Shared._Scp.Fear.Systems; public abstract partial class SharedFearSystem { - private static CancellationTokenSource _restartToken = new (); - private const int MinPossibleValue = (int) FearState.None; private const int MaxPossibleValue = (int) FearState.Terror; @@ -125,17 +121,6 @@ public static int GetGenericFearBasedModifier(FearState state, int scale = Gener protected void SetNextCalmDownTime(Entity ent) { ent.Comp.NextTimeDecreaseFearLevel = _timing.CurTime + ent.Comp.TimeToDecreaseFearLevel; - Dirty(ent); - } - - protected void RemoveComponentAfter(EntityUid ent, float removeAfter) where T : IComponent - { - Timer.Spawn(TimeSpan.FromSeconds(removeAfter), () => RemComp(ent), _restartToken.Token); - } - - protected void RemoveComponentAfter(EntityUid ent, TimeSpan removeAfter) where T : IComponent - { - Timer.Spawn(removeAfter, () => RemComp(ent), _restartToken.Token); } /// @@ -174,10 +159,4 @@ protected static float PercentToNormalized(float percent) { return Math.Clamp(percent / 100f, 0f, 1f); } - - protected virtual void Clear() - { - _restartToken.Cancel(); - _restartToken = new(); - } } diff --git a/Content.Shared/_Scp/Fear/Systems/SharedFearSystem.SoundEffects.cs b/Content.Shared/_Scp/Fear/Systems/SharedFearSystem.SoundEffects.cs index 936a6914322..25ef79214b2 100644 --- a/Content.Shared/_Scp/Fear/Systems/SharedFearSystem.SoundEffects.cs +++ b/Content.Shared/_Scp/Fear/Systems/SharedFearSystem.SoundEffects.cs @@ -43,7 +43,13 @@ private void StartEffects(EntityUid uid, bool playHeartbeatSound, bool playBreat effects.PlayBreathingSound = playBreathingSound; if (!existed || heartbeatChanged || breathingChanged) - Dirty(uid, effects); + { + DirtyFields(uid, + effects, + null, + nameof(FearActiveSoundEffectsComponent.PlayHeartbeatSound), + nameof(FearActiveSoundEffectsComponent.PlayBreathingSound)); + } if (!existed || (heartbeatChanged && playHeartbeatSound)) StartHeartBeat((uid, effects)); @@ -80,7 +86,12 @@ private void RecalculateEffectsStrength(Entity ent.Comp.Pitch = currentPitch; ent.Comp.NextHeartbeatCooldown = currentCooldown; - Dirty(ent); + DirtyFields(ent, + ent.Comp, + null, + nameof(FearActiveSoundEffectsComponent.AdditionalVolume), + nameof(FearActiveSoundEffectsComponent.Pitch), + nameof(FearActiveSoundEffectsComponent.NextHeartbeatCooldown)); } /// diff --git a/Content.Shared/_Scp/Fear/Systems/SharedFearSystem.cs b/Content.Shared/_Scp/Fear/Systems/SharedFearSystem.cs index 53627b36bf2..7a9d30bd796 100644 --- a/Content.Shared/_Scp/Fear/Systems/SharedFearSystem.cs +++ b/Content.Shared/_Scp/Fear/Systems/SharedFearSystem.cs @@ -8,7 +8,6 @@ using Content.Shared._Scp.Watching; using Content.Shared._Sunrise.Mood; using Content.Shared.Examine; -using Content.Shared.GameTicking; using Content.Shared.Mobs.Systems; using Content.Shared.Rejuvenate; using Robust.Shared.Timing; @@ -51,8 +50,6 @@ public override void Initialize() SubscribeLocalEvent(OnExamine); - SubscribeLocalEvent(_ => Clear()); - InitializeFears(); InitializeGameplay(); InitializeTraits(); @@ -182,7 +179,7 @@ public bool TrySetFearLevel(Entity ent, FearState state) SetFearBasedShaderStrength(entity); SetNextCalmDownTime(entity); - Dirty(ent); + DirtyField(ent, ent.Comp, nameof(FearComponent.State)); return true; } @@ -201,7 +198,7 @@ private void SetFearBasedShaderStrength(Entity ent) ent.Comp.CurrentFearBasedShaderStrength[nameof(GrainOverlayComponent)] = grainStrength; ent.Comp.CurrentFearBasedShaderStrength[nameof(VignetteOverlayComponent)] = vignetteStrength; - Dirty(ent); + DirtyField(ent, ent.Comp, nameof(FearComponent.CurrentFearBasedShaderStrength)); } private void ResetFear(Entity ent) From 2f3c3205f8d78e7adaf530e975f7aefb5cfa42d6 Mon Sep 17 00:00:00 2001 From: drdth Date: Fri, 20 Mar 2026 22:28:20 +0300 Subject: [PATCH 11/17] add: remove constant MoveEvent events --- .../_Scp/Fear/FearSystem.Gameplay.cs | 36 ----------- Content.Server/_Scp/Fear/FearSystem.Traits.cs | 10 ++-- Content.Server/_Scp/Fear/FearSystem.cs | 23 +++----- .../Components/ActiveFearFallOffComponent.cs | 28 +++++++++ .../_Scp/Fear/Components/FearComponent.cs | 14 +---- .../Traits/FearFaintingComponent.cs | 11 ++-- .../Components/Traits/FearStuporComponent.cs | 11 ++-- .../Systems/SharedFearSystem.CloseFear.cs | 4 +- .../Fear/Systems/SharedFearSystem.Fears.cs | 11 ++-- .../Fear/Systems/SharedFearSystem.Gameplay.cs | 59 +++++++++++++++++-- .../Fear/Systems/SharedFearSystem.Helpers.cs | 8 --- .../Fear/Systems/SharedFearSystem.Traits.cs | 10 +++- .../_Scp/Fear/Systems/SharedFearSystem.cs | 16 ++--- .../Administration/security_commander.yml | 2 +- ...xternal_administrative_zone_commandant.yml | 2 +- .../external_administrative_zone_officer.yml | 2 +- ...r_external_administrative_zone_officer.yml | 2 +- ...r_external_administrative_zone_officer.yml | 2 +- .../heavy_containment_zone_commandant.yml | 2 +- .../heavy_containment_zone_officer.yml | 2 +- .../junior_heavy_containment_zone_officer.yml | 2 +- .../senior_heavy_containment_zone_officer.yml | 2 +- 22 files changed, 139 insertions(+), 120 deletions(-) create mode 100644 Content.Shared/_Scp/Fear/Components/ActiveFearFallOffComponent.cs diff --git a/Content.Server/_Scp/Fear/FearSystem.Gameplay.cs b/Content.Server/_Scp/Fear/FearSystem.Gameplay.cs index f6cbe9391cb..09d113fde29 100644 --- a/Content.Server/_Scp/Fear/FearSystem.Gameplay.cs +++ b/Content.Server/_Scp/Fear/FearSystem.Gameplay.cs @@ -11,38 +11,10 @@ namespace Content.Server._Scp.Fear; public sealed partial class FearSystem { [Dependency] private readonly ChatSystem _chat = default!; - [Dependency] private readonly StandingStateSystem _standing = default!; [Dependency] private readonly IRobustRandom _random = default!; private static readonly ProtoId ScreamProtoId = "Scream"; - private void InitializeGameplay() - { - // TODO: Перенести это на отдельный компонент, чтобы не перебирать всех потенциально пугающихся. - SubscribeLocalEvent(OnMove); - } - - /// - /// Обрабатывает событие хождения. - /// Реализует случайное падение во время сильного страха - /// - private void OnMove(Entity ent, ref MoveInputEvent args) - { - if (ent.Comp.State < ent.Comp.FallOffRequiredState) - return; - - if (_timing.CurTime < ent.Comp.FallOffNextCheckTime) - return; - - var percentNormalized = PercentToNormalized(ent.Comp.FallOffChance); - SetNextFallOffTime(ent); // Даже если не прокнет, то время все равно должно устанавливаться - - if (!_random.Prob(percentNormalized)) - return; - - _standing.Down(ent, force: true); - } - /// /// Пытается закричать, если увиденный объект настолько страшный. /// @@ -55,12 +27,4 @@ protected override void TryScream(Entity ent) _chat.TryEmoteWithChat(ent, ScreamProtoId); } - - /// - /// Устанавливает следующее время возможности запнуться. - /// - private void SetNextFallOffTime(Entity ent) - { - ent.Comp.FallOffNextCheckTime = _timing.CurTime + ent.Comp.FallOffCheckInterval; - } } diff --git a/Content.Server/_Scp/Fear/FearSystem.Traits.cs b/Content.Server/_Scp/Fear/FearSystem.Traits.cs index 8c53935263b..1d71f53622a 100644 --- a/Content.Server/_Scp/Fear/FearSystem.Traits.cs +++ b/Content.Server/_Scp/Fear/FearSystem.Traits.cs @@ -31,11 +31,10 @@ private void OnStuporFearStateChanged(Entity ent, ref FearS if (args.NewState < ent.Comp.RequiredState) return; - var normalizedChance = PercentToNormalized(ent.Comp.Chance); - if (!_random.Prob(normalizedChance)) + if (!_random.Prob(ent.Comp.Chance)) return; - _statusEffects.TryAddStatusEffectDuration(ent, FearStuporComponent.StatusEffect, ent.Comp.StuporTime); + _statusEffects.TryAddStatusEffectDuration(ent, ent.Comp.StatusEffect, ent.Comp.StuporTime); } private void OnStutteringFearStateChanged(Entity ent, ref FearStateChangedEvent args) @@ -63,10 +62,9 @@ private void OnFaintingFearStateChanged(Entity ent, ref F if (args.NewState < ent.Comp.RequiredState) return; - var percentNormalized = PercentToNormalized(ent.Comp.Chance); - if (!_random.Prob(percentNormalized)) + if (!_random.Prob(ent.Comp.Chance)) return; - _statusEffects.TryAddStatusEffectDuration(ent, FearFaintingComponent.StatusEffect, ent.Comp.Time); + _statusEffects.TryAddStatusEffectDuration(ent, ent.Comp.StatusEffect, ent.Comp.Time); } } diff --git a/Content.Server/_Scp/Fear/FearSystem.cs b/Content.Server/_Scp/Fear/FearSystem.cs index 2db1af27b41..327507f0ae2 100644 --- a/Content.Server/_Scp/Fear/FearSystem.cs +++ b/Content.Server/_Scp/Fear/FearSystem.cs @@ -2,6 +2,7 @@ using Content.Shared._Scp.Fear.Components; using Content.Shared._Scp.Fear.Systems; using Content.Shared._Sunrise.Mood; +using Content.Shared.GameTicking; using Content.Shared.Mobs.Components; using Content.Shared.Rejuvenate; using Robust.Shared.Timing; @@ -12,8 +13,6 @@ public sealed partial class FearSystem : SharedFearSystem { [Dependency] private readonly IGameTiming _timing = default!; - private EntityQuery _activeFearEffects; - private static readonly TimeSpan CalmDownCheckCooldown = TimeSpan.FromSeconds(1f); private TimeSpan _nextCalmDownCheck = TimeSpan.Zero; @@ -21,13 +20,12 @@ public override void Initialize() { base.Initialize(); + SubscribeLocalEvent(OnCleanUp); + InitializeSoundEffects(); InitializeFears(); - InitializeGameplay(); InitializeTraits(); InitializeEntityEffects(); - - _activeFearEffects = GetEntityQuery(); } public override void Update(float frameTime) @@ -48,13 +46,13 @@ private void UpdateCalmDown() // Проходимся по людям с компонентом страха и уменьшаем уровень страха со временем while (query.MoveNext(out var uid, out var fear, out var mob)) { - if (!_mob.IsAlive(uid, mob)) + if (fear.State == FearState.None) continue; - if (fear.State == FearState.None) + if (!_mob.IsAlive(uid, mob)) continue; - if (fear.NextTimeDecreaseFearLevel > _timing.CurTime) + if (_timing.CurTime < fear.NextTimeDecreaseFearLevel) continue; var entity = (uid, fear); @@ -74,11 +72,6 @@ private void UpdateCalmDown() /// public bool TryCalmDown(Entity ent) { - // Немного костыль, но это означает, что мы прямо сейчас испытываем какие-то приколы со страхом - // И пугаемся чего-то в данный момент. Значит мы не должны успокаиваться. - if (_activeFearEffects.HasComp(ent)) - return false; - // Проверка на то, что мы в данный момент не смотрим на какую-то страшную сущность. // Нельзя успокоиться, когда мы смотрим на источник страха. if (_watching.TryGetAnyEntitiesVisibleTo(ent.Owner, ent.Comp.SeenBlockerLevel)) @@ -121,10 +114,8 @@ private void RemoveMoodEffects(EntityUid uid) RaiseLocalEvent(uid, new MoodRemoveEffectEvent(MoodHemophobicBleeding)); } - protected override void Clear() + private void OnCleanUp(RoundRestartCleanupEvent args) { - base.Clear(); - _nextHemophobiaCheck = TimeSpan.Zero; _nextCalmDownCheck = TimeSpan.Zero; } diff --git a/Content.Shared/_Scp/Fear/Components/ActiveFearFallOffComponent.cs b/Content.Shared/_Scp/Fear/Components/ActiveFearFallOffComponent.cs new file mode 100644 index 00000000000..b588852cad3 --- /dev/null +++ b/Content.Shared/_Scp/Fear/Components/ActiveFearFallOffComponent.cs @@ -0,0 +1,28 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared._Scp.Fear.Components; + +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, AutoGenerateComponentPause] +public sealed partial class ActiveFearFallOffComponent : Component +{ + /// + /// Шанс упасть при хождении во время страха постигшего + /// + [DataField, AutoNetworkedField] + public float FallOffChance = 0.03f; + + /// + /// Время между проверками на возможность запнуться. + /// + [DataField] + public TimeSpan FallOffCheckInterval = TimeSpan.FromSeconds(0.3f); + + /// + /// Время следующей проверки на возможность запнуться при высоком уровне страха. + /// + [AutoNetworkedField, ViewVariables, AutoPausedField] + public TimeSpan? FallOffNextCheckTime; + + [DataField] + public TimeSpan FallOffTime = TimeSpan.FromSeconds(0.5f); +} diff --git a/Content.Shared/_Scp/Fear/Components/FearComponent.cs b/Content.Shared/_Scp/Fear/Components/FearComponent.cs index 29cb7121d73..441e372066a 100644 --- a/Content.Shared/_Scp/Fear/Components/FearComponent.cs +++ b/Content.Shared/_Scp/Fear/Components/FearComponent.cs @@ -143,19 +143,7 @@ public sealed partial class FearComponent : Component /// Шанс упасть при хождении во время страха постигшего /// [DataField, AutoNetworkedField] - public float FallOffChance = 3f; // 3% - - /// - /// Время следующей проверки на возможность запнуться при высоком уровне страха. - /// - [ViewVariables] - public TimeSpan FallOffNextCheckTime = TimeSpan.Zero; - - /// - /// Время между проверками на возможность запнуться. - /// - [DataField] - public TimeSpan FallOffCheckInterval = TimeSpan.FromSeconds(0.3f); + public float FallOffChance = 0.03f; /// /// Какой уровень страха нужен, чтобы у человека появился адреналин. diff --git a/Content.Shared/_Scp/Fear/Components/Traits/FearFaintingComponent.cs b/Content.Shared/_Scp/Fear/Components/Traits/FearFaintingComponent.cs index 2c32b4cc1db..fa9b28cc4a3 100644 --- a/Content.Shared/_Scp/Fear/Components/Traits/FearFaintingComponent.cs +++ b/Content.Shared/_Scp/Fear/Components/Traits/FearFaintingComponent.cs @@ -12,20 +12,21 @@ public sealed partial class FearFaintingComponent : Component /// /// Требуемый уровень страха для падения в обморок /// - [DataField, ViewVariables] + [DataField] public FearState RequiredState = FearState.Fear; /// /// Время, которое персонаж проведет в обмороке /// - [DataField, ViewVariables] + [DataField] public TimeSpan Time = TimeSpan.FromSeconds(20f); /// /// Шанс упасть в обморок при достижении /// - [DataField, ViewVariables] - public float Chance = 30f; + [DataField] + public float Chance = 0.3f; - public static readonly EntProtoId StatusEffect = "StatusEffectForcedSleeping"; + [DataField] + public EntProtoId StatusEffect = "StatusEffectForcedSleeping"; } diff --git a/Content.Shared/_Scp/Fear/Components/Traits/FearStuporComponent.cs b/Content.Shared/_Scp/Fear/Components/Traits/FearStuporComponent.cs index b2638618c3e..7a3a95e5e58 100644 --- a/Content.Shared/_Scp/Fear/Components/Traits/FearStuporComponent.cs +++ b/Content.Shared/_Scp/Fear/Components/Traits/FearStuporComponent.cs @@ -9,14 +9,15 @@ namespace Content.Shared._Scp.Fear.Components.Traits; [RegisterComponent, NetworkedComponent] public sealed partial class FearStuporComponent : Component { - [DataField, ViewVariables] + [DataField] public FearState RequiredState = FearState.Fear; - [DataField, ViewVariables] - public float Chance = 10f; + [DataField] + public float Chance = 0.1f; - [DataField, ViewVariables] + [DataField] public TimeSpan StuporTime = TimeSpan.FromSeconds(10f); - public static readonly EntProtoId StatusEffect = "StatusEffectFearStupor"; + [DataField] + public EntProtoId StatusEffect = "StatusEffectFearStupor"; } diff --git a/Content.Shared/_Scp/Fear/Systems/SharedFearSystem.CloseFear.cs b/Content.Shared/_Scp/Fear/Systems/SharedFearSystem.CloseFear.cs index 71329b86ee5..8e4f671a20a 100644 --- a/Content.Shared/_Scp/Fear/Systems/SharedFearSystem.CloseFear.cs +++ b/Content.Shared/_Scp/Fear/Systems/SharedFearSystem.CloseFear.cs @@ -40,7 +40,7 @@ private void SyncCloseFear(Entity ent, Entity ent, Entity ent, ref ComponentShutdown args) { - if (!TryComp(ent, out var fear)) + if (!_fearQuery.TryComp(ent, out var fear)) return; ClearCloseFear((ent.Owner, fear)); diff --git a/Content.Shared/_Scp/Fear/Systems/SharedFearSystem.Fears.cs b/Content.Shared/_Scp/Fear/Systems/SharedFearSystem.Fears.cs index 5d56ac523b7..1a339019b37 100644 --- a/Content.Shared/_Scp/Fear/Systems/SharedFearSystem.Fears.cs +++ b/Content.Shared/_Scp/Fear/Systems/SharedFearSystem.Fears.cs @@ -22,8 +22,12 @@ private void OnHemophobiaInit(Entity ent, ref ComponentStar .OrderBy(kv => kv.Value) .ToList(); - if (!TryComp(ent, out var fearComponent)) + if (!_fearQuery.TryComp(ent, out var fearComponent)) + { + Log.Warning($"Found entity {ToPrettyString(ent)} with {nameof(HemophobiaComponent)} but without {nameof(FearComponent)}! {nameof(HemophobiaComponent)} will be deleted"); + return; + } fearComponent.Phobias.Add(ent.Comp.Phobia); DirtyField(ent, fearComponent, nameof(FearComponent.Phobias)); @@ -35,10 +39,7 @@ private void OnHemophobiaInit(Entity ent, ref ComponentStar /// private void OnHemophobiaShutdown(Entity ent, ref ComponentShutdown args) { - if (TerminatingOrDeleted(ent)) - return; - - if (!TryComp(ent, out var fearComponent)) + if (!_fearQuery.TryComp(ent, out var fearComponent)) return; fearComponent.Phobias.Remove(ent.Comp.Phobia); diff --git a/Content.Shared/_Scp/Fear/Systems/SharedFearSystem.Gameplay.cs b/Content.Shared/_Scp/Fear/Systems/SharedFearSystem.Gameplay.cs index e35385a3841..00d5a5c3058 100644 --- a/Content.Shared/_Scp/Fear/Systems/SharedFearSystem.Gameplay.cs +++ b/Content.Shared/_Scp/Fear/Systems/SharedFearSystem.Gameplay.cs @@ -1,10 +1,12 @@ using Content.Shared._Scp.Fear.Components; -using Content.Shared._Scp.Fear.Components.Traits; using Content.Shared._Scp.Weapons.Ranged; using Content.Shared._Sunrise.Mood; +using Content.Shared._Sunrise.Random; using Content.Shared.Drunk; using Content.Shared.Jittering; +using Content.Shared.Standing; using Content.Shared.StatusEffectNew; +using Content.Shared.Stunnable; using Robust.Shared.Prototypes; namespace Content.Shared._Scp.Fear.Systems; @@ -13,6 +15,9 @@ public abstract partial class SharedFearSystem { [Dependency] private readonly StatusEffectsSystem _effects = default!; [Dependency] private readonly SharedJitteringSystem _jittering = default!; + [Dependency] private readonly StandingStateSystem _standing = default!; + [Dependency] private readonly SharedStunSystem _stun = default!; + [Dependency] private readonly RandomPredictedSystem _random = default!; private const float BaseJitteringAmplitude = 1f; private const float BaseJitteringFrequency = 4f; @@ -35,6 +40,30 @@ public abstract partial class SharedFearSystem private void InitializeGameplay() { _drunkQuery = GetEntityQuery(); + + SubscribeLocalEvent(OnFallOffMapInit); + SubscribeLocalEvent(OnFallOffMove); + } + + private void OnFallOffMapInit(Entity ent, ref MapInitEvent args) + { + SetNextFallOffTime(ent); + } + + private void OnFallOffMove(Entity ent, ref MoveEvent args) + { + if (_timing.CurTime < ent.Comp.FallOffNextCheckTime) + return; + + SetNextFallOffTime(ent); // Даже если не прокнет, то время все равно должно устанавливаться + + if (!_random.ProbForEntity(ent, ent.Comp.FallOffChance)) + return; + + if (_standing.IsDown(ent.Owner)) + return; + + _stun.TryAddParalyzeDuration(ent.Owner, ent.Comp.FallOffTime); } /// @@ -68,10 +97,7 @@ private void SetSpreadParameters(EntityUid uid, float angleIncrease, float maxAn private void ManageJitter(Entity ent) { // При ступоре и обмороке персонаж не должен трястись - if (_effects.HasStatusEffect(ent, FearStuporComponent.StatusEffect)) - return; - - if (_effects.HasStatusEffect(ent, FearFaintingComponent.StatusEffect)) + if (_fearFaintingQuery.HasComp(ent) || _fearStuporQuery.HasComp(ent)) return; if (MathHelper.CloseTo(ent.Comp.BaseJitterTime, 0f)) @@ -155,4 +181,27 @@ private float GetDrunkModifier(EntityUid uid) } protected virtual void TryScream(Entity ent) {} + + private void ManageFallOff(Entity ent) + { + if (ent.Comp.State >= ent.Comp.FallOffRequiredState && !HasComp(ent)) + { + var comp = EnsureComp(ent); + comp.FallOffChance = ent.Comp.FallOffChance; + Dirty(ent, comp); + } + else + { + RemComp(ent); + } + } + + /// + /// Устанавливает следующее время возможности запнуться. + /// + private void SetNextFallOffTime(Entity ent) + { + ent.Comp.FallOffNextCheckTime = _timing.CurTime + ent.Comp.FallOffCheckInterval; + Dirty(ent); + } } diff --git a/Content.Shared/_Scp/Fear/Systems/SharedFearSystem.Helpers.cs b/Content.Shared/_Scp/Fear/Systems/SharedFearSystem.Helpers.cs index f8788180598..b5be92ead45 100644 --- a/Content.Shared/_Scp/Fear/Systems/SharedFearSystem.Helpers.cs +++ b/Content.Shared/_Scp/Fear/Systems/SharedFearSystem.Helpers.cs @@ -151,12 +151,4 @@ protected void SetNextCalmDownTime(Entity ent) _ => null, }; - - /// - /// Преобразует процент из человеческого формата в probный. - /// - protected static float PercentToNormalized(float percent) - { - return Math.Clamp(percent / 100f, 0f, 1f); - } } diff --git a/Content.Shared/_Scp/Fear/Systems/SharedFearSystem.Traits.cs b/Content.Shared/_Scp/Fear/Systems/SharedFearSystem.Traits.cs index 431b91e8bb7..f31615c399c 100644 --- a/Content.Shared/_Scp/Fear/Systems/SharedFearSystem.Traits.cs +++ b/Content.Shared/_Scp/Fear/Systems/SharedFearSystem.Traits.cs @@ -1,9 +1,15 @@ -namespace Content.Shared._Scp.Fear.Systems; +using Content.Shared._Scp.Fear.Components.Traits; + +namespace Content.Shared._Scp.Fear.Systems; public abstract partial class SharedFearSystem { + private EntityQuery _fearStuporQuery; + private EntityQuery _fearFaintingQuery; + private void InitializeTraits() { - + _fearStuporQuery = GetEntityQuery(); + _fearFaintingQuery = GetEntityQuery(); } } diff --git a/Content.Shared/_Scp/Fear/Systems/SharedFearSystem.cs b/Content.Shared/_Scp/Fear/Systems/SharedFearSystem.cs index 7a9d30bd796..66a24b0c2c1 100644 --- a/Content.Shared/_Scp/Fear/Systems/SharedFearSystem.cs +++ b/Content.Shared/_Scp/Fear/Systems/SharedFearSystem.cs @@ -35,7 +35,8 @@ public abstract partial class SharedFearSystem : EntitySystem private const string MoodSourceClose = "FearSourceClose"; private const string MoodFearSourceSeen = "FearSourceSeen"; - private EntityQuery _fears; + private EntityQuery _fearSourceQuery; + private EntityQuery _fearQuery; public override void Initialize() { @@ -55,7 +56,8 @@ public override void Initialize() InitializeTraits(); InitializeCloseFear(); - _fears = GetEntityQuery(); + _fearSourceQuery = GetEntityQuery(); + _fearQuery = GetEntityQuery(); } /// @@ -82,7 +84,7 @@ private void OnEntityLookedAt(Entity ent, ref EntityLookedAtEvent && _timing.CurTime < lastSeenTime + ent.Comp.TimeToGetScaredAgainOnLookAt) return; - if (!_fears.TryComp(args.Target, out var source)) + if (!_fearSourceQuery.TryComp(args.Target, out var source)) return; if (source.UponSeenState == FearState.None) @@ -109,14 +111,12 @@ private void OnEntityLookedAt(Entity ent, ref EntityLookedAtEvent /// private void OnFearStateChanged(Entity ent, ref FearStateChangedEvent args) { - if (!_timing.IsFirstTimePredicted) - return; - PlayFearStateSound(ent, args.OldState); // Добавляем геймплейные проблемы, завязанный на уровне страха ManageShootingProblems(ent); ManageStateBasedMood(ent); + ManageFallOff(ent); // Проверка на то, что уровень понизился -> мы успокоились. // Геймплейные штуки ниже не нужно триггерить. @@ -211,8 +211,8 @@ private void CleanupFear(Entity ent) { ClearCloseFear(ent); - RemoveSeenFearMood(ent.Owner); - WipeMood(ent.Owner); + RemoveSeenFearMood(ent); + WipeMood(ent); } private void RemoveSeenFearMood(EntityUid uid) diff --git a/Resources/Prototypes/_Scp/Roles/Jobs/Administration/security_commander.yml b/Resources/Prototypes/_Scp/Roles/Jobs/Administration/security_commander.yml index 6a5ea003419..1aed21a3b06 100644 --- a/Resources/Prototypes/_Scp/Roles/Jobs/Administration/security_commander.yml +++ b/Resources/Prototypes/_Scp/Roles/Jobs/Administration/security_commander.yml @@ -65,7 +65,7 @@ Fear: 60 Terror: 350 baseJitterTime: 2 - fallOffChance: 1 + fallOffChance: 0.01 - type: startingGear id: SecurityCommanderGear diff --git a/Resources/Prototypes/_Scp/Roles/Jobs/CommandantSquad/external_administrative_zone_commandant.yml b/Resources/Prototypes/_Scp/Roles/Jobs/CommandantSquad/external_administrative_zone_commandant.yml index 202a8866b95..ec42f204c1a 100644 --- a/Resources/Prototypes/_Scp/Roles/Jobs/CommandantSquad/external_administrative_zone_commandant.yml +++ b/Resources/Prototypes/_Scp/Roles/Jobs/CommandantSquad/external_administrative_zone_commandant.yml @@ -59,7 +59,7 @@ Fear: 60 Terror: 350 baseJitterTime: 2 - fallOffChance: 1 + fallOffChance: 0.01 - type: startingGear id: ExternalAdministrativeZoneCommandantGear diff --git a/Resources/Prototypes/_Scp/Roles/Jobs/CommandantSquad/external_administrative_zone_officer.yml b/Resources/Prototypes/_Scp/Roles/Jobs/CommandantSquad/external_administrative_zone_officer.yml index fd2f30a10e3..78920db7053 100644 --- a/Resources/Prototypes/_Scp/Roles/Jobs/CommandantSquad/external_administrative_zone_officer.yml +++ b/Resources/Prototypes/_Scp/Roles/Jobs/CommandantSquad/external_administrative_zone_officer.yml @@ -54,7 +54,7 @@ Fear: 60 Terror: 350 baseJitterTime: 2 - fallOffChance: 1 + fallOffChance: 0.01 - type: startingGear id: ExternalAdministrativeZoneOfficerGear diff --git a/Resources/Prototypes/_Scp/Roles/Jobs/CommandantSquad/junior_external_administrative_zone_officer.yml b/Resources/Prototypes/_Scp/Roles/Jobs/CommandantSquad/junior_external_administrative_zone_officer.yml index de8fc66ffd3..33f51739002 100644 --- a/Resources/Prototypes/_Scp/Roles/Jobs/CommandantSquad/junior_external_administrative_zone_officer.yml +++ b/Resources/Prototypes/_Scp/Roles/Jobs/CommandantSquad/junior_external_administrative_zone_officer.yml @@ -43,7 +43,7 @@ Fear: 2.2 Terror: 5 baseJitterTime: 3 - fallOffChance: 2 + fallOffChance: 0.02 - type: startingGear id: JuniorExternalAdministrativeZoneOfficerGear diff --git a/Resources/Prototypes/_Scp/Roles/Jobs/CommandantSquad/senior_external_administrative_zone_officer.yml b/Resources/Prototypes/_Scp/Roles/Jobs/CommandantSquad/senior_external_administrative_zone_officer.yml index 203577a659c..67ca4c5ca1b 100644 --- a/Resources/Prototypes/_Scp/Roles/Jobs/CommandantSquad/senior_external_administrative_zone_officer.yml +++ b/Resources/Prototypes/_Scp/Roles/Jobs/CommandantSquad/senior_external_administrative_zone_officer.yml @@ -54,7 +54,7 @@ Fear: 60 Terror: 350 baseJitterTime: 2 - fallOffChance: 1 + fallOffChance: 0.01 - type: startingGear id: SeniorExternalAdministrativeZoneOfficerGear diff --git a/Resources/Prototypes/_Scp/Roles/Jobs/SpecialPurposeSquad/heavy_containment_zone_commandant.yml b/Resources/Prototypes/_Scp/Roles/Jobs/SpecialPurposeSquad/heavy_containment_zone_commandant.yml index d3fcaa47cb1..07a3cf01b76 100644 --- a/Resources/Prototypes/_Scp/Roles/Jobs/SpecialPurposeSquad/heavy_containment_zone_commandant.yml +++ b/Resources/Prototypes/_Scp/Roles/Jobs/SpecialPurposeSquad/heavy_containment_zone_commandant.yml @@ -60,7 +60,7 @@ Fear: 60 Terror: 350 baseJitterTime: 2 - fallOffChance: 1 + fallOffChance: 0.01 - type: startingGear id: HeavyContainmentZoneCommandantGear diff --git a/Resources/Prototypes/_Scp/Roles/Jobs/SpecialPurposeSquad/heavy_containment_zone_officer.yml b/Resources/Prototypes/_Scp/Roles/Jobs/SpecialPurposeSquad/heavy_containment_zone_officer.yml index 496326553e2..914e026f047 100644 --- a/Resources/Prototypes/_Scp/Roles/Jobs/SpecialPurposeSquad/heavy_containment_zone_officer.yml +++ b/Resources/Prototypes/_Scp/Roles/Jobs/SpecialPurposeSquad/heavy_containment_zone_officer.yml @@ -54,7 +54,7 @@ Fear: 60 Terror: 350 baseJitterTime: 2 - fallOffChance: 1 + fallOffChance: 0.01 - type: startingGear id: HeavyContainmentZoneOfficerGear diff --git a/Resources/Prototypes/_Scp/Roles/Jobs/SpecialPurposeSquad/junior_heavy_containment_zone_officer.yml b/Resources/Prototypes/_Scp/Roles/Jobs/SpecialPurposeSquad/junior_heavy_containment_zone_officer.yml index 8b5b9b3f360..d991ca9b174 100644 --- a/Resources/Prototypes/_Scp/Roles/Jobs/SpecialPurposeSquad/junior_heavy_containment_zone_officer.yml +++ b/Resources/Prototypes/_Scp/Roles/Jobs/SpecialPurposeSquad/junior_heavy_containment_zone_officer.yml @@ -43,7 +43,7 @@ Fear: 2.2 Terror: 5 baseJitterTime: 3 - fallOffChance: 2 + fallOffChance: 0.02 - type: startingGear id: JuniorHeavyContainmentZoneOfficerGear diff --git a/Resources/Prototypes/_Scp/Roles/Jobs/SpecialPurposeSquad/senior_heavy_containment_zone_officer.yml b/Resources/Prototypes/_Scp/Roles/Jobs/SpecialPurposeSquad/senior_heavy_containment_zone_officer.yml index b83ff2489c4..965c40a0e8b 100644 --- a/Resources/Prototypes/_Scp/Roles/Jobs/SpecialPurposeSquad/senior_heavy_containment_zone_officer.yml +++ b/Resources/Prototypes/_Scp/Roles/Jobs/SpecialPurposeSquad/senior_heavy_containment_zone_officer.yml @@ -54,7 +54,7 @@ Fear: 60 Terror: 350 baseJitterTime: 2 - fallOffChance: 1 + fallOffChance: 0.01 - type: startingGear id: SeniorHeavyContainmentZoneOfficerGear From 023325200ff0980f8126e56cc6aa148dee9d5c98 Mon Sep 17 00:00:00 2001 From: drdth Date: Fri, 20 Mar 2026 22:56:12 +0300 Subject: [PATCH 12/17] refactor: move blinking alert logic to client --- .../_Scp/Blinking/BlinkingSystem.cs | 51 +++++++++++++++++++ .../_Scp/Blinking/SharedBlinkingSystem.cs | 23 --------- Resources/Prototypes/_Scp/Alerts/alerts.yml | 1 + 3 files changed, 52 insertions(+), 23 deletions(-) diff --git a/Content.Client/_Scp/Blinking/BlinkingSystem.cs b/Content.Client/_Scp/Blinking/BlinkingSystem.cs index 31ed601a2ec..83308c451ff 100644 --- a/Content.Client/_Scp/Blinking/BlinkingSystem.cs +++ b/Content.Client/_Scp/Blinking/BlinkingSystem.cs @@ -1,10 +1,12 @@ using Content.Client._Scp.Shaders.Common; using Content.Shared._Scp.Blinking; +using Content.Shared.Alert; using Robust.Client.Audio; using Robust.Client.Graphics; using Robust.Client.Player; using Robust.Shared.Audio; using Robust.Shared.Player; +using Robust.Shared.Prototypes; using Robust.Shared.Timing; namespace Content.Client._Scp.Blinking; @@ -13,6 +15,7 @@ public sealed class BlinkingSystem : SharedBlinkingSystem { [Dependency] private readonly AudioSystem _audio = default!; [Dependency] private readonly CompatibilityModeActiveWarningSystem _compatibility = default!; + [Dependency] private readonly AlertsSystem _alerts = default!; [Dependency] private readonly IPlayerManager _player = default!; [Dependency] private readonly IOverlayManager _overlayMan = default!; [Dependency] private readonly IGameTiming _timing = default!; @@ -25,6 +28,7 @@ public sealed class BlinkingSystem : SharedBlinkingSystem private BlinkingOverlay _overlay = default!; private const float DefaultAnimationDuration = 0.4f; + private ProtoId? _localBlinkingAlert; public override void Initialize() { @@ -42,6 +46,29 @@ public override void Initialize() _overlay.OnAnimationFinished += SetDefaultAnimationDuration; } + public override void Update(float frameTime) + { + base.Update(frameTime); + + if (_player.LocalEntity is not { Valid: true } localEntity) + return; + + if (!BlinkableQuery.TryComp(localEntity, out var blinkable)) + { + if (_localBlinkingAlert.HasValue) + _alerts.ClearAlert(localEntity, _localBlinkingAlert.Value); + + _localBlinkingAlert = null; + return; + } + + if (_localBlinkingAlert.HasValue && _localBlinkingAlert.Value != blinkable.BlinkingAlert) + _alerts.ClearAlert(localEntity, _localBlinkingAlert.Value); + + _localBlinkingAlert = blinkable.BlinkingAlert; + UpdateAlert((localEntity, blinkable)); + } + protected override void OnOpenedEyes(Entity ent, ref EntityOpenedEyesEvent args) { base.OnOpenedEyes(ent, ref args); @@ -66,6 +93,11 @@ private void OnAttached(Entity ent, ref LocalPlayerAttachedE private void OnDetached(Entity ent, ref LocalPlayerDetachedEvent args) { + if (_localBlinkingAlert.HasValue) + _alerts.ClearAlert(ent.Owner, _localBlinkingAlert.Value); + + _localBlinkingAlert = null; + if (!_overlayMan.HasOverlay()) return; @@ -195,4 +227,23 @@ private void SetDefaultAnimationDuration() { _overlay.AnimationDuration = _compatibility.ShouldUseShaders ? DefaultAnimationDuration : 0f; } + + /// + /// Актуализирует иконку моргания справа у панели чата игрока + /// + private void UpdateAlert(Entity ent) + { + // Если в данный момент глаза закрыты, то выставляем иконку с закрытым глазом + if (IsBlind(ent.AsNullable())) + { + _alerts.ShowAlert(ent.Owner, ent.Comp.BlinkingAlert, 4); + return; + } + + var timeToNextBlink = ent.Comp.NextBlink - _timing.CurTime; + var denom = MathF.Max(0.001f, (float)(ent.Comp.BlinkingInterval.TotalSeconds - ent.Comp.BlinkingDuration.TotalSeconds)); + var severity = (short) Math.Clamp(4 - (float) timeToNextBlink.TotalSeconds / denom * 4, 0, 4); + + _alerts.ShowAlert(ent.Owner, ent.Comp.BlinkingAlert, severity); + } } diff --git a/Content.Shared/_Scp/Blinking/SharedBlinkingSystem.cs b/Content.Shared/_Scp/Blinking/SharedBlinkingSystem.cs index 51346b465ee..e0dc50ecfbe 100644 --- a/Content.Shared/_Scp/Blinking/SharedBlinkingSystem.cs +++ b/Content.Shared/_Scp/Blinking/SharedBlinkingSystem.cs @@ -3,7 +3,6 @@ using Content.Shared._Scp.Scp173; using Content.Shared._Scp.Watching; using Content.Shared._Sunrise.Random; -using Content.Shared.Alert; using Content.Shared.Mobs; using Content.Shared.Mobs.Systems; using Robust.Shared.Network; @@ -20,7 +19,6 @@ public abstract partial class SharedBlinkingSystem : EntitySystem { [Dependency] private readonly MobStateSystem _mobState = default!; [Dependency] private readonly EyeWatchingSystem _watching = default!; - [Dependency] private readonly AlertsSystem _alerts = default!; [Dependency] private readonly RandomPredictedSystem _random = default!; [Dependency] private readonly IGameTiming _timing = default!; [Dependency] private readonly INetManager _net = default!; @@ -111,8 +109,6 @@ public override void Update(float frameTime) { var blinkableEntity = (uid, blinkableComponent); - UpdateAlert(blinkableEntity); - if (TryOpenEyes(blinkableEntity)) continue; @@ -230,25 +226,6 @@ private TimeSpan GetBlinkVariance(Entity ent) #endregion - /// - /// Актуализирует иконку моргания справа у панели чата игрока - /// - protected void UpdateAlert(Entity ent) - { - // Если в данный момент глаза закрыты, то выставляем иконку с закрытым глазом - if (IsBlind(ent.AsNullable())) - { - _alerts.ShowAlert(ent.Owner, ent.Comp.BlinkingAlert, 4); - return; - } - - var timeToNextBlink = ent.Comp.NextBlink - _timing.CurTime; - var denom = MathF.Max(0.001f, (float)(ent.Comp.BlinkingInterval.TotalSeconds - ent.Comp.BlinkingDuration.TotalSeconds)); - var severity = (short) Math.Clamp(4 - (float) timeToNextBlink.TotalSeconds / denom * 4, 0, 4); - - _alerts.ShowAlert(ent.Owner, ent.Comp.BlinkingAlert, severity); - } - /// /// Проверяет, есть ли рядом с игроком Scp, использующий механики зрения /// diff --git a/Resources/Prototypes/_Scp/Alerts/alerts.yml b/Resources/Prototypes/_Scp/Alerts/alerts.yml index 6317641b73d..ce9e16bdd43 100644 --- a/Resources/Prototypes/_Scp/Alerts/alerts.yml +++ b/Resources/Prototypes/_Scp/Alerts/alerts.yml @@ -15,6 +15,7 @@ description: alert-desc-blinking minSeverity: 0 maxSeverity: 4 + clientHandled: true - type: alert id: Scp106LifeEssence From 1630db38ad511e44c9d98e2ad6c19db897894431 Mon Sep 17 00:00:00 2001 From: drdth Date: Fri, 20 Mar 2026 23:48:11 +0300 Subject: [PATCH 13/17] refactor: optimize scp939 code --- Content.Client/_Scp/Scp939/Scp939HudSystem.cs | 192 ++++++++++++------ .../_Scp/Scp939/Scp939ResetAlphaOverlay.cs | 30 +++ .../_Scp/Scp939/Scp939SetAlphaOverlay.cs | 55 +++++ .../_Scp/Scp939/Scp939System.Visibility.cs | 122 +++++++++-- Content.Server/_Scp/Scp939/Scp939System.cs | 2 + .../Scp939/ActiveScp939VisibilityComponent.cs | 19 ++ Content.Shared/_Scp/Scp939/Scp939Component.cs | 3 + .../_Scp/Scp939/Scp939VisabilityComponent.cs | 25 +-- 8 files changed, 352 insertions(+), 96 deletions(-) create mode 100644 Content.Client/_Scp/Scp939/Scp939ResetAlphaOverlay.cs create mode 100644 Content.Client/_Scp/Scp939/Scp939SetAlphaOverlay.cs create mode 100644 Content.Shared/_Scp/Scp939/ActiveScp939VisibilityComponent.cs diff --git a/Content.Client/_Scp/Scp939/Scp939HudSystem.cs b/Content.Client/_Scp/Scp939/Scp939HudSystem.cs index f1a99bc2acb..3c285fd5408 100644 --- a/Content.Client/_Scp/Scp939/Scp939HudSystem.cs +++ b/Content.Client/_Scp/Scp939/Scp939HudSystem.cs @@ -1,74 +1,92 @@ -using System.Diagnostics.CodeAnalysis; using Content.Client.Overlays; using Content.Client.SSDIndicator; using Content.Client.Stealth; using Content.Shared._Scp.Scp939; using Content.Shared._Scp.Scp939.Protection; using Content.Shared.Examine; +using Content.Shared.Inventory.Events; using Content.Shared.Movement.Components; using Content.Shared.Standing; using Content.Shared.StatusIcon.Components; using Content.Shared.Throwing; using Content.Shared.Weapons.Melee.Events; -using Content.Shared.Weapons.Ranged.Events; using Robust.Client.GameObjects; using Robust.Client.Graphics; using Robust.Client.Player; using Robust.Shared.Physics.Components; using Robust.Shared.Physics.Events; using Robust.Shared.Player; -using Robust.Shared.Prototypes; using Robust.Shared.Random; namespace Content.Client._Scp.Scp939; public sealed class Scp939HudSystem : EquipmentHudSystem { - [Dependency] private readonly IPrototypeManager _prototypeManager = default!; + [Dependency] private readonly IOverlayManager _overlayManager = default!; [Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly IPlayerManager _playerManager = default!; + [Dependency] private readonly SpriteSystem _sprite = default!; - private ShaderInstance _shaderInstance = default!; - private readonly Dictionary _shaderCache = new(); + internal readonly List<(Entity Ent, float BaseAlpha)> CachedBaseAlphas = new(64); + + private Scp939SetAlphaOverlay _setAlphaOverlay = default!; + private Scp939ResetAlphaOverlay _resetAlphaOverlay = default!; // TODO: Выделить значения плохого зрения в отдельный компонент, не связанный с 939 private Scp939Component? _scp939Component; + private EntityQuery _eyeQuery; + + private bool _overlaysPresented; private float _lastUpdateTime; - private const float UpdateInterval = 0.05f; // Обновляем каждые n секунды + + private const float UpdateInterval = 0.05f; public override void Initialize() { base.Initialize(); - SubscribeLocalEvent((Entity ent, ref StartCollideEvent args) => OnCollide(ent, args.OtherEntity)); - SubscribeLocalEvent((Entity ent, ref EndCollideEvent args) => OnCollide(ent, args.OtherEntity)); + SubscribeLocalEvent((Entity ent, ref StartCollideEvent args) + => OnCollide(ent, args.OtherEntity)); + SubscribeLocalEvent((Entity ent, ref EndCollideEvent args) + => OnCollide(ent, args.OtherEntity)); #region Visibility - SubscribeLocalEvent(OnMove); + SubscribeLocalEvent(OnMove); - SubscribeLocalEvent(OnThrow); - SubscribeLocalEvent(OnStood); - SubscribeLocalEvent(OnMeleeAttack); + SubscribeLocalEvent(OnThrow); + SubscribeLocalEvent(OnStood); + SubscribeLocalEvent(OnMeleeAttack); #endregion - SubscribeLocalEvent(BeforeRender); - SubscribeLocalEvent(OnGetStatusIcons, after: new []{typeof(SSDIndicatorSystem)}); - SubscribeLocalEvent(OnExamine); + SubscribeLocalEvent(OnGetStatusIcons, after: [typeof(SSDIndicatorSystem)] ); + SubscribeLocalEvent(OnExamine); SubscribeLocalEvent(OnPlayerAttached); SubscribeLocalEvent(OnPlayerDetached); - SubscribeLocalEvent(OnEntityTerminating); + _eyeQuery = GetEntityQuery(); - _shaderInstance = _prototypeManager.Index("Hide").Instance(); + _setAlphaOverlay = new(); + _resetAlphaOverlay = new(); UpdatesAfter.Add(typeof(StealthSystem)); } - private void OnExamine(Entity ent, ref ExamineAttemptEvent args) + public override void Shutdown() + { + RestoreCachedBaseAlphas(); + RemoveOverlays(); + + _setAlphaOverlay.Dispose(); + _resetAlphaOverlay.Dispose(); + + base.Shutdown(); + } + + private void OnExamine(Entity ent, ref ExamineAttemptEvent args) { if (!IsActive) return; @@ -79,12 +97,9 @@ private void OnExamine(Entity ent, ref ExamineAttempt args.Cancel(); } - private void OnGetStatusIcons(Entity ent, ref GetStatusIconsEvent args) + private void OnGetStatusIcons(Entity ent, ref GetStatusIconsEvent args) { - // Олежа чурка - var playerEntity = _playerManager.LocalSession?.AttachedEntity; - - if (!HasComp(playerEntity)) + if (!IsActive) return; var visibility = GetVisibility(ent); @@ -93,51 +108,73 @@ private void OnGetStatusIcons(Entity ent, ref GetStat args.StatusIcons.Clear(); } + protected override void UpdateInternal(RefreshEquipmentHudEvent args) + { + base.UpdateInternal(args); + + _scp939Component = args.Components.Count > 0 ? args.Components[0] : null; + AddOverlays(); + } + protected override void DeactivateInternal() { base.DeactivateInternal(); - var query = EntityQueryEnumerator(); + _scp939Component = null; + _lastUpdateTime = 0f; - while (query.MoveNext(out _, out _, out var spriteComponent)) - { - spriteComponent.PostShader = null; - } + RestoreCachedBaseAlphas(); + RemoveOverlays(); } #region Visibility - private void OnCollide(Entity ent, EntityUid otherEntity) + private void OnCollide(Entity ent, EntityUid otherEntity) { + if (!IsActive) + return; + if (!HasComp(otherEntity)) return; MobDidSomething(ent); } - private void OnThrow(Entity ent, ref ThrowEvent args) + private void OnThrow(Entity ent, ref ThrowEvent args) { + if (!IsActive) + return; + MobDidSomething(ent); } - private void OnStood(Entity ent, ref StoodEvent args) + private void OnStood(Entity ent, ref StoodEvent args) { + if (!IsActive) + return; + MobDidSomething(ent); } - private void OnMeleeAttack(Entity ent, ref MeleeAttackEvent args) + private void OnMeleeAttack(Entity ent, ref MeleeAttackEvent args) { + if (!IsActive) + return; + MobDidSomething(ent); } - private void MobDidSomething(Entity ent) + private void MobDidSomething(Entity ent) { - ent.Comp.VisibilityAcc = 0.001f; + ent.Comp.VisibilityAcc = Scp939VisibilityComponent.InitialVisibilityAcc; Dirty(ent); } - private void OnMove(Entity ent, ref MoveEvent args) + private void OnMove(Entity ent, ref MoveEvent args) { + if (!IsActive) + return; + // В зависимости от наличие защит или проблем со зрением у 939 изменяется то, насколько хорошо мы видим жертву if (ModifyAcc(ent.Comp, out var modifier)) // Если зрение затруднено { @@ -161,9 +198,7 @@ private void OnMove(Entity ent, ref MoveEvent args) var currentVelocity = physicsComponent.LinearVelocity.Length(); if (speedModifierComponent.BaseWalkSpeed > currentVelocity) - { ent.Comp.VisibilityAcc = ent.Comp.HideTime / 2f; - } } #endregion @@ -171,6 +206,7 @@ private void OnMove(Entity ent, ref MoveEvent args) private void OnPlayerAttached(Entity ent, ref PlayerAttachedEvent args) { _scp939Component = ent.Comp; + AddOverlays(); } private void OnPlayerDetached(Entity ent, ref PlayerDetachedEvent args) @@ -178,11 +214,6 @@ private void OnPlayerDetached(Entity ent, ref PlayerDetachedEve _scp939Component = null; } - private void OnEntityTerminating(Entity ent, ref EntityTerminatingEvent args) - { - _shaderCache.Remove(ent); - } - public override void Update(float frameTime) { base.Update(frameTime); @@ -194,35 +225,47 @@ public override void Update(float frameTime) if (_lastUpdateTime < UpdateInterval) return; + var delta = _lastUpdateTime; _lastUpdateTime = 0f; - var query = EntityQueryEnumerator(); - - while (query.MoveNext(out var uid, out var spriteComponent, out var visibilityComponent)) + var query = EntityQueryEnumerator(); + while (query.MoveNext(out _, out var visibilityComponent)) { - // Обновляем только если нужно - if (!_shaderCache.TryGetValue(uid, out var shader)) - { - shader = _shaderInstance.Duplicate(); - _shaderCache[uid] = shader; - UpdateVisibility(spriteComponent, shader); - } - - if (visibilityComponent.VisibilityAcc < 3f) - visibilityComponent.VisibilityAcc += frameTime; + if (visibilityComponent.VisibilityAcc >= visibilityComponent.HideTime) + continue; + + visibilityComponent.VisibilityAcc = MathF.Min(visibilityComponent.VisibilityAcc + delta, visibilityComponent.HideTime); } } - private static void UpdateVisibility(SpriteComponent spriteComponent, ShaderInstance shader) + internal bool CanDraw(in OverlayDrawArgs args) { - spriteComponent.Color = Color.White; - spriteComponent.GetScreenTexture = true; - spriteComponent.RaiseShaderEvent = true; + if (!IsActive) + return false; + + if (_playerManager.LocalEntity is not { } player) + return false; - spriteComponent.PostShader = shader; + if (!_eyeQuery.TryComp(player, out var eye)) + return false; + + return args.Viewport.Eye == eye.Eye; } - private static float GetVisibility(Entity ent) + internal void RestoreCachedBaseAlphas() + { + foreach (var (ent, baseAlpha) in CachedBaseAlphas) + { + if (!EntityManager.EntityExists(ent)) + continue; + + _sprite.SetColor(ent.AsNullable(), ent.Comp.Color.WithAlpha(baseAlpha)); + } + + CachedBaseAlphas.Clear(); + } + + internal static float GetVisibility(Entity ent) { var acc = ent.Comp.VisibilityAcc; @@ -232,17 +275,34 @@ private static float GetVisibility(Entity ent) return Math.Clamp(1f - (acc / ent.Comp.HideTime), 0f, 1f); } - private static void BeforeRender(Entity ent, ref BeforePostShaderRenderEvent args) + private void AddOverlays() { - var visibility = GetVisibility(ent); - args.Sprite.PostShader?.SetParameter("visibility", visibility); + if (_overlaysPresented) + return; + + _overlayManager.AddOverlay(_setAlphaOverlay); + _overlayManager.AddOverlay(_resetAlphaOverlay); + + _overlaysPresented = true; + } + + private void RemoveOverlays() + { + if (!_overlaysPresented) + return; + + _overlayManager.RemoveOverlay(_setAlphaOverlay); + _overlayManager.RemoveOverlay(_resetAlphaOverlay); + + CachedBaseAlphas.Clear(); + _overlaysPresented = false; } // TODO: Переделать под статус эффект и добавить его в панель статус эффектов, а то непонятно игруну /// /// Если вдруг собачка плохо видит /// - private bool ModifyAcc(Scp939VisibilityComponent visibilityComponent, [NotNullWhen(true)] out int modifier) + private bool ModifyAcc(ActiveScp939VisibilityComponent visibilityComponent, out int modifier) { // 1 = отсутствие модификатора modifier = 1; diff --git a/Content.Client/_Scp/Scp939/Scp939ResetAlphaOverlay.cs b/Content.Client/_Scp/Scp939/Scp939ResetAlphaOverlay.cs new file mode 100644 index 00000000000..2eb83579c89 --- /dev/null +++ b/Content.Client/_Scp/Scp939/Scp939ResetAlphaOverlay.cs @@ -0,0 +1,30 @@ +using Robust.Client.Graphics; +using Robust.Shared.Enums; + +namespace Content.Client._Scp.Scp939; + +public sealed class Scp939ResetAlphaOverlay : Overlay +{ + [Dependency] private readonly IEntityManager _ent = default!; + + private readonly Scp939HudSystem _hud; + + public override OverlaySpace Space => OverlaySpace.WorldSpace; + + public Scp939ResetAlphaOverlay() + { + IoCManager.InjectDependencies(this); + + _hud = _ent.System(); + } + + protected override bool BeforeDraw(in OverlayDrawArgs args) + { + return _hud.CachedBaseAlphas.Count > 0 && _hud.CanDraw(in args); + } + + protected override void Draw(in OverlayDrawArgs args) + { + _hud.RestoreCachedBaseAlphas(); + } +} diff --git a/Content.Client/_Scp/Scp939/Scp939SetAlphaOverlay.cs b/Content.Client/_Scp/Scp939/Scp939SetAlphaOverlay.cs new file mode 100644 index 00000000000..d081f9afa4b --- /dev/null +++ b/Content.Client/_Scp/Scp939/Scp939SetAlphaOverlay.cs @@ -0,0 +1,55 @@ +using Content.Shared._Scp.Scp939; +using Robust.Client.GameObjects; +using Robust.Client.Graphics; +using Robust.Shared.Enums; + +namespace Content.Client._Scp.Scp939; + +public sealed class Scp939SetAlphaOverlay : Overlay +{ + [Dependency] private readonly IEntityManager _ent = default!; + + private readonly Scp939HudSystem _hud; + private readonly TransformSystem _transform; + private readonly SpriteSystem _sprite; + + public override OverlaySpace Space => OverlaySpace.WorldSpaceBelowEntities; + + public Scp939SetAlphaOverlay() + { + IoCManager.InjectDependencies(this); + + _hud = _ent.System(); + _transform = _ent.System(); + _sprite = _ent.System(); + } + + protected override bool BeforeDraw(in OverlayDrawArgs args) + { + return _hud.CanDraw(in args); + } + + protected override void Draw(in OverlayDrawArgs args) + { + _hud.CachedBaseAlphas.Clear(); + + var query = _ent.EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var visibility, out var sprite, out var xform)) + { + if (xform.MapID != args.MapId || !sprite.Visible) + continue; + + var worldPosition = _transform.GetWorldPosition(xform); + if (!args.WorldBounds.Contains(worldPosition)) + continue; + + var targetAlpha = Scp939HudSystem.GetVisibility((uid, visibility)); + if (MathF.Abs(sprite.Color.A - targetAlpha) <= 0.01f) + continue; + + var entity = (uid, sprite); + _hud.CachedBaseAlphas.Add((entity, sprite.Color.A)); + _sprite.SetColor(entity, sprite.Color.WithAlpha(targetAlpha)); + } + } +} diff --git a/Content.Server/_Scp/Scp939/Scp939System.Visibility.cs b/Content.Server/_Scp/Scp939/Scp939System.Visibility.cs index b2421d6bb4c..e6cc09d097d 100644 --- a/Content.Server/_Scp/Scp939/Scp939System.Visibility.cs +++ b/Content.Server/_Scp/Scp939/Scp939System.Visibility.cs @@ -1,4 +1,3 @@ -using Content.Server.Chat.Systems; using Content.Server.Popups; using Content.Shared._Scp.Scp939; using Content.Shared.Chat; @@ -8,6 +7,7 @@ using Content.Shared.Popups; using Content.Shared.Standing; using Content.Shared.Weapons.Ranged.Systems; +using Robust.Shared.Map; using Robust.Shared.Timing; namespace Content.Server._Scp.Scp939; @@ -17,16 +17,27 @@ public sealed partial class Scp939System [Dependency] private readonly PopupSystem _popup = default!; [Dependency] private readonly IGameTiming _timing = default!; + private static readonly TimeSpan VisibilityRefreshInterval = TimeSpan.FromSeconds(0.25f); + + private TimeSpan _nextVisibilityRefresh = TimeSpan.Zero; + private readonly HashSet _visibilityActiveTargets = []; + private readonly HashSet> _visibilityCandidates = []; + private readonly List _visibilityRemovalQueue = []; + + private EntityQuery _activeQuery; + private void InitializeVisibility() { SubscribeLocalEvent(OnMobStartup); - SubscribeLocalEvent(OnTargetSpoke); - SubscribeLocalEvent(OnTargetEmote); - SubscribeLocalEvent(OnDown); + SubscribeLocalEvent(OnTargetSpoke); + SubscribeLocalEvent(OnTargetEmote); + SubscribeLocalEvent(OnDown); SubscribeLocalEvent(OnShot); SubscribeLocalEvent(OnFlash); + + _activeQuery = GetEntityQuery(); } private void OnFlash(Entity ent, ref AfterFlashedEvent args) @@ -40,22 +51,22 @@ private void OnFlash(Entity ent, ref AfterFlashedEvent args) Dirty(ent); } - private void OnTargetEmote(Entity ent, ref EmoteEvent args) + private void OnTargetEmote(Entity ent, ref EmoteEvent args) { MobDidSomething(ent); } - private void OnDown(Entity ent, ref DownedEvent args) + private void OnDown(Entity ent, ref DownedEvent args) { MobDidSomething(ent); } private void OnShot(Entity ent, ref GunShotEvent args) { - if (!TryComp(args.User, out var scp939VisibilityComponent)) + if (!_activeQuery.TryComp(args.User, out var visibilityComponent)) return; - MobDidSomething((args.User, scp939VisibilityComponent)); + MobDidSomething((args.User, visibilityComponent)); } private void OnMobStartup(Entity ent, ref ComponentStartup args) @@ -63,21 +74,100 @@ private void OnMobStartup(Entity ent, ref ComponentStartup ar if (HasComp(ent)) return; - var visibilityComponent = EnsureComp(ent); - visibilityComponent.VisibilityAcc = 0.001f; - - Dirty(ent, visibilityComponent); + EnsureComp(ent); } - private void OnTargetSpoke(Entity ent, ref EntitySpokeEvent args) + private void OnTargetSpoke(Entity ent, ref EntitySpokeEvent args) { MobDidSomething(ent); } - // TODO: Перенести на клиент? - private void MobDidSomething(Entity ent) + private void MobDidSomething(Entity ent) { - ent.Comp.VisibilityAcc = 0.001f; + ent.Comp.VisibilityAcc = Scp939VisibilityComponent.InitialVisibilityAcc; Dirty(ent); } + + private void UpdateVisibilityTargets() + { + if (_timing.CurTime < _nextVisibilityRefresh) + return; + + _nextVisibilityRefresh = _timing.CurTime + VisibilityRefreshInterval; + _visibilityActiveTargets.Clear(); + + var scpQuery = EntityQueryEnumerator(); + while (scpQuery.MoveNext(out var uid, out var scp939, out var xform)) + { + if (xform.MapID == MapId.Nullspace) + continue; + + _visibilityCandidates.Clear(); + _entityLookup.GetEntitiesInRange(xform.Coordinates, + scp939.VisibilityActivationRange, + _visibilityCandidates, + LookupFlags.Dynamic | LookupFlags.Approximate); + + foreach (var target in _visibilityCandidates) + { + if (target.Owner == uid) + continue; + + _visibilityActiveTargets.Add(target); + EnsureActiveVisibility(target); + } + } + + _visibilityRemovalQueue.Clear(); + + var activeQuery = EntityQueryEnumerator(); + while (activeQuery.MoveNext(out var uid, out _)) + { + if (_visibilityActiveTargets.Contains(uid)) + continue; + + _visibilityRemovalQueue.Add(uid); + } + + foreach (var uid in _visibilityRemovalQueue) + { + RemComp(uid); + } + + _visibilityCandidates.Clear(); + _visibilityRemovalQueue.Clear(); + } + + private void EnsureActiveVisibility(Entity ent) + { + var dirty = false; + + if (!_activeQuery.TryComp(ent, out var active)) + { + active = AddComp(ent); + active.VisibilityAcc = Scp939VisibilityComponent.InitialVisibilityAcc; + dirty = true; + } + + if (!MathHelper.CloseTo(active.HideTime, ent.Comp.HideTime)) + { + active.HideTime = ent.Comp.HideTime; + dirty = true; + } + + if (active.MinValue != ent.Comp.MinValue) + { + active.MinValue = ent.Comp.MinValue; + dirty = true; + } + + if (active.MaxValue != ent.Comp.MaxValue) + { + active.MaxValue = ent.Comp.MaxValue; + dirty = true; + } + + if (dirty) + Dirty(ent, active); + } } diff --git a/Content.Server/_Scp/Scp939/Scp939System.cs b/Content.Server/_Scp/Scp939/Scp939System.cs index 596bc383283..5506db3310d 100644 --- a/Content.Server/_Scp/Scp939/Scp939System.cs +++ b/Content.Server/_Scp/Scp939/Scp939System.cs @@ -66,6 +66,8 @@ public override void Update(float frameTime) { base.Update(frameTime); + UpdateVisibilityTargets(); + // Все 939, что спят var querySleeping = EntityQueryEnumerator(); diff --git a/Content.Shared/_Scp/Scp939/ActiveScp939VisibilityComponent.cs b/Content.Shared/_Scp/Scp939/ActiveScp939VisibilityComponent.cs new file mode 100644 index 00000000000..e1547a20eab --- /dev/null +++ b/Content.Shared/_Scp/Scp939/ActiveScp939VisibilityComponent.cs @@ -0,0 +1,19 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared._Scp.Scp939; + +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +public sealed partial class ActiveScp939VisibilityComponent : Component +{ + [DataField, AutoNetworkedField] + public float VisibilityAcc = Scp939VisibilityComponent.InitialVisibilityAcc; + + [DataField, AutoNetworkedField] + public float HideTime = Scp939VisibilityComponent.DefaultHideTime; + + [DataField, AutoNetworkedField] + public int MinValue = Scp939VisibilityComponent.DefaultMinValue; + + [DataField, AutoNetworkedField] + public int MaxValue = Scp939VisibilityComponent.DefaultMaxValue; +} diff --git a/Content.Shared/_Scp/Scp939/Scp939Component.cs b/Content.Shared/_Scp/Scp939/Scp939Component.cs index 9f57ec9ec0e..6848f025d8f 100644 --- a/Content.Shared/_Scp/Scp939/Scp939Component.cs +++ b/Content.Shared/_Scp/Scp939/Scp939Component.cs @@ -63,6 +63,9 @@ public sealed partial class Scp939Component : Component [AutoNetworkedField] public TimeSpan? PoorEyesightTimeStart; // Когда начали плохо видеть + [DataField, ViewVariables(VVAccess.ReadWrite)] + public float VisibilityActivationRange = 20f; + #endregion [DataField] diff --git a/Content.Shared/_Scp/Scp939/Scp939VisabilityComponent.cs b/Content.Shared/_Scp/Scp939/Scp939VisabilityComponent.cs index d75ad4f7b5d..94a85140e37 100644 --- a/Content.Shared/_Scp/Scp939/Scp939VisabilityComponent.cs +++ b/Content.Shared/_Scp/Scp939/Scp939VisabilityComponent.cs @@ -1,22 +1,19 @@ -using Robust.Shared.GameStates; - namespace Content.Shared._Scp.Scp939; -[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +[RegisterComponent] public sealed partial class Scp939VisibilityComponent : Component { - [ViewVariables(VVAccess.ReadWrite), AutoNetworkedField] - public float VisibilityAcc; - - public readonly float HideTime = 2.5f; - - #region PoorEyesight + public const float InitialVisibilityAcc = 0.001f; + public const float DefaultHideTime = 2.5f; + public const int DefaultMinValue = 40; + public const int DefaultMaxValue = 400; - [DataField, ViewVariables(VVAccess.ReadWrite)] - public int MinValue = 40; + [DataField] + public float HideTime = DefaultHideTime; - [DataField, ViewVariables(VVAccess.ReadWrite)] - public int MaxValue = 400; + [DataField] + public int MinValue = DefaultMinValue; - #endregion + [DataField] + public int MaxValue = DefaultMaxValue; } From 2f066f98ba3f381003710155a0fdd46d63e80313 Mon Sep 17 00:00:00 2001 From: drdth Date: Sat, 21 Mar 2026 00:10:31 +0300 Subject: [PATCH 14/17] refactor: a bit more optimized 939 --- .../_Scp/Scp939/Scp939System.Actions.cs | 17 +++++------ .../_Scp/Scp939/Scp939System.Visibility.cs | 28 +++++++++++-------- Content.Server/_Scp/Scp939/Scp939System.cs | 12 ++++---- .../Scp939/ActiveScp939VisibilityComponent.cs | 2 +- Content.Shared/_Scp/Scp939/Scp939Component.cs | 14 +++++----- 5 files changed, 39 insertions(+), 34 deletions(-) diff --git a/Content.Server/_Scp/Scp939/Scp939System.Actions.cs b/Content.Server/_Scp/Scp939/Scp939System.Actions.cs index f84644780a6..fd97893262d 100644 --- a/Content.Server/_Scp/Scp939/Scp939System.Actions.cs +++ b/Content.Server/_Scp/Scp939/Scp939System.Actions.cs @@ -1,5 +1,6 @@ using Content.Server.Chat.Systems; using Content.Server.Examine; +using Content.Shared._Scp.Helpers; using Content.Shared._Scp.Scp939; using Content.Shared._Scp.ScpMask; using Content.Shared._Sunrise.TTS; @@ -7,7 +8,6 @@ using Content.Shared.Chat; using Content.Shared.Coordinates.Helpers; using Content.Shared.IdentityManagement; -using Content.Shared.Mobs.Components; using Robust.Shared.Prototypes; using Robust.Shared.Random; @@ -28,8 +28,6 @@ private void InitializeActions() SubscribeLocalEvent(OnSleepAction); SubscribeLocalEvent(OnGasAction); SubscribeLocalEvent(OnMimic); - - SubscribeLocalEvent(OnEntitySpoke); } private void OnSleepAction(Entity ent, ref Scp939SleepAction args) @@ -93,15 +91,19 @@ private void OnMimic(Entity ent, ref Scp939MimicActionEvent arg /// /// Запоминание последних сказанных возле 939 слов /// - private void OnEntitySpoke(Entity ent, ref EntitySpokeEvent args) + private void TryRememberPhrase(Entity ent, string message) { - var query = _entityLookup.GetEntitiesInRange(Transform(ent).Coordinates, 16f); + using var scp939Set = HashSetPoolEntity.Rent(); + _entityLookup.GetEntitiesInRange(Transform(ent).Coordinates, 16f, scp939Set.Value, LookupFlags.Dynamic | LookupFlags.Approximate); + if (scp939Set.Value.Count == 0) + return; + string? voicePrototype = null; if (TryComp(ent, out var ttsComponent)) voicePrototype = ttsComponent.VoicePrototypeId; - foreach (var scp in query) + foreach (var scp in scp939Set.Value) { if (!_examine.InRangeUnOccluded(ent, scp)) continue; @@ -113,8 +115,7 @@ private void OnEntitySpoke(Entity ent, ref EntitySpokeEvent a } var username = Identity.Name(ent, EntityManager); - scp.Comp.RememberedMessages.TryAdd(args.Message, new(username, voicePrototype)); - Dirty(scp); + scp.Comp.RememberedMessages.TryAdd(message, new(username, voicePrototype)); } } } diff --git a/Content.Server/_Scp/Scp939/Scp939System.Visibility.cs b/Content.Server/_Scp/Scp939/Scp939System.Visibility.cs index e6cc09d097d..b8e62c1f0d0 100644 --- a/Content.Server/_Scp/Scp939/Scp939System.Visibility.cs +++ b/Content.Server/_Scp/Scp939/Scp939System.Visibility.cs @@ -48,7 +48,11 @@ private void OnFlash(Entity ent, ref AfterFlashedEvent args) var message = Loc.GetString("scp939-flashed", ("time", ent.Comp.PoorEyesightTime)); _popup.PopupEntity(message, ent, ent, PopupType.MediumCaution); - Dirty(ent); + DirtyFields(ent, + ent.Comp, + null, + nameof(Scp939Component.PoorEyesight), + nameof(Scp939Component.PoorEyesightTimeStart)); } private void OnTargetEmote(Entity ent, ref EmoteEvent args) @@ -80,12 +84,16 @@ private void OnMobStartup(Entity ent, ref ComponentStartup ar private void OnTargetSpoke(Entity ent, ref EntitySpokeEvent args) { MobDidSomething(ent); + TryRememberPhrase(ent, args.Message); } private void MobDidSomething(Entity ent) { + if (MathHelper.CloseTo(ent.Comp.VisibilityAcc, Scp939VisibilityComponent.InitialVisibilityAcc)) + return; + ent.Comp.VisibilityAcc = Scp939VisibilityComponent.InitialVisibilityAcc; - Dirty(ent); + DirtyField(ent, ent.Comp, nameof(ActiveScp939VisibilityComponent.VisibilityAcc)); } private void UpdateVisibilityTargets() @@ -140,34 +148,32 @@ private void UpdateVisibilityTargets() private void EnsureActiveVisibility(Entity ent) { - var dirty = false; - if (!_activeQuery.TryComp(ent, out var active)) { active = AddComp(ent); active.VisibilityAcc = Scp939VisibilityComponent.InitialVisibilityAcc; - dirty = true; + active.HideTime = ent.Comp.HideTime; + active.MinValue = ent.Comp.MinValue; + active.MaxValue = ent.Comp.MaxValue; + return; } if (!MathHelper.CloseTo(active.HideTime, ent.Comp.HideTime)) { active.HideTime = ent.Comp.HideTime; - dirty = true; + DirtyField(ent, active, nameof(ActiveScp939VisibilityComponent.HideTime)); } if (active.MinValue != ent.Comp.MinValue) { active.MinValue = ent.Comp.MinValue; - dirty = true; + DirtyField(ent, active, nameof(ActiveScp939VisibilityComponent.MinValue)); } if (active.MaxValue != ent.Comp.MaxValue) { active.MaxValue = ent.Comp.MaxValue; - dirty = true; + DirtyField(ent, active, nameof(ActiveScp939VisibilityComponent.MaxValue)); } - - if (dirty) - Dirty(ent, active); } } diff --git a/Content.Server/_Scp/Scp939/Scp939System.cs b/Content.Server/_Scp/Scp939/Scp939System.cs index 5506db3310d..f671db25756 100644 --- a/Content.Server/_Scp/Scp939/Scp939System.cs +++ b/Content.Server/_Scp/Scp939/Scp939System.cs @@ -68,19 +68,13 @@ public override void Update(float frameTime) UpdateVisibilityTargets(); - // Все 939, что спят var querySleeping = EntityQueryEnumerator(); - - // Обработка лечения 939 во сне while (querySleeping.MoveNext(out var uid, out var scp939Component, out _)) { _damageableSystem.TryChangeDamage(uid, scp939Component.HibernationHealingRate * frameTime); } - // Просто все 939 var querySimple = EntityQueryEnumerator(); - - // Обработка плохого зрения 939 while (querySimple.MoveNext(out var uid, out var scp939Component)) { if (!scp939Component.PoorEyesight) @@ -96,7 +90,11 @@ public override void Update(float frameTime) scp939Component.PoorEyesight = false; scp939Component.PoorEyesightTimeStart = null; - Dirty(uid, scp939Component); + DirtyFields(uid, + scp939Component, + null, + nameof(Scp939Component.PoorEyesight), + nameof(Scp939Component.PoorEyesightTimeStart)); } } } diff --git a/Content.Shared/_Scp/Scp939/ActiveScp939VisibilityComponent.cs b/Content.Shared/_Scp/Scp939/ActiveScp939VisibilityComponent.cs index e1547a20eab..ea67df506ec 100644 --- a/Content.Shared/_Scp/Scp939/ActiveScp939VisibilityComponent.cs +++ b/Content.Shared/_Scp/Scp939/ActiveScp939VisibilityComponent.cs @@ -2,7 +2,7 @@ namespace Content.Shared._Scp.Scp939; -[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(fieldDeltas: true)] public sealed partial class ActiveScp939VisibilityComponent : Component { [DataField, AutoNetworkedField] diff --git a/Content.Shared/_Scp/Scp939/Scp939Component.cs b/Content.Shared/_Scp/Scp939/Scp939Component.cs index 6848f025d8f..3b6052b1f0b 100644 --- a/Content.Shared/_Scp/Scp939/Scp939Component.cs +++ b/Content.Shared/_Scp/Scp939/Scp939Component.cs @@ -5,16 +5,16 @@ namespace Content.Shared._Scp.Scp939; -[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(fieldDeltas: true)] public sealed partial class Scp939Component : Component { - [DataField, ViewVariables(VVAccess.ReadWrite)] + [DataField] public Solution SmokeSolution = new("АМН-С227", 40); - [DataField, ViewVariables(VVAccess.ReadWrite)] + [DataField] public float SmokeDuration = 30.0f; - [DataField, ViewVariables(VVAccess.ReadWrite)] + [DataField] public int SmokeSpreadRadius = 10; [DataField] @@ -28,10 +28,10 @@ public sealed partial class Scp939Component : Component "Scp939Sleep", }; - [DataField, ViewVariables(VVAccess.ReadWrite)] - public float HibernationDuration = 60.0f; + [DataField] + public float HibernationDuration = 60f; - [DataField, ViewVariables(VVAccess.ReadWrite)] + [DataField] public DamageSpecifier HibernationHealingRate = new() { DamageDict = new() From 3753814478892c383ebdcf59a91e7409179cb9d5 Mon Sep 17 00:00:00 2001 From: drdth Date: Sat, 21 Mar 2026 16:09:40 +0300 Subject: [PATCH 15/17] fix: ai review --- Content.Client/_Scp/Scp939/Scp939HudSystem.cs | 13 +++++--- .../_Scp/Scp939/Scp939System.Visibility.cs | 2 +- .../Traits/FearFaintingComponent.cs | 2 +- .../Fear/Systems/SharedFearSystem.Fears.cs | 1 + .../Fear/Systems/SharedFearSystem.Gameplay.cs | 5 ++- Content.Shared/_Scp/Helpers/CollectionPool.cs | 2 +- .../_Scp/Scp173/SharedScp173System.cs | 3 +- .../Watching/EyeWatchingSystem.API.Base.cs | 33 +++++++++++-------- .../_Scp/Watching/EyeWatchingSystem.cs | 10 ++++++ 9 files changed, 49 insertions(+), 22 deletions(-) diff --git a/Content.Client/_Scp/Scp939/Scp939HudSystem.cs b/Content.Client/_Scp/Scp939/Scp939HudSystem.cs index 3c285fd5408..e1409ddb742 100644 --- a/Content.Client/_Scp/Scp939/Scp939HudSystem.cs +++ b/Content.Client/_Scp/Scp939/Scp939HudSystem.cs @@ -36,6 +36,9 @@ public sealed class Scp939HudSystem : EquipmentHudSystem private Scp939Component? _scp939Component; private EntityQuery _eyeQuery; + private EntityQuery _scp939ProtectionQuery; + private EntityQuery _movementSpeedQuery; + private EntityQuery _physicsQuery; private bool _overlaysPresented; private float _lastUpdateTime; @@ -68,6 +71,9 @@ public override void Initialize() SubscribeLocalEvent(OnPlayerDetached); _eyeQuery = GetEntityQuery(); + _scp939ProtectionQuery = GetEntityQuery(); + _movementSpeedQuery = GetEntityQuery(); + _physicsQuery = GetEntityQuery(); _setAlphaOverlay = new(); _resetAlphaOverlay = new(); @@ -167,7 +173,6 @@ private void OnMeleeAttack(Entity ent, ref Mele private void MobDidSomething(Entity ent) { ent.Comp.VisibilityAcc = Scp939VisibilityComponent.InitialVisibilityAcc; - Dirty(ent); } private void OnMove(Entity ent, ref MoveEvent args) @@ -180,7 +185,7 @@ private void OnMove(Entity ent, ref MoveEvent a { ent.Comp.VisibilityAcc *= modifier; } - else if (HasComp(ent)) // Если имеется защита(тихое хождение) + else if (_scp939ProtectionQuery.HasComp(ent)) // Если имеется защита(тихое хождение) { return; } @@ -189,8 +194,8 @@ private void OnMove(Entity ent, ref MoveEvent a ent.Comp.VisibilityAcc = 0; } - if (!TryComp(ent, out var speedModifierComponent) - || !TryComp(ent, out var physicsComponent)) + if (!_movementSpeedQuery.TryComp(ent, out var speedModifierComponent) + || !_physicsQuery.TryComp(ent, out var physicsComponent)) { return; } diff --git a/Content.Server/_Scp/Scp939/Scp939System.Visibility.cs b/Content.Server/_Scp/Scp939/Scp939System.Visibility.cs index b8e62c1f0d0..f99461c46eb 100644 --- a/Content.Server/_Scp/Scp939/Scp939System.Visibility.cs +++ b/Content.Server/_Scp/Scp939/Scp939System.Visibility.cs @@ -17,7 +17,7 @@ public sealed partial class Scp939System [Dependency] private readonly PopupSystem _popup = default!; [Dependency] private readonly IGameTiming _timing = default!; - private static readonly TimeSpan VisibilityRefreshInterval = TimeSpan.FromSeconds(0.25f); + private static readonly TimeSpan VisibilityRefreshInterval = TimeSpan.FromSeconds(0.2f); private TimeSpan _nextVisibilityRefresh = TimeSpan.Zero; private readonly HashSet _visibilityActiveTargets = []; diff --git a/Content.Shared/_Scp/Fear/Components/Traits/FearFaintingComponent.cs b/Content.Shared/_Scp/Fear/Components/Traits/FearFaintingComponent.cs index fa9b28cc4a3..afa361dd4ed 100644 --- a/Content.Shared/_Scp/Fear/Components/Traits/FearFaintingComponent.cs +++ b/Content.Shared/_Scp/Fear/Components/Traits/FearFaintingComponent.cs @@ -28,5 +28,5 @@ public sealed partial class FearFaintingComponent : Component public float Chance = 0.3f; [DataField] - public EntProtoId StatusEffect = "StatusEffectForcedSleeping"; + public EntProtoId StatusEffect = "StatusEffectForcedSleeping"; } diff --git a/Content.Shared/_Scp/Fear/Systems/SharedFearSystem.Fears.cs b/Content.Shared/_Scp/Fear/Systems/SharedFearSystem.Fears.cs index 1a339019b37..39ed91ba066 100644 --- a/Content.Shared/_Scp/Fear/Systems/SharedFearSystem.Fears.cs +++ b/Content.Shared/_Scp/Fear/Systems/SharedFearSystem.Fears.cs @@ -25,6 +25,7 @@ private void OnHemophobiaInit(Entity ent, ref ComponentStar if (!_fearQuery.TryComp(ent, out var fearComponent)) { Log.Warning($"Found entity {ToPrettyString(ent)} with {nameof(HemophobiaComponent)} but without {nameof(FearComponent)}! {nameof(HemophobiaComponent)} will be deleted"); + RemComp(ent); return; } diff --git a/Content.Shared/_Scp/Fear/Systems/SharedFearSystem.Gameplay.cs b/Content.Shared/_Scp/Fear/Systems/SharedFearSystem.Gameplay.cs index 00d5a5c3058..199314d82ee 100644 --- a/Content.Shared/_Scp/Fear/Systems/SharedFearSystem.Gameplay.cs +++ b/Content.Shared/_Scp/Fear/Systems/SharedFearSystem.Gameplay.cs @@ -184,8 +184,11 @@ protected virtual void TryScream(Entity ent) {} private void ManageFallOff(Entity ent) { - if (ent.Comp.State >= ent.Comp.FallOffRequiredState && !HasComp(ent)) + if (ent.Comp.State >= ent.Comp.FallOffRequiredState) { + if (HasComp(ent)) + return; + var comp = EnsureComp(ent); comp.FallOffChance = ent.Comp.FallOffChance; Dirty(ent, comp); diff --git a/Content.Shared/_Scp/Helpers/CollectionPool.cs b/Content.Shared/_Scp/Helpers/CollectionPool.cs index d75eec96bcf..1a073bf58d7 100644 --- a/Content.Shared/_Scp/Helpers/CollectionPool.cs +++ b/Content.Shared/_Scp/Helpers/CollectionPool.cs @@ -53,7 +53,7 @@ public static PooledCollection Rent() /// /// Returns a collection to the pool. Clears the collection before storing it. - /// Collections will be dropped and garbage collected if the pool is full (>= 512 items) or if the collection capacity exceeds 1024. + /// Collections will be dropped and garbage collected if the pool is full (>= 512 items) or if the collection capacity exceeds 2048. /// /// The collection to return to the pool. internal static void Return(TCollection collection) diff --git a/Content.Shared/_Scp/Scp173/SharedScp173System.cs b/Content.Shared/_Scp/Scp173/SharedScp173System.cs index b208125f186..cb0275be7a2 100644 --- a/Content.Shared/_Scp/Scp173/SharedScp173System.cs +++ b/Content.Shared/_Scp/Scp173/SharedScp173System.cs @@ -187,7 +187,8 @@ public bool IsContained(EntityUid uid) { return Watching.TryGetAnyEntitiesVisibleTo(uid, LineOfSightBlockerLevel.None, - LookupFlags.Sensors | LookupFlags.Sundries); + LookupFlags.Sensors | LookupFlags.Sundries, + ContainmentRoomSearchRadius); } private bool CanBlind(EntityUid uid, bool showPopups = true) diff --git a/Content.Shared/_Scp/Watching/EyeWatchingSystem.API.Base.cs b/Content.Shared/_Scp/Watching/EyeWatchingSystem.API.Base.cs index e2731ab1df4..f84b83170bf 100644 --- a/Content.Shared/_Scp/Watching/EyeWatchingSystem.API.Base.cs +++ b/Content.Shared/_Scp/Watching/EyeWatchingSystem.API.Base.cs @@ -41,17 +41,19 @@ private void InitializeApi() /// Список всех, кто находится в радиусе видимости /// Требуемая прозрачность линии видимости. /// Список флагов для поиска целей в + /// Если нужно использовать другой радиус поиска, отличный от /// Компонент, который должны иметь все сущности в радиусе видимости /// Удалось ли найти хоть кого-то public bool TryGetAllEntitiesVisibleTo( Entity ent, List> potentialWatchers, LineOfSightBlockerLevel type = LineOfSightBlockerLevel.Transparent, - LookupFlags flags = LookupFlags.Uncontained | LookupFlags.Approximate) + LookupFlags flags = LookupFlags.Uncontained | LookupFlags.Approximate, + float? rangeOverride = null) where T : IComponent { using var searchSet = HashSetPoolEntity.Rent(); - return TryGetAllEntitiesVisibleTo(ent, potentialWatchers, searchSet.Value, type, flags); + return TryGetAllEntitiesVisibleTo(ent, potentialWatchers, searchSet.Value, type, flags, rangeOverride); } /// @@ -68,6 +70,7 @@ public bool TryGetAllEntitiesVisibleTo( /// Заранее заготовленный список, который будет использоваться в /// Требуемая прозрачность линии видимости. /// Список флагов для поиска целей в + /// Если нужно использовать другой радиус поиска, отличный от /// Компонент, который должны иметь все сущности в радиусе видимости /// Удалось ли найти хоть кого-то private bool TryGetAllEntitiesVisibleTo( @@ -75,14 +78,15 @@ private bool TryGetAllEntitiesVisibleTo( List> potentialWatchers, HashSet> searchSet, LineOfSightBlockerLevel type = LineOfSightBlockerLevel.Transparent, - LookupFlags flags = LookupFlags.Uncontained | LookupFlags.Approximate) + LookupFlags flags = LookupFlags.Uncontained | LookupFlags.Approximate, + float? rangeOverride = null) where T : IComponent { if (!Resolve(ent.Owner, ref ent.Comp)) return false; searchSet.Clear(); - _lookup.GetEntitiesInRange(ent.Comp.Coordinates, SeeRange, searchSet, flags); + _lookup.GetEntitiesInRange(ent.Comp.Coordinates, rangeOverride ?? SeeRange, searchSet, flags); foreach (var target in searchSet) { @@ -107,16 +111,18 @@ private bool TryGetAllEntitiesVisibleTo( /// Цель, для которой ищем сущности в радиусе видимости /// Требуемая прозрачность линии видимости. /// Список флагов для поиска целей в + /// Если нужно использовать другой радиус поиска, отличный от /// Компонент, который должны иметь все сущности в радиусе видимости /// Удалось ли найти хоть кого-то public bool TryGetAnyEntitiesVisibleTo( Entity viewer, LineOfSightBlockerLevel type = LineOfSightBlockerLevel.Transparent, - LookupFlags flags = LookupFlags.Uncontained | LookupFlags.Approximate) + LookupFlags flags = LookupFlags.Uncontained | LookupFlags.Approximate, + float? rangeOverride = null) where T : IComponent { using var searchSet = HashSetPoolEntity.Rent(); - if (!TryGetAnyEntitiesVisibleTo(viewer, out _, searchSet.Value, type, flags)) + if (!TryGetAnyEntitiesVisibleTo(viewer, out _, searchSet.Value, type, flags, rangeOverride)) return false; return true; @@ -135,19 +141,21 @@ public bool TryGetAnyEntitiesVisibleTo( /// Первая попавшаяся сущность в радиусе видимости цели /// Требуемая прозрачность линии видимости. /// Список флагов для поиска целей в + /// Если нужно использовать другой радиус поиска, отличный от /// Компонент, который должны иметь все сущности в радиусе видимости /// Удалось ли найти хоть кого-то public bool TryGetAnyEntitiesVisibleTo( Entity viewer, [NotNullWhen(true)] out Entity? firstVisible, LineOfSightBlockerLevel type = LineOfSightBlockerLevel.Transparent, - LookupFlags flags = LookupFlags.Uncontained | LookupFlags.Approximate) + LookupFlags flags = LookupFlags.Uncontained | LookupFlags.Approximate, + float? rangeOverride = null) where T : IComponent { firstVisible = null; using var searchSet = HashSetPoolEntity.Rent(); - if (!TryGetAnyEntitiesVisibleTo(viewer, out var first, searchSet.Value, type, flags)) + if (!TryGetAnyEntitiesVisibleTo(viewer, out var first, searchSet.Value, type, flags, rangeOverride)) return false; firstVisible = first; @@ -168,6 +176,7 @@ public bool TryGetAnyEntitiesVisibleTo( /// Заранее заготовленный список, который будет использоваться в /// Требуемая прозрачность линии видимости. /// Список флагов для поиска целей в + /// Если нужно использовать другой радиус поиска, отличный от /// Компонент, который должны иметь все сущности в радиусе видимости /// Удалось ли найти хоть кого-то private bool TryGetAnyEntitiesVisibleTo( @@ -175,7 +184,8 @@ private bool TryGetAnyEntitiesVisibleTo( [NotNullWhen(true)] out Entity? firstVisible, HashSet> searchSet, LineOfSightBlockerLevel type = LineOfSightBlockerLevel.Transparent, - LookupFlags flags = LookupFlags.Uncontained | LookupFlags.Approximate) + LookupFlags flags = LookupFlags.Uncontained | LookupFlags.Approximate, + float? rangeOverride = null) where T : IComponent { firstVisible = null; @@ -184,7 +194,7 @@ private bool TryGetAnyEntitiesVisibleTo( return false; searchSet.Clear(); - _lookup.GetEntitiesInRange(viewer.Comp.Coordinates, SeeRange, searchSet, flags); + _lookup.GetEntitiesInRange(viewer.Comp.Coordinates, rangeOverride ?? SeeRange, searchSet, flags); foreach (var target in searchSet) { @@ -239,9 +249,6 @@ public bool CanSee(Entity viewer, if (checkBlinking && _blinking.IsBlind(viewer, useTimeCompensation)) return false; - if (!checkBlinking && _blinking.AreEyesClosedManually(viewer)) - return false; - var canSeeAttempt = new CanSeeAttemptEvent(); RaiseLocalEvent(viewer, canSeeAttempt); diff --git a/Content.Shared/_Scp/Watching/EyeWatchingSystem.cs b/Content.Shared/_Scp/Watching/EyeWatchingSystem.cs index 14ba83fe092..86eaa8291ac 100644 --- a/Content.Shared/_Scp/Watching/EyeWatchingSystem.cs +++ b/Content.Shared/_Scp/Watching/EyeWatchingSystem.cs @@ -50,7 +50,12 @@ public override void Update(float frameTime) // Все потенциально возможные смотрящие. Среди них те, что прошли фаст-чек из самых простых проверок using var potentialWatchers = ListPoolEntity.Rent(); if (!TryGetAllEntitiesVisibleTo(uid, potentialWatchers.Value)) + { + SetNextTime(watchingComponent); + Dirty(uid, watchingComponent); + continue; + } // Вызываем ивенты на потенциально смотрящих. Без особых проверок // Полезно в коде, который уже использует подобные проверки или не требует этого @@ -78,7 +83,12 @@ public override void Update(float frameTime) // Выдает полный список всех сущностей, кто действительно видит цель using var realWatchers = ListPoolEntity.Rent(); if (!TryGetWatchersFrom(uid, realWatchers.Value, potentialWatchers.Value, checkProximity: false)) + { + SetNextTime(watchingComponent); + Dirty(uid, watchingComponent); + continue; + } // Вызываем ивент на смотрящем, говорящие, что он действительно видит цель foreach (var viewer in realWatchers.Value) From ff35ec13400bef9c4ef75be93abbd71800096ade Mon Sep 17 00:00:00 2001 From: drdth Date: Sat, 21 Mar 2026 16:34:54 +0300 Subject: [PATCH 16/17] refactor: partialize scp939 hud class + replace PlayerAttached/Detached events with local player variants --- .../_Scp/Scp939/Scp939HudSystem.Overlay.cs | 50 +++++ .../_Scp/Scp939/Scp939HudSystem.Visibility.cs | 152 +++++++++++++++ Content.Client/_Scp/Scp939/Scp939HudSystem.cs | 184 +----------------- .../_Scp/Scp939/Scp939System.Visibility.cs | 6 - .../Scp939/ActiveScp939VisibilityComponent.cs | 2 + 5 files changed, 208 insertions(+), 186 deletions(-) create mode 100644 Content.Client/_Scp/Scp939/Scp939HudSystem.Overlay.cs create mode 100644 Content.Client/_Scp/Scp939/Scp939HudSystem.Visibility.cs diff --git a/Content.Client/_Scp/Scp939/Scp939HudSystem.Overlay.cs b/Content.Client/_Scp/Scp939/Scp939HudSystem.Overlay.cs new file mode 100644 index 00000000000..e14458dfc95 --- /dev/null +++ b/Content.Client/_Scp/Scp939/Scp939HudSystem.Overlay.cs @@ -0,0 +1,50 @@ +using Content.Shared._Scp.Scp939; +using Robust.Client.Graphics; +using Robust.Shared.Player; + +namespace Content.Client._Scp.Scp939; + +public sealed partial class Scp939HudSystem +{ + [Dependency] private readonly IOverlayManager _overlayManager = default!; + + private void InitializeOverlay() + { + SubscribeLocalEvent(OnPlayerAttached); + SubscribeLocalEvent(OnPlayerDetached); + } + + private void OnPlayerAttached(Entity ent, ref LocalPlayerAttachedEvent args) + { + _scp939Component = ent.Comp; + AddOverlays(); + } + + private void OnPlayerDetached(Entity ent, ref LocalPlayerDetachedEvent args) + { + _scp939Component = null; + } + + private void AddOverlays() + { + if (_overlaysPresented) + return; + + _overlayManager.AddOverlay(_setAlphaOverlay); + _overlayManager.AddOverlay(_resetAlphaOverlay); + + _overlaysPresented = true; + } + + private void RemoveOverlays() + { + if (!_overlaysPresented) + return; + + _overlayManager.RemoveOverlay(_setAlphaOverlay); + _overlayManager.RemoveOverlay(_resetAlphaOverlay); + + CachedBaseAlphas.Clear(); + _overlaysPresented = false; + } +} diff --git a/Content.Client/_Scp/Scp939/Scp939HudSystem.Visibility.cs b/Content.Client/_Scp/Scp939/Scp939HudSystem.Visibility.cs new file mode 100644 index 00000000000..f76edfc0d0e --- /dev/null +++ b/Content.Client/_Scp/Scp939/Scp939HudSystem.Visibility.cs @@ -0,0 +1,152 @@ +using Content.Shared._Scp.Scp939; +using Content.Shared.Standing; +using Content.Shared.Throwing; +using Content.Shared.Weapons.Melee.Events; +using Robust.Shared.Physics.Events; +using Robust.Shared.Random; + +namespace Content.Client._Scp.Scp939; + +public sealed partial class Scp939HudSystem +{ + [Dependency] private readonly IRobustRandom _random = default!; + + private void InitializeVisibility() + { + SubscribeLocalEvent((Entity ent, ref StartCollideEvent args) + => OnCollide(ent, args.OtherEntity)); + SubscribeLocalEvent((Entity ent, ref EndCollideEvent args) + => OnCollide(ent, args.OtherEntity)); + + SubscribeLocalEvent(OnMove); + + SubscribeLocalEvent(OnThrow); + SubscribeLocalEvent(OnStood); + SubscribeLocalEvent(OnMeleeAttack); + SubscribeLocalEvent(OnDown); + } + + public override void Update(float frameTime) + { + base.Update(frameTime); + + if (!IsActive) + return; + + _lastUpdateTime += frameTime; + if (_lastUpdateTime < UpdateInterval) + return; + + var delta = _lastUpdateTime; + _lastUpdateTime = 0f; + + var query = EntityQueryEnumerator(); + while (query.MoveNext(out _, out var visibilityComponent)) + { + if (visibilityComponent.VisibilityAccClient >= visibilityComponent.HideTime) + continue; + + visibilityComponent.VisibilityAccClient = MathF.Min(visibilityComponent.VisibilityAccClient + delta, visibilityComponent.HideTime); + } + } + + private void OnMove(Entity ent, ref MoveEvent args) + { + if (!IsActive) + return; + + // В зависимости от наличие защит или проблем со зрением у 939 изменяется то, насколько хорошо мы видим жертву + if (ModifyAcc(ent.Comp, out var modifier)) // Если зрение затруднено + { + ent.Comp.VisibilityAccClient *= modifier; + } + else if (_scp939ProtectionQuery.HasComp(ent)) // Если имеется защита(тихое хождение) + { + return; + } + else // Если со зрением все ок + { + ent.Comp.VisibilityAccClient = 0; + } + + if (!_movementSpeedQuery.TryComp(ent, out var speedModifierComponent) + || !_physicsQuery.TryComp(ent, out var physicsComponent)) + { + return; + } + + var currentVelocity = physicsComponent.LinearVelocity.Length(); + + if (speedModifierComponent.BaseWalkSpeed > currentVelocity) + ent.Comp.VisibilityAccClient = ent.Comp.HideTime / 2f; + } + + + private void OnCollide(Entity ent, EntityUid otherEntity) + { + if (!IsActive) + return; + + if (!HasComp(otherEntity)) + return; + + MobDidSomething(ent); + } + + private void OnThrow(Entity ent, ref ThrowEvent args) + { + if (!IsActive) + return; + + MobDidSomething(ent); + } + + private void OnStood(Entity ent, ref StoodEvent args) + { + if (!IsActive) + return; + + MobDidSomething(ent); + } + + private void OnMeleeAttack(Entity ent, ref MeleeAttackEvent args) + { + if (!IsActive) + return; + + MobDidSomething(ent); + } + + private void OnDown(Entity ent, ref DownedEvent args) + { + if (!IsActive) + return; + + MobDidSomething(ent); + } + + private void MobDidSomething(Entity ent) + { + ent.Comp.VisibilityAccClient = Scp939VisibilityComponent.InitialVisibilityAcc; + } + + // TODO: Переделать под статус эффект и добавить его в панель статус эффектов, а то непонятно игруну + /// + /// Если вдруг собачка плохо видит + /// + private bool ModifyAcc(ActiveScp939VisibilityComponent visibilityComponent, out int modifier) + { + // 1 = отсутствие модификатора + modifier = 1; + + if (_scp939Component == null) + return false; + + if (!_scp939Component.PoorEyesight) + return false; + + modifier = _random.Next(visibilityComponent.MinValue, visibilityComponent.MaxValue); + + return true; + } +} diff --git a/Content.Client/_Scp/Scp939/Scp939HudSystem.cs b/Content.Client/_Scp/Scp939/Scp939HudSystem.cs index e1409ddb742..adc04c4c2d5 100644 --- a/Content.Client/_Scp/Scp939/Scp939HudSystem.cs +++ b/Content.Client/_Scp/Scp939/Scp939HudSystem.cs @@ -6,24 +6,16 @@ using Content.Shared.Examine; using Content.Shared.Inventory.Events; using Content.Shared.Movement.Components; -using Content.Shared.Standing; using Content.Shared.StatusIcon.Components; -using Content.Shared.Throwing; -using Content.Shared.Weapons.Melee.Events; using Robust.Client.GameObjects; using Robust.Client.Graphics; using Robust.Client.Player; using Robust.Shared.Physics.Components; -using Robust.Shared.Physics.Events; -using Robust.Shared.Player; -using Robust.Shared.Random; namespace Content.Client._Scp.Scp939; -public sealed class Scp939HudSystem : EquipmentHudSystem +public sealed partial class Scp939HudSystem : EquipmentHudSystem { - [Dependency] private readonly IOverlayManager _overlayManager = default!; - [Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly IPlayerManager _playerManager = default!; [Dependency] private readonly SpriteSystem _sprite = default!; @@ -49,27 +41,12 @@ public override void Initialize() { base.Initialize(); - SubscribeLocalEvent((Entity ent, ref StartCollideEvent args) - => OnCollide(ent, args.OtherEntity)); - SubscribeLocalEvent((Entity ent, ref EndCollideEvent args) - => OnCollide(ent, args.OtherEntity)); - - #region Visibility - - SubscribeLocalEvent(OnMove); - - SubscribeLocalEvent(OnThrow); - SubscribeLocalEvent(OnStood); - SubscribeLocalEvent(OnMeleeAttack); - - #endregion + InitializeVisibility(); + InitializeOverlay(); SubscribeLocalEvent(OnGetStatusIcons, after: [typeof(SSDIndicatorSystem)] ); SubscribeLocalEvent(OnExamine); - SubscribeLocalEvent(OnPlayerAttached); - SubscribeLocalEvent(OnPlayerDetached); - _eyeQuery = GetEntityQuery(); _scp939ProtectionQuery = GetEntityQuery(); _movementSpeedQuery = GetEntityQuery(); @@ -133,116 +110,6 @@ protected override void DeactivateInternal() RemoveOverlays(); } - #region Visibility - - private void OnCollide(Entity ent, EntityUid otherEntity) - { - if (!IsActive) - return; - - if (!HasComp(otherEntity)) - return; - - MobDidSomething(ent); - } - - private void OnThrow(Entity ent, ref ThrowEvent args) - { - if (!IsActive) - return; - - MobDidSomething(ent); - } - - private void OnStood(Entity ent, ref StoodEvent args) - { - if (!IsActive) - return; - - MobDidSomething(ent); - } - - private void OnMeleeAttack(Entity ent, ref MeleeAttackEvent args) - { - if (!IsActive) - return; - - MobDidSomething(ent); - } - - private void MobDidSomething(Entity ent) - { - ent.Comp.VisibilityAcc = Scp939VisibilityComponent.InitialVisibilityAcc; - } - - private void OnMove(Entity ent, ref MoveEvent args) - { - if (!IsActive) - return; - - // В зависимости от наличие защит или проблем со зрением у 939 изменяется то, насколько хорошо мы видим жертву - if (ModifyAcc(ent.Comp, out var modifier)) // Если зрение затруднено - { - ent.Comp.VisibilityAcc *= modifier; - } - else if (_scp939ProtectionQuery.HasComp(ent)) // Если имеется защита(тихое хождение) - { - return; - } - else // Если со зрением все ок - { - ent.Comp.VisibilityAcc = 0; - } - - if (!_movementSpeedQuery.TryComp(ent, out var speedModifierComponent) - || !_physicsQuery.TryComp(ent, out var physicsComponent)) - { - return; - } - - var currentVelocity = physicsComponent.LinearVelocity.Length(); - - if (speedModifierComponent.BaseWalkSpeed > currentVelocity) - ent.Comp.VisibilityAcc = ent.Comp.HideTime / 2f; - } - - #endregion - - private void OnPlayerAttached(Entity ent, ref PlayerAttachedEvent args) - { - _scp939Component = ent.Comp; - AddOverlays(); - } - - private void OnPlayerDetached(Entity ent, ref PlayerDetachedEvent args) - { - _scp939Component = null; - } - - public override void Update(float frameTime) - { - base.Update(frameTime); - - if (!IsActive) - return; - - _lastUpdateTime += frameTime; - if (_lastUpdateTime < UpdateInterval) - return; - - var delta = _lastUpdateTime; - _lastUpdateTime = 0f; - - var query = EntityQueryEnumerator(); - while (query.MoveNext(out _, out var visibilityComponent)) - { - if (visibilityComponent.VisibilityAcc >= visibilityComponent.HideTime) - continue; - - visibilityComponent.VisibilityAcc = MathF.Min(visibilityComponent.VisibilityAcc + delta, visibilityComponent.HideTime); - } - } - internal bool CanDraw(in OverlayDrawArgs args) { if (!IsActive) @@ -272,54 +139,11 @@ internal void RestoreCachedBaseAlphas() internal static float GetVisibility(Entity ent) { - var acc = ent.Comp.VisibilityAcc; + var acc = ent.Comp.VisibilityAccClient + ent.Comp.VisibilityAcc; if (acc > ent.Comp.HideTime) return 0; return Math.Clamp(1f - (acc / ent.Comp.HideTime), 0f, 1f); } - - private void AddOverlays() - { - if (_overlaysPresented) - return; - - _overlayManager.AddOverlay(_setAlphaOverlay); - _overlayManager.AddOverlay(_resetAlphaOverlay); - - _overlaysPresented = true; - } - - private void RemoveOverlays() - { - if (!_overlaysPresented) - return; - - _overlayManager.RemoveOverlay(_setAlphaOverlay); - _overlayManager.RemoveOverlay(_resetAlphaOverlay); - - CachedBaseAlphas.Clear(); - _overlaysPresented = false; - } - - // TODO: Переделать под статус эффект и добавить его в панель статус эффектов, а то непонятно игруну - /// - /// Если вдруг собачка плохо видит - /// - private bool ModifyAcc(ActiveScp939VisibilityComponent visibilityComponent, out int modifier) - { - // 1 = отсутствие модификатора - modifier = 1; - - if (_scp939Component == null) - return false; - - if (!_scp939Component.PoorEyesight) - return false; - - modifier = _random.Next(visibilityComponent.MinValue, visibilityComponent.MaxValue); - - return true; - } } diff --git a/Content.Server/_Scp/Scp939/Scp939System.Visibility.cs b/Content.Server/_Scp/Scp939/Scp939System.Visibility.cs index f99461c46eb..8aa967c9b9e 100644 --- a/Content.Server/_Scp/Scp939/Scp939System.Visibility.cs +++ b/Content.Server/_Scp/Scp939/Scp939System.Visibility.cs @@ -32,7 +32,6 @@ private void InitializeVisibility() SubscribeLocalEvent(OnTargetSpoke); SubscribeLocalEvent(OnTargetEmote); - SubscribeLocalEvent(OnDown); SubscribeLocalEvent(OnShot); SubscribeLocalEvent(OnFlash); @@ -60,11 +59,6 @@ private void OnTargetEmote(Entity ent, ref Emot MobDidSomething(ent); } - private void OnDown(Entity ent, ref DownedEvent args) - { - MobDidSomething(ent); - } - private void OnShot(Entity ent, ref GunShotEvent args) { if (!_activeQuery.TryComp(args.User, out var visibilityComponent)) diff --git a/Content.Shared/_Scp/Scp939/ActiveScp939VisibilityComponent.cs b/Content.Shared/_Scp/Scp939/ActiveScp939VisibilityComponent.cs index ea67df506ec..9a5d959e1f4 100644 --- a/Content.Shared/_Scp/Scp939/ActiveScp939VisibilityComponent.cs +++ b/Content.Shared/_Scp/Scp939/ActiveScp939VisibilityComponent.cs @@ -5,6 +5,8 @@ namespace Content.Shared._Scp.Scp939; [RegisterComponent, NetworkedComponent, AutoGenerateComponentState(fieldDeltas: true)] public sealed partial class ActiveScp939VisibilityComponent : Component { + public float VisibilityAccClient = Scp939VisibilityComponent.InitialVisibilityAcc; + [DataField, AutoNetworkedField] public float VisibilityAcc = Scp939VisibilityComponent.InitialVisibilityAcc; From d9641205e89181877ee1f0af0f700c2815872eaf Mon Sep 17 00:00:00 2001 From: drdth Date: Sat, 21 Mar 2026 17:13:12 +0300 Subject: [PATCH 17/17] fix: better implementation --- .../_Scp/Scp939/Scp939HudSystem.Visibility.cs | 22 ++++++++++++++----- Content.Client/_Scp/Scp939/Scp939HudSystem.cs | 2 +- .../_Scp/Scp939/Scp939System.Visibility.cs | 9 ++------ .../Scp939/ActiveScp939VisibilityComponent.cs | 12 ++++++---- 4 files changed, 27 insertions(+), 18 deletions(-) diff --git a/Content.Client/_Scp/Scp939/Scp939HudSystem.Visibility.cs b/Content.Client/_Scp/Scp939/Scp939HudSystem.Visibility.cs index f76edfc0d0e..dc111bf2bfe 100644 --- a/Content.Client/_Scp/Scp939/Scp939HudSystem.Visibility.cs +++ b/Content.Client/_Scp/Scp939/Scp939HudSystem.Visibility.cs @@ -17,6 +17,7 @@ private void InitializeVisibility() => OnCollide(ent, args.OtherEntity)); SubscribeLocalEvent((Entity ent, ref EndCollideEvent args) => OnCollide(ent, args.OtherEntity)); + SubscribeLocalEvent(OnVisibilityStateUpdated); SubscribeLocalEvent(OnMove); @@ -26,6 +27,15 @@ private void InitializeVisibility() SubscribeLocalEvent(OnDown); } + private void OnVisibilityStateUpdated(Entity ent, ref AfterAutoHandleStateEvent args) + { + if (ent.Comp.LastHandledVisibilityResetCounter == ent.Comp.VisibilityResetCounter) + return; + + ent.Comp.LastHandledVisibilityResetCounter = ent.Comp.VisibilityResetCounter; + ent.Comp.VisibilityAcc = Scp939VisibilityComponent.InitialVisibilityAcc; + } + public override void Update(float frameTime) { base.Update(frameTime); @@ -43,10 +53,10 @@ public override void Update(float frameTime) var query = EntityQueryEnumerator(); while (query.MoveNext(out _, out var visibilityComponent)) { - if (visibilityComponent.VisibilityAccClient >= visibilityComponent.HideTime) + if (visibilityComponent.VisibilityAcc >= visibilityComponent.HideTime) continue; - visibilityComponent.VisibilityAccClient = MathF.Min(visibilityComponent.VisibilityAccClient + delta, visibilityComponent.HideTime); + visibilityComponent.VisibilityAcc = MathF.Min(visibilityComponent.VisibilityAcc + delta, visibilityComponent.HideTime); } } @@ -58,7 +68,7 @@ private void OnMove(Entity ent, ref MoveEvent a // В зависимости от наличие защит или проблем со зрением у 939 изменяется то, насколько хорошо мы видим жертву if (ModifyAcc(ent.Comp, out var modifier)) // Если зрение затруднено { - ent.Comp.VisibilityAccClient *= modifier; + ent.Comp.VisibilityAcc *= modifier; } else if (_scp939ProtectionQuery.HasComp(ent)) // Если имеется защита(тихое хождение) { @@ -66,7 +76,7 @@ private void OnMove(Entity ent, ref MoveEvent a } else // Если со зрением все ок { - ent.Comp.VisibilityAccClient = 0; + ent.Comp.VisibilityAcc = 0; } if (!_movementSpeedQuery.TryComp(ent, out var speedModifierComponent) @@ -78,7 +88,7 @@ private void OnMove(Entity ent, ref MoveEvent a var currentVelocity = physicsComponent.LinearVelocity.Length(); if (speedModifierComponent.BaseWalkSpeed > currentVelocity) - ent.Comp.VisibilityAccClient = ent.Comp.HideTime / 2f; + ent.Comp.VisibilityAcc = ent.Comp.HideTime / 2f; } @@ -127,7 +137,7 @@ private void OnDown(Entity ent, ref DownedEvent private void MobDidSomething(Entity ent) { - ent.Comp.VisibilityAccClient = Scp939VisibilityComponent.InitialVisibilityAcc; + ent.Comp.VisibilityAcc = Scp939VisibilityComponent.InitialVisibilityAcc; } // TODO: Переделать под статус эффект и добавить его в панель статус эффектов, а то непонятно игруну diff --git a/Content.Client/_Scp/Scp939/Scp939HudSystem.cs b/Content.Client/_Scp/Scp939/Scp939HudSystem.cs index adc04c4c2d5..a0136d8844b 100644 --- a/Content.Client/_Scp/Scp939/Scp939HudSystem.cs +++ b/Content.Client/_Scp/Scp939/Scp939HudSystem.cs @@ -139,7 +139,7 @@ internal void RestoreCachedBaseAlphas() internal static float GetVisibility(Entity ent) { - var acc = ent.Comp.VisibilityAccClient + ent.Comp.VisibilityAcc; + var acc = ent.Comp.VisibilityAcc; if (acc > ent.Comp.HideTime) return 0; diff --git a/Content.Server/_Scp/Scp939/Scp939System.Visibility.cs b/Content.Server/_Scp/Scp939/Scp939System.Visibility.cs index 8aa967c9b9e..e8817310dbc 100644 --- a/Content.Server/_Scp/Scp939/Scp939System.Visibility.cs +++ b/Content.Server/_Scp/Scp939/Scp939System.Visibility.cs @@ -5,7 +5,6 @@ using Content.Shared.Item; using Content.Shared.Mobs.Components; using Content.Shared.Popups; -using Content.Shared.Standing; using Content.Shared.Weapons.Ranged.Systems; using Robust.Shared.Map; using Robust.Shared.Timing; @@ -83,11 +82,8 @@ private void OnTargetSpoke(Entity ent, ref Enti private void MobDidSomething(Entity ent) { - if (MathHelper.CloseTo(ent.Comp.VisibilityAcc, Scp939VisibilityComponent.InitialVisibilityAcc)) - return; - - ent.Comp.VisibilityAcc = Scp939VisibilityComponent.InitialVisibilityAcc; - DirtyField(ent, ent.Comp, nameof(ActiveScp939VisibilityComponent.VisibilityAcc)); + ent.Comp.VisibilityResetCounter++; + DirtyField(ent, ent.Comp, nameof(ActiveScp939VisibilityComponent.VisibilityResetCounter)); } private void UpdateVisibilityTargets() @@ -145,7 +141,6 @@ private void EnsureActiveVisibility(Entity ent) if (!_activeQuery.TryComp(ent, out var active)) { active = AddComp(ent); - active.VisibilityAcc = Scp939VisibilityComponent.InitialVisibilityAcc; active.HideTime = ent.Comp.HideTime; active.MinValue = ent.Comp.MinValue; active.MaxValue = ent.Comp.MaxValue; diff --git a/Content.Shared/_Scp/Scp939/ActiveScp939VisibilityComponent.cs b/Content.Shared/_Scp/Scp939/ActiveScp939VisibilityComponent.cs index 9a5d959e1f4..d28eeba9bf6 100644 --- a/Content.Shared/_Scp/Scp939/ActiveScp939VisibilityComponent.cs +++ b/Content.Shared/_Scp/Scp939/ActiveScp939VisibilityComponent.cs @@ -2,14 +2,15 @@ namespace Content.Shared._Scp.Scp939; -[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(fieldDeltas: true)] +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(raiseAfterAutoHandleState: true, fieldDeltas: true)] public sealed partial class ActiveScp939VisibilityComponent : Component { - public float VisibilityAccClient = Scp939VisibilityComponent.InitialVisibilityAcc; - - [DataField, AutoNetworkedField] + [ViewVariables] public float VisibilityAcc = Scp939VisibilityComponent.InitialVisibilityAcc; + [AutoNetworkedField] + public uint VisibilityResetCounter; + [DataField, AutoNetworkedField] public float HideTime = Scp939VisibilityComponent.DefaultHideTime; @@ -18,4 +19,7 @@ public sealed partial class ActiveScp939VisibilityComponent : Component [DataField, AutoNetworkedField] public int MaxValue = Scp939VisibilityComponent.DefaultMaxValue; + + [NonSerialized] + public uint LastHandledVisibilityResetCounter; }