@ikirmikir/game-sdk 1.0.0 → 1.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/README.md +6 -2
- package/dist/ikirmikir.js +2 -2
- package/dist/index.d.ts +5 -1
- package/dist/index.js +207 -24
- package/dist/multiplayer.d.ts +56 -0
- package/dist/protocol.d.ts +57 -1
- package/dist/protocol.js +59 -4
- package/dist/room.d.ts +56 -0
- package/dist/room.js +75 -0
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @ikirmikir/game-sdk
|
|
2
2
|
|
|
3
|
-
Optional SDK for browser games on [Ikirmikir](https://ikirmikir.com): real loading progress, gameplay start and stop, pause and resume, the player's sound switch, player context (language, device, touch) and
|
|
3
|
+
Optional SDK for browser games on [Ikirmikir](https://ikirmikir.com): real loading progress, gameplay start and stop, pause and resume, the player's sound switch, player context (language, device, touch), sharing, and multiplayer rooms to play with friends (1.1). Any engine that runs in a browser. Outside Ikirmikir it runs in local mode, so one build works everywhere, and no call ever throws.
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
6
|
npm install @ikirmikir/game-sdk
|
|
@@ -14,12 +14,16 @@ ik.game.loadingProgress(50);
|
|
|
14
14
|
ik.game.loadingDone();
|
|
15
15
|
ik.on("pause", () => game.pause());
|
|
16
16
|
ik.on("mute", muted => { audio.muted = muted; });
|
|
17
|
+
|
|
18
|
+
const { room, error } = await ik.multiplayer.create({ maxPlayers: 2 }); // inside Ikirmikir only
|
|
19
|
+
room?.on("message", (data, from) => applyMove(data));
|
|
20
|
+
room?.send({ cell: 4 });
|
|
17
21
|
```
|
|
18
22
|
|
|
19
23
|
Without a bundler, use `dist/ikirmikir.js` (gives `window.Ikirmikir`), from this package or the CDN:
|
|
20
24
|
|
|
21
25
|
```html
|
|
22
|
-
<script src="https://cdn.jsdelivr.net/npm/@ikirmikir/game-sdk@1.
|
|
26
|
+
<script src="https://cdn.jsdelivr.net/npm/@ikirmikir/game-sdk@1.1.0/dist/ikirmikir.js"></script>
|
|
23
27
|
```
|
|
24
28
|
|
|
25
29
|
Quick start, API reference and a test tool: https://ikirmikir.com/developers/
|
package/dist/ikirmikir.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
/*! @ikirmikir/game-sdk 1.
|
|
2
|
-
"use strict";(()=>{var I=["loading","gameplay","pause","mute","context","share"],
|
|
1
|
+
/*! @ikirmikir/game-sdk 1.1.0 | MIT | https://ikirmikir.com/developers/ */
|
|
2
|
+
"use strict";(()=>{var h={maxPlayers:8,maxMessageBytes:4096,messagesPerSecond:20,reconnectSeconds:30,emptyMinutes:10};var P=e=>typeof e=="string"&&/^[A-HJ-NP-Z2-9]{5}$/.test(e),G=["not-found","full","too-big","slow-down","unavailable","bad-request"];function j(e){try{let n=JSON.stringify(e);return n===void 0?-1:new TextEncoder().encode(n).length}catch{return-1}}var I=1,U=["loading","gameplay","pause","mute","context","share","multiplayer"],$=["reconnecting","connected","closed"],C=e=>typeof e=="object"&&e!==null&&!Array.isArray(e),A=(e,n)=>typeof e=="string"&&e.length>0&&e.length<=n,E=(e,n,m)=>Number.isInteger(e)&&e>=n&&e<=m,R=e=>typeof e=="boolean",T=e=>typeof e=="string"&&/^[a-z0-9]{1,16}$/.test(e),N=e=>C(e)&&T(e.id)&&A(e.name,32)&&R(e.connected),F=e=>Array.isArray(e)&&e.length<=h.maxPlayers&&e.every(N),M=e=>({id:e.id,name:e.name,connected:e.connected});function L(e){if(!C(e)||e.ik!==I)return null;switch(e.type){case"welcome":{let n=e.context;if(!Array.isArray(e.features)||!R(e.muted)||!C(n)||!A(n.language,35)||n.device!=="mobile"&&n.device!=="desktop"||!R(n.touch))return null;let m=U.filter(p=>e.features.includes(p)),c=P(e.invite)?e.invite:null;return{ik:1,type:"welcome",features:m,context:{language:n.language,device:n.device,touch:n.touch},muted:e.muted,invite:c}}case"pause":return{ik:1,type:"pause"};case"resume":return{ik:1,type:"resume"};case"mute":return R(e.muted)?{ik:1,type:"mute",muted:e.muted}:null;case"shared":return E(e.id,0,1e9)&&R(e.shared)?{ik:1,type:"shared",id:e.id,shared:e.shared}:null;case"mp.room":return E(e.id,0,1e9)&&P(e.code)&&A(e.link,300)&&N(e.you)&&F(e.players)&&E(e.max,2,h.maxPlayers)?{ik:1,type:"mp.room",id:e.id,code:e.code,link:e.link,you:M(e.you),players:e.players.map(M),max:e.max}:null;case"mp.error":return E(e.id,0,1e9)&&G.includes(e.error)?{ik:1,type:"mp.error",id:e.id,error:e.error}:null;case"mp.player":return N(e.player)?{ik:1,type:"mp.player",player:M(e.player)}:null;case"mp.away":case"mp.back":case"mp.gone":return T(e.id)?{ik:1,type:e.type,id:e.id}:null;case"mp.msg":return T(e.from)&&"data"in e?{ik:1,type:"mp.msg",from:e.from,data:e.data}:null;case"mp.status":return $.includes(e.status)&&F(e.players)?{ik:1,type:"mp.status",status:e.status,players:e.players.map(M)}:null;default:return null}}function _(e){return e==="https://ikirmikir.com"||e==="http://localhost:3000"||e==="http://127.0.0.1:3000"||/^https:\/\/([0-9a-f]{8}\.)?ikirmikir\.pages\.dev$/.test(e)}function z(e,n,m,c){let p=new Map,v=1,y=null;function d(r){let o={join:new Set,leave:new Set,away:new Set,back:new Set,message:new Set,status:new Set},i=r.players,g=!0,k=(t,...a)=>o[t].forEach(f=>{try{f(...a)}catch(w){console.error(w)}}),b=t=>i.find(a=>a.id===t),l=(t,a,f)=>{let w=b(t);w&&(w.connected=a,k(f,{...w}))},s={code:r.code,link:r.link,you:r.you,max:r.max,get players(){return i.map(t=>({...t}))},send(t){let a=j(t);return!g||a<0||a>h.maxMessageBytes?!1:(e({type:"mp.send",data:t}),!0)},invite:()=>g?c():Promise.resolve({shared:!1}),leave(){g&&(g=!1,y=null,e({type:"mp.leave"}))},on(t,a){let f=o[t];return!f||typeof a!="function"?()=>{}:(f.add(a),()=>{f.delete(a)})}};function u(t){if(g)switch(t.type){case"mp.player":return i=[...i.filter(a=>a.id!==t.player.id),t.player],k("join",{...t.player});case"mp.away":return l(t.id,!1,"away");case"mp.back":return l(t.id,!0,"back");case"mp.gone":{let a=b(t.id);i=i.filter(f=>f.id!==t.id),a&&k("leave",{...a});return}case"mp.msg":{let a=b(t.from);a&&k("message",t.data,{...a});return}case"mp.status":return t.status==="connected"&&(i=t.players),t.status==="closed"&&(g=!1,y=null),k("status",t.status)}}return{room:s,handle:u}}function x(r){if(!n())return Promise.resolve({error:"not-available-here"});y?.room.leave();let o=v++;return new Promise(i=>{p.set(o,i),setTimeout(()=>{p.delete(o)&&i({error:"unavailable"})},2e4),e({...r,id:o})})}let O={get invite(){return m()},create(r){let o=Math.round(Number(r?.maxPlayers??h.maxPlayers));return o>=2&&o<=h.maxPlayers?x({type:"mp.create",maxPlayers:o}):Promise.resolve({error:"bad-request"})},join(r){let o=String(r??"").trim().toUpperCase();return P(o)?x({type:"mp.join",code:o}):Promise.resolve({error:"not-found"})}};function S(r){if(r.type==="mp.room"){let o=p.get(r.id);return o&&(p.delete(r.id),y=d(r),o({room:y.room})),!0}return r.type==="mp.error"?(p.get(r.id)?.({error:r.error}),p.delete(r.id),!0):r.type.startsWith("mp.")?(y?.handle(r),!0):!1}return{api:O,receive:S}}var J="1.1.0",q=1500,D;function V(e){return D??(D=K(e))}async function K({gameId:e}){let n={pause:new Set,resume:new Set,mute:new Set},m=(l,...s)=>n[l].forEach(u=>{try{u(...s)}catch(t){console.error(t)}}),c="local",p=W(),v=!1,y=[],d,x=new Map,O=1,S=-1,r=!1,o=!1,i=l=>{if(d)try{parent.postMessage({ik:I,...l},d)}catch{}},g=null,k=z(i,()=>c==="ikirmikir"&&y.includes("multiplayer"),()=>g,()=>b(!0));async function b(l){if(c==="local"){if(l||!H())return{shared:!1};try{return await navigator.share({url:location.href}),{shared:!0}}catch{return{shared:!1}}}let s=O++;return new Promise(u=>{x.set(s,t=>u({shared:t})),setTimeout(()=>{x.delete(s)&&u({shared:!1})},6e4),i({type:"share",id:s,...l?{room:!0}:{}})})}if(typeof window<"u"&&window.parent!==window){let l=new Promise(s=>{addEventListener("message",u=>{if(u.source!==window.parent||!_(u.origin)||d&&u.origin!==d)return;let t=L(u.data);if(t){if(t.type==="welcome")return d?void 0:(d=u.origin,c="ikirmikir",p=t.context,v=t.muted,y=t.features,g=t.invite,s(!0));!d||k.receive(t)||(t.type==="pause"||t.type==="resume"?m(t.type):t.type==="mute"?t.muted!==v&&(v=t.muted,m("mute",v)):t.type==="shared"&&(x.get(t.id)?.(t.shared),x.delete(t.id)))}}),setTimeout(()=>s(!1),q)});try{parent.postMessage({ik:I,type:"hello",gameId:String(e).slice(0,64),sdk:J},"*")}catch{}await l}return c==="local"&&(y=["gameplay","loading","pause","context",...H()?["share"]:[]],typeof document<"u"&&document.addEventListener("visibilitychange",()=>m(document.hidden?"pause":"resume"))),{get env(){return c},get context(){return{...p}},get muted(){return v},has:l=>y.includes(l),game:{loadingProgress(l){let s=Math.max(0,Math.min(100,Math.round(Number(l)||0)));o||s===S||(S=s,i({type:"loading",progress:s}))},loadingDone(){o||(o=!0,i({type:"ready"}))},gameplayStart(){r||(r=!0,i({type:"gameplay",playing:r}))},gameplayStop(){r&&(r=!1,i({type:"gameplay",playing:r}))}},on(l,s){let u=n[l];return!u||typeof s!="function"?()=>{}:(u.add(s),()=>{u.delete(s)})},share:()=>b(!1),multiplayer:k.api}}function W(){let e=typeof navigator<"u"?navigator:void 0,n=!!e&&e.maxTouchPoints>0,m=typeof matchMedia<"u"&&matchMedia("(pointer: coarse)").matches;return{language:(e?.language||"en").split("-")[0],device:m?"mobile":"desktop",touch:n}}var H=()=>typeof navigator<"u"&&typeof navigator.share=="function",B={init:V,VERSION:J};globalThis.Ikirmikir=B;})();
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { type Context, type Feature } from "./protocol";
|
|
2
|
+
import { type Multiplayer } from "./multiplayer";
|
|
2
3
|
export type { Context, Feature } from "./protocol";
|
|
3
|
-
export
|
|
4
|
+
export type { Multiplayer, MultiplayerError, Player, Room, RoomResult, RoomStatus } from "./multiplayer";
|
|
5
|
+
export declare const VERSION = "1.1.0";
|
|
4
6
|
type Events = {
|
|
5
7
|
pause: () => void;
|
|
6
8
|
resume: () => void;
|
|
@@ -31,6 +33,8 @@ export interface Sdk {
|
|
|
31
33
|
share(): Promise<{
|
|
32
34
|
shared: boolean;
|
|
33
35
|
}>;
|
|
36
|
+
/** Rooms to play with friends, inside Ikirmikir only (has("multiplayer")). */
|
|
37
|
+
readonly multiplayer: Multiplayer;
|
|
34
38
|
}
|
|
35
39
|
export interface InitOptions {
|
|
36
40
|
/** The game's id on Ikirmikir, for example "runbro". */
|
package/dist/index.js
CHANGED
|
@@ -1,12 +1,40 @@
|
|
|
1
|
-
/*! @ikirmikir/game-sdk 1.
|
|
1
|
+
/*! @ikirmikir/game-sdk 1.1.0 | MIT | https://ikirmikir.com/developers/ */
|
|
2
|
+
|
|
3
|
+
// src/room.ts
|
|
4
|
+
var LIMITS = {
|
|
5
|
+
maxPlayers: 8,
|
|
6
|
+
maxMessageBytes: 4096,
|
|
7
|
+
// one game message, as JSON
|
|
8
|
+
messagesPerSecond: 20,
|
|
9
|
+
// per player
|
|
10
|
+
reconnectSeconds: 30,
|
|
11
|
+
// a dropped player keeps their seat this long
|
|
12
|
+
emptyMinutes: 10
|
|
13
|
+
// a room nobody is in closes after this long
|
|
14
|
+
};
|
|
15
|
+
var isRoomCode = (s) => typeof s === "string" && /^[A-HJ-NP-Z2-9]{5}$/.test(s);
|
|
16
|
+
var ROOM_ERRORS = ["not-found", "full", "too-big", "slow-down", "unavailable", "bad-request"];
|
|
17
|
+
function jsonSize(v) {
|
|
18
|
+
try {
|
|
19
|
+
const s = JSON.stringify(v);
|
|
20
|
+
return s === void 0 ? -1 : new TextEncoder().encode(s).length;
|
|
21
|
+
} catch {
|
|
22
|
+
return -1;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
2
25
|
|
|
3
26
|
// src/protocol.ts
|
|
4
27
|
var PROTOCOL = 1;
|
|
5
|
-
var FEATURES = ["loading", "gameplay", "pause", "mute", "context", "share"];
|
|
28
|
+
var FEATURES = ["loading", "gameplay", "pause", "mute", "context", "share", "multiplayer"];
|
|
29
|
+
var STATUSES = ["reconnecting", "connected", "closed"];
|
|
6
30
|
var obj = (d) => typeof d === "object" && d !== null && !Array.isArray(d);
|
|
7
31
|
var str = (v, max) => typeof v === "string" && v.length > 0 && v.length <= max;
|
|
8
32
|
var int = (v, min, max) => Number.isInteger(v) && v >= min && v <= max;
|
|
9
33
|
var bool = (v) => typeof v === "boolean";
|
|
34
|
+
var pid = (v) => typeof v === "string" && /^[a-z0-9]{1,16}$/.test(v);
|
|
35
|
+
var player = (p) => obj(p) && pid(p.id) && str(p.name, 32) && bool(p.connected);
|
|
36
|
+
var players = (v) => Array.isArray(v) && v.length <= LIMITS.maxPlayers && v.every(player);
|
|
37
|
+
var cleanPlayer = (p) => ({ id: p.id, name: p.name, connected: p.connected });
|
|
10
38
|
function readPageMessage(d) {
|
|
11
39
|
if (!obj(d) || d.ik !== PROTOCOL) return null;
|
|
12
40
|
switch (d.type) {
|
|
@@ -15,7 +43,8 @@ function readPageMessage(d) {
|
|
|
15
43
|
if (!Array.isArray(d.features) || !bool(d.muted) || !obj(c)) return null;
|
|
16
44
|
if (!str(c.language, 35) || c.device !== "mobile" && c.device !== "desktop" || !bool(c.touch)) return null;
|
|
17
45
|
const features = FEATURES.filter((f) => d.features.includes(f));
|
|
18
|
-
|
|
46
|
+
const invite = isRoomCode(d.invite) ? d.invite : null;
|
|
47
|
+
return { ik: 1, type: "welcome", features, context: { language: c.language, device: c.device, touch: c.touch }, muted: d.muted, invite };
|
|
19
48
|
}
|
|
20
49
|
case "pause":
|
|
21
50
|
return { ik: 1, type: "pause" };
|
|
@@ -25,6 +54,20 @@ function readPageMessage(d) {
|
|
|
25
54
|
return bool(d.muted) ? { ik: 1, type: "mute", muted: d.muted } : null;
|
|
26
55
|
case "shared":
|
|
27
56
|
return int(d.id, 0, 1e9) && bool(d.shared) ? { ik: 1, type: "shared", id: d.id, shared: d.shared } : null;
|
|
57
|
+
case "mp.room":
|
|
58
|
+
return int(d.id, 0, 1e9) && isRoomCode(d.code) && str(d.link, 300) && player(d.you) && players(d.players) && int(d.max, 2, LIMITS.maxPlayers) ? { ik: 1, type: "mp.room", id: d.id, code: d.code, link: d.link, you: cleanPlayer(d.you), players: d.players.map(cleanPlayer), max: d.max } : null;
|
|
59
|
+
case "mp.error":
|
|
60
|
+
return int(d.id, 0, 1e9) && ROOM_ERRORS.includes(d.error) ? { ik: 1, type: "mp.error", id: d.id, error: d.error } : null;
|
|
61
|
+
case "mp.player":
|
|
62
|
+
return player(d.player) ? { ik: 1, type: "mp.player", player: cleanPlayer(d.player) } : null;
|
|
63
|
+
case "mp.away":
|
|
64
|
+
case "mp.back":
|
|
65
|
+
case "mp.gone":
|
|
66
|
+
return pid(d.id) ? { ik: 1, type: d.type, id: d.id } : null;
|
|
67
|
+
case "mp.msg":
|
|
68
|
+
return pid(d.from) && "data" in d ? { ik: 1, type: "mp.msg", from: d.from, data: d.data } : null;
|
|
69
|
+
case "mp.status":
|
|
70
|
+
return STATUSES.includes(d.status) && players(d.players) ? { ik: 1, type: "mp.status", status: d.status, players: d.players.map(cleanPlayer) } : null;
|
|
28
71
|
default:
|
|
29
72
|
return null;
|
|
30
73
|
}
|
|
@@ -33,8 +76,143 @@ function isIkirmikirOrigin(origin) {
|
|
|
33
76
|
return origin === "https://ikirmikir.com" || origin === "http://localhost:3000" || origin === "http://127.0.0.1:3000" || /^https:\/\/([0-9a-f]{8}\.)?ikirmikir\.pages\.dev$/.test(origin);
|
|
34
77
|
}
|
|
35
78
|
|
|
79
|
+
// src/multiplayer.ts
|
|
80
|
+
function multiplayer(send, inside, invite, shareInvite) {
|
|
81
|
+
const pending = /* @__PURE__ */ new Map();
|
|
82
|
+
let nextId = 1, current = null;
|
|
83
|
+
function makeRoom(m) {
|
|
84
|
+
const listeners = { join: /* @__PURE__ */ new Set(), leave: /* @__PURE__ */ new Set(), away: /* @__PURE__ */ new Set(), back: /* @__PURE__ */ new Set(), message: /* @__PURE__ */ new Set(), status: /* @__PURE__ */ new Set() };
|
|
85
|
+
let players2 = m.players, open = true;
|
|
86
|
+
const emit = (e, ...args) => listeners[e].forEach((fn) => {
|
|
87
|
+
try {
|
|
88
|
+
fn(...args);
|
|
89
|
+
} catch (err) {
|
|
90
|
+
console.error(err);
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
const find = (id) => players2.find((p) => p.id === id);
|
|
94
|
+
const mark = (id, connected, e) => {
|
|
95
|
+
const p = find(id);
|
|
96
|
+
if (p) {
|
|
97
|
+
p.connected = connected;
|
|
98
|
+
emit(e, { ...p });
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
const room = {
|
|
102
|
+
code: m.code,
|
|
103
|
+
link: m.link,
|
|
104
|
+
you: m.you,
|
|
105
|
+
max: m.max,
|
|
106
|
+
get players() {
|
|
107
|
+
return players2.map((p) => ({ ...p }));
|
|
108
|
+
},
|
|
109
|
+
send(data) {
|
|
110
|
+
const n = jsonSize(data);
|
|
111
|
+
if (!open || n < 0 || n > LIMITS.maxMessageBytes) return false;
|
|
112
|
+
send({ type: "mp.send", data });
|
|
113
|
+
return true;
|
|
114
|
+
},
|
|
115
|
+
invite: () => open ? shareInvite() : Promise.resolve({ shared: false }),
|
|
116
|
+
leave() {
|
|
117
|
+
if (open) {
|
|
118
|
+
open = false;
|
|
119
|
+
current = null;
|
|
120
|
+
send({ type: "mp.leave" });
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
on(event, listener) {
|
|
124
|
+
const set = listeners[event];
|
|
125
|
+
if (!set || typeof listener !== "function") return () => {
|
|
126
|
+
};
|
|
127
|
+
set.add(listener);
|
|
128
|
+
return () => {
|
|
129
|
+
set.delete(listener);
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
function handle(e) {
|
|
134
|
+
if (!open) return;
|
|
135
|
+
switch (e.type) {
|
|
136
|
+
case "mp.player":
|
|
137
|
+
players2 = [...players2.filter((p) => p.id !== e.player.id), e.player];
|
|
138
|
+
return emit("join", { ...e.player });
|
|
139
|
+
case "mp.away":
|
|
140
|
+
return mark(e.id, false, "away");
|
|
141
|
+
case "mp.back":
|
|
142
|
+
return mark(e.id, true, "back");
|
|
143
|
+
case "mp.gone": {
|
|
144
|
+
const p = find(e.id);
|
|
145
|
+
players2 = players2.filter((x) => x.id !== e.id);
|
|
146
|
+
if (p) emit("leave", { ...p });
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
case "mp.msg": {
|
|
150
|
+
const p = find(e.from);
|
|
151
|
+
if (p) emit("message", e.data, { ...p });
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
case "mp.status":
|
|
155
|
+
if (e.status === "connected") players2 = e.players;
|
|
156
|
+
if (e.status === "closed") {
|
|
157
|
+
open = false;
|
|
158
|
+
current = null;
|
|
159
|
+
}
|
|
160
|
+
return emit("status", e.status);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return { room, handle };
|
|
164
|
+
}
|
|
165
|
+
function ask(m) {
|
|
166
|
+
if (!inside()) return Promise.resolve({ error: "not-available-here" });
|
|
167
|
+
current?.room.leave();
|
|
168
|
+
const id = nextId++;
|
|
169
|
+
return new Promise((resolve) => {
|
|
170
|
+
pending.set(id, resolve);
|
|
171
|
+
setTimeout(() => {
|
|
172
|
+
if (pending.delete(id)) resolve({ error: "unavailable" });
|
|
173
|
+
}, 2e4);
|
|
174
|
+
send({ ...m, id });
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
const api = {
|
|
178
|
+
get invite() {
|
|
179
|
+
return invite();
|
|
180
|
+
},
|
|
181
|
+
create(options) {
|
|
182
|
+
const max = Math.round(Number(options?.maxPlayers ?? LIMITS.maxPlayers));
|
|
183
|
+
if (!(max >= 2 && max <= LIMITS.maxPlayers)) return Promise.resolve({ error: "bad-request" });
|
|
184
|
+
return ask({ type: "mp.create", maxPlayers: max });
|
|
185
|
+
},
|
|
186
|
+
join(code) {
|
|
187
|
+
const c = String(code ?? "").trim().toUpperCase();
|
|
188
|
+
return isRoomCode(c) ? ask({ type: "mp.join", code: c }) : Promise.resolve({ error: "not-found" });
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
function receive(m) {
|
|
192
|
+
if (m.type === "mp.room") {
|
|
193
|
+
const done = pending.get(m.id);
|
|
194
|
+
if (!done) return true;
|
|
195
|
+
pending.delete(m.id);
|
|
196
|
+
current = makeRoom(m);
|
|
197
|
+
done({ room: current.room });
|
|
198
|
+
return true;
|
|
199
|
+
}
|
|
200
|
+
if (m.type === "mp.error") {
|
|
201
|
+
pending.get(m.id)?.({ error: m.error });
|
|
202
|
+
pending.delete(m.id);
|
|
203
|
+
return true;
|
|
204
|
+
}
|
|
205
|
+
if (m.type.startsWith("mp.")) {
|
|
206
|
+
current?.handle(m);
|
|
207
|
+
return true;
|
|
208
|
+
}
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
211
|
+
return { api, receive };
|
|
212
|
+
}
|
|
213
|
+
|
|
36
214
|
// src/index.ts
|
|
37
|
-
var VERSION = "1.
|
|
215
|
+
var VERSION = "1.1.0";
|
|
38
216
|
var HANDSHAKE_MS = 1500;
|
|
39
217
|
var instance;
|
|
40
218
|
function init(options) {
|
|
@@ -59,6 +237,27 @@ async function start({ gameId }) {
|
|
|
59
237
|
} catch {
|
|
60
238
|
}
|
|
61
239
|
};
|
|
240
|
+
let invite = null;
|
|
241
|
+
const mp = multiplayer(send, () => env === "ikirmikir" && features.includes("multiplayer"), () => invite, () => shareVia(true));
|
|
242
|
+
async function shareVia(room) {
|
|
243
|
+
if (env === "local") {
|
|
244
|
+
if (room || !canShare()) return { shared: false };
|
|
245
|
+
try {
|
|
246
|
+
await navigator.share({ url: location.href });
|
|
247
|
+
return { shared: true };
|
|
248
|
+
} catch {
|
|
249
|
+
return { shared: false };
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
const id = nextShare++;
|
|
253
|
+
return new Promise((resolve) => {
|
|
254
|
+
shares.set(id, (shared) => resolve({ shared }));
|
|
255
|
+
setTimeout(() => {
|
|
256
|
+
if (shares.delete(id)) resolve({ shared: false });
|
|
257
|
+
}, 6e4);
|
|
258
|
+
send({ type: "share", id, ...room ? { room: true } : {} });
|
|
259
|
+
});
|
|
260
|
+
}
|
|
62
261
|
if (typeof window !== "undefined" && window.parent !== window) {
|
|
63
262
|
const welcome = new Promise((resolve) => {
|
|
64
263
|
addEventListener("message", (e) => {
|
|
@@ -72,9 +271,10 @@ async function start({ gameId }) {
|
|
|
72
271
|
context = m.context;
|
|
73
272
|
muted = m.muted;
|
|
74
273
|
features = m.features;
|
|
274
|
+
invite = m.invite;
|
|
75
275
|
return resolve(true);
|
|
76
276
|
}
|
|
77
|
-
if (!page) return;
|
|
277
|
+
if (!page || mp.receive(m)) return;
|
|
78
278
|
if (m.type === "pause" || m.type === "resume") emit(m.type);
|
|
79
279
|
else if (m.type === "mute") {
|
|
80
280
|
if (m.muted !== muted) {
|
|
@@ -145,25 +345,8 @@ async function start({ gameId }) {
|
|
|
145
345
|
set.delete(listener);
|
|
146
346
|
};
|
|
147
347
|
},
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
if (!canShare()) return { shared: false };
|
|
151
|
-
try {
|
|
152
|
-
await navigator.share({ url: location.href });
|
|
153
|
-
return { shared: true };
|
|
154
|
-
} catch {
|
|
155
|
-
return { shared: false };
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
const id = nextShare++;
|
|
159
|
-
return new Promise((resolve) => {
|
|
160
|
-
shares.set(id, (shared) => resolve({ shared }));
|
|
161
|
-
setTimeout(() => {
|
|
162
|
-
if (shares.delete(id)) resolve({ shared: false });
|
|
163
|
-
}, 6e4);
|
|
164
|
-
send({ type: "share", id });
|
|
165
|
-
});
|
|
166
|
-
}
|
|
348
|
+
share: () => shareVia(false),
|
|
349
|
+
multiplayer: mp.api
|
|
167
350
|
};
|
|
168
351
|
}
|
|
169
352
|
function localContext() {
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { GameMessage, MultiplayerError, PageMessage, Player, RoomStatus } from "./protocol";
|
|
2
|
+
export type { MultiplayerError, Player, RoomStatus } from "./protocol";
|
|
3
|
+
type RoomEvents = {
|
|
4
|
+
join: (player: Player) => void;
|
|
5
|
+
leave: (player: Player) => void;
|
|
6
|
+
away: (player: Player) => void;
|
|
7
|
+
back: (player: Player) => void;
|
|
8
|
+
message: (data: unknown, from: Player) => void;
|
|
9
|
+
status: (status: RoomStatus) => void;
|
|
10
|
+
};
|
|
11
|
+
export interface Room {
|
|
12
|
+
/** The room code, for example "KX7Q2". Friends can type it to join. */
|
|
13
|
+
readonly code: string;
|
|
14
|
+
/** The invite link: opening it brings a friend into this room. */
|
|
15
|
+
readonly link: string;
|
|
16
|
+
/** You, as the other players see you (a fun name like "Rickshaw 42"). */
|
|
17
|
+
readonly you: Player;
|
|
18
|
+
/** Everyone in the room, you included, in the order they came in. */
|
|
19
|
+
readonly players: Player[];
|
|
20
|
+
/** Most players the room takes. */
|
|
21
|
+
readonly max: number;
|
|
22
|
+
/** Send something to every other player: anything JSON can hold, up to 4 KB. False when it can't be sent. */
|
|
23
|
+
send(data: unknown): boolean;
|
|
24
|
+
/** Open the share sheet with this room's invite link (Ikirmikir copies the link when there is no share sheet). */
|
|
25
|
+
invite(): Promise<{
|
|
26
|
+
shared: boolean;
|
|
27
|
+
}>;
|
|
28
|
+
/** Leave the room for good. */
|
|
29
|
+
leave(): void;
|
|
30
|
+
/** Listen for room events. Returns a function that stops listening. */
|
|
31
|
+
on<E extends keyof RoomEvents>(event: E, listener: RoomEvents[E]): () => void;
|
|
32
|
+
}
|
|
33
|
+
export type RoomResult = {
|
|
34
|
+
room: Room;
|
|
35
|
+
error?: undefined;
|
|
36
|
+
} | {
|
|
37
|
+
room?: undefined;
|
|
38
|
+
error: MultiplayerError;
|
|
39
|
+
};
|
|
40
|
+
export interface Multiplayer {
|
|
41
|
+
/** The room code when the player arrived through an invite link, else null. Join it with join(invite). */
|
|
42
|
+
readonly invite: string | null;
|
|
43
|
+
/** Make a new room and join it. maxPlayers: 2 to 8 (default 8). */
|
|
44
|
+
create(options?: {
|
|
45
|
+
maxPlayers?: number;
|
|
46
|
+
}): Promise<RoomResult>;
|
|
47
|
+
/** Join a room by its code (letters are not case sensitive). */
|
|
48
|
+
join(code: string): Promise<RoomResult>;
|
|
49
|
+
}
|
|
50
|
+
type Send = (m: GameMessage extends infer M ? M extends GameMessage ? Omit<M, "ik"> : never : never) => void;
|
|
51
|
+
export declare function multiplayer(send: Send, inside: () => boolean, invite: () => string | null, shareInvite: () => Promise<{
|
|
52
|
+
shared: boolean;
|
|
53
|
+
}>): {
|
|
54
|
+
api: Multiplayer;
|
|
55
|
+
receive: (m: PageMessage) => boolean;
|
|
56
|
+
};
|
package/dist/protocol.d.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
|
+
import { type Player, type RoomError } from "./room";
|
|
1
2
|
export declare const PROTOCOL = 1;
|
|
2
|
-
export type Feature = "loading" | "gameplay" | "pause" | "mute" | "context" | "share";
|
|
3
|
+
export type Feature = "loading" | "gameplay" | "pause" | "mute" | "context" | "share" | "multiplayer";
|
|
4
|
+
export type { Player } from "./room";
|
|
5
|
+
/** Why a room could not be made or joined. "not-available-here": the game is not inside Ikirmikir. */
|
|
6
|
+
export type MultiplayerError = RoomError | "not-available-here";
|
|
7
|
+
export type RoomStatus = "reconnecting" | "connected" | "closed";
|
|
3
8
|
export interface Context {
|
|
4
9
|
language: string;
|
|
5
10
|
device: "mobile" | "desktop";
|
|
@@ -25,6 +30,24 @@ export type GameMessage = {
|
|
|
25
30
|
ik: 1;
|
|
26
31
|
type: "share";
|
|
27
32
|
id: number;
|
|
33
|
+
room?: true;
|
|
34
|
+
} | {
|
|
35
|
+
ik: 1;
|
|
36
|
+
type: "mp.create";
|
|
37
|
+
id: number;
|
|
38
|
+
maxPlayers: number;
|
|
39
|
+
} | {
|
|
40
|
+
ik: 1;
|
|
41
|
+
type: "mp.join";
|
|
42
|
+
id: number;
|
|
43
|
+
code: string;
|
|
44
|
+
} | {
|
|
45
|
+
ik: 1;
|
|
46
|
+
type: "mp.send";
|
|
47
|
+
data: unknown;
|
|
48
|
+
} | {
|
|
49
|
+
ik: 1;
|
|
50
|
+
type: "mp.leave";
|
|
28
51
|
};
|
|
29
52
|
export type PageMessage = {
|
|
30
53
|
ik: 1;
|
|
@@ -32,6 +55,7 @@ export type PageMessage = {
|
|
|
32
55
|
features: Feature[];
|
|
33
56
|
context: Context;
|
|
34
57
|
muted: boolean;
|
|
58
|
+
invite: string | null;
|
|
35
59
|
} | {
|
|
36
60
|
ik: 1;
|
|
37
61
|
type: "pause";
|
|
@@ -47,6 +71,38 @@ export type PageMessage = {
|
|
|
47
71
|
type: "shared";
|
|
48
72
|
id: number;
|
|
49
73
|
shared: boolean;
|
|
74
|
+
} | {
|
|
75
|
+
ik: 1;
|
|
76
|
+
type: "mp.room";
|
|
77
|
+
id: number;
|
|
78
|
+
code: string;
|
|
79
|
+
link: string;
|
|
80
|
+
you: Player;
|
|
81
|
+
players: Player[];
|
|
82
|
+
max: number;
|
|
83
|
+
} | {
|
|
84
|
+
ik: 1;
|
|
85
|
+
type: "mp.error";
|
|
86
|
+
id: number;
|
|
87
|
+
error: RoomError;
|
|
88
|
+
} | {
|
|
89
|
+
ik: 1;
|
|
90
|
+
type: "mp.player";
|
|
91
|
+
player: Player;
|
|
92
|
+
} | {
|
|
93
|
+
ik: 1;
|
|
94
|
+
type: "mp.away" | "mp.back" | "mp.gone";
|
|
95
|
+
id: string;
|
|
96
|
+
} | {
|
|
97
|
+
ik: 1;
|
|
98
|
+
type: "mp.msg";
|
|
99
|
+
from: string;
|
|
100
|
+
data: unknown;
|
|
101
|
+
} | {
|
|
102
|
+
ik: 1;
|
|
103
|
+
type: "mp.status";
|
|
104
|
+
status: RoomStatus;
|
|
105
|
+
players: Player[];
|
|
50
106
|
};
|
|
51
107
|
/** For the Ikirmikir page: a valid message from a game, or null. */
|
|
52
108
|
export declare function readGameMessage(d: unknown): GameMessage | null;
|
package/dist/protocol.js
CHANGED
|
@@ -1,12 +1,44 @@
|
|
|
1
|
-
/*! @ikirmikir/game-sdk 1.
|
|
1
|
+
/*! @ikirmikir/game-sdk 1.1.0 | MIT | https://ikirmikir.com/developers/ */
|
|
2
|
+
|
|
3
|
+
// src/room.ts
|
|
4
|
+
var LIMITS = {
|
|
5
|
+
maxPlayers: 8,
|
|
6
|
+
maxMessageBytes: 4096,
|
|
7
|
+
// one game message, as JSON
|
|
8
|
+
messagesPerSecond: 20,
|
|
9
|
+
// per player
|
|
10
|
+
reconnectSeconds: 30,
|
|
11
|
+
// a dropped player keeps their seat this long
|
|
12
|
+
emptyMinutes: 10
|
|
13
|
+
// a room nobody is in closes after this long
|
|
14
|
+
};
|
|
15
|
+
var isRoomCode = (s) => typeof s === "string" && /^[A-HJ-NP-Z2-9]{5}$/.test(s);
|
|
16
|
+
var ROOM_ERRORS = ["not-found", "full", "too-big", "slow-down", "unavailable", "bad-request"];
|
|
17
|
+
function jsonSize(v) {
|
|
18
|
+
try {
|
|
19
|
+
const s = JSON.stringify(v);
|
|
20
|
+
return s === void 0 ? -1 : new TextEncoder().encode(s).length;
|
|
21
|
+
} catch {
|
|
22
|
+
return -1;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
2
25
|
|
|
3
26
|
// src/protocol.ts
|
|
4
27
|
var PROTOCOL = 1;
|
|
5
|
-
var FEATURES = ["loading", "gameplay", "pause", "mute", "context", "share"];
|
|
28
|
+
var FEATURES = ["loading", "gameplay", "pause", "mute", "context", "share", "multiplayer"];
|
|
29
|
+
var STATUSES = ["reconnecting", "connected", "closed"];
|
|
6
30
|
var obj = (d) => typeof d === "object" && d !== null && !Array.isArray(d);
|
|
7
31
|
var str = (v, max) => typeof v === "string" && v.length > 0 && v.length <= max;
|
|
8
32
|
var int = (v, min, max) => Number.isInteger(v) && v >= min && v <= max;
|
|
9
33
|
var bool = (v) => typeof v === "boolean";
|
|
34
|
+
var pid = (v) => typeof v === "string" && /^[a-z0-9]{1,16}$/.test(v);
|
|
35
|
+
var player = (p) => obj(p) && pid(p.id) && str(p.name, 32) && bool(p.connected);
|
|
36
|
+
var players = (v) => Array.isArray(v) && v.length <= LIMITS.maxPlayers && v.every(player);
|
|
37
|
+
var cleanPlayer = (p) => ({ id: p.id, name: p.name, connected: p.connected });
|
|
38
|
+
var sendable = (v) => {
|
|
39
|
+
const n = jsonSize(v);
|
|
40
|
+
return n >= 0 && n <= LIMITS.maxMessageBytes;
|
|
41
|
+
};
|
|
10
42
|
function readGameMessage(d) {
|
|
11
43
|
if (!obj(d) || d.ik !== PROTOCOL) return null;
|
|
12
44
|
switch (d.type) {
|
|
@@ -19,7 +51,15 @@ function readGameMessage(d) {
|
|
|
19
51
|
case "gameplay":
|
|
20
52
|
return bool(d.playing) ? { ik: 1, type: "gameplay", playing: d.playing } : null;
|
|
21
53
|
case "share":
|
|
22
|
-
return int(d.id, 0, 1e9) ? { ik: 1, type: "share", id: d.id } : null;
|
|
54
|
+
return int(d.id, 0, 1e9) && (d.room === void 0 || bool(d.room)) ? { ik: 1, type: "share", id: d.id, ...d.room ? { room: true } : {} } : null;
|
|
55
|
+
case "mp.create":
|
|
56
|
+
return int(d.id, 0, 1e9) && int(d.maxPlayers, 2, LIMITS.maxPlayers) ? { ik: 1, type: "mp.create", id: d.id, maxPlayers: d.maxPlayers } : null;
|
|
57
|
+
case "mp.join":
|
|
58
|
+
return int(d.id, 0, 1e9) && isRoomCode(d.code) ? { ik: 1, type: "mp.join", id: d.id, code: d.code } : null;
|
|
59
|
+
case "mp.send":
|
|
60
|
+
return "data" in d && sendable(d.data) ? { ik: 1, type: "mp.send", data: d.data } : null;
|
|
61
|
+
case "mp.leave":
|
|
62
|
+
return { ik: 1, type: "mp.leave" };
|
|
23
63
|
default:
|
|
24
64
|
return null;
|
|
25
65
|
}
|
|
@@ -32,7 +72,8 @@ function readPageMessage(d) {
|
|
|
32
72
|
if (!Array.isArray(d.features) || !bool(d.muted) || !obj(c)) return null;
|
|
33
73
|
if (!str(c.language, 35) || c.device !== "mobile" && c.device !== "desktop" || !bool(c.touch)) return null;
|
|
34
74
|
const features = FEATURES.filter((f) => d.features.includes(f));
|
|
35
|
-
|
|
75
|
+
const invite = isRoomCode(d.invite) ? d.invite : null;
|
|
76
|
+
return { ik: 1, type: "welcome", features, context: { language: c.language, device: c.device, touch: c.touch }, muted: d.muted, invite };
|
|
36
77
|
}
|
|
37
78
|
case "pause":
|
|
38
79
|
return { ik: 1, type: "pause" };
|
|
@@ -42,6 +83,20 @@ function readPageMessage(d) {
|
|
|
42
83
|
return bool(d.muted) ? { ik: 1, type: "mute", muted: d.muted } : null;
|
|
43
84
|
case "shared":
|
|
44
85
|
return int(d.id, 0, 1e9) && bool(d.shared) ? { ik: 1, type: "shared", id: d.id, shared: d.shared } : null;
|
|
86
|
+
case "mp.room":
|
|
87
|
+
return int(d.id, 0, 1e9) && isRoomCode(d.code) && str(d.link, 300) && player(d.you) && players(d.players) && int(d.max, 2, LIMITS.maxPlayers) ? { ik: 1, type: "mp.room", id: d.id, code: d.code, link: d.link, you: cleanPlayer(d.you), players: d.players.map(cleanPlayer), max: d.max } : null;
|
|
88
|
+
case "mp.error":
|
|
89
|
+
return int(d.id, 0, 1e9) && ROOM_ERRORS.includes(d.error) ? { ik: 1, type: "mp.error", id: d.id, error: d.error } : null;
|
|
90
|
+
case "mp.player":
|
|
91
|
+
return player(d.player) ? { ik: 1, type: "mp.player", player: cleanPlayer(d.player) } : null;
|
|
92
|
+
case "mp.away":
|
|
93
|
+
case "mp.back":
|
|
94
|
+
case "mp.gone":
|
|
95
|
+
return pid(d.id) ? { ik: 1, type: d.type, id: d.id } : null;
|
|
96
|
+
case "mp.msg":
|
|
97
|
+
return pid(d.from) && "data" in d ? { ik: 1, type: "mp.msg", from: d.from, data: d.data } : null;
|
|
98
|
+
case "mp.status":
|
|
99
|
+
return STATUSES.includes(d.status) && players(d.players) ? { ik: 1, type: "mp.status", status: d.status, players: d.players.map(cleanPlayer) } : null;
|
|
45
100
|
default:
|
|
46
101
|
return null;
|
|
47
102
|
}
|
package/dist/room.d.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
export declare const LIMITS: {
|
|
2
|
+
maxPlayers: number;
|
|
3
|
+
maxMessageBytes: number;
|
|
4
|
+
messagesPerSecond: number;
|
|
5
|
+
reconnectSeconds: number;
|
|
6
|
+
emptyMinutes: number;
|
|
7
|
+
};
|
|
8
|
+
/** Room codes: 5 characters, no I, O, 0 or 1, so they are easy to read out and type. */
|
|
9
|
+
export declare const CODE_CHARS = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
|
|
10
|
+
export declare const isRoomCode: (s: unknown) => s is string;
|
|
11
|
+
export type RoomError = "not-found" | "full" | "too-big" | "slow-down" | "unavailable" | "bad-request";
|
|
12
|
+
export declare const ROOM_ERRORS: RoomError[];
|
|
13
|
+
export interface Player {
|
|
14
|
+
id: string;
|
|
15
|
+
name: string;
|
|
16
|
+
connected: boolean;
|
|
17
|
+
}
|
|
18
|
+
export type ServerMessage = {
|
|
19
|
+
t: "welcome";
|
|
20
|
+
code: string;
|
|
21
|
+
you: Player;
|
|
22
|
+
token: string;
|
|
23
|
+
players: Player[];
|
|
24
|
+
max: number;
|
|
25
|
+
} | {
|
|
26
|
+
t: "join";
|
|
27
|
+
player: Player;
|
|
28
|
+
} | {
|
|
29
|
+
t: "away";
|
|
30
|
+
id: string;
|
|
31
|
+
} | {
|
|
32
|
+
t: "back";
|
|
33
|
+
id: string;
|
|
34
|
+
} | {
|
|
35
|
+
t: "leave";
|
|
36
|
+
id: string;
|
|
37
|
+
} | {
|
|
38
|
+
t: "msg";
|
|
39
|
+
from: string;
|
|
40
|
+
data: unknown;
|
|
41
|
+
} | {
|
|
42
|
+
t: "error";
|
|
43
|
+
error: RoomError;
|
|
44
|
+
};
|
|
45
|
+
export type ClientMessage = {
|
|
46
|
+
t: "send";
|
|
47
|
+
data: unknown;
|
|
48
|
+
} | {
|
|
49
|
+
t: "leave";
|
|
50
|
+
};
|
|
51
|
+
/** Size of a value as JSON, or -1 when it can't be sent (functions, cycles, undefined). */
|
|
52
|
+
export declare function jsonSize(v: unknown): number;
|
|
53
|
+
/** For the room server: a valid message from a page, or null. */
|
|
54
|
+
export declare function readClientMessage(text: string): ClientMessage | null;
|
|
55
|
+
/** For the page: a valid message from the room server, or null. */
|
|
56
|
+
export declare function readServerMessage(text: string): ServerMessage | null;
|
package/dist/room.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/*! @ikirmikir/game-sdk 1.1.0 | MIT | https://ikirmikir.com/developers/ */
|
|
2
|
+
|
|
3
|
+
// src/room.ts
|
|
4
|
+
var LIMITS = {
|
|
5
|
+
maxPlayers: 8,
|
|
6
|
+
maxMessageBytes: 4096,
|
|
7
|
+
// one game message, as JSON
|
|
8
|
+
messagesPerSecond: 20,
|
|
9
|
+
// per player
|
|
10
|
+
reconnectSeconds: 30,
|
|
11
|
+
// a dropped player keeps their seat this long
|
|
12
|
+
emptyMinutes: 10
|
|
13
|
+
// a room nobody is in closes after this long
|
|
14
|
+
};
|
|
15
|
+
var CODE_CHARS = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
|
|
16
|
+
var isRoomCode = (s) => typeof s === "string" && /^[A-HJ-NP-Z2-9]{5}$/.test(s);
|
|
17
|
+
var ROOM_ERRORS = ["not-found", "full", "too-big", "slow-down", "unavailable", "bad-request"];
|
|
18
|
+
var obj = (d) => typeof d === "object" && d !== null && !Array.isArray(d);
|
|
19
|
+
var id = (v) => typeof v === "string" && /^[a-z0-9]{1,16}$/.test(v);
|
|
20
|
+
var player = (p) => obj(p) && id(p.id) && typeof p.name === "string" && p.name.length <= 32 && typeof p.connected === "boolean";
|
|
21
|
+
var clean = (p) => ({ id: p.id, name: p.name, connected: p.connected });
|
|
22
|
+
function jsonSize(v) {
|
|
23
|
+
try {
|
|
24
|
+
const s = JSON.stringify(v);
|
|
25
|
+
return s === void 0 ? -1 : new TextEncoder().encode(s).length;
|
|
26
|
+
} catch {
|
|
27
|
+
return -1;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function readClientMessage(text) {
|
|
31
|
+
let d;
|
|
32
|
+
try {
|
|
33
|
+
d = JSON.parse(text);
|
|
34
|
+
} catch {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
if (!obj(d)) return null;
|
|
38
|
+
if (d.t === "leave") return { t: "leave" };
|
|
39
|
+
if (d.t === "send" && "data" in d) return { t: "send", data: d.data };
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
function readServerMessage(text) {
|
|
43
|
+
let d;
|
|
44
|
+
try {
|
|
45
|
+
d = JSON.parse(text);
|
|
46
|
+
} catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
if (!obj(d)) return null;
|
|
50
|
+
switch (d.t) {
|
|
51
|
+
case "welcome":
|
|
52
|
+
return isRoomCode(d.code) && player(d.you) && typeof d.token === "string" && Array.isArray(d.players) && d.players.every(player) && Number.isInteger(d.max) ? { t: "welcome", code: d.code, you: clean(d.you), token: d.token, players: d.players.map(clean), max: d.max } : null;
|
|
53
|
+
case "join":
|
|
54
|
+
return player(d.player) ? { t: "join", player: clean(d.player) } : null;
|
|
55
|
+
case "away":
|
|
56
|
+
case "back":
|
|
57
|
+
case "leave":
|
|
58
|
+
return id(d.id) ? { t: d.t, id: d.id } : null;
|
|
59
|
+
case "msg":
|
|
60
|
+
return id(d.from) && "data" in d ? { t: "msg", from: d.from, data: d.data } : null;
|
|
61
|
+
case "error":
|
|
62
|
+
return ROOM_ERRORS.includes(d.error) ? { t: "error", error: d.error } : null;
|
|
63
|
+
default:
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
export {
|
|
68
|
+
CODE_CHARS,
|
|
69
|
+
LIMITS,
|
|
70
|
+
ROOM_ERRORS,
|
|
71
|
+
isRoomCode,
|
|
72
|
+
jsonSize,
|
|
73
|
+
readClientMessage,
|
|
74
|
+
readServerMessage
|
|
75
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ikirmikir/game-sdk",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Optional SDK for games on Ikirmikir: loading, gameplay, pause, sound, player context and share. Works standalone too.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://ikirmikir.com/developers/",
|
|
@@ -20,6 +20,10 @@
|
|
|
20
20
|
"./protocol": {
|
|
21
21
|
"types": "./dist/protocol.d.ts",
|
|
22
22
|
"default": "./dist/protocol.js"
|
|
23
|
+
},
|
|
24
|
+
"./room": {
|
|
25
|
+
"types": "./dist/room.d.ts",
|
|
26
|
+
"default": "./dist/room.js"
|
|
23
27
|
}
|
|
24
28
|
},
|
|
25
29
|
"publishConfig": {
|