import { Buffer } from 'node:buffer'; import { mkdir, readFile, stat, writeFile } from 'node:fs/promises'; import path from 'node:path'; import type { CodingPluginToolDefinition } from '../../../shared/coding-plugins'; import { PiGameAssetTools } from '../../coding-runtime/pi/extensions/game-assets'; import type { MarketplacePackageClientPort, PluginPackageStore } from '../package-store'; import { MarketplaceHostedAdmissionError, MarketplaceHostedAdmissionResolver, } from '../hosted-admission'; import { GameResourceClient, GameResourceClientError, type GameResourceGeneration, } from '../../services/game-resource-client'; import type { AdapterInvocationResult, CodingPluginAdapter, PluginBackendProjection, TrustedCodingCapabilityContext, } from '../registry'; const PLUGIN_ID = 'makelore.game-resource'; const MAX_REFERENCE_FILE_BYTES = 8 * 1024 * 1024; const MAX_REFERENCE_BYTES = 16 * 1024 * 1024; const EXECUTION_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/u; type Input = Record; export interface GameResourcePluginAdapterOptions { readonly client: GameResourceClient; readonly marketplace: MarketplacePackageClientPort; readonly packageStore: Pick; readonly makeloreVersion: string; readonly admissionResolver?: MarketplaceHostedAdmissionResolver; readonly gameAssets?: PiGameAssetTools; } function isRecord(value: unknown): value is Input { return value !== null && typeof value === 'object' && !Array.isArray(value); } function success( data: T, status = 200, billing?: Extract, { mode: 'platform_metered' }>, ): AdapterInvocationResult { return { success: true, status, code: null, error: null, retryable: false, payload_schema: 'game-resource.v1', data, ...(billing ? { billing } : {}), }; } function failure( code: string, error: string, status: number, retryable: boolean, billing?: Extract, { mode: 'platform_metered' }>, ): AdapterInvocationResult { return { success: false, status, code, error, retryable, payload_schema: 'game-resource.v1', data: null, ...(billing ? { billing } : {}), }; } function generationData(value: GameResourceGeneration, includeBilling: boolean) { return { executionId: value.executionId, status: value.status, outputCount: value.outputCount, ...(value.pollIntervalSeconds === undefined ? {} : { pollIntervalSeconds: value.pollIntervalSeconds }), ...(value.errorCode === undefined ? {} : { errorCode: value.errorCode }), ...(includeBilling ? { generationBilling: value.billing } : {}), }; } function clientFailure(error: unknown): AdapterInvocationResult { if (error instanceof GameResourceClientError) { return failure(error.code, error.message, error.status, error.retryable); } if (error instanceof MarketplaceHostedAdmissionError) { return failure(error.code, error.message, error.status, error.retryable); } return failure( 'plugin_backend_unavailable', 'Hosted game-resource service is temporarily unavailable', 503, true, ); } function relativeProjectPath(projectPath: string, candidate: unknown): { absolute: string; relative: string } { if (typeof candidate !== 'string' || !candidate.trim() || candidate.length > 1_024) { throw new GameResourceClientError('plugin_input_invalid', 422, false, 'Project-relative path is invalid'); } const root = path.resolve(projectPath); const absolute = path.resolve(root, candidate); const relative = path.relative(root, absolute); if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) { throw new GameResourceClientError('plugin_input_invalid', 422, false, 'Project-relative path is invalid'); } return { absolute, relative: relative.split(path.sep).join('/') }; } function mimeType(filePath: string): string { switch (path.extname(filePath).toLowerCase()) { case '.jpg': case '.jpeg': return 'image/jpeg'; case '.webp': return 'image/webp'; case '.gif': return 'image/gif'; default: return 'image/png'; } } async function references(projectPath: string, value: unknown) { if (value === undefined) return []; if (!Array.isArray(value) || value.length > 8) { throw new GameResourceClientError('plugin_input_invalid', 422, false, 'Reference paths are invalid'); } let total = 0; return await Promise.all(value.map(async (candidate) => { const target = relativeProjectPath(projectPath, candidate); const metadata = await stat(target.absolute).catch(() => null); if (!metadata?.isFile() || metadata.size <= 0 || metadata.size > MAX_REFERENCE_FILE_BYTES) { throw new GameResourceClientError('plugin_input_invalid', 422, false, 'Reference file is unavailable or too large'); } total += metadata.size; if (total > MAX_REFERENCE_BYTES) { throw new GameResourceClientError('plugin_input_invalid', 422, false, 'Reference files exceed their total bound'); } return { name: path.basename(target.absolute).slice(0, 160), mimeType: mimeType(target.absolute), dataBase64: (await readFile(target.absolute)).toString('base64'), }; })); } function templateConfig(value: unknown): Readonly> { if (value === undefined || value === '') return {}; if (typeof value !== 'string' || Buffer.byteLength(value, 'utf8') > 16_384) { throw new GameResourceClientError('plugin_input_invalid', 422, false, 'Template config JSON is invalid'); } let parsed: unknown; try { parsed = JSON.parse(value) as unknown; } catch { throw new GameResourceClientError('plugin_input_invalid', 422, false, 'Template config JSON is invalid'); } if (!isRecord(parsed)) { throw new GameResourceClientError('plugin_input_invalid', 422, false, 'Template config JSON must be an object'); } return parsed; } export class GameResourcePluginAdapter implements CodingPluginAdapter { readonly pluginId = PLUGIN_ID; private readonly gameAssets: PiGameAssetTools; private readonly admissionResolver: MarketplaceHostedAdmissionResolver; constructor(private readonly options: GameResourcePluginAdapterOptions) { this.gameAssets = options.gameAssets ?? new PiGameAssetTools(); this.admissionResolver = options.admissionResolver ?? new MarketplaceHostedAdmissionResolver({ marketplace: options.marketplace, packageStore: options.packageStore, makeloreVersion: options.makeloreVersion, }); } async inspect(): Promise { const installed = await this.options.packageStore.getInstalled(PLUGIN_ID).catch(() => null); return installed ? { status: 'ready' } : { status: 'unconfigured' }; } async invoke( context: TrustedCodingCapabilityContext, tool: CodingPluginToolDefinition, input: unknown, ): Promise { if (!isRecord(input)) return failure('plugin_input_invalid', 'Game-resource tool input is invalid', 422, false); try { switch (tool.name) { case 'game_resource_templates': { const kind = input.kind === 'pixel' || input.kind === 'hd' ? input.kind : null; if (!kind) return failure('plugin_input_invalid', 'Game-resource kind is invalid', 422, false); const admission = await this.admission(context); return success({ kind, templates: await this.options.client.templates({ ...admission, kind }) }); } case 'game_resource_generate': { if (input.confirmed !== true) { return failure( 'confirmation_required', 'Explicit Token Point generation confirmation is required', 400, false, ); } const kind = input.kind === 'pixel' || input.kind === 'hd' ? input.kind : null; if (!kind || typeof input.templateName !== 'string' || typeof input.requirement !== 'string') { return failure('plugin_input_invalid', 'Game-resource generation input is invalid', 422, false); } const admission = await this.admission(context); const generated = await this.options.client.generate({ ...admission, projectId: context.durableProjectId, logicalOperationId: context.requestId, kind, templateName: input.templateName, templateConfig: templateConfig(input.templateConfigJson), requirement: input.requirement, ...(typeof input.aspectRatio === 'string' ? { aspectRatio: input.aspectRatio } : {}), ...(typeof input.temperature === 'number' ? { temperature: input.temperature } : {}), ...(typeof input.jobName === 'string' ? { jobName: input.jobName } : {}), ...(typeof input.modelName === 'string' ? { modelName: input.modelName } : {}), ...(typeof input.resolution === 'string' ? { resolution: input.resolution } : {}), ...(typeof input.hdRemoveBgMode === 'string' ? { hdRemoveBgMode: input.hdRemoveBgMode } : {}), ...(typeof input.threadId === 'string' ? { threadId: input.threadId } : {}), referenceFiles: await references(context.projectPath, input.referencePaths), }); if (generated.status === 'failed' || generated.status === 'cancelled') { return failure( generated.errorCode ?? 'game_resource_generation_failed', 'Hosted game-resource generation was rejected', 422, false, generated.billing, ); } return success( generationData(generated, false), generated.status === 'succeeded' ? 200 : 202, generated.billing, ); } case 'game_resource_status': { const executionId = this.executionId(input.executionId); const generated = await this.options.client.get(executionId); return success(generationData(generated, true)); } case 'game_resource_cancel': { const executionId = this.executionId(input.executionId); const generated = await this.options.client.cancel(executionId); return success(generationData(generated, true)); } case 'game_resource_save_output': { if (input.confirmed !== true) return failure('confirmation_required', 'Explicit save confirmation is required', 400, false); const executionId = this.executionId(input.executionId); const outputIndex = input.outputIndex === undefined ? undefined : input.outputIndex; if (outputIndex !== undefined && (!Number.isSafeInteger(outputIndex) || (outputIndex as number) < 0 || (outputIndex as number) > 99)) { return failure('plugin_input_invalid', 'Game-resource output index is invalid', 422, false); } const target = relativeProjectPath(context.projectPath, input.relativePath); const content = await this.options.client.download(executionId, outputIndex as number | undefined); await mkdir(path.dirname(target.absolute), { recursive: true }); try { await writeFile(target.absolute, content.bytes, { flag: 'wx' }); } catch (error) { if (isRecord(error) && error.code === 'EEXIST') { return failure('game_resource_destination_exists', 'Destination file already exists', 409, false); } throw error; } return success({ savedPath: target.relative, bytes: content.bytes.byteLength }); } case 'game_asset_browser': { const result = await this.gameAssets.browse(context.projectPath, input, context.requestId); const parsed = JSON.parse(result.content[0]?.text ?? '{}') as unknown; return success(parsed); } case 'game_asset_review': { const result = await this.gameAssets.review(context.projectPath, input, context.requestId); return success(result.details); } default: return failure('plugin_contract_unsupported', 'Game-resource operation is unavailable', 503, true); } } catch (error) { return clientFailure(error); } } private executionId(value: unknown): string { if (typeof value !== 'string' || !EXECUTION_ID.test(value)) { throw new GameResourceClientError('plugin_input_invalid', 422, false, 'Execution ID is invalid'); } return value; } private async admission(context: TrustedCodingCapabilityContext): Promise<{ releaseId: string; releaseAdmissionId: string; }> { return this.admissionResolver.resolve({ pluginId: PLUGIN_ID, workerSnapshot: { requestId: context.requestId, ...(context.pluginReleaseId === undefined ? {} : { pluginReleaseId: context.pluginReleaseId }), }, }); } } export function createGameResourcePluginAdapter( options: GameResourcePluginAdapterOptions, ): GameResourcePluginAdapter { return new GameResourcePluginAdapter(options); }