Basic networking #9
5 changed files with 115 additions and 31 deletions
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue