diff --git a/client/src/index.ts b/client/src/index.ts index a839e8cc..0ad580d4 100644 --- a/client/src/index.ts +++ b/client/src/index.ts @@ -1,8 +1,16 @@ -import "./event_types"; - export { Address } from './networking/EventWire'; export { BotRunner, BotConfig } from './planetwars/BotRunner'; export { PwClient } from './planetwars/PwClient'; +export { Replayer } from "./replay"; +export { Reactor } from "./reactors/Reactor"; +export { ServerRunner, ServerParams } from "./planetwars/ServerRunner"; +export { Logger } from "./Logger"; +export { Event, SimpleEventEmitter } from "./reactors/SimpleEventEmitter" +export { PwMatch } from "./planetwars/PwMatch" +export { Client } from "./networking/Client" +import * as events from "./eventTypes"; import * as PwTypes from './planetwars/PwTypes'; + +export { events }; export { PwTypes }; diff --git a/client/src/replay.ts b/client/src/replay.ts index 4e53f1a2..bdff52e3 100644 --- a/client/src/replay.ts +++ b/client/src/replay.ts @@ -1,7 +1,8 @@ -import { createReadStream } from "fs"; +import { createReadStream, ReadStream } from "fs"; import { ProtobufReader } from "./networking/ProtobufStream"; import * as protocol_root from './proto'; import { SimpleEventEmitter, EventType } from "./reactors/SimpleEventEmitter"; +import { SimpleEventDispatcher } from 'ste-simple-events'; import LogEvent = protocol_root.mozaic.log.LogEvent; import * as events from './eventTypes'; import { ISimpleEvent } from "ste-simple-events"; @@ -12,6 +13,7 @@ import { ISimpleEvent } from "ste-simple-events"; export class Replayer { emitters: {[clientId: number]: SimpleEventEmitter}; + public clientSpottedDispatcher: SimpleEventDispatcher = new SimpleEventDispatcher(); constructor() { this.emitters = {}; @@ -22,6 +24,7 @@ export class Replayer { if (!emitter) { emitter = new SimpleEventEmitter(); this.emitters[clientId] = emitter; + this.clientSpottedDispatcher.dispatch(clientId); } return emitter; } @@ -31,17 +34,19 @@ export class Replayer { } private emit(logEvent: LogEvent) { - const emitter = this.emitters[logEvent.clientId]; - if (emitter) { - emitter.handleWireEvent({ - typeId: logEvent.eventType, - data: logEvent.data, - }); - } + const emitter = this.clientStream(logEvent.clientId); + emitter.handleWireEvent({ + typeId: logEvent.eventType, + data: logEvent.data, + }); } public replayFile(path: string) { const logStream = createReadStream(path); + this.replayReadStream(logStream); + } + + public replayReadStream(logStream: ReadStream) { const messageStream = logStream.pipe(new ProtobufReader()); messageStream.on('data', (bytes: Uint8Array) => { @@ -49,15 +54,4 @@ export class Replayer { this.emit(logEvent); }); } -} - -const replayer = new Replayer(); - -// just print all events for now -Object.keys(events).forEach((eventName) => { - replayer.on(events[eventName]).subscribe((event) => { - console.log(event); - }); -}); - -replayer.replayFile('log.out'); +} \ No newline at end of file diff --git a/planetwars/client/app/actions/matches/hosting.ts b/planetwars/client/app/actions/matches/hosting.ts index e2f7b723..dbf31132 100644 --- a/planetwars/client/app/actions/matches/hosting.ts +++ b/planetwars/client/app/actions/matches/hosting.ts @@ -9,4 +9,4 @@ export const changeBotSlot = actionCreator('CHANGE_BOT_SLOT'); export const selectMap = actionCreator('SELECT_MAP'); export const playerConnected = actionCreator('PLAYER_CONNECT'); export const playerDisconnected = actionCreator('PLAYER_DISCONNECT'); -export const serverStarted = actionCreator('SERVER_STARTED'); +export const serverStarted = actionCreator('SERVER_STARTED'); diff --git a/planetwars/client/app/actions/matches/matches.ts b/planetwars/client/app/actions/matches/matches.ts index 47dff0b0..de0561f2 100644 --- a/planetwars/client/app/actions/matches/matches.ts +++ b/planetwars/client/app/actions/matches/matches.ts @@ -11,6 +11,7 @@ import { Config } from '../../utils/Config'; import * as Notify from '../notifications'; import * as Host from './hosting'; import { actionCreator } from '../helpers'; +import { createWriteStream } from 'fs'; export const importMatchFromDB = actionCreator('IMPORT_MATCH_FROM_DB'); export const importMatchError = actionCreator('IMPORT_MATCH_ERROR'); @@ -37,7 +38,7 @@ function createHostedMatch(params: M.MatchParams): M.HostedMatch { return match; } -export function joinMatch(host: M.Address, bot: M.InternalBotSlot) { +export function joinMatch(address: M.Address, bot: M.InternalBotSlot) { return (dispatch: any, getState: any) => { const state: GState = getState(); @@ -48,7 +49,7 @@ export function joinMatch(host: M.Address, bot: M.InternalBotSlot) { type: M.MatchType.joined, status: M.MatchStatus.playing, timestamp: new Date(), - network: host, + network: address, logPath: Config.matchLogPath(matchId), bot, }; @@ -57,36 +58,36 @@ export function joinMatch(host: M.Address, bot: M.InternalBotSlot) { const [command, ...args] = stringArgv(state.bots[bot.botId].command); const botConfig = { command, args }; - const address = host; - const number = 1; - const token = new Buffer(bot.token, 'hex'); - const connectionData = { token, address: host }; - const logger = new PwClient.Logger(match.logPath); - const clientParams = { token, address, logger, number, botConfig }; - - PwClient.Client.connect({ - host: address.host, - port: address.port, - token: new Buffer(bot.token, 'hex'), - logger: new PwClient.Logger(match.logPath), - }).then((client) => { - const pwClient = new PwClient.PwClient(client, botConfig); - pwClient.onExit.subscribe(() => { + const host = address.host; + const port = address.port; + const token = Buffer.from(bot.token, 'hex'); + const clientParams = { + token, + host, + port, + botConfig, + clientId: bot.clientid, + logSink: createWriteStream(match.logPath), + }; + try { + const pwClient = new PwClient.PwClient(clientParams); + pwClient.on(PwClient.events.GameFinished).subscribe(() => { dispatch(completeMatch(match.uuid)); const title = 'Match ended'; const body = `A remote match has ended`; const link = `/matches/${match.uuid}`; dispatch(Notify.addNotification({ title, body, link, type: 'Finished' })); }); - }).catch((error) => { - console.log(error); - dispatch(handleMatchError(match.uuid, error)); + pwClient.run(); + } catch (err) { + console.log(err); + dispatch(handleMatchError(match.uuid, err)); const title = 'Match errored'; const body = `A remote match on map has errored`; const link = `/matches/${match.uuid}`; dispatch(Notify.addNotification({ title, body, link, type: 'Error' })); - }); - }; + } + } } // https://github.com/ZeusWPI/MOZAIC/blob/1f9ab238e96028e3306bfe6b27920f70f9fba430/client/src/test.ts#L38 @@ -97,13 +98,13 @@ export function sendGo() { if (!matchParams) { throw Error('Under construction'); } const config = { - max_turns: matchParams.maxTurns, - map_file: state.maps[matchParams.map].mapPath, + maxTurns: matchParams.maxTurns, + mapPath: state.maps[matchParams.map].mapPath, }; if (runner) { console.log("running"); - runner.matchControl.startGame(config); + runner.dispatch(PwClient.events.StartGame.create(config)); } }; } @@ -140,7 +141,7 @@ export function runMatch() { }; }); - const config: PwClient.MatchParams = { + const config: PwClient.ServerParams = { address: params.address, logFile: match.logPath, ctrl_token: params.ctrl_token, @@ -148,31 +149,36 @@ export function runMatch() { console.log("This probably doesn't work!!!!"); - PwClient.MatchRunner.create(Config.matchRunner, config).then((runner) => { + try { + const server = new PwClient.ServerRunner(Config.matchRunner, config); + server.runServer(); + + const runner = new PwClient.Reactor(new PwClient.Logger(0, createWriteStream(match.logPath))); + + dispatch(Host.serverStarted(runner)); - runner.matchControl.onPlayerConnected.subscribe((clientId) => { - dispatch(Host.playerConnected(players[clientId - 1].token)); + runner.on(PwClient.events.ClientConnected).subscribe((event) => { + dispatch(Host.playerConnected(players[event.clientId - 1].token)); }); - runner.matchControl.onPlayerDisconnected.subscribe((clientId) => { - dispatch(Host.playerDisconnected(players[clientId - 1].token)); + runner.on(PwClient.events.ClientDisconnected).subscribe((event) => { + dispatch(Host.playerDisconnected(players[event.clientId - 1].token)); }); - runner.onComplete.subscribe(() => { + server.onExit.subscribe(() => { dispatch(completeMatch(match.uuid)); const title = 'Match ended'; const body = `A match on map '${state.maps[params.map].name}' has ended`; const link = `/matches/${match.uuid}`; dispatch(Notify.addNotification({ title, body, link, type: 'Finished' })); }); - }) - .catch((error) => { + } catch (error) { dispatch(handleMatchError(match.uuid, error)); const title = 'Match errored'; const body = `A match on map '${state.maps[params.map].name}' has errored`; const link = `/matches/${match.uuid}`; dispatch(Notify.addNotification({ title, body, link, type: 'Error' })); - }); - }; + } + } } export function completeMatch(matchId: M.MatchId) { diff --git a/planetwars/client/app/components/join/Join.tsx b/planetwars/client/app/components/join/Join.tsx index 52004581..e1c6bd52 100644 --- a/planetwars/client/app/components/join/Join.tsx +++ b/planetwars/client/app/components/join/Join.tsx @@ -24,6 +24,7 @@ export interface ImportCopy { name: string; host: string; port: number; + clientId: number; } export interface JoinState { @@ -32,6 +33,7 @@ export interface JoinState { token: string; lastClipboard: string; import?: ImportCopy; + clientId?: number; } export class Join extends React.Component { @@ -47,6 +49,7 @@ export class Join extends React.Component { }; this.setAddress = this.setAddress.bind(this); this.setToken = this.setToken.bind(this); + this.setClientid = this.setClientid.bind(this); this.setBotId = this.setBotId.bind(this); this.joinGame = this.joinGame.bind(this); } @@ -63,6 +66,10 @@ export class Join extends React.Component { + + + + { Import "{this.state.import.name}" from clipboard - ): + ) : undefined } @@ -95,17 +102,17 @@ export class Join extends React.Component { private importConfig = () => { if (!this.state.import) { return; } - const {host, port, name, token} = this.state.import; - this.setState({ address: {host, port}, token }); + const {host, port, name, token, clientId } = this.state.import; + this.setState({ address: {host, port}, token, clientId }); } private checkClipboard = () => { const clipBoardtext = clipboard.readText(); if (this.state.lastClipboard !== clipBoardtext) { try { - const {host, port, name, token} = JSON.parse(clipBoardtext); - if (host && port && name && token) { - this.setState({ import: { host, port, name, token } }); + const {host, port, name, token, clientId } = JSON.parse(clipBoardtext); + if (host && port && name && token && clientId) { + this.setState({ import: { host, port, name, token, clientId } }); } else { this.setState({ import: undefined }); } @@ -118,7 +125,7 @@ export class Join extends React.Component { } private isValid() { - return this.state.botId && this.state.token; + return this.state.botId && this.state.token && this.state.clientId; } private setAddress(address: M.Address) { @@ -133,6 +140,12 @@ export class Join extends React.Component { }); } + private setClientid(evt: React.FormEvent) { + this.setState({ + clientId: parseInt(evt.currentTarget.value, 10), + }); + } + private setBotId(botId: M.BotId) { this.setState({ botId }); } @@ -144,6 +157,7 @@ export class Join extends React.Component { botId: this.state.botId!, name: this.props.allBots[this.state.botId!].name, connected: true, + clientid: this.state.clientId!, }; this.props.joinMatch(this.state.address, bot); } diff --git a/planetwars/client/app/components/matches/LogView.tsx b/planetwars/client/app/components/matches/LogView.tsx index c4196e88..ee2da671 100644 --- a/planetwars/client/app/components/matches/LogView.tsx +++ b/planetwars/client/app/components/matches/LogView.tsx @@ -4,7 +4,6 @@ import { MatchLog, GameState, Player, - PwTypes, } from '../../lib/match'; import * as classNames from 'classnames'; @@ -118,6 +117,9 @@ export const PlayerTurnView: SFC<{ turn: PlayerTurn }> = ({ turn }) => { case 'commands': { return ; } + default: { + return
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAa"
; + } } }; @@ -143,9 +145,10 @@ export const ParseErrorView: SFC = (props) => { ); }; -export interface CommandsViewProps { commands: PwTypes.PlayerCommand[]; } +// TODO: typing +export interface CommandsViewProps { commands: any/*PlayerCommand[]*/; } export const CommandsView: SFC = (props) => { - const dispatches = props.commands.map((cmd, idx) => { + const dispatches = props.commands.map((cmd: any , idx: number) => { const isWarning = { [styles.warning]: !!cmd.error }; return (
  • @@ -171,7 +174,7 @@ export const DispatchError: SFC<{ error?: string }> = ({ error }) => { } }; -export const DispatchView: SFC<{ cmd: PwTypes.Command }> = ({ cmd }) => { +export const DispatchView: SFC<{ cmd: any /*Command*/ }> = ({ cmd }) => { const { ship_count, origin, destination } = cmd; return (
    diff --git a/planetwars/client/app/components/play/lobby/Lobby.tsx b/planetwars/client/app/components/play/lobby/Lobby.tsx index 1a817f91..6756ead6 100644 --- a/planetwars/client/app/components/play/lobby/Lobby.tsx +++ b/planetwars/client/app/components/play/lobby/Lobby.tsx @@ -6,12 +6,14 @@ import * as PwClient from 'mozaic-client'; import { Config } from '../../../utils/Config'; import { generateToken } from '../../../utils/GameRunner'; import * as M from '../../../database/models'; +import * as crypto from 'crypto'; import * as Lib from '../types'; import Section from '../Section'; import { SlotList } from './SlotList'; import { ServerControls } from './ServerControls'; import { SlotManager, Slot } from './SlotManager'; +import { createWriteStream } from 'fs'; // tslint:disable-next-line:no-var-requires const styles = require('./Lobby.scss'); @@ -48,7 +50,8 @@ export interface RunningState { export class Lobby extends React.Component { private slotManager: SlotManager; - private server?: PwClient.MatchRunner; + private server?: PwClient.ServerRunner; + private match: PwClient.PwMatch; constructor(props: LobbyProps) { super(props); @@ -124,38 +127,43 @@ export class Lobby extends React.Component { } private connectLocalBot = (slot: Slot, playerNum: number) => { - if (!this.validifyRunning(this.state)) { return; } - if (!this.server) { return; } - if (!slot.bot || !slot.clientId) { return; } + if (!this.validifyRunning(this.state)) { console.log("invalid state"); return; } + if (!this.server) { console.log("server isn't running"); return; } + if (!slot.bot || !slot.clientId) { console.log("invalid slot"); return; } + console.log("connecting..."); // callbacks should be set on the current slotmanager, // not the one belonging to 'this'. (It changes when a match is launched). const slotManager = this.slotManager; const { config } = this.state; - const { bot, token: stringToken } = slot; + const { bot, token: stringToken, clientId } = slot; const { name, command: fullCommand } = bot; const [command, ...args] = stringArgv(bot.command); - const botConfig = { name, command, args }; + const botConfig = { name , command, args }; - PwClient.Client.connect({ - token: Buffer.from(stringToken, 'hex'), + const client = new PwClient.PwClient({ host: config.address.host, port: config.address.port, - logger: this.server.logger, - }).then((client) => { - const _pwClient = new PwClient.PwClient(client, botConfig); - console.log('connected local bot'); + token: Buffer.from(stringToken, 'hex'), + botConfig: { + command: botConfig.command, + args: botConfig.args, + }, + logSink: createWriteStream(this.state.logFile), + clientId, }); + client.run(); + console.log('connected local bot'); } private removeBot = (num: number) => this.slotManager.removeBot(num); private removeExternalBot = (token: M.Token, playerNum: number, clientId: number) => { if (!this.validifyRunning(this.state)) { return; } - if (this.server) { - this.server.matchControl.removePlayer(clientId); + if (this.server && this.match) { + this.match.send(PwClient.events.RemoveClient.create({clientId})); this.slotManager.disconnectClient(clientId); this.slotManager.removeBot(playerNum); } @@ -193,64 +201,85 @@ export class Lobby extends React.Component { // TODO this is dirty, cause we have to create a matchId already const matchId = Config.generateMatchId(); const ctrlToken = generateToken(); + const serverToken = crypto.randomBytes(16).toString("hex"); const logFile = Config.matchLogPath(matchId); - const params = { ctrl_token: ctrlToken, address: config.address, logFile }; - console.log('launching server with', params); - + const serverParams: PwClient.ServerParams = { ctrl_token: serverToken, address: config.address, logFile }; + console.log('launching server with', serverParams); // callbacks should be set on the current slotmanager, // not the one belonging to 'this'. (It changes when a match is launched). const slotManager = this.slotManager; + const slots = this.state.slots; + + // Start the server + this.server = new PwClient.ServerRunner(Config.matchRunner, serverParams); + this.server.runServer(); + + const newState: RunningState = { + type: 'running', + config, + slots, + matchId, + logFile, + }; + this.setState(newState); - PwClient.MatchRunner.create(Config.matchRunner, params) - .then((server) => { - console.log('test proc'); - const slots = this.state.slots; - this.server = server; - this.slotManager.setMatchRunner(server); - this.server.onPlayerConnected.subscribe((clientId) => { - slotManager.connectClient(clientId); - }); - this.server.onPlayerDisconnected.subscribe((clientId) => { - slotManager.connectClient(clientId); - }); - - this.server.onComplete.subscribe(() => { - this.props.sendNotification( - "Match ended", - `A match on map '${ - this.state.type === "configuring" ? - "unknown" : - this.props.maps[this.state.config.mapId] - }' has ended`, - "Finished", - ); - }); - this.server.onError.subscribe(() => { - this.props.sendNotification( - "Match errored", - `A match on map '${ - this.state.type === "configuring" ? - "unknown" : - this.props.maps[this.state.config.mapId] - }' has errored`, - "Error", - ); - }); - const newState: RunningState = { - type: 'running', - config, - slots, - matchId, - logFile, - }; - this.setState(newState); - }) - .catch((err) => { - this.stopServer(); - alert(`Could not start game server: \n ${err}`); - console.log('Failed to start server.', err); - }); + const clientParams = { + host: config.address.host, + port: config.address.port, + token: Buffer.from(ctrlToken, 'hex'), + }; + + // Create the control client + const emitter = new PwClient.SimpleEventEmitter(); + const controlClient = new PwClient.Client({ + token: serverToken, + ...clientParams, + }, emitter); + + emitter.on(PwClient.events.Connected).subscribe((_) => { + console.log("Creating match"); + controlClient.send(PwClient.events.CreateMatch.create({ + matchUuid: Buffer.from(matchId, "hex"), + controlToken: Buffer.from(ctrlToken, "hex"), + })); + }); + + emitter.on(PwClient.events.MatchCreated).subscribe((e) => { + console.log("Created Match", e); + if (e.matchUuid.toString() === matchId) { + const logStream = createWriteStream(logFile); + + try { + this.match = new PwClient.PwMatch(clientParams, new PwClient.Logger(0, logStream)); + + this.match.on(PwClient.events.GameFinished).subscribe(() => { + this.props.sendNotification( + "Match ended", + `A match on map '${ + this.state.type === "configuring" ? + "unknown" : + this.props.maps[this.state.config.mapId] + }' has ended`, + "Finished", + ); + }); + + this.match.on(PwClient.events.Connected).subscribe(() => { + console.log("Match connected") + this.slotManager.setMatch(this.match); + }); + + this.match.connect(); + } catch (err) { + this.stopServer(); + alert(`Could not start game server: \n ${err}`); + console.error('Failed to start server.', err); + } + } + controlClient.exit(); + }); + controlClient.connect(); } private stopServer = () => { @@ -262,7 +291,7 @@ export class Lobby extends React.Component { } private launchGame = () => { - if (!this.server || !this.validifyRunning(this.state)) { + if (!this.match || !this.server || !this.validifyRunning(this.state)) { alert('Something went wrong'); return; } @@ -271,71 +300,75 @@ export class Lobby extends React.Component { const gameConf = Lib.exportConfig(this.state.config, this.props.maps); // Clear old listeners from the lobby - this.server.onPlayerConnected.clear(); - this.server.onPlayerDisconnected.clear(); + this.match.on(PwClient.events.ClientConnected).clear(); + this.match.on(PwClient.events.ClientDisconnected).clear(); // Bind completion listeners - this.server.onComplete.subscribe(() => { + this.server.onExit.subscribe(() => { this.props.onMatchComplete(matchId); }); this.server.onError.subscribe((err) => { - this.props.onMatchErrored(matchId, err) + this.props.onMatchErrored(matchId, err); }); // Bind connection listeners - this.server.onPlayerDisconnected.subscribe( - (id: number) => this.props.onPlayerDisconnectDuringMatch(id)); - this.server.onPlayerConnected.subscribe( - (id: number) => this.props.onPlayerReconnectedDuringMatch(id)); + this.match.on(PwClient.events.ClientDisconnected).subscribe( + (event) => this.props.onPlayerDisconnectDuringMatch(event.clientId)); + this.match.on(PwClient.events.ClientConnected).subscribe( + (event) => this.props.onPlayerReconnectedDuringMatch(event.clientId)); // Start game - this.server.matchControl.startGame(gameConf) - // This gets procced when the game has actually started - .then(() => { - const { host, port, maxTurns, mapId } = this.props.config!; - if (!this.validifyRunning(this.state)) { return; } - const match: M.PlayingHostedMatch = { - uuid: this.state.matchId, - type: M.MatchType.hosted, - status: M.MatchStatus.playing, - maxTurns, - map: mapId!, - network: { host, port }, - timestamp: new Date(), - logPath: this.state.logFile, - players: this.state.slots.map((slot) => { - if (slot.bot) { - const botSlot: M.InternalBotSlot = { - type: M.BotSlotType.internal, - token: slot.token, - botId: slot.bot.uuid, - name: slot.name, - connected: slot.connected, - }; - return botSlot; - } else { - const botSlot: M.ExternalBotSlot = { - type: M.BotSlotType.external, - token: slot.token, - name: slot.name, - connected: slot.connected, - }; - return botSlot; - } - }), - }; - this.props.saveMatch(match); - this.resetState(); - }) - .catch((err) => { - alert('Failed to start match. See console for info.'); - console.log(err); - }); + const matchConfig = { + mapPath: gameConf.map_file, + maxTurns: gameConf.max_turns, + } + this.match.send(PwClient.events.StartGame.create(matchConfig)); + try { + const { host, port, maxTurns, mapId } = this.props.config!; + if (!this.validifyRunning(this.state)) { return; } + const match: M.PlayingHostedMatch = { + uuid: this.state.matchId, + type: M.MatchType.hosted, + status: M.MatchStatus.playing, + maxTurns, + map: mapId!, + network: { host, port }, + timestamp: new Date(), + logPath: this.state.logFile, + players: this.state.slots.map((slot) => { + if (slot.bot) { + const botSlot: M.InternalBotSlot = { + type: M.BotSlotType.internal, + token: slot.token, + botId: slot.bot.uuid, + name: slot.name, + connected: slot.connected, + clientid: slot.clientId || 0, + }; + return botSlot; + } else { + const botSlot: M.ExternalBotSlot = { + type: M.BotSlotType.external, + token: slot.token, + name: slot.name, + connected: slot.connected, + clientid: slot.clientId || 0, + }; + return botSlot; + } + }), + }; + this.props.saveMatch(match); + this.resetState(); + } catch(err) { + alert('Failed to start match. See console for info.'); + console.log(err); + } } private killServer() { if (this.server) { - this.server.shutdown(); + this.server.killServer(); this.server = undefined; console.log('server killed'); } diff --git a/planetwars/client/app/components/play/lobby/SlotList.tsx b/planetwars/client/app/components/play/lobby/SlotList.tsx index 579d86e3..419ccc37 100644 --- a/planetwars/client/app/components/play/lobby/SlotList.tsx +++ b/planetwars/client/app/components/play/lobby/SlotList.tsx @@ -52,7 +52,7 @@ export class SlotElement extends React.Component { public render() { const { slot, index } = this.props; - const { token, name } = slot; + const { token, name, clientId } = slot; const kicked = (this.props.willBeKicked) ? (styles.kicked) : ''; return ( @@ -61,6 +61,7 @@ export class SlotElement extends React.Component {

    {token}

    Status: {this.statusToFriendly(slot)}

    Name: {name}

    +

    Client ID: {clientId}

    {this.getActions()}
    @@ -72,8 +73,8 @@ export class SlotElement extends React.Component { } private copyFull = (): void => { - const { slot: { token, name }, index, port, host } = this.props; - const data = { token, name, port, host }; + const { slot: { token, name, clientId }, index, port, host } = this.props; + const data = { token, name, port, host, clientId }; clipboard.writeText(JSON.stringify(data)); } diff --git a/planetwars/client/app/components/play/lobby/SlotManager.ts b/planetwars/client/app/components/play/lobby/SlotManager.ts index 9b9ec513..7761cc59 100644 --- a/planetwars/client/app/components/play/lobby/SlotManager.ts +++ b/planetwars/client/app/components/play/lobby/SlotManager.ts @@ -3,7 +3,7 @@ import { Chance } from 'chance'; import * as M from '../../../database/models'; import { generateToken } from '../../../utils/GameRunner'; import { WeakConfig, StrongConfig } from '../types'; -import { MatchRunner } from 'mozaic-client'; +import { PwMatch, events, PwClient } from 'mozaic-client'; export interface Slot { name: string; @@ -17,11 +17,12 @@ export type Slots = { [token: string]: Slot }; export type Clients = { [clientId: number]: Slot}; export class SlotManager { - public matchRunner?: MatchRunner; + public match?: PwMatch; public connectedClients: Set = new Set(); public slots: Slots; public slotList: string[]; public clients: Clients; + private lastClientId: number = 0; private onSlotChange: (self: SlotManager) => void; @@ -84,21 +85,25 @@ export class SlotManager { this.notifyListeners(); } - public setMatchRunner(matchRunner: MatchRunner) { - this.matchRunner = matchRunner; + public setMatch(match: PwMatch) { + this.match = match; - matchRunner.onPlayerConnected.subscribe((clientId) => { - this.connectClient(clientId); + match.on(events.ClientConnected).subscribe((event: events.ClientConnected) => { + console.log("Got ClientConnected for " + event.clientId); + this.connectClient(event.clientId); }); - matchRunner.onPlayerDisconnected.subscribe((clientId) => { - this.disconnectClient(clientId); + match.on(events.ClientDisconnected).subscribe((event: events.ClientDisconnected) => { + this.disconnectClient(event.clientId); + }); + + match.on(events.RegisterClient).subscribe((evt: events.RegisterClient) => { + console.log("Got RegisterClient for " + evt.clientId); }); this.slotList.forEach((token) => { const slot = this.slots[token]; this.registerSlot(slot); - this.notifyListeners(); }); } @@ -111,24 +116,28 @@ export class SlotManager { } private registerSlot(slot: Slot) { - if (this.matchRunner) { + if (this.match) { const token = Buffer.from(slot.token, 'hex'); - this.matchRunner.matchControl.addPlayer(token).then((clientId) => { - slot.clientId = clientId; - this.clients[clientId] = slot; - this.notifyListeners(); - }); + this.match.send(events.RegisterClient.create({ + clientId: slot.clientId, + token, + })); + if (slot.clientId) { + this.clients[slot.clientId] = slot; + } + this.notifyListeners(); } } private unregisterSlot(slot: Slot) { - if (this.matchRunner && slot.clientId) { + if (this.match && slot.clientId) { const clientId = slot.clientId; - this.matchRunner.matchControl.removePlayer(slot.clientId).then(() => { - delete this.clients[clientId]; - delete this.slots[slot.token]; - this.notifyListeners(); - }); + this.match.send(events.RemoveClient.create({ + clientId: slot.clientId, + })); + delete this.clients[clientId]; + delete this.slots[slot.token]; + this.notifyListeners(); } } @@ -137,9 +146,15 @@ export class SlotManager { name: new Chance().name({ prefix: true, nationality: 'it' }), token: generateToken(), connected: false, + clientId: this.createClientId(), }; this.slots[slot.token] = slot; this.registerSlot(slot); return slot; } + + private createClientId() { + this.lastClientId += 1; + return this.lastClientId; + } } diff --git a/planetwars/client/app/database/migrate.ts b/planetwars/client/app/database/migrate.ts index e32bb007..d614c0fb 100644 --- a/planetwars/client/app/database/migrate.ts +++ b/planetwars/client/app/database/migrate.ts @@ -86,6 +86,7 @@ function upgradeV3(db: V3.DbSchema): V4.DbSchema { connected: true, botId: player, name: newBots[player].name, + clientid: -1, }}); const matchParams: V4.HostedMatchProps = { type: V4.MatchType.hosted, diff --git a/planetwars/client/app/database/migrationV4.ts b/planetwars/client/app/database/migrationV4.ts index f72c7467..c4dbb0df 100644 --- a/planetwars/client/app/database/migrationV4.ts +++ b/planetwars/client/app/database/migrationV4.ts @@ -155,6 +155,7 @@ export interface BotSlotProps { type: BotSlotType; token: Token; connected: boolean; // Maybe more info? + clientid: number; } export type ExternalBotSlot = BotSlotProps & { diff --git a/planetwars/client/app/lib/match/MatchLog.ts b/planetwars/client/app/lib/match/MatchLog.ts index c0f4b22a..6304cd90 100644 --- a/planetwars/client/app/lib/match/MatchLog.ts +++ b/planetwars/client/app/lib/match/MatchLog.ts @@ -1,6 +1,5 @@ -import { PwTypes } from 'mozaic-client'; -import { PlanetList, Expedition, Player } from './types'; -import * as _ from 'lodash'; +import { PlanetList, Expedition, Player, JsonExpedition, JsonPlanet } from './types'; +import { events, Event, PwTypes } from "mozaic-client"; export abstract class MatchLog { public playerLogs: PlayerMap; @@ -19,7 +18,12 @@ export abstract class MatchLog { return this.gameStates[this.gameStates.length - 1].livingPlayers(); } - public abstract addEntry(entry: PwTypes.LogEntry): void; + // This may be a hack... + public addPlayer(clientId: number) { + this.getPlayerLog(clientId); + } + + public abstract addEntry(entry: Event): void; protected getPlayerLog(playerNum: number) { let playerLog = this.playerLogs[playerNum]; @@ -32,30 +36,57 @@ export abstract class MatchLog { } export class HostedMatchLog extends MatchLog { - public addEntry(entry: PwTypes.LogEntry) { - switch (entry.type) { - case "game_state": { - const state = GameState.fromJson(entry.state); - this.gameStates.push(state); + public addEntry(entry: Event) { + switch (entry.eventType) { + case events.PlayerAction: { + const entryPA = entry as events.PlayerAction; + + this.getPlayerLog(entryPA.clientId).addRecord(entry); + + break; + } + case events.GameStep: { + const entryGS = entry as events.GameStep; + + Object.keys(this.playerLogs).forEach((clientIdStr) => { + this.playerLogs[parseInt(clientIdStr, 10)].addRecord(entryGS); + }); + + this.gameStates.push(GameState.fromJson(JSON.parse(entryGS.state))); + break; } - case "player_entry": { - this.getPlayerLog(entry.player).addRecord(entry.record); + case events.RegisterClient: { + const entryRC = entry as events.RegisterClient; + + this.getPlayerLog(entryRC.clientId); + + break; } } } } export class JoinedMatchLog extends MatchLog { - public addEntry(entry: PwTypes.LogEntry) { - if (entry.type === 'player_entry') { - // this should always be the case since this is a joined match - const { player, record } = entry; + public addEntry(entry: Event) { + switch (entry.eventType) { + case events.PlayerAction: { + const entryPA = entry as events.PlayerAction; + + this.getPlayerLog(entryPA.clientId).addRecord(entry); + + break; + } + case events.GameStep: { + const entryGS = entry as events.GameStep; + + Object.keys(this.playerLogs).forEach((clientIdStr) => { + this.playerLogs[parseInt(clientIdStr, 10)].addRecord(entryGS); + }); - this.getPlayerLog(player).addRecord(record); + this.gameStates.push(GameState.fromJson(JSON.parse(entryGS.state))); - if (player === 1 && record.type === 'step') { - this.gameStates.push(GameState.fromJson(record.state)); + break; } } } @@ -72,7 +103,7 @@ export class GameState { public static fromJson(json: PwTypes.GameState) { const planets: PlanetList = {}; - json.planets.forEach((p) => { + json.planets.forEach((p: JsonPlanet) => { planets[p.name] = { name: p.name, x: p.x, @@ -82,7 +113,7 @@ export class GameState { }; }); - const expeditions = json.expeditions.map((e) => { + const expeditions = json.expeditions.map((e: JsonExpedition) => { return { id: e.id, origin: planets[e.origin], @@ -118,26 +149,28 @@ export class PlayerLog { this.turns = []; } - public addRecord(record: PwTypes.LogRecord) { - switch (record.type) { - case 'step': { - this.turns.push({ state: record.state }); - break; - } - case 'command': { - this.turns[this.turns.length - 1].command = record.content; + public addRecord(record: Event) { + switch (record.eventType) { + case events.GameStep: { + const recordGS = record as events.GameStep; + this.turns.push({ state: JSON.parse(recordGS.state) }); break; } - case 'player_action': { - this.turns[this.turns.length - 1].action = record.action; + case events.PlayerAction: { + const recordPA = record as events.PlayerAction; + this.turns[this.turns.length - 1].command = recordPA.action; break; } + // case 'command': { + // this.turns[this.turns.length - 1].action = record.action; + // break; + // } } } } export interface PlayerTurn { - state: PwTypes.GameState; + state: GameState; command?: string; action?: PwTypes.PlayerAction; } diff --git a/planetwars/client/app/lib/match/index.ts b/planetwars/client/app/lib/match/index.ts index 00d2c87b..938e281f 100644 --- a/planetwars/client/app/lib/match/index.ts +++ b/planetwars/client/app/lib/match/index.ts @@ -1,7 +1,3 @@ -import { MatchLog } from '.'; - export { MatchLog, PlayerMap } from './MatchLog'; export * from './types'; export * from './utils'; -// TODO: this is not exactly ideal -export { PwTypes } from 'mozaic-client'; diff --git a/planetwars/client/app/lib/match/utils.ts b/planetwars/client/app/lib/match/utils.ts index bfb23d17..c0e3a6b5 100644 --- a/planetwars/client/app/lib/match/utils.ts +++ b/planetwars/client/app/lib/match/utils.ts @@ -1,7 +1,7 @@ import * as M from '../../database/models'; import * as fs from 'fs'; -import { PwTypes } from '.'; import { MatchLog, HostedMatchLog, JoinedMatchLog } from './MatchLog'; +import { Replayer, SimpleEventEmitter, events } from "mozaic-client"; export function emptyLog(type: M.MatchType): MatchLog { switch (type) { @@ -12,19 +12,41 @@ export function emptyLog(type: M.MatchType): MatchLog { } } -export function logFileEntries(path: string): PwTypes.LogEntry[] { +// TODO: typing +export function logFileEntries(path: string): any[] { const lines = fs.readFileSync(path, 'utf-8').trim().split('\n'); return lines.map((line: string) => JSON.parse(line)); } export function parseLogFile(path: string, type: M.MatchType): MatchLog { const log = emptyLog(type); - logFileEntries(path).forEach((entry) => { - log.addEntry(entry); + const replayer = new Replayer(); + + registerStreamToLog(log, replayer); + + replayer.clientSpottedDispatcher.subscribe((clientId) => { + log.addPlayer(clientId); + registerStreamToLog(log, replayer.clientStream(clientId)); }); + + replayer.replayFile(path); return log; } +function registerStreamToLog(log: MatchLog, stream: Replayer | SimpleEventEmitter) { + stream.on(events.GameStep).subscribe((event) => { + log.addEntry(event); + }); + + stream.on(events.PlayerAction).subscribe((event) => { + log.addEntry(event); + }); + + stream.on(events.RegisterClient).subscribe((event) => { + log.addEntry(event); + }); +} + export function calcStats(log: MatchLog): M.MatchStats { return { winners: Array.from(log.getWinners()), diff --git a/planetwars/client/app/reducers/hostPage.ts b/planetwars/client/app/reducers/hostPage.ts index e074e65f..14794f65 100644 --- a/planetwars/client/app/reducers/hostPage.ts +++ b/planetwars/client/app/reducers/hostPage.ts @@ -8,7 +8,7 @@ export interface HostPageState { slots: M.BotSlot[]; serverRunning: boolean; matchParams?: M.MatchParams; - runner?: PwClient.MatchRunner; + runner?: PwClient.Reactor; } const defaultState = { slots: [], serverRunning: false }; @@ -31,6 +31,7 @@ export function hostReducer(state: HostPageState = defaultState, action: any) { name: 'Player ' + i, token: generateToken(), connected: false, + clientid: i, }); }