@zaisma/game-server 1.10.0 → 1.12.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 (46) hide show
  1. package/README.md +9 -0
  2. package/bench/brand/README.md +22 -17
  3. package/bench/brand/app-icon-1024.png +0 -0
  4. package/bench/brand/app-icon-192.png +0 -0
  5. package/bench/brand/app-icon-512.png +0 -0
  6. package/bench/brand/app-icon-dark-512.png +0 -0
  7. package/bench/brand/app-icon-maskable-512.png +0 -0
  8. package/bench/brand/apple-touch-icon.png +0 -0
  9. package/bench/brand/favicon-32.png +0 -0
  10. package/bench/brand/favicon.ico +0 -0
  11. package/bench/brand/favicon.svg +1 -1
  12. package/bench/brand/manifest.webmanifest +26 -0
  13. package/bench/brand/share-card-dark.png +0 -0
  14. package/bench/brand/share-card-dark.svg +1 -0
  15. package/bench/brand/share-card-light.png +0 -0
  16. package/bench/brand/share-card-light.svg +1 -0
  17. package/bench/brand/stripe-icon.png +0 -0
  18. package/bench/brand/zaisma-icon-dark.svg +1 -0
  19. package/bench/brand/zaisma-icon-light.svg +1 -0
  20. package/bench/brand/zaisma-mark-glass-black-2048.png +0 -0
  21. package/bench/brand/zaisma-mark-glass-black.svg +1 -0
  22. package/bench/brand/zaisma-mark-glass-white-2048.png +0 -0
  23. package/bench/brand/zaisma-mark-glass-white.svg +1 -0
  24. package/bench/brand/zaisma-wordmark-glass-black-2400.png +0 -0
  25. package/bench/brand/zaisma-wordmark-glass-black.svg +1 -0
  26. package/bench/brand/zaisma-wordmark-glass-white-2400.png +0 -0
  27. package/bench/brand/zaisma-wordmark-glass-white.svg +1 -0
  28. package/bench/glass.js +3904 -1907
  29. package/bin/fast-lane.js +212 -0
  30. package/bin/local-play-certificate.js +11 -6
  31. package/bin/node-parts.js +9 -1
  32. package/bin/node.d.ts +12 -0
  33. package/bin/node.js +59 -15
  34. package/package.json +13 -1
  35. package/sdk/zaisma.d.ts +53 -7
  36. package/sdk/zaisma.js +196 -8
  37. package/src/qr-code.d.ts +5 -4
  38. package/src/qr-code.js +11 -10
  39. package/src/room.d.ts +18 -3
  40. package/src/room.js +12 -12
  41. package/bench/brand/zaisma-app-icon.svg +0 -1
  42. package/bench/brand/zaisma-mark-metal-2048.png +0 -0
  43. package/bench/brand/zaisma-mark-metal.svg +0 -1
  44. package/bench/brand/zaisma-mark-tricolor.svg +0 -1
  45. package/bench/brand/zaisma-wordmark-metal-2400.png +0 -0
  46. package/bench/brand/zaisma-wordmark-metal.svg +0 -1
package/sdk/zaisma.js CHANGED
@@ -18,16 +18,20 @@
18
18
  var OWN_SERVER_URL = /^(wss|https):\/\/[^/]/;
19
19
 
20
20
  // What each call takes. Anything else is refused, so a misspelled or old option never goes quietly unused.
21
- var JOIN_OPTIONS = ["roomCode", "serverUrl", "isListed", "friendJoinsEnabled", "name", "labels", "joinData", "idleTimeoutSec", "idleCheckEnabled", "isSpectator"];
21
+ var JOIN_OPTIONS = ["roomCode", "serverUrl", "transport", "isListed", "friendJoinsEnabled", "name", "labels", "joinData", "idleTimeoutSec", "idleCheckEnabled", "isSpectator"];
22
22
  // quickJoin finds Zaisma rooms, so it takes no serverUrl.
23
23
  var QUICK_JOIN_OPTIONS = ["isListed", "friendJoinsEnabled", "name", "labels", "joinData", "idleTimeoutSec", "idleCheckEnabled", "region"];
24
24
  var LIST_OPTIONS = ["region", "labels"];
25
- // move.join joins the room the move names, on this room's server, as a player: the game server moves only players.
26
- var MOVE_JOIN_OPTIONS = JOIN_OPTIONS.filter(function (name) { return name !== "roomCode" && name !== "serverUrl" && name !== "isSpectator"; });
25
+ // move.join joins the room the move names, on this room's server and over its transport, as a player: the game server moves only players.
26
+ var MOVE_JOIN_OPTIONS = JOIN_OPTIONS.filter(function (name) { return name !== "roomCode" && name !== "serverUrl" && name !== "transport" && name !== "isSpectator"; });
27
27
  var KICK_OPTIONS = ["keepOut"];
28
28
  var SEND_OPTIONS = ["delivery", "latestKey"];
29
29
  var SMOOTHING_OPTIONS = ["delayMs"];
30
30
  var DELIVERIES = ["reliable", "latest_only"];
31
+ /** How a room on the game's own server is reached: the WebSocket, or the fast lane (WebTransport), which carries latest-only sends as datagrams. */
32
+ var TRANSPORTS = ["websocket", "webtransport"];
33
+ /** A fast lane that isn't up in this long gives way to the WebSocket, for the rest of the room's life. */
34
+ var FAST_LANE_CONNECT_TIMEOUT_MS = 3000;
31
35
  var ROOM_INFO_FIELDS = ["name", "labels"];
32
36
  var ROOM_EVENTS = ["message", "bytes", "state", "players", "connectionStatus", "ended", "ending", "endingCancelled", "kicked", "settings", "move", "moved", "idle", "away", "back", "muted", "error", "versionUpdate", "chat"];
33
37
  /** Chat (room.chat): the longest message, the lines a room keeps in memory for reports, and how many of them a report carries. */
@@ -37,6 +41,7 @@
37
41
  var SAVES_EVENTS = ["conflict", "developerChange"];
38
42
  var ITEMS_EVENTS = ["purchase", "refund", "sheetOpen", "sheetClose"];
39
43
  var VERSIONS_EVENTS = ["update"];
44
+ var PAGE_EVENTS = ["coveredBy"];
40
45
  var BUY_OPTIONS = ["itemCount", "gameAccountId"];
41
46
  /** The room messages this SDK speaks (zaisma.d.ts: the wire protocol), sent in the hello. */
42
47
  var PROTOCOL_VERSION_NUMBER = 1;
@@ -424,6 +429,11 @@
424
429
  this.isSpectator = setup.isSpectator === true;
425
430
  this.spectatorCount = 0;
426
431
  this._url = setup.url;
432
+ // The fast lane's address (joinRoom({ transport: "webtransport" }) on an own server), or null, and whether this room gave it up for the WebSocket.
433
+ this._fastLaneUrl = setup.fastLaneUrl || null;
434
+ this._isFastLaneOff = false;
435
+ // What this connection uses now: "webtransport" once the fast lane opens, else "websocket".
436
+ this.transport = "websocket";
427
437
  this._pass = setup.pass;
428
438
  this._made = { isListed: this.isListed, friendJoinsEnabled: setup.friendJoinsEnabled === false ? false : undefined, name: setup.name, labels: setup.labels };
429
439
  this._joinData = setup.joinData === undefined ? null : setup.joinData;
@@ -472,6 +482,135 @@
472
482
  }
473
483
  Room.prototype = Object.create(Emitter.prototype);
474
484
 
485
+ /**
486
+ * The fast lane to the game's own server (WebTransport), with what the room uses on a WebSocket: readyState,
487
+ * bufferedAmount, send(text or bytes), close(code, reason), onopen, onmessage and onclose; and sendDatagram
488
+ * for latest-only sends, and onfail when it never came up. One stream carries every frame in order, each
489
+ * [4-byte big-endian payload length][1-byte kind: 0 text, 1 bytes][payload]; a datagram is [kind][payload]
490
+ * (the wire: packages/game-server/src/room.ts, above ServerMessage).
491
+ */
492
+ function FastLane(url) {
493
+ var lane = this;
494
+ this.readyState = 0;
495
+ this.bufferedAmount = 0;
496
+ this.onopen = this.onmessage = this.onclose = this.onfail = null;
497
+ this._writer = null;
498
+ this._datagrams = null;
499
+ this._datagramWriter = null;
500
+ var session;
501
+ function fail(reason) {
502
+ if (lane.readyState !== 0) return;
503
+ lane.readyState = 3;
504
+ clearTimeout(timer);
505
+ try { session.close(); } catch (error) { /* fine */ }
506
+ if (lane.onfail) lane.onfail(reason);
507
+ }
508
+ var timer = setTimeout(function () { fail("no answer in " + FAST_LANE_CONNECT_TIMEOUT_MS / 1000 + " seconds"); }, FAST_LANE_CONNECT_TIMEOUT_MS);
509
+ try {
510
+ session = new WebTransport(url);
511
+ } catch (error) {
512
+ session = null;
513
+ // Called from the room's _connect: it hears the failure once that has set it up.
514
+ setTimeout(function () { fail(error && error.message ? error.message : "refused"); }, 0);
515
+ return;
516
+ }
517
+ this._session = session;
518
+ session.ready.then(function () { return session.createBidirectionalStream(); }).then(function (stream) {
519
+ if (lane.readyState !== 0) return;
520
+ clearTimeout(timer);
521
+ lane._writer = stream.writable.getWriter();
522
+ lane._datagrams = session.datagrams;
523
+ lane._datagramWriter = (typeof session.datagrams.createWritable === "function" ? session.datagrams.createWritable() : session.datagrams.writable).getWriter();
524
+ lane.readyState = 1;
525
+ session.closed.then(function (closeInfo) { lane._closed(closeInfo && typeof closeInfo.closeCode === "number" ? closeInfo.closeCode : 1005, closeInfo && closeInfo.reason ? closeInfo.reason : ""); }, function () { lane._closed(1006, ""); });
526
+ lane._read(stream.readable);
527
+ lane._readDatagrams(session.datagrams.readable);
528
+ if (lane.onopen) lane.onopen();
529
+ }, function (error) {
530
+ fail(error && error.message ? error.message : "refused");
531
+ });
532
+ }
533
+ FastLane.prototype._closed = function (code, reason) {
534
+ if (this.readyState === 3) return;
535
+ this.readyState = 3;
536
+ if (this.onclose) this.onclose({ code: code, reason: reason });
537
+ };
538
+ /** A frame from the stream or a datagram, to the room as a WebSocket's message: text as a string, bytes as an ArrayBuffer. */
539
+ FastLane.prototype._take = function (kind, payload) {
540
+ if (this.readyState !== 1 || !this.onmessage) return;
541
+ if (kind === 0) this.onmessage({ data: new TextDecoder().decode(payload) });
542
+ else if (kind === 1) this.onmessage({ data: payload.slice().buffer });
543
+ };
544
+ FastLane.prototype._read = function (readable) {
545
+ var lane = this;
546
+ var reader = readable.getReader();
547
+ var buffered = new Uint8Array(0);
548
+ function next() {
549
+ reader.read().then(function (read) {
550
+ if (read.done) return;
551
+ var joined = new Uint8Array(buffered.length + read.value.length);
552
+ joined.set(buffered);
553
+ joined.set(read.value, buffered.length);
554
+ buffered = joined;
555
+ while (buffered.length >= 5) {
556
+ var payloadSizeBytes = new DataView(buffered.buffer, buffered.byteOffset, 4).getUint32(0);
557
+ if (buffered.length < 5 + payloadSizeBytes) break;
558
+ lane._take(buffered[4], buffered.subarray(5, 5 + payloadSizeBytes));
559
+ buffered = buffered.subarray(5 + payloadSizeBytes);
560
+ }
561
+ next();
562
+ }, function () { /* the session closed: closed says how */ });
563
+ }
564
+ next();
565
+ };
566
+ FastLane.prototype._readDatagrams = function (readable) {
567
+ var lane = this;
568
+ var reader = readable.getReader();
569
+ function next() {
570
+ reader.read().then(function (read) {
571
+ if (read.done) return;
572
+ if (read.value.length) lane._take(read.value[0], read.value.subarray(1));
573
+ next();
574
+ }, function () { /* the session closed */ });
575
+ }
576
+ next();
577
+ };
578
+ /** A text or bytes, as [kind][payload]. */
579
+ function fastLanePayload(message) {
580
+ var payload = typeof message === "string" ? new TextEncoder().encode(message) : new Uint8Array(message);
581
+ var framed = new Uint8Array(1 + payload.length);
582
+ framed[0] = typeof message === "string" ? 0 : 1;
583
+ framed.set(payload, 1);
584
+ return framed;
585
+ }
586
+ /** On the stream, in order, after everything sent before it. */
587
+ FastLane.prototype.send = function (message) {
588
+ if (this.readyState !== 1) return;
589
+ var payload = fastLanePayload(message);
590
+ var frame = new Uint8Array(4 + payload.length);
591
+ new DataView(frame.buffer).setUint32(0, payload.length - 1);
592
+ frame.set(payload, 4);
593
+ var lane = this;
594
+ this.bufferedAmount += frame.length;
595
+ this._writer.write(frame).then(function () { lane.bufferedAmount -= frame.length; }, function () { /* the session closed */ });
596
+ };
597
+ /** As a datagram, when it fits one: false when it doesn't, for the stream to carry it instead. */
598
+ FastLane.prototype.sendDatagram = function (message) {
599
+ if (this.readyState !== 1) return false;
600
+ var datagram = fastLanePayload(message);
601
+ var maximumSizeBytes = this._datagrams.maxDatagramSize;
602
+ if (typeof maximumSizeBytes !== "number" || datagram.length > maximumSizeBytes) return false;
603
+ this._datagramWriter.write(datagram).catch(function () { /* dropped, like a lost one */ });
604
+ return true;
605
+ };
606
+ FastLane.prototype.close = function (code, reason) {
607
+ if (this.readyState === 3) return;
608
+ var wasConnecting = this.readyState === 0;
609
+ this.readyState = 3;
610
+ if (!this._session) return;
611
+ try { this._session.close(wasConnecting ? undefined : { closeCode: typeof code === "number" ? code : 1000, reason: reason || "" }); } catch (error) { /* fine */ }
612
+ };
613
+
475
614
  /** The link that opens this room: on Zaisma, the game's page with ?room=, and &server= for a room on the game's own server. */
476
615
  function inviteUrlFor(roomCode, serverUrl) {
477
616
  if (!config.site || !config.game) return null;
@@ -527,13 +666,23 @@
527
666
 
528
667
  Room.prototype._connect = function () {
529
668
  var room = this;
669
+ var isFastLane = Boolean(this._fastLaneUrl && !this._isFastLaneOff && typeof WebTransport === "function");
530
670
  // From a tester link, Zaisma rooms hear which one, so its players leave when it's turned off.
531
- var ws = new WebSocket(this._url + (config.tester && !this._serverUrl ? "?tester=" + config.tester : ""));
532
- ws.binaryType = "arraybuffer";
671
+ var ws = isFastLane ? new FastLane(this._fastLaneUrl) : new WebSocket(this._url + (config.tester && !this._serverUrl ? "?tester=" + config.tester : ""));
672
+ if (!isFastLane) ws.binaryType = "arraybuffer";
533
673
  this._ws = ws;
534
674
  this._isWelcomed = false;
535
675
  watch();
676
+ // The fast lane didn't come up (no UDP on this network, a server without it): the WebSocket, from now on.
677
+ ws.onfail = function (reason) {
678
+ if (room._ws !== ws) return;
679
+ room._isFastLaneOff = true;
680
+ if (typeof console !== "undefined") console.warn("Zaisma: the fast lane (WebTransport) to " + room._fastLaneUrl + " didn't connect (" + reason + "), so this room uses the WebSocket.");
681
+ room._ws = null;
682
+ if (!room._done) room._connect();
683
+ };
536
684
  ws.onopen = function () {
685
+ room.transport = isFastLane ? "webtransport" : "websocket";
537
686
  room._pingAtMs = room._pongAtMs = room._heardAtMs = Date.now();
538
687
  room._probeIntervalSec = null;
539
688
  var hello = { type: "hello", protocolVersionNumber: PROTOCOL_VERSION_NUMBER, pass: room._pass, room: room._made, joinData: room._joinData, idleTimeoutSec: room._clientIdle.idleTimeoutSec, idleCheckEnabled: room._clientIdle.idleCheckEnabled };
@@ -872,6 +1021,11 @@
872
1021
 
873
1022
  /** A latest-only send: now, after anything still waiting, or into its slot while the socket is backed up. */
874
1023
  Room.prototype._sendLatest = function (slotKey, payload) {
1024
+ // On the fast lane, a datagram: a late one is lost rather than waited for, so it skips the slots.
1025
+ if (this._canSend() && !this._queue.length && this._ws.sendDatagram && this._ws.sendDatagram(payload)) {
1026
+ this._sentAtMs = Date.now();
1027
+ return;
1028
+ }
875
1029
  if (this._canSend() && !this._queue.length && !(this._ws.bufferedAmount >= BACKED_UP_SIZE_BYTES)) {
876
1030
  this._sendWaiting();
877
1031
  this._ws.send(payload);
@@ -1042,6 +1196,7 @@
1042
1196
  */
1043
1197
  Room.prototype._move = function (message) {
1044
1198
  var serverUrl = this._serverUrl;
1199
+ var transport = this._fastLaneUrl ? "webtransport" : undefined;
1045
1200
  var room = this;
1046
1201
  return {
1047
1202
  roomCode: message.roomCode,
@@ -1049,7 +1204,7 @@
1049
1204
  join: function (joinOptions) {
1050
1205
  var refused = unknownOption(joinOptions, MOVE_JOIN_OPTIONS, "move.join");
1051
1206
  if (refused) return Promise.reject(refused);
1052
- return connect(message.roomCode, Object.assign({}, joinOptions || {}, { serverUrl: serverUrl }));
1207
+ return connect(message.roomCode, Object.assign({}, joinOptions || {}, { serverUrl: serverUrl, transport: transport }));
1053
1208
  },
1054
1209
  };
1055
1210
  };
@@ -1070,6 +1225,7 @@
1070
1225
  var fromRoomCode = this.roomCode;
1071
1226
  this.roomCode = roomCode;
1072
1227
  this._url = this._url.replace(/[A-Z0-9]{6}$/, roomCode);
1228
+ if (this._fastLaneUrl) this._fastLaneUrl = this._fastLaneUrl.replace(/[A-Z0-9]{6}$/, roomCode);
1073
1229
  this.inviteUrl = inviteUrlFor(roomCode, this._serverUrl);
1074
1230
  // A room the game server moves players to is listed only if the game server lists it.
1075
1231
  this._made = { isListed: false };
@@ -1371,6 +1527,7 @@
1371
1527
  var idleTimeoutSec = joinOptions.idleTimeoutSec;
1372
1528
  if (idleTimeoutSec !== undefined && !(typeof idleTimeoutSec === "number" && idleTimeoutSec >= MINIMUM_IDLE_TIMEOUT_SEC && idleTimeoutSec <= MAXIMUM_IDLE_TIMEOUT_SEC)) return Promise.reject(codedError("bad_idle_timeout", "idleTimeoutSec runs from " + MINIMUM_IDLE_TIMEOUT_SEC + " to " + MAXIMUM_IDLE_TIMEOUT_SEC + "."));
1373
1529
  if (serverUrl && !SERVER_URL.test(serverUrl)) return Promise.reject(codedError("bad_server_url", "serverUrl takes a full address starting with wss://, ws://, https:// or http://, like \"wss://play.example.com\"."));
1530
+ if (joinOptions.transport !== undefined && TRANSPORTS.indexOf(joinOptions.transport) === -1) return Promise.reject(codedError("bad_transport", "transport is " + JSON.stringify(joinOptions.transport) + '. Give "websocket" (the default) or "webtransport" (the fast lane to your own server).'));
1374
1531
  if (roomCode && !ROOM_CODE.test(roomCode)) return Promise.reject(codedError("bad_room_code", JSON.stringify(roomCode) + " isn't a room code: room codes are six characters like \"K7Q2MX\", from 2 to 9 and A to Z without I or O."));
1375
1532
 
1376
1533
  var roomCodeReady = roomCode
@@ -1380,10 +1537,13 @@
1380
1537
  : post("/v1/rooms", { gameSlug: config.game }).then(function (answer) { return answer.roomCode; });
1381
1538
  return Promise.all([roomCodeReady, getPass()]).then(function (ready) {
1382
1539
  var base = (serverUrl || config.rooms).replace(/^http/, "ws");
1540
+ var path = "/v1/rooms/" + config.game + "/" + ready[0];
1383
1541
  var room = new Room({
1384
1542
  roomCode: ready[0],
1385
1543
  pass: ready[1].pass,
1386
- url: base + "/v1/rooms/" + config.game + "/" + ready[0],
1544
+ url: base + path,
1545
+ // The fast lane reaches only the game's own secure servers: Zaisma's rooms take the WebSocket, whatever transport says.
1546
+ fastLaneUrl: joinOptions.transport === "webtransport" && serverUrl && OWN_SERVER_URL.test(serverUrl) ? serverUrl.replace(/^wss:/, "https:") + path : null,
1387
1547
  isListed: joinOptions.isListed,
1388
1548
  friendJoinsEnabled: joinOptions.friendJoinsEnabled,
1389
1549
  name: joinOptions.name,
@@ -2023,6 +2183,34 @@
2023
2183
  });
2024
2184
  }
2025
2185
 
2186
+ /*
2187
+ * Whether Zaisma's page covers the game: "sheet" while one of its sheets is open over it (buying, Players, Report), "card" while one of
2188
+ * its cards is over the game's corner, null otherwise, and always null outside Zaisma's page. The page says so whenever it changes
2189
+ * (zaisma:covered), and only the page is heard. A game can pause while a sheet covers it.
2190
+ */
2191
+ var pageEvents = new Emitter(PAGE_EVENTS, "Zaisma.page has");
2192
+ var coveredBy = null;
2193
+ var zaismaPage = {
2194
+ on: function (event, listener) {
2195
+ pageEvents.on(event, listener);
2196
+ return zaismaPage;
2197
+ },
2198
+ off: function (event, listener) {
2199
+ pageEvents.off(event, listener);
2200
+ return zaismaPage;
2201
+ },
2202
+ };
2203
+ Object.defineProperty(zaismaPage, "coveredBy", { enumerable: true, get: function () { return coveredBy; } });
2204
+ if (config.site && window.parent !== window) {
2205
+ window.addEventListener("message", function (event) {
2206
+ if (event.origin !== config.site || event.source !== window.parent || !event.data || event.data.type !== "zaisma:covered") return;
2207
+ var now = event.data.coveredBy === "sheet" || event.data.coveredBy === "card" ? event.data.coveredBy : null;
2208
+ if (now === coveredBy) return;
2209
+ coveredBy = now;
2210
+ pageEvents._emit("coveredBy", now);
2211
+ });
2212
+ }
2213
+
2026
2214
  /*
2027
2215
  * The player's cloud saves, for games that draw their own save screens. The game page on Zaisma holds
2028
2216
  * the player's session and answers (apps/site/src/saves-client.ts); the game never talks to a server.
@@ -2506,5 +2694,5 @@
2506
2694
  });
2507
2695
 
2508
2696
  var friends = { list: listFriends, setRoom: setOwnRoom, sendRequest: sendFriendRequest };
2509
- window.Zaisma = { sdkVersionNumber: 1, gameSlug: config.game || null, player: player, joinRoom: joinRoom, rooms: rooms, server: server, invite: invite, checkChat: checkChat, openPlayersPanel: openPlayersPanel, links: links, items: items, friends: friends, saves: saves, versions: versions };
2697
+ window.Zaisma = { sdkVersionNumber: 1, gameSlug: config.game || null, player: player, joinRoom: joinRoom, rooms: rooms, server: server, invite: invite, checkChat: checkChat, openPlayersPanel: openPlayersPanel, links: links, items: items, friends: friends, saves: saves, versions: versions, page: zaismaPage };
2510
2698
  })();
package/src/qr-code.d.ts CHANGED
@@ -1,13 +1,14 @@
1
1
  /**
2
2
  * QR codes for game and room links (docs/DECISIONS.md, "QR codes"): players get one behind Share,
3
- * developers on the dashboard and through the API and MCP. Byte mode, error correction level M,
4
- * versions 1 to 10 (up to 213 bytes, far more than a link needs). Written here rather than taken from a
3
+ * developers on the dashboard and through the API and MCP. Byte mode, error correction level H, so a
4
+ * small picture over its middle (a game's icon) still leaves it readable, versions 1 to 10 (up to 119
5
+ * bytes, far more than a link needs). Written here rather than taken from a
5
6
  * package so the site's page and the API draw the same code with no dependency; it follows ISO/IEC
6
7
  * 18004 the way Project Nayuki's reference encoder does.
7
8
  */
8
9
  export declare const QR_CODE_LIMITS: {
9
- /** Version 10 at level M holds 213 bytes. */
10
- readonly maximumTextBytes: 213;
10
+ /** Version 10 at level H holds 119 bytes. */
11
+ readonly maximumTextBytes: 119;
11
12
  /** Light modules around the code, as the standard asks. */
12
13
  readonly quietZoneModuleCount: 4;
13
14
  };
package/src/qr-code.js CHANGED
@@ -1,21 +1,22 @@
1
1
  /**
2
2
  * QR codes for game and room links (docs/DECISIONS.md, "QR codes"): players get one behind Share,
3
- * developers on the dashboard and through the API and MCP. Byte mode, error correction level M,
4
- * versions 1 to 10 (up to 213 bytes, far more than a link needs). Written here rather than taken from a
3
+ * developers on the dashboard and through the API and MCP. Byte mode, error correction level H, so a
4
+ * small picture over its middle (a game's icon) still leaves it readable, versions 1 to 10 (up to 119
5
+ * bytes, far more than a link needs). Written here rather than taken from a
5
6
  * package so the site's page and the API draw the same code with no dependency; it follows ISO/IEC
6
7
  * 18004 the way Project Nayuki's reference encoder does.
7
8
  */
8
9
  export const QR_CODE_LIMITS = {
9
- /** Version 10 at level M holds 213 bytes. */
10
- maximumTextBytes: 213,
10
+ /** Version 10 at level H holds 119 bytes. */
11
+ maximumTextBytes: 119,
11
12
  /** Light modules around the code, as the standard asks. */
12
13
  quietZoneModuleCount: 4,
13
14
  };
14
- // Level M: error correction codewords per block and blocks, by version (index 0 unused).
15
- const ERROR_CORRECTION_CODEWORD_COUNTS_PER_BLOCK = [0, 10, 16, 26, 18, 24, 16, 18, 22, 22, 26];
16
- const ERROR_CORRECTION_BLOCK_COUNTS = [0, 1, 1, 1, 2, 2, 4, 4, 4, 5, 5];
17
- /** Error correction level M (medium)'s two bits in the format information. */
18
- const MEDIUM_CORRECTION_FORMAT_NUMBER = 0;
15
+ // Level H: error correction codewords per block and blocks, by version (index 0 unused).
16
+ const ERROR_CORRECTION_CODEWORD_COUNTS_PER_BLOCK = [0, 17, 28, 22, 16, 22, 28, 26, 26, 24, 28];
17
+ const ERROR_CORRECTION_BLOCK_COUNTS = [0, 1, 1, 2, 4, 4, 4, 5, 6, 8, 8];
18
+ /** Error correction level H (high)'s two bits in the format information. */
19
+ const HIGH_CORRECTION_FORMAT_NUMBER = 2;
19
20
  const HIGHEST_VERSION_NUMBER = 10;
20
21
  const isBitSet = (bitsNumber, bitIndex) => ((bitsNumber >>> bitIndex) & 1) !== 0;
21
22
  function rawDataModuleCount(versionNumber) {
@@ -226,7 +227,7 @@ export function qrCodeModules(text) {
226
227
  }
227
228
  }));
228
229
  const drawFormat = (maskIndex) => {
229
- const formatNumber = (MEDIUM_CORRECTION_FORMAT_NUMBER << 3) | maskIndex;
230
+ const formatNumber = (HIGH_CORRECTION_FORMAT_NUMBER << 3) | maskIndex;
230
231
  let remainderNumber = formatNumber;
231
232
  for (let stepIndex = 0; stepIndex < 10; stepIndex++)
232
233
  remainderNumber = (remainderNumber << 1) ^ ((remainderNumber >>> 9) * 0x537);
package/src/room.d.ts CHANGED
@@ -828,6 +828,17 @@ export interface RoomSettings {
828
828
  * sentAt}; the sender gets {type: "chat_sent", chatNumber, sentAt}, {type: "chat_stopped", chatNumber,
829
829
  * reason, message}, or an error with its chatNumber (chat_not_allowed, chat_empty, chat_too_long,
830
830
  * chat_too_fast). chat_sent never says who read it. welcome says canChat: whether this player can chat at all.
831
+ *
832
+ * The fast lane (WebTransport, own servers only: joinRoom({ transport: "webtransport" })): the client opens
833
+ * a session at https://<server>/v1/rooms/<game>/<code>, the server checks its Origin as for the WebSocket,
834
+ * and the client opens one bidirectional stream first. That stream carries every frame the WebSocket
835
+ * carries, in order, each as [4-byte big-endian payload length][1-byte kind: 0 text, 1 bytes][payload]:
836
+ * text is the same UTF-8 JSON (hello, probe, probe_answer, ...) or "ping"/"pong", bytes the same binary
837
+ * frame (bytesFrame from the room, raw bytes from the client). A datagram is [1-byte kind][payload] with
838
+ * the same payloads, and carries only "latest_only" sends, both ways, when it fits the session's
839
+ * maxDatagramSize; a bigger one goes on the stream. A server takes datagrams only from a player it let in.
840
+ * Close codes and reasons go in the session's close info (closeCode, reason), the same numbers as the
841
+ * WebSocket's. Zaisma's own rooms don't take WebTransport: the SDK uses the WebSocket for them.
831
842
  */
832
843
  export type ServerMessage = {
833
844
  type: "welcome";
@@ -1051,9 +1062,13 @@ export interface ClientIdle {
1051
1062
  }
1052
1063
  /** How the room reaches players: a Durable Object's sockets, or a Node server's. */
1053
1064
  export interface Transport {
1054
- send(playerId: string, text: string): void;
1055
- /** A binary frame (bytesFrame). Without it, rooms can't send bytes. */
1056
- sendBytes?(playerId: string, frame: Uint8Array): void;
1065
+ /**
1066
+ * A text. delivery says how the game sent it: "latest_only" may go as a datagram on the fast lane to
1067
+ * your own server (WebTransport); everything else, and every send on a WebSocket, goes in order.
1068
+ */
1069
+ send(playerId: string, text: string, delivery?: Delivery): void;
1070
+ /** A binary frame (bytesFrame), with its delivery as for send. Without it, rooms can't send bytes. */
1071
+ sendBytes?(playerId: string, frame: Uint8Array, delivery?: Delivery): void;
1057
1072
  close(playerId: string, closeCode: number, reason: string): void;
1058
1073
  /** Set up a reserved room, then send each of its players a "move" message. Without it, rooms can't reserve. */
1059
1074
  reserve?(roomCode: string, start: RoomStart): void;
package/src/room.js CHANGED
@@ -2162,7 +2162,7 @@ export class RoomCore {
2162
2162
  deliver(playerIds, outgoing, delivery) {
2163
2163
  const ids = playerIds.filter((id) => !this.isLost(id));
2164
2164
  if (!delivery.isLatestOnly)
2165
- return this.sendOut(ids, outgoing, ids.filter((id) => this.isBackedUp(id)).length);
2165
+ return this.sendOut(ids, outgoing, ids.filter((id) => this.isBackedUp(id)).length, "reliable");
2166
2166
  const ready = [];
2167
2167
  for (const id of ids) {
2168
2168
  if (!this.isBackedUp(id)) {
@@ -2181,20 +2181,20 @@ export class RoomCore {
2181
2181
  if (slots.size > ROOM_LIMITS.maximumLatestOnlySlotCount)
2182
2182
  slots.delete(slots.keys().next().value);
2183
2183
  }
2184
- this.sendOut(ready, outgoing, 0);
2184
+ this.sendOut(ready, outgoing, 0, "latest_only");
2185
2185
  }
2186
2186
  /** A player's waiting latest-only messages, least recently replaced first. */
2187
2187
  sendLatest(playerId) {
2188
2188
  const slots = this.latestSlots.get(playerId);
2189
2189
  this.latestSlots.delete(playerId);
2190
2190
  for (const outgoing of slots?.values() ?? [])
2191
- this.sendOut([playerId], outgoing, 0);
2191
+ this.sendOut([playerId], outgoing, 0, "latest_only");
2192
2192
  }
2193
- sendOut(playerIds, outgoing, backedUpMessageCount) {
2193
+ sendOut(playerIds, outgoing, backedUpMessageCount, delivery) {
2194
2194
  if ("frame" in outgoing)
2195
- this.sendFrame(playerIds, outgoing.frame, backedUpMessageCount);
2195
+ this.sendFrame(playerIds, outgoing.frame, backedUpMessageCount, delivery);
2196
2196
  else
2197
- this.sendEach(playerIds, outgoing.text, outgoing.messageType, backedUpMessageCount);
2197
+ this.sendEach(playerIds, outgoing.text, outgoing.messageType, backedUpMessageCount, delivery);
2198
2198
  }
2199
2199
  /**
2200
2200
  * The match moves to a version published "now": every player hears it's coming at appliesAt, and when
@@ -2267,7 +2267,7 @@ export class RoomCore {
2267
2267
  return [...this.players.values()].map((player) => ({ ...shownPlayer(player), awayReason: this.kept.get(player.id) ?? null }));
2268
2268
  }
2269
2269
  /** Send a binary frame to some players, counting it like text: toward each one's backlog, and the room's traffic. */
2270
- sendFrame(given, frame, backedUpMessageCount = 0) {
2270
+ sendFrame(given, frame, backedUpMessageCount = 0, delivery = "reliable") {
2271
2271
  if (!this.transport.sendBytes)
2272
2272
  throw new Error("Sending bytes isn't available here");
2273
2273
  const playerIds = given.filter((id) => !this.isLost(id));
@@ -2275,7 +2275,7 @@ export class RoomCore {
2275
2275
  this.depth++;
2276
2276
  try {
2277
2277
  for (const id of playerIds) {
2278
- this.transport.sendBytes(id, frame);
2278
+ this.transport.sendBytes(id, frame, delivery);
2279
2279
  this.sent(id, frame.length);
2280
2280
  }
2281
2281
  }
@@ -2291,14 +2291,14 @@ export class RoomCore {
2291
2291
  * Send one text to several players, counting its bytes once. A player it drops for falling behind
2292
2292
  * leaves once everyone has it, so nobody hears of the leave before the rest of this text.
2293
2293
  */
2294
- sendEach(playerIds, text, messageType = ZAISMA_MESSAGE_TYPE, backedUpMessageCount = 0) {
2294
+ sendEach(playerIds, text, messageType = ZAISMA_MESSAGE_TYPE, backedUpMessageCount = 0, delivery = "reliable") {
2295
2295
  const byteCount = utf8ByteCount(text);
2296
2296
  const ids = [...playerIds].filter((id) => !this.isLost(id));
2297
2297
  this.countTraffic("outbound", messageType, ids.length, byteCount, backedUpMessageCount);
2298
2298
  this.depth++;
2299
2299
  try {
2300
2300
  for (const id of ids)
2301
- this.sendText(id, text, byteCount, null);
2301
+ this.sendText(id, text, byteCount, null, delivery);
2302
2302
  }
2303
2303
  finally {
2304
2304
  this.depth--;
@@ -2311,10 +2311,10 @@ export class RoomCore {
2311
2311
  * every ROOM_TUNING.probeSentSizeBytes, they're probed, so a backed-up connection shows as a late answer.
2312
2312
  * A player whose connection dropped gets nothing: nothing is sent, counted or probed until they're back.
2313
2313
  */
2314
- sendText(playerId, text, byteCount = utf8ByteCount(text), messageType = ZAISMA_MESSAGE_TYPE) {
2314
+ sendText(playerId, text, byteCount = utf8ByteCount(text), messageType = ZAISMA_MESSAGE_TYPE, delivery = "reliable") {
2315
2315
  if (this.isLost(playerId))
2316
2316
  return;
2317
- this.transport.send(playerId, text);
2317
+ this.transport.send(playerId, text, delivery);
2318
2318
  // sendEach counts its text once for everyone (messageType null).
2319
2319
  if (messageType !== null)
2320
2320
  this.countTraffic("outbound", messageType, 1, byteCount);
@@ -1 +0,0 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024" role="img" aria-label="Zaisma"><rect width="1024" height="1024" rx="230" fill="#1C1C1C"/><g transform="translate(132 113) scale(0.17728)"><defs><linearGradient id="ag" x1="0" y1="721" x2="0" y2="3779" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#cdcdcd"/><stop offset="1" stop-color="#333"/></linearGradient><clipPath id="acb"><path d="M1884.08,3033.91c767.645,0 1535.29,0 2302.93,0c33.233,248.238 66.466,496.475 99.699,744.712l-4286.71,-0c627.367,-243.599 1255.99,-491.89 1884.08,-744.712Z"/></clipPath><clipPath id="acm"><path d="M415.598,3484.54c777.297,-719.363 1475.76,-1400.24 2108.05,-2044.32c404.715,-158.588 821.55,-323.759 1248.51,-495.17c-547.607,610.584 -1151.51,1254.04 -1823.08,1932.14c-520.17,209.105 -1032.22,411.7 -1533.48,607.345Z"/></clipPath><clipPath id="act"><path d="M2575.7,1305.06l-1740.58,0c21.608,-194.559 43.215,-389.119 64.823,-583.678l3147.9,0c-485.752,195.366 -976.426,389.899 -1472.14,583.678Z"/></clipPath></defs><path d="M1884.08,3033.91c767.645,0 1535.29,0 2302.93,0c33.233,248.238 66.466,496.475 99.699,744.712l-4286.71,-0c627.367,-243.599 1255.99,-491.89 1884.08,-744.712Z" fill="url(#ag)"/><g clip-path="url(#acb)" fill="none" stroke-width="250" stroke-linecap="square"><path d="M4187.01,3033.91c33.233,248.238 66.466,496.475 99.699,744.712l-4286.71,-0" stroke="#000"/><path d="M-0,3778.62c627.367,-243.599 1255.99,-491.89 1884.08,-744.712c767.645,0 1535.29,0 2302.93,0" stroke="#fff" stroke-linejoin="miter"/></g><path d="M415.598,3484.54c777.297,-719.363 1475.76,-1400.24 2108.05,-2044.32c404.715,-158.588 821.55,-323.759 1248.51,-495.17c-547.607,610.584 -1151.51,1254.04 -1823.08,1932.14c-520.17,209.105 -1032.22,411.7 -1533.48,607.345Z" fill="url(#ag)"/><g clip-path="url(#acm)" fill="none" stroke-width="250" stroke-linecap="square"><path d="M3772.16,945.051c-547.607,610.584 -1151.51,1254.04 -1823.08,1932.14c-520.17,209.105 -1032.22,411.7 -1533.48,607.345" stroke="#000"/><path d="M415.598,3484.54c777.297,-719.363 1475.76,-1400.24 2108.05,-2044.32c404.715,-158.588 821.55,-323.759 1248.51,-495.17" stroke="#fff" stroke-linejoin="miter"/></g><path d="M2575.7,1305.06l-1740.58,0c21.608,-194.559 43.215,-389.119 64.823,-583.678l3147.9,0c-485.752,195.366 -976.426,389.899 -1472.14,583.678Z" fill="url(#ag)"/><g clip-path="url(#act)" fill="none" stroke-width="250" stroke-linecap="square"><path d="M4047.85,721.381c-485.752,195.366 -976.426,389.899 -1472.14,583.678c-580.193,0 -1160.39,0 -1740.58,0" stroke="#000"/><path d="M835.121,1305.06c21.608,-194.559 43.215,-389.119 64.823,-583.678l3147.9,0" stroke="#fff" stroke-linejoin="miter"/></g></g></svg>
@@ -1 +0,0 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="-120 600 4527 3300" role="img" aria-label="Zaisma"><defs><linearGradient id="mg" x1="0" y1="721" x2="0" y2="3779" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#cdcdcd"/><stop offset="1" stop-color="#333"/></linearGradient><clipPath id="mcb"><path d="M1884.08,3033.91c767.645,0 1535.29,0 2302.93,0c33.233,248.238 66.466,496.475 99.699,744.712l-4286.71,-0c627.367,-243.599 1255.99,-491.89 1884.08,-744.712Z"/></clipPath><clipPath id="mcm"><path d="M415.598,3484.54c777.297,-719.363 1475.76,-1400.24 2108.05,-2044.32c404.715,-158.588 821.55,-323.759 1248.51,-495.17c-547.607,610.584 -1151.51,1254.04 -1823.08,1932.14c-520.17,209.105 -1032.22,411.7 -1533.48,607.345Z"/></clipPath><clipPath id="mct"><path d="M2575.7,1305.06l-1740.58,0c21.608,-194.559 43.215,-389.119 64.823,-583.678l3147.9,0c-485.752,195.366 -976.426,389.899 -1472.14,583.678Z"/></clipPath></defs><path d="M1884.08,3033.91c767.645,0 1535.29,0 2302.93,0c33.233,248.238 66.466,496.475 99.699,744.712l-4286.71,-0c627.367,-243.599 1255.99,-491.89 1884.08,-744.712Z" fill="url(#mg)"/><g clip-path="url(#mcb)" fill="none" stroke-width="250" stroke-linecap="square"><path d="M4187.01,3033.91c33.233,248.238 66.466,496.475 99.699,744.712l-4286.71,-0" stroke="#000"/><path d="M-0,3778.62c627.367,-243.599 1255.99,-491.89 1884.08,-744.712c767.645,0 1535.29,0 2302.93,0" stroke="#fff" stroke-linejoin="miter"/></g><path d="M415.598,3484.54c777.297,-719.363 1475.76,-1400.24 2108.05,-2044.32c404.715,-158.588 821.55,-323.759 1248.51,-495.17c-547.607,610.584 -1151.51,1254.04 -1823.08,1932.14c-520.17,209.105 -1032.22,411.7 -1533.48,607.345Z" fill="url(#mg)"/><g clip-path="url(#mcm)" fill="none" stroke-width="250" stroke-linecap="square"><path d="M3772.16,945.051c-547.607,610.584 -1151.51,1254.04 -1823.08,1932.14c-520.17,209.105 -1032.22,411.7 -1533.48,607.345" stroke="#000"/><path d="M415.598,3484.54c777.297,-719.363 1475.76,-1400.24 2108.05,-2044.32c404.715,-158.588 821.55,-323.759 1248.51,-495.17" stroke="#fff" stroke-linejoin="miter"/></g><path d="M2575.7,1305.06l-1740.58,0c21.608,-194.559 43.215,-389.119 64.823,-583.678l3147.9,0c-485.752,195.366 -976.426,389.899 -1472.14,583.678Z" fill="url(#mg)"/><g clip-path="url(#mct)" fill="none" stroke-width="250" stroke-linecap="square"><path d="M4047.85,721.381c-485.752,195.366 -976.426,389.899 -1472.14,583.678c-580.193,0 -1160.39,0 -1740.58,0" stroke="#000"/><path d="M835.121,1305.06c21.608,-194.559 43.215,-389.119 64.823,-583.678l3147.9,0" stroke="#fff" stroke-linejoin="miter"/></g></svg>
@@ -1 +0,0 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="-120 600 4527 3300" role="img" aria-label="Zaisma"><clipPath id="_clip1"><rect x="0" y="0" width="4500" height="4500"/></clipPath><g clip-path="url(#_clip1)"><g><g><path d="M1884.08,3033.91c767.645,0 1535.29,0 2302.93,0c33.233,248.238 66.466,496.475 99.699,744.712l-4286.71,-0c627.367,-243.599 1255.99,-491.89 1884.08,-744.712Z" style="fill:#fff;fill-rule:nonzero;"/><clipPath id="_clip2"><path d="M1884.08,3033.91c767.645,0 1535.29,0 2302.93,0c33.233,248.238 66.466,496.475 99.699,744.712l-4286.71,-0c627.367,-243.599 1255.99,-491.89 1884.08,-744.712Z" clip-rule="nonzero"/></clipPath><g clip-path="url(#_clip2)"><path d="M4187.01,3033.91c33.233,248.238 66.466,496.475 99.699,744.712l-4286.71,-0" style="fill:none;fill-rule:nonzero;stroke:#171c32;stroke-width:250px;"/><path d="M-0,3778.62c627.367,-243.599 1255.99,-491.89 1884.08,-744.712c767.645,0 1535.29,0 2302.93,0" style="fill:none;fill-rule:nonzero;stroke:#6577ce;stroke-width:250px;stroke-linejoin:miter;"/></g><path d="M415.598,3484.54c777.297,-719.363 1475.76,-1400.24 2108.05,-2044.32c404.715,-158.588 821.55,-323.759 1248.51,-495.17c-547.607,610.584 -1151.51,1254.04 -1823.08,1932.14c-520.17,209.105 -1032.22,411.7 -1533.48,607.345Z" style="fill:#fff;fill-rule:nonzero;"/><clipPath id="_clip3"><path d="M415.598,3484.54c777.297,-719.363 1475.76,-1400.24 2108.05,-2044.32c404.715,-158.588 821.55,-323.759 1248.51,-495.17c-547.607,610.584 -1151.51,1254.04 -1823.08,1932.14c-520.17,209.105 -1032.22,411.7 -1533.48,607.345Z" clip-rule="nonzero"/></clipPath><g clip-path="url(#_clip3)"><path d="M3772.16,945.051c-547.607,610.584 -1151.51,1254.04 -1823.08,1932.14c-520.17,209.105 -1032.22,411.7 -1533.48,607.345" style="fill:none;fill-rule:nonzero;stroke:#17321a;stroke-width:250px;"/><path d="M415.598,3484.54c777.297,-719.363 1475.76,-1400.24 2108.05,-2044.32c404.715,-158.588 821.55,-323.759 1248.51,-495.17" style="fill:none;fill-rule:nonzero;stroke:#53ae48;stroke-width:250px;stroke-linejoin:miter;"/></g><path d="M2575.7,1305.06l-1740.58,0c21.608,-194.559 43.215,-389.119 64.823,-583.678l3147.9,0c-485.752,195.366 -976.426,389.899 -1472.14,583.678Z" style="fill:#fff;fill-rule:nonzero;"/><clipPath id="_clip4"><path d="M2575.7,1305.06l-1740.58,0c21.608,-194.559 43.215,-389.119 64.823,-583.678l3147.9,0c-485.752,195.366 -976.426,389.899 -1472.14,583.678Z" clip-rule="nonzero"/></clipPath><g clip-path="url(#_clip4)"><path d="M4047.85,721.381c-485.752,195.366 -976.426,389.899 -1472.14,583.678c-580.193,0 -1160.39,0 -1740.58,0" style="fill:none;fill-rule:nonzero;stroke:#321718;stroke-width:250px;"/><path d="M835.121,1305.06c21.608,-194.559 43.215,-389.119 64.823,-583.678l3147.9,0" style="fill:none;fill-rule:nonzero;stroke:#af5053;stroke-width:250px;"/></g></g></g></g></svg>
@@ -1 +0,0 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="-120 1080 9988 2820" role="img" aria-label="Zaisma"><defs><linearGradient id="wg" x1="0" y1="721" x2="0" y2="3779" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#cdcdcd"/><stop offset="1" stop-color="#333"/></linearGradient><linearGradient id="wh" x1="0" y1="1879" x2="0" y2="3779" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#cdcdcd"/><stop offset="1" stop-color="#333"/></linearGradient><clipPath id="wcb"><path d="M1884.08,3033.91c767.645,0 1535.29,0 2302.93,0c33.233,248.238 66.466,496.475 99.699,744.712l-4286.71,-0c627.367,-243.599 1255.99,-491.89 1884.08,-744.712Z"/></clipPath><clipPath id="wcm"><path d="M415.598,3484.54c777.297,-719.363 1475.76,-1400.24 2108.05,-2044.32c404.715,-158.588 821.55,-323.759 1248.51,-495.17c-547.607,610.584 -1151.51,1254.04 -1823.08,1932.14c-520.17,209.105 -1032.22,411.7 -1533.48,607.345Z"/></clipPath><clipPath id="wct"><path d="M2575.7,1305.06l-1740.58,0c21.608,-194.559 43.215,-389.119 64.823,-583.678l3147.9,0c-485.752,195.366 -976.426,389.899 -1472.14,583.678Z"/></clipPath></defs><g transform="translate(0 3779) scale(0.62132) translate(0 -3779)"><path d="M1884.08,3033.91c767.645,0 1535.29,0 2302.93,0c33.233,248.238 66.466,496.475 99.699,744.712l-4286.71,-0c627.367,-243.599 1255.99,-491.89 1884.08,-744.712Z" fill="url(#wg)"/><g clip-path="url(#wcb)" fill="none" stroke-width="250" stroke-linecap="square"><path d="M4187.01,3033.91c33.233,248.238 66.466,496.475 99.699,744.712l-4286.71,-0" stroke="#000"/><path d="M-0,3778.62c627.367,-243.599 1255.99,-491.89 1884.08,-744.712c767.645,0 1535.29,0 2302.93,0" stroke="#fff" stroke-linejoin="miter"/></g><path d="M415.598,3484.54c777.297,-719.363 1475.76,-1400.24 2108.05,-2044.32c404.715,-158.588 821.55,-323.759 1248.51,-495.17c-547.607,610.584 -1151.51,1254.04 -1823.08,1932.14c-520.17,209.105 -1032.22,411.7 -1533.48,607.345Z" fill="url(#wg)"/><g clip-path="url(#wcm)" fill="none" stroke-width="250" stroke-linecap="square"><path d="M3772.16,945.051c-547.607,610.584 -1151.51,1254.04 -1823.08,1932.14c-520.17,209.105 -1032.22,411.7 -1533.48,607.345" stroke="#000"/><path d="M415.598,3484.54c777.297,-719.363 1475.76,-1400.24 2108.05,-2044.32c404.715,-158.588 821.55,-323.759 1248.51,-495.17" stroke="#fff" stroke-linejoin="miter"/></g><path d="M2575.7,1305.06l-1740.58,0c21.608,-194.559 43.215,-389.119 64.823,-583.678l3147.9,0c-485.752,195.366 -976.426,389.899 -1472.14,583.678Z" fill="url(#wg)"/><g clip-path="url(#wct)" fill="none" stroke-width="250" stroke-linecap="square"><path d="M4047.85,721.381c-485.752,195.366 -976.426,389.899 -1472.14,583.678c-580.193,0 -1160.39,0 -1740.58,0" stroke="#000"/><path d="M835.121,1305.06c21.608,-194.559 43.215,-389.119 64.823,-583.678l3147.9,0" stroke="#fff" stroke-linejoin="miter"/></g></g><g transform="translate(-1953 0)"><path d="M4957,1879 L5877,1879 L6132,3779 L5212,3779 L4837,3449 L4765,2909 L5051,2579 L5601,2579 L5557,2249 L4677,2249 Z M5140,2949 L5650,2949 L5712,3409 L5202,3409 Z M6077,1879 L6447,1879 L6702,3779 L6332,3779 Z M5991,1239 L6541,1239 L6412,1619 L6042,1619 Z M6647,1879 L7847,1879 L7667,2249 L7067,2249 L7120,2644 L7950,2644 L8102,3779 L6902,3779 L7082,3409 L7682,3409 L7629,3014 L6799,3014 Z M8091,2209 L8377,1879 L9997,1879 L10252,3779 L9882,3779 L9677,2249 L9257,2249 L9462,3779 L9092,3779 L8887,2249 L8467,2249 L8672,3779 L8302,3779 Z M10527,1879 L11447,1879 L11702,3779 L10782,3779 L10407,3449 L10335,2909 L10621,2579 L11171,2579 L11127,2249 L10247,2249 Z M10710,2949 L11220,2949 L11282,3409 L10772,3409 Z" fill="#000" fill-rule="evenodd"/><path d="M4957.0,1879.0 L5877.0,1879.0 L5809.1,1956.7 L4995.6,1956.7 Z M4837.0,3449.0 L4765.0,2909.0 L4846.6,2933.4 L4910.1,3409.9 Z M4765.0,2909.0 L5051.0,2579.0 L5086.5,2656.7 L4846.6,2933.4 Z M5051.0,2579.0 L5601.0,2579.0 L5689.7,2656.7 L5086.5,2656.7 Z M5601.0,2579.0 L5557.0,2249.0 L5625.0,2171.3 L5689.7,2656.7 Z M4677.0,2249.0 L4957.0,1879.0 L4995.6,1956.7 L4833.2,2171.3 Z M5650.0,2949.0 L5712.0,3409.0 L5800.8,3486.7 L5717.9,2871.3 Z M5712.0,3409.0 L5202.0,3409.0 L5134.1,3486.7 L5800.8,3486.7 Z M6077.0,1879.0 L6447.0,1879.0 L6379.1,1956.7 L6165.8,1956.7 Z M6332.0,3779.0 L6077.0,1879.0 L6165.8,1956.7 L6399.9,3701.3 Z M5991.0,1239.0 L6541.0,1239.0 L6432.6,1316.7 L6079.8,1316.7 Z M6042.0,1619.0 L5991.0,1239.0 L6079.8,1316.7 L6109.9,1541.3 Z M6647.0,1879.0 L7847.0,1879.0 L7722.8,1956.7 L6735.8,1956.7 Z M7120.0,2644.0 L7950.0,2644.0 L7882.0,2721.7 L7052.1,2721.7 Z M6902.0,3779.0 L7082.0,3409.0 L7130.6,3486.7 L7026.2,3701.3 Z M7082.0,3409.0 L7682.0,3409.0 L7770.8,3486.7 L7130.6,3486.7 Z M7682.0,3409.0 L7629.0,3014.0 L7696.9,2936.3 L7770.8,3486.7 Z M6799.0,3014.0 L6647.0,1879.0 L6735.8,1956.7 L6867.0,2936.3 Z M8091.0,2209.0 L8377.0,1879.0 L8412.5,1956.7 L8172.6,2233.4 Z M8377.0,1879.0 L9997.0,1879.0 L9929.1,1956.7 L8412.5,1956.7 Z M9882.0,3779.0 L9677.0,2249.0 L9745.0,2171.3 L9950.0,3701.3 Z M9092.0,3779.0 L8887.0,2249.0 L8955.0,2171.3 L9160.0,3701.3 Z M8302.0,3779.0 L8091.0,2209.0 L8172.6,2233.4 L8369.9,3701.3 Z M10527.0,1879.0 L11447.0,1879.0 L11379.1,1956.7 L10565.6,1956.7 Z M10407.0,3449.0 L10335.0,2909.0 L10416.6,2933.4 L10480.1,3409.9 Z M10335.0,2909.0 L10621.0,2579.0 L10656.5,2656.7 L10416.6,2933.4 Z M10621.0,2579.0 L11171.0,2579.0 L11259.7,2656.7 L10656.5,2656.7 Z M11171.0,2579.0 L11127.0,2249.0 L11195.0,2171.3 L11259.7,2656.7 Z M10247.0,2249.0 L10527.0,1879.0 L10565.6,1956.7 L10403.2,2171.3 Z M11220.0,2949.0 L11282.0,3409.0 L11370.8,3486.7 L11287.9,2871.3 Z M11282.0,3409.0 L10772.0,3409.0 L10704.1,3486.7 L11370.8,3486.7 Z" fill="#fff" stroke="#fff" stroke-width="2" stroke-linejoin="miter"/><path d="M5877.0,1879.0 L6132.0,3779.0 L6043.2,3701.3 L5809.1,1956.7 Z M6132.0,3779.0 L5212.0,3779.0 L5241.3,3701.3 L6043.2,3701.3 Z M5212.0,3779.0 L4837.0,3449.0 L4910.1,3409.9 L5241.3,3701.3 Z M5557.0,2249.0 L4677.0,2249.0 L4833.2,2171.3 L5625.0,2171.3 Z M5140.0,2949.0 L5650.0,2949.0 L5717.9,2871.3 L5051.2,2871.3 Z M5202.0,3409.0 L5140.0,2949.0 L5051.2,2871.3 L5134.1,3486.7 Z M6447.0,1879.0 L6702.0,3779.0 L6613.2,3701.3 L6379.1,1956.7 Z M6702.0,3779.0 L6332.0,3779.0 L6399.9,3701.3 L6613.2,3701.3 Z M6541.0,1239.0 L6412.0,1619.0 L6356.3,1541.3 L6432.6,1316.7 Z M6412.0,1619.0 L6042.0,1619.0 L6109.9,1541.3 L6356.3,1541.3 Z M7847.0,1879.0 L7667.0,2249.0 L7618.4,2171.3 L7722.8,1956.7 Z M7667.0,2249.0 L7067.0,2249.0 L6978.2,2171.3 L7618.4,2171.3 Z M7067.0,2249.0 L7120.0,2644.0 L7052.1,2721.7 L6978.2,2171.3 Z M7950.0,2644.0 L8102.0,3779.0 L8013.2,3701.3 L7882.0,2721.7 Z M8102.0,3779.0 L6902.0,3779.0 L7026.2,3701.3 L8013.2,3701.3 Z M7629.0,3014.0 L6799.0,3014.0 L6867.0,2936.3 L7696.9,2936.3 Z M9997.0,1879.0 L10252.0,3779.0 L10163.2,3701.3 L9929.1,1956.7 Z M10252.0,3779.0 L9882.0,3779.0 L9950.0,3701.3 L10163.2,3701.3 Z M9677.0,2249.0 L9257.0,2249.0 L9168.2,2171.3 L9745.0,2171.3 Z M9257.0,2249.0 L9462.0,3779.0 L9373.2,3701.3 L9168.2,2171.3 Z M9462.0,3779.0 L9092.0,3779.0 L9160.0,3701.3 L9373.2,3701.3 Z M8887.0,2249.0 L8467.0,2249.0 L8378.2,2171.3 L8955.0,2171.3 Z M8467.0,2249.0 L8672.0,3779.0 L8583.2,3701.3 L8378.2,2171.3 Z M8672.0,3779.0 L8302.0,3779.0 L8369.9,3701.3 L8583.2,3701.3 Z M11447.0,1879.0 L11702.0,3779.0 L11613.2,3701.3 L11379.1,1956.7 Z M11702.0,3779.0 L10782.0,3779.0 L10811.3,3701.3 L11613.2,3701.3 Z M10782.0,3779.0 L10407.0,3449.0 L10480.1,3409.9 L10811.3,3701.3 Z M11127.0,2249.0 L10247.0,2249.0 L10403.2,2171.3 L11195.0,2171.3 Z M10710.0,2949.0 L11220.0,2949.0 L11287.9,2871.3 L10621.2,2871.3 Z M10772.0,3409.0 L10710.0,2949.0 L10621.2,2871.3 L10704.1,3486.7 Z" fill="#000" stroke="#000" stroke-width="2" stroke-linejoin="miter"/><path d="M4995.6,1956.7 L5809.1,1956.7 L6043.2,3701.3 L5241.3,3701.3 L4910.1,3409.9 L4846.6,2933.4 L5086.5,2656.7 L5689.7,2656.7 L5625.0,2171.3 L4833.2,2171.3 Z M5051.2,2871.3 L5717.9,2871.3 L5800.8,3486.7 L5134.1,3486.7 Z M6165.8,1956.7 L6379.1,1956.7 L6613.2,3701.3 L6399.9,3701.3 Z M6079.8,1316.7 L6432.6,1316.7 L6356.3,1541.3 L6109.9,1541.3 Z M6735.8,1956.7 L7722.8,1956.7 L7618.4,2171.3 L6978.2,2171.3 L7052.1,2721.7 L7882.0,2721.7 L8013.2,3701.3 L7026.2,3701.3 L7130.6,3486.7 L7770.8,3486.7 L7696.9,2936.3 L6867.0,2936.3 Z M8172.6,2233.4 L8412.5,1956.7 L9929.1,1956.7 L10163.2,3701.3 L9950.0,3701.3 L9745.0,2171.3 L9168.2,2171.3 L9373.2,3701.3 L9160.0,3701.3 L8955.0,2171.3 L8378.2,2171.3 L8583.2,3701.3 L8369.9,3701.3 Z M10565.6,1956.7 L11379.1,1956.7 L11613.2,3701.3 L10811.3,3701.3 L10480.1,3409.9 L10416.6,2933.4 L10656.5,2656.7 L11259.7,2656.7 L11195.0,2171.3 L10403.2,2171.3 Z M10621.2,2871.3 L11287.9,2871.3 L11370.8,3486.7 L10704.1,3486.7 Z" fill="url(#wh)" fill-rule="evenodd"/></g></svg>