Skip to content

Система удержания#928

Open
ThereDrD0 wants to merge 24 commits intomasterfrom
add/multipulling
Open

Система удержания#928
ThereDrD0 wants to merge 24 commits intomasterfrom
add/multipulling

Conversation

@ThereDrD0
Copy link
Copy Markdown
Collaborator

@ThereDrD0 ThereDrD0 commented Apr 8, 2026

Краткое описание | Short description

Добавил систему удержания. Она позволяет двум людям схватить человека и не дать ему двигаться некоторое время.

Ее получит Комендантский отряд для всего персонала и отряд ОСН для SCP-096 с особыми условиями для процедуры лечения.

Ссылка на багрепорт/Предложение | Related Issue/Bug Report

Closes #824

Медиа (Видео/Скриншоты) | Media (Video/Screenshots)

Changelog

🆑 ThereDrD

  • add: Добавлена новая механика - удержание. Если два человека схватят другого - он на некоторое время не сможет двигаться и вырваться из захвата. По умолчанию будет работать у Комендантского отряда для всего персонала и у отряда ОСН для SCP-096
  • fix: SCP-096 должен перестать терять возможность встать

Summary by CodeRabbit

Новые функции

  • Добавлена система удержания SCP-сущностей с механикой попыток побега и иммунитета
  • Реализована система оповещений и ограничений для удерживаемых целей
  • Добавлены специализированные правила удержания для различных ролей

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 8, 2026

📝 Walkthrough

Walkthrough

Реализована новая система захвата SCP-объектов. Персонажи с компонентом ScpHolder могут захватывать цели, ограничивающие их движение и действия. Захватанные могут попытаться вырваться по истечении времени восстановления с механикой полного и мягкого захвата.

Changes

Cohort / File(s) Summary
Ядро системы захвата
Content.Shared/_Scp/Holding/ScpHoldStage.cs, Content.Shared/_Scp/Holding/ScpHoldingEvents.cs
Введены новые типы событий (ScpHoldAttemptEvent, ScpHoldBreakoutEvent, ScpHoldBreakoutDoAfterEvent) и перечисление ScpHoldStage для различия мягкого и полного захвата.
Компоненты состояния захвата
Content.Shared/_Scp/Holding/Components/ActiveScpHoldableComponent.cs, Content.Shared/_Scp/Holding/Components/ActiveScpHolderComponent.cs, Content.Shared/_Scp/Holding/Components/ActiveStateScpHoldable*.cs, Content.Shared/_Scp/Holding/Components/ActiveStateScpHolder*.cs
Сетевые компоненты для отслеживания активных захватов, состояния полного захвата, замедления движения и курсорного движения.
Компоненты конфигурации захвата
Content.Shared/_Scp/Holding/Components/ScpHoldableComponent.cs, Content.Shared/_Scp/Holding/Components/ScpHolderComponent.cs, Content.Shared/_Scp/Holding/Components/ScpHold*.cs
Компоненты данных для настройки параметров захвата: требуемое количество рук, время задержки, список белых/чёрных списков целей, параметры мягкого перемещения, модификаторы скорости.
Основная система захвата (серверная/общая)
Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.cs, Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.*.cs
Многофайловая реализация основной логики захватов, включая действия, состояние, мягкое перемещение, блокировку рук, управление жизненным циклом и ограничения действий.
Система захвата (клиентская)
Content.Client/_Scp/Holding/ScpHoldingSystem.cs
Предсказание клиента для захватов: синхронизация блокирующих виртуальных предметов и управление физическим состоянием захватанных сущностей.
Система захвата (серверная)
Content.Server/_Scp/Holding/ScpHoldingSystem.cs
Обработка жизненного цикла захватов на сервере, включая очистку состояния и синхронизацию при изменении количества рук.
Интеграция с системой вытягивания
Content.Shared/_Scp/Holding/Compatibility/PullingSystem.ScpHolding.cs, Content.Shared/Movement/Pulling/Systems/PullingSystem.cs
Система вытягивания переориентирована для преобразования попыток вытягивания в захваты SCP при наличии соответствующих компонентов.
Интеграция с SCP-096
Content.Shared/_Scp/Scp096/Main/Components/Scp096Component.cs, Content.Shared/_Scp/Scp096/Main/Systems/SharedScp096System.Holding.cs, Content.Shared/_Scp/Scp096/Main/Systems/SharedScp096System.Rage.cs, Content.Shared/_Scp/Scp096/Main/Systems/SharedScp096System.WithoutFace.cs
SCP-096 получает параметры для вырывания из захвата (урон, паралич, импульс) и вызывает вырывание при переходах состояния.
Компоненты взаимодействия
Content.Shared/Hands/EntitySystems/SharedHandsSystem.Drop.cs, Content.Shared/Interaction/SharedInteractionSystem.Blocking.cs
Улучшено обращение с виртуальными предметами при сбросе и ранний выход при блокировании движения вне активного состояния.
Система мировых алертов
Content.Shared/_Scp/Other/WorldAlert/WorldAlertSettings.cs, Content.Shared/_Scp/Other/WorldAlert/WorldAlertSystem.cs, Resources/Prototypes/_Scp/Entities/Effects/world_alerts.yml
Новая система визуальных уведомлений для попыток вырывания, включая конфигурацию прототипов и звуки.
Курсорное движение
Content.Shared/_Scp/Holding/Components/ActiveStateScpHoldableCursorMoveComponent.cs, Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.CursorMove.cs, Content.Server/Movement/Systems/PullController.cs
Система управления захватанной целью через курсор с физическими расчётами и интеграцией в контроллер вытягивания.
Локализация
Resources/Locale/en-US/_strings/_scp/holding/holding.ftl, Resources/Locale/ru-RU/_strings/_scp/holding/holding.ftl
Добавлены сообщения для: попыток захвата, блокировок (иммунитета, дальности, рук), вырывания и уведомлений о захвате.
Конфигурация ролей и действий
Resources/Prototypes/Actions/types.yml, Resources/Prototypes/_Scp/Actions/scp096.yml, Resources/Prototypes/_Scp/Roles/Jobs/*/*.yml
Действия получают ограничения захвата по стадиям. Роли (CommandantSquad, HeavyContainmentZone, ClassD) получают компоненты ScpHolder с белыми/чёрными списками целей.
Прототипы сущностей
Resources/Prototypes/Entities/Mobs/Species/base.yml, Resources/Prototypes/_Scp/Entities/Mobs/Player/Scp/Main/scp096.yml, Resources/Prototypes/_Scp/Entities/StatusEffects/holding.yml, Resources/Prototypes/_Scp/Alerts/holding.yml
Базовые виды, SCP-096 и эффекты статуса получают компоненты захвата и конфигурацию.
Удаление устаревшего теста
Content.IntegrationTests/Tests/_Scp/ScpHeadsetEncryptionKeysTest.cs
Удалён интеграционный тест, не связанный с новой функциональностью захватов.

Sequence Diagrams

sequenceDiagram
    participant Player as Игрок/Захватывающий
    participant PullSys as PullingSystem
    participant ScpSys as SharedScpHoldingSystem
    participant HeldEnt as Захватанная сущность
    participant ActionSys as ActionSystem
    
    Player->>PullSys: Попытка вытягивания
    PullSys->>ScpSys: TryRedirectPullToScpHold()
    alt Захват допустим
        ScpSys->>ScpSys: CanToggleHold() проверка
        ScpSys->>HeldEnt: Создать ActiveScpHoldableComponent
        ScpSys->>Player: Создать ActiveScpHolderComponent
        ScpSys->>ActionSys: Заблокировать ограниченные действия
        ScpSys-->>PullSys: true (обработано)
    else Захват не допустим
        ScpSys-->>PullSys: false (обычное вытягивание)
    end
Loading
sequenceDiagram
    participant HeldEnt as Захватанная сущность
    participant Player as Игрок (захватывающий)
    participant ScpSys as SharedScpHoldingSystem
    participant DoAfterSys as DoAfterSystem
    participant SoundSys as AudioSystem
    participant AlertSys as WorldAlertSystem
    
    HeldEnt->>ScpSys: Нажать действие вырывания
    ScpSys->>ScpSys: TryBreakOut(viaMovement)
    alt Мягкий захват
        ScpSys->>ScpSys: Проверить SoftEscapeAvailableAt
        ScpSys-->>HeldEnt: false (слишком рано)
    else Полный захват
        ScpSys->>DoAfterSys: Создать DoAfter (FullBreakoutDuration)
        DoAfterSys->>ScpSys: На завершение DoAfter
        ScpSys->>HeldEnt: Применить импульс
        ScpSys->>Player: Применить урон + паралич
        ScpSys->>AlertSys: TrySpawnAlert() уведомление
        AlertSys->>SoundSys: Воспроизвести звук
        ScpSys->>HeldEnt: ClearHoldState()
        ScpSys->>HeldEnt: Применить иммунитет на 5 сек
    end
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Это комплексная реализация новой системы механики с множеством взаимосвязанных компонентов ECS, событий, систем (клиентская, серверная, общая), интеграцией с существующими системами (вытягивание, действия, движение, SCP-096), локализацией на двух языках и обширной конфигурацией прототипов. Требуется тщательная проверка логики захватов, переходов состояния, обработки жизненного цикла компонентов, предсказания на клиенте и интеграции с движением.

Poem

🐰 Захват за руки теперь есть в игре,
Скромника держим в уверенной мере!
Вырваться можно, но нужно спешить,
Кролик помощник поможет служить! 🐇✨


Important

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

❌ Failed checks (4 errors, 1 warning)

Check name Status Explanation Resolution
Ss14 C# Rules ❌ Error Обнаружены критические нарушения SS14 C# правил: неправильные маркеры Fire edit/added вместо Sunrise-Edit, отсутствие base.Update(), отсутствие [AutoGenerateComponentState], неиспользуемые импорты. Заменить маркеры на Sunrise-Edit, добавить base.Update(frameTime), добавить [AutoGenerateComponentState], удалить неиспользуемые импорты.
Ss14 Yaml/Ftl Rules ❌ Error PR содержит критические нарушения SS14 правил: отсутствие Sunrise-Edit маркеров в 14 job-файлах, неправильные Fire edit маркеры в upstream-файлах и использование сырого текста вместо локализационного ключа в StatusEffectScpHeld. Добавить Sunrise-Edit маркеры к ScpHolder блокам во всех job файлах, заменить Fire edit на Sunrise-Edit в Actions/types.yml и base.yml, использовать локализационный ключ alerts-scp-held-name.
Ss14 Prototype ↔ Ftl Parity ❌ Error PR добавляет StatusEffectScpHeld прототип, но отсутствуют локализационные файлы holding.ftl в Resources/Locale/_prototypes/_scp/entities/statuseffects/ для обеих локалей Создать holding.ftl файлы в en-US и ru-RU локалях с локализацией ent-StatusEffectScpHeld согласно паттерну из fear.ftl
Ss14 Prediction Safety ❌ Error SharedScpHoldingSystem содержит hazards детерминированной предсказания: PopupClient вызовы в shared коде без IsFirstTimePredicted guards и сырые ключи локализации. Обёрните PopupClient с Loc.GetString(), добавьте IsFirstTimePredicted guards, добавьте [AutoGenerateComponentState] к ScpBreakoutAttemptComponent и base.Update() в SharedScpHoldingSystem.Update().
Docstring Coverage ⚠️ Warning Docstring coverage is 1.23% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title "Система удержания" (Holding System) accurately describes the main feature added in this PR - a new holding/grappling mechanic.
Linked Issues check ✅ Passed The PR implements the core holding system requirements: two-person grab mechanics, movement/ability restriction while held, 10-second escape action, 5-second struggle cooldown, breakout effects (damage, stun, knockdown), and state-change triggered breakout.
Out of Scope Changes check ✅ Passed All changes are directly related to implementing the SCP holding system: new holding components/systems, SCP-096 hold integration, hand blocking, cursor movement, status effects, job restrictions, and localization strings.
Ss14 Bridge Sync ✅ Passed PR не изменяет канонические правила или навыки в .agent/rules/ или .agent/skills/. Все изменения касаются только игровой логики.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch add/multipulling

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 21

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Content.Client/_Scp/Holding/ScpHoldingPredictionSystem.cs`:
- Around line 22-26: Replace the fixed 500ms timeout suppression with
state-based suppression: remove BlockerRespawnSuppressionDuration and the use of
_suppressedUntil and any time comparisons, and instead keep suppression until
the authoritative ScpHolderComponent for the suppressed holder either changes
its Target or the component is removed; implement this by storing
_suppressedHolder and _suppressedTarget when you start suppression and, where
the code currently checks _suppressedUntil, instead query the current
ScpHolderComponent for that holder (or its existence) and clear suppression only
when ScpHolderComponent.Target is different from _suppressedTarget or the
component no longer exists. Ensure all locations that reference
BlockerRespawnSuppressionDuration or _suppressedUntil (including the code paths
around the logic that spawns/filters the virtual blocker) are updated to use the
new state-based check.

In `@Content.Shared/_Scp/Holding/ScpHeldHandBlockerComponent.cs`:
- Around line 8-20: Компонент ScpHeldHandBlockerComponent помечен как
[NetworkedComponent] но не генерирует состояние и не маркирует поля для сетевой
синхронизации, из‑за чего предсказание/откат в ScpHoldingPredictionSystem
некорректны; добавьте атрибут [AutoGenerateComponentState] к классу
ScpHeldHandBlockerComponent и пометьте поля Target и Holder атрибутом
[AutoNetworkedField], чтобы они синхронизировались и откатывались так же, как в
аналогичных компонентах ScpHolderComponent, ScpHeldComponent и ScpHoldComponent.

In `@Content.Shared/_Scp/Holding/ScpHoldableComponent.cs`:
- Around line 9-10: The ScpHoldableComponent in Shared lacks the
NetworkedComponent attribute needed for client-side replication; add the
[NetworkedComponent] attribute to the ScpHoldableComponent declaration (the
public sealed partial class ScpHoldableComponent : Component) in Content.Shared
so the component is networked and eligible checks replicate correctly, and
ensure any required using/import for the networking attribute's namespace is
added if missing.

In `@Content.Shared/_Scp/Holding/ScpHoldRestrictedComponent.cs`:
- Around line 5-9: Добавьте атрибуты автогенерации и автосинхронизации для
сетевого компонента: пометьте класс ScpHoldRestrictedComponent атрибутом
[AutoGenerateComponentState] и пометьте поле Stage атрибутом
[AutoNetworkedField]; это гарантирует, что поле Stage будет участвовать в
сериализации/десериализации и откате состояния при предсказании для
NetworkedComponent.

In `@Content.Shared/_Scp/Holding/SharedScpHoldingSystem.Actions.cs`:
- Around line 22-40: CanToggleHold currently only validates starting a hold and
thus disagrees with TryToggleHold's actual toggle logic; update CanToggleHold to
mirror TryToggleHold's preconditions by returning true for the release case when
the holder's active target equals the provided target, false when the holder is
already holding a different target (so UI/prediction matches the PopupHolder
rejection), and otherwise perform the start-hold checks (or delegate to
CanStartHold) when the holder has no active target; ensure the method signature
and behavior follow the OnEvent -> TryDo -> CanDo -> Do pattern and reference
CanToggleHold and TryToggleHold so any callers/predictions stay consistent with
the public API.
- Around line 245-247: Вызов PopupTarget(held.Owner, "scp-hold-breakout-start")
выполняется без prediction-gating; либо оберните этот вызов в проверку
IsFirstTimePredicted (как это сделано в ShowBreakoutAttemptFeedback), либо
замените вызов на prediction-safe хелпер (например
PopupPredicted/PopupEntity-перенаправление), чтобы обеспечить one-shot UX effect
только при первой предсказанной итерации; отредактируйте функцию PopupTarget или
место вызова, чтобы использовать IsFirstTimePredicted или вызвать
соответствующий prediction-safe wrapper (см. ShowBreakoutAttemptFeedback,
PopupTarget, PopupPredicted, IsFirstTimePredicted).

In `@Content.Shared/_Scp/Holding/SharedScpHoldingSystem.Drag.cs`:
- Around line 104-112: GetSoftDragDirection currently falls back to
Transform(holderUid).LocalRotation.ToWorldVec(), which returns a parent-relative
direction and can mismatch the map-space vectors (offset, holderVelocity);
change the fallback to use the entity's world rotation instead by replacing the
LocalRotation usage with WorldRotation so the returned direction is in
world/map-space and consistent with the normalized offset and velocity checks
(symbols: GetSoftDragDirection, SoftDragSnapTolerance,
SoftDragVelocityDirectionThreshold,
Transform(holderUid).WorldRotation.ToWorldVec()).

In `@Content.Shared/_Scp/Holding/SharedScpHoldingSystem.Events.cs`:
- Around line 221-233: OnHolderBlockerDropped currently releases the holder
contribution but lets the normal drop flow continue, which can race with the
blocker being removed; after calling ReleaseHolderContribution(args.User,
ent.Comp.Target, clearIfEmpty: true) immediately stop the default drop handling
on the GettingDroppedAttemptEvent (e.g. call args.Cancel() or set args.Cancel =
true / args.Handled = true depending on the event API) so the virtual item's
drop logic does not run against the already-processed blocker; keep this change
inside OnHolderBlockerDropped right after the ReleaseHolderContribution call.
- Around line 135-155: The handlers OnHeldAttemptMobCollide and
OnHeldAttemptMobTargetCollide are currently cancelling all mob-collisions for
any ScpHeldComponent; change them to only cancel when the held is in a FullHold
state OR when the other entity is the actual holder for this held pair.
Concretely: in both OnHeldAttemptMobCollide and OnHeldAttemptMobTargetCollide,
early-return unless ent.Component.FullHold is true OR
_holderQuery.TryComp(args.OtherEntity, out var holder) && holder.Target ==
ent.Owner; keep the finer-grained logic in OnHeldPreventCollide untouched so
pair-wise holder↔held collisions still work.

In `@Content.Shared/_Scp/Holding/SharedScpHoldingSystem.Hands.cs`:
- Around line 27-69: SyncPlaceholderHands currently calls DeleteHeldHandBlockers
unconditionally and then recreates all virtual blockers; change it to reuse
existing ScpHeldHandBlockerComponent entities like SyncHolderHandBlocker does
by: first collect existing blocker entities for held.Owner (inspect components
via ScpHeldHandBlockerComponent and _placeholderIcons), compute which blockers
already match a holder in held.Comp.Holders (matching blocker.Holder ==
holderUid and blocker.Target == held.Owner) and keep them, delete only obsolete
blockers and their virtual items, then for any missing holders spawn virtual
items with _virtualItem.TrySpawnVirtualItemInHand,
EnsureComp<UnremoveableComponent> and set
ScpHeldHandBlockerComponent.Target/Holder as now done; do not unconditionally
call DeleteHeldHandBlockers and avoid dropping/respawning valid held items via
_hands.TryGetHeldItem/_hands.DoDrop unless necessary.

In `@Content.Shared/_Scp/Holding/SharedScpHoldingSystem.Restrictions.cs`:
- Around line 10-16: В методе OnHoldRestrictedActionAttempt (обработчик
ActionAttemptEvent) замените вызов _popup.PopupClient(...) на
_popup.PopupPredicted(...), чтобы использовать предсказанный вариант попапа для
предсказанных событий и избежать двойного отображения; оставьте те же аргументы
(сообщение Loc.GetString("scp-hold-action-restricted"), args.User, args.User)
при замене, как в аналогичных системах (SharedArtifactCrusherSystem,
ScpMaskSystem).

In `@Content.Shared/Interaction/SharedInteractionSystem.Blocking.cs`:
- Around line 44-47: В коде комментарии-маркеры "Fire edit start" и "Fire edit
end" (см. строку с "// Fire edit start - do not let a blocker cancel its own
shutdown refresh" и соответствующий конец блока) используют неправильный формат;
заменить эти форк-маркеры на Sunrise-формат: используйте "Sunrise-Edit" for
single-line or "Sunrise edit start" / "Sunrise edit end" for multi-line edit
blocks so они соответствуют стандарту репозитория (обновить оба места, где
встречается "Fire edit start" и "Fire edit end").

In `@Content.Shared/Movement/Pulling/Systems/PullingSystem.cs`:
- Around line 532-535: TryRedirectPullToScpHold currently runs before CanPull
and bypasses key checks and events (NeedsHands, CanInteract/distance, bound
status) and does not raise StartPullAttemptEvent / BeingPulledAttemptEvent /
PullAttemptEvent, so subscriber systems (CuffableSystem, BuckleSystem,
VentCrawSystem, CarryingSystem, AdminFrozenSystem, etc.) can't veto the action;
fix by unifying the guard logic: either have TryRedirectPullToScpHold call the
same validation and raise the same events as CanPull (including NeedsHands,
CanInteract checks and binding checks) before returning a redirect, or extract
the shared pre-pull guard into a helper (e.g., ValidateAndRaisePullAttemptEvents
or similar) and call it from both TryRedirectPullToScpHold and the original pull
path so both paths perform identical checks and raise StartPullAttemptEvent /
BeingPulledAttemptEvent / PullAttemptEvent.

In `@Resources/Locale/en-US/_strings/_scp/holding/holding.ftl`:
- Around line 13-14: Update the locale entry alerts-scp-held-desc so the player
instruction is marked as out-of-character: prefix the sentence "Move or click
this alert to try to break free." with "OOC:" (e.g., "OOC: Move or click this
alert to try to break free.") while keeping the rest of the description intact;
edit the alerts-scp-held-desc string accordingly to ensure the control hint
complies with OOC tagging guidelines.

In `@Resources/Prototypes/_Scp/Roles/Jobs/Administration/security_commander.yml`:
- Around line 36-39: В текущей конфигурации у Security Commander блок ScpHold
использует holdableWhitelist, который содержит только ClassDAppearance и потому
не покрывает весь персонал; исправьте это, расширив или заменив
holdableWhitelist в записи для Security Commander так, чтобы включить все типы
персонала (например добавить/заменить на сущности вроде ResearchAppearance,
StaffAppearance, GuardAppearance, SecurityAppearance или общий маркер
PersonnelAppearance/персонал вместо ClassDAppearance), либо удалить/исправить
whitelist на более общий критерий, чтобы соответствовать заявленному охвату
механика; изменяйте именно секцию ScpHold и поле holdableWhitelist в записи
Security Commander.

In
`@Resources/Prototypes/_Scp/Roles/Jobs/LowAccessPersonnel/class_d_botanist.yml`:
- Around line 26-34: В файле конфигурации ClassDBotanist удалите назначение
механики удержания: уберите или переместите блок type: ScpHold (и связанные поля
holdableWhitelist / holdableBlacklist) из class_d_botanist.yml, т.е. удалите
упоминание ScpHold у ClassDBotanist и перенесите эту фичу в правильный
scope/роль, где удержание должно быть доступно; убедитесь, что поля
holdableWhitelist и holdableBlacklist больше не присутствуют в описании
ClassDBotanist (или добавьте вместо них ссылку на корректный класс/роль,
отвечающую за ScpHold).

In
`@Resources/Prototypes/_Scp/Roles/Jobs/SpecialPurposeSquad/heavy_containment_zone_officer.yml`:
- Around line 31-40: Конфигурация ScpHold с одинаковыми секциями
holdableWhitelist и holdableBlacklist (components: Scp096, ClassDAppearance и
blacklist: ActiveScp096Rage, ActiveScp096HeatingUp, ActiveScp096WithoutFace)
повторяется в нескольких job-файлах; вынесите её в общий шаблон или базовый
job-прототип (например: базовый прототип/компонент с именем ScpHoldBase) и
замените дублирующие блоки в файлах на ссылку/наследование этого шаблона,
сохранив возможность переопределения по нужде.

In
`@Resources/Prototypes/_Scp/Roles/Jobs/SpecialPurposeSquad/junior_heavy_containment_zone_officer.yml`:
- Around line 30-39: В блоке конфигурации ScpHold удалите запись
ClassDAppearance из holdableWhitelist — оставьте только Scp096 в whitelist и
сохраните текущий holdableBlacklist (ActiveScp096Rage, ActiveScp096HeatingUp,
ActiveScp096WithoutFace) чтобы фокусировать механику только на SCP-096;
проверьте секцию ScpHold и поля holdableWhitelist/holdableBlacklist и уберите
только ClassDAppearance, не трогая остальные ключи.

In `@Resources/Prototypes/Actions/types.yml`:
- Around line 264-265: The comment on the upstream entry for the action type
ScpHoldRestricted uses the wrong upstream marker; replace the inline comment "#
Fire edit" with the required repository upstream marker "# Sunrise-Edit" (exact
casing) so the ScpHoldRestricted line uses the Sunrise upstream edit marker as
per guidelines.

In `@Resources/Prototypes/Entities/Mobs/Species/base.yml`:
- Around line 281-282: The upstream YAML changes use a Fire-format inline
comment instead of the required Sunrise markers; replace the Fire comment on the
modified entries (types ScpHold and ScpHoldable) with the Sunrise upstream edit
markers—wrap the edited lines with the Sunrise edit markers (e.g., use the
"Sunrise-Edit" or "Sunrise edit start" / "Sunrise edit end" pattern) so the
change follows the repository's Sunrise upstream-edit convention for the Species
base.yml entries where ScpHold and ScpHoldable are altered.
- Around line 281-282: В BaseMobSpeciesOrganic удалите запись типа ScpHold
(оставьте ScpHoldable если нужно), чтобы базовый органический прототип больше не
получал механику удержания; вместо этого назначьте компонент ScpHold только
нужным ролям через job "special" (проверьте где определяется job "special" и
добавьте/переместите тип ScpHold туда).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 4181336a-3b73-42ff-802a-15d03d2894e6

📥 Commits

Reviewing files that changed from the base of the PR and between b873bec and e43a8f6.

📒 Files selected for processing (51)
  • Content.Client/_Scp/Holding/ScpHoldingPredictionSystem.cs
  • Content.IntegrationTests/Tests/_Scp/ScpHoldingTest.cs
  • Content.Shared/Hands/EntitySystems/SharedHandsSystem.Drop.cs
  • Content.Shared/Interaction/SharedInteractionSystem.Blocking.cs
  • Content.Shared/Movement/Pulling/Systems/PullingSystem.cs
  • Content.Shared/_Scp/Holding/PullingSystem.ScpHolding.cs
  • Content.Shared/_Scp/Holding/ScpHeldComponent.cs
  • Content.Shared/_Scp/Holding/ScpHeldHandBlockerComponent.cs
  • Content.Shared/_Scp/Holding/ScpHoldComponent.cs
  • Content.Shared/_Scp/Holding/ScpHoldHandBlockerComponent.cs
  • Content.Shared/_Scp/Holding/ScpHoldImmuneComponent.cs
  • Content.Shared/_Scp/Holding/ScpHoldRestrictedComponent.cs
  • Content.Shared/_Scp/Holding/ScpHoldStage.cs
  • Content.Shared/_Scp/Holding/ScpHoldableComponent.cs
  • Content.Shared/_Scp/Holding/ScpHolderComponent.cs
  • Content.Shared/_Scp/Holding/ScpHoldingEvents.cs
  • Content.Shared/_Scp/Holding/SharedScpHoldingSystem.Actions.cs
  • Content.Shared/_Scp/Holding/SharedScpHoldingSystem.Drag.cs
  • Content.Shared/_Scp/Holding/SharedScpHoldingSystem.Events.cs
  • Content.Shared/_Scp/Holding/SharedScpHoldingSystem.Feedback.cs
  • Content.Shared/_Scp/Holding/SharedScpHoldingSystem.Hands.cs
  • Content.Shared/_Scp/Holding/SharedScpHoldingSystem.Restrictions.cs
  • Content.Shared/_Scp/Holding/SharedScpHoldingSystem.State.cs
  • Content.Shared/_Scp/Holding/SharedScpHoldingSystem.cs
  • Content.Shared/_Scp/Scp096/Main/Components/Scp096Component.cs
  • Content.Shared/_Scp/Scp096/Main/Systems/SharedScp096System.Holding.cs
  • Content.Shared/_Scp/Scp096/Main/Systems/SharedScp096System.Rage.cs
  • Content.Shared/_Scp/Scp096/Main/Systems/SharedScp096System.WithoutFace.cs
  • Content.Shared/_Scp/Scp096/Main/Systems/SharedScp096System.cs
  • Resources/Locale/en-US/_strings/_scp/holding/holding.ftl
  • Resources/Locale/ru-RU/_strings/_scp/holding/holding.ftl
  • Resources/Prototypes/Actions/types.yml
  • Resources/Prototypes/Entities/Mobs/Species/base.yml
  • Resources/Prototypes/_Scp/Actions/scp096.yml
  • Resources/Prototypes/_Scp/Alerts/holding.yml
  • Resources/Prototypes/_Scp/Entities/Mobs/Player/Scp/Main/scp096.yml
  • Resources/Prototypes/_Scp/Entities/StatusEffects/holding.yml
  • Resources/Prototypes/_Scp/Roles/Jobs/Administration/security_commander.yml
  • Resources/Prototypes/_Scp/Roles/Jobs/CommandantSquad/external_administrative_zone_commandant.yml
  • Resources/Prototypes/_Scp/Roles/Jobs/CommandantSquad/external_administrative_zone_officer.yml
  • Resources/Prototypes/_Scp/Roles/Jobs/CommandantSquad/field_medical_specialist.yml
  • Resources/Prototypes/_Scp/Roles/Jobs/CommandantSquad/junior_external_administrative_zone_officer.yml
  • Resources/Prototypes/_Scp/Roles/Jobs/CommandantSquad/senior_external_administrative_zone_officer.yml
  • Resources/Prototypes/_Scp/Roles/Jobs/LowAccessPersonnel/class_d.yml
  • Resources/Prototypes/_Scp/Roles/Jobs/LowAccessPersonnel/class_d_botanist.yml
  • Resources/Prototypes/_Scp/Roles/Jobs/LowAccessPersonnel/class_d_cook.yml
  • Resources/Prototypes/_Scp/Roles/Jobs/LowAccessPersonnel/class_d_janitor.yml
  • Resources/Prototypes/_Scp/Roles/Jobs/SpecialPurposeSquad/heavy_containment_zone_commandant.yml
  • Resources/Prototypes/_Scp/Roles/Jobs/SpecialPurposeSquad/heavy_containment_zone_officer.yml
  • Resources/Prototypes/_Scp/Roles/Jobs/SpecialPurposeSquad/junior_heavy_containment_zone_officer.yml
  • Resources/Prototypes/_Scp/Roles/Jobs/SpecialPurposeSquad/senior_heavy_containment_zone_officer.yml

Comment thread Content.Client/_Scp/Holding/ScpHoldingPredictionSystem.cs Outdated
Comment thread Content.Shared/_Scp/Holding/ScpHeldHandBlockerComponent.cs Outdated
Comment thread Content.Shared/_Scp/Holding/ScpHoldableComponent.cs Outdated
Comment thread Content.Shared/_Scp/Holding/Components/ScpHoldRestrictedComponent.cs Outdated
Comment thread Content.Shared/_Scp/Holding/SharedScpHoldingSystem.Actions.cs Outdated
Comment thread Resources/Prototypes/_Scp/Roles/Jobs/LowAccessPersonnel/class_d_botanist.yml Outdated
Comment thread Resources/Prototypes/Actions/types.yml
Comment thread Resources/Prototypes/Entities/Mobs/Species/base.yml Outdated
@ThereDrD0 ThereDrD0 marked this pull request as draft April 14, 2026 03:19
@ThereDrD0 ThereDrD0 marked this pull request as ready for review April 18, 2026 00:27
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 15

♻️ Duplicate comments (5)
Resources/Prototypes/_Scp/Roles/Jobs/Administration/security_commander.yml (1)

36-39: ⚠️ Potential issue | 🟠 Major

Расширьте Security Commander до всего персонала.

ClassDAppearance оставляет удержание только для D-класса, хотя для командного контура заявлено удержание всего персонала. Используйте общий humanoid whitelist и тот же SCP blacklist, что у CommandantSquad.

Предлагаемое исправление
     - type: ScpHolder
       holdableWhitelist:
         components:
-        - ClassDAppearance
+        - HumanoidAppearance
+      holdableBlacklist:
+        components:
+        - Scp
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Resources/Prototypes/_Scp/Roles/Jobs/Administration/security_commander.yml`
around lines 36 - 39, The ScpHolder config for Security Commander currently
limits holds to ClassDAppearance (ScpHolder with holdableWhitelist.components
including ClassDAppearance); change it to use the general humanoid whitelist and
apply the same SCP blacklist used by CommandantSquad: replace the
holdableWhitelist.components entry that references ClassDAppearance with the
shared humanoid whitelist identifier (the same whitelist key used by
CommandantSquad) and ensure the ScpHolder includes the identical scp
blacklist/settings from CommandantSquad so Security Commander holds all
personnel rather than only D-class.
Resources/Prototypes/Entities/Mobs/Species/base.yml (1)

281-282: ⚠️ Potential issue | 🔴 Critical

Уберите ScpHolder из базового органического прототипа.

Line 281 выдаёт механику держателя всем органическим мобам, обходя job special и ролевые ограничения PR. Оставьте здесь только holdable-маркер, если цель — сделать органиков доступными для удержания.

Предлагаемое исправление
-  - type: ScpHolder # TODO: Убрать перед мержем
-  - type: ScpHoldable
+  - type: ScpHoldable # Sunrise-Edit

As per coding guidelines: "This repository's repository name is sunrise-station, so use the Sunrise prefix, the _Sunrise project folder, and Sunrise-Edit / Sunrise edit start/end markers for unavoidable upstream edits."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Resources/Prototypes/Entities/Mobs/Species/base.yml` around lines 281 - 282,
Удалите маркер типа ScpHolder из базового органического прототипа и оставьте
только ScpHoldable (убирается строка с "type: ScpHolder" и не
трогать/восстановить "type: ScpHoldable"), чтобы не давать всем органикам
механики держателя в обход job `special` и ролевых ограничений; если это
неизбежный апстрим-фикс — поместите изменения в проектную папку _Sunrise,
используйте префикс Sunrise для необходимых идентификаторов и обрамите правку
метками "Sunrise-Edit start" / "Sunrise-Edit end".
Content.Shared/_Scp/Holding/Components/ScpHoldRestrictedComponent.cs (1)

5-9: ⚠️ Potential issue | 🟠 Major

Синхронизируйте Stage у сетевого компонента.

Сейчас компонент помечен как NetworkedComponent, но его состояние не генерируется и Stage не попадает в auto-networked state. Это может рассинхронизировать ограничения действий на клиенте и сервере.

Предлагаемое исправление
-[RegisterComponent, NetworkedComponent]
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
 public sealed partial class ScpHoldRestrictedComponent : Component
 {
-    [DataField]
+    [DataField, AutoNetworkedField]
     public ScpHoldStage Stage = ScpHoldStage.Full;
 }

As per coding guidelines, "Apply the [AutoGenerateComponentState] attribute to components used in prediction" and "Ensure all predicted fields in networked components are marked with [AutoNetworkedField]".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Content.Shared/_Scp/Holding/Components/ScpHoldRestrictedComponent.cs` around
lines 5 - 9, The networked component ScpHoldRestrictedComponent currently has a
predicted field Stage that isn't included in auto-networked state; add the
[AutoGenerateComponentState] attribute to the ScpHoldRestrictedComponent class
and mark the Stage field with [AutoNetworkedField] so its value is included in
generated component state for prediction and syncing between client and server.
Resources/Prototypes/_Scp/Roles/Jobs/LowAccessPersonnel/class_d_botanist.yml (1)

26-34: ⚠️ Potential issue | 🟠 Major

ClassDBotanist все еще не должен получать ScpHolder.

Это расширяет механику удержания на low-access роль вне заявленного scope. Удержание SCP-096 должно оставаться у ОСН/целевой роли процедуры.

Предлагаемое исправление
-    - type: ScpHolder
-      holdableWhitelist:
-        components:
-        - Scp096
-      holdableBlacklist:
-        components:
-        - ActiveScp096Rage
-        - ActiveScp096HeatingUp
-        - ActiveScp096WithoutFace
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Resources/Prototypes/_Scp/Roles/Jobs/LowAccessPersonnel/class_d_botanist.yml`
around lines 26 - 34, В файле конфигурации роли ClassDBotanist удалите или
отключите блок типа ScpHolder (строки с "type: ScpHolder", "holdableWhitelist" и
"holdableBlacklist") чтобы ClassDBotanist больше не получает возможность
удерживать SCP-096; найдите упоминание "ClassDBotanist" и уберите весь связанный
подблок ScpHolder/holdableWhitelist/holdableBlacklist так, чтобы механика
удержания оставалась только у целевых SCP/ОСН ролей.
Resources/Prototypes/_Scp/Roles/Jobs/SpecialPurposeSquad/junior_heavy_containment_zone_officer.yml (1)

30-39: ⚠️ Potential issue | 🟠 Major

Уберите ClassDAppearance из whitelist.

Для ОСН здесь должен быть доступ к удержанию SCP-096, а ClassDAppearance расширяет механику на Class D/похожие цели.

Предлагаемое исправление
     - type: ScpHolder
       holdableWhitelist:
         components:
         - Scp096
-        - ClassDAppearance
       holdableBlacklist:
         components:
         - ActiveScp096Rage
         - ActiveScp096HeatingUp
         - ActiveScp096WithoutFace
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@Resources/Prototypes/_Scp/Roles/Jobs/SpecialPurposeSquad/junior_heavy_containment_zone_officer.yml`
around lines 30 - 39, В блоке конфигурации для ScpHolder удалите
ClassDAppearance из списка holdableWhitelist: оставьте только Scp096 в
holdableWhitelist компонентов, чтобы роль могла удерживать исключительно
SCP-096; убедитесь, что запись holdableWhitelist в сущности ScpHolder больше не
содержит ClassDAppearance (остальные blacklist-элементы, например
ActiveScp096Rage/HeatingUp/WithoutFace, оставьте без изменений).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Content.Client/_Scp/Holding/ScpHoldingSystem.cs`:
- Around line 199-229: The server-side path currently treats any
VirtualItemComponent with a matching BlockingEntity as an authoritative blocker
even if it lacks the marker component; update the loop so you check
_blockerQuery.HasComp(heldItem) for server-side holders before assigning
authoritativeBlocker (i.e. move or duplicate the _blockerQuery.HasComp check
into the !IsClientSide branch), keeping the existing behavior for
predictedBlocker and QueueDel; ensure references to _virtualItemQuery.TryComp,
VirtualItemComponent.BlockingEntity, IsClientSide, _blockerQuery.HasComp,
authoritativeBlocker, predictedBlocker and QueueDel are used to locate and apply
this change.

In `@Content.IntegrationTests/Tests/_Scp/ScpHoldingTwoClientCombatTest.cs`:
- Around line 435-459: The two local overloads named RunTicks that loop awaiting
WaitRunTicks(1) should be replaced with the standard test helpers that guarantee
server/client alignment: remove these custom RunTicks methods and instead call
the framework helpers (e.g. RunTicksSync(...) to advance server and all client
instances together and then SyncTicks(targetDelta: ...) after critical steps)
wherever the custom RunTicks was used; ensure you reference WaitRunTicks only
inside those helpers and use RunTicksSync and SyncTicks(targetDelta: ...) to
prevent cross-instance desync during replication/combat assertions.
- Around line 44-52: The test creates raw ServerIntegrationInstance and
ClientIntegrationInstance with Pool = false and using-var instead of the pooled
lifecycle; replace this by acquiring a pooled pair via
PoolManager.GetServerClient(CreateServerOptions/CreateClientOptions) using
"await using var pair = await PoolManager.GetServerClient(...)" and call
pair.CleanReturnAsync() (or explicit await pair.CleanReturnAsync() before
disposal) so the server/client are returned to the pool, or if you must keep
Pool = false add an explicit justification comment and implement deterministic
async cleanup for ServerIntegrationInstance and ClientIntegrationInstance
(ensure Connect and RunTicks usages remain unchanged); update all similar blocks
referenced (lines ~251-257 and ~382-432) to follow the same pattern.
- Around line 80-96: Move all mutations/side-effects (SpawnEntity calls,
sEntMan.EnsureComponent<ScpHolderComponent>(...),
sTransform.SetCoordinates(...), and any creation/positioning or calls like
GetCombatToggleAction(...)) out of WaitAssertion and into a WaitPost block so
they only run once and cannot be repeated on retry; leave only checks/assertions
(Assert.Multiple, Assert.That on targetSession.AttachedEntity and
holderSession.AttachedEntity, reading sTarget/sHolder values for assertions)
inside WaitAssertion. Ensure SpawnEntity is called once in WaitPost before the
WaitAssertion that asserts attached entities, and that WaitAssertion only
verifies state without performing mutations.

In `@Content.Server/_Scp/Holding/ScpHoldingSystem.cs`:
- Around line 17-23: In the OnHeldStateShutdown method, the foreach loop
iterates directly over held.Comp.Holders while the RemComp call triggers a chain
of events that removes elements from this same collection, which can cause
elements to be skipped during iteration. Fix this by creating a snapshot of the
Holders collection before iterating (for example, by calling ToList() on the
collection) and then iterate over that snapshot instead of the original
collection, ensuring all holders are properly processed even as the original
collection is modified.

In `@Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Actions.cs`:
- Around line 76-193: CanToggleHold (and CanStartHold / CanPassHoldAttempt)
currently produce side effects (Popup calls and event dispatches) which must be
removed so these methods are pure boolean checks; refactor by moving all
user-feedback and event-raising into the Try... layer (e.g., implement
TryToggleHold/TryStartHold that call CanToggleHold/CanStartHold then perform
Popup(holder, ...) and dispatch the hold attempt events only on success/failure
there), ensure CanToggleHold only computes and returns true/false and does not
call Popup, Try...must not call Can... in a way that triggers handlers, and
update related locations referenced by the review (around the Can... usages at
the other noted ranges) to follow OnEvent -> TryDo -> CanDo -> Do pattern and
avoid mutating components or raising events inside any Can... method.

In
`@Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.BreakoutAttempt.cs`:
- Around line 72-76: В cancel-ветке обработчика BreakoutAttempt (в
SharedScpHoldingSystem.BreakoutAttempt.cs) пометить отменённый do-after как
обработанный: перед вызовом return после Popup(ent,
"scp-hold-breakout-interrupted") установить args.Handled = true, чтобы
отменённое событие не оставалось доступным для дальнейшей обработки;
гарантировать, что логика для args.Cancelled и для успешного ветвления ведёт
себя консистентно.

In `@Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Restrictions.cs`:
- Around line 12-14: The shared predicted handler is calling the one-shot popup
directly (e.g., _popup.PopupClient inside the ActionAttemptEvent path) which can
cause duplicate popups during prediction replay; replace direct calls with the
shared predicted-safe mechanism by either using the Popup(...) hook or the
predicted helper (e.g., PopupPredicted/PlayPredicted) or guard the call with
IsFirstTimePredicted/ ApplyingState so the UX side-effect runs only once. Locate
uses of _popup.PopupClient in SharedScpHoldingSystem.Restrictions (and lines
~65-71) and refactor them to call the predicted popup helper or wrap them with
an IsFirstTimePredicted check.
- Around line 28-31: В обработчике OnRestrictRemove (удаление
ScpHoldRestrictedComponent по ComponentShutdown) не нужно вызывать
ValidateActions() — это приводит к повторному применению отключения действий и
оставляет их disabled после удаления компонента; уберите вызов ValidateActions()
из OnRestrictRemove или замените его на проверку реального состояния удержания
(например, проверять наличие компонента удержания/флага удержания у владельца) и
вызывать ValidateActions() только когда удержание действительно завершается
(функции/события, которые обрабатывают реальный релиз), сохранив ссылки на
ScpHoldRestrictedComponent, ComponentShutdown и метод ValidateActions для
навигации по коду.

In `@Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.State.cs`:
- Around line 76-85: EnsureHeldState currently updates
ActiveScpHoldableComponent.RequiredHolderCount without marking the component
dirty for network prediction; after setting held.RequiredHolderCount =
GetRequiredHolderCount(target) call Dirty(target, held) so the change is
replicated and prediction/reconciliation works correctly, and apply the same fix
wherever RequiredHolderCount is modified (the other block referenced in the diff
that updates RequiredHolderCount) to call Dirty(uid, comp) after each
assignment.

In `@Content.Shared/_Scp/Scp096/Main/Systems/SharedScp096System.Holding.cs`:
- Around line 45-56: held.Holders can contain stale UIDs so before calling
ApplyHoldBreakoutEffects you must filter to only the current active holder(s):
check that each HolderUid still has an ActiveScpHolderComponent whose Target
equals ent.Owner and skip any that don’t match; update the creation of the
holders array (or the second loop) to call
_entityManager.TryGetComponent/HasComponent for ActiveScpHolderComponent and
compare ActiveScpHolderComponent.Target == ent.Owner, only then compute position
via _transform.GetWorldPosition(holderUid) and call ApplyHoldBreakoutEffects for
that holder.

In
`@Resources/Prototypes/_Scp/Roles/Jobs/CommandantSquad/junior_external_administrative_zone_officer.yml`:
- Around line 30-36: The new ScpHolder block (the added fragment containing
ScpHolder with holdableWhitelist and holdableBlacklist entries, including
HumanoidAppearance and Scp) lacks the required Sunrise fork marker; insert a
single-line comment "# Sunrise-Edit" immediately above the ScpHolder mapping to
mark this YAML change as a Sunrise edit, ensuring the marker follows the
coding-guideline format for YAML/FTL/Python/Shell files and does not alter the
existing keys (ScpHolder, holdableWhitelist, holdableBlacklist).

In `@Resources/Prototypes/_Scp/Roles/Jobs/LowAccessPersonnel/class_d_janitor.yml`:
- Around line 23-31: В файле роли ClassDJanitor удалите или откатите блок
ScpHolder (включая поля holdableWhitelist/holdableBlacklist и ссылку на Scp096),
потому что low-access роль не должна иметь возможность удерживать SCP-096; либо
переместите этот ScpHolder-конфиг в правильную роль с ОСН-скоупом.
Обновите/проверьте упоминания Scp096 в полях
holdableWhitelist/holdableBlacklist, чтобы гарантировать, что только роли с
корректным scope (ОСН) содержат эту возможность.

In `@Resources/Prototypes/_Scp/Roles/Jobs/LowAccessPersonnel/class_d.yml`:
- Around line 21-29: В файле ClassD удалите или отключите блок типа ScpHolder
(ключи type: ScpHolder, holdableWhitelist: components: - Scp096), т.к. он даёт
обычным ClassD возможность удерживать SCP-096; вместо этого перенесите либо
добавьте аналогичный ScpHolder-блок только в файлы ролей командного состава и
ОСН (например роли, которые отвечают за командный состав/ChaosInsurgency) или
ограничьте доступ в этом блоке конкретными флагами/ролью, чтобы только эти
специализированные роли могли удерживать Scp096.

In
`@Resources/Prototypes/_Scp/Roles/Jobs/SpecialPurposeSquad/heavy_containment_zone_officer.yml`:
- Around line 31-40: The added ScpHolder block (the block starting with "type:
ScpHolder" containing holdableWhitelist/holdableBlacklist and components like
Scp096 and ClassDAppearance) lacks the required Sunrise fork marker; insert a
single-line comment "# Sunrise-Edit" immediately above that ScpHolder block so
the YAML change is clearly marked as a Sunrise fork edit, ensuring the marker
uses exact capitalization and spacing.

---

Duplicate comments:
In `@Content.Shared/_Scp/Holding/Components/ScpHoldRestrictedComponent.cs`:
- Around line 5-9: The networked component ScpHoldRestrictedComponent currently
has a predicted field Stage that isn't included in auto-networked state; add the
[AutoGenerateComponentState] attribute to the ScpHoldRestrictedComponent class
and mark the Stage field with [AutoNetworkedField] so its value is included in
generated component state for prediction and syncing between client and server.

In `@Resources/Prototypes/_Scp/Roles/Jobs/Administration/security_commander.yml`:
- Around line 36-39: The ScpHolder config for Security Commander currently
limits holds to ClassDAppearance (ScpHolder with holdableWhitelist.components
including ClassDAppearance); change it to use the general humanoid whitelist and
apply the same SCP blacklist used by CommandantSquad: replace the
holdableWhitelist.components entry that references ClassDAppearance with the
shared humanoid whitelist identifier (the same whitelist key used by
CommandantSquad) and ensure the ScpHolder includes the identical scp
blacklist/settings from CommandantSquad so Security Commander holds all
personnel rather than only D-class.

In
`@Resources/Prototypes/_Scp/Roles/Jobs/LowAccessPersonnel/class_d_botanist.yml`:
- Around line 26-34: В файле конфигурации роли ClassDBotanist удалите или
отключите блок типа ScpHolder (строки с "type: ScpHolder", "holdableWhitelist" и
"holdableBlacklist") чтобы ClassDBotanist больше не получает возможность
удерживать SCP-096; найдите упоминание "ClassDBotanist" и уберите весь связанный
подблок ScpHolder/holdableWhitelist/holdableBlacklist так, чтобы механика
удержания оставалась только у целевых SCP/ОСН ролей.

In
`@Resources/Prototypes/_Scp/Roles/Jobs/SpecialPurposeSquad/junior_heavy_containment_zone_officer.yml`:
- Around line 30-39: В блоке конфигурации для ScpHolder удалите ClassDAppearance
из списка holdableWhitelist: оставьте только Scp096 в holdableWhitelist
компонентов, чтобы роль могла удерживать исключительно SCP-096; убедитесь, что
запись holdableWhitelist в сущности ScpHolder больше не содержит
ClassDAppearance (остальные blacklist-элементы, например
ActiveScp096Rage/HeatingUp/WithoutFace, оставьте без изменений).

In `@Resources/Prototypes/Entities/Mobs/Species/base.yml`:
- Around line 281-282: Удалите маркер типа ScpHolder из базового органического
прототипа и оставьте только ScpHoldable (убирается строка с "type: ScpHolder" и
не трогать/восстановить "type: ScpHoldable"), чтобы не давать всем органикам
механики держателя в обход job `special` и ролевых ограничений; если это
неизбежный апстрим-фикс — поместите изменения в проектную папку _Sunrise,
используйте префикс Sunrise для необходимых идентификаторов и обрамите правку
метками "Sunrise-Edit start" / "Sunrise-Edit end".
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: c112d4df-81d1-44b7-b0b5-a9d2d8fb7314

📥 Commits

Reviewing files that changed from the base of the PR and between e43a8f6 and 807521b.

📒 Files selected for processing (45)
  • Content.Client/_Scp/Holding/ScpHoldingSystem.Feedback.cs
  • Content.Client/_Scp/Holding/ScpHoldingSystem.cs
  • Content.IntegrationTests/Tests/_Scp/ScpHoldingTest.cs
  • Content.IntegrationTests/Tests/_Scp/ScpHoldingTwoClientCombatTest.cs
  • Content.Server/_Scp/Holding/ScpHoldingSystem.Feedback.cs
  • Content.Server/_Scp/Holding/ScpHoldingSystem.cs
  • Content.Shared/_Scp/Holding/Compatibility/PullingSystem.ScpHolding.cs
  • Content.Shared/_Scp/Holding/Components/ActiveScpHoldableComponent.cs
  • Content.Shared/_Scp/Holding/Components/ActiveScpHolderComponent.cs
  • Content.Shared/_Scp/Holding/Components/ActiveStateScpHoldableFullHoldComponent.cs
  • Content.Shared/_Scp/Holding/Components/ActiveStateScpHolderSlowdownComponent.cs
  • Content.Shared/_Scp/Holding/Components/ScpBreakoutAttemptComponent.cs
  • Content.Shared/_Scp/Holding/Components/ScpHeldHandBlockerComponent.cs
  • Content.Shared/_Scp/Holding/Components/ScpHoldHandBlockerComponent.cs
  • Content.Shared/_Scp/Holding/Components/ScpHoldImmuneComponent.cs
  • Content.Shared/_Scp/Holding/Components/ScpHoldRestrictedComponent.cs
  • Content.Shared/_Scp/Holding/Components/ScpHoldableComponent.cs
  • Content.Shared/_Scp/Holding/Components/ScpHolderComponent.cs
  • Content.Shared/_Scp/Holding/ScpHoldingEvents.cs
  • Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Actions.cs
  • Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.BreakoutAttempt.cs
  • Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Drag.cs
  • Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Feedback.cs
  • Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Hands.cs
  • Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Lifecycle.cs
  • Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Restrictions.cs
  • Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.State.cs
  • Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.cs
  • Content.Shared/_Scp/Scp096/Main/Components/Scp096Component.cs
  • Content.Shared/_Scp/Scp096/Main/Systems/SharedScp096System.Holding.cs
  • Resources/Prototypes/Entities/Mobs/Species/base.yml
  • Resources/Prototypes/_Scp/Roles/Jobs/Administration/security_commander.yml
  • Resources/Prototypes/_Scp/Roles/Jobs/CommandantSquad/external_administrative_zone_commandant.yml
  • Resources/Prototypes/_Scp/Roles/Jobs/CommandantSquad/external_administrative_zone_officer.yml
  • Resources/Prototypes/_Scp/Roles/Jobs/CommandantSquad/field_medical_specialist.yml
  • Resources/Prototypes/_Scp/Roles/Jobs/CommandantSquad/junior_external_administrative_zone_officer.yml
  • Resources/Prototypes/_Scp/Roles/Jobs/CommandantSquad/senior_external_administrative_zone_officer.yml
  • Resources/Prototypes/_Scp/Roles/Jobs/LowAccessPersonnel/class_d.yml
  • Resources/Prototypes/_Scp/Roles/Jobs/LowAccessPersonnel/class_d_botanist.yml
  • Resources/Prototypes/_Scp/Roles/Jobs/LowAccessPersonnel/class_d_cook.yml
  • Resources/Prototypes/_Scp/Roles/Jobs/LowAccessPersonnel/class_d_janitor.yml
  • Resources/Prototypes/_Scp/Roles/Jobs/SpecialPurposeSquad/heavy_containment_zone_commandant.yml
  • Resources/Prototypes/_Scp/Roles/Jobs/SpecialPurposeSquad/heavy_containment_zone_officer.yml
  • Resources/Prototypes/_Scp/Roles/Jobs/SpecialPurposeSquad/junior_heavy_containment_zone_officer.yml
  • Resources/Prototypes/_Scp/Roles/Jobs/SpecialPurposeSquad/senior_heavy_containment_zone_officer.yml

Comment thread Content.Client/_Scp/Holding/ScpHoldingSystem.cs Outdated
Comment thread Content.IntegrationTests/Tests/_Scp/ScpHoldingTwoClientCombatTest.cs Outdated
Comment thread Content.IntegrationTests/Tests/_Scp/ScpHoldingTwoClientCombatTest.cs Outdated
Comment thread Content.IntegrationTests/Tests/_Scp/ScpHoldingTwoClientCombatTest.cs Outdated
Comment thread Content.Server/_Scp/Holding/ScpHoldingSystem.cs
Comment thread Resources/Prototypes/_Scp/Roles/Jobs/LowAccessPersonnel/class_d.yml
ThereDrD0 and others added 3 commits April 18, 2026 03:52
@ThereDrD0
Copy link
Copy Markdown
Collaborator Author

@coderabbitai full review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 19, 2026

✅ Actions performed

Full review triggered.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 30

♻️ Duplicate comments (12)
Resources/Prototypes/_Scp/Roles/Jobs/Administration/security_commander.yml (1)

36-39: ⚠️ Potential issue | 🟠 Major

holdableWhitelist не соответствует охвату фичи.

По целям PR командный состав должен иметь возможность удерживать весь персонал, однако ScpHolder у Security Commander ограничен только ClassDAppearance. Расширьте whitelist (например, на HumanoidAppearance с blacklist Scp, по аналогии с SeniorExternalAdministrativeZoneOfficer) либо уточните требования.

💡 Предлагаемое исправление
     - type: ScpHolder
       holdableWhitelist:
         components:
-        - ClassDAppearance
+        - HumanoidAppearance
+      holdableBlacklist:
+        components:
+        - Scp
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Resources/Prototypes/_Scp/Roles/Jobs/Administration/security_commander.yml`
around lines 36 - 39, Расширьте конфигурацию ScpHolder для роли Security
Commander: вместо единственного элемента в holdableWhitelist (ClassDAppearance)
добавьте также HumanoidAppearance и добавьте соответствующий blacklist (Scp) по
аналогии с реализацией в SeniorExternalAdministrativeZoneOfficer; убедитесь, что
ключи holdableWhitelist и blacklist правильно указаны и соответствуют остальной
структуре файла.
Resources/Prototypes/Actions/types.yml (1)

264-265: ⚠️ Potential issue | 🟡 Minor

Замените # Fire edit на # Sunrise-Edit.

Resources/Prototypes/Actions/types.yml — upstream-файл, правка должна помечаться Sunrise-маркером.

💡 Предлагаемое исправление
-  - type: ScpHoldRestricted # Fire edit - block combat mode while held
+  - type: ScpHoldRestricted # Sunrise-Edit - block combat mode while held
     stage: Soft

As per coding guidelines: "use the Sunrise prefix … and Sunrise-Edit / Sunrise edit start/end markers for unavoidable upstream edits."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Resources/Prototypes/Actions/types.yml` around lines 264 - 265, В блоке, где
определён type ScpHoldRestricted (the YAML mapping with "type:
ScpHoldRestricted" and "stage: Soft"), замените комментарий "# Fire edit - block
combat mode while held" на маркер Sunrise согласно гайду — используйте именно "#
Sunrise-Edit" (или при многострочных изменениях обрамляющие "Sunrise edit
start"/"Sunrise edit end"), чтобы правка помечалась как upstream
Sunrise-изменение.
Resources/Prototypes/Entities/Mobs/Species/base.yml (1)

280-282: ⚠️ Potential issue | 🔴 Critical

ScpHolder в BaseMobSpeciesOrganic + неверный маркер форка.

  1. ScpHolder добавлен в базовый органический прототип с TODO: Убрать перед мержем — в итоге механика удержания выдаётся всем органикам, а не только целевым ролям через special в job'ах (Security Commander / Senior External Administrative Zone Officer). Это ломает ролевые ограничения, заявленные в PR.
  2. Блок помечен # Fire start / # Fire end — это upstream-файл, для него должны использоваться маркеры # Sunrise-Edit / # Sunrise edit start-# Sunrise edit end.

Уберите ScpHolder до мержа и замените маркеры.

💡 Предлагаемое исправление
-  # Fire start
-  - type: ScpHolder # TODO: Убрать перед мержем
-  - type: ScpHoldable
+  # Sunrise edit start
+  - type: ScpHoldable
+  # Sunrise edit end

As per coding guidelines: "use the Sunrise prefix, the _Sunrise project folder, and Sunrise-Edit / Sunrise edit start/end markers for unavoidable upstream edits."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Resources/Prototypes/Entities/Mobs/Species/base.yml` around lines 280 - 282,
Remove the unintended ScpHolder entry from the BaseMobSpeciesOrganic prototype
(the line with "type: ScpHolder" and its TODO) so holding mechanics are not
granted to all organics; leave or keep "type: ScpHoldable" only if that is
required for the specific role mechanics. Also replace the upstream fork markers
"# Fire start" / "# Fire end" with the required Sunrise markers: add a single "#
Sunrise-Edit" header and wrap the local modification block with "# Sunrise edit
start" and "# Sunrise edit end" (use the Sunrise prefix for any project-specific
markers/folders) to mark the unavoidable upstream edit. Ensure references to
ScpHolder and BaseMobSpeciesOrganic are the targets of the change.
Resources/Prototypes/_Scp/Roles/Jobs/LowAccessPersonnel/class_d_janitor.yml (1)

23-31: ⚠️ Potential issue | 🟠 Major

Не выдавайте удержание SCP-096 роли ClassDJanitor.

Это low-access роль, а заявленный scope PR — Command Squad для персонала и ОСН для SCP-096. Сейчас ClassDJanitor сможет участвовать в удержании SCP-096 вне целевого баланса.

Предлагаемая правка
-    - type: ScpHolder
-      holdableWhitelist:
-        components:
-        - Scp096
-      holdableBlacklist:
-        components:
-        - ActiveScp096Rage
-        - ActiveScp096HeatingUp
-        - ActiveScp096WithoutFace
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Resources/Prototypes/_Scp/Roles/Jobs/LowAccessPersonnel/class_d_janitor.yml`
around lines 23 - 31, В роли ClassDJanitor удалите или отключите блок ScpHolder
(т.е. удалите ключи type: ScpHolder и связанные
holdableWhitelist/holdableBlacklist), чтобы ClassDJanitor больше не имел
возможности участвовать в удержании SCP-096; найдите секцию с именем
ClassDJanitor и ключами ScpHolder / holdableWhitelist / holdableBlacklist и
удалите её целиком или замените на разрешённый для low-access набор, если нужно
сохранить структуру.
Resources/Prototypes/_Scp/Roles/Jobs/CommandantSquad/junior_external_administrative_zone_officer.yml (1)

30-36: ⚠️ Potential issue | 🟡 Minor

Добавьте # Sunrise-Edit к добавленному ScpHolder.

Блок добавлен в существующий YAML-прототип без обязательного Sunrise-маркера.

Предлагаемая правка
-    - type: ScpHolder
+    - type: ScpHolder # Sunrise-Edit
       holdableWhitelist:
         components:
         - HumanoidAppearance

As per coding guidelines, **/*.{yaml,yml,ftl,py,sh}: Use # Sunrise-Edit marker format in YAML, FTL, Python, and Shell script files for Sunrise fork edits.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@Resources/Prototypes/_Scp/Roles/Jobs/CommandantSquad/junior_external_administrative_zone_officer.yml`
around lines 30 - 36, Добавленный блок ScpHolder не содержит обязательного
маркера Sunrise и должен быть помечен; перед строкой с "type: ScpHolder"
вставьте строку с комментарием "# Sunrise-Edit" (в том же YAML-уровне отступа),
чтобы отметить правку по правилам для файлов YAML/FTL/PY/SH; проверьте блокы с
идентификаторами ScpHolder, holdableWhitelist и holdableBlacklist чтобы маркер
однозначно относится к этому добавленному прототипу.
Resources/Prototypes/_Scp/Roles/Jobs/LowAccessPersonnel/class_d_botanist.yml (1)

26-34: ⚠️ Potential issue | 🟠 Major

ClassDBotanist не должен получать удержание SCP-096.

Это low-access роль вне заявленного scope PR. Переименование компонента на ScpHolder не меняет проблему: роль всё ещё получает возможность удерживать SCP-096.

Предлагаемая правка
-    - type: ScpHolder
-      holdableWhitelist:
-        components:
-        - Scp096
-      holdableBlacklist:
-        components:
-        - ActiveScp096Rage
-        - ActiveScp096HeatingUp
-        - ActiveScp096WithoutFace
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Resources/Prototypes/_Scp/Roles/Jobs/LowAccessPersonnel/class_d_botanist.yml`
around lines 26 - 34, ClassDBotanist currently gets Scp096 via the ScpHolder
block — remove Scp096 from its holdableWhitelist (or remove the ScpHolder entry
entirely for ClassDBotanist) so the role cannot hold SCP-096; specifically edit
the ClassDBotanist definition to either delete the ScpHolder section or ensure
holdableWhitelist does not list Scp096 (or add Scp096 to holdableBlacklist) to
prevent granting hold ability.
Resources/Prototypes/_Scp/Roles/Jobs/LowAccessPersonnel/class_d.yml (1)

21-29: ⚠️ Potential issue | 🟠 Major

Не выдавайте удержание обычному ClassD.

По scope PR удержание должно быть у командного состава и ОСН для SCP-096; этот блок даёт low-access роли возможность удерживать SCP-096 и расширяет доступ к механике.

Предлагаемое исправление
-    - type: ScpHolder
-      holdableWhitelist:
-        components:
-        - Scp096
-      holdableBlacklist:
-        components:
-        - ActiveScp096Rage
-        - ActiveScp096HeatingUp
-        - ActiveScp096WithoutFace
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Resources/Prototypes/_Scp/Roles/Jobs/LowAccessPersonnel/class_d.yml` around
lines 21 - 29, В файле роли ClassD удалите или отключите блок ScpHolder, который
даёт low-access ролям возможность удерживать SCP-096: конкретно уберите
holdableWhitelist с компонентом Scp096 (и связанные
holdableBlacklist/ActiveScp096* записи) из class_d.yml; вместо этого
добавьте/оставьте этот ScpHolder только в ролях командного состава и ОСН (где
требуется механика), т.е. перенесите или создайте аналогичный блок ScpHolder с
holdableWhitelist: components: - Scp096 в соответствующих ролях команд/ОСН и
убедитесь, что class_d.yml больше не содержит компонента Scp096 в списках
удержания.
Resources/Prototypes/_Scp/Roles/Jobs/SpecialPurposeSquad/heavy_containment_zone_officer.yml (1)

31-40: ⚠️ Potential issue | 🟡 Minor

Добавьте # Sunrise-Edit к добавленному блоку ScpHolder.

Блок всё ещё добавлен без fork-маркера на строке начала изменения.

Предлагаемая правка
-    - type: ScpHolder
+    - type: ScpHolder # Sunrise-Edit
       holdableWhitelist:
         components:
         - Scp096

As per coding guidelines, **/*.{yaml,yml,ftl,py,sh}: Use '# Sunrise-Edit' marker format in YAML, FTL, Python, and Shell script files for Sunrise fork edits.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@Resources/Prototypes/_Scp/Roles/Jobs/SpecialPurposeSquad/heavy_containment_zone_officer.yml`
around lines 31 - 40, The added ScpHolder block (the mapping starting with
"type: ScpHolder" and containing holdableWhitelist and holdableBlacklist) is
missing the required fork marker; insert a YAML comment line "# Sunrise-Edit"
immediately above the ScpHolder block (i.e., directly before the "type:
ScpHolder" key) so the change is marked as a Sunrise fork edit while leaving the
existing keys (holdableWhitelist, holdableBlacklist, components, etc.)
unchanged.
Content.Shared/Movement/Pulling/Systems/PullingSystem.cs (1)

532-535: ⚠️ Potential issue | 🔴 Critical

Не редиректите SCP-hold до общей валидации pull-flow.

Line 533 возвращает результат TryRedirectPullToScpHold() до CanPull() и до PullAttemptEvent, поэтому SCP-hold путь обходит NeedsHands, CanInteract, container/buckle guards и veto-события StartPullAttemptEvent / BeingPulledAttemptEvent / PullAttemptEvent. Подписчики pull-системы не смогут запретить действие; вынесите общие guards/events в общий helper или вызывайте их и для redirect-пути.

As per coding guidelines, Enforce the interaction rule from .agent/rules/ss14-interaction-flow.md: OnEvent -> TryDo -> CanDo -> Do.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Content.Shared/Movement/Pulling/Systems/PullingSystem.cs` around lines 532 -
535, Вызов TryRedirectPullToScpHold возвращает результат до выполнения общих
проверок/событий и тем самым обходит CanPull(),
StartPullAttemptEvent/BeingPulledAttemptEvent/PullAttemptEvent и guards
(NeedsHands, CanInteract, container/buckle), поэтому измените порядок/реализацию
так, чтобы общие валидации и публикация стартовых veto-событий выполнялись перед
перенаправлением: либо переместите вызовы CanPull() и генерацию
StartPullAttemptEvent/BeingPulledAttemptEvent/PullAttemptEvent в общий helper и
вызовите его и из основного пути, и из TryRedirectPullToScpHold, либо вызовите
эти проверки/события сразу перед TryRedirectPullToScpHold и только затем
применяйте redirect; используйте идентификаторы TryRedirectPullToScpHold,
CanPull, StartPullAttemptEvent, BeingPulledAttemptEvent, PullAttemptEvent,
NeedsHands и CanInteract для поиска мест в коде.
Resources/Locale/en-US/_strings/_scp/holding/holding.ftl (1)

14-14: ⚠️ Potential issue | 🟡 Minor

Пометьте подсказку управления как OOC:.

Фраза “Move or click this alert to try to break free” — это инструкция игроку, а не внутриигровое описание.

💬 Предлагаемая правка
-alerts-scp-held-desc = Someone is physically gripping you. Move or click this alert to try to break free. In a hard restraint you must endure the hold before the breakout starts.
+alerts-scp-held-desc = Someone is physically gripping you. OOC: Move or click this alert to try to break free. In a hard restraint you must endure the hold before the breakout starts.

As per coding guidelines, “Require explicit OOC: markers for out-of-character hints, controls, or gameplay instructions.”

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Resources/Locale/en-US/_strings/_scp/holding/holding.ftl` at line 14, The
string value for alerts-scp-held-desc contains an out-of-character control
instruction; update the alerts-scp-held-desc message so the player instruction
is explicitly marked with "OOC:" — e.g., prepend or insert "OOC:" before "Move
or click this alert to try to break free" so the line reads as an in-world
description followed by an OOC control hint, leaving the rest of the sentence
unchanged.
Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Restrictions.cs (2)

28-31: ⚠️ Potential issue | 🟠 Major

Не пере-применяйте restriction во время удаления компонента.

ValidateActions() на ComponentShutdown может оставить action выключенным уже после удаления ScpHoldRestrictedComponent.

🐛 Предлагаемая правка
-        SubscribeLocalEvent<ScpHoldRestrictedComponent, ComponentShutdown>(OnRestrictRemove);
         SubscribeLocalEvent<ScpHoldRestrictedComponent, ActionAttemptEvent>(OnHoldRestrictedActionAttempt);
     }
@@
-    private void OnRestrictRemove(Entity<ScpHoldRestrictedComponent> ent, ref ComponentShutdown args)
-    {
-        ValidateActions(ent.AsNullable());
-    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Restrictions.cs`
around lines 28 - 31, Метод OnRestrictRemove(Entity<ScpHoldRestrictedComponent>
ent, ref ComponentShutdown args) не должен вызывать ValidateActions() на
ComponentShutdown — это пере-применяет restriction и может оставить действия
отключёнными после удаления компонента. Уберите вызов
ValidateActions(ent.AsNullable()) из OnRestrictRemove и переместите логику в
обработчик удаления компонента (например, обработчик
ComponentRemove/ComponentRemoved для Entity<ScpHoldRestrictedComponent>) или
вызывайте ValidateActions только после окончательного удаления (не в
ComponentShutdown), чтобы ValidateActions выполнялся когда компонент
действительно удалён.

65-71: ⚠️ Potential issue | 🟠 Major

Не вызывайте one-shot popup напрямую из shared predicted path.

ActionAttemptEvent может проходить через prediction replay, поэтому прямой _popup.PopupClient(...) рискует дать дублирующиеся popups.

🐛 Предлагаемая правка
-        _popup.PopupClient(Loc.GetString("scp-hold-action-restricted"), args.User, args.User);
         args.Cancelled = true;
+        Popup(args.User, "scp-hold-action-restricted");

As per coding guidelines, Content.Shared/**/*.cs: “Fail if shared predicted code adds predicted side effects such as PlayPvs, PopupEntity, or similar one-shot UX effects without IsFirstTimePredicted, ApplyingState, or a predicted helper such as PlayPredicted / PopupPredicted.”

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Restrictions.cs`
around lines 65 - 71, OnHoldRestrictedActionAttempt currently calls
_popup.PopupClient directly in a shared predicted path which can produce
duplicate popups during prediction replay; change the code to only emit the
one-shot UX when this is the first predicted execution by guarding with the
prediction check (e.g., IsFirstTimePredicted on the ActionAttemptEvent) or by
routing the notification through the predicted helper (e.g., use a
PopupPredicted-style helper instead of _popup.PopupClient), keeping the existing
cancellation behavior (args.Cancelled = true) and the IsHeldAtStage check
intact.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Content.Client/_Scp/Holding/ScpHoldingSystem.cs`:
- Around line 99-136: In OnUpdateHeldPredicted, fix the inconsistent indentation
of the if ((EntityUid) ent == local) block: remove the extra nesting level so
the block body aligns with the other if blocks in the method (adjust the lines
that set args.IsPredicted = true; return; inside that if), keeping the same
logic and using the existing ent, local, and args symbols.

In `@Content.Server/Movement/Systems/PullController.cs`:
- Around line 125-128: The fork markers used in PullController.cs are incorrect;
replace the `Fire edit start`/`Fire edit end` markers around the upstream edit
that routes cursor-move through _scpHolding.TryMoveHeldToCursor(player, coords)
with the repository-standard Sunrise markers (use `Sunrise-Edit` for file header
and `Sunrise edit start` / `Sunrise edit end` around the changed block) so the
edit is correctly identified as an upstream modification.
- Around line 2-4: В файле PullController.cs удалите неиспользуемые импорты:
уберите строки с using Content.Shared._Scp.Holding.Components и using
Content.Shared._Scp.Holding.Systems, оставив только using
Content.Server._Scp.Holding (поскольку код использует только ScpHoldingSystem);
после удаления пересоберите/проверьте отсутствие ошибок и выполните
форматирование файла.

In `@Content.Shared/_Scp/Holding/Compatibility/PullingSystem.ScpHolding.cs`:
- Around line 45-65: Update the XML doc for TryRedirectPullToScpHold to
explicitly state return semantics: "Returns true if the pull was
handled/consumed (including being rejected); the out parameter 'result'
indicates whether the redirect succeeded." Mention that this covers the code
paths that call _scpHolding.CanToggleHold, TryStopPull and
_scpHolding.TryToggleHold so callers understand that true does not necessarily
mean success. Optionally rename the out parameter 'result' to 'success' (or
rename the method to indicate "handled") for clarity; if renaming, update all
call sites (e.g., PullingSystem.TryStartPull) to use the new name.

In
`@Content.Shared/_Scp/Holding/Components/ActiveStateScpHoldableCursorMoveComponent.cs`:
- Around line 17-18: Поле Holder в компоненте
ActiveStateScpHoldableCursorMoveComponent должно быть nullable (EntityUid?
Holder) так как текущее значение EntityUid.Invalid может быть ошибочно принято
за валидную цель; измените объявление Holder на nullable, обновите места
использования (включая методы SetCursorMoveState и ClearCursorMoveState) чтобы
проверять Holder.HasValue перед доступом и распаковывать через Holder.Value там,
где требуется, и при очистке состояния удалять/обнулять компонент как сейчас.

In `@Content.Shared/_Scp/Holding/Components/ScpBreakoutAttemptComponent.cs`:
- Around line 9-10: ScpBreakoutAttemptComponent is a shared, networked component
and needs the AutoGenerateComponentState attribute to enable proper
serialization/deserialization and prediction rollback; add the
[AutoGenerateComponentState] attribute to the ScpBreakoutAttemptComponent
declaration (alongside existing [RegisterComponent, NetworkedComponent] and
[Access(typeof(SharedScpHoldingSystem))]) so the component state is
auto-generated for network syncing.

In `@Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Actions.cs`:
- Around line 23-46: The popup is showing the raw localization key because
PopupClient is being passed the literal "scp-hold-already-holding-other"; update
the PopupClient calls to pass the localized string instead (use
Loc.GetString("scp-hold-already-holding-other") where PopupClient is invoked in
TryToggleHold and the other PopupClient invocation later in this file) so the
user sees the translated message.
- Around line 246-267: In TryStartFullBreakout, when fullHeld.StartedAt ==
TimeSpan.Zero do not call Loc.GetString("scp-hold-breakout-too-early",
("seconds", 1)) because it misreports remaining time; instead show a distinct
"not ready" message (e.g. use a new localization key like
"scp-hold-breakout-not-ready") via _popup.PopupClient(Loc.GetString(...), held),
or alternatively compute remaining time from a valid start time if one can be
derived — update the branch that checks fullHeld.StartedAt to use the new key
(or computed remaining) while keeping the existing logic that handles the normal
breakoutAvailableAt path.
- Around line 325-335: The ApplyFullBreakoutHolderCooldown method uses
TimeSpan.FromTicks(hold.HoldActionCooldown.Ticks * 2) which is equivalent to
hold.HoldActionCooldown * 2; update the calculation to use
hold.HoldActionCooldown * 2 to simplify and clarify intent (leave the rest of
the logic — the TryComp check, cooldownEnd comparison against
hold.HoldAvailableAt, and the SetHoldAvailableAt((holderUid, hold), cooldownEnd)
call — unchanged).

In
`@Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.BreakoutAttempt.cs`:
- Around line 136-149: В методе ShowBreakoutAttemptFeedback вы вызываете
Comp<ScpHolderComponent>(holderUid) без проверки и это бросит, если конфигурация
была удалена; замените этот вызов на TryComp<ScpHolderComponent>(holderUid, out
var scpHolderComp) и, если TryComp вернул false, пропускайте этого holderUid
(как уже делаете при отсутствии ActiveScpHolderComponent), иначе берите
scpHolderComp.BreakoutAttemptAlertSettings и передавайте их в
_worldAlert.TrySpawnAlert(holderUid, ...); сохраните текущую финальную отправку
_worldAlert.TrySpawnAlert(held, holdable.BreakoutAttemptAlertSettings) без
изменений.

In `@Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.cs`:
- Around line 60-64: The override of Update in SharedScpHoldingSystem currently
calls only UpdateSharedState() and UpdateHeldStates() and omits calling
base.Update(frameTime); modify the Update(float frameTime) method to invoke
base.Update(frameTime) (either before or after the helper calls) to conform to
EntitySystem lifecycle conventions and ensure base class behavior is preserved.

In `@Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.CursorMove.cs`:
- Around line 41-82: The parameter quiet on CanMoveHeldToCursor is unused (only
_ = quiet;) — either remove the parameter and the discard or actually thread
quiet into the same user-facing checks/popups as CanToggleHold: update
CanMoveHeldToCursor to pass quiet into any validation methods or popup calls
(e.g., any checks that emit messages or the
TryClampHeldCursorMoveTargetCoordinates/_interaction checks) so the method can
suppress UI when requested, or remove quiet and the discard to avoid dead code
and update all call sites accordingly.
- Around line 270-285: В обработчике OnHolderMove текущая ранняя отрезка
учитывает только совпадение EntityId и точное равенство позиции, из‑за чего
смена родителя (re-parent) с той же позицией всё равно считается изменением;
исправьте условие так, чтобы также проверять родителя/корневой EntityId и
сравнивать позиции с допуском (используйте EntityCoordinates.TryDelta или
сравнение LengthSquared() с маленьким epsilon вместо точного 0f), и только если
и родитель совпадает и дельта позиции меньше epsilon — вернуть; в остальных
случаях продолжать и не вызывать ClearCursorMoveState для чистого re-parent-а.
- Around line 196-200: The code currently calls Dirty(held, cursorMove) after
updating fields on ActiveStateScpHoldableCursorMoveComponent (cursorMove.Holder,
cursorMove.TargetCoordinates, cursorMove.Active); replace those calls with
point-updates using DirtyField(held, cursorMove,
nameof(ActiveStateScpHoldableCursorMoveComponent.<Property>)) for each changed
property (i.e., call DirtyField(..., nameof(Holder)) after setting Holder,
DirtyField(..., nameof(TargetCoordinates)) after setting TargetCoordinates, and
DirtyField(..., nameof(Active)) after setting Active). Apply the same
replacement to the other similar update sites that set those properties on the
same component (the other blocks that currently call Dirty(...)).

In `@Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Hands.cs`:
- Around line 128-143: In EnsureHeldHandBlockers, after calling
_hands.DoPickup(held, emptyHand, virtualItem.Value, held.Comp) verify the pickup
actually filled the hand (use _hands.TryGetEmptyHand or the component/state that
indicates empty vs filled) and if the hand remains empty increment a local
failedAttempts counter and break (or bail out) once a small threshold is
reached; alternatively break immediately on failure to avoid repeatedly spawning
virtualItem. Reference EnsureHeldHandBlockers, _hands.TryGetEmptyHand,
_hands.DoPickup and virtualItem to locate and guard the loop.

In `@Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Lifecycle.cs`:
- Around line 79-86: In OnHolderShutdown, set ActiveScpHolderComponent.Target to
null with a corresponding dirty call so the network delta is sent; after
assigning Target = null invoke the existing Dirty helper (or DirtyField) for the
entity/component to mark the networked field dirty (use Dirty(ent, ent.Comp) or
DirtyField(ent, ent.Comp, nameof(ActiveScpHolderComponent.Target)) so the change
is propagated).

In `@Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.State.cs`:
- Around line 126-129: The code currently uses an else if that prevents removing
both components when a holder has both ActiveScpHolderComponent and
ActiveStateScpHolderSlowdownComponent; update the logic in the removal sites so
you call RemComp<ActiveScpHolderComponent>(holderUid) and
RemComp<ActiveStateScpHolderSlowdownComponent>(holderUid) from two separate if
checks using _activeHolderQuery.HasComp(holderUid) and
_activeHolderSlowdownStateQuery.HasComp(holderUid) respectively (replace the
else if with an independent if), and apply the same change in ClearHoldState so
both components are removed independently (do not rely on OnHolderShutdown
side-effects).

In `@Content.Shared/_Scp/Other/WorldAlert/WorldAlertSystem.cs`:
- Around line 10-13: Member block ordering is wrong: move the dependency fields
(_audio and _net with [Dependency]) to appear before the constant
DefaultLifetimeSeconds so that Dependencies block precedes Constants/static;
update the WorldAlertSystem class so the readonly fields with [Dependency] come
first, then keep DefaultLifetimeSeconds as the constants/static block.
- Around line 32-40: The current EnsureTimedDespawn method returns immediately
if a TimedDespawnComponent already exists, ignoring the incoming
lifetime/settings; change it so that if HasComp<TimedDespawnComponent>(uid) is
true you fetch the existing component (e.g. via Comp<TimedDespawnComponent>(uid)
or TryComp) and, if the lifetime parameter has a value, update that component's
Lifetime to lifetime.Value.TotalSeconds (otherwise leave it unchanged); if no
component exists keep the existing EnsureComp<TimedDespawnComponent>(uid) path
and set Lifetime to lifetime.HasValue ? (float)lifetime.Value.TotalSeconds :
DefaultLifetimeSeconds.

In `@Content.Shared/_Scp/Scp096/Main/Systems/SharedScp096System.Holding.cs`:
- Around line 89-100: The branch handling holderCount <= 0 inside
GetHoldBreakoutDirection is unreachable; make holderCount a strict positive
parameter by removing the fallback branch (the MathF.Cos/MathF.Sin angle path
remains) and add a defensive check at the top of GetHoldBreakoutDirection (e.g.,
Debug.Assert(holderCount > 0) or throw
ArgumentOutOfRangeException("holderCount")) to document the contract; update
callers to ensure they only call GetHoldBreakoutDirection when holderCount > 0
(or adjust call sites to pass a positive holderCount) so the function no longer
contains the dead holderCount <= 0 branch.

In `@Content.Shared/Hands/EntitySystems/SharedHandsSystem.Drop.cs`:
- Around line 142-148: Замените блок с пометкой "Fire edit" на формат Sunrise
(используйте либо однострочный "Sunrise-Edit" либо "Sunrise edit start"/"Sunrise
edit end") и упростите проверку компонента: вместо TryComp(entity, out
VirtualItemComponent? _) используйте HasComp<VirtualItemComponent>(entity); при
совпадении продолжайте вызывать DoDrop(ent, handId, doDropInteraction: false) и
вернуть true (сохраняйте существующие вызовы DoDrop, ent и handId).

In `@Content.Shared/Movement/Pulling/Systems/PullingSystem.cs`:
- Around line 42-44: Замените все однострочные маркеры форка вида "Fire edit
start"/"Fire edit end" на "Sunrise-Edit" в этом файле; конкретно обновите блок
вокруг объявления public sealed partial class PullingSystem : EntitySystem (и
аналогичные блоки на участках примерно 65-67 и 532-535), сохранив остальную
обёртку/комментарии, но используя точно "Sunrise-Edit" как единую метку для
C#/C++ Sunrise форка.

In `@Resources/Locale/ru-RU/_strings/_scp/holding/holding.ftl`:
- Line 14: The string alerts-scp-held-desc contains an in-game control hint that
must be marked as out-of-character; update the alerts-scp-held-desc value so the
player instruction segment ("Двигайтесь или нажмите на этот статус-эффект, чтобы
попытаться вырваться...") is explicitly prefixed with "OOC:" (keeping the rest
of the sentence and meaning intact and preserving the note about full hold
requiring breaking the grab).

In `@Resources/Prototypes/_Scp/Entities/Mobs/Player/Scp/Main/scp096.yml`:
- Around line 118-121: Сделайте удержание SCP‑096 доступным для роли: в сущности
SCP‑096 (символ ScpHoldable в scp096.yml) добавьте явный holderWhitelist:
[Scp096] чтобы переопределить роль SeniorExternalAdministrativeZoneOfficer с
ScpHolder.holdableBlacklist: [Scp]; альтернативно можно изменить компонент
ScpHolder у SeniorExternalAdministrativeZoneOfficer, убрав или скорректировав
holdableBlacklist так, чтобы Scp096 не блокировался — используйте имена
компонентов ScpHoldable, holderWhitelist, ScpHolder, holdableBlacklist и
идентификатор Scp096 для локализации и внесения правки.

In
`@Resources/Prototypes/_Scp/Roles/Jobs/CommandantSquad/field_medical_specialist.yml`:
- Around line 34-40: The new ScpHolder block added to the YAML must be marked as
a Sunrise fork edit: insert the Sunrise marker (use the "# Sunrise-Edit"
comment) adjacent to the added block so the change to the existing prototype is
explicitly flagged; locate the ScpHolder stanza (keys: ScpHolder,
holdableWhitelist, holdableBlacklist, components/HumanoidAppearance,
components/Scp) and add the "# Sunrise-Edit" marker in the same YAML scope above
or immediately next to that block.

In
`@Resources/Prototypes/_Scp/Roles/Jobs/CommandantSquad/senior_external_administrative_zone_officer.yml`:
- Around line 31-37: Blacklist "Scp" in the ScpHolder entry blocks holding
SCP-096 and prevents the OCN squad feature; either remove "Scp" from
holdableBlacklist.components in the ScpHolder block so SCPs can be held, or add
an exception on the SCP side by updating the ScpHoldable logic (e.g., implement
a holderWhitelist on the ScpHoldable for SCP-096 / its entity ID) so that
SCP-096 is allowed to be held despite the global blacklist; locate the ScpHolder
block (holdableWhitelist / holdableBlacklist) and the ScpHoldable implementation
to apply the chosen fix.

In `@Resources/Prototypes/_Scp/Roles/Jobs/LowAccessPersonnel/class_d_cook.yml`:
- Around line 26-34: В файле class_d_cook.yml удалите блок, который добавляет
возможность удержания: весь узел с ключом ScpHolder (включая поля
holdableWhitelist и holdableBlacklist) для роли ClassDCook; т.е. уберите
упоминание ScpHolder и связанные подключи (Scp096, ActiveScp096Rage,
ActiveScp096HeatingUp, ActiveScp096WithoutFace) из определения ClassDCook, и
проверьте, что нет других ссылок на ScpHolder внутри ClassDCook, чтобы
гарантировать, что роль больше не получает механики удержания.

In
`@Resources/Prototypes/_Scp/Roles/Jobs/SpecialPurposeSquad/heavy_containment_zone_commandant.yml`:
- Around line 31-40: The newly added ScpHolder block (the mapping starting with
"type: ScpHolder" and its keys holdableWhitelist/holdableBlacklist and nested
components like Scp096, ClassDAppearance, ActiveScp096Rage,
ActiveScp096HeatingUp, ActiveScp096WithoutFace) must be marked as a Sunrise fork
edit; insert the single-line comment "# Sunrise-Edit" immediately above the
ScpHolder block so the entire added block is clearly annotated per the Sunrise
marker convention for YAML files.

In
`@Resources/Prototypes/_Scp/Roles/Jobs/SpecialPurposeSquad/junior_heavy_containment_zone_officer.yml`:
- Around line 30-39: Добавьте маркер fork-редакта "# Sunrise-Edit"
непосредственно перед вновь добавленным блоком ScpHolder (блок, начинающийся с
ключа ScpHolder и содержащий holdableWhitelist/holdableBlacklist и компоненты
Scp096, ClassDAppearance, ActiveScp096Rage, ActiveScp096HeatingUp,
ActiveScp096WithoutFace), чтобы пометить Sunrise-правку в YAML по правилам;
просто вставьте строку с точным комментарием "# Sunrise-Edit" перед этим блока.

In
`@Resources/Prototypes/_Scp/Roles/Jobs/SpecialPurposeSquad/senior_heavy_containment_zone_officer.yml`:
- Around line 31-40: The newly added ScpHolder block (the block with keys
ScpHolder, holdableWhitelist, holdableBlacklist and components like Scp096,
ClassDAppearance, ActiveScp096Rage, ActiveScp096HeatingUp,
ActiveScp096WithoutFace) needs the Sunrise fork marker: add a line with exactly
"# Sunrise-Edit" immediately above the ScpHolder mapping so the inserted YAML
block is clearly marked as a Sunrise edit per repository guidelines.

---

Duplicate comments:
In `@Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Restrictions.cs`:
- Around line 28-31: Метод OnRestrictRemove(Entity<ScpHoldRestrictedComponent>
ent, ref ComponentShutdown args) не должен вызывать ValidateActions() на
ComponentShutdown — это пере-применяет restriction и может оставить действия
отключёнными после удаления компонента. Уберите вызов
ValidateActions(ent.AsNullable()) из OnRestrictRemove и переместите логику в
обработчик удаления компонента (например, обработчик
ComponentRemove/ComponentRemoved для Entity<ScpHoldRestrictedComponent>) или
вызывайте ValidateActions только после окончательного удаления (не в
ComponentShutdown), чтобы ValidateActions выполнялся когда компонент
действительно удалён.
- Around line 65-71: OnHoldRestrictedActionAttempt currently calls
_popup.PopupClient directly in a shared predicted path which can produce
duplicate popups during prediction replay; change the code to only emit the
one-shot UX when this is the first predicted execution by guarding with the
prediction check (e.g., IsFirstTimePredicted on the ActionAttemptEvent) or by
routing the notification through the predicted helper (e.g., use a
PopupPredicted-style helper instead of _popup.PopupClient), keeping the existing
cancellation behavior (args.Cancelled = true) and the IsHeldAtStage check
intact.

In `@Content.Shared/Movement/Pulling/Systems/PullingSystem.cs`:
- Around line 532-535: Вызов TryRedirectPullToScpHold возвращает результат до
выполнения общих проверок/событий и тем самым обходит CanPull(),
StartPullAttemptEvent/BeingPulledAttemptEvent/PullAttemptEvent и guards
(NeedsHands, CanInteract, container/buckle), поэтому измените порядок/реализацию
так, чтобы общие валидации и публикация стартовых veto-событий выполнялись перед
перенаправлением: либо переместите вызовы CanPull() и генерацию
StartPullAttemptEvent/BeingPulledAttemptEvent/PullAttemptEvent в общий helper и
вызовите его и из основного пути, и из TryRedirectPullToScpHold, либо вызовите
эти проверки/события сразу перед TryRedirectPullToScpHold и только затем
применяйте redirect; используйте идентификаторы TryRedirectPullToScpHold,
CanPull, StartPullAttemptEvent, BeingPulledAttemptEvent, PullAttemptEvent,
NeedsHands и CanInteract для поиска мест в коде.

In `@Resources/Locale/en-US/_strings/_scp/holding/holding.ftl`:
- Line 14: The string value for alerts-scp-held-desc contains an
out-of-character control instruction; update the alerts-scp-held-desc message so
the player instruction is explicitly marked with "OOC:" — e.g., prepend or
insert "OOC:" before "Move or click this alert to try to break free" so the line
reads as an in-world description followed by an OOC control hint, leaving the
rest of the sentence unchanged.

In `@Resources/Prototypes/_Scp/Roles/Jobs/Administration/security_commander.yml`:
- Around line 36-39: Расширьте конфигурацию ScpHolder для роли Security
Commander: вместо единственного элемента в holdableWhitelist (ClassDAppearance)
добавьте также HumanoidAppearance и добавьте соответствующий blacklist (Scp) по
аналогии с реализацией в SeniorExternalAdministrativeZoneOfficer; убедитесь, что
ключи holdableWhitelist и blacklist правильно указаны и соответствуют остальной
структуре файла.

In
`@Resources/Prototypes/_Scp/Roles/Jobs/CommandantSquad/junior_external_administrative_zone_officer.yml`:
- Around line 30-36: Добавленный блок ScpHolder не содержит обязательного
маркера Sunrise и должен быть помечен; перед строкой с "type: ScpHolder"
вставьте строку с комментарием "# Sunrise-Edit" (в том же YAML-уровне отступа),
чтобы отметить правку по правилам для файлов YAML/FTL/PY/SH; проверьте блокы с
идентификаторами ScpHolder, holdableWhitelist и holdableBlacklist чтобы маркер
однозначно относится к этому добавленному прототипу.

In
`@Resources/Prototypes/_Scp/Roles/Jobs/LowAccessPersonnel/class_d_botanist.yml`:
- Around line 26-34: ClassDBotanist currently gets Scp096 via the ScpHolder
block — remove Scp096 from its holdableWhitelist (or remove the ScpHolder entry
entirely for ClassDBotanist) so the role cannot hold SCP-096; specifically edit
the ClassDBotanist definition to either delete the ScpHolder section or ensure
holdableWhitelist does not list Scp096 (or add Scp096 to holdableBlacklist) to
prevent granting hold ability.

In `@Resources/Prototypes/_Scp/Roles/Jobs/LowAccessPersonnel/class_d_janitor.yml`:
- Around line 23-31: В роли ClassDJanitor удалите или отключите блок ScpHolder
(т.е. удалите ключи type: ScpHolder и связанные
holdableWhitelist/holdableBlacklist), чтобы ClassDJanitor больше не имел
возможности участвовать в удержании SCP-096; найдите секцию с именем
ClassDJanitor и ключами ScpHolder / holdableWhitelist / holdableBlacklist и
удалите её целиком или замените на разрешённый для low-access набор, если нужно
сохранить структуру.

In `@Resources/Prototypes/_Scp/Roles/Jobs/LowAccessPersonnel/class_d.yml`:
- Around line 21-29: В файле роли ClassD удалите или отключите блок ScpHolder,
который даёт low-access ролям возможность удерживать SCP-096: конкретно уберите
holdableWhitelist с компонентом Scp096 (и связанные
holdableBlacklist/ActiveScp096* записи) из class_d.yml; вместо этого
добавьте/оставьте этот ScpHolder только в ролях командного состава и ОСН (где
требуется механика), т.е. перенесите или создайте аналогичный блок ScpHolder с
holdableWhitelist: components: - Scp096 в соответствующих ролях команд/ОСН и
убедитесь, что class_d.yml больше не содержит компонента Scp096 в списках
удержания.

In
`@Resources/Prototypes/_Scp/Roles/Jobs/SpecialPurposeSquad/heavy_containment_zone_officer.yml`:
- Around line 31-40: The added ScpHolder block (the mapping starting with "type:
ScpHolder" and containing holdableWhitelist and holdableBlacklist) is missing
the required fork marker; insert a YAML comment line "# Sunrise-Edit"
immediately above the ScpHolder block (i.e., directly before the "type:
ScpHolder" key) so the change is marked as a Sunrise fork edit while leaving the
existing keys (holdableWhitelist, holdableBlacklist, components, etc.)
unchanged.

In `@Resources/Prototypes/Actions/types.yml`:
- Around line 264-265: В блоке, где определён type ScpHoldRestricted (the YAML
mapping with "type: ScpHoldRestricted" and "stage: Soft"), замените комментарий
"# Fire edit - block combat mode while held" на маркер Sunrise согласно гайду —
используйте именно "# Sunrise-Edit" (или при многострочных изменениях
обрамляющие "Sunrise edit start"/"Sunrise edit end"), чтобы правка помечалась
как upstream Sunrise-изменение.

In `@Resources/Prototypes/Entities/Mobs/Species/base.yml`:
- Around line 280-282: Remove the unintended ScpHolder entry from the
BaseMobSpeciesOrganic prototype (the line with "type: ScpHolder" and its TODO)
so holding mechanics are not granted to all organics; leave or keep "type:
ScpHoldable" only if that is required for the specific role mechanics. Also
replace the upstream fork markers "# Fire start" / "# Fire end" with the
required Sunrise markers: add a single "# Sunrise-Edit" header and wrap the
local modification block with "# Sunrise edit start" and "# Sunrise edit end"
(use the Sunrise prefix for any project-specific markers/folders) to mark the
unavoidable upstream edit. Ensure references to ScpHolder and
BaseMobSpeciesOrganic are the targets of the change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 309a468f-f830-4c7f-af77-04c92ded739a

📥 Commits

Reviewing files that changed from the base of the PR and between d6987a7 and c9392e5.

📒 Files selected for processing (61)
  • Content.Client/_Scp/Holding/ScpHoldingSystem.cs
  • Content.IntegrationTests/Tests/_Scp/ScpHeadsetEncryptionKeysTest.cs
  • Content.Server/Movement/Systems/PullController.cs
  • Content.Server/_Scp/Holding/ScpHoldingSystem.cs
  • Content.Shared/Hands/EntitySystems/SharedHandsSystem.Drop.cs
  • Content.Shared/Interaction/SharedInteractionSystem.Blocking.cs
  • Content.Shared/Movement/Pulling/Systems/PullingSystem.cs
  • Content.Shared/_Scp/Holding/Compatibility/PullingSystem.ScpHolding.cs
  • Content.Shared/_Scp/Holding/Components/ActiveScpHoldableComponent.cs
  • Content.Shared/_Scp/Holding/Components/ActiveScpHolderComponent.cs
  • Content.Shared/_Scp/Holding/Components/ActiveStateScpHoldableCursorMoveComponent.cs
  • Content.Shared/_Scp/Holding/Components/ActiveStateScpHoldableFullHoldComponent.cs
  • Content.Shared/_Scp/Holding/Components/ActiveStateScpHolderSlowdownComponent.cs
  • Content.Shared/_Scp/Holding/Components/ScpBreakoutAttemptComponent.cs
  • Content.Shared/_Scp/Holding/Components/ScpHeldHandBlockerComponent.cs
  • Content.Shared/_Scp/Holding/Components/ScpHoldHandBlockerComponent.cs
  • Content.Shared/_Scp/Holding/Components/ScpHoldImmuneComponent.cs
  • Content.Shared/_Scp/Holding/Components/ScpHoldRestrictedComponent.cs
  • Content.Shared/_Scp/Holding/Components/ScpHoldableComponent.cs
  • Content.Shared/_Scp/Holding/Components/ScpHolderComponent.cs
  • Content.Shared/_Scp/Holding/ScpHoldStage.cs
  • Content.Shared/_Scp/Holding/ScpHoldingEvents.cs
  • Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Actions.cs
  • Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.BreakoutAttempt.cs
  • Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.CursorMove.cs
  • Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Drag.cs
  • Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Hands.cs
  • Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Lifecycle.cs
  • Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Restrictions.cs
  • Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.State.cs
  • Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.cs
  • Content.Shared/_Scp/Other/WorldAlert/WorldAlertSettings.cs
  • Content.Shared/_Scp/Other/WorldAlert/WorldAlertSystem.cs
  • Content.Shared/_Scp/Scp096/Main/Components/Scp096Component.cs
  • Content.Shared/_Scp/Scp096/Main/Systems/SharedScp096System.Holding.cs
  • Content.Shared/_Scp/Scp096/Main/Systems/SharedScp096System.Rage.cs
  • Content.Shared/_Scp/Scp096/Main/Systems/SharedScp096System.WithoutFace.cs
  • Content.Shared/_Scp/Scp096/Main/Systems/SharedScp096System.cs
  • Resources/Locale/en-US/_strings/_scp/holding/holding.ftl
  • Resources/Locale/ru-RU/_strings/_scp/holding/holding.ftl
  • Resources/Prototypes/Actions/types.yml
  • Resources/Prototypes/Entities/Mobs/Species/base.yml
  • Resources/Prototypes/_Scp/Actions/scp096.yml
  • Resources/Prototypes/_Scp/Alerts/holding.yml
  • Resources/Prototypes/_Scp/Entities/Effects/world_alerts.yml
  • Resources/Prototypes/_Scp/Entities/Mobs/Player/Scp/Main/scp096.yml
  • Resources/Prototypes/_Scp/Entities/StatusEffects/holding.yml
  • Resources/Prototypes/_Scp/Roles/Jobs/Administration/security_commander.yml
  • Resources/Prototypes/_Scp/Roles/Jobs/CommandantSquad/external_administrative_zone_commandant.yml
  • Resources/Prototypes/_Scp/Roles/Jobs/CommandantSquad/external_administrative_zone_officer.yml
  • Resources/Prototypes/_Scp/Roles/Jobs/CommandantSquad/field_medical_specialist.yml
  • Resources/Prototypes/_Scp/Roles/Jobs/CommandantSquad/junior_external_administrative_zone_officer.yml
  • Resources/Prototypes/_Scp/Roles/Jobs/CommandantSquad/senior_external_administrative_zone_officer.yml
  • Resources/Prototypes/_Scp/Roles/Jobs/LowAccessPersonnel/class_d.yml
  • Resources/Prototypes/_Scp/Roles/Jobs/LowAccessPersonnel/class_d_botanist.yml
  • Resources/Prototypes/_Scp/Roles/Jobs/LowAccessPersonnel/class_d_cook.yml
  • Resources/Prototypes/_Scp/Roles/Jobs/LowAccessPersonnel/class_d_janitor.yml
  • Resources/Prototypes/_Scp/Roles/Jobs/SpecialPurposeSquad/heavy_containment_zone_commandant.yml
  • Resources/Prototypes/_Scp/Roles/Jobs/SpecialPurposeSquad/heavy_containment_zone_officer.yml
  • Resources/Prototypes/_Scp/Roles/Jobs/SpecialPurposeSquad/junior_heavy_containment_zone_officer.yml
  • Resources/Prototypes/_Scp/Roles/Jobs/SpecialPurposeSquad/senior_heavy_containment_zone_officer.yml
💤 Files with no reviewable changes (1)
  • Content.IntegrationTests/Tests/_Scp/ScpHeadsetEncryptionKeysTest.cs

Comment on lines +99 to +136
private void OnUpdateHeldPredicted(Entity<ActiveScpHoldableComponent> ent, ref UpdateIsPredictedEvent args)
{
if (_player.LocalEntity is not { Valid: true } local)
return;

if ((EntityUid) ent == local)
{
args.IsPredicted = true;
return;
}

if (HasComp<ActiveStateScpHoldableCursorMoveComponent>(ent))
{
args.BlockPrediction = true;
return;
}

if (_activeHolderQuery.TryComp(local, out var localHolder))
{
if (localHolder.Target == ent)
{
args.IsPredicted = true;
return;
}
}

foreach (var holder in ent.Comp.Holders)
{
if (holder != local)
continue;

args.IsPredicted = true;
return;
}

if (ent.Comp.Holders.Count > 0)
args.BlockPrediction = true;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Некорректный отступ в OnUpdateHeldPredicted.

Блок if ((EntityUid) ent == local) имеет лишний уровень отступа у тела, что рассогласуется со стилем остального файла и сбивает чтение. Нитпик.

🛠️ Предлагаемая правка
-        if ((EntityUid) ent == local)
-            {
-                args.IsPredicted = true;
-                return;
-            }
+        if ((EntityUid) ent == local)
+        {
+            args.IsPredicted = true;
+            return;
+        }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private void OnUpdateHeldPredicted(Entity<ActiveScpHoldableComponent> ent, ref UpdateIsPredictedEvent args)
{
if (_player.LocalEntity is not { Valid: true } local)
return;
if ((EntityUid) ent == local)
{
args.IsPredicted = true;
return;
}
if (HasComp<ActiveStateScpHoldableCursorMoveComponent>(ent))
{
args.BlockPrediction = true;
return;
}
if (_activeHolderQuery.TryComp(local, out var localHolder))
{
if (localHolder.Target == ent)
{
args.IsPredicted = true;
return;
}
}
foreach (var holder in ent.Comp.Holders)
{
if (holder != local)
continue;
args.IsPredicted = true;
return;
}
if (ent.Comp.Holders.Count > 0)
args.BlockPrediction = true;
}
private void OnUpdateHeldPredicted(Entity<ActiveScpHoldableComponent> ent, ref UpdateIsPredictedEvent args)
{
if (_player.LocalEntity is not { Valid: true } local)
return;
if ((EntityUid) ent == local)
{
args.IsPredicted = true;
return;
}
if (HasComp<ActiveStateScpHoldableCursorMoveComponent>(ent))
{
args.BlockPrediction = true;
return;
}
if (_activeHolderQuery.TryComp(local, out var localHolder))
{
if (localHolder.Target == ent)
{
args.IsPredicted = true;
return;
}
}
foreach (var holder in ent.Comp.Holders)
{
if (holder != local)
continue;
args.IsPredicted = true;
return;
}
if (ent.Comp.Holders.Count > 0)
args.BlockPrediction = true;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Content.Client/_Scp/Holding/ScpHoldingSystem.cs` around lines 99 - 136, In
OnUpdateHeldPredicted, fix the inconsistent indentation of the if ((EntityUid)
ent == local) block: remove the extra nesting level so the block body aligns
with the other if blocks in the method (adjust the lines that set
args.IsPredicted = true; return; inside that if), keeping the same logic and
using the existing ent, local, and args symbols.

Comment on lines +2 to +4
using Content.Server._Scp.Holding;
using Content.Shared._Scp.Holding.Components;
using Content.Shared._Scp.Holding.Systems;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -nP '\b(ScpHoldableComponent|ScpHolderComponent|SharedScpHoldingSystem)\b' Content.Server/Movement/Systems/PullController.cs

Repository: space-sunrise/project-fire

Length of output: 52


🏁 Script executed:

cat -n Content.Server/Movement/Systems/PullController.cs | head -100

Repository: space-sunrise/project-fire

Length of output: 5509


🏁 Script executed:

rg -n 'ScpHoldingSystem' Content.Server/Movement/Systems/PullController.cs

Repository: space-sunrise/project-fire

Length of output: 147


🏁 Script executed:

rg -n '\b[A-Z][a-zA-Z]*Component\b|\bSharedScpHoldingSystem\b' Content.Server/Movement/Systems/PullController.cs | head -50

Repository: space-sunrise/project-fire

Length of output: 2110


🏁 Script executed:

fd -e cs Content.Shared/_Scp/Holding/Components/ | head -20 | xargs -I {} bash -c 'echo "=== {} ===" && grep "^public.*class\|^public.*component\|^public.*record" {}'

Repository: space-sunrise/project-fire

Length of output: 531


🏁 Script executed:

fd . Content.Shared/_Scp/Holding/Components/ -e cs | xargs -I {} bash -c 'echo "=== {} ===" && grep "^public.*class\|^public.*component\|^public.*record" {}'

Repository: space-sunrise/project-fire

Length of output: 1842


🏁 Script executed:

fd . Content.Shared/_Scp/Holding/Systems/ -e cs | xargs -I {} bash -c 'echo "=== {} ===" && grep "^public.*class\|^public.*system" {}'

Repository: space-sunrise/project-fire

Length of output: 1266


Удалите неиспользуемые импорты из строк 3–4.

Файл использует только ScpHoldingSystem из Content.Server._Scp.Holding (строка 65). Импорты Content.Shared._Scp.Holding.Components и Content.Shared._Scp.Holding.Systems не задействованы и загрязняют upstream-файл.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Content.Server/Movement/Systems/PullController.cs` around lines 2 - 4, В
файле PullController.cs удалите неиспользуемые импорты: уберите строки с using
Content.Shared._Scp.Holding.Components и using
Content.Shared._Scp.Holding.Systems, оставив только using
Content.Server._Scp.Holding (поскольку код использует только ScpHoldingSystem);
после удаления пересоберите/проверьте отсутствие ошибок и выполните
форматирование файла.

Comment thread Content.Server/Movement/Systems/PullController.cs
Comment on lines +45 to +65
if (!_scpHolding.CanToggleHold(holder,
pullableUid,
ignoreHandAvailability: pullerComp.Pulling != null,
checkAttempt: true))
return true;

if (pullerComp.Pulling is { } currentPullUid &&
_pullableQuery.TryComp(currentPullUid, out var currentPull) &&
!TryStopPull(currentPullUid, currentPull, pullerUid))
{
return true;
}

if (pullableComp.Puller != null &&
!TryStopPull(pullableUid, pullableComp, pullableComp.Puller))
{
return true;
}

result = _scpHolding.TryToggleHold(holder, pullableUid, attemptChecked: true);
return true;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Поведение «redirect=true, result=false» неочевидно — добавьте комментарий или верните false явно.

При неуспешном CanToggleHold (строки 45‑49) или при неудачной остановке существующего pull (строки 51‑62) метод возвращает true, и вызывающий код в PullingSystem.TryStartPull пропускает обычный pull‑путь. По смыслу это, видимо, и нужно (SCP‑holdable нельзя тащить ванильным pull), но с точки зрения читаемости имя TryRedirectPullToScpHold намекает «вернул true, значит redirect выполнен». Стоит либо:

  • задокументировать инвариант в xmldoc метода («true означает что pull для SCP‑holdable обработан/проглочен, в т.ч. отказом»),
  • либо явно различать «handled (consume input)» и «success» — например, оставить result для успеха и переименовать возвращаемое значение в handled.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Content.Shared/_Scp/Holding/Compatibility/PullingSystem.ScpHolding.cs` around
lines 45 - 65, Update the XML doc for TryRedirectPullToScpHold to explicitly
state return semantics: "Returns true if the pull was handled/consumed
(including being rejected); the out parameter 'result' indicates whether the
redirect succeeded." Mention that this covers the code paths that call
_scpHolding.CanToggleHold, TryStopPull and _scpHolding.TryToggleHold so callers
understand that true does not necessarily mean success. Optionally rename the
out parameter 'result' to 'success' (or rename the method to indicate "handled")
for clarity; if renaming, update all call sites (e.g.,
PullingSystem.TryStartPull) to use the new name.

Comment on lines +17 to +18
[AutoNetworkedField, ViewVariables]
public EntityUid Holder;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Рассмотрите nullable для Holder.

Holder объявлен как EntityUid (без nullable) со значением по умолчанию EntityUid.Invalid. Если компонент создаётся до первого валидного SetCursorMoveState, состояние Invalid легко спутать с валидной целью. Более идиоматично объявить EntityUid? Holder и проверять наличие через HasValue, тем более что ClearCursorMoveState всё равно удаляет компонент целиком.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@Content.Shared/_Scp/Holding/Components/ActiveStateScpHoldableCursorMoveComponent.cs`
around lines 17 - 18, Поле Holder в компоненте
ActiveStateScpHoldableCursorMoveComponent должно быть nullable (EntityUid?
Holder) так как текущее значение EntityUid.Invalid может быть ошибочно принято
за валидную цель; измените объявление Holder на nullable, обновите места
использования (включая методы SetCursorMoveState и ClearCursorMoveState) чтобы
проверять Holder.HasValue перед доступом и распаковывать через Holder.Value там,
где требуется, и при очистке состояния удалять/обнулять компонент как сейчас.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Балансировка скромника

1 participant