deadem
Version:
JavaScript (Node.js & Browsers) parser for Deadlock (Valve Source 2 Engine) demo/replay files
262 lines (200 loc) • 7.98 kB
JavaScript
import SnappyDecompressor from '#core/SnappyDecompressor.instance.js';
import TransformStream from '#core/stream/TransformStream.js';
import DemoPacket from '#data/DemoPacket.js';
import MessagePacket from '#data/MessagePacket.js';
import DemoPacketType from '#data/enums/DemoPacketType.js';
import MessagePacketType from '#data/enums/MessagePacketType.js';
import PerformanceTrackerCategory from '#data/enums/PerformanceTrackerCategory.js';
import MessagePacketRawExtractor from '#extractors/MessagePacketRawExtractor.js';
import WorkerRequestDHPParse from '#workers/requests/WorkerRequestDHPParse.js';
/**
* Given a stream of {@link DemoPacketRaw}, parses its payload and
* passes through instances of:
* - {@link DemoPacket} - in case of success.
* - {@link DemoPacketRaw} - in case of failure.
* Packets that may hypothetically include large amounts of data (e.g. DEM_PACKET,
* DEM_SIGNON_PACKET, DEM_FULL_PACKET) are processed through
* asynchronous worker threads. As a result, the order of the {@link DemoPacket}
* or {@link DemoPacketRaw} instances is not guaranteed.
*/
class DemoStreamPacketParser extends TransformStream {
/**
* @constructor
* @public
* @param {ParserEngine} engine
*/
constructor(engine) {
super();
this._engine = engine;
this._counts = {
batches: 0,
requests: 0
};
this._pendingRequests = [ ];
}
/**
* @protected
* @returns {Promise<void>}
*/
async _finalize() {
const wait = async () => {
await Promise.all(this._pendingRequests);
if (this._pendingRequests.length > 0) {
await wait();
}
};
await wait();
}
/**
* @protected
* @param {Array<DemoPacketRaw>} batch
*/
async _handle(batch) {
this._counts.batches += 1;
if (!this._engine.getIsMultiThreaded()) {
this._engine.getPerformanceTracker().start(PerformanceTrackerCategory.DEMO_PACKET_PARSER);
const demoPackets = this._processSync(batch);
this._engine.getPerformanceTracker().end(PerformanceTrackerCategory.DEMO_PACKET_PARSER);
demoPackets.forEach((demoPacket, index) => {
if (demoPacket !== null) {
this._push(demoPacket);
} else {
this._push(batch[index]);
}
});
} else {
const getIsHeavy = demoPacketRaw => DemoPacketType.parseById(demoPacketRaw.getTypeId())?.heavy;
const getIsOther = demoPacketRaw => !getIsHeavy(demoPacketRaw);
const heavy = batch.filter(getIsHeavy);
const other = batch.filter(getIsOther);
const demoPackets = this._processSync(other);
demoPackets.forEach((demoPacket, index) => {
if (demoPacket !== null) {
this._push(demoPacket);
} else {
this._push(batch[index]);
}
});
if (heavy.length > 0) {
const promise = this._engine.workerManager.allocate();
this._pendingRequests.push(promise);
const thread = await promise;
this._removePending(promise);
this._processAsync(thread, heavy)
.then((demoPackets) => {
this._engine.workerManager.free(thread);
demoPackets.forEach((demoPacket) => {
this._push(demoPacket);
});
});
}
}
}
/**
* @protected
* @param {Array<DemoPacketRaw>} packets
* @returns {Array<DemoPacket|null>}
*/
_processSync(packets) {
return packets.map(demoPacketRaw => parseDemoPacket.call(this, demoPacketRaw));
}
/**
* @protected
* @param {WorkerThread} thread
* @param {Array<DemoPacketRaw>} packets
* @returns {Promise<Array<DemoPacket>>}
*/
async _processAsync(thread, packets) {
this._counts.requests += 1;
const promise = thread.send(new WorkerRequestDHPParse(packets.map(p => [ p.getIsCompressed(), p.payload ])));
this._pendingRequests.push(promise);
return promise
.then((response) => {
this._removePending(promise);
const demoPackets = [ ];
response.payload.forEach((batch, batchIndex) => {
const demoPacketRaw = packets[batchIndex];
const messagePackets = [ ];
batch.forEach((messagePacketRaw) => {
const messagePacket = parseMessagePacket.call(this, messagePacketRaw);
if (messagePacket !== null) {
messagePackets.push(messagePacket);
} else {
this._engine.getPacketTracker().handleMessagePacketRaw(demoPacketRaw, messagePacketRaw);
}
});
const demoPacketType = DemoPacketType.parseById(demoPacketRaw.getTypeId());
const demoTick = demoPacketRaw.tick.value;
const demoPacket = new DemoPacket(demoPacketRaw.sequence, demoPacketType, demoTick, messagePackets);
demoPackets.push(demoPacket);
});
return demoPackets;
});
}
/**
* @protected
* @param {Promise<any>} promise
*/
_removePending(promise) {
const index = this._pendingRequests.findIndex(p => promise === p);
if (index >= 0) {
this._pendingRequests.splice(index, 1);
}
}
}
/**
* @param {DemoPacketRaw} demoPacketRaw
* @returns {DemoPacket|null}
*/
function parseDemoPacket(demoPacketRaw) {
const demoPacketTypeId = demoPacketRaw.getTypeId();
const demoPacketType = DemoPacketType.parseById(demoPacketTypeId);
if (demoPacketType === null) {
this._engine.logger.warn(`Unknown DemoPacketType [ ${demoPacketTypeId} ]`);
return null;
}
let data;
if (demoPacketRaw.getIsCompressed()) {
data = SnappyDecompressor.decompress(demoPacketRaw.payload);
} else {
data = demoPacketRaw.payload;
}
const decoded = demoPacketType.proto.decode(data);
let demoPacket;
if (demoPacketType.heavy) {
const extractor = new MessagePacketRawExtractor(decoded.data);
const messagePacketsRaw = extractor.all();
const messagePackets = [ ];
messagePacketsRaw.forEach((messagePacketRaw) => {
const messagePacket = parseMessagePacket.call(this, messagePacketRaw);
if (messagePacket !== null) {
messagePackets.push(messagePacket);
} else {
this._engine.getPacketTracker().handleMessagePacketRaw(demoPacketRaw, messagePacketRaw);
}
});
demoPacket = new DemoPacket(demoPacketRaw.sequence, demoPacketType, demoPacketRaw.tick.value, messagePackets);
} else {
demoPacket = new DemoPacket(demoPacketRaw.sequence, demoPacketType, demoPacketRaw.tick.value, decoded);
}
return demoPacket;
}
/**
* @param {MessagePacketRaw} messagePacketRaw
* @returns {MessagePacket|null}
*/
function parseMessagePacket(messagePacketRaw) {
const messagePacketType = MessagePacketType.parseById(messagePacketRaw.type) || null;
if (messagePacketType === null) {
return null;
}
let data;
try {
data = messagePacketType.proto.decode(messagePacketRaw.payload);
} catch {
this._engine.logger.warn(`Unable to decode protobuf message for [ ${messagePacketType.code} ]`);
return null;
}
return new MessagePacket(messagePacketType, data);
}
export default DemoStreamPacketParser;