@zaisma/game-server 1.3.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 +71 -5
  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/bin/node.js CHANGED
@@ -178,18 +178,19 @@ export function startServer(options) {
178
178
  // for its own databases. Answers 204 when that and onWebhook finished, so Zaisma counts it delivered;
179
179
  // anything else is tried again later.
180
180
  function answerWebhook(request, response) {
181
- const reply = (httpStatus, message) => {
182
- response.writeHead(httpStatus, { "Content-Type": "text/plain", "Cache-Control": "no-store" });
181
+ const reply = (httpStatus, message, isClosing = false) => {
182
+ response.writeHead(httpStatus, { "Content-Type": "text/plain", "Cache-Control": "no-store", ...(isClosing ? { Connection: "close" } : {}) });
183
183
  response.end(message);
184
184
  };
185
185
  if (request.method !== "POST") return reply(405, "Webhooks are POSTed.");
186
186
  if (!webhookSigningSecret) return reply(503, "Set ZAISMA_WEBHOOK_SIGNING_SECRET to the webhook's signing_secret to take its events.");
187
187
  const header = (name) => (typeof request.headers[name] === "string" ? request.headers[name] : null);
188
188
  const headers = { webhookId: header("webhook-id"), webhookTimestamp: header("webhook-timestamp"), webhookSignature: header("webhook-signature") };
189
- // A POST with no headers, or an old timestamp, is turned away before its body is read.
189
+ // A POST with no headers, or an old timestamp, is turned away before its body is read. Its connection
190
+ // closes, and says so, so the sender doesn't send its next request down a connection that's gone.
190
191
  const refuse = (httpStatus, error) => {
191
192
  response.once("finish", () => request.destroy());
192
- reply(httpStatus, `${error.message} ${error.fix}`);
193
+ reply(httpStatus, `${error.message} ${error.fix}`, true);
193
194
  };
194
195
  const chunks = [];
195
196
  let sizeBytes = 0;
@@ -690,7 +691,7 @@ function binaryBytes(received) {
690
691
  return new Uint8Array(received.buffer.slice(received.byteOffset, received.byteOffset + received.byteLength));
691
692
  }
692
693
 
693
- const USAGE = "Usage: zaisma-game-server <server.ts | relay> --game <slug> [--port 8080] [--origin <url>,<url>] [--issuer-url https://rooms.zaisma.com] [--database-file zaisma-database.sqlite] [--room-state-file zaisma-room-states.sqlite]";
694
+ const USAGE = "Usage: zaisma-game-server <server.ts | relay> --game <slug> [--port 8080] [--origin <url>,<url>] [--issuer-url https://rooms.zaisma.com] [--database-file zaisma-database.sqlite] [--room-state-file zaisma-room-states.sqlite]\n or: zaisma-game-server play-locally [folder] [--port 4800] [--game <slug>] [--on-network] [--no-open] [--json]";
694
695
  const FLAGS = ["game", "port", "origin", "issuer-url", "database-file", "room-state-file"];
695
696
 
696
697
  /** The command line's flags by name, or null for one this runner doesn't take (or one missing its value). */
@@ -704,8 +705,73 @@ function flagsFrom(argumentList) {
704
705
  return flags;
705
706
  }
706
707
 
708
+ /** play-locally's flags: whether each takes a value. */
709
+ const PLAY_LOCALLY_FLAGS = { port: true, game: true, "on-network": false, "no-open": false, json: false };
710
+
711
+ /** play-locally [folder] and its flags, or null with the refusal printed: said plainly, before anything listens. */
712
+ function playLocallyArguments(argumentList) {
713
+ const refuse = (message, fix) => {
714
+ console.error(`${message} ${fix} (bad_option)`);
715
+ return null;
716
+ };
717
+ const flags = {};
718
+ let folder = null;
719
+ for (let i = 0; i < argumentList.length; i++) {
720
+ const given = argumentList[i];
721
+ if (!given.startsWith("--")) {
722
+ if (folder !== null) return refuse(`play-locally takes one folder, and got ${folder} and ${given}.`, "Give the folder of your game, the one with index.html at the top.");
723
+ folder = given;
724
+ continue;
725
+ }
726
+ const name = given.slice(2);
727
+ if (!(name in PLAY_LOCALLY_FLAGS)) return refuse(`${given} isn't a play-locally flag.`, `play-locally takes: ${Object.keys(PLAY_LOCALLY_FLAGS).map((each) => `--${each}`).join(", ")}.`);
728
+ if (!PLAY_LOCALLY_FLAGS[name]) {
729
+ flags[name] = true;
730
+ continue;
731
+ }
732
+ const flagValue = argumentList[++i];
733
+ if (flagValue === undefined || flagValue.startsWith("--")) return refuse(`${given} needs a value.`, name === "port" ? "Give the first of four ports, like --port 4800." : "Give your game's slug, like --game sky-hopper.");
734
+ flags[name] = flagValue;
735
+ }
736
+ const portNumber = flags.port === undefined ? 4800 : Number(flags.port);
737
+ if (!Number.isInteger(portNumber) || portNumber < 1 || portNumber > 65532) return refuse("--port isn't a whole number from 1 to 65532.", "Give the first of four ports, like --port 4800: the bench, the game's site, the stand-in rooms and your game server take it and the next three.");
738
+ if (flags.game !== undefined && !/^[a-z0-9-]{1,40}$/.test(flags.game)) return refuse("--game isn't a game's slug.", "A slug is 1 to 40 lowercase letters, digits or hyphens, like sky-hopper.");
739
+ return { folder: folder ?? ".", portNumber, gameSlug: flags.game ?? null, isOnNetwork: flags["on-network"] === true, isOpening: flags["no-open"] !== true, isJson: flags.json === true };
740
+ }
741
+
742
+ /** zaisma-game-server play-locally: your game and its game server on this computer, with a test bench (bin/local-play.js). */
743
+ async function playLocally(argumentList) {
744
+ const given = playLocallyArguments(argumentList);
745
+ if (!given) process.exit(1);
746
+ const { startLocalPlay, terminalQr } = await import("./local-play.js");
747
+ const say = given.isJson ? (line) => console.error(line) : (line) => console.log(line);
748
+ let running;
749
+ try {
750
+ running = await startLocalPlay({ gameFolder: given.folder, port: given.portNumber, isOnNetwork: given.isOnNetwork, gameSlug: given.gameSlug, log: say });
751
+ } catch (error) {
752
+ console.error(error instanceof GameServerError ? `${error.message} ${error.fix ?? ""} (${error.code})` : error);
753
+ process.exit(1);
754
+ }
755
+ for (const signal of ["SIGTERM", "SIGINT"]) process.once(signal, () => running.close().then(() => process.exit(0)));
756
+ if (given.isJson) console.log(JSON.stringify({ bench_url: running.benchUrl, game_site_url: running.gameSiteUrl, rooms_url: running.roomsUrl, network_url: running.networkUrl }));
757
+ say(`Playing ${given.gameSlug ?? "your game"} on this computer. The test bench is at ${running.benchUrl}`);
758
+ say("Each player plays on Zaisma's own game page. Add a player in the dev bar to play with yourself in another tab. A change to a game file reloads the game, and a change to the game server starts it again with its rooms kept.");
759
+ say("Nothing here talks to Zaisma. What the bench keeps is in .zaisma-local/ in the game's folder: add it to your .gitignore. Press Ctrl-C to stop.");
760
+ if (running.networkUrl) {
761
+ say(`On a phone on the same Wi-Fi: ${running.networkUrl}`);
762
+ say(terminalQr(running.networkUrl));
763
+ say("Your phone will say the connection isn't private, because the bench signs its own certificate. Continue to the site.");
764
+ }
765
+ if (given.isOpening) {
766
+ const { spawn } = await import("node:child_process");
767
+ const opener = process.platform === "darwin" ? ["open", [running.benchUrl]] : process.platform === "win32" ? ["cmd", ["/c", "start", "", running.benchUrl]] : ["xdg-open", [running.benchUrl]];
768
+ spawn(opener[0], opener[1], { stdio: "ignore", detached: true }).on("error", () => {}).unref();
769
+ }
770
+ }
771
+
707
772
  // Run as a command. An npm bin is a symlink to this file, so both sides are resolved before they're compared.
708
773
  async function runCommand() {
774
+ if (process.argv[2] === "play-locally") return playLocally(process.argv.slice(3));
709
775
  const file = process.argv[2];
710
776
  const flags = file ? flagsFrom(process.argv.slice(3)) : null;
711
777
  if (!file || file.startsWith("--") || !flags?.game) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zaisma/game-server",
3
- "version": "1.3.0",
3
+ "version": "1.5.0",
4
4
  "description": "Run your Zaisma game server: the same rooms on Zaisma or on your own server.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://zaisma.com",
package/sdk/picture.js ADDED
@@ -0,0 +1,97 @@
1
+ /*
2
+ * Report pictures. Zaisma adds this to every game page. When a player opens Report on the Zaisma
3
+ * game page, the page asks for one picture of the game as it is right now: the game's biggest
4
+ * canvas, copied straight after the game draws a frame (so WebGL games work too), shrunk and
5
+ * handed back (unless it's all one color). Nothing is taken unless the Zaisma page asks, and nothing leaves the player's
6
+ * browser unless they send the report with the picture. Games that don't draw on a canvas give
7
+ * no picture. Plain JavaScript, no dependencies, MIT licensed.
8
+ */
9
+ (function () {
10
+ "use strict";
11
+ var site = typeof __ZAISMA_PICTURE__ === "object" && __ZAISMA_PICTURE__ ? __ZAISMA_PICTURE__.site : "";
12
+ if (!site || window.parent === window) return;
13
+ var MAXIMUM_SIDE_PIXEL_COUNT = 1280;
14
+ var waiting = [];
15
+
16
+ function biggestCanvas() {
17
+ var best = null;
18
+ var bestPixelCount = 0;
19
+ var all = document.getElementsByTagName("canvas");
20
+ for (var i = 0; i < all.length; i++) {
21
+ var pixelCount = all[i].width * all[i].height;
22
+ if (pixelCount > bestPixelCount && all[i].getClientRects().length) {
23
+ best = all[i];
24
+ bestPixelCount = pixelCount;
25
+ }
26
+ }
27
+ return best;
28
+ }
29
+
30
+ // A canvas that's all one color (a title screen drawn in HTML over it, a game still loading) shows nothing worth sending.
31
+ function blank(canvas) {
32
+ var small = document.createElement("canvas");
33
+ small.width = 32;
34
+ small.height = 18;
35
+ var canvasContext = small.getContext("2d");
36
+ canvasContext.drawImage(canvas, 0, 0, 32, 18);
37
+ var pixelBytes = canvasContext.getImageData(0, 0, 32, 18).data;
38
+ var darkestByte = 255, lightestByte = 0;
39
+ for (var i = 0; i < pixelBytes.length; i += 4) {
40
+ var lightnessByte = (pixelBytes[i] * 3 + pixelBytes[i + 1] * 6 + pixelBytes[i + 2]) / 10;
41
+ if (lightnessByte < darkestByte) darkestByte = lightnessByte;
42
+ if (lightnessByte > lightestByte) lightestByte = lightnessByte;
43
+ }
44
+ return lightestByte - darkestByte < 10;
45
+ }
46
+
47
+ function picture() {
48
+ try {
49
+ var from = biggestCanvas();
50
+ if (!from) return null;
51
+ var longestSidePixelCount = Math.max(from.width, from.height);
52
+ var shrunkSidePixelCount = Math.min(longestSidePixelCount, MAXIMUM_SIDE_PIXEL_COUNT);
53
+ var to = document.createElement("canvas");
54
+ to.width = Math.max(1, Math.round((from.width * shrunkSidePixelCount) / longestSidePixelCount));
55
+ to.height = Math.max(1, Math.round((from.height * shrunkSidePixelCount) / longestSidePixelCount));
56
+ var canvasContext = to.getContext("2d");
57
+ canvasContext.fillStyle = "#000";
58
+ canvasContext.fillRect(0, 0, to.width, to.height);
59
+ canvasContext.drawImage(from, 0, 0, to.width, to.height);
60
+ return blank(to) ? null : to.toDataURL("image/jpeg", 0.8);
61
+ } catch (error) {
62
+ return null; // A canvas showing pictures from another site can't be copied.
63
+ }
64
+ }
65
+
66
+ function answer() {
67
+ if (!waiting.length) return;
68
+ var pictureUrl = picture();
69
+ waiting.splice(0).forEach(function (id) {
70
+ window.parent.postMessage({ type: "zaisma:picture", id: id, picture: pictureUrl }, site);
71
+ });
72
+ }
73
+
74
+ // Copy inside the game's own frame callback: after it has drawn, before the browser clears a WebGL canvas.
75
+ var nativeFrame = window.requestAnimationFrame;
76
+ if (nativeFrame) {
77
+ window.requestAnimationFrame = function (callback) {
78
+ return nativeFrame.call(window, function (frameTimeMs) {
79
+ try {
80
+ callback(frameTimeMs);
81
+ } finally {
82
+ answer();
83
+ }
84
+ });
85
+ };
86
+ }
87
+
88
+ window.addEventListener("message", function (event) {
89
+ if (event.origin !== site || event.source !== window.parent || !event.data || event.data.type !== "zaisma:picture-request") return;
90
+ var id = String(event.data.id);
91
+ waiting.push(id);
92
+ // A game that isn't drawing right now (paused, between turns) is copied straight away.
93
+ setTimeout(function () {
94
+ if (waiting.indexOf(id) !== -1) answer();
95
+ }, 200);
96
+ });
97
+ })();
package/sdk/relay.js ADDED
@@ -0,0 +1,269 @@
1
+ /*
2
+ * Zaisma's relay. Zaisma adds this first to every game page. A connection between two players'
3
+ * browsers (WebRTC, what PeerJS and Godot use) would show each the other's internet address, so
4
+ * every one goes through Cloudflare's relay (TURN) instead: players only ever see Cloudflare's
5
+ * address, and connections also get through school and office networks where direct ones fail.
6
+ * The game's own WebRTC code works unchanged: this wraps RTCPeerConnection so each connection uses
7
+ * only the relay, with Zaisma's relay servers, whatever servers the game asks for (the game's own
8
+ * relay stays when it's one of the game's listed servers). Plain JavaScript, no dependencies, MIT licensed.
9
+ */
10
+ (function (window) {
11
+ "use strict";
12
+ var config = typeof __ZAISMA_RELAY__ === "object" && __ZAISMA_RELAY__ ? __ZAISMA_RELAY__ : {};
13
+ var NAMES = ["RTCPeerConnection", "webkitRTCPeerConnection"];
14
+ var ownRelayHosts = (config.ownRelayHosts || []).map(function (host) { return String(host).toLowerCase(); });
15
+ /** Ask again this long after the relay couldn't be had, not on every connection. */
16
+ var RETRY_AFTER_MS = 60 * 1000;
17
+ /** Credentials with less left than this are fetched anew. */
18
+ var RENEW_BEFORE_MS = 10 * 60 * 1000;
19
+ var storageKey = "zaisma:relay:" + config.game;
20
+
21
+ /** What this window wrapped: our getters (so nothing is wrapped twice), and each original's wrapper. */
22
+ var getters = new WeakSet();
23
+ var wrappers = new WeakMap();
24
+ /** The configuration the game asked each connection for, and the relay servers it was given. */
25
+ var askedConfigurations = new WeakMap();
26
+ var givenServers = new WeakMap();
27
+
28
+ // ---- Zaisma's relay servers for this tab ----
29
+
30
+ var kept = null; // { iceServers, expiresAtMs }
31
+ var fetching = null;
32
+ var failedAtMs = 0;
33
+ var hasWarned = false;
34
+
35
+ function warn(message) {
36
+ if (hasWarned) return;
37
+ hasWarned = true;
38
+ window.console.warn("Zaisma: connections between players go only through Zaisma's relay, so they don't connect right now. " + message);
39
+ }
40
+
41
+ function isFresh(credentials) {
42
+ return Boolean(credentials && credentials.iceServers && credentials.expiresAtMs - Date.now() > RENEW_BEFORE_MS);
43
+ }
44
+
45
+ function storedCredentials() {
46
+ try {
47
+ var stored = JSON.parse(window.sessionStorage.getItem(storageKey) || "null");
48
+ return isFresh(stored) ? stored : null;
49
+ } catch (error) {
50
+ return null; // storage off: fetch new ones
51
+ }
52
+ }
53
+
54
+ /** The game page's key to player passes, which also gives relay credentials (the SDK asks for it the same way). */
55
+ function roomKey() {
56
+ return new Promise(function (resolve, reject) {
57
+ if (!config.site || !config.rooms || window.parent === window) return reject(new Error("Connections between players work when the game is played on Zaisma."));
58
+ var timerNumber = window.setTimeout(function () {
59
+ window.removeEventListener("message", onMessage);
60
+ reject(new Error("Zaisma couldn't check this browser. Reload the game to try again."));
61
+ }, 30000);
62
+ function onMessage(event) {
63
+ if (event.origin !== config.site || event.source !== window.parent || !event.data || event.data.type !== "zaisma:room-key") return;
64
+ window.removeEventListener("message", onMessage);
65
+ window.clearTimeout(timerNumber);
66
+ if (typeof event.data.key === "string") resolve(event.data.key);
67
+ else reject(new Error("Zaisma couldn't check this browser, or this game doesn't use online play. Reload the game to try again."));
68
+ }
69
+ window.addEventListener("message", onMessage);
70
+ window.parent.postMessage({ type: "zaisma:room-key" }, config.site);
71
+ });
72
+ }
73
+
74
+ /** Zaisma's relay servers, as iceServers: [] when there are none to be had (connections then don't connect, rather than go direct). */
75
+ function relayServers() {
76
+ if (isFresh(kept)) return Promise.resolve(kept.iceServers);
77
+ var stored = storedCredentials();
78
+ if (stored) {
79
+ kept = stored;
80
+ return Promise.resolve(kept.iceServers);
81
+ }
82
+ if (Date.now() - failedAtMs < RETRY_AFTER_MS) return Promise.resolve([]);
83
+ if (!fetching) {
84
+ fetching = roomKey()
85
+ .then(function (key) {
86
+ // text/plain keeps this a simple request, so the browser needs no preflight.
87
+ return window.fetch(config.rooms + "/v1/relay", { method: "POST", headers: { "Content-Type": "text/plain" }, body: JSON.stringify({ gameSlug: config.game, roomKey: key }) });
88
+ })
89
+ .then(function (response) {
90
+ return response.json().then(function (answer) {
91
+ if (!response.ok || !answer || !Array.isArray(answer.iceServers)) throw new Error((answer && answer.error && answer.error.message) || "Zaisma's relay didn't answer.");
92
+ return answer;
93
+ });
94
+ })
95
+ .then(
96
+ function (answer) {
97
+ fetching = null;
98
+ kept = { iceServers: answer.iceServers, expiresAtMs: Date.parse(answer.expiresAt) };
99
+ try { window.sessionStorage.setItem(storageKey, JSON.stringify(kept)); } catch (error) { /* fine */ }
100
+ return kept.iceServers;
101
+ },
102
+ function (error) {
103
+ fetching = null;
104
+ failedAtMs = Date.now();
105
+ warn(error && error.message ? error.message : "");
106
+ return [];
107
+ },
108
+ );
109
+ }
110
+ return fetching;
111
+ }
112
+
113
+ // ---- What each connection is given ----
114
+
115
+ /** The game's own relay servers among its listed servers: turn: and turns: addresses on those hosts, nothing else. */
116
+ function ownRelays(iceServers) {
117
+ var out = [];
118
+ (Array.isArray(iceServers) ? iceServers : []).forEach(function (server) {
119
+ if (!server || typeof server !== "object") return;
120
+ var urls = [].concat(server.urls !== undefined ? server.urls : server.url !== undefined ? server.url : []).filter(function (address) {
121
+ var host = /^turns?:(\[[^\]]+\]|[^:?\/]+)/i.exec(String(address));
122
+ return host !== null && ownRelayHosts.indexOf(host[1].toLowerCase()) !== -1;
123
+ });
124
+ if (urls.length) out.push({ urls: urls, username: server.username, credential: server.credential });
125
+ });
126
+ return out;
127
+ }
128
+
129
+ /** A configuration from base that only relays, through Zaisma's servers and the game's own relay. */
130
+ function relayedConfiguration(base, asked, servers) {
131
+ var out = {};
132
+ for (var name in base) if (Object.prototype.hasOwnProperty.call(base, name)) out[name] = base[name];
133
+ out.iceTransportPolicy = "relay";
134
+ out.iceServers = servers.concat(ownRelays(asked.iceServers));
135
+ // Gathering ahead of time waits until there are servers to gather from.
136
+ if (!servers.length) out.iceCandidatePoolSize = 0;
137
+ else if (asked.iceCandidatePoolSize !== undefined) out.iceCandidatePoolSize = asked.iceCandidatePoolSize;
138
+ return out;
139
+ }
140
+
141
+ function define(target, name, method) {
142
+ Object.defineProperty(target, name, { value: method, writable: true, configurable: true, enumerable: false });
143
+ }
144
+
145
+ /** The wrapper for one window's RTCPeerConnection, and its prototype's calls made to relay first. */
146
+ function wrap(Native) {
147
+ var proto = Native.prototype;
148
+ var nativeSetConfiguration = proto.setConfiguration;
149
+ var nativeGetConfiguration = proto.getConfiguration;
150
+
151
+ function RTCPeerConnection(configuration) {
152
+ if (!new.target) throw new TypeError("Failed to construct 'RTCPeerConnection': Please use the 'new' operator.");
153
+ var asked = configuration && typeof configuration === "object" ? configuration : {};
154
+ var servers = isFresh(kept) ? kept.iceServers : [];
155
+ var parts = Array.prototype.slice.call(arguments);
156
+ parts[0] = relayedConfiguration(asked, asked, servers);
157
+ var connection = Reflect.construct(Native, parts, new.target);
158
+ askedConfigurations.set(connection, asked);
159
+ if (servers.length) givenServers.set(connection, servers);
160
+ // Fetched now, so they're there by the game's first offer or answer.
161
+ if (!servers.length) relayServers();
162
+ return connection;
163
+ }
164
+ RTCPeerConnection.prototype = proto;
165
+ Object.setPrototypeOf(RTCPeerConnection, Native);
166
+ define(proto, "constructor", RTCPeerConnection);
167
+
168
+ /**
169
+ * Before a connection gathers addresses (its first offer or answer): Zaisma's relay servers, and
170
+ * a check that it only relays. Covers every connection in this window, however it was made.
171
+ */
172
+ function relayFirst(connection) {
173
+ return relayServers().then(function (servers) {
174
+ if (connection.signalingState === "closed") return; // the call itself says so
175
+ var current = nativeGetConfiguration.call(connection);
176
+ // Servers it already has stay when new ones couldn't be had.
177
+ var use = servers.length ? servers : givenServers.get(connection) || [];
178
+ if (givenServers.get(connection) !== use || current.iceTransportPolicy !== "relay") {
179
+ try {
180
+ nativeSetConfiguration.call(connection, relayedConfiguration(current, askedConfigurations.get(connection) || current, use));
181
+ givenServers.set(connection, use);
182
+ } catch (error) {
183
+ /* checked just below */
184
+ }
185
+ }
186
+ if (nativeGetConfiguration.call(connection).iceTransportPolicy !== "relay") {
187
+ throw new DOMException("On Zaisma, connections between players go only through Zaisma's relay, and this one couldn't be set to.", "InvalidStateError");
188
+ }
189
+ });
190
+ }
191
+ ["createOffer", "createAnswer", "setLocalDescription"].forEach(function (name) {
192
+ var native = proto[name];
193
+ if (typeof native !== "function") return;
194
+ define(proto, name, function () {
195
+ var connection = this;
196
+ var parts = arguments;
197
+ return relayFirst(connection).then(function () { return native.apply(connection, parts); });
198
+ });
199
+ });
200
+ if (typeof nativeSetConfiguration === "function") {
201
+ // A game changing its servers keeps Zaisma's relay.
202
+ define(proto, "setConfiguration", function (configuration) {
203
+ var asked = configuration && typeof configuration === "object" ? configuration : {};
204
+ askedConfigurations.set(this, asked);
205
+ var servers = givenServers.get(this) || [];
206
+ return nativeSetConfiguration.call(this, relayedConfiguration(asked, asked, servers));
207
+ });
208
+ }
209
+ wrappers.set(Native, RTCPeerConnection);
210
+ return RTCPeerConnection;
211
+ }
212
+
213
+ // ---- Every window the game can reach ----
214
+
215
+ function install(target) {
216
+ try {
217
+ if (!target) return;
218
+ NAMES.forEach(function (name) {
219
+ var descriptor = Object.getOwnPropertyDescriptor(target, name);
220
+ if (!descriptor || (descriptor.get && getters.has(descriptor.get))) return;
221
+ var Native = target[name];
222
+ if (typeof Native !== "function") return;
223
+ var Relayed = wrappers.get(Native) || wrap(Native);
224
+ var current = Relayed;
225
+ var get = function () { return current; };
226
+ getters.add(get);
227
+ // Libraries like webrtc-adapter put their own wrapper around this one; nothing puts the original back.
228
+ Object.defineProperty(target, name, { configurable: true, enumerable: false, get: get, set: function (assigned) { current = assigned === Native ? Relayed : assigned; } });
229
+ });
230
+ watchFrames(target);
231
+ } catch (error) {
232
+ /* another site's window: nothing of it is reachable from here */
233
+ }
234
+ }
235
+
236
+ /** A frame the game makes has its own RTCPeerConnection: it's wrapped the moment the game reaches into the frame. */
237
+ function watchFrames(target) {
238
+ ["HTMLIFrameElement", "HTMLFrameElement", "HTMLObjectElement"].forEach(function (name) {
239
+ var Element = target[name];
240
+ if (typeof Element !== "function") return;
241
+ ["contentWindow", "contentDocument"].forEach(function (property) {
242
+ var descriptor = Object.getOwnPropertyDescriptor(Element.prototype, property);
243
+ if (!descriptor || !descriptor.get || getters.has(descriptor.get)) return;
244
+ var nativeGet = descriptor.get;
245
+ var get = function () {
246
+ var reached = nativeGet.call(this);
247
+ if (reached) install(property === "contentDocument" ? reached.defaultView : reached);
248
+ return reached;
249
+ };
250
+ getters.add(get);
251
+ Object.defineProperty(Element.prototype, property, { configurable: true, enumerable: descriptor.enumerable, get: get });
252
+ });
253
+ });
254
+ var nativeOpen = target.open;
255
+ if (typeof nativeOpen === "function" && !getters.has(nativeOpen)) {
256
+ var open = function () {
257
+ var opened = nativeOpen.apply(this, arguments);
258
+ install(opened);
259
+ return opened;
260
+ };
261
+ getters.add(open);
262
+ target.open = open;
263
+ }
264
+ }
265
+
266
+ install(window);
267
+ // A game that plays online gets its relay servers while it loads, so its first connection doesn't wait.
268
+ if (config.fetchEarly && NAMES.some(function (name) { return typeof window[name] === "function"; })) relayServers();
269
+ })(window);