@mpgd/cli 0.64.0 → 0.65.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/native-build-info.json +2 -2
- package/dist/native-build-target.js +92 -80
- package/package.json +19 -19
- package/templates/lite-overlay/README.md +4 -0
- package/templates/lite-overlay/apps/target-devvit/README.md +10 -0
- package/templates/phaser-game/README.md +32 -3
- package/templates/phaser-game/apps/target-ait/package.json +2 -2
- package/templates/phaser-game/apps/target-cloudflare-pages/package.json +2 -2
- package/templates/phaser-game/apps/target-devvit/README.md +10 -0
- package/templates/phaser-game/apps/target-devvit/package.json +2 -2
- package/templates/phaser-game/package.json +2 -2
- package/templates/phaser-game/src/i18n/messages.ts +3 -0
- package/templates/phaser-game/src/main.ts +42 -0
- package/templates/phaser-game/src/platform/buildGateways/capacitorAndroid.ts +5 -0
- package/templates/phaser-game/src/platform/buildGateways/capacitorIos.ts +5 -0
- package/templates/phaser-game/src/platform/commerceAccountBinding.ts +81 -1
- package/templates/phaser-game/src/platform/gameServices.ts +88 -4
- package/templates/phaser-game/src/platform/guestIdentity.ts +4 -3
- package/templates/phaser-game/src/platform/sessionGatedServices.ts +149 -21
- package/templates/phaser-game/src/styles.css +12 -0
|
@@ -1,20 +1,22 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
import {
|
|
2
|
+
gameServicesPlayerChangedCode,
|
|
3
|
+
gameServicesRuntimeDisposedCode,
|
|
4
|
+
type GameServicesClient,
|
|
5
|
+
type GameServicesPurchaseResult,
|
|
6
|
+
type GameServicesRewardedAdResult,
|
|
7
|
+
type GameServicesRuntime,
|
|
6
8
|
} from '@mpgd/game-services';
|
|
7
9
|
|
|
8
10
|
/** The diagnostic of a purchase refused because the run is not signed in yet (retryable). */
|
|
9
11
|
export const starterSessionRequiredCode = 'STARTER_SESSION_REQUIRED';
|
|
10
12
|
|
|
11
|
-
function
|
|
13
|
+
function refusedPurchase(code: string): GameServicesPurchaseResult {
|
|
12
14
|
return {
|
|
13
15
|
status: 'failed',
|
|
14
16
|
purchase: {
|
|
15
17
|
status: 'failed',
|
|
16
18
|
entitlementIds: [],
|
|
17
|
-
diagnostic: { code
|
|
19
|
+
diagnostic: { code, retryable: true },
|
|
18
20
|
},
|
|
19
21
|
};
|
|
20
22
|
}
|
|
@@ -25,11 +27,16 @@ function sessionRequiredReward(): GameServicesRewardedAdResult {
|
|
|
25
27
|
|
|
26
28
|
/** A purchase, restore, or reward claim was asked for before the guest sign-in succeeded. */
|
|
27
29
|
export class StarterSessionRequiredError extends Error {
|
|
28
|
-
readonly code
|
|
30
|
+
readonly code: string;
|
|
29
31
|
|
|
30
|
-
constructor() {
|
|
31
|
-
super(
|
|
32
|
+
constructor(code: string = starterSessionRequiredCode) {
|
|
33
|
+
super(
|
|
34
|
+
code === starterSessionRequiredCode
|
|
35
|
+
? 'The game is not signed in to its identity service yet; try again when it is online.'
|
|
36
|
+
: 'Another player signed in; the game restarts for that player.',
|
|
37
|
+
);
|
|
32
38
|
this.name = 'StarterSessionRequiredError';
|
|
39
|
+
this.code = code;
|
|
33
40
|
}
|
|
34
41
|
}
|
|
35
42
|
|
|
@@ -69,8 +76,14 @@ export type StarterSessionGatedServices = GameServicesRuntime & {
|
|
|
69
76
|
* runtime binds its late reward evidence recovery at boot.
|
|
70
77
|
* - When the player becomes known, the services are created once; the
|
|
71
78
|
* caller runs the game runtime's `reconcile()` from `onActivated` (the
|
|
72
|
-
* startup recovery, with the action coordinator's). A later sign-in naming
|
|
73
|
-
* (not expected within one run)
|
|
79
|
+
* startup recovery, with the action coordinator's). A later sign-in naming
|
|
80
|
+
* another player (not expected within one run) closes them for good, as
|
|
81
|
+
* the services' own session check does: purchases answer `failed` with the
|
|
82
|
+
* retryable diagnostic `GAME_SERVICES_PLAYER_CHANGED`, recovery and Delete
|
|
83
|
+
* account go away, and `onPlayerChanged` asks the host to boot again for
|
|
84
|
+
* that player once the old services stopped. They are never re-created
|
|
85
|
+
* for the new player inside this run: the scenes, the action coordinator,
|
|
86
|
+
* and the notifications still hold the boot player.
|
|
74
87
|
* - `accountDeletion` is `undefined` until then, and the services' after
|
|
75
88
|
* (read live); `onReady` tells Delete account to appear then.
|
|
76
89
|
* - `ensureSession` is present only when the configuration has a client
|
|
@@ -90,19 +103,39 @@ export function createSessionGatedGameServices(input: {
|
|
|
90
103
|
* one a purchase or restore started): run the game runtime's `reconcile()`.
|
|
91
104
|
*/
|
|
92
105
|
readonly onActivated?: () => void;
|
|
106
|
+
/** Called at once when a later sign-in names another player: block input or show a switching screen. */
|
|
107
|
+
readonly onPlayerChanging?: (playerId: string) => void;
|
|
108
|
+
/** Called once, after a sign-in named another player closed the services: boot again for it. */
|
|
109
|
+
readonly onPlayerChanged?: (playerId: string) => void;
|
|
93
110
|
readonly warn?: (message: string, error?: unknown) => void;
|
|
94
111
|
}): StarterSessionGatedServices {
|
|
95
112
|
const warn = input.warn ?? ((message: string, error?: unknown) => console.warn(message, error ?? ''));
|
|
96
113
|
let inner: GameServicesRuntime | undefined;
|
|
97
114
|
let innerPlayer: string | undefined;
|
|
115
|
+
/** Set once another player signed in, or the services were disposed: nothing comes up again. */
|
|
116
|
+
let closedCode: string | undefined;
|
|
98
117
|
const readyListeners = new Set<() => void>();
|
|
118
|
+
/** The services the scenes may use now: none once closed. */
|
|
119
|
+
const live = (): GameServicesRuntime | undefined => (closedCode === undefined
|
|
120
|
+
? inner
|
|
121
|
+
: undefined);
|
|
99
122
|
|
|
100
123
|
const activate = (playerId: string): void => {
|
|
124
|
+
if (closedCode !== undefined) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
101
127
|
if (inner !== undefined) {
|
|
102
128
|
if (playerId !== innerPlayer) {
|
|
129
|
+
closedCode = gameServicesPlayerChangedCode;
|
|
130
|
+
readyListeners.clear();
|
|
103
131
|
warn(
|
|
104
|
-
'[game-services] a later sign-in named another player; the services
|
|
132
|
+
'[game-services] a later sign-in named another player; the services closed until restart.',
|
|
105
133
|
);
|
|
134
|
+
notifyChanging(input.onPlayerChanging, playerId, warn);
|
|
135
|
+
// Fail closed first; the host restarts once the old reconciler has stopped.
|
|
136
|
+
void inner.dispose().catch((error: unknown) => {
|
|
137
|
+
warn('[game-services] the services could not be closed cleanly', error);
|
|
138
|
+
}).finally(() => input.onPlayerChanged?.(playerId));
|
|
106
139
|
}
|
|
107
140
|
return;
|
|
108
141
|
}
|
|
@@ -132,16 +165,17 @@ export function createSessionGatedGameServices(input: {
|
|
|
132
165
|
input.onPlayer(activate);
|
|
133
166
|
|
|
134
167
|
const ready = async (): Promise<GameServicesRuntime> => {
|
|
135
|
-
if (inner === undefined) {
|
|
168
|
+
if (inner === undefined && closedCode === undefined) {
|
|
136
169
|
const playerId = await input.ensurePlayer();
|
|
137
170
|
if (playerId !== undefined) {
|
|
138
171
|
activate(playerId);
|
|
139
172
|
}
|
|
140
173
|
}
|
|
141
|
-
|
|
142
|
-
|
|
174
|
+
const services = live();
|
|
175
|
+
if (services === undefined) {
|
|
176
|
+
throw new StarterSessionRequiredError(closedCode);
|
|
143
177
|
}
|
|
144
|
-
return
|
|
178
|
+
return services;
|
|
145
179
|
};
|
|
146
180
|
const readyClientOrUndefined = async (): Promise<GameServicesClient | undefined> => {
|
|
147
181
|
try {
|
|
@@ -166,7 +200,9 @@ export function createSessionGatedGameServices(input: {
|
|
|
166
200
|
client = {
|
|
167
201
|
purchase: async (purchase, options) => {
|
|
168
202
|
const ready = await readyClientOrUndefined();
|
|
169
|
-
return ready === undefined
|
|
203
|
+
return ready === undefined
|
|
204
|
+
? refusedPurchase(closedCode ?? starterSessionRequiredCode)
|
|
205
|
+
: ready.purchase(purchase, options);
|
|
170
206
|
},
|
|
171
207
|
claimRewardedAd: async (reward, options) => {
|
|
172
208
|
const ready = await readyClientOrUndefined();
|
|
@@ -177,7 +213,7 @@ export function createSessionGatedGameServices(input: {
|
|
|
177
213
|
// Read live: the reconciler of the services created after sign-in.
|
|
178
214
|
Object.defineProperty(client, 'commerceReconciler', {
|
|
179
215
|
enumerable: true,
|
|
180
|
-
get: () =>
|
|
216
|
+
get: () => live()?.client?.commerceReconciler,
|
|
181
217
|
});
|
|
182
218
|
}
|
|
183
219
|
|
|
@@ -205,13 +241,105 @@ export function createSessionGatedGameServices(input: {
|
|
|
205
241
|
}
|
|
206
242
|
},
|
|
207
243
|
}),
|
|
244
|
+
async dispose() {
|
|
245
|
+
closedCode ??= gameServicesRuntimeDisposedCode;
|
|
246
|
+
readyListeners.clear();
|
|
247
|
+
await inner?.dispose();
|
|
248
|
+
},
|
|
208
249
|
} as StarterSessionGatedServices;
|
|
209
250
|
// Read live: `undefined` until the player is known, so rewarded ads stay off until then,
|
|
210
251
|
// and Delete account appears once there is an account (the lobby's next visit).
|
|
211
252
|
Object.defineProperties(services, {
|
|
212
253
|
mode: { enumerable: true, get: () => inner?.mode ?? input.probe.mode },
|
|
213
|
-
monetizationRecovery: { enumerable: true, get: () =>
|
|
214
|
-
accountDeletion: { enumerable: true, get: () =>
|
|
254
|
+
monetizationRecovery: { enumerable: true, get: () => live()?.monetizationRecovery },
|
|
255
|
+
accountDeletion: { enumerable: true, get: () => live()?.accountDeletion },
|
|
215
256
|
});
|
|
216
257
|
return services;
|
|
217
258
|
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Closes `services` once the server session names another player than
|
|
262
|
+
* `playerId` (an account switch within the run): `onPlayerChanging` hears it
|
|
263
|
+
* at once (the runtime's own `sessionPlayerId` check already refuses every
|
|
264
|
+
* request from that moment; block input or show a switching screen), then
|
|
265
|
+
* the services are disposed (an open store sheet first journals its answer)
|
|
266
|
+
* and `onPlayerChanged` asks the host to restart for that player. Services
|
|
267
|
+
* created for another player than the session's current one stay closed,
|
|
268
|
+
* which `warn` reports. `onDisposed` runs once, after the services were
|
|
269
|
+
* disposed either way (the store binding pin is released there).
|
|
270
|
+
* `dispose()` also stops watching.
|
|
271
|
+
*/
|
|
272
|
+
export function bindStarterServicesToPlayer(input: {
|
|
273
|
+
readonly services: GameServicesRuntime;
|
|
274
|
+
readonly playerId: string;
|
|
275
|
+
readonly session: {
|
|
276
|
+
current(): { readonly playerId: string } | undefined;
|
|
277
|
+
onAuthenticated(listener: (session: { readonly playerId: string }) => void): () => void;
|
|
278
|
+
};
|
|
279
|
+
readonly onPlayerChanging?: (playerId: string) => void;
|
|
280
|
+
readonly onPlayerChanged?: (playerId: string) => void;
|
|
281
|
+
readonly onDisposed?: () => void;
|
|
282
|
+
readonly warn?: (message: string, error?: unknown) => void;
|
|
283
|
+
}): GameServicesRuntime {
|
|
284
|
+
const warn = input.warn ?? ((message: string, error?: unknown) => console.warn(message, error ?? ''));
|
|
285
|
+
const { services } = input;
|
|
286
|
+
const current = input.session.current()?.playerId;
|
|
287
|
+
const createdForAnother = current !== undefined && current !== input.playerId;
|
|
288
|
+
let closing: Promise<void> | undefined;
|
|
289
|
+
const close = (): Promise<void> => {
|
|
290
|
+
closing ??= services.dispose().catch((error: unknown) => {
|
|
291
|
+
warn('[game-services] the services could not be closed cleanly', error);
|
|
292
|
+
}).finally(() => input.onDisposed?.());
|
|
293
|
+
return closing;
|
|
294
|
+
};
|
|
295
|
+
let changed = false;
|
|
296
|
+
/** The unsubscribe, once `onAuthenticated` returned it. */
|
|
297
|
+
const watching: { stop?: () => void } = {};
|
|
298
|
+
const onSession = (next: { readonly playerId: string }): void => {
|
|
299
|
+
if (changed || next.playerId === input.playerId) {
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
changed = true;
|
|
303
|
+
watching.stop?.();
|
|
304
|
+
notifyChanging(input.onPlayerChanging, next.playerId, warn);
|
|
305
|
+
void close().finally(() => input.onPlayerChanged?.(next.playerId));
|
|
306
|
+
};
|
|
307
|
+
if (createdForAnother) {
|
|
308
|
+
// Created for another player than the session's (a local placeholder):
|
|
309
|
+
// closed at once and the pin released, so services for the session's
|
|
310
|
+
// player can be created. No restart: the host that created these would
|
|
311
|
+
// create them the same way again.
|
|
312
|
+
changed = true;
|
|
313
|
+
warn(
|
|
314
|
+
"[game-services] created for a player that is not the server session's; they are closed. "
|
|
315
|
+
+ "Create them with the session's player.",
|
|
316
|
+
);
|
|
317
|
+
void close();
|
|
318
|
+
} else {
|
|
319
|
+
watching.stop = input.session.onAuthenticated(onSession);
|
|
320
|
+
if (changed) {
|
|
321
|
+
// The session announced another player while subscribing.
|
|
322
|
+
watching.stop();
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
return {
|
|
326
|
+
...services,
|
|
327
|
+
async dispose() {
|
|
328
|
+
watching.stop?.();
|
|
329
|
+
await close();
|
|
330
|
+
},
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/** Tells the host at once that another player signed in; a throwing callback is only reported. */
|
|
335
|
+
function notifyChanging(
|
|
336
|
+
callback: ((playerId: string) => void) | undefined,
|
|
337
|
+
playerId: string,
|
|
338
|
+
warn: (message: string, error?: unknown) => void,
|
|
339
|
+
): void {
|
|
340
|
+
try {
|
|
341
|
+
callback?.(playerId);
|
|
342
|
+
} catch (error) {
|
|
343
|
+
warn('[game-services] the player change callback failed', error);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
@@ -61,3 +61,15 @@ canvas {
|
|
|
61
61
|
font-size: 16px;
|
|
62
62
|
line-height: 1.5;
|
|
63
63
|
}
|
|
64
|
+
|
|
65
|
+
.player-switch {
|
|
66
|
+
position: fixed;
|
|
67
|
+
inset: 0;
|
|
68
|
+
z-index: 1000;
|
|
69
|
+
display: flex;
|
|
70
|
+
align-items: center;
|
|
71
|
+
justify-content: center;
|
|
72
|
+
background: rgb(0 0 0 / 70%);
|
|
73
|
+
color: #f8fafc;
|
|
74
|
+
font-size: 18px;
|
|
75
|
+
}
|