upload --pre-check/--evidence, shots copy translate, game-center --locale-map, play games API-disabled hint

This commit is contained in:
CRTkafa 2026-10-24 12:10:23 +08:00
parent 29a9683ab7
commit 8ffa27d4f8
24 changed files with 598 additions and 27 deletions

View file

@ -1,5 +1,7 @@
// Game Center through the App Store Connect API, from the `games` config section (cli/lib/game-services.ts).
// storebound apple game-center [status] [--dry] [--yes] [--json]
// storebound apple game-center [status] [--locale-map en=en-GB] [--dry] [--yes] [--json]
// --locale-map (config games.localeMap): config language -> the locale the live Game Center uses; the dry run suggests it
// when Game Center has exactly one variant of a language (en-GB) and the config says another (en-US).
// status: read-only (App ID capability, Game Center detail, the editable version's switch, leaderboards and
// achievements on the store vs the config, the project's entitlement). Default: the upsert. Every write is a
// find-then-create (createOnce: a conflict or an unclear answer reads the store again, never a second create) or a
@ -10,7 +12,7 @@ import { consoleLink } from '../lib/console-links';
import type { Ctx } from '../lib/ctx';
import { confirm, need } from '../lib/ctx';
import { dryPlan } from '../lib/dry';
import { type GcExisting, gcFindings, type ItemPlan, planGameCenter, planRows, usesGameCenter } from '../lib/game-services';
import { type GcExisting, gcFindings, gcLocaleMap, type ItemPlan, liveGcLocales, planGameCenter, planRows, suggestLocaleMap, usesGameCenter } from '../lib/game-services';
import { done, log, warn } from '../lib/log';
import { createOnce } from '../lib/upsert';
import { type Asc, ascClient, type J } from './asc';
@ -61,21 +63,24 @@ export async function run(ctx: Ctx): Promise<void> {
const { config, root } = ctx.loaded;
const a = ctx.args;
const sub = a._[0] ?? 'sync';
if (!['sync', 'status'].includes(sub)) throw new Error('usage: storebound apple game-center [status] [--dry] [--yes] [--json]');
if (!['sync', 'status'].includes(sub)) throw new Error('usage: storebound apple game-center [status] [--locale-map en=en-GB] [--dry] [--yes] [--json]');
if (!usesGameCenter(config)) throw new Error('games.gameCenter is not true in storebound.config.ts (game-services skill): nothing to do');
const bundleId = need(config.apple?.bundleId, 'apple.bundleId');
const c = await ascClient(root, bundleId, config.apple?.appId);
const appId = await c.app();
const s = await readGameCenter(c, bundleId);
const g = config.games!;
const plan = planGameCenter(g, config.languages, s);
const localeMap = gcLocaleMap(g, a.many('locale-map'));
const plan = planGameCenter(g, config.languages, s, localeMap);
const suggest = suggestLocaleMap(config.languages, liveGcLocales(s), localeMap);
const project = gcFindings(config, readProjectEntitlements(root));
if (a.has('json')) { console.log(JSON.stringify({ ...stateSummary(s, plan), project, plan }, null, 2)); return; }
if (a.has('json')) { console.log(JSON.stringify({ ...stateSummary(s, plan), project, localeMap, ...(suggest.length ? { suggestLocaleMap: suggest } : {}), plan }, null, 2)); return; }
if (s.capability === false) warn(TAG, `the App ID ${bundleId} has no GAME_CENTER capability: storebound apple signing ensure --dry, then without --dry (games.gameCenter adds it)`);
if (s.capability === null) warn(TAG, `bundle id ${bundleId} not found in Certificates, Identifiers & Profiles: storebound apple bundle`);
for (const f of project) (f.level === 'ok' ? log : warn)(TAG, `${f.msg}${f.fix ? ` (${f.fix})` : ''}`);
for (const x of suggest) warn(TAG, `config language "${x.id}" is ${x.from}, but the live Game Center has only ${x.to} for that language, so ${x.from} would be created next to it. Map it: storebound apple game-center --locale-map ${x.id}=${x.to} (or games: { localeMap: { ${x.id}: '${x.to}' } } in the config)`);
const rows = [
{ item: 'Game Center (app)', action: s.detail ? 'keep' : 'create', detail: 'POST /v1/gameCenterDetails' },
...(s.version ? [{ item: `Game Center (version ${s.version.versionString})`, action: s.version.enabled ? 'keep' : 'create', detail: 'POST /v1/gameCenterAppVersions' }] : []),
@ -122,7 +127,7 @@ export async function run(ctx: Ctx): Promise<void> {
}
// read back: the store must now match the config (lists can lag right after a create: say so, do not fail)
const after = await readGameCenter(c, bundleId);
const left = planGameCenter(g, config.languages, after).filter(p => p.action !== 'keep' || p.locales.some(l => l.action !== 'keep'));
const left = planGameCenter(g, config.languages, after, localeMap).filter(p => p.action !== 'keep' || p.locales.some(l => l.action !== 'keep'));
if (!after.detail || left.length) warn(TAG, `read back: ${!after.detail ? 'no Game Center detail yet; ' : ''}${left.map(p => `${p.kind} ${p.id}`).join(', ') || ''} not listed as written yet (App Store Connect lists lag after a create): run storebound apple game-center status in a minute`);
done(TAG, `Game Center: ${after.leaderboards.length} leaderboard(s), ${after.achievements.length} achievement(s); images and review go with the next version (${consoleLink('asc.version')})`);
}