Версия 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,321 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using HarmonyLib;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NecromancerTome
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds the mod's own paint to the game's opaque block texture atlas, so the Pyramid of
|
||||
/// Spirits can ship with a surface that does not exist in vanilla.
|
||||
///
|
||||
/// WHY A PATCH IS THE ONLY WAY. A block's Texture property is an INDEX into a prebuilt
|
||||
/// atlas; a paint entry in painting.xml carries TextureId/PaintCost/Group/SortIndex and
|
||||
/// never a path to an image, and the atlas itself is compiled into
|
||||
/// blocktextureatlases_assets_all.bundle. Nothing in the XML layer can introduce new image
|
||||
/// data, so the array has to be extended at runtime.
|
||||
///
|
||||
/// WHY THIS HOOK. CreateBlockTextures is the coroutine that reads painting.xml. Hooking its
|
||||
/// completion is deliberate and was learned the hard way: an earlier probe ran from
|
||||
/// MeshDescription.ReloadTextureArrays and found BlockTextureData.list still null, because
|
||||
/// the texture arrays load BEFORE painting.xml. By the time this coroutine finishes, both the
|
||||
/// arrays and the paint table exist.
|
||||
///
|
||||
/// THE NUMBERS THIS RELIES ON were measured in-game rather than assumed (BlockAtlasProbe,
|
||||
/// 2026-09-10): the opaque atlas holds 407 slices of 512x512 with a full 10-level mip chain,
|
||||
/// diffuse as DXT1 and normal/specular as DXT5, and all three arrays are non-readable. Two
|
||||
/// consequences drive the code below - our textures must match those numbers exactly, and
|
||||
/// every copy must go through the GPU, since a non-readable array cannot be read back.
|
||||
///
|
||||
/// SAFETY. Everything is wrapped: this runs inside the game's own XML loading, and an
|
||||
/// escaping exception aborts that step - which is exactly how a careless earlier version
|
||||
/// produced "XML loader: Executing post load step on 'materials.xml' failed". If anything
|
||||
/// here fails, the mod logs it and leaves the game exactly as it was.
|
||||
/// </summary>
|
||||
public static class CustomBlockPaintPatch
|
||||
{
|
||||
/// <summary>Bundle we ship the paint textures in, relative to the mod folder.</summary>
|
||||
const string BundlePath = "Resources/necroatlas";
|
||||
|
||||
const string DiffuseAsset = "Assets/NecroAtlas/atlas_necroPyramid_d.png";
|
||||
const string NormalAsset = "Assets/NecroAtlas/atlas_necroPyramid_n.png";
|
||||
const string SpecularAsset = "Assets/NecroAtlas/atlas_necroPyramid_m.png";
|
||||
|
||||
/// <summary>Name the paint is registered under; blocks.xml refers to the resulting id.</summary>
|
||||
public const string PaintName = "txName_NecroAsh";
|
||||
|
||||
/// <summary>Paint id handed out by the game once registration succeeds, -1 while unset.
|
||||
/// Logged on success so it can be written into blocks.xml.</summary>
|
||||
public static int AssignedPaintId = -1;
|
||||
|
||||
static bool alreadyRan;
|
||||
|
||||
/// <summary>
|
||||
/// Patch the coroutine's MoveNext. A coroutine compiles into a hidden state-machine
|
||||
/// class, so the method that actually runs is MoveNext, not CreateBlockTextures itself -
|
||||
/// AccessTools.EnumeratorMoveNext resolves it for us.
|
||||
/// </summary>
|
||||
[HarmonyPatch]
|
||||
static class CreateBlockTexturesHook
|
||||
{
|
||||
static IEnumerable<MethodBase> TargetMethods()
|
||||
{
|
||||
MethodBase coroutine = AccessTools.Method(
|
||||
typeof(BlockTexturesFromXML), "CreateBlockTextures");
|
||||
if (coroutine == null)
|
||||
{
|
||||
Debug.LogWarning("[NecromancerTome] CustomBlockPaint: " +
|
||||
"BlockTexturesFromXML.CreateBlockTextures not found - paint not added");
|
||||
yield break;
|
||||
}
|
||||
MethodBase moveNext = AccessTools.EnumeratorMoveNext(coroutine);
|
||||
if (moveNext == null)
|
||||
{
|
||||
Debug.LogWarning("[NecromancerTome] CustomBlockPaint: " +
|
||||
"could not resolve the coroutine's MoveNext - paint not added");
|
||||
yield break;
|
||||
}
|
||||
yield return moveNext;
|
||||
}
|
||||
|
||||
// __result == false means the enumerator is done: the XML has been read in full.
|
||||
static void Postfix(bool __result)
|
||||
{
|
||||
if (__result || alreadyRan) return;
|
||||
alreadyRan = true;
|
||||
try
|
||||
{
|
||||
AddPaint();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.LogWarning("[NecromancerTome] CustomBlockPaint: failed, game left " +
|
||||
"untouched: " + e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void AddPaint()
|
||||
{
|
||||
if (GameManager.IsDedicatedServer)
|
||||
{
|
||||
Debug.Log("[NecromancerTome] CustomBlockPaint: dedicated server, textures skipped");
|
||||
return;
|
||||
}
|
||||
|
||||
MeshDescription mesh = MeshDescription.meshes[MeshDescription.MESH_OPAQUE];
|
||||
var atlas = mesh == null ? null : mesh.textureAtlas as TextureAtlasBlocks;
|
||||
if (atlas == null)
|
||||
{
|
||||
Debug.LogWarning("[NecromancerTome] CustomBlockPaint: opaque atlas unavailable");
|
||||
return;
|
||||
}
|
||||
|
||||
AssetBundle bundle = LoadBundle();
|
||||
if (bundle == null) return;
|
||||
|
||||
try
|
||||
{
|
||||
var diffuse = bundle.LoadAsset<Texture2D>(DiffuseAsset);
|
||||
var normal = bundle.LoadAsset<Texture2D>(NormalAsset);
|
||||
var specular = bundle.LoadAsset<Texture2D>(SpecularAsset);
|
||||
if (diffuse == null || normal == null || specular == null)
|
||||
{
|
||||
Debug.LogWarning("[NecromancerTome] CustomBlockPaint: bundle is missing one " +
|
||||
"of the three textures - nothing added");
|
||||
return;
|
||||
}
|
||||
|
||||
Describe("our diffuse ", diffuse);
|
||||
Describe("our normal ", normal);
|
||||
Describe("our specular", specular);
|
||||
|
||||
int slice = Append(ref atlas.diffuseTexture, diffuse, "diffuse");
|
||||
Append(ref atlas.normalTexture, normal, "normal");
|
||||
Append(ref atlas.specularTexture, specular, "specular");
|
||||
if (slice < 0) return;
|
||||
|
||||
mesh.TexDiffuse = atlas.diffuseTexture;
|
||||
mesh.TexNormal = atlas.normalTexture;
|
||||
mesh.TexSpecular = atlas.specularTexture;
|
||||
mesh.ReloadTextureArrays(false);
|
||||
|
||||
int textureId = RegisterUvMapping(atlas, slice);
|
||||
RegisterPaint(textureId);
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Keep the loaded textures alive: only the bundle wrapper is released.
|
||||
bundle.Unload(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Give the new slice an entry in uvMapping and return its index - that index is what a
|
||||
/// block's Texture property in blocks.xml actually refers to.
|
||||
///
|
||||
/// The entry is CLONED from an existing plain opaque paint rather than built field by
|
||||
/// field. UVRectTiling carries more than a slice number (tiling, block size, material
|
||||
/// flags), and copying a known-good neighbour keeps every one of those correct without
|
||||
/// guessing at fields we have never inspected. Only the slice index is changed.
|
||||
/// </summary>
|
||||
static int RegisterUvMapping(TextureAtlasBlocks atlas, int slice)
|
||||
{
|
||||
// 356 is txName_Steel_wall - an ordinary full-block opaque paint, which is exactly
|
||||
// the shape of entry we want.
|
||||
const int TemplateTextureId = 356;
|
||||
if (atlas.uvMapping == null || atlas.uvMapping.Length <= TemplateTextureId)
|
||||
{
|
||||
Debug.LogWarning("[NecromancerTome] CustomBlockPaint: uvMapping too small to " +
|
||||
"clone a template from - paint not registered");
|
||||
return -1;
|
||||
}
|
||||
|
||||
int textureId = atlas.uvMapping.Length;
|
||||
Array.Resize(ref atlas.uvMapping, textureId + 1);
|
||||
UVRectTiling tile = atlas.uvMapping[TemplateTextureId];
|
||||
// Только индекс слоя: имени у UVRectTiling нет, оно живёт в BlockTextureData.
|
||||
tile.index = slice;
|
||||
atlas.uvMapping[textureId] = tile;
|
||||
|
||||
Debug.Log("[NecromancerTome] CustomBlockPaint: uvMapping entry " + textureId +
|
||||
" points at slice " + slice + " (cloned from " + TemplateTextureId + ")");
|
||||
return textureId;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Register the paint itself, so it has a name, shows up in the paint brush, and can be
|
||||
/// referred to by name. The block only needs the texture id, but a nameless texture with
|
||||
/// no paint entry would be invisible to the rest of the game.
|
||||
/// </summary>
|
||||
static void RegisterPaint(int textureId)
|
||||
{
|
||||
if (textureId < 0) return;
|
||||
if (BlockTextureData.list == null)
|
||||
{
|
||||
Debug.LogWarning("[NecromancerTome] CustomBlockPaint: paint table missing - " +
|
||||
"texture added but not named");
|
||||
return;
|
||||
}
|
||||
|
||||
int free = -1;
|
||||
for (int i = 0; i < BlockTextureData.list.Length; i++)
|
||||
{
|
||||
if (BlockTextureData.list[i] == null) { free = i; break; }
|
||||
}
|
||||
if (free < 0)
|
||||
{
|
||||
Debug.LogWarning("[NecromancerTome] CustomBlockPaint: no free paint slot - " +
|
||||
"texture added but not named");
|
||||
return;
|
||||
}
|
||||
|
||||
var data = new BlockTextureData
|
||||
{
|
||||
ID = free,
|
||||
Name = PaintName,
|
||||
LocalizedName = Localization.Get(PaintName),
|
||||
TextureID = (ushort)textureId,
|
||||
Group = "txGroupMasonry",
|
||||
PaintCost = 1,
|
||||
SortIndex = 0,
|
||||
Hidden = false,
|
||||
};
|
||||
data.Init();
|
||||
|
||||
AssignedPaintId = free;
|
||||
Debug.Log("[NecromancerTome] CustomBlockPaint: paint registered, slot " + free +
|
||||
", texture id " + textureId + " -> put Texture=\"" + textureId +
|
||||
"\" on the block in blocks.xml");
|
||||
}
|
||||
|
||||
static AssetBundle LoadBundle()
|
||||
{
|
||||
if (ModEntry.Instance == null)
|
||||
{
|
||||
Debug.LogWarning("[NecromancerTome] CustomBlockPaint: mod path unknown");
|
||||
return null;
|
||||
}
|
||||
|
||||
string path = Path.Combine(ModEntry.Instance.Path, BundlePath);
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
Debug.LogWarning("[NecromancerTome] CustomBlockPaint: bundle not found at " + path);
|
||||
return null;
|
||||
}
|
||||
|
||||
AssetBundle bundle = AssetBundle.LoadFromFile(path);
|
||||
if (bundle == null)
|
||||
{
|
||||
Debug.LogWarning("[NecromancerTome] CustomBlockPaint: bundle failed to load: " + path);
|
||||
}
|
||||
return bundle;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rebuild a texture array one slice larger and put our texture in the new last slot.
|
||||
/// Returns the new slice index, or -1 if the arrays disagree on anything that makes a
|
||||
/// copy impossible.
|
||||
/// </summary>
|
||||
static int Append(ref Texture target, Texture2D ours, string label)
|
||||
{
|
||||
var src = target as Texture2DArray;
|
||||
if (src == null)
|
||||
{
|
||||
Debug.LogWarning("[NecromancerTome] CustomBlockPaint: " + label +
|
||||
" is not a Texture2DArray - skipped");
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (ours.width != src.width || ours.height != src.height)
|
||||
{
|
||||
Debug.LogWarning("[NecromancerTome] CustomBlockPaint: " + label + " size mismatch, " +
|
||||
"atlas is " + src.width + "x" + src.height + " but ours is " +
|
||||
ours.width + "x" + ours.height + " - skipped");
|
||||
return -1;
|
||||
}
|
||||
if (ours.graphicsFormat != src.graphicsFormat)
|
||||
{
|
||||
Debug.LogWarning("[NecromancerTome] CustomBlockPaint: " + label + " format mismatch, " +
|
||||
"atlas is " + src.graphicsFormat + " but ours is " + ours.graphicsFormat +
|
||||
" - skipped");
|
||||
return -1;
|
||||
}
|
||||
if (ours.mipmapCount != src.mipmapCount)
|
||||
{
|
||||
Debug.LogWarning("[NecromancerTome] CustomBlockPaint: " + label + " mip mismatch, " +
|
||||
"atlas has " + src.mipmapCount + " but ours has " + ours.mipmapCount +
|
||||
" - skipped");
|
||||
return -1;
|
||||
}
|
||||
|
||||
int slice = src.depth;
|
||||
var grown = new Texture2DArray(src.width, src.height, slice + 1,
|
||||
src.graphicsFormat, UnityEngine.Experimental.Rendering.TextureCreationFlags.MipChain,
|
||||
src.mipmapCount);
|
||||
grown.name = src.name + "+necro";
|
||||
grown.wrapMode = src.wrapMode;
|
||||
grown.filterMode = src.filterMode;
|
||||
grown.anisoLevel = src.anisoLevel;
|
||||
|
||||
// GPU-side copy: the game's arrays are non-readable, so nothing can be pulled back
|
||||
// to the CPU. CopyTexture moves whole slices with their mip chains.
|
||||
for (int i = 0; i < slice; i++) Graphics.CopyTexture(src, i, grown, i);
|
||||
Graphics.CopyTexture(ours, 0, grown, slice);
|
||||
|
||||
target = grown;
|
||||
Debug.Log("[NecromancerTome] CustomBlockPaint: " + label + " grown from " + slice +
|
||||
" to " + (slice + 1) + " slices");
|
||||
return slice;
|
||||
}
|
||||
|
||||
static void Describe(string label, Texture2D tex)
|
||||
{
|
||||
Debug.Log("[NecromancerTome] CustomBlockPaint: " + label + " " +
|
||||
tex.width + "x" + tex.height + " format=" + tex.format +
|
||||
" graphicsFormat=" + tex.graphicsFormat + " mips=" + tex.mipmapCount);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,8 +10,14 @@ namespace NecromancerTome
|
||||
/// </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();
|
||||
|
||||
@@ -44,6 +44,13 @@
|
||||
<HintPath>..\..\..\7DaysToDie_Data\Managed\UnityEngine.AnimationModule.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
<!-- AssetBundle.LoadFromFile for CustomBlockPaintPatch.cs (2026-09-10) - the mod ships its
|
||||
own block paint textures in Resources/necroatlas, and Unity keeps bundle loading in its
|
||||
own module rather than CoreModule. -->
|
||||
<Reference Include="UnityEngine.AssetBundleModule">
|
||||
<HintPath>..\..\..\7DaysToDie_Data\Managed\UnityEngine.AssetBundleModule.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
<!-- PlayerActionsLocal.Secondary (PlayerAction) for PortalStonePatch.cs's power-attack
|
||||
channel-cancel (2026-08-29) - the game's own input layer, not something this mod
|
||||
previously needed to touch directly. -->
|
||||
|
||||
Reference in New Issue
Block a user