deadem
Version:
JavaScript (Node.js & Browsers) parser for Deadlock (Valve Source 2 Engine) demo/replay files
316 lines (233 loc) • 10.2 kB
JavaScript
import TransformStream from '#core/stream/TransformStream.js';
import DeferredPromise from '#data/DeferredPromise.js';
import EntityMutation from '#data/entity/EntityMutation.js';
import EntityMutationEvent from '#data/entity/EntityMutationEvent.js';
import DemoPacketType from '#data/enums/DemoPacketType.js';
import EntityOperation from '#data/enums/EntityOperation.js';
import InterceptorStage from '#data/enums/InterceptorStage.js';
import MessagePacketType from '#data/enums/MessagePacketType.js';
import FieldPathBuilder from '#data/fields/path/FieldPathBuilder.js';
import DemoEntityHandler from '#handlers/DemoEntityHandler.js';
import DemoMessageHandler from '#handlers/DemoMessageHandler.js';
import DemoPacketHandler from '#handlers/DemoPacketHandler.js';
import WorkerRequestDPacketSync from '#workers/requests/WorkerRequestDPacketSync.js';
import WorkerRequestSvcCreatedEntities from '#workers/requests/WorkerRequestSvcCreatedEntities.js';
import WorkerRequestSvcUpdatedEntities from '#workers/requests/WorkerRequestSvcUpdatedEntities.js';
class DemoStreamPacketAnalyzerConcurrent extends TransformStream {
/**
* @public
* @constructor
* @param {ParserEngine} engine
*/
constructor(engine) {
super();
this._engine = engine;
this._demoEntityHandler = new DemoEntityHandler(engine.demo);
this._demoMessageHandler = new DemoMessageHandler(engine.demo);
this._demoPacketHandler = new DemoPacketHandler(engine.demo);
this._queue = [ ];
}
/**
* @protected
*/
async _finalize() {
const wait = async () => {
await Promise.all(this._queue.filter(i => i.deferred !== null).map(i => i.deferred.promise));
await this._drain();
if (this._queue.length > 0) {
await wait();
}
};
await wait();
}
/**
* @protected
* @param {DemoPacket} demoPacket
*/
async _handle(demoPacket) {
if (!demoPacket.type.heavy) {
this._enqueue(demoPacket);
await this._drain();
return;
}
const packets = demoPacket.data.filter(messagePacket => messagePacket.type === MessagePacketType.SVC_PACKET_ENTITIES);
if (packets.length === 0) {
this._enqueue(demoPacket);
await this._drain();
return;
}
if (packets.length !== 1) {
throw new Error(`Unhandled: the amount of SVC_PACKET_ENTITIES packets is more than one - [ ${packets.length} ]`);
}
const packet = packets[0];
const thread = await this._engine.workerManager.allocate();
const deferred = new DeferredPromise();
thread.send(new WorkerRequestSvcUpdatedEntities(packet))
.then((response) => {
this._engine.workerManager.free(thread);
deferred.resolve(response);
}).catch((error) => {
deferred.reject(error);
});
this._enqueue(demoPacket, deferred);
await this._drain();
}
/**
* @protected
* @returns {Promise<void>}
*/
async _drain() {
if (this._queue.length === 0) {
return;
}
let i = -1;
while (i + 1 < this._queue.length) {
const item = this._queue[i + 1];
if (item.deferred !== null && !item.deferred.fulfilled) {
break;
}
i++;
await this._handlePacket(item.demoPacket, item.deferred);
}
if (i >= 0) {
this._queue = this._queue.slice(i + 1);
}
}
/**
* @protected
* @param {DemoPacket} demoPacket
* @param {DeferredPromise|null} deferred
*/
_enqueue(demoPacket, deferred = null) {
this._queue.push({ demoPacket, deferred });
}
/**
* @protected
* @param {DemoPacket} demoPacket
* @param {DeferredPromise} deferred
* @returns {Promise<void>}
*/
async _handlePacket(demoPacket, deferred) {
await this._engine.interceptPre(InterceptorStage.DEMO_PACKET, demoPacket);
switch (demoPacket.type) {
case DemoPacketType.DEM_SEND_TABLES:
this._demoPacketHandler.handleDemSendTables(demoPacket);
await this._engine.workerManager.broadcast(new WorkerRequestDPacketSync(demoPacket));
break;
case DemoPacketType.DEM_CLASS_INFO:
this._demoPacketHandler.handleDemClassInfo(demoPacket);
await this._engine.workerManager.broadcast(new WorkerRequestDPacketSync(demoPacket));
break;
case DemoPacketType.DEM_STRING_TABLES:
this._demoPacketHandler.handleDemStringTables(demoPacket);
break;
case DemoPacketType.DEM_FULL_PACKET:
case DemoPacketType.DEM_PACKET:
case DemoPacketType.DEM_SIGNON_PACKET: {
const messagePackets = demoPacket.data;
for (let i = 0; i < messagePackets.length; i++) {
const messagePacket = messagePackets[i];
await this._engine.interceptPre(InterceptorStage.MESSAGE_PACKET, demoPacket, messagePacket);
switch (messagePacket.type) {
case MessagePacketType.SVC_SERVER_INFO:
this._demoMessageHandler.handleSvcServerInfo(messagePacket);
break;
case MessagePacketType.SVC_CREATE_STRING_TABLE:
this._demoMessageHandler.handleSvcCreateStringTable(messagePacket);
break;
case MessagePacketType.SVC_UPDATE_STRING_TABLE:
this._demoMessageHandler.handleSvcUpdateStringTable(messagePacket);
break;
case MessagePacketType.SVC_CLEAR_ALL_STRING_TABLES:
this._demoMessageHandler.handleSvcClearAllStringTables(messagePacket);
break;
case MessagePacketType.SVC_PACKET_ENTITIES: {
const response = await deferred.promise;
const { events, lastIndex } = this._getEntityEvents(demoPacket, messagePacket, response.payload);
const created = [];
for (let i = lastIndex; i < events.length; i++) {
if (events[i].operation === EntityOperation.CREATE) {
created.push(events[i]);
}
}
if (created.length > 0) {
this._broadcastCreatedEntities(created);
}
await this._engine.interceptPre(InterceptorStage.ENTITY_PACKET, demoPacket, messagePacket, events);
this._demoEntityHandler.handleEntityEvents(events);
await this._engine.interceptPost(InterceptorStage.ENTITY_PACKET, demoPacket, messagePacket, events);
break;
}
}
this._engine.getPacketTracker().handleMessagePacket(demoPacket, messagePacket);
await this._engine.interceptPost(InterceptorStage.MESSAGE_PACKET, demoPacket, messagePacket);
}
break;
}
default:
break;
}
this._engine.getPacketTracker().handleDemoPacket(demoPacket);
await this._engine.interceptPost(InterceptorStage.DEMO_PACKET, demoPacket);
}
/**
* @protected
* @param {Array<EntityMutationEvent>} events
*/
_broadcastCreatedEntities(events) {
const step = 3;
const createdData = new Uint32Array(events.length * step);
for (let i = 0; i < events.length; i++) {
const event = events[i];
createdData[i * step] = event.entity.index;
createdData[i * step + 1] = event.entity.serial;
createdData[i * step + 2] = event.entity.class.id;
}
this._engine.workerManager.broadcast(new WorkerRequestSvcCreatedEntities(createdData));
}
/**
* @protected
* @param {DemoPacket} demoPacket
* @param {MessagePacket} messagePacket
* @param {Array<EntityMutationPartialEvent>} partial
* @returns {Array<EntityMutationEvent>}
*/
_getEntityEvents(demoPacket, messagePacket, partial) {
if (partial.length === 0) {
return {
events: this._demoMessageHandler.handleSvcPacketEntities(messagePacket),
lastIndex: 0
};
}
const events = [ ];
let i = -1;
for (; i < partial.length - 1; i += 1) {
const eventPartial = partial[i + 1];
const entity = this._engine.demo.getEntity(eventPartial.entityIndex) || null;
if (entity === null || entity.class.id !== eventPartial.entityClassId) {
break;
}
const mutations = [ ];
for (let i = 0; i < eventPartial.mutations.length; i += 2) {
const fieldPath = FieldPathBuilder.reconstruct(eventPartial.mutations[i]);
mutations.push(new EntityMutation(fieldPath, eventPartial.mutations[i + 1]));
}
events.push(new EntityMutationEvent(EntityOperation.UPDATE, entity, mutations));
}
if (i < 0) {
return {
events: this._demoMessageHandler.handleSvcPacketEntities(messagePacket),
lastIndex: 0
};
}
const last = partial[i];
return {
events: [
...events,
...this._demoMessageHandler.handleSvcPacketEntities(messagePacket, last.bitPointer, i + 1, last.entityIndex)
],
lastIndex: i
};
}
}
export default DemoStreamPacketAnalyzerConcurrent;