Исправляет первый баг-репорт мода на Nexus (youkia96581, 11.09.2026): "Items stored in the space bracelet will disappear after leaving the game and going online again". Причина была записана в коде как нерешённая: PlayerVaults - обычный статический Dictionary, save/load не существовало. ХРАНИЛИЩЕ ТЕПЕРЬ ЖИВЁТ В PlayerDataFile, рядом с рюкзаком игрока. Так решено после вопроса пользователя "почему не сделать принцип как у ящика?": ящик хранит вещи тем, что они лежат в чанке (у TileEntity единственный конструктор TileEntity(Chunk)), а браслету нужен был дом в чём-то, что движок и так сохраняет. Четыре постфикса - FromPlayer/Write/Read/ToPlayer, блоб с магией "NECROVLT" и явной длиной дописывается после всего ванильного. Байтовая часть - в сателлитной сборке: PooledBinaryWriter.Write не резолвится из основного проекта (CS7069), как и у PyramidWardWriteHelper. Два дефекта, найденные и убитые по дороге живыми тестами: 1. ModEvents.WorldShuttingDown приходит ПЕРЕД финальным сохранением игрока (GameManager.SaveAndCleanupWorld: событие на IL_0026, SaveLocalPlayerData на IL_00c4). Обработчик, чистивший там кэш, затирал хранилище на каждом корректном выходе. Обработчик убран; свежесть решает авторитетность ToPlayer, а не таймер. 2. Пустой сессионный кэш трактовался как "хранилища нет" и записывался поверх настоящего. Путь восстановления имеет право не сработать, удалять он права не имеет - добавлена страховка LastLoadedVault. Проверено в игре: положил -> вышел -> запустил заново -> вещи на месте, блоб на 54 байта сверен в .ttp побайтово. ТОРГОВЦЫ (npcTraderJoel/Rekt/Bob/Hugh/Jen) - чёрно-белые. Шейдер НЕ подменяется: материал клонируется со своим шейдером, меняется только текстура альбедо на обесцвеченную копию, так что свет, нормали и скиннинг остаются движковыми. Альбедо ищется обходом свойств шейдера, а не по имени: тело - Game/Character/_Albedo, волосы - Game/Autodesk/_MainTex. Плюс 1% прозрачности с сохранением _ZWrite. Опрос раз в 2 с, потому что торговцы стримятся на подходе, а Джен собирается в рантайме. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FEXvXg1FSAQJHrvYbeAKqq
61 lines
2.9 KiB
C#
61 lines
2.9 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");
|
|
VerifyPrefixAttached(typeof(Entity), "DropBagServer");
|
|
}
|
|
|
|
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");
|
|
}
|
|
}
|
|
}
|