diff --git a/src/network/Client.ts b/src/network/Client.ts index cf4e87b..1268a2d 100644 --- a/src/network/Client.ts +++ b/src/network/Client.ts @@ -1,7 +1,7 @@ import { PeerMetadata, NetworkMessage } from "./types"; import EventEmitter from "eventemitter3"; -export class Client extends EventEmitter { +export abstract class Client extends EventEmitter { public metadata: PeerMetadata; public players: string[]; @@ -47,6 +47,23 @@ export class Client extends EventEmitter { public get name(): string { return this.metadata.name; } + + public abstract send(data: T): void; + + public leave(): void { + this.send({ + kind: "leave-req" + }); + } + + public say(to: string | null, message: string) { + this.send({ + kind: "chat", + from: this.name, + to: to ? to : "", + message + }); + } } export default Client; diff --git a/src/network/PeerClient.ts b/src/network/PeerClient.ts index 76b5253..b728dab 100644 --- a/src/network/PeerClient.ts +++ b/src/network/PeerClient.ts @@ -34,8 +34,11 @@ export class PeerClient extends Client { return this.metadata.name; } - protected send(conn: DataConnection, data: T) { - conn.send(data); + public send(data: T) { + if (!this.connection) { + throw new Error("Client is not connected to a server"); + } + this.connection.send(data); } } diff --git a/src/network/PeerServer.ts b/src/network/PeerServer.ts index 12b34ee..40da2ce 100644 --- a/src/network/PeerServer.ts +++ b/src/network/PeerServer.ts @@ -13,7 +13,8 @@ import { RenameMessage, LeaveMessage, NetworkPlayer, - AckMessage + AckMessage, + ChatMessage } from "./types"; import { LocalClient } from "."; import EventEmitter from "eventemitter3"; @@ -70,7 +71,12 @@ export class PeerServer extends EventEmitter { private _connection(conn: DataConnection) { const metadata = conn.metadata as PeerMetadata; - console.info("%s (%s) connected!", metadata.name, conn.label); + + let player: NetworkPlayer = { + kind: "remote", + name: metadata.name, + conn: conn + }; // // Check if this connection should be allowed @@ -78,7 +84,7 @@ export class PeerServer extends EventEmitter { // Check if room is full if (this.playerCount >= this.room.info.max_players) { - this.send(conn, { kind: "error", error: "room is full" }); + this.send(player, { kind: "error", error: "room is full" }); conn.close(); return; } @@ -90,12 +96,13 @@ export class PeerServer extends EventEmitter { while (this.room.players[newname]) { newname = nextName(metadata.name); } - this.send(conn, { + this.send(player, { kind: "rename", oldname: metadata.name, newname: newname }); - metadata.name = newname; + player.name = newname; + player.conn.metadata.name = newname; } // Check for password @@ -104,43 +111,42 @@ export class PeerServer extends EventEmitter { try { let resp = data as PasswordResponse; if (resp.password != this.room.info.password) { - this.send(conn, { + this.send(player, { kind: "error", error: "invalid password" }); return; } conn.off("data", checkPasswordResponse); - this.addPlayer(conn); + this.addPlayer(player); } catch (e) { - this.send(conn, { + this.send(player, { kind: "error", error: "not a password" }); } }; - this.send(conn, { kind: "password-req" }); + this.send(player, { kind: "password-req" }); conn.on("data", checkPasswordResponse.bind(this)); return; } - this.addPlayer(conn); + this.addPlayer(player); } - private addPlayer(conn: DataConnection) { - const playerName = conn.metadata.name; - this.room.players[playerName] = { - kind: "remote", - name: conn.metadata.name, - conn: conn - }; + private addPlayer(player: NetworkPlayer) { + const playerName = player.name; + this.room.players[playerName] = player; // Start listening for new messages - conn.on("data", this._received.bind(this, this.room.players[playerName])); + player.conn.on( + "data", + this._received.bind(this, this.room.players[playerName]) + ); // Send the player info about the room - this.send(conn, { + this.send(player, { kind: "room-info", room: { ...this.room.info, @@ -158,7 +164,7 @@ export class PeerServer extends EventEmitter { private removePlayer(player: NetworkPlayer) { // Tell the player everything's fine - this.send(player.conn, { kind: "ok", what: "leave-req" }); + this.send(player, { kind: "ok", what: "leave-req" }); // Close connection with player player.conn.close(); @@ -172,6 +178,24 @@ export class PeerServer extends EventEmitter { 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(this.players[data.to], data); + } else { + this.send(player, { + kind: "error", + error: `player not found: ${data.to}` + }); + } + } + break; // Player is leaving! case "leave-req": // If we're leaving, end the server @@ -193,18 +217,18 @@ export class PeerServer extends EventEmitter { return this.room.players; } - protected send(conn: DataConnection, data: T) { - conn.send(data); + 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]; - if (player.kind == "remote") { - this.send(player.conn, message); - } else { - player.client.receive(message); - } + this.send(player, message); } } } diff --git a/src/network/types.ts b/src/network/types.ts index e6991af..a01b192 100644 --- a/src/network/types.ts +++ b/src/network/types.ts @@ -69,6 +69,7 @@ export type NetworkMessage = | JoinMessage | LeaveMessage | RenameMessage + | ChatMessage | AckMessage; export interface PasswordRequest { @@ -106,6 +107,13 @@ export interface RenameMessage { newname: string; } +export interface ChatMessage { + kind: "chat"; + from: string; + to: string; // "" means everyone + message: string; +} + export interface AckMessage { kind: "ok"; what: string; diff --git a/src/tests/unit/network.spec.ts b/src/tests/unit/network.spec.ts index cc77397..cf17778 100644 --- a/src/tests/unit/network.spec.ts +++ b/src/tests/unit/network.spec.ts @@ -1,5 +1,5 @@ import { MockHelper } from "@/testing"; -import { NetworkMessage, LocalClient } from "@/network"; +import { NetworkMessage, LocalClient, ChatMessage } from "@/network"; import { EventHook } from "@/testing/EventHook"; const sampleRoom = () => ({ @@ -74,4 +74,36 @@ describe("network/PeerClient", () => { // Client must have changed its internal data to match the new name expect(client1.name).not.toEqual(client2.name); }); + + test("Client can leave the room", async () => { + const mox = new MockHelper(); + const hook = new EventHook(); + const local = new LocalClient({ name: "server-player" }); + const server = mox.createServer(sampleRoom(), "test-server", local); + const client = mox.createClient(); + hook.hookEmitter(local, "player-joined", "client-joined"); + hook.hookEmitter(local, "player-left", "client-left"); + client.connect("test-server"); + await hook.expect("client-joined", 1000); + client.leave(); + await hook.expect("client-left", 1000); + }); + + test("Clients can chat", async () => { + const mox = new MockHelper(); + const hook = new EventHook(); + const local = new LocalClient({ name: "server-player" }); + const server = mox.createServer(sampleRoom(), "test-server", local); + const client = mox.createClient(); + hook.hookEmitter(local, "player-joined", "client-joined"); + hook.hookEmitter(local, "chat", "client-chat"); + client.connect("test-server"); + await hook.expect("client-joined", 1000); + const hellomsg = "Hello everyone"; + client.say("", hellomsg); + await hook.expect("client-chat", 1000, (msg: ChatMessage) => { + expect(msg.from).toEqual("client-peer"); + expect(msg.message).toEqual(hellomsg); + }); + }); });