diff --git a/CHANGELOG.md b/CHANGELOG.md index 3644e30b..c8a83576 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,8 @@ between (`--json` for structured output). ### Fixed +- `pickModel` / `resolveModelPlan` warn once in dev when a `ModelPick.fallbackModel` is not in the asset catalog (a typo or an undeclared pack), instead of silently shipping a fallback that can never take over. + - Volumetric clouds no longer cut through distant buildings: the proxy dome writes per-fragment depth at the raymarch's first cloud hit, so geometry between the camera and the cloud slab occludes the clouds instead of the dome radius deciding. `STUDIO_STAGE_POST` ambient occlusion is softer (radius 1.1, intensity 1.5) so tower silhouettes stop streaking. - Separate SDK publication validation from external Games quality checks while retaining the full local and main-branch gates. diff --git a/packages/shell/src/render/resolveModel.test.ts b/packages/shell/src/render/resolveModel.test.ts index 5066b684..8269e26b 100644 --- a/packages/shell/src/render/resolveModel.test.ts +++ b/packages/shell/src/render/resolveModel.test.ts @@ -1,5 +1,6 @@ import { describe, expect, test } from "bun:test"; +import { resetWarnOnce } from "@jgengine/core/devtools/warnOnce"; import { createAssetCatalog } from "@jgengine/core/scene/assetCatalog"; import { @@ -176,6 +177,23 @@ describe("pickModel / resolveModelPlan", () => { expect(pickModel(assets, { model: "a/b", fallbackModel: "c/d" })).toBeUndefined(); }); + test("warns once when fallbackModel names an id the catalog does not have", () => { + resetWarnOnce(); + const warn = console.warn; + const messages: string[] = []; + console.warn = (text: string) => { + messages.push(text); + }; + try { + pickModel(assets, { model: "kaykit-adventurers/Rogue", fallbackModel: "dungeon/chest" }); + pickModel(assets, { model: "kaykit-adventurers/Rogue", fallbackModel: "dungeon/chest" }); + pickModel(assets, { model: "kaykit-adventurers/Rogue", fallbackModel: "quaternius-modular-scifi/astronautA" }); + } finally { + console.warn = warn; + } + expect(messages.filter((m) => m.includes('fallbackModel "dungeon/chest"'))).toHaveLength(1); + }); + test("resolveModelPlan omits unresolved keys", () => { expect( resolveModelPlan(assets, { diff --git a/packages/shell/src/render/resolveModel.ts b/packages/shell/src/render/resolveModel.ts index 2d78c2d5..8881f119 100644 --- a/packages/shell/src/render/resolveModel.ts +++ b/packages/shell/src/render/resolveModel.ts @@ -1,6 +1,7 @@ import type { AssetCatalog, ModelAssetRef } from "@jgengine/core/scene/assetCatalog"; import type { ModelConfig } from "@jgengine/core/game/playableGame"; import type { GameContextModels } from "@jgengine/core/runtime/gameContext"; +import { warnOnce } from "@jgengine/core/devtools/warnOnce"; /** * The `ModelConfig` a resolved catalog ref renders as. Stamps `animation: "auto"` so any rigged @@ -105,6 +106,12 @@ export type ModelPick = { * @internal */ export function pickModel(assets: AssetCatalog, pick: ModelPick): ModelConfig | undefined { + if (pick.fallbackModel !== undefined && !assets.has(pick.fallbackModel)) { + warnOnce( + `pickModel:fallback:${pick.fallbackModel}`, + `[jgengine] fallbackModel "${pick.fallbackModel}" is not in the asset catalog, so it can never stand in for "${pick.model ?? "(none)"}" — fix the id or declare its pack in the catalog sources.`, + ); + } for (const id of [pick.model, pick.fallbackModel]) { if (id === undefined) continue; const resolved = tryResolveCatalogModel(id, assets);