@mpgd/cli 0.36.1 → 0.37.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/dist/index.js CHANGED
@@ -72,6 +72,7 @@ const mpgdTemplateDependencyPackages = [
72
72
  { name: '@mpgd/catalog', packageDir: 'packages/catalog' },
73
73
  { name: '@mpgd/cli', packageDir: 'packages/cli' },
74
74
  { name: '@mpgd/game-services', packageDir: 'packages/game-services' },
75
+ { name: '@mpgd/game-runtime', packageDir: 'packages/game-runtime' },
75
76
  { name: '@mpgd/i18n', packageDir: 'packages/i18n' },
76
77
  { name: '@mpgd/phaser-assets', packageDir: 'packages/phaser-assets' },
77
78
  { name: '@mpgd/platform', packageDir: 'packages/platform' },
@@ -1,5 +1,5 @@
1
1
  {
2
- "kitGitSha": "646745bcd2b326e05dfb7030e8c15d8a102e054b",
2
+ "kitGitSha": "9573fe71b8e4a1a338f88582f28bf058448e0aa8",
3
3
  "kitDirty": false,
4
- "packageVersion": "0.36.1"
4
+ "packageVersion": "0.37.0"
5
5
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mpgd/cli",
3
- "version": "0.36.1",
3
+ "version": "0.37.0",
4
4
  "description": "Gunshi CLI for mpgd-kit starter generation and target workflow orchestration.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -54,25 +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.6",
58
- "@mpgd/target-config": "0.17.0",
59
- "@mpgd/phaser-assets": "0.7.0"
57
+ "@mpgd/catalog": "0.7.7",
58
+ "@mpgd/phaser-assets": "0.7.0",
59
+ "@mpgd/target-config": "0.17.1"
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-ait": "0.12.7",
67
- "@mpgd/adapter-browser": "0.7.8",
68
- "@mpgd/adapter-capacitor": "0.6.1",
69
- "@mpgd/adapter-verse8": "0.3.11",
70
- "@mpgd/adapter-devvit": "0.9.13",
71
- "@mpgd/bridge": "0.10.0",
72
- "@mpgd/analytics": "0.4.0",
73
- "@mpgd/game-services": "0.17.1",
74
- "@mpgd/i18n": "0.6.5",
75
- "@mpgd/platform": "0.14.0"
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",
75
+ "@mpgd/bridge": "0.11.0",
76
+ "@mpgd/game-services": "0.18.0"
76
77
  },
77
78
  "main": "./dist/index.js",
78
79
  "types": "./dist/index.d.ts",
@@ -63,3 +63,16 @@ locked orientation modes as soft prompts instead of unsafe WebView hard locks.
63
63
  Verify safe-area geometry is reserved exactly once: CSS-padded containers must
64
64
  not also subtract `safeArea.contentBounds`, while full-bleed game surfaces use
65
65
  the snapshot bounds for persistent controls.
66
+
67
+ ## Shared native presentation
68
+
69
+ - Purchase, rewarded, and interstitial requests use the same game-owned scope.
70
+ - Native closure resumes gameplay while backend verification can remain pending.
71
+ - Timeouts preserve unknown occupancy and late native/recovery observers.
72
+ - Scene shutdown removes its action view and keyboard handler; it never unmutes
73
+ a live ad or updates a replacement scene from an old Promise.
74
+ - Background and settings ownership remain after another native lease closes.
75
+ - Rewarded v2 is unavailable until a durable encrypted journal is supplied via
76
+ bootstrap monetization ports. A test memory store is not durable recovery.
77
+ - Use the real adapter's SDK-driven conformance tests and a game-owned platform
78
+ test environment; generic unit tests do not certify live ad delivery.
@@ -39,3 +39,12 @@ the game-owned PWA before package generation, then run package acceptance on
39
39
  Windows before Partner Center submission. WACK remains an optional recommended
40
40
  evidence source, not a universal prerequisite.
41
41
  <!-- mpgd:microsoft-store:end -->
42
+
43
+ ## Game-owned execution and advertising
44
+
45
+ Construct one `createGamePlatformRuntime` during bootstrap. Scenes bind gameplay
46
+ execution with `audioOwner: 'game'`; the Phaser game's sound manager owns audio
47
+ projection. Add SDKs only in target assembly or adapters. All rewarded views use
48
+ the game-owned action coordinator and authoritative services, with an
49
+ application-owned durable encrypted operation store. Keep recovery above scenes
50
+ and reconcile existing operations on startup and resume without opening SDK UI.
@@ -5,8 +5,19 @@
5
5
  "title": "__GAME_TITLE__",
6
6
  "engine": "phaser-4"
7
7
  },
8
- "targets": ["web-preview", "microsoft-store", "verse8", "android", "ios", "ait", "reddit"],
9
- "futureTargets": ["telegram", "tauri"],
8
+ "targets": [
9
+ "web-preview",
10
+ "microsoft-store",
11
+ "verse8",
12
+ "android",
13
+ "ios",
14
+ "ait",
15
+ "reddit"
16
+ ],
17
+ "futureTargets": [
18
+ "telegram",
19
+ "tauri"
20
+ ],
10
21
  "platformBoundary": "PlatformGateway",
11
22
  "agentWorkflow": {
12
23
  "guide": "docs/MPGD_KIT_WORKFLOWS.md",
@@ -67,6 +78,22 @@
67
78
  {
68
79
  "id": "telemetry.analytics.best-effort",
69
80
  "owner": "src/main.ts"
81
+ },
82
+ {
83
+ "id": "runtime.game.owned-presentation",
84
+ "owner": "src/main.ts",
85
+ "capabilities": [
86
+ "game-owned-execution",
87
+ "shared-full-screen-presentation",
88
+ "late-claim-recovery",
89
+ "game-owned-audio"
90
+ ],
91
+ "gotchas": [
92
+ "Create one game runtime above scene lifetimes; scene teardown must not release native presentation or global audio.",
93
+ "Rewarded v2 requires an application-owned durable encrypted operation journal; never substitute an in-memory map.",
94
+ "SDK earning and physical closure are separate from an authoritative backend ledger grant.",
95
+ "Recover existing claims on startup and resume without reopening SDK UI."
96
+ ]
70
97
  }
71
98
  ],
72
99
  "acceptance": [
@@ -72,7 +72,8 @@
72
72
  "@mpgd/platform": "__MPGD_DEPENDENCY_VERSION_PLATFORM__",
73
73
  "@mpgd/phaser-assets": "__MPGD_DEPENDENCY_VERSION_PHASER_ASSETS__",
74
74
  "@mpgd/target-config": "__MPGD_DEPENDENCY_VERSION_TARGET_CONFIG__",
75
- "phaser": "4.2.0"
75
+ "phaser": "4.2.0",
76
+ "@mpgd/game-runtime": "__MPGD_DEPENDENCY_VERSION_GAME_RUNTIME__"
76
77
  },
77
78
  "devDependencies": {
78
79
  "@ait-co/console-cli": "^0.1.49",
@@ -1,9 +1,11 @@
1
+ import { createGamePlatformRuntime, type GamePlatformRuntime } from '@mpgd/game-runtime/game';
1
2
  import './styles.css';
2
3
 
3
4
  import { createAnalyticsReporter, createBufferedAnalyticsSink } from '@mpgd/analytics';
4
5
  import { resolveTargetMpgdLocale, type Locale } from '@mpgd/i18n';
5
6
  import type { IdentitySession, LaunchIntent, PlatformGateway } from '@mpgd/platform';
6
7
  import {
8
+ getEffectiveAdPlacementConfig,
7
9
  measureTargetViewport,
8
10
  resolveTargetViewportSnapshot,
9
11
  waitForTargetViewportMeasurement,
@@ -15,14 +17,15 @@ import { t } from './i18n/messages';
15
17
  import { createClientId } from './runtime/id';
16
18
  import { createStarterGame } from './runtime/createGame';
17
19
  import { detectRuntime } from './platform/runtimeDetector';
18
- import { createStarterGameServices } from './platform/gameServices';
20
+ import { createStarterGameServices, type StarterMonetizationPorts } from './platform/gameServices';
19
21
  import { installPlatform } from './platform/installPlatform';
20
22
  import { installMicrosoftStorePwa } from './platform/microsoftStorePwa';
21
23
 
22
24
  await bootstrap();
23
25
 
24
26
  /** Install target services, analytics, and viewport state before starting the game. */
25
- async function bootstrap(): Promise<void> {
27
+ async function bootstrap(options: { readonly monetization?: StarterMonetizationPorts } = {}): Promise<void> {
28
+ let gameRuntime: GamePlatformRuntime | undefined;
26
29
  let locale: Locale = 'en';
27
30
  let disposeMicrosoftStorePwa: (() => void) | undefined;
28
31
 
@@ -71,13 +74,43 @@ async function bootstrap(): Promise<void> {
71
74
  sessionId: analyticsSessionId,
72
75
  sink: analyticsSink,
73
76
  });
74
- const gameServices = createStarterGameServices({
77
+ const ownedRuntime = createGamePlatformRuntime({
75
78
  gateway: platform,
76
- playerId: identitySession.playerId ?? player.playerId,
77
- configTarget: runtime.configTarget,
78
- analytics: analyticsSink,
79
- analyticsSessionId,
79
+ initialLifecycleState: document.visibilityState === 'hidden' ? 'inactive' : 'active',
80
+ canShow: (placement) => {
81
+ const enabled = placement.format === 'rewarded'
82
+ ? runtime.config.features.rewardedAds
83
+ : runtime.config.features.interstitialAds;
84
+ const configured = runtime.effectiveConfig === undefined
85
+ ? undefined
86
+ : getEffectiveAdPlacementConfig(runtime.effectiveConfig, placement.placementId);
87
+ return enabled && (runtime.effectiveConfig === undefined || configured !== undefined && configured.reason !== 'target-disabled');
88
+ },
89
+ deadline: {
90
+ milliseconds: 30000,
91
+ schedule(callback, milliseconds) {
92
+ const timer = setTimeout(callback, milliseconds);
93
+ return () => clearTimeout(timer);
94
+ },
95
+ },
96
+ ...(options.monetization?.reconciliation === undefined ? {} : { reconciliation: options.monetization.reconciliation }),
97
+ ...(options.monetization?.purchasePresentation === undefined ? {} : { purchasePresentation: options.monetization.purchasePresentation }),
98
+ onObserverError: (error) => {
99
+ console.error('[game-runtime]', error);
100
+ },
101
+ createServices: (gateway) =>
102
+ createStarterGameServices({
103
+ gateway,
104
+ playerId: identitySession.playerId ?? player.playerId,
105
+ configTarget: runtime.configTarget,
106
+ ...(options.monetization === undefined ? {} : { operationStore: options.monetization.operationStore }),
107
+ analytics: analyticsSink,
108
+ analyticsSessionId,
109
+ }),
80
110
  });
111
+ gameRuntime = ownedRuntime;
112
+ const gameServices = ownedRuntime.services;
113
+ await ownedRuntime.reconcile();
81
114
 
82
115
  await analytics.track({
83
116
  name: 'game_started',
@@ -92,7 +125,8 @@ async function bootstrap(): Promise<void> {
92
125
  preserveBrowserTouchGestures:
93
126
  document.body.dataset.mpgdPreserveBrowserTouchGestures === 'true',
94
127
  context: {
95
- platform,
128
+ platform: ownedRuntime.gateway,
129
+ gameRuntime: ownedRuntime,
96
130
  runtime,
97
131
  viewport,
98
132
  player,
@@ -105,6 +139,7 @@ async function bootstrap(): Promise<void> {
105
139
  },
106
140
  });
107
141
  } catch (error) {
142
+ gameRuntime?.dispose();
108
143
  disposeMicrosoftStorePwa?.();
109
144
  renderBootstrapError(error, locale);
110
145
  console.error('[bootstrap]', error);
@@ -5,8 +5,18 @@ import {
5
5
  resolveGameServicesTransport,
6
6
  type GameServicesRuntime,
7
7
  type GameServicesRuntimeMode,
8
+ type MonetizationOperationStore,
8
9
  } from '@mpgd/game-services';
9
10
  import type { PlatformGateway } from '@mpgd/platform';
11
+ import type { GameActionReconciliationPort } from '@mpgd/game-runtime/actions';
12
+ import type { PurchasePresentationEvent } from '@mpgd/game-runtime/ads';
13
+
14
+ /** Supply a game-owned durable encrypted journal and trusted recovery/native facts. */
15
+ export interface StarterMonetizationPorts {
16
+ readonly operationStore: MonetizationOperationStore;
17
+ readonly reconciliation?: GameActionReconciliationPort;
18
+ readonly purchasePresentation?: { subscribe(listener: (event: PurchasePresentationEvent) => void): () => void };
19
+ }
10
20
 
11
21
  export type StarterBackendMode = GameServicesRuntimeMode;
12
22
  export type StarterGameServices = GameServicesRuntime;
@@ -15,12 +25,14 @@ export function createStarterGameServices(input: {
15
25
  readonly gateway: PlatformGateway;
16
26
  readonly playerId: string;
17
27
  readonly configTarget: string;
28
+ readonly operationStore?: MonetizationOperationStore;
18
29
  readonly analytics?: AnalyticsSink;
19
30
  readonly analyticsSessionId?: string;
20
31
  }): StarterGameServices {
21
32
  return createGameServicesRuntime({
22
33
  gateway: input.gateway,
23
34
  playerId: input.playerId,
35
+ ...(input.operationStore === undefined ? {} : { operationStore: input.operationStore }),
24
36
  ...(input.analytics === undefined ? {} : { analytics: input.analytics }),
25
37
  ...(input.analyticsSessionId === undefined
26
38
  ? {}
@@ -1,3 +1,4 @@
1
+ import { bindGameAudio } from '@mpgd/game-runtime/audio';
1
2
  import Phaser from 'phaser';
2
3
 
3
4
  import { BootScene } from '../scenes/BootScene';
@@ -26,9 +27,35 @@ export function createStarterGame(input: {
26
27
  autoCenter: Phaser.Scale.CENTER_BOTH,
27
28
  },
28
29
  scene: [BootScene, LobbyScene, PlayScene],
30
+ callbacks: {
31
+ postBoot(game: Phaser.Game) {
32
+ game.registry.set(starterContextKey, input.context);
33
+ ownGameAudio(game, input.context);
34
+ },
35
+ },
29
36
  });
30
37
 
31
38
  game.registry.set(starterContextKey, input.context);
32
39
 
33
40
  return game;
34
41
  }
42
+
43
+ /** The sound manager and execution projection belong to the game, not a scene. */
44
+ function ownGameAudio(game: Phaser.Game, context: StarterContext): void {
45
+ const audio = bindGameAudio({
46
+ execution: context.gameRuntime.execution,
47
+ sink: {
48
+ getMuted: () => game.sound.mute,
49
+ setMuted: (muted) => {
50
+ game.sound.mute = muted;
51
+ },
52
+ },
53
+ onError: (error) => {
54
+ console.error('[game-audio]', error);
55
+ },
56
+ });
57
+ game.events.once('destroy', () => {
58
+ context.gameRuntime.dispose();
59
+ audio.dispose();
60
+ });
61
+ }
@@ -1,4 +1,5 @@
1
1
  import type { AnalyticsReporter, BufferedAnalyticsSink } from '@mpgd/analytics';
2
+ import type { GamePlatformRuntime } from '@mpgd/game-runtime/game';
2
3
  import type { Locale } from '@mpgd/i18n';
3
4
  import type { IdentitySession, LaunchIntent, PlayerIdentity } from '@mpgd/platform';
4
5
  import type {
@@ -12,6 +13,7 @@ import type { StarterGameServices } from '../platform/gameServices';
12
13
  export const starterContextKey = 'starterContext';
13
14
 
14
15
  export interface StarterContext {
16
+ readonly gameRuntime: GamePlatformRuntime<TargetConfiguredGateway, StarterGameServices>;
15
17
  readonly platform: TargetConfiguredGateway;
16
18
  readonly runtime: TargetRuntimeSnapshot;
17
19
  readonly viewport: TargetViewportSnapshot;
@@ -1,3 +1,5 @@
1
+ import type { GameActionController } from '@mpgd/game-runtime/actions';
2
+ import { bindPhaserGameScene } from '@mpgd/game-runtime/phaser';
1
3
  import Phaser from 'phaser';
2
4
 
3
5
  import type { LogicalAdPlacementId } from '@mpgd/platform';
@@ -17,6 +19,7 @@ export class PlayScene extends Phaser.Scene {
17
19
  private rewardText!: Phaser.GameObjects.Text;
18
20
  private analyticsText!: Phaser.GameObjects.Text;
19
21
  private context!: StarterContext;
22
+ private rewardedAction: GameActionController<'rewarded-ad'> | undefined;
20
23
  private lastScore = -1;
21
24
  private lastAnalyticsCount = -1;
22
25
 
@@ -26,6 +29,18 @@ export class PlayScene extends Phaser.Scene {
26
29
 
27
30
  create(): void {
28
31
  this.context = this.registry.get(starterContextKey) as StarterContext;
32
+ bindPhaserGameScene({
33
+ controller: this.context.gameRuntime.execution,
34
+ scene: this,
35
+ renderingPolicy: 'visibility',
36
+ audioOwner: 'game',
37
+ resetInput: () => {
38
+ this.input.keyboard?.resetKeys();
39
+ },
40
+ onUnsupportedState: (_snapshot, reason) => {
41
+ console.error('[game-execution]', reason);
42
+ },
43
+ });
29
44
 
30
45
  this.marker = this.add.image(480, 250, starterAssetKeys.marker).setDisplaySize(72, 72);
31
46
  this.scoreText = this.add
@@ -50,8 +65,16 @@ export class PlayScene extends Phaser.Scene {
50
65
  })
51
66
  .setOrigin(0.5);
52
67
 
53
- this.input.keyboard?.on('keydown-R', () => {
68
+ this.rewardedAction = this.context.gameRuntime.actions?.createRewardedAdController();
69
+ const action = this.rewardedAction;
70
+ const keyboard = this.input.keyboard;
71
+ const onReward = () => {
54
72
  void this.requestRewardedAd();
73
+ };
74
+ keyboard?.on('keydown-R', onReward);
75
+ this.events.once('shutdown', () => {
76
+ action?.dispose();
77
+ keyboard?.off('keydown-R', onReward);
55
78
  });
56
79
  }
57
80
 
@@ -77,7 +100,7 @@ export class PlayScene extends Phaser.Scene {
77
100
  private rewardHint(): string {
78
101
  const rewardedAds = this.context.runtime.features.rewardedAds;
79
102
 
80
- if (!rewardedAds.enabled || this.context.gameServices.client === undefined) {
103
+ if (!rewardedAds.enabled || this.context.gameServices.client === undefined || this.context.gameServices.monetizationRecovery === undefined) {
81
104
  return t(this.context.locale, 'rewardUnavailable');
82
105
  }
83
106
 
@@ -86,19 +109,25 @@ export class PlayScene extends Phaser.Scene {
86
109
 
87
110
  private async requestRewardedAd(): Promise<void> {
88
111
  const rewardedAds = this.context.runtime.features.rewardedAds;
89
- const gameServicesClient = this.context.gameServices.client;
112
+ const action = this.rewardedAction;
90
113
 
91
- if (!rewardedAds.enabled || gameServicesClient === undefined) {
114
+ if (!rewardedAds.enabled || action === undefined || this.context.gameServices.monetizationRecovery === undefined) {
92
115
  this.rewardText.setText(t(this.context.locale, 'rewardUnavailable'));
93
116
  return;
94
117
  }
95
118
 
119
+ if (this.context.gameRuntime.actions?.getAvailability() !== 'ready') {
120
+ return;
121
+ }
96
122
  try {
97
- const result = await gameServicesClient.claimRewardedAd({
123
+ const result = await action.execute({
98
124
  placementId: rewardedPlacementId,
99
125
  idempotencyKey: createClientId('starter-reward'),
100
126
  });
101
127
 
128
+ if (action.isDisposed() || this.rewardedAction !== action) {
129
+ return;
130
+ }
102
131
  this.rewardText.setText(
103
132
  result.claim?.granted === true
104
133
  ? t(this.context.locale, 'reward', { status: result.status })
@@ -112,7 +141,9 @@ export class PlayScene extends Phaser.Scene {
112
141
  granted: false,
113
142
  },
114
143
  });
115
- this.rewardText.setText(t(this.context.locale, 'rewardError'));
144
+ if (!action.isDisposed() && this.rewardedAction === action) {
145
+ this.rewardText.setText(t(this.context.locale, 'rewardError'));
146
+ }
116
147
  console.error('[rewarded-ad]', error);
117
148
  }
118
149
  }