diff --git a/src/common/GameCommon.ts b/src/common/GameCommon.ts index 8d48b70..f498009 100644 --- a/src/common/GameCommon.ts +++ b/src/common/GameCommon.ts @@ -170,8 +170,11 @@ function getPlayerIdByIndex(gameId: string, playerIndex: number): string|null { return null } -function getPlayer(gameId: string, playerId: string): Player { +function getPlayer(gameId: string, playerId: string): Player|null { const idx = getPlayerIndexById(gameId, playerId) + if (idx === -1) { + return null + } return Util.decodePlayer(GAMES[gameId].players[idx]) } @@ -299,6 +302,10 @@ function changePlayer( change: any ): void { const player = getPlayer(gameId, playerId) + if (player === null) { + return + } + for (let k of Object.keys(change)) { // @ts-ignore player[k] = change[k] @@ -647,9 +654,13 @@ function handleInput( } const _playerChange = (): void => { + const player = getPlayer(gameId, playerId) + if (!player) { + return + } changes.push([ Protocol.CHANGE_PLAYER, - Util.encodePlayer(getPlayer(gameId, playerId)), + Util.encodePlayer(player), ]) } diff --git a/src/common/Protocol.ts b/src/common/Protocol.ts index 868e4c9..baa0280 100644 --- a/src/common/Protocol.ts +++ b/src/common/Protocol.ts @@ -40,10 +40,10 @@ EV_SERVER_INIT: event sent to one client after that client */ const EV_SERVER_EVENT = 1 const EV_SERVER_INIT = 4 -const EV_SERVER_INIT_REPLAY = 5 +const EV_SERVER_REPLAY_DATA = 5 const EV_CLIENT_EVENT = 2 const EV_CLIENT_INIT = 3 -const EV_CLIENT_INIT_REPLAY = 6 +const EV_CLIENT_REPLAY_DATA = 6 const LOG_HEADER = 1 const LOG_ADD_PLAYER = 2 @@ -68,10 +68,10 @@ const CHANGE_PLAYER = 3 export default { EV_SERVER_EVENT, EV_SERVER_INIT, - EV_SERVER_INIT_REPLAY, + EV_SERVER_REPLAY_DATA, EV_CLIENT_EVENT, EV_CLIENT_INIT, - EV_CLIENT_INIT_REPLAY, + EV_CLIENT_REPLAY_DATA, LOG_HEADER, LOG_ADD_PLAYER, diff --git a/src/frontend/Communication.ts b/src/frontend/Communication.ts index b0843dc..e4d7d8f 100644 --- a/src/frontend/Communication.ts +++ b/src/frontend/Communication.ts @@ -96,6 +96,13 @@ function connect( }) } +function requestReplayData( + offset: number, + size: number +): void { + send([Protocol.EV_CLIENT_REPLAY_DATA, offset, size]) +} + // TOOD: change replay stuff function connectReplay( address: string, @@ -109,16 +116,25 @@ function connectReplay( ws = new WebSocket(address, clientId + '|' + gameId) ws.onopen = (e) => { setConnectionState(CONN_STATE_CONNECTED) - send([Protocol.EV_CLIENT_INIT_REPLAY]) + requestReplayData(0, 10000) } ws.onmessage = (e) => { const msg = JSON.parse(e.data) const msgType = msg[0] - if (msgType === Protocol.EV_SERVER_INIT_REPLAY) { - const game = msg[1] - const log = msg[2] - const replay: { game: any, log: Array } = { game, log } - resolve(replay) + if (msgType === Protocol.EV_SERVER_REPLAY_DATA) { + const log: any[] = msg[1] + const game = msg[2] // can be null or encoded game + if (game !== null) { + // this is the first/initial message + const replay: { + game: any, + log: any[] + } = { game, log } + resolve(replay) + } else { + // this is just the next batch of log entries + changesCallback(msg) + } } else { throw `[ 2021-05-09 invalid connectReplay msgType ${msgType} ]` } @@ -158,6 +174,7 @@ function sendClientEvent(evt: any): void { export default { connect, connectReplay, + requestReplayData, disconnect, sendClientEvent, onServerChange, diff --git a/src/frontend/components/GameTeaser.vue b/src/frontend/components/GameTeaser.vue index ce72bb9..94b01c3 100644 --- a/src/frontend/components/GameTeaser.vue +++ b/src/frontend/components/GameTeaser.vue @@ -7,7 +7,7 @@ {{time(game.started, game.finished)}}
- + ↪️ Watch replay diff --git a/src/frontend/game.ts b/src/frontend/game.ts index 4dbb127..50be60e 100644 --- a/src/frontend/game.ts +++ b/src/frontend/game.ts @@ -48,7 +48,10 @@ interface Hud { setReplayPaused?: (v: boolean) => void } interface Replay { + final: boolean + requesting: boolean log: Array + logPointer: number, logIdx: number speeds: Array speedIdx: number @@ -260,7 +263,10 @@ export async function main( // stuff only available in replay mode... // TODO: refactor const REPLAY: Replay = { + final: false, + requesting: true, log: [], + logPointer: 0, logIdx: 0, speeds: [0.5, 1, 2, 5, 10, 20, 50], speedIdx: 1, @@ -283,12 +289,26 @@ export async function main( TIME = () => Time.timestamp() } else if (MODE === MODE_REPLAY) { // TODO: change how replay connect is done... + Communication.onServerChange((msg) => { + const log = msg[1] + + // cut log that was already handled + REPLAY.log = REPLAY.log.slice(REPLAY.logPointer) + REPLAY.logPointer = 0 + + REPLAY.log.push(...msg[1]) + if (log.length < 10000) { + REPLAY.final = true + } + REPLAY.requesting = false + }) const replay: {game: any, log: Array} = await Communication.connectReplay(wsAddress, gameId, clientId) const gameObject = Util.decodeGame(replay.game) Game.setGame(gameObject.id, gameObject) + REPLAY.requesting = false REPLAY.log = replay.log REPLAY.lastRealTs = Time.timestamp() - REPLAY.gameStartTs = parseInt(REPLAY.log[0][REPLAY.log[0].length - 2], 10) + REPLAY.gameStartTs = parseInt(REPLAY.log[0][4], 10) REPLAY.lastGameTs = REPLAY.gameStartTs TIME = () => REPLAY.lastGameTs } else { @@ -426,6 +446,7 @@ export async function main( } if (MODE === MODE_PLAY) { + // TODO: register onServerChange function before connecting to server Communication.onServerChange((msg) => { const msgType = msg[0] const evClientId = msg[1] @@ -458,6 +479,18 @@ export async function main( // only the REPLAY.log is relevant let inter = setInterval(() => { const realTs = Time.timestamp() + if (REPLAY.requesting) { + REPLAY.lastRealTs = realTs + return + } + + if (REPLAY.logPointer + 1 >= REPLAY.log.length) { + REPLAY.lastRealTs = realTs + REPLAY.requesting = true + Communication.requestReplayData(REPLAY.logIdx, 10000) + return + } + if (REPLAY.paused) { REPLAY.lastRealTs = realTs return @@ -469,9 +502,11 @@ export async function main( if (REPLAY.paused) { break } - const nextIdx = REPLAY.logIdx + 1 + const nextIdx = REPLAY.logPointer + 1 if (nextIdx >= REPLAY.log.length) { - clearInterval(inter) + if (REPLAY.final) { + clearInterval(inter) + } break } @@ -502,7 +537,8 @@ export async function main( Game.handleInput(gameId, playerId, input, nextTs) RERENDER = true } - REPLAY.logIdx = nextIdx + REPLAY.logPointer = nextIdx + REPLAY.logIdx++ } while (true) REPLAY.lastRealTs = realTs REPLAY.lastGameTs = maxGameTs diff --git a/src/server/GameLog.ts b/src/server/GameLog.ts index 385c17a..7fb614a 100644 --- a/src/server/GameLog.ts +++ b/src/server/GameLog.ts @@ -1,4 +1,6 @@ import fs from 'fs' +import readline from 'readline' +import stream from 'stream' import { logger } from './../common/Util' import { DATA_DIR } from './../server/Dirs' @@ -27,19 +29,39 @@ const _log = (gameId: string, ...args: Array) => { fs.appendFileSync(file, str + "\n") } -const get = (gameId: string) => { +const get = async ( + gameId: string, + offset: number = 0, + size: number = 10000 +): Promise => { const file = filename(gameId) if (!fs.existsSync(file)) { return [] } - const lines = fs.readFileSync(file, 'utf-8').split("\n") - return lines.filter((line: string) => !!line).map((line: string) => { - try { - return JSON.parse(line) - } catch (e) { - log.log(line) - log.log(e) - } + return new Promise((resolve) => { + const instream = fs.createReadStream(file) + const outstream = new stream.Writable() + const rl = readline.createInterface(instream, outstream) + const lines: any[] = [] + let i = -1 + rl.on('line', (line) => { + if (!line) { + // skip empty + return + } + i++ + if (offset > i) { + return + } + if (offset + size <= i) { + rl.close() + return + } + lines.push(JSON.parse(line)) + }) + rl.on('close', () => { + resolve(lines) + }) }) } diff --git a/src/server/main.ts b/src/server/main.ts index c5c89c4..7314148 100644 --- a/src/server/main.ts +++ b/src/server/main.ts @@ -203,20 +203,27 @@ wss.on('message', async ({socket, data} : { socket: WebSocket, data: any }) => { const msg = JSON.parse(data) const msgType = msg[0] switch (msgType) { - case Protocol.EV_CLIENT_INIT_REPLAY: { + case Protocol.EV_CLIENT_REPLAY_DATA: { if (!GameLog.exists(gameId)) { throw `[gamelog ${gameId} does not exist... ]` } - const log = GameLog.get(gameId) - const game = await Game.createGameObject( - gameId, - log[0][2], - log[0][3], - log[0][4], - log[0][5] || ScoreMode.FINAL - ) + const offset = msg[1] + const size = msg[2] + + const log = await GameLog.get(gameId, offset, size) + let game = null + if (offset === 0) { + // also need the game + game = await Game.createGameObject( + gameId, + log[0][2], + log[0][3], + log[0][4], + log[0][5] || ScoreMode.FINAL + ) + } notify( - [Protocol.EV_SERVER_INIT_REPLAY, Util.encodeGame(game), log], + [Protocol.EV_SERVER_REPLAY_DATA, log, game ? Util.encodeGame(game) : null], [socket] ) } break