@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.
- package/README.md +9 -0
- package/bench/brand/README.md +22 -17
- package/bench/brand/app-icon-1024.png +0 -0
- package/bench/brand/app-icon-192.png +0 -0
- package/bench/brand/app-icon-512.png +0 -0
- package/bench/brand/app-icon-dark-512.png +0 -0
- package/bench/brand/app-icon-maskable-512.png +0 -0
- package/bench/brand/apple-touch-icon.png +0 -0
- package/bench/brand/favicon-32.png +0 -0
- package/bench/brand/favicon.ico +0 -0
- package/bench/brand/favicon.svg +1 -1
- package/bench/brand/manifest.webmanifest +26 -0
- package/bench/brand/share-card-dark.png +0 -0
- package/bench/brand/share-card-dark.svg +1 -0
- package/bench/brand/share-card-light.png +0 -0
- package/bench/brand/share-card-light.svg +1 -0
- package/bench/brand/stripe-icon.png +0 -0
- package/bench/brand/zaisma-icon-dark.svg +1 -0
- package/bench/brand/zaisma-icon-light.svg +1 -0
- package/bench/brand/zaisma-mark-glass-black-2048.png +0 -0
- package/bench/brand/zaisma-mark-glass-black.svg +1 -0
- package/bench/brand/zaisma-mark-glass-white-2048.png +0 -0
- package/bench/brand/zaisma-mark-glass-white.svg +1 -0
- package/bench/brand/zaisma-wordmark-glass-black-2400.png +0 -0
- package/bench/brand/zaisma-wordmark-glass-black.svg +1 -0
- package/bench/brand/zaisma-wordmark-glass-white-2400.png +0 -0
- package/bench/brand/zaisma-wordmark-glass-white.svg +1 -0
- package/bench/glass.js +44 -21
- package/bin/fast-lane.js +212 -0
- package/bin/local-play-certificate.js +11 -6
- package/bin/node-parts.js +9 -1
- package/bin/node.d.ts +12 -0
- package/bin/node.js +59 -15
- package/package.json +13 -1
- package/sdk/zaisma.d.ts +27 -7
- package/sdk/zaisma.js +166 -7
- package/src/room.d.ts +18 -3
- package/src/room.js +12 -12
- package/bench/brand/zaisma-app-icon.svg +0 -1
- package/bench/brand/zaisma-mark-metal-2048.png +0 -0
- package/bench/brand/zaisma-mark-metal.svg +0 -1
- package/bench/brand/zaisma-mark-tricolor.svg +0 -1
- package/bench/brand/zaisma-wordmark-metal-2400.png +0 -0
- 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 +
|
|
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
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
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>
|
|
Binary file
|
|
@@ -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>
|
|
Binary file
|
|
@@ -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>
|