Add chatting, leaving
This commit is contained in:
parent
942aef279d
commit
dd37f3233c
5 changed files with 115 additions and 31 deletions
|
@ -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;
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in a new issue