namespace AcDream.Core.Terrain;
///
/// Asheron's Call terrain texture-merge math: pure functions that turn raw
/// per-corner terrain/road data into the palette codes, surface blend recipes,
/// and per-cell vertex data that drive per-cell texture blending. All
/// algorithms are ports of WorldBuilder (Chorizite) — see
/// references/WorldBuilder/Chorizite.OpenGLSDLBackend/Lib/LandSurfaceManager.cs.
/// Pure CPU math, no GL, no dats — everything here is unit-testable in isolation.
///
public static class TerrainBlending
{
// --- Phase 3c.1 algorithms ---
///
/// Packs the 4 corner terrain types and 4 corner road flags of a cell into
/// a single 32-bit palette code. Two cells with identical corner layouts
/// always hash to the same code, which keys a cache of precomputed blend
/// overlays in later phases.
/// Ported verbatim from WorldBuilder LandSurfaceManager.GetPalCode.
/// Bit layout (from LSB):
/// bits 0- 4 t4 (5 bits)
/// bits 5- 9 t3
/// bits 10-14 t2
/// bits 15-19 t1
/// bits 20-21 r4 (2 bits)
/// bits 22-23 r3
/// bits 24-25 r2
/// bits 26-27 r1
/// bit 28 size marker (always set)
///
public static uint GetPalCode(
int r1, int r2, int r3, int r4,
int t1, int t2, int t3, int t4)
{
uint terrainBits = (uint)((t1 << 15) | (t2 << 10) | (t3 << 5) | t4);
uint roadBits = (uint)((r1 << 26) | (r2 << 24) | (r3 << 22) | (r4 << 20));
uint sizeBits = 1u << 28;
return sizeBits | roadBits | terrainBits;
}
///
/// Picks which way a 24×24 terrain cell is split into triangles. The hash
/// is designed so that both the visible mesh and the server's collision
/// triangulation agree on the same split — diverging from it causes
/// visual/physics disagreement at cell boundaries.
///
/// Uses the RENDER-PATH formula from AC2D and ACViewer's
/// LandblockMesh.cs (constants 0x0CCAC033, 0x421BE3BD, 0x6C1AC587,
/// 0x519B8F25), NOT the physics-path formula from WorldBuilder
/// (214614067 / 1813693831). The two produce different splits for
/// some cells, and the render formula is what the real AC client
/// uses for the visible terrain mesh. See
/// docs/research/2026-04-12-movement-deep-dive.md Part 3.
///
public static CellSplitDirection CalculateSplitDirection(
uint landblockX, uint cellX, uint landblockY, uint cellY)
{
uint x = landblockX * 8 + cellX;
uint y = landblockY * 8 + cellY;
uint dw = unchecked(x * y * 0x0CCAC033u - x * 0x421BE3BDu + y * 0x6C1AC587u - 0x519B8F25u);
// Bit 31 set = NE/SW diagonal (SWtoNE in our enum)
// Bit 31 clear = NW/SE diagonal (SEtoNW in our enum)
return (dw & 0x80000000u) != 0
? CellSplitDirection.SWtoNE
: CellSplitDirection.SEtoNW;
}
// --- Phase 3c.3 algorithms (surface recipe + vertex data packing) ---
///
/// Extracts the 4 corner terrain types from a palette code, in the order
/// [t1, t2, t3, t4]. Inverse of 's terrain packing.
///
public static uint[] ExtractTerrainCodes(uint palCode) => new uint[]
{
(palCode >> 15) & 0x1Fu,
(palCode >> 10) & 0x1Fu,
(palCode >> 5) & 0x1Fu,
palCode & 0x1Fu,
};
///
/// Pseudo-random index in [0, count) derived from the palette code. Same
/// code always returns the same index, so neighbor cells that share a
/// palette code pick the same alpha variant. Ported from
/// WorldBuilder.GeneratePseudoRandomIndex.
///
public static int PseudoRandomIndex(uint palCode, int count)
{
if (count <= 0) return 0;
// The computation uses int overflow and double multiplication
// exactly as in WorldBuilder — do NOT "clean up" the casts.
int pseudoRandom = (int)Math.Floor((1379576222 * palCode - 1372186442) * 2.3283064e-10 * count);
return pseudoRandom >= count ? 0 : pseudoRandom;
}
///
/// Rotates a terrain-code bit pattern one step (1→2→4→8→1). Used by
/// and to walk
/// the 4 rotations of an alpha map looking for one that matches the
/// target terrain/road code pattern.
///
public static uint RotateTerrainCode(uint code)
{
code *= 2;
return code >= 16 ? code - 15 : code;
}
///
/// Decodes the road-bits region of a palette code into up to two road
/// "codes" (corner patterns) and an allRoad flag. Ported verbatim
/// from WorldBuilder.GetRoadCodes. The magic constants describe which of
/// the 4 corners have roads (mask bit 0 = corner 1, bit 1 = corner 2,
/// etc.) and which canonical patterns the result maps to.
///
public static (uint road0, uint road1, bool allRoad) GetRoadCodes(uint palCode)
{
uint mask = 0;
if ((palCode & 0x0C000000u) != 0) mask |= 1; // r1 in bits 26-27
if ((palCode & 0x03000000u) != 0) mask |= 2; // r2 in bits 24-25
if ((palCode & 0x00C00000u) != 0) mask |= 4; // r3 in bits 22-23
if ((palCode & 0x00300000u) != 0) mask |= 8; // r4 in bits 20-21
if (mask == 0xF)
return (0, 0, allRoad: true);
return mask switch
{
0x0 => (0, 0, false),
0xE => (6, 12, false),
0xD => (9, 12, false),
0xB => (9, 3, false),
0x7 => (3, 6, false),
_ => (mask, 0, false),
};
}
///
/// Finds an alpha-map layer and rotation for a single terrain overlay.
/// is a 4-bit pattern indicating which
/// corners the overlay applies to (1,2,4,8 = single-corner patterns →
/// use corner maps; other patterns → use side maps). Returns
/// and rotation 0 if no alpha map
/// matches under any rotation, which is a valid "don't draw this
/// overlay" state.
/// Ported from WorldBuilder.FindTerrainAlpha.
///
public static (byte alphaLayer, byte rotation) FindTerrainAlpha(
uint palCode, uint terrainCode, TerrainBlendingContext ctx)
{
bool isCornerTerrain = terrainCode is 1 or 2 or 4 or 8;
var maps = isCornerTerrain ? ctx.CornerAlphaLayers : ctx.SideAlphaLayers;
var tCodes = isCornerTerrain ? ctx.CornerAlphaTCodes : ctx.SideAlphaTCodes;
if (maps.Count == 0 || tCodes.Count == 0)
return (SurfaceInfo.None, 0);
int randomIndex = PseudoRandomIndex(palCode, maps.Count);
uint currentCode = tCodes[randomIndex];
int rotationCount = 0;
while (currentCode != terrainCode && rotationCount < 4)
{
currentCode = RotateTerrainCode(currentCode);
rotationCount++;
}
if (rotationCount >= 4)
return (SurfaceInfo.None, 0);
return (maps[randomIndex], (byte)rotationCount);
}
///
/// Finds an alpha-map layer and rotation for a road overlay. Unlike
/// terrain alphas, roads iterate through all maps (starting at a random
/// offset) looking for one whose RCode matches the requested pattern
/// under some rotation.
/// Returns if no match exists.
/// Ported from WorldBuilder.FindRoadAlpha.
///
public static (byte alphaLayer, byte rotation) FindRoadAlpha(
uint palCode, uint roadCode, TerrainBlendingContext ctx)
{
var maps = ctx.RoadAlphaLayers;
var rCodes = ctx.RoadAlphaRCodes;
if (maps.Count == 0 || rCodes.Count == 0)
return (SurfaceInfo.None, 0);
int randomIndex = PseudoRandomIndex(palCode, maps.Count);
for (int i = 0; i < maps.Count; i++)
{
int index = (i + randomIndex) % maps.Count;
uint currentCode = rCodes[index];
for (int rotationCount = 0; rotationCount < 4; rotationCount++)
{
if (currentCode == roadCode)
return (maps[index], (byte)rotationCount);
currentCode = RotateTerrainCode(currentCode);
}
}
return (SurfaceInfo.None, 0);
}
///
/// Turns a palette code into a full blend
/// recipe. Handles three cases internally:
///
/// - All-road cell → base = road texture, no overlays
/// - All corners same terrain → base only, no overlays
/// - 2-4 distinct terrain types → base + up to 3 overlays per
/// WorldBuilder's duplicate-handling logic
///
/// Road overlays (up to 2) are added on top if any corner has a road bit.
/// Ported from WorldBuilder.BuildTexture + ProcessTerrainOverlays +
/// ProcessRoadOverlays + GetTerrainTextures + BuildTerrainCodesWithDuplicates.
///
public static SurfaceInfo BuildSurface(uint palCode, TerrainBlendingContext ctx)
{
var terrainCorners = ExtractTerrainCodes(palCode); // t1..t4
var (road0, road1, allRoad) = GetRoadCodes(palCode);
// All-road cell: base IS the road texture, no overlays at all.
if (allRoad)
{
return SurfaceInfo.BaseOnly(ctx.RoadLayer);
}
// Resolve base + up to 3 overlay terrain layers and the per-overlay
// "which corners" code. For 4 distinct corners this produces layers
// = [t1,t2,t3,t4] and codes = [2,4,8] (so overlays 0,1,2 cover corners
// 1,2,3 respectively while the base covers corner 0). For cells with
// duplicates the layout is rebuilt via BuildTerrainCodesWithDuplicates.
byte[] terrainLayers = new byte[4];
uint[] overlayCodes = new uint[3];
BuildOverlayLayers(terrainCorners, ctx, terrainLayers, overlayCodes);
// Base is always terrainLayers[0]. Overlays 0..2 correspond to
// terrainLayers[1..3] when present.
byte baseLayer = terrainLayers[0];
// Find alpha layer + rotation for each of up to 3 overlays.
// A zero terrain code means "no more overlays" (short-circuit loop).
byte ovl0Layer = SurfaceInfo.None, ovl0Alpha = SurfaceInfo.None, ovl0Rot = 0;
byte ovl1Layer = SurfaceInfo.None, ovl1Alpha = SurfaceInfo.None, ovl1Rot = 0;
byte ovl2Layer = SurfaceInfo.None, ovl2Alpha = SurfaceInfo.None, ovl2Rot = 0;
if (overlayCodes[0] != 0)
{
var (a, r) = FindTerrainAlpha(palCode, overlayCodes[0], ctx);
if (a != SurfaceInfo.None)
{
ovl0Layer = terrainLayers[1]; ovl0Alpha = a; ovl0Rot = r;
}
}
if (overlayCodes[1] != 0)
{
var (a, r) = FindTerrainAlpha(palCode, overlayCodes[1], ctx);
if (a != SurfaceInfo.None)
{
ovl1Layer = terrainLayers[2]; ovl1Alpha = a; ovl1Rot = r;
}
}
if (overlayCodes[2] != 0)
{
var (a, r) = FindTerrainAlpha(palCode, overlayCodes[2], ctx);
if (a != SurfaceInfo.None)
{
ovl2Layer = terrainLayers[3]; ovl2Alpha = a; ovl2Rot = r;
}
}
// Road overlays: share the road terrain layer, differ in alpha and rotation.
byte roadLayer = SurfaceInfo.None;
byte road0Alpha = SurfaceInfo.None, road0Rot = 0;
byte road1Alpha = SurfaceInfo.None, road1Rot = 0;
if (ctx.RoadLayer != SurfaceInfo.None && road0 != 0)
{
var (a0, r0) = FindRoadAlpha(palCode, road0, ctx);
if (a0 != SurfaceInfo.None)
{
roadLayer = ctx.RoadLayer;
road0Alpha = a0;
road0Rot = r0;
if (road1 != 0)
{
var (a1, r1) = FindRoadAlpha(palCode, road1, ctx);
if (a1 != SurfaceInfo.None)
{
road1Alpha = a1;
road1Rot = r1;
}
}
}
}
return new SurfaceInfo(
baseLayer,
ovl0Layer, ovl0Alpha, ovl0Rot,
ovl1Layer, ovl1Alpha, ovl1Rot,
ovl2Layer, ovl2Alpha, ovl2Rot,
roadLayer, road0Alpha, road0Rot,
road1Alpha, road1Rot);
}
///
/// Fills terrainLayers[0..3] and overlayCodes[0..2] based on the 4 corner
/// terrain codes. If all 4 corners are distinct we use the straightforward
/// mapping; otherwise we delegate to the duplicate-handling path. Combined
/// port of WorldBuilder.GetTerrainTextures +
/// BuildTerrainCodesWithDuplicates.
///
private static void BuildOverlayLayers(
uint[] terrainCorners,
TerrainBlendingContext ctx,
byte[] terrainLayers,
uint[] overlayCodes)
{
// Find first duplicate pair — if any exists, use the duplicate path.
for (int i = 0; i < 4; i++)
{
for (int j = i + 1; j < 4; j++)
{
if (terrainCorners[i] == terrainCorners[j])
{
BuildWithDuplicates(terrainCorners, ctx, terrainLayers, overlayCodes, i);
return;
}
}
}
// All 4 distinct: layers = [t1,t2,t3,t4], overlay codes = [2, 4, 8]
for (int i = 0; i < 4; i++)
terrainLayers[i] = LookupTerrainLayer(ctx, terrainCorners[i]);
overlayCodes[0] = 2;
overlayCodes[1] = 4;
overlayCodes[2] = 8;
}
private static void BuildWithDuplicates(
uint[] terrainCorners,
TerrainBlendingContext ctx,
byte[] terrainLayers,
uint[] overlayCodes,
int duplicateIndex)
{
uint primaryTerrain = terrainCorners[duplicateIndex];
uint secondaryTerrain = 0;
terrainLayers[0] = LookupTerrainLayer(ctx, primaryTerrain);
terrainLayers[1] = SurfaceInfo.None;
terrainLayers[2] = SurfaceInfo.None;
terrainLayers[3] = SurfaceInfo.None;
overlayCodes[0] = 0;
overlayCodes[1] = 0;
overlayCodes[2] = 0;
bool foundSecondary = false;
for (int k = 0; k < 4; k++)
{
if (primaryTerrain == terrainCorners[k]) continue;
if (!foundSecondary)
{
overlayCodes[0] = (uint)(1 << k);
secondaryTerrain = terrainCorners[k];
terrainLayers[1] = LookupTerrainLayer(ctx, secondaryTerrain);
foundSecondary = true;
}
else
{
// Another corner differs from primary.
if (secondaryTerrain == terrainCorners[k]
&& overlayCodes[0] == (1u << (k - 1)))
{
// Adjacent same-secondary corners merge into a 2-bit pattern.
overlayCodes[0] += (uint)(1 << k);
}
else
{
terrainLayers[2] = LookupTerrainLayer(ctx, terrainCorners[k]);
overlayCodes[1] = (uint)(1 << k);
}
break;
}
}
}
private static byte LookupTerrainLayer(TerrainBlendingContext ctx, uint terrainCode)
{
return ctx.TerrainTypeToLayer.TryGetValue(terrainCode, out var layer)
? layer
: SurfaceInfo.None;
}
///
/// Packs a and split direction into the 4 uint32s
/// that appear as vertex attributes (Data0..Data3) on every vertex
/// in a cell. Bit layout matches WorldBuilder's Landscape.vert so the
/// shader port can be verbatim.
///
///
/// Byte layout (little-endian, consumed by the shader as uvec4
/// byte-attribute reads):
///
///
/// data0: [ baseTex, baseAlpha=255, ovl0Tex, ovl0Alpha ]
/// data1: [ ovl1Tex, ovl1Alpha, ovl2Tex, ovl2Alpha ]
/// data2: [ road0Tex, road0Alpha, road1Tex, road1Alpha ]
/// data3: bit 0-1 rotBase (unused, 0)
/// bit 2-3 rotOvl0
/// bit 4-5 rotOvl1
/// bit 6-7 rotOvl2
/// bit 8-9 rotRd0
/// bit 10-11 rotRd1
/// bit 12 splitDirection (0=SWtoNE, 1=SEtoNW)
///
///
public static (uint d0, uint d1, uint d2, uint d3) FillCellData(
SurfaceInfo s, CellSplitDirection split)
{
uint texBase = s.BaseLayer;
uint alphaBase = SurfaceInfo.None; // unused, always 255
uint texOvl0 = s.Ovl0Layer;
uint alphaOvl0 = s.Ovl0AlphaLayer;
uint rotOvl0 = s.Ovl0Rotation;
uint texOvl1 = s.Ovl1Layer;
uint alphaOvl1 = s.Ovl1AlphaLayer;
uint rotOvl1 = s.Ovl1Rotation;
uint texOvl2 = s.Ovl2Layer;
uint alphaOvl2 = s.Ovl2AlphaLayer;
uint rotOvl2 = s.Ovl2Rotation;
// Road0 and Road1 share s.RoadLayer (same road texture, different alpha masks).
uint texRd0 = s.RoadLayer;
uint alphaRd0 = s.Road0AlphaLayer;
uint rotRd0 = s.Road0Rotation;
uint texRd1 = s.Road1AlphaLayer == SurfaceInfo.None ? SurfaceInfo.None : s.RoadLayer;
uint alphaRd1 = s.Road1AlphaLayer;
uint rotRd1 = s.Road1Rotation;
uint d0 = (texBase | (alphaBase << 8)) | ((texOvl0 | (alphaOvl0 << 8)) << 16);
uint d1 = (texOvl1 | (alphaOvl1 << 8)) | ((texOvl2 | (alphaOvl2 << 8)) << 16);
uint d2 = (texRd0 | (alphaRd0 << 8)) | ((texRd1 | (alphaRd1 << 8)) << 16);
uint d3 = 0u // rotBase in bits 0-1 (unused)
| (rotOvl0 << 2)
| (rotOvl1 << 4)
| (rotOvl2 << 6)
| (rotRd0 << 8)
| (rotRd1 << 10)
| ((((uint)split) & 1u) << 12);
return (d0, d1, d2, d3);
}
}