- Могильная крыса: своя модель без волчьей шерсти; стоит на земле (центр капсулы); E - осмотр контейнеров без права взять (окно игры, рюкзак скрыт, отказ «инвентарь заполнен», контейнер не разрушается); ПКМ - проход сквозь постройки, полупрозрачная. - Стервятник: курица в когтях - слепок с обвисшими крыльями и шеей, без хитбокса. - Зомбособака: нюх - все существа рядом на компасе и карте. - Медведь: толстая шкура (вдвое меньше урона), рёв-провокация, удар лапой по блокам. - Лютоволк: бросок на цель с опрокидыванием, след крови раненых, сумка на 3 ячейки (лут, выгрузка у тела, выпадает мешком при гибели и отзыве; отдельный файл сейва). - Череп (останки): 2% при обычном обыске красного трупа вместо браслета. - Прицел книги не рисуется в одержимости; подсказки вселения на 13 языках. - Замер EntityLeakProbe: удалённые, но живые в памяти сущности - в лог. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
139 lines
5.1 KiB
C#
139 lines
5.1 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using System.Text;
|
|
using HarmonyLib;
|
|
using Debug = UnityEngine.Debug;
|
|
|
|
namespace NecromancerTome
|
|
{
|
|
/// <summary>
|
|
/// Замер утечки сущностей (указание 09.10: «добавь замер сразу; если не будет тормозить, пригодится для
|
|
/// анализа»). Повод - лог 09.10: в строке игры «Ent: 7 (31)» живых сущностей 7, а экземпляров в памяти 31
|
|
/// (Entity.InstanceCount уменьшается только в финализаторе), и разрыв рос за сессию.
|
|
///
|
|
/// Как работает. Каждое удаление сущности из мира проходит через World.unloadEntity (смерть, отзыв,
|
|
/// выгрузка чанка, чёрный разрыв NecroBurst). Там удалённая сущность запоминается СЛАБОЙ ссылкой
|
|
/// (WeakReference сама не держит объект), с классом, причиной и источником - первым методом мода в стеке
|
|
/// вызова (или «игра»). Раз в минуту: если с удаления прошло не меньше двух сборок мусора (GC.CollectionCount),
|
|
/// а объект всё ещё жив, - его кто-то держит. В лог идёт строка «EntityLeakProbe» со сводкой по классу и
|
|
/// источнику; ничего не держится - строки нет.
|
|
///
|
|
/// Нагрузка: удаления редки (единицы в минуту), стек снимается только в этот момент; раз в минуту - проход
|
|
/// по короткому списку. GC принудительно НЕ вызывается (дал бы рывок). Вышедшие из памяти записи
|
|
/// выбрасываются, список не растёт.
|
|
/// </summary>
|
|
public static class EntityLeakProbe
|
|
{
|
|
class Rec
|
|
{
|
|
public WeakReference Ref;
|
|
public string What;
|
|
public int Gc;
|
|
}
|
|
|
|
static readonly List<Rec> _recs = new List<Rec>();
|
|
static readonly Dictionary<string, int> _sum = new Dictionary<string, int>();
|
|
static float _next;
|
|
const float Period = 60f;
|
|
|
|
public static void OnUnload(Entity _e, EnumRemoveEntityReason _reason)
|
|
{
|
|
if (_e == null || _e is EntityPlayerLocal)
|
|
{
|
|
return;
|
|
}
|
|
string cls = EntityClass.list.TryGetValue(_e.entityClass, out EntityClass ec) && ec != null ? ec.entityClassName : _e.GetType().Name;
|
|
_recs.Add(new Rec
|
|
{
|
|
Ref = new WeakReference(_e),
|
|
What = cls + " / " + _reason + " / " + Origin(),
|
|
Gc = GC.CollectionCount(0)
|
|
});
|
|
}
|
|
|
|
/// <summary>Первый метод мода в стеке (кроме этого класса и патчей Harmony) - кто удалил; иначе «игра».</summary>
|
|
static string Origin()
|
|
{
|
|
StackTrace st = new StackTrace(2, false);
|
|
for (int i = 0; i < st.FrameCount && i < 24; i++)
|
|
{
|
|
System.Reflection.MethodBase m = st.GetFrame(i)?.GetMethod();
|
|
Type t = m?.DeclaringType;
|
|
if (t == null || t == typeof(EntityLeakProbe) || t.Namespace != nameof(NecromancerTome) || t.Name.StartsWith("Patch_World_unloadEntity"))
|
|
{
|
|
continue;
|
|
}
|
|
return (t.DeclaringType != null ? t.DeclaringType.Name + "." : "") + t.Name + "." + m.Name;
|
|
}
|
|
return "игра";
|
|
}
|
|
|
|
/// <summary>Раз в минуту (из ModEvents.UnityUpdate мода).</summary>
|
|
public static void Tick()
|
|
{
|
|
float now = UnityEngine.Time.time;
|
|
if (now < _next)
|
|
{
|
|
return;
|
|
}
|
|
_next = now + Period;
|
|
int gc = GC.CollectionCount(0);
|
|
_sum.Clear();
|
|
int held = 0;
|
|
for (int i = _recs.Count - 1; i >= 0; i--)
|
|
{
|
|
Rec r = _recs[i];
|
|
object o = r.Ref.Target;
|
|
if (o == null)
|
|
{
|
|
_recs.RemoveAt(i);
|
|
continue;
|
|
}
|
|
if (gc - r.Gc < 2)
|
|
{
|
|
continue; // сборок мусора ещё не было - рано судить
|
|
}
|
|
held++;
|
|
_sum.TryGetValue(r.What, out int n);
|
|
_sum[r.What] = n + 1;
|
|
}
|
|
if (held == 0)
|
|
{
|
|
return;
|
|
}
|
|
StringBuilder sb = new StringBuilder();
|
|
sb.Append("[NecromancerTome] EntityLeakProbe: удалённых, но живых в памяти ").Append(held)
|
|
.Append(" (Entity.InstanceCount ").Append(Entity.InstanceCount).Append(", сборок мусора ").Append(gc).Append("):");
|
|
foreach (KeyValuePair<string, int> kv in _sum)
|
|
{
|
|
sb.Append("\n ").Append(kv.Value).Append(" x ").Append(kv.Key);
|
|
}
|
|
Debug.Log(sb.ToString());
|
|
}
|
|
|
|
/// <summary>Выход из мира - забыть всё.</summary>
|
|
public static void Reset()
|
|
{
|
|
_recs.Clear();
|
|
_next = 0f;
|
|
}
|
|
}
|
|
|
|
[HarmonyPatch(typeof(World), nameof(World.unloadEntity))]
|
|
public static class Patch_World_unloadEntity_LeakProbe
|
|
{
|
|
public static void Prefix(Entity _e, EnumRemoveEntityReason _reason)
|
|
{
|
|
try
|
|
{
|
|
EntityLeakProbe.OnUnload(_e, _reason);
|
|
}
|
|
catch (Exception)
|
|
{
|
|
// Замер не должен ломать удаление сущности ни при каких условиях.
|
|
}
|
|
}
|
|
}
|
|
}
|