UNPKG

xcom2charpool

Version:

Library for reading, manipulating, and managing XCOM 2 character pool binary files, supporting both browser and Node.js environments.

123 lines (122 loc) 4.38 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.FSting = void 0; /** * FString codec used by Reader/Writer implementations. * Length prefix is int32 including null terminator: * - Positive length: 8-bit bytes (low byte of UCS-2), null-terminated. * - Negative length: UTF-16LE code units, null-terminated. */ class FSting { static read(reader) { const length = reader.int32(); if (length === 0) return ''; if (length > 0) { return FSting.readAnsi(reader, length); } return FSting.readUtf16(reader, length); } static write(writer, value) { if (value.length === 0) { writer.int32(0); return; } if (FSting.isAnsi(value)) { const encoded = FSting.encodeAnsi(value); writer.int32(encoded.length + 1); writer.bytes(encoded); writer.byte(FSting.NullByte); return; } else { const encoded = FSting.encodeUtf16LE(value); const codeUnitCount = value.length + 1; // Negative length in number of 16-bit code units + null terminator writer.int32(-codeUnitCount); writer.bytes(encoded); writer.byte(FSting.NullByte).byte(FSting.NullByte); // Two bytes null terminator for UTF-16 } } static readAnsi(reader, length) { const bytes = reader.bytes(length); if (bytes[bytes.length - 1] !== FSting.NullByte) { throw new Error('Invalid FString ANSI terminator'); } return FSting.decodeAnsi(bytes.subarray(0, bytes.length - 1)); } static readUtf16(reader, length) { const codeUnitCount = -length; if (codeUnitCount < 0) { throw new Error('Invalid FString UTF-16 length'); } const byteLength = codeUnitCount * 2; const bytes = reader.bytes(byteLength); if (bytes.length < 2) { throw new Error('Invalid FString UTF-16 length'); } const last = bytes.length - 1; if (bytes[last] !== FSting.NullByte || bytes[last - 1] !== FSting.NullByte) { throw new Error('Invalid FString UTF-16 terminator'); } return FSting.decodeUtf16LE(bytes.subarray(0, bytes.length - 2)); } static isAnsi(value) { for (let i = 0; i < value.length; i++) { if (value.charCodeAt(i) > 0xff) return false; } return true; } static encodeAnsi(value) { const bytes = new Uint8Array(value.length); for (let i = 0; i < value.length; i++) { const code = value.charCodeAt(i); if (code > 0xff) { throw new Error('FString ANSI encode received non-ANSI code unit'); } bytes[i] = code; } return bytes; } static decodeAnsi(bytes) { if (bytes.length === 0) return ''; const chunkSize = 0x8000; let result = ''; for (let i = 0; i < bytes.length; i += chunkSize) { const chunk = bytes.subarray(i, i + chunkSize); result += String.fromCharCode(...chunk); } return result; } static encodeUtf16LE(value) { const buffer = new ArrayBuffer(value.length * 2); const view = new DataView(buffer); for (let i = 0; i < value.length; i++) { view.setUint16(i * 2, value.charCodeAt(i), true); } return new Uint8Array(buffer); } static decodeUtf16LE(bytes) { if (bytes.length === 0) return ''; if (bytes.length % 2 !== 0) { throw new Error('Invalid FString UTF-16 byte length'); } const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); const codeUnitCount = bytes.length / 2; const chunkSize = 0x4000; let result = ''; for (let i = 0; i < codeUnitCount; i += chunkSize) { const size = Math.min(chunkSize, codeUnitCount - i); const chunk = new Array(size); for (let j = 0; j < size; j++) { chunk[j] = view.getUint16((i + j) * 2, true); } result += String.fromCharCode(...chunk); } return result; } } exports.FSting = FSting; FSting.NullByte = 0x00;