chore(scenery): audit SceneryGenerator against decompiled acclient.exe — all MATCH
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 <noreply@anthropic.com>
This commit is contained in:
parent
11974c2099
commit
eeee4c5733
1 changed files with 49 additions and 5 deletions
|
|
@ -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.
|
||||
/// </summary>
|
||||
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);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue