@playdrop/playdrop-cli 0.14.0 → 0.14.2
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 +2 -2
- package/dist/apps/loadCheck.d.ts +2 -21
- package/dist/apps/loadCheck.js +22 -106
- package/dist/apps/surfaceProfiles.d.ts +1 -0
- package/dist/apps/surfaceProfiles.js +5 -4
- package/dist/apps/validate.d.ts +1 -1
- package/dist/apps/validate.js +2 -420
- package/dist/catalogue.js +32 -1
- package/dist/commands/captureListing.d.ts +13 -0
- package/dist/commands/captureListing.js +187 -0
- package/dist/commands/check.d.ts +11 -1
- package/dist/commands/check.js +19 -39
- package/dist/commands/detail.js +35 -2
- package/dist/commands/review.d.ts +11 -1
- package/dist/commands/review.js +20 -13
- package/dist/commands/upload.d.ts +1 -7
- package/dist/commands/upload.js +20 -462
- package/dist/commands/validate.js +0 -3
- package/dist/commands/worker.d.ts +1 -3
- package/dist/commands/worker.js +39 -37
- package/dist/index.js +2 -4
- package/dist/listingPreflight.d.ts +3 -0
- package/dist/listingPreflight.js +37 -10
- package/node_modules/@playdrop/config/client-meta.json +2 -2
- package/node_modules/@playdrop/types/dist/app-playtest-tape.d.ts.map +1 -1
- package/node_modules/@playdrop/types/dist/app-playtest-tape.js +23 -15
- package/package.json +1 -1
|
@@ -32,6 +32,9 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
32
32
|
return result;
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
35
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
39
|
exports.resolveListingCaptureRouteSegment = resolveListingCaptureRouteSegment;
|
|
37
40
|
exports.resolveCaptureListingDevRouterPort = resolveCaptureListingDevRouterPort;
|
|
@@ -49,6 +52,7 @@ exports.resolveListingCaptureBrowserContextOptions = resolveListingCaptureBrowse
|
|
|
49
52
|
exports.assertExportedListingAudio = assertExportedListingAudio;
|
|
50
53
|
exports.assertListingCaptureWindowCanContainViewport = assertListingCaptureWindowCanContainViewport;
|
|
51
54
|
exports.computeRecordedCrop = computeRecordedCrop;
|
|
55
|
+
exports.assertListingCapturePixelMetrics = assertListingCapturePixelMetrics;
|
|
52
56
|
exports.runListingRecorder = runListingRecorder;
|
|
53
57
|
exports.formatCommandError = formatCommandError;
|
|
54
58
|
exports.captureCommandMatchesExceptPoster = captureCommandMatchesExceptPoster;
|
|
@@ -59,6 +63,7 @@ const node_child_process_1 = require("node:child_process");
|
|
|
59
63
|
const node_crypto_1 = require("node:crypto");
|
|
60
64
|
const promises_1 = require("node:fs/promises");
|
|
61
65
|
const node_path_1 = require("node:path");
|
|
66
|
+
const sharp_1 = __importDefault(require("sharp"));
|
|
62
67
|
const appUrls_1 = require("../appUrls");
|
|
63
68
|
const build_1 = require("../apps/build");
|
|
64
69
|
const catalogue_1 = require("../catalogue");
|
|
@@ -96,6 +101,11 @@ const SURFACE_CAPTURE_DIMENSIONS = {
|
|
|
96
101
|
MOBILE_LANDSCAPE: { width: 844, height: 390 },
|
|
97
102
|
MOBILE_PORTRAIT: { width: 390, height: 844 },
|
|
98
103
|
};
|
|
104
|
+
const MIN_LISTING_REFERENCE_FRAME_SIMILARITY = 0.25;
|
|
105
|
+
const MIN_LISTING_FRAME_MEAN_LUMA = 0.02;
|
|
106
|
+
const MIN_LISTING_VISIBLE_PIXEL_RATIO = 0.005;
|
|
107
|
+
const MIN_LISTING_MOTION_MEAN_DELTA = 0.002;
|
|
108
|
+
const MIN_LISTING_MOTION_CHANGED_PIXEL_RATIO = 0.01;
|
|
99
109
|
const SURFACE_OUTPUT_SLUG = {
|
|
100
110
|
DESKTOP: 'desktop',
|
|
101
111
|
MOBILE_LANDSCAPE: 'mobile-landscape',
|
|
@@ -627,6 +637,125 @@ async function probeMediaFile(filePath, deadlineAt) {
|
|
|
627
637
|
async function computeFileSha256(filePath) {
|
|
628
638
|
return (0, node_crypto_1.createHash)('sha256').update(await (0, promises_1.readFile)(filePath)).digest('hex');
|
|
629
639
|
}
|
|
640
|
+
async function readListingValidationPixels(filePath) {
|
|
641
|
+
return (0, sharp_1.default)(filePath)
|
|
642
|
+
.resize(64, 64, { fit: 'fill' })
|
|
643
|
+
.greyscale()
|
|
644
|
+
.raw()
|
|
645
|
+
.toBuffer();
|
|
646
|
+
}
|
|
647
|
+
async function measureListingFrameVisibility(filePath) {
|
|
648
|
+
const pixels = await readListingValidationPixels(filePath);
|
|
649
|
+
if (pixels.length === 0) {
|
|
650
|
+
throw new Error('listing_capture_frame_pixels_missing');
|
|
651
|
+
}
|
|
652
|
+
let total = 0;
|
|
653
|
+
let visiblePixels = 0;
|
|
654
|
+
for (const pixel of pixels) {
|
|
655
|
+
total += pixel;
|
|
656
|
+
if (pixel >= 24)
|
|
657
|
+
visiblePixels += 1;
|
|
658
|
+
}
|
|
659
|
+
return {
|
|
660
|
+
meanLuma: total / pixels.length / 255,
|
|
661
|
+
visiblePixelRatio: visiblePixels / pixels.length,
|
|
662
|
+
};
|
|
663
|
+
}
|
|
664
|
+
async function assertListingCaptureFrameVisible(surface, label, filePath) {
|
|
665
|
+
const visibility = await measureListingFrameVisibility(filePath);
|
|
666
|
+
if (visibility.meanLuma < MIN_LISTING_FRAME_MEAN_LUMA
|
|
667
|
+
&& visibility.visiblePixelRatio < MIN_LISTING_VISIBLE_PIXEL_RATIO) {
|
|
668
|
+
throw new Error(`listing_capture_black_frame:${surface}:${label}`);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
async function measureListingFrameMotion(firstPath, lastPath) {
|
|
672
|
+
const [first, last] = await Promise.all([
|
|
673
|
+
readListingValidationPixels(firstPath),
|
|
674
|
+
readListingValidationPixels(lastPath),
|
|
675
|
+
]);
|
|
676
|
+
if (first.length !== last.length || first.length === 0) {
|
|
677
|
+
throw new Error('listing_capture_frame_shape_mismatch');
|
|
678
|
+
}
|
|
679
|
+
let deltaTotal = 0;
|
|
680
|
+
let changedPixels = 0;
|
|
681
|
+
for (let index = 0; index < first.length; index += 1) {
|
|
682
|
+
const delta = Math.abs(first[index] - last[index]);
|
|
683
|
+
deltaTotal += delta;
|
|
684
|
+
if (delta >= 16)
|
|
685
|
+
changedPixels += 1;
|
|
686
|
+
}
|
|
687
|
+
return {
|
|
688
|
+
meanDelta: deltaTotal / first.length / 255,
|
|
689
|
+
changedPixelRatio: changedPixels / first.length,
|
|
690
|
+
};
|
|
691
|
+
}
|
|
692
|
+
function calculateListingFrameSimilarity(referencePath, capturedPath, deadlineAt) {
|
|
693
|
+
const { stderr } = runTool('ffmpeg', [
|
|
694
|
+
'-hide_banner',
|
|
695
|
+
'-i',
|
|
696
|
+
referencePath,
|
|
697
|
+
'-i',
|
|
698
|
+
capturedPath,
|
|
699
|
+
'-filter_complex',
|
|
700
|
+
'[0:v]scale=320:320:force_original_aspect_ratio=decrease,pad=320:320:(ow-iw)/2:(oh-ih)/2,format=yuv420p[ref];[1:v]scale=320:320:force_original_aspect_ratio=decrease,pad=320:320:(ow-iw)/2:(oh-ih)/2,format=yuv420p[cap];[ref][cap]ssim',
|
|
701
|
+
'-f',
|
|
702
|
+
'null',
|
|
703
|
+
'-',
|
|
704
|
+
], 'ffmpeg_failed', deadlineAt);
|
|
705
|
+
const match = stderr.match(/All:([0-9.]+)/);
|
|
706
|
+
if (!match) {
|
|
707
|
+
throw new Error('listing_capture_similarity_missing');
|
|
708
|
+
}
|
|
709
|
+
return Number.parseFloat(match[1]);
|
|
710
|
+
}
|
|
711
|
+
function assertListingCapturePixelMetrics(surface, metrics) {
|
|
712
|
+
if (!Number.isFinite(metrics.referenceFrameSimilarity)
|
|
713
|
+
|| metrics.referenceFrameSimilarity < MIN_LISTING_REFERENCE_FRAME_SIMILARITY) {
|
|
714
|
+
throw new Error(`listing_capture_wrong_game:${surface}:${Number.isFinite(metrics.referenceFrameSimilarity) ? metrics.referenceFrameSimilarity.toFixed(3) : 'unknown'}`);
|
|
715
|
+
}
|
|
716
|
+
if (metrics.firstFrameMeanLuma < MIN_LISTING_FRAME_MEAN_LUMA
|
|
717
|
+
&& metrics.firstFrameVisiblePixelRatio < MIN_LISTING_VISIBLE_PIXEL_RATIO) {
|
|
718
|
+
throw new Error(`listing_capture_black_frame:${surface}:first`);
|
|
719
|
+
}
|
|
720
|
+
if (metrics.posterFrameMeanLuma < MIN_LISTING_FRAME_MEAN_LUMA
|
|
721
|
+
&& metrics.posterFrameVisiblePixelRatio < MIN_LISTING_VISIBLE_PIXEL_RATIO) {
|
|
722
|
+
throw new Error(`listing_capture_black_frame:${surface}:poster`);
|
|
723
|
+
}
|
|
724
|
+
if (metrics.motionMeanDelta < MIN_LISTING_MOTION_MEAN_DELTA
|
|
725
|
+
&& metrics.motionChangedPixelRatio < MIN_LISTING_MOTION_CHANGED_PIXEL_RATIO) {
|
|
726
|
+
throw new Error(`listing_capture_stale_video:${surface}`);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
async function validateListingCapturePixels(input) {
|
|
730
|
+
const [firstVisibility, posterVisibility, motion] = await Promise.all([
|
|
731
|
+
measureListingFrameVisibility(input.firstFramePath),
|
|
732
|
+
measureListingFrameVisibility(input.posterPath),
|
|
733
|
+
measureListingFrameMotion(input.firstFramePath, input.lastFramePath),
|
|
734
|
+
]);
|
|
735
|
+
const metrics = {
|
|
736
|
+
referenceFrameSimilarity: calculateListingFrameSimilarity(input.referencePath, input.firstFramePath, input.deadlineAt),
|
|
737
|
+
firstFrameMeanLuma: firstVisibility.meanLuma,
|
|
738
|
+
firstFrameVisiblePixelRatio: firstVisibility.visiblePixelRatio,
|
|
739
|
+
posterFrameMeanLuma: posterVisibility.meanLuma,
|
|
740
|
+
posterFrameVisiblePixelRatio: posterVisibility.visiblePixelRatio,
|
|
741
|
+
motionMeanDelta: motion.meanDelta,
|
|
742
|
+
motionChangedPixelRatio: motion.changedPixelRatio,
|
|
743
|
+
};
|
|
744
|
+
assertListingCapturePixelMetrics(input.surface, metrics);
|
|
745
|
+
return { status: 'passed', ...metrics };
|
|
746
|
+
}
|
|
747
|
+
function extractListingValidationFrame(videoPath, atSeconds, outputPath, deadlineAt) {
|
|
748
|
+
runTool('ffmpeg', [
|
|
749
|
+
'-y',
|
|
750
|
+
'-ss',
|
|
751
|
+
String(atSeconds),
|
|
752
|
+
'-i',
|
|
753
|
+
videoPath,
|
|
754
|
+
'-frames:v',
|
|
755
|
+
'1',
|
|
756
|
+
outputPath,
|
|
757
|
+
], 'ffmpeg_failed', deadlineAt);
|
|
758
|
+
}
|
|
630
759
|
function parseFrameRate(raw) {
|
|
631
760
|
if (!raw) {
|
|
632
761
|
return null;
|
|
@@ -741,6 +870,13 @@ function formatCommandError(error) {
|
|
|
741
870
|
suggestions: ['Fix or remove the incomplete canonical capture directory, then record the runtime once.'],
|
|
742
871
|
};
|
|
743
872
|
}
|
|
873
|
+
if (error.message === 'listing_capture_report_pixels_unvalidated'
|
|
874
|
+
|| error.message.startsWith('listing_capture_artifact_changed:')) {
|
|
875
|
+
return {
|
|
876
|
+
message: 'The existing canonical recording is unvalidated or its files changed after capture.',
|
|
877
|
+
suggestions: ['Remove the invalid capture directory and rerun "playdrop project capture" once.'],
|
|
878
|
+
};
|
|
879
|
+
}
|
|
744
880
|
if (error.message.startsWith('listing_capture_dimensions_exceed_display:')) {
|
|
745
881
|
const [, requested = 'unknown', available = 'unknown'] = error.message.split(':');
|
|
746
882
|
return {
|
|
@@ -760,6 +896,24 @@ function formatCommandError(error) {
|
|
|
760
896
|
suggestions: [],
|
|
761
897
|
};
|
|
762
898
|
}
|
|
899
|
+
if (error.message.startsWith('listing_capture_wrong_game:')) {
|
|
900
|
+
return {
|
|
901
|
+
message: 'The recorded pixels do not match the selected game preview.',
|
|
902
|
+
suggestions: ['Verify preview mode shows the selected game, then rerun "playdrop project capture".'],
|
|
903
|
+
};
|
|
904
|
+
}
|
|
905
|
+
if (error.message.startsWith('listing_capture_black_frame:')) {
|
|
906
|
+
return {
|
|
907
|
+
message: 'The gameplay recording contains a black first frame or poster.',
|
|
908
|
+
suggestions: ['Make preview mode visibly ready before capture starts, then rerun "playdrop project capture".'],
|
|
909
|
+
};
|
|
910
|
+
}
|
|
911
|
+
if (error.message.startsWith('listing_capture_stale_video:')) {
|
|
912
|
+
return {
|
|
913
|
+
message: 'The gameplay recording is visually static.',
|
|
914
|
+
suggestions: ['Make preview mode show real gameplay motion, then rerun "playdrop project capture".'],
|
|
915
|
+
};
|
|
916
|
+
}
|
|
763
917
|
return {
|
|
764
918
|
message: error.message,
|
|
765
919
|
suggestions: [],
|
|
@@ -830,6 +984,9 @@ async function reuseCurrentListingCapture(input) {
|
|
|
830
984
|
if (report.appName !== input.appName) {
|
|
831
985
|
throw new Error(`listing_capture_report_app_mismatch:${report.appName}:${input.appName}`);
|
|
832
986
|
}
|
|
987
|
+
if (report.binding?.pixelValidation !== 'passed') {
|
|
988
|
+
throw new Error('listing_capture_report_pixels_unvalidated');
|
|
989
|
+
}
|
|
833
990
|
if (!input.runtimeBundleHash || report.binding?.runtimeBundleHash !== input.runtimeBundleHash) {
|
|
834
991
|
return false;
|
|
835
992
|
}
|
|
@@ -837,6 +994,15 @@ async function reuseCurrentListingCapture(input) {
|
|
|
837
994
|
const captures = input.surfaces.map((surface) => report.captures.find((candidate) => candidate.surface === surface));
|
|
838
995
|
for (const capture of captures) {
|
|
839
996
|
await (0, promises_1.access)(capture.finalVideo.path);
|
|
997
|
+
await (0, promises_1.access)(capture.posterPath);
|
|
998
|
+
for (const artifactPath of [capture.finalVideo.path, capture.posterPath]) {
|
|
999
|
+
const artifactKey = (0, node_path_1.relative)(input.outputPaths.outputDir, artifactPath);
|
|
1000
|
+
const expectedHash = report.binding.artifactHashes[artifactKey];
|
|
1001
|
+
const actualHash = await computeFileSha256(artifactPath);
|
|
1002
|
+
if (!expectedHash || actualHash !== expectedHash) {
|
|
1003
|
+
throw new Error(`listing_capture_artifact_changed:${artifactKey}`);
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
840
1006
|
}
|
|
841
1007
|
if (action === 'reuse') {
|
|
842
1008
|
console.log('[listing] The current runtime already has a matching canonical capture; reusing it.');
|
|
@@ -857,6 +1023,7 @@ async function reuseCurrentListingCapture(input) {
|
|
|
857
1023
|
capture.command.posterAtSeconds = input.parsedOptions.posterAtSeconds;
|
|
858
1024
|
const artifactKey = (0, node_path_1.relative)(input.outputPaths.outputDir, capture.posterPath) || 'poster.png';
|
|
859
1025
|
report.binding.artifactHashes[artifactKey] = await computeFileSha256(capture.posterPath);
|
|
1026
|
+
await assertListingCaptureFrameVisible(capture.surface, 'poster', capture.posterPath);
|
|
860
1027
|
}
|
|
861
1028
|
await (0, promises_1.writeFile)(input.outputPaths.reportPath, `${JSON.stringify(report, null, 2)}\n`, 'utf8');
|
|
862
1029
|
console.log(`[listing] Refreshed the canonical poster at ${input.parsedOptions.posterAtSeconds}s without relaunching the game.`);
|
|
@@ -1206,6 +1373,12 @@ async function captureListing(targetArg, options = {}) {
|
|
|
1206
1373
|
const dimensions = resolveCaptureDimensions(surface, parsedOptions);
|
|
1207
1374
|
const captureSceneId = resolveListingCaptureSceneId(dimensions.width, dimensions.height);
|
|
1208
1375
|
const surfaceOutputPaths = buildSurfaceOutputPaths(outputPaths.outputDir, surface, useSurfacePrefix);
|
|
1376
|
+
const validationPrefix = useSurfacePrefix ? `${surface.toLowerCase().replace(/_/g, '-')}-` : '';
|
|
1377
|
+
const validationPaths = {
|
|
1378
|
+
reference: (0, node_path_1.join)(outputPaths.outputDir, `.${validationPrefix}capture-reference.png`),
|
|
1379
|
+
first: (0, node_path_1.join)(outputPaths.outputDir, `.${validationPrefix}capture-first.png`),
|
|
1380
|
+
last: (0, node_path_1.join)(outputPaths.outputDir, `.${validationPrefix}capture-last.png`),
|
|
1381
|
+
};
|
|
1209
1382
|
console.log(`[listing] Capturing ${surface} at ${dimensions.width}x${dimensions.height}.`);
|
|
1210
1383
|
const contextOptions = resolveListingCaptureBrowserContextOptions(surface, dimensions);
|
|
1211
1384
|
browserHandle = await launchListingBrowser(dimensions, frameUrlObject.origin, contextOptions);
|
|
@@ -1233,6 +1406,7 @@ async function captureListing(targetArg, options = {}) {
|
|
|
1233
1406
|
});
|
|
1234
1407
|
let recorderMetadata;
|
|
1235
1408
|
try {
|
|
1409
|
+
await browserHandle.page.locator(CAPTURE_FRAME_SELECTOR).screenshot({ path: validationPaths.reference });
|
|
1236
1410
|
recorderMetadata = await runListingRecorder(recorderPath, browserHandle.processId, parsedOptions.durationSeconds, surfaceOutputPaths.rawVideoPath, surfaceOutputPaths.metadataPath, shouldExportPreviewAudio ? false : parsedOptions.audio, captureDeadlineAt);
|
|
1237
1411
|
}
|
|
1238
1412
|
finally {
|
|
@@ -1262,6 +1436,16 @@ async function captureListing(targetArg, options = {}) {
|
|
|
1262
1436
|
if (parsedOptions.audio && finalAudioTrackCount === 0) {
|
|
1263
1437
|
throw new Error('audio_track_missing');
|
|
1264
1438
|
}
|
|
1439
|
+
extractListingValidationFrame(surfaceOutputPaths.finalVideoPath, Math.min(0.1, finalDurationSeconds / 4), validationPaths.first, captureDeadlineAt);
|
|
1440
|
+
extractListingValidationFrame(surfaceOutputPaths.finalVideoPath, Math.max(0, finalDurationSeconds - 0.1), validationPaths.last, captureDeadlineAt);
|
|
1441
|
+
const validation = await validateListingCapturePixels({
|
|
1442
|
+
surface,
|
|
1443
|
+
referencePath: validationPaths.reference,
|
|
1444
|
+
firstFramePath: validationPaths.first,
|
|
1445
|
+
lastFramePath: validationPaths.last,
|
|
1446
|
+
posterPath: surfaceOutputPaths.posterPath,
|
|
1447
|
+
deadlineAt: captureDeadlineAt,
|
|
1448
|
+
});
|
|
1265
1449
|
const warnings = createWarnings(finalProbe, parsedOptions.audio);
|
|
1266
1450
|
for (const warning of warnings) {
|
|
1267
1451
|
allWarnings.push(`${surface}: ${warning}`);
|
|
@@ -1300,6 +1484,7 @@ async function captureListing(targetArg, options = {}) {
|
|
|
1300
1484
|
fps: finalFps,
|
|
1301
1485
|
},
|
|
1302
1486
|
posterPath: surfaceOutputPaths.posterPath,
|
|
1487
|
+
validation,
|
|
1303
1488
|
warnings,
|
|
1304
1489
|
});
|
|
1305
1490
|
if (!parsedOptions.keepRaw) {
|
|
@@ -1314,6 +1499,7 @@ async function captureListing(targetArg, options = {}) {
|
|
|
1314
1499
|
}
|
|
1315
1500
|
}
|
|
1316
1501
|
finally {
|
|
1502
|
+
await Promise.all(Object.values(validationPaths).map((filePath) => (0, promises_1.rm)(filePath, { force: true })));
|
|
1317
1503
|
await browserHandle.close();
|
|
1318
1504
|
browserHandle = null;
|
|
1319
1505
|
}
|
|
@@ -1326,6 +1512,7 @@ async function captureListing(targetArg, options = {}) {
|
|
|
1326
1512
|
binding: {
|
|
1327
1513
|
artifactHashes,
|
|
1328
1514
|
capturedSurfaces,
|
|
1515
|
+
pixelValidation: 'passed',
|
|
1329
1516
|
runtimeBundleHash,
|
|
1330
1517
|
},
|
|
1331
1518
|
};
|
package/dist/commands/check.d.ts
CHANGED
|
@@ -1,9 +1,19 @@
|
|
|
1
|
+
import { type AppSurface } from '@playdrop/types';
|
|
1
2
|
type ProjectCheckOptions = {
|
|
2
3
|
app?: string;
|
|
3
4
|
timeout?: string | number;
|
|
4
5
|
screenshot?: string;
|
|
5
|
-
actions?: string;
|
|
6
6
|
tape?: string;
|
|
7
7
|
};
|
|
8
|
+
export declare function formatPlaytestCheckSuccess(input: {
|
|
9
|
+
creatorUsername: string;
|
|
10
|
+
appName: string;
|
|
11
|
+
surface: AppSurface;
|
|
12
|
+
actionCount: number;
|
|
13
|
+
meanDelta: number;
|
|
14
|
+
changedPixelRatio: number;
|
|
15
|
+
idleCapture: string;
|
|
16
|
+
tapeCapture: string;
|
|
17
|
+
}): string;
|
|
8
18
|
export declare function check(targetArg: string | undefined, options?: ProjectCheckOptions): Promise<void>;
|
|
9
19
|
export {};
|
package/dist/commands/check.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.formatPlaytestCheckSuccess = formatPlaytestCheckSuccess;
|
|
3
4
|
exports.check = check;
|
|
4
5
|
const node_fs_1 = require("node:fs");
|
|
5
6
|
const node_path_1 = require("node:path");
|
|
@@ -42,14 +43,6 @@ function parseProjectCheckTimeoutMs(value) {
|
|
|
42
43
|
}
|
|
43
44
|
return Math.round(numeric * 1000);
|
|
44
45
|
}
|
|
45
|
-
function readProjectCheckActions(actionsPath) {
|
|
46
|
-
const normalizedPath = actionsPath?.trim() ?? '';
|
|
47
|
-
if (!normalizedPath) {
|
|
48
|
-
return [];
|
|
49
|
-
}
|
|
50
|
-
const raw = (0, node_fs_1.readFileSync)(normalizedPath, 'utf8');
|
|
51
|
-
return (0, loadCheck_1.normalizeProjectCheckActions)(JSON.parse(raw));
|
|
52
|
-
}
|
|
53
46
|
function defaultProjectCheckScreenshotPath(appName) {
|
|
54
47
|
return (0, node_path_1.join)('assets', 'marketing', 'playdrop', 'check', `${appName}.png`);
|
|
55
48
|
}
|
|
@@ -60,15 +53,18 @@ function buildComparisonScreenshotPath(basePath, run) {
|
|
|
60
53
|
}
|
|
61
54
|
return `${basePath.slice(0, -extension.length)}-${run}${extension}`;
|
|
62
55
|
}
|
|
56
|
+
function formatPlaytestCheckSuccess(input) {
|
|
57
|
+
return `[check] Passed ${input.creatorUsername}/${input.appName} on ${input.surface}: input delivery ${input.actionCount}/${input.actionCount}, `
|
|
58
|
+
+ `visible change mean=${input.meanDelta.toFixed(4)} changedPixels=${(input.changedPixelRatio * 100).toFixed(1)}%. `
|
|
59
|
+
+ `Gameplay success was not assessed. Evidence: idle=${input.idleCapture}; tape=${input.tapeCapture}`;
|
|
60
|
+
}
|
|
63
61
|
async function check(targetArg, options = {}) {
|
|
64
62
|
let timeoutMs;
|
|
65
|
-
let actions;
|
|
66
63
|
try {
|
|
67
64
|
timeoutMs = parseProjectCheckTimeoutMs(options.timeout);
|
|
68
|
-
actions = readProjectCheckActions(options.actions);
|
|
69
65
|
}
|
|
70
66
|
catch (error) {
|
|
71
|
-
(0, messages_1.printErrorWithHelp)(error?.message || 'Invalid project check options.', ['Use --
|
|
67
|
+
(0, messages_1.printErrorWithHelp)(error?.message || 'Invalid project check options.', ['Use --tape with DESKTOP, MOBILE_LANDSCAPE, or MOBILE_PORTRAIT.'], { command: 'project check' });
|
|
72
68
|
process.exitCode = 1;
|
|
73
69
|
return;
|
|
74
70
|
}
|
|
@@ -96,11 +92,6 @@ async function check(targetArg, options = {}) {
|
|
|
96
92
|
let tapeSurface = null;
|
|
97
93
|
let playtestTape = null;
|
|
98
94
|
if (options.tape) {
|
|
99
|
-
if (actions.length > 0) {
|
|
100
|
-
(0, messages_1.printErrorWithHelp)('--tape and --actions cannot be used together.', ['Use the catalogue playtest tape by itself so the timed sequence remains deterministic.'], { command: 'project check' });
|
|
101
|
-
process.exitCode = 1;
|
|
102
|
-
return;
|
|
103
|
-
}
|
|
104
95
|
tapeSurface = (0, types_1.parseAppSurface)(options.tape);
|
|
105
96
|
if (!tapeSurface) {
|
|
106
97
|
(0, messages_1.printErrorWithHelp)(`Invalid tape surface "${options.tape}".`, ['Use DESKTOP, MOBILE_LANDSCAPE, or MOBILE_PORTRAIT.'], { command: 'project check' });
|
|
@@ -192,18 +183,6 @@ async function check(targetArg, options = {}) {
|
|
|
192
183
|
process.exitCode = 1;
|
|
193
184
|
return;
|
|
194
185
|
}
|
|
195
|
-
const projectInfo = (0, devShared_1.findProjectInfo)(filePath);
|
|
196
|
-
const devScriptAvailable = Boolean(projectInfo.projectDir
|
|
197
|
-
&& projectInfo.packageJson
|
|
198
|
-
&& typeof projectInfo.packageJson.scripts?.dev === 'string');
|
|
199
|
-
const entryLabel = (0, node_path_1.relative)(process.cwd(), filePath) || filePath;
|
|
200
|
-
console.log(`[check] Preparing ${entryLabel}.`);
|
|
201
|
-
if (projectInfo.projectDir && !devScriptAvailable && projectInfo.packageJsonPath) {
|
|
202
|
-
const projectLabel = (0, devShared_1.formatProjectLabel)(projectInfo);
|
|
203
|
-
if (projectLabel) {
|
|
204
|
-
console.log(`[check] package.json detected at ${projectLabel}, but no "dev" script was found. Run your app build manually if needed.`);
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
186
|
const runCheck = async (input) => await (0, loadCheck_1.runLocalHostedLoadCheck)({
|
|
208
187
|
client,
|
|
209
188
|
apiBase: envConfig.apiBase,
|
|
@@ -215,7 +194,6 @@ async function check(targetArg, options = {}) {
|
|
|
215
194
|
currentUser: null,
|
|
216
195
|
captureSession,
|
|
217
196
|
screenshotPath: input.screenshotPath,
|
|
218
|
-
actions,
|
|
219
197
|
surface: input.surface,
|
|
220
198
|
playtestTape: input.tape,
|
|
221
199
|
postReadyWaitMs: input.postReadyWaitMs,
|
|
@@ -223,7 +201,6 @@ async function check(targetArg, options = {}) {
|
|
|
223
201
|
if (tapeSurface && playtestTape) {
|
|
224
202
|
const idleScreenshotPath = buildComparisonScreenshotPath(screenshotPath, 'idle');
|
|
225
203
|
const tapeScreenshotPath = buildComparisonScreenshotPath(screenshotPath, 'tape');
|
|
226
|
-
console.log(`[check] Running ${playtestTape.durationMs} ms zero-input baseline on ${tapeSurface}.`);
|
|
227
204
|
const idleResult = await runCheck({
|
|
228
205
|
screenshotPath: idleScreenshotPath,
|
|
229
206
|
surface: tapeSurface,
|
|
@@ -234,7 +211,6 @@ async function check(targetArg, options = {}) {
|
|
|
234
211
|
process.exitCode = 1;
|
|
235
212
|
return;
|
|
236
213
|
}
|
|
237
|
-
console.log(`[check] Reopening a clean run and executing the ${tapeSurface} tape.`);
|
|
238
214
|
const tapeResult = await runCheck({
|
|
239
215
|
screenshotPath: tapeScreenshotPath,
|
|
240
216
|
surface: tapeSurface,
|
|
@@ -258,10 +234,16 @@ async function check(targetArg, options = {}) {
|
|
|
258
234
|
process.exitCode = 1;
|
|
259
235
|
return;
|
|
260
236
|
}
|
|
261
|
-
console.log(
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
237
|
+
console.log(formatPlaytestCheckSuccess({
|
|
238
|
+
creatorUsername: currentUsername,
|
|
239
|
+
appName,
|
|
240
|
+
surface: tapeSurface,
|
|
241
|
+
actionCount: playtestTape.events.length,
|
|
242
|
+
meanDelta: comparison.meanDelta,
|
|
243
|
+
changedPixelRatio: comparison.changedPixelRatio,
|
|
244
|
+
idleCapture,
|
|
245
|
+
tapeCapture,
|
|
246
|
+
}));
|
|
265
247
|
return;
|
|
266
248
|
}
|
|
267
249
|
const result = await runCheck({ screenshotPath });
|
|
@@ -270,9 +252,7 @@ async function check(targetArg, options = {}) {
|
|
|
270
252
|
process.exitCode = 1;
|
|
271
253
|
return;
|
|
272
254
|
}
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
console.log(`[check] Hardware renderer: ${result.webglRenderer}`);
|
|
276
|
-
}
|
|
255
|
+
const checkedSurface = (0, loadCheck_1.resolveLoadCheckSurface)(taskLookup.task.surfaceTargets, taskLookup.task.primarySurface);
|
|
256
|
+
console.log(`[check] Passed ${currentUsername}/${appName} on ${checkedSurface}: launch ready${result.webglRenderer ? ` with hardware renderer ${result.webglRenderer}` : ''}. Gameplay success was not assessed. Screenshot=${result.screenshotPath ?? screenshotPath}`);
|
|
277
257
|
}, { workspacePath });
|
|
278
258
|
}
|
package/dist/commands/detail.js
CHANGED
|
@@ -72,9 +72,34 @@ function decorateAsset(apiBase, ref, asset) {
|
|
|
72
72
|
},
|
|
73
73
|
};
|
|
74
74
|
}
|
|
75
|
-
function
|
|
75
|
+
function decorateAssetPackMember(asset) {
|
|
76
|
+
const revisionLabel = asset.currentVersion?.revisionLabel?.trim();
|
|
77
|
+
if (!revisionLabel) {
|
|
78
|
+
throw new Error(`asset_pack_member_current_revision_missing:${asset.creatorUsername}/${asset.name}`);
|
|
79
|
+
}
|
|
80
|
+
const files = asset.currentVersion?.fileManifest?.files;
|
|
81
|
+
return {
|
|
82
|
+
assetRef: `asset:${asset.creatorUsername}/${asset.name}@${revisionLabel}`,
|
|
83
|
+
name: asset.name,
|
|
84
|
+
displayName: asset.displayName,
|
|
85
|
+
category: asset.category,
|
|
86
|
+
subcategory: asset.currentVersion?.subcategory ?? null,
|
|
87
|
+
format: asset.currentVersion?.format ?? null,
|
|
88
|
+
files: Array.isArray(files)
|
|
89
|
+
? files
|
|
90
|
+
.filter((file) => typeof file?.role === 'string' && typeof file?.key === 'string')
|
|
91
|
+
.map((file) => ({
|
|
92
|
+
role: file.role,
|
|
93
|
+
key: file.key,
|
|
94
|
+
contentType: typeof file.contentType === 'string' ? file.contentType : null,
|
|
95
|
+
}))
|
|
96
|
+
: [],
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
function decorateAssetPack(apiBase, ref, pack, assets) {
|
|
76
100
|
return {
|
|
77
101
|
...pack,
|
|
102
|
+
members: assets.map(decorateAssetPackMember),
|
|
78
103
|
urls: {
|
|
79
104
|
download: pack.currentVersion?.version ? buildAssetPackDownloadUrl(apiBase, ref, pack.currentVersion.version) : null,
|
|
80
105
|
source: pack.currentVersion?.version ? buildAssetPackSourceUrl(apiBase, ref, pack.currentVersion.version) : null,
|
|
@@ -154,6 +179,14 @@ function printAssetPackText(ref, pack) {
|
|
|
154
179
|
console.log(`Updated: ${(0, output_1.formatTimestamp)(pack.updatedAt)}`);
|
|
155
180
|
console.log(`Current version: ${(0, output_1.formatOptionalValue)(pack.currentVersion?.version)}`);
|
|
156
181
|
console.log(`Tags: ${formatTagSummary(pack.tags)}`);
|
|
182
|
+
console.log(`Pack members: ${pack.members.length}`);
|
|
183
|
+
for (const member of pack.members) {
|
|
184
|
+
const classification = [member.category, member.subcategory, member.format].filter(Boolean).join('/');
|
|
185
|
+
const files = member.files.length > 0
|
|
186
|
+
? member.files.map((file) => `${file.role}:${file.contentType ?? file.key}`).join(', ')
|
|
187
|
+
: 'none';
|
|
188
|
+
console.log(`- assetRef=${member.assetRef} | ${member.displayName} | ${classification} | files=${files}`);
|
|
189
|
+
}
|
|
157
190
|
console.log(`Download URL: ${(0, output_1.formatOptionalValue)(pack.urls.download)}`);
|
|
158
191
|
console.log(`Source URL: ${(0, output_1.formatOptionalValue)(pack.urls.source)}`);
|
|
159
192
|
console.log('\nNext: run "playdrop versions browse ' + ref + '" to inspect other versions.');
|
|
@@ -205,7 +238,7 @@ async function detail(rawRef, options = {}) {
|
|
|
205
238
|
}
|
|
206
239
|
if (ref.kind === 'asset-pack') {
|
|
207
240
|
const response = await client.fetchAssetPackBySlug(ref.creator, ref.name);
|
|
208
|
-
const item = decorateAssetPack(envConfig.apiBase, ref, response.pack);
|
|
241
|
+
const item = decorateAssetPack(envConfig.apiBase, ref, response.pack, response.assets);
|
|
209
242
|
if (options.json) {
|
|
210
243
|
(0, output_1.printJson)({ kind: ref.kind, ref: ref.ref, item });
|
|
211
244
|
return;
|
|
@@ -1,4 +1,13 @@
|
|
|
1
|
-
export declare const REVIEW_CRITERIA: readonly ["Gameplay / Core Loop", "Depth / Replayability", "Controls / Input", "UX / Usability", "First Time User Experience", "Visuals / Art Direction", "
|
|
1
|
+
export declare const REVIEW_CRITERIA: readonly ["Gameplay / Core Loop", "Depth / Replayability", "Controls / Input", "UX / Usability", "First Time User Experience", "Visuals / Art Direction", "Store Listing & Metadata Accuracy", "Safety / Age Rating / Compliance", "Performance / Stability"];
|
|
2
|
+
declare const STATE_OUTCOME: {
|
|
3
|
+
readonly EXCELLENT: "Excellent";
|
|
4
|
+
readonly FAILED: "Blocked";
|
|
5
|
+
readonly GOOD: "Good";
|
|
6
|
+
readonly LOW_QUALITY: "Limited";
|
|
7
|
+
readonly PASSED: "Passed";
|
|
8
|
+
};
|
|
9
|
+
export type TerminalReviewState = keyof typeof STATE_OUTCOME;
|
|
10
|
+
export declare function normalizeTerminalReviewState(value: string | undefined): TerminalReviewState;
|
|
2
11
|
export declare const REQUIRED_REVIEW_EVIDENCE_FILES: string[];
|
|
3
12
|
export type ValidateGameReviewResultInput = {
|
|
4
13
|
creatorFeedback?: string;
|
|
@@ -44,3 +53,4 @@ export declare function createReviewRatingCard(options: ReviewRatingCardOptions)
|
|
|
44
53
|
width: number;
|
|
45
54
|
height: number;
|
|
46
55
|
}>;
|
|
56
|
+
export {};
|
package/dist/commands/review.js
CHANGED
|
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.REQUIRED_REVIEW_EVIDENCE_FILES = exports.REVIEW_CRITERIA = void 0;
|
|
7
|
+
exports.normalizeTerminalReviewState = normalizeTerminalReviewState;
|
|
7
8
|
exports.validateGameReviewResult = validateGameReviewResult;
|
|
8
9
|
exports.validateReviewResultCommand = validateReviewResultCommand;
|
|
9
10
|
exports.composeReviewEvidence = composeReviewEvidence;
|
|
@@ -19,18 +20,10 @@ exports.REVIEW_CRITERIA = [
|
|
|
19
20
|
'UX / Usability',
|
|
20
21
|
'First Time User Experience',
|
|
21
22
|
'Visuals / Art Direction',
|
|
22
|
-
'Audio / Feedback',
|
|
23
23
|
'Store Listing & Metadata Accuracy',
|
|
24
24
|
'Safety / Age Rating / Compliance',
|
|
25
25
|
'Performance / Stability',
|
|
26
26
|
];
|
|
27
|
-
const TERMINAL_REVIEW_STATES = new Set([
|
|
28
|
-
'FAILED',
|
|
29
|
-
'LOW_QUALITY',
|
|
30
|
-
'PASSED',
|
|
31
|
-
'GOOD',
|
|
32
|
-
'EXCELLENT',
|
|
33
|
-
]);
|
|
34
27
|
const STATE_OUTCOME = {
|
|
35
28
|
EXCELLENT: 'Excellent',
|
|
36
29
|
FAILED: 'Blocked',
|
|
@@ -38,6 +31,23 @@ const STATE_OUTCOME = {
|
|
|
38
31
|
LOW_QUALITY: 'Limited',
|
|
39
32
|
PASSED: 'Passed',
|
|
40
33
|
};
|
|
34
|
+
const REVIEW_STATE_ALIASES = {
|
|
35
|
+
BLOCKED: 'FAILED',
|
|
36
|
+
EXCELLENT: 'EXCELLENT',
|
|
37
|
+
FAILED: 'FAILED',
|
|
38
|
+
GOOD: 'GOOD',
|
|
39
|
+
LIMITED: 'LOW_QUALITY',
|
|
40
|
+
LOW_QUALITY: 'LOW_QUALITY',
|
|
41
|
+
PASSED: 'PASSED',
|
|
42
|
+
};
|
|
43
|
+
function normalizeTerminalReviewState(value) {
|
|
44
|
+
const input = typeof value === 'string' ? value.trim() : '';
|
|
45
|
+
const state = REVIEW_STATE_ALIASES[input.toUpperCase()];
|
|
46
|
+
if (!state) {
|
|
47
|
+
throw new Error(`invalid_review_state:${input || 'missing'}:expected=Blocked|Limited|Passed|Good|Excellent`);
|
|
48
|
+
}
|
|
49
|
+
return state;
|
|
50
|
+
}
|
|
41
51
|
exports.REQUIRED_REVIEW_EVIDENCE_FILES = [
|
|
42
52
|
'first-frame.png',
|
|
43
53
|
'core.png',
|
|
@@ -135,7 +145,7 @@ function validateRequiredEvidenceLines(reviewMessage) {
|
|
|
135
145
|
throw new Error('challenge_evidence_too_vague');
|
|
136
146
|
}
|
|
137
147
|
if (punchline.length < 8 || punchline.length > 160) {
|
|
138
|
-
throw new Error(
|
|
148
|
+
throw new Error(`punchline_assessment_invalid_length: Punchline assessment length=${punchline.length}; expected 8..160 characters.`);
|
|
139
149
|
}
|
|
140
150
|
if (benchmark.length < 8) {
|
|
141
151
|
throw new Error('comparable_benchmark_too_vague');
|
|
@@ -169,10 +179,7 @@ async function assertPngFile(filePath) {
|
|
|
169
179
|
}
|
|
170
180
|
}
|
|
171
181
|
async function validateGameReviewResult(input) {
|
|
172
|
-
const normalizedState =
|
|
173
|
-
if (!TERMINAL_REVIEW_STATES.has(normalizedState)) {
|
|
174
|
-
return { skipped: true, reason: 'non_terminal_state' };
|
|
175
|
-
}
|
|
182
|
+
const normalizedState = normalizeTerminalReviewState(input.reviewState);
|
|
176
183
|
if (!input.reviewMessage.trim()) {
|
|
177
184
|
throw new Error('missing_review_message');
|
|
178
185
|
}
|
|
@@ -21,8 +21,6 @@ export type WorkerAppPublishInput = {
|
|
|
21
21
|
expectedSubtitle?: string | null;
|
|
22
22
|
expectedPrimarySurface?: AppSurface | null;
|
|
23
23
|
remixSourceRef?: string | null;
|
|
24
|
-
playdropAssetRequirement?: WorkerPlaydropAssetRequirement | null;
|
|
25
|
-
creatorRequest?: string | null;
|
|
26
24
|
projectDir: string;
|
|
27
25
|
creatorUsername: string;
|
|
28
26
|
apiBase: string;
|
|
@@ -62,16 +60,12 @@ export type StaticHtmlPublishResult = WorkerAppPublishResult & {
|
|
|
62
60
|
runtimeContainsInjectedSdk: true;
|
|
63
61
|
hostedLoadCheckPassed: true;
|
|
64
62
|
};
|
|
65
|
-
export
|
|
66
|
-
export declare function resolveWorkerPlaydropAssetRequirement(value: unknown): WorkerPlaydropAssetRequirement | null;
|
|
63
|
+
export declare function assertAgentPrimarySurfaceContract(task: AppTask, kind: 'NEW_GAME' | 'REMIX_GAME' | 'GAME_UPDATE'): void;
|
|
67
64
|
export declare function assertAgentNewGamePlaytestContract(task: AppTask): void;
|
|
68
65
|
export declare function assertWorkerAppLocalUploadPreflight(input: {
|
|
69
66
|
task: AppTask;
|
|
70
67
|
kind: 'NEW_GAME' | 'REMIX_GAME' | 'GAME_UPDATE';
|
|
71
68
|
executionTarget?: AgentExecutionTarget;
|
|
72
|
-
creatorUsername?: string | null;
|
|
73
|
-
playdropAssetRequirement?: WorkerPlaydropAssetRequirement | null;
|
|
74
|
-
creatorRequest?: string | null;
|
|
75
69
|
stage?: 'PRE_CAPTURE' | 'COMPLETE';
|
|
76
70
|
expectedRuntimeBundleHash?: string;
|
|
77
71
|
}): Promise<void>;
|