Одержимость питомцев: роли медведя, волка, собаки и крысы; ноша стервятника; череп при обыске
- Могильная крыса: своя модель без волчьей шерсти; стоит на земле (центр капсулы); E - осмотр контейнеров без права взять (окно игры, рюкзак скрыт, отказ «инвентарь заполнен», контейнер не разрушается); ПКМ - проход сквозь постройки, полупрозрачная. - Стервятник: курица в когтях - слепок с обвисшими крыльями и шеей, без хитбокса. - Зомбособака: нюх - все существа рядом на компасе и карте. - Медведь: толстая шкура (вдвое меньше урона), рёв-провокация, удар лапой по блокам. - Лютоволк: бросок на цель с опрокидыванием, след крови раненых, сумка на 3 ячейки (лут, выгрузка у тела, выпадает мешком при гибели и отзыве; отдельный файл сейва). - Череп (останки): 2% при обычном обыске красного трупа вместо браслета. - Прицел книги не рисуется в одержимости; подсказки вселения на 13 языках. - Замер EntityLeakProbe: удалённые, но живые в памяти сущности - в лог. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
beab1640b0
commit
31aa99860c
@@ -0,0 +1,138 @@
|
||||
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)
|
||||
{
|
||||
// Замер не должен ломать удаление сущности ни при каких условиях.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user