Skip to content

Commit

Permalink
feat: webworker clientstream + webrtc datachannels as workers (#21)
Browse files Browse the repository at this point in the history
* feat: webworker server + webrtc connections

* fix: disable prompt on peer if connected

* note

* fix: terser workaround

* fix bundles?

* fix

* fix

* add error msg for peer world

* add login errors for worker and webrtc

* fix

* fix

* remove wasm deps, copy while bundling

* move all to /public instead of src/public, simplify

* fix

* fix

* logoff X tabbed players

* only send crypto id from webrtc server and allow peers on insecure context

* fix

* ok one more change...

* fix

* note

* fix

* fix properties due to terser
  • Loading branch information
lesleyrs authored Jul 31, 2024
1 parent 92e74f1 commit bf004a8
Show file tree
Hide file tree
Showing 10 changed files with 555 additions and 14 deletions.
10 changes: 9 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
/node_modules/

/public/
/public/*
/.idea/

bz2.d.ts
bz2.js
bz2.wasm

!/test/resources/*

public/data/players/

# !/public/data/
# !/public/LoginThread.js
# !/public/worker.js
# !/public/bzip2.wasm
# !/public/rsmod-pathfinder.wasm
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,16 @@ http://localhost:8080/?world=0&detail=high&method=0 (TypeScript)
This is not to be confused with the Java TeaVM client which is hosted here if the local server is running:

http://localhost/client?world=0&detail=high&method=0 (Java)

## Web Worker server and WebRTC peer to peer connections

A web worker server will start when loading world 999. This works as a no install, offline, singleplayer version of the server. You will need to self host in order to load saves.

How to use:
1. Run `npm run build` and then `npm run bundle` in the server, this copies all required files to `../Client2/public`.
2. A save dialog will open on logout, you should save to `/public/data/players`.
3. Optional: To host on github uncomment the lines starting with `!/public` in the [.gitignore](.gitignore).

Combined with WebRTC connections you'll be able to host servers using just your browser by manually exchanging a message for each peer. Players that want to join will have to be on world 998 which won't start a web worker server.

Clicking `New user` on login screen or additionally `::peer` ingame for hosts will open a prompt and write either an offer (host) or answer (peer) to clipboard automatically. This message can contain your public IP. Pass the offer to the peer, peer returns the answer and you'll be connected! Closing the prompt will reset the process and allows for any amount of peers.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@
"prepare": "husky install",
"test": "npm run asc && jest",
"asc": "asc --target release --optimizeLevel 3 --converge",
"local": "npm run build:dev && npx serve public"
"local": "npm run build:dev && npx serve public",
"prod": "npm run build && npx http-server -c-1"
},
"repository": {
"type": "git",
Expand Down
104 changes: 101 additions & 3 deletions src/js/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ import VarpType from './jagex2/config/VarpType';
import AnimBase from './jagex2/graphics/AnimBase';
import AnimFrame from './jagex2/graphics/AnimFrame';
import Tile from './jagex2/dash3d/type/Tile';
import ClientWorkerStream from './jagex2/io/ClientWorkerStream';
import {downloadURL} from './jagex2/util/SaveUtil';
import {Host, Peer} from './jagex2/io/RTCDataChannels';

// noinspection JSSuspiciousNameCombination
export abstract class Client extends GameShell {
Expand Down Expand Up @@ -105,7 +108,7 @@ export abstract class Client extends GameShell {
protected db: Database | null = null;
protected loopCycle: number = 0;
protected archiveChecksums: number[] = [];
protected stream: ClientStream | null = null;
protected stream: ClientStream | ClientWorkerStream | null = null;
protected in: Packet = Packet.alloc(1);
protected out: Packet = Packet.alloc(1);
protected loginout: Packet = Packet.alloc(1);
Expand Down Expand Up @@ -483,6 +486,12 @@ export abstract class Client extends GameShell {
protected midiSize: number = 0;
protected midiVolume: number = 192;

// webworker + webrtc
protected worker: Worker | undefined = undefined;
protected workerReady: boolean = false;
protected host: Host | null = null;
protected peer: Peer | null = null;

// debug
// alt+shift click to add a tile overlay
protected userTileMarkers: (Tile | null)[] = new TypedArray1d(16, null);
Expand All @@ -499,6 +508,9 @@ export abstract class Client extends GameShell {
} catch (e) {
/* empty */
}
if (this.peer && Client.getParameter('world') === '998') {
this.peer.dc?.send(JSON.stringify({type: 'close', id: this.peer.uniqueId}));
}
this.stream = null;
stopMidi(false);
// this.midiThreadActive = false;
Expand Down Expand Up @@ -707,6 +719,84 @@ export abstract class Client extends GameShell {
this.imageFlamesRight = null;
};

protected initWorkerP2P = (): void => {
if (Client.getParameter('world') === '999') {
this.worker = new Worker('worker.js', {type: 'module'});
this.worker.onmessage = this.onmessage;
this.host = new Host(this.worker);
} else if (Client.getParameter('world') === '998') {
this.worker = {
onmessage: (e: MessageEvent): void => {
(this.stream as ClientWorkerStream).wwin.onmessage(e);
},
postMessage: (e: MessageEvent): void => {
if (this.peer && this.peer.dc && this.peer.dc.readyState === 'open') {
this.peer.dc.send(JSON.stringify(e));
}
},
onerror: null,
onmessageerror: null,
terminate: (): void => {
throw new Error();
},
addEventListener: (): void => {
throw new Error();
},
removeEventListener: (): void => {
throw new Error();
},
dispatchEvent: (): boolean => {
throw new Error();
}
};
this.peer = new Peer(this.worker);
}
};

protected onmessage = (e: MessageEvent): void => {
switch (e.data.type) {
case 'ready':
this.workerReady = true;
return;
case 'save':
downloadURL(e.data.value, e.data.path.split('/').pop().split('\\').pop());
URL.revokeObjectURL(e.data.value);
return;
case 'close':
this.worker?.postMessage({type: 'close', id: e.data.id});
return;
}

if (this.host?.uniqueId === e.data.id) {
(this.stream as ClientWorkerStream).wwin.onmessage(e.data);
} else {
this.host?.postMessage(e);
}
};

protected exchangeSDP = async (): Promise<void> => {
if (+Client.getParameter('world') === 999) {
if (this.host) {
await this.host.setupPeerConnection();
}
} else if (+Client.getParameter('world') === 998) {
if (this.peer) {
if (this.peer.dc) {
console.log('You are already connected.');
return;
}

let offer: string | null;
try {
while ((offer = prompt('Paste offer here, answer will be copied to clipboard')) === null);
await this.peer.handleOffer(offer);
} catch (e) {
console.error(e);
}
}
}
};

protected loadArchive = async (filename: string, displayName: string, crc: number, progress: number): Promise<Jagfile> => {
let retry: number = 5;
let data: Int8Array | undefined = await this.db?.cacheload(filename);
Expand All @@ -722,7 +812,11 @@ export abstract class Client extends GameShell {
await this.showProgress(progress, `Requesting ${displayName}`);

try {
data = await downloadUrl(`${Client.httpAddress}/${filename}${crc}`);
if (+Client.getParameter('world') < 998) {
data = await downloadUrl(`${Client.httpAddress}/${filename}${crc}`);
} else {
data = await downloadUrl(`${Client.httpAddress}/${filename}`);
}
} catch (e) {
data = undefined;
for (let i: number = retry; i > 0; i--) {
Expand All @@ -747,7 +841,11 @@ export abstract class Client extends GameShell {

if (!data) {
try {
data = await downloadUrl(`${Client.httpAddress}/${name}_${crc}.mid`);
if (+Client.getParameter('world') < 998) {
data = await downloadUrl(`${Client.httpAddress}/${name}_${crc}.mid`);
} else {
data = await downloadUrl(`${Client.httpAddress}/songs/${name}.mid`);
}
if (length !== data.length) {
data = data.slice(0, length);
}
Expand Down
7 changes: 6 additions & 1 deletion src/js/configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ async function world(): Promise<void> {
if (GameShell.getParameter('world').length === 0) {
GameShell.setParameter('world', '1');
}
if (GameShell.getParameter('world') === '0') {
if (['0', '998', '999'].includes(GameShell.getParameter('world'))) {
localConfiguration();
} else {
await liveConfiguration(window.location.protocol.startsWith('https'));
Expand Down Expand Up @@ -51,6 +51,11 @@ function method(): void {
// ---

function localConfiguration(): void {
if (+GameShell.getParameter('world') >= 998) {
Client.httpAddress = 'data/pack/client';
return;
}

Client.serverAddress = 'http://localhost';
Client.httpAddress = 'http://localhost';
Client.portOffset = 0;
Expand Down
33 changes: 31 additions & 2 deletions src/js/game.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ import FloType from './jagex2/config/FloType';
import {setupConfiguration} from './configuration';
import Tile from './jagex2/dash3d/type/Tile';
import DirectionFlag from './jagex2/dash3d/DirectionFlag';
import ClientWorkerStream from './jagex2/io/ClientWorkerStream';
import {Host, Peer} from './jagex2/io/RTCDataChannels';

// noinspection JSSuspiciousNameCombination
class Game extends Client {
Expand All @@ -76,6 +78,8 @@ class Game extends Client {
this.alreadyStarted = true;

try {
this.initWorkerP2P();

await this.showProgress(10, 'Connecting to fileserver');

await Bzip.load(await (await fetch('bz2.wasm')).arrayBuffer());
Expand Down Expand Up @@ -647,6 +651,11 @@ class Game extends Client {

y += 20;
if (this.mouseClickButton === 1 && this.mouseClickX >= x - 75 && this.mouseClickX <= x + 75 && this.mouseClickY >= y - 20 && this.mouseClickY <= y + 20) {
if (+Client.getParameter('world') >= 998) {
this.exchangeSDP();
return;
}

this.titleScreenState = 3;
this.titleLoginField = 0;
}
Expand Down Expand Up @@ -836,8 +845,24 @@ class Game extends Client {
this.loginMessage1 = 'Connecting to server...';
await this.drawTitleScreen();
}
this.stream = new ClientStream(await ClientStream.openSocket({host: Client.serverAddress, port: 43594 + Client.portOffset}));
await this.stream?.readBytes(this.in.data, 0, 8);
if (Game.getParameter('world') === '998') {
if (this.peer && !this.peer.dc) {
this.loginMessage0 = 'You are not connected to a host.';
this.loginMessage1 = 'Please try using world 999.';
return;
}
this.stream = new ClientWorkerStream(this.worker!, this.peer!.uniqueId!);
} else if (Game.getParameter('world') === '999') {
if (!this.workerReady) {
this.loginMessage0 = 'The server is starting up.';
this.loginMessage1 = 'Please try again in a moment.';
return;
}
this.stream = new ClientWorkerStream(this.worker!, this.host!.uniqueId);
} else {
this.stream = new ClientStream(await ClientStream.openSocket({host: Client.serverAddress, port: 43594 + Client.portOffset}));
}
await this.stream.readBytes(this.in.data, 0, 8);
this.in.pos = 0;
this.serverSeed = this.in.g8;
const seed: Int32Array = new Int32Array([Math.floor(Math.random() * 99999999), Math.floor(Math.random() * 99999999), Number(this.serverSeed >> 32n), Number(this.serverSeed & BigInt(0xffffffff))]);
Expand Down Expand Up @@ -4245,6 +4270,10 @@ class Game extends Client {
Client.showDebug = !Client.showDebug;
} else if (this.chatTyped === '::chat') {
Client.chatEra = (Client.chatEra + 1) % 3;
} else if (this.chatTyped === '::peer') {
if (+Client.getParameter('world') === 999) {
this.exchangeSDP();
}
} else if (this.chatTyped.startsWith('::fps ')) {
try {
this.setTargetedFramerate(parseInt(this.chatTyped.substring(6), 10));
Expand Down
4 changes: 3 additions & 1 deletion src/js/jagex2/io/ClientStream.ts
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,9 @@ class WebSocketReader {
await Promise.race([
new Promise((resolve): ((value: PromiseLike<((data: WebSocketEvent | null) => void) | null>) => void) => (this.callback = resolve)),
sleep(2000).then((): void => {
throw new Error('WebSocketReader timed out or closed while reading.');
if (this.closed) {
throw new Error('WebSocketReader closed while reading.');
}
})
]);
}
Expand Down
Loading

0 comments on commit bf004a8

Please sign in to comment.