Skip to content

Новые призраки#1070

Open
nekosich wants to merge 4 commits intoWWhiteDreamProject:masterfrom
nekosich:new-ghost
Open

Новые призраки#1070
nekosich wants to merge 4 commits intoWWhiteDreamProject:masterfrom
nekosich:new-ghost

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

Рефакторинг спауна призраков с добавлением режима наблюдателя лобби и снимков внешности; клиентские per‑entity post‑shader для тонирования призраков и управление alpha_cutoff в outline; массовое добавление прототипов визуальных наблюдателей по видам и новые шейдеры ресурсов.

Changes

Cohort / File(s) Summary
Сервер: система спауна призраков
Content.Server/Ghost/GhostSystem.cs, Content.Server/Abilities/Psionics/PsionicAbilitiesSystem.cs, Content.Server/GameTicking/GameTicker.Spawning.cs
Введён SpawnLobbyObserverGhost, вынесена общая SpawnGhostInternal с режимами (Default, LobbyObserver), добавлена поддержка sourceEntity, логика захвата/кеширования/применения визуальных снимков (внешность/инвентарь/движение), подписки на события жизненного цикла.
Клиент: визуализация призраков и outline
Content.Client/Ghost/GhostSystem.cs, Content.Client/Interactable/Components/InteractionOutlineComponent.cs
Добавлены per‑entity composite post‑shader для призраков (параметры ghost_tint/ghost_alpha), хранение и очистка ShaderInstance, применение визуалов при стартапе/обновлениях/удалении; outline — добавлено alpha_cutoff, восстановление предыдущего PostShader и управление lifecycle шейдера.
Прототипы наблюдателей
Resources/Prototypes/Entities/Mobs/Player/observer.yml
Добавлена абстрактная MobObserverVisualBase, MobObserverVisualHumanoid и ~30 видов наследников с индивидуальными слоёвыми спрайтами, inventory/templates, DamageVisuals и связанной конфигурацией; удалён блок нескольких публичных action.
Шейдеры и ресурсы шейдеров
Resources/Textures/Shaders/ghost_composite_tint.swsl, Resources/Textures/Shaders/outline.swsl, Resources/Prototypes/Shaders/shaders.yml, Resources/Prototypes/Shaders/outline.yml
Добавлен шейдер GhostCompositeTint и SWSL с uniform'ами ghost_tint/ghost_alpha; outline.swsl получил alpha_cutoff и функцию маскирования альфа; параметры alpha_cutoff добавлены в outline.yml; shaders.yml обновлён (регистрация нового шейдера, путь Hologram выровнен).

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested labels

Changes: Sprite

Suggested reviewers

  • Remuchi
  • DVOniksWyvern
  • Spatison
🚥 Pre-merge checks | ✅ 3 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 19.05% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Новые призраки' accurately describes the main change - adding new ghost/observer visuals for all player races with per-species customization.
Description check ✅ Passed The description is directly related to the changeset, detailing new ghosts for all game races, lobby support, damage reflections, and including appropriate changelog entries.
Merge Conflict Detection ✅ Passed ✅ No merge conflicts detected when merging into master

✏️ 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

No actionable comments were generated in the recent review. 🎉

🧹 Recent nitpick comments
Content.Client/Ghost/GhostSystem.cs (2)

27-27: _compositeGhostShaders не очищается при Shutdown системы.

Если GhostSystem будет выгружена (например, при дисконнекте от сервера), шейдеры в словаре не будут Dispose'd. Рекомендуется добавить override void Shutdown() для очистки.

♻️ Предложение: очистка шейдеров при Shutdown
+        public override void Shutdown()
+        {
+            base.Shutdown();
+
+            foreach (var shader in _compositeGhostShaders.Values)
+                shader.Dispose();
+
+            _compositeGhostShaders.Clear();
+        }

249-261: EnsureGhostCompositeShader: потенциальный конфликт с PostShader от других систем.

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

Resources/Prototypes/Entities/Mobs/Player/observer.yml (2)

96-143: MobObserverVisualBase дублирует почти все компоненты из MobObserverBase.

Оба определения содержат идентичный набор: ContentEye, Eye, Input, Examiner, Ghost, GhostHearing, ShowElectrocutionHUD, IntrinsicRadioReceiver, ActiveRadio, UniversalLanguageSpeaker, PointLight, UserInterface. Отличия: MobObserverVisualBase не имеет Sprite и добавляет NoNormalInteraction, Spectral, AllowGhostShownByEvent.

Если любой из этих общих компонентов изменится (например, добавится новый visMask в Eye или изменится радиус PointLight), придётся обновлять оба места. Рассмотрите вынесение общих компонентов в промежуточный абстрактный родитель.


221-302: MobObserverVisualArachne наследует MobObserverVisualBase, а не MobObserverVisualHumanoid, и не имеет IgnoreSlowOnDamage/SpeedModifierImmunity.

MobObserverVisualHumanoid (строки 218-219) добавляет IgnoreSlowOnDamage и SpeedModifierImmunity. У MobObserverVisualArachne (родитель — MobObserverVisualBase) этих компонентов нет. Для призрака это, вероятно, не критично (призраки и так проходят сквозь стены), но для консистентности стоит добавить или убедиться, что это не влияет на UX при наличии DamageVisuals + Damageable.


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
Пометил мои изменения
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: 3

🤖 Fix all issues with AI agents
In `@Content.Client/Ghost/GhostSystem.cs`:
- Around line 239-246: The code is using the non-existent lowercase property
component.color; update the three usages to the correct PascalCase property
GhostComponent.Color — specifically replace component.color in the
shader.SetParameter("ghost_tint", ...), in the Math.Clamp line for
"ghost_alpha", and in the final sprite.Color assignment where
RemoveGhostCompositeShader is called; ensure you reference GhostComponent.Color
and keep the rest of the expressions (VisualObserverAlphaMultiplier,
RemoveGhostCompositeShader, shader.SetParameter) unchanged.

In `@Content.Server/Ghost/GhostSystem.cs`:
- Around line 795-815: TryPickLobbyObserverJob currently returns the first
High-priority entry from profile.JobPriorities which is non-deterministic
because Dictionary iteration order is undefined; instead collect all entries
where priority == JobPriority.High and
_prototypeManager.HasIndex<JobPrototype>(job), then pick one deterministically
(e.g. OrderBy the job's unique identifier or name) so selection is stable across
runs; keep the existing overflow fallback logic and return jobId/default the
same way. Reference: TryPickLobbyObserverJob, profile.JobPriorities,
JobPriority.High, _prototypeManager.HasIndex<JobPrototype>, and
SharedGameTicker.FallbackOverflowJob.
- Line 104: _mindHumanoidSnapshotSources can retain mappings for long-lived mind
entities leaving snapshots in Nullspace; in addition to removing entries on
EntityTerminatingEvent, update TrySpawnBodySnapshotGhost to remove (or null out)
the entry for the mind when a snapshot is successfully consumed: locate
TrySpawnBodySnapshotGhost and after the code path that successfully spawns/uses
the snapshot, call _mindHumanoidSnapshotSources.Remove(mindEntity) (or otherwise
clear the mapping) so the snapshot reference is released and not left in
Nullspace.
🧹 Nitpick comments (3)
Content.Client/Ghost/GhostSystem.cs (1)

27-27: Словарь _compositeGhostShaders не очищается при Shutdown системы.

При переподключении или пересоздании системы шейдеры не будут dispose'нуты. Рекомендуется переопределить Shutdown() и вычистить/dispose все шейдеры из словаря.

♻️ Добавить очистку при Shutdown
+        public override void Shutdown()
+        {
+            base.Shutdown();
+
+            foreach (var shader in _compositeGhostShaders.Values)
+                shader.Dispose();
+
+            _compositeGhostShaders.Clear();
+        }
Content.Server/Ghost/GhostSystem.cs (2)

739-793: В TrySpawnLobbyObserverGhost при неудаче LoadProfile или экипировки ghost-сущность не удаляется.

Если _humanoid.LoadProfile выбросит исключение или _stationSpawning.EquipStartingGear упадёт, спаунённая сущность ghost останется, но метод вернёт true с частично настроенным призраком. Это может привести к визуальным артефактам. Рекомендуется обернуть в try-catch или валидировать состояние перед возвратом.

Также, если TryPickLobbyObserverJob не находит подходящую работу (строки 765-769), призрак создаётся без одежды — убедитесь, что это ожидаемое поведение.


917-935: TryGetVisualObserverPrototypeFromDoll использует хрупкую строковую эвристику для извлечения species ID.

Метод предполагает формат Mob{SpeciesId}Dummy для doll-прототипов. Если формат именования изменится, метод молча перестанет работать. Это допустимо как fallback, но стоит добавить комментарий о предполагаемом формате.


private EntityQuery<GhostComponent> _ghostQuery;
private EntityQuery<PhysicsComponent> _physicsQuery;
private readonly Dictionary<EntityUid, EntityUid> _mindHumanoidSnapshotSources = new();
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 | 🟡 Minor

_mindHumanoidSnapshotSources может накапливать сущности в Nullspace, если mind-сущности живут долго.

Снапшоты очищаются только при EntityTerminatingEvent на mind-сущности (строка 334-337). Если mind-сущность существует дольше, чем нужен снапшот (например, игрок возвращается в тело), снапшот остаётся в Nullspace до конца раунда. Стоит рассмотреть очистку снапшота и при успешном использовании в TrySpawnBodySnapshotGhost.

♻️ Очистка снапшота после использования
         private EntityUid? ResolveVisualSnapshotSource(EntityUid mindId, EntityUid? sourceEntity)
         {
             if (sourceEntity is { } source && Exists(source) && HasComp<HumanoidAppearanceComponent>(source))
+            {
+                // Source entity is still available, clean up any stale snapshot
+                ClearMindHumanoidSnapshot(mindId);
                 return source;
+            }
 
             return TryGetCachedMindHumanoidSnapshot(mindId, out var cached) ? cached : null;
         }
🤖 Prompt for AI Agents
In `@Content.Server/Ghost/GhostSystem.cs` at line 104,
_mindHumanoidSnapshotSources can retain mappings for long-lived mind entities
leaving snapshots in Nullspace; in addition to removing entries on
EntityTerminatingEvent, update TrySpawnBodySnapshotGhost to remove (or null out)
the entry for the mind when a snapshot is successfully consumed: locate
TrySpawnBodySnapshotGhost and after the code path that successfully spawns/uses
the snapshot, call _mindHumanoidSnapshotSources.Remove(mindEntity) (or otherwise
clear the mapping) so the snapshot reference is released and not left in
Nullspace.

Comment on lines +795 to +815
private bool TryPickLobbyObserverJob(HumanoidCharacterProfile profile, out ProtoId<JobPrototype> jobId)
{
foreach (var (job, priority) in profile.JobPriorities)
{
if (priority != JobPriority.High || !_prototypeManager.HasIndex<JobPrototype>(job))
continue;

jobId = job;
return true;
}

var overflowJob = new ProtoId<JobPrototype>(SharedGameTicker.FallbackOverflowJob);
if (_prototypeManager.HasIndex<JobPrototype>(overflowJob))
{
jobId = overflowJob;
return true;
}

jobId = default;
return false;
}
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 | 🟡 Minor

TryPickLobbyObserverJob берёт только первую работу с JobPriority.High.

Если у профиля несколько High-приоритетных работ, будет выбрана первая из Dictionary итерации, порядок которой не детерминирован. Это может приводить к непредсказуемому выбору работы при каждом спауне.

🤖 Prompt for AI Agents
In `@Content.Server/Ghost/GhostSystem.cs` around lines 795 - 815,
TryPickLobbyObserverJob currently returns the first High-priority entry from
profile.JobPriorities which is non-deterministic because Dictionary iteration
order is undefined; instead collect all entries where priority ==
JobPriority.High and _prototypeManager.HasIndex<JobPrototype>(job), then pick
one deterministically (e.g. OrderBy the job's unique identifier or name) so
selection is stable across runs; keep the existing overflow fallback logic and
return jobId/default the same way. Reference: TryPickLobbyObserverJob,
profile.JobPriorities, JobPriority.High,
_prototypeManager.HasIndex<JobPrototype>, and
SharedGameTicker.FallbackOverflowJob.

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