UNPKG

rapier

Version:

A JavaScript Dota 2 (Source 2) replay parsing library.

1,432 lines (1,424 loc) 2.24 MB
(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){ (function (Buffer){ /** * Converts a given buffer of bytes to a stream of bits and provides methods for reading individual bits (non-aligned reads) **/ var Long = require('long'); //accepts a native buffer object var BitStream = function(buf) { this.offset = 0; this.limit = buf.length * 8; this.bytes = buf; }; /** * Reads the specified number of bits (possibly non-aligned) and returns as 32bit int **/ BitStream.prototype.readBits = function(n) { if (n > (this.limit - this.offset)) { throw "not enough bits left in stream to read!"; } var bitOffset = this.offset % 8; var bitsToRead = bitOffset + n; var bytesToRead = ~~(bitsToRead / 8); //if reading a multiple of 8 bits, read an additional byte if (bitsToRead % 8) { bytesToRead += 1; } var value = null; if (!bitOffset && n === 8) { //if we are byte-aligned and only want one byte, we can read quickly without shifting operations value = this.bytes.readUInt8(this.offset / 8); } //32 bit shifting else if (bitsToRead <= 31) { value = 0; //console.error(bits, this.offset, bitOffset, bitsToRead,bytesToRead); for (var i = 0; i < bytesToRead; i++) { //extract the byte from the backing buffer var m = this.bytes[~~(this.offset / 8) + i]; //move these 8 bits to the correct location //looks like most significant 8 bits come last, so this flips endianness value += (m << (i * 8)); } //drop the extra bits, since we started from the beginning of the byte regardless of offset value >>= bitOffset; //shift a single 1 over, subtract 1 to form a bit mask that removes the first bit value &= ((1 << n) - 1); } else { //trying to read 32+ bits with native JS probably won't work because we must then read five bytes from the backing buffer //this means in practice we may have difficulty with n >= 25 bits (since offset can be up to 7) //can't fit that into a 32 bit int unless we use JS Long, which is slow console.error(bitsToRead); //64 bit shifting, we only need this if our operations cant fit into 32 bits value = new Long(); //console.error(bits, this.offset, bitOffset, bitsToRead,bytesToRead); for (var i = 0; i < bytesToRead; i++) { //extract the byte from the backing buffer var m64 = this.bytes[~~(this.offset / 8) + i]; //console.error(m, this.bytes); //copy m into a 64bit holder so we can shift bits around more m64 = new Long.fromNumber(m64); //shift to get the bits we want value = value.add(m64.shiftLeft(i * 8)); } value = value.shiftRight(bitOffset); //shift a single 1 over, subtract 1 to form a bit mask value = value.and((1 << n) - 1); value = value.toInt(); } this.offset += n; return value; }; /** * Reads the specified number of bits into a Buffer and returns **/ BitStream.prototype.readBuffer = function(bits) { var bytes = Math.ceil(bits / 8); var result = new Buffer(bytes); var offset = 0; result.length = bytes; while (bits > 0) { //read up to 8 bits at a time (we may read less at the end if not aligned) var bitsToRead = Math.min(bits, 8); result.writeUInt8(this.readBits(bitsToRead), offset); offset += 1; bits -= bitsToRead; } return result; }; BitStream.prototype.readBoolean = function() { return this.readBits(1); }; /** * Reads until we reach a null terminator character and returns the result as a string **/ BitStream.prototype.readNullTerminatedString = function() { var str = ""; while (true) { var byteInt = this.readBits(8); if (!byteInt) { break; } var byteBuf = new Buffer(1); byteBuf.writeUInt8(byteInt); str += byteBuf.toString(); } //console.log(str); return str; }; BitStream.prototype.readVarUInt = function() { var max = 32; var m = ((max + 6) / 7) * 7; var value = 0; var shift = 0; while (true) { var byte = this.readBits(8); value |= (byte & 0x7F) << shift; shift += 7; if ((byte & 0x80) === 0 || shift == m) { return value; } } }; BitStream.prototype.readUBitVar = function() { // Thanks to Robin Dietrich for providing a clean version of this code :-) // The header looks like this: [XY00001111222233333333333333333333] where everything > 0 is optional. // The first 2 bits (X and Y) tell us how much (if any) to read other than the 6 initial bits: // Y set -> read 4 // X set -> read 8 // X + Y set -> read 28 var v = this.readBits(6); //bitwise & 0x30 (0b110000) (determines whether the first two bits are set) switch (v & 0x30) { case 0x10: v = (v & 15) | (this.readBits(4) << 4); break; case 0x20: v = (v & 15) | (this.readBits(8) << 4); break; case 0x30: v = (v & 15) | (this.readBits(28) << 4); break; } return v; }; module.exports = BitStream; }).call(this,require("buffer").Buffer) },{"buffer":28,"long":33}],2:[function(require,module,exports){ (function (global,Buffer){ /** * Class creating a Source 2 Dota 2 replay parser **/ var ProtoBuf = require('protobufjs'); var snappy = require('./snappy'); var BitStream = require('./BitStream'); var EventEmitter = require('events').EventEmitter; var async = require('async'); var stream = require('stream'); var types = require('./build/types.json'); var protos = require('./build/protos.json'); var packetTypes = types.packets; var demTypes = types.dems; //read the protobufs and build a dota object for reference var builder = ProtoBuf.newBuilder(); ProtoBuf.loadJson(protos, builder); var dota = builder.build(); //CDemoSignonPacket is a special case and should be decoded with CDemoPacket since it doesn't have its own protobuf //it appears that things like the gameeventlist and createstringtables calls are here? dota["CDemoSignonPacket"] = dota["CDemoPacket"]; //console.error(Object.keys(dota)); var Parser = function(input) { //if a JS ArrayBuffer, convert to native node buffer if (input.byteLength) { var buffer = new Buffer(input.byteLength); var view = new Uint8Array(input); for (var i = 0; i < buffer.length; i++) { buffer[i] = view[i]; } input = buffer; } //wrap a passed buffer in a stream if (Buffer.isBuffer(input)) { var bufferStream = new stream.PassThrough(); bufferStream.end(input); input = bufferStream; } var stop = false; var p = new EventEmitter(); //expose the gameeventdescriptor, stringtables, types, entities to the user and have the parser update them as it parses p.types = types; p.game_event_descriptors = {}; p.string_tables = { tables: [], byName: {} }; p.entities = {}; p.start = function start(cb) { input.on('end', function() { stop = true; input.removeAllListeners(); return cb(); }); async.series({ "header": function(cb) { readString(8, function(err, header) { //verify the file magic number is correct cb(err || header.toString() !== "PBDEMS2\0", header); }); }, //two uint32s related to replay size "size1": readUint32, "size2": readUint32, "demo": function(cb) { //keep parsing demo messages until it hits a stop condition async.until(function() { return stop; }, readDemoMessage, cb); } }, cb); }; /** * Internal listeners to automatically process certain packets. * We abstract this away from the user so they don't need to worry about it. * For optimal speed we could allow the user to disable the ones they don't need **/ p.on("CDemoStop", function(data) { //don't stop on CDemoStop since some replays have CDemoGameInfo after it //stop = true; }); //p.on("CDemoStringTables", readCDemoStringTables); p.on("CDemoSignonPacket", readCDemoPacket); p.on("CDemoPacket", readCDemoPacket); p.on("CDemoFullPacket", function(data) { //console.error(data); //readCDemoStringTables(data.string_table); readCDemoPacket(data.packet); }); //string tables may mutate over the lifetime of the replay. //Therefore we listen for create/update events and modify the table as needed. p.on("CSVCMsg_CreateStringTable", createStringTable); p.on("CSVCMsg_UpdateStringTable", updateStringTable); //this packet sets up our game event descriptors p.on("CMsgSource1LegacyGameEventList", function(data) { //console.error(data); var gameEventDescriptors = p.game_event_descriptors; for (var i = 0; i < data.descriptors.length; i++) { gameEventDescriptors[data.descriptors[i].eventid] = data.descriptors[i]; } }); //TODO entities. huffman trees, property decoding?! requires parsing CDemoClassInfo, and instancebaseline string table? return p; /** * Reads the next DEM message from the replay (outer message) **/ function readDemoMessage(cb) { async.series({ command: readVarint32, tick: readVarint32, size: readVarint32 }, function(err, result) { if (err) { return cb(err); } readBytes(result.size, function(err, buf) { // Read a command header, which includes both the message type // well as a flag to determine whether or not whether or not the // message is compressed with snappy. var command = result.command; var tick = result.tick; var size = result.size; // Extract the type and compressed flag out of the command //msgType: = int32(command & ^ dota.EDemoCommands_DEM_IsCompressed) //msgCompressed: = (command & dota.EDemoCommands_DEM_IsCompressed) == dota.EDemoCommands_DEM_IsCompressed var demType = command & ~dota.EDemoCommands.DEM_IsCompressed; var isCompressed = (command & dota.EDemoCommands.DEM_IsCompressed) === dota.EDemoCommands.DEM_IsCompressed; // Read the tick that the message corresponds with. //tick: = p.reader.readVarUint32() // This appears to actually be an int32, where a -1 means pre-game. /* if tick == 4294967295 { tick = 0 } */ if (tick === 4294967295) { tick = 0; } if (isCompressed) { buf = snappy.uncompressSync(buf); } var dem = { tick: tick, type: demType, size: size, data: buf }; //console.error(dem); if (demType in demTypes) { //lookup the name of the protobuf message to decode with var name = demTypes[demType]; if (dota[name]) { if (listening(name)) { dem.data = dota[name].decode(dem.data); dem.data.proto_name = name; p.emit("*", dem.data); p.emit(name, dem.data); } } else { console.error("no proto definition for dem type %s", demType); } } else { console.error("no proto name for dem type %s", demType); } return cb(err); }); }); } // Internal parser for callback OnCDemoPacket, responsible for extracting // multiple inner packets from a single CDemoPacket. This is the main structure // that contains all other data types in the demo file. function readCDemoPacket(msg) { /* message CDemoPacket { optional int32 sequence_in = 1; optional int32 sequence_out_ack = 2; optional bytes data = 3; } */ var priorities = { "CNETMsg_Tick": -10, "CSVCMsg_CreateStringTable": -10, "CSVCMsg_UpdateStringTable": -10, "CNETMsg_SpawnGroup_Load": -10, "CSVCMsg_PacketEntities": 5, "CMsgSource1LegacyGameEvent": 10 }; //the inner data of a CDemoPacket is raw bits (no longer byte aligned!) var packets = []; //extract the native buffer from the ByteBuffer decoded by protobufjs //rewrap it in a new Buffer to force usage of node buffer shim rather than ArrayBuffer when in browser var buf = new Buffer(msg.data.toBuffer()); //convert the buffer object into a bitstream so we can read bits from it var bs = new BitStream(buf); //read until less than 8 bits left while (bs.limit - bs.offset >= 8) { var t = bs.readUBitVar(); var s = bs.readVarUInt(); var d = bs.readBuffer(s * 8); var pack = { type: t, size: s, data: d, position: packets.length }; packets.push(pack); } //sort the inner packets by priority in order to ensure we parse dependent packets last packets.sort(function(a, b) { //we must use a stable sort here in order to preserve order of packets when possible (for example, string tables) var p1 = priorities[packetTypes[a.type]] || 0; var p2 = priorities[packetTypes[b.type]] || 0; if (p1 === p2) { return a.position - b.position; } return p1 - p2; }); for (var i = 0; i < packets.length; i++) { var packet = packets[i]; var packType = packet.type; if (packType in packetTypes) { //lookup the name of the proto message for this packet type var name = packetTypes[packType]; if (dota[name]) { if (listening(name)) { packet.data = dota[name].decode(packet.data); packet.data.proto_name = name; p.emit("*", packet.data); p.emit(name, packet.data); } } else { console.error("no proto definition for packet name %s", name); } } else { console.error("no proto name for packet type %s", packType); } } } function createStringTable(data) { //create a stringtable //console.error(data); //extract the native buffer from the string_data ByteBuffer, with the offset removed var buf = new Buffer(data.string_data.toBuffer()); if (data.data_compressed) { //decompress the string data with snappy //early source 2 replays may use LZSS, we can detect this by reading the first four bytes of buffer buf = snappy.uncompressSync(buf); } //pass the buffer and parse string table data from it var items = parseStringTableData(buf, data.num_entries, data.user_data_fixed_size, data.user_data_size); //console.error(items); //remove the buf and replace with items, which is a decoded version of it data.string_data = {}; // Insert the items into the table as an object items.forEach(function(it) { data.string_data[it.index] = it; }); /* // Apply the updates to baseline state if t.name == "instancebaseline" { p.updateInstanceBaseline() } */ p.string_tables.byName[data.name] = data; p.string_tables.tables.push(data); } function updateStringTable(data) { //update a string table //retrieve table by id var table = p.string_tables.tables[data.table_id]; //extract native buffer var buf = new Buffer(data.string_data.toBuffer()); if (table) { var items = parseStringTableData(buf, data.num_changed_entries, table.user_data_fixed_size, table.user_data_size); var string_data = table.string_data; items.forEach(function(it) { //console.error(it); if (!string_data[it.index]) { //we don't have this item in the string table yet, add it string_data[it.index] = it; } else { //we're updating an existing item //only update key if the new key is not blank if (it.key) { //console.error("updating key %s->%s at index %s on %s, id %s", string_data[it.index].key, it.key, it.index, table.name, data.table_id); string_data[it.index].key = it.key; //string_data[it.index].key = [].concat(string_data[it.index].key).concat(it.key); } //only update value if the new item has a nonempty value buffer if (it.value.length) { //console.error("updating value length %s->%s at index %s on %s", string_data[it.index].value.length, it.value.length, it.index, table.name); string_data[it.index].value = it.value; } } }); } else { throw "string table doesn't exist!"; } /* // Apply the updates to baseline state if t.name == "instancebaseline" { p.updateInstanceBaseline() } */ } /** * Parses a buffer of string table data and returns an array of decoded items **/ function parseStringTableData(buf, num_entries, userDataFixedSize, userDataSize) { // Some tables have no data if (!buf.length) { return []; } var items = []; var bs = new BitStream(buf); // Start with an index of -1. // If the first item is at index 0 it will use a incr operation. var index = -1; var STRINGTABLE_KEY_HISTORY_SIZE = 32; // Maintain a list of key history // each entry is a string var keyHistory = []; // Loop through entries in the data structure // Each entry is a tuple consisting of {index, key, value} // Index can either be incremented from the previous position or overwritten with a given entry. // Key may be omitted (will be represented here as "") // Value may be omitted for (var i = 0; i < num_entries; i++) { var key = null; var value = new Buffer(0); // Read a boolean to determine whether the operation is an increment or // has a fixed index position. A fixed index position of zero should be // the last data in the buffer, and indicates that all data has been read. var incr = bs.readBoolean(); if (incr) { index += 1; } else { index = bs.readVarUInt() + 1; } // Some values have keys, some don't. var hasKey = bs.readBoolean(); if (hasKey) { // Some entries use reference a position in the key history for // part of the key. If referencing the history, read the position // and size from the buffer, then use those to build the string // combined with an extra string read (null terminated). // Alternatively, just read the string. var useHistory = bs.readBoolean(); if (useHistory) { var pos = bs.readBits(5); var size = bs.readBits(5); if (pos >= keyHistory.length) { //history doesn't have this position, just read key = bs.readNullTerminatedString(); } else { var s = keyHistory[pos]; if (size > s.length) { //our target size is longer than the key stored in history //pad the remaining size with a null terminated string from stream key = (s + bs.readNullTerminatedString()); } else { //we only want a piece of the historical string, slice it out and read the null terminator key = s.slice(0, size) + bs.readNullTerminatedString(); } } } else { //don't use the history, just read the string key = bs.readNullTerminatedString(); } keyHistory.push(key); if (keyHistory.length > STRINGTABLE_KEY_HISTORY_SIZE) { //drop the oldest key if we hit the cap keyHistory.shift(); } } // Some entries have a value. var hasValue = bs.readBoolean(); if (hasValue) { // Values can be either fixed size (with a size specified in // bits during table creation, or have a variable size with // a 14-bit prefixed size. if (userDataFixedSize) { value = bs.readBuffer(userDataSize); } else { var valueSize = bs.readBits(14); //TODO mysterious 3 bits of data? bs.readBits(3); value = bs.readBuffer(valueSize * 8); } } items.push({ index: index, key: key, value: value }); } //console.error(keyHistory, items, num_entries); return items; } /** * Returns whether there is an attached listener for this message name. **/ function listening(name) { return p.listeners(name).length || p.listeners("*").length; } function readCDemoStringTables(data) { //rather than processing when we read this demo message, we want to create when we read the packet CSVCMsg_CreateStringTable //this packet is just emitted as a state dump at intervals return; } function readByte(cb) { readBytes(1, function(err, buf) { cb(err, buf.readInt8(0)); }); } function readString(size, cb) { readBytes(size, function(err, buf) { cb(err, buf.toString()); }); } function readUint32(cb) { readBytes(4, function(err, buf) { cb(err, buf.readUInt32LE(0)); }); } function readVarint32(cb) { readByte(function(err, tmp) { if (tmp >= 0) { return cb(err, tmp); } var result = tmp & 0x7f; readByte(function(err, tmp) { if (tmp >= 0) { result |= tmp << 7; return cb(err, result); } else { result |= (tmp & 0x7f) << 7; readByte(function(err, tmp) { if (tmp >= 0) { result |= tmp << 14; return cb(err, result); } else { result |= (tmp & 0x7f) << 14; readByte(function(err, tmp) { if (tmp >= 0) { result |= tmp << 21; return cb(err, result); } else { result |= (tmp & 0x7f) << 21; readByte(function(err, tmp) { result |= tmp << 28; if (tmp < 0) { err = "malformed varint detected"; } return cb(err, result); }); } }); } }); } }); }); } function readBytes(size, cb) { if (!size) { //return an empty buffer if reading 0 bytes return cb(null, new Buffer("")); } var buf = input.read(size); if (buf) { return cb(null, buf); } else { input.once('readable', function() { return readBytes(size, cb); }); } } }; global.Parser = Parser; module.exports = Parser; }).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {},require("buffer").Buffer) },{"./BitStream":1,"./build/protos.json":3,"./build/types.json":4,"./snappy":35,"async":5,"buffer":28,"events":8,"protobufjs":34,"stream":26}],3:[function(require,module,exports){ module.exports={ "package": null, "options": { "optimize_for": "SPEED", "cc_generic_services": false }, "messages": [ { "name": "CMsgProtoBufHeader", "options": { "(msgpool_soft_limit)": 256, "(msgpool_hard_limit)": 1024 }, "fields": [ { "rule": "optional", "type": "fixed64", "name": "client_steam_id", "id": 1 }, { "rule": "optional", "type": "int32", "name": "client_session_id", "id": 2 }, { "rule": "optional", "type": "uint32", "name": "source_app_id", "id": 3 }, { "rule": "optional", "type": "fixed64", "name": "job_id_source", "id": 10, "options": { "default": 18446744073709552000 } }, { "rule": "optional", "type": "fixed64", "name": "job_id_target", "id": 11, "options": { "default": 18446744073709552000 } }, { "rule": "optional", "type": "string", "name": "target_job_name", "id": 12 }, { "rule": "optional", "type": "int32", "name": "eresult", "id": 13, "options": { "default": 2 } }, { "rule": "optional", "type": "string", "name": "error_message", "id": 14 }, { "rule": "optional", "type": "GCProtoBufMsgSrc", "name": "gc_msg_src", "id": 200, "options": { "default": "GCProtoBufMsgSrc_Unspecified" } }, { "rule": "optional", "type": "uint32", "name": "gc_dir_index_source", "id": 201 } ] }, { "name": "CMsgWebAPIKey", "fields": [ { "rule": "optional", "type": "uint32", "name": "status", "id": 1, "options": { "default": 255 } }, { "rule": "optional", "type": "uint32", "name": "account_id", "id": 2, "options": { "default": 0 } }, { "rule": "optional", "type": "uint32", "name": "publisher_group_id", "id": 3, "options": { "default": 0 } }, { "rule": "optional", "type": "uint32", "name": "key_id", "id": 4 }, { "rule": "optional", "type": "string", "name": "domain", "id": 5 } ] }, { "name": "CMsgHttpRequest", "fields": [ { "rule": "optional", "type": "uint32", "name": "request_method", "id": 1 }, { "rule": "optional", "type": "string", "name": "hostname", "id": 2 }, { "rule": "optional", "type": "string", "name": "url", "id": 3 }, { "rule": "repeated", "type": "RequestHeader", "name": "headers", "id": 4 }, { "rule": "repeated", "type": "QueryParam", "name": "get_params", "id": 5 }, { "rule": "repeated", "type": "QueryParam", "name": "post_params", "id": 6 }, { "rule": "optional", "type": "bytes", "name": "body", "id": 7 }, { "rule": "optional", "type": "uint32", "name": "absolute_timeout", "id": 8 } ], "messages": [ { "name": "RequestHeader", "fields": [ { "rule": "optional", "type": "string", "name": "name", "id": 1 }, { "rule": "optional", "type": "string", "name": "value", "id": 2 } ] }, { "name": "QueryParam", "fields": [ { "rule": "optional", "type": "string", "name": "name", "id": 1 }, { "rule": "optional", "type": "bytes", "name": "value", "id": 2 } ] } ] }, { "name": "CMsgWebAPIRequest", "fields": [ { "rule": "optional", "type": "string", "name": "UNUSED_job_name", "id": 1 }, { "rule": "optional", "type": "string", "name": "interface_name", "id": 2 }, { "rule": "optional", "type": "string", "name": "method_name", "id": 3 }, { "rule": "optional", "type": "uint32", "name": "version", "id": 4 }, { "rule": "optional", "type": "CMsgWebAPIKey", "name": "api_key", "id": 5 }, { "rule": "optional", "type": "CMsgHttpRequest", "name": "request", "id": 6 }, { "rule": "optional", "type": "uint32", "name": "routing_app_id", "id": 7 } ] }, { "name": "CMsgHttpResponse", "fields": [ { "rule": "optional", "type": "uint32", "name": "status_code", "id": 1 }, { "rule": "repeated", "type": "ResponseHeader", "name": "headers", "id": 2 }, { "rule": "optional", "type": "bytes", "name": "body", "id": 3 } ], "messages": [ { "name": "ResponseHeader", "fields": [ { "rule": "optional", "type": "string", "name": "name", "id": 1 }, { "rule": "optional", "type": "string", "name": "value", "id": 2 } ] } ] }, { "name": "CMsgAMFindAccounts", "fields": [ { "rule": "optional", "type": "uint32", "name": "search_type", "id": 1 }, { "rule": "optional", "type": "string", "name": "search_string", "id": 2 } ] }, { "name": "CMsgAMFindAccountsResponse", "fields": [ { "rule": "repeated", "type": "fixed64", "name": "steam_id", "id": 1 } ] }, { "name": "CMsgNotifyWatchdog", "fields": [ { "rule": "optional", "type": "uint32", "name": "source", "id": 1 }, { "rule": "optional", "type": "uint32", "name": "alert_type", "id": 2 }, { "rule": "optional", "type": "uint32", "name": "alert_destination", "id": 3 }, { "rule": "optional", "type": "bool", "name": "critical", "id": 4 }, { "rule": "optional", "type": "uint32", "name": "time", "id": 5 }, { "rule": "optional", "type": "uint32", "name": "appid", "id": 6 }, { "rule": "optional", "type": "string", "name": "text", "id": 7 } ] }, { "name": "CMsgAMGetLicenses", "fields": [ { "rule": "optional", "type": "fixed64", "name": "steamid", "id": 1 } ] }, { "name": "CMsgPackageLicense", "fields": [ { "rule": "optional", "type": "uint32", "name": "package_id", "id": 1 }, { "rule": "optional", "type": "uint32", "name": "time_created", "id": 2 }, { "rule": "optional", "type": "uint32", "name": "owner_id", "id": 3 } ] }, { "name": "CMsgAMGetLicensesResponse", "fields": [ { "rule": "repeated", "type": "CMsgPackageLicense", "name": "license", "id": 1 }, { "rule": "optional", "type": "uint32", "name": "result", "id": 2 } ] }, { "name": "CMsgAMGetUserGameStats", "fields": [ { "rule": "optional", "type": "fixed64", "name": "steam_id", "id": 1 }, { "rule": "optional", "type": "fixed64", "name": "game_id", "id": 2 }, { "rule": "repeated", "type": "uint32", "name": "stats", "id": 3 } ] }, { "name": "CMsgAMGetUserGameStatsResponse", "fields": [ { "rule": "optional", "type": "fixed64", "name": "steam_id", "id": 1 }, { "rule": "optional", "type": "fixed64", "name": "game_id", "id": 2 }, { "rule": "optional", "type": "int32", "name": "eresult", "id": 3, "options": { "default": 2 } }, { "rule": "repeated", "type": "Stats", "name": "stats", "id": 4 }, { "rule": "repeated", "type": "Achievement_Blocks", "name": "achievement_blocks", "id": 5 } ], "messages": [ { "name": "Stats", "fields": [ { "rule": "optional", "type": "uint32", "name": "stat_id", "id": 1 }, { "rule": "optional", "type": "uint32", "name": "stat_value", "id": 2 } ] }, { "name": "Achievement_Blocks", "fields": [ { "rule": "optional", "type": "uint32", "name": "achievement_id", "id": 1 }, { "rule": "optional", "type": "uint32", "name": "achievement_bit_id", "id": 2 }, { "rule": "optional", "type": "fixed32", "name": "unlock_time", "id": 3 } ] } ] }, { "name": "CMsgGCGetCommandList", "fields": [ { "rule": "optional", "type": "uint32", "name": "app_id", "id": 1 }, { "rule": "optional", "type": "string", "name": "command_prefix", "id": 2 } ] }, { "name": "CMsgGCGetCommandListResponse", "fields": [ { "rule": "repeated", "type": "string", "name": "command_name", "id": 1 } ] }, { "name": "CGCMsgMemCachedGet", "fields": [ { "rule": "repeated", "type": "string", "name": "keys", "id": 1 } ] }, { "name": "CGCMsgMemCachedGetResponse", "fields": [ { "rule": "repeated", "type": "ValueTag", "name": "values", "id": 1 } ], "messages": [ { "name": "ValueTag", "fields": [ { "rule": "optional", "type": "bool", "name": "found", "id": 1 }, { "rule": "optional", "type": "bytes", "name": "value", "id": 2 } ] } ] }, { "name": "CGCMsgMemCachedSet", "fields": [ { "rule": "repeated", "type": "KeyPair", "name": "keys", "id": 1 } ], "messages": [ { "name": "KeyPair", "fields": [ { "rule": "optional", "type": "string", "name": "name", "id": 1 }, { "rule": "optional", "type": "bytes", "name": "value", "id": 2 } ] } ] }, { "name": "CGCMsgMemCachedDelete", "fields": [ { "rule": "repeated", "type": "string", "name": "keys", "id": 1 } ] }, { "name": "CGCMsgMemCachedStats", "fields": [] }, { "name": "CGCMsgMemCachedStatsResponse", "fields": [ { "rule": "optional", "type": "uint64", "name": "curr_connections", "id": 1 }, { "rule": "optional", "type": "uint64", "name": "cmd_get", "id": 2 }, { "rule": "optional", "type": "uint64", "name": "cmd_set", "id": 3 }, { "rule": "optional", "type": "uint64", "name": "cmd_flush", "id": 4 }, { "rule": "optional", "type": "uint64", "name": "get_hits", "id": 5 }, { "rule": "optional", "type": "uint64", "name": "get_misses", "id": 6 }, { "rule": "optional", "type": "uint64", "name": "delete_hits", "id": 7 }, { "rule": "optional", "type": "uint64", "name": "delete_misses", "id": 8 }, { "rule": "optional", "type": "uint64", "name": "bytes_read", "id": 9 }, { "rule": "optional", "type": "uint64", "name": "bytes_written", "id": 10 }, { "rule": "optional", "type": "uint64", "name": "limit_maxbytes", "id": 11 }, { "rule": "optional", "type": "uint64", "name": "curr_items", "id": 12 }, { "rule": "optional", "type": "uint64", "name": "evictions", "id": 13 }, { "rule": "optional", "type": "uint64", "name": "bytes", "id": 14 } ] }, { "name": "CGCMsgSQLStats", "fields": [ { "rule": "optional", "type": "uint32", "name": "schema_catalog", "id": 1 } ] }, { "name": "CGCMsgSQLStatsResponse", "fields": [ { "rule": "optional", "type": "uint32", "name": "threads", "id": 1 }, { "rule": "optional", "type": "uint32", "name": "threads_connected", "id": 2 }, { "rule": "optional", "type": "uint32", "name": "threads_active", "id": 3 }, { "rule": "optional", "type": "uint32", "name": "operations_su