@forgeax/engine-pack 0.1.32 → 0.1.34

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.
@@ -1,4 +1,4 @@
1
- import { mkdtemp, readFile, rm } from 'node:fs/promises';
1
+ import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
2
2
  import { tmpdir } from 'node:os';
3
3
  import { join } from 'node:path';
4
4
  import { afterEach, describe, expect, it } from 'vitest';
@@ -296,3 +296,73 @@ describe('Pack authoring gateway', () => {
296
296
  expect(verified).toMatchObject({ ok: true, value: { summary: { assetCount: 1 } } });
297
297
  });
298
298
  });
299
+
300
+ it('inspects the exact source revision used by rebuild conflict checks', async () => {
301
+ const root = await mkdtemp(join(tmpdir(), 'forgeax-inspect-revision-'));
302
+ roots.push(root);
303
+ const outputs = [
304
+ {
305
+ packageId: SOURCE_PACKAGE,
306
+ sourceKey: 'mesh/main',
307
+ guid: AssetGuid.format(AssetGuid.derive(packageId(SOURCE_PACKAGE), 'mesh/main')),
308
+ kind: 'mesh',
309
+ sourcePath: 'assets/source.pack.ts',
310
+ ready: true,
311
+ },
312
+ ];
313
+ let published = false;
314
+ const gateway = createFileSystemPackAuthoringGateway({
315
+ gameRoot: root,
316
+ materialized: () => (published ? outputs : []),
317
+ rebuild: async () => ({ ok: true, value: undefined }),
318
+ });
319
+ const created = await gateway.execute({
320
+ operation: 'asset-source.create',
321
+ requestId: 'create',
322
+ targetPath: 'assets/source.pack.ts',
323
+ format: 'pack.ts',
324
+ packageId: SOURCE_PACKAGE,
325
+ });
326
+ expect(created.ok).toBe(true);
327
+ if (!created.ok) return;
328
+ published = true;
329
+ const inspected = await gateway.execute({
330
+ operation: 'asset.inspect',
331
+ subject: 'assets/source.pack.ts',
332
+ requestId: 'inspect',
333
+ });
334
+ expect(inspected).toMatchObject({
335
+ ok: true,
336
+ value: {
337
+ revision: created.value.revision,
338
+ sourcePath: 'assets/source.pack.ts',
339
+ assets: outputs,
340
+ },
341
+ });
342
+ const rebuilt = await gateway.execute({
343
+ operation: 'asset-source.rebuild',
344
+ sourcePath: 'assets/source.pack.ts',
345
+ expectedRevision: created.value.revision,
346
+ requestId: 'rebuild',
347
+ });
348
+ expect(rebuilt).toMatchObject({
349
+ ok: true,
350
+ value: { assets: outputs, revision: created.value.revision },
351
+ });
352
+ const path = join(root, 'assets/source.pack.ts');
353
+ await writeFile(path, `${await readFile(path, 'utf8')}\n// source edit\n`);
354
+ const changed = await gateway.execute({
355
+ operation: 'asset.inspect',
356
+ subject: 'assets/source.pack.ts',
357
+ requestId: 'inspect-edited',
358
+ });
359
+ expect(changed.ok).toBe(true);
360
+ if (changed.ok) expect(changed.value.revision).not.toBe(created.value.revision);
361
+ const stale = await gateway.execute({
362
+ operation: 'asset-source.rebuild',
363
+ sourcePath: 'assets/source.pack.ts',
364
+ expectedRevision: created.value.revision,
365
+ requestId: 'stale',
366
+ });
367
+ expect(stale).toMatchObject({ ok: false, error: { code: 'pack-source-revision-conflict' } });
368
+ });
@@ -21,6 +21,32 @@ function packageId(value: string) {
21
21
  }
22
22
 
23
23
  describe('ScriptablePack and Pack authoring', () => {
24
+ it('accepts boolean and string defaults and overrides without coercion', () => {
25
+ const definition = definePack({
26
+ schemaVersion: '2.0.0',
27
+ packageId: definePackageId('01900000-0000-7000-8000-000000000031'),
28
+ parameters: [
29
+ { name: 'animated', type: 'bool', default: true },
30
+ { name: 'label', type: 'string', default: 'smoke' },
31
+ ],
32
+ build: () => ok({ 'scene/main': { kind: 'scene', entities: {} } }),
33
+ });
34
+ expect(resolvePackParameterValues(definition, {})).toMatchObject({
35
+ ok: true,
36
+ value: { animated: true, label: 'smoke' },
37
+ });
38
+ expect(resolvePackParameterValues(definition, { animated: false, label: '' })).toMatchObject({
39
+ ok: true,
40
+ value: { animated: false, label: '' },
41
+ });
42
+ for (const values of [{ animated: 'false' }, { animated: 0 }, { label: false }, { label: 0 }]) {
43
+ expect(resolvePackParameterValues(definition, values)).toMatchObject({
44
+ ok: false,
45
+ error: { code: 'pack-parameter-invalid' },
46
+ });
47
+ }
48
+ });
49
+
24
50
  it('derives the RFC 4122 UUIDv5 vector and keeps PackageId branded separately', () => {
25
51
  const namespace = packageId(NAMESPACE);
26
52
  expect(AssetGuid.format(AssetGuid.derive(namespace, 'www.widgets.com'))).toBe(
@@ -4,6 +4,7 @@ import type {
4
4
  MaterialPass,
5
5
  MaterialProgramAbi,
6
6
  MaterialProgramAddress,
7
+ MaterialSurfaceDeclaration,
7
8
  MaterialTextureReference,
8
9
  MaterialTextureValue,
9
10
  MaterialValue,
@@ -13,6 +14,7 @@ import {
13
14
  deriveStandardLayerPlan,
14
15
  err,
15
16
  isMaterialProgramAbi,
17
+ isMaterialSurfaceDeclaration,
16
18
  MATERIAL_TEXTURE_SLOTS,
17
19
  ok,
18
20
  } from '@forgeax/engine-types';
@@ -120,6 +122,7 @@ export interface CookedMaterialRecord {
120
122
  readonly passes: readonly MaterialPass[];
121
123
  readonly parameters: readonly MaterialParameter[];
122
124
  readonly values: Readonly<Record<string, MaterialValue | null>>;
125
+ readonly surface?: MaterialSurfaceDeclaration;
123
126
  };
124
127
  readonly refs: MaterialCookRefs;
125
128
  readonly receipt: MaterialCookReceipt;
@@ -231,7 +234,10 @@ export function collectMaterialCookRefs(material: Partial<MaterialAsset>): Mater
231
234
  return guid === undefined ? [] : [guid];
232
235
  }),
233
236
  ),
234
- modules: unique((material.passes ?? []).map((pass) => pass.program.module)),
237
+ modules: unique([
238
+ ...(material.passes ?? []).map((pass) => pass.program.module),
239
+ ...(material.surface === undefined ? [] : [material.surface.module]),
240
+ ]),
235
241
  };
236
242
  }
237
243
 
@@ -476,6 +482,8 @@ export function validateCookedMaterialRecord(
476
482
  Array.isArray(resolved.values)
477
483
  )
478
484
  return invalid('resolved.values', resolved.values);
485
+ if (resolved.surface !== undefined && !isMaterialSurfaceDeclaration(resolved.surface))
486
+ return invalid('resolved.surface', resolved.surface);
479
487
  const passes = resolved.passes as MaterialPass[];
480
488
  const passNames = new Set<string>();
481
489
  for (const [index, pass] of passes.entries()) {
@@ -817,6 +817,19 @@ async function inspectAsset(
817
817
  const selected = subjectFor(gameRoot, snapshot, operation);
818
818
  if (!selected.ok) return selected;
819
819
  const subject = selected.value;
820
+ const source = await readConfined({ gameRoot }, operation, subject.relativePath);
821
+ if (!source.ok) return source;
822
+ const inspectedSource = {
823
+ ...sourceResult(subject, operation),
824
+ revision: source.value.revision,
825
+ ...(subject.format === 'source'
826
+ ? {
827
+ assets: snapshot.materialized
828
+ .filter((asset) => asset.packageId === packageKey(subject.packageId))
829
+ .map((asset) => ({ ...asset })),
830
+ }
831
+ : {}),
832
+ };
820
833
  if (operation.sourceKey !== undefined) {
821
834
  if (!isValidPackSourceKey(operation.sourceKey)) {
822
835
  return err(
@@ -851,7 +864,7 @@ async function inspectAsset(
851
864
  candidate.sourceKey === operation.sourceKey,
852
865
  );
853
866
  return ok({
854
- ...sourceResult(subject, operation),
867
+ ...inspectedSource,
855
868
  sourceKey: asset.sourceKey,
856
869
  guid: asset.guid,
857
870
  kind: asset.kind,
@@ -871,7 +884,7 @@ async function inspectAsset(
871
884
  candidate.sourceKey === operation.sourceKey,
872
885
  );
873
886
  return ok({
874
- ...sourceResult(subject, operation),
887
+ ...inspectedSource,
875
888
  sourceKey: operation.sourceKey,
876
889
  guid,
877
890
  ...(materialized === undefined
@@ -897,7 +910,7 @@ async function inspectAsset(
897
910
  const resolved = await resolveInstance(snapshot, subject);
898
911
  if (!resolved.ok) return resolved;
899
912
  return ok({
900
- ...sourceResult(subject, operation),
913
+ ...inspectedSource,
901
914
  parameters: resolved.value.parameters.map((parameter) => ({
902
915
  name: parameter.name,
903
916
  type: parameter.type,
@@ -911,7 +924,7 @@ async function inspectAsset(
911
924
  parentChain: resolved.value.parentChain,
912
925
  });
913
926
  }
914
- return ok(sourceResult(subject, operation));
927
+ return ok(inspectedSource);
915
928
  }
916
929
 
917
930
  function listAssets(
@@ -2188,6 +2201,9 @@ async function executeRaw(
2188
2201
  selected.value.packageId,
2189
2202
  {
2190
2203
  format: refreshed.value.relative.endsWith('.pack.json') ? 'pack.json' : 'pack.ts',
2204
+ assets: refreshedSnapshot.value.materialized
2205
+ .filter((asset) => asset.packageId === packageKey(selected.value.packageId))
2206
+ .map((asset) => ({ ...asset })),
2191
2207
  },
2192
2208
  );
2193
2209
  }
@@ -256,8 +256,10 @@ function parameterValueError(
256
256
  propertyPath: string,
257
257
  ): PackAuthoringError | undefined {
258
258
  const { type } = parameter;
259
- if (type === 'bool' && typeof value !== 'boolean') {
260
- return parameterFailure(propertyPath, 'a boolean', value, 'bool value has the wrong type');
259
+ if (type === 'bool') {
260
+ return typeof value === 'boolean'
261
+ ? undefined
262
+ : parameterFailure(propertyPath, 'a boolean', value, 'bool value has the wrong type');
261
263
  }
262
264
  if (numericType(type)) {
263
265
  if (typeof value !== 'number' || !Number.isFinite(value)) {
@@ -318,8 +320,10 @@ function parameterValueError(
318
320
  }
319
321
  return undefined;
320
322
  }
321
- if (type === 'string' && typeof value !== 'string') {
322
- return parameterFailure(propertyPath, 'a string', value, 'string parameter has the wrong type');
323
+ if (type === 'string') {
324
+ return typeof value === 'string'
325
+ ? undefined
326
+ : parameterFailure(propertyPath, 'a string', value, 'string parameter has the wrong type');
323
327
  }
324
328
  if (type === 'enum') {
325
329
  if (typeof value !== 'string') {
@@ -52,7 +52,80 @@
52
52
  },
53
53
  "resolved": {
54
54
  "type": "object",
55
- "required": ["passes", "parameters", "values"]
55
+ "required": ["passes", "parameters", "values"],
56
+ "properties": {
57
+ "surface": {
58
+ "type": "object",
59
+ "required": ["model", "module"],
60
+ "properties": {
61
+ "model": {
62
+ "enum": ["standard", "single-layer-medium"]
63
+ },
64
+ "module": {
65
+ "type": "string",
66
+ "minLength": 1
67
+ },
68
+ "dynamicInput": {
69
+ "type": "object",
70
+ "required": [
71
+ "name",
72
+ "fields",
73
+ "maxRecords",
74
+ "maxDomains",
75
+ "maxPageBytes",
76
+ "maxBindings",
77
+ "maxEventsPerSample"
78
+ ],
79
+ "properties": {
80
+ "name": {
81
+ "type": "string",
82
+ "pattern": "^[A-Za-z_][A-Za-z0-9_]*$"
83
+ },
84
+ "fields": {
85
+ "type": "array",
86
+ "minItems": 1,
87
+ "items": {
88
+ "type": "object",
89
+ "required": ["name", "type"],
90
+ "properties": {
91
+ "name": {
92
+ "type": "string",
93
+ "pattern": "^[A-Za-z_][A-Za-z0-9_]*$"
94
+ },
95
+ "type": {
96
+ "enum": ["f32", "u32", "vec2<f32>", "vec3<f32>", "vec4<f32>"]
97
+ }
98
+ },
99
+ "additionalProperties": false
100
+ }
101
+ },
102
+ "maxRecords": {
103
+ "type": "integer",
104
+ "minimum": 1
105
+ },
106
+ "maxDomains": {
107
+ "type": "integer",
108
+ "minimum": 1
109
+ },
110
+ "maxPageBytes": {
111
+ "type": "integer",
112
+ "minimum": 1
113
+ },
114
+ "maxBindings": {
115
+ "type": "integer",
116
+ "minimum": 1
117
+ },
118
+ "maxEventsPerSample": {
119
+ "type": "integer",
120
+ "minimum": 1
121
+ }
122
+ },
123
+ "additionalProperties": false
124
+ }
125
+ },
126
+ "additionalProperties": false
127
+ }
128
+ }
56
129
  },
57
130
  "refs": {
58
131
  "type": "object",