import EventEmitter from "eventemitter3"; import Peer, { DataConnection } from "peerjs"; import { LocalClient } from "."; import { AckMessage, ChatMessage, ErrorMessage, JoinMessage, LeaveMessage, NetworkMessage, NetworkPlayer, PasswordRequest, PasswordResponse, PeerMetadata, Player, RenameMessage, Room, RoomInfo, RoomInfoMessage, } from "./types"; // Increment name, add number at the end if not present // Examples: // Guest -> Guest1 // Guest1 -> Guest2 // Guest9 -> Guest10 function nextName(name: string): string { let i = 1; for (; i < name.length; i++) { if (!isNaN(Number(name.slice(i - name.length)))) { break; } } return name.substr(0, i) + (Number(name.slice(i)) + 1); } function connectionOpen(conn: DataConnection): Promise { return new Promise(resolve => { conn.on("open", () => resolve()); }); } export class PeerServer extends EventEmitter { protected peer: Peer; private room: Room; public constructor( roomInfo: RoomInfo, local: LocalClient, customPeer?: Peer ) { super(); let players: Record = {}; // Add local player to server players[local.name] = { kind: "local", name: local.name, client: local }; local.receiver = this._received.bind(this, players[local.name]); this.room = { info: roomInfo, players }; local.players = Object.keys(this.players); local.roomInfo = this.room.info; // Setup peer this.peer = customPeer ? customPeer : new Peer(); this.peer.on("open", id => { console.info("Peer ID assigned: %s", id); this.emit("open", id); }); this.peer.on("connection", conn => { this._connection(conn); }); } private async _connection(conn: DataConnection) { const metadata = conn.metadata as PeerMetadata; // Wait for connection to be open await connectionOpen(conn); let player: NetworkPlayer = { kind: "remote", name: metadata.name, conn: conn }; // // Check if this connection should be allowed // // Check if room is full if (this.playerCount >= this.room.info.max_players) { this.send(player, { kind: "error", error: "room is full" }); conn.close(); return; } // Check if there is already a player called that way if (this.room.players[metadata.name]) { // Force rename let newname = metadata.name; while (this.room.players[newname]) { newname = nextName(metadata.name); } this.send(player, { kind: "rename", oldname: metadata.name, newname: newname }); player.name = newname; player.conn.metadata.name = newname; } // Check for password if (this.room.info.password != "") { const checkPasswordResponse = (data: any) => { try { let resp = data as PasswordResponse; if (resp.password != this.room.info.password) { this.send(player, { kind: "error", error: "invalid password" }); return; } conn.off("data", checkPasswordResponse); this.addPlayer(player); } catch (e) { this.send(player, { kind: "error", error: "not a password" }); } }; this.send(player, { kind: "password-req" }); conn.on("data", checkPasswordResponse.bind(this)); return; } this.addPlayer(player); } private addPlayer(player: NetworkPlayer) { const playerName = player.name; // Hacky: Give player list before this player was added, so join // message doesn't mess things up later const players = Object.keys(this.room.players); // Add player to room this.room.players[playerName] = player; // Start listening for new messages player.conn.on( "data", this._received.bind(this, this.room.players[playerName]) ); player.conn.on("error", err => { throw err; }); // Send the player info about the room this.send(player, { kind: "room-info", room: { ...this.room.info, password: "" }, players }); // Notify other players this.broadcast({ kind: "player-joined", name: playerName }); } private removePlayer(player: NetworkPlayer) { // Tell the player everything's fine this.send(player, { kind: "ok", what: "leave-req" }); // Close connection with player player.conn.close(); // Remove player from player list delete this.room.players[player.name]; // Notify other players this.broadcast({ kind: "player-left", name: player.name }); } private _received(player: Player, data: NetworkMessage) { switch (data.kind) { // Player wants to say something in chat case "chat": data.from = player.name; if (data.to == "") { // Players is saying that out loud this.broadcast(data); } else { // Player is telling someone specifically if (!(data.to in this.players)) { this.send(player, { kind: "error", error: `player not found: ${data.to}` }); return; } this.send(this.players[data.to], data); } break; // Player wants to change name case "rename": // Make sure new name is valid data.oldname = player.name; if (data.newname in this.players) { this.send(player, { kind: "error", error: "name not available" }); return; } player.name = data.newname; this.broadcast(data); break; // Player is leaving! case "leave-req": // If we're leaving, end the server if (player.kind == "local") { //TODO } else { // Remove and disconnect player this.removePlayer(player); } break; } } public get playerCount(): number { return Object.keys(this.room.players).length; } public get players() { return this.room.players; } protected send(player: Player, message: T) { if (player.kind == "remote") { player.conn.send(message); } else { player.client.receive(message); } } private broadcast(message: T) { for (const playerName in this.room.players) { const player = this.room.players[playerName]; this.send(player, message); } } public get id(): string { return this.peer.id; } } export default PeerServer;