Phase 13: real non-cube block models via blockstate/model JSON resolution

Adds worker/src/models.rs (parent-chain blockstate/model resolution,
texture-variable substitution reusing Phase 12's atlas keys), routes
non-cube blocks through new per-element mesh emission in mesh.rs while
leaving full-cube blocks on the existing cube mesher, and threads a
per-server ModelContext (vanilla worker-wide + modded per-job) through
main.rs. Modded model JSON is stored in a new block_models Postgres
table and read alongside the existing (previously write-only)
block_registry table. Fixes a pre-existing face-culling bug as a side
effect of excluding non-cube voxels from the cube mesher's input.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015tKdPZt78zbPUZMXWzKEKt
This commit is contained in:
2026-08-10 01:19:13 +02:00
parent 6ad8051eae
commit a7b69bd038
13 changed files with 1069 additions and 15 deletions
+54
View File
@@ -163,6 +163,60 @@ blockstate/model JSON parsing (`BlockAssetExtractor`'s texture matching is still
convention guess, not a real model resolution) — renumbered to **Phase 13** once the atlas/UV
scope above turned out to be its own full phase; Thaumcraft remains the named stress test for it.
### Real (non-cube) block models (Phase 13)
`worker/src/models.rs` is a real blockstate/model JSON resolver: given a block's registry name, it
picks a variant, walks the model's `parent` chain (merging `textures` maps as it goes, child
overrides win), and resolves each element's face textures down to the same atlas-lookup key
`atlas.rs` already uses (Phase 12) — so a resolved non-cube model's faces are textured with zero
atlas-side changes. `ResolvedModel::is_full_cube()` tells `mesh.rs` whether a block still belongs
on the existing cube greedy-mesher (untouched — lower risk, and re-rendering a plain cube through
the new per-element path would be pure overhead) or needs its own per-element geometry.
Two model sources, deliberately split:
- **Vanilla**: extracted from the same Mojang client jar Phase 11/12 already download (cached to
disk under its own `vanilla-<version>-models.json`, a third instance of the same accepted
"duplicate download, cached after first build" tradeoff as the palette/atlas), worker-wide.
- **Modded**: shipped by the mod over a new `block_models` WS message (mirrors Phase 11's
`block_textures`, batched the same way) and stored per-server in a new `block_models` Postgres
table (`server_id, kind, name, json`, `kind` distinguishing a blockstate entry from a model entry
since both are keyed by resource-location-shaped strings that could otherwise collide) — fetched
fresh per chunk-job by the worker, alongside the already-existing `block_registry` table (Phase
11 wrote it but nothing ever read it back until now). A modded model's `parent` can point at a
vanilla base model (e.g. `"minecraft:block/cross"`) via `ModelRegistry::resolve`'s `fallback`
parameter — common in practice, since plenty of modded blocks just extend a vanilla shape.
**Deliberately out of scope**, documented in `models.rs`'s doc comments rather than silently
dropped:
- **No `multipart` blockstates** (fences, walls, redstone wire, glass panes) — there's no way to
know a block's neighbor-dependent connection state from this project's raw block-id/meta data
model, so a multipart-only blockstate resolves to `None` and the block falls back to a flat cube,
same as pre-Phase-13.
- **No property-based variant selection** — chunk data here only ever carries a numeric `meta`
(legacy) or a truncated packed state id (modern, per Phase 10), never named property strings, so
`resolve()` always picks a deterministic representative variant (the `""` key if present, else
alphabetically first) rather than the "correct" one for a given block's actual state.
- **No per-face UV rectangle or per-variant rotation** — element geometry (`from`/`to`) is real,
but face texturing reuses the same tile-relative "UV span in block units" scheme the cube mesher
already uses, not the model's literal declared UV rect, to avoid needing per-pixel atlas
remapping/a more complex shader.
- Weighted multi-model variant lists always take the first entry (weights ignored); `meta` is never
consulted for modded block resolution, since `block_registry` only carries `block_id -> name`
(no per-state granularity) — a real, pre-existing schema constraint, not new to this phase.
A genuine pre-existing rendering bug got fixed as a side effect: `compute_face_masks` treated *any*
non-zero block as solid for neighbor face-culling, so a torch (or any non-cube block) sitting next
to a solid block incorrectly culled that solid block's adjacent face. Excluding non-cube-resolved
voxels from the cube mesher's input array (needed anyway, for Phase 13's own correctness) fixes
this for free — see `mesh.rs`'s `a_non_cube_neighbor_no_longer_incorrectly_culls_an_adjacent_solid_faces`
test.
Verified via real `cargo build`/`cargo test --lib` (worker, 65/65 passing, up from 52) and a real
`./gradlew :forge-1_12_2:compileJava` against the actual legacy ForgeGradle toolchain for the mod
side. `api`'s new `models.test.ts` (mirrors `textures.test.ts`'s pattern) was written but **not run
against a live Postgres** — Docker isn't available in this dev environment, the same honestly-
documented gap Phase 11 already carries; `bunx tsc --noEmit` is clean.
## Running
```