New WTVTellyScript class for detok and retok, ported from @wtvemac's disk_editor

This commit is contained in:
zefie
2025-02-20 10:44:57 -05:00
parent 7a476b7796
commit a3b9f14ed9
3 changed files with 1445 additions and 0 deletions

View File

@@ -0,0 +1,255 @@
class LZSS {
constructor() {
this.ring_buffer_size = 0x1000; // 4096 bytes
this.root_index = 0x1000; // 4096
this.max_match_length = 0x11; // 17 bytes
this.match_threshold = 0x03; // minimum match length = 3
this.match_position = 0;
this.match_length = 0;
this.clear();
}
clear() {
// Allocate ring buffer with extra room for lookahead.
this.ring_buffer = new Uint8Array(this.ring_buffer_size + this.max_match_length);
// Initialize helper arrays.
this.parent = new Array(this.ring_buffer_size + 1).fill(0);
this.rchild = new Array((this.ring_buffer_size * 2) + 1).fill(0);
this.lchild = new Array((this.ring_buffer_size * 2) + 1).fill(0);
// Fill the first part of the ring buffer with spaces (0x20).
for (let ii = 0; ii < (this.ring_buffer_size - this.max_match_length); ii++) {
this.ring_buffer[ii] = 0x20;
}
// Initialize rchild for indices beyond the ring buffer.
for (let ii = this.ring_buffer_size + 1; ii < this.rchild.length; ii++) {
this.rchild[ii] = this.root_index;
}
// Set all parent pointers to the root index.
for (let ii = 0; ii < (this.ring_buffer_size - 1); ii++) {
this.parent[ii] = this.root_index;
}
this.match_position = 0;
this.match_length = 0;
}
insertNode(i) {
let keyi = this.ring_buffer[i];
let keyii = this.ring_buffer[i + 1] ^ this.ring_buffer[i + 2];
keyii = ((keyii ^ (keyii >> 4)) & 0x0F) << 8;
let parent_index = i;
let parent_link = (this.ring_buffer_size + 1) + keyi + keyii;
let child_index = parent_link;
let child_link = i;
// Initialize children for node i.
this.rchild[i] = this.root_index;
this.lchild[i] = this.root_index;
this.match_length = 0;
let matched_list = this.rchild;
let cmp_index = 1;
let looped = 0;
while (true) {
looped++;
if (looped >= 0xFFFF) {
throw new Error("Runaway loop in insertNode!");
}
if (cmp_index >= 0) {
cmp_index = this.rchild[parent_link];
matched_list = this.rchild;
} else {
cmp_index = this.lchild[parent_link];
matched_list = this.lchild;
}
if (cmp_index === this.root_index) {
parent_index = i;
child_index = parent_link;
child_link = i;
break;
}
parent_link = cmp_index;
let ii = 1;
while (ii < this.max_match_length) {
if (this.ring_buffer[i + ii] !== this.ring_buffer[parent_link + ii]) {
break;
}
ii++;
}
if (ii > this.match_length) {
this.match_length = ii;
this.match_position = parent_link;
if (ii > (this.max_match_length - 1)) {
this.parent[i] = this.parent[parent_link];
this.rchild[i] = this.rchild[parent_link];
this.lchild[i] = this.lchild[parent_link];
this.parent[this.rchild[i]] = i;
this.parent[this.lchild[i]] = i;
if (this.rchild[this.parent[parent_link]] !== parent_link) {
matched_list = this.lchild;
} else {
matched_list = this.rchild;
}
child_index = this.parent[parent_link];
child_link = i;
parent_index = parent_link;
parent_link = this.root_index;
break;
}
}
}
this.parent[parent_index] = parent_link;
matched_list[child_index] = child_link;
}
deleteNode(i) {
if (this.parent[i] !== this.root_index) {
let ii = 0;
if (this.rchild[i] === this.root_index) {
ii = this.lchild[i];
} else if (this.lchild[i] === this.root_index) {
ii = this.rchild[i];
} else {
ii = this.lchild[i];
if (ii !== this.root_index) {
let looped = 0;
while (ii !== this.root_index) {
looped++;
if (looped >= 0xFFFF) {
throw new Error("Runaway loop in deleteNode!");
}
ii = this.rchild[ii];
}
this.rchild[this.parent[ii]] = this.lchild[ii];
this.parent[this.lchild[ii]] = this.parent[ii];
this.lchild[ii] = this.lchild[i];
this.parent[this.lchild[i]] = ii;
}
this.rchild[ii] = this.rchild[i];
this.parent[this.rchild[i]] = ii;
}
this.parent[ii] = this.parent[i];
let parent_link = this.parent[i];
if (this.rchild[parent_link] !== i) {
this.lchild[parent_link] = ii;
} else {
this.rchild[parent_link] = ii;
}
this.parent[i] = this.root_index;
}
}
compress(uncompressedData) {
const uncompressed_size = uncompressedData.length;
let i = 0;
let ring_index = 0;
const ring_footer_start = this.ring_buffer_size - this.max_match_length - 1;
let footer_index = ring_footer_start;
let length = 0;
// Fill lookahead buffer.
for (; length <= this.max_match_length && i < uncompressed_size; length++) {
this.ring_buffer[ring_footer_start + length] = uncompressedData[i++];
}
let mask = 1; // use as 8-bit flag
const code_buffer = new Array(0x14).fill(0); // 20 bytes buffer; first byte holds flags.
let code_buffer_index = 1;
const compressed_data = [];
this.insertNode(ring_footer_start);
while (length > 0) {
if (this.match_length > length) {
this.match_length = length;
}
if (this.match_length >= this.match_threshold) {
let _match_position = footer_index - this.match_position - 1;
if (_match_position < 0) {
_match_position += this.ring_buffer_size;
}
code_buffer[code_buffer_index++] = _match_position & 0xFF;
code_buffer[code_buffer_index++] =
(((_match_position >> 4) & 0xF0) | (this.match_length - this.match_threshold)) & 0xFF;
} else {
this.match_length = 1;
code_buffer[0] |= mask;
code_buffer[code_buffer_index++] = this.ring_buffer[footer_index];
}
// Shift mask as an 8-bit value.
mask = (mask << 1) & 0xFF;
if (mask === 0) {
for (let ii = 0; ii < code_buffer_index; ii++) {
compressed_data.push(code_buffer[ii]);
}
code_buffer[0] = 0;
mask = 1;
code_buffer_index = 1;
}
const last_match_length = this.match_length;
if (last_match_length > 0) {
for (let ii = 0; ii < last_match_length; ii++, i++) {
this.deleteNode(ring_index);
if (i < uncompressed_size) {
this.ring_buffer[ring_index] = uncompressedData[i];
if (ring_index <= (this.max_match_length - 1)) {
this.ring_buffer[this.ring_buffer_size + ring_index] = uncompressedData[i];
}
} else {
i = uncompressed_size - 1;
length--;
}
ring_index = (ring_index + 1) & (this.ring_buffer_size - 1);
footer_index = (footer_index + 1) & (this.ring_buffer_size - 1);
if (length !== 0) {
this.insertNode(footer_index);
}
}
}
}
if (code_buffer_index > 1) {
for (let ii = 0; ii < code_buffer_index; ii++) {
compressed_data.push(code_buffer[ii]);
}
}
return new Uint8Array(compressed_data);
}
expand(compressedData, uncompressedSize = 0, flagsStart = 0) {
const compressed_size = compressedData.length;
const uncompressed_data = [];
let flags = flagsStart >>> 0;
let i = 0;
while (i < compressed_size) {
if ((flags & 0x100) === 0) {
flags = compressedData[i] | 0xFF00;
i++;
}
const current_byte = compressedData[i];
if ((flags & 0x01) === 0x01) {
uncompressed_data.push(current_byte);
} else {
i++;
const next_byte = compressedData[i];
const m = (((next_byte & 0xF0) << 4) | current_byte) >>> 0;
const matchLen = (next_byte & 0x0F) + this.match_threshold;
for (let ii = 0; ii < matchLen; ii++) {
const index = uncompressed_data.length - (m + 1);
uncompressed_data.push(uncompressed_data[index]);
}
}
flags >>= 1;
i++;
if (uncompressedSize > 0 && uncompressed_data.length >= uncompressedSize) {
break;
}
}
return new Uint8Array(uncompressed_data);
}
}
module.exports = LZSS;