diff --git a/HarmonySrc/CinerariumWork.cs b/HarmonySrc/CinerariumWork.cs
index 52277f7..a7a6191 100644
--- a/HarmonySrc/CinerariumWork.cs
+++ b/HarmonySrc/CinerariumWork.cs
@@ -61,11 +61,29 @@ namespace NecromancerTome
/// У перевозки по нити - ящик «откуда» последнего шага (дух летит сначала к нему), иначе null.
public static Vector3i? LastFrom;
+ /// Что выбрал последний шаг (для necrourn probe, 23.10: «стою в 19 м - не работают»).
+ public static string LastWhat;
+
+ /// Пробный шаг урны без изменений: что в зоне и что дух выбрал бы сейчас (команда necrourn x y z probe).
+ public static string Probe(World _world, TEFeatureCinerarium _urn)
+ {
+ string zone = "";
+ if (!_urn.IsLogist && !_urn.IsWorker)
+ {
+ Zone z = Scan(_world, _urn);
+ zone = "коллекторов " + z.Collectors.Count + ", станций " + z.Stations.Count + ", ящиков " + z.Boxes.Count +
+ ", сумка " + (z.Bag != null) + ", спелых " + z.Ripe.Count + (z.Ripe.Count > 0 ? " (" + z.Ripe[0] + ")" : "") +
+ ", пустых клеток " + z.Empty.Count + "; ";
+ }
+ Vector3i? at = Step(_world, _urn, out bool stuck, true);
+ return zone + "шаг: " + (at.HasValue ? LastWhat + " в " + at.Value : "нечего делать") + (stuck ? ", класть некуда" : "");
+ }
static Vector3i? DoStep(World _world, TEFeatureCinerarium _urn, out bool _stuck)
{
_stuck = false;
LastFrom = null;
+ LastWhat = "?";
// Урна с нитями - логист (16.10: «занимается только перетаскиванием из ящиков в ящики»).
if (_urn.IsLogist)
{
@@ -88,6 +106,7 @@ namespace NecromancerTome
{
c.SetModified();
}
+ LastWhat = "расходники коллектору";
return z.Where[c];
}
}
@@ -103,6 +122,7 @@ namespace NecromancerTome
c.Items = items;
c.SetModified();
}
+ LastWhat = "готовое из коллектора";
return z.Where[c];
}
}
@@ -115,6 +135,7 @@ namespace NecromancerTome
{
w.Output = output;
}
+ LastWhat = "готовое со станции";
return z.Where[w];
}
}
@@ -123,6 +144,7 @@ namespace NecromancerTome
{
if (Harvest(_world, p, z, ref blocked))
{
+ LastWhat = "сбор урожая";
return p;
}
}
@@ -131,6 +153,7 @@ namespace NecromancerTome
Vector3i? planted = PlantRandom(_world, z);
if (planted.HasValue)
{
+ LastWhat = "посадка";
return planted;
}
}
diff --git a/HarmonySrc/DismemberGuard.cs b/HarmonySrc/DismemberGuard.cs
new file mode 100644
index 0000000..16db189
--- /dev/null
+++ b/HarmonySrc/DismemberGuard.cs
@@ -0,0 +1,51 @@
+using System;
+using HarmonyLib;
+using UnityEngine;
+
+namespace NecromancerTome
+{
+ ///
+ /// Стрела: NullReferenceException в ItemAction.GetDismemberChance (23.10.2026). Вызывается из ProjectileMoveScript.checkCollision
+ /// аргументом к ItemActionAttack.Hit - поэтому в первых логах исключение выглядело как ошибка «внутри Hit» (метод был встроен).
+ /// Пустое звено цепочки actionData.invData.holdingEntity.inventory / invData.item у стрелы, запомнившей данные лука при выстреле.
+ /// Исключение вылетало раньше, чем стрела сменит состояние, и она «попадала» снова каждый шаг физики - пачки по 6-19 ошибок.
+ /// Здесь: при исключении шанс отстрела конечности 0 (стрела долетает как обычно), в лог - какое звено пустое (не больше
+ /// MaxLogs раз за запуск). Причина звена - выяснить по этим строкам.
+ ///
+ [HarmonyPatch(typeof(ItemAction), nameof(ItemAction.GetDismemberChance))]
+ public static class Patch_ItemAction_GetDismemberChance_Guard
+ {
+ const int MaxLogs = 5;
+ static int _logged;
+
+ public static Exception Finalizer(Exception __exception, ItemActionData _actionData, WorldRayHitInfo hitInfo, ref float __result)
+ {
+ if (__exception == null)
+ {
+ return null;
+ }
+ __result = 0f;
+ if (_logged < MaxLogs)
+ {
+ _logged++;
+ try
+ {
+ ItemInventoryData inv = _actionData?.invData;
+ EntityAlive holder = inv?.holdingEntity;
+ Debug.Log("[NecromancerTome] DismemberGuard: " + __exception.GetType().Name + " в GetDismemberChance - actionData " +
+ (_actionData == null ? "NULL" : _actionData.GetType().Name) +
+ ", invData " + (inv == null ? "NULL" : "slot " + inv.slotIdx) +
+ ", item " + (inv?.item == null ? "NULL" : inv.item.Name) +
+ ", holdingEntity " + (holder == null ? "NULL" : holder.GetType().Name + " " + holder.entityId + (holder.IsDead() ? " dead" : "")) +
+ ", inventory " + (holder == null ? "-" : holder.inventory == null ? "NULL" : "в руке " + holder.inventory.holdingItem?.Name) +
+ ", tag " + (hitInfo?.tag ?? "NULL") + " - шанс отстрела 0");
+ }
+ catch (Exception e2)
+ {
+ Debug.Log("[NecromancerTome] DismemberGuard: не удалось описать - " + e2.Message);
+ }
+ }
+ return null; // проглотить: стрела долетает и попадает как обычно
+ }
+ }
+}
diff --git a/HarmonySrc/HitDiag.cs b/HarmonySrc/HitDiag.cs
deleted file mode 100644
index 5321563..0000000
--- a/HarmonySrc/HitDiag.cs
+++ /dev/null
@@ -1,50 +0,0 @@
-using System;
-using HarmonyLib;
-using UnityEngine;
-
-namespace NecromancerTome
-{
- ///
- /// Диагностика (23.10.2026): NullReferenceException внутри ItemActionAttack.Hit при попадании стрелы (ProjectileMoveScript.checkCollision)
- /// - пачками по одной стреле (исключение вылетает раньше, чем стрела сменит состояние, и она «попадает» снова каждый шаг физики).
- /// Было дважды, рядом с только что догруженными зомби спящих зон. Здесь только запись в лог, во что попала стрела; исключение
- /// уходит дальше как было. Не больше MaxLogs записей за запуск. Убрать, когда причина найдена.
- ///
- [HarmonyPatch(typeof(ItemActionAttack), nameof(ItemActionAttack.Hit))]
- public static class Patch_ItemActionAttack_Hit_Diag
- {
- const int MaxLogs = 5;
- static int _logged;
-
- public static Exception Finalizer(Exception __exception, WorldRayHitInfo hitInfo, int _attackerEntityId, ItemValue damagingItemValue)
- {
- if (__exception == null || _logged >= MaxLogs)
- {
- return __exception;
- }
- _logged++;
- try
- {
- string s = "tag=" + (hitInfo?.tag ?? "null") + ", transform=" + (hitInfo?.transform != null ? hitInfo.transform.name : "NULL") +
- ", collider=" + (hitInfo?.hitCollider != null ? hitInfo.hitCollider.name : "NULL");
- if (hitInfo != null && hitInfo.tag != null && hitInfo.tag.StartsWith("E_"))
- {
- Entity e = ItemActionAttack.FindHitEntityNoTagCheck(hitInfo, out string part);
- s += ", entity=" + (e == null ? "NULL" : e.GetType().Name + " " + EntityClass.list[e.entityClass].entityClassName + " " + e.entityId +
- (e is EntityAlive ea ? (ea.IsDead() ? " dead" : " alive") + ", emodel " + (ea.emodel != null) : "")) +
- ", part=" + (part ?? "null");
- }
- Entity atk = GameManager.Instance?.World?.GetEntity(_attackerEntityId);
- s += ", attacker=" + (atk == null ? "NULL " + _attackerEntityId : atk.GetType().Name + " " + atk.entityId) +
- ", item=" + (damagingItemValue?.ItemClass?.Name ?? "null");
- Debug.Log("[NecromancerTome] HitDiag: исключение в ItemActionAttack.Hit (" + __exception.GetType().Name + "): " + s +
- "\n" + __exception.StackTrace);
- }
- catch (Exception e2)
- {
- Debug.Log("[NecromancerTome] HitDiag: не удалось описать попадание - " + e2.Message);
- }
- return __exception;
- }
- }
-}
diff --git a/HarmonySrc/NecroDevCommands.cs b/HarmonySrc/NecroDevCommands.cs
index 574d6a1..299dedf 100644
--- a/HarmonySrc/NecroDevCommands.cs
+++ b/HarmonySrc/NecroDevCommands.cs
@@ -15,6 +15,7 @@ namespace NecromancerTome
/// necroplace блок x y z [поворот 0-3] - поставить блок (как игрок: OnBlockAdded, TE)
/// necrohold предмет [число] - положить в текущую ячейку пояса и взять в руку
/// necroslot [N] - какая ячейка пояса в руке и что в ячейках; N - взять в руку ячейку N (23.10: проверка Archer's Eye без мыши)
+ /// necrofire press|release - ЛКМ предметом в руке: нажать / отпустить (лук: натянуть / выстрелить; 23.10, ошибка в Hit от стрелы)
/// necrobox x y z list|clear|add предмет число - содержимое хранилища (ящик, сумка урны)
/// necrourn x y z info|skull 0|1|from|to|out bx by bz|clear|sample предмет число|group все|нет - урна
/// necroclick 0|1 - отпущенная ЛКМ/ПКМ ножом для нитей по блоку под прицелом
@@ -45,6 +46,13 @@ namespace NecromancerTome
case "necroaim": Aim(w, pl); break;
case "necroplace": PlaceBlock(w, _params); break;
case "necrohold": Hold(pl, _params); break;
+ case "necrofire":
+ {
+ bool rel = _params.Count > 0 && _params[0] == "release";
+ pl.inventory.Execute(0, rel, pl.playerInput);
+ Out("necrofire: " + (rel ? "отпущено" : "нажато") + ", в руке " + pl.inventory.holdingItem?.Name + ", заряжено " + pl.inventory.holdingItemItemValue.Meta);
+ break;
+ }
case "necroslot":
{
if (_params.Count > 0)
@@ -470,6 +478,7 @@ namespace NecromancerTome
u.SetSample(_p.Count > 5 ? new ItemStack(ItemClass.GetItem(_p[4], true), int.Parse(_p[5])) : ItemStack.Empty.Clone());
break;
case "contents": CinerariumContents.Open(GameManager.Instance.World.GetPrimaryPlayer(), u); break;
+ case "probe": Out("necrourn: " + pos + " " + CinerariumWork.Probe(GameManager.Instance.World, u)); return;
case "run": u.ZoneCommand("zurn_run", GameManager.Instance.World.GetPrimaryPlayer()); break;
case "stop": u.ZoneCommand("zurn_stop", null); break;
case "size": u.ZoneSide = int.Parse(_p[4]); u.SetModified(); break;
@@ -528,6 +537,15 @@ namespace NecromancerTome
public override void Execute(List _params, CommandSenderInfo _senderInfo) => NecroDev.Run("necrohold", _params);
}
+ [Preserve]
+ public class ConsoleCmdNecroFire : ConsoleCmdAbstract
+ {
+ public override string[] getCommands() => new[] { "necrofire" };
+ public override string getDescription() => "NecromancerTome (разработка): ЛКМ предметом в руке: press|release";
+ public override bool AllowedInMainMenu => false;
+ public override void Execute(List _params, CommandSenderInfo _senderInfo) => NecroDev.Run("necrofire", _params);
+ }
+
[Preserve]
public class ConsoleCmdNecroSlot : ConsoleCmdAbstract
{