Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/build
/.vs
*.pdb
/.managed-3.0
3 changes: 3 additions & 0 deletions CustomTextures.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@
<Reference Include="Assembly-CSharp">
<HintPath>$(PATH_7D2D_MANAGED)\Assembly-CSharp.dll</HintPath>
</Reference>
<Reference Include="netstandard">
<HintPath>$(PATH_7D2D_MANAGED)\netstandard.dll</HintPath>
</Reference>
<Reference Include="LogLibrary">
<HintPath>$(PATH_7D2D_MANAGED)\LogLibrary.dll</HintPath>
</Reference>
Expand Down
Binary file modified CustomTextures.dll
Binary file not shown.
106 changes: 101 additions & 5 deletions Harmony/OpaqueTextures.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion ModInfo.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@
<Description value="Core mod to allow other mods to add paint and terrain textures to the atlas." />
<Website value="https://github.com/OCB7D2D/OcbCustomTextures" />
<Author value="ocbMaurice" />
<Version value="0.8.0" />
<Version value="0.8.1" />
</xml>
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
24 changes: 22 additions & 2 deletions Utils/OcbTextureUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Expand Down