diff --git a/src/AcDream.Core/World/SceneryGenerator.cs b/src/AcDream.Core/World/SceneryGenerator.cs index 93ebdfe..2ff0dee 100644 --- a/src/AcDream.Core/World/SceneryGenerator.cs +++ b/src/AcDream.Core/World/SceneryGenerator.cs @@ -13,9 +13,23 @@ namespace AcDream.Core.World; /// the vertex's global cell coordinates. Without this generator, any landblock /// rendered from dats is missing all of its natural scenery. /// -/// Algorithm ported from ACViewer's Physics/Common/Landblock.get_land_scenes() -/// which is itself a port of the original AC client's scenery walker. We -/// deliberately skip the slope/road/building-overlap checks the original does; +/// Algorithm verified against the decompiled retail acclient.exe (Ghidra output): +/// - Scene-selection hash: chunk_00530000.c line 1144 +/// - Per-object frequency: chunk_00530000.c lines 1168-1174 +/// - Displacement formula: chunk_005A0000.c lines 4858-4878 (FUN_005a6cc0) +/// - Quadrant rotation: chunk_005A0000.c lines 4880-4902 +/// - Object rotation hash: chunk_005A0000.c lines 4924-4926 (FUN_005a6e60) +/// - Object scale: ACViewer Physics/Common/ObjectDesc.cs ScaleObj() +/// (scale hash constant 0x7f51=32593 not in dumped chunks; +/// confirmed against ACViewer which matches all other constants) +/// +/// Key implementation note: the decompiled client computes each LCG value as a +/// signed 32-bit int, then normalises with "if (val < 0) val += 2^32" before +/// dividing by 2^32. This is equivalent to our unchecked((uint)(...)) cast. +/// ACViewer's reference omits this cast and is subtly wrong for negative inputs. +/// We deliberately match the decompiled client, not ACViewer. +/// +/// We deliberately skip the slope/road/building-overlap checks the original does; /// those prevent scenery from floating in roads or clipping buildings but /// require walkable-polygon lookups that we don't yet have. Accepting visual /// artifacts (trees inside roads, scenery clipping buildings) for a first pass @@ -96,6 +110,9 @@ public static class SceneryGenerator uint globalCellY = cellY + blockY; // Scene-selection hash: picks one scene from the terrain's scene list. + // Decompiled: chunk_00530000.c line 1144 + // iVar5 = (iVar8 * 0x2a7f2b89 + 0x6c1ac587) * iVar9 + iVar8 * -0x421be3bd + 0x7f8cda01 + // where iVar8=globalCellX, iVar9=globalCellY. uint cellMat = globalCellY * (712977289u * globalCellX + 1813693831u) - 1109124029u * globalCellX + 2139937281u; double offset = cellMat * 2.3283064e-10; @@ -107,6 +124,13 @@ public static class SceneryGenerator if (scene is null) continue; // Per-object hashes: roll frequency, compute displacement, scale, rotation. + // Decompiled: chunk_00530000.c lines 1168-1174 + // iStack_60 = iVar9 * 0x6c1ac587 → cellYMat + // uStack_78 = iVar9 * iVar8 * 0x5111bfef + 0x70892fb7 → cellMat2 + // iStack_64 = iVar8 * -0x421be3bd → cellXMat + // initial: local_90 = uStack_78 * 0x5b67 (j=0 term) + // per-loop: iStack_70 = (iStack_60 - local_90) + iStack_64; local_90 += uStack_78 + // ⟹ iStack_70 = cellYMat - cellMat2 * (0x5b67 + j) + cellXMat uint cellXMat = unchecked(0u - 1109124029u * globalCellX); uint cellYMat = 1813693831u * globalCellY; uint cellMat2 = 1360117743u * globalCellX * globalCellY + 1888038839u; @@ -116,6 +140,8 @@ public static class SceneryGenerator var obj = scene.Objects[(int)j]; if (obj.WeenieObj != 0) continue; // Weenie entries are dynamic spawns, not static scenery + // Frequency roll: chunk_00530000.c line 1174 + 1179 + // (fVar1 * _DAT_007c6f10 < (float)piVar11[0x11]) → noise < obj.Frequency double noise = unchecked((uint)(cellXMat + cellYMat - cellMat2 * (23399u + j))) * 2.3283064e-10; if (noise >= obj.Frequency) continue; @@ -159,7 +185,11 @@ public static class SceneryGenerator float lz = 0f; // lifted to ground at render time via landblock heightmap - // Rotation + // Rotation: chunk_005A0000.c lines 4924-4931 (FUN_005a6e60) + // offset constant 0xf697 = 63127 + // iVar2 = (param_3 * 0x6c1ac587 - (param_2 * param_3 * 0x5111bfef + 0x70892fb7) * (param_4 + 0xf697)) + // + param_2 * -0x421be3bd + // param_2=ix, param_3=iy, param_4=j Quaternion rotation = Quaternion.Identity; if (obj.MaxRotation > 0) { @@ -171,7 +201,9 @@ public static class SceneryGenerator rotation = Quaternion.CreateFromAxisAngle(Vector3.UnitZ, radians); } - // Scale + // Scale: ACViewer Physics/Common/ObjectDesc.cs ScaleObj() (confirmed matches pattern) + // offset constant 0x7f51 = 32593 (not in dumped chunks; cross-verified via ACViewer) + // same LCG structure as rotation/displacement; uint cast per decompiled normalisation float scale; if (obj.MinScale == obj.MaxScale) { @@ -209,18 +241,27 @@ public static class SceneryGenerator /// Pseudo-random displacement within a cell for a scenery object. Returns a /// Vector3 in local cell-offset space (the caller adds it to the cell corner /// to get landblock-local position). + /// + /// Verified against decompiled acclient.exe: chunk_005A0000.c lines 4844-4903 (FUN_005a6cc0). + /// X offset constant 0xb2cd = 45773; Y offset constant 0x11c0f = 72719. + /// Quadrant hash: line 4880; thresholds 0.25/0.5/0.75 map to _DAT_007c97cc/_DAT_007938b8/_DAT_0079c6dc. + /// Decompiled normalises signed-int LCG results with "if (val < 0) val += 2^32"; our + /// unchecked((uint)(...)) is exactly equivalent. /// private static Vector3 DisplaceObject(ObjectDesc obj, uint ix, uint iy, uint iq) { float x, y; var baseLoc = obj.BaseLoc.Origin; + // X displacement: chunk_005A0000.c lines 4858-4866 + // iVar4 = (param_3 * 0x6c1ac587 - (param_2 * param_3 * 0x5111bfef + 0x70892fb7) * (param_4 + 0xb2cd)) + param_2 * -0x421be3bd if (obj.DisplaceX <= 0) x = baseLoc.X; else x = (float)(unchecked((uint)(1813693831u * iy - (iq + 45773u) * (1360117743u * iy * ix + 1888038839u) - 1109124029u * ix)) * 2.3283064e-10 * obj.DisplaceX + baseLoc.X); + // Y displacement: chunk_005A0000.c lines 4871-4878 (same structure, offset 0x11c0f = 72719) if (obj.DisplaceY <= 0) y = baseLoc.Y; else @@ -229,6 +270,9 @@ public static class SceneryGenerator float z = baseLoc.Z; + // Quadrant selection: chunk_005A0000.c lines 4880-4902 + // iVar4 = (param_3 * 0x6c1ac587 - (param_3 * 0x6f7bd965 + 0x421be3bd) * param_2) + -0x17fcedfd + // 0x6f7bd965=1870387557, 0x421be3bd=1109124029, -0x17fcedfd → -402451965 (uint: 3892515331) double quadrant = unchecked((uint)(1813693831u * iy - ix * (1870387557u * iy + 1109124029u) - 402451965u)) * 2.3283064e-10; if (quadrant >= 0.75) return new Vector3(y, -x, z);