User report: trees that exist in retail are missing in ACdream.
SceneryGenerator had an extra heuristic filter at lines 169-180
that rejected scenery whose cell-origin vertex was a road vertex,
on top of the proper retail post-displacement road check
(FUN_00530d30 port via IsOnRoad). The comment admitted it
wasn't in the retail decomp -- it was added to widen road
margins visually. Side effect: any cell whose SW corner
happened to touch a road vertex had ALL of its scenery
dropped, even when the displaced position was well clear of
the road ribbon.
Removing the extra guard. The retail FUN_00530d30 ribbon test
already handles road exclusion correctly; the heuristic was
strictly subtractive and silently dropped trees the retail
client renders.
Tests stay 1439 green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the first on-screen HUD for the dev client plus today's mouse-control
refinements. Also lands yesterday's scenery-alignment changes that were
left uncommitted in the working tree.
Overlay:
- BitmapFont rasterizes a system TTF via StbTrueTypeSharp into a 512x512
R8 atlas at startup (Consolas on Windows, DejaVu/Menlo fallbacks)
- TextRenderer batches 2D quads in screen-space with ortho projection;
one shader + two draw calls (rect then text) for panel backgrounds
under glyphs
- DebugOverlay composes info / stats / compass / help panels on top of
the 3D scene; toggles via F1/F4/F5/F6; transient toasts for key events
- DebugLineRenderer and its shaders (carried over from the scenery work)
are properly committed in this commit
Controls:
- Per-mode mouse sensitivity (Chase 0.15, Fly 1.0, Orbit 1.0); F8/F9 to
adjust the active mode multiplicatively (x1.2)
- Hold RMB to free-orbit the chase camera around the player; release
stays at the new angle (no snap-back)
- Mouse-wheel zooms chase distance between 2m and 40m
- Chase pitch widened to [-0.7, 1.4] so mouse-Y tilts both ways from
the default neutral angle
Scenery alignment (carried from yesterday's session):
- ShadowObjectRegistry AllEntriesForDebug + Scale field
- SceneryGenerator uses ACViewer's OnRoad polygon test + baseLoc +
set_heading rotation
- BSPQuery dispatchers accept localToWorld so normals/offsets transform
correctly per part
- TransitionTypes.CylinderCollision rewritten with wall-slide + push-out
- PhysicsDataCache caches visual-mesh AABB for scenery that lacks
physics Setup bounds
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>
Four fixes from the ACME StaticObjectManager cross-reference:
1. GfxObjMesh: normalize vertex normals (1d). Dat normals may not be
unit-length; without normalization, lighting is wrong per-vertex.
2. SetupMesh: add third-fallback placement frame (2a). If neither
Resting nor Default exists, use the first available frame from
PlacementFrames. Matches ACME's GetDefaultPlacementFrame.
3. SceneryGenerator: building cell exclusion (4d). Compute which
terrain vertices have buildings (from LandBlockInfo.Objects +
Buildings), skip scenery spawns in those cells. Prevents trees
from spawning inside building footprints.
4. SceneryGenerator: slope filter (4e). Compute terrain normal Z at
each displaced position and check against ObjectDesc.MinSlope /
MaxSlope bounds. Prevents trees from spawning on cliff faces.
Also confirmed 4f (scenery Z=0) is NOT a bug — GameWindow's hydrator
lifts scenery to terrain Z at line 1213. The Z=0 in SceneryGenerator
is a placeholder correctly overridden at render time.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Four targeted fixes for user-reported movement/visual bugs:
1. Player entity disappearing: GpuWorldState now supports persistent
entities (MarkPersistent/DrainRescued). The player character survives
landblock unloads and gets re-injected into the streaming window at
the current center landblock.
2. Feet sinking into terrain: +0.15 Z bias in PlayerMovementController
keeps the character model above terrain z-fighting edge cases.
3. Camera after portal teleport: ChaseCamera.Update now called
immediately after teleport snap so the camera recenters on the new
position instead of lingering at the pre-teleport location.
4. Scenery on roads: SceneryGenerator now checks road status at the
final displaced position (not just the origin vertex), catching
objects that drift from non-road vertices onto road cells.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The IsRoadVertex check and helper were dropped by a linter pass after the
previous commit. Re-adding them explicitly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds SceneryGenerator.Generate which walks Region.TerrainInfo.TerrainTypes
+ Region.SceneInfo.SceneTypes for each landblock vertex, selects a scene
using the AC client's pseudo-random LCG hash of global cell coordinates,
then rolls each ObjectDesc's frequency, computes a displaced cell-local
position, random scale, and random rotation — the exact algorithm
ACViewer ports from the retail AC client's get_land_scenes().
Phase 2 rendered 239 explicit Stab+Building entities on the 3x3 Holtburg
grid but was missing every procedurally-placed tree, bush, rock, fence,
and small decoration because these are not stored as LandBlockInfo entries.
This adds 419 scenery entities across the same 9 landblocks, bringing the
total to 658.
Integration in GameWindow.OnLoad: after the existing Stab/Building
hydration loop, iterate each landblock's scenery spawns, resolve each
to a GfxObj or Setup via the same mesh pipeline, bake the random scale
into each MeshRef's PartTransform so the static mesh renderer doesn't
need a scale field on WorldEntity, and sample the landblock heightmap
bilinearly for the ground Z (simpler than ACViewer's find_terrain_poly
slope-aware placement).
Deliberate deferrals for first pass:
- No slope-based rejection (obj.MinSlope/MaxSlope). Trees may end up on
cliffs they shouldn't be on.
- No road-overlap rejection. Scenery may spawn in roads.
- No building-overlap rejection. Scenery may clip buildings.
- No WeenieObj handling (those are dynamic spawns, not static scenery).
All three filters will be added in a follow-up phase when we have the
walkable-polygon infrastructure they need.
Build clean, 48 tests still pass, smoke verified: "scenery: spawned 419
entities across 9 landblocks", process runs without exceptions.
Addresses the user visual feedback after Phase 2b: "some extra details
are missing, like a tree and the statue on top of the foundry". The tree
issue is now fixed (419 trees/bushes/rocks/etc placed). The foundry
statue may still be missing if it's a hierarchical Setup part (Phase 2a's
SetupMesh.Flatten intentionally doesn't walk ParentIndex) — that's a
separate fix if smoke verification shows it's still missing.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>