@zaisma/game-server 1.11.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 (44) 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 +44 -21
  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 +27 -7
  36. package/sdk/zaisma.js +166 -7
  37. package/src/room.d.ts +18 -3
  38. package/src/room.js +12 -12
  39. package/bench/brand/zaisma-app-icon.svg +0 -1
  40. package/bench/brand/zaisma-mark-metal-2048.png +0 -0
  41. package/bench/brand/zaisma-mark-metal.svg +0 -1
  42. package/bench/brand/zaisma-mark-tricolor.svg +0 -1
  43. package/bench/brand/zaisma-wordmark-metal-2400.png +0 -0
  44. 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. */
@@ -425,6 +429,11 @@
425
429
  this.isSpectator = setup.isSpectator === true;
426
430
  this.spectatorCount = 0;
427
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";
428
437
  this._pass = setup.pass;
429
438
  this._made = { isListed: this.isListed, friendJoinsEnabled: setup.friendJoinsEnabled === false ? false : undefined, name: setup.name, labels: setup.labels };
430
439
  this._joinData = setup.joinData === undefined ? null : setup.joinData;
@@ -473,6 +482,135 @@
473
482
  }
474
483
  Room.prototype = Object.create(Emitter.prototype);
475
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
+
476
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. */
477
615
  function inviteUrlFor(roomCode, serverUrl) {
478
616
  if (!config.site || !config.game) return null;
@@ -528,13 +666,23 @@
528
666
 
529
667
  Room.prototype._connect = function () {
530
668
  var room = this;
669
+ var isFastLane = Boolean(this._fastLaneUrl && !this._isFastLaneOff && typeof WebTransport === "function");
531
670
  // From a tester link, Zaisma rooms hear which one, so its players leave when it's turned off.
532
- var ws = new WebSocket(this._url + (config.tester && !this._serverUrl ? "?tester=" + config.tester : ""));
533
- 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";
534
673
  this._ws = ws;
535
674
  this._isWelcomed = false;
536
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
+ };
537
684
  ws.onopen = function () {
685
+ room.transport = isFastLane ? "webtransport" : "websocket";
538
686
  room._pingAtMs = room._pongAtMs = room._heardAtMs = Date.now();
539
687
  room._probeIntervalSec = null;
540
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 };
@@ -873,6 +1021,11 @@
873
1021
 
874
1022
  /** A latest-only send: now, after anything still waiting, or into its slot while the socket is backed up. */
875
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
+ }
876
1029
  if (this._canSend() && !this._queue.length && !(this._ws.bufferedAmount >= BACKED_UP_SIZE_BYTES)) {
877
1030
  this._sendWaiting();
878
1031
  this._ws.send(payload);
@@ -1043,6 +1196,7 @@
1043
1196
  */
1044
1197
  Room.prototype._move = function (message) {
1045
1198
  var serverUrl = this._serverUrl;
1199
+ var transport = this._fastLaneUrl ? "webtransport" : undefined;
1046
1200
  var room = this;
1047
1201
  return {
1048
1202
  roomCode: message.roomCode,
@@ -1050,7 +1204,7 @@
1050
1204
  join: function (joinOptions) {
1051
1205
  var refused = unknownOption(joinOptions, MOVE_JOIN_OPTIONS, "move.join");
1052
1206
  if (refused) return Promise.reject(refused);
1053
- return connect(message.roomCode, Object.assign({}, joinOptions || {}, { serverUrl: serverUrl }));
1207
+ return connect(message.roomCode, Object.assign({}, joinOptions || {}, { serverUrl: serverUrl, transport: transport }));
1054
1208
  },
1055
1209
  };
1056
1210
  };
@@ -1071,6 +1225,7 @@
1071
1225
  var fromRoomCode = this.roomCode;
1072
1226
  this.roomCode = roomCode;
1073
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);
1074
1229
  this.inviteUrl = inviteUrlFor(roomCode, this._serverUrl);
1075
1230
  // A room the game server moves players to is listed only if the game server lists it.
1076
1231
  this._made = { isListed: false };
@@ -1372,6 +1527,7 @@
1372
1527
  var idleTimeoutSec = joinOptions.idleTimeoutSec;
1373
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 + "."));
1374
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).'));
1375
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."));
1376
1532
 
1377
1533
  var roomCodeReady = roomCode
@@ -1381,10 +1537,13 @@
1381
1537
  : post("/v1/rooms", { gameSlug: config.game }).then(function (answer) { return answer.roomCode; });
1382
1538
  return Promise.all([roomCodeReady, getPass()]).then(function (ready) {
1383
1539
  var base = (serverUrl || config.rooms).replace(/^http/, "ws");
1540
+ var path = "/v1/rooms/" + config.game + "/" + ready[0];
1384
1541
  var room = new Room({
1385
1542
  roomCode: ready[0],
1386
1543
  pass: ready[1].pass,
1387
- 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,
1388
1547
  isListed: joinOptions.isListed,
1389
1548
  friendJoinsEnabled: joinOptions.friendJoinsEnabled,
1390
1549
  name: joinOptions.name,
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>