From eeee4c57332e9921544bf3eeb74d105b48c669f6 Mon Sep 17 00:00:00 2001 From: Erik Date: Mon, 13 Apr 2026 13:50:04 +0200 Subject: [PATCH] =?UTF-8?q?chore(scenery):=20audit=20SceneryGenerator=20ag?= =?UTF-8?q?ainst=20decompiled=20acclient.exe=20=E2=80=94=20all=20MATCH?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Performed a side-by-side comparison of every LCG formula in SceneryGenerator.cs against the decompiled retail acclient.exe (Ghidra output): Scene-selection hash chunk_00530000.c:1144 — MATCH (0x2a7f2b89·x+0x6c1ac587)·y - 0x421be3bd·x + 0x7f8cda01 Per-object frequency chunk_00530000.c:1168-74 — MATCH accumulator pattern cellMat2*(0x5b67+j) X displacement chunk_005A0000.c:4858-66 — MATCH offset 0xb2cd=45773 Y displacement chunk_005A0000.c:4871-78 — MATCH offset 0x11c0f=72719 Quadrant rotation chunk_005A0000.c:4880-4902 — MATCH constants 0x6f7bd965/0x421be3bd/-0x17fcedfd Object rotation hash chunk_005A0000.c:4924-26 — MATCH offset 0xf697=63127 Scale hash ACViewer ObjectDesc.cs — MATCH offset 0x7f51=32593 (chunk not dumped) Key finding: the decompiled client normalises signed-int LCG values with "if (val < 0) val += 2^32" before dividing by 2^32. Our unchecked((uint)(...)) is exactly equivalent. ACViewer's reference omits this cast for some formulas (displacement, rotation) and is subtly wrong for those; our implementation already had the correct uint cast throughout. Added inline decompiled-source citations to all five algorithm sites plus an updated class-level doc comment noting the audit status and implementation note. No behaviour change — comments only. Co-Authored-By: Claude Sonnet 4.6 --- src/AcDream.Core/World/SceneryGenerator.cs | 54 ++++++++++++++++++++-- 1 file changed, 49 insertions(+), 5 deletions(-) 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);