From 3f9bf03e460baea9495b23ff86b261c58e39b345 Mon Sep 17 00:00:00 2001 From: Roger Andersen Date: Wed, 25 Jan 2017 20:09:24 +0100 Subject: [PATCH] feat: start parsing replay files --- src/const.js | 6 ++- src/rec.js | 147 ++++++++++++++++++++++++++++++++++++++++++++++++++- test/test.js | 18 ++++++- 3 files changed, 168 insertions(+), 3 deletions(-) diff --git a/src/const.js b/src/const.js index 8d855c4..e4baa6f 100644 --- a/src/const.js +++ b/src/const.js @@ -7,10 +7,14 @@ const HEAD_RADIUS = 0.238 const OBJECT_DIAMETER = 0.8 const OBJECT_RADIUS = 0.4 +// Markers +const EOR_MARKER = 0x00492F75 // replay marker + module.exports = { TOP10, HEAD_DIAMETER, HEAD_RADIUS, OBJECT_DIAMETER, - OBJECT_RADIUS + OBJECT_RADIUS, + EOR_MARKER } diff --git a/src/rec.js b/src/rec.js index 914bd3c..56f151a 100644 --- a/src/rec.js +++ b/src/rec.js @@ -1,4 +1,6 @@ const fs = require('fs') +const trimString = require('./util').trimString +const EOR_MARKER = require('./const').EOR_MARKER /** * Class containing all replay attributes. @@ -23,11 +25,154 @@ class Replay { fs.readFile(filePath, (error, buffer) => { if (error) reject(error) let replay = new Replay() - resolve(replay) + replay._parseFile(buffer).then(results => resolve(results)).catch(error => reject(error)) }) }) } + /** + * Parses file buffer data into a Replay. + * @private + * @returns {Promise} + */ + _parseFile (buffer) { + return new Promise((resolve, reject) => { + let offset = 0 + // frame count + let numFrames = buffer.readUInt32LE(offset) + offset += 8 // + 4 unused extra bytes + // multireplay? + this.multi = Boolean(buffer.readInt32LE(offset)) + offset += 4 + // flag-tag replay? + this.flagTag = Boolean(buffer.readInt32LE(offset)) + offset += 4 + // level link + this.link = buffer.readUInt32LE(offset) + offset += 4 + // level filename with extension + this.level = trimString(buffer.slice(offset, offset + 12)) + offset += 16 // + 4 unused extra bytes + + // frames + this.frames[0] = Replay._parseFrames(buffer.slice(offset, offset + (27 * numFrames)), numFrames) + offset += 27 * numFrames + // events + let numEvents = buffer.readUInt32LE(offset) + offset += 4 + this.events[0] = Replay._parseEvents(buffer.slice(offset, offset + (16 * numEvents)), numEvents) + offset += 16 * numEvents + + // end of replay marker + let expected = buffer.readInt32LE(offset) + if (expected !== EOR_MARKER) { + reject('End of replay marker mismatch') + return + } + + // if multi rec, parse another set of frames and events while skipping + // other fields we already gathered from the first half. probably? + if (this.multi) { + offset += 4 + let numFrames = buffer.readUInt32LE(offset) + offset += 36 // +32 bytes where skipping other fields + this.frames[1] = Replay._parseFrames(buffer.slice(offset, offset + (27 * numFrames)), numFrames) + offset += 27 * numFrames + let numEvents = buffer.readUInt32LE(offset) + offset += 4 + this.events[1] = Replay._parseEvents(buffer.slice(offset, offset + (16 * numEvents)), numEvents) + offset += 16 * numEvents + let expected = buffer.readInt32LE(offset) + if (expected !== EOR_MARKER) { + reject('End of replay marker mismatch') + return + } + } + + resolve(this) + }) + } + + /** + * Parses frame data into an array of frame objects. + * @private + * @param {Buffer} buffer Frame data to parse. + * @param {Number} numFrames Number of frames to parse. + * @returns {Array} + */ + static _parseFrames (buffer, numFrames) { + let frames = [] + for (let i = 0; i < numFrames; i++) { + let data = buffer.readUint8(i + (numFrames * 23)) // read in data field first to process it + let frame = { + bike_x: buffer.readFloatLE(i * 4), + bike_y: buffer.readFloatLE((i * 4) + (numFrames * 4)), + left_x: buffer.readInt16LE((i * 2) + (numFrames * 8)), + left_y: buffer.readInt16LE((i * 2) + (numFrames * 10)), + right_x: buffer.readInt16LE((i * 2) + (numFrames * 12)), + right_y: buffer.readInt16LE((i * 2) + (numFrames * 14)), + head_x: buffer.readInt16LE((i * 2) + (numFrames * 16)), + head_y: buffer.readInt16LE((i * 2) + (numFrames * 18)), + rotation: buffer.readInt16LE((i * 2) + (numFrames * 20)), + left_rotation: buffer.readUint8(i + (numFrames * 21)), + right_rotation: buffer.readUint8(i + (numFrames * 22)), + throttle: data & 1 !== 0, + right: data & (1 << 1) !== 0, + volume: buffer.readInt16LE((i * 2) + (numFrames * 25)) + } + frames.push(frame) + } + return frames + } + + /** + * Parses event data into an array of event objects. + * @private + * @param {Buffer} buffer Event data to parse. + * @param {Number} numEvents Number of events to parse. + * @returns {Array} + */ + static _parseEvents (buffer, numEvents) { + let events = [] + let offset = 0 + for (let i = 0; i < numEvents; i++) { + let event = {} + event.time = buffer.readDoubleLE(offset) + offset += 8 + event.info = buffer.readInt16LE(offset) + offset += 2 + let eventType = buffer.readUint8(offset) + offset += 6 // 1 + 5 unknown bytes + switch (eventType) { + case 0: + event.eventType = 'apple' + break + case 1: + event.eventType = 'ground1' + break + case 4: + event.eventType = 'ground2' + break + case 5: + event.eventType = 'turn' + break + case 6: + event.eventType = 'voltRight' + break + case 7: + event.eventType = 'voltLeft' + break + default: + event.eventType = undefined + break + } + + events.push(event) + } + + return events + } + /** * Get time of replay in milliseconds. * @param {bool} hs Return hundredths diff --git a/test/test.js b/test/test.js index 78a2dd8..6c9a693 100644 --- a/test/test.js +++ b/test/test.js @@ -211,7 +211,7 @@ test('Level save() method without modifications matches original level', t => { /* * * * * * * * * * Replay tests * * * * * * * * * */ -test('Replay load() static method returns instance of Replay', t => { +test('Valid replay 1: load() parses level correctly', t => { t.plan(1) return Replay.load('test/assets/replays/rec_valid_1.rec').then(result => { @@ -219,6 +219,22 @@ test('Replay load() static method returns instance of Replay', t => { }).catch(error => t.fail(error.Error)) }) +test('Valid replay 2: load() parses level correctly', t => { + t.plan(1) + + return Replay.load('test/assets/replays/rec_valid_2.rec').then(result => { + t.true(result instanceof Replay) + }).catch(error => t.fail(error.Error)) +}) + +test('Valid replay 3: load() parses level correctly', t => { + t.plan(1) + + return Replay.load('test/assets/replays/rec_valid_3.rec').then(result => { + t.true(result instanceof Replay) + }).catch(error => t.fail(error.Error)) +}) + test.todo('read replay file') test.todo('reject Across replays') test.todo('check all replay attributes with 3+ replays')