diff --git a/.gitignore b/.gitignore index 3a21cd3..56bad57 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /build /.vs *.pdb +/.managed-3.0 diff --git a/CustomTextures.csproj b/CustomTextures.csproj index 294b9b3..3d0e7d9 100644 --- a/CustomTextures.csproj +++ b/CustomTextures.csproj @@ -45,6 +45,9 @@ $(PATH_7D2D_MANAGED)\Assembly-CSharp.dll + + $(PATH_7D2D_MANAGED)\netstandard.dll + $(PATH_7D2D_MANAGED)\LogLibrary.dll diff --git a/CustomTextures.dll b/CustomTextures.dll index e2cc9a3..50cb598 100644 Binary files a/CustomTextures.dll and b/CustomTextures.dll differ diff --git a/Harmony/OpaqueTextures.cs b/Harmony/OpaqueTextures.cs index b2175d3..5d0bb0e 100644 --- a/Harmony/OpaqueTextures.cs +++ b/Harmony/OpaqueTextures.cs @@ -56,17 +56,85 @@ static void ParseOpaqueConfig(XElement root) public class BlockTexturesFromXMLCreateBlockTexturesPrefix { public static void Prefix(XmlFile _xmlFile) - => ParseOpaqueConfig(_xmlFile.XmlDoc.Root); + { + ParseOpaqueConfig(_xmlFile.XmlDoc.Root); + // Pre-size BlockTextureData.list BEFORE any chunk + // deserialization can access high paint IDs from saved + // data. Without this, reloading a save built against a + // larger texture set than the current process can fit + // throws IndexOutOfRange from the chunk-paint accessor. + EarlyResizeBlockTextureList(); + } } - // #################################################################### - // #################################################################### + static void EarlyResizeBlockTextureList() + { + // Generous floor: covers the existing vanilla range plus a + // sensible margin so a paint pack with several hundred + // entries doesn't trigger a resize during config-load. The + // GetFreePaintID safety net below grows the array further + // on demand if a config truly exceeds this. + const int minSize = 1024; + if (BlockTextureData.list == null) + { + BlockTextureData.list = new BlockTextureData[minSize]; + Log.Out("[OcbCustomTextures] Created BlockTextureData.list (was null) size={0}", minSize); + return; + } + var required = System.Math.Max(minSize, 512 + OpaqueConfigs.Count + 256); + if (BlockTextureData.list.Length < required) + { + var oldLen = BlockTextureData.list.Length; + Array.Resize(ref BlockTextureData.list, required); + Log.Out("[OcbCustomTextures] Pre-resized BlockTextureData.list {0} -> {1} (save reload protection)", + oldLen, required); + } + } + + // Safety net: if any code path tries to access + // BlockTextureData.list with an index >= Length, auto-grow. + // Catches chunk deserialization that runs before InitOpaqueConfig + // has a chance to size the array. No-op for the common case + // where the array is already large enough. + + [HarmonyPatch(typeof(BlockTextureData), nameof(BlockTextureData.Init))] + static class BlockTextureDataInitPatch + { + static bool Prefix(BlockTextureData __instance) + { + if (BlockTextureData.list == null) + { + BlockTextureData.list = new BlockTextureData[System.Math.Max(1024, __instance.ID + 256)]; + Log.Out("[OcbCustomTextures] Created BlockTextureData.list on-demand for ID {0}, size={1}", + __instance.ID, BlockTextureData.list.Length); + return true; + } + if (__instance.ID >= BlockTextureData.list.Length) + { + var oldLen = BlockTextureData.list.Length; + var newLen = System.Math.Max(1024, (((__instance.ID + 1) / 256) + 1) * 256); + Array.Resize(ref BlockTextureData.list, newLen); + Log.Out("[OcbCustomTextures] Safety resize for ID {0}: {1} -> {2}", + __instance.ID, oldLen, newLen); + } + return true; // continue with original Init + } + } static int GetFreePaintID() { for (var i = 0; i < BlockTextureData.list.Length; i++) if (BlockTextureData.list[i] == null) return i; - throw new Exception("No more free Paint IDs"); + // Was: throw new Exception("No more free Paint IDs"). Grow the + // backing array instead so a paint pack with more entries than + // the current capacity registers cleanly. This only fires when + // the array is genuinely full, so vanilla-sized configs never + // hit it. + var oldLen = BlockTextureData.list.Length; + Array.Resize(ref BlockTextureData.list, oldLen + 256); + Log.Out("[OcbCustomTextures] BlockTextureData.list grown {0} -> {1} to make room for new paint", + oldLen, BlockTextureData.list.Length); + return oldLen; } private static ushort PatchAtlasBlocks(MeshDescription mesh, TextureConfig tex) @@ -100,7 +168,30 @@ static void InitOpaqueConfig() var opaqueAtlas = opaque.textureAtlas as TextureAtlasBlocks; if (builtinOpaques == -1 && opaqueAtlas.diffuseTexture != null) builtinOpaques = (opaqueAtlas.diffuseTexture as Texture2DArray).depth; + // Dedicated server has no GPU atlas, so the diffuse texture + // check above never resolves a depth. Treating that as -1 falls + // through to incorrect ID assignment downstream; clamp to 0 so + // the headless install runs cleanly. tiling.index is a render + // concern, so leaving it 0 on server is fine. + if (builtinOpaques == -1) + { + builtinOpaques = 0; + Log.Out("[OcbCustomTextures] Dedicated server: no diffuse texture, builtinOpaques clamped to 0"); + } var textures = OpaqueConfigs.Values.ToList(); + // Pre-resize BlockTextureData.list to fit the about-to-be- + // assigned paint IDs. Grows in 256-slot blocks so a small + // config gets a small bump and a large pack doesn't trigger + // a resize per registered paint. + var idFloor = System.Math.Max(builtinOpaques, 512); + var required = idFloor + textures.Count + 1; + if (BlockTextureData.list != null && required > BlockTextureData.list.Length) + { + var oldLen2 = BlockTextureData.list.Length; + var newLen2 = ((required / 256) + 1) * 256; + Array.Resize(ref BlockTextureData.list, newLen2); + Log.Out("[OcbCustomTextures] Pre-resized BlockTextureData.list {0} -> {1}", oldLen2, newLen2); + } if (opaque == null) throw new Exception("MESH MISSING"); var atlas = opaque.textureAtlas as TextureAtlasBlocks; if (atlas == null) throw new Exception("INVALID ATLAS TYPE"); @@ -309,8 +400,13 @@ static Texture2DArray ApplyTextures(Texture2DArray texture, CommandBuffer cmds, if (OpaquesAdded == 0) return texture; if (GameManager.IsDedicatedServer) return texture; if (!texture.name.StartsWith("ta_opaque")) return texture; + // Was: texture.depth + OpaquesAdded. That undersizes the + // atlas whenever builtinOpaques > texture.depth (e.g. when the + // initial texture was allocated against an older atlas size). + // Max() keeps the original behavior in the common case where + // texture.depth >= builtinOpaques. var copy = ResizeTextureArray(cmds, texture, - texture.depth + OpaquesAdded, true, true); + System.Math.Max(texture.depth, builtinOpaques) + OpaquesAdded, true, true); foreach (TextureConfig cfg in OpaqueConfigs.Values) for (int i = 0; i < cfg.Length; i += 1) PatchTextures(cmds, copy, lookup(cfg), cfg.tiling, i, fallback); diff --git a/ModInfo.xml b/ModInfo.xml index 135639e..e0de7f1 100644 --- a/ModInfo.xml +++ b/ModInfo.xml @@ -5,5 +5,5 @@ - + \ No newline at end of file diff --git a/README.md b/README.md index 177cb82..b3c9c5e 100644 --- a/README.md +++ b/README.md @@ -238,6 +238,10 @@ These seem implemented fully, but unused for now! ## Changelog +### Version 0.8.1 + +- Rebuild for 7D2D V3.0 "Dead Hot Summer" (paint-limit-1023 fork). No source changes to the paint-limit logic — the texture-atlas API is unchanged. Added a `netstandard` reference so the project builds without a globally-installed .NET Framework 4.8 Developer Pack. + ### Version 0.8.0 - Update for 7D2D V2.0 exp (b285) diff --git a/Utils/OcbTextureUtils.cs b/Utils/OcbTextureUtils.cs index f1c2807..8a9daeb 100644 --- a/Utils/OcbTextureUtils.cs +++ b/Utils/OcbTextureUtils.cs @@ -224,17 +224,37 @@ public static void PatchTexture( Texture src, int srcidx = 0) { var offset = GetMipMapOffset(); + // Cap iteration at whichever is smaller: dst's full mip count, or + // src's available mips above the quality offset. The original + // implementation walked dst.mipmapCount unconditionally, which is + // fine when src is the same size or larger (the "2k into 1k" case + // the offset is designed for) but EXPLODES when src is smaller + // than dst ~ e.g. a 128×128 paint texture (8 mips, levels 0-7) + // being copied into a 1024+ atlas slot (11+ mips). The loop would + // ask src for mips 8, 9, 10, none of which exist, and Unity logs + // ERR Graphics.CopyTexture called with invalid source mip level + // (got 8, have 8 mips) + // for every iteration past the source's last mip. That floods the + // console at chunk paint time, especially when third-party paint + // mods (e.g. PyroPaints, "CK Textures N Paints") ship small paint + // textures that get patched into bigger atlas slots through here. + // Capping leaves the destination's smallest mip levels unwritten; + // those mips only matter at extreme view distance and the + // existing destination contents (vanilla or prior swap) are kept. + int maxMips = Math.Min(dst.mipmapCount, src.mipmapCount - offset); + if (maxMips <= 0) return; // Source has nothing usable at this quality + // Copy all mips individually, could optimize ideal case // Given that we don't do this often, not much to gain if (dst.isReadable && src.isReadable) { - for (int m = 0; m < dst.mipmapCount; m++) + for (int m = 0; m < maxMips; m++) SetPixelData(GetPixelData(src, srcidx, offset + m), dst, dstidx, m); ApplyPixelData(dst, false, false); } else { - for (int m = 0; m < dst.mipmapCount; m++) cmds. + for (int m = 0; m < maxMips; m++) cmds. CopyTexture(src, srcidx, m + offset, dst, dstidx, m); } }