Skip to content

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

Draft
ThereDrD0 wants to merge 9 commits intomasterfrom
add/multipulling
Draft

Система удержания#928
ThereDrD0 wants to merge 9 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-096 теперь может вырваться из удержания с эффектами урона и параличом.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 8, 2026

📝 Walkthrough

Walkthrough

Добавлена новая механика захвата SCP-объектов, позволяющая нескольким удерживающим (по одному на руку) брать целей за руки, накладывая замедление движения и блокируя действия. Система включает полный hold и мягкий hold с механиками вырывания, иммунитетом после вырывания, синхронизацией руки через виртуальные предметы, перенаправлением pull-интеракций и интеграцией с SCP-096.

Changes

Cohort / File(s) Summary
Core Holding System
Content.Shared/_Scp/Holding/SharedScpHoldingSystem*.cs
Основная система управления захватами: основной файл с инициализацией и обновлением, частичные реализации для действий (toggle/breakout), перетаскивания, событий, обратной связи, синхронизации рук, ограничений и управления состоянием (лист удерживающих, иммунитет, прогрессия захвата).
Holding Components
Content.Shared/_Scp/Holding/ScpHeld*.cs, Content.Shared/_Scp/Holding/ScpHold*.cs, Content.Shared/_Scp/Holding/ScpHoldImmune*.cs, Content.Shared/_Scp/Holding/ScpHoldable*.cs, Content.Shared/_Scp/Holding/ScpHoldStage.cs
Новые сетевые компоненты для отслеживания состояния захвата: ScpHeldComponent (цель с данными удерживающих и конфигурацией), ScpHolderComponent (удерживающий с целью и модификаторами замедления), ScpHoldComponent (конфигурация инициатора захвата), ScpHoldableComponent (конфигурация и правила цели), компоненты-блокеры и иммунитет, перечисление ScpHoldStage.
Holding Events
Content.Shared/_Scp/Holding/ScpHoldingEvents.cs
События для захватов: ScpHoldAttemptEvent (попытка захвата), ScpHoldBreakoutEvent (вырывание), ScpHoldBreakoutDoAfterEvent (do-after вырывания), ScpHoldBreakoutAlertEvent (алерт вырывания).
Pulling System Integration
Content.Shared/Movement/Pulling/Systems/PullingSystem.cs, Content.Shared/_Scp/Holding/PullingSystem.ScpHolding.cs
Интеграция захватов с системой тягания: PullingSystem теперь частичная, вызывает инициализацию и перенаправляет pull-попытки в SCP-hold, если условия соблюдены.
SCP-096 Holding Integration
Content.Shared/_Scp/Scp096/Main/Components/Scp096Component.cs, Content.Shared/_Scp/Scp096/Main/Systems/SharedScp096System*.cs
SCP-096 специфичные: компонент с конфигурацией урона/параличи при вырывании, система событий захвата с отменой при определённых состояниях (маска/агрессия/разогрев), вырывание при входе в эти состояния, применение эффектов урона/параличи/импульса к удерживающим.
Hands & Blocking Systems
Content.Shared/Hands/EntitySystems/SharedHandsSystem.Drop.cs, Content.Shared/Interaction/SharedInteractionSystem.Blocking.cs
Интеграция с существующими системами: модификация drop-логики для виртуальных предметов, добавление проверки жизненного цикла в систему блокирования движения.
Client Prediction
Content.Client/_Scp/Holding/ScpHoldingPredictionSystem.cs
Клиентская система прогнозирования захватов: управление состоянием прогнозирования, подавление спавна виртуальных предметов-блокеров на 0.5 сек, удаление блокеров руки при необходимости, обновление флагов прогнозирования.
Integration Tests
Content.IntegrationTests/Tests/_Scp/ScpHoldingTest.cs
Обширный набор интеграционных тестов: жизненный цикл мягкого/полного захвата, breakout механики, множественные удерживающие, прогнозирование клиента, перенаправление pull, рассинхронизация при дальности/телепортации, вспомогательные компоненты/системы для тестов.
Localization
Resources/Locale/en-US/_strings/_scp/holding/holding.ftl, Resources/Locale/ru-RU/_strings/_scp/holding/holding.ftl
Локализация захватов на английском и русском: сообщения об ошибках (цель невалидна/иммунна/далеко), ограничения (нет свободной руки), breakout статусы, описания алертов.
Prototypes & Configuration
Resources/Prototypes/Actions/types.yml, Resources/Prototypes/Entities/Mobs/Species/base.yml, Resources/Prototypes/_Scp/.../*.yml
Прототипы и конфигурация: добавление компонентов ScpHold/ScpHoldable в базовых видах и SCP-096, новый прототип алерта захвата, статус-эффект, ограничения захвата для различных ролей (командиры, медики, Class D с белыми/чёрными списками целей).

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Server
    participant Holder as Holder Entity
    participant Target as Target Entity

    rect rgba(100, 200, 100, 0.5)
    Note over Client,Target: Инициирование захвата (Hold Attempt)
    Client->>Server: PullingSystem.TryStartPull(holder, target)
    Server->>Server: TryRedirectPullToScpHold(holder, target)
    Server->>Server: SharedScpHoldingSystem.CanToggleHold()
    Server->>Server: SharedScpHoldingSystem.TryToggleHold(holder, target)
    Server->>Holder: AddHolderContribution(holder, target)
    Server->>Target: ScpHeldComponent создан/обновлен
    Server->>Server: RefreshHeldState(target)
    Server->>Client: Синхронизация компонентов
    end

    rect rgba(150, 100, 200, 0.5)
    Note over Client,Target: Во время захвата (Holding)
    Server->>Server: UpdateHeld() каждый тик
    Server->>Target: Применить мягкое перетаскивание или zero velocity
    Server->>Target: Блокировать движение/действия
    Server->>Holder: Применить модификатор замедления
    Client->>Client: ScpHoldingPredictionSystem обновляет прогноз
    end

    rect rgba(200, 100, 100, 0.5)
    Note over Client,Target: Вырывание (Breakout)
    Target->>Server: InputMoverSystem.OnMoveAttempt()
    Server->>Server: TryBreakOut(target, viaMovement=true)
    Server->>Server: Проверить SoftEscapeAvailableAt
    alt Full Hold
        Server->>Server: StartDoAfter(ScpHoldBreakoutDoAfterEvent)
    else Soft Hold
        Server->>Server: Выполнить вырывание сразу
    end
    Server->>Target: Применить иммунитет (ScpHoldImmuneComponent)
    Server->>Holder: Применить урон/параліч (для SCP-096)
    Server->>Server: ClearHoldState(target)
    Server->>Client: Синхронизация завершения захвата
    end
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Suggested labels

🛠️ Есть C# 🛠️, 📚 Изменения: Локализация 📚, 📦 YAML | Прототип 📦

Poem

🐰 Кролик радостно прыгает по коду,
Новый захват в системе шоу!
По рукам по одному держи беду,
А потом пусть вырывается в борьбу!
Балансировка скромника - вот победа,
Механика работает как надо, как это! 🎉


Important

Pre-merge checks failed

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

❌ Failed checks (3 errors, 1 warning)

Check name Status Explanation Resolution
Ss14 C# Rules ❌ Error Обнаружены критические нарушения SS14 C# правил: неправильные маркеры fork-edits (Fire вместо Sunrise), отсутствие AutoGenerateComponentState/AutoNetworkedField на сетевых компонентах, неправильный тип popup для предсказанных событий, использование LocalRotation вместо WorldRotation в world-space коде, и неправильная установка ScpHold компонента в базовом прототипе мобов. Замените маркеры fork-edits на Sunrise-Edit, добавьте AutoGenerateComponentState и AutoNetworkedField к сетевым компонентам, замените PopupClient на PopupPredicted, исправьте LocalRotation на WorldRotation, удалите ScpHold из базового прототипа.
Ss14 Yaml/Ftl Rules ❌ Error Обнаружены три критических нарушения правил SS14: компонент ScpHold в базовом прототипе, неправильные маркеры # Fire edit вместо # Sunrise-Edit и отсутствие префикса OOC: в локализации. Удалить ScpHold из BaseMobSpeciesOrganic, заменить # Fire edit на # Sunrise-Edit и добавить префикс OOC: в alerts-scp-held-desc.
Ss14 Prediction Safety ❌ Error Pull request introduces deterministic prediction safety violations: PopupTarget() calls without IsFirstTimePredicted gating in predicted paths, PopupClient() in predicted ActionAttemptEvent handler, DeleteVirtualItem() without state guards. Replace PopupTarget/PopupHolder with IsFirstTimePredicted gated versions, use PopupPredicted() in OnHoldRestrictedActionAttempt, wrap DeleteSuppressedBlockers with state application guard.
Docstring Coverage ⚠️ Warning Docstring coverage is 2.47% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (6 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed Заголовок «Система удержания» точно отражает основное изменение в PR — введение новой механики захвата/удержания целей.
Linked Issues check ✅ Passed PR полностью реализует все требования из issue #824: механика захвата двумя руками, блокировка движения/способностей, действие для побега через 10с, видимый 5с КД вырывания, отпихивание при неудаче, блокировка захвата в боевых режимах, поддержка SCP-096.
Out of Scope Changes check ✅ Passed Все изменения относятся к реализации системы удержания: компоненты, события, системы, прототипы, локализация и интеграционные тесты — без посторонних модификаций.
Ss14 Bridge Sync ✅ Passed PR вводит систему удержания SCP-096 без изменения канонических файлов правил и навыков в директориях .agent/ и их зеркалах в bridge-директориях.
Ss14 Prototype ↔ Ftl Parity ✅ Passed PR поддерживает полную паритет прототипа ↔ FTL во всех изменениях. Все 14 FTL ключей идентично присутствуют в обеих поддерживаемых локалях с правильными переводами.
✨ 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

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ast-grep (0.42.1)
Content.Shared/Hands/EntitySystems/SharedHandsSystem.Drop.cs

[
{
"text": "SubscribeLocalEvent<HandsComponent, EntRemovedFromContainerMessage>(HandleEntityRemoved)",
"range": {
"byteOffset": {
"start": 663,
"end": 751
},
"start": {
"line": 22,
"column": 8
},
"end": {
"line": 22,
"column": 96
}
},
"file": "Content.Shared/Hands/EntitySystems/SharedHandsSystem.Drop.cs",
"lines": " SubscribeLocalEvent<HandsComponent, EntRemovedFromContainerMessage>(HandleEntityRemoved);",
"charCount": {
"leading": 8,
"trailing": 1
},
"language": "CSharp",
"metaVariables": {
"single": {},
"multi": {
"secondary": [
{
"text": "private void InitializeDrop()\n {\n SubscribeLocalEvent<HandsComponent, EntRemovedFromContainerMessage>(HandleEntityRemoved);\n SubscribeLocalEvent<HandsComponent, EntityStorageIntoContainerAttemptEvent>(OnEntityStorageDump);\n }",
"range": {
"byteOffset": {
"start": 61

... [truncated 6455 characters] ...

pped = new GotUnequippedHandEvent(uid, args.Entity, hand.Value);\n RaiseLocalEvent(args.Entity, gotUnequipped);\n\n var didUnequip = new DidUnequipHandEvent(uid, args.Entity, hand.Value);\n RaiseLocalEvent(uid, didUnequip);\n\n if (TryComp(args.Entity, out VirtualItemComponent? @virtual))\n _virtualSystem.DeleteVirtualItem((args.Entity, @virtual), uid);\n }",
"range": {
"byteOffset": {
"start": 870,
"end": 1511
},
"start": {
"line": 26,
"column": 4
},
"end": {
"line": 41,
"column": 5
}
},
"style": "primary"
}
]
}
]
Error: 3 error(s) found in code.
Help: Scan succeeded and found error level diagnostics in the codebase.

Content.Shared/Interaction/SharedInteractionSystem.Blocking.cs

[
{
"text": "SubscribeLocalEvent<BlockMovementComponent, UpdateCanMoveEvent>(OnMoveAttempt)",
"range": {
"byteOffset": {
"start": 575,
"end": 653
},
"start": {
"line": 18,
"column": 8
},
"end": {
"line": 18,
"column": 86
}
},
"file": "Content.Shared/Interaction/SharedInteractionSystem.Blocking.cs",
"lines": " SubscribeLocalEvent<BlockMovementComponent, UpdateCanMoveEvent>(OnMoveAttempt);",
"charCount": {
"leading": 8,
"trailing": 1
},
"language": "CSharp",
"metaVariables": {
"single": {},
"multi": {
"secondary": [
{
"text": "private void InitializeBlocking()\n {\n SubscribeLocalEvent<BlockMovementComponent, UpdateCanMoveEvent>(OnMoveAttempt);\n SubscribeLocalEvent<BlockMovementComponent, UseAttemptEvent>(CancelUseEvent);\n SubscribeLocalEvent<BlockMovementComponent, InteractionAttemptEvent>(CancelInteractEvent);\n SubscribeLocalEven

... [truncated 30013 characters] ...

oveEvent args)\n {\n // Fire edit start - do not let a blocker cancel its own shutdown refresh\n if (component.LifeStage > ComponentLifeStage.Running)\n return;\n\n // If we're relaying then don't cancel.\n if (HasComp(uid))\n return;\n\n args.Cancel(); // no more scurrying around\n // Fire edit end\n }",
"range": {
"byteOffset": {
"start": 1684,
"end": 2158
},
"start": {
"line": 41,
"column": 4
},
"end": {
"line": 53,
"column": 5
}
},
"style": "primary"
}
]
}
]
Error: 9 error(s) found in code.
Help: Scan succeeded and found error level diagnostics in the codebase.

Content.IntegrationTests/Tests/_Scp/ScpHoldingTest.cs

[
{
"text": "private static void OnAttempt(Entity ent, ref ScpHoldAttemptEvent args)\n {\n args.Cancel();\n }",
"range": {
"byteOffset": {
"start": 92678,
"end": 92819
},
"start": {
"line": 2246,
"column": 4
},
"end": {
"line": 2249,
"column": 5
}
},
"file": "Content.IntegrationTests/Tests/_Scp/ScpHoldingTest.cs",
"lines": " private static void OnAttempt(Entity ent, ref ScpHoldAttemptEvent args)\n {\n args.Cancel();\n }",
"charCount": {
"leading": 4,
"trailing": 0
},
"language": "CSharp",
"metaVariables": {
"single": {},
"multi": {
"secondary": [
{
"text": "[RegisterComponent]\npublic sealed partial class ScpHoldAttemptCancelTestComponent : Component;\n\npublic sealed class ScpHoldAttemptListenerSystem : TestListenerSystem;\n\npublic sealed class ScpHo

... [truncated 1998 characters] ...

HoldBreakoutEvent>;\n\npublic sealed class ScpHoldAttemptCancelSystem : EntitySystem\n{\n public override void Initialize()\n {\n SubscribeLocalEvent<ScpHoldAttemptCancelTestComponent, ScpHoldAttemptEvent>(OnAttempt);\n }\n\n private static void OnAttempt(Entity ent, ref ScpHoldAttemptEvent args)\n {\n args.Cancel();\n }\n}",
"range": {
"byteOffset": {
"start": 92179,
"end": 92821
},
"start": {
"line": 2232,
"column": 0
},
"end": {
"line": 2250,
"column": 1
}
},
"style": "secondary"
}
]
}
]
Error: 1 error(s) found in code.
Help: Scan succeeded and found error level diagnostics in the codebase.

  • 5 others

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

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 on lines +22 to +26
private static readonly TimeSpan BlockerRespawnSuppressionDuration = TimeSpan.FromSeconds(0.5);

private EntityUid? _suppressedHolder;
private EntityUid? _suppressedTarget;
private TimeSpan _suppressedUntil;
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 | 🟠 Major

Не привязывайте suppression blocker'а к фиксированным 500 мс.

При RTT выше 500 мс _suppressedUntil истечёт раньше, чем приедет authoritative состояние holder'а, и виртуальный blocker снова начнёт респавниться/мигать на клиенте. Здесь безопаснее держать suppression до смены ScpHolderComponent.Target или удаления ScpHolderComponent, а не по wall-clock таймауту.

Also applies to: 48-50, 141-160

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

In `@Content.Client/_Scp/Holding/ScpHoldingPredictionSystem.cs` around lines 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.

Comment on lines +8 to +20
[RegisterComponent, NetworkedComponent]
[Access(typeof(SharedScpHoldingSystem))]
public sealed partial class ScpHeldHandBlockerComponent : Component
{
/// <summary>
/// Held target whose hand is occupied by this placeholder.
/// </summary>
public EntityUid Target;

/// <summary>
/// Holder whose sprite is shown in this placeholder.
/// </summary>
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.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Проверяем использование компонента в предсказуемом коде
rg -n "ScpHeldHandBlockerComponent" --type=cs -C3

Repository: space-sunrise/project-fire

Length of output: 3881


🏁 Script executed:

#!/bin/bash
# Поиск ScpHoldingPredictionSystem или prediction-related code
rg -n "ScpHoldingPrediction\|PredictionSystem.*Holding" --type=cs -A2

Repository: space-sunrise/project-fire

Length of output: 52


🏁 Script executed:

#!/bin/bash
# Поиск всех упоминаний слова "prediction" в контексте SCP Holding
rg -n -i "prediction.*holding\|holding.*prediction" --type=cs -A2

Repository: space-sunrise/project-fire

Length of output: 52


🏁 Script executed:

#!/bin/bash
# Проверяем, является ли SharedScpHoldingSystem системой предсказания
rg -n "class SharedScpHoldingSystem" --type=cs -A5

Repository: space-sunrise/project-fire

Length of output: 4972


🏁 Script executed:

#!/bin/bash
# Проверяем, используется ли компонент в любом контексте, который может быть связан с предсказанием
rg -n "ScpHeldHandBlockerComponent" --type=cs | head -20

Repository: space-sunrise/project-fire

Length of output: 780


🏁 Script executed:

#!/bin/bash
# Ищем все файлы, содержащие "Prediction" и "Scp"
fd -e cs | xargs grep -l "Prediction" | grep -i scp

Repository: space-sunrise/project-fire

Length of output: 233


🏁 Script executed:

#!/bin/bash
# Проверяем содержимое ScpHoldingPredictionSystem
cat Content.Client/_Scp/Holding/ScpHoldingPredictionSystem.cs

Repository: space-sunrise/project-fire

Length of output: 5950


🏁 Script executed:

#!/bin/bash
# Проверяем, содержит ли ScpHoldingPredictionSystem упоминания ScpHeldHandBlockerComponent
rg -n "ScpHeldHandBlockerComponent" Content.Client/_Scp/Holding/ScpHoldingPredictionSystem.cs

Repository: space-sunrise/project-fire

Length of output: 52


🏁 Script executed:

#!/bin/bash
# Проверяем, как другие компоненты в файле определены с точки зрения state attributes
rg -n "@(RegisterComponent|NetworkedComponent|AutoGenerateComponentState)" Content.Shared/_Scp/Holding/ --type=cs -B2 | head -50

Repository: space-sunrise/project-fire

Length of output: 52


🏁 Script executed:

#!/bin/bash
# Ищем все компоненты в Shared/_Scp/Holding/
rg -n "public sealed partial class.*Component.*:" Content.Shared/_Scp/Holding/ --type=cs -B3 | head -80

Repository: space-sunrise/project-fire

Length of output: 3430


🏁 Script executed:

#!/bin/bash
# Читаем точный файл из review
cat -n Content.Shared/_Scp/Holding/ScpHeldHandBlockerComponent.cs

Repository: space-sunrise/project-fire

Length of output: 799


🏁 Script executed:

#!/bin/bash
# Проверяем, есть ли поля с [AutoNetworkedField] в других компонентах
rg -n "\[AutoNetworkedField\]" Content.Shared/_Scp/Holding/ --type=cs -B1 -A1 | head -40

Repository: space-sunrise/project-fire

Length of output: 2529


🏁 Script executed:

#!/bin/bash
# Двойная проверка - показываем всё содержимое файла со всеми строками
wc -l Content.Shared/_Scp/Holding/ScpHeldHandBlockerComponent.cs && echo "---" && head -25 Content.Shared/_Scp/Holding/ScpHeldHandBlockerComponent.cs

Repository: space-sunrise/project-fire

Length of output: 718


Добавьте атрибуты сетевой синхронизации и предсказания к компоненту.

Компонент помечен [NetworkedComponent], но не имеет [AutoGenerateComponentState]. Поля Target и Holder не помечены [AutoNetworkedField]. Компонент используется в ScpHoldingPredictionSystem (client-side prediction) и должен правильно синхронизировать и откатывать состояние при рассогласовании. Все аналогичные компоненты в той же системе (ScpHolderComponent, ScpHeldComponent, ScpHoldComponent) имеют эти атрибуты.

🔧 Предлагаемое исправление
-[RegisterComponent, NetworkedComponent]
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(true)]
 [Access(typeof(SharedScpHoldingSystem))]
 public sealed partial class ScpHeldHandBlockerComponent : Component
 {
     /// <summary>
     /// Held target whose hand is occupied by this placeholder.
     /// </summary>
+    [AutoNetworkedField]
     public EntityUid Target;
 
     /// <summary>
     /// Holder whose sprite is shown in this placeholder.
     /// </summary>
+    [AutoNetworkedField]
     public EntityUid Holder;
 }
📝 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
[RegisterComponent, NetworkedComponent]
[Access(typeof(SharedScpHoldingSystem))]
public sealed partial class ScpHeldHandBlockerComponent : Component
{
/// <summary>
/// Held target whose hand is occupied by this placeholder.
/// </summary>
public EntityUid Target;
/// <summary>
/// Holder whose sprite is shown in this placeholder.
/// </summary>
public EntityUid Holder;
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(true)]
[Access(typeof(SharedScpHoldingSystem))]
public sealed partial class ScpHeldHandBlockerComponent : Component
{
/// <summary>
/// Held target whose hand is occupied by this placeholder.
/// </summary>
[AutoNetworkedField]
public EntityUid Target;
/// <summary>
/// Holder whose sprite is shown in this placeholder.
/// </summary>
[AutoNetworkedField]
public EntityUid Holder;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

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

Comment on lines +9 to +10
[RegisterComponent]
public sealed partial class ScpHoldableComponent : Component
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

Рекомендуется добавить атрибут [NetworkedComponent] для компонента в Shared.

Компонент находится в Content.Shared и используется в предиктивном коде для проверки eligibility удержания. Для корректной репликации на клиент рекомендуется добавить атрибут [NetworkedComponent].

♻️ Предлагаемое исправление
+using Robust.Shared.GameObjects;
+
 namespace Content.Shared._Scp.Holding;

 /// <summary>
 /// Marks an entity as a valid target for the SCP holding mechanic and stores per-target hold tuning.
 /// </summary>
 [RegisterComponent]
+[NetworkedComponent]
 public sealed partial class ScpHoldableComponent : Component
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Content.Shared/_Scp/Holding/ScpHoldableComponent.cs` around lines 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.

Comment on lines +5 to +9
[RegisterComponent, NetworkedComponent]
public sealed partial class ScpHoldRestrictedComponent : Component
{
[DataField]
public ScpHoldStage Stage = ScpHoldStage.Full;
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 | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Проверяем, изменяется ли Stage во время выполнения
rg -n "\.Stage\s*=" --type=cs -g '*Holding*' -C 2

Repository: space-sunrise/project-fire

Length of output: 52


🏁 Script executed:

# Search for Stage modifications across the codebase, not just Holding files
rg -n "\.Stage\s*=" --type=cs -A 2 -B 2 | grep -i "scphold\|stage"

Repository: space-sunrise/project-fire

Length of output: 1176


🏁 Script executed:

# Search for methods that might set Stage
rg -n "Stage\s*=" --type=cs -A 1 -B 1 | head -50

Repository: space-sunrise/project-fire

Length of output: 4079


🏁 Script executed:

# Check the full ScpHoldRestrictedComponent file to understand its scope
cat Content.Shared/_Scp/Holding/ScpHoldRestrictedComponent.cs

Repository: space-sunrise/project-fire

Length of output: 320


🏁 Script executed:

# Search for usages of ScpHoldRestrictedComponent in the codebase
rg -n "ScpHoldRestrictedComponent" --type=cs -A 2 -B 2

Repository: space-sunrise/project-fire

Length of output: 1825


🏁 Script executed:

# Look for ScpHoldStage enum to understand Stage values
rg -n "enum ScpHoldStage\|Stage\s*=" --type=cs -A 5 | head -100

Repository: space-sunrise/project-fire

Length of output: 52


🏁 Script executed:

# Check for any modification to ScpHoldRestrictedComponent or related Stage property
rg -n "ScpHoldRestricted" --type=cs -A 5 -B 5

Repository: space-sunrise/project-fire

Length of output: 3735


🏁 Script executed:

# Look for ScpHoldStage enum definition to understand its values
rg -n "ScpHoldStage" --type=cs

Repository: space-sunrise/project-fire

Length of output: 901


🏁 Script executed:

# Check if there's any property or method that might set Stage
rg -n "Stage.*=" --type=cs | grep -i "scphold\|restrict" | head -20

Repository: space-sunrise/project-fire

Length of output: 569


Добавьте атрибуты для сетевой синхронизации компонента.

Компонент помечен [NetworkedComponent] и находится в Content.Shared, поэтому должен соответствовать требованиям для предсказания и синхронизации состояния. Необходимо:

  1. Добавить [AutoGenerateComponentState] к классу компонента
  2. Добавить [AutoNetworkedField] к полю Stage

Это обеспечит корректную сериализацию, десериализацию и откат состояния при предсказании.

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

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

Comment on lines +22 to +40
public bool TryToggleHold(Entity<ScpHoldComponent> holder, EntityUid target, bool attemptChecked = false)
{
if (_holderQuery.TryComp(holder.Owner, out var activeHolder) && activeHolder.Target != null)
{
if (activeHolder.Target.Value == target)
{
ReleaseHolderContribution(holder.Owner, target, clearIfEmpty: true);
return true;
}

PopupHolder(holder.Owner, "scp-hold-already-holding-other");
return false;
}

if (!CanStartHold(holder))
return false;

if (!CanToggleHold(holder, target, checkAttempt: !attemptChecked))
return false;
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 | 🟠 Major

CanToggleHold() сейчас проверяет не toggle, а только start-hold.

TryToggleHold() уже обрабатывает ещё два состояния: release той же цели и отказ, если держатель занят другой целью. CanToggleHold() их игнорирует, поэтому даёт ложные true/false для precheck/UI/prediction и расходится с фактическим поведением публичного API.

🛠️ Вариант правки
 public bool CanToggleHold(
     Entity<ScpHoldComponent> holder,
     EntityUid target,
     bool quiet = false,
     bool ignoreHandAvailability = false,
     bool checkAttempt = false)
 {
     if (holder.Owner == target)
         return false;
+
+    if (_holderQuery.TryComp(holder.Owner, out var activeHolder) && activeHolder.Target is { } activeTarget)
+    {
+        if (activeTarget == target)
+            return true;
+
+        if (!quiet)
+            PopupHolder(holder.Owner, "scp-hold-already-holding-other");
+        return false;
+    }
 
     if (!CanStartHold(holder, quiet))
         return false;
As per coding guidelines, `OnEvent -> TryDo -> CanDo -> Do`.

Also applies to: 54-160

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

In `@Content.Shared/_Scp/Holding/SharedScpHoldingSystem.Actions.cs` around lines
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.

Comment on lines +26 to +34
- type: ScpHold
holdableWhitelist:
components:
- Scp096
holdableBlacklist:
components:
- ActiveScp096Rage
- ActiveScp096HeatingUp
- ActiveScp096WithoutFace
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 | 🟠 Major

ClassDBotanist не должен получать ScpHold по заявленному scope фичи.

Сейчас механика удержания уходит в low-access роль, что расширяет доступ вне целевых подразделений и ломает баланс/авторизацию фичи.

💡 Предлагаемое исправление
-    - type: ScpHold
-      holdableWhitelist:
-        components:
-        - Scp096
-      holdableBlacklist:
-        components:
-        - ActiveScp096Rage
-        - ActiveScp096HeatingUp
-        - ActiveScp096WithoutFace
📝 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
- type: ScpHold
holdableWhitelist:
components:
- Scp096
holdableBlacklist:
components:
- ActiveScp096Rage
- ActiveScp096HeatingUp
- ActiveScp096WithoutFace
# ScpHold component removed from ClassDBotanist role
# This role should not have SCP hold capability per feature scope requirements
🤖 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 удалите назначение
механики удержания: уберите или переместите блок type: ScpHold (и связанные поля
holdableWhitelist / holdableBlacklist) из class_d_botanist.yml, т.е. удалите
упоминание ScpHold у ClassDBotanist и перенесите эту фичу в правильный
scope/роль, где удержание должно быть доступно; убедитесь, что поля
holdableWhitelist и holdableBlacklist больше не присутствуют в описании
ClassDBotanist (или добавьте вместо них ссылку на корректный класс/роль,
отвечающую за ScpHold).

Comment on lines +31 to +40
- type: ScpHold
holdableWhitelist:
components:
- Scp096
- ClassDAppearance
holdableBlacklist:
components:
- ActiveScp096Rage
- ActiveScp096HeatingUp
- ActiveScp096WithoutFace
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

Рекомендация по уменьшению дублирования (опционально).

Конфигурация ScpHold с одинаковым whitelist/blacklist повторяется в нескольких job-файлах. Для улучшения поддерживаемости можно рассмотреть:

  • Создание базового job-прототипа с общей конфигурацией ScpHold
  • Или использование отдельного компонента-шаблона

Это необязательное улучшение — текущий подход работает и обеспечивает гибкость настройки per-job.

🤖 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, Конфигурация ScpHold с одинаковыми секциями
holdableWhitelist и holdableBlacklist (components: Scp096, ClassDAppearance и
blacklist: ActiveScp096Rage, ActiveScp096HeatingUp, ActiveScp096WithoutFace)
повторяется в нескольких job-файлах; вынесите её в общий шаблон или базовый
job-прототип (например: базовый прототип/компонент с именем ScpHoldBase) и
замените дублирующие блоки в файлах на ссылку/наследование этого шаблона,
сохранив возможность переопределения по нужде.

Comment on lines +30 to +39
- type: ScpHold
holdableWhitelist:
components:
- Scp096
- ClassDAppearance
holdableBlacklist:
components:
- ActiveScp096Rage
- ActiveScp096HeatingUp
- ActiveScp096WithoutFace
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 | 🟠 Major

Уберите ClassDAppearance из whitelist для этого SCP-096 сценария.

Добавление ClassDAppearance расширяет применение механики на нерелевантные цели. Для заявленного сценария OCN здесь достаточно фокуса на Scp096 + state blacklist.

💡 Предлагаемое исправление
     - type: ScpHold
       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, В блоке конфигурации ScpHold удалите запись
ClassDAppearance из holdableWhitelist — оставьте только Scp096 в whitelist и
сохраните текущий holdableBlacklist (ActiveScp096Rage, ActiveScp096HeatingUp,
ActiveScp096WithoutFace) чтобы фокусировать механику только на SCP-096;
проверьте секцию ScpHold и поля holdableWhitelist/holdableBlacklist и уберите
только ClassDAppearance, не трогая остальные ключи.

Comment on lines +264 to +265
- type: ScpHoldRestricted # Fire edit - block combat mode while held
stage: Soft
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

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

Это upstream-файл, поэтому для этой изменённой строки нужно использовать Sunrise-маркер.

As per coding guidelines: "This repository's repository name is sunrise-station, so use the Sunrise prefix ... and Sunrise-Edit ... 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, 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.

Comment on lines +281 to +282
- type: ScpHold # TODO: Убрать перед мержем
- type: ScpHoldable
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

Используйте Sunrise-маркер для upstream-правки.

Для изменённых строк в upstream YAML здесь должен использоваться # Sunrise-Edit, а не Fire-формат.

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,
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.

⚠️ Potential issue | 🔴 Critical

Уберите ScpHold из BaseMobSpeciesOrganic.

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

💡 Предлагаемое исправление
-  - type: ScpHold # TODO: Убрать перед мержем
   - type: ScpHoldable
🤖 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,
В BaseMobSpeciesOrganic удалите запись типа ScpHold (оставьте ScpHoldable если
нужно), чтобы базовый органический прототип больше не получал механику
удержания; вместо этого назначьте компонент ScpHold только нужным ролям через
job "special" (проверьте где определяется job "special" и добавьте/переместите
тип ScpHold туда).

@ThereDrD0 ThereDrD0 marked this pull request as draft April 14, 2026 03:19
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