Стрела: защита от NullReference в GetDismemberChance (+ какое звено пустое - в лог); necrourn probe - что урна видит в зоне и что выбрала; necrofire

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
AlexCube
2026-10-23 16:24:10 +03:00
co-authored by Claude Opus 5.5
parent 3ffb90cc57
commit f372660e39
4 changed files with 92 additions and 50 deletions
+23
View File
@@ -61,11 +61,29 @@ namespace NecromancerTome
/// <summary>У перевозки по нити - ящик «откуда» последнего шага (дух летит сначала к нему), иначе null.</summary>
public static Vector3i? LastFrom;
/// <summary>Что выбрал последний шаг (для necrourn probe, 23.10: «стою в 19 м - не работают»).</summary>
public static string LastWhat;
/// <summary>Пробный шаг урны без изменений: что в зоне и что дух выбрал бы сейчас (команда necrourn x y z probe).</summary>
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;
}
}
+51
View File
@@ -0,0 +1,51 @@
using System;
using HarmonyLib;
using UnityEngine;
namespace NecromancerTome
{
/// <summary>
/// Стрела: NullReferenceException в ItemAction.GetDismemberChance (23.10.2026). Вызывается из ProjectileMoveScript.checkCollision
/// аргументом к ItemActionAttack.Hit - поэтому в первых логах исключение выглядело как ошибка «внутри Hit» (метод был встроен).
/// Пустое звено цепочки actionData.invData.holdingEntity.inventory / invData.item у стрелы, запомнившей данные лука при выстреле.
/// Исключение вылетало раньше, чем стрела сменит состояние, и она «попадала» снова каждый шаг физики - пачки по 6-19 ошибок.
/// Здесь: при исключении шанс отстрела конечности 0 (стрела долетает как обычно), в лог - какое звено пустое (не больше
/// MaxLogs раз за запуск). Причина звена - выяснить по этим строкам.
/// </summary>
[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; // проглотить: стрела долетает и попадает как обычно
}
}
}
-50
View File
@@ -1,50 +0,0 @@
using System;
using HarmonyLib;
using UnityEngine;
namespace NecromancerTome
{
/// <summary>
/// Диагностика (23.10.2026): NullReferenceException внутри ItemActionAttack.Hit при попадании стрелы (ProjectileMoveScript.checkCollision)
/// - пачками по одной стреле (исключение вылетает раньше, чем стрела сменит состояние, и она «попадает» снова каждый шаг физики).
/// Было дважды, рядом с только что догруженными зомби спящих зон. Здесь только запись в лог, во что попала стрела; исключение
/// уходит дальше как было. Не больше MaxLogs записей за запуск. Убрать, когда причина найдена.
/// </summary>
[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;
}
}
}
+18
View File
@@ -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<string> _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<string> _params, CommandSenderInfo _senderInfo) => NecroDev.Run("necrofire", _params);
}
[Preserve]
public class ConsoleCmdNecroSlot : ConsoleCmdAbstract
{