t2-mapper/relay/HuffmanWriter.ts
2026-03-09 12:38:40 -07:00

223 lines
5.9 KiB
TypeScript

import { BitStreamWriter } from "./BitStreamWriter.js";
/** Hardcoded character frequency table from the V12 engine (bitStream.cc). */
const CSM_CHAR_FREQS: number[] = [
0, 0, 0, 0, 0, 0, 0, 0, 0, 329, 21, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
2809, 68, 0, 27, 0, 58, 3, 62, 4, 7, 0, 0, 15, 65, 554, 3,
394, 404, 189, 117, 30, 51, 27, 15, 34, 32, 80, 1, 142, 3, 142, 39,
0, 144, 125, 44, 122, 275, 70, 135, 61, 127, 8, 12, 113, 246, 122, 36,
185, 1, 149, 309, 335, 12, 11, 14, 54, 151, 0, 0, 2, 0, 0, 211,
0, 2090, 344, 736, 993, 2872, 701, 605, 646, 1552, 328, 305, 1240, 735, 1533, 1713,
562, 3, 1775, 1149, 1469, 979, 407, 553, 59, 279, 31, 0, 0, 0, 68, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
];
const PROB_BOOST = 1;
function isAlphaNumeric(c: number): boolean {
return (
(c >= 48 && c <= 57) ||
(c >= 65 && c <= 90) ||
(c >= 97 && c <= 122)
);
}
interface HuffLeaf {
pop: number;
symbol: number;
numBits: number;
code: number;
}
interface HuffNode {
pop: number;
index0: number;
index1: number;
}
interface HuffWrap {
node: HuffNode | null;
leaf: HuffLeaf | null;
}
function wrapGetPop(w: HuffWrap): number {
return w.node ? w.node.pop : w.leaf!.pop;
}
let leaves: HuffLeaf[] = [];
let tablesBuilt = false;
function buildTables(): void {
if (tablesBuilt) return;
tablesBuilt = true;
leaves = [];
for (let i = 0; i < 256; i++) {
leaves.push({
pop: CSM_CHAR_FREQS[i] + (isAlphaNumeric(i) ? PROB_BOOST : 0) + PROB_BOOST,
symbol: i,
numBits: 0,
code: 0,
});
}
const nodes: HuffNode[] = [{ pop: 0, index0: 0, index1: 0 }];
let currWraps = 256;
const wraps: HuffWrap[] = [];
for (let i = 0; i < 256; i++) {
wraps.push({ node: null, leaf: leaves[i] });
}
while (currWraps !== 1) {
let min1 = 0xfffffffe;
let min2 = 0xffffffff;
let index1 = -1;
let index2 = -1;
for (let i = 0; i < currWraps; i++) {
const pop = wrapGetPop(wraps[i]);
if (pop < min1) {
min2 = min1;
index2 = index1;
min1 = pop;
index1 = i;
} else if (pop < min2) {
min2 = pop;
index2 = i;
}
}
const determineIndex = (wrap: HuffWrap): number => {
if (wrap.leaf !== null) {
return -(leaves.indexOf(wrap.leaf) + 1);
}
return nodes.indexOf(wrap.node!);
};
const newNode: HuffNode = {
pop: wrapGetPop(wraps[index1]) + wrapGetPop(wraps[index2]),
index0: determineIndex(wraps[index1]),
index1: determineIndex(wraps[index2]),
};
nodes.push(newNode);
const mergeIndex = index1 < index2 ? index1 : index2;
const nukeIndex = index1 > index2 ? index1 : index2;
wraps[mergeIndex] = { node: newNode, leaf: null };
if (nukeIndex !== currWraps - 1) {
wraps[nukeIndex] = wraps[currWraps - 1];
}
currWraps--;
}
nodes[0] = wraps[0].node!;
function generateCodes(code: number, nodeIndex: number, depth: number): void {
if (nodeIndex < 0) {
const leaf = leaves[-(nodeIndex + 1)];
leaf.code = code;
leaf.numBits = depth;
} else {
const node = nodes[nodeIndex];
generateCodes(code, node.index0, depth + 1);
generateCodes(code | (1 << depth), node.index1, depth + 1);
}
}
generateCodes(0, 0, 0);
}
/** Write a Huffman-encoded string to a BitStreamWriter. */
export function writeHuffBuffer(bs: BitStreamWriter, str: string): void {
buildTables();
// Always use Huffman compression (flag=true)
bs.writeFlag(true);
bs.writeInt(str.length, 8);
for (let i = 0; i < str.length; i++) {
const charCode = str.charCodeAt(i) & 0xff;
const leaf = leaves[charCode];
// Write Huffman code bits LSB-first
for (let b = 0; b < leaf.numBits; b++) {
bs.writeFlag((leaf.code & (1 << b)) !== 0);
}
}
}
/**
* Write a Huffman-encoded string (readString inverse).
* When the server's BitStream has a stringBuffer set (compression point),
* readString reads an extra flag before the Huffman data. We must write
* that flag. Since we don't track buffer state, we always write false
* (no prefix match), then the full Huffman string.
*/
export function writeString(
bs: BitStreamWriter,
str: string,
compressed: boolean = false,
): void {
if (compressed) {
// Compression buffer is active on the reader side.
// Write false = no prefix match with buffer, send full string.
bs.writeFlag(false);
}
writeHuffBuffer(bs, str);
}
/**
* Pack a net string (inverse of BitStream.unpackNetString).
* Code 0 = empty, code 1 = Huffman string, code 3 = integer.
*/
export function packNetString(
bs: BitStreamWriter,
str: string,
compressed: boolean = false,
): void {
if (str === "" || str == null) {
bs.writeInt(0, 2);
return;
}
// Check if it's a tagged string (\x01<id>)
if (str.charCodeAt(0) === 1) {
bs.writeInt(2, 2);
const tag = parseInt(str.slice(1), 10);
bs.writeInt(tag, 10);
return;
}
// Check if it's a simple integer
const num = parseInt(str, 10);
if (!isNaN(num) && String(num) === str) {
bs.writeInt(3, 2);
const neg = num < 0;
const absNum = Math.abs(num);
bs.writeFlag(neg);
if (absNum < 128) {
bs.writeFlag(true);
bs.writeInt(absNum, 7);
} else if (absNum < 32768) {
bs.writeFlag(false);
bs.writeFlag(true);
bs.writeInt(absNum, 15);
} else {
bs.writeFlag(false);
bs.writeFlag(false);
bs.writeInt(absNum, 31);
}
return;
}
// Normal string
bs.writeInt(1, 2);
writeString(bs, str, compressed);
}