using System; using System.Text; using HarmonyLib; using UnityEngine; namespace NecromancerTome { /// /// 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. /// /// [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); } } }