@playdrop/playdrop-cli 0.18.8 → 0.19.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.
Files changed (69) hide show
  1. package/README.md +27 -0
  2. package/config/client-meta.json +2 -2
  3. package/dist/apps/upload.d.ts +2 -0
  4. package/dist/apps/upload.js +146 -60
  5. package/dist/catalogue.d.ts +18 -0
  6. package/dist/catalogue.js +171 -56
  7. package/dist/commands/devServer.js +3 -0
  8. package/dist/commands/validate.js +1 -0
  9. package/dist/commands/worker/game-source-git.js +7 -0
  10. package/dist/commands/worker/generation.d.ts +16 -1
  11. package/dist/commands/worker/generation.js +8 -2
  12. package/dist/commands/worker/ugc-processing.d.ts +13 -0
  13. package/dist/commands/worker/ugc-processing.js +146 -0
  14. package/dist/commands/worker.d.ts +27 -0
  15. package/dist/commands/worker.js +127 -38
  16. package/dist/proxyFetch.d.ts +5 -0
  17. package/dist/proxyFetch.js +10 -35
  18. package/dist/uploadDispatchers.d.ts +3 -0
  19. package/dist/uploadDispatchers.js +10 -0
  20. package/node_modules/@playdrop/api-client/dist/acquisition.d.ts +3 -1
  21. package/node_modules/@playdrop/api-client/dist/acquisition.d.ts.map +1 -1
  22. package/node_modules/@playdrop/api-client/dist/acquisition.js +2 -2
  23. package/node_modules/@playdrop/api-client/dist/chat.d.ts +13 -4
  24. package/node_modules/@playdrop/api-client/dist/chat.d.ts.map +1 -1
  25. package/node_modules/@playdrop/api-client/dist/client.d.ts +31 -10
  26. package/node_modules/@playdrop/api-client/dist/client.d.ts.map +1 -1
  27. package/node_modules/@playdrop/api-client/dist/core/request.d.ts.map +1 -1
  28. package/node_modules/@playdrop/api-client/dist/core/request.js +11 -1
  29. package/node_modules/@playdrop/api-client/dist/domains/chat.d.ts +30 -9
  30. package/node_modules/@playdrop/api-client/dist/domains/chat.d.ts.map +1 -1
  31. package/node_modules/@playdrop/api-client/dist/domains/chat.js +24 -19
  32. package/node_modules/@playdrop/api-client/dist/domains/comments.d.ts +3 -1
  33. package/node_modules/@playdrop/api-client/dist/domains/comments.d.ts.map +1 -1
  34. package/node_modules/@playdrop/api-client/dist/domains/comments.js +2 -2
  35. package/node_modules/@playdrop/api-client/dist/index.d.ts +17 -6
  36. package/node_modules/@playdrop/api-client/dist/index.d.ts.map +1 -1
  37. package/node_modules/@playdrop/api-client/dist/index.js +12 -10
  38. package/node_modules/@playdrop/api-client/dist/runtime.d.ts +3 -0
  39. package/node_modules/@playdrop/api-client/dist/runtime.d.ts.map +1 -1
  40. package/node_modules/@playdrop/api-client/dist/runtime.js +10 -1
  41. package/node_modules/@playdrop/config/client-meta.json +2 -2
  42. package/node_modules/@playdrop/game-source-git/dist/src/repository.d.ts +8 -0
  43. package/node_modules/@playdrop/game-source-git/dist/src/repository.d.ts.map +1 -1
  44. package/node_modules/@playdrop/game-source-git/dist/src/repository.js +30 -17
  45. package/node_modules/@playdrop/game-source-git/dist/test/core.test.js +10 -0
  46. package/node_modules/@playdrop/game-source-git/dist/tsconfig.tsbuildinfo +1 -1
  47. package/node_modules/@playdrop/game-source-git/package.json +2 -1
  48. package/node_modules/@playdrop/types/dist/agent-task-terminal.d.ts.map +1 -1
  49. package/node_modules/@playdrop/types/dist/agent-task-terminal.js +1 -1
  50. package/node_modules/@playdrop/types/dist/api.d.ts +30 -3
  51. package/node_modules/@playdrop/types/dist/api.d.ts.map +1 -1
  52. package/node_modules/@playdrop/types/dist/api.js +1 -0
  53. package/node_modules/@playdrop/types/dist/app-listing-localization.d.ts +34 -0
  54. package/node_modules/@playdrop/types/dist/app-listing-localization.d.ts.map +1 -0
  55. package/node_modules/@playdrop/types/dist/app-listing-localization.js +146 -0
  56. package/node_modules/@playdrop/types/dist/chat.d.ts +7 -2
  57. package/node_modules/@playdrop/types/dist/chat.d.ts.map +1 -1
  58. package/node_modules/@playdrop/types/dist/index.d.ts +3 -0
  59. package/node_modules/@playdrop/types/dist/index.d.ts.map +1 -1
  60. package/node_modules/@playdrop/types/dist/index.js +6 -0
  61. package/node_modules/@playdrop/types/dist/ugc-processing.d.ts +211 -0
  62. package/node_modules/@playdrop/types/dist/ugc-processing.d.ts.map +1 -0
  63. package/node_modules/@playdrop/types/dist/ugc-processing.js +144 -0
  64. package/node_modules/@playdrop/types/dist/ugc.d.ts +39 -0
  65. package/node_modules/@playdrop/types/dist/ugc.d.ts.map +1 -0
  66. package/node_modules/@playdrop/types/dist/ugc.js +70 -0
  67. package/node_modules/@playdrop/types/dist/version.d.ts +4 -0
  68. package/node_modules/@playdrop/types/dist/version.d.ts.map +1 -1
  69. package/package.json +3 -2
package/README.md CHANGED
@@ -153,3 +153,30 @@ For direct first-party server uploads from this checkout, start `npm run dev:age
153
153
  playdrop documentation browse
154
154
  playdrop documentation read runtime
155
155
  ```
156
+
157
+ ## Localized game listings
158
+
159
+ An app in `catalogue.json` can declare a BCP47 `sourceLocale` (including languages such as Italian or Arabic) and an inline `listingLocalizations` object keyed by a supported PlayDrop locale. The original app fields remain the source listing. Each localization is a complete presentation: `displayName`, optional `subtitle`, `description`, `controls`, `makingOf`, `releaseNotes`, and its own `listing` media paths and captions. Omitted optional fields and arrays stay empty; they do not inherit source values. Explicitly reuse a source path when the same artwork should appear in both languages. Social artwork accepts PNG or JPEG, other images use PNG, and trailers use MP4.
160
+
161
+ Run `playdrop project validate .` to obtain `listingSourceHash`. Each localization records that value as `sourceHash`. Source text or media changes make older localizations stale; runtime version changes do not. Stale localizations remain inspectable but are excluded from public presentation. Uploads replace the complete localization set, so omitting an entry removes that locale from the new version.
162
+
163
+ ```json
164
+ {
165
+ "sourceLocale": "en",
166
+ "listingLocalizations": {
167
+ "fr": {
168
+ "sourceHash": "<64-character listingSourceHash from validation>",
169
+ "displayName": "Mon jeu",
170
+ "description": "Description du jeu.",
171
+ "listing": {
172
+ "heroLandscape": "listing/hero-landscape.png",
173
+ "screenshotsLandscape": [
174
+ { "path": "listing/fr/screenshot.png", "alt": "Une partie en cours" }
175
+ ]
176
+ }
177
+ }
178
+ }
179
+ }
180
+ ```
181
+
182
+ Cloud task checkout, upload, and publish preserve this contract and include localized listing files in the source sync and listing locks.
@@ -1,7 +1,7 @@
1
1
  {
2
- "version": "0.18.8",
2
+ "version": "0.19.0",
3
3
  "build": 1,
4
- "runtimeSdkVersion": "0.18.8",
4
+ "runtimeSdkVersion": "0.19.0",
5
5
  "runtimeSdkBuild": 1,
6
6
  "clients": {
7
7
  "all": {
@@ -1,5 +1,6 @@
1
1
  import type { ApiClient } from "@playdrop/api-client";
2
2
  import type { AgentTaskPlaytestProof, FinalizeAppUploadRequest, UserResponse } from "@playdrop/types";
3
+ import { type AppListingLocalization } from "@playdrop/types";
3
4
  import type { AppTask } from "../catalogue";
4
5
  import type { TaskCaptureSession } from "../appUrls";
5
6
  import type { AppBuildArtifacts } from "./build";
@@ -73,6 +74,7 @@ type PreparedAppUploadFiles = {
73
74
  sessionFiles: PreparedSessionFile[];
74
75
  ownedAssets: PreparedOwnedAssetUpload[];
75
76
  gameServerBuild: GameServerBuild | null;
77
+ listingLocalizations: AppListingLocalization[];
76
78
  };
77
79
  export declare function prepareAppUploadFiles(task: AppTask, artifacts: AppBuildArtifacts | null, options?: {
78
80
  apiBase?: string;
@@ -7,6 +7,8 @@ const types_1 = require("@playdrop/types");
7
7
  const node_crypto_1 = require("node:crypto");
8
8
  const node_fs_1 = require("node:fs");
9
9
  const node_path_1 = require("node:path");
10
+ const proxyFetch_1 = require("../proxyFetch");
11
+ const uploadDispatchers_1 = require("../uploadDispatchers");
10
12
  const http_1 = require("../http");
11
13
  const proceduralValidation_1 = require("../proceduralValidation");
12
14
  const game_source_git_1 = require("../commands/worker/game-source-git");
@@ -178,17 +180,60 @@ function pushPreparedArtifact(task, currentTotal, file, maxBytes, code, label) {
178
180
  const bytes = ensurePreparedArtifactSize(file, maxBytes, code, label);
179
181
  return accumulateUploadBytes(task.name, currentTotal, bytes);
180
182
  }
181
- async function uploadSignedObject(client, creatorUsername, appName, sessionId, file, descriptor) {
183
+ function objectUploadTransportCode(error) {
184
+ return error && typeof error === "object" && "code" in error &&
185
+ typeof error.code === "string" && /^[A-Z][A-Z0-9_]{0,63}$/.test(error.code)
186
+ ? error.code
187
+ : null;
188
+ }
189
+ function objectUploadTransportDetail(error, signedUrl) {
190
+ const details = [];
191
+ const seen = new Set();
192
+ let current = error;
193
+ while (current && typeof current === "object" && !seen.has(current) && seen.size < 4) {
194
+ seen.add(current);
195
+ const value = current;
196
+ const code = objectUploadTransportCode(value);
197
+ // Error messages can echo the signed request. Keep the transport explanation,
198
+ // never its URL, authorization header or credentials, in task/operator logs.
199
+ const message = typeof value.message === "string" ? value.message
200
+ .split(signedUrl).join("[redacted-url]")
201
+ .replace(/\bhttps?:\/\/[^\s"'<>]+/gi, "[redacted-url]")
202
+ .replace(/\b(?:proxy-authorization|authorization)\s*:\s*[^\r\n]+/gi, "authorization: [redacted]")
203
+ .replace(/\b(?:Bearer|Basic)\s+[^\s,;]+/gi, "[redacted-authorization]")
204
+ .replace(/\b(?:X-Goog-(?:Signature|Credential|Security-Token)|token|signature|credential|authorization)\b["']?\s*[=:]\s*["']?[^\s"',;}]+/gi, "[redacted-credential]")
205
+ .replace(/[\x00-\x1f\x7f]+/g, " ").trim().slice(0, 200) : "";
206
+ if (message || code)
207
+ details.push([code, message].filter(Boolean).join(": "));
208
+ current = value.cause;
209
+ }
210
+ return details.join(" <- ").slice(0, 400);
211
+ }
212
+ async function uploadSignedObject(client, creatorUsername, appName, sessionId, file, descriptor, transport) {
182
213
  for (let attempt = 0; attempt < 2; attempt += 1) {
183
- const response = await fetch(descriptor.url, {
184
- method: descriptor.method,
185
- headers: descriptor.headers,
186
- body: file.file,
187
- });
214
+ let response;
215
+ try {
216
+ response = await fetch(descriptor.url, {
217
+ method: descriptor.method,
218
+ headers: descriptor.headers,
219
+ body: file.file,
220
+ ...transport,
221
+ });
222
+ }
223
+ catch (error) {
224
+ const cause = error instanceof Error ? error.cause : null;
225
+ const code = objectUploadTransportCode(cause) ?? objectUploadTransportCode(error) ?? "UNKNOWN";
226
+ const detail = objectUploadTransportDetail(cause ?? error, descriptor.url);
227
+ const annotated = new Error(`direct_object_upload_network_failed:${file.relativePath}:${code}${detail ? `: ${detail}` : ""}`);
228
+ Object.defineProperty(annotated, "cause", { value: error, configurable: true, writable: true });
229
+ throw annotated;
230
+ }
188
231
  if (response.ok) {
232
+ await response.body?.cancel();
189
233
  return;
190
234
  }
191
235
  if ((response.status === 401 || response.status === 403) && attempt === 0) {
236
+ await response.body?.cancel();
192
237
  const signed = await client.signAppUploadObjects(creatorUsername, appName, sessionId, {
193
238
  objectIds: [file.id],
194
239
  });
@@ -204,23 +249,48 @@ async function uploadSignedObject(client, creatorUsername, appName, sessionId, f
204
249
  }
205
250
  }
206
251
  async function uploadSignedObjects(client, creatorUsername, appName, sessionId, files) {
207
- for (let offset = 0; offset < files.length; offset += 100) {
208
- const batch = files.slice(offset, offset + 100);
209
- const signed = await client.signAppUploadObjects(creatorUsername, appName, sessionId, {
210
- objectIds: batch.map((file) => file.id),
211
- });
212
- const descriptors = new Map(signed.uploads.map((entry) => [entry.id, entry]));
213
- let cursor = 0;
214
- const workers = Array.from({ length: Math.min(8, batch.length) }, async () => {
215
- while (cursor < batch.length) {
216
- const file = batch[cursor++];
217
- const descriptor = descriptors.get(file.id);
218
- // A missing descriptor means the API verified this object is stored.
219
- if (descriptor)
220
- await uploadSignedObject(client, creatorUsername, appName, sessionId, file, descriptor);
221
- }
222
- });
223
- await Promise.all(workers);
252
+ if (files.length === 0)
253
+ return;
254
+ // Uploads run in the long-lived worker supervisor. Own this pool here so a
255
+ // later upload cannot inherit its connections or Node's changing H2 defaults.
256
+ const dispatcher = new uploadDispatchers_1.ObjectUploadAgent({
257
+ ...(0, proxyFetch_1.resolveProxyEnvironment)(),
258
+ allowH2: false,
259
+ requestTls: { allowH2: false },
260
+ proxyTls: { allowH2: false },
261
+ });
262
+ const controller = new AbortController();
263
+ const transport = { dispatcher, signal: controller.signal };
264
+ try {
265
+ for (let offset = 0; offset < files.length; offset += 100) {
266
+ const batch = files.slice(offset, offset + 100);
267
+ const signed = await client.signAppUploadObjects(creatorUsername, appName, sessionId, {
268
+ objectIds: batch.map((file) => file.id),
269
+ });
270
+ const descriptors = new Map(signed.uploads.map((entry) => [entry.id, entry]));
271
+ let cursor = 0;
272
+ const workers = Array.from({ length: Math.min(8, batch.length) }, async () => {
273
+ try {
274
+ while (!controller.signal.aborted && cursor < batch.length) {
275
+ const file = batch[cursor++];
276
+ const descriptor = descriptors.get(file.id);
277
+ // A missing descriptor means the API verified this object is stored.
278
+ if (descriptor)
279
+ await uploadSignedObject(client, creatorUsername, appName, sessionId, file, descriptor, transport);
280
+ }
281
+ }
282
+ catch (error) {
283
+ // Preserve the triggering failure, cancel peers and stop taking work.
284
+ controller.abort(error);
285
+ }
286
+ });
287
+ await Promise.all(workers);
288
+ controller.signal.throwIfAborted();
289
+ }
290
+ }
291
+ finally {
292
+ // Finish cancellation before the caller aborts the server upload session.
293
+ await dispatcher.destroy();
224
294
  }
225
295
  }
226
296
  async function prepareAppUploadFiles(task, artifacts, options = {}) {
@@ -264,46 +334,59 @@ async function prepareAppUploadFiles(task, artifacts, options = {}) {
264
334
  sessionFiles.push(await buildPreparedSessionFile("SERVER", "server/bundle.mjs", bundle.file));
265
335
  sessionFiles.push(await buildPreparedSessionFile("SERVER", "server/manifest.json", manifest.file));
266
336
  }
267
- if (listing?.iconPath) {
268
- const prepared = createFileFromPath(listing.iconPath, "image/png", config_1.MAX_VERSION_ICON_BYTES, "icon_too_large", "invalid_icon_content_type", "Icon", isPngSignature);
269
- totalUploadBytes = pushPreparedFile(task.name, totalUploadBytes, prepared);
270
- sessionFiles.push(await buildPreparedSessionFile("LISTING", "listing/icon.png", prepared.file));
271
- }
272
- if (listing?.heroPortraitPath) {
273
- const prepared = createFileFromPath(listing.heroPortraitPath, "image/png", config_1.MAX_VERSION_HERO_BYTES, "hero_portrait_too_large", "invalid_hero_portrait_content_type", "Hero portrait", isPngSignature);
274
- totalUploadBytes = pushPreparedFile(task.name, totalUploadBytes, prepared);
275
- sessionFiles.push(await buildPreparedSessionFile("LISTING", "listing/hero-portrait.png", prepared.file));
276
- }
277
- if (listing?.heroLandscapePath) {
278
- const prepared = createFileFromPath(listing.heroLandscapePath, "image/png", config_1.MAX_VERSION_HERO_BYTES, "hero_landscape_too_large", "invalid_hero_landscape_content_type", "Hero landscape", isPngSignature);
279
- totalUploadBytes = pushPreparedFile(task.name, totalUploadBytes, prepared);
280
- sessionFiles.push(await buildPreparedSessionFile("LISTING", "listing/hero-landscape.png", prepared.file));
281
- }
282
- for (let index = 0; index < (listing?.screenshotPortraitPaths ?? []).length; index += 1) {
283
- const filePath = listing.screenshotPortraitPaths[index];
284
- const prepared = createFileFromPath(filePath, "image/png", config_1.MAX_VERSION_SCREENSHOT_BYTES, "screenshot_portrait_too_large", "invalid_screenshot_portrait_content_type", `Portrait screenshot ${index + 1}`, isPngSignature);
337
+ const preparedByHash = new Map();
338
+ const prepareListingFile = async (filePath, destination, slot, reuse) => {
339
+ const video = slot.startsWith("videos");
340
+ const socialJpeg = slot === "socialLandscapePath" && [".jpg", ".jpeg"].includes((0, node_path_1.extname)(filePath).toLowerCase());
341
+ if (socialJpeg)
342
+ destination = destination.replace(/\.png$/, ".jpg");
343
+ const maxBytes = video ? config_1.MAX_VERSION_VIDEO_BYTES : slot === "iconPath" ? config_1.MAX_VERSION_ICON_BYTES : slot.startsWith("screenshots") ? config_1.MAX_VERSION_SCREENSHOT_BYTES : config_1.MAX_VERSION_HERO_BYTES;
344
+ const prepared = createFileFromPath(filePath, video ? "video/mp4" : socialJpeg ? "image/jpeg" : "image/png", maxBytes, "listing_media_too_large", "invalid_listing_media_content_type", destination, video ? isMp4Signature : socialJpeg ? (bytes) => bytes[0] === 0xff && bytes[1] === 0xd8 && bytes[2] === 0xff : isPngSignature);
345
+ const file = await buildPreparedSessionFile("LISTING", destination, prepared.file);
346
+ const hashKey = `${file.contentType}:${file.sha256}`;
347
+ const existing = preparedByHash.get(hashKey);
348
+ if (reuse && existing)
349
+ return existing;
285
350
  totalUploadBytes = pushPreparedFile(task.name, totalUploadBytes, prepared);
286
- sessionFiles.push(await buildPreparedSessionFile("LISTING", `listing/screenshots/portrait/${index}.png`, prepared.file));
287
- }
288
- for (let index = 0; index < (listing?.screenshotLandscapePaths ?? []).length; index += 1) {
289
- const filePath = listing.screenshotLandscapePaths[index];
290
- const prepared = createFileFromPath(filePath, "image/png", config_1.MAX_VERSION_SCREENSHOT_BYTES, "screenshot_landscape_too_large", "invalid_screenshot_landscape_content_type", `Landscape screenshot ${index + 1}`, isPngSignature);
291
- totalUploadBytes = pushPreparedFile(task.name, totalUploadBytes, prepared);
292
- sessionFiles.push(await buildPreparedSessionFile("LISTING", `listing/screenshots/landscape/${index}.png`, prepared.file));
351
+ sessionFiles.push(file);
352
+ preparedByHash.set(hashKey, destination);
353
+ return destination;
354
+ };
355
+ const singletonSlots = [
356
+ ["iconPath", "icon.png"], ["heroPortraitPath", "hero-portrait.png"],
357
+ ["heroLandscapePath", "hero-landscape.png"], ["socialLandscapePath", "social-landscape.png"],
358
+ ];
359
+ const arraySlots = [
360
+ ["screenshotsPortrait", "screenshotPortraitPaths", "screenshots/portrait", "png"],
361
+ ["screenshotsLandscape", "screenshotLandscapePaths", "screenshots/landscape", "png"],
362
+ ["videosPortrait", "videoPortraitPaths", "videos/portrait", "mp4"],
363
+ ["videosLandscape", "videoLandscapePaths", "videos/landscape", "mp4"],
364
+ ];
365
+ for (const [slot, filename] of singletonSlots) {
366
+ const file = listing?.[slot];
367
+ if (file)
368
+ await prepareListingFile(file, `listing/${filename}`, slot, false);
293
369
  }
294
- for (let index = 0; index < (listing?.videoPortraitPaths ?? []).length; index += 1) {
295
- const filePath = listing.videoPortraitPaths[index];
296
- const prepared = createFileFromPath(filePath, "video/mp4", config_1.MAX_VERSION_VIDEO_BYTES, "video_portrait_too_large", "invalid_video_portrait_content_type", `Portrait video ${index + 1}`, isMp4Signature);
297
- totalUploadBytes = pushPreparedFile(task.name, totalUploadBytes, prepared);
298
- sessionFiles.push(await buildPreparedSessionFile("LISTING", `listing/videos/portrait/${index}.mp4`, prepared.file));
370
+ for (const [slot, field, directory, extension] of arraySlots) {
371
+ const files = listing?.[field] ?? [];
372
+ for (const [index, file] of files.entries())
373
+ await prepareListingFile(file, `listing/${directory}/${index}.${extension}`, slot, false);
299
374
  }
300
- for (let index = 0; index < (listing?.videoLandscapePaths ?? []).length; index += 1) {
301
- const filePath = listing.videoLandscapePaths[index];
302
- const prepared = createFileFromPath(filePath, "video/mp4", config_1.MAX_VERSION_VIDEO_BYTES, "video_landscape_too_large", "invalid_video_landscape_content_type", `Landscape video ${index + 1}`, isMp4Signature);
303
- totalUploadBytes = pushPreparedFile(task.name, totalUploadBytes, prepared);
304
- sessionFiles.push(await buildPreparedSessionFile("LISTING", `listing/videos/landscape/${index}.mp4`, prepared.file));
375
+ const listingLocalizations = [];
376
+ for (const localization of task.listingLocalizations ?? []) {
377
+ const localized = { ...localization };
378
+ for (const [slot, filename] of singletonSlots) {
379
+ const file = localization[slot];
380
+ localized[slot] = file ? await prepareListingFile(file, `listing/locales/${localization.locale}/${filename}`, slot, true) : null;
381
+ }
382
+ for (const [slot, , directory, extension] of arraySlots) {
383
+ localized[slot] = [];
384
+ for (const [index, file] of localization[slot].entries())
385
+ localized[slot].push(await prepareListingFile(file, `listing/locales/${localization.locale}/${directory}/${index}.${extension}`, slot, true));
386
+ }
387
+ listingLocalizations.push(localized);
305
388
  }
306
- return { sessionFiles, ownedAssets, gameServerBuild: gameServer };
389
+ return { sessionFiles, ownedAssets, gameServerBuild: gameServer, listingLocalizations };
307
390
  }
308
391
  async function uploadApp(client, task, artifacts, options) {
309
392
  // Version is required - no legacy fallback
@@ -336,7 +419,7 @@ async function uploadAppVersion(client, task, artifacts, options) {
336
419
  "The prepared upload does not match the project; finish edits and checks before retrying.");
337
420
  }
338
421
  };
339
- const { sessionFiles: preparedSessionFiles, ownedAssets: preparedOwnedAssets, gameServerBuild } = await prepareAppUploadFiles(task, artifacts, { apiBase: options?.apiBase, client });
422
+ const { sessionFiles: preparedSessionFiles, ownedAssets: preparedOwnedAssets, gameServerBuild, listingLocalizations } = await prepareAppUploadFiles(task, artifacts, { apiBase: options?.apiBase, client });
340
423
  await assertPublicationUnchanged(inputFingerprint, false);
341
424
  const publicationFingerprint = options?.prepareGitTransition
342
425
  ? await (0, game_source_git_1.fingerprintGameSourcePublication)(task.projectDir)
@@ -354,6 +437,9 @@ async function uploadAppVersion(client, task, artifacts, options) {
354
437
  })));
355
438
  const preparedObjects = [...preparedSessionFiles, ...preparedOwnedAssetFiles];
356
439
  const initializeRequest = {
440
+ sourceLocale: task.sourceLocale,
441
+ listingSourceHash: task.listingSourceHash,
442
+ listingLocalizations,
357
443
  version: task.version,
358
444
  agentTaskId: options?.agentTaskId,
359
445
  playtestProof: options?.playtestProof,
@@ -1,3 +1,4 @@
1
+ import { type AppListingContent, type AppListingLocalization } from "@playdrop/types";
1
2
  import { type AppAchievementCatalogueDefinition, type AppAuthMode, type AppControllerMode, type AppCreationMetadata, type AppHostingMode, type AppLeaderboardCatalogueDefinition, type AppListingMediaMetadata, type AppMetadataAssetSpecSupport, type AppPlaytestTapes, type AppPlayersConfig, type AppSurface, type AppType, type AppTweaksDeclaration, type AppVersionDesign, type AppVersionVisibility, type AssetSpecContract, type AssetSpecStatus, type AssetSpecValidationKind, type CatalogueTagGroupDefinition, type ContentLicense, type PlayerMetaDefinitionStatus } from "@playdrop/types";
2
3
  import { type AssetSpecSupportDeclaration } from "./assetSpecs";
3
4
  export type CatalogueJson = {
@@ -21,6 +22,7 @@ export type AppListingConfig = {
21
22
  icon?: string;
22
23
  heroPortrait?: string;
23
24
  heroLandscape?: string;
25
+ socialLandscape?: string;
24
26
  screenshotsPortrait?: AppListingMediaConfig[];
25
27
  screenshotsLandscape?: AppListingMediaConfig[];
26
28
  videosPortrait?: AppListingMediaConfig[];
@@ -177,6 +179,7 @@ export type ResolvedListingAssets = {
177
179
  iconPath?: string;
178
180
  heroPortraitPath?: string;
179
181
  heroLandscapePath?: string;
182
+ socialLandscapePath?: string;
180
183
  screenshotPortraitPaths: string[];
181
184
  screenshotLandscapePaths: string[];
182
185
  videoPortraitPaths: string[];
@@ -194,6 +197,9 @@ export type AppServerTask = {
194
197
  rooms: string[];
195
198
  };
196
199
  export type AppTask = {
200
+ sourceLocale?: string | null;
201
+ listingSourceHash?: string;
202
+ listingLocalizations?: AppListingLocalization[];
197
203
  kind: "app";
198
204
  name: string;
199
205
  cataloguePath: string;
@@ -437,4 +443,16 @@ export declare function findAppTaskByFile(htmlPath: string, options?: Pick<Catal
437
443
  warnings: string[];
438
444
  errors: string[];
439
445
  };
446
+ export declare function catalogueListingContent(task: {
447
+ displayName?: string;
448
+ subtitle?: string;
449
+ description?: string;
450
+ controls?: string;
451
+ makingOf?: string;
452
+ releaseNotes?: string;
453
+ listing?: ResolvedListingAssets;
454
+ }): AppListingContent;
455
+ export declare function calculateCatalogueListingSourceHash(task: Parameters<typeof catalogueListingContent>[0] & {
456
+ sourceLocale?: string | null;
457
+ }): string;
440
458
  export {};