godot-webrtc-mplayer-testing/server.ts
2021-12-07 16:09:50 -06:00

449 lines
11 KiB
TypeScript

// import { randomInt } from "./deps.ts";
const SERVER_VERSION = "0.2.0";
// TODO: version comparison
type ID = string;
// app state
const allLobbies = new Map<ID, Lobby>();
const allClients = new Map<ID, Client>();
// TODO: client index by id
interface DataMessage {
type: string;
}
type ServerDataObject = Record<
string,
string | number | symbol | null | boolean
>;
type ServerData = ServerDataObject[] | ServerDataObject | string | boolean;
type Message = string | DataMessage;
const buildMessage = (
type: string,
data: ServerData | ServerData[],
) =>
Object.assign(
{ type },
Array.isArray(data)
? { data }
: (typeof data === "object" ? data : { data }),
);
class Client {
id: ID;
peerId: number | null;
name: string;
socket: WebSocket;
lobby: Lobby | null;
ready: boolean;
constructor(socket: WebSocket) {
this.id = crypto.randomUUID();
this.socket = socket;
this.peerId = null;
this.name = "Client";
this.lobby = null;
this.ready = false;
allClients.set(this.id, this);
}
isConnected() {
return this.socket.readyState == WebSocket.OPEN;
}
setReady(ready: boolean) {
this.ready = ready;
this.lobby?.broadcast(
buildMessage("ready_change", { id: this.id, ready: this.ready }),
);
}
remove() {
this.lobbyLeave();
if (this.isConnected()) {
this.socket.close();
}
allClients.delete(this.id);
}
send(message: Message) {
try {
if (this.isConnected()) {
this.socket.send(
typeof message === "object"
? ("json:" + JSON.stringify(message))
: message,
);
}
} catch (e) {
console.error(
`Failed to send on socket ${this.socket} to client ${this.id}: ${
JSON.stringify(e)
}`,
);
this.remove();
}
}
ping() {
this.send(buildMessage("ping", {}));
}
peerList() {
if (!this.lobby) return;
const peers: {
id: ID;
name: string;
peerId: number | null;
ready: boolean;
}[] = [];
this.lobby.clients.forEach(({ id, name, peerId, ready }) =>
peers.push({ id, name, peerId, ready })
);
this.send(buildMessage("peer_joined", peers));
// TODO: chunk async?
}
lobbyList() {
const netLobbies: {
id: ID;
name: string;
maxPlayers: number;
locked: boolean;
currentPlayers: number;
}[] = [];
allLobbies.forEach((lobby) => {
const { id, name, maxPlayers, locked } = lobby;
netLobbies.push({
id,
name,
maxPlayers,
locked,
currentPlayers: lobby.clients.size,
});
});
// TODO: chunk async?
this.send(buildMessage("lobby_list", netLobbies));
}
lobbyNew(lobby: Lobby) {
// if the client is already in a lobby, they don't care about new lobbies
if (this.lobby) return;
const { id, name, maxPlayers, locked } = lobby;
this.send(
buildMessage("lobby_new", {
id,
name,
maxPlayers,
locked,
currentPlayers: lobby.clients.size,
}),
);
}
lobbyDelete({ id }: Lobby) {
if (this.lobby) return;
this.send(buildMessage("lobby_delete", { id }));
}
lobbyCreate(opts?: Partial<Lobby>) {
if (this.lobby) {
this.send(
`[info] cannot create lobby (already in lobby ${this.lobby.id})`,
);
return;
}
new Lobby(this, opts?.name || `${this.name}'s lobby`, opts?.maxPlayers);
}
lobbyJoin(lobby: Lobby) {
if (this.lobby) {
this.send(`[info] cannot join lobby (already in lobby ${this.lobby.id})`);
return;
}
this.lobby = lobby;
lobby.addClient(this);
this.send(
buildMessage("lobby_joined", {
id: lobby.id,
name: lobby.name,
peerId: this.peerId,
}),
);
}
lobbyLeave() {
const leavingLobby = this.lobby;
if (!leavingLobby) {
this.send(`[info] cannot leave lobby (not in a lobby)`);
return;
}
this.peerId = null;
this.lobby = null;
if (this.isConnected()) {
this.send(buildMessage("lobby_left", { id: leavingLobby.id }));
}
leavingLobby.removeClient(this);
}
}
class Lobby {
id: ID;
name: string;
clients: Map<ID, Client>;
hostClientId: ID;
maxPlayers: number;
locked: boolean;
constructor(host: Client, name?: string, maxPlayers = 20) {
this.id = crypto.randomUUID();
this.hostClientId = host.id;
this.clients = new Map<ID, Client>();
this.name = name || this.id;
this.maxPlayers = maxPlayers;
this.locked = false;
allLobbies.set(this.id, this);
host.peerId = 1;
host.lobbyJoin(this);
this.notify();
}
update(
requestor: Client,
newValues: { name?: string; maxPlayers?: number; locked?: boolean },
) {
if (requestor.peerId === 1) {
for (const k in newValues) {
switch (k) {
case "name":
if (newValues?.name) this.name = newValues.name;
break;
case "maxPlayers":
if (
newValues?.maxPlayers && typeof newValues.maxPlayers === "number"
) {
this.maxPlayers = newValues.maxPlayers;
}
break;
case "locked":
if (newValues?.locked) this.locked = newValues.locked;
break;
}
}
this.notify();
} else {
requestor.send(
"[info] you cannot update this lobby (you are not the host!)",
);
}
}
notify() {
allClients.forEach((client) => client.lobbyNew(this));
}
remove() {
allLobbies.delete(this.id);
allClients.forEach((client) => client.lobbyDelete(this));
this.clients.forEach((client) => {
client.lobbyLeave();
});
}
broadcast(message: Message) {
this.clients.forEach((client) => client.send(message));
}
addClient(client: Client) {
if (!client.peerId) {
const arr = new Int32Array(1);
crypto.getRandomValues(arr);
client.peerId = Math.abs(arr[0]);
}
client.send(buildMessage("your_peer_id", client.peerId.toString()));
this.broadcast(
buildMessage("peer_joined", [{
id: client.id,
name: client.name,
peerId: client.peerId,
}]),
);
console.log("Sending peer_joined...");
client.send(
buildMessage(
"peer_joined",
Array.from(this.clients.values()).map(
({ id, name, peerId }) => ({ id, name, peerId }),
),
),
);
this.clients.set(client.id, client);
this.notify();
}
removeClient({ id }: Client) {
this.clients.delete(id);
this.broadcast(buildMessage("peer_left", [{ id }]));
if (id === this.hostClientId) {
console.warn("Host left!");
this.remove();
}
this.notify();
}
}
interface ClientMessage {
type:
| "candidate"
| "offer"
| "answer"
| "init"
| "lobby_create"
| "update_lobby";
data: ServerDataObject;
}
// events
function onMessage(client: Client, ev: MessageEvent) {
// TODO: log who from?
const msg = ev.data.trim();
if (msg === "pong") return;
console.log("Client Message Received", msg);
if (msg === "init") {
client.send(
buildMessage("init", {
id: client.id,
name: client.name,
serverVersion: SERVER_VERSION,
}),
);
}
if (msg === "lobby_create") client.lobbyCreate();
if (msg.startsWith("set_ready:")) {
client.setReady(JSON.parse(msg.split(":", 2)[1]));
}
if (msg === "lobby_leave") client.lobbyLeave();
if (msg === "request_lobby_list") client.lobbyList();
if (msg === "request_peer_list") {
if (client.lobby == null) {
client.send(`[info] not in a lobby`);
} else {
client.peerList();
}
}
if (msg.startsWith("update_display_name:")) {
const displayName = msg.substr("update_display_name:".indexOf(":") + 1);
client.name = displayName;
}
if (msg.startsWith("lobby_join:")) {
const id = msg.substr(11);
const lobby = allLobbies.get(id);
if (lobby) {
client.lobbyJoin(lobby);
// client.peerList();
} else client.send(`[info] could not find lobby ${id}`);
}
if (msg.startsWith("json:")) {
const data: ClientMessage = JSON.parse(msg.substr(5));
if (data.type === "init") {
if (data.data.name) {
client.name = data.data.name.toString();
}
client.send(buildMessage("init", { id: client.id, name: client.name }));
} else if (data.type === "update_lobby") {
if (client.lobby) {
client.lobby.update(client, data["data"]);
} else {
client.send("[info] cannot update lobby (you're not even in one!)");
}
} else if (data.type === "lobby_create") {
client.lobbyCreate({ name: data.data?.name?.toString() });
if (data.data.name) {
client.name = data.data.name.toString();
}
client.send(buildMessage("init", { id: client.id, name: client.name }));
} else if (["candidate", "answer", "offer"].includes(data.type)) {
console.log("Received WebRTC Negotiation Message...");
if (typeof data.data === "object") {
const subdata = data.data;
if (typeof subdata["peerId"] === "number") {
const destPeerId: number = subdata["peerId"];
for (const iClient of client.lobby?.clients.values() || []) {
if (iClient.peerId == destPeerId) {
const payload = Object.assign({}, data);
const srcPeerId = client.peerId;
payload.data.peerId = srcPeerId;
console.log(
`Forwarding WebRTC Negotiation Message from peer ${srcPeerId} to peer ${destPeerId}...`,
);
iClient.send(payload);
break;
}
}
}
}
}
}
}
function onSocketOpen(client: Client, _ev: Event) {
console.log("New Client", client.id);
}
function onClientLeave(client: Client) {
client.remove();
}
function onSocketClose(client: Client, _ev: Event) {
console.log("Client Close");
onClientLeave(client);
}
function onSocketError(client: Client, _ev: Event) {
console.log("Client Error");
onClientLeave(client);
}
setInterval(() => {
allLobbies.forEach((lobby) => {
if (lobby.clients.size <= 0) lobby.remove();
});
allClients.forEach((client) => client.ping());
}, 5000);
const PORT = parseInt(Deno.env.get("PORT") || "80");
console.log("Listening on port", PORT);
const listener = Deno.listen({ port: PORT });
for await (const conn of listener) {
// console.debug("Connection received:", conn);
(async () => {
const server = Deno.serveHttp(conn);
for await (const { respondWith, request } of server) {
// console.debug("HTTP Request Received", request);
try {
// console.warn(JSON.stringify([allClients, allLobbies]));
const { socket, response } = Deno.upgradeWebSocket(request);
const client = new Client(socket);
socket.onmessage = (ev) => onMessage(client, ev);
socket.onopen = (ev) => onSocketOpen(client, ev);
socket.onclose = (ev) => onSocketClose(client, ev);
socket.onerror = (ev) => onSocketError(client, ev);
respondWith(response);
} catch (e) {
console.log("Could not add client for unhandled reason:", e);
respondWith(
new Response("400 Bad Request", {
status: 400,
headers: { "content-type": "text/html" },
}),
);
}
}
})();
}