From 9969561af1323f540e85bf9ed478f5083557f578 Mon Sep 17 00:00:00 2001 From: Hamcha Date: Wed, 16 Oct 2019 08:31:03 +0000 Subject: [PATCH] Add Lobby (#43) --- public/index.html | 36 ++-- src/mlpccg/set.ts | 17 ++ src/network/Client.ts | 19 +- src/network/PeerServer.ts | 74 +++++-- src/router.ts | 22 ++- src/store/network/actions.ts | 42 +++- src/store/network/getters.ts | 39 ++++ src/store/network/index.ts | 6 + src/store/network/mutations.ts | 48 +++-- src/store/network/types.ts | 27 +-- src/views/Lobby.vue | 350 ++++++++++++++++++++++++++++++++- src/views/Room.vue | 14 -- vue.config.js | 9 +- 13 files changed, 595 insertions(+), 108 deletions(-) delete mode 100644 src/views/Room.vue diff --git a/public/index.html b/public/index.html index e4d8640..ce79e8c 100644 --- a/public/index.html +++ b/public/index.html @@ -1,18 +1,22 @@ - - - - - - - mcgvue - - - -
- - - + + + + + + + + MLPCARDGAME + + + + +
+ + + + \ No newline at end of file diff --git a/src/mlpccg/set.ts b/src/mlpccg/set.ts index c554c8f..2435338 100644 --- a/src/mlpccg/set.ts +++ b/src/mlpccg/set.ts @@ -19,6 +19,23 @@ export const allSets = [ "Promo" ]; +export const setNames = { + PR: "Premiere", + CN: "Canterlot Nights", + RR: "Rock and Rave", + CS: "Celestial Solstice", + CG: "Crystal Games", + AD: "Absolute Discord", + EO: "Equestrian Odysseys", + HM: "High Magic", + MT: "Marks In Time", + DE: "Defenders of Equestria", + SB: "Seaquestria and Beyond", + FF: "Friends Forever", + GF: "Promotional", + ST: "Sands of Time" +}; + export async function loadSets() { if (Database == null) { throw new Error("Database was not initialized, init with 'initDB()'"); diff --git a/src/network/Client.ts b/src/network/Client.ts index ad74990..f22e05c 100644 --- a/src/network/Client.ts +++ b/src/network/Client.ts @@ -1,10 +1,7 @@ -import { - PeerMetadata, - NetworkMessage, - PasswordResponse, - RoomInfo -} from "./types"; import EventEmitter from "eventemitter3"; +import Vue from "vue"; + +import { NetworkMessage, PasswordResponse, PeerMetadata, RoomInfo } from "./types"; export abstract class Client extends EventEmitter { public metadata: PeerMetadata; @@ -23,13 +20,19 @@ export abstract class Client extends EventEmitter { case "room-info": this.roomInfo = data.room; this.players = data.players; + this.emit("handshake"); break; // Someone changed name (or was forced to) case "rename": if (data.oldname == this.metadata.name) { // We got a name change! this.metadata.name = data.newname; - } else { + } + + // Only mutate player list if we have one + // This is because rename messages can be received during the initial + // handshake, to signal a forced name change before joining. + if (this.players) { let idx = this.players.indexOf(data.oldname); if (idx < 0) { // Weird @@ -39,7 +42,7 @@ export abstract class Client extends EventEmitter { this.players.push(data.newname); break; } - this.players[idx] = data.newname; + Vue.set(this.players, idx, data.newname); } this.emit("rename", data.oldname, data.newname); break; diff --git a/src/network/PeerServer.ts b/src/network/PeerServer.ts index e88ffbf..099d614 100644 --- a/src/network/PeerServer.ts +++ b/src/network/PeerServer.ts @@ -1,23 +1,24 @@ +import EventEmitter from "eventemitter3"; import Peer, { DataConnection } from "peerjs"; + +import { LocalClient } from "."; import { - RoomInfo, - PasswordRequest, - Room, + AckMessage, + ChatMessage, ErrorMessage, + JoinMessage, + LeaveMessage, + NetworkMessage, + NetworkPlayer, + PasswordRequest, PasswordResponse, PeerMetadata, - JoinMessage, - RoomInfoMessage, Player, - NetworkMessage, RenameMessage, - LeaveMessage, - NetworkPlayer, - AckMessage, - ChatMessage + Room, + RoomInfo, + RoomInfoMessage, } from "./types"; -import { LocalClient } from "."; -import EventEmitter from "eventemitter3"; // Increment name, add number at the end if not present // Examples: @@ -34,6 +35,12 @@ function nextName(name: string): string { return name.substr(0, i) + (Number(name.slice(i)) + 1); } +function connectionOpen(conn: DataConnection): Promise { + return new Promise(resolve => { + conn.on("open", () => resolve()); + }); +} + export class PeerServer extends EventEmitter { protected peer: Peer; private room: Room; @@ -64,17 +71,21 @@ export class PeerServer extends EventEmitter { // Setup peer this.peer = customPeer ? customPeer : new Peer(); - this.peer.on("open", function(id) { + this.peer.on("open", id => { console.info("Peer ID assigned: %s", id); + this.emit("open", id); }); this.peer.on("connection", conn => { this._connection(conn); }); } - private _connection(conn: DataConnection) { + private async _connection(conn: DataConnection) { const metadata = conn.metadata as PeerMetadata; + // Wait for connection to be open + await connectionOpen(conn); + let player: NetworkPlayer = { kind: "remote", name: metadata.name, @@ -140,6 +151,12 @@ export class PeerServer extends EventEmitter { private addPlayer(player: NetworkPlayer) { const playerName = player.name; + + // Hacky: Give player list before this player was added, so join + // message doesn't mess things up later + const players = Object.keys(this.room.players); + + // Add player to room this.room.players[playerName] = player; // Start listening for new messages @@ -148,6 +165,10 @@ export class PeerServer extends EventEmitter { this._received.bind(this, this.room.players[playerName]) ); + player.conn.on("error", err => { + throw err; + }); + // Send the player info about the room this.send(player, { kind: "room-info", @@ -155,7 +176,7 @@ export class PeerServer extends EventEmitter { ...this.room.info, password: "" }, - players: Object.keys(this.room.players) + players }); // Notify other players @@ -172,6 +193,9 @@ export class PeerServer extends EventEmitter { // Close connection with player player.conn.close(); + // Remove player from player list + delete this.room.players[player.name]; + // Notify other players this.broadcast({ kind: "player-left", @@ -189,16 +213,30 @@ export class PeerServer extends EventEmitter { this.broadcast(data); } else { // Player is telling someone specifically - if (data.to in this.players) { - this.send(this.players[data.to], data); - } else { + if (!(data.to in this.players)) { this.send(player, { kind: "error", error: `player not found: ${data.to}` }); + return; } + this.send(this.players[data.to], data); } break; + // Player wants to change name + case "rename": + // Make sure new name is valid + data.oldname = player.name; + if (data.newname in this.players) { + this.send(player, { + kind: "error", + error: "name not available" + }); + return; + } + player.name = data.newname; + this.broadcast(data); + break; // Player is leaving! case "leave-req": // If we're leaving, end the server diff --git a/src/router.ts b/src/router.ts index 98f83a2..05efe11 100644 --- a/src/router.ts +++ b/src/router.ts @@ -1,12 +1,11 @@ +import DeckBuilder from "@/views/DeckBuilder.vue"; +import DraftView from "@/views/Draft.vue"; +import GameView from "@/views/Game.vue"; +import Home from "@/views/Home.vue"; +import Lobby from "@/views/Lobby.vue"; +import SettingsView from "@/views/Settings.vue"; import Vue from "vue"; import Router from "vue-router"; -import Home from "@/views/Home.vue"; -import DeckBuilder from "@/views/DeckBuilder.vue"; -import GameView from "@/views/Game.vue"; -import DraftView from "@/views/Draft.vue"; -import Lobby from "@/views/Lobby.vue"; -import RoomView from "@/views/Room.vue"; -import SettingsView from "@/views/Settings.vue"; Vue.use(Router); @@ -46,9 +45,12 @@ export default new Router({ } }, { - path: "/room", - name: "room", - component: RoomView + path: "/join/:id", + name: "lobby-join", + component: Lobby, + meta: { + topnav: "Lobby" + } }, { path: "/settings", diff --git a/src/store/network/actions.ts b/src/store/network/actions.ts index dfc9f4b..ac38a3d 100644 --- a/src/store/network/actions.ts +++ b/src/store/network/actions.ts @@ -1,28 +1,58 @@ -import { ActionTree } from "vuex"; +import { ChatMessage, Client, LocalClient, NetworkMessage, PeerClient, PeerServer } from "@/network"; +import { ActionTree, Commit } from "vuex"; + import { AppState } from "../types"; -import { NetworkState, StartServerOptions, ConnectOptions } from "./types"; -import { PeerServer, LocalClient, PeerClient } from "@/network"; +import { ConnectOptions, NetworkState, StartServerOptions } from "./types"; + +function bindClientEvents(commit: Commit, client: Client) { + client.on("handshake", () => { + commit("playerListChanged", client.players); + }); + client.on("player-joined", () => commit("playerListChanged", client.players)); + client.on("player-left", () => commit("playerListChanged", client.players)); + client.on("rename", () => commit("playerListChanged", client.players)); +} const actions: ActionTree = { startServer({ commit }, options: StartServerOptions) { const local = new LocalClient(options.playerInfo); const server = new PeerServer(options.roomInfo, local, options._customPeer); + server.once("open", id => { + commit("serverAssignedID", id); + }); + bindClientEvents(commit, local); commit("becomeServer", { local, server }); }, connect({ commit }, options: ConnectOptions) { const client = new PeerClient(options.playerInfo, options._customPeer); - commit("becomeClient", { peer: client }); + commit("becomeClient", { peer: client, id: options.serverID }); client.on("connected", () => { - commit("connected"); + commit("connectionStatusChanged", "connected"); }); client.on("disconnected", () => { - commit("disconnected"); + commit("connectionStatusChanged", "disconnected"); }); client.on("error", err => { commit("connectionError", err); }); + bindClientEvents(commit, client); client.connect(options.serverID); + }, + + sendChatMessage({ commit, dispatch, getters }, message: ChatMessage) { + if (getters.connectionType == "none") { + throw new Error("not connected"); + } + dispatch("sendMessage", message); + commit("receivedChatMessage", message); + }, + + sendMessage({ getters }, message: NetworkMessage) { + if (getters.connectionType == "none") { + throw new Error("not connected"); + } + (getters.client as Client).send(message); } }; diff --git a/src/store/network/getters.ts b/src/store/network/getters.ts index 3840b22..76f509c 100644 --- a/src/store/network/getters.ts +++ b/src/store/network/getters.ts @@ -1,4 +1,6 @@ +import { Client } from "@/network"; import { GetterTree } from "vuex"; + import { AppState } from "../types"; import { NetworkState } from "./types"; @@ -13,8 +15,45 @@ const getters: GetterTree = { return null; }, + sessionID(state): string | null { + return state.serverID; + }, + + client(state): Client | null { + switch (state.peerType) { + case "server": + return state.local; + case "client": + return state.peer; + } + return null; + }, + connectionType(state): "client" | "server" | "none" { return state.peerType; + }, + + busy(state): boolean { + if (state.peerType == "client") { + if (state.connectionStatus == "connecting") { + return true; + } + } + return false; + }, + + inRoom(state): boolean { + if (state.peerType == "client") { + return state.connectionStatus == "connected"; + } + if (state.peerType == "server") { + return true; + } + return false; + }, + + players(state): string[] { + return state.players; } }; diff --git a/src/store/network/index.ts b/src/store/network/index.ts index 9a0ca12..3f6e3ef 100644 --- a/src/store/network/index.ts +++ b/src/store/network/index.ts @@ -10,6 +10,12 @@ const namespaced = true; export const state: NetworkState = { peerType: "none", + connectionStatus: null, + peer: null, + server: null, + local: null, + serverID: null, + players: [], chatLog: [] }; diff --git a/src/store/network/mutations.ts b/src/store/network/mutations.ts index 5a0d135..6832f2c 100644 --- a/src/store/network/mutations.ts +++ b/src/store/network/mutations.ts @@ -1,37 +1,43 @@ +import { ChatMessage, LocalClient, PeerClient, PeerServer } from "@/network"; +import Vue from "vue"; import { MutationTree } from "vuex"; -import { NetworkState, ServerNetworkState, ClientNetworkState } from "./types"; -import { LocalClient, PeerServer, PeerClient } from "@/network"; + +import { ClientNetworkState, ConnectionStatus, NetworkState, ServerNetworkState } from "./types"; const mutations: MutationTree = { becomeServer(state, payload: { local: LocalClient; server: PeerServer }) { - state = { - ...state, - peerType: "server", - local: payload.local, - server: payload.server - }; + state.peerType = "server"; + state.players = [payload.local.name]; + (state as ServerNetworkState).local = payload.local; + (state as ServerNetworkState).server = payload.server; }, - becomeClient(state, payload: { peer: PeerClient }) { - state = { - ...state, - connectionStatus: "connecting", - peerType: "client", - peer: payload.peer - }; + becomeClient(state, payload: { peer: PeerClient; id: string }) { + state.peerType = "client"; + (state as ClientNetworkState).connectionStatus = "connecting"; + (state as ClientNetworkState).peer = payload.peer; + (state as ClientNetworkState).serverID = payload.id; }, - connected(state) { - (state as ClientNetworkState).connectionStatus = "connected"; - }, - - disconnected(state) { - (state as ClientNetworkState).connectionStatus = "disconnected"; + connectionStatusChanged(state, status: ConnectionStatus) { + (state as ClientNetworkState).connectionStatus = status; }, connectionError(state, error) { (state as ClientNetworkState).connectionStatus = "error"; (state as ClientNetworkState).connectionError = error; + }, + + receivedChatMessage(state, message: ChatMessage) { + state.chatLog.push(message); + }, + + serverAssignedID(state, id: string) { + state.serverID = id; + }, + + playerListChanged(state, players: string[]) { + Vue.set(state, "players", players); } }; diff --git a/src/store/network/types.ts b/src/store/network/types.ts index 4c8e555..e810edf 100644 --- a/src/store/network/types.ts +++ b/src/store/network/types.ts @@ -1,29 +1,30 @@ -import { - PeerClient, - PeerServer, - LocalClient, - RoomInfo, - PeerMetadata -} from "@/network"; +import { ChatMessage, LocalClient, PeerClient, PeerMetadata, PeerServer, RoomInfo } from "@/network"; import Peer from "peerjs"; -export interface ChatMessage { - who: string; - to: string; - message: string; -} +export type ConnectionStatus = + | "connecting" + | "connected" + | "disconnected" + | "error"; export interface SharedNetworkState { chatLog: ChatMessage[]; + serverID: string | null; + players: string[]; } export interface NoNetworkState extends SharedNetworkState { peerType: "none"; + connectionStatus: null; + connectionError?: Error; + peer: null; + server: null; + local: null; } export interface ClientNetworkState extends SharedNetworkState { peerType: "client"; - connectionStatus: "connecting" | "connected" | "disconnected" | "error"; + connectionStatus: ConnectionStatus; connectionError?: Error; peer: PeerClient; } diff --git a/src/views/Lobby.vue b/src/views/Lobby.vue index 041677a..1750002 100644 --- a/src/views/Lobby.vue +++ b/src/views/Lobby.vue @@ -1,26 +1,374 @@ diff --git a/src/views/Room.vue b/src/views/Room.vue deleted file mode 100644 index 0fe4cae..0000000 --- a/src/views/Room.vue +++ /dev/null @@ -1,14 +0,0 @@ - - - - - diff --git a/vue.config.js b/vue.config.js index 002334f..a07f4bb 100644 --- a/vue.config.js +++ b/vue.config.js @@ -1,5 +1,12 @@ module.exports = { - publicPath: process.env.SUBPATH ? process.env.SUBPATH : "", + configureWebpack: { + devServer: { + disableHostCheck: true, + host: "0.0.0.0" + } + }, + + publicPath: process.env.SUBPATH ? process.env.SUBPATH : "/", pluginOptions: { gitDescribe: {