multiplayer rtc

This commit is contained in:
Andrey Kolosov
2019-08-13 18:15:20 +03:00
parent 79851342bb
commit 160a81032d
11 changed files with 782 additions and 64 deletions

209
package-lock.json generated
View File

@@ -1,6 +1,6 @@
{
"name": "diabloweb",
"version": "1.0.25",
"version": "1.0.27",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -1386,6 +1386,11 @@
"@types/istanbul-lib-report": "*"
}
},
"@types/node": {
"version": "10.14.15",
"resolved": "https://registry.npmjs.org/@types/node/-/node-10.14.15.tgz",
"integrity": "sha512-CBR5avlLcu0YCILJiDIXeU2pTw7UK/NIxfC63m7d7CVamho1qDEzXKkOtEauQRPMy6MI8mLozth+JJkas7HY6g=="
},
"@types/q": {
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/@types/q/-/q-1.5.2.tgz",
@@ -1396,6 +1401,11 @@
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-1.0.1.tgz",
"integrity": "sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw=="
},
"@types/webrtc": {
"version": "0.0.25",
"resolved": "https://registry.npmjs.org/@types/webrtc/-/webrtc-0.0.25.tgz",
"integrity": "sha512-ep/e+p2uUKV1h96GBgRhwomrBch/bPDHPOKbCHODLGRUDuuKe2s7sErlFVKw+5BYUzvpxSmUNqoadaZ44MePoQ=="
},
"@types/yargs": {
"version": "12.0.12",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-12.0.12.tgz",
@@ -2183,6 +2193,28 @@
"resolved": "https://registry.npmjs.org/babel-plugin-transform-react-remove-prop-types/-/babel-plugin-transform-react-remove-prop-types-0.4.24.tgz",
"integrity": "sha512-eqj0hVcJUR57/Ug2zE1Yswsw4LhuqqHhD+8v120T1cl3kjg76QwtyBrdIk4WVwK+lAhBJVYCd/v+4nc4y+8JsA=="
},
"babel-polyfill": {
"version": "6.23.0",
"resolved": "https://registry.npmjs.org/babel-polyfill/-/babel-polyfill-6.23.0.tgz",
"integrity": "sha1-g2TKYt+Or7gwSZ9pkXdGbDsDSZ0=",
"requires": {
"babel-runtime": "^6.22.0",
"core-js": "^2.4.0",
"regenerator-runtime": "^0.10.0"
},
"dependencies": {
"core-js": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.9.tgz",
"integrity": "sha512-HOpZf6eXmnl7la+cUdMnLvUxKNqLUzJvgIziQ0DiF3JwSImNphIqdGqzj6hIKyX04MmV0poclQ7+wjWvxQyR2A=="
},
"regenerator-runtime": {
"version": "0.10.5",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.10.5.tgz",
"integrity": "sha1-M2w+/BIgrc7dosn6tntaeVWjNlg="
}
}
},
"babel-preset-jest": {
"version": "24.6.0",
"resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-24.6.0.tgz",
@@ -3978,6 +4010,14 @@
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
"integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k="
},
"encoding": {
"version": "0.1.12",
"resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.12.tgz",
"integrity": "sha1-U4tm8+5izRq1HsMjgp0flIDHS+s=",
"requires": {
"iconv-lite": "~0.4.13"
}
},
"end-of-stream": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz",
@@ -7333,6 +7373,11 @@
"integrity": "sha512-M7kLczedRMYX4L8Mdh4MzyAMM9O5osx+4FcOQuTvr3A9F2D9S5JXheN0ewNbrvK2UatkTRhL5ejGmGSjNMiZuw==",
"dev": true
},
"js-binarypack": {
"version": "0.0.9",
"resolved": "https://registry.npmjs.org/js-binarypack/-/js-binarypack-0.0.9.tgz",
"integrity": "sha1-RUJD094hKWHMFRSi8Rnewvr2QDU="
},
"js-levenshtein": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/js-levenshtein/-/js-levenshtein-1.1.6.tgz",
@@ -8226,6 +8271,15 @@
"lower-case": "^1.1.1"
}
},
"node-fetch": {
"version": "1.6.3",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-1.6.3.tgz",
"integrity": "sha1-3CNO3WSJmC1Y6PDbT2lQKavNjAQ=",
"requires": {
"encoding": "^0.1.11",
"is-stream": "^1.0.1"
}
},
"node-forge": {
"version": "0.7.5",
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.7.5.tgz",
@@ -8653,6 +8707,110 @@
"mimic-fn": "^1.0.0"
}
},
"opencollective": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/opencollective/-/opencollective-1.0.3.tgz",
"integrity": "sha1-ruY3K8KBRFg2kMPKja7PwSDdDvE=",
"requires": {
"babel-polyfill": "6.23.0",
"chalk": "1.1.3",
"inquirer": "3.0.6",
"minimist": "1.2.0",
"node-fetch": "1.6.3",
"opn": "4.0.2"
},
"dependencies": {
"ansi-escapes": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-1.4.0.tgz",
"integrity": "sha1-06ioOzGapneTZisT52HHkRQiMG4="
},
"ansi-regex": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
"integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8="
},
"ansi-styles": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz",
"integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4="
},
"chalk": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
"integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
"requires": {
"ansi-styles": "^2.2.1",
"escape-string-regexp": "^1.0.2",
"has-ansi": "^2.0.0",
"strip-ansi": "^3.0.0",
"supports-color": "^2.0.0"
}
},
"chardet": {
"version": "0.4.2",
"resolved": "https://registry.npmjs.org/chardet/-/chardet-0.4.2.tgz",
"integrity": "sha1-tUc7M9yXxCTl2Y3IfVXU2KKci/I="
},
"external-editor": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/external-editor/-/external-editor-2.2.0.tgz",
"integrity": "sha512-bSn6gvGxKt+b7+6TKEv1ZycHleA7aHhRHyAqJyp5pbUFuYYNIzpZnQDk7AsYckyWdEnTeAnay0aCy2aV6iTk9A==",
"requires": {
"chardet": "^0.4.0",
"iconv-lite": "^0.4.17",
"tmp": "^0.0.33"
}
},
"inquirer": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/inquirer/-/inquirer-3.0.6.tgz",
"integrity": "sha1-4EqqnQW3o8ubD0B9BDdfBEcZA0c=",
"requires": {
"ansi-escapes": "^1.1.0",
"chalk": "^1.0.0",
"cli-cursor": "^2.1.0",
"cli-width": "^2.0.0",
"external-editor": "^2.0.1",
"figures": "^2.0.0",
"lodash": "^4.3.0",
"mute-stream": "0.0.7",
"run-async": "^2.2.0",
"rx": "^4.1.0",
"string-width": "^2.0.0",
"strip-ansi": "^3.0.0",
"through": "^2.3.6"
}
},
"opn": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/opn/-/opn-4.0.2.tgz",
"integrity": "sha1-erwi5kTf9jsKltWrfyeQwPAavJU=",
"requires": {
"object-assign": "^4.0.1",
"pinkie-promise": "^2.0.0"
}
},
"strip-ansi": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
"integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
"requires": {
"ansi-regex": "^2.0.0"
}
},
"supports-color": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
"integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc="
}
}
},
"opencollective-postinstall": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/opencollective-postinstall/-/opencollective-postinstall-2.0.2.tgz",
"integrity": "sha512-pVOEP16TrAO2/fjej1IdOyupJY8KDUM1CvsaScRbw6oddvpQoOfGk4ywha0HKKVAD6RkW4x6Q+tNBwhf3Bgpuw=="
},
"opn": {
"version": "5.4.0",
"resolved": "https://registry.npmjs.org/opn/-/opn-5.4.0.tgz",
@@ -8937,6 +9095,21 @@
"sha.js": "^2.4.8"
}
},
"peerjs": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/peerjs/-/peerjs-1.0.2.tgz",
"integrity": "sha512-lVXh+R7KnrVt3WBhP+QNO8dhdezmhEF+wwJdVfxlayRG7jgi7D3vx+9UoW2wv5OlVzuAVL+NGJeYGQR9VrMhbQ==",
"requires": {
"@types/node": "^10.14.12",
"@types/webrtc": "^0.0.25",
"eventemitter3": "^3.1.2",
"js-binarypack": "0.0.9",
"opencollective": "^1.0.3",
"opencollective-postinstall": "^2.0.0",
"reliable": "git+https://github.com/michelle/reliable.git",
"webrtc-adapter": "^7.2.6"
}
},
"performance-now": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
@@ -10439,6 +10612,13 @@
"resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz",
"integrity": "sha1-VNvzd+UUQKypCkzSdGANP/LYiKk="
},
"reliable": {
"version": "git+https://github.com/michelle/reliable.git#70604f577ae55a2eb015c17d73cc8d9dce5f9ec4",
"from": "git+https://github.com/michelle/reliable.git",
"requires": {
"js-binarypack": "0.0.9"
}
},
"remove-trailing-separator": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz",
@@ -10663,6 +10843,14 @@
"resolved": "https://registry.npmjs.org/rsvp/-/rsvp-4.8.5.tgz",
"integrity": "sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA=="
},
"rtcpeerconnection-shim": {
"version": "1.2.15",
"resolved": "https://registry.npmjs.org/rtcpeerconnection-shim/-/rtcpeerconnection-shim-1.2.15.tgz",
"integrity": "sha512-C6DxhXt7bssQ1nHb154lqeL0SXz5Dx4RczXZu2Aa/L1NJFnEVDxFwCBo3fqtuljhHIGceg5JKBV4XJ0gW5JKyw==",
"requires": {
"sdp": "^2.6.0"
}
},
"run-async": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz",
@@ -10679,6 +10867,11 @@
"aproba": "^1.1.1"
}
},
"rx": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/rx/-/rx-4.1.0.tgz",
"integrity": "sha1-pfE/957zt0D+MKqAP7CfmIBdR4I="
},
"rxjs": {
"version": "6.5.2",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.2.tgz",
@@ -11061,6 +11254,11 @@
}
}
},
"sdp": {
"version": "2.10.0",
"resolved": "https://registry.npmjs.org/sdp/-/sdp-2.10.0.tgz",
"integrity": "sha512-H+VjfyQpRz9GezhshJmkXTtCAT9/2g9az3GFDPYfGOz0eAOQU1fCrL3S9Dq/eUT9FtOyLi/czdR9PzK3fKUYOQ=="
},
"select-hose": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz",
@@ -12697,6 +12895,15 @@
}
}
},
"webrtc-adapter": {
"version": "7.2.9",
"resolved": "https://registry.npmjs.org/webrtc-adapter/-/webrtc-adapter-7.2.9.tgz",
"integrity": "sha512-98rcdbSqUBR+L+erotCFWgiPyYjCEq6NJYN/1Bl6cRl2CxSU3wanJlc4YdfWzaTGaK13ZVmHay2mlW4aOWXh0A==",
"requires": {
"rtcpeerconnection-shim": "^1.2.15",
"sdp": "^2.9.0"
}
},
"websocket-driver": {
"version": "0.7.3",
"resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.3.tgz",

View File

@@ -1,6 +1,6 @@
{
"name": "diabloweb",
"version": "1.0.25",
"version": "1.0.27",
"private": true,
"dependencies": {
"@babel/core": "7.4.3",
@@ -39,6 +39,7 @@
"jest-watch-typeahead": "0.3.0",
"mini-css-extract-plugin": "0.5.0",
"optimize-css-assets-webpack-plugin": "5.0.1",
"peerjs": "^1.0.2",
"pnp-webpack-plugin": "1.2.1",
"postcss-flexbugs-fixes": "4.1.0",
"postcss-loader": "3.0.0",

View File

@@ -7,6 +7,10 @@ import create_fs from './fs';
import load_game from './api/loader';
import { SpawnSizes } from './api/load_spawn';
import Peer from 'peerjs';
window.Peer = Peer;
if (process.env.NODE_ENV === 'production') {
ReactGA.initialize('UA-43123589-6');
ReactGA.pageview('/');

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

@@ -3,7 +3,6 @@ import DiabloModule from './Diablo.jscc';
import SpawnBinary from './DiabloSpawn.wasm';
import SpawnModule from './DiabloSpawn.jscc';
import axios from 'axios';
import websocket_open from './websocket';
const DiabloSize = 1316452;
const SpawnSize = 1196648;
@@ -17,7 +16,6 @@ let files = null;
let renderBatch = null;
let drawBelt = null;
let is_spawn = false;
let websocket = null;
const ChunkSize = 1 << 20;
class RemoteFile {
@@ -118,13 +116,8 @@ const DApi = {
worker.postMessage({action: "keyboard", rect: null});
},
websocket_send(data) {
if (websocket) {
websocket.send(data);
}
},
websocket_closed() {
return !websocket || websocket.readyState !== WebSocket.OPEN;
return false;
},
};
@@ -239,21 +232,22 @@ let maxSoundId = 0, maxBatchId = 0;
}
});
let packetBatch = null;
DApi.websocket_send = function(data) {
if (packetBatch) {
packetBatch.push(data.slice().buffer);
} else {
worker.postMessage({action: "packet", buffer: data});
}
};
worker.DApi = DApi;
let wasm = null;
function call_api(func, ...params) {
function try_api(func) {
try {
audioBatch = [];
audioTransfer = [];
wasm["_" + func](...params);
if (audioBatch.length) {
maxSoundId = maxBatchId;
worker.postMessage({action: "audioBatch", batch: audioBatch}, audioTransfer);
audioBatch = null;
audioTransfer = null;
}
func();
} catch (e) {
if (typeof e === "string") {
worker.postMessage({action: ""})
@@ -262,6 +256,25 @@ function call_api(func, ...params) {
}
}
function call_api(func, ...params) {
try_api(() => {
audioBatch = [];
audioTransfer = [];
packetBatch = [];
wasm["_" + func](...params);
if (audioBatch.length) {
maxSoundId = maxBatchId;
worker.postMessage({action: "audioBatch", batch: audioBatch}, audioTransfer);
}
if (packetBatch.length) {
worker.postMessage({action: "packetBatch", batch: packetBatch}, packetBatch);
}
audioBatch = null;
audioTransfer = null;
packetBatch = null;
});
}
function progress(text, loaded, total) {
worker.postMessage({action: "progress", text, loaded, total});
}
@@ -293,7 +306,7 @@ async function initWasm(spawn, progress) {
return result;
}
async function init_game(mpq, spawn, offscreen, serverUrl) {
async function init_game(mpq, spawn, offscreen) {
is_spawn = spawn;
if (offscreen) {
canvas = new OffscreenCanvas(640, 480);
@@ -312,16 +325,6 @@ async function init_game(mpq, spawn, offscreen, serverUrl) {
}
}
if (serverUrl) {
progress("Connecting...");
websocket = await websocket_open(serverUrl, data => {
if (wasm) {
const ptr = wasm._DApi_AllocPacket(data.byteLength);
wasm.HEAPU8.set(new Uint8Array(data), ptr);
}
});
}
progress("Loading...");
let mpqLoaded = 0, mpqTotal = (mpq ? mpq.size : 0), wasmLoaded = 0, wasmTotal = (spawn ? SpawnSize : DiabloSize);
const wasmWeight = 5;
@@ -346,9 +349,7 @@ async function init_game(mpq, spawn, offscreen, serverUrl) {
const vers = process.env.VERSION.match(/(\d+)\.(\d+)\.(\d+)/);
if (websocket) {
wasm._SNet_InitWebsocket();
}
wasm._SNet_InitWebsocket();
wasm._DApi_Init(Math.floor(performance.now()), offscreen ? 1 : 0, parseInt(vers[1]), parseInt(vers[2]), parseInt(vers[3]));
setInterval(() => {
@@ -360,13 +361,27 @@ worker.addEventListener("message", ({data}) => {
switch (data.action) {
case "init":
files = data.files;
init_game(data.mpq, data.spawn, data.offscreen, data.websocket).then(
init_game(data.mpq, data.spawn, data.offscreen).then(
() => worker.postMessage({action: "loaded"}),
e => worker.postMessage({action: "failed", error: e.toString(), stack: e.stack}));
break;
case "event":
call_api(data.func, ...data.params);
break;
case "packet":
try_api(() => {
const ptr = wasm._DApi_AllocPacket(data.buffer.byteLength);
wasm.HEAPU8.set(new Uint8Array(data.buffer), ptr);
});
break;
case "packetBatch":
try_api(() => {
for (let packet of data.batch) {
const ptr = wasm._DApi_AllocPacket(packet.byteLength);
wasm.HEAPU8.set(new Uint8Array(packet), ptr);
}
});
break;
default:
}
});

View File

@@ -1,6 +1,7 @@
import Worker from './game.worker.js';
import init_sound from './sound';
import load_spawn from './load_spawn';
import webrtc_open from './webrtc';
function onRender(api, ctx, {bitmap, images, text, clip, belt}) {
if (bitmap) {
@@ -65,6 +66,12 @@ async function do_load_game(api, audio, mpq, spawn) {
return await new Promise((resolve, reject) => {
try {
const worker = new Worker();
let packetQueue = [];
const webrtc = webrtc_open(data => {
packetQueue.push(data);
});
worker.addEventListener("message", ({data}) => {
switch (data.action) {
case "loaded":
@@ -106,14 +113,28 @@ async function do_load_game(api, audio, mpq, spawn) {
case "current_save":
api.setCurrentSave(data.name);
break;
case "packet":
webrtc.send(data.buffer);
break;
case "packetBatch":
for (let packet of data.batch) {
webrtc.send(packet);
}
break;
default:
}
});
});
const transfer= [];
for (let [, file] of fs.files) {
transfer.push(file.buffer);
}
worker.postMessage({action: "init", files: fs.files, mpq, spawn, offscreen, websocket: window.gameServer}, transfer);
worker.postMessage({action: "init", files: fs.files, mpq, spawn, offscreen}, transfer);
setInterval(() => {
if (packetQueue.length) {
worker.postMessage({action: "packetBatch", batch: packetQueue}, packetQueue);
packetQueue.length = 0;
}
}, 20);
delete fs.files;
} catch (e) {
reject(e);

View File

@@ -17,6 +17,7 @@ function decodeAudioData(context, buffer) {
export default function init_sound() {
const AudioContext = window.AudioContext || window.webkitAudioContext;
const StereoPannerNode = window.StereoPannerNode || window.webkitAudioPannerNode;
if (!AudioContext) {
return no_sound();
}
@@ -40,7 +41,7 @@ export default function init_sound() {
sounds.set(id, {
buffer: Promise.resolve(buffer),
gain: context.createGain(),
panner: new StereoPannerNode(context, {pan: 0}),
panner: StereoPannerNode && new StereoPannerNode(context, {pan: 0}),
});
},
create_sound(id, data) {
@@ -51,7 +52,7 @@ export default function init_sound() {
sounds.set(id, {
buffer,
gain: context.createGain(),
panner: new StereoPannerNode(context, {pan: 0}),
panner: StereoPannerNode && new StereoPannerNode(context, {pan: 0}),
});
},
duplicate_sound(id, srcId) {
@@ -65,7 +66,7 @@ export default function init_sound() {
sounds.set(id, {
buffer: src.buffer,
gain: context.createGain(),
panner: new StereoPannerNode(context, {pan: 0}),
panner: StereoPannerNode && new StereoPannerNode(context, {pan: 0}),
});
},
play_sound(id, volume, pan, loop) {
@@ -76,12 +77,18 @@ export default function init_sound() {
}
src.gain.gain.value = Math.pow(2.0, volume / 1000.0);
const relVolume = Math.pow(2.0, pan / 1000.0);
src.panner.pan.value = 1.0 - 2.0 / (1.0 + relVolume);
if (src.panner) {
src.panner.pan.value = 1.0 - 2.0 / (1.0 + relVolume);
}
src.source = src.buffer.then(buffer => {
const source = context.createBufferSource();
source.buffer = buffer;
source.loop = !!loop;
source.connect(src.gain).connect(src.panner).connect(context.destination);
let node = source.connect(src.gain);
if (src.panner) {
node = node.connect(src.panner);
}
node.connect(context.destination);
source.start();
return source;
});

469
src/api/webrtc.js Normal file
View File

@@ -0,0 +1,469 @@
import Peer from 'peerjs';
class buffer_reader {
constructor(buffer) {
this.buffer = (buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer));
this.pos = 0;
}
done() {
return this.pos === this.buffer.byteLength;
}
read8() {
if (this.pos >= this.buffer.byteLength) {
throw Error('packet too small');
}
return this.buffer[this.pos++];
}
read16() {
const {pos, buffer} = this;
if (pos + 2 > buffer.byteLength) {
throw Error('packet too small');
}
const result = buffer[pos] | (buffer[pos + 1] << 8);
this.pos += 2;
return result;
}
read32() {
const {pos, buffer} = this;
if (pos + 4 > buffer.byteLength) {
throw Error('packet too small');
}
const result = buffer[pos] | (buffer[pos + 1] << 8) | (buffer[pos + 2] << 16) | (buffer[pos + 3] << 24);
this.pos += 4;
return result;
}
read_str() {
const length = this.read8();
const {pos, buffer} = this;
if (pos + length > buffer.byteLength) {
throw Error('packet too small');
}
const result = String.fromCharCode(...buffer.subarray(pos, pos + length));
this.pos += length;
return result;
}
rest() {
const result = this.buffer.subarray(this.pos);
this.pos = this.buffer.length;
return result;
}
}
class buffer_writer {
constructor(length) {
this.buffer = new Uint8Array(length);
this.pos = 0;
}
get result() {
return this.buffer.buffer;
}
write8(value) {
this.buffer[this.pos++] = value;
return this;
}
write16(value) {
const {pos, buffer} = this;
buffer[pos] = value;
buffer[pos + 1] = value >> 8;
this.pos += 2;
return this;
}
write32(value) {
const {pos, buffer} = this;
buffer[pos] = value;
buffer[pos + 1] = value >> 8;
buffer[pos + 2] = value >> 16;
buffer[pos + 3] = value >> 24;
this.pos += 4;
return this;
}
write_str(value) {
const length = value.length;
this.write8(length);
const {pos, buffer} = this;
for (let i = 0; i < length; ++i) {
buffer[pos + i] = value.charCodeAt(i);
}
this.pos += length;
return this;
}
rest(value) {
this.buffer.set(value, this.pos);
return this;
}
}
const RejectionReason = {
JOIN_SUCCESS: 0x00,
JOIN_ALREADY_IN_GAME: 0x01,
JOIN_GAME_NOT_FOUND: 0x02,
JOIN_INCORRECT_PASSWORD: 0x03,
JOIN_VERSION_MISMATCH: 0x04,
JOIN_GAME_FULL: 0x05,
CREATE_GAME_EXISTS: 0x06,
};
const server_info_packet = {
code: 0x32,
read: reader => ({version: reader.read32()}),
write: ({version}) => new buffer_writer(5).write8(server_info_packet.code).write32(version).result,
};
const server_game_list_packet = {
code: 0x21,
read: reader => {
const count = reader.read8();
const games = [];
for (let i = 0; i < count; ++i) {
games.push({type: reader.read32(), name: reader.read_str()});
}
return {games};
},
write: ({games}) => {
const writer = new buffer_writer(games.reduce((sum, {name}) => sum + 5 + name.length, 2));
writer.write8(server_game_list_packet.code);
writer.write8(games.length);
for (let {code, name} of games) {
writer.write32(code);
writer.write_str(name);
}
return writer.result;
},
};
const server_join_accept_packet = {
code: 0x12,
read: reader => ({cookie: reader.read32(), index: reader.read8(), seed: reader.read32(), difficulty: reader.read32()}),
write: ({cookie, index, seed, difficulty}) => new buffer_writer(14).write8(server_join_accept_packet.code).write32(cookie).write8(index).write32(seed).write32(difficulty).result,
};
const server_join_reject_packet = {
code: 0x15,
read: reader => ({cookie: reader.read32(), reason: reader.read8()}),
write: ({cookie, reason}) => new buffer_writer(6).write8(server_join_reject_packet.code).write32(cookie).write8(reason).result,
};
const server_connect_packet = {
code: 0x13,
read: reader => ({id: reader.read8()}),
write: ({id}) => new buffer_writer(2).write8(server_connect_packet.code).write8(id).result,
};
const server_disconnect_packet = {
code: 0x14,
read: reader => ({id: reader.read8(), reason: reader.read32()}),
write: ({id, reason}) => new buffer_writer(6).write8(server_disconnect_packet.code).write8(id).write32(reason).result,
};
const server_message_packet = {
code: 0x01,
read: reader => ({id: reader.read8(), payload: reader.rest()}),
write: ({id, payload}) => new buffer_writer(2 + payload.byteLength).write8(server_message_packet.code).write8(id).rest(payload).result,
};
const server_turn_packet = {
code: 0x02,
read: reader => ({id: reader.read8(), turn: reader.read32()}),
write: ({id, turn}) => new buffer_writer(6).write8(server_turn_packet.code).write8(id).write32(turn).result,
};
const client_info_packet = {
code: 0x31,
read: reader => ({version: reader.read32()}),
write: ({version}) => new buffer_writer(5).write8(client_info_packet.code).write32(version).result,
};
const client_game_list_packet = {
code: 0x21,
read: () => ({}),
write: () => new buffer_writer(1).write8(client_game_list_packet.code).result,
};
const client_create_game_packet = {
code: 0x22,
read: reader => ({cookie: reader.read32(), name: reader.read_str(), password: reader.read_str(), difficulty: reader.read32()}),
write: ({cookie, name, password, difficulty}) => new buffer_writer(11 + name.length + password.length)
.write8(client_create_game_packet.code).write32(cookie).write_str(name).write_str(password).write32(difficulty).result,
};
const client_join_game_packet = {
code: 0x23,
read: reader => ({cookie: reader.read32(), name: reader.read_str(), password: reader.read_str()}),
write: ({cookie, name, password}) => new buffer_writer(7 + name.length + password.length)
.write8(client_join_game_packet.code).write32(cookie).write_str(name).write_str(password).result,
};
const client_leave_game_packet = {
code: 0x24,
read: () => ({}),
write: () => new buffer_writer(1).write8(client_leave_game_packet.code).result,
};
const client_drop_player_packet = {
code: 0x03,
read: reader => ({id: reader.read8(), reason: reader.read32()}),
write: ({id, reason}) => new buffer_writer(6).write8(client_drop_player_packet.code).write8(id).write32(reason).result,
};
const client_message_packet = {
code: 0x01,
read: reader => ({id: reader.read8(), payload: reader.rest()}),
write: ({id, payload}) => new buffer_writer(2 + payload.byteLength).write8(client_message_packet.code).write8(id).rest(payload).result,
};
const client_turn_packet = {
code: 0x02,
read: reader => ({turn: reader.read32()}),
write: ({turn}) => new buffer_writer(5).write8(client_turn_packet.code).write32(turn).result,
};
const PeerID = name => `diabloweb_${name}`;
const MAX_PLRS = 4;
class webrtc_server {
constructor(version, {cookie, name, password, difficulty}, onMessage, onClose) {
this.version = version;
this.name = name;
this.password = password;
this.difficulty = difficulty;
this.onMessage = onMessage;
this.onClose = onClose;
this.peer = new Peer(PeerID(name));
this.peer.on('connection', conn => this.onConnect(conn));
this.players = [];
this.myplr = 0;
this.seed = Math.floor(Math.random() * Math.pow(2, 32));
const onError = () => {
onMessage(server_join_reject_packet.write({cookie, reason: RejectionReason.CREATE_GAME_EXISTS}));
onClose();
this.peer.off('error', onError);
this.peer.off('open', onOpen);
};
const onOpen = () => {
onMessage(server_join_accept_packet.write({cookie, index: 0, seed: this.seed, difficulty}));
onMessage(server_connect_packet.write({id: 0}));
this.peer.off('error', onError);
this.peer.off('open', onOpen);
};
this.peer.on('error', onError);
this.peer.on('open', onOpen);
}
onConnect(conn) {
const peer = {conn};
conn.on('data', packet => {
const reader = new buffer_reader(packet);
const code = reader.read8();
let pkt;
switch (code) {
case client_info_packet.code:
pkt = client_info_packet.read(reader);
peer.version = pkt.version;
break;
case client_join_game_packet.code:
pkt = client_join_game_packet.read(reader);
if (peer.version !== this.version) {
conn.send(server_join_reject_packet.write({cookie: pkt.cookie, reason: RejectionReason.JOIN_VERSION_MISMATCH}));
} else if (pkt.name !== this.name) {
conn.send(server_join_reject_packet.write({cookie: pkt.cookie, reason: RejectionReason.JOIN_GAME_NOT_FOUND}));
} else if (pkt.password !== this.password) {
conn.send(server_join_reject_packet.write({cookie: pkt.cookie, reason: RejectionReason.JOIN_INCORRECT_PASSWORD}));
} else {
let i = 1;
while (i < MAX_PLRS && this.players[i]) {
++i;
}
if (i >= MAX_PLRS) {
conn.send(server_join_reject_packet.write({cookie: pkt.cookie, reason: RejectionReason.JOIN_GAME_FULL}));
} else {
this.players[i] = peer;
peer.id = i;
conn.send(server_join_accept_packet.write({cookie: pkt.cookie, index: i, seed: this.seed, difficulty: this.difficulty}));
this.send(0xFF, server_connect_packet.write({id: i}));
}
}
break;
default:
if (peer.id != null) {
this.handle(peer.id, code, reader);
} else {
return;
}
}
if (!reader.done()) {
throw Error('packet too large');
}
});
conn.on('close', () => {
if (peer.id != null) {
this.drop(peer.id, 0x40000006);
}
});
}
send(mask, pkt) {
for (let i = 1; i < MAX_PLRS; ++i) {
if ((mask & (1 << i)) && this.players[i]) {
if (this.players[i].conn) {
this.players[i].conn.send(pkt);
}
}
}
// self last since it will destroy the buffer
if (mask & 1) {
this.onMessage(pkt);
}
}
drop(id, reason) {
if (id === 0) {
for (let i = 1; i < MAX_PLRS; ++i) {
this.drop(i, 0x40000006);
}
this.onMessage(server_disconnect_packet.write({id, reason}));
this.peer.destroy();
this.onClose();
} else if (this.players[id]) {
this.send(0xFF, server_disconnect_packet.write({id, reason}));
this.players[id].id = null;
if (this.players[id].conn) {
this.players[id].conn.close();
}
this.players[id] = null;
}
}
handle(id, code, reader) {
let pkt;
switch (code) {
case client_leave_game_packet.code:
pkt = client_leave_game_packet.read(reader);
this.drop(id, 3);
break;
case client_drop_player_packet.code:
pkt = client_drop_player_packet.read(reader);
this.drop(pkt.id, pkt.reason);
break;
case client_message_packet.code:
pkt = client_message_packet.read(reader);
this.send(pkt.id === 0xFF ? ~(1 << id) : (1 << pkt.id), server_message_packet.write({id, payload: pkt.payload}));
break;
case client_turn_packet.code:
pkt = client_turn_packet.read(reader);
this.send(~(1 << id), server_turn_packet.write({id, turn: pkt.turn}));
break;
default:
throw Error(`invalid packet ${code}`);
}
}
}
class webrtc_client {
pending = [];
constructor(version, {cookie, name, password}, onMessage, onClose) {
this.peer = new Peer();
this.conn = this.peer.connect(PeerID(name));
const unreg = () => {
this.peer.off('error', onError);
this.conn.off('error', onError);
this.conn.off('open', onOpen);
clearTimeout(timeout);
};
const onError = () => {
onMessage(server_join_reject_packet.write({cookie, reason: RejectionReason.JOIN_GAME_NOT_FOUND}));
onClose();
unreg();
};
const onOpen = () => {
unreg();
this.conn.send(client_info_packet.write({version}));
this.conn.send(client_join_game_packet.write({cookie, name, password}));
for (let pkt of this.pending) {
this.conn.send(pkt);
}
this.pending = null;
};
const timeout = setTimeout(onError, 5000);
this.peer.on('error', onError);
this.conn.on('error', onError);
this.conn.on('open', onOpen);
this.conn.on('data', data => {
const reader = new buffer_reader(data);
const code = reader.read8();
let pkt;
switch (code) {
case server_join_accept_packet.code:
pkt = server_join_accept_packet.read(reader);
this.myplr = pkt.index;
break;
case server_join_reject_packet.code:
onClose();
break;
case server_disconnect_packet.code:
pkt = server_disconnect_packet.read(reader);
if (pkt.id === 'myplr') {
onClose();
}
break;
default:
}
onMessage(data);
});
this.conn.on('close', data => {
onClose();
});
}
send(packet) {
if (this.pending) {
this.pending.push(packet);
} else {
this.conn.send(packet);
}
}
}
export default function webrtc_open(onMessage) {
let server = null, client = null;
let version = 0;
return {
send: function(packet) {
const reader = new buffer_reader(packet);
const code = reader.read8();
let pkt;
switch (code) {
case client_info_packet.code:
pkt = client_info_packet.read(reader);
version = pkt.version;
break;
case client_create_game_packet.code:
pkt = client_create_game_packet.read(reader);
if (server || client) {
onMessage(server_join_reject_packet.write({cookie: pkt.cookie, reason: RejectionReason.JOIN_ALREADY_IN_GAME}));
} else {
server = new webrtc_server(version, pkt, onMessage, () => server = null);
}
break;
case client_join_game_packet.code:
pkt = client_join_game_packet.read(reader);
if (server || client) {
onMessage(server_join_reject_packet.write({cookie: pkt.cookie, reason: RejectionReason.JOIN_ALREADY_IN_GAME}));
} else {
client = new webrtc_client(version, pkt, onMessage, () => client = null);
}
break;
default:
if (server) {
server.handle(0, code, reader);
if (pkt === client_leave_game_packet.code) {
server = null;
}
} else if (client) {
client.send(packet);
if (pkt === client_leave_game_packet.code) {
client = null;
}
return;
} else {
throw Error(`invalid packet ${code}`);
}
}
if (!reader.done()) {
throw Error('packet too large');
}
},
};
}