@mpgd/cli 0.37.0 → 0.39.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mpgd/cli",
3
- "version": "0.37.0",
3
+ "version": "0.39.0",
4
4
  "description": "Gunshi CLI for mpgd-kit starter generation and target workflow orchestration.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -54,26 +54,26 @@
54
54
  "typia": "13.0.2",
55
55
  "undici": "^7.29.1",
56
56
  "vite": "8.1.3",
57
- "@mpgd/catalog": "0.7.7",
58
- "@mpgd/phaser-assets": "0.7.0",
59
- "@mpgd/target-config": "0.17.1"
57
+ "@mpgd/catalog": "0.8.0",
58
+ "@mpgd/target-config": "0.19.0",
59
+ "@mpgd/phaser-assets": "0.7.0"
60
60
  },
61
61
  "devDependencies": {
62
62
  "@types/node": "^24.0.0",
63
63
  "@types/semver": "^7.8.0",
64
64
  "ttsc": "0.30.4",
65
65
  "typescript": "7.0.2",
66
- "@mpgd/adapter-browser": "0.7.9",
67
- "@mpgd/adapter-ait": "0.13.0",
68
- "@mpgd/adapter-capacitor": "0.7.0",
69
- "@mpgd/analytics": "0.4.1",
70
- "@mpgd/adapter-verse8": "0.4.0",
71
- "@mpgd/game-runtime": "0.3.0",
72
- "@mpgd/i18n": "0.6.6",
73
- "@mpgd/adapter-devvit": "0.9.15",
74
- "@mpgd/platform": "0.15.0",
66
+ "@mpgd/adapter-ait": "0.13.2",
67
+ "@mpgd/adapter-browser": "0.9.0",
68
+ "@mpgd/adapter-capacitor": "0.7.2",
69
+ "@mpgd/analytics": "0.4.3",
70
+ "@mpgd/adapter-verse8": "0.4.2",
71
+ "@mpgd/game-services": "0.19.0",
72
+ "@mpgd/game-runtime": "0.5.0",
73
+ "@mpgd/adapter-devvit": "0.9.17",
75
74
  "@mpgd/bridge": "0.11.0",
76
- "@mpgd/game-services": "0.18.0"
75
+ "@mpgd/platform": "0.17.0",
76
+ "@mpgd/i18n": "0.6.8"
77
77
  },
78
78
  "main": "./dist/index.js",
79
79
  "types": "./dist/index.d.ts",
@@ -76,3 +76,9 @@ the snapshot bounds for persistent controls.
76
76
  bootstrap monetization ports. A test memory store is not durable recovery.
77
77
  - Use the real adapter's SDK-driven conformance tests and a game-owned platform
78
78
  test environment; generic unit tests do not certify live ad delivery.
79
+
80
+ ## ONE play acceptance
81
+
82
+ Build and smoke the configured ONE play target using the game-owned kit path. Validate server-issued reward/checkout binding, raw-byte signed SSV, purchase token API verification, ledger-before-consume/acknowledge, retries without duplicate grants, signed PNS orphan-purchase recovery and atomic cancellation fences. Missing finalization must keep PNS pending; a busy SDK must report native not-started before rejecting a new checkout. Permit five minutes of provider/server clock skew while rejecting timestamps outside the intent window.
83
+
84
+ Use an authenticated server session rather than the SDK player pseudonym. Confirm ad IDs, real payments, lifecycle, HTTPS iframe hosting and PNS/SSV in ONE store with issued ONEconsole credentials. Generic fixture and artifact smoke do not certify real-app commerce.
@@ -48,3 +48,7 @@ projection. Add SDKs only in target assembly or adapters. All rewarded views use
48
48
  the game-owned action coordinator and authoritative services, with an
49
49
  application-owned durable encrypted operation store. Keep recovery above scenes
50
50
  and reconcile existing operations on startup and resume without opening SDK UI.
51
+
52
+ ## ONE play server-owned monetization
53
+
54
+ Specify issued placement/product IDs, the authenticated reward and checkout intent routes, durable SSV/intent/ledger stores, an encrypted client operation journal, and reconciliation plus atomic cancellation fencing. Keep monetization disabled until these ports are wired. Purchase/ad callbacks are evidence; backend grants precede consume/acknowledge. Separate confirmed checkout closure from financial completion. Plan real-app acceptance with ONEconsole credentials and hosting headers.
@@ -9,6 +9,8 @@
9
9
  "web-preview",
10
10
  "microsoft-store",
11
11
  "verse8",
12
+ "crazygames",
13
+ "oneplay",
12
14
  "android",
13
15
  "ios",
14
16
  "ait",
@@ -94,6 +96,26 @@
94
96
  "SDK earning and physical closure are separate from an authoritative backend ledger grant.",
95
97
  "Recover existing claims on startup and resume without reopening SDK UI."
96
98
  ]
99
+ },
100
+ {
101
+ "id": "commerce.checkout.verified-intent",
102
+ "owner": "src/platform/buildGateways/oneplay.ts",
103
+ "capabilities": [
104
+ "server-issued-checkout-intent",
105
+ "managed-purchase-verification",
106
+ "ledger-before-finalization",
107
+ "signed-notification-recovery",
108
+ "durable-cancellation-fence",
109
+ "native-checkout-presentation",
110
+ "server-issued-ad-request",
111
+ "signed-reward-ssv"
112
+ ],
113
+ "gotchas": [
114
+ "ONE play monetization stays unavailable without game-owned authenticated issuer routes and durable intent, receipt, ledger and encrypted client recovery stores.",
115
+ "SDK player IDs are platform asserted, not authenticated backend identities. Client ad or purchase callbacks never grant.",
116
+ "Require completed consume/acknowledge before accepting PNS; implement atomic cancellation fencing and authoritative revocation in the game ledger.",
117
+ "Keep uncertain native outcomes quarantined; SDK resume is not closure proof. Validate issued IDs, SSV, payments and hosting in the real ONE store app."
118
+ ]
97
119
  }
98
120
  ],
99
121
  "acceptance": [
@@ -104,6 +104,22 @@ state behind target services. Treat client commerce as pending; authoritative
104
104
  grants belong in a game-owned Agent8 server purchase event with consume-once
105
105
  catalog handling and verified completion logic for leaderboards.
106
106
 
107
+ ### CrazyGames
108
+
109
+ Use the browser adapter's `/crazygames` entry point and the non-installable
110
+ `crazygames` web target. `crazyGamesLaunch: "basic"` disables monetization;
111
+ change it to `"full"` only after Full Launch approval. Configured midgame
112
+ interstitials use the shared advertising presentation contract. SDK callbacks
113
+ must never grant rewards or purchases: those capabilities remain disabled.
114
+
115
+ Playable scenes own gameplay scopes through the game runtime. Keep loading,
116
+ gameplay, and host `muteAudio` policy behind `PlatformGateway`; the host handles
117
+ focus reporting. Build and smoke with `pnpm exec mpgd target build crazygames`
118
+ and `pnpm exec mpgd target smoke crazygames`, providing `--kit-path` and the
119
+ project's `--targets-file`. See the kit's
120
+ [CrazyGames guide](https://github.com/imjlk/mpgd-kit/blob/main/docs/CRAZYGAMES.md)
121
+ and [official SDK documentation](https://docs.crazygames.com/sdk/intro/).
122
+
107
123
  ### Apps in Toss
108
124
 
109
125
  Use official Apps in Toss documentation/MCP before SDK or review-flow changes.
@@ -137,3 +153,9 @@ Keep source revision, config, build/smoke results, gameplay evidence, target
137
153
  preflight, artifact hashes, and target acceptance reports together. A generic
138
154
  kit smoke test proves the reusable contract only; it does not replace
139
155
  game-owned production credentials, deployment, signing, or store evidence.
156
+
157
+ ### ONE play
158
+
159
+ Use `@mpgd/adapter-browser/oneplay` through `PlatformGateway`. Build and smoke the `oneplay` target with the configured kit path. The adapter initializes the H5 SDK before gameplay; `completeLoading()` awaits host acknowledgement. Exit checkpoints must be synchronous through `storage.saveSync`. Use game-owned issued placement IDs and server verification before enabling ads or purchases. Check final HTTPS hosting headers for both ONE store domain families. See the [ONE play guide](https://github.com/imjlk/mpgd-kit/blob/main/docs/ONEPLAY.md) and [official SDK guide](https://onestore-dev.gitbook.io/dev/tools/web-sdk).
160
+
161
+ Wire `rewardRequests` and `commerceServer` in `src/platform/buildGateways/oneplay.ts`. Server-only `@mpgd/game-services/oneplay-reward`, `oneplay-purchase-client`, `oneplay-purchase` and `oneplay-pns` provide signed SSV, authenticated checkout intents, API V7 verification, post-ledger consume/acknowledge and signed notification recovery. The game owns durable stores, routes, an encrypted client journal and atomic cancellation fencing. A completed PNS requires completed finalization. Missing ports keep capabilities unavailable; subscriptions are unsupported. See `commerce.checkout.verified-intent` in the game manifest and the ONE play acceptance notes before enabling policies.
@@ -22,6 +22,28 @@
22
22
  "icon": { "profile": "web-preview" },
23
23
  "output": "artifacts/verse8"
24
24
  },
25
+ "crazygames": {
26
+ "kind": "web",
27
+ "gameApp": ".",
28
+ "adapter": "crazygames",
29
+ "icon": {
30
+ "profile": "web-preview"
31
+ },
32
+ "installable": false,
33
+ "crazyGamesLaunch": "basic",
34
+ "output": "artifacts/crazygames"
35
+ },
36
+ "oneplay": {
37
+ "kind": "web",
38
+ "gameApp": ".",
39
+ "adapter": "oneplay",
40
+ "icon": {
41
+ "profile": "web-preview"
42
+ },
43
+ "installable": false,
44
+ "output": "artifacts/oneplay",
45
+ "authoritativeGameServices": false
46
+ },
25
47
  "android": {
26
48
  "kind": "capacitor-android",
27
49
  "gameApp": ".",
@@ -49,6 +49,7 @@ async function bootstrap(options: { readonly monetization?: StarterMonetizationP
49
49
  });
50
50
  const viewport = resolveTargetViewportSnapshot({
51
51
  ...measurement,
52
+ ...(platform.viewport === undefined ? {} : { safeAreaInsets: platform.viewport.getState().safeAreaInsets }),
52
53
  runtime: runtime.config.runtime,
53
54
  orientationPolicy,
54
55
  });
@@ -63,6 +64,7 @@ async function bootstrap(options: { readonly monetization?: StarterMonetizationP
63
64
  ]);
64
65
  locale = resolveTargetMpgdLocale({
65
66
  capabilities: runtime.capabilities,
67
+ ...(platform.gameSettings?.getLocale === undefined ? {} : { preferredLocales: [platform.gameSettings.getLocale()] }),
66
68
  fallbackLocale:
67
69
  runtime.effectiveConfig?.localization.fallbackLocale
68
70
  ?? runtime.config.localization.fallbackLocale,
@@ -0,0 +1,14 @@
1
+ import { createCrazyGamesPlatformGateway } from '@mpgd/adapter-browser/crazygames';
2
+ import { resolveAdPlacementPlatformId, type AdPlacements } from '@mpgd/catalog';
3
+ import adPlacementsJson from '@mpgd/catalog/placements.json';
4
+ import type { PlatformGateway } from '@mpgd/platform';
5
+ import type { RuntimeConfig } from '../runtimeDetector';
6
+
7
+ export async function createBuildGateway(runtime: RuntimeConfig): Promise<PlatformGateway> {
8
+ const placements = (adPlacementsJson as AdPlacements).placements;
9
+ return createCrazyGamesPlatformGateway({
10
+ launch: __MPGD_PLATFORM_TARGET__?.crazyGamesLaunch ?? 'basic',
11
+ placementIds: placements.filter((placement) => placement.type === 'interstitial'
12
+ && resolveAdPlacementPlatformId(placement, runtime.configTarget) !== undefined).map((placement) => placement.id),
13
+ });
14
+ }
@@ -0,0 +1,32 @@
1
+ import {
2
+ createOnePlayPlatformGateway,
3
+ type OnePlayGatewayOptions,
4
+ } from '@mpgd/adapter-browser/oneplay';
5
+ import {
6
+ resolveAdPlacementPlatformId,
7
+ resolveProductPlatformId,
8
+ type AdPlacements,
9
+ type ProductCatalog,
10
+ } from '@mpgd/catalog';
11
+ import adPlacementsJson from '@mpgd/catalog/placements.json';
12
+ import productCatalogJson from '@mpgd/catalog/catalog.json';
13
+ import type { PlatformGateway } from '@mpgd/platform';
14
+ import type { RuntimeConfig } from '../runtimeDetector';
15
+
16
+ export async function createBuildGateway(runtime: RuntimeConfig, ports: Pick<OnePlayGatewayOptions, 'rewardRequests' | 'commerceServer'> = {}): Promise<PlatformGateway> {
17
+ const placementIds = Object.fromEntries(
18
+ (adPlacementsJson as AdPlacements).placements.flatMap((placement) => {
19
+ const platformId = resolveAdPlacementPlatformId(placement, runtime.configTarget);
20
+ return placement.type !== 'banner' && platformId !== undefined
21
+ ? [[placement.id, { format: placement.type, platformId }]]
22
+ : [];
23
+ }),
24
+ );
25
+ const products = (productCatalogJson as ProductCatalog).products.flatMap((product) => {
26
+ const platformId = resolveProductPlatformId(product, runtime.configTarget);
27
+ return product.type !== 'subscription' && platformId !== undefined
28
+ ? [{ id: product.id, platformId, type: product.type }]
29
+ : [];
30
+ });
31
+ return createOnePlayPlatformGateway({ ...ports, placementIds, products });
32
+ }
@@ -7,6 +7,8 @@ const validTargets = new Set<string>([
7
7
  'microsoft-store',
8
8
  'reddit',
9
9
  'verse8',
10
+ 'crazygames',
11
+ 'oneplay',
10
12
  'telegram',
11
13
  'tauri',
12
14
  'wechat',
@@ -28,15 +28,15 @@ export function createStarterGame(input: {
28
28
  },
29
29
  scene: [BootScene, LobbyScene, PlayScene],
30
30
  callbacks: {
31
- postBoot(game: Phaser.Game) {
31
+ preBoot(game: Phaser.Game) {
32
32
  game.registry.set(starterContextKey, input.context);
33
+ },
34
+ postBoot(game: Phaser.Game) {
33
35
  ownGameAudio(game, input.context);
34
36
  },
35
37
  },
36
38
  });
37
39
 
38
- game.registry.set(starterContextKey, input.context);
39
-
40
40
  return game;
41
41
  }
42
42
 
@@ -2,6 +2,8 @@ import Phaser from 'phaser';
2
2
 
3
3
  import { loadPhaserAssets } from '@mpgd/phaser-assets';
4
4
 
5
+ import { starterContextKey, type StarterContext } from '../runtime/gameContext';
6
+
5
7
  import { starterAssets } from '../assets/manifest';
6
8
 
7
9
  export class BootScene extends Phaser.Scene {
@@ -10,10 +12,17 @@ export class BootScene extends Phaser.Scene {
10
12
  }
11
13
 
12
14
  preload(): void {
15
+ const context = this.registry.get(starterContextKey) as StarterContext;
16
+ this.load.on('progress', (progress: number) =>
17
+ context.gameRuntime.setLoadingProgress(progress * 100),
18
+ );
13
19
  loadPhaserAssets(this, starterAssets);
14
20
  }
15
21
 
16
22
  create(): void {
17
- this.scene.start('LobbyScene');
23
+ const context = this.registry.get(starterContextKey) as StarterContext;
24
+ void context.gameRuntime.completeLoading().then(() => {
25
+ this.scene.start(context.launchIntent.entry === 'free-play' ? 'PlayScene' : 'LobbyScene');
26
+ }).catch((error: unknown) => { console.error('[game-loading]', error); });
18
27
  }
19
28
  }
@@ -29,8 +29,25 @@ export class PlayScene extends Phaser.Scene {
29
29
 
30
30
  create(): void {
31
31
  this.context = this.registry.get(starterContextKey) as StarterContext;
32
+ // A host exit callback cannot await a server upload. Keep this local checkpoint synchronous.
33
+ const checkpoint = () => {
34
+ try {
35
+ this.context.platform.storage.saveSync?.({
36
+ key: 'starter-run',
37
+ value: this.state,
38
+ });
39
+ } catch (error) {
40
+ console.error('[checkpoint]', error);
41
+ }
42
+ };
43
+ const unsubscribeExit = this.context.platform.lifecycle.onExit?.(checkpoint);
44
+ this.events.once('shutdown', () => {
45
+ checkpoint();
46
+ unsubscribeExit?.();
47
+ });
32
48
  bindPhaserGameScene({
33
49
  controller: this.context.gameRuntime.execution,
50
+ gameplayScope: this.context.gameRuntime.createGameplayScope(),
34
51
  scene: this,
35
52
  renderingPolicy: 'visibility',
36
53
  audioOwner: 'game',
@@ -20,6 +20,8 @@ const runtimeKinds = valueSet({
20
20
  'apps-in-toss': true,
21
21
  'devvit-web': true,
22
22
  'verse8-web': true,
23
+ 'crazygames-web': true,
24
+ 'oneplay-web': true,
23
25
  'wechat-minigame': true,
24
26
  'tiktok-minigame': true,
25
27
  } satisfies Record<TargetRuntimeKind, true>);
@@ -32,6 +34,8 @@ const releaseProfiles = valueSet({
32
34
  'apps-in-toss': true,
33
35
  devvit: true,
34
36
  verse8: true,
37
+ crazygames: true,
38
+ oneplay: true,
35
39
  'wechat-minigame': true,
36
40
  'tiktok-minigame': true,
37
41
  } satisfies Record<ReleaseProfile, true>);
@@ -14,6 +14,7 @@ import { resolveTextSelectionMode } from './src/platform/textSelection';
14
14
  interface RuntimePlatformTargetMetadata {
15
15
  readonly kind: string;
16
16
  readonly adapter: string;
17
+ readonly crazyGamesLaunch?: 'basic' | 'full';
17
18
  readonly authoritativeGameServices?: boolean;
18
19
  readonly integrations?: Record<string, unknown>;
19
20
  }
@@ -151,6 +152,10 @@ export function resolveBuildGatewayModule(input: {
151
152
  return input.debug && input.buildId === devvitSandboxBuildId
152
153
  ? 'src/platform/buildGateways/redditSandbox.ts'
153
154
  : 'src/platform/buildGateways/reddit.ts';
155
+ case 'oneplay':
156
+ return 'src/platform/buildGateways/oneplay.ts';
157
+ case 'crazygames':
158
+ return 'src/platform/buildGateways/crazygames.ts';
154
159
  case 'verse8':
155
160
  return 'src/platform/buildGateways/verse8.ts';
156
161
  case 'microsoft-store':
@@ -245,6 +250,9 @@ function readRuntimePlatformTarget(
245
250
  throw new Error(`Platform target ${configTarget} must define kind and adapter.`);
246
251
  }
247
252
 
253
+ if (target.crazyGamesLaunch !== undefined && (target.adapter !== 'crazygames' || !['basic', 'full'].includes(target.crazyGamesLaunch as string))) {
254
+ throw new Error(`Invalid CrazyGames launch setting for ${configTarget}.`);
255
+ }
248
256
  if (target.integrations !== undefined && !isRecord(target.integrations)) {
249
257
  throw new Error(`Platform target ${configTarget} integrations must be an object.`);
250
258
  }
@@ -262,6 +270,7 @@ function readRuntimePlatformTarget(
262
270
  ...(target.authoritativeGameServices === undefined
263
271
  ? {}
264
272
  : { authoritativeGameServices: target.authoritativeGameServices }),
273
+ ...(target.crazyGamesLaunch === undefined ? {} : { crazyGamesLaunch: target.crazyGamesLaunch as 'basic' | 'full' }),
265
274
  ...(target.integrations === undefined ? {} : { integrations: target.integrations }),
266
275
  };
267
276
  }