@mpgd/game-runtime 0.2.5 → 0.3.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/README.md +111 -0
- package/dist/actions/index.d.ts +5 -0
- package/dist/actions/index.js +37 -3
- package/dist/ads/index.d.ts +63 -0
- package/dist/ads/index.js +625 -0
- package/dist/audio/index.d.ts +14 -0
- package/dist/audio/index.js +35 -0
- package/dist/game/index.d.ts +41 -0
- package/dist/game/index.js +103 -0
- package/dist/phaser/index.d.ts +2 -0
- package/dist/phaser/index.js +4 -1
- package/dist/presentation/index.d.ts +40 -0
- package/dist/presentation/index.js +120 -0
- package/docs/phaser.md +11 -0
- package/package.json +19 -2
package/README.md
CHANGED
|
@@ -73,6 +73,10 @@ pnpm add phaser@^4.2.0
|
|
|
73
73
|
| `@mpgd/game-runtime/ui` | Scoped snapshots, commands and events |
|
|
74
74
|
| `@mpgd/game-runtime/platform` | Injected lifecycle source binding |
|
|
75
75
|
| `@mpgd/game-runtime/actions` | Purchase and rewarded-ad coordination |
|
|
76
|
+
| `@mpgd/game-runtime/presentation` | Shared native full-screen ownership |
|
|
77
|
+
| `@mpgd/game-runtime/game` | Game-owned gateway, service, lifecycle and action assembly |
|
|
78
|
+
| `@mpgd/game-runtime/audio` | Game-owned audio projection |
|
|
79
|
+
| `@mpgd/game-runtime/ads` | Coordinated provider and gateway, injected deadlines and policy |
|
|
76
80
|
| `@mpgd/game-runtime/phaser` | Optional gameplay scene binding |
|
|
77
81
|
|
|
78
82
|
Phaser is an optional peer dependency. The root and headless subpaths do not
|
|
@@ -96,6 +100,56 @@ and `node packages/game-runtime/test/dist-import.mjs`. The package test also
|
|
|
96
100
|
installs packed tarballs in an isolated consumer, checks headless declarations
|
|
97
101
|
without DOM types, and checks the optional binding with Phaser declarations.
|
|
98
102
|
|
|
103
|
+
## Native presentation and financial settlement
|
|
104
|
+
|
|
105
|
+
Create one `createFullScreenPresentationScope({ execution })` above all screens.
|
|
106
|
+
Pass it to `createCoordinatedPlatformGateway` with the original gateway and its
|
|
107
|
+
v2 ad provider. Use the returned gateway for both direct platform calls and the
|
|
108
|
+
game-services client; pass the same scope and execution controller to
|
|
109
|
+
`createGameActionCoordinator`. The coordinator rejects a mismatched controller.
|
|
110
|
+
|
|
111
|
+
The scope owns simulation, gameplay input, and audio blocks only while native UI
|
|
112
|
+
is open or uncertain. Native closure releases that invocation's blocks even
|
|
113
|
+
while the game-services client is waiting for a backend claim. Settings and
|
|
114
|
+
background blocks remain independently owned. Financial operations retain their
|
|
115
|
+
existing duplicate, pending, and reconciliation guards.
|
|
116
|
+
|
|
117
|
+
An injected advertising deadline ends caller waiting and returns an uncertain
|
|
118
|
+
result. It never cancels the native SDK or proves closure. Native close and late
|
|
119
|
+
reward observers survive view disposal while needed. `onClaimEvidence` reports
|
|
120
|
+
late candidates under the original invocation and idempotency key for an
|
|
121
|
+
application-owned claim journal; it cannot grant rewards. Use a durable
|
|
122
|
+
game-services operation store when recovery must survive process restart.
|
|
123
|
+
`claimEvidence` carries a server proof-lookup correlation without asserting local
|
|
124
|
+
SDK eligibility. Registered server verification and its ledger remain authoritative.
|
|
125
|
+
|
|
126
|
+
Use `createAdClaimEvidenceRecoveryObserver({ recovery, onResult })` to connect
|
|
127
|
+
`onClaimEvidence` to an application-owned recoverable monetization client. Bind
|
|
128
|
+
the handler during bootstrap before enabling SDK entrypoints. The observer
|
|
129
|
+
always forwards an ungranted pending candidate under the original idempotency
|
|
130
|
+
key, which may differ from the display invocation ID. Recovery serializes with
|
|
131
|
+
the original operation, attaches a newly registered lookup envelope to a pending
|
|
132
|
+
journal, and retries its recorded verification request without another SDK call.
|
|
133
|
+
Previously recorded evidence and the first journal timestamp remain fixed.
|
|
134
|
+
An eligibility change can prompt another lookup under the same identity.
|
|
135
|
+
`onResult` observes backend settlement; refresh authoritative state or reconcile
|
|
136
|
+
the action coordinator rather than adding rewards directly from SDK callbacks.
|
|
137
|
+
|
|
138
|
+
Purchase business results do not prove that native UI closed. Without a trusted
|
|
139
|
+
`classifyPurchasePresentation` function or a native `purchasePresentation`
|
|
140
|
+
event source, the wrapper keeps presentation unknown. Wire actual native close
|
|
141
|
+
facts before enabling the managed purchase path. Events are scoped to the
|
|
142
|
+
original idempotency key and a monotonic sequence; late or foreign events cannot
|
|
143
|
+
release another invocation. Disposal rejects new calls and retains uncertain
|
|
144
|
+
native close observation. Lightweight key history prevents retired purchase
|
|
145
|
+
keys from reopening the SDK; only active/uncertain results and the last settled
|
|
146
|
+
result are retained.
|
|
147
|
+
|
|
148
|
+
`canShow` is an injected placement policy. No target-specific branch belongs in
|
|
149
|
+
the game flow. Availability and preparation gate SDK calls; a shared busy surface
|
|
150
|
+
blocks purchases, rewarded ads, and interstitials together. The adapter-level
|
|
151
|
+
SDK conformance suites certify each provider separately from these runtime tests.
|
|
152
|
+
|
|
99
153
|
## Scoped UI bridge
|
|
100
154
|
|
|
101
155
|
`@mpgd/game-runtime/ui` is a separate, headless entrypoint. Importing the root
|
|
@@ -360,3 +414,60 @@ A reentrant same-key joiner cannot cancel that owner's startup by disposing itse
|
|
|
360
414
|
If owner/runtime disposal prevents any client invocation, the flight rejects with
|
|
361
415
|
a scheduling error, resets its observed state to `idle`, and emits no business
|
|
362
416
|
completion/exception event. An invoked client failure remains `exception`.
|
|
417
|
+
|
|
418
|
+
## Game-owned platform assembly
|
|
419
|
+
|
|
420
|
+
Create one `createGamePlatformRuntime` from `@mpgd/game-runtime/game` during
|
|
421
|
+
bootstrap. Pass the original gateway, an explicit initial lifecycle state,
|
|
422
|
+
a game policy, optional injected wait deadline, and `createServices(gateway)`.
|
|
423
|
+
The services factory must use the supplied coordinated gateway. Its structural
|
|
424
|
+
ports keep this subpath independent of DOM, Phaser, network and schema imports,
|
|
425
|
+
while preserving target-specific gateway extensions and concrete service types.
|
|
426
|
+
|
|
427
|
+
A versioned provider installs one shared scope for purchases, rewarded ads and
|
|
428
|
+
interstitials. Legacy gateways retain their existing surface and coarse action
|
|
429
|
+
blocking. V2 rewarded display is disabled until the services expose
|
|
430
|
+
`monetizationRecovery`: the application must supply a durable, atomic,
|
|
431
|
+
encrypted operation store rather than a memory fallback. Startup and resume
|
|
432
|
+
reconcile existing records without opening SDK UI. Account changes require a
|
|
433
|
+
new client/runtime bound to that account.
|
|
434
|
+
|
|
435
|
+
Keep the runtime above scenes. Create scene-owned action controllers from
|
|
436
|
+
`runtime.actions`; dispose those controllers and their input listeners on
|
|
437
|
+
scene shutdown. After an await, check the captured controller's disposal state
|
|
438
|
+
before touching scene objects. A trusted `reconciliation` port can unlock new
|
|
439
|
+
financial keys only after reading the matching authoritative ledger grant.
|
|
440
|
+
The original key/history remains retired and never becomes an SDK retry.
|
|
441
|
+
|
|
442
|
+
Late registered candidates reach `recoverRewardResult` under the original
|
|
443
|
+
operation idempotency key. `onLateResult` also exposes provider settlement
|
|
444
|
+
that follows a caller deadline. The game assembly journals a definitive
|
|
445
|
+
not-started/non-earned result only when no prior candidate was observed;
|
|
446
|
+
previous proof/request identity is never replaced by a negative callback.
|
|
447
|
+
Native closure and business settlement remain independent, including during
|
|
448
|
+
runtime teardown. Purchase results still require native `purchasePresentation`
|
|
449
|
+
facts to prove physical closure; a business success alone remains unknown.
|
|
450
|
+
|
|
451
|
+
Bind `bindGameAudio({ execution, sink })` once to the game's sound manager.
|
|
452
|
+
Scenes use `bindPhaserGameScene({ audioOwner: 'game', ... })` and never supply
|
|
453
|
+
a competing scene audio sink. Scene shutdown therefore cannot unmute a live
|
|
454
|
+
native presentation or release background/settings ownership. Disposing an
|
|
455
|
+
audio projection while blocked leaves mute in place. Destroy execution on game
|
|
456
|
+
teardown before detaching the audio projection. Express additional user mute
|
|
457
|
+
ownership with an audio execution block.
|
|
458
|
+
|
|
459
|
+
The AIT SDK/host/proxy integration test combines this assembly with the real
|
|
460
|
+
client recovery, registered independent verifier and replay-safe ledger. It
|
|
461
|
+
proves late evidence after view disposal, pending authority with zero grants,
|
|
462
|
+
authoritative settlement, immutable request/timestamp reuse, proof replay
|
|
463
|
+
rejection and reconstructed-client recovery with one SDK show. Its memory
|
|
464
|
+
journal/ledger are test fixtures, not production persistence or live Toss
|
|
465
|
+
certification.
|
|
466
|
+
|
|
467
|
+
`actions.confirmRecoveredRewardResult(key, result)` accepts only a trusted
|
|
468
|
+
journal recovery's confirmed non-grant result for a matching rewarded action.
|
|
469
|
+
It can retire a late SDK preparation failure or a definitive server rejection,
|
|
470
|
+
while preserving original key history and any live native lease. Foreign keys,
|
|
471
|
+
purchase actions, pending results and grant-shaped results cannot be unlocked
|
|
472
|
+
through this path. Positive grants continue through the authoritative ledger
|
|
473
|
+
reconciliation port. This method never updates a disposed view or opens UI.
|
package/dist/actions/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { GameServicesOperationClient, GameServicesPurchaseInput, GameServicesPurchaseProgress, GameServicesPurchaseResult, GameServicesRewardedAdInput, GameServicesRewardedAdProgress, GameServicesRewardedAdResult } from '@mpgd/game-services/operations';
|
|
2
2
|
import type { GameExecutionController } from '../index.js';
|
|
3
3
|
import { type ObserverErrorHandler } from '../observers.js';
|
|
4
|
+
import type { FullScreenPresentationScope } from '../presentation/index.js';
|
|
4
5
|
import { type GameUiScope, type UiListener } from '../ui/index.js';
|
|
5
6
|
export type GameActionKind = 'purchase' | 'rewarded-ad';
|
|
6
7
|
interface Inputs {
|
|
@@ -93,6 +94,8 @@ export interface GameActionCoordinator {
|
|
|
93
94
|
createPurchaseController(): GameActionController<'purchase'>;
|
|
94
95
|
createRewardedAdController(): GameActionController<'rewarded-ad'>;
|
|
95
96
|
getPendingOperation(): GameActionPendingOperation | undefined;
|
|
97
|
+
/** Trusted journal recovery only. Retire a matching non-grant reward without reopening SDK UI. */
|
|
98
|
+
confirmRecoveredRewardResult(idempotencyKey: string, result: GameServicesRewardedAdResult): boolean;
|
|
96
99
|
/** Concurrent callers join one recovery query; only a matching committed grant unlocks new keys. */
|
|
97
100
|
reconcile(): Promise<GameActionReconciliationResult>;
|
|
98
101
|
getAvailability(): 'ready' | 'busy' | 'reconciliation-required' | 'history-full' | 'disposed';
|
|
@@ -103,6 +106,8 @@ export interface GameActionCoordinator {
|
|
|
103
106
|
export declare function createGameActionCoordinator(options: {
|
|
104
107
|
readonly execution: GameExecutionController;
|
|
105
108
|
readonly client: Pick<GameServicesOperationClient, 'purchase' | 'claimRewardedAd'>;
|
|
109
|
+
/** Use only with a client backed by createCoordinatedPlatformGateway sharing this scope/execution. */
|
|
110
|
+
readonly presentation?: FullScreenPresentationScope;
|
|
106
111
|
/** Never evicts keys: once full, new keys are rejected until application teardown. Default 1024. */
|
|
107
112
|
readonly maxRememberedKeys?: number;
|
|
108
113
|
readonly reconciliation?: GameActionReconciliationPort;
|
package/dist/actions/index.js
CHANGED
|
@@ -18,6 +18,9 @@ const purchaseSources = {
|
|
|
18
18
|
/** Bind one coordinator to one runtime/client (including its player identity), above all screens. */
|
|
19
19
|
export function createGameActionCoordinator(options) {
|
|
20
20
|
const { execution, client, onObserverError } = options;
|
|
21
|
+
if (options.presentation !== undefined && options.presentation.execution !== execution) {
|
|
22
|
+
throw new TypeError('Action and presentation coordination must share one execution controller.');
|
|
23
|
+
}
|
|
21
24
|
const capacity = options.maxRememberedKeys ?? 1024;
|
|
22
25
|
const recoveryPlayerId = options.reconciliation?.playerId;
|
|
23
26
|
const recoveryPort = options.reconciliation;
|
|
@@ -34,9 +37,11 @@ export function createGameActionCoordinator(options) {
|
|
|
34
37
|
let nextId = 0;
|
|
35
38
|
let disposed = false;
|
|
36
39
|
let unconfirmed;
|
|
40
|
+
const recoveredNonGrants = new Set();
|
|
37
41
|
let recoveryFlight;
|
|
38
42
|
function isDisposed() {
|
|
39
|
-
return disposed || execution.getSnapshot().status === 'destroyed'
|
|
43
|
+
return disposed || execution.getSnapshot().status === 'destroyed'
|
|
44
|
+
|| options.presentation?.getSnapshot().status === 'disposed';
|
|
40
45
|
}
|
|
41
46
|
function reserve(kind, supplied, canStart) {
|
|
42
47
|
if (isDisposed()) {
|
|
@@ -78,6 +83,9 @@ export function createGameActionCoordinator(options) {
|
|
|
78
83
|
if (current !== undefined) {
|
|
79
84
|
throw new GameActionExecutionError('busy');
|
|
80
85
|
}
|
|
86
|
+
if (options.presentation?.getSnapshot().owner !== undefined) {
|
|
87
|
+
throw new GameActionExecutionError('busy');
|
|
88
|
+
}
|
|
81
89
|
if (history.size >= capacity) {
|
|
82
90
|
throw new GameActionExecutionError('history-full');
|
|
83
91
|
}
|
|
@@ -99,9 +107,10 @@ export function createGameActionCoordinator(options) {
|
|
|
99
107
|
let invoked = false;
|
|
100
108
|
function finish(status) {
|
|
101
109
|
flight.settled = true;
|
|
102
|
-
if (invoked && (status === 'pending' || status === 'exception')) {
|
|
110
|
+
if (invoked && (status === 'pending' || status === 'exception') && !(kind === 'rewarded-ad' && recoveredNonGrants.has(key))) {
|
|
103
111
|
unconfirmed = Object.freeze({ kind, operationId: id, input });
|
|
104
112
|
}
|
|
113
|
+
recoveredNonGrants.delete(key);
|
|
105
114
|
current = undefined;
|
|
106
115
|
if (invoked) {
|
|
107
116
|
last = flight;
|
|
@@ -163,7 +172,9 @@ export function createGameActionCoordinator(options) {
|
|
|
163
172
|
if (isDisposed() || !canStart()) {
|
|
164
173
|
throw new GameActionExecutionError('disposed');
|
|
165
174
|
}
|
|
166
|
-
|
|
175
|
+
if (options.presentation === undefined) {
|
|
176
|
+
block = execution.acquireBlock({ reason: `action:${kind}`, channels: ['simulation', 'gameplay-input'] });
|
|
177
|
+
}
|
|
167
178
|
if (isDisposed() || !canStart()) {
|
|
168
179
|
throw new GameActionExecutionError('disposed');
|
|
169
180
|
}
|
|
@@ -351,6 +362,26 @@ export function createGameActionCoordinator(options) {
|
|
|
351
362
|
return Object.freeze({
|
|
352
363
|
createPurchaseController: () => makeController('purchase'),
|
|
353
364
|
getPendingOperation: () => unconfirmed,
|
|
365
|
+
confirmRecoveredRewardResult(idempotencyKey, result) {
|
|
366
|
+
if (isDisposed()) {
|
|
367
|
+
throw new GameActionExecutionError('disposed');
|
|
368
|
+
}
|
|
369
|
+
const rejected = result.status === 'rejected' && result.claim?.granted === false && result.claim.disposition === 'rejected';
|
|
370
|
+
const noCandidate = ['failed', 'skipped', 'unavailable'].includes(result.status) && result.reward.status === result.status
|
|
371
|
+
&& result.reward.rewardGranted === false && result.reward.evidence === undefined && result.reward.ledgerEntryId === undefined && result.claim === undefined;
|
|
372
|
+
if ((!rejected && !noCandidate) || result.ledgerEntryId !== undefined || result.claim?.ledgerEntryId !== undefined || result.reward.ledgerEntryId !== undefined || result.reward.rewardGranted !== false) {
|
|
373
|
+
throw new GameActionExecutionError('invalid-reconciliation');
|
|
374
|
+
}
|
|
375
|
+
if (unconfirmed?.kind === 'rewarded-ad' && unconfirmed.input.idempotencyKey === idempotencyKey) {
|
|
376
|
+
unconfirmed = undefined;
|
|
377
|
+
return true;
|
|
378
|
+
}
|
|
379
|
+
if (current !== undefined && current.key === idempotencyKey && current.bridge.getSnapshot().kind === 'rewarded-ad' && !current.settled) {
|
|
380
|
+
recoveredNonGrants.add(idempotencyKey);
|
|
381
|
+
return true;
|
|
382
|
+
}
|
|
383
|
+
return false;
|
|
384
|
+
},
|
|
354
385
|
reconcile,
|
|
355
386
|
createRewardedAdController: () => makeController('rewarded-ad'),
|
|
356
387
|
getAvailability() {
|
|
@@ -360,6 +391,9 @@ export function createGameActionCoordinator(options) {
|
|
|
360
391
|
if (current !== undefined) {
|
|
361
392
|
return 'busy';
|
|
362
393
|
}
|
|
394
|
+
if (options.presentation?.getSnapshot().owner !== undefined) {
|
|
395
|
+
return 'busy';
|
|
396
|
+
}
|
|
363
397
|
if (unconfirmed !== undefined) {
|
|
364
398
|
return 'reconciliation-required';
|
|
365
399
|
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { PlatformEvidenceEnvelope, PlatformGateway, PurchaseResult } from '@mpgd/platform';
|
|
2
|
+
import type { GameServicesRewardedAdResult } from '@mpgd/game-services/operations';
|
|
3
|
+
import { type AdPlacementInput, type AdProvider, type AdShowInput, type AdShowResult } from '@mpgd/platform/ads';
|
|
4
|
+
import { type ObserverErrorHandler } from '../observers.js';
|
|
5
|
+
import { type FullScreenPresentationScope } from '../presentation/index.js';
|
|
6
|
+
/** Scheduling is injected so the runtime remains usable without DOM or Node globals. */
|
|
7
|
+
export interface AdWaitDeadline {
|
|
8
|
+
readonly milliseconds: number;
|
|
9
|
+
schedule(callback: () => void, milliseconds: number): () => void;
|
|
10
|
+
}
|
|
11
|
+
export interface AdClaimEvidenceObservation {
|
|
12
|
+
readonly input: AdShowInput;
|
|
13
|
+
readonly evidence: PlatformEvidenceEnvelope;
|
|
14
|
+
readonly eligibility: 'eligible' | 'unknown';
|
|
15
|
+
}
|
|
16
|
+
/** Connect late SDK candidates to the reserved journal operation, without reopening UI. */
|
|
17
|
+
export declare function createAdClaimEvidenceRecoveryObserver(input: {
|
|
18
|
+
readonly recovery: {
|
|
19
|
+
recoverRewardResult(idempotencyKey: string, reward: {
|
|
20
|
+
readonly status: 'pending';
|
|
21
|
+
readonly rewardGranted: false;
|
|
22
|
+
readonly evidence: PlatformEvidenceEnvelope;
|
|
23
|
+
}): Promise<GameServicesRewardedAdResult>;
|
|
24
|
+
};
|
|
25
|
+
/** Application-owned settlement observation, never a view-scoped SDK grant callback. */
|
|
26
|
+
readonly onResult?: (request: AdShowInput, result: GameServicesRewardedAdResult) => void | Promise<void>;
|
|
27
|
+
}): (observation: AdClaimEvidenceObservation) => Promise<void>;
|
|
28
|
+
export interface CoordinatedAdProvider extends AdProvider {
|
|
29
|
+
/** Detach projections and reject new calls; native terminal/reward observers remain while needed. */
|
|
30
|
+
dispose(): void;
|
|
31
|
+
}
|
|
32
|
+
export declare function createCoordinatedAdProvider(input: {
|
|
33
|
+
readonly provider: AdProvider;
|
|
34
|
+
readonly presentation: FullScreenPresentationScope;
|
|
35
|
+
readonly canShow?: (placement: AdPlacementInput) => boolean;
|
|
36
|
+
readonly deadline?: AdWaitDeadline;
|
|
37
|
+
readonly maxRememberedInvocations?: number;
|
|
38
|
+
readonly onObserverError?: ObserverErrorHandler;
|
|
39
|
+
/** Application-owned claim/journal observer, never a grant callback. Survives view disposal. */
|
|
40
|
+
readonly onClaimEvidence?: (observation: AdClaimEvidenceObservation) => void | Promise<void>;
|
|
41
|
+
/** Application-owned provider settlement after a caller deadline; never a UI callback. */
|
|
42
|
+
readonly onLateResult?: (observation: {
|
|
43
|
+
readonly input: AdShowInput;
|
|
44
|
+
readonly result: AdShowResult;
|
|
45
|
+
}) => void | Promise<void>;
|
|
46
|
+
}): CoordinatedAdProvider;
|
|
47
|
+
export interface PurchasePresentationEvent {
|
|
48
|
+
readonly idempotencyKey: string;
|
|
49
|
+
readonly sequence: number;
|
|
50
|
+
readonly state: 'open' | 'closed' | 'not-started' | 'unknown';
|
|
51
|
+
}
|
|
52
|
+
export interface CoordinatedPlatformGateway extends PlatformGateway {
|
|
53
|
+
dispose(): void;
|
|
54
|
+
}
|
|
55
|
+
/** Games receive this gateway so direct gateway calls use the same native surface. */
|
|
56
|
+
export declare function createCoordinatedPlatformGateway(input: Parameters<typeof createCoordinatedAdProvider>[0] & {
|
|
57
|
+
readonly gateway: PlatformGateway;
|
|
58
|
+
/** Trusted native facts only. A purchase business result alone defaults to unknown. */
|
|
59
|
+
readonly classifyPurchasePresentation?: (result: PurchaseResult) => 'closed' | 'not-started' | 'unknown';
|
|
60
|
+
readonly purchasePresentation?: {
|
|
61
|
+
subscribe(listener: (event: PurchasePresentationEvent) => void): () => void;
|
|
62
|
+
};
|
|
63
|
+
}): CoordinatedPlatformGateway;
|