@jgengine/assets 0.10.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +39 -2
- package/README.md +103 -50
- package/dist/aliases.d.ts +5 -0
- package/dist/aliases.js +28 -9
- package/dist/catalogs/build.d.ts +27 -0
- package/dist/catalogs/build.js +28 -2
- package/dist/catalogs/buildSprites.d.ts +10 -0
- package/dist/catalogs/buildSprites.js +15 -0
- package/dist/catalogs/starter.d.ts +5 -0
- package/dist/catalogs/starter.js +10 -1
- package/dist/cli/paths.d.ts +9 -1
- package/dist/cli/paths.js +11 -1
- package/dist/cli/pull.d.ts +12 -0
- package/dist/cli/pull.js +216 -25
- package/dist/collisionMeshAssets.d.ts +2 -0
- package/dist/collisionMeshAssets.js +6 -0
- package/dist/dims.d.ts +7 -0
- package/dist/dims.js +154 -0
- package/dist/download.d.ts +66 -7
- package/dist/download.js +264 -18
- package/dist/find.d.ts +20 -1
- package/dist/find.js +52 -2
- package/dist/generated/index.js +18 -6
- package/dist/generated/kaykit-adventurers.json +706 -0
- package/dist/generated/kaykit-city-builder.json +904 -0
- package/dist/generated/kaykit-dungeon.json +5002 -0
- package/dist/generated/kaykit-furniture.json +1168 -0
- package/dist/generated/kaykit-skeletons.json +410 -0
- package/dist/generated/kaykit-space-base.json +1199 -0
- package/dist/generated/quaternius-medieval-village.json +3698 -0
- package/dist/generated/quaternius-modular-scifi.json +4013 -0
- package/dist/generated/quaternius-stylized-nature.json +1498 -0
- package/dist/generated-sprites/index.d.ts +5 -0
- package/dist/generated-sprites/index.js +4 -0
- package/dist/index.d.ts +9 -2
- package/dist/index.js +9 -2
- package/dist/indexGen.d.ts +2 -2
- package/dist/indexGen.js +34 -10
- package/dist/manifest.d.ts +16 -1
- package/dist/materials.d.ts +61 -0
- package/dist/materials.js +98 -0
- package/dist/packs/starter.d.ts +46 -0
- package/dist/packs/starter.js +248 -0
- package/dist/provisioning.d.ts +88 -0
- package/dist/provisioning.js +74 -0
- package/dist/registry-catalog.json +35 -0
- package/dist/snippet.d.ts +4 -0
- package/dist/snippet.js +27 -0
- package/dist/sources/ambientcg.d.ts +5 -0
- package/dist/sources/ambientcg.js +64 -0
- package/dist/sources/gameicons.d.ts +10 -0
- package/dist/sources/gameicons.js +21 -0
- package/dist/sources/index.d.ts +9 -2
- package/dist/sources/index.js +11 -3
- package/dist/sources/kaykit.js +55 -6
- package/dist/sources/quaternius.js +60 -9
- package/dist/spriteIndexGen.d.ts +17 -0
- package/dist/spriteIndexGen.js +108 -0
- package/dist/verify.d.ts +1 -0
- package/dist/verify.js +38 -1
- package/package.json +4 -4
- package/skills/jgengine/SKILL.md +54 -0
- package/skills/jgengine/api.md +527 -0
- package/skills/jgengine/capabilities.md +131 -0
- package/skills/jgengine/recipes/minimal-game.md +88 -0
- package/skills/jgengine-assets/SKILL.md +45 -0
- package/skills/jgengine-assets/api.md +282 -0
- package/skills/jgengine-assets/capabilities.md +13 -0
- package/dist/generated/kenney-blaster.json +0 -362
- package/dist/generated/kenney-nature.json +0 -3292
- package/dist/generated/kenney-space.json +0 -1532
- package/dist/sources/kenney.d.ts +0 -2
- package/dist/sources/kenney.js +0 -36
- package/llms.txt +0 -1010
package/dist/cli/pull.js
CHANGED
|
@@ -1,26 +1,34 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { execFileSync } from "node:child_process";
|
|
2
3
|
import { copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
4
|
import { basename, dirname, join, resolve } from "node:path";
|
|
4
5
|
import { fileURLToPath } from "node:url";
|
|
5
|
-
import { downloadPackArchive, extractGlbs,
|
|
6
|
+
import { downloadPackArchive, extractGlbs, extractSpriteFiles } from "../download.js";
|
|
6
7
|
import { rankAssets } from "../find.js";
|
|
7
8
|
import { generatedIndex } from "../generated/index.js";
|
|
9
|
+
import { generatedSpriteIndex } from "../generated-sprites/index.js";
|
|
8
10
|
import { reindex } from "../indexGen.js";
|
|
9
11
|
import { isScrapeDownload } from "../manifest.js";
|
|
12
|
+
import { extractMaterialMaps } from "../materials.js";
|
|
13
|
+
import { validateAssetReferences } from "../provisioning.js";
|
|
10
14
|
import { registryCatalog } from "../registry.js";
|
|
11
|
-
import { componentWiringSnippet, iconWiringSnippet, modelWiringSnippet } from "../snippet.js";
|
|
12
|
-
import { sourceById } from "../sources/index.js";
|
|
15
|
+
import { componentWiringSnippet, iconWiringSnippet, materialWiringSnippet, modelWiringSnippet, spriteWiringSnippet, } from "../snippet.js";
|
|
16
|
+
import { materialSources, sourceById, spriteSources } from "../sources/index.js";
|
|
17
|
+
import { reindexSprites } from "../spriteIndexGen.js";
|
|
13
18
|
import { verifyManifest } from "../verify.js";
|
|
14
|
-
import { resolveGeneratedDir, resolvePackageRoot, resolvePackageTreeRoot } from "./paths.js";
|
|
19
|
+
import { resolveGeneratedDir, resolveGeneratedSpritesDir, resolvePackageRoot, resolvePackageTreeRoot } from "./paths.js";
|
|
15
20
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
16
21
|
const packageTreeRoot = resolvePackageTreeRoot(here);
|
|
17
22
|
const pkgRoot = resolvePackageRoot(here);
|
|
18
23
|
/** Sibling of `cli/` under `src/` (dev) or `dist/` (published) so reindex writes the tree consumers import. */
|
|
19
24
|
export const generatedDir = resolveGeneratedDir(here);
|
|
25
|
+
/** Sprite-pack counterpart of `generatedDir`. */
|
|
26
|
+
export const generatedSpritesDir = resolveGeneratedSpritesDir(here);
|
|
20
27
|
export { resolveGeneratedDir } from "./paths.js";
|
|
21
28
|
const singlesJson = join(packageTreeRoot, "singles.json");
|
|
22
29
|
const localDir = join(pkgRoot, "local");
|
|
23
30
|
const CDN_BASE = "https://cdn.jsdelivr.net/gh/Noisemaker111/jgengine@main/packages/assets/local";
|
|
31
|
+
/** @internal */
|
|
24
32
|
export function flag(argv, name) {
|
|
25
33
|
const index = argv.indexOf(`--${name}`);
|
|
26
34
|
return index >= 0 ? argv[index + 1] : undefined;
|
|
@@ -29,6 +37,31 @@ function fail(message) {
|
|
|
29
37
|
console.error(`error: ${message}`);
|
|
30
38
|
process.exit(1);
|
|
31
39
|
}
|
|
40
|
+
/**
|
|
41
|
+
* The runtime's own fetch, with a curl fallback for hosts it cannot reach —
|
|
42
|
+
* some proxied sandboxes tear down bun's TLS stream to GitHub's release-asset
|
|
43
|
+
* host while curl (which honors SSL_CERT_FILE) gets through fine.
|
|
44
|
+
*/
|
|
45
|
+
export const cliFetch = (async (input, init) => {
|
|
46
|
+
try {
|
|
47
|
+
return await fetch(input, init);
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
|
|
51
|
+
if (init?.method !== undefined && init.method !== "GET")
|
|
52
|
+
throw error;
|
|
53
|
+
try {
|
|
54
|
+
const bytes = execFileSync("curl", ["-fsSL", "--max-time", "600", url], {
|
|
55
|
+
maxBuffer: 1024 * 1024 * 1024,
|
|
56
|
+
});
|
|
57
|
+
return new Response(new Uint8Array(bytes));
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
/** @internal */
|
|
32
65
|
export function describeNetworkFailure(error) {
|
|
33
66
|
const chain = [];
|
|
34
67
|
let current = error;
|
|
@@ -46,6 +79,31 @@ function cmdList(argv) {
|
|
|
46
79
|
const category = flag(argv, "category");
|
|
47
80
|
const source = flag(argv, "source");
|
|
48
81
|
const limit = Number(flag(argv, "limit") ?? "50");
|
|
82
|
+
if (flag(argv, "kind") === "material") {
|
|
83
|
+
const rows = materialSources.filter((entry) => category === undefined || entry.categories.includes(category));
|
|
84
|
+
for (const entry of rows.slice(0, limit)) {
|
|
85
|
+
console.log(`${entry.id}\t[${entry.categories.join(",")}]\t${entry.license}`);
|
|
86
|
+
}
|
|
87
|
+
console.log(`— ${Math.min(rows.length, limit)} of ${rows.length} materials`);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (flag(argv, "kind") === "sprite") {
|
|
91
|
+
const rows = generatedSpriteIndex.filter((entry) => (category === undefined || entry.categories.includes(category)) &&
|
|
92
|
+
(source === undefined || entry.source === source));
|
|
93
|
+
for (const entry of rows.slice(0, limit)) {
|
|
94
|
+
console.log(`${entry.id}\t[${entry.categories.join(",")}]\t${entry.file}`);
|
|
95
|
+
}
|
|
96
|
+
console.log(`— ${Math.min(rows.length, limit)} of ${rows.length} sprite/icon files`);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (flag(argv, "kind") === "spritePack") {
|
|
100
|
+
const rows = spriteSources.filter((entry) => category === undefined || entry.categories.includes(category));
|
|
101
|
+
for (const entry of rows.slice(0, limit)) {
|
|
102
|
+
console.log(`${entry.id}\t[${entry.categories.join(",")}]\t${entry.license}`);
|
|
103
|
+
}
|
|
104
|
+
console.log(`— ${Math.min(rows.length, limit)} of ${rows.length} sprite/icon packs`);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
49
107
|
const rows = generatedIndex.filter((entry) => (category === undefined || entry.categories.includes(category)) &&
|
|
50
108
|
(source === undefined || entry.source === source));
|
|
51
109
|
for (const entry of rows.slice(0, limit)) {
|
|
@@ -65,9 +123,11 @@ function cmdSearch(argv) {
|
|
|
65
123
|
}
|
|
66
124
|
console.log(`— ${Math.min(rows.length, limit)} of ${rows.length} matches for "${term}"`);
|
|
67
125
|
}
|
|
126
|
+
/** @internal */
|
|
68
127
|
export function isPopulated(dir) {
|
|
69
128
|
return existsSync(dir) && readdirSync(dir).length > 0;
|
|
70
129
|
}
|
|
130
|
+
/** @internal */
|
|
71
131
|
export async function cmdPull(argv) {
|
|
72
132
|
const sourceId = argv[0];
|
|
73
133
|
if (sourceId === undefined) {
|
|
@@ -77,7 +137,7 @@ export async function cmdPull(argv) {
|
|
|
77
137
|
if (source === undefined)
|
|
78
138
|
fail(`unknown source: ${sourceId}`);
|
|
79
139
|
const outRoot = resolve(flag(argv, "dir") ?? "public");
|
|
80
|
-
const outDir = join(outRoot,
|
|
140
|
+
const outDir = join(outRoot, packSubdir(source), sourceId);
|
|
81
141
|
const offline = argv.includes("--offline");
|
|
82
142
|
const mirrorBase = flag(argv, "mirror") ?? process.env.JGENGINE_ASSETS_MIRROR;
|
|
83
143
|
if (offline) {
|
|
@@ -89,28 +149,62 @@ export async function cmdPull(argv) {
|
|
|
89
149
|
return;
|
|
90
150
|
}
|
|
91
151
|
const result = await fetchPackInto(source, outRoot, { mirrorBase });
|
|
152
|
+
console.log(describeFetchResult(source, result));
|
|
153
|
+
}
|
|
154
|
+
function packSubdir(source) {
|
|
155
|
+
if (source.kind === "material")
|
|
156
|
+
return "materials";
|
|
157
|
+
if (source.kind === "sprite")
|
|
158
|
+
return "sprites";
|
|
159
|
+
return "models";
|
|
160
|
+
}
|
|
161
|
+
function describeFetchResult(source, result) {
|
|
162
|
+
if (source.kind === "material") {
|
|
163
|
+
return `pulled ${result.textures} material map(s) -> ${result.outDir}`;
|
|
164
|
+
}
|
|
165
|
+
if (source.kind === "sprite") {
|
|
166
|
+
return `pulled ${result.models} sprite/icon file(s) -> ${result.outDir}`;
|
|
167
|
+
}
|
|
92
168
|
const textureNote = result.textures > 0 ? ` + ${result.textures} texture(s)` : "";
|
|
93
|
-
|
|
169
|
+
return `pulled ${result.models} models${textureNote} -> ${result.outDir}`;
|
|
94
170
|
}
|
|
95
171
|
async function fetchPackInto(source, outRoot, options) {
|
|
96
|
-
const outDir = join(outRoot,
|
|
172
|
+
const outDir = join(outRoot, packSubdir(source), source.id);
|
|
97
173
|
console.log(`resolving ${source.id}${isScrapeDownload(source.download) ? " (scrape)" : ""}` +
|
|
98
174
|
`${options.mirrorBase !== undefined ? ` [mirror override: ${options.mirrorBase}]` : ""}…`);
|
|
99
|
-
const { archive, url, attempted } = await downloadPackArchive(source, {
|
|
175
|
+
const { archive, url, attempted } = await downloadPackArchive(source, {
|
|
176
|
+
mirrorBase: options.mirrorBase,
|
|
177
|
+
fetchImpl: cliFetch,
|
|
178
|
+
});
|
|
100
179
|
console.log(`downloaded ${url}${attempted.length > 1 ? ` (after ${attempted.length - 1} failed attempt(s))` : ""}`);
|
|
101
|
-
|
|
102
|
-
|
|
180
|
+
if (source.kind === "material") {
|
|
181
|
+
const maps = extractMaterialMaps(archive);
|
|
182
|
+
if (maps.every((map) => map.role !== "color")) {
|
|
183
|
+
fail(`no color map found in ${source.id} archive`);
|
|
184
|
+
}
|
|
185
|
+
mkdirSync(outDir, { recursive: true });
|
|
186
|
+
for (const map of maps)
|
|
187
|
+
writeFileSync(join(outDir, map.file), map.bytes);
|
|
188
|
+
return { outDir, models: 0, textures: maps.length, url };
|
|
189
|
+
}
|
|
190
|
+
if (source.kind === "sprite") {
|
|
191
|
+
const files = extractSpriteFiles(archive);
|
|
192
|
+
if (files.length === 0)
|
|
193
|
+
fail(`no .svg/.png files found in ${source.id} archive`);
|
|
194
|
+
mkdirSync(outDir, { recursive: true });
|
|
195
|
+
for (const file of files)
|
|
196
|
+
writeFileSync(join(outDir, file.file), file.bytes);
|
|
197
|
+
return { outDir, models: files.length, textures: 0, url };
|
|
198
|
+
}
|
|
199
|
+
const { models, images } = extractGlbs(archive);
|
|
200
|
+
if (models.length === 0)
|
|
103
201
|
fail(`no .glb files found in ${source.id} archive`);
|
|
104
202
|
mkdirSync(outDir, { recursive: true });
|
|
105
|
-
for (const glb of
|
|
203
|
+
for (const glb of models)
|
|
106
204
|
writeFileSync(join(outDir, glb.file), glb.bytes);
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
mkdirSync(dirname(dest), { recursive: true });
|
|
111
|
-
writeFileSync(dest, texture.bytes);
|
|
112
|
-
}
|
|
113
|
-
return { outDir, models: glbs.length, textures: textures.length, url };
|
|
205
|
+
for (const image of images)
|
|
206
|
+
writeFileSync(join(outDir, image.file), image.bytes);
|
|
207
|
+
return { outDir, models: models.length, textures: images.length, url };
|
|
114
208
|
}
|
|
115
209
|
function readSingles() {
|
|
116
210
|
if (!existsSync(singlesJson))
|
|
@@ -157,17 +251,31 @@ function cmdRegisterSingle(argv) {
|
|
|
157
251
|
writeSingles(singles);
|
|
158
252
|
console.log(`added single ${id} -> ${url}${isUrl ? " (0 bytes stored)" : " (copied to local/)"}`);
|
|
159
253
|
}
|
|
160
|
-
const KINDS = [
|
|
254
|
+
const KINDS = [
|
|
255
|
+
"model",
|
|
256
|
+
"pack",
|
|
257
|
+
"material",
|
|
258
|
+
"component",
|
|
259
|
+
"icon",
|
|
260
|
+
"sprite",
|
|
261
|
+
"spritePack",
|
|
262
|
+
];
|
|
161
263
|
function describeMatch(match) {
|
|
162
264
|
switch (match.kind) {
|
|
163
265
|
case "model":
|
|
164
266
|
return `[model] ${match.id}${match.via === "alias" ? " (alias)" : match.via === "single" ? " (single)" : ""}`;
|
|
165
267
|
case "pack":
|
|
166
268
|
return `[pack] ${match.source} — ${match.title}`;
|
|
269
|
+
case "material":
|
|
270
|
+
return `[material] ${match.id} — ${match.title}`;
|
|
167
271
|
case "component":
|
|
168
272
|
return `[component] ${match.name} — ${match.title}`;
|
|
169
273
|
case "icon":
|
|
170
274
|
return `[icon] ${match.name}`;
|
|
275
|
+
case "sprite":
|
|
276
|
+
return `[sprite] ${match.id}`;
|
|
277
|
+
case "spritePack":
|
|
278
|
+
return `[spritePack] ${match.source} — ${match.title}`;
|
|
171
279
|
}
|
|
172
280
|
}
|
|
173
281
|
async function performAdd(match, argv) {
|
|
@@ -184,19 +292,58 @@ async function performAdd(match, argv) {
|
|
|
184
292
|
console.log(iconWiringSnippet(match.name));
|
|
185
293
|
return;
|
|
186
294
|
}
|
|
295
|
+
const outRoot = resolve(flag(argv, "dir") ?? "public");
|
|
296
|
+
const mirrorBase = flag(argv, "mirror") ?? process.env.JGENGINE_ASSETS_MIRROR;
|
|
297
|
+
if (match.kind === "material") {
|
|
298
|
+
const source = sourceById.get(match.id);
|
|
299
|
+
if (source === undefined)
|
|
300
|
+
fail(`match resolves to unknown source: ${match.id}`);
|
|
301
|
+
const outDir = join(outRoot, "materials", match.id);
|
|
302
|
+
if (isPopulated(outDir)) {
|
|
303
|
+
console.log(`${match.id} already present in ${outDir}`);
|
|
304
|
+
}
|
|
305
|
+
else {
|
|
306
|
+
const result = await fetchPackInto(source, outRoot, { mirrorBase });
|
|
307
|
+
console.log(describeFetchResult(source, result));
|
|
308
|
+
}
|
|
309
|
+
console.log(`\nwire it in:\n`);
|
|
310
|
+
console.log(materialWiringSnippet(match.id));
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
if (match.kind === "sprite" || match.kind === "spritePack") {
|
|
314
|
+
const source = sourceById.get(match.source);
|
|
315
|
+
if (source === undefined)
|
|
316
|
+
fail(`match resolves to unknown source: ${match.source}`);
|
|
317
|
+
const outDir = join(outRoot, "sprites", match.source);
|
|
318
|
+
if (isPopulated(outDir)) {
|
|
319
|
+
console.log(`${match.source} already present in ${outDir}`);
|
|
320
|
+
}
|
|
321
|
+
else {
|
|
322
|
+
const result = await fetchPackInto(source, outRoot, { mirrorBase });
|
|
323
|
+
console.log(describeFetchResult(source, result));
|
|
324
|
+
}
|
|
325
|
+
const result = reindexSprites(join(outRoot, "sprites"), generatedSpritesDir);
|
|
326
|
+
console.log(`reindexed ${result.total} entries -> ${generatedSpritesDir}`);
|
|
327
|
+
if (match.kind === "sprite") {
|
|
328
|
+
console.log(`\nwire it in:\n`);
|
|
329
|
+
console.log(spriteWiringSnippet(match.id));
|
|
330
|
+
}
|
|
331
|
+
else {
|
|
332
|
+
console.log(`\n${match.title}: browse ids with \`assets list --kind sprite --source ${match.source}\`, then wire like:\n`);
|
|
333
|
+
console.log(spriteWiringSnippet(`${match.source}/<icon>`));
|
|
334
|
+
}
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
187
337
|
const source = sourceById.get(match.source);
|
|
188
338
|
if (source === undefined)
|
|
189
339
|
fail(`match resolves to unknown source: ${match.source}`);
|
|
190
|
-
const outRoot = resolve(flag(argv, "dir") ?? "public");
|
|
191
340
|
const outDir = join(outRoot, "models", match.source);
|
|
192
|
-
const mirrorBase = flag(argv, "mirror") ?? process.env.JGENGINE_ASSETS_MIRROR;
|
|
193
341
|
if (isPopulated(outDir)) {
|
|
194
342
|
console.log(`${match.source} already present in ${outDir}`);
|
|
195
343
|
}
|
|
196
344
|
else {
|
|
197
345
|
const result = await fetchPackInto(source, outRoot, { mirrorBase });
|
|
198
|
-
|
|
199
|
-
console.log(`pulled ${result.models} models${textureNote} -> ${result.outDir}`);
|
|
346
|
+
console.log(describeFetchResult(source, result));
|
|
200
347
|
}
|
|
201
348
|
const result = reindex(join(outRoot, "models"), generatedDir);
|
|
202
349
|
console.log(`reindexed ${result.total} entries -> ${generatedDir}`);
|
|
@@ -212,7 +359,7 @@ async function performAdd(match, argv) {
|
|
|
212
359
|
async function cmdAdd(argv) {
|
|
213
360
|
const query = argv[0];
|
|
214
361
|
if (query === undefined) {
|
|
215
|
-
fail("usage: add <query> [--kind model|pack|component|icon] [--dir <dir>] [--mirror <baseUrl>] [--json]\n" +
|
|
362
|
+
fail("usage: add <query> [--kind model|pack|material|component|icon|sprite|spritePack] [--dir <dir>] [--mirror <baseUrl>] [--json]\n" +
|
|
216
363
|
" add <path|url> --license <l> [--category <c>] (register a one-off single into the shipped index)");
|
|
217
364
|
}
|
|
218
365
|
// Back-compat: the old `add` registered a one-off single, always with --license.
|
|
@@ -255,6 +402,15 @@ function cmdReindex(argv) {
|
|
|
255
402
|
console.log(` ${row.source}: ${row.count}`);
|
|
256
403
|
console.log(`reindexed ${result.total} entries -> ${generatedDir}`);
|
|
257
404
|
}
|
|
405
|
+
function cmdReindexSprites(argv) {
|
|
406
|
+
const spritesDir = resolve(argv[0] ?? join("public", "sprites"));
|
|
407
|
+
if (!existsSync(spritesDir))
|
|
408
|
+
fail(`sprites dir not found: ${spritesDir}`);
|
|
409
|
+
const result = reindexSprites(spritesDir, generatedSpritesDir);
|
|
410
|
+
for (const row of result.perSource)
|
|
411
|
+
console.log(` ${row.source}: ${row.count}`);
|
|
412
|
+
console.log(`reindexed ${result.total} entries -> ${generatedSpritesDir}`);
|
|
413
|
+
}
|
|
258
414
|
function cmdVerify() {
|
|
259
415
|
const result = verifyManifest();
|
|
260
416
|
if (result.ok) {
|
|
@@ -265,6 +421,35 @@ function cmdVerify() {
|
|
|
265
421
|
console.error(` ✗ ${error}`);
|
|
266
422
|
fail(`verify failed with ${result.errors.length} problem(s)`);
|
|
267
423
|
}
|
|
424
|
+
/**
|
|
425
|
+
* Resolve each id against the provisioning contract and print its owner —
|
|
426
|
+
* `committed`, `provisioned` (with the `assets pull <source>` step), or
|
|
427
|
+
* `dangling`. Exits non-zero if any id is dangling, so a clean-clone gate can
|
|
428
|
+
* feed it logical ids and fail on references no source or single owns.
|
|
429
|
+
*/
|
|
430
|
+
function cmdProvenance(argv) {
|
|
431
|
+
const ids = argv.filter((arg) => !arg.startsWith("--"));
|
|
432
|
+
if (ids.length === 0)
|
|
433
|
+
fail("usage: assets provenance <id> [<id>...]");
|
|
434
|
+
const references = ids.map((id) => ({ consumer: "cli", id }));
|
|
435
|
+
const result = validateAssetReferences(references);
|
|
436
|
+
for (const row of result.results) {
|
|
437
|
+
const p = row.provenance;
|
|
438
|
+
const detail = p.kind === "provisioned"
|
|
439
|
+
? `${p.resolvedPath} (${p.provisioningStep})`
|
|
440
|
+
: p.kind === "committed"
|
|
441
|
+
? `${p.resolvedPath}`
|
|
442
|
+
: "no owner";
|
|
443
|
+
const mark = row.ok ? "✓" : "✗";
|
|
444
|
+
console.log(` ${mark} ${p.id}: ${p.kind} — ${detail}`);
|
|
445
|
+
}
|
|
446
|
+
if (result.provisioning.length > 0) {
|
|
447
|
+
console.log(`provisioning steps: ${result.provisioning.join(", ")}`);
|
|
448
|
+
}
|
|
449
|
+
if (!result.ok)
|
|
450
|
+
fail(`provenance: ${result.errors.length} dangling reference(s)`);
|
|
451
|
+
console.log("provenance: ok");
|
|
452
|
+
}
|
|
268
453
|
if (import.meta.main) {
|
|
269
454
|
const [command, ...rest] = process.argv.slice(2);
|
|
270
455
|
switch (command) {
|
|
@@ -286,11 +471,17 @@ if (import.meta.main) {
|
|
|
286
471
|
case "reindex":
|
|
287
472
|
cmdReindex(rest);
|
|
288
473
|
break;
|
|
474
|
+
case "reindex-sprites":
|
|
475
|
+
cmdReindexSprites(rest);
|
|
476
|
+
break;
|
|
289
477
|
case "verify":
|
|
290
478
|
cmdVerify();
|
|
291
479
|
break;
|
|
480
|
+
case "provenance":
|
|
481
|
+
cmdProvenance(rest);
|
|
482
|
+
break;
|
|
292
483
|
default:
|
|
293
|
-
console.log("usage: assets <add|list|search|pull|register|reindex|verify> [...args]");
|
|
484
|
+
console.log("usage: assets <add|list|search|pull|register|reindex|reindex-sprites|verify|provenance> [...args]");
|
|
294
485
|
if (command !== undefined && command !== "help")
|
|
295
486
|
process.exit(1);
|
|
296
487
|
}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
/** Catalog assets that ship a collision mesh in the generated index. Opt-in only: each entry adds ~10-30KB of quantized triangles to the index and a BVH raycast per query, so list only concave models whose fitted box lies (archways, rings, frames). */
|
|
2
|
+
export declare const COLLISION_MESH_ASSET_IDS: ReadonlySet<string>;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Catalog assets that ship a collision mesh in the generated index. Opt-in only: each entry adds ~10-30KB of quantized triangles to the index and a BVH raycast per query, so list only concave models whose fitted box lies (archways, rings, frames). */
|
|
2
|
+
export const COLLISION_MESH_ASSET_IDS = new Set([
|
|
3
|
+
"kaykit-dungeon/wall_arched",
|
|
4
|
+
"kaykit-dungeon/wall_archedwindow_open",
|
|
5
|
+
"kaykit-dungeon/wall_archedwindow_gated",
|
|
6
|
+
]);
|
package/dist/dims.d.ts
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
1
|
import type { ModelDims } from "@jgengine/core/scene/assetCatalog";
|
|
2
|
+
import { type CollisionMeshData } from "@jgengine/core/scene/collisionMesh";
|
|
2
3
|
export type { ModelDims };
|
|
3
4
|
export declare function readGlbDims(bytes: Uint8Array): ModelDims | null;
|
|
5
|
+
/**
|
|
6
|
+
* Extract a model-space collision triangle soup from a GLB and compress it with {@link encodeCollisionMesh}.
|
|
7
|
+
* Walks the default scene, transforming each TRIANGLES primitive's float32 VEC3 positions by the node's world
|
|
8
|
+
* matrix, and returns `null` for a JSON-only GLB or when no triangle survives quantization.
|
|
9
|
+
*/
|
|
10
|
+
export declare function readGlbCollisionMesh(bytes: Uint8Array): CollisionMeshData | null;
|
package/dist/dims.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { encodeCollisionMesh } from "@jgengine/core/scene/collisionMesh";
|
|
1
2
|
const IDENTITY = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
|
|
2
3
|
const GLB_MAGIC = 0x46546c67;
|
|
3
4
|
const CHUNK_JSON = 0x4e4f534a;
|
|
5
|
+
const CHUNK_BIN = 0x004e4942;
|
|
4
6
|
function multiply(a, b) {
|
|
5
7
|
const out = new Array(16);
|
|
6
8
|
for (let col = 0; col < 4; col += 1) {
|
|
@@ -139,5 +141,157 @@ export function readGlbDims(bytes) {
|
|
|
139
141
|
footprint: { w: bounds.max[0] - bounds.min[0], d: bounds.max[2] - bounds.min[2] },
|
|
140
142
|
center: { x: (bounds.min[0] + bounds.max[0]) / 2, z: (bounds.min[2] + bounds.max[2]) / 2 },
|
|
141
143
|
minY: bounds.min[1],
|
|
144
|
+
maxY: bounds.max[1],
|
|
142
145
|
};
|
|
143
146
|
}
|
|
147
|
+
const COMPONENT_FLOAT = 5126;
|
|
148
|
+
const COMPONENT_U8 = 5121;
|
|
149
|
+
const COMPONENT_U16 = 5123;
|
|
150
|
+
const COMPONENT_U32 = 5125;
|
|
151
|
+
const MODE_TRIANGLES = 4;
|
|
152
|
+
/** The GLB's binary chunk, or `null` for a JSON-only GLB (no geometry buffer to read). */
|
|
153
|
+
function readBinChunk(bytes) {
|
|
154
|
+
if (bytes.byteLength < 20)
|
|
155
|
+
return null;
|
|
156
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
157
|
+
if (view.getUint32(0, true) !== GLB_MAGIC)
|
|
158
|
+
return null;
|
|
159
|
+
const jsonChunkLength = view.getUint32(12, true);
|
|
160
|
+
if (view.getUint32(16, true) !== CHUNK_JSON)
|
|
161
|
+
return null;
|
|
162
|
+
const header = 20 + jsonChunkLength;
|
|
163
|
+
if (header + 8 > bytes.byteLength)
|
|
164
|
+
return null;
|
|
165
|
+
const chunkLength = view.getUint32(header, true);
|
|
166
|
+
if (view.getUint32(header + 4, true) !== CHUNK_BIN)
|
|
167
|
+
return null;
|
|
168
|
+
const start = header + 8;
|
|
169
|
+
const end = Math.min(start + chunkLength, bytes.byteLength);
|
|
170
|
+
return bytes.subarray(start, end);
|
|
171
|
+
}
|
|
172
|
+
/** Float32 VEC3 accessor values in model space, honoring bufferView/accessor byteOffset and byteStride; `null` for a malformed or out-of-bounds accessor. */
|
|
173
|
+
function readVec3Accessor(gltf, bin, accessorIndex) {
|
|
174
|
+
const accessor = gltf.accessors?.[accessorIndex];
|
|
175
|
+
if (accessor === undefined)
|
|
176
|
+
return null;
|
|
177
|
+
if (accessor.componentType !== COMPONENT_FLOAT || accessor.type !== "VEC3")
|
|
178
|
+
return null;
|
|
179
|
+
const count = accessor.count;
|
|
180
|
+
if (count === undefined || count <= 0)
|
|
181
|
+
return null;
|
|
182
|
+
if (accessor.bufferView === undefined)
|
|
183
|
+
return null;
|
|
184
|
+
const bufferView = gltf.bufferViews?.[accessor.bufferView];
|
|
185
|
+
if (bufferView === undefined)
|
|
186
|
+
return null;
|
|
187
|
+
const stride = bufferView.byteStride ?? 12;
|
|
188
|
+
const base = (bufferView.byteOffset ?? 0) + (accessor.byteOffset ?? 0);
|
|
189
|
+
if (base < 0 || base + (count - 1) * stride + 12 > bin.byteLength)
|
|
190
|
+
return null;
|
|
191
|
+
const view = new DataView(bin.buffer, bin.byteOffset, bin.byteLength);
|
|
192
|
+
const out = new Float64Array(count * 3);
|
|
193
|
+
for (let i = 0; i < count; i += 1) {
|
|
194
|
+
const offset = base + i * stride;
|
|
195
|
+
out[i * 3] = view.getFloat32(offset, true);
|
|
196
|
+
out[i * 3 + 1] = view.getFloat32(offset + 4, true);
|
|
197
|
+
out[i * 3 + 2] = view.getFloat32(offset + 8, true);
|
|
198
|
+
}
|
|
199
|
+
return out;
|
|
200
|
+
}
|
|
201
|
+
/** Scalar index accessor values (u8/u16/u32), honoring byteOffset and byteStride; `null` for a malformed or out-of-bounds accessor. */
|
|
202
|
+
function readIndexAccessor(gltf, bin, accessorIndex) {
|
|
203
|
+
const accessor = gltf.accessors?.[accessorIndex];
|
|
204
|
+
if (accessor === undefined || accessor.type !== "SCALAR")
|
|
205
|
+
return null;
|
|
206
|
+
const componentType = accessor.componentType;
|
|
207
|
+
const componentBytes = componentType === COMPONENT_U8 ? 1 : componentType === COMPONENT_U16 ? 2 : componentType === COMPONENT_U32 ? 4 : 0;
|
|
208
|
+
if (componentBytes === 0)
|
|
209
|
+
return null;
|
|
210
|
+
const count = accessor.count;
|
|
211
|
+
if (count === undefined || count <= 0)
|
|
212
|
+
return null;
|
|
213
|
+
if (accessor.bufferView === undefined)
|
|
214
|
+
return null;
|
|
215
|
+
const bufferView = gltf.bufferViews?.[accessor.bufferView];
|
|
216
|
+
if (bufferView === undefined)
|
|
217
|
+
return null;
|
|
218
|
+
const stride = bufferView.byteStride ?? componentBytes;
|
|
219
|
+
const base = (bufferView.byteOffset ?? 0) + (accessor.byteOffset ?? 0);
|
|
220
|
+
if (base < 0 || base + (count - 1) * stride + componentBytes > bin.byteLength)
|
|
221
|
+
return null;
|
|
222
|
+
const view = new DataView(bin.buffer, bin.byteOffset, bin.byteLength);
|
|
223
|
+
const out = new Uint32Array(count);
|
|
224
|
+
for (let i = 0; i < count; i += 1) {
|
|
225
|
+
const offset = base + i * stride;
|
|
226
|
+
out[i] =
|
|
227
|
+
componentType === COMPONENT_U8
|
|
228
|
+
? view.getUint8(offset)
|
|
229
|
+
: componentType === COMPONENT_U16
|
|
230
|
+
? view.getUint16(offset, true)
|
|
231
|
+
: view.getUint32(offset, true);
|
|
232
|
+
}
|
|
233
|
+
return out;
|
|
234
|
+
}
|
|
235
|
+
/** Append one triangle primitive's world-space geometry to the soup; skips non-triangle modes and malformed accessors, appending nothing on failure. */
|
|
236
|
+
function addPrimitive(gltf, bin, primitive, world, soup) {
|
|
237
|
+
if ((primitive.mode ?? MODE_TRIANGLES) !== MODE_TRIANGLES)
|
|
238
|
+
return;
|
|
239
|
+
const accessorIndex = primitive.attributes?.POSITION;
|
|
240
|
+
if (accessorIndex === undefined)
|
|
241
|
+
return;
|
|
242
|
+
const positions = readVec3Accessor(gltf, bin, accessorIndex);
|
|
243
|
+
if (positions === null)
|
|
244
|
+
return;
|
|
245
|
+
let localIndices = null;
|
|
246
|
+
if (primitive.indices !== undefined) {
|
|
247
|
+
localIndices = readIndexAccessor(gltf, bin, primitive.indices);
|
|
248
|
+
if (localIndices === null)
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
const vertexBase = soup.positions.length / 3;
|
|
252
|
+
const vertexCount = positions.length / 3;
|
|
253
|
+
for (let v = 0; v < vertexCount; v += 1) {
|
|
254
|
+
const [wx, wy, wz] = transformPoint(world, positions[v * 3], positions[v * 3 + 1], positions[v * 3 + 2]);
|
|
255
|
+
soup.positions.push(wx, wy, wz);
|
|
256
|
+
}
|
|
257
|
+
if (localIndices === null) {
|
|
258
|
+
for (let v = 0; v < vertexCount; v += 1)
|
|
259
|
+
soup.indices.push(vertexBase + v);
|
|
260
|
+
}
|
|
261
|
+
else {
|
|
262
|
+
for (let i = 0; i < localIndices.length; i += 1)
|
|
263
|
+
soup.indices.push(vertexBase + localIndices[i]);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
function walkCollision(gltf, bin, nodeIndex, parent, soup) {
|
|
267
|
+
const node = gltf.nodes?.[nodeIndex];
|
|
268
|
+
if (node === undefined)
|
|
269
|
+
return;
|
|
270
|
+
const world = multiply(parent, fromTRS(node));
|
|
271
|
+
if (node.mesh !== undefined) {
|
|
272
|
+
const mesh = gltf.meshes?.[node.mesh];
|
|
273
|
+
for (const primitive of mesh?.primitives ?? [])
|
|
274
|
+
addPrimitive(gltf, bin, primitive, world, soup);
|
|
275
|
+
}
|
|
276
|
+
for (const child of node.children ?? [])
|
|
277
|
+
walkCollision(gltf, bin, child, world, soup);
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Extract a model-space collision triangle soup from a GLB and compress it with {@link encodeCollisionMesh}.
|
|
281
|
+
* Walks the default scene, transforming each TRIANGLES primitive's float32 VEC3 positions by the node's world
|
|
282
|
+
* matrix, and returns `null` for a JSON-only GLB or when no triangle survives quantization.
|
|
283
|
+
*/
|
|
284
|
+
export function readGlbCollisionMesh(bytes) {
|
|
285
|
+
const gltf = readJsonChunk(bytes);
|
|
286
|
+
if (gltf === null)
|
|
287
|
+
return null;
|
|
288
|
+
const bin = readBinChunk(bytes);
|
|
289
|
+
if (bin === null)
|
|
290
|
+
return null;
|
|
291
|
+
const sceneIndex = gltf.scene ?? 0;
|
|
292
|
+
const roots = gltf.scenes?.[sceneIndex]?.nodes ?? gltf.nodes?.map((_, index) => index) ?? [];
|
|
293
|
+
const soup = { positions: [], indices: [] };
|
|
294
|
+
for (const root of roots)
|
|
295
|
+
walkCollision(gltf, bin, root, IDENTITY, soup);
|
|
296
|
+
return encodeCollisionMesh(soup);
|
|
297
|
+
}
|
package/dist/download.d.ts
CHANGED
|
@@ -1,22 +1,38 @@
|
|
|
1
|
+
import { type UnzipFileFilter } from "fflate";
|
|
1
2
|
import { type AssetSource } from "./manifest.js";
|
|
2
3
|
export type FetchLike = typeof fetch;
|
|
4
|
+
/** Max size of a downloaded (still-compressed) archive, in bytes. Provider zips run tens of MB; this leaves headroom without buffering an unbounded response. */
|
|
5
|
+
export declare const MAX_ARCHIVE_DOWNLOAD_BYTES: number;
|
|
6
|
+
/** Max total uncompressed size this module will inflate out of one archive, in bytes. */
|
|
7
|
+
export declare const MAX_ARCHIVE_UNCOMPRESSED_BYTES: number;
|
|
8
|
+
/** Max number of entries this module will extract out of one archive. */
|
|
9
|
+
export declare const MAX_ARCHIVE_ENTRY_COUNT = 20000;
|
|
10
|
+
/** Max allowed originalSize/size ratio for a single archive entry — past this it's treated as a zip bomb. */
|
|
11
|
+
export declare const MAX_ARCHIVE_COMPRESSION_RATIO = 100;
|
|
12
|
+
/** @internal */
|
|
13
|
+
export declare function boundedExtractFilter(matches: UnzipFileFilter): UnzipFileFilter;
|
|
3
14
|
export interface ExtractedGlb {
|
|
4
15
|
file: string;
|
|
5
16
|
bytes: Uint8Array;
|
|
6
17
|
}
|
|
7
|
-
|
|
8
|
-
|
|
18
|
+
/** An image a pack's models reference, shipped flat beside the `.glb` files. */
|
|
19
|
+
export interface ExtractedPackImage {
|
|
20
|
+
/** Flat basename, e.g. "citybits_texture.png" — models reference it relative to their own URL. */
|
|
9
21
|
file: string;
|
|
10
22
|
bytes: Uint8Array;
|
|
11
23
|
}
|
|
24
|
+
/** @internal */
|
|
12
25
|
export declare function findArchiveUrl(html: string, pageUrl: string): string | null;
|
|
26
|
+
/** @internal */
|
|
13
27
|
export declare function resolveArchiveUrl(source: AssetSource, fetchImpl?: FetchLike): Promise<string>;
|
|
28
|
+
/** @internal */
|
|
14
29
|
export declare function downloadArchive(url: string, fetchImpl?: FetchLike): Promise<Uint8Array>;
|
|
15
30
|
/**
|
|
16
31
|
* Layout for the `--mirror` / `JGENGINE_ASSETS_MIRROR` base URL override: the
|
|
17
32
|
* archive for a pack is expected at `<baseUrl>/<provider>/<packId>.zip`, e.g.
|
|
18
33
|
* `https://my-mirror.example.com/kenney/kenney-nature.zip`.
|
|
19
|
-
|
|
34
|
+
* @internal
|
|
35
|
+
*/
|
|
20
36
|
export declare function mirrorOverrideUrl(baseUrl: string, source: AssetSource): string;
|
|
21
37
|
/**
|
|
22
38
|
* Default asset mirror: this repo's own GitHub Releases, reachable from every
|
|
@@ -29,7 +45,9 @@ export declare function mirrorOverrideUrl(baseUrl: string, source: AssetSource):
|
|
|
29
45
|
* `JGENGINE_ASSETS_NO_DEFAULT_MIRROR=1`.
|
|
30
46
|
*/
|
|
31
47
|
export declare const DEFAULT_RELEASE_BASE = "https://github.com/Noisemaker111/jgengine/releases/download/packs";
|
|
32
|
-
/** URL of `source`'s archive on the default GitHub-release mirror.
|
|
48
|
+
/** URL of `source`'s archive on the default GitHub-release mirror.
|
|
49
|
+
* @internal
|
|
50
|
+
*/
|
|
33
51
|
export declare function defaultReleaseUrl(source: AssetSource): string;
|
|
34
52
|
export interface DownloadPackOptions {
|
|
35
53
|
/** Explicit mirror base override (CLI `--mirror` / `JGENGINE_ASSETS_MIRROR`), tried before the primary provider path. */
|
|
@@ -52,8 +70,49 @@ export interface DownloadPackResult {
|
|
|
52
70
|
* source supplied the bytes; a mismatch is treated as a failed attempt so the
|
|
53
71
|
* next source in the chain is tried. Throws with every attempted URL and its
|
|
54
72
|
* failure reason when all sources fail.
|
|
55
|
-
|
|
73
|
+
* @internal
|
|
74
|
+
*/
|
|
56
75
|
export declare function downloadPackArchive(source: AssetSource, options?: DownloadPackOptions): Promise<DownloadPackResult>;
|
|
57
|
-
|
|
58
|
-
|
|
76
|
+
/**
|
|
77
|
+
* External image basenames a packed `.glb` still references (its textures must
|
|
78
|
+
* sit beside it in the pack dir). Empty for self-contained models.
|
|
79
|
+
* @internal
|
|
80
|
+
*/
|
|
81
|
+
export declare function externalGlbImages(bytes: Uint8Array): string[];
|
|
82
|
+
/**
|
|
83
|
+
* Pack a `.gltf` + optional external `.bin` into a single `.glb` so reindex /
|
|
84
|
+
* the shell only ever deal in one-file models. Strips buffer `uri` fields so
|
|
85
|
+
* the binary chunk is self-contained; external image URIs are flattened to
|
|
86
|
+
* basenames so the pack ships each referenced texture beside the models
|
|
87
|
+
* (loaders resolve the relative URI against the model's own URL).
|
|
88
|
+
* @internal
|
|
89
|
+
*/
|
|
90
|
+
export declare function packGltfToGlb(gltfBytes: Uint8Array, binBytes?: Uint8Array): Uint8Array;
|
|
91
|
+
/** What `extractGlbs` pulls out of a pack archive: the models plus the textures they reference. */
|
|
92
|
+
export interface ExtractedPack {
|
|
93
|
+
models: ExtractedGlb[];
|
|
94
|
+
/** Every archive image some extracted model references, flattened to basenames. */
|
|
95
|
+
images: ExtractedPackImage[];
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Pull every GLB out of an archive, plus the textures those models reference.
|
|
99
|
+
* Converts co-located `.gltf` + `.bin` pairs (Quaternius Standard packs on
|
|
100
|
+
* OpenGameArt, KayKit bits packs) into `.glb` so the catalog stays
|
|
101
|
+
* one-file-per-model; GLB wins when both formats share a basename. External
|
|
102
|
+
* image URIs are flattened to basenames and the matching archive images are
|
|
103
|
+
* returned for writing beside the models — a pulled pack renders textured, not
|
|
104
|
+
* as white clay (#1005).
|
|
105
|
+
* @internal
|
|
106
|
+
*/
|
|
107
|
+
export declare function extractGlbs(archive: Uint8Array): ExtractedPack;
|
|
108
|
+
/** One SVG/PNG file pulled out of a sprite/icon-pack archive by `extractSpriteFiles`. */
|
|
109
|
+
export interface ExtractedSpriteFile {
|
|
110
|
+
file: string;
|
|
111
|
+
bytes: Uint8Array;
|
|
112
|
+
}
|
|
113
|
+
/** Pulls every SVG/PNG out of a sprite/icon-pack archive, deduped by basename regardless of nesting depth.
|
|
114
|
+
* @internal
|
|
115
|
+
*/
|
|
116
|
+
export declare function extractSpriteFiles(archive: Uint8Array): ExtractedSpriteFile[];
|
|
117
|
+
/** @internal */
|
|
59
118
|
export declare function sha256Hex(bytes: Uint8Array): Promise<string>;
|