Add chatting, leaving

This commit is contained in:
Hamcha 2019-09-06 14:06:19 +02:00
parent 942aef279d
commit dd37f3233c
Signed by: hamcha
GPG key ID: 44AD3571EB09A39E
5 changed files with 115 additions and 31 deletions

View file

@ -1,7 +1,7 @@
import { PeerMetadata, NetworkMessage } from "./types"; import { PeerMetadata, NetworkMessage } from "./types";
import EventEmitter from "eventemitter3"; import EventEmitter from "eventemitter3";
export class Client extends EventEmitter { export abstract class Client extends EventEmitter {
public metadata: PeerMetadata; public metadata: PeerMetadata;
public players: string[]; public players: string[];
@ -47,6 +47,23 @@ export class Client extends EventEmitter {
public get name(): string { public get name(): string {
return this.metadata.name; return this.metadata.name;
} }
public abstract send<T extends NetworkMessage>(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; export default Client;

View file

@ -34,8 +34,11 @@ export class PeerClient extends Client {
return this.metadata.name; return this.metadata.name;
} }
protected send<T extends NetworkMessage>(conn: DataConnection, data: T) { public send<T extends NetworkMessage>(data: T) {
conn.send(data); if (!this.connection) {
throw new Error("Client is not connected to a server");
}
this.connection.send(data);
} }
} }

View file

@ -13,7 +13,8 @@ import {
RenameMessage, RenameMessage,
LeaveMessage, LeaveMessage,
NetworkPlayer, NetworkPlayer,
AckMessage AckMessage,
ChatMessage
} from "./types"; } from "./types";
import { LocalClient } from "."; import { LocalClient } from ".";
import EventEmitter from "eventemitter3"; import EventEmitter from "eventemitter3";
@ -70,7 +71,12 @@ export class PeerServer extends EventEmitter {
private _connection(conn: DataConnection) { private _connection(conn: DataConnection) {
const metadata = conn.metadata as PeerMetadata; 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 // Check if this connection should be allowed
@ -78,7 +84,7 @@ export class PeerServer extends EventEmitter {
// Check if room is full // Check if room is full
if (this.playerCount >= this.room.info.max_players) { if (this.playerCount >= this.room.info.max_players) {
this.send<ErrorMessage>(conn, { kind: "error", error: "room is full" }); this.send<ErrorMessage>(player, { kind: "error", error: "room is full" });
conn.close(); conn.close();
return; return;
} }
@ -90,12 +96,13 @@ export class PeerServer extends EventEmitter {
while (this.room.players[newname]) { while (this.room.players[newname]) {
newname = nextName(metadata.name); newname = nextName(metadata.name);
} }
this.send<RenameMessage>(conn, { this.send<RenameMessage>(player, {
kind: "rename", kind: "rename",
oldname: metadata.name, oldname: metadata.name,
newname: newname newname: newname
}); });
metadata.name = newname; player.name = newname;
player.conn.metadata.name = newname;
} }
// Check for password // Check for password
@ -104,43 +111,42 @@ export class PeerServer extends EventEmitter {
try { try {
let resp = data as PasswordResponse; let resp = data as PasswordResponse;
if (resp.password != this.room.info.password) { if (resp.password != this.room.info.password) {
this.send<ErrorMessage>(conn, { this.send<ErrorMessage>(player, {
kind: "error", kind: "error",
error: "invalid password" error: "invalid password"
}); });
return; return;
} }
conn.off("data", checkPasswordResponse); conn.off("data", checkPasswordResponse);
this.addPlayer(conn); this.addPlayer(player);
} catch (e) { } catch (e) {
this.send<ErrorMessage>(conn, { this.send<ErrorMessage>(player, {
kind: "error", kind: "error",
error: "not a password" error: "not a password"
}); });
} }
}; };
this.send<PasswordRequest>(conn, { kind: "password-req" }); this.send<PasswordRequest>(player, { kind: "password-req" });
conn.on("data", checkPasswordResponse.bind(this)); conn.on("data", checkPasswordResponse.bind(this));
return; return;
} }
this.addPlayer(conn); this.addPlayer(player);
} }
private addPlayer(conn: DataConnection) { private addPlayer(player: NetworkPlayer) {
const playerName = conn.metadata.name; const playerName = player.name;
this.room.players[playerName] = { this.room.players[playerName] = player;
kind: "remote",
name: conn.metadata.name,
conn: conn
};
// Start listening for new messages // 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 // Send the player info about the room
this.send<RoomInfoMessage>(conn, { this.send<RoomInfoMessage>(player, {
kind: "room-info", kind: "room-info",
room: { room: {
...this.room.info, ...this.room.info,
@ -158,7 +164,7 @@ export class PeerServer extends EventEmitter {
private removePlayer(player: NetworkPlayer) { private removePlayer(player: NetworkPlayer) {
// Tell the player everything's fine // Tell the player everything's fine
this.send<AckMessage>(player.conn, { kind: "ok", what: "leave-req" }); this.send<AckMessage>(player, { kind: "ok", what: "leave-req" });
// Close connection with player // Close connection with player
player.conn.close(); player.conn.close();
@ -172,6 +178,24 @@ export class PeerServer extends EventEmitter {
private _received(player: Player, data: NetworkMessage) { private _received(player: Player, data: NetworkMessage) {
switch (data.kind) { 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<ChatMessage>(this.players[data.to], data);
} else {
this.send<ErrorMessage>(player, {
kind: "error",
error: `player not found: ${data.to}`
});
}
}
break;
// Player is leaving! // Player is leaving!
case "leave-req": case "leave-req":
// If we're leaving, end the server // If we're leaving, end the server
@ -193,18 +217,18 @@ export class PeerServer extends EventEmitter {
return this.room.players; return this.room.players;
} }
protected send<T extends NetworkMessage>(conn: DataConnection, data: T) { protected send<T extends NetworkMessage>(player: Player, message: T) {
conn.send(data); if (player.kind == "remote") {
player.conn.send(message);
} else {
player.client.receive(message);
}
} }
private broadcast<T extends NetworkMessage>(message: T) { private broadcast<T extends NetworkMessage>(message: T) {
for (const playerName in this.room.players) { for (const playerName in this.room.players) {
const player = this.room.players[playerName]; const player = this.room.players[playerName];
if (player.kind == "remote") { this.send<T>(player, message);
this.send<T>(player.conn, message);
} else {
player.client.receive(message);
}
} }
} }
} }

View file

@ -69,6 +69,7 @@ export type NetworkMessage =
| JoinMessage | JoinMessage
| LeaveMessage | LeaveMessage
| RenameMessage | RenameMessage
| ChatMessage
| AckMessage; | AckMessage;
export interface PasswordRequest { export interface PasswordRequest {
@ -106,6 +107,13 @@ export interface RenameMessage {
newname: string; newname: string;
} }
export interface ChatMessage {
kind: "chat";
from: string;
to: string; // "" means everyone
message: string;
}
export interface AckMessage { export interface AckMessage {
kind: "ok"; kind: "ok";
what: string; what: string;

View file

@ -1,5 +1,5 @@
import { MockHelper } from "@/testing"; import { MockHelper } from "@/testing";
import { NetworkMessage, LocalClient } from "@/network"; import { NetworkMessage, LocalClient, ChatMessage } from "@/network";
import { EventHook } from "@/testing/EventHook"; import { EventHook } from "@/testing/EventHook";
const sampleRoom = () => ({ const sampleRoom = () => ({
@ -74,4 +74,36 @@ describe("network/PeerClient", () => {
// Client must have changed its internal data to match the new name // Client must have changed its internal data to match the new name
expect(client1.name).not.toEqual(client2.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);
});
});
}); });