1
0
Fork 0
mirror of https://github.com/demostf/demo.js synced 2026-06-04 00:54:14 +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 {handleGameEventList} from '../PacketHandler/GameEventList';
import {handlePacketEntities} from '../PacketHandler/PacketEntities'; import {handlePacketEntities} from '../PacketHandler/PacketEntities';
import {handleSayText2} from '../PacketHandler/SayText2'; import {handleSayText2} from '../PacketHandler/SayText2';
import {handleStringTable} from '../PacketHandler/StringTable'; import {handleStringTable, handleStringTables} from '../PacketHandler/StringTable';
import {Building} from './Building'; import {Building} from './Building';
import {Death} from './Death'; import {Death} from './Death';
import {GameEventDefinitionMap} from './GameEvent'; import {GameEventDefinitionMap} from './GameEvent';
@ -146,6 +146,10 @@ export class Match {
handleDataTable(packet, this); handleDataTable(packet, this);
break; break;
case 'stringTable': case 'stringTable':
handleStringTables(packet, this);
break;
case 'createStringTable':
case 'updateStringTable':
handleStringTable(packet, this); handleStringTable(packet, this);
break; break;
case 'gameEventList': case 'gameEventList':

View file

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

View file

@ -1,9 +1,18 @@
import {Match} from '../Data/Match'; import {Match} from '../Data/Match';
import {StringTablePacket} from '../Data/Packet'; import {CreateStringTablePacket, StringTablePacket, UpdateStringTablePacket} from '../Data/Packet';
import {StringTableEntry} from '../Data/StringTable'; 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) { for (const table of packet.tables) {
handleTable(table, match);
}
}
function handleTable(table: StringTable, match: Match) {
if (!match.getStringTable(table.name)) { if (!match.getStringTable(table.name)) {
match.stringTables.push(table); match.stringTables.push(table);
} }
@ -32,7 +41,6 @@ export function handleStringTable(packet: StringTablePacket, match: Match) {
} }
} }
} }
}
function saveInstanceBaseLine(entry: StringTableEntry, match: Match) { function saveInstanceBaseLine(entry: StringTableEntry, match: Match) {
if (entry.extraData) { if (entry.extraData) {

View file

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

View file

@ -1,14 +1,13 @@
import {BitStream} from 'bit-buffer'; import {BitStream} from 'bit-buffer';
import {StringTablePacket} from '../../Data/Packet'; import {CreateStringTablePacket} from '../../Data/Packet';
import {logBase2} from '../../Math'; import {logBase2} from '../../Math';
import {readVarInt} from '../readBitVar'; import {readVarInt, writeVarInt} from '../readBitVar';
import {uncompress} from 'snappyjs'; import {uncompress} from 'snappyjs';
import {Match} from '../../Data/Match';
import {StringTable} from '../../Data/StringTable'; 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 export function ParseCreateStringTable(stream: BitStream): CreateStringTablePacket { // 12: createStringTable
const tableName = stream.readASCIIString(); const tableName = stream.readASCIIString();
const maxEntries = stream.readUint16(); const maxEntries = stream.readUint16();
const encodeBits = logBase2(maxEntries); const encodeBits = logBase2(maxEntries);
@ -57,10 +56,38 @@ export function ParseCreateStringTable(stream: BitStream, match: Match): StringT
fixedUserDataSizeBits: userDataSizeBits, fixedUserDataSizeBits: userDataSizeBits,
}; };
parseStringTable(data, table, entityCount, match); table.entries = parseStringTableEntries(data, table, entityCount);
return { return {
packetType: 'stringTable', packetType: 'createStringTable',
tables: [table], 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,9 +1,9 @@
import {BitStream} from 'bit-buffer'; import {BitStream} from 'bit-buffer';
import {Match} from '../../Data/Match'; import {Match} from '../../Data/Match';
import {StringTablePacket} from '../../Data/Packet'; import {UpdateStringTablePacket} from '../../Data/Packet';
import {parseStringTable} from '../StringTableParser'; 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 tableId = stream.readBits(5);
const multipleChanged = stream.readBoolean(); const multipleChanged = stream.readBoolean();
@ -17,10 +17,16 @@ export function ParseUpdateStringTable(stream: BitStream, match: Match): StringT
} }
const table = match.stringTables[tableId]; 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 { return {
packetType: 'stringTable', packetType: 'updateStringTable',
tables: [table], table: table,
}; };
} }

View file

@ -3,18 +3,15 @@ import {Match} from '../Data/Match';
import {StringTable, StringTableEntry} from '../Data/StringTable'; import {StringTable, StringTableEntry} from '../Data/StringTable';
import {logBase2} from '../Math'; 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); const entryBits = logBase2(table.maxEntries);
const entries: StringTableEntry[] = [];
let lastEntry = -1; let lastEntry = -1;
const history: StringTableEntry[] = []; const history: StringTableEntry[] = [];
for (let i = 0; i < entries; i++) { for (let i = 0; i < entryCount; i++) {
let entryIndex = lastEntry + 1; const entryIndex = (!stream.readBoolean()) ? stream.readBits(entryBits) : lastEntry + 1;
if (!stream.readBoolean()) {
entryIndex = stream.readBits(entryBits);
}
lastEntry = entryIndex; lastEntry = entryIndex;
@ -54,8 +51,8 @@ export function parseStringTable(stream: BitStream, table: StringTable, entries:
} }
} }
if (table.entries[entryIndex]) { if (existingEntries[entryIndex]) {
const existingEntry = table.entries[entryIndex]; const existingEntry: StringTableEntry = {...existingEntries[entryIndex]};
if (userData) { if (userData) {
existingEntry.extraData = userData; existingEntry.extraData = userData;
} }
@ -63,16 +60,67 @@ export function parseStringTable(stream: BitStream, table: StringTable, entries:
if (value) { if (value) {
existingEntry.text = value; existingEntry.text = value;
} }
entries[entryIndex] = existingEntry;
history.push(existingEntry); history.push(existingEntry);
} else { } else {
table.entries[entryIndex] = { entries[entryIndex] = {
text: value, text: value,
extraData: userData, extraData: userData,
}; };
history.push(table.entries[entryIndex]); console.log(entries[entryIndex]);
history.push(entries[entryIndex]);
} }
if (history.length > 32) { if (history.length > 32) {
history.shift(); 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 type Encoder = (data: any, stream: BitStream) => void;
export function assertEncoder(parser: Parser, encoder: Encoder, data: any, length: number = 0) { 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); encoder(data as Packet, stream);
@ -28,14 +28,14 @@ export function assertEncoder(parser: Parser, encoder: Encoder, data: any, lengt
stream.index = 0; stream.index = 0;
const result = parser(stream); const result = parser(stream);
assert.deepEqual(data, result, 'Re-decoded value not equal to original value'); assert.deepEqual(result, data, 'Re-decoded value not equal to original value');
assert.equal(pos, stream.index, 'Number of bits used for encoding and parsing not equal'); assert.equal(stream.index, pos, 'Number of bits used for encoding and parsing not equal');
} }
export type Parser = (stream: BitStream) => any; export type Parser = (stream: BitStream) => any;
export function assertParser(parser: Parser, stream: BitStream, expected: any, length: number) { export function assertParser(parser: Parser, stream: BitStream, expected: any, length: number) {
const start = stream.index; 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'); assert.equal(stream.index - start, length, 'Unexpected number of bits consumed from stream');
} }