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
+62
View File
@@ -0,0 +1,62 @@
import { describe, test, expect, beforeAll, afterAll } from "bun:test";
import { and, eq } from "drizzle-orm";
import { db } from "./db/client";
import { blockModels } from "./db/schema";
import { storeBlockModels } from "./models";
import { createTestServer, deleteTestServer } from "./test-helpers";
describe("storeBlockModels", () => {
let server: { id: string };
beforeAll(async () => {
server = await createTestServer("block-models");
});
afterAll(async () => {
await deleteTestServer(server.id);
});
test("upserts blockstate and model entries, keyed by (serverId, kind, name)", async () => {
await storeBlockModels(server.id, [
{ kind: "blockstate", name: "thaumcraft:blockcustomplant", json: '{"variants":{}}' },
{ kind: "model", name: "thaumcraft:block/customplant", json: '{"elements":[]}' },
]);
const rows = await db.select().from(blockModels).where(eq(blockModels.serverId, server.id));
expect(rows.length).toBe(2);
expect(rows.find((r) => r.kind === "blockstate")?.name).toBe("thaumcraft:blockcustomplant");
expect(rows.find((r) => r.kind === "model")?.json).toBe('{"elements":[]}');
});
test("a blockstate and a model can share the same name without colliding (kind is part of the key)", async () => {
await storeBlockModels(server.id, [
{ kind: "blockstate", name: "thaumcraft:foo", json: '{"a":1}' },
{ kind: "model", name: "thaumcraft:foo", json: '{"b":2}' },
]);
const rows = await db
.select()
.from(blockModels)
.where(and(eq(blockModels.serverId, server.id), eq(blockModels.name, "thaumcraft:foo")));
expect(rows.length).toBe(2);
});
test("re-sending the same (kind, name) with new JSON overwrites, not duplicates", async () => {
await storeBlockModels(server.id, [{ kind: "model", name: "thaumcraft:block/customplant", json: '{"v":1}' }]);
await storeBlockModels(server.id, [{ kind: "model", name: "thaumcraft:block/customplant", json: '{"v":2}' }]);
const rows = await db
.select()
.from(blockModels)
.where(
and(
eq(blockModels.serverId, server.id),
eq(blockModels.kind, "model"),
eq(blockModels.name, "thaumcraft:block/customplant"),
),
);
expect(rows.length).toBe(1);
expect(rows[0]!.json).toBe('{"v":2}');
});
test("an empty list is a no-op, not an error", async () => {
await expect(storeBlockModels(server.id, [])).resolves.toBeUndefined();
});
});