Указание: «зомби после упокоения оставляет свой труп. Можем ли мы подменять этот труп точно таким же, но с другой текстурой и с функцией мешка жертвы?» ПОДМЕНЯТЬ НЕ ПРИШЛОСЬ НИЧЕГО. Труп - не отдельная модель, а та же скелетная модель зомби в позе смерти, так что «такой же, но другой текстуры» делается перекраской уже лежащего трупа. Ни нового префаба, ни новой сущности. Три части, каждая на своём ванильном механизме: НЕ ИСЧЕЗАЕТ. EntityAlive.timeStayAfterDeath - обычное публичное поле в тиках, у зомби 30 секунд, у ванильного мешка 3600. Пишем второе. ОБЫСКИВАЕТСЯ. У сущностей своя система команд, отдельная от блочной: InitLocalActivationCommands / AllowActivationCommand / OnEntityActivated. Объявляем ту же команду "search", что и ванильный мешок (EntityItem), поэтому подпись достаётся готовой и локализованной. Список команд кешируется при первом обращении - пока зомби жив, - поэтому на смерти он обнуляется. ВНУТРИ ЛУТ. Обход сознательный: хранилище живёт внутри EntityLootContainer вместе со своим сейвом и сетевой синхронизацией, и приделывать это трупу значит трогать сейвы. Поэтому обыск ПРЕВРАЩАЕТ труп в мешок «Жертва». ПЕРЕКРАСКА ЧЕРЕЗ ТЕКСТУРУ, А НЕ ЧЕРЕЗ _Color. Первая версия не работала вовсе: у шейдеров Game/Character и Game/Animal/Fur свойства цвета НЕТ (видно в пробе, которую печатает GhostTraderPatch: "tint property: <none>"). Красится сама текстура - прогоном через RenderTexture, как обесцвечиваются торговцы, только вместо сведения к яркости умножение на цвет. Результат кешируется по исходной текстуре. Если модель на кадре смерти не собрана, труп встаёт в очередь и докрашивается односекундным тиком. ПОДСКАЗКА ПРИ НАВЕДЕНИИ - отдельный механизм, и сначала я настроил не тот. Иконка EntityActivationCommand рисуется в РАДИАЛЬНОМ меню, то есть на зажатии. А то, что видно при простом наведении, приходит из Entity.GetActivationText(), и у EntityAlive он для существа возвращает пустую строку. Теперь возвращает то же, что ванильный лутаемый объект: строку lootTooltipNew со значком клавиши из разметки [action:...]. Имя берётся у зомби, так что читается «обыскать Арлин». ОБЫСК НЕ МГНОВЕННЫЙ: три секунды с круговым индикатором (XUiC_Timer, тот же, что при снятии верстака). Поле activateTime у команды не подошло - по нему игра дёргает игровое событие из entity_class, а не наш код. Обычный мешок это не отменяет: бросок вероятности остался как был, и теперь он печатается в лог целиком (шанс против броска) - чтобы «не повезло» отличалось от «сломано» без гадания. У ванильного зомби шанс 4%, то есть ноль мешков на два десятка убийств - исход с вероятностью около 40%. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
88 lines
5.0 KiB
C#
88 lines
5.0 KiB
C#
using System.Reflection;
|
||
using HarmonyLib;
|
||
using UnityEngine;
|
||
|
||
namespace NecromancerTome
|
||
{
|
||
/// <summary>
|
||
/// Mod entry point. The game finds this by scanning every assembly dropped in a
|
||
/// Mods/<ModFolder>/ directory for a type implementing IModApi.
|
||
/// </summary>
|
||
public class ModEntry : IModApi
|
||
{
|
||
/// <summary>The mod's own folder, kept from InitMod so patches can find files we ship
|
||
/// (currently Resources/necroatlas for the custom block paint). Nothing else knows where
|
||
/// the mod lives - the game hands it over exactly once, right here.</summary>
|
||
public static Mod Instance;
|
||
|
||
public void InitMod(Mod _modInstance)
|
||
{
|
||
Instance = _modInstance;
|
||
var harmony = new Harmony("necromancertome.harmony");
|
||
harmony.PatchAll(Assembly.GetExecutingAssembly());
|
||
PetFollowPatch.Init();
|
||
// SpatialVaultPersistence needs NO Init(): it is four Harmony postfixes that PatchAll
|
||
// above already attached. It used to register a WorldShuttingDown handler to clear its
|
||
// cache - that handler is exactly what wiped the vault on every clean exit, because
|
||
// that event fires BEFORE the final player save (GameManager.SaveAndCleanupWorld:
|
||
// event at IL_0026, SaveLocalPlayerData at IL_00c4). Freshness is decided by what was
|
||
// read instead; see that file.
|
||
// Traders rendered as washed-out ghosts (request 2026-09-13). Polls rather than
|
||
// hooks a spawn event - see that file for why the SDCS-built trader forces it.
|
||
GhostTraderPatch.Init();
|
||
// PyramidWardPatch.cs's TEFeaturePyramidWard needs no Init() call - it's discovered
|
||
// automatically by the engine's own TileEntityCompositeData reflection scan (see that
|
||
// file's class doc comment), not registered here like PetFollowPatch's UnityUpdate hook.
|
||
|
||
// Diagnostic-only, added 2026-08-28 while chasing "VictimPatch never logs anything at
|
||
// all for dropItemOnDeath, even though decompiling EntityAlive.OnEntityDeath()
|
||
// confirms it's called unconditionally right after the 'killed by' line seen in the
|
||
// log". This checks, at load time, whether Harmony actually attached our Prefix to
|
||
// that method at all - rules "patch never applied" in or out without waiting on
|
||
// another in-game death.
|
||
VerifyPrefixAttached(typeof(EntityAlive), "dropItemOnDeath");
|
||
// Патч на Entity.DropBagServer снят 2026-09-18 вместе с переделкой мешка «Жертвы»:
|
||
// он подменял собой ОБЫЧНЫЙ мешок, а тот теперь должен доставаться игроку как есть.
|
||
// Проверять нечего - см. VictimPatch.cs.
|
||
|
||
// Добавлено 2026-09-17 вместе с правкой "подчинённые не дерутся между собой"
|
||
// (CharmPatch.cs). Метод EAITarget.check в исходнике protected и назван со строчной
|
||
// буквы - если он когда-нибудь переименуется или сменит сигнатуру, PatchAll упадёт
|
||
// ещё на загрузке, но эта строка отвечает на тот же вопрос в логе явно и без
|
||
// раскопок: резолвится ли метод и висит ли на нём наш постфикс.
|
||
VerifyPatchAttached(typeof(EAITarget), "check");
|
||
}
|
||
|
||
/// <summary>То же, что VerifyPrefixAttached, но печатает и префиксы, и постфиксы - для
|
||
/// заплаток, которые стоят постфиксом (у VerifyPrefixAttached постфикс всегда выглядел бы
|
||
/// как "0 prefix patch(es)", то есть как ненайденная заплатка).</summary>
|
||
public static void VerifyPatchAttached(System.Type type, string methodName)
|
||
{
|
||
MethodBase method = AccessTools.Method(type, methodName);
|
||
if (method == null)
|
||
{
|
||
Debug.LogWarning("[NecromancerTome] ModEntry: could not resolve " + type.Name + "." + methodName + " via AccessTools - method not found");
|
||
return;
|
||
}
|
||
Patches info = Harmony.GetPatchInfo(method);
|
||
int prefixCount = info != null && info.Prefixes != null ? info.Prefixes.Count : 0;
|
||
int postfixCount = info != null && info.Postfixes != null ? info.Postfixes.Count : 0;
|
||
Debug.Log("[NecromancerTome] ModEntry: " + type.Name + "." + methodName + " resolved, has " +
|
||
prefixCount + " prefix and " + postfixCount + " postfix patch(es) attached after PatchAll");
|
||
}
|
||
|
||
public static void VerifyPrefixAttached(System.Type type, string methodName)
|
||
{
|
||
MethodBase method = AccessTools.Method(type, methodName);
|
||
if (method == null)
|
||
{
|
||
Debug.LogWarning("[NecromancerTome] ModEntry: could not resolve " + type.Name + "." + methodName + " via AccessTools - method not found");
|
||
return;
|
||
}
|
||
Patches info = Harmony.GetPatchInfo(method);
|
||
int prefixCount = info != null && info.Prefixes != null ? info.Prefixes.Count : 0;
|
||
Debug.Log("[NecromancerTome] ModEntry: " + type.Name + "." + methodName + " resolved, has " + prefixCount + " prefix patch(es) attached after PatchAll");
|
||
}
|
||
}
|
||
}
|