Files
T
AlexCubeandClaude Opus 5 ed5fea5192 1.3.0: шкала 20 убийств за уровень, два индикатора, мир между подчинёнными
Уровень навыка игра хранит ОДНИМ БАЙТОМ (ProgressionValue.Write/Read), поэтому
старая шкала "одно убийство - один уровень" при max_level=5000 на каждом
сохранении откатывала уровень на 256 назад. Снаружи это выглядело как
самопроизвольно закрывающиеся рецепты (жалоба со стрима: Слёзы мертвеца открыты,
Пир падальщика под замком при 250+ убитых), а тиры 500/2000/3000/5000 были
недостижимы в принципе. Подтверждено на двух живых сейвах: sezon8 - 384 убийства
при уровне 129, test8 - 303 при уровне 48.

Шкала переведена на 20 убийств = 1 уровень, max_level=250 - влезает в байт с
запасом. Уровень больше не накапливается, а вычисляется из necroZombieKillsCVar
(float, сохраняется честно) и на каждом убийстве, и постфиксом на
PlayerDataFile.ToPlayer - последнее чинит старые сейвы само, без команд и новой
игры. Открытое при этом не теряется: в старой шкале уровень всегда был не больше
счётчика, так что пересчёт может только вернуть украденное переполнением.
Отдельно закрыт случай "счётчик пуст, а уровень есть" - счётчик восстанавливается
из уровня по старой шкале.

Все пороги пересчитаны в уровни, числа убийств не тронуты, кроме воды: 30 на
сетку шагом 20 не ложится, по указанию пользователя мод переехал на 20 - туда же,
где браслет и Кровавая сфера. Тег necroNecromancyLvl30 удалён. Сходимость всех 18
рецептов (тег -> RecipeTagUnlocked -> unlock_tier) проверена скриптом.

Два индикатора: череп в статус-баре показывает уровень (раньше - общее число
убийств), фиолетовая шкала над полосой опыта - продвижение внутри уровня, 0..20.
Шкала сделана вёрсткой, а не баффом (бафф умеет число, но не полосу), заполнение
привязано через XUi-выражение cvar(). Закрывающая скобка у выражения - одна "}",
а не "%}": лишний "%" NCalc читает как остаток от деления и ждёт правый операнд,
на чём первая проверка в игре и споткнулась.

Третья правка: подчинённые зомби больше не дерутся между собой. Девиация выдаёт
приказ "бей зомби", а подчинённый сам EntityZombie, и в targetClasses выражается
только тип. Постфикс на EAITarget.check вычёркивает подчинённого из кандидатов
(зомби выбирает следующего, настоящего врага), префикс на
EntityAlive.SetAttackTarget гасит цель на путях мимо выбора - прежде всего месть,
когда подчиняют уже дерущихся.

Тексты на 13 языках, README (RU+EN), описания для сайта и Nexus приведены к новой
шкале.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 23:24:25 +03:00

86 lines
4.7 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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/&lt;ModFolder&gt;/ 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");
// Добавлено 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");
}
}
}