@helix3/helix-cli 0.1.14-helix3.176 → 0.1.14-helix3.178
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/bridge-plugins/helix.bridge.unreal/bin/unreal-skeletal-mesh-extract.js +632 -0
- package/bridge-plugins/helix.bridge.unreal/bridge-plugin.json +40 -0
- package/bridge-plugins/helix.bridge.unreal/test/exporter.test.js +332 -0
- package/bridge-plugins/helix.bridge.unreal/test/handler.test.js +257 -0
- package/bridge-plugins/helix.bridge.unreal/unreal/export_skeletal_mesh.py +318 -0
- package/bridge-recipes/unreal/character-customizer/README.md +60 -0
- package/bridge-recipes/unreal/character-customizer/all.json +13 -0
- package/bridge-recipes/unreal/character-customizer/base-male-body.json +6 -0
- package/bridge-recipes/unreal/character-customizer/base-male-head.json +6 -0
- package/bridge-recipes/unreal/character-customizer/beard.addon.json +1 -0
- package/bridge-recipes/unreal/character-customizer/beard.json +6 -0
- package/bridge-recipes/unreal/character-customizer/bottom.addon.json +1 -0
- package/bridge-recipes/unreal/character-customizer/bottom.json +6 -0
- package/bridge-recipes/unreal/character-customizer/eyewear.addon.json +1 -0
- package/bridge-recipes/unreal/character-customizer/eyewear.json +6 -0
- package/bridge-recipes/unreal/character-customizer/footwear.addon.json +1 -0
- package/bridge-recipes/unreal/character-customizer/footwear.json +6 -0
- package/bridge-recipes/unreal/character-customizer/hair.addon.json +1 -0
- package/bridge-recipes/unreal/character-customizer/hair.json +6 -0
- package/bridge-recipes/unreal/character-customizer/test/recipes.test.js +74 -0
- package/bridge-recipes/unreal/character-customizer/top.addon.json +1 -0
- package/bridge-recipes/unreal/character-customizer/top.json +6 -0
- package/bridge-recipes/unreal/helix-character-creator-male/README.md +13 -0
- package/bridge-recipes/unreal/helix-character-creator-male/all.json +18 -0
- package/bridge-recipes/unreal/helix-character-creator-male/back.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/back.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/base-male-face.json +6 -0
- package/bridge-recipes/unreal/helix-character-creator-male/base-male.json +6 -0
- package/bridge-recipes/unreal/helix-character-creator-male/bottom.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/bottom.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/eyewear.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/eyewear.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/footwear.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/footwear.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/gloves.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/gloves.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/hair.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/hair.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/headwear.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/headwear.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/mask.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/mask.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/neck.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/neck.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/outfit.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/outfit.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/test/recipes.test.js +43 -0
- package/bridge-recipes/unreal/helix-character-creator-male/top.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/top.json +1 -0
- package/dist/bridge/characterAddon.d.ts +15 -0
- package/dist/bridge/characterAddon.js +89 -0
- package/dist/bridge/characterAddon.js.map +1 -0
- package/dist/bridge/detect.js +3 -1
- package/dist/bridge/detect.js.map +1 -1
- package/dist/bridge/execute.js +25 -7
- package/dist/bridge/execute.js.map +1 -1
- package/dist/bridge/glb.js +1 -0
- package/dist/bridge/glb.js.map +1 -1
- package/dist/bridge/index.d.ts +1 -0
- package/dist/bridge/index.js +1 -0
- package/dist/bridge/index.js.map +1 -1
- package/dist/bridge/internal/builtInExecution.js +143 -0
- package/dist/bridge/internal/builtInExecution.js.map +1 -1
- package/dist/bridge/internal/externalExecution.d.ts +9 -1
- package/dist/bridge/internal/externalExecution.js +145 -32
- package/dist/bridge/internal/externalExecution.js.map +1 -1
- package/dist/bridge/internal/outputReservation.d.ts +1 -1
- package/dist/bridge/internal/outputReservation.js +17 -11
- package/dist/bridge/internal/outputReservation.js.map +1 -1
- package/dist/bridge/internal/snapshots.js +76 -22
- package/dist/bridge/internal/snapshots.js.map +1 -1
- package/dist/bridge/manifest.js.map +1 -1
- package/dist/bridge/metahuman.js +85 -15
- package/dist/bridge/metahuman.js.map +1 -1
- package/dist/bridge/planner.js +30 -10
- package/dist/bridge/planner.js.map +1 -1
- package/dist/bridge/plugins.d.ts +1 -0
- package/dist/bridge/plugins.js +68 -11
- package/dist/bridge/plugins.js.map +1 -1
- package/dist/bridge/resolution.js +4 -2
- package/dist/bridge/resolution.js.map +1 -1
- package/dist/bridge/types.d.ts +24 -3
- package/dist/bridge/types.js +10 -3
- package/dist/bridge/types.js.map +1 -1
- package/dist/bridge/validate.js +178 -0
- package/dist/bridge/validate.js.map +1 -1
- package/dist/character/composeBaseMale.d.ts +25 -0
- package/dist/character/composeBaseMale.js +712 -0
- package/dist/character/composeBaseMale.js.map +1 -0
- package/dist/character/fbx.d.ts +34 -0
- package/dist/character/fbx.js +256 -0
- package/dist/character/fbx.js.map +1 -1
- package/dist/character/imageLimits.d.ts +3 -0
- package/dist/character/imageLimits.js +7 -0
- package/dist/character/imageLimits.js.map +1 -0
- package/dist/character/importCharacter.d.ts +44 -2
- package/dist/character/importCharacter.js +318 -22
- package/dist/character/importCharacter.js.map +1 -1
- package/dist/character/metahumanToHelixMap.d.ts +2 -0
- package/dist/character/metahumanToHelixMap.js +34 -0
- package/dist/character/metahumanToHelixMap.js.map +1 -0
- package/dist/character/stages/bakeTexture.d.ts +2 -0
- package/dist/character/stages/bakeTexture.js +8 -1
- package/dist/character/stages/bakeTexture.js.map +1 -1
- package/dist/character/stages/conform.js +133 -7
- package/dist/character/stages/conform.js.map +1 -1
- package/dist/character/stages/ktx2.d.ts +6 -4
- package/dist/character/stages/ktx2.js +81 -27
- package/dist/character/stages/ktx2.js.map +1 -1
- package/dist/character/stages/resizeTextures.js +4 -2
- package/dist/character/stages/resizeTextures.js.map +1 -1
- package/dist/character/stages/sanitizeMaterials.js +4 -2
- package/dist/character/stages/sanitizeMaterials.js.map +1 -1
- package/dist/character/unrealMaterialSidecar.d.ts +72 -0
- package/dist/character/unrealMaterialSidecar.js +649 -0
- package/dist/character/unrealMaterialSidecar.js.map +1 -0
- package/dist/character/untrustedSource.d.ts +5 -1
- package/dist/character/untrustedSource.js +19 -4
- package/dist/character/untrustedSource.js.map +1 -1
- package/dist/index.js +87 -3
- package/dist/index.js.map +1 -1
- package/dist/item.d.ts +10 -0
- package/dist/item.js +19 -2
- package/dist/item.js.map +1 -1
- package/dist/itemSearch.d.ts +128 -0
- package/dist/itemSearch.js +140 -0
- package/dist/itemSearch.js.map +1 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/docs/bridge.md +75 -18
- package/package.json +5 -3
|
@@ -0,0 +1,632 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const { spawn, spawnSync } = require('node:child_process');
|
|
5
|
+
const { createHash } = require('node:crypto');
|
|
6
|
+
const {
|
|
7
|
+
closeSync,
|
|
8
|
+
existsSync,
|
|
9
|
+
lstatSync,
|
|
10
|
+
openSync,
|
|
11
|
+
readFileSync,
|
|
12
|
+
readSync,
|
|
13
|
+
readdirSync,
|
|
14
|
+
realpathSync,
|
|
15
|
+
statSync,
|
|
16
|
+
} = require('node:fs');
|
|
17
|
+
const { basename, extname, isAbsolute, join, relative, resolve, sep } = require('node:path');
|
|
18
|
+
|
|
19
|
+
const REQUEST_SCHEMA = 'helix.bridge-plugin-request/1';
|
|
20
|
+
const PROFILE_SCHEMA = 'helix.bridge.unreal-asset/1';
|
|
21
|
+
const PROFILE_SET_SCHEMA = 'helix.bridge.unreal-asset-set/1';
|
|
22
|
+
const CAPABILITY = 'unreal.project.extract';
|
|
23
|
+
const MAX_PROFILE_BYTES = 64 * 1024;
|
|
24
|
+
const MAX_PROJECT_ENTRIES = 100_000;
|
|
25
|
+
const LOG_TAIL_BYTES = 16 * 1024;
|
|
26
|
+
const FBX_HEADER_BYTES = 1024;
|
|
27
|
+
const FBX_BINARY_HEADER = Buffer.from('Kaydara FBX Binary \0\x1a\0', 'binary');
|
|
28
|
+
const FBX_ASCII_HEADER = /^;\s+FBX\s+\d+\.\d+\.\d+\s+project\s+file(?:\r?\n|$)/;
|
|
29
|
+
const MATERIAL_SIDECAR_SCHEMA = 'helix.bridge.unreal-material-sidecar/1';
|
|
30
|
+
const MAX_MATERIAL_SIDECAR_BYTES = 16 * 1024 * 1024;
|
|
31
|
+
const PNG_HEADER = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
|
|
32
|
+
const SHA256 = /^[a-f0-9]{64}$/;
|
|
33
|
+
const TEXTURE_RELATIVE_PATH = /^textures\/([a-f0-9]{64})\.png$/;
|
|
34
|
+
const SLUG = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
35
|
+
const WINDOWS_ABSOLUTE_PATH = /^(?:[a-zA-Z]:[\\/]|\\\\)/;
|
|
36
|
+
const UNREAL_ASSET_PATH = /^\/[A-Za-z0-9_+]+\/(?:[A-Za-z0-9_+]+\/)*[A-Za-z0-9_+]+(?:\.[A-Za-z0-9_+]+)?$/;
|
|
37
|
+
const WSL_BRIDGE_ENVIRONMENT = [
|
|
38
|
+
'HELIX_UNREAL_ASSET_PATH',
|
|
39
|
+
'HELIX_UNREAL_OUTPUT_FBX',
|
|
40
|
+
'HELIX_UNREAL_EXPORT_MORPH_TARGETS',
|
|
41
|
+
'HELIX_UNREAL_EXPORTS_JSON',
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
function fail(message) {
|
|
45
|
+
throw new Error(message);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function object(value, label) {
|
|
49
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
50
|
+
fail(`${label} must be an object`);
|
|
51
|
+
}
|
|
52
|
+
return value;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function nonEmptyString(value, label) {
|
|
56
|
+
if (typeof value !== 'string' || value.trim() === '') {
|
|
57
|
+
fail(`${label} must be a non-empty string`);
|
|
58
|
+
}
|
|
59
|
+
return value;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function exactKeys(value, expected, label) {
|
|
63
|
+
const actual = Object.keys(value).sort();
|
|
64
|
+
const allowed = [...expected].sort();
|
|
65
|
+
if (actual.length !== allowed.length || actual.some((key, index) => key !== allowed[index])) {
|
|
66
|
+
fail(`${label} must contain exactly: ${allowed.join(', ')}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function finiteNumber(value, label) {
|
|
71
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) fail(`${label} must be a finite number`);
|
|
72
|
+
return value;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function portableUnrealPath(value, label) {
|
|
76
|
+
const path = nonEmptyString(value, label);
|
|
77
|
+
if (!UNREAL_ASSET_PATH.test(path) || path.includes('//') || path.includes('\\') || path.includes('..')) {
|
|
78
|
+
fail(`${label} must be a portable mounted Unreal asset path`);
|
|
79
|
+
}
|
|
80
|
+
return path;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function containedPath(root, candidate, label) {
|
|
84
|
+
const rel = relative(root, candidate);
|
|
85
|
+
if (rel === '' || rel === '..' || rel.startsWith(`..${sep}`) || isAbsolute(rel)) {
|
|
86
|
+
fail(`${label} escapes the reviewed output directory`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function regularFile(path, label, maximumBytes) {
|
|
91
|
+
let info;
|
|
92
|
+
try {
|
|
93
|
+
info = lstatSync(path);
|
|
94
|
+
} catch (error) {
|
|
95
|
+
fail(`${label} is unavailable: ${error.message}`);
|
|
96
|
+
}
|
|
97
|
+
if (!info.isFile() || info.isSymbolicLink()) fail(`${label} must be a regular non-symlink file`);
|
|
98
|
+
if (maximumBytes !== undefined && info.size > maximumBytes) {
|
|
99
|
+
fail(`${label} exceeds the ${maximumBytes}-byte limit`);
|
|
100
|
+
}
|
|
101
|
+
return info;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function hashAndSize(path) {
|
|
105
|
+
const digest = createHash('sha256');
|
|
106
|
+
const buffer = Buffer.alloc(1024 * 1024);
|
|
107
|
+
const descriptor = openSync(path, 'r');
|
|
108
|
+
let sizeBytes = 0;
|
|
109
|
+
try {
|
|
110
|
+
while (true) {
|
|
111
|
+
const bytesRead = readSync(descriptor, buffer, 0, buffer.length, null);
|
|
112
|
+
if (bytesRead === 0) break;
|
|
113
|
+
digest.update(buffer.subarray(0, bytesRead));
|
|
114
|
+
sizeBytes += bytesRead;
|
|
115
|
+
}
|
|
116
|
+
} finally {
|
|
117
|
+
closeSync(descriptor);
|
|
118
|
+
}
|
|
119
|
+
return { sha256: digest.digest('hex'), sizeBytes };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function hasHeader(path, expected) {
|
|
123
|
+
const header = Buffer.alloc(expected.length);
|
|
124
|
+
const descriptor = openSync(path, 'r');
|
|
125
|
+
try {
|
|
126
|
+
return readSync(descriptor, header, 0, header.length, 0) === header.length && header.equals(expected);
|
|
127
|
+
} finally {
|
|
128
|
+
closeSync(descriptor);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function readJsonFile(path, label) {
|
|
133
|
+
let info;
|
|
134
|
+
try {
|
|
135
|
+
info = lstatSync(path);
|
|
136
|
+
} catch (error) {
|
|
137
|
+
fail(`${label} is unavailable: ${error.message}`);
|
|
138
|
+
}
|
|
139
|
+
if (!info.isFile() || info.isSymbolicLink()) {
|
|
140
|
+
fail(`${label} must be a regular non-symlink file`);
|
|
141
|
+
}
|
|
142
|
+
if (info.size > MAX_PROFILE_BYTES) {
|
|
143
|
+
fail(`${label} exceeds the ${MAX_PROFILE_BYTES}-byte limit`);
|
|
144
|
+
}
|
|
145
|
+
try {
|
|
146
|
+
return JSON.parse(readFileSync(path, 'utf8'));
|
|
147
|
+
} catch (error) {
|
|
148
|
+
fail(`${label} is not valid JSON: ${error.message}`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function validateRequest(value) {
|
|
153
|
+
const request = object(value, 'Bridge request');
|
|
154
|
+
if (request.schemaVersion !== REQUEST_SCHEMA) {
|
|
155
|
+
fail(`Bridge request schemaVersion must be '${REQUEST_SCHEMA}'`);
|
|
156
|
+
}
|
|
157
|
+
if (request.capability !== CAPABILITY) {
|
|
158
|
+
fail(`Bridge request capability must be '${CAPABILITY}'`);
|
|
159
|
+
}
|
|
160
|
+
const source = object(request.source, 'Bridge request source');
|
|
161
|
+
if (source.sourceKind !== 'unreal.project') {
|
|
162
|
+
fail("Bridge request sourceKind must be 'unreal.project'");
|
|
163
|
+
}
|
|
164
|
+
const sourcePath = resolve(nonEmptyString(source.source, 'Bridge request source path'));
|
|
165
|
+
const outputDir = resolve(nonEmptyString(request.outputDir, 'Bridge request outputDir'));
|
|
166
|
+
const inputs = object(request.inputs, 'Bridge request inputs');
|
|
167
|
+
const assetInput = object(inputs.unrealAsset, "Bridge request input 'unrealAsset'");
|
|
168
|
+
const profilePath = resolve(nonEmptyString(assetInput.path, "Bridge request input 'unrealAsset'.path"));
|
|
169
|
+
return { sourcePath, outputDir, profilePath };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function validateAsset(value, label) {
|
|
173
|
+
const profile = object(value, label);
|
|
174
|
+
const slug = nonEmptyString(profile.slug, 'Unreal asset profile slug');
|
|
175
|
+
if (slug.length > 64 || !SLUG.test(slug)) {
|
|
176
|
+
fail('Unreal asset profile slug must be lowercase alphanumeric words separated by single hyphens');
|
|
177
|
+
}
|
|
178
|
+
const assetPath = nonEmptyString(profile.assetPath, 'Unreal asset profile assetPath');
|
|
179
|
+
if (!UNREAL_ASSET_PATH.test(assetPath) || assetPath.includes('//')) {
|
|
180
|
+
fail("Unreal asset profile assetPath must be a mounted virtual Unreal asset path such as '/Game/...' or '/PluginName/...'");
|
|
181
|
+
}
|
|
182
|
+
if (profile.exportMorphTargets !== undefined && typeof profile.exportMorphTargets !== 'boolean') {
|
|
183
|
+
fail('Unreal asset profile exportMorphTargets must be a boolean when provided');
|
|
184
|
+
}
|
|
185
|
+
return {
|
|
186
|
+
slug,
|
|
187
|
+
assetPath,
|
|
188
|
+
exportMorphTargets: profile.exportMorphTargets ?? true,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function validateProfile(value) {
|
|
193
|
+
const profile = object(value, 'Unreal asset profile');
|
|
194
|
+
if (profile.schemaVersion === PROFILE_SCHEMA) return [validateAsset(profile, 'Unreal asset profile')];
|
|
195
|
+
if (profile.schemaVersion !== PROFILE_SET_SCHEMA) {
|
|
196
|
+
fail(`Unreal asset profile schemaVersion must be '${PROFILE_SCHEMA}' or '${PROFILE_SET_SCHEMA}'`);
|
|
197
|
+
}
|
|
198
|
+
if (!Array.isArray(profile.assets) || profile.assets.length === 0 || profile.assets.length > 64) {
|
|
199
|
+
fail('Unreal asset-set profile assets must contain between 1 and 64 entries');
|
|
200
|
+
}
|
|
201
|
+
const assets = profile.assets.map((asset, index) => validateAsset(asset, `Unreal asset-set profile assets[${index}]`));
|
|
202
|
+
if (new Set(assets.map((asset) => asset.slug)).size !== assets.length) {
|
|
203
|
+
fail('Unreal asset-set profile slugs must be unique');
|
|
204
|
+
}
|
|
205
|
+
return assets;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function findProject(sourcePath) {
|
|
209
|
+
let root;
|
|
210
|
+
try {
|
|
211
|
+
root = lstatSync(sourcePath);
|
|
212
|
+
} catch (error) {
|
|
213
|
+
fail(`Unreal project snapshot is unavailable: ${error.message}`);
|
|
214
|
+
}
|
|
215
|
+
if (!root.isDirectory() || root.isSymbolicLink()) {
|
|
216
|
+
fail('Unreal project snapshot must be a regular directory');
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const projects = [];
|
|
220
|
+
const pending = [sourcePath];
|
|
221
|
+
let entryCount = 0;
|
|
222
|
+
while (pending.length > 0) {
|
|
223
|
+
const directory = pending.pop();
|
|
224
|
+
const entries = readdirSync(directory, { withFileTypes: true });
|
|
225
|
+
for (const entry of entries) {
|
|
226
|
+
entryCount += 1;
|
|
227
|
+
if (entryCount > MAX_PROJECT_ENTRIES) {
|
|
228
|
+
fail(`Unreal project snapshot exceeds the ${MAX_PROJECT_ENTRIES}-entry adapter limit`);
|
|
229
|
+
}
|
|
230
|
+
const path = join(directory, entry.name);
|
|
231
|
+
if (entry.isSymbolicLink()) fail(`Unreal project snapshot contains a symlink: ${path}`);
|
|
232
|
+
if (entry.isDirectory()) {
|
|
233
|
+
pending.push(path);
|
|
234
|
+
} else if (entry.isFile() && extname(entry.name).toLowerCase() === '.uproject') {
|
|
235
|
+
projects.push(path);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
if (projects.length !== 1) {
|
|
240
|
+
fail(`Unreal project snapshot must contain exactly one .uproject file; found ${projects.length}`);
|
|
241
|
+
}
|
|
242
|
+
return projects[0];
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function runningInWsl() {
|
|
246
|
+
if (process.platform !== 'linux') return false;
|
|
247
|
+
if (process.env.WSL_DISTRO_NAME || process.env.WSL_INTEROP) return true;
|
|
248
|
+
try {
|
|
249
|
+
return /microsoft/i.test(readFileSync('/proc/sys/kernel/osrelease', 'utf8'));
|
|
250
|
+
} catch {
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function wslPath(mode, path) {
|
|
256
|
+
const result = spawnSync('wslpath', [mode, path], {
|
|
257
|
+
encoding: 'utf8',
|
|
258
|
+
shell: false,
|
|
259
|
+
windowsHide: true,
|
|
260
|
+
});
|
|
261
|
+
if (result.error || result.status !== 0) {
|
|
262
|
+
const reason = result.error?.message || String(result.stderr || '').trim() || `exit ${result.status}`;
|
|
263
|
+
fail(`Unable to translate WSL path '${path}': ${reason}`);
|
|
264
|
+
}
|
|
265
|
+
return result.stdout.trim();
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function editorInvocationPaths(editor, project, script, outputs) {
|
|
269
|
+
const wsl = runningInWsl();
|
|
270
|
+
if (!wsl) return { editor, project, script, outputs, wsl };
|
|
271
|
+
return {
|
|
272
|
+
editor: WINDOWS_ABSOLUTE_PATH.test(editor) ? wslPath('-u', editor) : editor,
|
|
273
|
+
project: wslPath('-w', project),
|
|
274
|
+
script: wslPath('-w', script),
|
|
275
|
+
outputs: outputs.map((output) => wslPath('-w', output)),
|
|
276
|
+
wsl,
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function editorEnvironment(environment, wsl) {
|
|
281
|
+
if (!wsl) return environment;
|
|
282
|
+
const inherited = String(environment.WSLENV || '')
|
|
283
|
+
.split(':')
|
|
284
|
+
.filter(Boolean)
|
|
285
|
+
.filter((entry) => !WSL_BRIDGE_ENVIRONMENT.includes(entry.split('/')[0]));
|
|
286
|
+
return {
|
|
287
|
+
...environment,
|
|
288
|
+
WSLENV: [...inherited, ...WSL_BRIDGE_ENVIRONMENT].join(':'),
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function logTail() {
|
|
293
|
+
const chunks = [];
|
|
294
|
+
let size = 0;
|
|
295
|
+
return {
|
|
296
|
+
append(chunk) {
|
|
297
|
+
let next = Buffer.from(chunk);
|
|
298
|
+
if (next.length >= LOG_TAIL_BYTES) {
|
|
299
|
+
chunks.length = 0;
|
|
300
|
+
next = next.subarray(next.length - LOG_TAIL_BYTES);
|
|
301
|
+
chunks.push(next);
|
|
302
|
+
size = next.length;
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
chunks.push(next);
|
|
306
|
+
size += next.length;
|
|
307
|
+
while (size > LOG_TAIL_BYTES) {
|
|
308
|
+
const excess = size - LOG_TAIL_BYTES;
|
|
309
|
+
const first = chunks[0];
|
|
310
|
+
if (first.length <= excess) {
|
|
311
|
+
chunks.shift();
|
|
312
|
+
size -= first.length;
|
|
313
|
+
} else {
|
|
314
|
+
chunks[0] = first.subarray(excess);
|
|
315
|
+
size -= excess;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
},
|
|
319
|
+
text() {
|
|
320
|
+
return Buffer.concat(chunks, size).toString('utf8').trim();
|
|
321
|
+
},
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
async function runEditor(editor, args, environment) {
|
|
326
|
+
const stderr = logTail();
|
|
327
|
+
const stdout = logTail();
|
|
328
|
+
return new Promise((resolvePromise, rejectPromise) => {
|
|
329
|
+
const child = spawn(editor, args, {
|
|
330
|
+
env: environment,
|
|
331
|
+
shell: false,
|
|
332
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
333
|
+
windowsHide: true,
|
|
334
|
+
});
|
|
335
|
+
child.stdout.on('data', (chunk) => stdout.append(chunk));
|
|
336
|
+
child.stderr.on('data', (chunk) => stderr.append(chunk));
|
|
337
|
+
child.on('error', (error) => rejectPromise(new Error(`Unreal Editor failed to start: ${error.message}`)));
|
|
338
|
+
child.on('close', (code, signal) => {
|
|
339
|
+
resolvePromise({
|
|
340
|
+
code,
|
|
341
|
+
signal,
|
|
342
|
+
tail: [stdout.text(), stderr.text()].filter(Boolean).join('\n'),
|
|
343
|
+
});
|
|
344
|
+
});
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function editorFailure(outcome) {
|
|
349
|
+
return `Unreal Editor failed${outcome.signal ? ` with signal ${outcome.signal}` : ` with exit ${outcome.code}`}` +
|
|
350
|
+
`${outcome.tail ? `: ${outcome.tail}` : ''}`;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function isKnownBenignPlatformValidationShutdown(outcome) {
|
|
354
|
+
if (outcome.signal || outcome.code !== 5) return false;
|
|
355
|
+
return /Running UnrealBuildTool:\s+dotnet\s+"[^"\r\n]*UnrealBuildTool\.dll"\s+-Mode=ValidatePlatforms\b[^\r\n]*\s-AllPlatforms(?:\s|$)/i.test(outcome.tail) &&
|
|
356
|
+
/Unable to find required SDK version ['"]MainVersion['"] for platform VisionOS\. Check your SDK\.json files/i.test(outcome.tail);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function validateFbxOutput(output) {
|
|
360
|
+
let exported;
|
|
361
|
+
try {
|
|
362
|
+
exported = lstatSync(output);
|
|
363
|
+
} catch (error) {
|
|
364
|
+
return `missing '${basename(output)}': ${error.message}`;
|
|
365
|
+
}
|
|
366
|
+
if (!exported.isFile() || exported.isSymbolicLink() || exported.size === 0) {
|
|
367
|
+
return `invalid or empty '${basename(output)}'`;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const header = Buffer.alloc(Math.min(FBX_HEADER_BYTES, exported.size));
|
|
371
|
+
let descriptor;
|
|
372
|
+
try {
|
|
373
|
+
descriptor = openSync(output, 'r');
|
|
374
|
+
const bytesRead = readSync(descriptor, header, 0, header.length, 0);
|
|
375
|
+
if (bytesRead !== header.length) return `could not read the complete header of '${basename(output)}'`;
|
|
376
|
+
} catch (error) {
|
|
377
|
+
return `could not read '${basename(output)}': ${error.message}`;
|
|
378
|
+
} finally {
|
|
379
|
+
if (descriptor !== undefined) closeSync(descriptor);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const binary = header.subarray(0, FBX_BINARY_HEADER.length).equals(FBX_BINARY_HEADER);
|
|
383
|
+
const ascii = FBX_ASCII_HEADER.test(header.toString('ascii'));
|
|
384
|
+
return binary || ascii ? null : `'${basename(output)}' is not a structurally valid FBX`;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function validateNumericDictionary(value, label) {
|
|
388
|
+
const dictionary = object(value, label);
|
|
389
|
+
for (const [name, number] of Object.entries(dictionary)) {
|
|
390
|
+
nonEmptyString(name, `${label} parameter name`);
|
|
391
|
+
finiteNumber(number, `${label}.${name}`);
|
|
392
|
+
}
|
|
393
|
+
return dictionary;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function validateVectorDictionary(value, label) {
|
|
397
|
+
const dictionary = object(value, label);
|
|
398
|
+
for (const [name, vectorValue] of Object.entries(dictionary)) {
|
|
399
|
+
nonEmptyString(name, `${label} parameter name`);
|
|
400
|
+
const vector = object(vectorValue, `${label}.${name}`);
|
|
401
|
+
exactKeys(vector, ['r', 'g', 'b', 'a'], `${label}.${name}`);
|
|
402
|
+
for (const channel of ['r', 'g', 'b', 'a']) finiteNumber(vector[channel], `${label}.${name}.${channel}`);
|
|
403
|
+
}
|
|
404
|
+
return dictionary;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function validateTextureBinding(value, outputRoot, label) {
|
|
408
|
+
const binding = object(value, label);
|
|
409
|
+
exactKeys(binding, ['sourceAssetPath', 'path', 'sha256', 'sizeBytes', 'colorSpace'], label);
|
|
410
|
+
portableUnrealPath(binding.sourceAssetPath, `${label}.sourceAssetPath`);
|
|
411
|
+
const relativePath = nonEmptyString(binding.path, `${label}.path`);
|
|
412
|
+
const pathMatch = TEXTURE_RELATIVE_PATH.exec(relativePath);
|
|
413
|
+
if (!pathMatch || pathMatch[1] !== binding.sha256) {
|
|
414
|
+
fail(`${label}.path must be the content-addressed textures/<sha256>.png path`);
|
|
415
|
+
}
|
|
416
|
+
if (!SHA256.test(binding.sha256)) fail(`${label}.sha256 must be a lowercase SHA-256 digest`);
|
|
417
|
+
if (!Number.isSafeInteger(binding.sizeBytes) || binding.sizeBytes <= 0) {
|
|
418
|
+
fail(`${label}.sizeBytes must be a positive safe integer`);
|
|
419
|
+
}
|
|
420
|
+
const colorSpace = object(binding.colorSpace, `${label}.colorSpace`);
|
|
421
|
+
const colorKeys = Object.keys(colorSpace).sort();
|
|
422
|
+
if (colorKeys.some((key) => key !== 'compressionSettings' && key !== 'srgb')) {
|
|
423
|
+
fail(`${label}.colorSpace contains unsupported metadata`);
|
|
424
|
+
}
|
|
425
|
+
if ('srgb' in colorSpace && typeof colorSpace.srgb !== 'boolean') {
|
|
426
|
+
fail(`${label}.colorSpace.srgb must be a boolean`);
|
|
427
|
+
}
|
|
428
|
+
if ('compressionSettings' in colorSpace) {
|
|
429
|
+
nonEmptyString(colorSpace.compressionSettings, `${label}.colorSpace.compressionSettings`);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const texturePath = resolve(outputRoot, relativePath);
|
|
433
|
+
containedPath(outputRoot, texturePath, label);
|
|
434
|
+
regularFile(texturePath, `${label} texture`, undefined);
|
|
435
|
+
const canonicalRoot = realpathSync(outputRoot);
|
|
436
|
+
const canonicalTexture = realpathSync(texturePath);
|
|
437
|
+
containedPath(canonicalRoot, canonicalTexture, `${label} texture`);
|
|
438
|
+
if (!hasHeader(texturePath, PNG_HEADER)) fail(`${label} texture is not a structurally valid PNG`);
|
|
439
|
+
const actual = hashAndSize(texturePath);
|
|
440
|
+
if (actual.sha256 !== binding.sha256 || actual.sizeBytes !== binding.sizeBytes) {
|
|
441
|
+
fail(`${label} texture bytes do not match the reviewed SHA-256 and size`);
|
|
442
|
+
}
|
|
443
|
+
return {
|
|
444
|
+
path: relativePath,
|
|
445
|
+
sha256: binding.sha256,
|
|
446
|
+
sizeBytes: binding.sizeBytes,
|
|
447
|
+
colorSpace,
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function validateTextureDictionary(value, outputRoot, label) {
|
|
452
|
+
const dictionary = object(value, label);
|
|
453
|
+
const textures = [];
|
|
454
|
+
for (const [name, textureValue] of Object.entries(dictionary)) {
|
|
455
|
+
nonEmptyString(name, `${label} parameter name`);
|
|
456
|
+
textures.push(validateTextureBinding(textureValue, outputRoot, `${label}.${name}`));
|
|
457
|
+
}
|
|
458
|
+
return textures;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function validateMaterialSidecar(sidecarPath, profile, outputRoot) {
|
|
462
|
+
const info = regularFile(sidecarPath, `Material sidecar '${basename(sidecarPath)}'`, MAX_MATERIAL_SIDECAR_BYTES);
|
|
463
|
+
let sidecar;
|
|
464
|
+
try {
|
|
465
|
+
sidecar = JSON.parse(readFileSync(sidecarPath, 'utf8'));
|
|
466
|
+
} catch (error) {
|
|
467
|
+
fail(`Material sidecar '${basename(sidecarPath)}' is not valid JSON: ${error.message}`);
|
|
468
|
+
}
|
|
469
|
+
object(sidecar, `Material sidecar '${basename(sidecarPath)}'`);
|
|
470
|
+
exactKeys(sidecar, ['schemaVersion', 'asset', 'materials'], `Material sidecar '${basename(sidecarPath)}'`);
|
|
471
|
+
if (sidecar.schemaVersion !== MATERIAL_SIDECAR_SCHEMA) {
|
|
472
|
+
fail(`Material sidecar schemaVersion must be '${MATERIAL_SIDECAR_SCHEMA}'`);
|
|
473
|
+
}
|
|
474
|
+
const asset = object(sidecar.asset, 'Material sidecar asset');
|
|
475
|
+
exactKeys(asset, ['slug', 'sourceMeshAssetPath'], 'Material sidecar asset');
|
|
476
|
+
if (asset.slug !== profile.slug || asset.sourceMeshAssetPath !== profile.assetPath) {
|
|
477
|
+
fail(`Material sidecar asset identity does not match reviewed profile '${profile.slug}'`);
|
|
478
|
+
}
|
|
479
|
+
portableUnrealPath(asset.sourceMeshAssetPath, 'Material sidecar asset sourceMeshAssetPath');
|
|
480
|
+
if (!Array.isArray(sidecar.materials)) fail('Material sidecar materials must be an array');
|
|
481
|
+
|
|
482
|
+
const textures = [];
|
|
483
|
+
sidecar.materials.forEach((materialValue, index) => {
|
|
484
|
+
const label = `Material sidecar materials[${index}]`;
|
|
485
|
+
const material = object(materialValue, label);
|
|
486
|
+
exactKeys(material, [
|
|
487
|
+
'slotIndex',
|
|
488
|
+
'slotName',
|
|
489
|
+
'importedSlotName',
|
|
490
|
+
'sourceMaterialAssetPath',
|
|
491
|
+
'sourceRootMaterialAssetPath',
|
|
492
|
+
'parameters',
|
|
493
|
+
], label);
|
|
494
|
+
if (material.slotIndex !== index) fail(`${label}.slotIndex must preserve ordered slot index ${index}`);
|
|
495
|
+
if (typeof material.slotName !== 'string' || typeof material.importedSlotName !== 'string') {
|
|
496
|
+
fail(`${label} slot names must be strings`);
|
|
497
|
+
}
|
|
498
|
+
portableUnrealPath(material.sourceMaterialAssetPath, `${label}.sourceMaterialAssetPath`);
|
|
499
|
+
portableUnrealPath(material.sourceRootMaterialAssetPath, `${label}.sourceRootMaterialAssetPath`);
|
|
500
|
+
const parameters = object(material.parameters, `${label}.parameters`);
|
|
501
|
+
exactKeys(parameters, ['scalars', 'vectors', 'textures'], `${label}.parameters`);
|
|
502
|
+
validateNumericDictionary(parameters.scalars, `${label}.parameters.scalars`);
|
|
503
|
+
validateVectorDictionary(parameters.vectors, `${label}.parameters.vectors`);
|
|
504
|
+
textures.push(...validateTextureDictionary(parameters.textures, outputRoot, `${label}.parameters.textures`));
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
const sidecarIdentity = hashAndSize(sidecarPath);
|
|
508
|
+
if (sidecarIdentity.sizeBytes !== info.size) fail(`Material sidecar '${basename(sidecarPath)}' changed during validation`);
|
|
509
|
+
const uniqueTextures = [...new Map(textures.map((texture) => [texture.path, texture])).values()]
|
|
510
|
+
.sort((left, right) => left.path.localeCompare(right.path));
|
|
511
|
+
return {
|
|
512
|
+
schemaVersion: MATERIAL_SIDECAR_SCHEMA,
|
|
513
|
+
path: basename(sidecarPath),
|
|
514
|
+
...sidecarIdentity,
|
|
515
|
+
textures: uniqueTextures,
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
function validateTextureInventory(outputRoot, materialSidecars) {
|
|
520
|
+
const expected = new Set(materialSidecars.flatMap((sidecar) => sidecar.textures.map((texture) => texture.path)));
|
|
521
|
+
const texturesDir = join(outputRoot, 'textures');
|
|
522
|
+
if (!existsSync(texturesDir)) {
|
|
523
|
+
if (expected.size > 0) fail('Reviewed texture directory is missing');
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
const directory = lstatSync(texturesDir);
|
|
527
|
+
if (!directory.isDirectory() || directory.isSymbolicLink()) {
|
|
528
|
+
fail('Reviewed texture directory must be a regular non-symlink directory');
|
|
529
|
+
}
|
|
530
|
+
const observed = new Set();
|
|
531
|
+
for (const entry of readdirSync(texturesDir, { withFileTypes: true })) {
|
|
532
|
+
const relativePath = `textures/${entry.name}`;
|
|
533
|
+
if (!entry.isFile() || entry.isSymbolicLink()) {
|
|
534
|
+
fail(`Reviewed texture inventory contains a non-file entry: ${relativePath}`);
|
|
535
|
+
}
|
|
536
|
+
if (!TEXTURE_RELATIVE_PATH.test(relativePath)) {
|
|
537
|
+
fail(`Reviewed texture inventory contains a non-content-addressed file: ${relativePath}`);
|
|
538
|
+
}
|
|
539
|
+
if (!expected.has(relativePath)) {
|
|
540
|
+
fail(`Reviewed texture inventory contains an unreferenced file: ${relativePath}`);
|
|
541
|
+
}
|
|
542
|
+
observed.add(relativePath);
|
|
543
|
+
}
|
|
544
|
+
if (observed.size !== expected.size) fail('Reviewed texture inventory is incomplete');
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
async function main() {
|
|
548
|
+
const chunks = [];
|
|
549
|
+
for await (const chunk of process.stdin) chunks.push(Buffer.from(chunk));
|
|
550
|
+
let request;
|
|
551
|
+
try {
|
|
552
|
+
request = JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
553
|
+
} catch (error) {
|
|
554
|
+
fail(`Bridge request is not valid JSON: ${error.message}`);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
const { sourcePath, outputDir, profilePath } = validateRequest(request);
|
|
558
|
+
const profiles = validateProfile(readJsonFile(profilePath, 'Unreal asset profile'));
|
|
559
|
+
const project = findProject(sourcePath);
|
|
560
|
+
const outputs = profiles.map((profile) => join(outputDir, `${profile.slug}.fbx`));
|
|
561
|
+
const script = resolve(__dirname, '..', 'unreal', 'export_skeletal_mesh.py');
|
|
562
|
+
if (!existsSync(script)) fail(`Packaged Unreal Python exporter is missing: ${script}`);
|
|
563
|
+
if (!existsSync(outputDir) || !statSync(outputDir).isDirectory()) {
|
|
564
|
+
fail(`Bridge output directory does not exist: ${outputDir}`);
|
|
565
|
+
}
|
|
566
|
+
for (const output of outputs) {
|
|
567
|
+
if (existsSync(output)) fail(`Bridge output already exists before Unreal extraction: ${output}`);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
const editorValue = nonEmptyString(process.env.HELIX_UNREAL_EDITOR, 'HELIX_UNREAL_EDITOR');
|
|
571
|
+
const paths = editorInvocationPaths(editorValue, project, script, outputs);
|
|
572
|
+
const environment = {
|
|
573
|
+
...process.env,
|
|
574
|
+
HELIX_UNREAL_ASSET_PATH: profiles[0].assetPath,
|
|
575
|
+
HELIX_UNREAL_OUTPUT_FBX: paths.outputs[0],
|
|
576
|
+
HELIX_UNREAL_EXPORT_MORPH_TARGETS: profiles[0].exportMorphTargets ? '1' : '0',
|
|
577
|
+
HELIX_UNREAL_EXPORTS_JSON: JSON.stringify(profiles.map((profile, index) => ({
|
|
578
|
+
slug: profile.slug,
|
|
579
|
+
assetPath: profile.assetPath,
|
|
580
|
+
outputPath: paths.outputs[index],
|
|
581
|
+
exportMorphTargets: profile.exportMorphTargets,
|
|
582
|
+
}))),
|
|
583
|
+
};
|
|
584
|
+
const editorOutcome = await runEditor(paths.editor, [
|
|
585
|
+
paths.project,
|
|
586
|
+
`-ExecutePythonScript=${paths.script}`,
|
|
587
|
+
'-DisablePipInstall',
|
|
588
|
+
'-stdout',
|
|
589
|
+
'-FullStdOutLogOutput',
|
|
590
|
+
'-unattended',
|
|
591
|
+
'-nop4',
|
|
592
|
+
'-nosplash',
|
|
593
|
+
'-nosound',
|
|
594
|
+
], editorEnvironment(environment, paths.wsl));
|
|
595
|
+
|
|
596
|
+
const invalidOutputs = outputs.map(validateFbxOutput).filter(Boolean);
|
|
597
|
+
if (invalidOutputs.length > 0) {
|
|
598
|
+
const suffix = editorOutcome.code === 0 && !editorOutcome.signal ? '' : `; ${editorFailure(editorOutcome)}`;
|
|
599
|
+
fail(`Unreal Editor did not produce the complete reviewed export set (${invalidOutputs.join(', ')})${suffix}`);
|
|
600
|
+
}
|
|
601
|
+
const materialSidecars = profiles.map((profile) => validateMaterialSidecar(
|
|
602
|
+
join(outputDir, `${profile.slug}.materials.json`),
|
|
603
|
+
profile,
|
|
604
|
+
outputDir,
|
|
605
|
+
));
|
|
606
|
+
validateTextureInventory(outputDir, materialSidecars);
|
|
607
|
+
if (editorOutcome.signal) fail(editorFailure(editorOutcome));
|
|
608
|
+
if (editorOutcome.code !== 0) {
|
|
609
|
+
if (!isKnownBenignPlatformValidationShutdown(editorOutcome)) fail(editorFailure(editorOutcome));
|
|
610
|
+
process.stderr.write(
|
|
611
|
+
`HELIX Unreal Bridge warning: all reviewed FBXs were produced before the known benign UE ` +
|
|
612
|
+
`platform-validation shutdown exited ${editorOutcome.code}.` +
|
|
613
|
+
`${editorOutcome.tail ? ` ${editorOutcome.tail}` : ''}\n`,
|
|
614
|
+
);
|
|
615
|
+
}
|
|
616
|
+
process.stdout.write(JSON.stringify({
|
|
617
|
+
artifacts: profiles.map((profile, index) => ({
|
|
618
|
+
kind: 'format.fbx',
|
|
619
|
+
path: basename(outputs[index]),
|
|
620
|
+
metadata: {
|
|
621
|
+
unrealAsset: profile.assetPath,
|
|
622
|
+
exportMorphTargets: profile.exportMorphTargets,
|
|
623
|
+
materialSidecar: materialSidecars[index],
|
|
624
|
+
},
|
|
625
|
+
})),
|
|
626
|
+
}));
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
main().catch((error) => {
|
|
630
|
+
process.stderr.write(`HELIX Unreal Bridge: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
631
|
+
process.exitCode = 1;
|
|
632
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": "helix.bridge-plugin/1",
|
|
3
|
+
"id": "helix.bridge.unreal",
|
|
4
|
+
"name": "HELIX Unreal Bridge",
|
|
5
|
+
"version": "1.0.0",
|
|
6
|
+
"trust": "helix-verified",
|
|
7
|
+
"description": "Official Unreal Engine skeletal-mesh extraction adapter for HELIX Bridge.",
|
|
8
|
+
"permissions": [
|
|
9
|
+
"filesystem.read-source",
|
|
10
|
+
"filesystem.write-output",
|
|
11
|
+
"spawn-process",
|
|
12
|
+
"unrestricted-host-execution",
|
|
13
|
+
"unreal-editor"
|
|
14
|
+
],
|
|
15
|
+
"capabilities": [
|
|
16
|
+
{
|
|
17
|
+
"id": "unreal.project.extract",
|
|
18
|
+
"stage": "extract",
|
|
19
|
+
"accepts": ["unreal.project"],
|
|
20
|
+
"produces": ["format.fbx"],
|
|
21
|
+
"targets": ["web", "unreal", "canonical"],
|
|
22
|
+
"permissions": [
|
|
23
|
+
"filesystem.read-source",
|
|
24
|
+
"filesystem.write-output",
|
|
25
|
+
"spawn-process",
|
|
26
|
+
"unrestricted-host-execution",
|
|
27
|
+
"unreal-editor"
|
|
28
|
+
],
|
|
29
|
+
"requires": [
|
|
30
|
+
"tool:unreal-editor-commandlet",
|
|
31
|
+
"input:unrealAsset"
|
|
32
|
+
],
|
|
33
|
+
"description": "Open the snapshotted Unreal project in the full editor and export one configured Skeletal Mesh to FBX.",
|
|
34
|
+
"command": {
|
|
35
|
+
"executable": "bin/unreal-skeletal-mesh-extract.js",
|
|
36
|
+
"timeoutSeconds": 3600
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
]
|
|
40
|
+
}
|