diff --git a/HarmonySrc/CinerariumKeepAlive.cs b/HarmonySrc/CinerariumKeepAlive.cs
new file mode 100644
index 0000000..90f1ca2
--- /dev/null
+++ b/HarmonySrc/CinerariumKeepAlive.cs
@@ -0,0 +1,147 @@
+using System.Collections.Generic;
+using HarmonyLib;
+using UnityEngine;
+
+namespace NecromancerTome
+{
+ ///
+ /// Урны работают, пока игрок далеко (решение 23.10.2026, #78: «Харвестры работают только когда рядом» - «если игрок далеко
+ /// можно выключить анимацию духов... и пусть духи продолжат работать. С остальными тоже. Если ящики переполнятся, то
+ /// харвестры встанут»; досчёт по времени не годится - сборщик следит за ростом и сеет). Игра обновляет участки, за
+ /// которыми кто-то наблюдает (ChunkManager: активный набор = все участки наблюдателей), - поэтому у каждой группы урн
+ /// свой наблюдатель без видимой геометрии: участки не выгружаются, урны и грядки идут. Анимация вдали и так выключена
+ /// (CinerariumLoad.AnimateNearest). Группа - клетка CellSize×CellSize блоков по реестру урн сейва (CinerariumRegistry),
+ /// наблюдатель в её центре держит (ViewDim+2) участков во все стороны - клетку и рабочие зоны у её краёв.
+ /// Нагрузку мерить по логу (Heap/RSS, Chunks); если тяжело - сначала оптимизация, запасной путь - урны стоят без игрока.
+ /// Консоль: necrournkeep on|off|dim N.
+ ///
+ public static class CinerariumKeepAlive
+ {
+ public static bool Enabled = true;
+ public const int CellSize = 64;
+ /// Радиус наблюдателя в участках минус 2 (так считает ChunkManager): 1 - 7×7 участков.
+ public static int ViewDim = 1;
+ const float SyncSeconds = 10f;
+
+ static readonly Dictionary _observers = new Dictionary();
+ static World _world;
+ static float _nextSync;
+
+ public static int Count => _observers.Count;
+
+ public static void Tick()
+ {
+ if (Time.time < _nextSync)
+ {
+ return;
+ }
+ _nextSync = Time.time + SyncSeconds;
+ GameManager gm = GameManager.Instance;
+ World w = gm?.World;
+ if (w != _world)
+ {
+ _observers.Clear(); // мир сменился - прежние наблюдатели ушли вместе с ним
+ _world = w;
+ }
+ if (w == null || !ConnectionManager.Instance.IsServer)
+ {
+ return;
+ }
+ // Клетки с урнами и средняя высота урн в каждой.
+ Dictionary cells = new Dictionary();
+ if (Enabled)
+ {
+ foreach (Vector3i p in CinerariumRegistry.All())
+ {
+ Vector2i c = new Vector2i(Mathf.FloorToInt(p.x / (float)CellSize), Mathf.FloorToInt(p.z / (float)CellSize));
+ cells.TryGetValue(c, out (int sum, int n) v);
+ cells[c] = (v.sum + p.y, v.n + 1);
+ }
+ }
+ List gone = null;
+ foreach (KeyValuePair kv in _observers)
+ {
+ if (!cells.ContainsKey(kv.Key) || kv.Value.viewDim != ViewDim)
+ {
+ (gone ??= new List()).Add(kv.Key);
+ }
+ }
+ if (gone != null)
+ {
+ foreach (Vector2i c in gone)
+ {
+ gm.RemoveChunkObserver(_observers[c]);
+ _observers.Remove(c);
+ Debug.Log("[NecromancerTome] CinerariumKeepAlive: группа урн " + c + " - наблюдатель снят, всего " + _observers.Count);
+ }
+ }
+ foreach (KeyValuePair kv in cells)
+ {
+ if (_observers.ContainsKey(kv.Key))
+ {
+ continue;
+ }
+ Vector3 center = new Vector3((kv.Key.x + 0.5f) * CellSize, kv.Value.sum / (float)kv.Value.n, (kv.Key.y + 0.5f) * CellSize);
+ _observers[kv.Key] = gm.AddChunkObserver(center, false, ViewDim, -1);
+ Debug.Log("[NecromancerTome] CinerariumKeepAlive: группа урн " + kv.Key + " (" + kv.Value.n + " шт.) - наблюдатель в " + center +
+ ", всего " + _observers.Count);
+ }
+ }
+
+ /// Сразу пересобрать (команда).
+ public static void SyncNow()
+ {
+ _nextSync = 0f;
+ Tick();
+ }
+ }
+
+ [HarmonyPatch(typeof(World), nameof(World.OnUpdateTick))]
+ public static class Patch_World_OnUpdateTick_CinerariumKeepAlive
+ {
+ public static void Postfix()
+ {
+ CinerariumKeepAlive.Tick();
+ }
+ }
+
+ public class ConsoleCmdNecroUrnKeep : ConsoleCmdAbstract
+ {
+ public override bool IsExecuteOnClient => false;
+
+ public override bool AllowedInMainMenu => false;
+
+ public override string[] getCommands()
+ {
+ return new string[] { "necrournkeep" };
+ }
+
+ public override string getDescription()
+ {
+ return "Necromancer's Tome: keep urn areas loaded so urns work while you are away - switch.";
+ }
+
+ public override string getHelp()
+ {
+ return "necrournkeep - status\n" +
+ "necrournkeep on|off - off: urns stop when you are far (areas unload), as before\n" +
+ "necrournkeep dim - observer size: (n+2) chunks each way around a 64-block group (default 1)";
+ }
+
+ public override void Execute(List _params, CommandSenderInfo _senderInfo)
+ {
+ SdtdConsole con = SingletonMonoBehaviour.Instance;
+ if (_params.Count >= 1 && (_params[0] == "on" || _params[0] == "off"))
+ {
+ CinerariumKeepAlive.Enabled = _params[0] == "on";
+ }
+ else if (_params.Count >= 2 && _params[0] == "dim" && int.TryParse(_params[1], out int d) && d >= 0 && d <= 6)
+ {
+ CinerariumKeepAlive.ViewDim = d;
+ }
+ CinerariumKeepAlive.SyncNow();
+ con.Output("necrournkeep: " + (CinerariumKeepAlive.Enabled ? "on" : "off") + ", dim " + CinerariumKeepAlive.ViewDim +
+ ", observers " + CinerariumKeepAlive.Count);
+ }
+ }
+}
diff --git a/NecromancerHarmony.dll b/NecromancerHarmony.dll
index 3a37014..74a128e 100644
Binary files a/NecromancerHarmony.dll and b/NecromancerHarmony.dll differ
diff --git a/NecromancerHarmony.pdb b/NecromancerHarmony.pdb
index 588eea9..1aa1422 100644
Binary files a/NecromancerHarmony.pdb and b/NecromancerHarmony.pdb differ