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
JavaScript
;
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;