@zaisma/game-server 1.4.0 → 1.5.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.
Files changed (342) hide show
  1. package/README.md +2 -0
  2. package/bench/brand/.assetsignore +4 -0
  3. package/bench/brand/README.md +51 -0
  4. package/bench/brand/_headers +13 -0
  5. package/bench/brand/app-icon-1024.png +0 -0
  6. package/bench/brand/app-icon-192.png +0 -0
  7. package/bench/brand/app-icon-512.png +0 -0
  8. package/bench/brand/apple-touch-icon.png +0 -0
  9. package/bench/brand/cover-guide/coin-cascade-busy-fine.webp +0 -0
  10. package/bench/brand/cover-guide/gem-grid-cover.webp +0 -0
  11. package/bench/brand/cover-guide/gem-grid-screenshot.webp +0 -0
  12. package/bench/brand/cover-guide/good-arena.webp +0 -0
  13. package/bench/brand/cover-guide/good-four-in-a-row.webp +0 -0
  14. package/bench/brand/cover-guide/lava-leap-extra-text.webp +0 -0
  15. package/bench/brand/cover-guide/lava-leap-fake-reward.webp +0 -0
  16. package/bench/brand/cover-guide/lava-leap-fine.webp +0 -0
  17. package/bench/brand/cover-guide/lava-leap-overlay.webp +0 -0
  18. package/bench/brand/cover-guide/screenshot-fine.webp +0 -0
  19. package/bench/brand/cover-guide/screenshot-not-fine.webp +0 -0
  20. package/bench/brand/design/covers/ashen-crown.webp +0 -0
  21. package/bench/brand/design/covers/frostline.webp +0 -0
  22. package/bench/brand/design/covers/glasswing.webp +0 -0
  23. package/bench/brand/design/covers/hollow-tide.webp +0 -0
  24. package/bench/brand/design/covers/iron-vigil.webp +0 -0
  25. package/bench/brand/design/covers/kestrel.webp +0 -0
  26. package/bench/brand/design/covers/neon-drift.webp +0 -0
  27. package/bench/brand/design/covers/verdant.webp +0 -0
  28. package/bench/brand/favicon-32.png +0 -0
  29. package/bench/brand/favicon.ico +0 -0
  30. package/bench/brand/favicon.svg +1 -0
  31. package/bench/brand/flags/LICENSE +21 -0
  32. package/bench/brand/flags/ad.png +0 -0
  33. package/bench/brand/flags/ae.png +0 -0
  34. package/bench/brand/flags/af.png +0 -0
  35. package/bench/brand/flags/ag.png +0 -0
  36. package/bench/brand/flags/ai.png +0 -0
  37. package/bench/brand/flags/al.png +0 -0
  38. package/bench/brand/flags/am.png +0 -0
  39. package/bench/brand/flags/ao.png +0 -0
  40. package/bench/brand/flags/aq.png +0 -0
  41. package/bench/brand/flags/ar.png +0 -0
  42. package/bench/brand/flags/as.png +0 -0
  43. package/bench/brand/flags/at.png +0 -0
  44. package/bench/brand/flags/au.png +0 -0
  45. package/bench/brand/flags/aw.png +0 -0
  46. package/bench/brand/flags/ax.png +0 -0
  47. package/bench/brand/flags/az.png +0 -0
  48. package/bench/brand/flags/ba.png +0 -0
  49. package/bench/brand/flags/bb.png +0 -0
  50. package/bench/brand/flags/bd.png +0 -0
  51. package/bench/brand/flags/be.png +0 -0
  52. package/bench/brand/flags/bf.png +0 -0
  53. package/bench/brand/flags/bg.png +0 -0
  54. package/bench/brand/flags/bh.png +0 -0
  55. package/bench/brand/flags/bi.png +0 -0
  56. package/bench/brand/flags/bj.png +0 -0
  57. package/bench/brand/flags/bl.png +0 -0
  58. package/bench/brand/flags/bm.png +0 -0
  59. package/bench/brand/flags/bn.png +0 -0
  60. package/bench/brand/flags/bo.png +0 -0
  61. package/bench/brand/flags/bq.png +0 -0
  62. package/bench/brand/flags/br.png +0 -0
  63. package/bench/brand/flags/bs.png +0 -0
  64. package/bench/brand/flags/bt.png +0 -0
  65. package/bench/brand/flags/bv.png +0 -0
  66. package/bench/brand/flags/bw.png +0 -0
  67. package/bench/brand/flags/by.png +0 -0
  68. package/bench/brand/flags/bz.png +0 -0
  69. package/bench/brand/flags/ca.png +0 -0
  70. package/bench/brand/flags/cc.png +0 -0
  71. package/bench/brand/flags/cd.png +0 -0
  72. package/bench/brand/flags/cf.png +0 -0
  73. package/bench/brand/flags/cg.png +0 -0
  74. package/bench/brand/flags/ch.png +0 -0
  75. package/bench/brand/flags/ci.png +0 -0
  76. package/bench/brand/flags/ck.png +0 -0
  77. package/bench/brand/flags/cl.png +0 -0
  78. package/bench/brand/flags/cm.png +0 -0
  79. package/bench/brand/flags/cn.png +0 -0
  80. package/bench/brand/flags/co.png +0 -0
  81. package/bench/brand/flags/cp.png +0 -0
  82. package/bench/brand/flags/cr.png +0 -0
  83. package/bench/brand/flags/cu.png +0 -0
  84. package/bench/brand/flags/cv.png +0 -0
  85. package/bench/brand/flags/cw.png +0 -0
  86. package/bench/brand/flags/cx.png +0 -0
  87. package/bench/brand/flags/cy.png +0 -0
  88. package/bench/brand/flags/cz.png +0 -0
  89. package/bench/brand/flags/de.png +0 -0
  90. package/bench/brand/flags/dg.png +0 -0
  91. package/bench/brand/flags/dj.png +0 -0
  92. package/bench/brand/flags/dk.png +0 -0
  93. package/bench/brand/flags/dm.png +0 -0
  94. package/bench/brand/flags/do.png +0 -0
  95. package/bench/brand/flags/dz.png +0 -0
  96. package/bench/brand/flags/ec.png +0 -0
  97. package/bench/brand/flags/ee.png +0 -0
  98. package/bench/brand/flags/eg.png +0 -0
  99. package/bench/brand/flags/eh.png +0 -0
  100. package/bench/brand/flags/er.png +0 -0
  101. package/bench/brand/flags/es.png +0 -0
  102. package/bench/brand/flags/et.png +0 -0
  103. package/bench/brand/flags/eu.png +0 -0
  104. package/bench/brand/flags/fi.png +0 -0
  105. package/bench/brand/flags/fj.png +0 -0
  106. package/bench/brand/flags/fk.png +0 -0
  107. package/bench/brand/flags/fm.png +0 -0
  108. package/bench/brand/flags/fo.png +0 -0
  109. package/bench/brand/flags/fr.png +0 -0
  110. package/bench/brand/flags/ga.png +0 -0
  111. package/bench/brand/flags/gb.png +0 -0
  112. package/bench/brand/flags/gd.png +0 -0
  113. package/bench/brand/flags/ge.png +0 -0
  114. package/bench/brand/flags/gf.png +0 -0
  115. package/bench/brand/flags/gg.png +0 -0
  116. package/bench/brand/flags/gh.png +0 -0
  117. package/bench/brand/flags/gi.png +0 -0
  118. package/bench/brand/flags/gl.png +0 -0
  119. package/bench/brand/flags/gm.png +0 -0
  120. package/bench/brand/flags/gn.png +0 -0
  121. package/bench/brand/flags/gp.png +0 -0
  122. package/bench/brand/flags/gq.png +0 -0
  123. package/bench/brand/flags/gr.png +0 -0
  124. package/bench/brand/flags/gs.png +0 -0
  125. package/bench/brand/flags/gt.png +0 -0
  126. package/bench/brand/flags/gu.png +0 -0
  127. package/bench/brand/flags/gw.png +0 -0
  128. package/bench/brand/flags/gy.png +0 -0
  129. package/bench/brand/flags/hk.png +0 -0
  130. package/bench/brand/flags/hm.png +0 -0
  131. package/bench/brand/flags/hn.png +0 -0
  132. package/bench/brand/flags/hr.png +0 -0
  133. package/bench/brand/flags/ht.png +0 -0
  134. package/bench/brand/flags/hu.png +0 -0
  135. package/bench/brand/flags/ic.png +0 -0
  136. package/bench/brand/flags/id.png +0 -0
  137. package/bench/brand/flags/ie.png +0 -0
  138. package/bench/brand/flags/il.png +0 -0
  139. package/bench/brand/flags/im.png +0 -0
  140. package/bench/brand/flags/in.png +0 -0
  141. package/bench/brand/flags/io.png +0 -0
  142. package/bench/brand/flags/iq.png +0 -0
  143. package/bench/brand/flags/ir.png +0 -0
  144. package/bench/brand/flags/is.png +0 -0
  145. package/bench/brand/flags/it.png +0 -0
  146. package/bench/brand/flags/je.png +0 -0
  147. package/bench/brand/flags/jm.png +0 -0
  148. package/bench/brand/flags/jo.png +0 -0
  149. package/bench/brand/flags/jp.png +0 -0
  150. package/bench/brand/flags/ke.png +0 -0
  151. package/bench/brand/flags/kg.png +0 -0
  152. package/bench/brand/flags/kh.png +0 -0
  153. package/bench/brand/flags/ki.png +0 -0
  154. package/bench/brand/flags/km.png +0 -0
  155. package/bench/brand/flags/kn.png +0 -0
  156. package/bench/brand/flags/kp.png +0 -0
  157. package/bench/brand/flags/kr.png +0 -0
  158. package/bench/brand/flags/kw.png +0 -0
  159. package/bench/brand/flags/ky.png +0 -0
  160. package/bench/brand/flags/kz.png +0 -0
  161. package/bench/brand/flags/la.png +0 -0
  162. package/bench/brand/flags/lb.png +0 -0
  163. package/bench/brand/flags/lc.png +0 -0
  164. package/bench/brand/flags/li.png +0 -0
  165. package/bench/brand/flags/lk.png +0 -0
  166. package/bench/brand/flags/lr.png +0 -0
  167. package/bench/brand/flags/ls.png +0 -0
  168. package/bench/brand/flags/lt.png +0 -0
  169. package/bench/brand/flags/lu.png +0 -0
  170. package/bench/brand/flags/lv.png +0 -0
  171. package/bench/brand/flags/ly.png +0 -0
  172. package/bench/brand/flags/ma.png +0 -0
  173. package/bench/brand/flags/mc.png +0 -0
  174. package/bench/brand/flags/md.png +0 -0
  175. package/bench/brand/flags/me.png +0 -0
  176. package/bench/brand/flags/mf.png +0 -0
  177. package/bench/brand/flags/mg.png +0 -0
  178. package/bench/brand/flags/mh.png +0 -0
  179. package/bench/brand/flags/mk.png +0 -0
  180. package/bench/brand/flags/ml.png +0 -0
  181. package/bench/brand/flags/mm.png +0 -0
  182. package/bench/brand/flags/mn.png +0 -0
  183. package/bench/brand/flags/mo.png +0 -0
  184. package/bench/brand/flags/mp.png +0 -0
  185. package/bench/brand/flags/mq.png +0 -0
  186. package/bench/brand/flags/mr.png +0 -0
  187. package/bench/brand/flags/ms.png +0 -0
  188. package/bench/brand/flags/mt.png +0 -0
  189. package/bench/brand/flags/mu.png +0 -0
  190. package/bench/brand/flags/mv.png +0 -0
  191. package/bench/brand/flags/mw.png +0 -0
  192. package/bench/brand/flags/mx.png +0 -0
  193. package/bench/brand/flags/my.png +0 -0
  194. package/bench/brand/flags/mz.png +0 -0
  195. package/bench/brand/flags/na.png +0 -0
  196. package/bench/brand/flags/nc.png +0 -0
  197. package/bench/brand/flags/ne.png +0 -0
  198. package/bench/brand/flags/nf.png +0 -0
  199. package/bench/brand/flags/ng.png +0 -0
  200. package/bench/brand/flags/ni.png +0 -0
  201. package/bench/brand/flags/nl.png +0 -0
  202. package/bench/brand/flags/no.png +0 -0
  203. package/bench/brand/flags/np.png +0 -0
  204. package/bench/brand/flags/nr.png +0 -0
  205. package/bench/brand/flags/nu.png +0 -0
  206. package/bench/brand/flags/nz.png +0 -0
  207. package/bench/brand/flags/om.png +0 -0
  208. package/bench/brand/flags/pa.png +0 -0
  209. package/bench/brand/flags/pc.png +0 -0
  210. package/bench/brand/flags/pe.png +0 -0
  211. package/bench/brand/flags/pf.png +0 -0
  212. package/bench/brand/flags/pg.png +0 -0
  213. package/bench/brand/flags/ph.png +0 -0
  214. package/bench/brand/flags/pk.png +0 -0
  215. package/bench/brand/flags/pl.png +0 -0
  216. package/bench/brand/flags/pm.png +0 -0
  217. package/bench/brand/flags/pn.png +0 -0
  218. package/bench/brand/flags/pr.png +0 -0
  219. package/bench/brand/flags/ps.png +0 -0
  220. package/bench/brand/flags/pt.png +0 -0
  221. package/bench/brand/flags/pw.png +0 -0
  222. package/bench/brand/flags/py.png +0 -0
  223. package/bench/brand/flags/qa.png +0 -0
  224. package/bench/brand/flags/re.png +0 -0
  225. package/bench/brand/flags/ro.png +0 -0
  226. package/bench/brand/flags/rs.png +0 -0
  227. package/bench/brand/flags/ru.png +0 -0
  228. package/bench/brand/flags/rw.png +0 -0
  229. package/bench/brand/flags/sa.png +0 -0
  230. package/bench/brand/flags/sb.png +0 -0
  231. package/bench/brand/flags/sc.png +0 -0
  232. package/bench/brand/flags/sd.png +0 -0
  233. package/bench/brand/flags/se.png +0 -0
  234. package/bench/brand/flags/sg.png +0 -0
  235. package/bench/brand/flags/sh.png +0 -0
  236. package/bench/brand/flags/si.png +0 -0
  237. package/bench/brand/flags/sj.png +0 -0
  238. package/bench/brand/flags/sk.png +0 -0
  239. package/bench/brand/flags/sl.png +0 -0
  240. package/bench/brand/flags/sm.png +0 -0
  241. package/bench/brand/flags/sn.png +0 -0
  242. package/bench/brand/flags/so.png +0 -0
  243. package/bench/brand/flags/sr.png +0 -0
  244. package/bench/brand/flags/ss.png +0 -0
  245. package/bench/brand/flags/st.png +0 -0
  246. package/bench/brand/flags/sv.png +0 -0
  247. package/bench/brand/flags/sx.png +0 -0
  248. package/bench/brand/flags/sy.png +0 -0
  249. package/bench/brand/flags/sz.png +0 -0
  250. package/bench/brand/flags/tc.png +0 -0
  251. package/bench/brand/flags/td.png +0 -0
  252. package/bench/brand/flags/tf.png +0 -0
  253. package/bench/brand/flags/tg.png +0 -0
  254. package/bench/brand/flags/th.png +0 -0
  255. package/bench/brand/flags/tj.png +0 -0
  256. package/bench/brand/flags/tk.png +0 -0
  257. package/bench/brand/flags/tl.png +0 -0
  258. package/bench/brand/flags/tm.png +0 -0
  259. package/bench/brand/flags/tn.png +0 -0
  260. package/bench/brand/flags/to.png +0 -0
  261. package/bench/brand/flags/tr.png +0 -0
  262. package/bench/brand/flags/tt.png +0 -0
  263. package/bench/brand/flags/tv.png +0 -0
  264. package/bench/brand/flags/tw.png +0 -0
  265. package/bench/brand/flags/tz.png +0 -0
  266. package/bench/brand/flags/ua.png +0 -0
  267. package/bench/brand/flags/ug.png +0 -0
  268. package/bench/brand/flags/um.png +0 -0
  269. package/bench/brand/flags/un.png +0 -0
  270. package/bench/brand/flags/us.png +0 -0
  271. package/bench/brand/flags/uy.png +0 -0
  272. package/bench/brand/flags/uz.png +0 -0
  273. package/bench/brand/flags/va.png +0 -0
  274. package/bench/brand/flags/vc.png +0 -0
  275. package/bench/brand/flags/ve.png +0 -0
  276. package/bench/brand/flags/vg.png +0 -0
  277. package/bench/brand/flags/vi.png +0 -0
  278. package/bench/brand/flags/vn.png +0 -0
  279. package/bench/brand/flags/vu.png +0 -0
  280. package/bench/brand/flags/wf.png +0 -0
  281. package/bench/brand/flags/ws.png +0 -0
  282. package/bench/brand/flags/xk.png +0 -0
  283. package/bench/brand/flags/xx.png +0 -0
  284. package/bench/brand/flags/ye.png +0 -0
  285. package/bench/brand/flags/yt.png +0 -0
  286. package/bench/brand/flags/za.png +0 -0
  287. package/bench/brand/flags/zm.png +0 -0
  288. package/bench/brand/flags/zw.png +0 -0
  289. package/bench/brand/fonts/OFL.txt +93 -0
  290. package/bench/brand/fonts/jost.7726a5cd.woff2 +0 -0
  291. package/bench/brand/loader/gif/zaisma-loader-black-128.gif +0 -0
  292. package/bench/brand/loader/gif/zaisma-loader-black-512.gif +0 -0
  293. package/bench/brand/loader/gif/zaisma-loader-white-128.gif +0 -0
  294. package/bench/brand/loader/gif/zaisma-loader-white-512.gif +0 -0
  295. package/bench/brand/loader/png/zaisma-loader-black-128.png +0 -0
  296. package/bench/brand/loader/png/zaisma-loader-black-512.png +0 -0
  297. package/bench/brand/loader/png/zaisma-loader-white-128.png +0 -0
  298. package/bench/brand/loader/png/zaisma-loader-white-512.png +0 -0
  299. package/bench/brand/loader/webp/zaisma-loader-black-128.webp +0 -0
  300. package/bench/brand/loader/webp/zaisma-loader-black-512.webp +0 -0
  301. package/bench/brand/loader/webp/zaisma-loader-white-128.webp +0 -0
  302. package/bench/brand/loader/webp/zaisma-loader-white-512.webp +0 -0
  303. package/bench/brand/loader/zaisma-loader-black.svg +1 -0
  304. package/bench/brand/loader/zaisma-loader-white.svg +1 -0
  305. package/bench/brand/loader/zaisma-loader.svg +1 -0
  306. package/bench/brand/sign-in/city-day-phone.webp +0 -0
  307. package/bench/brand/sign-in/city-day.webp +0 -0
  308. package/bench/brand/sign-in/city-night-phone.webp +0 -0
  309. package/bench/brand/sign-in/city-night.webp +0 -0
  310. package/bench/brand/zaisma-app-icon.svg +1 -0
  311. package/bench/brand/zaisma-mark-black-2048.png +0 -0
  312. package/bench/brand/zaisma-mark-black.svg +1 -0
  313. package/bench/brand/zaisma-mark-metal-2048.png +0 -0
  314. package/bench/brand/zaisma-mark-metal.svg +1 -0
  315. package/bench/brand/zaisma-mark-small-black.svg +1 -0
  316. package/bench/brand/zaisma-mark-small-white.svg +1 -0
  317. package/bench/brand/zaisma-mark-tricolor.svg +1 -0
  318. package/bench/brand/zaisma-mark-white-2048.png +0 -0
  319. package/bench/brand/zaisma-mark-white.svg +1 -0
  320. package/bench/brand/zaisma-wordmark-black-2400.png +0 -0
  321. package/bench/brand/zaisma-wordmark-black.svg +1 -0
  322. package/bench/brand/zaisma-wordmark-metal-2400.png +0 -0
  323. package/bench/brand/zaisma-wordmark-metal.svg +1 -0
  324. package/bench/brand/zaisma-wordmark-white-2400.png +0 -0
  325. package/bench/brand/zaisma-wordmark-white.svg +1 -0
  326. package/bench/glass.js +46466 -0
  327. package/bin/local-play-bench.d.ts +8 -0
  328. package/bin/local-play-bench.js +218 -0
  329. package/bin/local-play-certificate.js +99 -0
  330. package/bin/local-play-runner.js +43 -0
  331. package/bin/local-play.d.ts +41 -0
  332. package/bin/local-play.js +1567 -0
  333. package/bin/node.js +66 -1
  334. package/package.json +1 -1
  335. package/sdk/picture.js +97 -0
  336. package/sdk/relay.js +269 -0
  337. package/sdk/saves.js +884 -0
  338. package/sdk/zaisma.d.ts +1720 -0
  339. package/sdk/zaisma.js +2374 -0
  340. package/src/qr-code.d.ts +20 -0
  341. package/src/qr-code.js +319 -0
  342. package/src/room.js +1 -1
package/sdk/saves.js ADDED
@@ -0,0 +1,884 @@
1
+ /*
2
+ * Zaisma cloud saves, game side. The games server adds this to every page of a game
3
+ * that has cloud saves on; games don't include it themselves.
4
+ *
5
+ * It only ever talks to the Zaisma game page around it (zaisma.com), never to a server,
6
+ * and does nothing unless that page asks it to. The game page holds the player's
7
+ * session and does the saving; this script only reads and writes the game's own
8
+ * localStorage and IndexedDB, the storage the game already uses.
9
+ *
10
+ * A save is { save_format_version_number: 1, local_storage_items: { key: value }, indexeddb_databases: [databases] }:
11
+ * the game's localStorage, and the IndexedDB it keeps (Unity and Godot builds, localForage), [] when none.
12
+ *
13
+ * Two places it runs:
14
+ * - <game>/_zaisma/restore: before the game opens, it shows the game page what's saved
15
+ * on this device and puts the account's save in place if the page says so.
16
+ * - every game page: while the page says to sync, it sends the save over whenever it
17
+ * changes (checked every 30 seconds) and when the player leaves. When the page says to
18
+ * apply a save (a developer's change, for a game that takes it in place), it puts it in
19
+ * place while the game runs and says whether it could.
20
+ */
21
+ (function () {
22
+ "use strict";
23
+ var site = __ZAISMA_SAVES__.site;
24
+ // Past this a save stays on the device (SAVE_LIMIT_BYTES in packages/core/src/saves.ts), so nothing past it is read.
25
+ var saveLimitBytes = __ZAISMA_SAVES__.saveLimitBytes || Infinity;
26
+ // The game version the games server served this page from, sent with each save as a label; null when unknown.
27
+ var versionId = typeof __ZAISMA_SAVES__.versionId === "string" ? __ZAISMA_SAVES__.versionId : null;
28
+ // Databases of downloaded game files, not progress (like Unity's UnityCache): never copied.
29
+ var SKIP = /cache/i;
30
+ var VIEWS = ["Int8Array", "Uint8Array", "Uint8ClampedArray", "Int16Array", "Uint16Array", "Int32Array", "Uint32Array", "Float32Array", "Float64Array", "BigInt64Array", "BigUint64Array", "DataView"];
31
+ var global = typeof globalThis !== "undefined" ? globalThis : window;
32
+ // Records read from IndexedDB at a time, so a read stops soon after a save goes past the limit: 16
33
+ // first, doubling up to 1,024 while a batch's text stays under 1 MB (games keep whole files as records).
34
+ var FIRST_BATCH_RECORD_COUNT = 16;
35
+ var MAXIMUM_BATCH_RECORD_COUNT = 1024;
36
+ var SMALL_BATCH_CHARACTER_COUNT = 1024 * 1024;
37
+
38
+ // Tests check the save format through this (test/node/saves.test.ts).
39
+ if (__ZAISMA_SAVES__.test) return __ZAISMA_SAVES__.test({ encode: encode, decode: decode, saveText: saveText, putSave: putSave, readDatabases: readDatabases, databasesReader: databasesReader });
40
+ // The same script, started as a worker by the game page below: it reads the databases off the thread that draws the game.
41
+ if (typeof window === "undefined") return serveReads();
42
+ if (window.parent === window) return;
43
+ var scriptUrl = document.currentScript ? document.currentScript.src : "";
44
+
45
+ // --- The save format ---
46
+
47
+ function snapshot() {
48
+ var local = {};
49
+ for (var i = 0; i < localStorage.length; i++) {
50
+ var key = localStorage.key(i);
51
+ local[key] = localStorage.getItem(key);
52
+ }
53
+ return local;
54
+ }
55
+ // Keys sorted, so the same save is always the same text (matches canonicalSave on the server).
56
+ function canonical(local) {
57
+ var out = {};
58
+ Object.keys(local)
59
+ .sort()
60
+ .forEach(function (key) {
61
+ set(out, key, local[key]);
62
+ });
63
+ return JSON.stringify(out);
64
+ }
65
+ /** The whole save as text. idbText is the encoded databases, or "" when there are none. */
66
+ function saveText(local, idbText) {
67
+ return textOf(canonical(local), idbText);
68
+ }
69
+ // The same text as canonicalSave on the server: all three keys, always in this order.
70
+ function textOf(localText, idbText) {
71
+ return '{"save_format_version_number":1,"local_storage_items":' + localText + ',"indexeddb_databases":' + (idbText || "[]") + "}";
72
+ }
73
+ function set(target, key, entryValue) {
74
+ if (key === "__proto__") Object.defineProperty(target, key, { value: entryValue, enumerable: true, writable: true, configurable: true });
75
+ else target[key] = entryValue;
76
+ }
77
+
78
+ // IndexedDB holds more than JSON can (dates, binary data, files, maps), so those become
79
+ // tagged objects: { "$z": [kind, ...] }. A plain object with its own "$z" key is tagged
80
+ // too ("obj"), so nothing is ever mistaken for a tag. Blobs are read afterwards: their
81
+ // reads go into `pending`.
82
+ function encode(given, pending, seen) {
83
+ if (given === null || typeof given === "boolean" || typeof given === "string") return given;
84
+ if (typeof given === "number") return isFinite(given) && !(given === 0 && 1 / given < 0) ? given : { $z: ["num", given === 0 ? "-0" : String(given)] };
85
+ if (given === undefined) return { $z: ["undef"] };
86
+ if (typeof given === "bigint") return { $z: ["bigint", given.toString()] };
87
+ if (typeof given !== "object") throw new Error("Can't save a " + typeof given);
88
+ if (seen.indexOf(given) !== -1) throw new Error("Can't save a value that contains itself");
89
+ var kind = Object.prototype.toString.call(given).slice(8, -1);
90
+ if (kind === "Date") return { $z: ["date", encode(given.getTime(), pending, seen)] };
91
+ if (kind === "RegExp") return { $z: ["regexp", given.source, given.flags] };
92
+ if (kind === "ArrayBuffer") return { $z: ["bytes", toBase64(new Uint8Array(given))] };
93
+ if (ArrayBuffer.isView(given)) return { $z: ["view", VIEWS.indexOf(kind) === -1 ? "Uint8Array" : kind, toBase64(new Uint8Array(given.buffer, given.byteOffset, given.byteLength))] };
94
+ if (kind === "Blob" || kind === "File") {
95
+ var tag = kind === "File" ? { $z: ["file", given.type, "", given.name, given.lastModified] } : { $z: ["blob", given.type, ""] };
96
+ // What the blob adds once read: its bytes in base64.
97
+ pending.blobCharacterCount = (pending.blobCharacterCount || 0) + Math.ceil(given.size / 3) * 4;
98
+ pending.push(
99
+ given.arrayBuffer().then(function (buffer) {
100
+ tag.$z[2] = toBase64(new Uint8Array(buffer));
101
+ }),
102
+ );
103
+ return tag;
104
+ }
105
+ seen.push(given);
106
+ var out;
107
+ if (kind === "Map" || kind === "Set") {
108
+ var items = [];
109
+ given.forEach(function (entryValue, key) {
110
+ items.push(kind === "Map" ? [encode(key, pending, seen), encode(entryValue, pending, seen)] : encode(entryValue, pending, seen));
111
+ });
112
+ out = { $z: [kind === "Map" ? "map" : "set", items] };
113
+ } else if (Array.isArray(given)) {
114
+ out = [];
115
+ for (var i = 0; i < given.length; i++) out.push(encode(given[i], pending, seen));
116
+ } else {
117
+ out = {};
118
+ Object.keys(given).forEach(function (key) {
119
+ set(out, key, encode(given[key], pending, seen));
120
+ });
121
+ if (Object.prototype.hasOwnProperty.call(given, "$z")) out = { $z: ["obj", out] };
122
+ }
123
+ seen.pop();
124
+ return out;
125
+ }
126
+
127
+ function decode(encoded) {
128
+ if (encoded === null || typeof encoded !== "object") return encoded;
129
+ if (Array.isArray(encoded)) return encoded.map(decode);
130
+ var tag = Object.prototype.hasOwnProperty.call(encoded, "$z") ? encoded.$z : null;
131
+ if (!Array.isArray(tag)) return plain(encoded);
132
+ switch (tag[0]) {
133
+ case "num":
134
+ return Number(tag[1]);
135
+ case "undef":
136
+ return undefined;
137
+ case "bigint":
138
+ return BigInt(tag[1]);
139
+ case "date":
140
+ return new Date(decode(tag[1]));
141
+ case "regexp":
142
+ return new RegExp(tag[1], tag[2]);
143
+ case "bytes":
144
+ return fromBase64(tag[1]).buffer;
145
+ case "view":
146
+ var bytes = fromBase64(tag[2]);
147
+ if (tag[1] === "DataView") return new DataView(bytes.buffer);
148
+ return VIEWS.indexOf(tag[1]) === -1 ? bytes : new global[tag[1]](bytes.buffer);
149
+ case "blob":
150
+ return new Blob([fromBase64(tag[2])], { type: tag[1] });
151
+ case "file":
152
+ return new File([fromBase64(tag[2])], tag[3], { type: tag[1], lastModified: tag[4] });
153
+ case "map":
154
+ return new Map(
155
+ tag[1].map(function (entry) {
156
+ return [decode(entry[0]), decode(entry[1])];
157
+ }),
158
+ );
159
+ case "set":
160
+ return new Set(tag[1].map(decode));
161
+ case "obj":
162
+ return plain(tag[1]);
163
+ }
164
+ return plain(encoded);
165
+ }
166
+ function plain(object) {
167
+ var out = {};
168
+ Object.keys(object).forEach(function (key) {
169
+ set(out, key, decode(object[key]));
170
+ });
171
+ return out;
172
+ }
173
+
174
+ function toBase64(bytes) {
175
+ var binary = "";
176
+ for (var i = 0; i < bytes.length; i += 0x8000) binary += String.fromCharCode.apply(null, bytes.subarray(i, i + 0x8000));
177
+ return btoa(binary);
178
+ }
179
+ function fromBase64(text) {
180
+ var binary = atob(text);
181
+ var out = new Uint8Array(binary.length);
182
+ for (var i = 0; i < binary.length; i++) out[i] = binary.charCodeAt(i);
183
+ return out;
184
+ }
185
+
186
+ // --- IndexedDB ---
187
+
188
+ function request(databaseRequest) {
189
+ return new Promise(function (ok, fail) {
190
+ databaseRequest.onsuccess = function () {
191
+ ok(databaseRequest.result);
192
+ };
193
+ databaseRequest.onerror = function () {
194
+ fail(databaseRequest.error);
195
+ };
196
+ });
197
+ }
198
+ function names() {
199
+ if (typeof indexedDB === "undefined" || !indexedDB || !indexedDB.databases) return Promise.resolve([]);
200
+ return indexedDB.databases().then(function (list) {
201
+ return list
202
+ .map(function (database) {
203
+ return database.name;
204
+ })
205
+ .filter(function (name) {
206
+ return typeof name === "string" && !SKIP.test(name);
207
+ })
208
+ .sort();
209
+ });
210
+ }
211
+ function open(name, version, upgrade) {
212
+ return new Promise(function (ok, fail) {
213
+ var opening = version ? indexedDB.open(name, version) : indexedDB.open(name);
214
+ opening.onupgradeneeded = function () {
215
+ if (upgrade) upgrade(opening.result);
216
+ };
217
+ opening.onsuccess = function () {
218
+ var database = opening.result;
219
+ // Never hold up the game's own database upgrades.
220
+ database.onversionchange = function () {
221
+ database.close();
222
+ };
223
+ ok(database);
224
+ };
225
+ opening.onerror = function () {
226
+ fail(opening.error);
227
+ };
228
+ });
229
+ }
230
+ function storeNames(database) {
231
+ return Array.prototype.slice.call(database.objectStoreNames).sort();
232
+ }
233
+ /** How a store is set up, in a fixed shape so two can be compared as text. */
234
+ function shape(store) {
235
+ return {
236
+ name: store.name,
237
+ keyPath: keyPath(store.keyPath),
238
+ autoIncrement: store.autoIncrement,
239
+ indexes: Array.prototype.slice
240
+ .call(store.indexNames)
241
+ .sort()
242
+ .map(function (name) {
243
+ var index = store.index(name);
244
+ return { name: name, keyPath: keyPath(index.keyPath), unique: index.unique, multiEntry: index.multiEntry };
245
+ }),
246
+ };
247
+ }
248
+ function savedShape(saved) {
249
+ return { name: saved.name, keyPath: saved.keyPath, autoIncrement: saved.autoIncrement, indexes: saved.indexes };
250
+ }
251
+ // A key path is a name, a list of names (some browsers give a DOMStringList) or none.
252
+ function keyPath(path) {
253
+ return path === null || typeof path === "string" ? path : Array.prototype.slice.call(path);
254
+ }
255
+
256
+ /**
257
+ * Every database the game keeps, sorted by name so the same data always reads the same:
258
+ * [{ name, version, stores: [{ name, keyPath, autoIncrement, indexes, records: [[key, value]] }] }].
259
+ * Databases with nothing in them are left out.
260
+ */
261
+ function readDatabases() {
262
+ return names().then(function (list) {
263
+ return Promise.all(list.map(readDatabase)).then(function (databases) {
264
+ return databases.filter(Boolean);
265
+ });
266
+ });
267
+ }
268
+ function readDatabase(name) {
269
+ var created = false;
270
+ return open(name, 0, function () {
271
+ created = true;
272
+ }).then(function (database) {
273
+ var stores = storeNames(database);
274
+ if (created || !stores.length) {
275
+ database.close();
276
+ // It was deleted after the list was read; don't leave an empty one behind.
277
+ if (created) indexedDB.deleteDatabase(name);
278
+ return null;
279
+ }
280
+ return new Promise(function (ok, fail) {
281
+ var transaction = database.transaction(stores, "readonly");
282
+ var out = stores.map(function (storeName) {
283
+ var store = transaction.objectStore(storeName);
284
+ var copy = shape(store);
285
+ var keys = store.getAllKeys();
286
+ var values = store.getAll();
287
+ values.onsuccess = function () {
288
+ copy.records = values.result.map(function (record, i) {
289
+ return [keys.result[i], record];
290
+ });
291
+ };
292
+ return copy;
293
+ });
294
+ transaction.oncomplete = function () {
295
+ database.close();
296
+ var hasRecords = out.some(function (copied) {
297
+ return copied.records.length;
298
+ });
299
+ ok(hasRecords ? { name: name, version: database.version, stores: out } : null);
300
+ };
301
+ transaction.onerror = transaction.onabort = function () {
302
+ database.close();
303
+ fail(transaction.error);
304
+ };
305
+ });
306
+ });
307
+ }
308
+ /** The databases as save text: "" when there are none. */
309
+ function readNumberbText() {
310
+ return readDatabases().then(function (databases) {
311
+ if (!databases.length) return "";
312
+ var pending = [];
313
+ var out = encode(databases, pending, []);
314
+ return Promise.all(pending).then(function () {
315
+ return JSON.stringify(out);
316
+ });
317
+ });
318
+ }
319
+
320
+ /**
321
+ * readNumberbText while the game plays: each store's text is kept, and a read only reads again the stores
322
+ * the game changed (changed: { isAll, databases: [name], stores: ["<database>\n<store>"] }), a batch of
323
+ * records at a time, stopping once the save passes maximumCharacterCount characters. The text is exactly
324
+ * readNumberbText's, put together from the kept parts. Answers { text } or { isOverLimit: true }.
325
+ */
326
+ function databasesReader() {
327
+ // Database name -> { version, stores: { name: { text, hasRecords } } }
328
+ var kept = {};
329
+ return function read(changed, maximumCharacterCount) {
330
+ var changedStores = {};
331
+ changed.stores.forEach(function (storeKey) {
332
+ changedStores[storeKey] = true;
333
+ });
334
+ return names().then(function (list) {
335
+ var keptNow = {};
336
+ var characterCount = 0;
337
+ var index = 0;
338
+ function next() {
339
+ if (index === list.length) {
340
+ kept = keptNow;
341
+ var parts = list.filter(function (name) {
342
+ return keptNow[name] && keptNow[name].hasRecords;
343
+ }).map(function (name) {
344
+ var database = keptNow[name];
345
+ var storeTexts = database.storeNames.map(function (storeName) {
346
+ return database.stores[storeName].text;
347
+ });
348
+ return '{"name":' + JSON.stringify(name) + ',"version":' + JSON.stringify(encode(database.version, [], [])) + ',"stores":[' + storeTexts.join(",") + "]}";
349
+ });
350
+ return { text: parts.length ? "[" + parts.join(",") + "]" : "" };
351
+ }
352
+ var name = list[index++];
353
+ var before = kept[name];
354
+ var isWhole = changed.isAll || !before || changed.databases.indexOf(name) !== -1;
355
+ var isAnyStore = isWhole || Object.keys(changedStores).some(function (storeKey) {
356
+ return storeKey.indexOf(name + "\n") === 0;
357
+ });
358
+ if (!isAnyStore) {
359
+ keptNow[name] = before;
360
+ characterCount += before.characterCount;
361
+ return characterCount > maximumCharacterCount ? { isOverLimit: true } : next();
362
+ }
363
+ return readChanged(name, before, isWhole, changedStores, maximumCharacterCount - characterCount).then(function (database) {
364
+ if (database && database.isOverLimit) return { isOverLimit: true };
365
+ if (database) {
366
+ keptNow[name] = database;
367
+ characterCount += database.characterCount;
368
+ }
369
+ return next();
370
+ });
371
+ }
372
+ return next();
373
+ });
374
+ };
375
+ }
376
+ /** One database for databasesReader: its stores read again where changed, the rest as kept. null when it's gone or has no stores. */
377
+ function readChanged(name, before, isWhole, changedStores, maximumCharacterCount) {
378
+ var created = false;
379
+ return open(name, 0, function () {
380
+ created = true;
381
+ }).then(function (database) {
382
+ var stores = storeNames(database);
383
+ if (created || !stores.length) {
384
+ database.close();
385
+ if (created) indexedDB.deleteDatabase(name);
386
+ return null;
387
+ }
388
+ // Another version of the game's database is set up anew: read all of it.
389
+ if (!before || before.version !== database.version) isWhole = true;
390
+ var reading = stores.filter(function (storeName) {
391
+ return isWhole || !before.stores[storeName] || changedStores[name + "\n" + storeName];
392
+ });
393
+ var out = { version: database.version, storeNames: stores, stores: {}, characterCount: 0, hasRecords: false };
394
+ stores.forEach(function (storeName) {
395
+ if (reading.indexOf(storeName) === -1) out.stores[storeName] = before.stores[storeName];
396
+ });
397
+ stores.forEach(function (storeName) {
398
+ if (out.stores[storeName]) out.characterCount += out.stores[storeName].text.length;
399
+ });
400
+ if (!reading.length) {
401
+ database.close();
402
+ return finish(out);
403
+ }
404
+ return new Promise(function (ok, fail) {
405
+ var transaction = database.transaction(reading, "readonly");
406
+ var pending = [];
407
+ var read = [];
408
+ var isOverLimit = false;
409
+ var storeIndex = 0;
410
+ var store = null;
411
+ var copy = null;
412
+ function nextStore() {
413
+ if (storeIndex === reading.length) return;
414
+ store = transaction.objectStore(reading[storeIndex++]);
415
+ copy = { shape: shape(store), records: [] };
416
+ read.push(copy);
417
+ nextBatch(null, FIRST_BATCH_RECORD_COUNT);
418
+ }
419
+ function nextBatch(afterKey, batchRecordCount) {
420
+ var range = afterKey === null ? null : IDBKeyRange.lowerBound(afterKey, true);
421
+ var keys = store.getAllKeys(range, batchRecordCount);
422
+ var values = store.getAll(range, batchRecordCount);
423
+ values.onsuccess = function () {
424
+ var previousCharacterCount = out.characterCount;
425
+ var blobCharacterCount = pending.blobCharacterCount || 0;
426
+ values.result.forEach(function (record, i) {
427
+ var encoded = encode([keys.result[i], record], pending, []);
428
+ copy.records.push(encoded);
429
+ out.characterCount += JSON.stringify(encoded).length + 1;
430
+ });
431
+ out.characterCount += (pending.blobCharacterCount || 0) - blobCharacterCount;
432
+ if (out.characterCount > maximumCharacterCount) {
433
+ isOverLimit = true;
434
+ transaction.abort();
435
+ return;
436
+ }
437
+ var isSmall = out.characterCount - previousCharacterCount < SMALL_BATCH_CHARACTER_COUNT;
438
+ if (values.result.length === batchRecordCount) nextBatch(keys.result[keys.result.length - 1], isSmall ? Math.min(batchRecordCount * 2, MAXIMUM_BATCH_RECORD_COUNT) : batchRecordCount);
439
+ else nextStore();
440
+ };
441
+ }
442
+ nextStore();
443
+ transaction.oncomplete = function () {
444
+ database.close();
445
+ Promise.all(pending).then(function () {
446
+ read.forEach(function (stored) {
447
+ var shapeText = JSON.stringify(encode(stored.shape, [], []));
448
+ var recordTexts = stored.records.map(function (record) {
449
+ return JSON.stringify(record);
450
+ });
451
+ out.stores[stored.shape.name] = { text: shapeText.slice(0, -1) + ',"records":[' + recordTexts.join(",") + "]}", hasRecords: stored.records.length > 0 };
452
+ });
453
+ out.characterCount = 0;
454
+ stores.forEach(function (storeName) {
455
+ out.characterCount += out.stores[storeName].text.length;
456
+ });
457
+ ok(finish(out));
458
+ }, fail);
459
+ };
460
+ transaction.onerror = transaction.onabort = function () {
461
+ database.close();
462
+ if (isOverLimit) ok({ isOverLimit: true });
463
+ else fail(transaction.error);
464
+ };
465
+ });
466
+ });
467
+ }
468
+ function finish(database) {
469
+ database.hasRecords = database.storeNames.some(function (storeName) {
470
+ return database.stores[storeName].hasRecords;
471
+ });
472
+ return database;
473
+ }
474
+
475
+ /** The worker side: reads asked for by the game page, one at a time, each answered with its id. */
476
+ function serveReads() {
477
+ var read = databasesReader();
478
+ addEventListener("message", function (event) {
479
+ var asked = event.data || {};
480
+ read(asked.changed, asked.maximumCharacterCount).then(
481
+ function (answer) {
482
+ answer.id = asked.id;
483
+ postMessage(answer);
484
+ },
485
+ function () {
486
+ postMessage({ id: asked.id, isFailed: true });
487
+ },
488
+ );
489
+ });
490
+ }
491
+
492
+ /**
493
+ * Make IndexedDB hold exactly `dbs`. A database set up the same way is emptied and
494
+ * refilled; one set up differently (another version of the game) is deleted and made
495
+ * again, which waits while the game is open in another tab (onBlocked says so).
496
+ */
497
+ function writeDatabases(databases, onBlocked) {
498
+ var wanted = {};
499
+ databases.forEach(function (database) {
500
+ wanted[database.name] = database;
501
+ });
502
+ return names().then(function (list) {
503
+ var here = {};
504
+ list.forEach(function (name) {
505
+ here[name] = true;
506
+ });
507
+ var empty = list.filter(function (name) {
508
+ return !wanted[name];
509
+ });
510
+ return Promise.all(
511
+ empty
512
+ .map(function (name) {
513
+ return fill(name, null, true, onBlocked);
514
+ })
515
+ .concat(
516
+ databases.map(function (database) {
517
+ return fill(database.name, database, Boolean(here[database.name]), onBlocked);
518
+ }),
519
+ ),
520
+ );
521
+ });
522
+ }
523
+ function fill(name, saved, exists, onBlocked) {
524
+ if (!exists) return saved ? create(saved) : Promise.resolve();
525
+ return open(name).then(function (database) {
526
+ if (!saved || (database.version === saved.version && sameShape(database, saved))) return write(database, saved);
527
+ database.close();
528
+ return drop(name, onBlocked).then(function () {
529
+ return create(saved);
530
+ });
531
+ });
532
+ }
533
+ function sameShape(database, saved) {
534
+ var stores = storeNames(database);
535
+ var theirs = saved.stores.map(savedShape).sort(function (first, second) {
536
+ return first.name < second.name ? -1 : 1;
537
+ });
538
+ if (stores.join("\n") !== theirs.map(function (stored) { return stored.name; }).join("\n")) return false;
539
+ if (!stores.length) return true;
540
+ var transaction = database.transaction(stores, "readonly");
541
+ return JSON.stringify(stores.map(function (storeName) { return shape(transaction.objectStore(storeName)); })) === JSON.stringify(theirs);
542
+ }
543
+ function create(saved) {
544
+ return open(saved.name, saved.version, function (database) {
545
+ saved.stores.forEach(function (stored) {
546
+ var store = database.createObjectStore(stored.name, { keyPath: stored.keyPath, autoIncrement: stored.autoIncrement });
547
+ stored.indexes.forEach(function (savedIndex) {
548
+ store.createIndex(savedIndex.name, savedIndex.keyPath, { unique: savedIndex.unique, multiEntry: savedIndex.multiEntry });
549
+ });
550
+ });
551
+ }).then(function (database) {
552
+ return write(database, saved);
553
+ });
554
+ }
555
+ /** Empty every store, then put the saved records back (when there are any). */
556
+ function write(database, saved) {
557
+ var stores = storeNames(database);
558
+ if (!stores.length) {
559
+ database.close();
560
+ return Promise.resolve();
561
+ }
562
+ return new Promise(function (ok, fail) {
563
+ var transaction = database.transaction(stores, "readwrite");
564
+ transaction.oncomplete = function () {
565
+ database.close();
566
+ ok();
567
+ };
568
+ transaction.onerror = transaction.onabort = function () {
569
+ database.close();
570
+ fail(transaction.error);
571
+ };
572
+ try {
573
+ stores.forEach(function (storeName) {
574
+ transaction.objectStore(storeName).clear();
575
+ });
576
+ (saved ? saved.stores : []).forEach(function (stored) {
577
+ var store = transaction.objectStore(stored.name);
578
+ stored.records.forEach(function (record) {
579
+ if (store.keyPath === null) store.put(record[1], record[0]);
580
+ else store.put(record[1]);
581
+ });
582
+ });
583
+ } catch (error) {
584
+ transaction.abort();
585
+ }
586
+ });
587
+ }
588
+ function drop(name, onBlocked) {
589
+ var deleting = indexedDB.deleteDatabase(name);
590
+ deleting.onblocked = onBlocked;
591
+ return request(deleting);
592
+ }
593
+
594
+ /** Put the account's save in place of this device's. onBlocked: it's waiting on another tab. */
595
+ function putSave(save, onBlocked) {
596
+ // The page always hands over a v1 save.
597
+ var local = save.local_storage_items;
598
+ var databases = decode(save.indexeddb_databases);
599
+ // IndexedDB first: it's the part that can fail, and then localStorage is still as it was.
600
+ var writing =
601
+ window.indexedDB && indexedDB.databases
602
+ ? writeDatabases(databases, onBlocked)
603
+ : databases.length
604
+ ? Promise.reject(new Error("No IndexedDB here"))
605
+ : Promise.resolve();
606
+ return writing.then(function () {
607
+ localStorage.clear();
608
+ Object.keys(local).forEach(function (key) {
609
+ localStorage.setItem(key, String(local[key]));
610
+ });
611
+ });
612
+ }
613
+
614
+ // --- Talking to the game page ---
615
+
616
+ function post(message) {
617
+ message.type = "zaisma:saves";
618
+ parent.postMessage(message, site);
619
+ }
620
+ function listen(listener) {
621
+ addEventListener("message", function (event) {
622
+ if (event.origin !== site || event.source !== parent || !event.data || event.data.type !== "zaisma:saves") return;
623
+ listener(event.data);
624
+ });
625
+ }
626
+
627
+ if (/\/_zaisma\/restore$/.test(location.pathname)) {
628
+ // The game starts at its root, with the query the game page passed (like ?room=ABC234).
629
+ var query = decodeURIComponent(location.hash.slice(1));
630
+ var target = new URL("../" + (/^\?[\w=&%-]*$/.test(query) ? query : ""), location.href).href;
631
+ var go = function () {
632
+ location.replace(target);
633
+ };
634
+ // If the game page never answers, play anyway.
635
+ var timer = setTimeout(go, 8000);
636
+ // The game's database is open in another tab: say so here, where the player is looking.
637
+ var blocked = function () {
638
+ post({ step: "blocked" });
639
+ if (document.getElementById("zaisma-blocked")) return;
640
+ var note = document.createElement("p");
641
+ note.id = "zaisma-blocked";
642
+ note.textContent = "This game is open in another tab. Close it there to load your saved progress.";
643
+ note.style.cssText = "margin:0;position:fixed;inset:0;display:grid;place-items:center;padding:24px;color:#d8dde3;font:16px/1.5 system-ui,sans-serif;text-align:center";
644
+ document.body.appendChild(note);
645
+ };
646
+ listen(function (message) {
647
+ // The player is choosing between two saves: wait for them.
648
+ if (message.step === "wait") clearTimeout(timer);
649
+ if (message.step !== "restore") return;
650
+ clearTimeout(timer);
651
+ if (message.action !== "use-cloud" || !message.data || typeof message.data !== "object") return go();
652
+ Promise.resolve(message.data)
653
+ .then(function (save) {
654
+ return putSave(save, blocked);
655
+ })
656
+ .then(
657
+ function () {
658
+ post({ step: "restored", ok: true });
659
+ },
660
+ function () {
661
+ post({ step: "restored", ok: false });
662
+ },
663
+ )
664
+ .then(go);
665
+ });
666
+ // A device whose saves can't be read plays without saving to the account (broken).
667
+ readNumberbText()
668
+ .then(function (idbText) {
669
+ return saveText(snapshot(), idbText);
670
+ })
671
+ .then(
672
+ function (text) {
673
+ post({ step: "hello", phase: "restore", text: text, versionId: versionId });
674
+ },
675
+ function () {
676
+ post({ step: "hello", phase: "restore", text: "", broken: true, versionId: versionId });
677
+ },
678
+ );
679
+ return;
680
+ }
681
+
682
+ // What the game changed since the last check, so a check reads only that again: a store, a whole
683
+ // database (set up anew or deleted) or localStorage. Writes the game's own workers make, and
684
+ // localStorage set as a property, aren't seen, so every 10th check (5 minutes) reads everything.
685
+ var changedStores = {};
686
+ var changedDatabases = {};
687
+ var isLocalChanged = true;
688
+ var isAllChanged = true;
689
+ function watch(prototype, methods, mark) {
690
+ methods.forEach(function (name) {
691
+ var original = prototype && prototype[name];
692
+ if (typeof original !== "function") return;
693
+ prototype[name] = function () {
694
+ try {
695
+ mark(this, arguments);
696
+ } catch (error) {
697
+ isAllChanged = true;
698
+ }
699
+ return original.apply(this, arguments);
700
+ };
701
+ });
702
+ }
703
+ function markStore(store) {
704
+ changedStores[store.transaction.db.name + "\n" + store.name] = true;
705
+ }
706
+ if (window.IDBObjectStore) {
707
+ watch(IDBObjectStore.prototype, ["put", "add", "delete", "clear"], markStore);
708
+ watch(window.IDBCursor && IDBCursor.prototype, ["update", "delete"], function (cursor) {
709
+ markStore(cursor.source.objectStore || cursor.source);
710
+ });
711
+ watch(window.IDBDatabase && IDBDatabase.prototype, ["createObjectStore", "deleteObjectStore"], function (database) {
712
+ changedDatabases[database.name] = true;
713
+ });
714
+ watch(IDBFactory.prototype, ["deleteDatabase"], function (factory, given) {
715
+ changedDatabases[String(given[0])] = true;
716
+ });
717
+ watch(IDBFactory.prototype, ["open"], function (factory, given) {
718
+ if (given.length > 1) changedDatabases[String(given[0])] = true;
719
+ });
720
+ }
721
+ if (window.Storage) {
722
+ watch(Storage.prototype, ["setItem", "removeItem", "clear"], function (storage) {
723
+ if (storage === localStorage) isLocalChanged = true;
724
+ });
725
+ }
726
+ // Another tab of the game changed localStorage.
727
+ addEventListener("storage", function () {
728
+ isLocalChanged = true;
729
+ });
730
+
731
+ // Databases are read in a worker running this same script, so reading and encoding a big save never
732
+ // holds up the game's frames; where a worker can't start or read, the same reader runs here.
733
+ var worker = null;
734
+ var isWorkerTried = false;
735
+ var hereReader = null;
736
+ var waiting = null;
737
+ var readNumber = 0;
738
+ function readDatabasesText(changed, maximumCharacterCount) {
739
+ if (!isWorkerTried) {
740
+ isWorkerTried = true;
741
+ try {
742
+ if (scriptUrl && window.Worker) {
743
+ worker = new Worker(scriptUrl);
744
+ worker.onmessage = function (event) {
745
+ if (waiting && event.data && event.data.id === waiting.id) waiting.done(event.data);
746
+ };
747
+ worker.onerror = function () {
748
+ worker = null;
749
+ if (waiting) waiting.done({ isFailed: true });
750
+ };
751
+ }
752
+ } catch (error) {
753
+ worker = null;
754
+ }
755
+ }
756
+ if (!worker) {
757
+ hereReader = hereReader || databasesReader();
758
+ return hereReader(changed, maximumCharacterCount);
759
+ }
760
+ var id = ++readNumber;
761
+ return new Promise(function (ok) {
762
+ waiting = {
763
+ id: id,
764
+ done: function (answer) {
765
+ waiting = null;
766
+ ok(answer);
767
+ },
768
+ };
769
+ worker.postMessage({ id: id, changed: changed, maximumCharacterCount: maximumCharacterCount });
770
+ }).then(function (answer) {
771
+ if (!answer.isFailed) return answer;
772
+ // The worker can't read here (or stopped): read on the page from now on, from the start.
773
+ if (worker) worker.terminate();
774
+ worker = null;
775
+ hereReader = databasesReader();
776
+ return hereReader({ isAll: true, databases: [], stores: [] }, maximumCharacterCount);
777
+ });
778
+ }
779
+
780
+ var syncing = false;
781
+ var last = null;
782
+ var timer = null;
783
+ var localText = null;
784
+ var idbText = null;
785
+ var checkCount = 0;
786
+ var reading = false;
787
+ var again = false;
788
+ // localStorage this small is read at every check anyway, which also catches it set as a property.
789
+ var SMALL_LOCAL_CHARACTER_COUNT = 64 * 1024;
790
+ /** isLeaving: the player is leaving the tab, so localStorage is read whatever was seen. */
791
+ function check(isLeaving) {
792
+ if (!syncing) return;
793
+ if (reading) {
794
+ again = true;
795
+ return;
796
+ }
797
+ var isAll = isAllChanged || idbText === null || ++checkCount % 10 === 0;
798
+ var changed = { isAll: isAll, databases: Object.keys(changedDatabases), stores: Object.keys(changedStores) };
799
+ var readsIdb = isAll || changed.databases.length > 0 || changed.stores.length > 0;
800
+ var readsLocal = isAll || isLocalChanged || localText === null || isLeaving === true || localText.length < SMALL_LOCAL_CHARACTER_COUNT;
801
+ if (!readsIdb && !readsLocal) return;
802
+ changedStores = {};
803
+ changedDatabases = {};
804
+ isAllChanged = false;
805
+ isLocalChanged = false;
806
+ reading = true;
807
+ if (readsLocal) localText = canonical(snapshot());
808
+ var maximumCharacterCount = saveLimitBytes - localText.length;
809
+ (readsIdb ? readDatabasesText(changed, maximumCharacterCount) : Promise.resolve({ text: idbText }))
810
+ .then(function (answer) {
811
+ if (answer.isFailed) throw new Error("The save couldn't be read");
812
+ if (answer.isOverLimit || localText.length > saveLimitBytes) {
813
+ // Read again in full next time: what's kept stopped partway.
814
+ idbText = null;
815
+ post({ step: "over_limit" });
816
+ return;
817
+ }
818
+ idbText = answer.text;
819
+ var text = textOf(localText, answer.text);
820
+ if (text === last) return;
821
+ last = text;
822
+ post({ step: "save", text: text, versionId: versionId });
823
+ })
824
+ // Nothing is sent when the save can't be read, so a half-read save never replaces a whole one.
825
+ .catch(function () {
826
+ isAllChanged = true;
827
+ localText = null;
828
+ })
829
+ .then(function () {
830
+ reading = false;
831
+ if (again) {
832
+ again = false;
833
+ check(true);
834
+ }
835
+ });
836
+ }
837
+ listen(function (message) {
838
+ if (message.step === "sync") {
839
+ syncing = true;
840
+ last = typeof message.text === "string" ? message.text : null;
841
+ clearInterval(timer);
842
+ timer = setInterval(check, 30000);
843
+ check();
844
+ } else if (message.step === "pause") {
845
+ syncing = false;
846
+ clearInterval(timer);
847
+ } else if (message.step === "apply" && message.data && typeof message.data === "object") {
848
+ apply(message.data);
849
+ }
850
+ });
851
+ // Put a save in place under the running game. A database the game holds open can't be set up again
852
+ // while it runs, so that fails at once (the page reloads the game instead) rather than waiting.
853
+ function apply(save) {
854
+ new Promise(function (ok, fail) {
855
+ putSave(save, function () {
856
+ fail(new Error("The game holds its database open"));
857
+ }).then(ok, fail);
858
+ })
859
+ .then(function () {
860
+ // What's in storage now is the save the page sent: the next check has nothing new to send.
861
+ return readNumberbText();
862
+ })
863
+ .then(
864
+ function (freshIdbText) {
865
+ idbText = freshIdbText;
866
+ localText = canonical(snapshot());
867
+ // The worker's kept parts are from before: the next check reads everything (and finds nothing new).
868
+ isAllChanged = true;
869
+ last = textOf(localText, freshIdbText);
870
+ post({ step: "applied", ok: true });
871
+ },
872
+ function () {
873
+ post({ step: "applied", ok: false });
874
+ },
875
+ );
876
+ }
877
+ document.addEventListener("visibilitychange", function () {
878
+ if (document.visibilityState === "hidden") check(true);
879
+ });
880
+ addEventListener("pagehide", function () {
881
+ check(true);
882
+ });
883
+ post({ step: "hello", phase: "game", versionId: versionId });
884
+ })();