Версия 2.0: свои модели ножа и крови, своя краска блоков через патч атласа
Нож некроманта и Кровь некроманта получили собственные модели, а Пирамида духов - собственную поверхность. Три разных способа, каждый выбран по тому, как устроен сам предмет. НОЖ И КРОВЬ - свои префабы в бандлах мода. Форма записи "#@modfolder(...)?prefab" подтверждена живым примером; геометрия у обоих ванильная, меняются материал и текстура. Текстуры генерируются скриптами (см. _private/tools), а не рисуются: правка вида сводится к правке констант и одному запуску. Нож: состаренная кость, почти чёрная обмотка, пурпур во впадинах, плюс слой под ручную роспись рун - он подмешивается в альбедо и в эмиссию, поэтому руны светятся. Слой пользовательский, генератор его никогда не перезаписывает. Положение росписи посчитано по геометрии: развёртка выгружена из меша, и подобрано смещение, при котором вся роспись ложится на одну плоскую грань. Раньше она перегибалась через кромку, обращённую к игроку. Кровь: банка вместо мешка. Заодно чинится расхождение, жившее с самого начала - описание предмета говорило "Банка, наполненная кровью", а наследуемый medicalBloodBag показывал sackPrefab, обычный мешок. Жидкость перерисована по маскам мешей, а не перекрашена тинтом: тинт предмета на этот меш не действует вовсе, у шейдера Game_EntityTintMaskSSS выигрывает _Color материала. ПИРАМИДА - своя краска в атласе блоков через собственный Harmony-патч. Ваниль своих текстур блоков не умеет: Texture у блока это индекс в готовом атласе, а запись краски несёт только TextureId/PaintCost/Group/SortIndex и никогда путь к картинке. Поэтому CustomBlockPaintPatch дописывает наш слой в массивы атласа на лету. Путь через свою модель (Shape="ModelEntity") пробовался и отложен: там остались нерешёнными столкновения и маджента на дальних экземплярах. Краска лучше тем, что блок остаётся обычным Shape="New" - со всеми работающими столкновениями, наведением по E и правильной посадкой, - а поверхность у него своя. Побочно краска доступна кисточкой под именем "Некротический прах". Параметры атласа не угаданы, а замерены в игре (BlockAtlasProbePatch): 512x512, DXT1 для альбедо и DXT5 для нормалей со specular, 10 мип-уровней, массивы нечитаемые - отсюда GPU-копирование и явные настройки импорта текстур. Зонд оставлен намеренно: номер краски в blocks.xml (608) это длина ванильного uvMapping, и после обновления игры он может сдвинуться - зонд печатает фактические числа при каждом запуске. Патчи обёрнуты целиком: они работают внутри загрузки игры, где вылетевшее исключение срывает шаг загрузки. Resources/necropyramid в коммит не идёт - на него никто не ссылается. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W5F3AVwsusZHMqPBcSQcVJ
This commit is contained in:
co-authored by
Claude Opus 5
parent
026e006c9c
commit
ab1b3eadaf
@@ -0,0 +1,149 @@
|
||||
using System;
|
||||
using System.Text;
|
||||
using HarmonyLib;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NecromancerTome
|
||||
{
|
||||
/// <summary>
|
||||
/// DIAGNOSTIC - measures the game's opaque block texture atlas and logs what it finds. Adds
|
||||
/// nothing and changes nothing.
|
||||
///
|
||||
/// KEPT ON PURPOSE, though it started as throwaway reconnaissance for CustomBlockPaintPatch.
|
||||
/// That patch appends our paint to the end of the atlas, so the block's Texture number in
|
||||
/// blocks.xml (608 today) is simply "however many entries vanilla had". A game update that
|
||||
/// grows the atlas moves it. This probe prints the real numbers on every load, which is what
|
||||
/// turns that drift from a silent wrong texture into one line in the log.
|
||||
///
|
||||
/// WHY THIS EXISTS. The Pyramid of Spirits should ship with its own surface, but vanilla has
|
||||
/// no way to add one: a block's Texture property is an INDEX into a prebuilt atlas, and the
|
||||
/// atlas itself lives in blocktextureatlases_assets_all.bundle. Confirmed by reading the
|
||||
/// game's own strings - a paint entry carries only TextureId/PaintCost/Group/SortIndex, never
|
||||
/// a path to an image. So the only way in is to extend the atlas at runtime from a Harmony
|
||||
/// patch.
|
||||
///
|
||||
/// Extending it means building a bigger Texture2DArray, copying every existing slice across
|
||||
/// and appending ours. That REQUIRES knowing the array's exact width, height, format and
|
||||
/// mipmap count - a slice that disagrees on any of those cannot be copied in. None of it can
|
||||
/// be known statically, hence this probe: measure first, write the real patch second.
|
||||
///
|
||||
/// TWO LESSONS FROM THE FIRST ATTEMPT, both paid for in a broken load:
|
||||
///
|
||||
/// 1. A THROWING POSTFIX BREAKS THE GAME'S LOADING. The first version dereferenced
|
||||
/// BlockTextureData.list without checking it, threw, and the log answered with
|
||||
/// "XML loader: Executing post load step on 'materials.xml' failed". A probe must be
|
||||
/// incapable of harm, so everything here is wrapped and nothing is allowed to escape.
|
||||
///
|
||||
/// 2. THIS RUNS BEFORE THE PAINT TABLE EXISTS. ReloadTextureArrays fires during
|
||||
/// MeshDescription.Init, and the log shows painting.xml loading well after it - so
|
||||
/// BlockTextureData.list is still null at that point. Hence the probe reports several
|
||||
/// times instead of once: the early call shows the atlas as loaded, later calls show it
|
||||
/// once the rest of the game has caught up.
|
||||
///
|
||||
/// </summary>
|
||||
[HarmonyPatch(typeof(MeshDescription), "ReloadTextureArrays")]
|
||||
public static class BlockAtlasProbePatch
|
||||
{
|
||||
const int MaxReports = 4;
|
||||
static int reports;
|
||||
|
||||
static void Postfix()
|
||||
{
|
||||
if (reports >= MaxReports) return;
|
||||
reports++;
|
||||
// Never let a measurement break a load: the game calls this from inside its own
|
||||
// XML post-load step, and an escaping exception aborts that step.
|
||||
try
|
||||
{
|
||||
LogOpaqueAtlas(reports);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.LogWarning("[NecromancerTome] BlockAtlasProbe: measurement #" + reports +
|
||||
" failed harmlessly: " + e.Message);
|
||||
}
|
||||
}
|
||||
|
||||
public static void LogOpaqueAtlas(int report)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("[NecromancerTome] BlockAtlasProbe #" + report + ": opaque block atlas");
|
||||
|
||||
if (MeshDescription.meshes == null)
|
||||
{
|
||||
sb.AppendLine(" MeshDescription.meshes is null - too early");
|
||||
Debug.Log(sb.ToString());
|
||||
return;
|
||||
}
|
||||
|
||||
MeshDescription mesh = MeshDescription.meshes[MeshDescription.MESH_OPAQUE];
|
||||
if (mesh == null)
|
||||
{
|
||||
sb.AppendLine(" MESH_OPAQUE is null - too early");
|
||||
Debug.Log(sb.ToString());
|
||||
return;
|
||||
}
|
||||
|
||||
var atlas = mesh.textureAtlas as TextureAtlasBlocks;
|
||||
if (atlas == null)
|
||||
{
|
||||
sb.AppendLine(" textureAtlas is " + (mesh.textureAtlas == null
|
||||
? "null" : mesh.textureAtlas.GetType().Name) + ", expected TextureAtlasBlocks");
|
||||
Debug.Log(sb.ToString());
|
||||
return;
|
||||
}
|
||||
|
||||
sb.AppendLine(" uvMapping entries: " +
|
||||
(atlas.uvMapping == null ? "null" : atlas.uvMapping.Length.ToString()));
|
||||
Describe(sb, "diffuse ", atlas.diffuseTexture);
|
||||
Describe(sb, "normal ", atlas.normalTexture);
|
||||
Describe(sb, "specular", atlas.specularTexture);
|
||||
|
||||
// A new paint needs an unused index in BlockTextureData.list. The table is filled
|
||||
// from painting.xml, which loads AFTER the textures - so on the early call this is
|
||||
// still null, and that is expected rather than a fault.
|
||||
if (BlockTextureData.list == null)
|
||||
{
|
||||
sb.AppendLine(" paint table: not built yet (painting.xml loads later)");
|
||||
}
|
||||
else
|
||||
{
|
||||
int used = 0, free = 0;
|
||||
for (int i = 0; i < BlockTextureData.list.Length; i++)
|
||||
{
|
||||
if (BlockTextureData.list[i] == null) free++; else used++;
|
||||
}
|
||||
sb.AppendLine(" paint slots: " + used + " used, " + free + " free, " +
|
||||
BlockTextureData.list.Length + " total");
|
||||
}
|
||||
|
||||
Debug.Log(sb.ToString());
|
||||
}
|
||||
|
||||
static void Describe(StringBuilder sb, string label, Texture texture)
|
||||
{
|
||||
if (texture == null)
|
||||
{
|
||||
sb.AppendLine(" " + label + ": null");
|
||||
return;
|
||||
}
|
||||
|
||||
var arr = texture as Texture2DArray;
|
||||
if (arr == null)
|
||||
{
|
||||
sb.AppendLine(" " + label + ": " + texture.GetType().Name +
|
||||
" (expected Texture2DArray) " + texture.width + "x" + texture.height);
|
||||
return;
|
||||
}
|
||||
|
||||
// depth = how many slices are already in the array; ours would become index `depth`,
|
||||
// and every number below has to be matched exactly by our own texture.
|
||||
sb.AppendLine(" " + label + ": " + arr.width + "x" + arr.height +
|
||||
" slices=" + arr.depth +
|
||||
" format=" + arr.format +
|
||||
" graphicsFormat=" + arr.graphicsFormat +
|
||||
" mips=" + arr.mipmapCount +
|
||||
" readable=" + arr.isReadable);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user