Possibly functional deno server
This commit is contained in:
commit
308f09916b
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
|
@ -0,0 +1,6 @@
|
|||
.import/
|
||||
export.cfg
|
||||
export_presets.cfg
|
||||
*.translation
|
||||
.mono
|
||||
data_*/
|
10
.vim/coc-settings.json
Normal file
10
.vim/coc-settings.json
Normal file
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"deno.enable": true,
|
||||
"deno.lint": true,
|
||||
"deno.unstable": true,
|
||||
"prettier.disableLanguages": [
|
||||
"typescript",
|
||||
"javascript"
|
||||
],
|
||||
"tsserver.enable": false
|
||||
}
|
5
default_env.tres
Normal file
5
default_env.tres
Normal file
|
@ -0,0 +1,5 @@
|
|||
[gd_resource type="Environment" load_steps=2 format=2]
|
||||
[sub_resource type="ProceduralSky" id=1]
|
||||
[resource]
|
||||
background_mode = 2
|
||||
background_sky = SubResource( 1 )
|
35
icon.png.import
Normal file
35
icon.png.import
Normal file
|
@ -0,0 +1,35 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="StreamTexture"
|
||||
path="res://.import/icon.png-487276ed1e3a0c39cad0279d744ee560.stex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://icon.png"
|
||||
dest_files=[ "res://.import/icon.png-487276ed1e3a0c39cad0279d744ee560.stex" ]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/lossy_quality=0.7
|
||||
compress/hdr_mode=0
|
||||
compress/bptc_ldr=0
|
||||
compress/normal_map=0
|
||||
flags/repeat=0
|
||||
flags/filter=true
|
||||
flags/mipmaps=false
|
||||
flags/anisotropic=false
|
||||
flags/srgb=2
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/HDR_as_SRGB=false
|
||||
process/invert_color=false
|
||||
process/normal_map_invert_y=false
|
||||
stream=false
|
||||
size_limit=0
|
||||
detect_3d=true
|
||||
svg/scale=1.0
|
25
project.godot
Normal file
25
project.godot
Normal file
|
@ -0,0 +1,25 @@
|
|||
; Engine configuration file.
|
||||
; It's best edited using the editor UI and not directly,
|
||||
; since the parameters that go here are not all obvious.
|
||||
;
|
||||
; Format:
|
||||
; [section] ; section goes between []
|
||||
; param=value ; assign values to parameters
|
||||
|
||||
config_version=4
|
||||
|
||||
[application]
|
||||
|
||||
config/name="kdt"
|
||||
config/icon="res://icon.png"
|
||||
|
||||
[physics]
|
||||
|
||||
common/enable_pause_aware_picking=true
|
||||
|
||||
[rendering]
|
||||
|
||||
quality/driver/driver_name="GLES2"
|
||||
vram_compression/import_etc=true
|
||||
vram_compression/import_etc2=false
|
||||
environment/default_environment="res://default_env.tres"
|
266
server.ts
Normal file
266
server.ts
Normal file
|
@ -0,0 +1,266 @@
|
|||
const MAX_PEERS = 4096;
|
||||
const MAX_LOBBIES = 1024;
|
||||
const PORT = 9080;
|
||||
const ALFNUM = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
|
||||
const NO_LOBBY_TIMEOUT = 1000;
|
||||
const SEAL_CLOSE_TIMEOUT = 10000;
|
||||
const PING_INTERVAL = 10000;
|
||||
|
||||
const STR_NO_LOBBY = "Have not joined lobby yet";
|
||||
const STR_HOST_DISCONNECTED = "Room host has disconnected";
|
||||
const STR_ONLY_HOST_CAN_SEAL = "Only host can seal the lobby";
|
||||
const STR_SEAL_COMPLETE = "Seal complete";
|
||||
const STR_TOO_MANY_LOBBIES = "Too many lobbies open, disconnecting";
|
||||
const STR_ALREADY_IN_LOBBY = "Already in a lobby";
|
||||
const STR_LOBBY_DOES_NOT_EXISTS = "Lobby does not exists";
|
||||
const STR_LOBBY_IS_SEALED = "Lobby is sealed";
|
||||
const STR_INVALID_FORMAT = "Invalid message format";
|
||||
const STR_NEED_LOBBY = "Invalid message when not in a lobby";
|
||||
const STR_SERVER_ERROR = "Server error, lobby not found";
|
||||
const STR_INVALID_DEST = "Invalid destination";
|
||||
const STR_INVALID_CMD = "Invalid command";
|
||||
const STR_TOO_MANY_PEERS = "Too many peers connected";
|
||||
|
||||
function randomInt(low: number, high: number) {
|
||||
return Math.floor(Math.random() * (high - low + 1) + low);
|
||||
}
|
||||
|
||||
function randomId() {
|
||||
const arr = new Int32Array(1);
|
||||
crypto.getRandomValues(arr);
|
||||
return Math.abs(arr[0]);
|
||||
}
|
||||
|
||||
function randomSecret() {
|
||||
let out = "";
|
||||
for (let i = 0; i < 16; i++) {
|
||||
out += ALFNUM[randomInt(0, ALFNUM.length - 1)];
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
class ProtoError extends Error {
|
||||
code: number;
|
||||
|
||||
constructor(code: number, message: string) {
|
||||
super(message);
|
||||
this.code = code;
|
||||
}
|
||||
}
|
||||
|
||||
class Peer {
|
||||
id: number;
|
||||
ws: WebSocket;
|
||||
lobby: string;
|
||||
timeout: number;
|
||||
|
||||
constructor(id: number, ws: WebSocket) {
|
||||
this.id = id;
|
||||
this.ws = ws;
|
||||
this.lobby = "";
|
||||
// Close connection after 1 sec if client has not joined a lobby
|
||||
this.timeout = setTimeout(() => {
|
||||
if (!this.lobby) ws.close(4000, STR_NO_LOBBY);
|
||||
}, NO_LOBBY_TIMEOUT);
|
||||
}
|
||||
}
|
||||
|
||||
class Lobby {
|
||||
name: string;
|
||||
host: number;
|
||||
peers: Peer[];
|
||||
sealed: boolean;
|
||||
closeTimer: number;
|
||||
|
||||
constructor(name: string, host: number) {
|
||||
this.name = name;
|
||||
this.host = host;
|
||||
this.peers = [];
|
||||
this.sealed = false;
|
||||
this.closeTimer = -1;
|
||||
}
|
||||
|
||||
getPeerId(peer: Peer) {
|
||||
if (this.host === peer.id) return 1;
|
||||
return peer.id;
|
||||
}
|
||||
|
||||
join(peer: Peer) {
|
||||
const assigned = this.getPeerId(peer);
|
||||
peer.ws.send(`I: ${assigned}\n`);
|
||||
this.peers.forEach((p) => {
|
||||
p.ws.send(`N: ${assigned}\n`);
|
||||
peer.ws.send(`N: ${this.getPeerId(p)}\n`);
|
||||
});
|
||||
this.peers.push(peer);
|
||||
}
|
||||
leave(peer: Peer) {
|
||||
const idx = this.peers.findIndex((p) => peer === p);
|
||||
if (idx === -1) return false;
|
||||
const assigned = this.getPeerId(peer);
|
||||
const close = assigned === 1;
|
||||
this.peers.forEach((p) => {
|
||||
// Room host disconnected, must close.
|
||||
if (close) p.ws.close(4000, STR_HOST_DISCONNECTED);
|
||||
// Notify peer disconnect.
|
||||
else p.ws.send(`D: ${assigned}\n`);
|
||||
});
|
||||
this.peers.splice(idx, 1);
|
||||
if (close && this.closeTimer >= 0) {
|
||||
// We are closing already.
|
||||
clearTimeout(this.closeTimer);
|
||||
this.closeTimer = -1;
|
||||
}
|
||||
return close;
|
||||
}
|
||||
seal(peer: Peer) {
|
||||
// Only host can seal
|
||||
if (peer.id !== this.host) {
|
||||
throw new ProtoError(4000, STR_ONLY_HOST_CAN_SEAL);
|
||||
}
|
||||
this.sealed = true;
|
||||
this.peers.forEach((p) => {
|
||||
p.ws.send("S: \n");
|
||||
});
|
||||
console.log(
|
||||
`Peer ${peer.id} sealed lobby ${this.name} ` +
|
||||
`with ${this.peers.length} peers`,
|
||||
);
|
||||
this.closeTimer = setTimeout(() => {
|
||||
// Close peer connection to host (and thus the lobby)
|
||||
this.peers.forEach((p) => {
|
||||
p.ws.close(1000, STR_SEAL_COMPLETE);
|
||||
});
|
||||
}, SEAL_CLOSE_TIMEOUT);
|
||||
}
|
||||
}
|
||||
|
||||
const lobbies = new Map();
|
||||
let peersCount = 0;
|
||||
|
||||
function joinLobby(peer: Peer, lobbyName: string) {
|
||||
if (lobbyName === "") {
|
||||
if (lobbies.size >= MAX_LOBBIES) {
|
||||
throw new ProtoError(4000, STR_TOO_MANY_LOBBIES);
|
||||
}
|
||||
// Peer must not already be in a lobby
|
||||
if (peer.lobby !== "") {
|
||||
throw new ProtoError(4000, STR_ALREADY_IN_LOBBY);
|
||||
}
|
||||
lobbyName = randomSecret();
|
||||
lobbies.set(lobbyName, new Lobby(lobbyName, peer.id));
|
||||
console.log(`Peer ${peer.id} created lobby ${lobbyName}`);
|
||||
console.log(`Open lobbies: ${lobbies.size}`);
|
||||
}
|
||||
const lobby = lobbies.get(lobbyName);
|
||||
if (!lobby) throw new ProtoError(4000, STR_LOBBY_DOES_NOT_EXISTS);
|
||||
if (lobby.sealed) throw new ProtoError(4000, STR_LOBBY_IS_SEALED);
|
||||
peer.lobby = lobbyName;
|
||||
console.log(
|
||||
`Peer ${peer.id} joining lobby ${lobbyName} ` +
|
||||
`with ${lobby.peers.length} peers`,
|
||||
);
|
||||
lobby.join(peer);
|
||||
peer.ws.send(`J: ${lobbyName}\n`);
|
||||
}
|
||||
|
||||
function parseMsg(peer: Peer, msg: string) {
|
||||
const sep = msg.indexOf("\n");
|
||||
if (sep < 0) throw new ProtoError(4000, STR_INVALID_FORMAT);
|
||||
|
||||
const cmd = msg.slice(0, sep);
|
||||
if (cmd.length < 3) throw new ProtoError(4000, STR_INVALID_FORMAT);
|
||||
|
||||
const data = msg.slice(sep);
|
||||
|
||||
// Lobby joining.
|
||||
if (cmd.startsWith("J: ")) {
|
||||
joinLobby(peer, cmd.substr(3).trim());
|
||||
return;
|
||||
}
|
||||
|
||||
if (!peer.lobby) throw new ProtoError(4000, STR_NEED_LOBBY);
|
||||
const lobby = lobbies.get(peer.lobby);
|
||||
if (!lobby) throw new ProtoError(4000, STR_SERVER_ERROR);
|
||||
|
||||
// Lobby sealing.
|
||||
if (cmd.startsWith("S: ")) {
|
||||
lobby.seal(peer);
|
||||
return;
|
||||
}
|
||||
|
||||
// Message relaying format:
|
||||
//
|
||||
// [O|A|C]: DEST_ID\n
|
||||
// PAYLOAD
|
||||
//
|
||||
// O: Client is sending an offer.
|
||||
// A: Client is sending an answer.
|
||||
// C: Client is sending a candidate.
|
||||
let destId = parseInt(cmd.substr(3).trim());
|
||||
// Dest is not an ID.
|
||||
if (!destId) throw new ProtoError(4000, STR_INVALID_DEST);
|
||||
if (destId === 1) destId = lobby.host;
|
||||
const dest = lobby.peers.find((p: Peer) => p.id === destId);
|
||||
// Dest is not in this room.
|
||||
if (!dest) throw new ProtoError(4000, STR_INVALID_DEST);
|
||||
|
||||
function isCmd(what: string) {
|
||||
return cmd.startsWith(`${what}: `);
|
||||
}
|
||||
if (isCmd("O") || isCmd("A") || isCmd("C")) {
|
||||
dest.ws.send(cmd[0] + ": " + lobby.getPeerId(peer) + data);
|
||||
return;
|
||||
}
|
||||
throw new ProtoError(4000, STR_INVALID_CMD);
|
||||
}
|
||||
|
||||
console.log(`Server running on port ${PORT}`);
|
||||
const conn = Deno.listen({ port: PORT });
|
||||
const httpConn = Deno.serveHttp(await conn.accept());
|
||||
const e = await httpConn.nextRequest();
|
||||
if (e) {
|
||||
const { socket, response } = Deno.upgradeWebSocket(e.request);
|
||||
const id = randomId();
|
||||
const peer = new Peer(id, socket);
|
||||
socket.onopen = (_ev) => {
|
||||
if (peersCount >= MAX_PEERS) {
|
||||
socket.close(4000, STR_TOO_MANY_PEERS);
|
||||
return;
|
||||
}
|
||||
peersCount++;
|
||||
};
|
||||
socket.onmessage = (ev) => {
|
||||
try {
|
||||
parseMsg(peer, ev.data);
|
||||
} catch (e) {
|
||||
const code = e.code || 4000;
|
||||
console.log(`Error parsing message from ${id}:\n` + ev.data);
|
||||
socket.close(code, e.message);
|
||||
}
|
||||
};
|
||||
socket.onclose = (ev) => {
|
||||
peersCount--;
|
||||
console.log(
|
||||
`Connection with peer ${peer.id} closed ` +
|
||||
`with reason: ${ev.reason}`,
|
||||
);
|
||||
if (
|
||||
peer.lobby &&
|
||||
lobbies.has(peer.lobby) &&
|
||||
lobbies.get(peer.lobby).leave(peer)
|
||||
) {
|
||||
lobbies.delete(peer.lobby);
|
||||
console.log(`Deleted lobby ${peer.lobby}`);
|
||||
console.log(`Open lobbies: ${lobbies.size}`);
|
||||
peer.lobby = "";
|
||||
}
|
||||
if (peer.timeout >= 0) {
|
||||
clearTimeout(peer.timeout);
|
||||
peer.timeout = -1;
|
||||
}
|
||||
};
|
||||
socket.onerror = (e) => console.error("WebSocket error:", e);
|
||||
e.respondWith(response);
|
||||
}
|
Loading…
Reference in a new issue