@playdrop/playdrop-cli 0.18.9 → 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 (66) 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 +133 -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.js +86 -20
  15. package/dist/proxyFetch.d.ts +5 -0
  16. package/dist/proxyFetch.js +10 -35
  17. package/dist/uploadDispatchers.d.ts +3 -0
  18. package/dist/uploadDispatchers.js +10 -0
  19. package/node_modules/@playdrop/api-client/dist/acquisition.d.ts +3 -1
  20. package/node_modules/@playdrop/api-client/dist/acquisition.d.ts.map +1 -1
  21. package/node_modules/@playdrop/api-client/dist/acquisition.js +2 -2
  22. package/node_modules/@playdrop/api-client/dist/chat.d.ts +13 -4
  23. package/node_modules/@playdrop/api-client/dist/chat.d.ts.map +1 -1
  24. package/node_modules/@playdrop/api-client/dist/client.d.ts +31 -10
  25. package/node_modules/@playdrop/api-client/dist/client.d.ts.map +1 -1
  26. package/node_modules/@playdrop/api-client/dist/core/request.d.ts.map +1 -1
  27. package/node_modules/@playdrop/api-client/dist/core/request.js +11 -1
  28. package/node_modules/@playdrop/api-client/dist/domains/chat.d.ts +30 -9
  29. package/node_modules/@playdrop/api-client/dist/domains/chat.d.ts.map +1 -1
  30. package/node_modules/@playdrop/api-client/dist/domains/chat.js +24 -19
  31. package/node_modules/@playdrop/api-client/dist/domains/comments.d.ts +3 -1
  32. package/node_modules/@playdrop/api-client/dist/domains/comments.d.ts.map +1 -1
  33. package/node_modules/@playdrop/api-client/dist/domains/comments.js +2 -2
  34. package/node_modules/@playdrop/api-client/dist/index.d.ts +17 -6
  35. package/node_modules/@playdrop/api-client/dist/index.d.ts.map +1 -1
  36. package/node_modules/@playdrop/api-client/dist/index.js +12 -10
  37. package/node_modules/@playdrop/api-client/dist/runtime.d.ts +3 -0
  38. package/node_modules/@playdrop/api-client/dist/runtime.d.ts.map +1 -1
  39. package/node_modules/@playdrop/api-client/dist/runtime.js +10 -1
  40. package/node_modules/@playdrop/config/client-meta.json +2 -2
  41. package/node_modules/@playdrop/game-source-git/dist/src/repository.d.ts +8 -0
  42. package/node_modules/@playdrop/game-source-git/dist/src/repository.d.ts.map +1 -1
  43. package/node_modules/@playdrop/game-source-git/dist/src/repository.js +30 -17
  44. package/node_modules/@playdrop/game-source-git/dist/test/core.test.js +10 -0
  45. package/node_modules/@playdrop/game-source-git/dist/tsconfig.tsbuildinfo +1 -1
  46. package/node_modules/@playdrop/game-source-git/package.json +2 -1
  47. package/node_modules/@playdrop/types/dist/api.d.ts +30 -3
  48. package/node_modules/@playdrop/types/dist/api.d.ts.map +1 -1
  49. package/node_modules/@playdrop/types/dist/api.js +1 -0
  50. package/node_modules/@playdrop/types/dist/app-listing-localization.d.ts +34 -0
  51. package/node_modules/@playdrop/types/dist/app-listing-localization.d.ts.map +1 -0
  52. package/node_modules/@playdrop/types/dist/app-listing-localization.js +146 -0
  53. package/node_modules/@playdrop/types/dist/chat.d.ts +7 -2
  54. package/node_modules/@playdrop/types/dist/chat.d.ts.map +1 -1
  55. package/node_modules/@playdrop/types/dist/index.d.ts +3 -0
  56. package/node_modules/@playdrop/types/dist/index.d.ts.map +1 -1
  57. package/node_modules/@playdrop/types/dist/index.js +6 -0
  58. package/node_modules/@playdrop/types/dist/ugc-processing.d.ts +211 -0
  59. package/node_modules/@playdrop/types/dist/ugc-processing.d.ts.map +1 -0
  60. package/node_modules/@playdrop/types/dist/ugc-processing.js +144 -0
  61. package/node_modules/@playdrop/types/dist/ugc.d.ts +39 -0
  62. package/node_modules/@playdrop/types/dist/ugc.d.ts.map +1 -0
  63. package/node_modules/@playdrop/types/dist/ugc.js +70 -0
  64. package/node_modules/@playdrop/types/dist/version.d.ts +4 -0
  65. package/node_modules/@playdrop/types/dist/version.d.ts.map +1 -1
  66. 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.9",
2
+ "version": "0.19.0",
3
3
  "build": 1,
4
- "runtimeSdkVersion": "0.18.9",
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,7 +180,36 @@ 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
214
  let response;
184
215
  try {
@@ -186,22 +217,23 @@ async function uploadSignedObject(client, creatorUsername, appName, sessionId, f
186
217
  method: descriptor.method,
187
218
  headers: descriptor.headers,
188
219
  body: file.file,
220
+ ...transport,
189
221
  });
190
222
  }
191
223
  catch (error) {
192
224
  const cause = error instanceof Error ? error.cause : null;
193
- const code = cause && typeof cause === "object" && "code" in cause &&
194
- typeof cause.code === "string" && /^[A-Z][A-Z0-9_]{0,63}$/.test(cause.code)
195
- ? cause.code
196
- : "UNKNOWN";
197
- const annotated = new Error(`direct_object_upload_network_failed:${file.relativePath}:${code}`);
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}` : ""}`);
198
228
  Object.defineProperty(annotated, "cause", { value: error, configurable: true, writable: true });
199
229
  throw annotated;
200
230
  }
201
231
  if (response.ok) {
232
+ await response.body?.cancel();
202
233
  return;
203
234
  }
204
235
  if ((response.status === 401 || response.status === 403) && attempt === 0) {
236
+ await response.body?.cancel();
205
237
  const signed = await client.signAppUploadObjects(creatorUsername, appName, sessionId, {
206
238
  objectIds: [file.id],
207
239
  });
@@ -217,23 +249,48 @@ async function uploadSignedObject(client, creatorUsername, appName, sessionId, f
217
249
  }
218
250
  }
219
251
  async function uploadSignedObjects(client, creatorUsername, appName, sessionId, files) {
220
- for (let offset = 0; offset < files.length; offset += 100) {
221
- const batch = files.slice(offset, offset + 100);
222
- const signed = await client.signAppUploadObjects(creatorUsername, appName, sessionId, {
223
- objectIds: batch.map((file) => file.id),
224
- });
225
- const descriptors = new Map(signed.uploads.map((entry) => [entry.id, entry]));
226
- let cursor = 0;
227
- const workers = Array.from({ length: Math.min(8, batch.length) }, async () => {
228
- while (cursor < batch.length) {
229
- const file = batch[cursor++];
230
- const descriptor = descriptors.get(file.id);
231
- // A missing descriptor means the API verified this object is stored.
232
- if (descriptor)
233
- await uploadSignedObject(client, creatorUsername, appName, sessionId, file, descriptor);
234
- }
235
- });
236
- 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();
237
294
  }
238
295
  }
239
296
  async function prepareAppUploadFiles(task, artifacts, options = {}) {
@@ -277,46 +334,59 @@ async function prepareAppUploadFiles(task, artifacts, options = {}) {
277
334
  sessionFiles.push(await buildPreparedSessionFile("SERVER", "server/bundle.mjs", bundle.file));
278
335
  sessionFiles.push(await buildPreparedSessionFile("SERVER", "server/manifest.json", manifest.file));
279
336
  }
280
- if (listing?.iconPath) {
281
- const prepared = createFileFromPath(listing.iconPath, "image/png", config_1.MAX_VERSION_ICON_BYTES, "icon_too_large", "invalid_icon_content_type", "Icon", 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;
282
350
  totalUploadBytes = pushPreparedFile(task.name, totalUploadBytes, prepared);
283
- sessionFiles.push(await buildPreparedSessionFile("LISTING", "listing/icon.png", prepared.file));
284
- }
285
- if (listing?.heroPortraitPath) {
286
- 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);
287
- totalUploadBytes = pushPreparedFile(task.name, totalUploadBytes, prepared);
288
- sessionFiles.push(await buildPreparedSessionFile("LISTING", "listing/hero-portrait.png", prepared.file));
289
- }
290
- if (listing?.heroLandscapePath) {
291
- 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);
292
- totalUploadBytes = pushPreparedFile(task.name, totalUploadBytes, prepared);
293
- sessionFiles.push(await buildPreparedSessionFile("LISTING", "listing/hero-landscape.png", prepared.file));
294
- }
295
- for (let index = 0; index < (listing?.screenshotPortraitPaths ?? []).length; index += 1) {
296
- const filePath = listing.screenshotPortraitPaths[index];
297
- 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);
298
- totalUploadBytes = pushPreparedFile(task.name, totalUploadBytes, prepared);
299
- sessionFiles.push(await buildPreparedSessionFile("LISTING", `listing/screenshots/portrait/${index}.png`, prepared.file));
300
- }
301
- for (let index = 0; index < (listing?.screenshotLandscapePaths ?? []).length; index += 1) {
302
- const filePath = listing.screenshotLandscapePaths[index];
303
- 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);
304
- totalUploadBytes = pushPreparedFile(task.name, totalUploadBytes, prepared);
305
- 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);
306
369
  }
307
- for (let index = 0; index < (listing?.videoPortraitPaths ?? []).length; index += 1) {
308
- const filePath = listing.videoPortraitPaths[index];
309
- 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);
310
- totalUploadBytes = pushPreparedFile(task.name, totalUploadBytes, prepared);
311
- 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);
312
374
  }
313
- for (let index = 0; index < (listing?.videoLandscapePaths ?? []).length; index += 1) {
314
- const filePath = listing.videoLandscapePaths[index];
315
- 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);
316
- totalUploadBytes = pushPreparedFile(task.name, totalUploadBytes, prepared);
317
- 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);
318
388
  }
319
- return { sessionFiles, ownedAssets, gameServerBuild: gameServer };
389
+ return { sessionFiles, ownedAssets, gameServerBuild: gameServer, listingLocalizations };
320
390
  }
321
391
  async function uploadApp(client, task, artifacts, options) {
322
392
  // Version is required - no legacy fallback
@@ -349,7 +419,7 @@ async function uploadAppVersion(client, task, artifacts, options) {
349
419
  "The prepared upload does not match the project; finish edits and checks before retrying.");
350
420
  }
351
421
  };
352
- 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 });
353
423
  await assertPublicationUnchanged(inputFingerprint, false);
354
424
  const publicationFingerprint = options?.prepareGitTransition
355
425
  ? await (0, game_source_git_1.fingerprintGameSourcePublication)(task.projectDir)
@@ -367,6 +437,9 @@ async function uploadAppVersion(client, task, artifacts, options) {
367
437
  })));
368
438
  const preparedObjects = [...preparedSessionFiles, ...preparedOwnedAssetFiles];
369
439
  const initializeRequest = {
440
+ sourceLocale: task.sourceLocale,
441
+ listingSourceHash: task.listingSourceHash,
442
+ listingLocalizations,
370
443
  version: task.version,
371
444
  agentTaskId: options?.agentTaskId,
372
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 {};