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);
}
}