@myvillage/cli 2.8.0 → 2.8.2
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
|
@@ -136,9 +136,13 @@ function requestHostContext(timeoutMs) {
|
|
|
136
136
|
// `npm run dev` or the Studio preview — there is no platform session, so it
|
|
137
137
|
// runs in DEV MODE against the public staging match server: open two browser
|
|
138
138
|
// tabs and use the same code (or both press quick match) to play yourself.
|
|
139
|
+
// Inside a host that doesn't provide a session (mode 'unavailable'), every
|
|
140
|
+
// call fails with code 'host_unavailable' — real players never land on the
|
|
141
|
+
// dev server.
|
|
139
142
|
|
|
140
143
|
const DEFAULT_DEV_SERVER = 'https://myvillage-realtime-staging.myvillage-realtime.workers.dev';
|
|
141
|
-
|
|
144
|
+
// No vowels (or look-alikes), so random codes can't spell words.
|
|
145
|
+
const CODE_ALPHABET = 'BCDFGHJKMNPQRSTVWXZ23456789';
|
|
142
146
|
|
|
143
147
|
export class MultiplayerError extends Error {
|
|
144
148
|
constructor(code, message, status) {
|
|
@@ -175,40 +179,51 @@ function devPlayerId() {
|
|
|
175
179
|
|
|
176
180
|
class Multiplayer {
|
|
177
181
|
constructor(ctx, options) {
|
|
178
|
-
this.mode = ctx ? 'platform' : 'dev';
|
|
182
|
+
this.mode = ctx ? 'platform' : standalone ? 'dev' : 'unavailable';
|
|
179
183
|
this._ctx = ctx;
|
|
180
184
|
this._opts = options;
|
|
181
185
|
this._net = null;
|
|
182
186
|
this._ticket = null;
|
|
183
187
|
/** The current match (see MatchView in the platform docs), or null. */
|
|
184
188
|
this.match = null;
|
|
185
|
-
|
|
189
|
+
// Digits only: random letters can spell words.
|
|
190
|
+
this.me = { displayName: ctx ? ctx.player.displayName : options.name || 'Player ' + (100 + Math.floor(Math.random() * 900)) };
|
|
186
191
|
}
|
|
187
192
|
|
|
188
193
|
// ── lobby plumbing ──
|
|
189
194
|
|
|
190
195
|
/** Host a match. visibility: 'private' (code only) or 'village' (listed + quick match). */
|
|
191
196
|
async host({ visibility = 'private', maxPlayers, lobby } = {}) {
|
|
197
|
+
this._requireAvailable();
|
|
192
198
|
if (this.mode === 'dev') return this._devMatch(randomCode(), true, lobby);
|
|
193
199
|
return this._enter(await this._api('POST', `/api/v1/games/${this._game()}/matches`, { visibility, maxPlayers, lobby, platform: this._ctx.platform }));
|
|
194
200
|
}
|
|
195
201
|
|
|
196
202
|
/** Join a match by its code (forgiving: "abc-234" works). */
|
|
197
203
|
async join(code) {
|
|
204
|
+
this._requireAvailable();
|
|
198
205
|
if (this.mode === 'dev') return this._devMatch(normalizeCode(code), false);
|
|
199
206
|
return this._enter(await this._api('POST', `/api/v1/games/${this._game()}/matches/join`, { code, platform: this._ctx.platform }));
|
|
200
207
|
}
|
|
201
208
|
|
|
202
209
|
/** Join the fullest open match in the player's villages, or host one. */
|
|
203
210
|
async quickMatch({ lobby } = {}) {
|
|
211
|
+
this._requireAvailable();
|
|
204
212
|
// Finished match rooms can't be reused, so dev quick match rotates rooms
|
|
205
|
-
// every 10 minutes
|
|
206
|
-
|
|
213
|
+
// every 10 minutes, and connect() steps through numbered rooms in that
|
|
214
|
+
// window past any that are finished or full. Tabs follow the same
|
|
215
|
+
// sequence, so they still meet.
|
|
216
|
+
if (this.mode === 'dev') {
|
|
217
|
+
this._devQuickBase = 'QUICK' + Math.floor(Date.now() / 600000).toString(36).toUpperCase();
|
|
218
|
+
this._devQuickTry = 0;
|
|
219
|
+
return this._devMatch(this._devQuickBase, false, lobby);
|
|
220
|
+
}
|
|
207
221
|
return this._enter(await this._api('POST', `/api/v1/games/${this._game()}/matches/quick`, { platform: this._ctx.platform, lobby }));
|
|
208
222
|
}
|
|
209
223
|
|
|
210
224
|
/** Open village matches the player can join, plus matches they're in. */
|
|
211
225
|
async list() {
|
|
226
|
+
this._requireAvailable();
|
|
212
227
|
if (this.mode === 'dev') return this.match ? [this.match] : [];
|
|
213
228
|
return (await this._api('GET', `/api/v1/games/${this._game()}/matches`)).matches;
|
|
214
229
|
}
|
|
@@ -285,11 +300,25 @@ class Multiplayer {
|
|
|
285
300
|
* (src/myvillage-realtime.js). Call again after a disconnect to rejoin.
|
|
286
301
|
*/
|
|
287
302
|
async connect() {
|
|
303
|
+
this._requireAvailable();
|
|
288
304
|
if (!this.match) throw new MultiplayerError('no_match', 'Host, join or quick match first.');
|
|
289
|
-
const ticket = await this._freshTicket();
|
|
290
305
|
const { ArenaClient } = await import('./myvillage-realtime.js');
|
|
291
|
-
|
|
292
|
-
|
|
306
|
+
let net;
|
|
307
|
+
for (;;) {
|
|
308
|
+
const ticket = await this._freshTicket();
|
|
309
|
+
net = new ArenaClient({ wsUrl: ticket.wsUrl, ticket: ticket.ticket });
|
|
310
|
+
try {
|
|
311
|
+
await net.connect();
|
|
312
|
+
break;
|
|
313
|
+
} catch (err) {
|
|
314
|
+
// Dev quick match: that room is finished or full — try the next one.
|
|
315
|
+
const devQuick = this.mode === 'dev' && this._devQuickBase && this.match.code.startsWith(this._devQuickBase);
|
|
316
|
+
if (!devQuick || this._devQuickTry >= 20 || !/finished|full/i.test(String(err && err.message))) throw err;
|
|
317
|
+
this._devQuickTry++;
|
|
318
|
+
this._ticket = null;
|
|
319
|
+
this._devMatch(this._devQuickBase + '-' + this._devQuickTry, false, this.match.lobby);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
293
322
|
this._ticket = null; // single use: a reconnect fetches a fresh one
|
|
294
323
|
this._net = net;
|
|
295
324
|
return net;
|
|
@@ -297,6 +326,12 @@ class Multiplayer {
|
|
|
297
326
|
|
|
298
327
|
// ── internals ──
|
|
299
328
|
|
|
329
|
+
_requireAvailable() {
|
|
330
|
+
if (this.mode === 'unavailable') {
|
|
331
|
+
throw new MultiplayerError('host_unavailable', "Multiplayer isn't available here yet.");
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
300
335
|
_game() {
|
|
301
336
|
return encodeURIComponent(this._ctx.game);
|
|
302
337
|
}
|
|
@@ -320,9 +355,29 @@ class Multiplayer {
|
|
|
320
355
|
_enter(result) {
|
|
321
356
|
this.match = result.match;
|
|
322
357
|
this._ticket = result.realtime;
|
|
358
|
+
this._leaveOnUnload();
|
|
323
359
|
return this.match;
|
|
324
360
|
}
|
|
325
361
|
|
|
362
|
+
// Closing the page (or the host's Exit) frees the player's seat in the lobby
|
|
363
|
+
// straight away rather than when the lobby expires.
|
|
364
|
+
_leaveOnUnload() {
|
|
365
|
+
if (this._unloadHooked || !hasWindow) return;
|
|
366
|
+
this._unloadHooked = true;
|
|
367
|
+
window.addEventListener('pagehide', () => {
|
|
368
|
+
if (!this.match || this.mode !== 'platform') return;
|
|
369
|
+
try {
|
|
370
|
+
fetch(this._ctx.apiBase + '/api/v1/matches/' + this.match.id + '/leave', {
|
|
371
|
+
method: 'POST',
|
|
372
|
+
keepalive: true,
|
|
373
|
+
headers: { Authorization: 'Bearer ' + this._ctx.playToken },
|
|
374
|
+
});
|
|
375
|
+
} catch (_) {
|
|
376
|
+
// best effort
|
|
377
|
+
}
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
|
|
326
381
|
async _update(body) {
|
|
327
382
|
if (!this.match) throw new MultiplayerError('no_match', 'Not in a match.');
|
|
328
383
|
if (this.mode === 'dev') {
|
|
@@ -421,7 +476,7 @@ export const MyVillageGame = {
|
|
|
421
476
|
* devServer — dev-mode match server (defaults to the MyVillage staging server)
|
|
422
477
|
*/
|
|
423
478
|
async multiplayer(options = {}) {
|
|
424
|
-
const ctx = await requestHostContext(standalone ? 300 :
|
|
479
|
+
const ctx = await requestHostContext(standalone ? 300 : 10000);
|
|
425
480
|
return new Multiplayer(ctx, options);
|
|
426
481
|
},
|
|
427
482
|
};
|
|
@@ -86,6 +86,8 @@ function friendlyError(err) {
|
|
|
86
86
|
return 'That match has already started.';
|
|
87
87
|
case 'not_in_village':
|
|
88
88
|
return 'You can only play with people from your village.';
|
|
89
|
+
case 'host_unavailable':
|
|
90
|
+
return "Multiplayer isn't available in this player yet.";
|
|
89
91
|
case 'no_village':
|
|
90
92
|
return 'Join a village to play multiplayer — you play with your village.';
|
|
91
93
|
default:
|