@playdrop/playdrop-cli 0.9.5 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/config/client-meta.json +1 -2
- package/dist/apiClient.d.ts +2 -0
- package/dist/apiClient.js +27 -2
- package/dist/appUrls.d.ts +1 -0
- package/dist/appUrls.js +9 -0
- package/dist/apps/build.js +39 -28
- package/dist/apps/index.d.ts +1 -0
- package/dist/apps/index.js +2 -0
- package/dist/apps/launchCheck.d.ts +2 -0
- package/dist/apps/launchCheck.js +31 -6
- package/dist/apps/registration.d.ts +1 -0
- package/dist/apps/registration.js +1 -0
- package/dist/apps/upload.d.ts +1 -0
- package/dist/apps/upload.js +4 -17
- package/dist/captureRuntime.d.ts +1 -0
- package/dist/captureRuntime.js +308 -0
- package/dist/catalogue.d.ts +4 -2
- package/dist/catalogue.js +50 -7
- package/dist/commandContext.js +42 -3
- package/dist/commands/capture.d.ts +1 -0
- package/dist/commands/capture.js +30 -13
- package/dist/commands/create.d.ts +0 -1
- package/dist/commands/create.js +2 -151
- package/dist/commands/creations.d.ts +0 -13
- package/dist/commands/creations.js +0 -141
- package/dist/commands/dev.d.ts +2 -1
- package/dist/commands/dev.js +23 -6
- package/dist/commands/devServer.js +3 -1
- package/dist/commands/generation.d.ts +1 -0
- package/dist/commands/generation.js +274 -0
- package/dist/commands/upload.d.ts +27 -1
- package/dist/commands/upload.js +962 -21
- package/dist/commands/validate.js +5 -0
- package/dist/commands/worker/runtime.d.ts +69 -0
- package/dist/commands/worker/runtime.js +414 -0
- package/dist/commands/worker.d.ts +144 -0
- package/dist/commands/worker.js +2219 -0
- package/dist/config.d.ts +2 -0
- package/dist/config.js +23 -0
- package/dist/index.js +71 -30
- package/dist/shellProbe.d.ts +1 -1
- package/dist/shellProbe.js +3 -3
- package/dist/workspaceAuth.d.ts +1 -0
- package/dist/workspaceAuth.js +8 -0
- package/node_modules/@playdrop/api-client/dist/client.d.ts +31 -14
- package/node_modules/@playdrop/api-client/dist/client.d.ts.map +1 -1
- package/node_modules/@playdrop/api-client/dist/client.js +2 -2
- package/node_modules/@playdrop/api-client/dist/domains/admin.d.ts +3 -1
- package/node_modules/@playdrop/api-client/dist/domains/admin.d.ts.map +1 -1
- package/node_modules/@playdrop/api-client/dist/domains/admin.js +45 -0
- package/node_modules/@playdrop/api-client/dist/domains/agent-tasks.d.ts +72 -0
- package/node_modules/@playdrop/api-client/dist/domains/agent-tasks.d.ts.map +1 -0
- package/node_modules/@playdrop/api-client/dist/domains/agent-tasks.js +442 -0
- package/node_modules/@playdrop/api-client/dist/index.d.ts +31 -14
- package/node_modules/@playdrop/api-client/dist/index.d.ts.map +1 -1
- package/node_modules/@playdrop/api-client/dist/index.js +134 -38
- package/node_modules/@playdrop/config/client-meta.json +1 -2
- package/node_modules/@playdrop/types/dist/api.d.ts +501 -74
- package/node_modules/@playdrop/types/dist/api.d.ts.map +1 -1
- package/node_modules/@playdrop/types/dist/api.js +90 -9
- package/node_modules/@playdrop/types/dist/app.d.ts +2 -0
- package/node_modules/@playdrop/types/dist/app.d.ts.map +1 -1
- package/node_modules/@playdrop/types/dist/app.js +3 -0
- package/node_modules/@playdrop/types/dist/version.d.ts +1 -0
- package/node_modules/@playdrop/types/dist/version.d.ts.map +1 -1
- package/package.json +2 -1
- package/node_modules/@playdrop/api-client/dist/domains/game-ideas.d.ts +0 -46
- package/node_modules/@playdrop/api-client/dist/domains/game-ideas.d.ts.map +0 -1
- package/node_modules/@playdrop/api-client/dist/domains/game-ideas.js +0 -177
package/dist/commands/upload.js
CHANGED
|
@@ -1,6 +1,40 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
2
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
36
|
exports.upload = upload;
|
|
37
|
+
exports.publishWorkerAppProject = publishWorkerAppProject;
|
|
4
38
|
const node_fs_1 = require("node:fs");
|
|
5
39
|
const node_path_1 = require("node:path");
|
|
6
40
|
const semver_1 = require("semver");
|
|
@@ -812,15 +846,6 @@ function appendTaskRelations(graphState, fromNodeId, relations, contextLabel) {
|
|
|
812
846
|
});
|
|
813
847
|
}
|
|
814
848
|
async function uploadAppTask(state, task, taskCreator, options) {
|
|
815
|
-
const gameIdeaRef = typeof options?.gameIdea === 'string' ? options.gameIdea.trim() : '';
|
|
816
|
-
if (gameIdeaRef) {
|
|
817
|
-
await (0, registration_1.ensureRegisteredHostedAppShell)({
|
|
818
|
-
client: state.client,
|
|
819
|
-
creatorUsername: taskCreator,
|
|
820
|
-
task,
|
|
821
|
-
});
|
|
822
|
-
await state.client.startGameIdea(gameIdeaRef, { app: `${taskCreator}/${task.name}` });
|
|
823
|
-
}
|
|
824
849
|
const { upload, warnings } = await (0, apps_1.runAppPipeline)(state.client, task, {
|
|
825
850
|
skipEcs: options?.skipEcs,
|
|
826
851
|
skipReview: options?.skipReview,
|
|
@@ -854,9 +879,6 @@ async function uploadAppTask(state, task, taskCreator, options) {
|
|
|
854
879
|
};
|
|
855
880
|
state.uploadedAppsByName.set(task.name, uploadedApp);
|
|
856
881
|
(0, upload_graph_1.registerCanonicalNode)(state.graphState, appRef, uploadedApp.versionNodeId);
|
|
857
|
-
if (gameIdeaRef) {
|
|
858
|
-
await state.client.shipGameIdea(gameIdeaRef, { app: `${taskCreator}/${task.name}` });
|
|
859
|
-
}
|
|
860
882
|
(0, upload_graph_1.registerLocalRef)(state.graphState.localAppNodeByName, state.graphState.ambiguousAppNames, task.name, uploadedApp.versionNodeId);
|
|
861
883
|
appendTaskRelations(state.graphState, uploadedApp.versionNodeId, task.graph.relations, `[${task.cataloguePath}] app "${task.name}"`);
|
|
862
884
|
const entry = {
|
|
@@ -1179,15 +1201,6 @@ async function upload(pathOrName, options) {
|
|
|
1179
1201
|
}
|
|
1180
1202
|
const warnings = selection.warnings;
|
|
1181
1203
|
const tasks = selection.tasks;
|
|
1182
|
-
const gameIdeaRef = typeof options?.gameIdea === 'string' ? options.gameIdea.trim() : '';
|
|
1183
|
-
if (gameIdeaRef) {
|
|
1184
|
-
const appTasks = tasks.filter((task) => task.kind === 'app');
|
|
1185
|
-
if (appTasks.length !== 1) {
|
|
1186
|
-
(0, messages_1.printErrorWithHelp)('--game-idea can only be used when publishing exactly one app.', ['Publish a single app name or file, then retry with --game-idea.'], { command: 'project publish' });
|
|
1187
|
-
process.exitCode = 1;
|
|
1188
|
-
return;
|
|
1189
|
-
}
|
|
1190
|
-
}
|
|
1191
1204
|
if (tagGroupLoad.groups.length > 0 && userInfo.role !== 'ADMIN') {
|
|
1192
1205
|
(0, messages_1.printErrorWithHelp)('Publishing tagGroups requires an admin account.', [
|
|
1193
1206
|
'Log in as an admin before publishing taxonomy changes.',
|
|
@@ -1208,3 +1221,931 @@ async function upload(pathOrName, options) {
|
|
|
1208
1221
|
(0, uploadLog_1.printTaskSummary)(results, warnings, { action: 'upload', environment: env });
|
|
1209
1222
|
(0, uploadLog_1.printPublishedAppNextSteps)(results);
|
|
1210
1223
|
}
|
|
1224
|
+
const WORKER_SUPERVISOR_SOURCE_EXCLUDES = [
|
|
1225
|
+
'PLAYDROP_TASK.md',
|
|
1226
|
+
'.playdrop/',
|
|
1227
|
+
'.playdrop-task-events/',
|
|
1228
|
+
'bin/playdrop',
|
|
1229
|
+
];
|
|
1230
|
+
const MIN_HERO_PORTRAIT_WIDTH = 512;
|
|
1231
|
+
const MIN_HERO_PORTRAIT_HEIGHT = 768;
|
|
1232
|
+
const MIN_HERO_LANDSCAPE_WIDTH = 768;
|
|
1233
|
+
const MIN_HERO_LANDSCAPE_HEIGHT = 432;
|
|
1234
|
+
const OWNED_RUNTIME_IMAGE_CHECK_SIZE = 96;
|
|
1235
|
+
const OWNED_RUNTIME_IMAGE_GRID_CHECK_SIZE = 512;
|
|
1236
|
+
function appTaskSatisfiesPlaydropAssetRequirement(task, requirement) {
|
|
1237
|
+
if (!requirement) {
|
|
1238
|
+
return true;
|
|
1239
|
+
}
|
|
1240
|
+
const hasFirstPartyPack = task.uses.packs.some((ref) => {
|
|
1241
|
+
const parsed = (0, types_1.parseContentVersionRef)(ref);
|
|
1242
|
+
return parsed?.kind === 'pack' && parsed.creatorUsername.toLowerCase() === 'playdrop';
|
|
1243
|
+
});
|
|
1244
|
+
if (requirement === 'PACK') {
|
|
1245
|
+
return hasFirstPartyPack;
|
|
1246
|
+
}
|
|
1247
|
+
const hasFirstPartyAsset = task.uses.assets.some((dependency) => {
|
|
1248
|
+
const parsed = (0, types_1.parseContentVersionRef)(dependency.ref);
|
|
1249
|
+
return parsed?.kind === 'asset' && parsed.creatorUsername.toLowerCase() === 'playdrop';
|
|
1250
|
+
});
|
|
1251
|
+
return hasFirstPartyPack || hasFirstPartyAsset || task.ownedAssets.length > 0;
|
|
1252
|
+
}
|
|
1253
|
+
function appTaskHasRequiredNewGameAssets(task) {
|
|
1254
|
+
return task.uses.packs.length > 0 || task.uses.assets.length > 0 || task.ownedAssets.length > 0;
|
|
1255
|
+
}
|
|
1256
|
+
function isUnsafeDirectPlaydropAiImageAsset(detail, parsedRevision) {
|
|
1257
|
+
const asset = detail?.asset;
|
|
1258
|
+
if (!asset || asset.category !== 'IMAGE') {
|
|
1259
|
+
return false;
|
|
1260
|
+
}
|
|
1261
|
+
const currentVersion = asset.currentVersion;
|
|
1262
|
+
if (!currentVersion || Number(currentVersion.revision) !== parsedRevision) {
|
|
1263
|
+
return false;
|
|
1264
|
+
}
|
|
1265
|
+
const sourceKind = typeof currentVersion.sourceKind === 'string' ? currentVersion.sourceKind.toUpperCase() : '';
|
|
1266
|
+
if (sourceKind !== 'AI') {
|
|
1267
|
+
return false;
|
|
1268
|
+
}
|
|
1269
|
+
const format = typeof currentVersion.format === 'string' ? currentVersion.format.toUpperCase() : '';
|
|
1270
|
+
const files = Array.isArray(currentVersion.fileManifest?.files) ? currentVersion.fileManifest.files : [];
|
|
1271
|
+
const primary = files.find((file) => file?.role === 'primary');
|
|
1272
|
+
const primaryContentType = typeof primary?.contentType === 'string' ? primary.contentType.toLowerCase() : '';
|
|
1273
|
+
return format === 'JPEG' || primaryContentType === 'image/jpeg' || primaryContentType === 'image/jpg';
|
|
1274
|
+
}
|
|
1275
|
+
async function assertNewGameDirectPlaydropAssetsAreRuntimeSafe(input) {
|
|
1276
|
+
const unsafeRefs = [];
|
|
1277
|
+
for (const dependency of input.task.uses.assets) {
|
|
1278
|
+
const parsed = (0, types_1.parseContentVersionRef)(dependency.ref);
|
|
1279
|
+
if (!parsed || parsed.kind !== 'asset' || parsed.creatorUsername.toLowerCase() !== 'playdrop') {
|
|
1280
|
+
continue;
|
|
1281
|
+
}
|
|
1282
|
+
const detail = await input.client.fetchAssetBySlug(parsed.creatorUsername, parsed.name);
|
|
1283
|
+
if (isUnsafeDirectPlaydropAiImageAsset(detail, parsed.revision)) {
|
|
1284
|
+
unsafeRefs.push((0, types_1.formatContentVersionRef)(parsed));
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
if (unsafeRefs.length > 0) {
|
|
1288
|
+
throw new Error(`agent_task_playdrop_direct_ai_image_asset_unsafe: direct PlayDrop AI JPEG image assets cannot be used as new-game runtime sprites because their backgrounds are baked into the image. Use a PlayDrop asset pack or create owned PNG assets through the PlayDrop plugin asset-extraction-2d workflow instead. Unsafe refs: ${unsafeRefs.join(', ')}`);
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
function isOwnedRuntimeImageAsset(task, ownedAsset) {
|
|
1292
|
+
if (!ownedAsset.runtimeKey || ownedAsset.category !== 'IMAGE') {
|
|
1293
|
+
return false;
|
|
1294
|
+
}
|
|
1295
|
+
const primaryPath = ownedAsset.filePaths?.primary;
|
|
1296
|
+
if (!primaryPath) {
|
|
1297
|
+
return false;
|
|
1298
|
+
}
|
|
1299
|
+
const relativePath = primaryPath.slice((0, node_path_1.resolve)(task.projectDir).length + 1).replace(/\\/g, '/');
|
|
1300
|
+
return !relativePath.startsWith('assets/marketing/') && !relativePath.startsWith('listing/');
|
|
1301
|
+
}
|
|
1302
|
+
function isBackgroundLikeOwnedRuntimeImageAsset(ownedAsset) {
|
|
1303
|
+
const value = `${ownedAsset.name ?? ''} ${ownedAsset.runtimeKey ?? ''}`.toLowerCase();
|
|
1304
|
+
return /\b(background|backdrop|scene|sky|floor|ground|wall|terrain|landscape|level|arena)\b/i.test(value);
|
|
1305
|
+
}
|
|
1306
|
+
function looksLikeBakedCheckerboardPreview(input) {
|
|
1307
|
+
const { data, width, height, channels } = input;
|
|
1308
|
+
if (width < 24 || height < 24 || channels < 4) {
|
|
1309
|
+
return false;
|
|
1310
|
+
}
|
|
1311
|
+
const border = Math.max(8, Math.floor(Math.min(width, height) * 0.16));
|
|
1312
|
+
let sampled = 0;
|
|
1313
|
+
let transparent = 0;
|
|
1314
|
+
let opaqueGray = 0;
|
|
1315
|
+
let lowGray = 0;
|
|
1316
|
+
let highGray = 0;
|
|
1317
|
+
for (let y = 0; y < height; y += 1) {
|
|
1318
|
+
for (let x = 0; x < width; x += 1) {
|
|
1319
|
+
const inBorder = x < border || y < border || x >= width - border || y >= height - border;
|
|
1320
|
+
if (!inBorder) {
|
|
1321
|
+
continue;
|
|
1322
|
+
}
|
|
1323
|
+
sampled += 1;
|
|
1324
|
+
const offset = (y * width + x) * channels;
|
|
1325
|
+
const red = data[offset] ?? 0;
|
|
1326
|
+
const green = data[offset + 1] ?? 0;
|
|
1327
|
+
const blue = data[offset + 2] ?? 0;
|
|
1328
|
+
const alpha = data[offset + 3] ?? 255;
|
|
1329
|
+
if (alpha < 32) {
|
|
1330
|
+
transparent += 1;
|
|
1331
|
+
continue;
|
|
1332
|
+
}
|
|
1333
|
+
const brightness = (red + green + blue) / 3;
|
|
1334
|
+
const grayish = Math.max(Math.abs(red - green), Math.abs(red - blue), Math.abs(green - blue)) <= 10;
|
|
1335
|
+
if (alpha > 240 && grayish && brightness >= 70 && brightness <= 210) {
|
|
1336
|
+
opaqueGray += 1;
|
|
1337
|
+
if (brightness < 125) {
|
|
1338
|
+
lowGray += 1;
|
|
1339
|
+
}
|
|
1340
|
+
else if (brightness > 145) {
|
|
1341
|
+
highGray += 1;
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
if (sampled === 0 || transparent / sampled > 0.18) {
|
|
1347
|
+
return false;
|
|
1348
|
+
}
|
|
1349
|
+
return opaqueGray / sampled > 0.58
|
|
1350
|
+
&& lowGray / sampled > 0.08
|
|
1351
|
+
&& highGray / sampled > 0.18;
|
|
1352
|
+
}
|
|
1353
|
+
function looksLikeBakedSolidMattePreview(input) {
|
|
1354
|
+
const { data, width, height, channels } = input;
|
|
1355
|
+
if (width < 24 || height < 24 || channels < 4) {
|
|
1356
|
+
return false;
|
|
1357
|
+
}
|
|
1358
|
+
const border = Math.max(8, Math.floor(Math.min(width, height) * 0.18));
|
|
1359
|
+
let sampled = 0;
|
|
1360
|
+
let transparent = 0;
|
|
1361
|
+
let matteLike = 0;
|
|
1362
|
+
for (let y = 0; y < height; y += 1) {
|
|
1363
|
+
for (let x = 0; x < width; x += 1) {
|
|
1364
|
+
const inBorder = x < border || y < border || x >= width - border || y >= height - border;
|
|
1365
|
+
if (!inBorder) {
|
|
1366
|
+
continue;
|
|
1367
|
+
}
|
|
1368
|
+
sampled += 1;
|
|
1369
|
+
const offset = (y * width + x) * channels;
|
|
1370
|
+
const red = data[offset] ?? 0;
|
|
1371
|
+
const green = data[offset + 1] ?? 0;
|
|
1372
|
+
const blue = data[offset + 2] ?? 0;
|
|
1373
|
+
const alpha = data[offset + 3] ?? 255;
|
|
1374
|
+
if (alpha < 32) {
|
|
1375
|
+
transparent += 1;
|
|
1376
|
+
continue;
|
|
1377
|
+
}
|
|
1378
|
+
const dominant = Math.max(red, green, blue);
|
|
1379
|
+
const weakest = Math.min(red, green, blue);
|
|
1380
|
+
const chroma = dominant - weakest;
|
|
1381
|
+
const brightGreenMatte = green >= 220 && green - Math.max(red, blue) >= 65 && Math.max(red, blue) >= 70;
|
|
1382
|
+
const brightRedMatte = red >= 220 && red - Math.max(green, blue) >= 65;
|
|
1383
|
+
const brightBlueMatte = blue >= 220 && blue - Math.max(red, green) >= 65;
|
|
1384
|
+
const brightTwoChannelMatte = dominant >= 220
|
|
1385
|
+
&& chroma >= 65
|
|
1386
|
+
&& ((red >= 220 && green >= 220) || (red >= 220 && blue >= 220) || (green >= 220 && blue >= 220));
|
|
1387
|
+
if (brightGreenMatte || brightRedMatte || brightBlueMatte || brightTwoChannelMatte) {
|
|
1388
|
+
matteLike += 1;
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
if (sampled === 0 || transparent / sampled > 0.18) {
|
|
1393
|
+
return false;
|
|
1394
|
+
}
|
|
1395
|
+
return matteLike / sampled > 0.58;
|
|
1396
|
+
}
|
|
1397
|
+
function looksLikeBakedNeutralSolidMattePreview(input) {
|
|
1398
|
+
const { data, width, height, channels } = input;
|
|
1399
|
+
if (width < 24 || height < 24 || channels < 4) {
|
|
1400
|
+
return false;
|
|
1401
|
+
}
|
|
1402
|
+
const border = Math.max(8, Math.floor(Math.min(width, height) * 0.18));
|
|
1403
|
+
let sampled = 0;
|
|
1404
|
+
let transparent = 0;
|
|
1405
|
+
let neutralMatteLike = 0;
|
|
1406
|
+
for (let y = 0; y < height; y += 1) {
|
|
1407
|
+
for (let x = 0; x < width; x += 1) {
|
|
1408
|
+
const inBorder = x < border || y < border || x >= width - border || y >= height - border;
|
|
1409
|
+
if (!inBorder) {
|
|
1410
|
+
continue;
|
|
1411
|
+
}
|
|
1412
|
+
sampled += 1;
|
|
1413
|
+
const offset = (y * width + x) * channels;
|
|
1414
|
+
const red = data[offset] ?? 0;
|
|
1415
|
+
const green = data[offset + 1] ?? 0;
|
|
1416
|
+
const blue = data[offset + 2] ?? 0;
|
|
1417
|
+
const alpha = data[offset + 3] ?? 255;
|
|
1418
|
+
if (alpha < 32) {
|
|
1419
|
+
transparent += 1;
|
|
1420
|
+
continue;
|
|
1421
|
+
}
|
|
1422
|
+
const brightness = (red + green + blue) / 3;
|
|
1423
|
+
const grayish = Math.max(Math.abs(red - green), Math.abs(red - blue), Math.abs(green - blue)) <= 12;
|
|
1424
|
+
if (alpha > 240 && grayish && (brightness >= 235 || brightness <= 20)) {
|
|
1425
|
+
neutralMatteLike += 1;
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
if (sampled === 0 || transparent / sampled > 0.18) {
|
|
1430
|
+
return false;
|
|
1431
|
+
}
|
|
1432
|
+
return neutralMatteLike / sampled > 0.58;
|
|
1433
|
+
}
|
|
1434
|
+
function looksLikeResidualMatteGridArtifact(input) {
|
|
1435
|
+
const { data, width, height, channels } = input;
|
|
1436
|
+
if (width < 48 || height < 48 || channels < 4) {
|
|
1437
|
+
return false;
|
|
1438
|
+
}
|
|
1439
|
+
const rowCounts = new Uint32Array(height);
|
|
1440
|
+
const columnCounts = new Uint32Array(width);
|
|
1441
|
+
let matteLike = 0;
|
|
1442
|
+
for (let y = 0; y < height; y += 1) {
|
|
1443
|
+
for (let x = 0; x < width; x += 1) {
|
|
1444
|
+
const offset = (y * width + x) * channels;
|
|
1445
|
+
const red = data[offset] ?? 0;
|
|
1446
|
+
const green = data[offset + 1] ?? 0;
|
|
1447
|
+
const blue = data[offset + 2] ?? 0;
|
|
1448
|
+
const alpha = data[offset + 3] ?? 255;
|
|
1449
|
+
if (alpha < 24 || alpha > 190) {
|
|
1450
|
+
continue;
|
|
1451
|
+
}
|
|
1452
|
+
const greenMatte = green >= 35 && green - Math.max(red, blue) >= 25 && red <= 110 && blue <= 110;
|
|
1453
|
+
const redMatte = red >= 35 && red - Math.max(green, blue) >= 25 && green <= 120 && blue <= 120;
|
|
1454
|
+
const blueMatte = blue >= 35 && blue - Math.max(red, green) >= 25 && red <= 120 && green <= 120;
|
|
1455
|
+
const purpleMatte = red >= 35 && blue >= 35 && Math.min(red, blue) - green >= 25 && green <= 120;
|
|
1456
|
+
if (!greenMatte && !redMatte && !blueMatte && !purpleMatte) {
|
|
1457
|
+
continue;
|
|
1458
|
+
}
|
|
1459
|
+
matteLike += 1;
|
|
1460
|
+
rowCounts[y] += 1;
|
|
1461
|
+
columnCounts[x] += 1;
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
const matteRatio = matteLike / (width * height);
|
|
1465
|
+
if (matteRatio < 0.0015) {
|
|
1466
|
+
return false;
|
|
1467
|
+
}
|
|
1468
|
+
let rowsWithLines = 0;
|
|
1469
|
+
for (const count of rowCounts) {
|
|
1470
|
+
if (count / width >= 0.12) {
|
|
1471
|
+
rowsWithLines += 1;
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
let columnsWithLines = 0;
|
|
1475
|
+
for (const count of columnCounts) {
|
|
1476
|
+
if (count / height >= 0.12) {
|
|
1477
|
+
columnsWithLines += 1;
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
return rowsWithLines + columnsWithLines >= 3
|
|
1481
|
+
&& (rowsWithLines >= 2 || columnsWithLines >= 2);
|
|
1482
|
+
}
|
|
1483
|
+
function looksLikeResidualTransparentGridLineArtifact(input) {
|
|
1484
|
+
const { data, width, height, channels } = input;
|
|
1485
|
+
if (width < 48 || height < 48 || channels < 4) {
|
|
1486
|
+
return false;
|
|
1487
|
+
}
|
|
1488
|
+
const rowVisibleCounts = new Uint32Array(height);
|
|
1489
|
+
const rowSemiCounts = new Uint32Array(height);
|
|
1490
|
+
const columnVisibleCounts = new Uint32Array(width);
|
|
1491
|
+
const columnSemiCounts = new Uint32Array(width);
|
|
1492
|
+
for (let y = 0; y < height; y += 1) {
|
|
1493
|
+
for (let x = 0; x < width; x += 1) {
|
|
1494
|
+
const offset = (y * width + x) * channels;
|
|
1495
|
+
const alpha = data[offset + 3] ?? 255;
|
|
1496
|
+
if (alpha <= 0) {
|
|
1497
|
+
continue;
|
|
1498
|
+
}
|
|
1499
|
+
rowVisibleCounts[y] += 1;
|
|
1500
|
+
columnVisibleCounts[x] += 1;
|
|
1501
|
+
if (alpha < 255) {
|
|
1502
|
+
rowSemiCounts[y] += 1;
|
|
1503
|
+
columnSemiCounts[x] += 1;
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
let rowsWithLines = 0;
|
|
1508
|
+
for (let row = 0; row < height; row += 1) {
|
|
1509
|
+
const visibleRatio = rowVisibleCounts[row] / width;
|
|
1510
|
+
const semiRatio = rowSemiCounts[row] / width;
|
|
1511
|
+
if (visibleRatio >= 0.94 && semiRatio >= 0.72) {
|
|
1512
|
+
rowsWithLines += 1;
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
let columnsWithLines = 0;
|
|
1516
|
+
for (let column = 0; column < width; column += 1) {
|
|
1517
|
+
const visibleRatio = columnVisibleCounts[column] / height;
|
|
1518
|
+
const semiRatio = columnSemiCounts[column] / height;
|
|
1519
|
+
if (visibleRatio >= 0.94 && semiRatio >= 0.72) {
|
|
1520
|
+
columnsWithLines += 1;
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
return rowsWithLines + columnsWithLines >= 3
|
|
1524
|
+
&& (rowsWithLines >= 2 || columnsWithLines >= 2);
|
|
1525
|
+
}
|
|
1526
|
+
async function imageHasBakedPreviewBackground(filePath, options = {}) {
|
|
1527
|
+
let sharp;
|
|
1528
|
+
try {
|
|
1529
|
+
const sharpModule = await Promise.resolve().then(() => __importStar(require('sharp')));
|
|
1530
|
+
sharp = sharpModule.default ?? sharpModule;
|
|
1531
|
+
}
|
|
1532
|
+
catch {
|
|
1533
|
+
return false;
|
|
1534
|
+
}
|
|
1535
|
+
try {
|
|
1536
|
+
const { data, info } = await sharp(filePath, { failOn: 'error' })
|
|
1537
|
+
.rotate()
|
|
1538
|
+
.resize(OWNED_RUNTIME_IMAGE_CHECK_SIZE, OWNED_RUNTIME_IMAGE_CHECK_SIZE, { fit: 'fill' })
|
|
1539
|
+
.ensureAlpha()
|
|
1540
|
+
.raw()
|
|
1541
|
+
.toBuffer({ resolveWithObject: true });
|
|
1542
|
+
const hasPreviewBackground = looksLikeBakedCheckerboardPreview({
|
|
1543
|
+
data,
|
|
1544
|
+
width: info.width,
|
|
1545
|
+
height: info.height,
|
|
1546
|
+
channels: info.channels,
|
|
1547
|
+
}) || looksLikeBakedSolidMattePreview({
|
|
1548
|
+
data,
|
|
1549
|
+
width: info.width,
|
|
1550
|
+
height: info.height,
|
|
1551
|
+
channels: info.channels,
|
|
1552
|
+
}) || (!options.allowNeutralSolidMatte && looksLikeBakedNeutralSolidMattePreview({
|
|
1553
|
+
data,
|
|
1554
|
+
width: info.width,
|
|
1555
|
+
height: info.height,
|
|
1556
|
+
channels: info.channels,
|
|
1557
|
+
}));
|
|
1558
|
+
if (hasPreviewBackground) {
|
|
1559
|
+
return true;
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1562
|
+
catch {
|
|
1563
|
+
return false;
|
|
1564
|
+
}
|
|
1565
|
+
try {
|
|
1566
|
+
const { data, info } = await sharp(filePath, { failOn: 'error' })
|
|
1567
|
+
.rotate()
|
|
1568
|
+
.resize(OWNED_RUNTIME_IMAGE_GRID_CHECK_SIZE, OWNED_RUNTIME_IMAGE_GRID_CHECK_SIZE, { fit: 'inside', withoutEnlargement: true })
|
|
1569
|
+
.ensureAlpha()
|
|
1570
|
+
.raw()
|
|
1571
|
+
.toBuffer({ resolveWithObject: true });
|
|
1572
|
+
if (looksLikeResidualMatteGridArtifact({
|
|
1573
|
+
data,
|
|
1574
|
+
width: info.width,
|
|
1575
|
+
height: info.height,
|
|
1576
|
+
channels: info.channels,
|
|
1577
|
+
}) || looksLikeResidualTransparentGridLineArtifact({
|
|
1578
|
+
data,
|
|
1579
|
+
width: info.width,
|
|
1580
|
+
height: info.height,
|
|
1581
|
+
channels: info.channels,
|
|
1582
|
+
})) {
|
|
1583
|
+
return true;
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
catch {
|
|
1587
|
+
return false;
|
|
1588
|
+
}
|
|
1589
|
+
return false;
|
|
1590
|
+
}
|
|
1591
|
+
async function assertNewGameOwnedRuntimeImageAssetsAreClean(task) {
|
|
1592
|
+
const unsafeAssets = [];
|
|
1593
|
+
for (const ownedAsset of task.ownedAssets) {
|
|
1594
|
+
if (!isOwnedRuntimeImageAsset(task, ownedAsset)) {
|
|
1595
|
+
continue;
|
|
1596
|
+
}
|
|
1597
|
+
const primaryPath = ownedAsset.filePaths?.primary;
|
|
1598
|
+
if (!primaryPath) {
|
|
1599
|
+
continue;
|
|
1600
|
+
}
|
|
1601
|
+
if (await imageHasBakedPreviewBackground(primaryPath, {
|
|
1602
|
+
allowNeutralSolidMatte: isBackgroundLikeOwnedRuntimeImageAsset(ownedAsset),
|
|
1603
|
+
})) {
|
|
1604
|
+
unsafeAssets.push(`${ownedAsset.name}:${ownedAsset.runtimeKey}`);
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
if (unsafeAssets.length > 0) {
|
|
1608
|
+
throw new Error(`agent_task_owned_image_asset_checkerboard_background: owned runtime image assets must not contain baked transparency checkerboard, matte-color, or preview backgrounds. Recreate the asset through the PlayDrop plugin asset-extraction-2d workflow, use a cleaner PlayDrop asset, or use the image as non-runtime listing art only. Unsafe owned assets: ${unsafeAssets.join(', ')}`);
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
const RUNTIME_SOURCE_EXTENSIONS = new Set([
|
|
1612
|
+
'.html',
|
|
1613
|
+
'.js',
|
|
1614
|
+
'.mjs',
|
|
1615
|
+
'.cjs',
|
|
1616
|
+
'.ts',
|
|
1617
|
+
'.tsx',
|
|
1618
|
+
'.jsx',
|
|
1619
|
+
'.css',
|
|
1620
|
+
]);
|
|
1621
|
+
const RUNTIME_SOURCE_EXCLUDED_DIRS = new Set([
|
|
1622
|
+
'.git',
|
|
1623
|
+
'.playdrop',
|
|
1624
|
+
'node_modules',
|
|
1625
|
+
'dist',
|
|
1626
|
+
'build',
|
|
1627
|
+
'.playdrop-task-events',
|
|
1628
|
+
'listing',
|
|
1629
|
+
'assets/marketing',
|
|
1630
|
+
]);
|
|
1631
|
+
function has3dIntent(value) {
|
|
1632
|
+
return /\b3d\b|\bthree[-\s]?d\b|\bthree\.js\b|\bglb\b|\bgltf\b/i.test(value ?? '');
|
|
1633
|
+
}
|
|
1634
|
+
function blankPreservingNewlines(value) {
|
|
1635
|
+
return value.replace(/[^\n\r]/g, ' ');
|
|
1636
|
+
}
|
|
1637
|
+
function stripRuntimeSourceCommentsAndStrings(source) {
|
|
1638
|
+
return source
|
|
1639
|
+
.replace(/\/\*[\s\S]*?\*\//g, blankPreservingNewlines)
|
|
1640
|
+
.replace(/`(?:\\[\s\S]|[^`\\])*`/g, blankPreservingNewlines)
|
|
1641
|
+
.replace(/'(?:\\.|[^'\\])*'/g, blankPreservingNewlines)
|
|
1642
|
+
.replace(/"(?:\\.|[^"\\])*"/g, blankPreservingNewlines)
|
|
1643
|
+
.replace(/\/\/[^\n\r]*/g, blankPreservingNewlines);
|
|
1644
|
+
}
|
|
1645
|
+
function hasDisallowed3dPlaydropAssetFallback(source) {
|
|
1646
|
+
const executableSource = stripRuntimeSourceCommentsAndStrings(source);
|
|
1647
|
+
return /\bemergency\s+mesh\b|\bemergency[A-Z][A-Za-z]*Mesh\b|\bbuildEmergencyMesh\b|\bfallback(?:Mesh|Model|Geometry|Material|Mats?|Size|Character|Obstacle|Enemy|Collectible)?\b|\bfallback\s+(?:mesh|model|geometry|material|character|obstacle|enemy|collectible)\b|\bplaceholder(?:Mesh|Model|Geometry|Material|Character|Obstacle|Enemy|Collectible)?\b|\bplaceholder\s+(?:mesh|model|geometry|material|character|obstacle|enemy|collectible)\b|catch\s*\([^)]*\)\s*=>\s*[^;\n]*(?:new\s+THREE\.(?:Box|Sphere|Capsule|Cone|Cylinder)Geometry|fallback|placeholder|emergency)|if\s*\([^)]*!url[^)]*\)\s*return\s+(?:new\s+THREE\.(?:Box|Sphere|Capsule|Cone|Cylinder)Geometry|fallback|placeholder|emergency)/i.test(executableSource);
|
|
1648
|
+
}
|
|
1649
|
+
function findDisallowedVisible3dPrimitiveGeometry(runtimeSource) {
|
|
1650
|
+
const executableSource = stripRuntimeSourceCommentsAndStrings(runtimeSource.source);
|
|
1651
|
+
const primitivePattern = /new\s+THREE\.(?:Box|Sphere|Capsule|Cone|Cylinder|Torus|TorusKnot|Dodecahedron|Icosahedron|Octahedron|Tetrahedron|Ring)Geometry\s*\(/ig;
|
|
1652
|
+
let match;
|
|
1653
|
+
while ((match = primitivePattern.exec(executableSource)) !== null) {
|
|
1654
|
+
const before = executableSource.slice(Math.max(0, match.index - 220), match.index);
|
|
1655
|
+
const after = executableSource.slice(match.index, Math.min(executableSource.length, match.index + 420));
|
|
1656
|
+
const meshStart = before.lastIndexOf('new THREE.Mesh');
|
|
1657
|
+
const meshWindow = `${meshStart >= 0 ? before.slice(meshStart) : before}${after}`;
|
|
1658
|
+
if (/\bvisible\s*:\s*false\b/i.test(meshWindow)) {
|
|
1659
|
+
continue;
|
|
1660
|
+
}
|
|
1661
|
+
const location = resolveRuntimeSourceLocation(runtimeSource, match.index);
|
|
1662
|
+
const constructorName = match[0]
|
|
1663
|
+
.replace(/^new\s+THREE\./i, 'THREE.')
|
|
1664
|
+
.replace(/\s*\($/, '');
|
|
1665
|
+
if (location) {
|
|
1666
|
+
return `${constructorName} at ${location.path}:${location.line}:${location.column}`;
|
|
1667
|
+
}
|
|
1668
|
+
return constructorName;
|
|
1669
|
+
}
|
|
1670
|
+
return null;
|
|
1671
|
+
}
|
|
1672
|
+
function hasDisallowedGameplayAssetFallback(source) {
|
|
1673
|
+
const executableSource = stripRuntimeSourceCommentsAndStrings(source);
|
|
1674
|
+
return /\bdrawFallback[A-Za-z0-9_]*\b|\bfallback(?:Image|Sprite|Asset|Draw|Visual|Character|Obstacle|Collectible|Prop|Enemy|Item|Piñata|Pinata|Candy)?\b|\bplaceholder(?:Image|Sprite|Asset|Draw|Visual|Character|Obstacle|Collectible|Prop|Enemy|Item)?\b|img\.onerror\s*=\s*(?:\([^)]*\)|[A-Za-z_$][\w$]*)\s*=>\s*(?:\{[^}]*\bresolve\s*\(|\bresolve\s*\(|\{[^}]*\breturn\b)|if\s*\([^)]*!url[^)]*\)\s*\{[^}]*\bresolve\s*\([^}]*\breturn\b|if\s*\([^)]*!url[^)]*\)\s*(?:return|continue|resolve\s*\()/i.test(executableSource);
|
|
1675
|
+
}
|
|
1676
|
+
function hasRuntimeAssetUrlExtensionAssumption(source) {
|
|
1677
|
+
return /(?:\.url|\.href|\burl\b)[^;\n]{0,220}(?:\\\.gl(?:b|tf)|\.gl(?:b|tf)|\.endsWith\s*\(\s*['"]\.gl(?:b|tf)['"]\s*\)|\.includes\s*\(\s*['"]\.gl(?:b|tf)['"]\s*\)|\.match\s*\([^)]*\.gl(?:b|tf))/i.test(source);
|
|
1678
|
+
}
|
|
1679
|
+
function hasPackRuntimeManifestShape(source) {
|
|
1680
|
+
return /\bassetRef\b/.test(source) && /\bcontentType\b/.test(source);
|
|
1681
|
+
}
|
|
1682
|
+
function manifestHasGltfFile(fileManifest) {
|
|
1683
|
+
const files = fileManifest && typeof fileManifest === 'object' && Array.isArray(fileManifest.files)
|
|
1684
|
+
? fileManifest.files
|
|
1685
|
+
: [];
|
|
1686
|
+
return files.some((file) => {
|
|
1687
|
+
const contentType = typeof file?.contentType === 'string' ? file.contentType.toLowerCase() : '';
|
|
1688
|
+
return contentType === 'model/gltf-binary' || contentType === 'model/gltf+json';
|
|
1689
|
+
});
|
|
1690
|
+
}
|
|
1691
|
+
function assetVersionHasGltfFile(version) {
|
|
1692
|
+
if (version.format === 'GLB' || version.format === 'GLTF') {
|
|
1693
|
+
return true;
|
|
1694
|
+
}
|
|
1695
|
+
const primaryContentType = typeof version.primaryContentType === 'string'
|
|
1696
|
+
? version.primaryContentType.toLowerCase()
|
|
1697
|
+
: '';
|
|
1698
|
+
return primaryContentType === 'model/gltf-binary'
|
|
1699
|
+
|| primaryContentType === 'model/gltf+json'
|
|
1700
|
+
|| manifestHasGltfFile(version.fileManifest);
|
|
1701
|
+
}
|
|
1702
|
+
async function findRemoteAssetVersionForRef(client, ref) {
|
|
1703
|
+
let offset = 0;
|
|
1704
|
+
for (let page = 0; page < 200; page += 1) {
|
|
1705
|
+
const response = await client.listAssetVersions(ref.creatorUsername, ref.name, {
|
|
1706
|
+
limit: 100,
|
|
1707
|
+
offset,
|
|
1708
|
+
});
|
|
1709
|
+
const version = (response.versions ?? []).find((entry) => entry.revision === ref.revision);
|
|
1710
|
+
if (version) {
|
|
1711
|
+
return version;
|
|
1712
|
+
}
|
|
1713
|
+
if (!response.pagination?.hasMore) {
|
|
1714
|
+
break;
|
|
1715
|
+
}
|
|
1716
|
+
offset += response.pagination.limit;
|
|
1717
|
+
}
|
|
1718
|
+
return null;
|
|
1719
|
+
}
|
|
1720
|
+
async function assertDeclared3dPlaydropPacksContainRuntimeModels(input) {
|
|
1721
|
+
if (!has3dIntent(input.creatorRequest) || input.task.uses.packs.length === 0) {
|
|
1722
|
+
return;
|
|
1723
|
+
}
|
|
1724
|
+
const inspectedRefs = [];
|
|
1725
|
+
for (const rawRef of input.task.uses.packs) {
|
|
1726
|
+
const parsed = (0, types_1.parseContentVersionRef)(rawRef);
|
|
1727
|
+
if (!parsed || parsed.kind !== 'pack') {
|
|
1728
|
+
throw new Error(`agent_task_playdrop_3d_pack_ref_invalid: invalid pack ref "${rawRef}".`);
|
|
1729
|
+
}
|
|
1730
|
+
inspectedRefs.push(rawRef);
|
|
1731
|
+
let offset = 0;
|
|
1732
|
+
let packVersion = null;
|
|
1733
|
+
for (let page = 0; page < 200; page += 1) {
|
|
1734
|
+
const response = await input.client.listAssetPackVersions(parsed.creatorUsername, parsed.name, {
|
|
1735
|
+
limit: 100,
|
|
1736
|
+
offset,
|
|
1737
|
+
});
|
|
1738
|
+
packVersion = (response.versions ?? []).find((entry) => entry.version === parsed.version) ?? null;
|
|
1739
|
+
if (packVersion || !response.pagination?.hasMore) {
|
|
1740
|
+
break;
|
|
1741
|
+
}
|
|
1742
|
+
offset += response.pagination.limit;
|
|
1743
|
+
}
|
|
1744
|
+
if (!packVersion) {
|
|
1745
|
+
throw new Error(`agent_task_playdrop_3d_pack_models_missing: could not inspect exact pack version "${rawRef}".`);
|
|
1746
|
+
}
|
|
1747
|
+
const modelCount = Number(packVersion.assetCategoryBreakdown?.MODEL_3D ?? 0);
|
|
1748
|
+
if (!Number.isFinite(modelCount) || modelCount <= 0) {
|
|
1749
|
+
continue;
|
|
1750
|
+
}
|
|
1751
|
+
for (const assetRef of packVersion.assets ?? []) {
|
|
1752
|
+
const version = await findRemoteAssetVersionForRef(input.client, assetRef);
|
|
1753
|
+
if (version && assetVersionHasGltfFile(version)) {
|
|
1754
|
+
return;
|
|
1755
|
+
}
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1758
|
+
throw new Error(`agent_task_playdrop_3d_pack_models_missing: declared 3D PlayDrop pack refs do not expose model/gltf-binary or model/gltf+json files. Inspected: ${inspectedRefs.join(', ')}.`);
|
|
1759
|
+
}
|
|
1760
|
+
function collectLineStarts(source) {
|
|
1761
|
+
const starts = [0];
|
|
1762
|
+
for (let index = 0; index < source.length; index += 1) {
|
|
1763
|
+
if (source[index] === '\n') {
|
|
1764
|
+
starts.push(index + 1);
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1767
|
+
return starts;
|
|
1768
|
+
}
|
|
1769
|
+
function resolveRuntimeSourceLocation(runtimeSource, offset) {
|
|
1770
|
+
let entry = null;
|
|
1771
|
+
for (const candidate of runtimeSource.offsetEntries) {
|
|
1772
|
+
if (candidate.start <= offset) {
|
|
1773
|
+
entry = candidate;
|
|
1774
|
+
continue;
|
|
1775
|
+
}
|
|
1776
|
+
break;
|
|
1777
|
+
}
|
|
1778
|
+
if (!entry) {
|
|
1779
|
+
return null;
|
|
1780
|
+
}
|
|
1781
|
+
const localOffset = Math.max(0, offset - entry.start);
|
|
1782
|
+
let lineIndex = 0;
|
|
1783
|
+
for (let index = 0; index < entry.lineStarts.length; index += 1) {
|
|
1784
|
+
if (entry.lineStarts[index] <= localOffset) {
|
|
1785
|
+
lineIndex = index;
|
|
1786
|
+
continue;
|
|
1787
|
+
}
|
|
1788
|
+
break;
|
|
1789
|
+
}
|
|
1790
|
+
const lineStart = entry.lineStarts[lineIndex] ?? 0;
|
|
1791
|
+
return {
|
|
1792
|
+
path: entry.path,
|
|
1793
|
+
line: lineIndex + 1,
|
|
1794
|
+
column: localOffset - lineStart + 1,
|
|
1795
|
+
};
|
|
1796
|
+
}
|
|
1797
|
+
function readRuntimeSourceText(rootDir) {
|
|
1798
|
+
const root = (0, node_path_1.resolve)(rootDir);
|
|
1799
|
+
const chunks = [];
|
|
1800
|
+
const offsetEntries = [];
|
|
1801
|
+
let totalBytes = 0;
|
|
1802
|
+
const maxBytes = 1000000;
|
|
1803
|
+
const walk = (dir) => {
|
|
1804
|
+
if (totalBytes >= maxBytes) {
|
|
1805
|
+
return;
|
|
1806
|
+
}
|
|
1807
|
+
for (const entry of (0, node_fs_1.readdirSync)(dir, { withFileTypes: true })) {
|
|
1808
|
+
const absolute = (0, node_path_1.join)(dir, entry.name);
|
|
1809
|
+
const relative = absolute.slice(root.length + 1).replace(/\\/g, '/');
|
|
1810
|
+
if (entry.isDirectory()) {
|
|
1811
|
+
if (RUNTIME_SOURCE_EXCLUDED_DIRS.has(entry.name) || RUNTIME_SOURCE_EXCLUDED_DIRS.has(relative)) {
|
|
1812
|
+
continue;
|
|
1813
|
+
}
|
|
1814
|
+
walk(absolute);
|
|
1815
|
+
continue;
|
|
1816
|
+
}
|
|
1817
|
+
if (!entry.isFile() || !RUNTIME_SOURCE_EXTENSIONS.has((0, node_path_1.extname)(entry.name).toLowerCase())) {
|
|
1818
|
+
continue;
|
|
1819
|
+
}
|
|
1820
|
+
const stats = (0, node_fs_1.statSync)(absolute);
|
|
1821
|
+
if (stats.size > 300000) {
|
|
1822
|
+
continue;
|
|
1823
|
+
}
|
|
1824
|
+
totalBytes += stats.size;
|
|
1825
|
+
const source = (0, node_fs_1.readFileSync)(absolute, 'utf8');
|
|
1826
|
+
const prefix = chunks.length > 0 ? '\n' : '';
|
|
1827
|
+
const start = chunks.join('').length + prefix.length;
|
|
1828
|
+
chunks.push(prefix, source);
|
|
1829
|
+
offsetEntries.push({
|
|
1830
|
+
path: relative,
|
|
1831
|
+
start,
|
|
1832
|
+
lineStarts: collectLineStarts(source),
|
|
1833
|
+
});
|
|
1834
|
+
if (totalBytes >= maxBytes) {
|
|
1835
|
+
return;
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
};
|
|
1839
|
+
walk(root);
|
|
1840
|
+
return {
|
|
1841
|
+
source: chunks.join(''),
|
|
1842
|
+
offsetEntries,
|
|
1843
|
+
};
|
|
1844
|
+
}
|
|
1845
|
+
function assertRequestedPlaydropAssetsAreUsedAtRuntime(input) {
|
|
1846
|
+
if (!input.requirement) {
|
|
1847
|
+
return;
|
|
1848
|
+
}
|
|
1849
|
+
const runtimeSource = readRuntimeSourceText(input.task.projectDir);
|
|
1850
|
+
const source = runtimeSource.source;
|
|
1851
|
+
if (!/\bsdk\.assets\b|\.resolveAppAsset\s*\(|\.listAppAssets\s*\(/.test(source)) {
|
|
1852
|
+
throw new Error('agent_task_playdrop_asset_runtime_missing: the creator asked for PlayDrop assets, but the app source does not load runtime assets through sdk.assets.');
|
|
1853
|
+
}
|
|
1854
|
+
if (input.requirement === 'PACK' && !/\.listAppAssets\s*\(/.test(source)) {
|
|
1855
|
+
throw new Error('agent_task_playdrop_pack_runtime_missing: the creator asked for a PlayDrop asset pack, but the app source does not enumerate pack assets with sdk.assets.listAppAssets().');
|
|
1856
|
+
}
|
|
1857
|
+
if (has3dIntent(input.creatorRequest) && input.task.uses.packs.length > 0 && !/\bGLTFLoader\b|\.gltf\b|\.glb\b|model\/gltf/i.test(source)) {
|
|
1858
|
+
throw new Error('agent_task_playdrop_3d_asset_runtime_missing: the creator asked for a 3D PlayDrop asset pack, but the app source does not load GLB/GLTF models at runtime.');
|
|
1859
|
+
}
|
|
1860
|
+
if (has3dIntent(input.creatorRequest) && (input.task.uses.packs.length > 0 || input.task.uses.assets.length > 0) && hasRuntimeAssetUrlExtensionAssumption(source)) {
|
|
1861
|
+
throw new Error('agent_task_playdrop_3d_url_extension_assumption: PlayDrop runtime asset URLs do not need to end in .glb or .gltf; choose GLB/GLTF files using file.contentType from the SDK asset manifest.');
|
|
1862
|
+
}
|
|
1863
|
+
if (has3dIntent(input.creatorRequest) && input.task.uses.packs.length > 0 && !hasPackRuntimeManifestShape(source)) {
|
|
1864
|
+
throw new Error('agent_task_playdrop_pack_manifest_shape_invalid: 3D PlayDrop pack runtime code must select pack files through asset.assetRef and file.contentType from sdk.assets.listAppAssets().');
|
|
1865
|
+
}
|
|
1866
|
+
if (has3dIntent(input.creatorRequest) && input.task.uses.packs.length > 0 && hasDisallowed3dPlaydropAssetFallback(source)) {
|
|
1867
|
+
throw new Error('agent_task_playdrop_3d_asset_fallback_disallowed: the creator asked for a 3D PlayDrop asset pack, but the app source includes emergency or placeholder mesh fallbacks instead of failing when pack models are unavailable.');
|
|
1868
|
+
}
|
|
1869
|
+
if (has3dIntent(input.creatorRequest) && input.task.uses.packs.length > 0) {
|
|
1870
|
+
const primitiveGeometry = findDisallowedVisible3dPrimitiveGeometry(runtimeSource);
|
|
1871
|
+
if (primitiveGeometry) {
|
|
1872
|
+
throw new Error(`agent_task_playdrop_3d_primitive_geometry_disallowed: the creator asked for a 3D PlayDrop asset pack, but the app source includes visible Three.js primitive geometry instead of real pack models for gameplay visuals. First match: ${primitiveGeometry}.`);
|
|
1873
|
+
}
|
|
1874
|
+
}
|
|
1875
|
+
}
|
|
1876
|
+
function assertNewGameDeclaredAssetsAreUsedAtRuntime(task) {
|
|
1877
|
+
const source = readRuntimeSourceText(task.projectDir).source;
|
|
1878
|
+
if (!/\bsdk\.assets\b|\.resolveAppAsset\s*\(|\.listAppAssets\s*\(/.test(source)) {
|
|
1879
|
+
throw new Error('agent_task_game_assets_runtime_missing: new game tasks must load declared gameplay assets through sdk.assets at runtime.');
|
|
1880
|
+
}
|
|
1881
|
+
if (task.uses.packs.length > 0 && !/\.listAppAssets\s*\(/.test(source)) {
|
|
1882
|
+
throw new Error('agent_task_game_pack_runtime_missing: new game tasks that declare uses.packs must enumerate pack assets with sdk.assets.listAppAssets().');
|
|
1883
|
+
}
|
|
1884
|
+
const hasDirectGameplayAssets = task.ownedAssets.length > 0 || task.uses.assets.length > 0;
|
|
1885
|
+
if (hasDirectGameplayAssets && hasDisallowedGameplayAssetFallback(source)) {
|
|
1886
|
+
throw new Error('agent_task_game_asset_fallback_disallowed: new game tasks must fail clearly when declared gameplay assets cannot load instead of rendering placeholder or fallback visuals.');
|
|
1887
|
+
}
|
|
1888
|
+
}
|
|
1889
|
+
function readPngDimensions(filePath) {
|
|
1890
|
+
const buffer = (0, node_fs_1.readFileSync)(filePath);
|
|
1891
|
+
if (buffer.length < 24) {
|
|
1892
|
+
throw new Error(`agent_task_listing_hero_art_invalid: ${filePath} is too small to be a valid PNG.`);
|
|
1893
|
+
}
|
|
1894
|
+
const isPng = (buffer[0] === 0x89
|
|
1895
|
+
&& buffer[1] === 0x50
|
|
1896
|
+
&& buffer[2] === 0x4e
|
|
1897
|
+
&& buffer[3] === 0x47
|
|
1898
|
+
&& buffer[4] === 0x0d
|
|
1899
|
+
&& buffer[5] === 0x0a
|
|
1900
|
+
&& buffer[6] === 0x1a
|
|
1901
|
+
&& buffer[7] === 0x0a);
|
|
1902
|
+
if (!isPng || buffer.toString('ascii', 12, 16) !== 'IHDR') {
|
|
1903
|
+
throw new Error(`agent_task_listing_hero_art_invalid: ${filePath} must be a PNG with an IHDR header.`);
|
|
1904
|
+
}
|
|
1905
|
+
const width = buffer.readUInt32BE(16);
|
|
1906
|
+
const height = buffer.readUInt32BE(20);
|
|
1907
|
+
if (!Number.isInteger(width) || !Number.isInteger(height) || width <= 0 || height <= 0) {
|
|
1908
|
+
throw new Error(`agent_task_listing_hero_art_invalid: ${filePath} has invalid PNG dimensions.`);
|
|
1909
|
+
}
|
|
1910
|
+
return { width, height };
|
|
1911
|
+
}
|
|
1912
|
+
function readPngTextMetadata(filePath) {
|
|
1913
|
+
const buffer = (0, node_fs_1.readFileSync)(filePath);
|
|
1914
|
+
const metadata = {};
|
|
1915
|
+
if (buffer.length < 24) {
|
|
1916
|
+
return metadata;
|
|
1917
|
+
}
|
|
1918
|
+
let offset = 8;
|
|
1919
|
+
while (offset + 12 <= buffer.length) {
|
|
1920
|
+
const length = buffer.readUInt32BE(offset);
|
|
1921
|
+
const dataStart = offset + 8;
|
|
1922
|
+
const dataEnd = dataStart + length;
|
|
1923
|
+
const chunkEnd = dataEnd + 4;
|
|
1924
|
+
if (chunkEnd > buffer.length) {
|
|
1925
|
+
break;
|
|
1926
|
+
}
|
|
1927
|
+
const type = buffer.toString('ascii', offset + 4, offset + 8);
|
|
1928
|
+
if (type === 'tEXt') {
|
|
1929
|
+
const chunkData = buffer.subarray(dataStart, dataEnd);
|
|
1930
|
+
const separatorOffset = chunkData.indexOf(0);
|
|
1931
|
+
if (separatorOffset > 0) {
|
|
1932
|
+
const keyword = chunkData.toString('latin1', 0, separatorOffset).trim();
|
|
1933
|
+
const value = chunkData.toString('latin1', separatorOffset + 1).trim();
|
|
1934
|
+
if (keyword && value) {
|
|
1935
|
+
metadata[keyword] = value;
|
|
1936
|
+
}
|
|
1937
|
+
}
|
|
1938
|
+
}
|
|
1939
|
+
offset = chunkEnd;
|
|
1940
|
+
if (type === 'IEND') {
|
|
1941
|
+
break;
|
|
1942
|
+
}
|
|
1943
|
+
}
|
|
1944
|
+
return metadata;
|
|
1945
|
+
}
|
|
1946
|
+
function normalizeHeroPromptText(value) {
|
|
1947
|
+
return value.normalize('NFKC').replace(/\s+/g, ' ').trim().toLowerCase();
|
|
1948
|
+
}
|
|
1949
|
+
function heroPromptContainsRequiredTitleInstruction(prompt, displayName) {
|
|
1950
|
+
const normalizedPrompt = normalizeHeroPromptText(prompt);
|
|
1951
|
+
const normalizedDisplayName = normalizeHeroPromptText(displayName);
|
|
1952
|
+
if (!normalizedDisplayName || !normalizedPrompt.includes(normalizedDisplayName)) {
|
|
1953
|
+
return false;
|
|
1954
|
+
}
|
|
1955
|
+
const hasTitleLanguage = /\btitle\b|\blogo\b|\bwordmark\b/.test(normalizedPrompt);
|
|
1956
|
+
const hasProminenceLanguage = /\bfront[-\s]?and[-\s]?center\b|\bfront center\b|\bcenter(?:ed)?\b|\bprominent\b|\blarge readable\b/.test(normalizedPrompt);
|
|
1957
|
+
return hasTitleLanguage && hasProminenceLanguage && heroPromptForbidsExtraReadableText(prompt);
|
|
1958
|
+
}
|
|
1959
|
+
function heroPromptForbidsExtraReadableText(prompt) {
|
|
1960
|
+
const normalizedPrompt = normalizeHeroPromptText(prompt);
|
|
1961
|
+
return /\bno\b[^.]{0,180}\b(?:extra|other|additional)\b[^.]{0,180}\b(?:readable\s+)?(?:text|words|letters|numbers|banners|signs|captions|labels|dates|taglines|subtitles)\b/i.test(normalizedPrompt)
|
|
1962
|
+
|| /\bno\b[^.]{0,180}\b(?:readable\s+)?(?:text|words|letters|numbers|typography|writing|banners|signs|signage|captions|labels|logos|titles|dates|taglines|subtitles)\b/i.test(normalizedPrompt)
|
|
1963
|
+
|| /\bno\b[^.]{0,180}\b(?:readable\s+)?(?:text|words|letters|numbers|banners|signs|captions|labels|dates|taglines|subtitles)\b[^.]{0,180}\banywhere\b/i.test(normalizedPrompt)
|
|
1964
|
+
|| /\bforbid\b[^.]{0,180}\b(?:all\s+)?(?:readable\s+)?(?:text|words|letters|numbers|typography|writing|banners|signs|signage|captions|labels|logos|titles|dates|taglines|subtitles)\b/i.test(normalizedPrompt)
|
|
1965
|
+
|| /\b(?:text|word|letter|typography|writing)[-\s]?free\b/i.test(normalizedPrompt)
|
|
1966
|
+
|| /\bwithout\b[^.]{0,180}\b(?:any\s+)?(?:readable\s+)?(?:text|words|letters|numbers|typography|writing|banners|signs|signage|captions|labels|logos|titles|dates|taglines|subtitles)\b/i.test(normalizedPrompt)
|
|
1967
|
+
|| /\b(?:do\s+not|don't|dont|never)\b[^.]{0,180}\b(?:include|add|render|show|place|draw|write)\b[^.]{0,180}\b(?:any\s+)?(?:readable\s+)?(?:text|words|letters|numbers|typography|writing|banners|signs|signage|captions|labels|logos|titles|dates|taglines|subtitles)\b/i.test(normalizedPrompt)
|
|
1968
|
+
|| /\b(?:avoid|exclude|omit|remove)\b[^.]{0,180}\b(?:all\s+|any\s+)?(?:readable\s+)?(?:text|words|letters|numbers|typography|writing|banners|signs|signage|captions|labels|logos|titles|dates|taglines|subtitles)\b/i.test(normalizedPrompt);
|
|
1969
|
+
}
|
|
1970
|
+
function assertNewGameHeroListingPromptMetadata(input) {
|
|
1971
|
+
const metadata = readPngTextMetadata(input.filePath);
|
|
1972
|
+
const tool = metadata['playdrop:cliOutputTool'] ?? '';
|
|
1973
|
+
const prompt = metadata['playdrop:cliOutputPrompt'] ?? '';
|
|
1974
|
+
const titleTool = metadata['playdrop:listingTitleTool'] ?? '';
|
|
1975
|
+
if (titleTool === 'playdrop compose-listing-title') {
|
|
1976
|
+
throw new Error(`agent_task_listing_hero_art_composed_title_disallowed: ${input.field} must be generated directly by PlayDrop AI with the exact display name "${input.displayName}" integrated front and center, not added afterward by a title overlay script.`);
|
|
1977
|
+
}
|
|
1978
|
+
if (tool !== 'playdrop ai create image --output' || !prompt) {
|
|
1979
|
+
throw new Error(`agent_task_listing_hero_art_metadata_missing: ${input.field} must be produced by "playdrop ai create image --output" or by the staged listing title composition script so PlayDrop can verify listing-art metadata.`);
|
|
1980
|
+
}
|
|
1981
|
+
if (!heroPromptContainsRequiredTitleInstruction(prompt, input.displayName)) {
|
|
1982
|
+
throw new Error(`agent_task_listing_hero_art_title_prompt_missing: ${input.field} must be generated from a prompt containing the exact display name "${input.displayName}" as prominent front-and-center title/logo text and forbidding all extra readable text.`);
|
|
1983
|
+
}
|
|
1984
|
+
}
|
|
1985
|
+
function readAgentTaskCatalogueApp(task) {
|
|
1986
|
+
let data;
|
|
1987
|
+
try {
|
|
1988
|
+
data = JSON.parse((0, node_fs_1.readFileSync)(task.catalogueAbsolutePath, 'utf8'));
|
|
1989
|
+
}
|
|
1990
|
+
catch (error) {
|
|
1991
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
1992
|
+
throw new Error(`agent_task_catalogue_invalid_json: ${task.cataloguePath} must be valid JSON. ${reason}`);
|
|
1993
|
+
}
|
|
1994
|
+
const apps = Array.isArray(data?.apps) ? data.apps : [];
|
|
1995
|
+
const app = apps.find((entry) => entry && typeof entry === 'object' && entry.name === task.name);
|
|
1996
|
+
if (!app || typeof app !== 'object') {
|
|
1997
|
+
throw new Error(`agent_task_catalogue_app_missing: ${task.cataloguePath} must contain apps[] entry "${task.name}".`);
|
|
1998
|
+
}
|
|
1999
|
+
return app;
|
|
2000
|
+
}
|
|
2001
|
+
function assertNewGameHeroListingKeys(task) {
|
|
2002
|
+
const app = readAgentTaskCatalogueApp(task);
|
|
2003
|
+
const listing = app.listing;
|
|
2004
|
+
if (!listing || typeof listing !== 'object' || Array.isArray(listing)) {
|
|
2005
|
+
return;
|
|
2006
|
+
}
|
|
2007
|
+
const hasLegacyPortrait = typeof listing.heroPortraitPath === 'string' && listing.heroPortraitPath.trim().length > 0;
|
|
2008
|
+
const hasLegacyLandscape = typeof listing.heroLandscapePath === 'string' && listing.heroLandscapePath.trim().length > 0;
|
|
2009
|
+
if (hasLegacyPortrait || hasLegacyLandscape) {
|
|
2010
|
+
throw new Error('agent_task_listing_hero_art_legacy_keys: new game tasks must reference hero art through listing.heroPortrait and listing.heroLandscape, not listing.heroPortraitPath or listing.heroLandscapePath.');
|
|
2011
|
+
}
|
|
2012
|
+
}
|
|
2013
|
+
function assertNewGameHeroListingPath(task, filePath, field) {
|
|
2014
|
+
const relativePath = (0, node_path_1.relative)((0, node_path_1.resolve)(task.projectDir), (0, node_path_1.resolve)(filePath)).replace(/\\/g, '/');
|
|
2015
|
+
if (relativePath === '' || relativePath === '..' || relativePath.startsWith('../') || relativePath.startsWith('/')) {
|
|
2016
|
+
throw new Error(`agent_task_listing_hero_art_path_invalid: ${field} must point to a PNG inside the project under assets/marketing/playdrop/.`);
|
|
2017
|
+
}
|
|
2018
|
+
if (!relativePath.startsWith('assets/marketing/playdrop/') || (0, node_path_1.extname)(relativePath).toLowerCase() !== '.png') {
|
|
2019
|
+
throw new Error(`agent_task_listing_hero_art_path_invalid: ${field} must point to a PNG under assets/marketing/playdrop/.`);
|
|
2020
|
+
}
|
|
2021
|
+
}
|
|
2022
|
+
function assertNewGameHeroListingArt(task) {
|
|
2023
|
+
const app = readAgentTaskCatalogueApp(task);
|
|
2024
|
+
const displayName = typeof app.displayName === 'string' && app.displayName.trim()
|
|
2025
|
+
? app.displayName.trim()
|
|
2026
|
+
: task.name;
|
|
2027
|
+
const heroPortraitPath = task.listing?.heroPortraitPath;
|
|
2028
|
+
const heroLandscapePath = task.listing?.heroLandscapePath;
|
|
2029
|
+
if (!heroPortraitPath || !heroLandscapePath) {
|
|
2030
|
+
throw new Error('agent_task_listing_hero_art_missing: new game tasks must include listing.heroPortrait and listing.heroLandscape PNG assets.');
|
|
2031
|
+
}
|
|
2032
|
+
assertNewGameHeroListingKeys(task);
|
|
2033
|
+
assertNewGameHeroListingPath(task, heroPortraitPath, 'listing.heroPortrait');
|
|
2034
|
+
assertNewGameHeroListingPath(task, heroLandscapePath, 'listing.heroLandscape');
|
|
2035
|
+
const portrait = readPngDimensions(heroPortraitPath);
|
|
2036
|
+
if (portrait.width < MIN_HERO_PORTRAIT_WIDTH || portrait.height < MIN_HERO_PORTRAIT_HEIGHT || portrait.height <= portrait.width) {
|
|
2037
|
+
throw new Error(`agent_task_listing_hero_art_invalid: listing.heroPortrait must be a portrait PNG at least ${MIN_HERO_PORTRAIT_WIDTH}x${MIN_HERO_PORTRAIT_HEIGHT}.`);
|
|
2038
|
+
}
|
|
2039
|
+
const landscape = readPngDimensions(heroLandscapePath);
|
|
2040
|
+
if (landscape.width < MIN_HERO_LANDSCAPE_WIDTH || landscape.height < MIN_HERO_LANDSCAPE_HEIGHT || landscape.width <= landscape.height) {
|
|
2041
|
+
throw new Error(`agent_task_listing_hero_art_invalid: listing.heroLandscape must be a landscape PNG at least ${MIN_HERO_LANDSCAPE_WIDTH}x${MIN_HERO_LANDSCAPE_HEIGHT}.`);
|
|
2042
|
+
}
|
|
2043
|
+
assertNewGameHeroListingPromptMetadata({
|
|
2044
|
+
projectDir: task.projectDir,
|
|
2045
|
+
filePath: heroPortraitPath,
|
|
2046
|
+
field: 'listing.heroPortrait',
|
|
2047
|
+
displayName,
|
|
2048
|
+
});
|
|
2049
|
+
assertNewGameHeroListingPromptMetadata({
|
|
2050
|
+
projectDir: task.projectDir,
|
|
2051
|
+
filePath: heroLandscapePath,
|
|
2052
|
+
field: 'listing.heroLandscape',
|
|
2053
|
+
displayName,
|
|
2054
|
+
});
|
|
2055
|
+
}
|
|
2056
|
+
// Task upload path used by "playdrop task upload": same internal pipeline as
|
|
2057
|
+
// "playdrop project publish", but always as one PRIVATE draft version for the
|
|
2058
|
+
// task's creator, with browser launch checks on before the version is created.
|
|
2059
|
+
async function publishWorkerAppProject(input) {
|
|
2060
|
+
const creatorUsername = input.creatorUsername.trim();
|
|
2061
|
+
if (!creatorUsername) {
|
|
2062
|
+
throw new Error('agent_task_creator_username_missing');
|
|
2063
|
+
}
|
|
2064
|
+
if (input.user.username !== creatorUsername && input.user.role !== 'ADMIN') {
|
|
2065
|
+
throw new Error(`worker_publish_on_behalf_requires_admin: the worker session user "${input.user.username}" cannot publish for "${creatorUsername}".`);
|
|
2066
|
+
}
|
|
2067
|
+
const selection = (0, taskSelection_1.selectTasks)(input.projectDir);
|
|
2068
|
+
if (selection.errors.length > 0) {
|
|
2069
|
+
throw new Error(`agent_task_project_invalid: ${selection.errors.map((error) => error.message).join('; ')}`);
|
|
2070
|
+
}
|
|
2071
|
+
const appTask = selection.tasks.find((task) => task.kind === 'app');
|
|
2072
|
+
if (!appTask || selection.tasks.length !== 1) {
|
|
2073
|
+
const found = selection.tasks.map((task) => `${task.kind}:${task.name}`).join(', ') || 'none';
|
|
2074
|
+
throw new Error(`agent_task_project_requires_single_app: expected exactly one app task in ${input.projectDir}, found ${found}.`);
|
|
2075
|
+
}
|
|
2076
|
+
const expectedAppName = input.expectedAppName?.trim() ?? '';
|
|
2077
|
+
if (expectedAppName && appTask.name !== expectedAppName) {
|
|
2078
|
+
throw new Error(`agent_task_output_app_name_mismatch: expected catalogue app "${expectedAppName}", found "${appTask.name}".`);
|
|
2079
|
+
}
|
|
2080
|
+
const task = {
|
|
2081
|
+
...appTask,
|
|
2082
|
+
versionVisibility: 'PRIVATE',
|
|
2083
|
+
sourceArchiveExcludeRelativeFiles: WORKER_SUPERVISOR_SOURCE_EXCLUDES,
|
|
2084
|
+
bundleArchiveExcludeRelativeFiles: WORKER_SUPERVISOR_SOURCE_EXCLUDES,
|
|
2085
|
+
};
|
|
2086
|
+
if (input.kind === 'NEW_GAME') {
|
|
2087
|
+
assertNewGameHeroListingArt(task);
|
|
2088
|
+
if (!appTaskHasRequiredNewGameAssets(task)) {
|
|
2089
|
+
throw new Error('agent_task_game_assets_missing: new game tasks must declare at least one real owned asset, PlayDrop asset, or PlayDrop asset pack in catalogue.json.');
|
|
2090
|
+
}
|
|
2091
|
+
}
|
|
2092
|
+
if (!appTaskSatisfiesPlaydropAssetRequirement(task, input.playdropAssetRequirement)) {
|
|
2093
|
+
throw new Error('agent_task_playdrop_asset_dependency_missing: the creator asked for PlayDrop assets, but catalogue.json does not declare a matching uses.assets, uses.packs, or ownedAssets dependency.');
|
|
2094
|
+
}
|
|
2095
|
+
if (input.kind === 'NEW_GAME') {
|
|
2096
|
+
await assertNewGameDirectPlaydropAssetsAreRuntimeSafe({
|
|
2097
|
+
client: input.client,
|
|
2098
|
+
task,
|
|
2099
|
+
});
|
|
2100
|
+
await assertNewGameOwnedRuntimeImageAssetsAreClean(task);
|
|
2101
|
+
assertNewGameDeclaredAssetsAreUsedAtRuntime(task);
|
|
2102
|
+
}
|
|
2103
|
+
assertRequestedPlaydropAssetsAreUsedAtRuntime({
|
|
2104
|
+
task,
|
|
2105
|
+
requirement: input.playdropAssetRequirement,
|
|
2106
|
+
creatorRequest: input.creatorRequest,
|
|
2107
|
+
});
|
|
2108
|
+
await assertDeclared3dPlaydropPacksContainRuntimeModels({
|
|
2109
|
+
client: input.client,
|
|
2110
|
+
task,
|
|
2111
|
+
creatorRequest: input.creatorRequest,
|
|
2112
|
+
});
|
|
2113
|
+
const { app } = await (0, registration_1.ensureRegisteredHostedAppShell)({
|
|
2114
|
+
client: input.client,
|
|
2115
|
+
creatorUsername,
|
|
2116
|
+
task,
|
|
2117
|
+
agentTaskId: input.taskId,
|
|
2118
|
+
});
|
|
2119
|
+
if (typeof app.id !== 'number' || !Number.isInteger(app.id) || app.id <= 0) {
|
|
2120
|
+
throw new Error(`App "${task.name}" registration did not return an app id.`);
|
|
2121
|
+
}
|
|
2122
|
+
const { upload: uploadResult, warnings } = await (0, apps_1.runAppPipeline)(input.client, task, {
|
|
2123
|
+
creatorUsername,
|
|
2124
|
+
apiBase: input.apiBase,
|
|
2125
|
+
webBase: normalizePortalBase(input.webBase ?? undefined),
|
|
2126
|
+
token: input.token,
|
|
2127
|
+
user: input.user,
|
|
2128
|
+
runLocalLaunchCheck: true,
|
|
2129
|
+
runStagedUploadLaunchCheck: true,
|
|
2130
|
+
ensureRegisteredAppShell: true,
|
|
2131
|
+
agentTaskId: input.taskId,
|
|
2132
|
+
});
|
|
2133
|
+
if (!uploadResult.versionCreated || !uploadResult.version) {
|
|
2134
|
+
throw new Error(`App "${task.name}" upload did not return a created version.`);
|
|
2135
|
+
}
|
|
2136
|
+
if (typeof uploadResult.versionId !== 'number') {
|
|
2137
|
+
throw new Error(`App "${task.name}" upload did not return version ID.`);
|
|
2138
|
+
}
|
|
2139
|
+
if (typeof uploadResult.versionNodeId !== 'string' || uploadResult.versionNodeId.trim().length === 0) {
|
|
2140
|
+
throw new Error(`App "${task.name}" upload did not return versionNodeId.`);
|
|
2141
|
+
}
|
|
2142
|
+
return {
|
|
2143
|
+
appId: app.id,
|
|
2144
|
+
appVersionId: uploadResult.versionId,
|
|
2145
|
+
versionNodeId: uploadResult.versionNodeId,
|
|
2146
|
+
version: uploadResult.version,
|
|
2147
|
+
appName: task.name,
|
|
2148
|
+
creatorUsername,
|
|
2149
|
+
warnings: [...selection.warnings, ...warnings],
|
|
2150
|
+
};
|
|
2151
|
+
}
|