tickcli-js 0.0.0-stage → 0.1.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/CHANGELOG.md +15 -0
- package/LICENSE +21 -0
- package/README.md +360 -2
- package/client/game.js +982 -0
- package/client/game3d.js +109 -0
- package/client/relay.js +196 -0
- package/client/room.js +632 -0
- package/dist/cli.d.ts +11 -0
- package/dist/cli.js +82 -0
- package/dist/cli.js.map +1 -0
- package/dist/hub.d.ts +619 -0
- package/dist/hub.js +1777 -0
- package/dist/hub.js.map +1 -0
- package/dist/node.d.ts +106 -0
- package/dist/node.js +301 -0
- package/dist/node.js.map +1 -0
- package/dist/remote.d.ts +75 -0
- package/dist/remote.js +214 -0
- package/dist/remote.js.map +1 -0
- package/dist/serve.d.ts +42 -0
- package/dist/serve.js +231 -0
- package/dist/serve.js.map +1 -0
- package/examples/games/arena.html +108 -0
- package/examples/games/arena3d.html +110 -0
- package/examples/games/chess.html +132 -0
- package/examples/games/cup.html +52 -0
- package/examples/games/delve.html +137 -0
- package/examples/games/dungeon.html +110 -0
- package/examples/games/neon-paint.html +79 -0
- package/examples/games/own-engine.html +41 -0
- package/examples/games/race.html +83 -0
- package/examples/games/shooter.html +95 -0
- package/examples/games/sky-duel.html +136 -0
- package/examples/games/space-squad.html +3627 -0
- package/examples/games/tag.html +51 -0
- package/examples/games/tictactoe.html +53 -0
- package/package.json +101 -4
- package/src/cli.ts +59 -0
- package/src/hub.ts +1521 -0
- package/src/node.ts +245 -0
- package/src/remote.ts +236 -0
- package/src/serve.ts +224 -0
package/client/game3d.js
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/* tickcli three.js - three.js scenes in tickcli games. MIT. Load three.js first (it sets window.THREE; nothing of it is bundled here), then
|
|
2
|
+
room.js, game.js and this file, and call Game({context: "webgl", ...}). Everything here is optional glue; the game owns its scene.
|
|
3
|
+
|
|
4
|
+
var R3 = TickcliThree, renderer, scene, camera, others;
|
|
5
|
+
Game({
|
|
6
|
+
context: "webgl",
|
|
7
|
+
multiplayer: {max: 8, player: {x: [-20, 20], y: [0, 5], z: [-20, 20], qx: [-1, 1], qy: [-1, 1], qz: [-1, 1], qw: [-1, 1]}},
|
|
8
|
+
setup(g) {
|
|
9
|
+
renderer = renderer || R3.renderer(g); // a WebGLRenderer on the shell's canvas (once: setup runs every match)
|
|
10
|
+
scene = new THREE.Scene(); camera = new THREE.PerspectiveCamera(60, 1, 0.1, 200);
|
|
11
|
+
others = R3.pool(scene, function (p) { return new THREE.Mesh(box, mat); });
|
|
12
|
+
},
|
|
13
|
+
update(g, dt) { ... move my object ...; g.me.set(R3.pose(myObj)); }, // x, y, z and the quaternion, rounded (12 sends a second at most)
|
|
14
|
+
draw(g) {
|
|
15
|
+
R3.resize(renderer, camera, g); // drawing size, pixel ratio (the quality ladder) and camera aspect
|
|
16
|
+
others.sync(g.players.filter(function (p) { return !p.me; })); // one object per player, placed where they are (eased), gone when they leave
|
|
17
|
+
renderer.render(scene, camera);
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
R3.renderer(g, opts) new THREE.WebGLRenderer({canvas: g.canvas, antialias: true, alpha: true, ...opts})
|
|
22
|
+
R3.resize(renderer, camera, g) when g.w, g.h or g.dpr changed: setPixelRatio, setSize (the CSS size is left to the shell), camera aspect.
|
|
23
|
+
True when it changed something. Call it at the top of draw.
|
|
24
|
+
R3.place(obj, s) obj.position from s.x, y, z; obj.quaternion from s.qx, qy, qz, qw (or obj.rotation.y from s.angle). Fields missing
|
|
25
|
+
in s are left alone.
|
|
26
|
+
R3.pose(obj, decimals) {x, y, z, qx, qy, qz, qw} of obj (position to 2 decimals, quaternion to 3): pass it to g.me.set or into g.shared.
|
|
27
|
+
R3.pool(parent, make, drop) objects kept in step with a list: pool.sync(list, pick) where list is g.players (an array, by id) or an object
|
|
28
|
+
keyed by id (g.shared.mobs). New ids get make(item, id) added to parent, every object is placed from pick(item)
|
|
29
|
+
(default: item.state for players, else the item itself), ids gone from the list are removed (drop(obj, id) to
|
|
30
|
+
free geometry). pool.get(id), pool.map (a Map), pool.clear().
|
|
31
|
+
Others are already eased by room.js (positions and the quaternion as one turn, the short way round); place() only copies. */
|
|
32
|
+
(function () {
|
|
33
|
+
"use strict";
|
|
34
|
+
function three() {
|
|
35
|
+
var T = window.THREE;
|
|
36
|
+
if (!T || !T.WebGLRenderer) throw new Error("TickcliThree: load three.js first (window.THREE)");
|
|
37
|
+
return T;
|
|
38
|
+
}
|
|
39
|
+
var num = function (v) { return typeof v === "number" && v === v; };
|
|
40
|
+
var round = function (v, n) { var k = Math.pow(10, n); return Math.round(v * k) / k; };
|
|
41
|
+
|
|
42
|
+
function renderer(g, opts) {
|
|
43
|
+
var o = { canvas: g.canvas, antialias: true, alpha: true };
|
|
44
|
+
if (opts) for (var k in opts) o[k] = opts[k];
|
|
45
|
+
var r = new (three().WebGLRenderer)(o);
|
|
46
|
+
r.__cw = { w: 0, h: 0, d: 0 };
|
|
47
|
+
return r;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function resize(r, camera, g) {
|
|
51
|
+
var s = r.__cw || (r.__cw = { w: 0, h: 0, d: 0 }), w = Math.max(1, g.w || 1), h = Math.max(1, g.h || 1), d = g.dpr || 1;
|
|
52
|
+
if (s.w === w && s.h === h && s.d === d) return false;
|
|
53
|
+
s.w = w; s.h = h; s.d = d;
|
|
54
|
+
r.setPixelRatio(d);
|
|
55
|
+
r.setSize(w, h, false); // false: the shell sizes the canvas on the page; this sets only its drawing buffer
|
|
56
|
+
if (camera && camera.isPerspectiveCamera) { camera.aspect = w / h; camera.updateProjectionMatrix(); }
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function place(obj, s) {
|
|
61
|
+
if (!obj || !s) return obj;
|
|
62
|
+
var p = obj.position;
|
|
63
|
+
if (num(s.x)) p.x = s.x;
|
|
64
|
+
if (num(s.y)) p.y = s.y;
|
|
65
|
+
if (num(s.z)) p.z = s.z;
|
|
66
|
+
if (num(s.qx) && num(s.qy) && num(s.qz) && num(s.qw)) obj.quaternion.set(s.qx, s.qy, s.qz, s.qw);
|
|
67
|
+
else if (num(s.angle)) obj.rotation.y = s.angle;
|
|
68
|
+
return obj;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function pose(obj, decimals) {
|
|
72
|
+
var n = decimals == null ? 2 : decimals, p = obj.position, q = obj.quaternion;
|
|
73
|
+
return { x: round(p.x, n), y: round(p.y, n), z: round(p.z, n), qx: round(q.x, 3), qy: round(q.y, 3), qz: round(q.z, 3), qw: round(q.w, 3) };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function pool(parent, make, drop) {
|
|
77
|
+
var map = new Map();
|
|
78
|
+
function remove(id) {
|
|
79
|
+
var obj = map.get(id);
|
|
80
|
+
map.delete(id);
|
|
81
|
+
if (obj && obj.parent) obj.parent.remove(obj);
|
|
82
|
+
if (obj && drop) try { drop(obj, id); } catch (e) { console.error(e); }
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
map: map,
|
|
86
|
+
get: function (id) { return map.get(id); },
|
|
87
|
+
sync: function (list, pick) {
|
|
88
|
+
var seen = {}, arr = Array.isArray(list), keys = arr ? null : Object.keys(list || {}), n = arr ? list.length : keys.length;
|
|
89
|
+
for (var i = 0; i < n; i++) {
|
|
90
|
+
var item = arr ? list[i] : list[keys[i]], id = arr ? item && item.id : keys[i];
|
|
91
|
+
if (id == null || !item || typeof item !== "object") continue;
|
|
92
|
+
id = String(id); seen[id] = 1;
|
|
93
|
+
var obj = map.get(id);
|
|
94
|
+
if (!obj) {
|
|
95
|
+
try { obj = make(item, id); } catch (e) { console.error(e); obj = null; }
|
|
96
|
+
if (!obj) continue;
|
|
97
|
+
parent.add(obj); map.set(id, obj);
|
|
98
|
+
}
|
|
99
|
+
place(obj, pick ? pick(item) : arr && item.state ? item.state : item);
|
|
100
|
+
}
|
|
101
|
+
map.forEach(function (_, id) { if (!seen[id]) remove(id); });
|
|
102
|
+
return map;
|
|
103
|
+
},
|
|
104
|
+
clear: function () { map.forEach(function (_, id) { remove(id); }); },
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
window.TickcliThree = { renderer: renderer, resize: resize, place: place, pose: pose, pool: pool };
|
|
109
|
+
})();
|
package/client/relay.js
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/* tickcli relay.js. MIT. Runs in the TRUSTED page around a game frame (your app, or a wrapper page), never inside the game. For a game that is
|
|
2
|
+
itself the top page, run it in the same window with frame: () => ({ contentWindow: window }).
|
|
3
|
+
The game (room.js, maybe sandboxed with no network) says hello; this answers with a private MessageChannel port and from then on relays room messages between
|
|
4
|
+
that port and one WebSocket. The game never sees the socket address, a ticket, a pass or a user id: the connect() callback below makes those.
|
|
5
|
+
|
|
6
|
+
tickcliRelay({
|
|
7
|
+
frame: () => iframeElement, // the game's frame (only its own window may say hello)
|
|
8
|
+
connect: async () => ({ url, first }), // a fresh socket address, and an optional first message (the member ticket). Reject with an
|
|
9
|
+
// error that has final: true (and a reason) when trying again cannot help (signed out, the
|
|
10
|
+
// game deleted, rooms turned off): the game then shows the reason instead of retrying
|
|
11
|
+
guest: false, // public link visitor (may rename in the lobby)
|
|
12
|
+
name: () => "", onName: (n) => {}, // guests: the name to start with, and where to keep a new one
|
|
13
|
+
profile: { load: () => obj|null, save: (obj) => {} }, // optional: keep the player's saved profile here (a public link's visitor: in their own
|
|
14
|
+
// browser) instead of on the server; the game's profile and save messages never leave the page
|
|
15
|
+
}) -> { stop() }
|
|
16
|
+
|
|
17
|
+
Reconnects by itself (random, growing delay; at once when the tab comes back or the network returns), tells the server when the tab is hidden, and
|
|
18
|
+
checks the line: a ping every 5 s, no answer for 20 s = reconnect. Close codes 4001-4003 and 4005-4007 are final (the game shows why), so is a
|
|
19
|
+
final connect() error, and so is 10 minutes of failed tries in a row. A message over 16,000 bytes is not sent: the game gets error too_big. */
|
|
20
|
+
(function (root) {
|
|
21
|
+
"use strict";
|
|
22
|
+
var FINAL = { 4001: 1, 4002: 1, 4003: 1, 4005: 1, 4006: 1, 4007: 1, 4010: 1 };
|
|
23
|
+
var FROM_GAME = { me: 1, shared: 1, act: 1, result: 1, fx: 1, alive: 1, name: 1, start: 1, end: 1, kick: 1, leave: 1, profile: 1, save: 1, world: 1, sync: 1, rooms: 1, rate: 1, stay: 1 };
|
|
24
|
+
var MAX_BYTES = 16000, GIVE_UP_MS = 600000;
|
|
25
|
+
var enc = typeof TextEncoder === "function" ? new TextEncoder() : null;
|
|
26
|
+
var bytes = function (t) { return enc ? enc.encode(t).length : unescape(encodeURIComponent(t)).length; };
|
|
27
|
+
var isObj = function (v) { return !!v && typeof v === "object" && !Array.isArray(v); };
|
|
28
|
+
function merge(into, patch) { for (var k in patch) { var v = patch[k]; if (v === null) delete into[k]; else if (isObj(v)) { if (!isObj(into[k])) into[k] = {}; merge(into[k], v); } else into[k] = v; } return into; }
|
|
29
|
+
|
|
30
|
+
function relay(o) {
|
|
31
|
+
var port = null, ws = null, join = null, stopped = false, attempt = 0, retryT = 0, pingT = 0, lastRx = 0, hiddenAt = 0, open = false, failSince = 0;
|
|
32
|
+
var rtts = []; // the last 4 clock-ping round trips (ms): the best goes along with the next ping (the room list shows a host's link)
|
|
33
|
+
var portN = null;
|
|
34
|
+
var code0 = null, firstVer = "";
|
|
35
|
+
try { var mm = /(?:^|[#&])room=([A-Za-z0-9]{4,6})\b/.exec(location.hash || ""); if (mm) code0 = mm[1].toUpperCase(); } catch (e) { /* no hash */ }
|
|
36
|
+
|
|
37
|
+
function toGame(m) { if (port) try { port.postMessage(m); } catch (e) { /* port closed */ } }
|
|
38
|
+
function status(s, extra) { var m = { k: "status", s: s }; for (var k in extra || {}) m[k] = extra[k]; toGame(m); }
|
|
39
|
+
function sendRaw(obj) { if (ws && ws.readyState === 1) try { ws.send(JSON.stringify(obj)); return true; } catch (e) { /* closing */ } return false; }
|
|
40
|
+
|
|
41
|
+
function closeSocket() {
|
|
42
|
+
clearInterval(pingT); pingT = 0; open = false;
|
|
43
|
+
if (ws) { var w = ws; ws = null; w.onopen = w.onmessage = w.onclose = w.onerror = null; try { w.close(1000, "bye"); } catch (e) { /* gone */ } }
|
|
44
|
+
}
|
|
45
|
+
function giveUp(reason) { clearTimeout(retryT); closeSocket(); status("closed", { code: 0, reason: reason }); join = null; failSince = 0; }
|
|
46
|
+
function retry(code) {
|
|
47
|
+
if (stopped || !join) return;
|
|
48
|
+
clearTimeout(retryT);
|
|
49
|
+
if (!failSince) failSince = Date.now();
|
|
50
|
+
else if (Date.now() - failSince > GIVE_UP_MS) return giveUp("Could not reach the server for 10 minutes.");
|
|
51
|
+
// full jitter: anywhere between 0 and an exponentially growing cap (15 s), so many players do not come back at the same moment
|
|
52
|
+
// a restart (1012) or a busy server (1013): everyone is coming back at once, so the first try is spread over 0.3 to 3 s
|
|
53
|
+
var cap = Math.min(15000, 500 * Math.pow(2, attempt));
|
|
54
|
+
var wait = (code === 1012 || code === 1013) && attempt === 0 ? 300 + Math.random() * 2700 : Math.max(code === 1012 || code === 1013 ? 300 : code === 4004 ? 3000 : 0, Math.random() * cap);
|
|
55
|
+
attempt++;
|
|
56
|
+
status("retry", { code: code || 0, wait: Math.round(wait) });
|
|
57
|
+
retryT = setTimeout(connect, wait);
|
|
58
|
+
}
|
|
59
|
+
function connect() {
|
|
60
|
+
if (stopped || !join) return;
|
|
61
|
+
clearTimeout(retryT); closeSocket();
|
|
62
|
+
status("connecting");
|
|
63
|
+
Promise.resolve().then(o.connect).then(function (c) {
|
|
64
|
+
if (stopped) return;
|
|
65
|
+
var w;
|
|
66
|
+
try { w = new WebSocket(new URL(c.url, location.href).href.replace(/^http/, "ws")); } catch (e) { return retry(0); }
|
|
67
|
+
ws = w;
|
|
68
|
+
w.onopen = function () {
|
|
69
|
+
open = true; lastRx = Date.now();
|
|
70
|
+
if (c.first) sendRaw(c.first);
|
|
71
|
+
var j = { v: 1, t: "join", spec: join.spec, hidden: document.hidden === true };
|
|
72
|
+
if (join.code) j.code = join.code; else if (join.priv) j.private = true;
|
|
73
|
+
if (join.alone) j.alone = true; // Play alone: a room of one with the player's own saved game
|
|
74
|
+
if (join.name) j.name = join.name;
|
|
75
|
+
if (join.f && join.f.length) j.f = join.f; // what the page understands (effects in the frame, ...); old pages send none
|
|
76
|
+
sendRaw(j);
|
|
77
|
+
// 4 clock pings 250 ms apart (a good shared clock within a second of joining), then one every 5 s
|
|
78
|
+
var ping = function () { var r = rtts.length ? Math.min.apply(null, rtts) : 0; sendRaw(r ? { v: 1, t: "time", c: Date.now(), r: r } : { v: 1, t: "time", c: Date.now() }); };
|
|
79
|
+
ping();
|
|
80
|
+
for (var pi = 1; pi < 4; pi++) setTimeout(function () { if (ws === w && open) ping(); }, pi * 250);
|
|
81
|
+
clearInterval(pingT);
|
|
82
|
+
pingT = setInterval(function () {
|
|
83
|
+
if (Date.now() - lastRx > 20000) { closeSocket(); return retry(0); } // a dead line (a phone that slept): start over
|
|
84
|
+
ping();
|
|
85
|
+
}, 5000);
|
|
86
|
+
};
|
|
87
|
+
w.onmessage = function (e) {
|
|
88
|
+
lastRx = Date.now();
|
|
89
|
+
var m; try { m = JSON.parse(e.data); } catch (x) { return; }
|
|
90
|
+
if (!m || typeof m !== "object") return;
|
|
91
|
+
if (m.t === "time" && typeof m.c === "number") { var rt = Date.now() - m.c; if (rt >= 0 && rt < 30000) { rtts.push(rt); if (rtts.length > 4) rtts.shift(); } }
|
|
92
|
+
if (m.t === "welcome") {
|
|
93
|
+
attempt = 0; failSince = 0; if (m.code) join.code = m.code; status("open", { code: m.code });
|
|
94
|
+
// the game was edited: after a restart (the page reloads, this relay stays) go back to this code, where the new version plays now
|
|
95
|
+
if (m.ver && firstVer && m.ver !== firstVer && m.code) code0 = m.code;
|
|
96
|
+
if (m.ver && !firstVer) firstVer = m.ver;
|
|
97
|
+
}
|
|
98
|
+
if (m.t === "newer" && join && join.code) code0 = join.code;
|
|
99
|
+
toGame({ k: "msg", m: m });
|
|
100
|
+
};
|
|
101
|
+
w.onclose = function (e) {
|
|
102
|
+
if (ws !== w) return;
|
|
103
|
+
ws = null; open = false; clearInterval(pingT);
|
|
104
|
+
if (FINAL[e.code]) { status("closed", { code: e.code, reason: String(e.reason || "") }); join = null; return; }
|
|
105
|
+
retry(e.code);
|
|
106
|
+
};
|
|
107
|
+
w.onerror = function () { /* onclose follows */ };
|
|
108
|
+
}).catch(function (err) {
|
|
109
|
+
if (stopped) return;
|
|
110
|
+
if (err && err.final) return giveUp(typeof err.reason === "string" && err.reason ? err.reason.slice(0, 200) : "Open this game again from the app.");
|
|
111
|
+
retry(0);
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function onPort(e) {
|
|
116
|
+
var d = e.data;
|
|
117
|
+
if (!d || typeof d !== "object") return;
|
|
118
|
+
if (d.k === "open" && d.join && typeof d.join === "object") {
|
|
119
|
+
var feats = Array.isArray(d.join.f) ? d.join.f.filter(function (x) { return typeof x === "string" && x.length <= 8; }).slice(0, 16) : null;
|
|
120
|
+
join = { f: feats, spec: d.join.spec, code: typeof d.join.code === "string" ? d.join.code : code0, priv: d.join.priv === true, alone: d.join.alone === true, name: !o.guest ? "" : typeof d.join.name === "string" && d.join.name ? d.join.name.slice(0, 40) : o.name ? String(o.name() || "").slice(0, 40) : "" };
|
|
121
|
+
attempt = 0; failSince = 0; connect();
|
|
122
|
+
} else if (d.k === "send" && d.m && typeof d.m === "object" && FROM_GAME[d.m.t] === 1) {
|
|
123
|
+
if (o.profile && d.m.t === "profile") {
|
|
124
|
+
var cur = null; try { cur = o.profile.load(); } catch (x) { /* storage blocked */ }
|
|
125
|
+
toGame({ k: "msg", m: { t: "profile", p: isObj(cur) ? cur : {} } });
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
if (o.profile && d.m.t === "save") {
|
|
129
|
+
try {
|
|
130
|
+
var base = o.profile.load(), merged = merge(isObj(base) ? base : {}, JSON.parse(JSON.stringify(d.m.p || {})));
|
|
131
|
+
if (JSON.stringify(merged).length <= 16000) o.profile.save(merged);
|
|
132
|
+
} catch (x) { /* storage blocked or full */ }
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
var text; try { text = JSON.stringify(Object.assign({}, d.m, { v: 1 })); } catch (x) { return; }
|
|
136
|
+
if (bytes(text) > MAX_BYTES) toGame({ k: "msg", m: { t: "error", code: "too_big" } }); // the server would refuse it: say so, do not drop it silently
|
|
137
|
+
else if (ws && ws.readyState === 1) ws.send(text);
|
|
138
|
+
if (d.m.t === "leave") { join = null; closeSocket(); }
|
|
139
|
+
if (d.m.t === "name" && o.onName && typeof d.m.name === "string") try { o.onName(d.m.name); } catch (x) { /* storage blocked */ }
|
|
140
|
+
} else if (d.k === "invite-anyone" && typeof o.inviteAnyone === "function") {
|
|
141
|
+
try { o.inviteAnyone(); } catch (x) { /* the host app's own problem */ } // the app shows its own way to let anyone in (a public link)
|
|
142
|
+
} else if (d.k === "invite" && typeof d.code === "string" && /^[A-Za-z0-9]{4,6}$/.test(d.code)) {
|
|
143
|
+
var link = location.origin + location.pathname + location.search + "#room=" + d.code;
|
|
144
|
+
var done = function (ok) { toGame({ k: "invite", ok: ok }); };
|
|
145
|
+
try { navigator.clipboard.writeText(link).then(function () { done(true); }, function () { done(false); }); } catch (x) { done(false); }
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function onHello(e) {
|
|
150
|
+
var f = o.frame();
|
|
151
|
+
if (!f || e.source !== f.contentWindow || !e.data || e.data.tickcli !== "hello") return;
|
|
152
|
+
if (port && e.data.n === portN) return; // the same game saying hello again before our answer arrived
|
|
153
|
+
// A (re)loaded game: drop the old line and give it a fresh private port.
|
|
154
|
+
if (port) { try { port.close(); } catch (x) { /* closed */ } }
|
|
155
|
+
join = null; closeSocket(); clearTimeout(retryT);
|
|
156
|
+
var ch = new MessageChannel();
|
|
157
|
+
port = ch.port1;
|
|
158
|
+
portN = e.data.n;
|
|
159
|
+
port.onmessage = onPort;
|
|
160
|
+
f.contentWindow.postMessage({ tickcli: "port", n: e.data.n }, "*", [ch.port2]);
|
|
161
|
+
toGame({ k: "info", code: code0, guest: !!o.guest, anyone: typeof o.inviteAnyone === "function" });
|
|
162
|
+
}
|
|
163
|
+
function onVis() {
|
|
164
|
+
if (document.hidden) { hiddenAt = Date.now(); sendRaw({ v: 1, t: "vis", hidden: true }); return; }
|
|
165
|
+
var away = hiddenAt ? Date.now() - hiddenAt : 0;
|
|
166
|
+
hiddenAt = 0;
|
|
167
|
+
if (!join) return;
|
|
168
|
+
if (!open) { attempt = 0; return connect(); }
|
|
169
|
+
sendRaw({ v: 1, t: "vis", hidden: false });
|
|
170
|
+
if (away > 5000) {
|
|
171
|
+
// after a while away the socket may be dead without knowing it: ask, and start over if no answer comes
|
|
172
|
+
var t0 = lastRx;
|
|
173
|
+
sendRaw({ v: 1, t: "time", c: Date.now() });
|
|
174
|
+
setTimeout(function () { if (join && lastRx === t0) { closeSocket(); attempt = 0; connect(); } }, 3000);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
function onOnline() { if (join && !open) { attempt = 0; connect(); } }
|
|
178
|
+
function onShow(e) { if (e.persisted && join) { attempt = 0; connect(); } }
|
|
179
|
+
|
|
180
|
+
root.addEventListener("message", onHello);
|
|
181
|
+
document.addEventListener("visibilitychange", onVis);
|
|
182
|
+
root.addEventListener("online", onOnline);
|
|
183
|
+
root.addEventListener("pageshow", onShow);
|
|
184
|
+
return {
|
|
185
|
+
stop: function () {
|
|
186
|
+
stopped = true; join = null; clearTimeout(retryT); closeSocket();
|
|
187
|
+
if (port) try { port.close(); } catch (x) { /* closed */ }
|
|
188
|
+
root.removeEventListener("message", onHello);
|
|
189
|
+
document.removeEventListener("visibilitychange", onVis);
|
|
190
|
+
root.removeEventListener("online", onOnline);
|
|
191
|
+
root.removeEventListener("pageshow", onShow);
|
|
192
|
+
},
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
root.tickcliRelay = relay;
|
|
196
|
+
})(typeof window !== "undefined" ? window : globalThis);
|