Files
necromants-tome-7d2d-3-2/HarmonySrc/CorpseLootPatch.cs
T
AlexCubeandClaude Opus 5.5 77b56be9ae 1.4.1: утечка видеопамяти от трупов «Жертвы», труп лежит 5 минут
Было: CorpseLootPatch перекрашивал труп через renderer.materials и Tint(),
и ничего за собой не убирал. Копии материалов Unity с объектом не удаляет,
перекрашенная текстура была несжатой RGBA32 (16-64 МБ) и читаемой (копия
ещё и в RAM), кеш только рос. Лог игры: Texture.currentTextureMemory
1,2 -> 7,4 ГБ за два часа при desired ~3,5 ГБ, FPS 60 -> 11. Те же жалобы
на Nexus (sanovskiy, Kriloko, Megaladon43).

Стало: каждый перекрашенный труп записан (рендереры, материалы, текстуры),
тик раз в секунду удаляет всё за трупом, которого нет в мире, текстура
удаляется со счётчиком пользователей. Текстура не больше 1024, DXT,
makeNoLongerReadable. Проверено в игре: обыск и истечение срока доводят
счёт до «трупов 0, текстур 0».

Необысканный труп лежит 300 с вместо 3600 (указание пользователя).
buffNecroZombieKillTrackerDisplay: update_rate .1 -> 1 (отзыв zomb1);
урон ножа растёт после убийства, проверено.

Документы: версия 1.4.1, README (статус + несовместимость с 3.3
experimental), описания Nexus и сайта (пять минут вместо часа),
NEXUS_CHANGELOG/FILE_SUMMARY_1.4.1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 23:36:51 +03:00

552 lines
28 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.Collections.Generic;
using HarmonyLib;
using UnityEngine;
namespace NecromancerTome
{
/// <summary>
/// ТРУП ПОМЕЧЕННОГО ЗОМБИ САМ СТАНОВИТСЯ ДОБЫЧЕЙ — указание 2026-09-18: «зомби после упокоения
/// оставляет свой труп. Можем ли мы подменять этот труп в момент смерти точно таким же, но с
/// другой текстурой и с функцией мешка жертвы (не исчезает, можно забрать лут)?»
///
/// ПОДМЕНЯТЬ НЕ ПРИШЛОСЬ НИЧЕГО, и это главное, что стоит знать. Труп — не отдельная модель, а
/// та же скелетная модель зомби в позе смерти, поэтому «такой же, но с другой текстурой» — это
/// перекраска материалов уже лежащего трупа, тем же приёмом, что призрачные питомцы и глаза
/// собаки. Ни нового префаба, ни новой сущности.
///
/// ТРИ ЧАСТИ, И КАЖДАЯ ДЕРЖИТСЯ НА СВОЁМ ВАНИЛЬНОМ МЕХАНИЗМЕ:
///
/// 1. НЕ ИСЧЕЗАЕТ. `EntityAlive.timeStayAfterDeath` — обычное публичное поле экземпляра,
/// которое при спавне заполняется из entity_class (`TimeStayAfterDeath * 20`). У зомби
/// там 30 секунд, у ванильного мешка с лутом — 3600. Достаточно записать другое число.
///
/// 2. ЕГО МОЖНО ОБЫСКАТЬ. У сущностей есть своя система команд взаимодействия, отдельная от
/// блочной: `Entity.GetActivationCommands()` собирает список из виртуального
/// `InitLocalActivationCommands`, `AllowActivationCommand` решает, доступна ли команда
/// сейчас, `OnEntityActivated` её исполняет. Мешок с лутом объявляет свою так
/// (EntityItem, от которого он наследуется):
///
/// _addCallback(new EntityActivationCommand("search", "search"));
///
/// Мы объявляем ровно такую же — с тем же идентификатором и той же иконкой. Значит и
/// подпись на экране, и значок берутся готовые, локализованные на все языки игры, и
/// игроку это выглядит ровно как обыск мешка, потому что это и есть он.
///
/// 3. ВНУТРИ ЛУТ. А вот здесь сознательный обход. Само хранилище живёт внутри
/// `EntityLootContainer`: его `bag`, его `Read`/`Write` для сейва, его сетевая
/// синхронизация. Приделать всё это трупу зомби — работа масштаба Пространственного
/// хранилища, с выходом на сейвы, то есть на риск номер один этого проекта.
///
/// Поэтому обыск НЕ открывает труп, а ПРЕВРАЩАЕТ его в мешок «Жертва»: мешок спавнится
/// на месте трупа, труп убирается. Снаружи читается как «обыскал и нашёл», внутри —
/// обычный ванильный контейнер, который сам себя сохраняет, синхронизирует и открывает.
/// Ни одной новой сущности с состоянием.
///
/// ОБЫЧНЫЙ МЕШОК ЭТО НЕ ОТМЕНЯЕТ. Бросок вероятности в VictimPatch.cs остаётся как был: выпал
/// — обычный мешок лежит рядом, не выпал — только труп. «Жертва» переехала из «падает сразу» в
/// «лежит внутри трупа», и больше ничего не изменилось.
///
/// ПОЧЕМУ СПИСОК КОМАНД СБРАСЫВАЕТСЯ. `GetActivationCommands` кеширует результат в
/// `activationCommands` при первом обращении — а первое обращение случается, пока зомби ещё
/// жив и командой не обладает. Обнуление поля на смерти заставляет пересобрать список; тем же
/// приёмом пользуется сама игра в CopyPropertiesFromEntityClass.
/// </summary>
public static class CorpseLoot
{
/// <summary>Сколько секунд лежит помеченный труп. В 1.4.0 было 3600, как у ванильного мешка с
/// лутом; 2026-09-24 уменьшено до 5 минут по указанию пользователя: «если игрок не будет
/// убирать, у него FPS начнёт падать» - каждый лежащий труп это отрисовываемая модель со
/// своими копиями материалов, а необобранных за час набирается много.</summary>
public const float CorpseSecondsAfterDeath = 300f;
/// <summary>Во что красится труп. Тёмно-багровый: издалека видно, что этот не как все, и
/// при этом не выглядит подсветкой из другой игры.</summary>
public static readonly Color CorpseTint = new Color(0.55f, 0.12f, 0.16f);
/// <summary>Идентификатор берётся у ванильного мешка (EntityItem), чтобы подпись досталась
/// готовой и локализованной на все языки игры. ИКОНКА - "hand", а не "search", по указанию
/// 2026-09-18 («при наведении на зомби пиктограмма руки»); имена иконок к идентификатору
/// команды не привязаны, у ванильного "take" стоит ровно эта.</summary>
public const string SearchCommand = "search";
public const string SearchIcon = "hand";
/// <summary>Секунды удержания. Круговой индикатор рисует сама игра - XUiC_Timer, тот же,
/// что крутится при снятии верстака.</summary>
public const float SearchSeconds = 3f;
/// <summary>Трупы, которые ещё не обысканы. Ключ — entityId. Чистится сама: запись
/// снимается при обыске, а остальные уходят вместе с миром.</summary>
public static readonly HashSet<int> SearchableCorpses = new HashSet<int>();
/// <summary>Трупы, которые ещё не удалось перекрасить: модель бывает не собрана на том
/// кадре, когда сущность умирает. Добивается односекундным тиком PetFollowPatch.</summary>
public static readonly HashSet<int> PendingTint = new HashSet<int>();
/// <summary>УТЕЧКА ПАМЯТИ, НАЙДЕНА 2026-09-24 (жалоба: «память течёт, лечится только
/// перезапуском»). Версия 1.4.0 перекрашивала труп и НИЧЕГО за собой не убирала:
///
/// - `renderer.materials` создаёт копии материалов, и Unity НЕ удаляет их вместе с
/// объектом - они живут до перезапуска;
/// - перекрашенная текстура создавалась несжатой (RGBA32, 16-64 МБ на штуку) и оставалась
/// читаемой, то есть лежала дважды - в видеопамяти и копией в оперативной;
/// - кеш текстур только рос.
///
/// Лог игры (`60sec delta: ... current: ..., desired: ...` - это Texture.currentTextureMemory)
/// показал рост видеопамяти текстур с 1,2 до 7,4 ГБ за два часа при нужных игре ~3,5 ГБ, и
/// FPS упал с 60 до 11. За сессию было перекрашено 53 трупа по 4-10 материалов.
///
/// Теперь каждый перекрашенный труп записан здесь вместе с копиями материалов и текстурами,
/// а тик раз в секунду (ReleaseGoneCorpses) убирает за теми, кого уже нет. Текстура
/// удаляется, когда её не использует ни один труп.</summary>
public class TintedCorpse
{
public readonly List<Renderer> Renderers = new List<Renderer>();
public readonly List<Material> Materials = new List<Material>();
public readonly List<Texture2D> Textures = new List<Texture2D>();
}
public static readonly Dictionary<int, TintedCorpse> Tinted = new Dictionary<int, TintedCorpse>();
/// <summary>Сколько трупов сейчас носит каждую перекрашенную текстуру.</summary>
public static readonly Dictionary<Texture2D, int> TextureUsers = new Dictionary<Texture2D, int>();
/// <summary>Предел стороны перекрашенной текстуры. Исходные 2048-4096; труп рассматривают
/// вблизи, но 1024 со сжатием это ~1 МБ вместо 16-64 МБ, и разница на глаз незаметна.</summary>
public const int MaxTintSize = 1024;
/// <summary>Повторная попытка перекраски. Зовётся из общего тика раз в секунду.</summary>
public static void RetryPendingTints(World _world)
{
ReleaseGoneCorpses(_world);
if (PendingTint.Count == 0 || _world == null)
{
return;
}
List<int> done = new List<int>();
foreach (int id in PendingTint)
{
Entity corpse = _world.GetEntity(id);
if (corpse == null || TintCorpse(corpse))
{
done.Add(id);
}
}
foreach (int id in done)
{
PendingTint.Remove(id);
}
}
/// <summary>Уборка за исчезнувшими трупами. Трупом считается исчезнувшим, когда его нет в
/// мире И уничтожены все его рендереры: удалять материал, который ещё рисуется, нельзя.</summary>
public static void ReleaseGoneCorpses(World _world)
{
if (Tinted.Count == 0 || _world == null)
{
return;
}
List<int> gone = null;
foreach (KeyValuePair<int, TintedCorpse> pair in Tinted)
{
if (_world.GetEntity(pair.Key) != null)
{
continue;
}
bool rendered = false;
foreach (Renderer renderer in pair.Value.Renderers)
{
if (renderer != null)
{
rendered = true;
break;
}
}
if (rendered)
{
continue;
}
if (gone == null)
{
gone = new List<int>();
}
gone.Add(pair.Key);
}
if (gone == null)
{
return;
}
foreach (int id in gone)
{
TintedCorpse corpse = Tinted[id];
Tinted.Remove(id);
SearchableCorpses.Remove(id);
foreach (Material material in corpse.Materials)
{
if (material != null)
{
UnityEngine.Object.Destroy(material);
}
}
foreach (Texture2D texture in corpse.Textures)
{
ReleaseTexture(texture);
}
}
Debug.Log("[NecromancerTome] CorpseLoot: убрано за исчезнувшими трупами: " + gone.Count +
", осталось трупов " + Tinted.Count + ", текстур " + TextureUsers.Count);
}
public static void ReleaseTexture(Texture2D _texture)
{
if (_texture == null || !TextureUsers.TryGetValue(_texture, out int users))
{
return;
}
if (users > 1)
{
TextureUsers[_texture] = users - 1;
return;
}
TextureUsers.Remove(_texture);
List<Texture> keys = new List<Texture>();
foreach (KeyValuePair<Texture, Texture2D> pair in TintedTextures)
{
if (pair.Value == _texture)
{
keys.Add(pair.Key);
}
}
foreach (Texture key in keys)
{
TintedTextures.Remove(key);
}
UnityEngine.Object.Destroy(_texture);
}
public static bool IsSearchable(Entity _entity)
{
return _entity != null && SearchableCorpses.Contains(_entity.entityId);
}
/// <summary>Зовётся из VictimPatch, когда помеченный зомби умер.</summary>
public static void MarkCorpse(EntityAlive _victim)
{
if (_victim == null)
{
return;
}
SearchableCorpses.Add(_victim.entityId);
// timeStayAfterDeath хранится в ТИКАХ: игра заполняет его как TimeStayAfterDeath * 20
// (EntityAlive.CopyPropertiesFromEntityClass), то есть двадцать тиков на секунду.
_victim.timeStayAfterDeath = (int)(CorpseSecondsAfterDeath * 20f);
// Список команд уже собран и закеширован, пока зомби был жив и обыскивать его было
// нельзя. Обнуляем, чтобы собрался заново - уже с "search".
_victim.activationCommands = null;
if (!TintCorpse(_victim))
{
// Модель на этом кадре может быть ещё не готова - ровно тем же болели призрачные
// торговцы и разведение коллайдеров у питомцев. Кладём в очередь, тик добьёт.
PendingTint.Add(_victim.entityId);
}
Debug.Log("[NecromancerTome] CorpseLoot: труп " + _victim.entityId +
" помечен как обыскиваемый, лежит " + CorpseSecondsAfterDeath + " с");
}
/// <summary>Перекраска трупа. ПЕРЕПИСАНА 2026-09-18, первая версия не работала вообще:
/// пользователь сообщил «труп не перекрасился», и причина нашлась в нашем же логе.
///
/// Первая версия ставила `_Color` на материалах - ровно как сделано у глаз собаки. Но у
/// собаки глаз сидит на шейдере `Standard`, а тело зомби и шкура зверей - на собственных
/// шейдерах игры, и проба, которую печатает GhostTraderPatch, по ним говорит прямо:
///
/// shader 'Game/Character' ... tint property: &lt;none&gt;
/// shader 'Game/Animal/Fur' ... tint property: &lt;none&gt;
///
/// Свойства цвета у них НЕТ ВООБЩЕ. `HasProperty("_Color")` возвращал false, цикл не делал
/// ни одной итерации, и метод честно печатал «материалов 0» - которое никто не прочитал.
///
/// Поэтому красится САМА ТЕКСТУРА, тем же приёмом, которым мод обесцвечивает торговцев:
/// прогон через RenderTexture (игровые текстуры сжаты и с isReadable=false, напрямую
/// GetPixels по ним падает), затем попиксельное умножение на цвет. Разница с
/// обесцвечиванием ровно в одной строке арифметики, всё остальное - тот же путь, уже
/// проверенный на шести торговцах.
///
/// Результат кешируется по исходной текстуре: они 2048x2048 и больше, а помеченных трупов
/// за игру будет много - пересчитывать на каждый незачем.</summary>
public static readonly Dictionary<Texture, Texture2D> TintedTextures = new Dictionary<Texture, Texture2D>();
public static bool TintCorpse(Entity _corpse)
{
Renderer[] renderers = _corpse.GetComponentsInChildren<Renderer>(true);
if (renderers == null || renderers.Length == 0)
{
return false;
}
if (!Tinted.TryGetValue(_corpse.entityId, out TintedCorpse record))
{
record = new TintedCorpse();
Tinted[_corpse.entityId] = record;
}
int tinted = 0;
foreach (Renderer renderer in renderers)
{
if (renderer == null || renderer is ParticleSystemRenderer)
{
continue;
}
Material[] shared = renderer.sharedMaterials;
if (shared == null || shared.Length == 0)
{
continue;
}
// Копии материалов создаются этой строкой, и все они теперь наши - записываем
// каждую, даже ту, что перекрасить не удалось: удалять её тоже нам.
Material[] instances = renderer.materials;
if (!record.Renderers.Contains(renderer))
{
record.Renderers.Add(renderer);
}
for (int i = 0; i < instances.Length && i < shared.Length; i++)
{
Material material = instances[i];
if (material == null)
{
continue;
}
if (!record.Materials.Contains(material))
{
record.Materials.Add(material);
}
string albedo = GhostTraderPatch.FindAlbedoProperty(material);
if (albedo == null)
{
continue;
}
Texture2D repainted = Tint(material.GetTexture(albedo));
if (repainted == null)
{
continue;
}
material.SetTexture(albedo, repainted);
record.Textures.Add(repainted);
TextureUsers.TryGetValue(repainted, out int users);
TextureUsers[repainted] = users + 1;
tinted++;
}
}
Debug.Log("[NecromancerTome] CorpseLoot: труп " + _corpse.entityId + " перекрашен, материалов " + tinted);
return tinted > 0;
}
/// <summary>Копия GhostTraderPatch.Desaturate с одной изменённой строкой: вместо сведения
/// каналов к яркости - умножение на CorpseTint. Прогон через RenderTexture обязателен и там
/// и здесь по одной причине: игровые текстуры сжаты и не читаются процессором напрямую.</summary>
public static Texture2D Tint(Texture _source)
{
if (_source == null)
{
return null;
}
if (TintedTextures.TryGetValue(_source, out Texture2D cached))
{
return cached;
}
Texture2D painted = null;
RenderTexture rt = null;
RenderTexture previous = RenderTexture.active;
try
{
// Уменьшение идёт бесплатно, самим Blit в RenderTexture меньшего размера. Стороны
// кратны четырём - иначе Compress ниже откажется сжимать.
float scale = Mathf.Min(1f, (float)MaxTintSize / Mathf.Max(_source.width, _source.height));
int width = Mathf.Max(4, (int)(_source.width * scale) / 4 * 4);
int height = Mathf.Max(4, (int)(_source.height * scale) / 4 * 4);
rt = RenderTexture.GetTemporary(width, height, 0,
RenderTextureFormat.ARGB32, RenderTextureReadWrite.sRGB);
Graphics.Blit(_source, rt);
RenderTexture.active = rt;
painted = new Texture2D(width, height, TextureFormat.RGBA32, false);
painted.ReadPixels(new Rect(0f, 0f, width, height), 0, 0);
Color32[] pixels = painted.GetPixels32();
for (int i = 0; i < pixels.Length; i++)
{
Color32 p = pixels[i];
p.r = (byte)(p.r * CorpseTint.r);
p.g = (byte)(p.g * CorpseTint.g);
p.b = (byte)(p.b * CorpseTint.b);
pixels[i] = p;
}
painted.SetPixels32(pixels);
// Сжатие в DXT (вчетверо-восьмеро меньше RGBA32) и makeNoLongerReadable=true: без
// него копия пикселей оставалась в оперативной памяти рядом с видеопамятью.
painted.Compress(false);
painted.Apply(false, true);
}
catch (Exception e)
{
Debug.LogError("[NecromancerTome] CorpseLoot: не удалось перекрасить текстуру '" +
_source.name + "': " + e.Message);
if (painted != null)
{
UnityEngine.Object.Destroy(painted);
}
painted = null;
}
finally
{
RenderTexture.active = previous;
if (rt != null)
{
RenderTexture.ReleaseTemporary(rt);
}
}
TintedTextures[_source] = painted;
return painted;
}
/// <summary>Обыск: труп превращается в мешок «Жертва» на своём же месте.</summary>
public static void SearchCorpse(EntityAlive _corpse)
{
SearchableCorpses.Remove(_corpse.entityId);
Patch_EntityAlive_dropItemOnDeath_VictimBag.SpawnVictimBag(_corpse, false);
Debug.Log("[NecromancerTome] CorpseLoot: труп " + _corpse.entityId + " обыскан, заменён мешком");
if (_corpse.world != null)
{
_corpse.world.RemoveEntity(_corpse.entityId, EnumRemoveEntityReason.Despawned);
}
}
}
/// <summary>Добавляет команду «обыскать» в список. Постфикс, а не префикс: ванильные команды
/// сущности (у EntityAlive это «подобрать» для тех, у кого задан PickupItem) должны остаться.</summary>
[HarmonyPatch(typeof(EntityAlive), "InitLocalActivationCommands")]
public static class Patch_EntityAlive_InitLocalActivationCommands_Corpse
{
public static void Postfix(EntityAlive __instance, Action<EntityActivationCommand> _addCallback)
{
if (!CorpseLoot.IsSearchable(__instance))
{
return;
}
_addCallback(new EntityActivationCommand(CorpseLoot.SearchCommand, CorpseLoot.SearchIcon));
}
}
/// <summary>Разрешает команду только помеченному трупу. Без этого «обыскать» досталось бы
/// любому существу, у которого список команд пересобрался бы по любой причине.</summary>
[HarmonyPatch(typeof(EntityAlive), "AllowActivationCommand")]
public static class Patch_EntityAlive_AllowActivationCommand_Corpse
{
public static void Postfix(EntityAlive __instance, ReadOnlySpan<char> _commandName, ref bool __result)
{
if (!MemoryExtensions.Equals(_commandName, CorpseLoot.SearchCommand.AsSpan(), StringComparison.Ordinal))
{
return;
}
__result = CorpseLoot.IsSearchable(__instance);
}
}
/// <summary>ПОДСКАЗКА ПРИ НАВЕДЕНИИ. Баг-репорт 2026-09-18: «при наведении пиктограмма руки так
/// и не появилась. А без неё интуитивно непонятно что тут можно лутать».
///
/// Оказалось, я настроил не то место. Иконка у EntityActivationCommand рисуется в РАДИАЛЬНОМ
/// МЕНЮ (XUiC_Radial.SetCurrentEntityData -> CreateRadialEntry со спрайтом
/// $"ui_game_symbol_{icon}"), то есть только когда игрок ЗАЖМЁТ кнопку взаимодействия. А
/// подпись, которая появляется просто при наведении, берётся совсем из другого места - из
/// виртуального Entity.GetActivationText(), и у EntityAlive он для обычного существа
/// возвращает пустую строку.
///
/// Здесь возвращается ровно то же, что у ванильного лутаемого объекта (EntityItem):
///
/// string arg = playerInput.Activate.GetBindingXuiMarkupString()
/// + playerInput.PermanentActions.Activate.GetBindingXuiMarkupString();
/// return string.Format(Localization.Get("lootTooltipNew"), arg, имя);
///
/// `lootTooltipNew` - штатная строка лутаемых объектов, «{0} Search {1} (Untouched)» /
/// «{0}: обыскать {1} (нетронуто)», уже переведённая на все языки игры. Значок кнопки
/// подставляется разметкой [action:...] из первого аргумента, поэтому у игрока он будет ровно
/// такой же, как на любом мешке, и подпишется его собственной раскладкой.
///
/// Имя берётся у самого зомби (LocalizedEntityName), так что подсказка читается как
/// «обыскать Арлин», а не «обыскать мешок».</summary>
[HarmonyPatch(typeof(EntityAlive), "GetActivationText")]
public static class Patch_EntityAlive_GetActivationText_Corpse
{
public static void Postfix(EntityAlive __instance, ref string __result)
{
if (!CorpseLoot.IsSearchable(__instance))
{
return;
}
EntityPlayerLocal player = GameManager.Instance?.World?.GetPrimaryPlayer();
if (player == null)
{
return;
}
PlayerActionsLocal input = player.playerInput;
string key = input.Activate.GetBindingXuiMarkupString() +
input.PermanentActions.Activate.GetBindingXuiMarkupString();
__result = string.Format(Localization.Get("lootTooltipNew"), key,
Localization.Get(__instance.LocalizedEntityName));
}
}
/// <summary>Запускает обыск - НЕ мгновенно, а с трёхсекундным круговым индикатором (указание
/// 2026-09-18). Индикатор рисует сама игра: XUiC_Timer.OpenTimer, тот же механизм, что крутится
/// при снятии верстака, и знакомый игроку без объяснений.
///
/// ПОЧЕМУ НЕ ЧЕРЕЗ activateTime У КОМАНДЫ. У EntityActivationCommand есть поле activateTime, и
/// Entity.ActivateEntityCommand действительно открывает по нему таймер - но по завершении он
/// дёргает ИГРОВОЕ СОБЫТИЕ из entity_class (`onActivateEvent`), а не наш код, и запускается
/// только если у команды заполнено eventName. Нам нужен свой обработчик в конце, поэтому таймер
/// открывается вручную, а не описанием команды.
///
/// CloseOnHit=true: если по игроку ударили посреди обыска, полоса закрывается и труп остаётся
/// нетронутым. Обыскивать труп посреди боя не должно быть бесплатно.</summary>
[HarmonyPatch(typeof(EntityAlive), "OnEntityActivated")]
public static class Patch_EntityAlive_OnEntityActivated_Corpse
{
public static void Postfix(EntityAlive __instance, EntityActivationCommand _command,
EntityPlayerLocal _playerFocusing)
{
if (_command.commandId != CorpseLoot.SearchCommand || !CorpseLoot.IsSearchable(__instance))
{
return;
}
if (_playerFocusing == null || _playerFocusing.PlayerUI == null)
{
CorpseLoot.SearchCorpse(__instance);
return;
}
EntityAlive corpse = __instance;
TimerEventData timer = new TimerEventData
{
Data = corpse,
CloseOnHit = true,
};
timer.FullTimeFinishEvent += delegate
{
// Повторная проверка на финише: за три секунды труп мог истлеть, а игрок - уйти.
if (CorpseLoot.IsSearchable(corpse))
{
CorpseLoot.SearchCorpse(corpse);
}
};
XUiC_Timer.OpenTimer(_playerFocusing.PlayerUI.xui, CorpseLoot.SearchSeconds, timer);
Debug.Log("[NecromancerTome] CorpseLoot: начат обыск трупа " + corpse.entityId +
", " + CorpseLoot.SearchSeconds + " с");
}
}
}