@mpgd/cli 0.36.0 → 0.36.1
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/capacitor-shell-starter.d.ts +21 -0
- package/dist/capacitor-shell-starter.js +121 -1
- package/dist/native-build-info.json +2 -2
- package/dist/native-build-target.js +381 -292
- package/package.json +12 -12
- package/templates/phaser-game/README.md +7 -0
- package/templates/phaser-game/apps/target-devvit/README.md +12 -0
- package/templates/phaser-game/apps/target-devvit/src/server/bridge.ts +513 -0
- package/templates/phaser-game/apps/target-devvit/src/server/index.ts +8 -291
- package/templates/phaser-game/vite.shared.ts +5 -1
|
@@ -2,22 +2,20 @@ import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
|
2
2
|
|
|
3
3
|
import { context, createServer, getServerPort, reddit, redis } from '@devvit/web/server';
|
|
4
4
|
import type { UiResponse } from '@devvit/web/shared';
|
|
5
|
-
import {
|
|
6
|
-
bridgeStorageLoadProtocol,
|
|
7
|
-
createBridgeError,
|
|
8
|
-
type BridgeRequest,
|
|
9
|
-
type BridgeResponse,
|
|
10
|
-
type BridgeStorageLoadData,
|
|
11
|
-
} from '@mpgd/bridge';
|
|
12
5
|
import { createBridgeRpcRouter, defaultBridgeRpcEndpoint } from '@mpgd/bridge/orpc';
|
|
13
6
|
import { createBridgeRpcNodeHandler } from '@mpgd/bridge/orpc/node';
|
|
14
7
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
const maxStorageValueBytes = 262_144;
|
|
8
|
+
import { createDevvitBridgeHandler } from './bridge.js';
|
|
9
|
+
|
|
18
10
|
const maxRequestBodySize = 1_048_576;
|
|
19
11
|
const gameName = '__GAME_NAME__';
|
|
20
12
|
const gameTitle = __GAME_TITLE_TS_LITERAL__;
|
|
13
|
+
const handleBridgeRequest = createDevvitBridgeHandler({
|
|
14
|
+
redis,
|
|
15
|
+
currentPlayerId,
|
|
16
|
+
currentDisplayName,
|
|
17
|
+
storageKeyNamespace: gameName,
|
|
18
|
+
});
|
|
21
19
|
const bridgeRpcHandler = createBridgeRpcNodeHandler(createBridgeRpcRouter(handleBridgeRequest), {
|
|
22
20
|
maxBodySize: maxRequestBodySize,
|
|
23
21
|
prefix: defaultBridgeRpcEndpoint,
|
|
@@ -126,279 +124,6 @@ server.listen(port, () => {
|
|
|
126
124
|
console.log(`devvit server listening on ${port}`);
|
|
127
125
|
});
|
|
128
126
|
|
|
129
|
-
async function handleBridgeRequest(input: BridgeRequest): Promise<BridgeResponse> {
|
|
130
|
-
switch (input.method) {
|
|
131
|
-
case 'runtime.getCapabilities':
|
|
132
|
-
return ok(input, {
|
|
133
|
-
nativeIap: false,
|
|
134
|
-
nativeAds: false,
|
|
135
|
-
rewardedAds: false,
|
|
136
|
-
interstitialAds: false,
|
|
137
|
-
nativeLeaderboard: false,
|
|
138
|
-
remoteLeaderboard: false,
|
|
139
|
-
achievements: false,
|
|
140
|
-
cloudSave: true,
|
|
141
|
-
socialShare: false,
|
|
142
|
-
haptics: false,
|
|
143
|
-
localizedContent: true,
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
case 'identity.getPlayer': {
|
|
147
|
-
const playerId = currentPlayerId();
|
|
148
|
-
|
|
149
|
-
if (playerId === undefined) {
|
|
150
|
-
return ok(input, null);
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
return ok(input, {
|
|
154
|
-
playerId,
|
|
155
|
-
displayName: await currentDisplayName(playerId),
|
|
156
|
-
});
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
case 'identity.getSession': {
|
|
160
|
-
const playerId = currentPlayerId();
|
|
161
|
-
|
|
162
|
-
return ok(
|
|
163
|
-
input,
|
|
164
|
-
playerId === undefined
|
|
165
|
-
? {
|
|
166
|
-
identityLevel: 'guest',
|
|
167
|
-
trustLevel: 'local',
|
|
168
|
-
}
|
|
169
|
-
: {
|
|
170
|
-
identityLevel: 'authenticated',
|
|
171
|
-
playerId,
|
|
172
|
-
trustLevel: 'server-verified',
|
|
173
|
-
},
|
|
174
|
-
);
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
case 'identity.requestUpgrade': {
|
|
178
|
-
const authenticated = currentPlayerId() !== undefined;
|
|
179
|
-
|
|
180
|
-
return ok(input, {
|
|
181
|
-
status: authenticated ? 'completed' : 'unavailable',
|
|
182
|
-
reloadExpected: false,
|
|
183
|
-
});
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
case 'presentation.getLaunchIntent':
|
|
187
|
-
return ok(input, { entry: 'home' });
|
|
188
|
-
|
|
189
|
-
case 'presentation.requestGameSurface':
|
|
190
|
-
return ok(input, 'unavailable');
|
|
191
|
-
|
|
192
|
-
case 'share.share':
|
|
193
|
-
return ok(input, { status: 'unavailable' });
|
|
194
|
-
|
|
195
|
-
case 'share.readInboundShare':
|
|
196
|
-
return ok(input, null);
|
|
197
|
-
|
|
198
|
-
case 'notifications.getStatus':
|
|
199
|
-
return ok(input, 'approval-required');
|
|
200
|
-
|
|
201
|
-
case 'notifications.requestSubscription':
|
|
202
|
-
return ok(input, 'unavailable');
|
|
203
|
-
|
|
204
|
-
case 'commerce.getProducts':
|
|
205
|
-
case 'commerce.getEntitlements':
|
|
206
|
-
return ok(input, []);
|
|
207
|
-
|
|
208
|
-
case 'commerce.purchase':
|
|
209
|
-
return ok(input, {
|
|
210
|
-
status: 'cancelled',
|
|
211
|
-
entitlementIds: [],
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
case 'commerce.restore':
|
|
215
|
-
return ok(input, {
|
|
216
|
-
restoredEntitlements: [],
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
case 'ads.preload':
|
|
220
|
-
return ok(input, {});
|
|
221
|
-
|
|
222
|
-
case 'leaderboard.open':
|
|
223
|
-
return createBridgeError(
|
|
224
|
-
input.id,
|
|
225
|
-
'DEVVIT_LEADERBOARD_OPEN_UNAVAILABLE',
|
|
226
|
-
'Devvit leaderboard display is not implemented yet.',
|
|
227
|
-
);
|
|
228
|
-
|
|
229
|
-
case 'ads.showRewarded':
|
|
230
|
-
return ok(input, {
|
|
231
|
-
status: 'unavailable',
|
|
232
|
-
rewardGranted: false,
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
case 'ads.showInterstitial':
|
|
236
|
-
return ok(input, {
|
|
237
|
-
status: 'unavailable',
|
|
238
|
-
});
|
|
239
|
-
|
|
240
|
-
case 'leaderboard.submitScore':
|
|
241
|
-
return ok(input, {
|
|
242
|
-
submitted: false,
|
|
243
|
-
});
|
|
244
|
-
|
|
245
|
-
case 'storage.load':
|
|
246
|
-
return loadStorage(input);
|
|
247
|
-
|
|
248
|
-
case 'storage.save':
|
|
249
|
-
return saveStorage(input);
|
|
250
|
-
|
|
251
|
-
default:
|
|
252
|
-
return createBridgeError(
|
|
253
|
-
input.id,
|
|
254
|
-
'UNSUPPORTED_METHOD',
|
|
255
|
-
`Unsupported Devvit bridge method: ${input.method}`,
|
|
256
|
-
);
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
async function loadStorage(input: BridgeRequest): Promise<BridgeResponse> {
|
|
261
|
-
const playerId = currentPlayerId();
|
|
262
|
-
|
|
263
|
-
if (playerId === undefined) {
|
|
264
|
-
return createBridgeError(
|
|
265
|
-
input.id,
|
|
266
|
-
'DEVVIT_STORAGE_IDENTITY_REQUIRED',
|
|
267
|
-
'A current Reddit player is required to load storage.',
|
|
268
|
-
);
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
const key = storageKey(input, playerId);
|
|
272
|
-
|
|
273
|
-
if (typeof key !== 'string') {
|
|
274
|
-
return key;
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
let stored: string | null | undefined;
|
|
278
|
-
|
|
279
|
-
try {
|
|
280
|
-
stored = await redis.get(key);
|
|
281
|
-
} catch (error) {
|
|
282
|
-
console.warn(`devvit storage load failed: ${errorMessage(error)}`);
|
|
283
|
-
return createBridgeError(
|
|
284
|
-
input.id,
|
|
285
|
-
'DEVVIT_STORAGE_LOAD_FAILED',
|
|
286
|
-
'Devvit storage could not be loaded.',
|
|
287
|
-
true,
|
|
288
|
-
);
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
if (stored === undefined || stored === null) {
|
|
292
|
-
return ok(input, {
|
|
293
|
-
__mpgdBridgeProtocol: bridgeStorageLoadProtocol,
|
|
294
|
-
found: false,
|
|
295
|
-
} satisfies BridgeStorageLoadData);
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
try {
|
|
299
|
-
return ok(
|
|
300
|
-
input,
|
|
301
|
-
{
|
|
302
|
-
__mpgdBridgeProtocol: bridgeStorageLoadProtocol,
|
|
303
|
-
found: true,
|
|
304
|
-
value: JSON.parse(stored),
|
|
305
|
-
} satisfies BridgeStorageLoadData,
|
|
306
|
-
);
|
|
307
|
-
} catch {
|
|
308
|
-
return createBridgeError(input.id, 'CORRUPTED_STORAGE_VALUE', 'Stored data is not valid JSON.');
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
async function saveStorage(input: BridgeRequest): Promise<BridgeResponse> {
|
|
313
|
-
const playerId = currentPlayerId();
|
|
314
|
-
|
|
315
|
-
if (playerId === undefined) {
|
|
316
|
-
return createBridgeError(
|
|
317
|
-
input.id,
|
|
318
|
-
'DEVVIT_STORAGE_IDENTITY_REQUIRED',
|
|
319
|
-
'A current Reddit player is required to save storage.',
|
|
320
|
-
);
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
const key = storageKey(input, playerId);
|
|
324
|
-
|
|
325
|
-
if (typeof key !== 'string') {
|
|
326
|
-
return key;
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
const payload = optionalObjectPayload(input.payload) as { readonly value?: unknown };
|
|
330
|
-
let serialized: string;
|
|
331
|
-
|
|
332
|
-
try {
|
|
333
|
-
const candidate = JSON.stringify(payload.value);
|
|
334
|
-
|
|
335
|
-
if (typeof candidate !== 'string') {
|
|
336
|
-
throw new Error('JSON serialization did not produce a string.');
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
serialized = candidate;
|
|
340
|
-
} catch {
|
|
341
|
-
return createBridgeError(
|
|
342
|
-
input.id,
|
|
343
|
-
'INVALID_STORAGE_VALUE',
|
|
344
|
-
'Storage values must be JSON serializable.',
|
|
345
|
-
);
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
if (new TextEncoder().encode(serialized).length > maxStorageValueBytes) {
|
|
349
|
-
return createBridgeError(
|
|
350
|
-
input.id,
|
|
351
|
-
'DEVVIT_STORAGE_QUOTA_EXCEEDED',
|
|
352
|
-
`Storage values must not exceed ${String(maxStorageValueBytes)} UTF-8 bytes.`,
|
|
353
|
-
);
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
try {
|
|
357
|
-
await redis.set(key, serialized);
|
|
358
|
-
} catch (error) {
|
|
359
|
-
console.warn(`devvit storage save was not persisted: ${errorMessage(error)}`);
|
|
360
|
-
return createBridgeError(
|
|
361
|
-
input.id,
|
|
362
|
-
'DEVVIT_STORAGE_SAVE_FAILED',
|
|
363
|
-
'Devvit storage could not be saved.',
|
|
364
|
-
true,
|
|
365
|
-
);
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
return ok(input, {
|
|
369
|
-
saved: true,
|
|
370
|
-
playerId,
|
|
371
|
-
});
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
function ok(input: BridgeRequest, data: unknown): BridgeResponse {
|
|
375
|
-
return {
|
|
376
|
-
id: input.id,
|
|
377
|
-
ok: true,
|
|
378
|
-
data,
|
|
379
|
-
};
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
function storageKey(input: BridgeRequest, playerId: string): string | BridgeResponse {
|
|
383
|
-
const payload = optionalObjectPayload(input.payload);
|
|
384
|
-
|
|
385
|
-
if (typeof payload.key !== 'string' || payload.key.length === 0) {
|
|
386
|
-
return createBridgeError(input.id, 'INVALID_STORAGE_KEY', 'Storage key is required.');
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
if (payload.key.length > maxStorageKeyLength) {
|
|
390
|
-
return createBridgeError(input.id, 'INVALID_STORAGE_KEY', 'Storage key is too long.');
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
const encodedKey = encodeURIComponent(payload.key);
|
|
394
|
-
|
|
395
|
-
if (encodedKey.length > maxEncodedStorageKeyLength) {
|
|
396
|
-
return createBridgeError(input.id, 'INVALID_STORAGE_KEY', 'Encoded storage key is too long.');
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
return `${gameName}:save:${encodeURIComponent(playerId)}:${encodedKey}`;
|
|
400
|
-
}
|
|
401
|
-
|
|
402
127
|
function errorMessage(error: unknown): string {
|
|
403
128
|
return error instanceof Error ? error.message : String(error);
|
|
404
129
|
}
|
|
@@ -435,14 +160,6 @@ function currentSubredditName(): string | undefined {
|
|
|
435
160
|
: undefined;
|
|
436
161
|
}
|
|
437
162
|
|
|
438
|
-
function optionalObjectPayload(payload: unknown): Record<string, unknown> {
|
|
439
|
-
if (typeof payload !== 'object' || payload === null) {
|
|
440
|
-
return {};
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
return payload as Record<string, unknown>;
|
|
444
|
-
}
|
|
445
|
-
|
|
446
163
|
function requestPathname(request: IncomingMessage): string {
|
|
447
164
|
return new URL(request.url ?? '/', 'http://localhost').pathname;
|
|
448
165
|
}
|
|
@@ -18,6 +18,10 @@ interface RuntimePlatformTargetMetadata {
|
|
|
18
18
|
readonly integrations?: Record<string, unknown>;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
// Sandbox gateways self-complete purchases and rewards without any backend
|
|
22
|
+
// verification, so a non-production build alone must never select them. Each
|
|
23
|
+
// one additionally requires the explicit BUILD_ID its dev script sets.
|
|
24
|
+
const aitSandboxBuildId = 'ait-sandbox';
|
|
21
25
|
const devvitSandboxBuildId = 'devvit-sandbox';
|
|
22
26
|
|
|
23
27
|
export interface CreateGameViteSharedConfigInput {
|
|
@@ -140,7 +144,7 @@ export function resolveBuildGatewayModule(input: {
|
|
|
140
144
|
case 'ios':
|
|
141
145
|
return 'src/platform/buildGateways/capacitorIos.ts';
|
|
142
146
|
case 'ait':
|
|
143
|
-
return input.debug
|
|
147
|
+
return input.debug && input.buildId === aitSandboxBuildId
|
|
144
148
|
? 'src/platform/buildGateways/aitSandbox.ts'
|
|
145
149
|
: 'src/platform/buildGateways/ait.ts';
|
|
146
150
|
case 'reddit':
|