Skip to content

Conversation

@nekosich
Copy link
Contributor

@nekosich nekosich commented Feb 12, 2026

Описание PR

Новые призраки для всех игровых рас!
Поддерживает заход из лобби и "вроде бы" не ломает кастомных гостов.
Все повреждения на призраке отражают повреждения на момент смерти.
Бууу!


Медиа

Список

image

trim.CF68E131-A262-48A4-B1D6-834812DBDB14.MOV


Изменения

🆑 nekosich

  • tweak: Новые призраки для игровых рас. Бу~

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 12, 2026

Walkthrough

Внедрение системы визуального отображения призраков с поддержкой снимков тела и выделенных призраков лобби. Включает рефакторинг серверной системы призраков для управления различными режимами спаунинга, добавление клиентского затенения на основе шейдеров, расширение прототипов сущностей наблюдателя для множества видов, а также обновление логики взаимодействия с новыми параметрами альфа-отсечки.

Changes

Cohort / File(s) Summary
Серверная система призраков
Content.Server/Ghost/GhostSystem.cs, Content.Server/Abilities/Psionics/PsionicAbilitiesSystem.cs, Content.Server/GameTicking/GameTicker.Spawning.cs
Существенный рефакторинг логики спаунинга призраков с поддержкой снимков тела, добавлением режима SpawnLobbyObserverGhost, копированием движения и состояния сущности-источника, управлением загрузками работ для наблюдателей лобби, кешированием гуманоидных появлений и обновленной сигнатурой SpawnGhost.
Клиентская визуализация и шейдеры
Content.Client/Ghost/GhostSystem.cs, Content.Client/Interactable/Components/InteractionOutlineComponent.cs, Resources/Textures/Shaders/ghost_composite_tint.swsl, Resources/Textures/Shaders/outline.swsl
Внедрение затенения призраков на основе шейдеров с новыми параметрами tint и alpha, добавление управления шейдерами на уровне сущности, введение альфа-отсечки в компонент взаимодействия с учетом наличия GhostComponent и создание новой функции маскирования альфа.
Прототипы шейдеров
Resources/Prototypes/Shaders/shaders.yml, Resources/Prototypes/Shaders/outline.yml
Добавление новой записи GhostCompositeTint в шейдеры, исправление выравнивания пути для Hologram, добавление параметра alpha_cutoff к SelectionOutline и SelectionOutlineInrange.
Прототипы сущностей наблюдателей
Resources/Prototypes/Entities/Mobs/Player/observer.yml
Массовое расширение с базовой сущностью MobObserverVisualBase, набором видоспецифичных вариантов MobObserverVisualHumanoid для ~30 видов с индивидуальными спрайтами, инвентарем, повреждениями и визуалом, а также новыми действиями и псионическим наблюдателем.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested labels

Changes: Sprite

Suggested reviewers

  • Remuchi
  • Spatison
🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 4.88% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description check ✅ Passed Описание PR связано с наборами изменений: упоминает новых призраков для рас, поддержку лобби и сохранение повреждений при смерти, что соответствует коду.
Title check ✅ Passed Заголовок 'Новые призраки' соответствует основному изменению: добавление новых визуальных вариантов призраков для всех игровых рас с поддержкой входа из лобби.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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
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: 2

🤖 Fix all issues with AI agents
In `@Content.Server/Ghost/GhostSystem.cs`:
- Around line 859-878: TryCaptureMindHumanoidSnapshot currently spawns a full
entity in Nullspace and calls CopyInventorySnapshot which creates real item
instances (with active components); change the snapshot workflow so spawned
snapshot entities are visual-only: modify CopyInventorySnapshot (or add a new
CreateVisualInventorySnapshot helper) to spawn lightweight visual placeholders
or clone only appearance/state (using CloneAppearance-like logic) instead of
full prototypes, or spawn entities with initialization suppressed/stripped of
runtime components (remove/avoid SolutionContainerComponent,
DeviceNetworkComponent, etc.) before they initialize; ensure
_mindHumanoidSnapshotSources still maps to the placeholder snapshot and that
CloneAppearance/CopyDamageSnapshot operate on that visual-only entity so no
side-effectful components run in Nullspace.
🧹 Nitpick comments (7)
Resources/Prototypes/Entities/Mobs/Player/observer.yml (1)

95-143: Значительное дублирование компонентов с MobObserverBase.

MobObserverVisualBase практически полностью дублирует список компонентов из MobObserverBase (lines 31–83), с добавлением NoNormalInteraction, Spectral и тега AllowGhostShownByEvent. Если MobObserverBase изменится (например, добавится новый компонент для призраков), MobObserverVisualBase нужно будет синхронизировать вручную.

Рассмотрите возможность наследования MobObserverVisualBase от MobObserverBase (или общего абстрактного родителя) с добавлением только различающихся компонентов, чтобы избежать рассинхронизации.

Content.Client/Interactable/Components/InteractionOutlineComponent.cs (1)

74-82: Owner vs uid — незначительная несогласованность.

MakeNewShader использует Owner для проверки GhostComponent, тогда как вызывающие методы (OnMouseEnter, UpdateInRange) получают uid в качестве параметра. Хотя Owner == uid в данном контексте, передача uid в MakeNewShader была бы более явной и согласованной с остальным кодом компонента.

♻️ Предлагаемое изменение
-        private ShaderInstance MakeNewShader(bool inRange, int renderScale)
+        private ShaderInstance MakeNewShader(bool inRange, int renderScale, EntityUid uid)
         {
             var shaderName = inRange ? ShaderInRange : ShaderOutOfRange;
 
             var instance = _prototypeManager.Index<ShaderPrototype>(shaderName).InstanceUnique();
             instance.SetParameter("outline_width", DefaultWidth * renderScale);
-            instance.SetParameter("alpha_cutoff", _entMan.HasComponent<GhostComponent>(Owner) ? GhostAlphaCutoff : DefaultAlphaCutoff);
+            instance.SetParameter("alpha_cutoff", _entMan.HasComponent<GhostComponent>(uid) ? GhostAlphaCutoff : DefaultAlphaCutoff);
             return instance;
         }

И обновить вызовы:

-            _shader = MakeNewShader(inInteractionRange, renderScale);
+            _shader = MakeNewShader(inInteractionRange, renderScale, uid);
-                _shader = MakeNewShader(_inRange, _lastRenderScale);
+                _shader = MakeNewShader(_inRange, _lastRenderScale, uid);
Content.Client/Ghost/GhostSystem.cs (1)

230-244: Перезапись PostShader может конфликтовать с другими системами.

EnsureGhostCompositeShader безусловно назначает sprite.PostShader, затирая любой ранее установленный шейдер (например, от системы аутлайнов или кастомных эффектов). Если другая система уже задала PostShader для этого спрайта, он будет потерян без возможности восстановления.

Стоит рассмотреть сохранение предыдущего PostShader перед назначением и его восстановление в RemoveGhostCompositeShader, либо как минимум документировать, что призраки-наблюдатели не поддерживают другие пост-шейдеры.

Content.Server/Ghost/GhostSystem.cs (4)

790-810: TryPickLobbyObserverJob — выбирает только JobPriority.High.

Метод берёт первую работу с приоритетом High. Если у игрока несколько таких работ, выбор зависит от порядка итерации Dictionary<ProtoId<JobPrototype>, JobPriority>. Это недетерминировано — разные запуски могут давать разные результаты для одного и того же профиля.

Если это намеренно (любая High-работа подойдёт), стоит добавить комментарий. Если нет — рассмотреть сортировку или использование profile.PreferenceUnavailable / приоритизацию по другому критерию.


912-930: Хрупкий парсинг имени прототипа куклы.

TryGetVisualObserverPrototypeFromDoll полагается на жёсткую конвенцию Mob{Species}Dummy для извлечения имени вида. Это работает для стандартных прототипов, но любое отклонение от конвенции (например, MobCustomSpeciesDoll, MobHumanoidDummy) приведёт к генерации неправильного имени.

Это скорее brittle code, а не баг, т.к. fallback на VisualObserverPrototypeName обеспечивает безопасный путь. Однако стоит добавить комментарий, документирующий ожидаемый формат.

📝 Предложение: добавить документирующий комментарий
+        /// <summary>
+        /// Extracts species ID from doll prototype following the convention "Mob{SpeciesId}Dummy".
+        /// Returns null if the prototype name doesn't match this pattern.
+        /// </summary>
         private static string? TryGetVisualObserverPrototypeFromDoll(EntProtoId dollPrototype)

598-608: Два публичных метода SpawnGhost с разными сигнатурами — потенциальная путаница в API.

Существуют два перегрузки SpawnGhost:

  • SpawnGhost(mind, targetEntity, canReturn) (строка 598) — принимает EntityUid, использует targetEntity и как точку спауна, и как sourceEntity.
  • SpawnGhost(mind, spawnPosition, canReturn, sourceEntity) (строка 628) — принимает EntityCoordinates?.

Плюс SpawnLobbyObserverGhost (строка 605).

Перегрузка на строке 598-603 принимает EntityUid targetEntity, вычисляет из него координаты и передаёт тот же targetEntity как sourceEntity. Это предполагает, что targetEntity — это тело игрока. Название targetEntity вводит в заблуждение — sourceBody или bodyEntity было бы яснее.

💡 Предложение: переименовать параметр для ясности
-        public EntityUid? SpawnGhost(Entity<MindComponent?> mind, EntityUid targetEntity,
+        public EntityUid? SpawnGhost(Entity<MindComponent?> mind, EntityUid sourceBody,
             bool canReturn = false)
         {
-            _transformSystem.TryGetMapOrGridCoordinates(targetEntity, out var spawnPosition);
-            return SpawnGhost(mind, spawnPosition, canReturn, targetEntity);
+            _transformSystem.TryGetMapOrGridCoordinates(sourceBody, out var spawnPosition);
+            return SpawnGhost(mind, spawnPosition, canReturn, sourceBody);
         }

104-104: Словарь _mindHumanoidSnapshotSources не очищается при RoundRestartCleanupEvent.

Если раунд перезапускается, а сущности разума уничтожаются (с вызовом EntityTerminatingEvent), снапшоты будут очищены поштучно. Однако, если по какой-то причине EntityTerminatingEvent не сработает для всех mind-сущностей, словарь будет содержать stale EntityUid. Рекомендуется подписаться на RoundRestartCleanupEvent для полной очистки.

🧹 Предложение: очистка при перезапуске раунда

В Initialize():

  SubscribeLocalEvent<MindComponent, EntityTerminatingEvent>(OnMindTerminating);
+ SubscribeLocalEvent<RoundRestartCleanupEvent>(OnRoundRestart);

Новый метод:

+ private void OnRoundRestart(RoundRestartCleanupEvent ev)
+ {
+     foreach (var snapshot in _mindHumanoidSnapshotSources.Values)
+     {
+         if (Exists(snapshot) && !TerminatingOrDeleted(snapshot))
+             QueueDel(snapshot);
+     }
+     _mindHumanoidSnapshotSources.Clear();
+ }

Comment on lines +859 to +878
private bool TryCaptureMindHumanoidSnapshot(EntityUid mindId, EntityUid sourceEntity)
{
if (!Exists(sourceEntity) || !TryGetVisualObserverPrototype(sourceEntity, out var ghostPrototype))
return false;

ClearMindHumanoidSnapshot(mindId);

var snapshot = Spawn(ghostPrototype, MapCoordinates.Nullspace);
if (!HasComp<HumanoidAppearanceComponent>(snapshot))
{
QueueDel(snapshot);
return false;
}

_mindHumanoidSnapshotSources[mindId] = snapshot;
_humanoid.CloneAppearance(sourceEntity, snapshot);
CopyDamageSnapshot(sourceEntity, snapshot);
CopyInventorySnapshot(sourceEntity, snapshot);
return true;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Снапшот-сущность в Nullspace содержит полные клоны предметов инвентаря.

TryCaptureMindHumanoidSnapshot спаунит сущность в Nullspace и вызывает CopyInventorySnapshot, который создаёт полные экземпляры предметов (не визуальные заглушки). Эти предметы могут содержать компоненты с побочными эффектами (например, SolutionContainerComponent, DeviceNetworkComponent и т.д.), которые будут инициализированы при спауне.

Для чисто визуального снапшота это избыточно и потенциально проблематично — побочные эффекты инициализации компонентов, утечки ресурсов, нагрузка на сеть и ECS.

🤖 Prompt for AI Agents
In `@Content.Server/Ghost/GhostSystem.cs` around lines 859 - 878,
TryCaptureMindHumanoidSnapshot currently spawns a full entity in Nullspace and
calls CopyInventorySnapshot which creates real item instances (with active
components); change the snapshot workflow so spawned snapshot entities are
visual-only: modify CopyInventorySnapshot (or add a new
CreateVisualInventorySnapshot helper) to spawn lightweight visual placeholders
or clone only appearance/state (using CloneAppearance-like logic) instead of
full prototypes, or spawn entities with initialization suppressed/stripped of
runtime components (remove/avoid SolutionContainerComponent,
DeviceNetworkComponent, etc.) before they initialize; ensure
_mindHumanoidSnapshotSources still maps to the placeholder snapshot and that
CloneAppearance/CopyDamageSnapshot operate on that visual-only entity so no
side-effectful components run in Nullspace.

Comment on lines +970 to +994
private void CopyInventorySlotVisualSnapshot(EntityUid sourceItem, EntityUid ghost, string slotName, EntityCoordinates ghostCoordinates)
{
var prototype = MetaData(sourceItem).EntityPrototype?.ID;
if (prototype == null || !_prototypeManager.HasIndex<EntityPrototype>(prototype))
return;

var clone = SpawnAtPosition(prototype, ghostCoordinates);

if (TryComp<ItemComponent>(sourceItem, out var sourceItemComp) &&
TryComp<ItemComponent>(clone, out var cloneItemComp))
{
_item.CopyVisuals(clone, sourceItemComp, cloneItemComp);
}

if (TryComp<ClothingComponent>(sourceItem, out var sourceClothingComp) &&
TryComp<ClothingComponent>(clone, out var cloneClothingComp))
{
_clothing.CopyVisuals(clone, sourceClothingComp, cloneClothingComp);
}

_appearance.CopyData(sourceItem, clone);

if (!_inventory.TryEquip(ghost, clone, slotName, silent: true, force: true))
QueueDel(clone);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Полный спаун предметов для визуального клонирования — риск побочных эффектов.

CopyInventorySlotVisualSnapshot создаёт полноценные сущности через SpawnAtPosition по прототипу исходного предмета. Каждый такой предмет проходит полную инициализацию всех компонентов (MapInit, ComponentStartup и т.д.), что может вызвать:

  • Регистрацию в сетевых системах устройств
  • Инициализацию реагентов/растворов
  • Срабатывание триггеров
  • Регистрацию в системах питания/атмосферы

Для визуальной копии одежды на призраке достаточно было бы спаунить упрощённые визуальные заглушки или отключать ненужные компоненты после спауна.

@nekosich nekosich changed the title new-ghost Новые призраки Feb 12, 2026
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