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 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<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;

View file

@ -34,8 +34,11 @@ export class PeerClient extends Client {
return this.metadata.name;
}
protected send<T extends NetworkMessage>(conn: DataConnection, data: T) {
conn.send(data);
public send<T extends NetworkMessage>(data: T) {
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,
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<ErrorMessage>(conn, { kind: "error", error: "room is full" });
this.send<ErrorMessage>(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<RenameMessage>(conn, {
this.send<RenameMessage>(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<ErrorMessage>(conn, {
this.send<ErrorMessage>(player, {
kind: "error",
error: "invalid password"
});
return;
}
conn.off("data", checkPasswordResponse);
this.addPlayer(conn);
this.addPlayer(player);
} catch (e) {
this.send<ErrorMessage>(conn, {
this.send<ErrorMessage>(player, {
kind: "error",
error: "not a password"
});
}
};
this.send<PasswordRequest>(conn, { kind: "password-req" });
this.send<PasswordRequest>(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<RoomInfoMessage>(conn, {
this.send<RoomInfoMessage>(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<AckMessage>(player.conn, { kind: "ok", what: "leave-req" });
this.send<AckMessage>(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<ChatMessage>(this.players[data.to], data);
} else {
this.send<ErrorMessage>(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<T extends NetworkMessage>(conn: DataConnection, data: T) {
conn.send(data);
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];
if (player.kind == "remote") {
this.send<T>(player.conn, message);
} else {
player.client.receive(message);
}
this.send<T>(player, message);
}
}
}

View file

@ -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;

View file

@ -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);
});
});
});