Basic networking #9

Merged
hamcha merged 17 commits from feature/basic-networking into master 2019-09-06 12:36:11 +00:00
13 changed files with 375 additions and 17 deletions
Showing only changes of commit ebf846c154 - Show all commits

View file

@ -5,3 +5,5 @@ Work in progress name, work in progress game
## License
Code is ISC, Assets "depends", some stuff is taken from FreeSound and DeviantArt so your best bet is to just praise the copyright gods.
PeerJS mocking is based on works by Rolf Erik Lekang, which is licensed under MIT. Check [peerjs-mock](https://github.com/relekang/peerjs-mock) for more details.

View file

@ -12,6 +12,7 @@
"buefy": "^0.8.2",
"core-js": "^2.6.5",
"dexie": "^2.0.4",
"eventemitter3": "^4.0.0",
"peerjs": "^1.0.4",
"register-service-worker": "^1.6.2",
"vue": "^2.6.10",

View file

@ -1,21 +1,26 @@
import NetworkPeer from "./peer";
import { DataConnection } from "peerjs";
import Peer, { DataConnection } from "peerjs";
import { PeerMetadata, NetworkMessage } from "./types";
export default class PeerClient extends NetworkPeer {
export class PeerClient extends NetworkPeer {
private connection: DataConnection;
public constructor(peerid: string, metadata: PeerMetadata) {
super();
public constructor(
peerid: string,
metadata: PeerMetadata,
customPeer?: Peer
) {
super(customPeer);
this.connection = this.peer.connect(peerid, {
label: "server",
metadata,
reliable: true
});
this.connection.on("open", () => {
console.info("Connected to server");
});
this.connection.on("data", this._received);
this.connection.on("data", this._received.bind(this));
}
private _received(data: NetworkMessage) {}
}
export default PeerClient;

5
src/network/index.ts Normal file
View file

@ -0,0 +1,5 @@
export * from "./client";
export * from "./local";
export * from "./peer";
export * from "./server";
export * from "./types";

View file

@ -1,6 +1,6 @@
import { PeerMetadata, NetworkMessage } from "./types";
export default class LocalClient {
export class LocalClient {
public metadata: PeerMetadata;
public receiver!: (data: NetworkMessage) => void;
@ -16,3 +16,5 @@ export default class LocalClient {
this.receiver(data);
}
}
export default LocalClient;

View file

@ -1,10 +1,10 @@
import Peer, { DataConnection } from "peerjs";
import { NetworkMessage } from "./types";
export default class NetworkPeer {
export class NetworkPeer {
protected peer: Peer;
protected constructor() {
this.peer = new Peer();
protected constructor(peer?: Peer) {
this.peer = peer ? peer : new Peer();
this.peer.on("open", function(id) {
console.info("Peer ID assigned: %s", id);
});
@ -15,3 +15,5 @@ export default class NetworkPeer {
conn.send(data);
}
}
export default NetworkPeer;

View file

@ -1,5 +1,5 @@
import NetworkPeer from "./peer";
import { DataConnection } from "peerjs";
import Peer, { DataConnection } from "peerjs";
import {
RoomInfo,
PasswordRequest,
@ -33,11 +33,15 @@ function nextName(name: string): string {
return name.substr(0, i) + (Number(name.slice(i)) + 1);
}
export default class PeerServer extends NetworkPeer {
export class PeerServer extends NetworkPeer {
private room: Room;
public constructor(roomInfo: RoomInfo, local: LocalClient) {
super();
public constructor(
roomInfo: RoomInfo,
local: LocalClient,
customPeer?: Peer
) {
super(customPeer);
let players: Record<string, Player> = {};
// Add local player to server
@ -52,7 +56,7 @@ export default class PeerServer extends NetworkPeer {
info: roomInfo,
players
};
this.peer.on("connection", this._connection);
this.peer.on("connection", this._connection.bind(this));
}
private _connection(conn: DataConnection) {
@ -105,7 +109,7 @@ export default class PeerServer extends NetworkPeer {
};
this.send<PasswordRequest>(conn, { kind: "password-req" });
conn.on("data", checkPasswordResponse);
conn.on("data", checkPasswordResponse.bind(this));
return;
}
@ -184,3 +188,5 @@ export default class PeerServer extends NetworkPeer {
}
}
}
export default PeerServer;

View file

@ -27,7 +27,7 @@ export interface Room {
export interface RoomInfo {
max_players: number;
password: string;
game: GameInfo;
game?: GameInfo;
}
type GameInfo = MatchInfo | DraftInfo;

View file

@ -0,0 +1,219 @@
import EventEmitter from "eventemitter3";
import LocalPeer from "./LocalPeer";
import { PeerConnectOption } from "peerjs";
export default class LocalDataConnection extends EventEmitter {
public options: PeerConnectOption;
public peer: string;
public provider: LocalPeer;
public open: boolean = false;
private _connection?: LocalDataConnection;
public id: string;
public label: string;
public constructor(
peer: string,
provider: LocalPeer,
options?: PeerConnectOption
) {
super();
this.options = {
serialization: "binary",
reliable: false
};
if (options) {
this.options = Object.assign(this.options, options);
}
this.id = "fake_" + Math.random().toString(32);
this.label = this.options.label ? this.options.label : this.id;
this.provider = provider;
this.peer = peer;
}
public send(message: any) {
if (!this.open) {
this.emit(
"error",
new Error(
"Connection is not open. You should listen for the `open` event before sending messages."
)
);
}
const connection = this._connection;
if (connection) {
connection.receive(message);
}
}
public receive(message: any) {
this.emit("data", message);
}
public close() {
if (!this.open) {
return;
}
this.open = false;
this.emit("close");
}
public _setRemote(connection: LocalDataConnection) {
this._connection = connection;
this._connection.on("open", () => {
if (!this.open) {
this.open = true;
this.emit("open");
}
});
}
public _configureDataChannel(
mocks: Record<string, LocalPeer>,
options?: PeerConnectOption
) {
if (!this._connection && this.peer in mocks) {
this._setRemote(
mocks[this.peer]._negotiate(this.provider.id, this, options)
);
this.open = true;
this.emit("open");
} else {
throw new Error(`Peer(${this.peer}) not found`);
}
}
public get metadata() {
return this.options.metadata;
}
public get reliable() {
return this.options.reliable ? this.options.reliable : false;
}
public get serialization() {
return this.options.serialization ? this.options.serialization : "binary";
}
public get peerConnection() {
return this._connection;
}
/*
UNIMPLEMENTED STUFF
*/
public dataChannel: unknown;
public bufferSize: unknown;
public type: unknown;
public canTrickleIceCandidates: unknown;
public connectionState: unknown;
public currentLocalDescription: unknown;
public currentRemoteDescription: unknown;
public iceConnectionState: unknown;
public iceGatheringState: unknown;
public idpErrorInfo: unknown;
public idpLoginUrl: unknown;
public localDescription: unknown;
public onconnectionstatechange: unknown;
public ondatachannel: unknown;
public onicecandidate: unknown;
public onicecandidateerror: unknown;
public oniceconnectionstatechange: unknown;
public onicegatheringstatechange: unknown;
public onnegotiationneeded: unknown;
public onsignalingstatechange: unknown;
public onstatsended: unknown;
public ontrack: unknown;
public peerIdentity: unknown;
public pendingLocalDescription: unknown;
public pendingRemoteDescription: unknown;
public remoteDescription: unknown;
public sctp: unknown;
public signalingState: unknown;
public addIceCandidate(
candidate: RTCIceCandidateInit | RTCIceCandidate
): Promise<void> {
throw new Error("not implemented");
}
public addTrack(track: MediaStreamTrack, ...streams: MediaStream[]): unknown {
throw new Error("not implemented");
}
public addTransceiver(
trackOrKind: MediaStreamTrack | string,
init?: RTCRtpTransceiverInit
): unknown {
throw new Error("not implemented");
}
public createAnswer(options?: RTCOfferOptions): Promise<unknown> {
throw new Error("not implemented");
}
public createDataChannel(
label: string,
dataChannelDict?: RTCDataChannelInit
): unknown {
throw new Error("not implemented");
}
public createOffer(options?: RTCOfferOptions): Promise<unknown> {
throw new Error("not implemented");
}
public getConfiguration(): unknown {
throw new Error("not implemented");
}
public getIdentityAssertion(): Promise<string> {
throw new Error("not implemented");
}
public getReceivers(): unknown[] {
throw new Error("not implemented");
}
public getSenders(): unknown[] {
throw new Error("not implemented");
}
public getStats(selector?: MediaStreamTrack | null): Promise<RTCStatsReport> {
throw new Error("not implemented");
}
public getTransceivers(): unknown[] {
throw new Error("not implemented");
}
public removeTrack(sender: RTCRtpSender): void {
throw new Error("not implemented");
}
public setConfiguration(configuration: RTCConfiguration): void {
throw new Error("not implemented");
}
public setIdentityProvider(
provider: string,
options?: RTCIdentityProviderOptions
) {
throw new Error("not implemented");
}
public setLocalDescription(
description: RTCSessionDescriptionInit
): Promise<void> {
throw new Error("not implemented");
}
public setRemoteDescription(
description: RTCSessionDescriptionInit
): Promise<void> {
throw new Error("not implemented");
}
addEventListener(
type: string,
listener: EventListenerOrEventListenerObject,
options?: boolean | AddEventListenerOptions
): void {
throw new Error("not implemented");
}
removeEventListener(
type: string,
listener: EventListenerOrEventListenerObject,
options?: boolean | EventListenerOptions
): void {
throw new Error("not implemented");
}
dispatchEvent(event: Event): boolean {
throw new Error("not implemented");
}
}

88
src/testing/LocalPeer.ts Normal file
View file

@ -0,0 +1,88 @@
import EventEmitter from "eventemitter3";
import Peer, { PeerConnectOption, PeerJSOption } from "peerjs";
import LocalDataConnection from "./LocalDataConnection";
let mocks: Record<string, LocalPeer> = {};
export default class LocalPeer extends EventEmitter {
public connections: Record<string, LocalDataConnection[]>;
public id: string;
public options: PeerJSOption;
public disconnected: boolean;
public destroyed: boolean;
public prototype: unknown;
public constructor(id: string, options?: PeerJSOption) {
super();
this.id = id;
this.options = options ? options : {};
this.connections = {};
this.disconnected = false;
this.destroyed = false;
mocks[id] = this;
}
public connect(id: string, options?: PeerConnectOption): LocalDataConnection {
let connection = new LocalDataConnection(id, this, options);
connection._configureDataChannel(mocks, options);
this._addConnection(id, connection);
return connection;
}
public disconnect() {
this.disconnected = true;
this.emit("disconnected", this.id);
this.id = "";
}
public destroy() {
this.destroyed = true;
this.emit("destroyed", this.id);
this.disconnect();
}
private _addConnection(peer: string, connection: LocalDataConnection) {
if (!this.connections[peer]) {
this.connections[peer] = [];
}
this.connections[peer].push(connection);
}
public _negotiate(
peer: string,
remoteConnection: LocalDataConnection,
options?: PeerConnectOption
) {
let localConnection = new LocalDataConnection(peer, this, options);
localConnection._setRemote(remoteConnection);
this._addConnection(peer, localConnection);
this.emit("connection", localConnection);
return localConnection;
}
public call(
id: string,
stream: MediaStream,
options?: Peer.CallOption
): Peer.MediaConnection {
throw new Error("unimplemented");
}
public reconnect() {}
public getConnection(
peerId: string,
connectionId: string
): LocalDataConnection | null {
if (peerId in mocks) {
let map = mocks[peerId].connections;
if (connectionId in map) {
return map[connectionId][0];
}
return null;
}
return null;
}
public listAllPeers() {}
}

View file

@ -0,0 +1,23 @@
import LocalPeer from "@/testing/LocalPeer";
import Peer from "peerjs";
import { PeerServer, LocalClient, PeerClient } from "@/network";
describe("network/PeerServer", () => {
test("Create server and client", () => {
const serverPeer = new LocalPeer("test-server", {});
const clientPeer = new LocalPeer("test-client", {});
const serverPlayer = new LocalClient({
name: "server-client"
});
const roomInfo = {
max_players: 3,
password: ""
};
const server = new PeerServer(roomInfo, serverPlayer, serverPeer as Peer);
const client = new PeerClient(
"test-server",
{ name: "test" },
clientPeer as Peer
);
});
});

View file

@ -3889,6 +3889,11 @@ eventemitter3@^3.0.0, eventemitter3@^3.1.2:
resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-3.1.2.tgz#2d3d48f9c346698fce83a85d7d664e98535df6e7"
integrity sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q==
eventemitter3@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.0.tgz#d65176163887ee59f386d64c82610b696a4a74eb"
integrity sha512-qerSRB0p+UDEssxTtm6EDKcE7W4OaoisfIMl4CngyEhjpYglocpNg6UEqCvemdGhosAsg4sO2dXJOdyBifPGCg==
events@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/events/-/events-3.0.0.tgz#9a0a0dfaf62893d92b875b8f2698ca4114973e88"