@homie-rocks/studio 0.46.2 → 0.46.3
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/CHANGELOG.md +61 -0
- package/guides/game/RULES.md +104 -3
- package/guides/game/SKILL.md +5 -0
- package/lib/rules-check-worker.mjs +1 -1
- package/lib/rules-check.mjs +2 -2
- package/lib/rules-types.mjs +1 -1
- package/lib/template-history.json +42 -0
- package/netplay/netplay.ts +1 -1
- package/package.json +1 -1
- package/rules/core.ts +29 -4
- package/rules/host.ts +85 -13
- package/rules/pack.ts +1 -0
- package/rules/rules.ts +16 -5
- package/rules/types.ts +5 -0
- package/rules/view.ts +229 -25
- package/saves/SAVES.md +83 -3
- package/saves/saves.ts +2 -0
- package/worker/build-id.mjs +1 -1
- package/worker/index.mjs +2 -0
- package/worker/players.mjs +13 -15
- package/worker/room.mjs +28 -3
- package/worker/saves-shell.mjs +22 -2
- package/worker/saves.mjs +282 -19
- package/worker/version.mjs +1 -1
package/saves/SAVES.md
CHANGED
|
@@ -64,8 +64,8 @@ landing. Pressing Play never needs an account.
|
|
|
64
64
|
security key. No password, no email. A guest who makes an account keeps everything: it is the same player.
|
|
65
65
|
- **Another device**: "Sign in" with the same passkey. The device's own passkey sync carries it (iCloud Keychain,
|
|
66
66
|
Google Password Manager, a password manager), or the browser offers a phone's QR code. The same saves are there.
|
|
67
|
-
A guest who signs in to an account they already have
|
|
68
|
-
|
|
67
|
+
A guest who signs in to an account they already have loses nothing: what only the guest had (keys, stats,
|
|
68
|
+
memorials) moves over, and a key both have is combined ("When a guest signs in to their account", below).
|
|
69
69
|
- **Recovery**: add another passkey on another device or a security key (`/account/`). A **recovery email** is
|
|
70
70
|
offered only when the studio has a mail sender: a Cloudflare Email Service binding named `PLAYER_MAIL`
|
|
71
71
|
(`"send_email": [{ "name": "PLAYER_MAIL" }]` in wrangler.jsonc; Email Sending needs the Workers paid plan and a
|
|
@@ -130,6 +130,82 @@ an old offline copy of a dead hero is a conflict with the wipe, and the wipe win
|
|
|
130
130
|
|
|
131
131
|
A write is atomic **per key**, not across keys: keep one character in one key.
|
|
132
132
|
|
|
133
|
+
## When a guest signs in to their account
|
|
134
|
+
|
|
135
|
+
Somebody plays as a guest, then signs in to the account they made on another device. Both players have saves.
|
|
136
|
+
Whatever only the guest had moves to the account as it is (keys, stats, memorials), and the guest is gone. For a
|
|
137
|
+
key **both** have, the game says how the two combine, once, when the studio's Worker loads
|
|
138
|
+
(`site/src/worker.mjs`):
|
|
139
|
+
|
|
140
|
+
```js
|
|
141
|
+
import { mergeSaves } from '@homie-rocks/studio/worker';
|
|
142
|
+
|
|
143
|
+
mergeSaves({
|
|
144
|
+
'night-run': {
|
|
145
|
+
saves: {
|
|
146
|
+
'server:career-v1': { union: 'matches', by: 'id', order: 'endedAt', keep: 100 },
|
|
147
|
+
settings: 'newer', 'best-level': 'larger', coins: 'sum', 'slot-*': 'account',
|
|
148
|
+
inventory: (guest, account, ctx) => ({ items: [...new Set([...account.items, ...guest.items])] }),
|
|
149
|
+
},
|
|
150
|
+
stats: { kills: 'sum', level: 'larger', 'best-time': 'smaller' },
|
|
151
|
+
},
|
|
152
|
+
});
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
A name is the exact key, or a prefix ending in `*`. The exact key beats a prefix; a longer prefix beats a shorter
|
|
156
|
+
one. A rule that is none of the ones below throws when the Worker loads, naming it.
|
|
157
|
+
|
|
158
|
+
| A save's rule | The account ends with |
|
|
159
|
+
| --- | --- |
|
|
160
|
+
| `'account'`, `'guest'` | that side's value. |
|
|
161
|
+
| `'newer'` | the one written last (a tie: the account's). |
|
|
162
|
+
| `'larger'`, `'smaller'` | the larger or smaller of two numbers. `{ rule: 'larger', by: 'level' }` compares that number in two objects and keeps the whole object. |
|
|
163
|
+
| `'sum'` | two numbers added; or two objects with every number they share added, and for anything else the account's (the guest's where the account has none). |
|
|
164
|
+
| `{ union: 'matches', by: 'id', order: 'endedAt', keep: 100 }` | the account's object with that list joined to the guest's by `by` (on the same id, the account's entry), newest `order` first, cut to `keep`. `order` and `keep` are optional; `'union'` alone is exactly the rule shown. A joined list too large for a save loses entries from its end until it fits. |
|
|
165
|
+
| `(guest, account, ctx) => value` | what the game's own function returns, run on the server. It may be async. `ctx` is `{ game, key, guest: { updatedAt, version }, account: { updatedAt, version } }`. |
|
|
166
|
+
|
|
167
|
+
A stat's rule is `'sum'`, `'larger'`, `'smaller'`, `'guest'` or `'account'`.
|
|
168
|
+
|
|
169
|
+
**With no rule**, and whenever a rule cannot apply (two values that are not numbers to compare, a function that
|
|
170
|
+
throws, returns nothing, or returns more than a save holds), the default applies. It never fails the sign-in:
|
|
171
|
+
|
|
172
|
+
1. Equal values: nothing to do.
|
|
173
|
+
2. Two values of the **record shape** the room server writes (`{ matches: [{ id, … }] }`, every `id` a text):
|
|
174
|
+
joined by id, newest first. For a `server:` key the newest 100 are kept, the record's own bound; only the server
|
|
175
|
+
can write such a key, so nothing else could combine the two. A key the browser wrote that happens to have this
|
|
176
|
+
shape is joined the same way and not cut (it is cut only if it would not fit a save). Declare `'account'` for a
|
|
177
|
+
key of that shape that must not be joined.
|
|
178
|
+
3. Anything else: the account's value stays, and the guest's is **handed back** to the browser that signed in. The
|
|
179
|
+
play page raises it as an ordinary conflict, `mine` the guest's value and `theirs` the account's: a game with
|
|
180
|
+
`onConflict` keeps or merges it, and its answer is written on top; without `onConflict` the account's stays and
|
|
181
|
+
the game hears `conflict` with `kept: 'theirs'`. A stat keeps the account's number.
|
|
182
|
+
|
|
183
|
+
The sign-in answers with what happened, beside `adoptedGuest: true`:
|
|
184
|
+
|
|
185
|
+
```js
|
|
186
|
+
adopted: {
|
|
187
|
+
merged: [{ game, key, rule, took }], // took: 'merged', 'guest' or 'account' (a rule chose it)
|
|
188
|
+
moved: 3, // keys only the guest had
|
|
189
|
+
kept: [{ game, key, value }], // case 3; `rule` and `error` when a declared rule could not apply
|
|
190
|
+
stats: { merged: [{ game, name, rule }], kept: [{ game, name, value }] },
|
|
191
|
+
}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
`kept` carries at most 256 KiB of values; past that a key is named with `truncated: true` and the play page uses
|
|
195
|
+
its own copy of the guest's value. A sign-in on `/account/` has no game to ask, so there the account's simply stays.
|
|
196
|
+
|
|
197
|
+
It happens **exactly once**, in the one database transaction of the sign-in. If a room writes a record to either
|
|
198
|
+
player between the read and that transaction, the transaction fails whole, and it is read and tried again; after
|
|
199
|
+
four tries the account keeps every key both have and the answer says `fallback: true`. A round that ended a moment
|
|
200
|
+
before the sign-in writes its record a moment after, under the guest's id: the guest leaves a forwarding row
|
|
201
|
+
(`saves`, game `''`, key `server:moved`) and the record follows it to the account.
|
|
202
|
+
|
|
203
|
+
**What is new and what is a fix.** `mergeSaves` and its rules are opt-in: a studio that declares nothing has the
|
|
204
|
+
results it always had for every key and stat (the account's stays). Two things changed for everyone, both fixes:
|
|
205
|
+
server records are joined (a guest's verified matches used to vanish), and a value the account did not take is
|
|
206
|
+
handed back instead of dropped. A merged value larger than 64 KiB falls to the default; a merge never counts
|
|
207
|
+
against a game's 256 KiB.
|
|
208
|
+
|
|
133
209
|
## The hall of the fallen: a hardcore recipe
|
|
134
210
|
|
|
135
211
|
```ts
|
|
@@ -182,6 +258,10 @@ Migration `site/migrations/0004_players.sql` (deploy applies it; an older studio
|
|
|
182
258
|
| `player_emails` | a recovery email, only if the player added one | |
|
|
183
259
|
| `saves`, `player_stats`, `memorials` | what the game saved | anything the game did not |
|
|
184
260
|
|
|
261
|
+
A guest who signed in to their account leaves two small rows in `saves` under game `''`: `server:moved` under the
|
|
262
|
+
guest's id (the account's id and when) and `server:from:<guest>` under the account's. They are in no export and no
|
|
263
|
+
count, and they are deleted with the account.
|
|
264
|
+
|
|
185
265
|
- **The player** sees all of it at `/account/`, downloads it as one JSON file (`/api/player/export`), and deletes
|
|
186
266
|
it all (`/api/player/delete`): their passkeys, sessions, saves, stats, memorials and email, and their large
|
|
187
267
|
saves in R2. The page tells them to remove the passkey from their device too, and tells the device itself where
|
|
@@ -227,7 +307,7 @@ Same origin only (a POST must come from this site's own pages, as JSON). For ref
|
|
|
227
307
|
| --- | --- |
|
|
228
308
|
| `GET /api/player/me` | the signed-in player, the features (`accounts`, `email`), the limits |
|
|
229
309
|
| `POST /api/player/signup/options`, `POST /api/player/signup` | make an account, or add a passkey to this browser's guest or account |
|
|
230
|
-
| `POST /api/player/signin/options`, `POST /api/player/signin` | sign in with a passkey (discoverable: no name typed) |
|
|
310
|
+
| `POST /api/player/signin/options`, `POST /api/player/signin` | sign in with a passkey (discoverable: no name typed): `{ ok, player, adoptedGuest, adopted? }` |
|
|
231
311
|
| `POST /api/player/signout`, `POST /api/player/name`, `GET /api/player/passkeys`, `POST /api/player/passkeys/remove` | |
|
|
232
312
|
| `GET /api/player/export`, `POST /api/player/delete` (`{ "confirm": "delete" }`) | everything, and nothing |
|
|
233
313
|
| `GET /api/player/saves/<game>[?values=1]`, `GET /api/player/saves/<game>/<key>` | read |
|
package/saves/saves.ts
CHANGED
|
@@ -54,6 +54,8 @@ export interface SavesOptions {
|
|
|
54
54
|
* Another device saved a key since this one's copy (this browser was offline, or two devices played at once).
|
|
55
55
|
* Return the value to keep (merge them, or return `mine`), or `undefined` for the cloud's (the default: a stale
|
|
56
56
|
* offline copy never overwrites newer progress, so a hardcore character cannot come back from the dead).
|
|
57
|
+
* It is also asked when a guest signs in to an account that has the same key with another value and no
|
|
58
|
+
* `mergeSaves` rule combined them on the server: `mine` is the guest's value, `theirs` the account's.
|
|
57
59
|
*/
|
|
58
60
|
onConflict?: (c: Conflict) => unknown | Promise<unknown>;
|
|
59
61
|
/** Tests and custom shells: where messages go (default: the parent frame) and the shell's answer window. */
|
package/worker/build-id.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Generated by lib/build-identity.mjs; identifies the bundled runtime sources.
|
|
2
|
-
export const STUDIO_BUILD = Object.freeze({"version":"0.46.
|
|
2
|
+
export const STUDIO_BUILD = Object.freeze({"version":"0.46.3","sha256":"b36c0a219e734a32d419796f59d6965723849e1f462c75211468524c7b165266"});
|
package/worker/index.mjs
CHANGED
|
@@ -143,6 +143,8 @@ export { usePlayers } from './office.mjs';
|
|
|
143
143
|
export { countVisit } from './stats.mjs';
|
|
144
144
|
/** Players and cloud saves (saves/SAVES.md): `players.list`, `players.get`, `players.isOwner`… for the back office. */
|
|
145
145
|
export { players, cleanName } from './players.mjs';
|
|
146
|
+
/** How a guest's saves combine with the account's when they sign in (saves/SAVES.md): the studio's Worker calls it once, at load. */
|
|
147
|
+
export { mergeSaves } from './saves.mjs';
|
|
146
148
|
|
|
147
149
|
const GAME_ID = /^[a-z0-9][a-z0-9-]{0,39}$/;
|
|
148
150
|
const POST_SLUG = /^[a-z0-9][a-z0-9-]{0,79}$/;
|
package/worker/players.mjs
CHANGED
|
@@ -28,7 +28,7 @@ import { toolIdentity } from './tool-identity.mjs';
|
|
|
28
28
|
import { handleFor } from './room.mjs';
|
|
29
29
|
import { isAiName } from './agents.mjs';
|
|
30
30
|
import { cookieValue } from './stats.mjs';
|
|
31
|
-
import {
|
|
31
|
+
import { adoptGuest, bumpPlayerStats, dropPlayerData, fall, fallenList, getSave, listSaves, playerData, readPlayerStats, SAVE_LIMITS, savesSummary, wipeSaves, writeSaves } from './saves.mjs';
|
|
32
32
|
import { bytesToB64url, randomToken, sha256Hex, verifyAssertion, verifyRegistration } from './webauthn.mjs';
|
|
33
33
|
import { ACCOUNT_JS, accountPage } from './account-page.mjs';
|
|
34
34
|
import { adoptShopStatements, migrated as shopMigrated, forgetPlayerShop, livePurchases, shopDataOf } from './shop-store.mjs';
|
|
@@ -693,21 +693,19 @@ async function apiRoute(request, env, ctx, url, parts, { catalogueOf, read, f })
|
|
|
693
693
|
session.statement,
|
|
694
694
|
];
|
|
695
695
|
let adopted = null;
|
|
696
|
-
if (current && current.player.id !== key.player)
|
|
697
|
-
|
|
698
|
-
// A guest signing in to their account: what only the guest had moves over,
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
}
|
|
707
|
-
await env.DB.batch(writes);
|
|
708
|
-
if (adopted) await adopted.after();
|
|
696
|
+
if (current && current.player.id !== key.player) writes.push(env.DB.prepare('DELETE FROM player_sessions WHERE hash = ?1').bind(current.hash));
|
|
697
|
+
if (current && current.player.id !== key.player && current.player.guest) {
|
|
698
|
+
// A guest signing in to their account: what only the guest had moves over, what both have is combined by the
|
|
699
|
+
// game's rules (mergeSaves), then the guest is gone. One batch; read and tried again if a room wrote in between.
|
|
700
|
+
const guest = current.player.id;
|
|
701
|
+
const shop = await adoptShopStatements(env, guest, key.player);
|
|
702
|
+
adopted = await adoptGuest(env, guest, key.player, (statements) => [...writes, ...statements, ...shop,
|
|
703
|
+
env.DB.prepare('DELETE FROM player_sessions WHERE player = ?1').bind(guest),
|
|
704
|
+
env.DB.prepare('DELETE FROM player_challenges WHERE player = ?1').bind(guest),
|
|
705
|
+
env.DB.prepare('DELETE FROM players WHERE id = ?1 AND guest = 1').bind(guest)]);
|
|
706
|
+
} else await env.DB.batch(writes);
|
|
709
707
|
const player = { id: key.player, name: key.name, named: Number(key.named) === 1, guest: false, owner: Number(key.owner) === 1, since: Number(key.created_at) };
|
|
710
|
-
return withCookies(json({ ok: true, player: await meOf(env, player), adoptedGuest: Boolean(adopted) }), [session.cookie]);
|
|
708
|
+
return withCookies(json({ ok: true, player: await meOf(env, player), adoptedGuest: Boolean(adopted), ...(adopted ? { adopted } : {}) }), [session.cookie]);
|
|
711
709
|
}
|
|
712
710
|
|
|
713
711
|
/* ---------------- a recovery email, signed out: the link in the mail is the proof */
|
package/worker/room.mjs
CHANGED
|
@@ -452,7 +452,8 @@ export class NetRoom {
|
|
|
452
452
|
this.stats.evs += 1;
|
|
453
453
|
const out = { t: 'ev', from: Number.isInteger(m.from) ? m.from : null, k: kind, d: m.d ?? null };
|
|
454
454
|
const line = JSON.stringify(out);
|
|
455
|
-
|
|
455
|
+
// `to: -1` (0.46.3): everyone with no seat, a watcher. An effect with an audience reaches them whole.
|
|
456
|
+
if (Number.isInteger(m.to)) { for (const o of this.live()) if (m.to === -1 ? o.seat === null : o.seat === m.to) this.sendText(o.conn, line); return; }
|
|
456
457
|
for (const o of this.live()) if (!this.lite(o) || this.liteHears(kind)) this.sendText(o.conn, line);
|
|
457
458
|
return;
|
|
458
459
|
}
|
|
@@ -460,6 +461,25 @@ export class NetRoom {
|
|
|
460
461
|
}
|
|
461
462
|
}
|
|
462
463
|
|
|
464
|
+
/**
|
|
465
|
+
* One measurement of a connection's round trip (0.46.3), and what the rules are told: a LOW PERCENTILE of the last ten
|
|
466
|
+
* seconds of them. A measurement is never shorter than the real round trip, so the low end of a window is close to
|
|
467
|
+
* it and one slow answer does not raise it; a window, so a connection that really got slower or faster is followed.
|
|
468
|
+
* A client can still pretend to be slower by answering late every time: what that buys is bounded by the game's
|
|
469
|
+
* `room.rewindSeconds`, and with acknowledgements on the input frames it has to send its input late as well.
|
|
470
|
+
*/
|
|
471
|
+
latencySample(c, ms, now) {
|
|
472
|
+
if (!(ms >= 0 && ms <= 10000)) return;
|
|
473
|
+
const w = (c.rttWindow ??= []);
|
|
474
|
+
w.push([now, ms]);
|
|
475
|
+
while (w.length > 256 || (w.length > 1 && w[0][0] < now - 10000)) w.shift();
|
|
476
|
+
const sorted = w.map((x) => x[1]).sort((a, b) => a - b);
|
|
477
|
+
const low = sorted[Math.floor((sorted.length - 1) * 0.1)];
|
|
478
|
+
if (c.measuredRtt !== undefined && Math.abs(low - c.measuredRtt) < 2 && now - (c.rttToldAt ?? 0) < 1000) return;
|
|
479
|
+
c.measuredRtt = low; c.rttToldAt = now;
|
|
480
|
+
if (c.seat !== null) this.toServer({ t: 'latency', seat: c.seat, ms: low });
|
|
481
|
+
}
|
|
482
|
+
|
|
463
483
|
/** A snapshot from the host (a browser's socket `c`, or the server: `c` null), kept for joiners and fanned out. */
|
|
464
484
|
hostSnap(c, m, now, text = null) {
|
|
465
485
|
// The server's stamp is this relay's own clock; a browser's is its estimate of it, and is checked.
|
|
@@ -468,6 +488,7 @@ export class NetRoom {
|
|
|
468
488
|
// Revision 10: a rules game's snapshot names the room's epoch.
|
|
469
489
|
if ((!c || this.rules && c.rules) && Number.isFinite(m.e)) snap.e = m.e;
|
|
470
490
|
this.lastSnap = snap;
|
|
491
|
+
if (!c) { const sentAt = (this.snapAt ??= new Map()); sentAt.set(snap.k, now); if (sentAt.size > 128) sentAt.delete(sentAt.keys().next().value); }
|
|
471
492
|
if (c) c.lastSnapAt = now;
|
|
472
493
|
this.stats.snaps += 1;
|
|
473
494
|
this.snapTimes.push(now);
|
|
@@ -870,6 +891,9 @@ export class NetRoom {
|
|
|
870
891
|
if (this.server) {
|
|
871
892
|
if (!Array.isArray(m.s)) return;
|
|
872
893
|
this.stats.ins += 1;
|
|
894
|
+
// 0.46.3: the frame names the newest snapshot this browser had when it sent it (`g`). From when that snapshot
|
|
895
|
+
// left here to when this input arrived is this connection's round trip, measured on the input's own path.
|
|
896
|
+
if (Number.isInteger(m.g)) { c.inBand = true; const sent = this.snapAt?.get(m.g); if (sent !== undefined) this.latencySample(c, now - sent, now); }
|
|
873
897
|
this.toServer({ t: 'in', from: c.seat, e: Number(m.e) || 0, k: Number(m.k) || 0, s: m.s.slice(0, 64), r: Number(m.r) || 0 });
|
|
874
898
|
return;
|
|
875
899
|
}
|
|
@@ -909,7 +933,7 @@ export class NetRoom {
|
|
|
909
933
|
const recipients = isHost && this.rules && c.rules ? this.live() : this.others(c);
|
|
910
934
|
if (isHost) {
|
|
911
935
|
if (typeof m.to === 'string') { const o = this.clients.get(m.to); if (o && o.helloed && (o !== c || (this.rules && c.rules))) this.send(o, out); }
|
|
912
|
-
else if (Number.isInteger(m.to)) { for (const o of recipients) if (o.seat === m.to) this.send(o, out); }
|
|
936
|
+
else if (Number.isInteger(m.to)) { for (const o of recipients) if (m.to === -1 ? o.seat === null : o.seat === m.to) this.send(o, out); }
|
|
913
937
|
// An agent with no game client hears only what is addressed to its seat (the lite feed), and the party's lines.
|
|
914
938
|
else for (const o of recipients) { if (!this.lite(o) || this.liteHears(kind)) this.send(o, out); }
|
|
915
939
|
} else {
|
|
@@ -958,7 +982,8 @@ export class NetRoom {
|
|
|
958
982
|
case 'probeAck': {
|
|
959
983
|
if (c.probe && m.n === c.probe.n) {
|
|
960
984
|
const ms = now - c.probe.at; c.probe = null;
|
|
961
|
-
|
|
985
|
+
// A browser that acknowledges on its input frames is measured there; this separate answer is for one that does not.
|
|
986
|
+
if(ms>=0 && ms<=10000 && !c.inBand) this.latencySample(c, ms, now);
|
|
962
987
|
}
|
|
963
988
|
return;
|
|
964
989
|
}
|
package/worker/saves-shell.mjs
CHANGED
|
@@ -321,6 +321,25 @@ export const SAVES_SHELL_JS = `${PASSKEY_JS}
|
|
|
321
321
|
});
|
|
322
322
|
}
|
|
323
323
|
|
|
324
|
+
/**
|
|
325
|
+
* This browser's guest signed in to an account that had some of the same keys with other values, and no rule of
|
|
326
|
+
* the game's combined them on the server (mergeSaves): the account's stayed, and the sign-in answered with the
|
|
327
|
+
* guest's. Each is an ordinary conflict, the guest's value (mine) against the account's (theirs), so a game with
|
|
328
|
+
* onConflict keeps or merges it. Never a server: key: a browser cannot write one.
|
|
329
|
+
*/
|
|
330
|
+
function keptByAccount(adopted, guestCopy) {
|
|
331
|
+
var asked = [];
|
|
332
|
+
((adopted && adopted.kept) || []).forEach(function (k) {
|
|
333
|
+
var key = String(k.key);
|
|
334
|
+
if (k.game !== GAME || key.indexOf('server:') === 0 || !KEY.test(key)) return;
|
|
335
|
+
var mine = k.value !== undefined ? k.value : guestCopy[key];
|
|
336
|
+
if (mine === undefined || mine === null) return;
|
|
337
|
+
var c = S.cache[key];
|
|
338
|
+
asked.push(resolve(key, { op: 'set', v: mine }, { value: c ? c.v : null, version: c ? c.ver : 0 }, false));
|
|
339
|
+
});
|
|
340
|
+
if (asked.length) Promise.all(asked).then(function () { flush(); });
|
|
341
|
+
}
|
|
342
|
+
|
|
324
343
|
addEventListener('online', function () { online(true); S.backoff = 0; flush(); });
|
|
325
344
|
addEventListener('offline', function () { online(false); });
|
|
326
345
|
document.addEventListener('visibilitychange', function () { if (document.visibilityState === 'hidden') flush(true); });
|
|
@@ -385,9 +404,10 @@ export const SAVES_SHELL_JS = `${PASSKEY_JS}
|
|
|
385
404
|
};
|
|
386
405
|
inn.onclick = function () {
|
|
387
406
|
busy(true); said.className = 'said'; said.textContent = 'Follow your device…';
|
|
388
|
-
|
|
407
|
+
var guestCopy = {};
|
|
408
|
+
flush().then(function () { guestCopy = snapshot(); return P.signIn(); }).then(function (r) {
|
|
389
409
|
setPlayer({ id: r.player.id, name: r.player.name, guest: false, owner: r.player.owner }, false);
|
|
390
|
-
return refresh().then(function () { tell('player', { player: me(), cache: snapshot() }); close(); });
|
|
410
|
+
return refresh().then(function () { tell('player', { player: me(), cache: snapshot() }); close(); keptByAccount(r.adopted, guestCopy); });
|
|
391
411
|
}).catch(function (e) { said.className = 'said bad'; said.textContent = e && e.name ? P.said(e) : (e && e.message) || 'That did not work.'; busy(false); P.prime('in', '/api/player/signin/options'); });
|
|
392
412
|
};
|
|
393
413
|
}
|