mlpcardgame/src/network/PeerServer.ts

282 lines
6.8 KiB
TypeScript

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<void> {
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<string, Player> = {};
// 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<ErrorMessage>(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<RenameMessage>(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<ErrorMessage>(player, {
kind: "error",
error: "invalid password"
});
return;
}
conn.off("data", checkPasswordResponse);
this.addPlayer(player);
} catch (e) {
this.send<ErrorMessage>(player, {
kind: "error",
error: "not a password"
});
}
};
this.send<PasswordRequest>(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<RoomInfoMessage>(player, {
kind: "room-info",
room: {
...this.room.info,
password: ""
},
players
});
// Notify other players
this.broadcast<JoinMessage>({
kind: "player-joined",
name: playerName
});
}
private removePlayer(player: NetworkPlayer) {
// Tell the player everything's fine
this.send<AckMessage>(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<LeaveMessage>({
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<ErrorMessage>(player, {
kind: "error",
error: `player not found: ${data.to}`
});
return;
}
this.send<ChatMessage>(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<ErrorMessage>(player, {
kind: "error",
error: "name not available"
});
return;
}
player.name = data.newname;
this.broadcast<RenameMessage>(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<T extends NetworkMessage>(player: Player, message: T) {
if (player.kind == "remote") {
player.conn.send(message);
} else {
player.client.receive(message);
}
}
private broadcast<T extends NetworkMessage>(message: T) {
for (const playerName in this.room.players) {
const player = this.room.players[playerName];
this.send<T>(player, message);
}
}
public get id(): string {
return this.peer.id;
}
}
export default PeerServer;