Skip to content

feat: Terrain Quad-Tree LOD, Biome Landscape System & Biome-Aware Tree Allocation#1018

Merged
lalalune merged 59 commits into
developfrom
tcm/biome-terrain-generation
Mar 12, 2026
Merged

feat: Terrain Quad-Tree LOD, Biome Landscape System & Biome-Aware Tree Allocation#1018
lalalune merged 59 commits into
developfrom
tcm/biome-terrain-generation

Conversation

@tcm390

@tcm390 tcm390 commented Mar 12, 2026

Copy link
Copy Markdown
Contributor

Assets sub-PR: PlayHyperia/assets#15


Summary

This PR introduces three major terrain features: a quad-tree LOD rendering system for large map support, a data-driven biome and landscape system for terrain generation, and a biome-aware tree placement pipeline.


1. Quad-Tree LOD Terrain Rendering

A hybrid quad-tree visual LOD system that replaces the flat 100m grid for rendering only — gameplay logic, physics, resource spawning, and server sync continue to use the flat grid.

Config flags (in TerrainSystem.CONFIG):

  • USE_QUADTREE_LOD — enables the quad-tree LOD system (set to false to use the previous flat grid renderer, no breakage)
  • QUADTREE_DEBUG_WIREFRAME — renders terrain as wireframe with depth-colored chunks: red (depth 0, largest) → orange → yellow → cyan → green (depth 4, finest)

2. Biome & Landscape Terrain System

A data-driven terrain generation pipeline where biomes and landscape features are defined declaratively.

3 biomes defined in TerrainBiomeTypes.ts via the BiomeType enum:

  • Forest — default biome, lush deciduous/conifer terrain
  • Canyon — arid desert terrain with cliffs
  • Tundra — cold sparse terrain

2 landscape types defined in TerrainHeightParams.ts via the LandscapeType enum:

  • Mountain — radial envelope × domain-warped noise → terrace quantization, producing organic tiered terrain with configurable layers, shape, edge sharpness, and noise blend
  • Pond — terrain depression for water bodies using the same algorithm with inverted strength

Currently 3 example features are placed in LANDSCAPE_FEATURES[]: 2 mountains and 1 pond. To add new biomes or landscapes, add entries to TerrainBiomeTypes.ts (biomes) and TerrainHeightParams.ts (landscape features).

Terrain shader (TerrainShader.ts):

  • Replaced flat biome colors with texture sampling — each biome has its own set of terrain textures
  • Per-biome cliff colors and biome-aware snow caps
  • Biome blending at boundaries using vertex weights

3. Biome-Aware Tree Placement

Each biome now defines its own tree distribution, density, spacing, and per-tree placement rules in TerrainBiomeTypes.ts:

4. GLBTreeBatchedInstancer — Multi-Variant Tree Rendering

A new BatchedMesh-based instancer (GLBTreeBatchedInstancer.ts) for tree types that have multiple model variants (e.g. tree_dead has 5 dead tree models, tree_cactus has 8).

Why BatchedMesh instead of InstancedMesh:

InstancedMesh binds a single geometry — to support N model variants you'd need N separate InstancedMesh objects per material slot per LOD level, multiplying draw calls. BatchedMesh registers all variant geometries via addGeometry() and each instance picks its variant via addInstance(geometryId), keeping it to 1 draw call per material slot regardless of how many variants exist.

The old GLBTreeInstancer (InstancedMesh-based) is still used for single-model resources. TreeGLBVisualStrategy routes to the correct instancer based on whether the manifest entry has modelVariants or a single modelPath.

tcm390 and others added 30 commits March 3, 2026 00:25
Replace procgen tree rendering with GLB model loading via modelVariants
array in woodcutting manifest. Consolidate tree types to single fir tree
with 5 model variations. Refactor GLBTreeInstancer to support
multi-material/multi-primitive GLB meshes, dynamic collision proxy sizing,
double-sided leaf textures with repeat wrapping, and per-instance fresnel
rim highlight driven by an InstancedBufferAttribute — eliminating the
previous highlight mesh overlay and outline post-process pass for trees.

Made-with: Cursor
Shadow receiving on VRM characters, duel arena objects, GLB resources,
and placeholder instancers is a significant FPS bottleneck with no
meaningful visual benefit for these objects.

Made-with: Cursor
Replace post-processing outline highlight with per-instance Fresnel rim
shader across GLBResourceInstancer, PlaceholderInstancer, and their
visual strategies. This eliminates the costly extra render pass and
separate highlight meshes, significantly reducing FPS drops on hover.

Also split the monolithic GPUVegetation.ts (1370 lines) into:
- LODConfig.ts: LOD distance table, scaling, caching (pure data/math)
- GPUMaterials.ts: material factories (vegetation, dissolve, imposter)

Tuned highlight brightness (BRIGHTEN 0.2→0.08) to reduce whitewash.

Made-with: Cursor
…eShadow on stations

Extend shader-based Fresnel rim highlight to all non-instanced entities
(stations, items, mobs, NPCs, players) via base Entity class. Uses a
per-material uniform toggle instead of instanced attributes, lazily
applied on first hover. Eliminates post-processing outline pass for
these entities, reducing GPU overhead.

- Add applyRimHighlight() to GPUMaterials.ts for uniform-based highlight
- Add setShaderHighlight() to base Entity class with lazy initialization
- Remove dead legacy highlight code (getHighlightRoot, getHighlightMesh,
  activeHighlightMesh) from ResourceEntity, ResourceVisualStrategy, and
  EntityHighlightService
- Disable receiveShadow on all station entities for performance

Made-with: Cursor
The resourceVariants map was only populated for tree-type resources,
causing ore/fishing/herb gathering to fall back to 'tree_normal' which
no longer exists in manifests after tree type consolidation.

Made-with: Cursor
…d terrain lighting

- Add createTreeDissolveMaterial with vertex-color AO, saturation boost,
  and per-instance rim highlight for trees
- Extract shared applyTerrainSunShade function used by both terrain and
  tree shaders for consistent shadow-side sky tinting
- Refactor TerrainShader to use shared computeTerrainBaseColor function
  and applyTerrainSunShade in outputNode
- Sync sun direction and shade color uniforms from Environment system
  for both terrain and tree materials (day/night cycle)
- Remove failed terrain color blending at tree base

Made-with: Cursor
…sed variant matching

Replace per-variant InstancedMesh with BatchedMesh to consolidate all
model variants into a single draw call per tree type per material slot.
Update GPUMaterials highlight detection to use vBatchColor, and
TreeGLBVisualStrategy to pass treeType + variants + variantIndex.
Add texture fingerprint matching to fix material-geometry mismatch
when GLB files have meshes in different traversal order.

Made-with: Cursor
…d InstancedMesh for single models

Split tree rendering into two paths: GLBTreeBatchedInstancer (BatchedMesh)
for multi-variant trees and GLBTreeInstancer (InstancedMesh) for single-model
trees. Unified TreeGLBVisualStrategy dispatches to the correct instancer
based on config.modelVariants. GPUMaterials supports both highlight methods
via a batched option.

Made-with: Cursor
Models without COLOR_0 attribute (e.g. willow) were rendered at 35%
brightness because the AO shader read zeroes from missing vertex data.
Now checks source material's vertexColors flag and skips AO darkening
when vertex colors are absent.

Made-with: Cursor
Add a client-side quadtree LOD terrain rendering system that runs
alongside the existing flat-grid for game logic. Key changes:

- TerrainQuadTree: hierarchical quad-tree with configurable depth,
  split ratio, uniform resolution per chunk, and neighbor tracking
- TerrainVisualManager: bridges quad-tree events to Three.js scene
  with async web worker dispatch and sync fallback
- QuadChunkWorker: inline web worker for off-thread heightmap, normal,
  color, and biome computation with Transferable buffers
- TerrainQuadChunkGenerator: main-thread assembly from worker output
  with road influence, flat zone overrides, and skirt generation
- Centralized shadow config (TERRAIN_RECEIVE_SHADOW / TERRAIN_CAST_SHADOW)
  as single source of truth across flat-grid and quad-tree meshes

Made-with: Cursor
…up, retry

- Fix flat zone blending: use getFlatZoneHeight (smoothstep) instead of
  hard cutoff getFlatZoneAt
- Add split/unsplit hysteresis (unsplitMultiplier: 1.2) to prevent
  thrashing at LOD boundaries
- Fix qtContainer leak: remove from scene graph on dispose
- Guard terrainContainer.parent null access in init
- Add bounded retry (max 3 attempts) for failed chunk generation
- Remove unused heightModifier from worker biomes type
- Deduplicate sync fallback: generateQuadChunkDataSync produces
  QuadChunkWorkerOutput, then feeds through assembleQuadChunkGeometry
- Make per-frame limits configurable via CONFIG

Made-with: Cursor
…itecture

- Add BiomeType enum (Tundra/Forest/Desert) as single source of truth
- Create TerrainWorkerShared.ts to eliminate code duplication between workers
- Implement per-biome noise profiles (terrain shape, terracing, power curves)
- Add biome-blended GPU shader with distinct palettes per biome
- Place exactly 3 biome centers (one per type) in equilateral triangle layout
- Set Forest as default biome with sharp Gaussian boundaries
- Clean up dead code (mountain boost, legacy biome types, duplicate worker logic)

Made-with: Cursor
- Add quadtree chunk refresh pass to refreshRoadInfluence() so roads
  render correctly when USE_QUADTREE_LOD is enabled
- Increase QUADTREE_RESOLUTION from 32 to 64 to match tile vertex
  density, preventing roads from appearing thinner in quadtree mode

Made-with: Cursor
…are snow cap

- Increase ISLAND_RADIUS from 350 to 525 (1.5x), scale falloff and ocean buffer proportionally
- Add terraceSteps to BiomeNoiseProfile for per-biome terrace density (tundra: 4, forest: 0, desert: 8)
- Remove global TERRACE_STEPS constant — now blended per-vertex from biome weights
- Snow overlay now only appears in tundra biome areas instead of all non-desert high elevations
- Landscape features (mountains/ponds) sourced from LANDSCAPE_FEATURES constant array

Made-with: Cursor
Replace Gaussian bell-curve landscape features with a configurable
stepped plateau algorithm. Each feature now supports per-layer terracing
with tunable shape, edge sharpness, slope, and noise wobble.

- New LandscapeFeatureDef params: layers, shapePower, edgeSharpness,
  layerSlope, noiseScale, noiseAmount (replaces gaussianCoeff)
- layerSlope controls incline within each terrace band (0=flat, 1=full slope)
- Noise wobble on boundaries for organic non-circular edges
- Island radius increased to 788 (1.5x from 525)
- Updated worker mirror and serialization to match

Made-with: Cursor
…control

Replace round-based terracing (caused low-lying artifacts) with floor-quantize
algorithm that creates natural downward-stepping contour bands. Add per-biome
terraceSharpness param controlling flat shelf vs cliff transition ratio.
Tune biome profiles: disable terracing for forest, strengthen for desert.

Made-with: Cursor
- Increase MAX_HEIGHT from 50 to 150 for taller terrain
- Raise WATER_THRESHOLD from 9 to 50 to match new height scale
- Halve all noise layer scales for spatially larger terrain features
- Tweak forest hillWeight and landscape feature positions/strengths

Made-with: Cursor
…fy constants

- Move MAX_HEIGHT, WATER_LEVEL_NORMALIZED, SHORELINE_CONFIG to TerrainHeightParams
- Replace per-biome terraceSteps with global TERRACE_STEPS (10) to fix boundary artifacts
- Add terraceHeightScale to BiomeNoiseProfile — stretches shelf separation for taller cliffs
- Add terraceSlope to BiomeNoiseProfile — blends natural slope back onto shelves
- Fix Forest terraceSharpness from 5.1 to 0, increase hillWeight for better terrain depth
- Sync worker mirror (buildGetBaseHeightAtJS / PROFILES_JS) with all changes

Made-with: Cursor
…r, GameConstants

- Rename TerrainShader TERRAIN_CONSTANTS to TERRAIN_SHADER_CONSTANTS to avoid naming conflict
- TerrainSystem now imports MAX_HEIGHT, WATER_LEVEL_NORMALIZED, SHORELINE_CONFIG from TerrainHeightParams
- Remove dead CONFIG fields (CAMERA_FAR, FOG_NEAR, FOG_FAR, shoreline duplicates) from TerrainSystem
- TILE_SIZE now sourced from TERRAIN_CONSTANTS.TERRAIN_TILE_SIZE
- Clean up stale WORLD_CONSTANTS (WORLD_SIZE, TERRAIN_HEIGHT_SCALE, SEA_LEVEL, etc.)
- Fix WATER_THRESHOLD to 8.0 for correct water level

Made-with: Cursor
…ystem

- Rename BiomeType.Desert to BiomeType.Canyon across all code, types, workers, shaders, and tests
- Remove stale biome types (plains, valley, mountains, swamp, lakes, jungle) from all configs
- Update biomes.json manifest to only contain tundra, forest, canyon
- Align TownSystem, RoadNetworkSystem, POISystem, BiomeResourceGenerator, ProcgenRockCache,
  ProcgenPlantCache suitability/cost/preset maps to 3-biome system
- Update all test files to reference only tundra/forest/canyon
- Narrow type unions in terrain.ts and world-types.ts

Made-with: Cursor
Root cause: during duel combat, camera radius and phi (pitch) were both
computed from live entity separation distance. As agents repositioned
every ~1.2s via DuelCombatAI, separation oscillated causing ~2+ units
of camera height swing per movement tick.

Additionally, the LOS grid search (500ms refresh cycle) caused phi
oscillation when it alternated between rating adjacent angles as
"clear" vs "blocked", and 3 cascading smoothing layers with different
linear rate caps created mechanical motion and inter-layer conflicts.

Changes:
- Lock entity Y positions during duel combat (COUNTDOWN/FIGHTING/RESOLUTION)
  to eliminate terrain sampling noise from competing systems
- Lock separation distance at phase start so radius and phi stay stable
- Skip LOS grid search during combat phases (arena has clear sightlines);
  use smooth exponential damping toward desired angles instead
- Collapse 3-layer smoothing (cache→targetSpherical→spherical) into
  single-layer exponential damping for film-quality motion
- Smooth phase bias transitions to prevent focus-point Y jumps
- Remove separation-dependent phi variation that caused discontinuous
  pitch changes when agents moved
- Remove all combat-reactive camera effects (punch-in, shake, dramatic
  low angle, FOV intensification) for clean cinematic experience
- Add phase-aware camera parameters (radius, phi, FOV, orbit amplitude,
  focus bias) per duel phase for intentional cinematic language
…iming

- Use MIN_ANNOUNCEMENT_DURATION for getPhaseEndTime during ANNOUNCEMENT
  phase since the phase early-exits as soon as both agents are alive
- Tune FIGHTING camera hold timing: reduce min/max hold and idle threshold
  for more dynamic camera switching during combat
…olors

Replace stepped-power-plateau with envelope × domain-warped ridge noise
so terrace contours follow organic ridges instead of concentric circles.
Add per-biome cliff colors (tundra/forest/canyon) and remove snow line.

Made-with: Cursor
…tect external agents in monitor

- Add `type: "STREAMING_STATE_UPDATE"` to streaming state API response so
  the keeper bot can process duel lifecycle events (was silently discarding)
- Add external agent detection in admin monitor endpoint by scanning world
  entities with isAgent flag or agent- ID prefix
- Update local anvil world address
Resolve contract address conflicts by taking main's values (ephemeral
local anvil addresses). Camera system auto-merged cleanly.
Swap procedural flat-color terrain shading for CDN-loaded biome textures
with triplanar mapping on cliffs and top-down projection on flat surfaces.
Textures load async with palette-colored placeholders for instant display.

Made-with: Cursor
- Replace PBR toon banding with portfolio-style Lambert lighting
- Bypass PBR output, sample albedo directly for custom soft lighting
- smoothstep + clamp compresses light range; clamp floor scales with
  dayFactor so trees properly darken at night
- Add Fresnel edge brightening on leaves (morpho effect)
- Apply terrain sun shade for consistent shadow-side tinting
- Wire wind uniforms and isLeafMaterial detection in both instancers
- Add knotwood tree type with spawn weight

Made-with: Cursor
…dows

CSM is too heavy and exceeds WebGPU's 16-texture binding limit when
combined with the terrain's 13 biome/utility textures. Default to a
single 2048 shadow map (ENABLE_CSM=false) which adds only 2 shadow
textures (total 15, within limit). Terrain now receives shadows with
an auto-disable guard when CSM is on.

Made-with: Cursor
tcm390 and others added 20 commits March 10, 2026 16:53
- Remove unused DEFAULT_BIOMES, BIOME_IDS, BIOME_TYPE_WEIGHTS from
  BiomeSystem and their exports from procgen/terrain/index
- Remove hardcoded town override from TerrainSystem.getBiomeAt() that
  forced forest biome near legacy starter town coordinates
- Fix BiomeSystem coordinate math: use tile center (tileX * tileSize)
  instead of offset (tileX * tileSize + tileSize/2)
- Delegate TerrainSystem.getBiomeAt() directly to BiomeSystem.getBiomeForTile()
- Use dynamic biome keys for fallbacks instead of hardcoded "plains"
- Update TerrainGenerator.test.ts to use local TEST_BIOMES fixture
- Remove temporary biome debug logging

Made-with: Cursor
…init

The World constructor registers a default PhysicsSystem whose init() calls
waitForPhysX() with a 120s timeout. In streaming/spectator mode we skip
replacing it with the full Physics system, but the default stub remained
registered and stalled the entire init chain — causing the loading screen
to freeze at 3% "Initializing world systems...".

Now we explicitly removeSystem("physics") in streaming mode so the init
pipeline proceeds immediately without waiting for PhysX WASM.
- Add physics null guards in RigidBody.ts and Collider.ts for stream
  mode viewports where physics system is removed
- Re-enable autonomous behavior (mining, chopping, fishing) for duel
  bot agents between duels instead of suppressing it
- Remove dedicatedDuelBot gates that killed all open-world autonomy
- Make shouldRunOpenWorldAutonomy() always return true
- Start autonomous behavior on ElizaDuelBot connect
- Request bank state on player spawn so goal planner has item data
- Relax post-duel restore position from 120-unit lobby radius to
  2000-unit world boundary so agents roam freely
- Interleave Anthropic/Groq model agents for provider diversity
- Bump @elizaos/plugin-elizacloud to 2.0.0-alpha.7
Resolve conflicts by accepting main for contract deploy artifacts,
stream capture simplification, and createClientWorld PhysX block.

Made-with: Cursor
…e unload

Fixes sparse trees when moving between distant locations. Root causes:
1. Individual entityAdded packets were silently dropped by BandwidthBudget
   during rapid tile generation (NORMAL priority throttled under load)
2. ResourceEntities were never destroyed in EntityManager on tile unload,
   causing entity leaks and preventing re-creation on tile revisit

Changes:
- Batch all entity spawns for a tile into a single entitiesBatchAdded packet
- Add sendHighPriority to ServerNetwork (configurable via useHighPriorityBatch)
- Add suppressBroadcast option to EntityManager.spawnEntity() for batching
- Destroy ResourceEntities in EntityManager when tiles unload
- Register entitiesBatchAdded packet and client handler

Made-with: Cursor
…-biome placement

- Add TreeId enum to replace hardcoded "tree_" prefix strings
- Move per-biome tree configs (distribution, placement, density) to
  TerrainBiomeTypes.ts alongside BiomeType enum
- Add per-biome TreePlacementRules so each tree type can have different
  placement constraints per biome (e.g. dead tree minHeight differs
  between canyon and tundra)
- Remove spawnWeight from TreeTypeDefinition (now dead code since
  getTreeConfigForBiome always returns a config)
- Remove DEFAULT_TREE_CONFIG from TerrainSystem
- Add WindPine tree type using dead_06.glb for tundra biome
- Update BiomeResourceGenerator to read placement from biome config
  instead of global tree type registry

Made-with: Cursor
Resolved conflicts:
- stream-to-rtmp.ts: kept darwin channel fix, removed duplicate CAPTURE_MODE
- createClientWorld.ts: kept main's PhysX background load for stream mode

Made-with: Cursor
@claude

claude Bot commented Mar 12, 2026

Copy link
Copy Markdown

PR Review: Terrain Quad-Tree LOD, Biome Landscape System & Biome-Aware Tree Allocation

Nice work on this PR — the biome-aware tree placement pipeline, batched entity spawning, and tile unload cleanup are solid improvements. Here's a detailed review:


Code Quality & Design

Strengths:

  • TreeId enum replacing string literals is a great move for type safety and refactoring confidence.
  • Moving per-biome tree configs into TerrainBiomeTypes.ts is clean separation of concerns — biome data lives with biome definitions.
  • The suppressBroadcast option on spawnEntity is a well-designed escape hatch for batching.
  • Entity cleanup on tile unload (destroyEntity in onTerrainTileUnloaded) fixes what would have been a leak / duplicate-ID bug. Good catch.

Suggestions:

  1. useHighPriorityBatch is a public mutable field (ResourceSystem.ts). It has no validation and could be toggled at any time. Consider making it private with a setter, or at minimum readonly if it's meant to be set once at construction:

    readonly useHighPriorityBatch = true;
  2. Type assertions for network/entityManager (ResourceSystem.ts lines ~857, ~1014, ~1280) — the as { spawnEntity?: ...; } | null pattern is used in multiple places. Consider extracting a shared interface or using the actual system types to avoid drift between the cast shape and the real API.

  3. Reference equality for distribution comparison (BiomeResourceGenerator.ts):

    if (posConfig && posConfig.distribution !== tileDist) {

    This compares object references. It works today because getTreeConfigForBiome returns the same static config objects, but it's fragile — if configs are ever cloned or generated dynamically, this silently breaks. A biome ID comparison would be more robust.


Potential Bugs

  1. waterAffinity logic may be inverted (BiomeResourceGenerator.ts ~lines 287-291):

    if (rules.waterAffinity && rules.waterAffinity > 0) {
      const proximityLimit = rules.waterProximityHeight ?? 10;
      if (heightAboveWater > proximityLimit) {
        if (rng() < rules.waterAffinity) continue;  // skip
      }
    }

    When a tree is far from water (heightAboveWater > proximityLimit), it gets skipped with probability waterAffinity. This means with waterAffinity: 1.0, the tree never spawns far from water — that's correct. But with waterAffinity: 0.3 (coconut palm), only 30% of far-from-water positions are rejected, so 70% of coconut palms still spawn far from water. That seems like a weak affinity. Is this intentional? The naming suggests it should pull trees toward water more strongly. Consider:

    // Skip far-from-water positions with probability (1 - waterAffinity) inverted
    if (rng() > (1 - rules.waterAffinity)) continue;

    Or just document the current behavior clearly as "rejection probability when far from water."

  2. Missing batch size limitbatchedEntityData has no upper bound. If a tile generates an unusually large number of resources, a single HIGH-priority packet could be very large. Consider chunking at a reasonable limit (e.g., 50–100 entities per batch packet).


Performance Considerations

  1. HIGH priority bypasses bandwidth throttling — The PR sends all batched entity spawns at PacketPriority.HIGH, which bypasses per-connection bandwidth budgeting. During rapid exploration (player moving quickly through new tiles), multiple tiles may generate simultaneously, each flushing a HIGH-priority batch. This could spike bandwidth for clients on slower connections. Consider:

    • Using NORMAL priority but with a larger per-message budget allowance
    • Rate-limiting tile generation to max N concurrent tile spawns
    • At minimum, logging a warning if batch size exceeds a threshold
  2. getDominantBiome called per candidate tree position — Inside the tree placement loop (BiomeResourceGenerator.ts), ctx.getDominantBiome(worldX, worldZ) is called for every candidate position after Poisson-disk sampling. If getDominantBiome involves noise evaluation, this could be expensive for high-density tiles. The cost depends on the implementation, but worth profiling if tile generation feels slow.


Security

No security concerns — the changes are server-authoritative and don't expose new client-controlled inputs.


Test Coverage

  1. No tests included — This is a large functional change (batched entity networking, biome-aware placement rules, tile unload cleanup) with no test additions. Per the project's testing philosophy, these features should have Playwright tests that verify:
    • Entities appear correctly when entering a new biome tile
    • Entities are cleaned up when tiles unload (no entity leak)
    • Batch packets are received and processed by the client
    • Tree placement respects biome boundaries (no forest trees in canyon)

Minor / Nits

  1. The config.ts change (line 365) is just a formatting fix — fine, but could be in a separate commit to keep the diff focused.

  2. The Socket.ts whitespace changes (() => { }() => {}) are also formatting-only — same note.


Overall: Strong architecture changes with clean data-driven design. The batched entity spawn is a meaningful optimization. Main action items are the potential waterAffinity logic clarification (#4), adding a batch size cap (#5), and considering bandwidth impact of HIGH-priority batches during rapid tile generation (#6). Tests would give good confidence for the networking and tile lifecycle changes.

Add maxSlope to BiomeTreeConfig — estimates terrain gradient at each
candidate position using central differences and skips placement when
the slope exceeds the threshold. Defaults: Forest 1.5, Canyon 2.0,
Tundra 1.5.

Made-with: Cursor
@claude

claude Bot commented Mar 12, 2026

Copy link
Copy Markdown

PR Review: Terrain Quad-Tree LOD, Biome Landscape System & Biome-Aware Tree Allocation

Overall this is a well-structured PR that cleanly separates biome-specific tree configs from the core tree type registry, introduces batched entity networking to reduce per-entity overhead, and fixes an entity leak on tile unload. The PR description is excellent. Below are findings organized by severity.


Potential Bugs / Correctness

1. Batch broadcast goes to ALL clients, not spatially filtered
ResourceSystem (lines ~1014-1032) uses network.sendHighPriority("entitiesBatchAdded", batchedEntityData) which calls broadcastManager.sendToAll(). This sends every tile's full resource batch to every connected client, regardless of their distance from the tile. For a large map with many concurrent players, this is both wasted bandwidth and defeats the purpose of the bandwidth budget system. Consider using sendToNearby() (which already defaults to PacketPriority.HIGH) to spatially scope the broadcast to players near the generating tile.

2. onEntitiesBatchAdded missing special-case handling from onEntityAdded
In ClientNetwork.ts, onEntityAdded has important logic for position clamping (local player), spectator target detection, and camera setup. The new onEntitiesBatchAdded handler (lines 1426-1434) skips all of this and only calls world.entities.add() + applyPendingModifications(). While resource entities (trees) won't be players, there's no type guard to prevent a non-resource entity from being included in the batch. Consider either:

  • Adding a defensive check that skips entities requiring special handling, or
  • Documenting clearly that this packet is resource-only, or
  • Delegating to onEntityAdded per-item to preserve all side effects

3. Reference equality check for distribution objects may be fragile
In BiomeResourceGenerator.ts (line ~264):

if (posConfig && posConfig.distribution !== tileDist) {

This relies on reference equality. If getTreeConfigForBiome ever returns a copy/clone or if the biome config objects are reconstructed, this guard silently fails and redundantly re-computes treeTypes/totalWeight for every position. Not a bug today, but a subtle assumption worth a comment.


Performance Considerations

4. getDominantBiome called per candidate tree position in hot loop
In BiomeResourceGenerator.ts (line ~261), ctx.getDominantBiome(worldX, worldZ) is called for every candidate tree position within the tile. If getDominantBiome involves multi-octave noise sampling or other expensive computation, this could significantly increase tile generation time. Consider:

  • Caching the biome result on a coarser sub-grid (e.g., 8m cells) and reusing for nearby positions
  • Profiling to confirm this isn't a bottleneck in practice

5. Per-position distribution recomputation
When the position biome differs from the tile biome (lines ~264-272), Object.keys() and Object.values().reduce() are recomputed for every such position. Since there are only 3 biomes, these could be pre-computed once per biome and cached.


Code Quality

6. Inline type casts for network in ResourceSystem
In ResourceSystem.ts (lines ~1017-1020), the network object is cast with an inline structural type:

const network = this.world.network as {
  sendHighPriority?: (name: string, data: unknown) => void;
  send?: (name: string, data: unknown) => void;
} | null;

This pattern also appears for entityManager casts. Per the project convention of strong typing and preferring classes/interfaces, consider importing or defining a proper interface (e.g., NetworkWithBroadcast) to avoid inline structural casts that can drift from the real API.

7. useHighPriorityBatch as a public mutable field
ResourceSystem.useHighPriorityBatch = true (line ~178) is a runtime config knob exposed as a bare public property. If this is meant to be toggled, consider making it part of a config object or at minimum marking it readonly if it's not meant to change after init.

8. batchedEntityData typed as unknown[]
In ResourceSystem.ts (line ~860), batchedEntityData is unknown[] but it holds serialized entity data. Using EntityData[] (matching the client-side handler's type) would provide better type safety and documentation.


What Looks Good

  • TreeId enum replacing magic strings is a solid improvement — centralizes tree IDs and enables refactoring safety.
  • treeIdToSubType helper is clean and the enum constraint makes the replace("tree_", "") safe.
  • Entity leak fix on tile unload (destroyEntity call in onTerrainTileUnloaded) correctly addresses the duplicate-ID issue on tile revisit.
  • suppressBroadcast pattern in EntityManager.spawnEntity is a clean opt-in that doesn't break existing callers.
  • Per-biome tree placement rules (TreePlacementRules) with water affinity, height bounds, and slope rejection are well-designed and data-driven.
  • Slope rejection using central-difference gradient estimation is efficient (O(4) height samples per candidate).
  • Deterministic RNG preserved throughout — reproducible across runs.
  • The macOS "chromium""chrome" capture channel fix is a nice platform-specific edge case catch.

Minor / Nits

  • config.ts (line 365): Whitespace-only change (indentation alignment). Fine, but could have been a separate formatting commit.
  • Socket.ts: Whitespace-only cleanup (removing spaces inside empty arrow functions). Same note.

Summary

The biome system and tree placement architecture are well-designed and data-driven. The main concern is the broadcast-to-all for batched entities (#1 above) — this should be spatially scoped before merging to avoid unnecessary bandwidth on populated servers. The batch handler's missing special-case logic (#2) is worth addressing or explicitly documenting. The performance items (#4, #5) are worth profiling but not blocking.

…p in BiomeTreeConfig

Eliminates duplicate tree definitions by combining spawn weight and placement
rules into a unified TreeSpawnConfig per tree type per biome.

Made-with: Cursor
@claude

claude Bot commented Mar 12, 2026

Copy link
Copy Markdown

PR Review: Terrain Quad-Tree LOD, Biome Landscape System & Biome-Aware Tree Allocation

Overall this is a well-structured PR that cleanly separates biome-specific tree configuration from the core tree type registry. The batch entity spawning optimization and the per-position biome lookup for tree placement are solid improvements. Here are some specific observations:


Code Quality & Design

Strengths:

  • The TreeId enum and TreeSpawnConfig / TreePlacementRules interfaces are a great improvement over scattered string literals and flat spawnWeight values. Moving per-biome tree configs to TerrainBiomeTypes.ts is the right separation of concerns.
  • The suppressBroadcast option on spawnEntity() is a clean API addition that keeps the batching logic in the caller without polluting the entity manager.
  • Test updates are thorough — every test fixture was migrated from distribution to the new trees shape.

Minor items:

  • ResourceSystem.ts:860batchedEntityData is typed as unknown[]. Consider using EntityData[] (matching the ClientNetwork.onEntitiesBatchAdded parameter type) for stronger type safety, per the project's "NO any types" convention. unknown isn't any, but it loses the signal of what the array actually contains.
  • ResourceSystem.ts:1020–1033 — The network object is cast inline with as { sendHighPriority?: ...; send?: ... } | null. This pattern appears a couple of times now. Consider whether a shared interface (e.g. NetworkBroadcaster) could reduce these ad-hoc casts across the codebase.

Potential Bugs & Issues

  1. onEntitiesBatchAdded handler registration — The handler method is defined on ClientNetwork but I don't see explicit registration for the entitiesBatchAdded packet in this diff. If the framework auto-registers handlers by convention (on + PascalCase packet name), this should work. But if registration is manual, this handler will be silently ignored and batch packets will be dropped on the client. Worth double-checking that the wiring is in place.

  2. Tile unload entity destruction — In ResourceSystem.onTerrainTileUnloaded, the new code calls entityManager.destroyEntity(resource.id) before this.resources.delete(resourceId). If destroyEntity triggers any event listeners that reference the resource map synchronously, the resource entry still exists during the event — which is probably fine but is a subtlety to be aware of. The fix comment about duplicate-ID prevention on tile revisit is good context.

  3. posConfig.trees !== tileTreeMap identity check (BiomeResourceGenerator.ts:271) — This compares object references. If getTreeConfigForBiome() returns the same default config (FOREST_TREE_CONFIG) that the tile already has, this correctly skips re-computation. But if a future refactor causes getTreeConfigForBiome to return new objects (e.g. spreading/cloning), this identity check will silently break and recompute on every position. A comment noting the reference-equality assumption would help future maintainers.


Performance Considerations

  • Per-position biome lookup in generateTrees — Calling ctx.getDominantBiome(worldX, worldZ) for every candidate tree position could be expensive depending on the biome system's implementation. The code short-circuits when the result matches the tile-level map (the identity check), which is a good optimization for positions in the tile's interior. For tiles near biome boundaries, the Object.keys() + Object.values().reduce() recomputation per-position is worth monitoring. Consider caching the per-biome config maps if profiling shows this as a hot path.

  • Slope calculation (BiomeResourceGenerator.ts:252–259) — 4 height samples per tree candidate. This is standard for gradient estimation and should be fine at current densities (10–15 trees per tile). Good use of early continue to skip before the more expensive biome lookup.

  • Batch entity spawning — The HIGH priority bypass is a practical solution for rapid tile generation. The useHighPriorityBatch flag is a good escape hatch. Just be mindful that if many tiles generate simultaneously, HIGH priority batches could still saturate bandwidth. The flag's existence mitigates this well.


Security Concerns

No security issues identified. The changes are server-authoritative (entities spawned server-side, broadcast to clients). The onEntitiesBatchAdded client handler has a defensive Array.isArray(batch) check which is good.


Test Coverage

  • The BiomeResourceSpawning.test.ts updates properly migrate all fixture data to the new trees format.
  • Missing test coverage: No tests for the new placement rules (waterAffinity, avoidsWaterBelow, minHeight, maxHeight, slope rejection). These are non-trivial filtering paths in generateTrees that would benefit from targeted unit tests to prevent regressions.
  • Missing test coverage: No tests for onEntitiesBatchAdded on the client side or for the suppressBroadcast + batch send flow in ResourceSystem.
  • Missing test coverage: No tests for sendHighPriority in ServerNetwork.

Summary

Solid PR with clean architecture. The main things to verify before merge:

  1. Confirm entitiesBatchAdded handler is actually registered on the client (framework convention vs. manual wiring)
  2. Consider adding tests for the new tree placement rules (waterAffinity, height constraints, slope)
  3. The posConfig.trees !== tileTreeMap reference-equality check is subtle — add a comment

@lalalune lalalune changed the base branch from main to develop March 12, 2026 06:03
@lalalune lalalune merged commit cc498b2 into develop Mar 12, 2026
10 of 11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants