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:
@@ -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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user