1
0
Fork 0
mirror of https://github.com/demostf/demo.js synced 2026-06-03 16:44:12 +02:00

encoder for createStringTable

This commit is contained in:
Robin Appelman 2017-08-13 21:32:18 +02:00
commit 89b9c3b25c
10 changed files with 276 additions and 77 deletions

View file

@ -4,7 +4,7 @@ import {handleGameEvent} from '../PacketHandler/GameEvent';
import {handleGameEventList} from '../PacketHandler/GameEventList';
import {handlePacketEntities} from '../PacketHandler/PacketEntities';
import {handleSayText2} from '../PacketHandler/SayText2';
import {handleStringTable} from '../PacketHandler/StringTable';
import {handleStringTable, handleStringTables} from '../PacketHandler/StringTable';
import {Building} from './Building';
import {Death} from './Death';
import {GameEventDefinitionMap} from './GameEvent';
@ -146,6 +146,10 @@ export class Match {
handleDataTable(packet, this);
break;
case 'stringTable':
handleStringTables(packet, this);
break;
case 'createStringTable':
case 'updateStringTable':
handleStringTable(packet, this);
break;
case 'gameEventList':

View file

@ -11,6 +11,16 @@ export interface StringTablePacket {
tables: StringTable[];
}
export interface CreateStringTablePacket {
packetType: 'createStringTable';
table: StringTable;
}
export interface UpdateStringTablePacket {
packetType: 'updateStringTable';
table: StringTable;
}
export interface ConsoleCmdPacket {
packetType: 'consoleCmd';
command: string;
@ -81,7 +91,7 @@ export interface ParseSoundsPacket {
export interface SetConVarPacket {
packetType: 'setConVar';
vars: {[key: string]: string};
vars: { [key: string]: string };
}
export interface TempEntitiesPacket {
@ -142,6 +152,8 @@ export type UserMessagePacket = SayText2Packet | TextMessagePacket | UnknownUser
export type Packet = BSPDecalPacket |
StringTablePacket |
CreateStringTablePacket |
UpdateStringTablePacket |
DataTablePacket |
ClassInfoPacket |
EntityMessagePacket |

View file

@ -1,34 +1,42 @@
import {Match} from '../Data/Match';
import {StringTablePacket} from '../Data/Packet';
import {StringTableEntry} from '../Data/StringTable';
import {CreateStringTablePacket, StringTablePacket, UpdateStringTablePacket} from '../Data/Packet';
import {StringTable, StringTableEntry} from '../Data/StringTable';
export function handleStringTable(packet: StringTablePacket, match: Match) {
export function handleStringTable(packet: CreateStringTablePacket | UpdateStringTablePacket, match: Match) {
handleTable(packet.table, match);
}
export function handleStringTables(packet: StringTablePacket, match: Match) {
for (const table of packet.tables) {
if (!match.getStringTable(table.name)) {
match.stringTables.push(table);
}
if (table.name === 'userinfo') {
for (const userData of table.entries) {
if (userData.extraData) {
if (userData.extraData.bitsLeft > (32 * 8)) {
const name = userData.extraData.readUTF8String(32);
const userId = userData.extraData.readUint32();
const steamId = userData.extraData.readUTF8String();
if (steamId) {
const userState = match.getUserInfo(userId);
userState.name = name;
userState.steamId = steamId;
userState.entityId = parseInt(userData.text, 10) + 1;
}
handleTable(table, match);
}
}
function handleTable(table: StringTable, match: Match) {
if (!match.getStringTable(table.name)) {
match.stringTables.push(table);
}
if (table.name === 'userinfo') {
for (const userData of table.entries) {
if (userData.extraData) {
if (userData.extraData.bitsLeft > (32 * 8)) {
const name = userData.extraData.readUTF8String(32);
const userId = userData.extraData.readUint32();
const steamId = userData.extraData.readUTF8String();
if (steamId) {
const userState = match.getUserInfo(userId);
userState.name = name;
userState.steamId = steamId;
userState.entityId = parseInt(userData.text, 10) + 1;
}
}
}
}
if (table.name === 'instancebaseline') {
for (const instanceBaseLine of table.entries) {
if (instanceBaseLine) {
saveInstanceBaseLine(instanceBaseLine, match);
}
}
if (table.name === 'instancebaseline') {
for (const instanceBaseLine of table.entries) {
if (instanceBaseLine) {
saveInstanceBaseLine(instanceBaseLine, match);
}
}
}

View file

@ -3,7 +3,7 @@ import {make} from '../Packet/ParserGenerator';
import {ParseBSPDecal} from '../Packet/BSPDecal';
import {EncodeClassInfo, ParseClassInfo} from '../Packet/ClassInfo';
import {ParseCmdKeyValues} from '../Packet/CmdKeyValues';
import {ParseCreateStringTable} from '../Packet/CreateStringTable';
import {EncodeCreateStringTable, ParseCreateStringTable} from '../Packet/CreateStringTable';
import {ParseEntityMessage} from '../Packet/EntityMessage';
import {ParseGameEvent} from '../Packet/GameEvent';
import {ParseGameEventList} from '../Packet/GameEventList';
@ -41,7 +41,7 @@ export class Packet extends Parser {
'game{s}map{s}skybox{s}serverName{s}replay{b}'),
10: {parser: ParseClassInfo, encoder: EncodeClassInfo},
11: make('setPause', 'paused{b}'),
12: {parser: ParseCreateStringTable, encoder: voidEncoder},
12: {parser: ParseCreateStringTable, encoder: EncodeCreateStringTable},
13: {parser: ParseUpdateStringTable, encoder: voidEncoder},
14: {parser: ParseVoiceInit, encoder: voidEncoder},
15: {parser: ParseVoiceData, encoder: voidEncoder},

View file

@ -10,13 +10,13 @@ export class StringTable extends Parser {
// tables: []
// }];
// https://github.com/StatsHelix/demoinfo/blob/3d28ea917c3d44d987b98bb8f976f1a3fcc19821/DemoInfo/ST/StringTableParser.cs
const tableCount = this.stream.readUint8();
const tableCount = this.stream.readUint8();
const tables: StringTableObject[] = [];
let extraDataLength;
for (let i = 0; i < tableCount; i++) {
const entries: StringTableEntry[] = [];
const tableName = this.stream.readASCIIString();
const entryCount = this.stream.readUint16();
const tableName = this.stream.readASCIIString();
const entryCount = this.stream.readUint16();
for (let j = 0; j < entryCount; j++) {
let entry: StringTableEntry;
try {
@ -45,7 +45,7 @@ export class StringTable extends Parser {
}
const table: StringTableObject = {
entries,
name: tableName,
name: tableName,
maxEntries: entryCount,
};
tables.push(table);

View file

@ -1,27 +1,26 @@
import {BitStream} from 'bit-buffer';
import {StringTablePacket} from '../../Data/Packet';
import {CreateStringTablePacket} from '../../Data/Packet';
import {logBase2} from '../../Math';
import {readVarInt} from '../readBitVar';
import {readVarInt, writeVarInt} from '../readBitVar';
import {uncompress} from 'snappyjs';
import {Match} from '../../Data/Match';
import {StringTable} from '../../Data/StringTable';
import {parseStringTable} from '../StringTableParser';
import {encodeStringTableEntries, guessStringTableEntryLength, parseStringTableEntries} from '../StringTableParser';
export function ParseCreateStringTable(stream: BitStream, match: Match): StringTablePacket { // 12: createStringTable
const tableName = stream.readASCIIString();
const maxEntries = stream.readUint16();
const encodeBits = logBase2(maxEntries);
export function ParseCreateStringTable(stream: BitStream): CreateStringTablePacket { // 12: createStringTable
const tableName = stream.readASCIIString();
const maxEntries = stream.readUint16();
const encodeBits = logBase2(maxEntries);
const entityCount = stream.readBits(encodeBits + 1);
const bitCount = readVarInt(stream);
let userDataSize = 0;
let userDataSize = 0;
let userDataSizeBits = 0;
// userdata fixed size
if (stream.readBoolean()) {
userDataSize = stream.readBits(12);
userDataSize = stream.readBits(12);
userDataSizeBits = stream.readBits(4);
}
@ -31,7 +30,7 @@ export function ParseCreateStringTable(stream: BitStream, match: Match): StringT
if (isCompressed) {
const decompressedByteSize = data.readUint32();
const compressedByteSize = data.readUint32();
const compressedByteSize = data.readUint32();
const magic = data.readASCIIString(4);
@ -50,17 +49,45 @@ export function ParseCreateStringTable(stream: BitStream, match: Match): StringT
}
const table: StringTable = {
name: tableName,
entries: [],
name: tableName,
entries: [],
maxEntries,
fixedUserDataSize: userDataSize,
fixedUserDataSize: userDataSize,
fixedUserDataSizeBits: userDataSizeBits,
};
parseStringTable(data, table, entityCount, match);
table.entries = parseStringTableEntries(data, table, entityCount);
return {
packetType: 'stringTable',
tables: [table],
packetType: 'createStringTable',
table: table,
};
}
export function EncodeCreateStringTable(packet: CreateStringTablePacket, stream: BitStream) {
stream.writeASCIIString(packet.table.name);
stream.writeUint16(packet.table.maxEntries);
const encodeBits = logBase2(packet.table.maxEntries);
stream.writeBits(packet.table.entries.length, encodeBits + 1);
const entryData = new BitStream(new ArrayBuffer(guessStringTableEntryLength(packet.table)));
encodeStringTableEntries(entryData, packet.table);
const entryLength = entryData.index;
entryData.index = 0;
writeVarInt(entryLength, stream);
if (packet.table.fixedUserDataSize && packet.table.fixedUserDataSizeBits) {
stream.writeBoolean(true);
stream.writeBits(packet.table.fixedUserDataSize, 12);
stream.writeBits(packet.table.fixedUserDataSizeBits, 4);
} else {
stream.writeBoolean(false);
}
// we never compress table data
stream.writeBoolean(false);
stream.writeBitStream(entryData, entryLength);
}

View file

@ -1,26 +1,32 @@
import {BitStream} from 'bit-buffer';
import {Match} from '../../Data/Match';
import {StringTablePacket} from '../../Data/Packet';
import {parseStringTable} from '../StringTableParser';
import {UpdateStringTablePacket} from '../../Data/Packet';
import {parseStringTableEntries} from '../StringTableParser';
export function ParseUpdateStringTable(stream: BitStream, match: Match): StringTablePacket { // 12: updateStringTable
export function ParseUpdateStringTable(stream: BitStream, match: Match): UpdateStringTablePacket { // 12: updateStringTable
const tableId = stream.readBits(5);
const multipleChanged = stream.readBoolean();
const changedEntries = (multipleChanged) ? stream.readBits(16) : 1;
const changedEntries = (multipleChanged) ? stream.readBits(16) : 1;
const bitCount = stream.readBits(20);
const data = stream.readBitStream(bitCount);
const data = stream.readBitStream(bitCount);
if (!match.stringTables[tableId]) {
throw new Error('Table not found for update');
}
const table = match.stringTables[tableId];
parseStringTable(data, table, changedEntries, match);
const updatedEntries = parseStringTableEntries(data, table, changedEntries, table.entries);
for (let i = 0; i < updatedEntries.length; i++) {
if (updatedEntries[i]) {
table.entries[i] = updatedEntries[i];
}
}
return {
packetType: 'stringTable',
tables: [table],
packetType: 'updateStringTable',
table: table,
};
}

View file

@ -3,18 +3,15 @@ import {Match} from '../Data/Match';
import {StringTable, StringTableEntry} from '../Data/StringTable';
import {logBase2} from '../Math';
export function parseStringTable(stream: BitStream, table: StringTable, entries: number, match: Match) {
export function parseStringTableEntries(stream: BitStream, table: StringTable, entryCount: number, existingEntries: StringTableEntry[] = []): StringTableEntry[] {
const entryBits = logBase2(table.maxEntries);
let lastEntry = -1;
const entries: StringTableEntry[] = [];
let lastEntry = -1;
const history: StringTableEntry[] = [];
for (let i = 0; i < entries; i++) {
let entryIndex = lastEntry + 1;
if (!stream.readBoolean()) {
entryIndex = stream.readBits(entryBits);
}
for (let i = 0; i < entryCount; i++) {
const entryIndex = (!stream.readBoolean()) ? stream.readBits(entryBits) : lastEntry + 1;
lastEntry = entryIndex;
@ -28,7 +25,7 @@ export function parseStringTable(stream: BitStream, table: StringTable, entries:
const subStringCheck = stream.readBoolean();
if (subStringCheck) {
const index = stream.readBits(5);
const index = stream.readBits(5);
const bytesToCopy = stream.readBits(5);
const restOfString = stream.readASCIIString();
@ -43,19 +40,19 @@ export function parseStringTable(stream: BitStream, table: StringTable, entries:
}
}
let userData: BitStream|undefined;
let userData: BitStream | undefined;
if (stream.readBoolean()) {
if (table.fixedUserDataSize && table.fixedUserDataSizeBits) {
userData = stream.readBitStream(table.fixedUserDataSizeBits);
} else {
const userDataBytes = stream.readBits(14);
userData = stream.readBitStream(userDataBytes * 8);
userData = stream.readBitStream(userDataBytes * 8);
}
}
if (table.entries[entryIndex]) {
const existingEntry = table.entries[entryIndex];
if (existingEntries[entryIndex]) {
const existingEntry: StringTableEntry = {...existingEntries[entryIndex]};
if (userData) {
existingEntry.extraData = userData;
}
@ -63,16 +60,67 @@ export function parseStringTable(stream: BitStream, table: StringTable, entries:
if (value) {
existingEntry.text = value;
}
entries[entryIndex] = existingEntry;
history.push(existingEntry);
} else {
table.entries[entryIndex] = {
text: value,
entries[entryIndex] = {
text: value,
extraData: userData,
};
history.push(table.entries[entryIndex]);
console.log(entries[entryIndex]);
history.push(entries[entryIndex]);
}
if (history.length > 32) {
history.shift();
}
}
return entries;
}
export function guessStringTableEntryLength(table: StringTable): number {
// a rough guess of how many bytes are needed to encode the table entries
const entryBytes = Math.ceil(logBase2(table.maxEntries) / 8);
return table.entries.reduce((length: number, entry: StringTableEntry) => {
return length +
entryBytes +
1 + // misc boolean
entry.text.length + 1 + // +1 for null termination
(entry.extraData ? Math.ceil(entry.extraData.length / 8) : 0);
}, 1);
}
export function encodeStringTableEntries(stream: BitStream, table: StringTable) {
const entryBits = logBase2(table.maxEntries);
let lastIndex = -1;
for (let i = 0; i < table.entries.length; i++) {
if (table.entries[i]) {
const entry = table.entries[i];
if (i !== lastIndex) {
stream.writeBoolean(false);
stream.writeBits(i, entryBits);
} else {
stream.writeBoolean(true);
}
// we always encode a value
stream.writeBoolean(true);
// we don't encode substring optimizations
stream.writeBoolean(false);
stream.writeASCIIString(entry.text);
if (entry.extraData) {
stream.writeBoolean(true);
if (!table.fixedUserDataSizeBits) {
stream.writeBits(Math.ceil(entry.extraData.length / 8), 14);
}
stream.writeBitStream(entry.extraData);
entry.extraData.index = 0;
} else {
stream.writeBoolean(false);
}
}
}
}

View file

@ -0,0 +1,94 @@
import {BitStream} from 'bit-buffer';
import {assertEncoder, assertParser, getStream} from './PacketTest';
import {EncodeCreateStringTable, ParseCreateStringTable} from '../../../../Parser/Packet/CreateStringTable';
const exampleData = [
100,
111,
119,
110,
108,
111,
97,
100,
97,
98,
108,
101,
115,
0,
0,
32,
1,
0,
121,
0,
107,
11,
131,
155,
227,
130,
99,
251,
18,
11,
35,
187,
11,
163,
43,
147,
251,
130,
147,
123,
251,
178,
203,
113,
17,
155,
131,
3,
192];
const examplePacket = {
packetType: 'createStringTable',
table: {
name: 'downloadables',
entries: [{text: 'maps\\pl_badwater_pro_v9.bsp', extraData: undefined}],
maxEntries: 8192,
fixedUserDataSize: 0,
fixedUserDataSizeBits: 0
}
};
const examplePacket2 = {
packetType: 'createStringTable',
table: {
name: 'downloadables',
entries: [
{text: 'maps\\pl_badwater_pro_v9.bsp', extraData: undefined},
{text: 'foobar', extraData: undefined},
{text: 'assadasdas', extraData: undefined},
{text: 'foo', extraData: undefined}
],
maxEntries: 8192,
fixedUserDataSize: 0,
fixedUserDataSizeBits: 0
}
};
suite('CreateStringTable', () => {
test('Parse createStringTable', () => {
assertParser(ParseCreateStringTable, getStream(exampleData), examplePacket, 388);
});
test('Encode createStringTable', () => {
// more bits are used for encoding because we don't do a lot of compression tricks
assertEncoder(ParseCreateStringTable, EncodeCreateStringTable, examplePacket, 401);
assertEncoder(ParseCreateStringTable, EncodeCreateStringTable, examplePacket2, 628);
});
});

View file

@ -15,7 +15,7 @@ export function getStream(data: string | number[]) {
export type Encoder = (data: any, stream: BitStream) => void;
export function assertEncoder(parser: Parser, encoder: Encoder, data: any, length: number = 0) {
const stream = new BitStream(new ArrayBuffer(64));
const stream = new BitStream(new ArrayBuffer(Math.max(64, length * 8)));
encoder(data as Packet, stream);
@ -28,14 +28,14 @@ export function assertEncoder(parser: Parser, encoder: Encoder, data: any, lengt
stream.index = 0;
const result = parser(stream);
assert.deepEqual(data, result, 'Re-decoded value not equal to original value');
assert.equal(pos, stream.index, 'Number of bits used for encoding and parsing not equal');
assert.deepEqual(result, data, 'Re-decoded value not equal to original value');
assert.equal(stream.index, pos, 'Number of bits used for encoding and parsing not equal');
}
export type Parser = (stream: BitStream) => any;
export function assertParser(parser: Parser, stream: BitStream, expected: any, length: number) {
const start = stream.index;
assert.deepEqual(expected, parser(stream));
assert.deepEqual(parser(stream), expected);
assert.equal(stream.index - start, length, 'Unexpected number of bits consumed from stream');
}