@forgeax/engine-import 0.1.28 → 0.1.30
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 +15 -0
- package/dist/__tests__/scriptable-pack-fingerprint.unit.test.d.ts +2 -0
- package/dist/__tests__/scriptable-pack-fingerprint.unit.test.d.ts.map +1 -0
- package/dist/build-production.d.ts.map +1 -1
- package/dist/index.mjs +1250 -1144
- package/dist/index.mjs.map +1 -1
- package/dist/scriptable-pack-build.d.ts.map +1 -1
- package/dist/scriptable-pack-file-snapshot.d.ts.map +1 -1
- package/dist/scriptable-pack-fingerprint.d.ts +3 -0
- package/dist/scriptable-pack-fingerprint.d.ts.map +1 -0
- package/dist/scriptable-pack-output-producers.d.ts.map +1 -1
- package/dist/scriptable-pack-staged-snapshot.d.ts.map +1 -1
- package/dist/scriptable-pack.d.ts +9 -4
- package/dist/scriptable-pack.d.ts.map +1 -1
- package/package.json +8 -8
- package/src/__tests__/scriptable-pack-build.unit.test.ts +193 -1
- package/src/__tests__/scriptable-pack-fingerprint.unit.test.ts +65 -0
- package/src/__tests__/scriptable-pack-host.unit.test.ts +4 -4
- package/src/__tests__/scriptable-pack-production-producers.unit.test.ts +65 -26
- package/src/build-production.ts +3 -7
- package/src/scriptable-pack-build.ts +129 -35
- package/src/scriptable-pack-file-snapshot.ts +2 -28
- package/src/scriptable-pack-fingerprint.ts +48 -0
- package/src/scriptable-pack-output-producers.ts +24 -2
- package/src/scriptable-pack-staged-snapshot.ts +2 -22
- package/src/scriptable-pack.ts +9 -3
package/src/build-production.ts
CHANGED
|
@@ -109,16 +109,11 @@ function runtimePublicationFor(
|
|
|
109
109
|
...(input.digest === undefined ? {} : { digest: input.digest }),
|
|
110
110
|
...(input.inputFingerprint === undefined ? {} : { inputFingerprint: input.inputFingerprint }),
|
|
111
111
|
...(input.outputs === undefined ? {} : { outputs: input.outputs }),
|
|
112
|
+
...(input.sourceKeys === undefined ? {} : { sourceKeys: input.sourceKeys }),
|
|
112
113
|
...(input.externalEvidence === undefined ? {} : { externalEvidence: input.externalEvidence }),
|
|
113
114
|
generation: context.runtimeBinding?.generation ?? context.generation,
|
|
114
115
|
};
|
|
115
|
-
|
|
116
|
-
const derived = createRuntimePackPublication(publicationInput);
|
|
117
|
-
const outputs = derived.publication.outputs.map((output) => ({
|
|
118
|
-
...output,
|
|
119
|
-
sourceKey: input.sourceKeys?.get(output.guid.toLowerCase()) ?? output.sourceKey,
|
|
120
|
-
}));
|
|
121
|
-
return createRuntimePackPublication({ ...publicationInput, outputs });
|
|
116
|
+
return createRuntimePackPublication(publicationInput);
|
|
122
117
|
}
|
|
123
118
|
|
|
124
119
|
function sourceKeysFor(
|
|
@@ -622,6 +617,7 @@ async function emitAuthoredPack(
|
|
|
622
617
|
};
|
|
623
618
|
const runtimePublication = runtimePublicationFor(work.context, {
|
|
624
619
|
pack: authoredPack,
|
|
620
|
+
sourceKeys: sourceKeysFor(legacy.assets),
|
|
625
621
|
sourcePath: entry.sourcePath,
|
|
626
622
|
sourceRevision: declaration.sourceRevision,
|
|
627
623
|
packageUrl:
|
|
@@ -9,6 +9,8 @@ import {
|
|
|
9
9
|
PackageId,
|
|
10
10
|
type PackBuildContextWithoutParameters,
|
|
11
11
|
type PackBuildContextWithParameters,
|
|
12
|
+
type PackCookSource,
|
|
13
|
+
type PackOutputMap,
|
|
12
14
|
type PackParameterDefinition,
|
|
13
15
|
type PackParameterValue,
|
|
14
16
|
resolvePackParameterValues,
|
|
@@ -18,6 +20,7 @@ import type {
|
|
|
18
20
|
Asset,
|
|
19
21
|
AssetGuid as AssetGuidType,
|
|
20
22
|
AssetPublicationEnvelope,
|
|
23
|
+
AssetRef,
|
|
21
24
|
ImportedAsset,
|
|
22
25
|
Result,
|
|
23
26
|
} from '@forgeax/engine-types';
|
|
@@ -37,6 +40,8 @@ import type {
|
|
|
37
40
|
ScriptablePackSourceClosureEntry,
|
|
38
41
|
ScriptablePackStagedOutput,
|
|
39
42
|
} from './scriptable-pack.js';
|
|
43
|
+
import { scriptablePackFingerprint as digest } from './scriptable-pack-fingerprint.js';
|
|
44
|
+
import { materialAssetOutputProducer } from './scriptable-pack-output-producers.js';
|
|
40
45
|
|
|
41
46
|
export interface ScriptablePackBuildOptions {
|
|
42
47
|
readonly definition: AnyScriptablePackDefinition;
|
|
@@ -80,7 +85,7 @@ export type ScriptablePackBuildResult = Result<
|
|
|
80
85
|
|
|
81
86
|
interface ObservedRead {
|
|
82
87
|
readonly guid: string;
|
|
83
|
-
readonly asset: Asset;
|
|
88
|
+
readonly asset: Asset | PackCookSource;
|
|
84
89
|
readonly generation: number;
|
|
85
90
|
readonly digest: string;
|
|
86
91
|
}
|
|
@@ -101,35 +106,17 @@ function clone<T>(value: T): T {
|
|
|
101
106
|
return structuredClone(value);
|
|
102
107
|
}
|
|
103
108
|
|
|
104
|
-
function stable(value: unknown): string {
|
|
105
|
-
if (value instanceof Uint8Array) return JSON.stringify(Array.from(value));
|
|
106
|
-
if (Array.isArray(value)) return `[${value.map(stable).join(',')}]`;
|
|
107
|
-
if (value !== null && typeof value === 'object') {
|
|
108
|
-
const object = value as Record<string, unknown>;
|
|
109
|
-
return `{${Object.keys(object)
|
|
110
|
-
.sort()
|
|
111
|
-
.map((key) => `${JSON.stringify(key)}:${stable(object[key])}`)
|
|
112
|
-
.join(',')}}`;
|
|
113
|
-
}
|
|
114
|
-
return JSON.stringify(value) ?? 'null';
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
async function digest(value: unknown): Promise<string> {
|
|
118
|
-
const crypto = globalThis.crypto?.subtle;
|
|
119
|
-
if (crypto === undefined) throw new Error('Web Crypto API is required for Pack fingerprints');
|
|
120
|
-
const bytes = await crypto.digest('SHA-256', new TextEncoder().encode(stable(value)));
|
|
121
|
-
return `sha256:${Array.from(new Uint8Array(bytes), (byte) => byte.toString(16).padStart(2, '0')).join('')}`;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
109
|
function observedReader(source: ScriptablePackAssetSnapshotSource | undefined): {
|
|
125
110
|
readonly reader: {
|
|
126
|
-
readByGuid<TAsset extends Asset = Asset>(
|
|
111
|
+
readByGuid<TAsset extends Asset | PackCookSource = Asset>(
|
|
112
|
+
guid: AssetGuidType,
|
|
113
|
+
): Promise<Result<TAsset, unknown>>;
|
|
127
114
|
};
|
|
128
115
|
readonly reads: ReadonlyMap<string, ObservedRead>;
|
|
129
116
|
} {
|
|
130
117
|
const reads = new Map<string, ObservedRead>();
|
|
131
118
|
const reader = {
|
|
132
|
-
async readByGuid<TAsset extends Asset = Asset>(
|
|
119
|
+
async readByGuid<TAsset extends Asset | PackCookSource = Asset>(
|
|
133
120
|
guid: AssetGuidType,
|
|
134
121
|
): Promise<Result<TAsset, unknown>> {
|
|
135
122
|
const key = AssetGuid.format(guid).toLowerCase();
|
|
@@ -218,7 +205,7 @@ function referenceError(
|
|
|
218
205
|
function productError(
|
|
219
206
|
sourcePath: string,
|
|
220
207
|
sourceKey: string,
|
|
221
|
-
producer: AssetOutputProducer,
|
|
208
|
+
producer: Pick<AssetOutputProducer, 'kind'>,
|
|
222
209
|
product: unknown,
|
|
223
210
|
): PackAuthoringError | undefined {
|
|
224
211
|
if (!record(product)) {
|
|
@@ -338,9 +325,7 @@ export async function buildScriptablePack(
|
|
|
338
325
|
});
|
|
339
326
|
}
|
|
340
327
|
|
|
341
|
-
let built:
|
|
342
|
-
| Result<Record<string, Asset>, unknown>
|
|
343
|
-
| Promise<Result<Record<string, Asset>, unknown>>;
|
|
328
|
+
let built: Result<PackOutputMap, unknown> | Promise<Result<PackOutputMap, unknown>>;
|
|
344
329
|
try {
|
|
345
330
|
const context = buildContext(subjectPackageId, effectiveValues, observed.reader);
|
|
346
331
|
built = options.definition.build(context as never);
|
|
@@ -392,14 +377,32 @@ export async function buildScriptablePack(
|
|
|
392
377
|
const imported: ImportedAsset<unknown>[] = [];
|
|
393
378
|
const stagedOutputs: ScriptablePackStagedOutput[] = [];
|
|
394
379
|
const localGuids = new Set<string>();
|
|
395
|
-
const
|
|
396
|
-
for (const cooker of options.cookers ?? [])
|
|
380
|
+
const cookerRegistry = new NativeCookerRegistry();
|
|
381
|
+
for (const cooker of options.cookers ?? []) cookerRegistry.register(cooker);
|
|
397
382
|
const nativeFingerprints = new Map<string, string>();
|
|
398
383
|
for (const sourceKey of Object.keys(built.value).sort()) {
|
|
399
384
|
if (!isValidPackSourceKey(sourceKey))
|
|
400
385
|
return err(sourceKeyFailure(options.sourcePath, sourceKey));
|
|
401
386
|
const asset = built.value[sourceKey];
|
|
402
|
-
|
|
387
|
+
const customSource = record(asset) && asset.execution === 'cooked';
|
|
388
|
+
if (
|
|
389
|
+
customSource &&
|
|
390
|
+
(typeof asset.kind !== 'string' ||
|
|
391
|
+
asset.kind.trim().length === 0 ||
|
|
392
|
+
isScriptablePackAssetKind(asset.kind) ||
|
|
393
|
+
!('source' in asset) ||
|
|
394
|
+
Object.keys(asset).some((key) => !['kind', 'execution', 'source'].includes(key)))
|
|
395
|
+
) {
|
|
396
|
+
return err(
|
|
397
|
+
outputValueError(
|
|
398
|
+
options.sourcePath,
|
|
399
|
+
sourceKey,
|
|
400
|
+
'a custom kind with only execution: cooked and source; ordinary Assets use their standard producers',
|
|
401
|
+
asset,
|
|
402
|
+
),
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
if (!customSource && !isAsset(asset)) {
|
|
403
406
|
return err(
|
|
404
407
|
outputValueError(
|
|
405
408
|
options.sourcePath,
|
|
@@ -420,6 +423,47 @@ export async function buildScriptablePack(
|
|
|
420
423
|
});
|
|
421
424
|
}
|
|
422
425
|
localGuids.add(normalizedGuid);
|
|
426
|
+
if (customSource) {
|
|
427
|
+
const cooked = await cookerRegistry.runDraft(asset.kind, {
|
|
428
|
+
guid,
|
|
429
|
+
sourceKey,
|
|
430
|
+
sourcePath: options.sourcePath,
|
|
431
|
+
source: asset.source,
|
|
432
|
+
refs: [],
|
|
433
|
+
});
|
|
434
|
+
if (!cooked.ok) return err(cooked.error);
|
|
435
|
+
if (cooked.value.guid.toLowerCase() !== normalizedGuid)
|
|
436
|
+
return err({
|
|
437
|
+
code: 'pack-source-output-invalid',
|
|
438
|
+
expected: 'the custom cooker to preserve the derived AssetGuid',
|
|
439
|
+
hint: 'remove cooker-owned identity generation and retain the host GUID',
|
|
440
|
+
detail: {
|
|
441
|
+
sourcePath: options.sourcePath,
|
|
442
|
+
sourceKey,
|
|
443
|
+
expectedGuid: guid,
|
|
444
|
+
actualGuid: cooked.value.guid,
|
|
445
|
+
},
|
|
446
|
+
});
|
|
447
|
+
const product = {
|
|
448
|
+
payload: cooked.value.payload,
|
|
449
|
+
refs: cooked.value.refs.map((guid) => ({ guid })),
|
|
450
|
+
artifacts: cooked.value.artifacts,
|
|
451
|
+
};
|
|
452
|
+
const invalid = productError(options.sourcePath, sourceKey, { kind: asset.kind }, product);
|
|
453
|
+
if (invalid !== undefined) return err(invalid);
|
|
454
|
+
imported.push({ guid, kind: asset.kind, ...product });
|
|
455
|
+
nativeFingerprints.set(sourceKey, cooked.value.inputFingerprint);
|
|
456
|
+
stagedOutputs.push({
|
|
457
|
+
guid: AssetGuid.derive(subjectPackageId, sourceKey),
|
|
458
|
+
sourceKey,
|
|
459
|
+
asset: clone(asset),
|
|
460
|
+
digest: await digest(asset),
|
|
461
|
+
});
|
|
462
|
+
continue;
|
|
463
|
+
}
|
|
464
|
+
// A custom source has been handled above; ordinary producers retain the closed Asset union.
|
|
465
|
+
if (!isAsset(asset))
|
|
466
|
+
return err(outputValueError(options.sourcePath, sourceKey, 'an ordinary Asset', asset));
|
|
423
467
|
const producer = options.outputs.get(asset.kind);
|
|
424
468
|
if (producer === undefined) {
|
|
425
469
|
return err(
|
|
@@ -472,8 +516,9 @@ export async function buildScriptablePack(
|
|
|
472
516
|
const product = produced.value as ImportAssetProduct<unknown>;
|
|
473
517
|
let payload = product.payload;
|
|
474
518
|
let artifacts = product.artifacts;
|
|
475
|
-
|
|
476
|
-
|
|
519
|
+
let refs = product.refs;
|
|
520
|
+
if (needsMaterialCook(asset) && cookerRegistry.get('material') !== undefined) {
|
|
521
|
+
const cooked = await cookerRegistry.runDraft('material', {
|
|
477
522
|
guid,
|
|
478
523
|
source: asset,
|
|
479
524
|
sourceKey,
|
|
@@ -494,15 +539,64 @@ export async function buildScriptablePack(
|
|
|
494
539
|
},
|
|
495
540
|
});
|
|
496
541
|
}
|
|
497
|
-
|
|
498
|
-
|
|
542
|
+
// Native cooking owns shader/program composition, but the Pack output
|
|
543
|
+
// producer owns the serialized wire shape. Re-project the cooked
|
|
544
|
+
// material once so texture/sampler/parent values become refsIndex
|
|
545
|
+
// entries again instead of leaking authored GUID strings into JSON.
|
|
546
|
+
const cookedAsset = cooked.value.payload;
|
|
547
|
+
if (!isAsset(cookedAsset) || cookedAsset.kind !== 'material') {
|
|
548
|
+
return err({
|
|
549
|
+
code: 'pack-source-output-invalid',
|
|
550
|
+
expected: 'the authored material cooker to return a material payload',
|
|
551
|
+
hint: 'repair the native material cooker payload and rebuild the Pack',
|
|
552
|
+
detail: { sourcePath: options.sourcePath, sourceKey },
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
const projected = await materialAssetOutputProducer.produce({
|
|
556
|
+
guid,
|
|
557
|
+
sourceKey,
|
|
558
|
+
asset: cookedAsset,
|
|
559
|
+
});
|
|
560
|
+
if (!projected.ok) return err(projected.error);
|
|
561
|
+
const projectedInvalid = productError(
|
|
562
|
+
options.sourcePath,
|
|
563
|
+
sourceKey,
|
|
564
|
+
materialAssetOutputProducer,
|
|
565
|
+
projected.value,
|
|
566
|
+
);
|
|
567
|
+
if (projectedInvalid !== undefined) return err(projectedInvalid);
|
|
568
|
+
payload = projected.value.payload;
|
|
569
|
+
artifacts = {
|
|
570
|
+
...product.artifacts,
|
|
571
|
+
...cooked.value.artifacts,
|
|
572
|
+
...projected.value.artifacts,
|
|
573
|
+
};
|
|
574
|
+
const mergedRefs: AssetRef[] = [...projected.value.refs];
|
|
575
|
+
const seenRefs = new Set(mergedRefs.map((reference) => reference.guid.toLowerCase()));
|
|
576
|
+
for (const cookerRef of cooked.value.refs) {
|
|
577
|
+
const parsed = AssetGuid.parse(cookerRef);
|
|
578
|
+
if (!parsed.ok) {
|
|
579
|
+
return err({
|
|
580
|
+
code: 'pack-source-output-invalid',
|
|
581
|
+
expected: 'the authored material cooker refs to contain valid AssetGuid values',
|
|
582
|
+
hint: 'repair the native material cooker refs and rebuild the Pack',
|
|
583
|
+
detail: { sourcePath: options.sourcePath, sourceKey, ref: cookerRef },
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
const formatted = AssetGuid.format(parsed.value);
|
|
587
|
+
const normalizedRef = formatted.toLowerCase();
|
|
588
|
+
if (seenRefs.has(normalizedRef)) continue;
|
|
589
|
+
seenRefs.add(normalizedRef);
|
|
590
|
+
mergedRefs.push({ guid: formatted });
|
|
591
|
+
}
|
|
592
|
+
refs = mergedRefs;
|
|
499
593
|
nativeFingerprints.set(sourceKey, cooked.value.inputFingerprint);
|
|
500
594
|
}
|
|
501
595
|
imported.push({
|
|
502
596
|
guid,
|
|
503
597
|
kind: asset.kind,
|
|
504
598
|
payload,
|
|
505
|
-
refs
|
|
599
|
+
refs,
|
|
506
600
|
artifacts,
|
|
507
601
|
});
|
|
508
602
|
stagedOutputs.push({
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { createHash } from 'node:crypto';
|
|
2
1
|
import { readdir, readFile, stat } from 'node:fs/promises';
|
|
3
2
|
import { resolve } from 'node:path';
|
|
4
3
|
import { isScriptablePackAssetKind, validatePack } from '@forgeax/engine-pack';
|
|
@@ -9,6 +8,7 @@ import type {
|
|
|
9
8
|
ScriptablePackAssetSnapshot,
|
|
10
9
|
ScriptablePackAssetSnapshotSource,
|
|
11
10
|
} from './scriptable-pack.js';
|
|
11
|
+
import { scriptablePackFingerprint as digest } from './scriptable-pack-fingerprint.js';
|
|
12
12
|
|
|
13
13
|
export interface ScriptablePackFileAssetSnapshotSourceOptions {
|
|
14
14
|
readonly assetRoots: readonly string[];
|
|
@@ -90,30 +90,6 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
|
|
90
90
|
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
function stable(value: unknown): string {
|
|
94
|
-
if (value instanceof ArrayBuffer) {
|
|
95
|
-
return `ArrayBuffer:${JSON.stringify(Array.from(new Uint8Array(value)))}`;
|
|
96
|
-
}
|
|
97
|
-
if (ArrayBuffer.isView(value)) {
|
|
98
|
-
return `${value.constructor.name}:${JSON.stringify(
|
|
99
|
-
Array.from(new Uint8Array(value.buffer, value.byteOffset, value.byteLength)),
|
|
100
|
-
)}`;
|
|
101
|
-
}
|
|
102
|
-
if (Array.isArray(value)) return `[${value.map(stable).join(',')}]`;
|
|
103
|
-
if (value !== null && typeof value === 'object') {
|
|
104
|
-
const record = value as Record<string, unknown>;
|
|
105
|
-
return `{${Object.keys(record)
|
|
106
|
-
.sort()
|
|
107
|
-
.map((key) => `${JSON.stringify(key)}:${stable(record[key])}`)
|
|
108
|
-
.join(',')}}`;
|
|
109
|
-
}
|
|
110
|
-
return JSON.stringify(value) ?? 'null';
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
function digest(value: unknown): string {
|
|
114
|
-
return `sha256:${createHash('sha256').update(stable(value)).digest('hex')}`;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
93
|
function generationFromDigest(value: string): number {
|
|
118
94
|
const parsed = Number.parseInt(value.slice('sha256:'.length, 'sha256:'.length + 8), 16);
|
|
119
95
|
return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : 1;
|
|
@@ -302,9 +278,7 @@ async function indexPackFiles(
|
|
|
302
278
|
}
|
|
303
279
|
}
|
|
304
280
|
|
|
305
|
-
const sourceDigest = digest(
|
|
306
|
-
[...documents].sort((left, right) => stable(left).localeCompare(stable(right))),
|
|
307
|
-
);
|
|
281
|
+
const sourceDigest = digest(documents.map(digest).sort());
|
|
308
282
|
return ok({
|
|
309
283
|
generation: generationFromDigest(sourceDigest),
|
|
310
284
|
assets: byGuid,
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { sha256 } from '@noble/hashes/sha2.js';
|
|
2
|
+
import { bytesToHex } from '@noble/hashes/utils.js';
|
|
3
|
+
|
|
4
|
+
/** One bounded fingerprint encoding for authored, staged and file-backed assets. */
|
|
5
|
+
export function scriptablePackFingerprint(value: unknown): string {
|
|
6
|
+
const hash = sha256.create();
|
|
7
|
+
const encoder = new TextEncoder();
|
|
8
|
+
const text = (part: string) => {
|
|
9
|
+
hash.update(encoder.encode(part));
|
|
10
|
+
};
|
|
11
|
+
const binary = (type: string, bytes: Uint8Array) => {
|
|
12
|
+
// Raw NUL cannot occur in JSON tokens. The type and byte length delimit the
|
|
13
|
+
// binary lane without expanding every float into a sorted JSON property.
|
|
14
|
+
text(`\0${type}:${bytes.byteLength}:`);
|
|
15
|
+
hash.update(bytes);
|
|
16
|
+
text('\0');
|
|
17
|
+
};
|
|
18
|
+
const visit = (item: unknown): void => {
|
|
19
|
+
if (ArrayBuffer.isView(item)) {
|
|
20
|
+
binary(item.constructor.name, new Uint8Array(item.buffer, item.byteOffset, item.byteLength));
|
|
21
|
+
} else if (item instanceof ArrayBuffer) {
|
|
22
|
+
binary('ArrayBuffer', new Uint8Array(item));
|
|
23
|
+
} else if (Array.isArray(item)) {
|
|
24
|
+
text('[');
|
|
25
|
+
for (let i = 0; i < item.length; i++) {
|
|
26
|
+
if (i) text(',');
|
|
27
|
+
visit(item[i]);
|
|
28
|
+
}
|
|
29
|
+
text(']');
|
|
30
|
+
} else if (item !== null && typeof item === 'object') {
|
|
31
|
+
const object = item as Record<string, unknown>;
|
|
32
|
+
text('{');
|
|
33
|
+
let first = true;
|
|
34
|
+
for (const key of Object.keys(object).sort()) {
|
|
35
|
+
if (!first) text(',');
|
|
36
|
+
first = false;
|
|
37
|
+
text(`${JSON.stringify(key)}:`);
|
|
38
|
+
visit(object[key]);
|
|
39
|
+
}
|
|
40
|
+
text('}');
|
|
41
|
+
} else {
|
|
42
|
+
text(JSON.stringify(item) ?? 'null');
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
text('scriptable-pack-fingerprint/2:');
|
|
46
|
+
visit(value);
|
|
47
|
+
return `sha256:${bytesToHex(hash.digest())}`;
|
|
48
|
+
}
|
|
@@ -466,9 +466,24 @@ function ordinaryPodProduct(input: AssetOutputInput): AssetOutputProduct {
|
|
|
466
466
|
case 'particle-effect': {
|
|
467
467
|
const effect = asset as ParticleEffectAsset;
|
|
468
468
|
const cooked = particleProgramArtifact(effect);
|
|
469
|
+
const refs = new Set<string>();
|
|
470
|
+
for (const emitter of cooked.program.emitters) {
|
|
471
|
+
for (const renderer of emitter.renderers) {
|
|
472
|
+
if (!('material' in renderer) || typeof renderer.material !== 'string') {
|
|
473
|
+
throw new Error(`Particle emitter '${emitter.id}' requires a renderer Material GUID`);
|
|
474
|
+
}
|
|
475
|
+
refs.add(formatGuid(renderer.material));
|
|
476
|
+
if ('kind' in renderer && renderer.kind === 'mesh') {
|
|
477
|
+
if (!('mesh' in renderer) || typeof renderer.mesh !== 'string') {
|
|
478
|
+
throw new Error(`Particle emitter '${emitter.id}' requires a renderer Mesh GUID`);
|
|
479
|
+
}
|
|
480
|
+
refs.add(formatGuid(renderer.mesh));
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
}
|
|
469
484
|
return {
|
|
470
485
|
payload: { ...effect, programFingerprint: cooked.fingerprint, program: cooked.program },
|
|
471
|
-
refs: [],
|
|
486
|
+
refs: [...refs].sort().map((guid) => ({ guid })),
|
|
472
487
|
artifacts: {
|
|
473
488
|
'particle-effect/program.json': {
|
|
474
489
|
mediaType: 'application/json',
|
|
@@ -504,6 +519,7 @@ export const materialAssetOutputProducer = createSafeProducer(
|
|
|
504
519
|
'material-pack/2',
|
|
505
520
|
materialProduct,
|
|
506
521
|
);
|
|
522
|
+
|
|
507
523
|
export const meshAssetOutputProducer = createSafeProducer('mesh', 'mesh-binary/4', meshProduct);
|
|
508
524
|
export const textureAssetOutputProducer = createSafeProducer(
|
|
509
525
|
'texture',
|
|
@@ -547,7 +563,13 @@ export function createStandardAssetOutputProducerRegistry(
|
|
|
547
563
|
'particle-effect',
|
|
548
564
|
'ies-profile',
|
|
549
565
|
] as const) {
|
|
550
|
-
registry.register(
|
|
566
|
+
registry.register(
|
|
567
|
+
createSafeProducer(
|
|
568
|
+
kind,
|
|
569
|
+
kind === 'particle-effect' ? 'particle-effect/2' : 'ordinary-pod/1',
|
|
570
|
+
ordinaryPodProduct,
|
|
571
|
+
),
|
|
572
|
+
);
|
|
551
573
|
}
|
|
552
574
|
return registry;
|
|
553
575
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { AssetGuid } from '@forgeax/engine-pack/guid';
|
|
2
2
|
import type { PackAuthoringError } from '@forgeax/engine-pack/source';
|
|
3
|
-
import type {
|
|
3
|
+
import type { AssetGuid as AssetGuidType, ImportError, Result } from '@forgeax/engine-types';
|
|
4
4
|
import { AssetError, err, ok } from '@forgeax/engine-types';
|
|
5
5
|
import type {
|
|
6
6
|
ScriptablePackAssetSnapshot,
|
|
@@ -8,6 +8,7 @@ import type {
|
|
|
8
8
|
ScriptablePackDomainError,
|
|
9
9
|
ScriptablePackStagedOutput,
|
|
10
10
|
} from './scriptable-pack.js';
|
|
11
|
+
import { scriptablePackFingerprint as assetDigest } from './scriptable-pack-fingerprint.js';
|
|
11
12
|
|
|
12
13
|
export type ScriptablePackSnapshotError =
|
|
13
14
|
| AssetError
|
|
@@ -29,27 +30,6 @@ export interface ScriptablePackStagedSnapshotOptions {
|
|
|
29
30
|
readonly declaredExternalOutputs?: readonly ScriptablePackStagedOutput[];
|
|
30
31
|
}
|
|
31
32
|
|
|
32
|
-
function stable(value: unknown): string {
|
|
33
|
-
if (ArrayBuffer.isView(value)) {
|
|
34
|
-
return `${value.constructor.name}:${JSON.stringify(Array.from(new Uint8Array(value.buffer, value.byteOffset, value.byteLength)))}`;
|
|
35
|
-
}
|
|
36
|
-
if (Array.isArray(value)) return `[${value.map(stable).join(',')}]`;
|
|
37
|
-
if (value !== null && typeof value === 'object') {
|
|
38
|
-
const record = value as Record<string, unknown>;
|
|
39
|
-
return `{${Object.keys(record)
|
|
40
|
-
.sort()
|
|
41
|
-
.map((key) => `${JSON.stringify(key)}:${stable(record[key])}`)
|
|
42
|
-
.join(',')}}`;
|
|
43
|
-
}
|
|
44
|
-
return JSON.stringify(value) ?? 'null';
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
async function assetDigest(asset: Asset): Promise<string> {
|
|
48
|
-
const bytes = new TextEncoder().encode(stable(asset));
|
|
49
|
-
const digest = await globalThis.crypto.subtle.digest('SHA-256', bytes);
|
|
50
|
-
return `sha256:${Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, '0')).join('')}`;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
33
|
function cycleError(
|
|
54
34
|
stack: readonly string[],
|
|
55
35
|
owner: string,
|
package/src/scriptable-pack.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { ScriptablePackAssetKind } from '@forgeax/engine-pack';
|
|
|
2
2
|
import type {
|
|
3
3
|
AssetReader,
|
|
4
4
|
PackAuthoringError,
|
|
5
|
+
PackCookSource,
|
|
5
6
|
ScriptablePackReadError,
|
|
6
7
|
} from '@forgeax/engine-pack/source';
|
|
7
8
|
import type {
|
|
@@ -22,7 +23,7 @@ import type { ImportAssetProduct, TerminalImportProduct } from './import-product
|
|
|
22
23
|
export type { ScriptablePackSourceClosureEntry } from '@forgeax/engine-pack/source';
|
|
23
24
|
|
|
24
25
|
export interface ScriptablePackAssetSnapshot {
|
|
25
|
-
readonly asset: Asset;
|
|
26
|
+
readonly asset: Asset | PackCookSource;
|
|
26
27
|
readonly generation: number;
|
|
27
28
|
readonly digest: string;
|
|
28
29
|
}
|
|
@@ -30,7 +31,7 @@ export interface ScriptablePackAssetSnapshot {
|
|
|
30
31
|
export interface ScriptablePackStagedOutput {
|
|
31
32
|
readonly guid: AssetGuidType;
|
|
32
33
|
readonly sourceKey: string;
|
|
33
|
-
readonly asset: Asset;
|
|
34
|
+
readonly asset: Asset | PackCookSource;
|
|
34
35
|
readonly digest?: string;
|
|
35
36
|
}
|
|
36
37
|
|
|
@@ -44,6 +45,8 @@ export interface AssetOutputInput {
|
|
|
44
45
|
readonly guid: string;
|
|
45
46
|
readonly sourceKey: string;
|
|
46
47
|
readonly asset: Asset;
|
|
48
|
+
/** Filesystem-backed authoring host; absent for pure payload projections. */
|
|
49
|
+
readonly sourcePath?: string;
|
|
47
50
|
}
|
|
48
51
|
|
|
49
52
|
type MaterialPackPayload = Omit<MaterialAsset, 'parent' | 'values'> & {
|
|
@@ -91,7 +94,10 @@ export type AssetOutputPayloadByKind = {
|
|
|
91
94
|
};
|
|
92
95
|
|
|
93
96
|
export type AssetOutputPayload = AssetOutputPayloadByKind[ScriptablePackAssetKind];
|
|
94
|
-
export
|
|
97
|
+
export interface AssetOutputProduct extends ImportAssetProduct<AssetOutputPayload> {
|
|
98
|
+
readonly inputFingerprint?: string;
|
|
99
|
+
readonly sourceDependencies?: readonly string[];
|
|
100
|
+
}
|
|
95
101
|
|
|
96
102
|
export interface AssetOutputProducer {
|
|
97
103
|
readonly kind: string;
|