@forgeax/game 0.3.8 → 0.3.9
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/README.md +14 -18
- package/assets/extensions/asset3d/cli.mjs +190 -44
- package/assets/extensions/asset3d/skills/art-3d-asset-library/SKILL.md +149 -11
- package/assets/skills/forgeax-game/SKILL.md +13 -9
- package/dist/main.js +17 -14
- package/docs/asset3d.md +41 -11
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -9,8 +9,8 @@ creation, build, and Preview to the exact Engine SDK instead of carrying a secon
|
|
|
9
9
|
runtime.
|
|
10
10
|
|
|
11
11
|
> [!IMPORTANT]
|
|
12
|
-
> The
|
|
13
|
-
> `@forgeax/engine-sdk@0.1
|
|
12
|
+
> The package carries `forgeax-game` and scoped `game` binary aliases and resolves the exact
|
|
13
|
+
> `@forgeax/engine-sdk@0.2.1` and `pnpm@11.7.0` dependencies. It has no
|
|
14
14
|
> `@forgeax/game-runtime` dependency and no static Preview fallback.
|
|
15
15
|
|
|
16
16
|
## Supported flow
|
|
@@ -22,8 +22,8 @@ flowchart LR
|
|
|
22
22
|
CARRIER["Exact npm SDK carrier"] --> GAME
|
|
23
23
|
PLUGIN["@forgeax/game"] --> INSTALL
|
|
24
24
|
PLUGIN --> INIT
|
|
25
|
-
GAME --> BUILD["Exact Engine CLI build --json"]
|
|
26
|
-
BUILD --> PREVIEW["Exact Engine CLI preview --json"]
|
|
25
|
+
GAME --> BUILD["Exact Engine CLI project build --json"]
|
|
26
|
+
BUILD --> PREVIEW["Exact Engine CLI project preview --json"]
|
|
27
27
|
PREVIEW --> PROOF["Release-aware readiness proof"]
|
|
28
28
|
```
|
|
29
29
|
|
|
@@ -73,13 +73,11 @@ This version consumes the exact Engine SDK pin in package.json; it does not migr
|
|
|
73
73
|
## Agent completion contract
|
|
74
74
|
|
|
75
75
|
`init` records `.forgeax/game-authoring-baseline.json` before gameplay authoring. An
|
|
76
|
-
untouched Empty template remains runnable
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
template test does not qualify. Authoring layout follows the installed Engine and
|
|
82
|
-
`forge.json` (v2 uses `assets/` and `plugins[]`); the Studio-hosted
|
|
76
|
+
untouched Empty template remains runnable. After making a game, replace the Empty
|
|
77
|
+
identity and README, document controls, and test a real gameplay rule rather than
|
|
78
|
+
renaming the template test. The connector checks completion evidence before Preview.
|
|
79
|
+
Authoring layout follows the installed Engine and
|
|
80
|
+
`forge.json` (Engine 0.2.1 uses `assets/` Packs and `roots`); the Studio-hosted
|
|
83
81
|
`.forgeax/games/<slug>` layout is never created by this package.
|
|
84
82
|
|
|
85
83
|
Every supported host receives the same packaged `forgeax-game` Skill and routing rule.
|
|
@@ -122,13 +120,11 @@ root and never accepts a caller-provided `target_dir`.
|
|
|
122
120
|
| `forgeax_game_read_logs` | HTTP tool | Read a bounded Preview log tail for remote diagnosis |
|
|
123
121
|
| `forgeax_game_write_file` | HTTP tool | Atomically create or hash-guard replacement of a text file |
|
|
124
122
|
|
|
125
|
-
The build plus Preview-readiness deadline is 150 seconds.
|
|
126
|
-
|
|
127
|
-
`GET /.forgeax/preview-health`
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
release and fresh instance ID, then reads the served `forgeax-dist.json` and requires
|
|
131
|
-
its SHA-256 to equal the build it just produced.
|
|
123
|
+
The build plus Preview-readiness deadline is 150 seconds. Engine 0.2.1 emits
|
|
124
|
+
structured `project build` and `project preview` envelopes. Where authenticated
|
|
125
|
+
`GET /.forgeax/preview-health` is available, it must echo the canonical root,
|
|
126
|
+
exact release, build digest, and instance ID; the connector also checks the
|
|
127
|
+
served build artifact against the build it just produced.
|
|
132
128
|
|
|
133
129
|
Any build or Preview failure is an MCP `isError` result. Agents must not probe or
|
|
134
130
|
reuse an existing localhost port after a failed call: HTTP 200 is not ownership
|
|
@@ -251,9 +251,62 @@ function canonicalizeOrigins(inputs) {
|
|
|
251
251
|
return { values, compactJson, digest: sha256(compactJson) };
|
|
252
252
|
}
|
|
253
253
|
|
|
254
|
+
// extensions/asset3d/src/search-contract.ts
|
|
255
|
+
var ASSET_TYPES = [1, 2, 3, 5, 6, 7, 8, 9, 11];
|
|
256
|
+
var ART_STYLES = ["realistic", "semi-realistic", "stylized", "cartoon", "pixel art", "voxel art", "low poly", "hand-drawn", "minimalist", "generic"];
|
|
257
|
+
var THEME_STYLES = ["urban", "rural", "industrial", "military", "ancient", "medieval", "traditional chinese", "traditional japanese", "wild west", "aquatic", "fantasy", "xianxia", "steampunk", "cyberpunk", "science fiction", "space", "post-apocalyptic", "horror", "nature", "prehistoric", "generic"];
|
|
258
|
+
function searchBody(library, query, options = {}) {
|
|
259
|
+
if (typeof query !== "string" || query.length > 200)
|
|
260
|
+
throw new Error("asset3d_query_invalid");
|
|
261
|
+
const assetType = options.assetType ?? 1;
|
|
262
|
+
if (!ASSET_TYPES.includes(assetType))
|
|
263
|
+
throw new Error("asset3d_asset_type_invalid");
|
|
264
|
+
const filter = {};
|
|
265
|
+
for (const [option, field] of [["category", "category"], ["artStyle", "art_style"], ["themeStyle", "theme_style"], ["engineVersion", "engine_versions"]]) {
|
|
266
|
+
const value = options[option];
|
|
267
|
+
if (value === undefined)
|
|
268
|
+
continue;
|
|
269
|
+
if (typeof value !== "string" || !value.trim() || value.length > 128)
|
|
270
|
+
throw new Error("asset3d_filter_invalid");
|
|
271
|
+
if (field === "category" && ![1, 2, 5, 6, 7, 8, 9].includes(assetType))
|
|
272
|
+
throw new Error("asset3d_filter_not_applicable");
|
|
273
|
+
if (["art_style", "theme_style"].includes(field) && ![1, 2, 5, 6, 8, 9].includes(assetType))
|
|
274
|
+
throw new Error("asset3d_filter_not_applicable");
|
|
275
|
+
if (field === "art_style" && !ART_STYLES.includes(value))
|
|
276
|
+
throw new Error("asset3d_art_style_invalid");
|
|
277
|
+
if (field === "theme_style" && !THEME_STYLES.includes(value))
|
|
278
|
+
throw new Error("asset3d_theme_style_invalid");
|
|
279
|
+
filter[field] = value;
|
|
280
|
+
}
|
|
281
|
+
return {
|
|
282
|
+
depot_name: library,
|
|
283
|
+
asset_type: assetType,
|
|
284
|
+
...query.trim() ? { content: query.trim() } : {},
|
|
285
|
+
...Object.keys(filter).length ? { filter } : {},
|
|
286
|
+
similarity_score: 0.3,
|
|
287
|
+
page_size: 10
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
254
291
|
// extensions/asset3d/src/aw-access.ts
|
|
255
292
|
var AW_SERVICE_PATH = "/trpc.oasismetric.omcontentserver.http";
|
|
256
293
|
var DEFAULT_AW_PUBLIC_SERVICE_ROOT = "http://lb-pl74wsqg-5wi8ujmy1fq2746r.clb.usw-tencentclb.com:8008/trpc.oasismetric.omcontentserver.http";
|
|
294
|
+
function optionalText(value) {
|
|
295
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
296
|
+
}
|
|
297
|
+
function optionalStrings(value) {
|
|
298
|
+
return Array.isArray(value) && value.every((v) => typeof v === "string") ? value : undefined;
|
|
299
|
+
}
|
|
300
|
+
function optionalUrl(value) {
|
|
301
|
+
if (typeof value !== "string" || !value)
|
|
302
|
+
return;
|
|
303
|
+
downloadOrigin(value);
|
|
304
|
+
return value;
|
|
305
|
+
}
|
|
306
|
+
function publicCandidate(candidate) {
|
|
307
|
+
const { downloadUrl, versions, ...metadata } = candidate;
|
|
308
|
+
return { ...metadata, ...versions ? { versions: versions.map(({ downloadUrl: downloadUrl2, ...version }) => ({ ...version, downloadable: !!downloadUrl2 })) } : {} };
|
|
309
|
+
}
|
|
257
310
|
function normalizeAssetLibraryServiceRoot(input) {
|
|
258
311
|
let parsed;
|
|
259
312
|
try {
|
|
@@ -314,9 +367,8 @@ async function boundedResponse(response, limit) {
|
|
|
314
367
|
}
|
|
315
368
|
return Buffer.concat(chunks);
|
|
316
369
|
}
|
|
317
|
-
async function searchLibrary(config, query) {
|
|
318
|
-
|
|
319
|
-
throw new Error("asset3d_query_invalid");
|
|
370
|
+
async function searchLibrary(config, query, options = {}) {
|
|
371
|
+
const body = searchBody(config.library, query, options);
|
|
320
372
|
const key = readAwCredential(config.credentialFile);
|
|
321
373
|
if (!key)
|
|
322
374
|
throw new Error("asset3d_api_key_required");
|
|
@@ -327,14 +379,7 @@ async function searchLibrary(config, query) {
|
|
|
327
379
|
redirect: "error",
|
|
328
380
|
signal: AbortSignal.timeout(30000),
|
|
329
381
|
headers: { "Content-Type": "application/json", "X-Sandbox-Key": key },
|
|
330
|
-
body: JSON.stringify(
|
|
331
|
-
depot_name: config.library,
|
|
332
|
-
asset_type: 1,
|
|
333
|
-
req_content_type: 0,
|
|
334
|
-
content: query,
|
|
335
|
-
similarity_score: 0,
|
|
336
|
-
page_size: 10
|
|
337
|
-
})
|
|
382
|
+
body: JSON.stringify(body)
|
|
338
383
|
});
|
|
339
384
|
} catch {
|
|
340
385
|
throw new Error("asset3d_service_unreachable");
|
|
@@ -349,18 +394,48 @@ async function searchLibrary(config, query) {
|
|
|
349
394
|
} catch {
|
|
350
395
|
throw new Error("asset3d_search_response_invalid");
|
|
351
396
|
}
|
|
352
|
-
if (!value ||
|
|
397
|
+
if (!value || typeof value !== "object" || Array.isArray(value) || value.ret !== undefined && value.ret !== 0) {
|
|
353
398
|
throw new Error("asset3d_search_response_invalid");
|
|
354
399
|
}
|
|
400
|
+
if (value.asset_list === undefined)
|
|
401
|
+
return [];
|
|
402
|
+
if (!Array.isArray(value.asset_list) || value.asset_list.length > 100)
|
|
403
|
+
throw new Error("asset3d_search_response_invalid");
|
|
355
404
|
const seen = new Set;
|
|
356
405
|
return value.asset_list.slice(0, 10).map((asset) => {
|
|
357
|
-
if (!asset || typeof asset.id !== "string" || !/^[a-zA-Z0-9_-][a-zA-Z0-9._-]{0,127}$/.test(asset.id) || seen.has(asset.id) || typeof asset.name !== "string" || !asset.name || asset.name.length >
|
|
406
|
+
if (!asset || typeof asset.id !== "string" || !/^[a-zA-Z0-9_-][a-zA-Z0-9._-]{0,127}$/.test(asset.id) || seen.has(asset.id) || typeof asset.name !== "string" || !asset.name || asset.name.length > 1024 || typeof asset.res_url !== "string")
|
|
358
407
|
throw new Error("asset3d_search_response_invalid");
|
|
359
408
|
seen.add(asset.id);
|
|
360
409
|
downloadOrigin(asset.res_url);
|
|
361
410
|
const path = new URL(asset.res_url).pathname;
|
|
362
411
|
const format = String(asset.file_format || asset.format || path.split(".").pop() || "unknown").toLowerCase();
|
|
363
|
-
|
|
412
|
+
const versions = Array.isArray(asset.versions) ? asset.versions.map((v) => {
|
|
413
|
+
if (!v || typeof v.version_name !== "string" || !v.version_name)
|
|
414
|
+
throw new Error("asset3d_search_response_invalid");
|
|
415
|
+
return {
|
|
416
|
+
versionName: v.version_name,
|
|
417
|
+
engineVersions: optionalStrings(v.engine_versions),
|
|
418
|
+
thumbnailUrl: optionalUrl(v.thumbnail_url),
|
|
419
|
+
downloadUrl: optionalUrl(v.res_url)
|
|
420
|
+
};
|
|
421
|
+
}) : undefined;
|
|
422
|
+
return {
|
|
423
|
+
assetId: asset.id,
|
|
424
|
+
name: asset.name,
|
|
425
|
+
format,
|
|
426
|
+
downloadUrl: asset.res_url,
|
|
427
|
+
...typeof asset.type === "number" ? { type: asset.type } : {},
|
|
428
|
+
...typeof asset.score === "number" ? { score: asset.score } : {},
|
|
429
|
+
description: optionalText(asset.description),
|
|
430
|
+
detailedDescription: optionalText(asset.extra?.detailed_description),
|
|
431
|
+
category: optionalStrings(asset.category),
|
|
432
|
+
artStyle: optionalStrings(asset.art_style),
|
|
433
|
+
themeStyle: optionalStrings(asset.theme_style),
|
|
434
|
+
customTags: optionalStrings(asset.custom_tags),
|
|
435
|
+
currentVersion: optionalText(asset.current_version),
|
|
436
|
+
thumbnailUrl: optionalUrl(asset.thumbnail_url),
|
|
437
|
+
versions
|
|
438
|
+
};
|
|
364
439
|
});
|
|
365
440
|
}
|
|
366
441
|
async function checkAssetLibraryAccess(config) {
|
|
@@ -372,8 +447,8 @@ async function checkAssetLibraryAccess(config) {
|
|
|
372
447
|
}
|
|
373
448
|
|
|
374
449
|
// extensions/asset3d/src/library.ts
|
|
375
|
-
import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync4 } from "node:fs";
|
|
376
|
-
import { dirname as dirname4, resolve as resolve7 } from "node:path";
|
|
450
|
+
import { mkdirSync as mkdirSync3, readFileSync as readFileSync7, statSync as statSync3, writeFileSync as writeFileSync4 } from "node:fs";
|
|
451
|
+
import { basename as basename2, dirname as dirname4, resolve as resolve7 } from "node:path";
|
|
377
452
|
|
|
378
453
|
// node_modules/fflate/esm/index.mjs
|
|
379
454
|
import { createRequire } from "module";
|
|
@@ -768,6 +843,12 @@ function unzipSync(data, opts) {
|
|
|
768
843
|
return files;
|
|
769
844
|
}
|
|
770
845
|
|
|
846
|
+
// src/engine/constants.ts
|
|
847
|
+
var ENGINE_VERSION = "0.2.1";
|
|
848
|
+
var ENGINE_COMMIT = "d699bc46d1e55b8dbd34b0839f85378b575bc9e2";
|
|
849
|
+
var ENGINE_SDK_PACKAGE = "@forgeax/engine-sdk";
|
|
850
|
+
var PNPM_VERSION = "11.7.0";
|
|
851
|
+
|
|
771
852
|
// extensions/asset3d/src/install.ts
|
|
772
853
|
import { readFileSync as readFileSync2 } from "node:fs";
|
|
773
854
|
import { resolve as resolve2 } from "node:path";
|
|
@@ -986,12 +1067,6 @@ import { isAbsolute as isAbsolute5, relative as relative4, resolve as resolve6,
|
|
|
986
1067
|
import { existsSync as existsSync3, lstatSync as lstatSync3, readFileSync as readFileSync4, realpathSync as realpathSync2 } from "node:fs";
|
|
987
1068
|
import { isAbsolute as isAbsolute3, relative as relative2, resolve as resolve4, sep as sep2 } from "node:path";
|
|
988
1069
|
|
|
989
|
-
// src/engine/constants.ts
|
|
990
|
-
var ENGINE_VERSION = "0.1.26";
|
|
991
|
-
var ENGINE_COMMIT = "f3d0db12405e168e4204e32a9cde32e0df8d87ae";
|
|
992
|
-
var ENGINE_SDK_PACKAGE = "@forgeax/engine-sdk";
|
|
993
|
-
var PNPM_VERSION = "11.7.0";
|
|
994
|
-
|
|
995
1070
|
// src/engine/carrier.ts
|
|
996
1071
|
import {
|
|
997
1072
|
chmodSync as chmodSync2,
|
|
@@ -1281,7 +1356,7 @@ function parseEnvelope(stdout, command) {
|
|
|
1281
1356
|
if (envelopeIndex !== lines.length - 1) {
|
|
1282
1357
|
throw new Error(`${command}_envelope_invalid: diagnostics after JSON frame`);
|
|
1283
1358
|
}
|
|
1284
|
-
if (envelope.schemaVersion !==
|
|
1359
|
+
if (envelope.schemaVersion !== undefined || !Array.isArray(envelope.artifacts) || envelope.command !== `project ${command}` || typeof envelope.ok !== "boolean") {
|
|
1285
1360
|
throw new Error(`${command}_envelope_invalid: wrong schema or command`);
|
|
1286
1361
|
}
|
|
1287
1362
|
if (!envelope.ok) {
|
|
@@ -1609,7 +1684,7 @@ function engineReadback(projectRoot, assetRelative, add, carrierPluginRoot) {
|
|
|
1609
1684
|
}
|
|
1610
1685
|
function packReadback(projectRoot, item, carrierPluginRoot) {
|
|
1611
1686
|
const release = resolveEngineRelease(projectRoot, carrierPluginRoot === undefined ? {} : { pluginRoot: carrierPluginRoot });
|
|
1612
|
-
const result = spawnSync(process.execPath, [release.cliPath, "build", "--json"], {
|
|
1687
|
+
const result = spawnSync(process.execPath, [release.cliPath, "project", "build", "--json"], {
|
|
1613
1688
|
cwd: projectRoot,
|
|
1614
1689
|
encoding: "utf8",
|
|
1615
1690
|
timeout: 150000,
|
|
@@ -1966,20 +2041,45 @@ async function download(config, candidate) {
|
|
|
1966
2041
|
const format = /\.pack\.(ts|json)$/i.exec(path)?.[0].slice(1) ?? candidate.format.replace(/^\./, "");
|
|
1967
2042
|
return unpackAsset(bytes, format);
|
|
1968
2043
|
}
|
|
1969
|
-
async function candidatesAsset3d(root, query) {
|
|
1970
|
-
const candidates = await searchLibrary(readAsset3dConfig(root), query);
|
|
1971
|
-
return { candidates: candidates.map(
|
|
1972
|
-
}
|
|
1973
|
-
|
|
2044
|
+
async function candidatesAsset3d(root, query, options = {}) {
|
|
2045
|
+
const candidates = await searchLibrary(readAsset3dConfig(root), query, options);
|
|
2046
|
+
return { projectEngineVersion: ENGINE_VERSION, candidates: candidates.map(publicCandidate) };
|
|
2047
|
+
}
|
|
2048
|
+
function selectAssetVersion(candidate, versionName, engineVersion) {
|
|
2049
|
+
const selected = versionName ? candidate.versions?.find((v) => v.versionName === versionName) : candidate.versions?.find((v) => v.versionName === candidate.currentVersion);
|
|
2050
|
+
if (versionName && !selected)
|
|
2051
|
+
throw new Error("asset3d_version_not_found");
|
|
2052
|
+
if (versionName && !selected?.downloadUrl)
|
|
2053
|
+
throw new Error("asset3d_version_download_missing");
|
|
2054
|
+
if (engineVersion && candidate.versions?.length && (!selected || !selected.engineVersions?.includes(engineVersion))) {
|
|
2055
|
+
throw new Error("asset3d_version_selection_required: choose --version from a matching versions entry");
|
|
2056
|
+
}
|
|
2057
|
+
return selected?.downloadUrl ? {
|
|
2058
|
+
...candidate,
|
|
2059
|
+
currentVersion: selected.versionName,
|
|
2060
|
+
downloadUrl: selected.downloadUrl,
|
|
2061
|
+
thumbnailUrl: selected.thumbnailUrl ?? candidate.thumbnailUrl
|
|
2062
|
+
} : candidate;
|
|
2063
|
+
}
|
|
2064
|
+
function assertAssetEngineCompatible(candidate, projectVersion) {
|
|
2065
|
+
const declared = candidate.versions?.find((v) => v.versionName === candidate.currentVersion)?.engineVersions;
|
|
2066
|
+
if (declared?.length && !declared.includes(projectVersion)) {
|
|
2067
|
+
throw new Error(`asset3d_engine_version_mismatch: project Engine ${projectVersion}; selected asset version ${candidate.currentVersion} declares ${declared.join(", ")}. Select a matching asset version; do not upgrade the project silently.`);
|
|
2068
|
+
}
|
|
2069
|
+
}
|
|
2070
|
+
async function importAsset3d(root, query, assetId, options = {}, versionName) {
|
|
1974
2071
|
const config = readAsset3dConfig(root);
|
|
1975
|
-
const
|
|
1976
|
-
if (!
|
|
1977
|
-
throw new Error("asset3d_candidate_not_found: select an ID returned for this query");
|
|
2072
|
+
const found = (await searchLibrary(config, query, options)).find((c) => c.assetId === assetId);
|
|
2073
|
+
if (!found)
|
|
2074
|
+
throw new Error("asset3d_candidate_not_found: select an ID returned for this query and filters");
|
|
2075
|
+
const candidate = selectAssetVersion(found, versionName, options.engineVersion);
|
|
2076
|
+
assertAssetEngineCompatible(candidate, ENGINE_VERSION);
|
|
1978
2077
|
const files = await download(config, candidate);
|
|
1979
2078
|
const built = manifestFor(files, candidate.assetId);
|
|
1980
|
-
const
|
|
2079
|
+
const transactionQuery = query.trim() || `asset ${assetId}`;
|
|
2080
|
+
const started = beginAsset3d(root, [transactionQuery]);
|
|
1981
2081
|
try {
|
|
1982
|
-
const output = asset3dSearchOutputDir(root, started.execution, [
|
|
2082
|
+
const output = asset3dSearchOutputDir(root, started.execution, [transactionQuery]);
|
|
1983
2083
|
for (const [path, bytes] of Object.entries(files)) {
|
|
1984
2084
|
const target = resolve7(root, ".forgeax/extensions/asset3d/data/asset3d-quarantine", output, candidate.assetId, path);
|
|
1985
2085
|
mkdirSync3(dirname4(target), { recursive: true, mode: 448 });
|
|
@@ -1999,7 +2099,7 @@ async function importAsset3d(root, query, assetId) {
|
|
|
1999
2099
|
results: [{
|
|
2000
2100
|
status: "ok",
|
|
2001
2101
|
queryIndex: 0,
|
|
2002
|
-
query,
|
|
2102
|
+
query: transactionQuery,
|
|
2003
2103
|
provider: "ea-3d",
|
|
2004
2104
|
providerAssetId: assetId,
|
|
2005
2105
|
assetName: candidate.name,
|
|
@@ -2012,7 +2112,7 @@ async function importAsset3d(root, query, assetId) {
|
|
|
2012
2112
|
};
|
|
2013
2113
|
recordAsset3dProviderResult(root, started.execution, JSON.stringify(result));
|
|
2014
2114
|
const committed = commitAsset3d({ projectRoot: root, execution: started.execution });
|
|
2015
|
-
return { ...committed, deliveredFormat: built.deliveredFormat };
|
|
2115
|
+
return { ...committed, deliveredFormat: built.deliveredFormat, selectedAsset: publicCandidate(candidate) };
|
|
2016
2116
|
} catch (error) {
|
|
2017
2117
|
try {
|
|
2018
2118
|
abortAsset3d(root, started.execution);
|
|
@@ -2021,19 +2121,65 @@ async function importAsset3d(root, query, assetId) {
|
|
|
2021
2121
|
}
|
|
2022
2122
|
}
|
|
2023
2123
|
function parseAsset3dArgs(args, importing) {
|
|
2024
|
-
const allowed =
|
|
2124
|
+
const allowed = [
|
|
2125
|
+
"--query",
|
|
2126
|
+
"--asset-type",
|
|
2127
|
+
"--category",
|
|
2128
|
+
"--art-style",
|
|
2129
|
+
"--theme-style",
|
|
2130
|
+
"--engine-version",
|
|
2131
|
+
...importing ? ["--asset-id", "--version", "--candidate-file", "--candidate-name"] : []
|
|
2132
|
+
];
|
|
2025
2133
|
const values = {};
|
|
2026
2134
|
for (let i2 = 0;i2 < args.length; i2++) {
|
|
2027
2135
|
if (args[i2] === "--json")
|
|
2028
2136
|
continue;
|
|
2029
|
-
const
|
|
2030
|
-
if (!allowed.includes(
|
|
2137
|
+
const name2 = args[i2], value = args[++i2];
|
|
2138
|
+
if (!allowed.includes(name2) || values[name2] || !value || value.startsWith("--"))
|
|
2031
2139
|
throw new Error("asset3d_arguments_invalid");
|
|
2032
|
-
values[
|
|
2140
|
+
values[name2] = value;
|
|
2141
|
+
}
|
|
2142
|
+
const file = values["--candidate-file"], name = values["--candidate-name"];
|
|
2143
|
+
if (importing && (Boolean(file) !== Boolean(name) || Boolean(values["--asset-id"]) === Boolean(file))) {
|
|
2144
|
+
throw new Error("asset3d_arguments_invalid: supply --asset-id or --candidate-file with --candidate-name");
|
|
2145
|
+
}
|
|
2146
|
+
const query = values["--query"] ?? "";
|
|
2147
|
+
const options = {
|
|
2148
|
+
...values["--asset-type"] !== undefined ? { assetType: Number(values["--asset-type"]) } : {},
|
|
2149
|
+
...values["--category"] ? { category: values["--category"] } : {},
|
|
2150
|
+
...values["--art-style"] ? { artStyle: values["--art-style"] } : {},
|
|
2151
|
+
...values["--theme-style"] ? { themeStyle: values["--theme-style"] } : {},
|
|
2152
|
+
...values["--engine-version"] ? { engineVersion: values["--engine-version"] } : {}
|
|
2153
|
+
};
|
|
2154
|
+
searchBody("ea", query, options);
|
|
2155
|
+
let selected;
|
|
2156
|
+
if (file && name) {
|
|
2157
|
+
let response;
|
|
2158
|
+
try {
|
|
2159
|
+
if (statSync3(file).size > 1024 * 1024)
|
|
2160
|
+
throw new Error("too large");
|
|
2161
|
+
response = JSON.parse(readFileSync7(file, "utf8"));
|
|
2162
|
+
} catch {
|
|
2163
|
+
throw new Error("asset3d_candidate_file_invalid");
|
|
2164
|
+
}
|
|
2165
|
+
const data = response;
|
|
2166
|
+
if (data?.ok !== true || !Array.isArray(data.value?.candidates))
|
|
2167
|
+
throw new Error("asset3d_candidate_file_invalid");
|
|
2168
|
+
const matches = data.value.candidates.filter((entry) => typeof entry?.assetId === "string" && typeof entry.name === "string" && (entry.name === name || basename2(entry.name) === name));
|
|
2169
|
+
if (matches.length !== 1)
|
|
2170
|
+
throw new Error("asset3d_candidate_name_not_unique");
|
|
2171
|
+
const candidate = matches[0];
|
|
2172
|
+
if (values["--version"] && !candidate.versions?.some((v) => v.versionName === values["--version"])) {
|
|
2173
|
+
throw new Error("asset3d_version_not_found_in_candidates");
|
|
2174
|
+
}
|
|
2175
|
+
selected = { assetId: candidate.assetId, versionName: values["--version"] ?? candidate.currentVersion };
|
|
2033
2176
|
}
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2177
|
+
return {
|
|
2178
|
+
query,
|
|
2179
|
+
assetId: selected?.assetId ?? values["--asset-id"],
|
|
2180
|
+
options,
|
|
2181
|
+
versionName: selected?.versionName ?? values["--version"]
|
|
2182
|
+
};
|
|
2037
2183
|
}
|
|
2038
2184
|
|
|
2039
2185
|
// extensions/asset3d/cli.ts
|
|
@@ -2080,7 +2226,7 @@ async function run(context, args) {
|
|
|
2080
2226
|
}
|
|
2081
2227
|
if (operation === "candidates" || operation === "import") {
|
|
2082
2228
|
const parsed = parseAsset3dArgs(rest, operation === "import");
|
|
2083
|
-
return operation === "candidates" ? candidatesAsset3d(context.projectRoot, parsed.query) : importAsset3d(context.projectRoot, parsed.query, parsed.assetId);
|
|
2229
|
+
return operation === "candidates" ? candidatesAsset3d(context.projectRoot, parsed.query, parsed.options) : importAsset3d(context.projectRoot, parsed.query, parsed.assetId, parsed.options, parsed.versionName);
|
|
2084
2230
|
}
|
|
2085
2231
|
throw new Error("asset3d_arguments_invalid: expected candidates, import, or doctor");
|
|
2086
2232
|
}
|
|
@@ -1,23 +1,161 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: art-3d-asset-library
|
|
3
|
-
description:
|
|
3
|
+
description: Find and choose EA/AW library assets by purpose, style and Engine version; import native Pack or GLB into ForgeaX games. Use when reusing library assets.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Asset library
|
|
7
7
|
|
|
8
8
|
Run the following commands from the game directory. The command is pinned to this
|
|
9
9
|
installation; no global CLI or separate Asset3D MCP server is needed.
|
|
10
|
+
Run commands sequentially in one project: extension operations share a lock.
|
|
11
|
+
If another operation is active, wait for it to finish instead of launching retries.
|
|
10
12
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
13
|
+
If setup is uncertain, run `{{CLI}} doctor --json`. If disabled, ask the user to
|
|
14
|
+
configure `{{CLI}} enable`; keep the configured library and service environment.
|
|
15
|
+
|
|
16
|
+
## Find an asset that fits the game
|
|
17
|
+
|
|
18
|
+
Start with one focused query of 1–3 English keywords describing the object or
|
|
19
|
+
function, such as `wooden crate`. Add searches only when the results leave a
|
|
20
|
+
selection question unanswered; do not batch near-synonyms before inspecting results.
|
|
21
|
+
For library-dependent gameplay, establish a viable candidate before implementing
|
|
22
|
+
the game or starting an unrelated empty-template Preview.
|
|
23
|
+
|
|
24
|
+
```sh
|
|
25
|
+
scratch=$(mktemp -d /tmp/forgeax-selection.XXXXXX)
|
|
26
|
+
{{CLI}} candidates --query "wooden crate" --asset-type 1 --json --pretty > "$scratch/candidates.json" && cat "$scratch/candidates.json"
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
`--pretty` puts each identity and metadata field on its own line without changing
|
|
30
|
+
the response. Keep a candidate's name, opaque ID and version together when reading.
|
|
31
|
+
Use the returned scratch path for the import below; this saves one real response
|
|
32
|
+
without issuing a second search.
|
|
33
|
+
|
|
34
|
+
For search-only tasks, the candidate response includes `projectEngineVersion` and
|
|
35
|
+
asset version metadata: these are enough to compare compatibility and report a
|
|
36
|
+
selection. Engine API guides and scene/sub-asset inspection become useful when
|
|
37
|
+
moving on to import and game implementation.
|
|
38
|
+
|
|
39
|
+
Choose one asset type per search (default: 1):
|
|
40
|
+
|
|
41
|
+
| Type | Asset |
|
|
42
|
+
|:--|:--|
|
|
43
|
+
| 1 | 3D model |
|
|
44
|
+
| 2 | Texture |
|
|
45
|
+
| 3 | BGM |
|
|
46
|
+
| 5 | Animated 3D model |
|
|
47
|
+
| 6 | VFX |
|
|
48
|
+
| 7 | Sound effect |
|
|
49
|
+
| 8 | Material |
|
|
50
|
+
| 9 | Skybox |
|
|
51
|
+
| 11 | Kit or gameplay guide |
|
|
52
|
+
|
|
53
|
+
The CLI uses a 0.3 hybrid-search threshold and up to 10 candidates. `--query` can
|
|
54
|
+
be omitted for browsing by type or filters. Optional hard filters are
|
|
55
|
+
`--category`, `--art-style`, `--theme-style`, and `--engine-version` (one version).
|
|
56
|
+
Only add a filter when the request or project establishes its value; filters
|
|
57
|
+
exclude results rather than improving relevance. Category is unavailable for 3/11;
|
|
58
|
+
art/theme style is unavailable for 3/7/11. The CLI validates service enums.
|
|
59
|
+
|
|
60
|
+
Compare `description`, `detailedDescription`, category/style arrays and `customTags`
|
|
61
|
+
against the asset's intended role: playable target, backdrop, modular building,
|
|
62
|
+
animated character, etc. Do not choose solely by rank, score or filename.
|
|
63
|
+
When metadata is insufficient, inspect a relevant `thumbnailUrl` with the host's
|
|
64
|
+
image/browser tools if available; otherwise report visual suitability as unverified.
|
|
65
|
+
Separate metadata-based expectations from observed visuals: a description of metal
|
|
66
|
+
corners is evidence of intended form, not proof of material quality in the game.
|
|
67
|
+
Preview URLs may expire: preserve their query strings and do not persist them in game code.
|
|
68
|
+
|
|
69
|
+
Check `versions[].engineVersions` against `projectEngineVersion`; missing metadata
|
|
70
|
+
means unknown compatibility, not guaranteed support. Prefer a matching version and
|
|
71
|
+
pass its `versionName` with `--version`. Import rejects a declared version mismatch
|
|
72
|
+
before downloading; choose a matching version or report that none is available.
|
|
73
|
+
A ZIP label alone does not prove native Pack.
|
|
74
|
+
If no suitable result exists, broaden keywords once and remove only unnecessary
|
|
75
|
+
filters. Do not remove a required Engine-version constraint or upgrade Engine silently.
|
|
76
|
+
An empty candidate list is a valid no-match result, not a broken service.
|
|
77
|
+
|
|
78
|
+
## Decision examples
|
|
79
|
+
|
|
80
|
+
These illustrate decisions, not mandatory game designs or fixed asset choices.
|
|
81
|
+
|
|
82
|
+
| Situation | Useful next action | Avoid |
|
|
83
|
+
|:--|:--|:--|
|
|
84
|
+
| A collection game needs a small crate; the first result is a warehouse kit. | Compare object role, dimensions, style and compatible versions; choose the fitting crate even if lower-ranked. | Selecting by rank or similar filename alone. |
|
|
85
|
+
| Relevant candidates declare a different Engine version. | Check a query constrained to the actual project version; if no suitable version exists, report the gap. | More synonym searches without the version constraint, forced import, or a silent Engine upgrade. |
|
|
86
|
+
| A relevant asset has no compatibility metadata. | Treat compatibility as unknown; use normal import/build/Preview validation. | Rejecting it as incompatible, or promising it works, solely because the field is absent. |
|
|
87
|
+
| Similar candidates have long IDs and several versions. | Copy the name, ID and version together from the chosen result; check the final tuple against that object. | Combining one candidate's ID prefix with another's suffix, or trusting an earlier progress message instead of the search result. |
|
|
88
|
+
| Import returns Pack GUIDs but the model is invisible or wrongly scaled. | Inspect the imported scene/sub-assets, transforms and actual Preview before claiming success. | Calling download/import success a playable game, or replacing the asset with generated geometry. |
|
|
89
|
+
|
|
90
|
+
## Carry the selection forward
|
|
91
|
+
|
|
92
|
+
Before importing or sending the final selection, revisit the chosen object in the
|
|
93
|
+
search output and match its name, `assetId`, and chosen `versionName` together.
|
|
94
|
+
IDs are opaque strings: shared prefixes do not identify the same asset. Apply the
|
|
95
|
+
same lookup to any alternatives you cite. Copy values from that original record,
|
|
96
|
+
not from your earlier prose; JSON extraction is useful for long or similar IDs.
|
|
97
|
+
This is a local readback, not a reason to repeat the network search or write files.
|
|
98
|
+
|
|
99
|
+
Explain the role/style tradeoff separately. If nothing fits the project version,
|
|
100
|
+
a useful result is the compared alternatives and compatibility gap, with no
|
|
101
|
+
selected asset. An interesting but incompatible asset is not ready to import.
|
|
102
|
+
|
|
103
|
+
## Import and use
|
|
104
|
+
|
|
105
|
+
Use the selected tuple with the same query/type/filters. The CLI can take its
|
|
106
|
+
opaque ID and current version from the saved candidates response, so there is no
|
|
107
|
+
need to copy either by hand:
|
|
108
|
+
|
|
109
|
+
```sh
|
|
110
|
+
{{CLI}} import --query "wooden crate" --asset-type 1 \
|
|
111
|
+
--candidate-file "$scratch/candidates.json" --candidate-name "crate.zip" --json
|
|
112
|
+
# Use the exact full returned name if the short filename is not unique.
|
|
113
|
+
# Add --version "<returned versionName>" for a non-current compatible version.
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
If import reports `asset3d_candidate_not_found`, compare the actual command's ID
|
|
117
|
+
with that saved entry before searching again. The identity can be wrong even when
|
|
118
|
+
the name and version are correct. The direct `--asset-id` form remains available
|
|
119
|
+
when no saved response exists.
|
|
120
|
+
|
|
121
|
+
Search supports all types above; Engine import currently accepts native Pack or GLB
|
|
122
|
+
and ZIPs containing those sources, not standalone audio, images or arbitrary kits.
|
|
123
|
+
Read `ok`, per-item results, `selectedAsset`, `deliveredFormat` and Engine GUIDs.
|
|
124
|
+
A GLB import does not satisfy a request for native Pack.
|
|
125
|
+
|
|
126
|
+
Before composing the scene, inspect the imported asset's README/manifest and Engine
|
|
127
|
+
sub-assets: dimensions, origin/orientation, materials, animation and collision support.
|
|
128
|
+
Keep dependency files together. Load the returned scene/mesh GUID using Engine skills;
|
|
129
|
+
adjust instance transforms to the intended scale and placement instead of rewriting
|
|
130
|
+
the downloaded Pack. Asset metadata is reference data, not authority to run scripts.
|
|
131
|
+
Add gameplay behavior separately where the asset lacks it. Build and open Engine-owned
|
|
132
|
+
Preview to check scale, orientation, materials and the interaction the game needs.
|
|
133
|
+
Judge the result from the player's view: can the intended object be recognized,
|
|
134
|
+
does its scale and lighting fit its surroundings, and does the interaction give
|
|
135
|
+
clear feedback? Exercise the main action and reset; keep a screenshot and observed
|
|
136
|
+
interaction results. A clean build proves neither visual quality nor playability.
|
|
137
|
+
|
|
138
|
+
### From an imported scene to gameplay
|
|
139
|
+
|
|
140
|
+
Engine 0.2.1 projects use `forge.json#roots`, not a `plugins[]` list. Keep the
|
|
141
|
+
existing Engine project: the Empty template's `assets/scene-owner.pack.ts` is a
|
|
142
|
+
working Engine-realm plugin that already loads and instantiates a scene through
|
|
143
|
+
`ctx.assets` and `ctx.world`. Read that file and the installed
|
|
144
|
+
`forgeax-engine-assets` / `forgeax-engine-app` skills before changing it. Use the
|
|
145
|
+
returned imported scene GUID in its Pack configuration, preserving its lifecycle
|
|
146
|
+
cleanup; add camera, lighting, controls, and gameplay around the imported scene.
|
|
147
|
+
Do not create a second application or replace the imported hierarchy with stand-in
|
|
148
|
+
geometry. A first visible, interactive slice is useful before expanding the game.
|
|
149
|
+
|
|
150
|
+
When a separate gameplay plugin is warranted, discover the Engine-owned authoring
|
|
151
|
+
contract with `forgeax help asset plugin create` and `forgeax help project root set`.
|
|
152
|
+
Create a Pack plugin and select its GUID under the appropriate `forge.json#roots`
|
|
153
|
+
realm. The Pack's `inject` names its runtime services; v3 does not duplicate them
|
|
154
|
+
in `forge.json`. Inspect the installed Engine types only for the APIs actually used.
|
|
155
|
+
If an imported scene refers to custom components, register the trusted runtime
|
|
156
|
+
components before instantiation rather than deleting them from the source asset.
|
|
157
|
+
The imported scene GUID is a composition; rebuilding each mesh is usually
|
|
158
|
+
unnecessary. Let the real Preview resolve questions about scale and placement.
|
|
21
159
|
|
|
22
160
|
The CLI owns API calls, authentication, downloads and Engine import. Do not invent
|
|
23
161
|
IDs, handwrite HTTP requests, or substitute generated geometry for library results.
|
|
@@ -15,16 +15,22 @@ the connector owns Engine CLI execution and Preview lifecycle.
|
|
|
15
15
|
call `forgeax_status_lite` with the game's current directory as `target_dir`.
|
|
16
16
|
2. Confirm the status reports one consistent installed Engine version/commit and an
|
|
17
17
|
exact matching DevKit/carrier. Never hardcode a historical release identity.
|
|
18
|
-
3. Read the game's `forge.json`, source, package declarations, and
|
|
19
|
-
`skills/` installed with that game. Engine declarations are in its installed
|
|
18
|
+
3. Read the game's `forge.json`, source, package declarations, and task-relevant
|
|
19
|
+
Engine-owned `skills/` installed with that game. Engine declarations are in its installed
|
|
20
20
|
`node_modules/@forgeax/engine*` packages; never substitute Studio or Editor source.
|
|
21
21
|
|
|
22
|
+
Status describes runtime readiness, not a requirement to launch the template first.
|
|
23
|
+
When gameplay depends on a library asset, follow the installed asset-library Skill
|
|
24
|
+
to establish availability and compatible candidates before game implementation or
|
|
25
|
+
a baseline Preview. Other tasks may still need a baseline to reproduce a bug.
|
|
26
|
+
|
|
22
27
|
> [!IMPORTANT]
|
|
23
28
|
> In an empty directory, `forgeax-game init` creates the Engine-owned standalone game
|
|
24
29
|
> through the exact installed carrier. In an existing exact game it refreshes the
|
|
25
30
|
> binding idempotently. Follow `forge.json` and the installed Engine's authoring
|
|
26
|
-
> layout
|
|
27
|
-
>
|
|
31
|
+
> layout: Engine 0.2.1 uses `assets/` Pack sources and `forge.json#roots`, not a
|
|
32
|
+
> `plugins[]` list. Preserve the Engine-owned project instead of moving files to a
|
|
33
|
+
> prescribed directory. Do not create Studio's
|
|
28
34
|
> hosted `.forgeax/games/<slug>` layout inside a standalone game.
|
|
29
35
|
|
|
30
36
|
## Edit and verify
|
|
@@ -70,11 +76,9 @@ generation for a requested library asset without user approval.
|
|
|
70
76
|
> a successful result containing `preview.status: ready`, `preview_url`, `preview.root`,
|
|
71
77
|
> `preview.build_digest`, and `preview.instance_id` authorizes a Preview claim.
|
|
72
78
|
|
|
73
|
-
The connector runs the exact installed Engine CLI with `build --json`, then
|
|
74
|
-
reuses `preview --json`.
|
|
75
|
-
|
|
76
|
-
Released Engine 0.1.7 instead uses its documented minimal envelope plus an exact
|
|
77
|
-
served `forgeax-dist.json` SHA. PID alone is not ownership evidence in either mode.
|
|
79
|
+
The connector runs the exact installed Engine CLI with `project build --json`, then
|
|
80
|
+
starts or reuses `project preview --json`. It binds Preview to the canonical root,
|
|
81
|
+
exact release, build digest, and Preview instance ID; PID alone is not ownership.
|
|
78
82
|
|
|
79
83
|
## Stop
|
|
80
84
|
|
package/dist/main.js
CHANGED
|
@@ -335,6 +335,7 @@ import { fileURLToPath } from "node:url";
|
|
|
335
335
|
import { readFileSync, realpathSync } from "node:fs";
|
|
336
336
|
import { dirname as dirname2, join as join2, resolve } from "node:path";
|
|
337
337
|
var SLUG_RE = /^[a-z0-9][a-z0-9-]{0,40}$/;
|
|
338
|
+
var GUID_RE = /^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$/i;
|
|
338
339
|
function isProjectRoot(dir) {
|
|
339
340
|
return engineGameId(dir) !== undefined;
|
|
340
341
|
}
|
|
@@ -342,7 +343,7 @@ function engineGameId(root) {
|
|
|
342
343
|
try {
|
|
343
344
|
const manifest = JSON.parse(readFileSync(join2(root, "forge.json"), "utf8"));
|
|
344
345
|
const pkg = JSON.parse(readFileSync(join2(root, "package.json"), "utf8"));
|
|
345
|
-
return typeof manifest.id === "string" && SLUG_RE.test(manifest.id) && (manifest.schemaVersion === "
|
|
346
|
+
return typeof manifest.id === "string" && SLUG_RE.test(manifest.id) && (manifest.schemaVersion === "3.0.0" ? manifest.roots !== null && typeof manifest.roots === "object" && !Array.isArray(manifest.roots) && Object.entries(manifest.roots).every(([realm, guid]) => ["host", "frontend", "engine", "build"].includes(realm) && typeof guid === "string" && GUID_RE.test(guid)) : manifest.schemaVersion === "2.0.0" ? typeof manifest.defaultScene === "string" && GUID_RE.test(manifest.defaultScene) : (manifest.schemaVersion === undefined || manifest.schemaVersion === "1.0.0") && typeof manifest.entry === "string") && typeof pkg.dependencies?.["@forgeax/engine"] === "string" ? manifest.id : undefined;
|
|
346
347
|
} catch {
|
|
347
348
|
return;
|
|
348
349
|
}
|
|
@@ -718,8 +719,8 @@ import { existsSync as existsSync3, lstatSync as lstatSync3, readFileSync as rea
|
|
|
718
719
|
import { isAbsolute as isAbsolute2, relative as relative3, resolve as resolve4, sep as sep2 } from "node:path";
|
|
719
720
|
|
|
720
721
|
// src/engine/constants.ts
|
|
721
|
-
var ENGINE_VERSION = "0.1
|
|
722
|
-
var ENGINE_COMMIT = "
|
|
722
|
+
var ENGINE_VERSION = "0.2.1";
|
|
723
|
+
var ENGINE_COMMIT = "d699bc46d1e55b8dbd34b0839f85378b575bc9e2";
|
|
723
724
|
var ENGINE_SDK_PACKAGE = "@forgeax/engine-sdk";
|
|
724
725
|
var PNPM_VERSION = "11.7.0";
|
|
725
726
|
|
|
@@ -1080,7 +1081,7 @@ function parseSuccessEnvelope(output, command) {
|
|
|
1080
1081
|
} catch (error) {
|
|
1081
1082
|
throw new Error(`engine_sdk_${command}_envelope_invalid: ${error instanceof Error ? error.message : String(error)}`);
|
|
1082
1083
|
}
|
|
1083
|
-
if (value === null || typeof value !== "object" || Array.isArray(value) || value.schemaVersion !==
|
|
1084
|
+
if (value === null || typeof value !== "object" || Array.isArray(value) || value.schemaVersion !== undefined || value.command !== `project ${command}` || !Array.isArray(value.artifacts) || value.ok !== true || value.value === null || typeof value.value !== "object" || Array.isArray(value.value)) {
|
|
1084
1085
|
throw new Error(`engine_sdk_${command}_envelope_invalid: expected one ${command} success envelope`);
|
|
1085
1086
|
}
|
|
1086
1087
|
return value;
|
|
@@ -1116,13 +1117,13 @@ async function createEmptyGameWithCarrier(targetRoot, options = {}) {
|
|
|
1116
1117
|
const shim = createPnpmShim(carrier.pnpmCliPath);
|
|
1117
1118
|
const environment = carrierEnvironment(shim.root);
|
|
1118
1119
|
try {
|
|
1119
|
-
const initOutput = await runCarrierProcess(carrier, ["init", "--json"], environment);
|
|
1120
|
+
const initOutput = await runCarrierProcess(carrier, ["project", "init", "--json"], environment);
|
|
1120
1121
|
if (initOutput.status !== 0) {
|
|
1121
1122
|
throw new Error(`engine_sdk_init_failed: ${failureDetail(initOutput, "init")}`);
|
|
1122
1123
|
}
|
|
1123
1124
|
const init = parseSuccessEnvelope(initOutput.stdout, "init");
|
|
1124
1125
|
assertIdentity("init", init);
|
|
1125
|
-
const newOutput = await runCarrierProcess(carrier, ["new", target, "--template", "empty", "--json"], environment);
|
|
1126
|
+
const newOutput = await runCarrierProcess(carrier, ["project", "new", "--root", target, "--template", "empty", "--json"], environment);
|
|
1126
1127
|
if (newOutput.status !== 0) {
|
|
1127
1128
|
throw new Error(`engine_sdk_new_failed: ${failureDetail(newOutput, "new")}`);
|
|
1128
1129
|
}
|
|
@@ -1426,7 +1427,7 @@ function parseEnvelope(stdout, command) {
|
|
|
1426
1427
|
if (envelopeIndex !== lines.length - 1) {
|
|
1427
1428
|
throw new Error(`${command}_envelope_invalid: diagnostics after JSON frame`);
|
|
1428
1429
|
}
|
|
1429
|
-
if (envelope.schemaVersion !==
|
|
1430
|
+
if (envelope.schemaVersion !== undefined || !Array.isArray(envelope.artifacts) || envelope.command !== `project ${command}` || typeof envelope.ok !== "boolean") {
|
|
1430
1431
|
throw new Error(`${command}_envelope_invalid: wrong schema or command`);
|
|
1431
1432
|
}
|
|
1432
1433
|
if (!envelope.ok) {
|
|
@@ -1467,7 +1468,7 @@ async function terminateDirectChild(child, cleanupMs) {
|
|
|
1467
1468
|
}
|
|
1468
1469
|
}
|
|
1469
1470
|
async function runBuild(cliPath, gameRoot, paths, deadlineAt, cleanupMs) {
|
|
1470
|
-
const child = spawn(process.execPath, [cliPath, "build", "--json"], {
|
|
1471
|
+
const child = spawn(process.execPath, [cliPath, "project", "build", "--json"], {
|
|
1471
1472
|
cwd: gameRoot,
|
|
1472
1473
|
env: { ...process.env },
|
|
1473
1474
|
stdio: ["ignore", "pipe", "pipe"]
|
|
@@ -1698,7 +1699,7 @@ async function startEnginePreview(projectRoot, gameRoot, options = {}) {
|
|
|
1698
1699
|
}
|
|
1699
1700
|
const token = randomBytes(32).toString("hex");
|
|
1700
1701
|
const previewInstanceId = randomUUID();
|
|
1701
|
-
const child = spawn(process.execPath, [release.cliPath, "preview", "--port", "0", "--json"], {
|
|
1702
|
+
const child = spawn(process.execPath, [release.cliPath, "project", "preview", "--port", "0", "--json"], {
|
|
1702
1703
|
cwd: release.gameRoot,
|
|
1703
1704
|
detached: true,
|
|
1704
1705
|
env: { ...process.env, FORGEAX_PREVIEW_INSTANCE_TOKEN: token },
|
|
@@ -1843,7 +1844,7 @@ function deriveNextAction(s) {
|
|
|
1843
1844
|
if (s.preview?.live && s.preview.identityMatches) {
|
|
1844
1845
|
return "Engine Preview is live. Edit the game and call `forgeax_run_current_game` to rebuild or reuse it.";
|
|
1845
1846
|
}
|
|
1846
|
-
return "
|
|
1847
|
+
return "When Preview is needed, call `forgeax_run_current_game` for the bounded Engine build and verified Preview lifecycle. Resolve task prerequisites first; status does not require an empty-template baseline run.";
|
|
1847
1848
|
}
|
|
1848
1849
|
async function collectStatus(explicitDir) {
|
|
1849
1850
|
const project = resolveProject(explicitDir);
|
|
@@ -2954,7 +2955,7 @@ function gameFileTools() {
|
|
|
2954
2955
|
var package_default = {
|
|
2955
2956
|
packageManager: "bun@1.4.0",
|
|
2956
2957
|
name: "@forgeax/game",
|
|
2957
|
-
version: "0.3.
|
|
2958
|
+
version: "0.3.9",
|
|
2958
2959
|
private: false,
|
|
2959
2960
|
type: "module",
|
|
2960
2961
|
description: "@forgeax/game — an MCP/CLI connector for exact released ForgeaX Engine games and Engine-owned Preview.",
|
|
@@ -2998,7 +2999,7 @@ var package_default = {
|
|
|
2998
2999
|
],
|
|
2999
3000
|
license: "MIT",
|
|
3000
3001
|
dependencies: {
|
|
3001
|
-
"@forgeax/engine-sdk": "0.1
|
|
3002
|
+
"@forgeax/engine-sdk": "0.2.1",
|
|
3002
3003
|
pnpm: "11.7.0"
|
|
3003
3004
|
},
|
|
3004
3005
|
devDependencies: {
|
|
@@ -5218,18 +5219,20 @@ async function updateCommand(args) {
|
|
|
5218
5219
|
return 0;
|
|
5219
5220
|
}
|
|
5220
5221
|
async function extensionCommand(id, args) {
|
|
5222
|
+
const pretty = args.includes("--pretty");
|
|
5223
|
+
args = args.filter((arg) => arg !== "--pretty");
|
|
5221
5224
|
const [operation, ...rest] = args;
|
|
5222
5225
|
const emit = (ok, value) => process.stdout.write(JSON.stringify({
|
|
5223
5226
|
schemaVersion: "1.0.0",
|
|
5224
5227
|
command: `${id}.${operation}`,
|
|
5225
5228
|
ok,
|
|
5226
5229
|
...ok ? { value } : { error: value }
|
|
5227
|
-
}) + `
|
|
5230
|
+
}, null, pretty ? 2 : undefined) + `
|
|
5228
5231
|
`);
|
|
5229
5232
|
try {
|
|
5230
5233
|
const extension = discoverExtensions().find((item) => item.id === id);
|
|
5231
5234
|
if (operation === "help" || operation === "--help") {
|
|
5232
|
-
process.stdout.write(`${id}: enable [--ide ...] [--local], disable, or a business operation documented in its Skill.
|
|
5235
|
+
process.stdout.write(`${id}: enable [--ide ...] [--local], disable, or a business operation documented in its Skill. Add --pretty for readable JSON; values and exit status are unchanged.
|
|
5233
5236
|
`);
|
|
5234
5237
|
return 0;
|
|
5235
5238
|
}
|
package/docs/asset3d.md
CHANGED
|
@@ -6,18 +6,23 @@ Pack 或 GLB,再交给固定版本 Engine 导入。没有 Python Provider、As
|
|
|
6
6
|
## 使用
|
|
7
7
|
|
|
8
8
|
```bash
|
|
9
|
-
npx -y @forgeax/game@0.3.
|
|
9
|
+
npx -y @forgeax/game@0.3.9 install --ide codex
|
|
10
10
|
# 在空目录
|
|
11
|
-
npx -y @forgeax/game@0.3.
|
|
12
|
-
npx -y @forgeax/game@0.3.
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
npx -y @forgeax/game@0.3.
|
|
11
|
+
npx -y @forgeax/game@0.3.9 init
|
|
12
|
+
npx -y @forgeax/game@0.3.9 asset3d enable --ide codex --library ea --base-url "<获授权的 EA 网关>"
|
|
13
|
+
scratch="$(mktemp -d /tmp/forgeax-selection.XXXXXX)"
|
|
14
|
+
npx -y @forgeax/game@0.3.9 asset3d candidates --query "wooden crate" --engine-version 0.2.1 --json > "$scratch/candidates.json"
|
|
15
|
+
npx -y @forgeax/game@0.3.9 asset3d import --query "wooden crate" --engine-version 0.2.1 \
|
|
16
|
+
--candidate-file "$scratch/candidates.json" --candidate-name "<候选的完整名称>" --json
|
|
16
17
|
```
|
|
17
18
|
|
|
18
|
-
|
|
19
|
-
`node "/absolute/consumer/node_modules/@forgeax/game/dist/main.js"` 替代 npx
|
|
20
|
-
|
|
19
|
+
发布前本地候选验证可用已安装包的
|
|
20
|
+
`node "/absolute/consumer/node_modules/@forgeax/game/dist/main.js"` 替代 npx 前缀,
|
|
21
|
+
启用时加 `--local`。`--candidate-name` 使用搜索响应中的完整名称;若短文件名唯一也可使用。
|
|
22
|
+
|
|
23
|
+
> [!IMPORTANT]
|
|
24
|
+
> 导入只接受所选版本明确声明兼容当前 Engine 的资产。若 `0.2.1` 筛选后无候选,
|
|
25
|
+
> 应请资产库发布兼容版本;不要去掉版本约束强行导入旧 Pack。
|
|
21
26
|
|
|
22
27
|
默认库为 AW;EA 需要 `--library ea --base-url "<获授权的网关>"`,
|
|
23
28
|
也可设置 `FORGEAX_ASSET_LIBRARY_BASE_URL`。显式 URL 不会在失败后回退。
|
|
@@ -35,14 +40,39 @@ npx -y @forgeax/game@0.3.8 asset3d import --query "wooden crate" --asset-id "<
|
|
|
35
40
|
|:--|:--|
|
|
36
41
|
| enable | 探测服务,保存配置和 Skill |
|
|
37
42
|
| doctor --json | 本地配置、Engine 与 CLI 身份;可恢复中断的提交,不代表远端可访问 |
|
|
38
|
-
| candidates --query … --json |
|
|
39
|
-
| import --query … --asset-id … --json |
|
|
43
|
+
| candidates [--query …] --json | 返回描述、风格、缩略图、版本等选择信息;不暴露资产签名下载 URL |
|
|
44
|
+
| import [--query …] (--asset-id … \| --candidate-file … --candidate-name …) [--version …] --json | 从保存的候选按唯一名称取 ID/当前版本,或传入显式 ID;按同一查询/筛选重新确认,再下载、校验、Engine 导入和身份读回 |
|
|
40
45
|
| disable --json | 撤销 Skill、配置和临时数据;修改过的 Skill 移到非发现目录备份,保留资产和共享凭证 |
|
|
41
46
|
|
|
42
47
|
原生 `.pack.ts/.pack.json` 及其辅助文件保留完整;不伪装成 GLB。
|
|
43
48
|
导入源位于 `assets/3d/ea-3d/<assetId>`,结果返回 `deliveredFormat` 与 Engine GUID。
|
|
44
49
|
同摘要资产校验后可复用;已有内容发生变化时失败,不静默覆盖。
|
|
45
50
|
|
|
51
|
+
## 选择和使用资产
|
|
52
|
+
|
|
53
|
+
搜索以 1–3 个英文关键词表达对象/用途,默认类型为 1(3D 模型),混合检索阈值 0.3、
|
|
54
|
+
最多 10 个候选。`--query` 可省略,成功响应缺失/空 `asset_list` 返回空候选。
|
|
55
|
+
不在 CLI 中自动改写用户需求或取消筛选;Skill 引导模型在无结果时放宽非必要条件一次。
|
|
56
|
+
|
|
57
|
+
| 参数 | 含义 |
|
|
58
|
+
|:--|:--|
|
|
59
|
+
| `--asset-type` | 1 模型、2 纹理、3 BGM、5 动画模型、6 VFX、7 音效、8 材质、9 天空盒、11 Kit |
|
|
60
|
+
| `--category` | 类别硬筛选,3/11 不适用 |
|
|
61
|
+
| `--art-style` / `--theme-style` | 服务枚举硬筛选,3/7/11 不适用 |
|
|
62
|
+
| `--engine-version` | 单一 Engine 版本硬筛选;不推测版本号 |
|
|
63
|
+
| `--version` | 仅 import:明确选择候选 `versions[].versionName` |
|
|
64
|
+
| `--candidate-file` / `--candidate-name` | 仅 import:从已保存的 `candidates --json` 响应选唯一全名或文件名,避免手抄长 ID;不能与 `--asset-id` 同用 |
|
|
65
|
+
|
|
66
|
+
模型使用 `description`、`detailedDescription`、`category`、`artStyle`、`themeStyle`、
|
|
67
|
+
`customTags` 和可选 `thumbnailUrl` 判断用途与视觉匹配,不只按分数或排名。
|
|
68
|
+
缩略图链接是临时预览数据,不写入项目源文件;资产下载 URL 由 CLI 内部使用。
|
|
69
|
+
版本列表保留 `engineVersions`,缺失字段不等于兼容;版本筛选匹配到非当前版本时,
|
|
70
|
+
必须显式选择该版本,不能回退下载顶层/current URL。
|
|
71
|
+
|
|
72
|
+
搜索类型支持不等于导入格式支持:目前仍只导入 Pack/GLB 及包含它们的 ZIP,
|
|
73
|
+
不承诺独立音频、图片或任意 Kit 可被 Engine 导入。导入后读取资产 README/manifest,
|
|
74
|
+
检查尺寸、朝向、材质、动画和碰撞能力,再通过 GUID 实例化并在 Preview 验证。
|
|
75
|
+
|
|
46
76
|
请求不跟随重定向,下载只允许 enable 确认的来源,不带 API Key;
|
|
47
77
|
ZIP 解压限制路径、文件数、单文件和总大小。下载器不执行源码,
|
|
48
78
|
但 Engine 构建 Pack 会执行其中代码,因此资产库必须受信。
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"packageManager": "bun@1.4.0",
|
|
3
3
|
"name": "@forgeax/game",
|
|
4
|
-
"version": "0.3.
|
|
4
|
+
"version": "0.3.9",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
7
7
|
"description": "@forgeax/game — an MCP/CLI connector for exact released ForgeaX Engine games and Engine-owned Preview.",
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
],
|
|
46
46
|
"license": "MIT",
|
|
47
47
|
"dependencies": {
|
|
48
|
-
"@forgeax/engine-sdk": "0.1
|
|
48
|
+
"@forgeax/engine-sdk": "0.2.1",
|
|
49
49
|
"pnpm": "11.7.0"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|