From a3b9f14ed972cccc8efe6eaca0d95da930f5e432 Mon Sep 17 00:00:00 2001 From: zefie Date: Thu, 20 Feb 2025 10:44:57 -0500 Subject: [PATCH] New WTVTellyScript class for detok and retok, ported from @wtvemac's disk_editor --- zefie_wtvp_minisrv/includes/classes/LZSS.js | 255 ++++ .../includes/classes/WTVTellyScript.js | 1161 +++++++++++++++++ zefie_wtvp_minisrv/test_telly.js | 29 + 3 files changed, 1445 insertions(+) create mode 100644 zefie_wtvp_minisrv/includes/classes/LZSS.js create mode 100644 zefie_wtvp_minisrv/includes/classes/WTVTellyScript.js create mode 100644 zefie_wtvp_minisrv/test_telly.js diff --git a/zefie_wtvp_minisrv/includes/classes/LZSS.js b/zefie_wtvp_minisrv/includes/classes/LZSS.js new file mode 100644 index 00000000..0ec8e079 --- /dev/null +++ b/zefie_wtvp_minisrv/includes/classes/LZSS.js @@ -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; \ No newline at end of file diff --git a/zefie_wtvp_minisrv/includes/classes/WTVTellyScript.js b/zefie_wtvp_minisrv/includes/classes/WTVTellyScript.js new file mode 100644 index 00000000..b3f19088 --- /dev/null +++ b/zefie_wtvp_minisrv/includes/classes/WTVTellyScript.js @@ -0,0 +1,1161 @@ +const LZSS = require("./LZSS.js"); + +const WhitespaceInstruction = { + ADD_NONE: 0, + ADD_BEFORE: 1, + ADD_AFTER: 2, + ADD_BOTH: 3, + CHECK_NEWSCOPE1: 4, + CHECK_NEWSCOPE2: 5, + CHECK_IF_MATH: 6, +}; + +const ScopeCheckStep = { + OFF: 0, + RB_DELIMITER_CHECK: 1, + CB_DELIMITER_CHECK: 2, + SINGLE_LINE_CHECK: 3, + SINGLE_LINE_HIT: 4, + MULTI_LINE_CHECK: 5, +}; + +// --- Helper Enums & Constants --- + +const PACKED_TELLYSCRIPT_HEADER_SIZE = 0x24; // 36 bytes +const UNKNOWN1_VALUE = 0x0101FFFF; + +const TellyScriptState = { + UNKNOWN: -1, + PACKED: 0, + TOKENIZED: 1, + RAW: 2, +}; + +const TellyScriptType = { + ORIGINAL: 0, + DIALSCRIPT: 1, +}; + + +class WTVTellyScriptTokenizer { + constructor(rawData) { + this.rawData = rawData; + this.tokenizedData = []; + this.index = 0; + this.endIndex = rawData.length - 1; + this.lineNumber = 1; + this.setupReplacements(); + } + + setupReplacements() { + // Map strings (or symbol sequences) to their token codes. + this.replacements = new Map(); + this.replacements.set("!", 0x21); + this.replacements.set("%", 0x25); + this.replacements.set("&&", 0x26); + this.replacements.set("(", 0x28); + this.replacements.set(")", 0x29); + this.replacements.set("*", 0x2A); + this.replacements.set("+", 0x2B); + this.replacements.set(",", 0x2C); + this.replacements.set("-", 0x2D); + this.replacements.set("/", 0x2F); + this.replacements.set(";", 0x3B); + this.replacements.set("<", 0x3C); + this.replacements.set("=", 0x3D); + this.replacements.set(">", 0x3E); + this.replacements.set("&", 0x40); + this.replacements.set("+=", 0x41); + this.replacements.set("-=", 0x42); + this.replacements.set("--", 0x44); + this.replacements.set("==", 0x45); + this.replacements.set(">=", 0x47); + this.replacements.set("<=", 0x4C); + this.replacements.set("*=", 0x4D); + this.replacements.set("!=", 0x4E); + this.replacements.set("++", 0x50); + this.replacements.set("/=", 0x56); + this.replacements.set("[", 0x5B); + this.replacements.set("]", 0x5D); + this.replacements.set("break", 0x62); + this.replacements.set("char", 0x63); + this.replacements.set("else", 0x65); + this.replacements.set("for", 0x66); + this.replacements.set("if", 0x69); + this.replacements.set("int", 0x6C); + this.replacements.set("return", 0x72); + this.replacements.set("while", 0x77); + this.replacements.set("{", 0x7B); + this.replacements.set("||", 0x7C); + this.replacements.set("}", 0x7D); + // Newline tokens (both "\n" and "\r") map to 0x7F + this.replacements.set("\n", 0x7F); + this.replacements.set("\r", 0x7F); + } + + getNextCharacter() { + if (this.index <= this.endIndex) { + return this.rawData[this.index++]; + } + return null; + } + + // Peek at the next character without advancing the index. + peekCharacter() { + if (this.index <= this.endIndex) { + return this.rawData[this.index]; + } + return null; + } + + // Build a sequence from the current character while it matches the given regex pattern. + buildCheckSequence(firstChar, pattern) { + let sequence = ""; + const regex = new RegExp(pattern); + if (regex.test(firstChar)) { + sequence += firstChar; + while (this.index <= this.endIndex) { + let nextChar = this.getNextCharacter(); + if (regex.test(nextChar)) { + sequence += nextChar; + } else { + this.index--; // step back if it doesn't match + break; + } + } + } + return sequence; + } + + // Handle escape sequences inside strings and constants. + consumeEscapeSequence() { + const ch = this.getNextCharacter(); + switch (ch) { + case "a": + return String.fromCharCode(0x07); + case "b": + return String.fromCharCode(0x08); + case "t": + return String.fromCharCode(0x09); + case "n": + return String.fromCharCode(0x0A); + case "v": + return String.fromCharCode(0x0B); + case "f": + return String.fromCharCode(0x0C); + case "r": + return String.fromCharCode(0x0D); + case "x": + case "X": + const digit1 = this.getNextCharacter(); + const digit2 = this.getNextCharacter(); + return String.fromCharCode(parseInt(digit1 + digit2, 16)); + default: + return ch; + } + } + + // Tokenize a double-quoted string. + tokenizeString() { + // Token code 0x53 denotes a string. + this.tokenizedData.push(0x53); + while (this.index <= this.endIndex) { + let ch = this.getNextCharacter(); + if (ch === '"') break; + if (ch === "\\") { + ch = this.consumeEscapeSequence(); + } + this.tokenizedData.push(ch.charCodeAt(0)); + } + // Null terminator. + this.tokenizedData.push(0x00); + } + + // Add a constant token. The constant is stored as token 0x43 followed by digit bytes. + tokenizeConstantNumber(constantDigits) { + this.tokenizedData.push(0x43); + let pastLeadingZeros = false; + for (let i = 0; i < constantDigits.length; i++) { + if (pastLeadingZeros || constantDigits[i] !== 0) { + pastLeadingZeros = true; + // Store the digit as its ASCII code. + this.tokenizedData.push(constantDigits[i] + 0x30); + } + } + this.tokenizedData.push(0x00); + } + + // Tokenize a constant delimited by single quotes. + tokenizeConstant() { + const constantDigits = []; + // Read characters until we hit the closing constant delimiter. + while (this.index <= this.endIndex) { + let ch = this.getNextCharacter(); + if (ch === "'") break; + if (ch === "\\") { + ch = this.consumeEscapeSequence(); + } + // In the original code the character code is adjusted. + constantDigits.push(ch.charCodeAt(0) - 0x30); + } + this.tokenizeConstantNumber(constantDigits); + } + + // Tokenize an identifier or (if it matches a numeric pattern) a constant. + tokenizeIdentifierOrConstant(checkSequence) { + if (/^[a-zA-Z_]/.test(checkSequence)) { + // Token 0x49 indicates an identifier. + this.tokenizedData.push(0x49); + for (let i = 0; i < checkSequence.length; i++) { + this.tokenizedData.push(checkSequence.charCodeAt(i)); + } + this.tokenizedData.push(0x00); + } else if (/^(0x|)?[0-9a-fA-F]+$/.test(checkSequence)) { + // Process as a constant. + let hexString = ""; + if (checkSequence.startsWith("0x") || checkSequence.startsWith("0X")) { + hexString = checkSequence.substring(2); + } else { + hexString = parseInt(checkSequence, 10).toString(16).toUpperCase(); + } + const constantDigits = []; + for (let i = 0; i < hexString.length; i++) { + constantDigits.push(parseInt(hexString[i], 16)); + } + this.tokenizeConstantNumber(constantDigits); + } else { + throw new Error("Invalid constant '" + checkSequence + "' at line " + this.lineNumber); + } + } + + // The main tokenize method processes the raw script and produces an array of token bytes. + tokenize() { + while (this.index <= this.endIndex) { + let ch = this.getNextCharacter(); + + // Handle comments starting with "/*" and ending with "*/". + if (ch === "/" && this.peekCharacter() === "*") { + this.getNextCharacter(); // consume '*' + while (this.index <= this.endIndex) { + let commentChar = this.getNextCharacter(); + if (commentChar === "*" && this.peekCharacter() === "/") { + this.getNextCharacter(); // consume '/' + break; + } + } + continue; + } + + // Skip whitespace (spaces and tabs). + if (ch === " " || ch === "\t") { + continue; + } else if (ch === '"') { + this.tokenizeString(); + } else if (ch === "'") { + this.tokenizeConstant(); + } else { + // Try to build an identifier/number sequence. + let currentIdx = this.index; + let checkSequence = this.buildCheckSequence(ch, "^[a-zA-Z0-9_]$"); + if (checkSequence !== "") { + if (this.replacements.has(checkSequence)) { + this.tokenizedData.push(this.replacements.get(checkSequence)); + } else { + this.tokenizeIdentifierOrConstant(checkSequence); + } + } else { + // Not alphanumeric – try symbol sequence. + this.index = currentIdx; + checkSequence = this.buildCheckSequence(ch, "^[\\-+=<>!\\|\\&]$"); + if (this.replacements.has(checkSequence)) { + this.tokenizedData.push(this.replacements.get(checkSequence)); + } else if (this.replacements.has(ch)) { + let replacement = this.replacements.get(ch); + if (replacement === 0x7F) { + this.lineNumber++; + } + this.tokenizedData.push(replacement); + } else { + throw new Error("Invalid character '" + ch + "' at line " + this.lineNumber); + } + } + } + } + // Append the EOF token. + this.tokenizedData.push(0xFF); + return this.tokenizedData; + } +} + +/** + * TellyScriptDetokenizer converts an array of token bytes back into a formatted script string. + */ +class WTVTellyScriptDetokenizer { + constructor(tokenizedData) { + this.tokenizedData = tokenizedData; + this.rawData = ""; + this.index = 0; + this.endIndex = tokenizedData.length - 1; + this.scriptStarted = false; + this.scopeCheckStep = ScopeCheckStep.OFF; + this.checkScopeCBLevel = 0; + this.checkScopeRBLevel = 0; + this.cBracketScopeLevel = 0; + this.rBracketScopeLevel = 0; + this.setupInstructions(); + } + + setupInstructions() { + // Constants used for formatting. + this.INDENT_WHITESPACE_COUNT = 1; + this.INDENT_WHITESPACE_CHARACTER = "\t"; + this.SEPARATOR_WHITESPACE_CHARACTER = " "; + this.NEWLINE_WHITESPACE_CHARACTER = "\n"; + this.USE_HEX_INTEGER_AFTER = 0x1000; + + // The mapping of token values to detokenization instructions. + // Each instruction can specify: + // - an optional function (instruction) to handle the token, + // - a literal output string, + // - whitespace handling, + // - scope adjustments, and termination. + this.instructions = {}; + const addInstruction = (token, instr) => { + this.instructions[token] = instr; + }; + + addInstruction(0x21, { + instruction: null, + output: "", + whitespace: WhitespaceInstruction.ADD_NONE, + enterScopeCheck: ScopeCheckStep.OFF, + moveCBracket: 0, + moveRBracket: 0, + scriptStartIgnore: false, + terminate: false, + }); + addInstruction(0x25, { + instruction: null, + output: "", + whitespace: WhitespaceInstruction.ADD_BOTH, + enterScopeCheck: ScopeCheckStep.OFF, + moveCBracket: 0, + moveRBracket: 0, + scriptStartIgnore: false, + terminate: false, + }); + addInstruction(0x26, { + instruction: null, + output: "&&", + whitespace: WhitespaceInstruction.ADD_BOTH, + enterScopeCheck: ScopeCheckStep.OFF, + moveCBracket: 0, + moveRBracket: 0, + scriptStartIgnore: false, + terminate: false, + }); + addInstruction(0x28, { + instruction: null, + output: "", + whitespace: WhitespaceInstruction.ADD_NONE, + enterScopeCheck: ScopeCheckStep.OFF, + moveCBracket: 0, + moveRBracket: 1, + scriptStartIgnore: false, + terminate: false, + }); + addInstruction(0x29, { + instruction: null, + output: "", + whitespace: WhitespaceInstruction.ADD_NONE, + enterScopeCheck: ScopeCheckStep.OFF, + moveCBracket: 0, + moveRBracket: -1, + scriptStartIgnore: false, + terminate: false, + }); + addInstruction(0x2A, { + instruction: null, + output: "", + whitespace: WhitespaceInstruction.CHECK_IF_MATH, + enterScopeCheck: ScopeCheckStep.OFF, + moveCBracket: 0, + moveRBracket: 0, + scriptStartIgnore: false, + terminate: false, + }); + addInstruction(0x2B, { + instruction: null, + output: "", + whitespace: WhitespaceInstruction.ADD_BOTH, + enterScopeCheck: ScopeCheckStep.OFF, + moveCBracket: 0, + moveRBracket: 0, + scriptStartIgnore: false, + terminate: false, + }); + addInstruction(0x2C, { + instruction: null, + output: "", + whitespace: WhitespaceInstruction.ADD_AFTER, + enterScopeCheck: ScopeCheckStep.OFF, + moveCBracket: 0, + moveRBracket: 0, + scriptStartIgnore: false, + terminate: false, + }); + addInstruction(0x2D, { + instruction: null, + output: "", + whitespace: WhitespaceInstruction.ADD_BOTH, + enterScopeCheck: ScopeCheckStep.OFF, + moveCBracket: 0, + moveRBracket: 0, + scriptStartIgnore: false, + terminate: false, + }); + addInstruction(0x2F, { + instruction: null, + output: "", + whitespace: WhitespaceInstruction.ADD_BOTH, + enterScopeCheck: ScopeCheckStep.OFF, + moveCBracket: 0, + moveRBracket: 0, + scriptStartIgnore: false, + terminate: false, + }); + addInstruction(0x3B, { + instruction: null, + output: "", + whitespace: WhitespaceInstruction.ADD_NONE, + enterScopeCheck: ScopeCheckStep.OFF, + moveCBracket: 0, + moveRBracket: 0, + scriptStartIgnore: false, + terminate: false, + }); + addInstruction(0x3C, { + instruction: null, + output: "", + whitespace: WhitespaceInstruction.ADD_BOTH, + enterScopeCheck: ScopeCheckStep.OFF, + moveCBracket: 0, + moveRBracket: 0, + scriptStartIgnore: false, + terminate: false, + }); + addInstruction(0x3D, { + instruction: null, + output: "", + whitespace: WhitespaceInstruction.ADD_BOTH, + enterScopeCheck: ScopeCheckStep.OFF, + moveCBracket: 0, + moveRBracket: 0, + scriptStartIgnore: false, + terminate: false, + }); + addInstruction(0x3E, { + instruction: null, + output: "", + whitespace: WhitespaceInstruction.ADD_BOTH, + enterScopeCheck: ScopeCheckStep.OFF, + moveCBracket: 0, + moveRBracket: 0, + scriptStartIgnore: false, + terminate: false, + }); + addInstruction(0x40, { + instruction: null, + output: "&", + whitespace: WhitespaceInstruction.ADD_NONE, + enterScopeCheck: ScopeCheckStep.OFF, + moveCBracket: 0, + moveRBracket: 0, + scriptStartIgnore: false, + terminate: false, + }); + addInstruction(0x41, { + instruction: null, + output: "+=", + whitespace: WhitespaceInstruction.ADD_BOTH, + enterScopeCheck: ScopeCheckStep.OFF, + moveCBracket: 0, + moveRBracket: 0, + scriptStartIgnore: false, + terminate: false, + }); + addInstruction(0x42, { + instruction: null, + output: "-=", + whitespace: WhitespaceInstruction.ADD_BOTH, + enterScopeCheck: ScopeCheckStep.OFF, + moveCBracket: 0, + moveRBracket: 0, + scriptStartIgnore: false, + terminate: false, + }); + addInstruction(0x43, { + // Constant token: call the dedicated handler. + instruction: () => this.detokenizeConstant(), + output: "", + whitespace: WhitespaceInstruction.ADD_NONE, + enterScopeCheck: ScopeCheckStep.OFF, + moveCBracket: 0, + moveRBracket: 0, + scriptStartIgnore: false, + terminate: false, + }); + addInstruction(0x44, { + instruction: null, + output: "--", + whitespace: WhitespaceInstruction.ADD_NONE, + enterScopeCheck: ScopeCheckStep.OFF, + moveCBracket: 0, + moveRBracket: 0, + scriptStartIgnore: false, + terminate: false, + }); + addInstruction(0x45, { + instruction: null, + output: "==", + whitespace: WhitespaceInstruction.ADD_BOTH, + enterScopeCheck: ScopeCheckStep.OFF, + moveCBracket: 0, + moveRBracket: 0, + scriptStartIgnore: false, + terminate: false, + }); + addInstruction(0x47, { + instruction: null, + output: ">=", + whitespace: WhitespaceInstruction.ADD_BOTH, + enterScopeCheck: ScopeCheckStep.OFF, + moveCBracket: 0, + moveRBracket: 0, + scriptStartIgnore: false, + terminate: false, + }); + addInstruction(0x49, { + // Identifier token. + instruction: () => this.detokenizeIdentifier(), + output: "", + whitespace: WhitespaceInstruction.ADD_NONE, + enterScopeCheck: ScopeCheckStep.OFF, + moveCBracket: 0, + moveRBracket: 0, + scriptStartIgnore: false, + terminate: false, + }); + addInstruction(0x4C, { + instruction: null, + output: "<=", + whitespace: WhitespaceInstruction.ADD_BOTH, + enterScopeCheck: ScopeCheckStep.OFF, + moveCBracket: 0, + moveRBracket: 0, + scriptStartIgnore: false, + terminate: false, + }); + addInstruction(0x4D, { + instruction: null, + output: "*=", + whitespace: WhitespaceInstruction.ADD_BOTH, + enterScopeCheck: ScopeCheckStep.OFF, + moveCBracket: 0, + moveRBracket: 0, + scriptStartIgnore: false, + terminate: false, + }); + addInstruction(0x4E, { + instruction: null, + output: "!=", + whitespace: WhitespaceInstruction.ADD_BOTH, + enterScopeCheck: ScopeCheckStep.OFF, + moveCBracket: 0, + moveRBracket: 0, + scriptStartIgnore: false, + terminate: false, + }); + addInstruction(0x50, { + instruction: null, + output: "++", + whitespace: WhitespaceInstruction.ADD_NONE, + enterScopeCheck: ScopeCheckStep.OFF, + moveCBracket: 0, + moveRBracket: 0, + scriptStartIgnore: false, + terminate: false, + }); + addInstruction(0x53, { + // String token. + instruction: () => this.detokenizeString(), + output: "", + whitespace: WhitespaceInstruction.ADD_NONE, + enterScopeCheck: ScopeCheckStep.OFF, + moveCBracket: 0, + moveRBracket: 0, + scriptStartIgnore: false, + terminate: false, + }); + addInstruction(0x56, { + instruction: null, + output: "/=", + whitespace: WhitespaceInstruction.ADD_BOTH, + enterScopeCheck: ScopeCheckStep.OFF, + moveCBracket: 0, + moveRBracket: 0, + scriptStartIgnore: false, + terminate: false, + }); + addInstruction(0x5B, { + instruction: null, + output: "", + whitespace: WhitespaceInstruction.ADD_NONE, + enterScopeCheck: ScopeCheckStep.OFF, + moveCBracket: 0, + moveRBracket: 0, + scriptStartIgnore: false, + terminate: false, + }); + addInstruction(0x5D, { + instruction: null, + output: "", + whitespace: WhitespaceInstruction.ADD_NONE, + enterScopeCheck: ScopeCheckStep.OFF, + moveCBracket: 0, + moveRBracket: 0, + scriptStartIgnore: false, + terminate: false, + }); + addInstruction(0x62, { + instruction: null, + output: "break", + whitespace: WhitespaceInstruction.ADD_NONE, + enterScopeCheck: ScopeCheckStep.OFF, + moveCBracket: 0, + moveRBracket: 0, + scriptStartIgnore: false, + terminate: false, + }); + addInstruction(0x63, { + instruction: null, + output: "char", + whitespace: WhitespaceInstruction.ADD_AFTER, + enterScopeCheck: ScopeCheckStep.OFF, + moveCBracket: 0, + moveRBracket: 0, + scriptStartIgnore: false, + terminate: false, + }); + addInstruction(0x65, { + instruction: null, + output: "else", + whitespace: WhitespaceInstruction.CHECK_NEWSCOPE1, + enterScopeCheck: ScopeCheckStep.CB_DELIMITER_CHECK, + moveCBracket: 0, + moveRBracket: 0, + scriptStartIgnore: false, + terminate: false, + }); + addInstruction(0x66, { + instruction: null, + output: "for", + whitespace: WhitespaceInstruction.ADD_AFTER, + enterScopeCheck: ScopeCheckStep.RB_DELIMITER_CHECK, + moveCBracket: 0, + moveRBracket: 0, + scriptStartIgnore: false, + terminate: false, + }); + addInstruction(0x69, { + instruction: null, + output: "if", + whitespace: WhitespaceInstruction.ADD_AFTER, + enterScopeCheck: ScopeCheckStep.RB_DELIMITER_CHECK, + moveCBracket: 0, + moveRBracket: 0, + scriptStartIgnore: false, + terminate: false, + }); + addInstruction(0x6C, { + instruction: null, + output: "int", + whitespace: WhitespaceInstruction.ADD_AFTER, + enterScopeCheck: ScopeCheckStep.OFF, + moveCBracket: 0, + moveRBracket: 0, + scriptStartIgnore: false, + terminate: false, + }); + addInstruction(0x72, { + instruction: null, + output: "return", + whitespace: WhitespaceInstruction.ADD_AFTER, + enterScopeCheck: ScopeCheckStep.OFF, + moveCBracket: 0, + moveRBracket: 0, + scriptStartIgnore: false, + terminate: false, + }); + addInstruction(0x77, { + instruction: null, + output: "while", + whitespace: WhitespaceInstruction.ADD_AFTER, + enterScopeCheck: ScopeCheckStep.RB_DELIMITER_CHECK, + moveCBracket: 0, + moveRBracket: 0, + scriptStartIgnore: false, + terminate: false, + }); + addInstruction(0x7B, { + instruction: null, + output: "", + whitespace: WhitespaceInstruction.CHECK_NEWSCOPE2, + enterScopeCheck: ScopeCheckStep.OFF, + moveCBracket: 1, + moveRBracket: 0, + scriptStartIgnore: false, + terminate: false, + }); + addInstruction(0x7C, { + instruction: null, + output: "||", + whitespace: WhitespaceInstruction.ADD_BOTH, + enterScopeCheck: ScopeCheckStep.OFF, + moveCBracket: 0, + moveRBracket: 0, + scriptStartIgnore: false, + terminate: false, + }); + addInstruction(0x7D, { + instruction: null, + output: "", + whitespace: WhitespaceInstruction.ADD_NONE, + enterScopeCheck: ScopeCheckStep.OFF, + moveCBracket: -1, + moveRBracket: 0, + scriptStartIgnore: false, + terminate: false, + }); + addInstruction(0x7F, { + // Newline token. + instruction: () => this.detokenizeNewline(), + output: "", + whitespace: WhitespaceInstruction.ADD_NONE, + enterScopeCheck: ScopeCheckStep.OFF, + moveCBracket: 0, + moveRBracket: 0, + scriptStartIgnore: true, + terminate: false, + }); + addInstruction(0xFF, { + // EOF token. + terminate: true, + }); + } + + // Reads a constant token. It converts the following digit bytes into a number, + // and if large, outputs in hexadecimal (and may include a comment with its alphanumeric form). + detokenizeConstant() { + let constantValue = 0; + let alphanumericValue = ""; + let ii = 0; + this.index++; // skip the token identifier (0x43) + while (this.index < this.tokenizedData.length) { + let byteVal = this.tokenizedData[this.index]; + if (byteVal === 0x00) break; + let digit = byteVal - 0x30; + if (digit >= 0 && digit <= 0x0F) { + constantValue = (constantValue << 4) + digit; + } + if (ii >= 1 && (ii % 2) === 1 && alphanumericValue !== null) { + let charValue = constantValue & 0xFF; + if (charValue >= 0x30 && charValue <= 0x5A) { + alphanumericValue += String.fromCharCode(charValue); + } else { + alphanumericValue = null; + } + } + ii++; + this.index++; + } + if (alphanumericValue === "") { + alphanumericValue = null; + } + if (constantValue < this.USE_HEX_INTEGER_AFTER) { + this.rawData += constantValue.toString(); + } else { + this.rawData += "0x" + constantValue.toString(16).toUpperCase(); + if (alphanumericValue !== null) { + this.rawData += " /* " + alphanumericValue + " */"; + } + } + } + + // Reads a string token until the null terminator and outputs it with proper escape replacements. + detokenizeString() { + let count = 0; + let startIndex = ++this.index; + while (this.index <= this.endIndex) { + if (this.tokenizedData[this.index] === 0x00) break; + count++; + this.index++; + } + let chars = []; + for (let i = startIndex; i < startIndex + count; i++) { + chars.push(String.fromCharCode(this.tokenizedData[i])); + } + let str = chars.join(""); + str = str.replace(/\x07/g, "\\a") + .replace(/\x08/g, "\\b") + .replace(/\x09/g, "\\t") + .replace(/\x0A/g, "\\n") + .replace(/\x0B/g, "\\v") + .replace(/\x0C/g, "\\f") + .replace(/\x0D/g, "\\r"); + this.rawData += '"' + str + '"'; + } + + // Reads an identifier token (until the null terminator). + detokenizeIdentifier() { + this.index++; // skip the identifier token indicator (0x49) + while (this.index < this.tokenizedData.length) { + let byteVal = this.tokenizedData[this.index]; + if (byteVal === 0x00) break; + this.rawData += String.fromCharCode(byteVal); + this.index++; + } + } + + // Handles newline tokens: outputs a newline plus indentation based on scope. + detokenizeNewline() { + if (!this.scriptStarted) return; + this.rawData += this.NEWLINE_WHITESPACE_CHARACTER; + let whitespaceAmount = this.cBracketScopeLevel * this.INDENT_WHITESPACE_COUNT; + if (this.index < this.endIndex && this.tokenizedData[this.index + 1] === 0x7D) { + whitespaceAmount -= this.INDENT_WHITESPACE_COUNT; + } + for (let i = 0; i < whitespaceAmount; i++) { + this.rawData += this.INDENT_WHITESPACE_CHARACTER; + } + } + + // The main detokenize loop reads each token and either calls its custom handler or + // outputs the mapped literal text (adding whitespace as required). + detokenize() { + this.rawData = ""; + this.index = 0; + while (this.index <= this.endIndex) { + let token = this.tokenizedData[this.index]; + if (this.instructions.hasOwnProperty(token)) { + let instr = this.instructions[token]; + if (instr.terminate) break; + if (instr.instruction) { + instr.instruction(); + } else { + let output = ""; + if (!instr.output) { + output += String.fromCharCode(token); + } else { + output += instr.output; + } + if (instr.whitespace === WhitespaceInstruction.ADD_BEFORE) { + output = this.SEPARATOR_WHITESPACE_CHARACTER + output; + } else if (instr.whitespace === WhitespaceInstruction.ADD_AFTER) { + output += this.SEPARATOR_WHITESPACE_CHARACTER; + } else if (instr.whitespace === WhitespaceInstruction.ADD_BOTH) { + output = this.SEPARATOR_WHITESPACE_CHARACTER + output + this.SEPARATOR_WHITESPACE_CHARACTER; + } else if (instr.whitespace === WhitespaceInstruction.CHECK_NEWSCOPE1) { + if (this.index >= 2 && this.tokenizedData[this.index - 1] === 0x7D) + output = this.SEPARATOR_WHITESPACE_CHARACTER + output + this.SEPARATOR_WHITESPACE_CHARACTER; + else + output = output + this.SEPARATOR_WHITESPACE_CHARACTER; + } else if (instr.whitespace === WhitespaceInstruction.CHECK_NEWSCOPE2) { + if (this.index >= 2 && this.tokenizedData[this.index - 1] === 0x29) + output = this.SEPARATOR_WHITESPACE_CHARACTER + output; + } else if (instr.whitespace === WhitespaceInstruction.CHECK_IF_MATH) { + if (this.index >= 2 && this.tokenizedData[this.index - 1] === 0x00) + output = " " + output + " "; + } + + this.cBracketScopeLevel += instr.moveCBracket; + this.rBracketScopeLevel += instr.moveRBracket; + + if (instr.enterScopeCheck !== ScopeCheckStep.OFF) { + this.scopeCheckStep = instr.enterScopeCheck; + this.checkScopeCBLevel = this.cBracketScopeLevel; + this.checkScopeRBLevel = this.rBracketScopeLevel; + } + + this.rawData += output; + } + if (!instr.scriptStartIgnore) { + this.scriptStarted = true; + } + } else { + this.rawData += token.toString(); + } + this.index++; + } + return this.rawData; + } +} + + +class WTVTellyScript { + + // --- TellyScript Class --- + /* + * Constructs a new TellyScript object. + * @param {Uint8Array|string} data - The TellyScript data (either packed, tokenized, or raw). + * @param {number} dataState - One of TellyScriptState (default: PACKED). + * @param {number} tellyscriptType - One of TellyScriptType (default: ORIGINAL). + */ + constructor(data, dataState = TellyScriptState.PACKED, tellyscriptType = TellyScriptType.ORIGINAL) { + this.tellyscript_type = tellyscriptType; + this.packed_data = null; + this.packed_header = null; + this.tokenized_data = null; + this.raw_data = null; + + this.process(data, dataState); + } + + // --- Big Endian Converter Helpers --- + + toUint32(bytes, offset) { + return ( + (bytes[offset] << 24) >>> 0 | + (bytes[offset + 1] << 16) | + (bytes[offset + 2] << 8) | + (bytes[offset + 3]) + ) >>> 0; + } + + uint32ToBytes(num) { + return [ + (num >>> 24) & 0xff, + (num >>> 16) & 0xff, + (num >>> 8) & 0xff, + num & 0xff, + ]; + } + + // --- CRC32 Calculation --- + + crc32(data) { + const crc32Table = [ + 0x00000000, 0x77073096, 0xee0e612c, 0x990951ba, 0x076dc419, 0x706af48f, + 0xe963a535, 0x9e6495a3, 0x0edb8832, 0x79dcb8a4, 0xe0d5e91e, 0x97d2d988, + 0x09b64c2b, 0x7eb17cbd, 0xe7b82d07, 0x90bf1d91, 0x1db71064, 0x6ab020f2, + 0xf3b97148, 0x84be41de, 0x1adad47d, 0x6ddde4eb, 0xf4d4b551, 0x83d385c7, + 0x136c9856, 0x646ba8c0, 0xfd62f97a, 0x8a65c9ec, 0x14015c4f, 0x63066cd9, + 0xfa0f3d63, 0x8d080df5, 0x3b6e20c8, 0x4c69105e, 0xd56041e4, 0xa2677172, + 0x3c03e4d1, 0x4b04d447, 0xd20d85fd, 0xa50ab56b, 0x35b5a8fa, 0x42b2986c, + 0xdbbbc9d6, 0xacbcf940, 0x32d86ce3, 0x45df5c75, 0xdcd60dcf, 0xabd13d59, + 0x26d930ac, 0x51de003a, 0xc8d75180, 0xbfd06116, 0x21b4f4b5, 0x56b3c423, + 0xcfba9599, 0xb8bda50f, 0x2802b89e, 0x5f058808, 0xc60cd9b2, 0xb10be924, + 0x2f6f7c87, 0x58684c11, 0xc1611dab, 0xb6662d3d, 0x76dc4190, 0x01db7106, + 0x98d220bc, 0xefd5102a, 0x71b18589, 0x06b6b51f, 0x9fbfe4a5, 0xe8b8d433, + 0x7807c9a2, 0x0f00f934, 0x9609a88e, 0xe10e9818, 0x7f6a0dbb, 0x086d3d2d, + 0x91646c97, 0xe6635c01, 0x6b6b51f4, 0x1c6c6162, 0x856530d8, 0xf262004e, + 0x6c0695ed, 0x1b01a57b, 0x8208f4c1, 0xf50fc457, 0x65b0d9c6, 0x12b7e950, + 0x8bbeb8ea, 0xfcb9887c, 0x62dd1ddf, 0x15da2d49, 0x8cd37cf3, 0xfbd44c65, + 0x4db26158, 0x3ab551ce, 0xa3bc0074, 0xd4bb30e2, 0x4adfa541, 0x3dd895d7, + 0xa4d1c46d, 0xd3d6f4fb, 0x4369e96a, 0x346ed9fc, 0xad678846, 0xda60b8d0, + 0x44042d73, 0x33031de5, 0xaa0a4c5f, 0xdd0d7cc9, 0x5005713c, 0x270241aa, + 0xbe0b1010, 0xc90c2086, 0x5768b525, 0x206f85b3, 0xb966d409, 0xce61e49f, + 0x5edef90e, 0x29d9c998, 0xb0d09822, 0xc7d7a8b4, 0x59b33d17, 0x2eb40d81, + 0xb7bd5c3b, 0xc0ba6cad, 0xedb88320, 0x9abfb3b6, 0x03b6e20c, 0x74b1d29a, + 0xead54739, 0x9dd277af, 0x04db2615, 0x73dc1683, 0xe3630b12, 0x94643b84, + 0x0d6d6a3e, 0x7a6a5aa8, 0xe40ecf0b, 0x9309ff9d, 0x0a00ae27, 0x7d079eb1, + 0xf00f9344, 0x8708a3d2, 0x1e01f268, 0x6906c2fe, 0xf762575d, 0x806567cb, + 0x196c3671, 0x6e6b06e7, 0xfed41b76, 0x89d32be0, 0x10da7a5a, 0x67dd4acc, + 0xf9b9df6f, 0x8ebeeff9, 0x17b7be43, 0x60b08ed5, 0xd6d6a3e8, 0xa1d1937e, + 0x38d8c2c4, 0x4fdff252, 0xd1bb67f1, 0xa6bc5767, 0x3fb506dd, 0x48b2364b, + 0xd80d2bda, 0xaf0a1b4c, 0x36034af6, 0x41047a60, 0xdf60efc3, 0xa867df55, + 0x316e8eef, 0x4669be79, 0xcb61b38c, 0xbc66831a, 0x256fd2a0, 0x5268e236, + 0xcc0c7795, 0xbb0b4703, 0x220216b9, 0x5505262f, 0xc5ba3bbe, 0xb2bd0b28, + 0x2bb45a92, 0x5cb36a04, 0xc2d7ffa7, 0xb5d0cf31, 0x2cd99e8b, 0x5bdeae1d, + 0x9b64c2b0, 0xec63f226, 0x756aa39c, 0x026d930a, 0x9c0906a9, 0xeb0e363f, + 0x72076785, 0x05005713, 0x95bf4a82, 0xe2b87a14, 0x7bb12bae, 0x0cb61b38, + 0x92d28e9b, 0xe5d5be0d, 0x7cdcefb7, 0x0bdbdf21, 0x86d3d2d4, 0xf1d4e242, + 0x68ddb3f8, 0x1fda836e, 0x81be16cd, 0xf6b9265b, 0x6fb077e1, 0x18b74777, + 0x88085ae6, 0xff0f6a70, 0x66063bca, 0x11010b5c, 0x8f659eff, 0xf862ae69, + 0x616bffd3, 0x166ccf45, 0xa00ae278, 0xd70dd2ee, 0x4e048354, 0x3903b3c2, + 0xa7672661, 0xd06016f7, 0x4969474d, 0x3e6e77db, 0xaed16a4a, 0xd9d65adc, + 0x40df0b66, 0x37d83bf0, 0xa9bcae53, 0xdebb9ec5, 0x47b2cf7f, 0x30b5ffe9, + 0xbdbdf21c, 0xcabac28a, 0x53b39330, 0x24b4a3a6, 0xbad03605, 0xcdd70693, + 0x54de5729, 0x23d967bf, 0xb3667a2e, 0xc4614ab8, 0x5d681b02, 0x2a6f2b94, + 0xb40bbe37, 0xc30c8ea1, 0x5a05df1b, 0x2d02ef8d + ]; + let crc = 0xffffffff; + for (let i = 0; i < data.length; i++) { + crc = crc32Table[(crc ^ data[i]) & 0xff] ^ (crc >>> 8); + } + return crc >>> 0; + } + + // --- Auto-Detection --- + autoDetectState(data) { + if (data instanceof Uint8Array) { + if (data.length > 4) { + const magic = this.toUint32(data, 0); + if (magic === 0x414e4459) { // "ANDY" + this.tellyscript_type = TellyScriptType.ORIGINAL; + return TellyScriptState.PACKED; + } else if (magic === 0x564b4154) { // "VKAT" + this.tellyscript_type = TellyScriptType.DIALSCRIPT; + return TellyScriptState.PACKED; + } else { + let hasNull = false, hasEOF = false; + for (let byte of data) { + if (byte === 0x00) hasNull = true; + if (byte === 0xff) hasEOF = true; + } + if (hasNull && hasEOF) return TellyScriptState.TOKENIZED; + else return TellyScriptState.RAW; + } + } else { + return TellyScriptState.UNKNOWN; + } + } else if (typeof data === "string") { + return TellyScriptState.RAW; + } + return TellyScriptState.UNKNOWN; + } + + // --- Process Input --- + process(data, dataState) { + if (dataState === TellyScriptState.UNKNOWN) { + dataState = this.autoDetectState(data); + } + + if (data instanceof Uint8Array) { + if (dataState === TellyScriptState.PACKED) { + this.packed_data = data; + this.unpack(); + this.detokenize(); + } else if (dataState === TellyScriptState.TOKENIZED) { + this.tokenized_data = data; + this.pack(); + this.detokenize(); + } else if (dataState === TellyScriptState.RAW) { + // For RAW byte data, convert to string (assuming UTF-8) + this.process(new TextDecoder().decode(data), dataState); + } + } else if (typeof data === "string") { + if (dataState === TellyScriptState.RAW) { + this.raw_data = data; + this.tokenize(); + this.pack(); + } else if (dataState === TellyScriptState.PACKED || dataState === TellyScriptState.TOKENIZED) { + // For string input with a PACKED/TOKENIZED flag, convert to bytes using UTF-8. + const bytes = new TextEncoder().encode(data); + this.process(bytes, dataState); + } + } + } + + // --- Unpacking --- + unpack() { + // Read header fields from the first 36 bytes. + const headerBytes = this.packed_data.slice(0, PACKED_TELLYSCRIPT_HEADER_SIZE); + this.packed_header = { + magic: String.fromCharCode(...headerBytes.slice(0, 4)), + version_major: this.toUint32(headerBytes, 4), + version_minor: this.toUint32(headerBytes, 8), + script_id: this.toUint32(headerBytes, 12), + script_mod: this.toUint32(headerBytes, 16), + compressed_data_length: this.toUint32(headerBytes, 20), + decompressed_data_length: this.toUint32(headerBytes, 24), + decompressed_checksum: this.toUint32(headerBytes, 28), + unknown1: this.toUint32(headerBytes, 32), + }; + + // Extract compressed data from the remainder of the packed_data. + const compressed_data = this.packed_data.slice(PACKED_TELLYSCRIPT_HEADER_SIZE); + // Decompress using LZSS + const comp = new LZSS(); + this.tokenized_data = comp.expand(compressed_data, this.packed_header.decompressed_data_length); + // Calculate and store the checksum. + this.packed_header.actual_decompressed_checksum = this.crc32(this.tokenized_data); + return this.tokenized_data; + } + + // --- Detokenization --- + detokenize() { + // Uses the previously defined TellyScriptDetokenizer class. + this.raw_data = new WTVTellyScriptDetokenizer(this.tokenized_data).detokenize(); + return this.raw_data; + } + + // --- Tokenization --- + tokenize() { + // Uses the previously defined TellyScriptTokenizer class. + this.tokenized_data = new WTVTellyScriptTokenizer(this.raw_data).tokenize(); + return this.tokenized_data; + } + + // --- Packing --- + pack() { + // Compress tokenized data using LZSS. + const comp = new LZSS(); + const compressed_data = comp.compress(this.tokenized_data); + const crc = this.crc32(this.tokenized_data); + + // Generate a random script_id. + const r1 = Math.floor(Math.random() * (1 << 16)); + const r2 = Math.floor(Math.random() * (1 << 16)); + let script_id = (r1 << 16) | r2; + + let end_value = 0x1b39; + if ((script_id & 0x80000000) !== 0) { + end_value = 0xdd67; + } + script_id = (script_id & 0x80000000) | (Math.floor((script_id & 0x7fffffff) / 10000) * 10000 + end_value); + + this.packed_header = { + magic: (this.tellyscript_type === TellyScriptType.DIALSCRIPT) ? "VKAT" : "ANDY", + version_major: (this.packed_header && this.packed_header.version_major) ? this.packed_header.version_major : 1, + version_minor: (this.packed_header && this.packed_header.version_minor) ? this.packed_header.version_minor : 1, + script_id: script_id, + script_mod: Math.floor(Date.now() / 1000), + compressed_data_length: compressed_data.length, + decompressed_data_length: this.tokenized_data.length, + decompressed_checksum: crc, + actual_decompressed_checksum: crc, + unknown1: UNKNOWN1_VALUE, + }; + + const headerBytes = this.serializePackedHeader(this.packed_header); + const totalLength = PACKED_TELLYSCRIPT_HEADER_SIZE + compressed_data.length; + this.packed_data = new Uint8Array(totalLength); + this.packed_data.set(headerBytes, 0); + this.packed_data.set(compressed_data, PACKED_TELLYSCRIPT_HEADER_SIZE); + + return this.packed_data; + } + + // --- Serialize Header --- + serializePackedHeader(header) { + const buffer = new Uint8Array(PACKED_TELLYSCRIPT_HEADER_SIZE); + // magic: 4 characters + for (let i = 0; i < 4; i++) { + buffer[i] = header.magic.charCodeAt(i); + } + // Next fields: each 4 bytes in Big Endian order. + buffer.set(this.uint32ToBytes(header.version_major), 4); + buffer.set(this.uint32ToBytes(header.version_minor), 8); + buffer.set(this.uint32ToBytes(header.script_id), 12); + buffer.set(this.uint32ToBytes(header.script_mod), 16); + buffer.set(this.uint32ToBytes(header.compressed_data_length), 20); + buffer.set(this.uint32ToBytes(header.decompressed_data_length), 24); + buffer.set(this.uint32ToBytes(header.decompressed_checksum), 28); + buffer.set(this.uint32ToBytes(header.unknown1), 32); + + return buffer; + } +} + +module.exports = WTVTellyScript; \ No newline at end of file diff --git a/zefie_wtvp_minisrv/test_telly.js b/zefie_wtvp_minisrv/test_telly.js new file mode 100644 index 00000000..d4659cd4 --- /dev/null +++ b/zefie_wtvp_minisrv/test_telly.js @@ -0,0 +1,29 @@ +const process = require('process'); +const fs = require('fs'); +const path = require('path'); +const classPath = path.resolve(__dirname + path.sep + "includes" + path.sep + "classes" + path.sep) + path.sep; +const { WTVShared, clientShowAlert } = require(classPath + "/WTVShared.js"); +const WTVTellyScript = require(classPath + "/WTVTellyScript.js") + +//const bf0app = classPath + "/../ServiceDeps/wtv-1800/tellyscripts/bf0app/bf0app_WTV_18006138199.tok" +//const tokened = fs.readFileSync(bf0app); + +const LC2 = classPath + "/../ServiceDeps/wtv-1800/tellyscripts/LC2/LC2_WTV_18006138199.tok"; +const tokened = fs.readFileSync(LC2); + +console.log("OGTOK:", tokened) +var token = new WTVTellyScript(tokened); +console.log("OGTOK Header:", token.packed_header); + +token.raw_data = token.raw_data.replaceAll("zefie", "testing"); +token.tokenize(); +retok = token.pack() + +retok = Buffer.from(retok) +console.log("Retok:", retok) + +var token2 = new WTVTellyScript(retok); + + +console.log("Retok Header:", token2.packed_header); +console.log("Retok detok:", token2.raw_data);