diff --git a/ts/Gvas.ts b/ts/Gvas.ts
index fce9414..a9bf349 100644
--- a/ts/Gvas.ts
+++ b/ts/Gvas.ts
@@ -79,7 +79,12 @@ export interface CustomData {
value: number;
}
-export type GvasText = GvasTextNone | GvasTextArgumentFormat | GvasTextBase;
+export type GvasText =
+ | GvasTextNone
+ | GvasTextBase
+ | GvasTextArgumentFormat
+ | GvasTextAsNumber
+ ;
// Component type 255
export interface GvasTextNone {
@@ -98,13 +103,44 @@ export interface GvasTextBase {
// Component type 3
export interface GvasTextArgumentFormat {
flags: number;
- guid: GvasString;
- pattern: GvasString;
- args: FormatArgumentValue[];
+ sourceFormat: GvasText;
+ args: FormatArgumentValueMap[];
+}
+
+// Component type 4
+export interface GvasTextAsNumber {
+ flags: number;
+ sourceValue: FormatArgumentValue;
+ formatOptions?: NumberFormattingOptions | undefined;
+ targetCulture: GvasString;
}
-export interface FormatArgumentValue {
+export interface FormatArgumentValueMap {
name: GvasString;
- contentType: number;
- values: GvasString[];
+ value: FormatArgumentValue;
+}
+
+export type FormatArgumentValue =
+ | ['Int', number]
+ | ['Text', GvasText]
+ ;
+
+export interface NumberFormattingOptions {
+ alwaysIncludeSign: boolean;
+ useGrouping: boolean;
+ roundingMode: RoundingMode;
+ minimumIntegralDigits: number;
+ maximumIntegralDigits: number;
+ minimumFractionalDigits: number;
+ maximumFractionalDigits: number;
+}
+
+export enum RoundingMode {
+ HalfToEven = 0,
+ HalfFromZero = 1,
+ HalfToZero = 2,
+ FromZero = 3,
+ ToZero = 4,
+ ToNegativeInfinity = 5,
+ ToPositiveInfinity = 6,
}
diff --git a/ts/exporter.ts b/ts/exporter.ts
index 7587eb3..d83ee83 100644
--- a/ts/exporter.ts
+++ b/ts/exporter.ts
@@ -8,6 +8,7 @@ import {
GvasString,
GvasText,
GvasTypes,
+ NumberFormattingOptions,
} from './Gvas';
import {Permission} from './Permission';
import {Quaternion} from './Quaternion';
@@ -1159,10 +1160,11 @@ function stringToBlob(str: GvasString): BlobPart {
function textToBlob(text: GvasText, largeWorldCoords: boolean): BlobPart {
if ('values' in text) {
+ if (text.values.length > 1) throw new Error('Only zero or one values are allowed');
// (u32) Flags
// (u8) Component Type (None = 255)
- // (u32) Component Count
- // (str*) Component Array
+ // (b32) Has Culture Invariant String
+ // (str?) Culture Invariant String
return new Blob([
new Uint32Array([text.flags]),
new Uint8Array([255]),
@@ -1180,46 +1182,79 @@ function textToBlob(text: GvasText, largeWorldCoords: boolean): BlobPart {
new Uint8Array([0]),
new Blob([text.namespace, text.key, text.value].map(stringToBlob)),
]);
- } else if ('pattern' in text) {
+ } else if ('args' in text) {
// (u32) Flags
- // (u8) Component Type (Argument Format = 3)
- // (u8) Unknown (8)
- // (u32) Unknown (0)
- // (str) Unknown
- // (str) GUID
- // (str) Pattern
+ // (u8) Component Type (ArgumentFormat = 3)
+ // (txt) Source Format
// (u32) TextFormat Count
// (...) TextFormat Array
return new Blob([
new Uint32Array([text.flags]),
new Uint8Array([3]),
- new Uint8Array([8, 0, 0, 0, 0]),
- stringToBlob(largeWorldCoords ? '' : null),
- stringToBlob(text.guid),
- stringToBlob(text.pattern),
+ textToBlob(text.sourceFormat, largeWorldCoords),
new Uint32Array([text.args.length]),
- new Blob(text.args.map(rtfToBlob)),
+ new Blob(text.args.map((favm) => new Blob([
+ stringToBlob(favm.name),
+ formatArgumentValueToBlob(favm.value),
+ ]))),
+ ]);
+ } else if ('targetCulture' in text) {
+ // (u32) Flags
+ // (u8) Component Type (AsNumber = 4)
+ // (FAV) Source Value
+ // (b32) Has Number Formatting Options
+ // (NFO?) Number Formatting Options
+ // (str) Target Culture
+ const formatOptions = [text.formatOptions]
+ .filter(Boolean)
+ .map(numberFormattingOptionsToBlob);
+ return new Blob([
+ new Uint32Array([text.flags]),
+ new Uint8Array([4]),
+ formatArgumentValueToBlob(text.sourceValue),
+ new Uint32Array([formatOptions.length]),
+ ...formatOptions,
+ stringToBlob(text.targetCulture),
]);
} else {
throw new Error('Unexpected text type');
}
}
-function rtfToBlob(rtf: FormatArgumentValue): BlobPart {
- // TextFormat:
- // (str) Format Key
- // (u8) Unknown (4)
- // (u32) Content Type
- // (u8) Unknown (255)
- // (u32) Values Count
- // (str*) Values Array
+function formatArgumentValueToBlob(value: FormatArgumentValue): BlobPart {
+ const largeWorldCoords = true; // FIXME
+ if (value[0] === 'Int') {
+ // Int (0)
+ return new Blob([
+ new Uint8Array([0]),
+ new Int32Array([value[1], 0]),
+ ]);
+ } else if (value[0] === 'Text') {
+ // Text (4)
+ return new Blob([
+ new Uint8Array([4]),
+ textToBlob(value[1], largeWorldCoords),
+ ]);
+ } else {
+ throw new Error(`Unexpected FormatArgumentValue ${value}`);
+ }
+}
+
+function numberFormattingOptionsToBlob(value: NumberFormattingOptions): BlobPart {
return new Blob([
- stringToBlob(rtf.name),
- new Uint8Array([4]),
- new Uint32Array([rtf.contentType]),
- new Uint8Array([255]),
- new Uint32Array([rtf.values.length]),
- new Blob(rtf.values.map(stringToBlob)),
+ new Uint32Array([
+ value.alwaysIncludeSign ? 1 : 0,
+ value.useGrouping ? 1 : 0,
+ ]),
+ new Uint8Array([
+ value.roundingMode,
+ ]),
+ new Int32Array([
+ value.minimumIntegralDigits,
+ value.maximumIntegralDigits,
+ value.minimumFractionalDigits,
+ value.maximumFractionalDigits,
+ ]),
]);
}
diff --git a/ts/parser.ts b/ts/parser.ts
index 9204bdd..9b099e4 100644
--- a/ts/parser.ts
+++ b/ts/parser.ts
@@ -10,6 +10,10 @@ import {
GvasTextNone,
FormatArgumentValue,
gvasToString,
+ GvasTextAsNumber,
+ FormatArgumentValueMap,
+ NumberFormattingOptions,
+ RoundingMode,
} from './Gvas';
import {Permission} from './Permission';
import {Quaternion} from './Quaternion';
@@ -623,115 +627,148 @@ function parseStructArray(
/**
* Parse a struct array from a buffer.
* @param {ArrayBuffer} buffer
+ * @param {number} pos
* @return {GvasText[]}
*/
-function parseTextArray(buffer: ArrayBuffer): [number, GvasText[]] {
+function parseTextArray(buffer: ArrayBuffer, pos = 0): [number, GvasText[]] {
// (u32) Entry Count
const entryCount = new Uint32Array(buffer, 0, 1)[0];
- let pos = 4;
+ pos += 4;
const array: GvasText[] = [];
for (let i = 0; i < entryCount; i++) {
- // (u32) Flags
- const flags = new Uint32Array(buffer.slice(pos, pos + 4))[0];
+ let text;
+ [pos, text] = parseText(buffer, pos);
+ array.push(text);
+ }
+ return [pos, array];
+}
+
+function parseText(buffer: ArrayBuffer, pos = 0): [number, GvasText] {
+ // (u32) Flags
+ const flags = new Uint32Array(buffer.slice(pos, pos + 4))[0];
+ pos += 4;
+ // (u8) FTextHistory type
+ const componentType = new Uint8Array(buffer, pos, 1)[0];
+ pos++;
+ // FTextHistory
+ if (componentType === 255) { // None
+ const count = new Uint32Array(buffer.slice(pos, pos + 4))[0];
pos += 4;
- // (u8) Component Type
- const componentType = new Uint8Array(buffer, pos, 1)[0];
- pos++;
- // Read the component
- if (componentType === 255) {
- const count = new Uint32Array(buffer.slice(pos, pos + 4))[0];
- pos += 4;
- const values: GvasString[] = [];
- for (let k = 0; k < count; k++) {
- let value;
- [pos, value] = parseString(buffer, pos);
- values.push(value);
- }
- const value: GvasTextNone = {flags, values};
- array.push(value);
- } else if (componentType === 0) {
- let namespace;
- let key;
+ const values: GvasString[] = [];
+ for (let k = 0; k < count; k++) {
let value;
- [pos, namespace] = parseString(buffer, pos);
- [pos, key] = parseString(buffer, pos);
[pos, value] = parseString(buffer, pos);
- const values: GvasTextBase = {flags, key, namespace, value};
- array.push(values);
- } else if (componentType === 3) {
- // text_rich:
- // seq:
- // - id: flags
- // contents: [8, 0, 0, 0, 0, 0, 0, 0, 0]
- const argumentType = new Uint8Array(buffer, pos, 1)[0];
- if (argumentType !== 8) throw new Error(`Expected argumentType == 8, ${argumentType}`);
- const unknown = new Uint32Array(buffer.slice(pos + 1, pos + 5))[0];
- if (unknown !== 0) throw new Error(`Expected unknown == 0, ${unknown}`);
- pos += 5;
- let unknownStr;
- [pos, unknownStr] = parseString(buffer, pos);
- if (unknownStr && unknownStr.length) throw new Error(`Expected empty str, ${unknownStr}`);
- // - id: component_guid
- // type: string
- let guid;
- [pos, guid] = parseString(buffer, pos);
- // - id: text_format_pattern
- // type: string
- let pattern;
- [pos, pattern] = parseString(buffer, pos);
- // - id: text_format_arg_count
- // type: u4
- const textFormatArgCount = new Uint32Array(buffer.slice(pos, pos + 4))[0];
- pos += 4;
- // - id: text_format
- // type: text_format
- // repeat: expr
- // repeat-expr: text_format_arg_count
- const args: FormatArgumentValue[] = [];
- for (let j = 0; j < textFormatArgCount; j++) {
- // textFormat:
- // seq:
- // - id: format_key
- // type: string
- let name;
- [pos, name] = parseString(buffer, pos);
- // - id: separator
- // contents: [4]
- const separator = new Uint8Array(buffer, pos++, 1)[0];
- if (separator !== 4) {
- throw new Error(`Expected separator == 4, ${separator}`);
- }
- // - id: flags
- // type: u4
- const contentType = new Uint32Array(buffer.slice(pos, pos + 4))[0];
- pos += 4;
- // - id: component_type
- // contents: [255]
- const type = new Uint8Array(buffer, pos++, 1)[0];
- if (type !== 255) {
- throw new Error(`Expected type == 255, ${type}`);
- }
- // - id: count
- // type: u4
- const count = new Uint32Array(buffer.slice(pos, pos + 4))[0];
- pos += 4;
- const values = [];
- for (let k = 0; k < count; k++) {
- // - id: value
- // type: string
- // repeat: expr
- // repeat-expr: count
- let value;
- [pos, value] = parseString(buffer, pos);
- values.push(value);
- }
- args.push({contentType, name, values});
- }
- const values: GvasTextArgumentFormat = {args, flags, guid, pattern};
- array.push(values);
+ values.push(value);
+ }
+ const value: GvasTextNone = {flags, values};
+ return [pos, value];
+ } else if (componentType === 0) { // Base
+ let namespace;
+ let key;
+ let value;
+ [pos, namespace] = parseString(buffer, pos);
+ [pos, key] = parseString(buffer, pos);
+ [pos, value] = parseString(buffer, pos);
+ const base: GvasTextBase = {flags, key, namespace, value};
+ return [pos, base];
+ } else if (componentType === 3) { // ArgumentFormat
+ let sourceFormat;
+ [pos, sourceFormat] = parseText(buffer, pos);
+
+ // (u32) Argument Count
+ const textFormatArgCount = new Uint32Array(buffer.slice(pos, pos + 4))[0];
+ pos += 4;
+ const args: FormatArgumentValueMap[] = [];
+ for (let j = 0; j < textFormatArgCount; j++) {
+ // (str) Name
+ let name;
+ [pos, name] = parseString(buffer, pos);
+ // (text) Value
+ let value;
+ [pos, value] = parseFormatArgumentValue(buffer, pos);
+ args.push({name, value});
+ }
+
+ const value: GvasTextArgumentFormat = {args, flags, sourceFormat};
+ return [pos, value];
+ } else if (componentType === 4) { // AsNumber
+ // (FAV) Source Value
+ let sourceValue: FormatArgumentValue;
+ [pos, sourceValue] = parseFormatArgumentValue(buffer, pos);
+
+ // (b32) Has Number Formatting Options
+ let formatOptions: NumberFormattingOptions | undefined;
+ const hasFormatOptions = new Uint32Array(buffer.slice(pos, pos + 4))[0];
+ pos += 4;
+ if (hasFormatOptions === 0) {
+ // Nothing to do
+ } else if (hasFormatOptions === 1) {
+ // (NFO) Number Formatting Options
+ [pos, formatOptions] = parseNumberFormattingOptions(buffer, pos);
} else {
- throw new Error(`Unexpected component type ${componentType} with flags ${flags}`);
+ throw new Error(`Unexpected hasFormatOptions ${hasFormatOptions}`);
}
+
+ // (str) Target Culture
+ let targetCulture: GvasString;
+ [pos, targetCulture] = parseString(buffer, pos);
+
+ const value: GvasTextAsNumber = {flags, formatOptions, sourceValue, targetCulture};
+ // array.push(value);
+ return [pos, value];
+ } else {
+ throw new Error(`Unexpected component type ${componentType} with flags ${flags}`);
}
- return [pos, array];
+}
+
+function parseFormatArgumentValue(buffer: ArrayBuffer, pos: number): [number, FormatArgumentValue] {
+ const type = new Uint8Array(buffer, pos++, 1)[0];
+ if (type === 0) {
+ const [value, extra] = new Int32Array(buffer.slice(pos, pos + 8));
+ pos += 8;
+ if (extra !== 0) throw new Error('Overflow');
+ return [pos, ['Int', value]];
+ } else if (type === 4) {
+ let text;
+ [pos, text] = parseText(buffer, pos);
+ return [pos, ['Text', text]];
+ } else {
+ throw new Error(`Unknown FormatArgumentValue type ${type}`);
+ }
+}
+
+function parseNumberFormattingOptions(buffer: ArrayBuffer, pos: number): [number, NumberFormattingOptions] {
+ // (b32) Always Include Sign
+ // (b32) Use Grouping
+ // (e8) Rounding Mode
+ // (i32) Minimum Integral Digits
+ // (i32) Maximum Integral Digits
+ // (i32) Minimum Fractional Digits
+ // (i32) Maximum Fractional Digits
+
+ const uint32s = new Uint32Array(buffer.slice(pos, pos + 8));
+ const u8s = new Uint8Array(buffer, pos + 8, 1);
+ const int32s = new Int32Array(buffer.slice(pos + 9, pos + 25));
+
+ if (u8s[0] > RoundingMode.ToPositiveInfinity) {
+ throw new Error(`Invalid RoundingMode ${u8s[0]}`);
+ }
+
+ const alwaysIncludeSign = Boolean(uint32s[0]);
+ const useGrouping = Boolean(uint32s[1]);
+ const roundingMode: RoundingMode = u8s[0];
+ const minimumIntegralDigits = int32s[0];
+ const maximumIntegralDigits = int32s[1];
+ const minimumFractionalDigits = int32s[2];
+ const maximumFractionalDigits = int32s[3];
+
+ return [pos + 25, {
+ alwaysIncludeSign,
+ maximumFractionalDigits,
+ maximumIntegralDigits,
+ minimumFractionalDigits,
+ minimumIntegralDigits,
+ roundingMode,
+ useGrouping,
+ }];
}
diff --git a/ts/util.ts b/ts/util.ts
index 9356208..82fa2dc 100644
--- a/ts/util.ts
+++ b/ts/util.ts
@@ -1,4 +1,4 @@
-import {GvasString, GvasText} from './Gvas';
+import {FormatArgumentValueMap, GvasString, GvasText, GvasTextArgumentFormat} from './Gvas';
import {Quaternion} from './Quaternion';
import {Rotator} from './Rotator';
import {Vector} from './Vector';
@@ -38,35 +38,57 @@ export function stringToText(str: GvasString): GvasText {
if (lines.length === 1) return {flags: 0, values: [str]};
return {
args: lines.map((line, i) => ({
- contentType: 2,
name: String(i),
- values: line ? [line] : [],
- })),
+ value: ['Text', {
+ flags: 2,
+ values: line ? [line] : [],
+ }],
+ } satisfies FormatArgumentValueMap)),
flags: 1,
- guid: RRO_TEXT_GUID,
- pattern: lines.map((_, i) => '{' + i + '}').join('
'),
- };
+ sourceFormat: {
+ flags: 8,
+ key: RRO_TEXT_GUID,
+ namespace: '',
+ value: lines.map((_, i) => '{' + i + '}').join('
'),
+ },
+ } satisfies GvasTextArgumentFormat;
}
export function textToString(value: GvasText): GvasString {
if (value === null) return null;
- if ('pattern' in value) {
- // ArgumentFormat
- switch (value.guid) {
+ if ('key' in value) {
+ // Base (0)
+ if (value.namespace !== '') throw new Error(`Unexpected unknown value: ${value.namespace}`);
+ return value.value;
+ } else if ('args' in value) {
+ // ArgumentFormat (3)
+ const sourceFormat = value.sourceFormat;
+ if (!('key' in sourceFormat)) throw new Error('Unexpected sourceFormat');
+ switch (sourceFormat.key) {
case RRO_TEXT_GUID:
case '1428110346E6AD292230C4AA503E3FE9':
case '69981E2B47B2AABC01CE39842FB03A96':
break;
default:
- throw new Error(`Unexpected GUID: ${value.guid}`);
+ throw new Error(`Unexpected GUID: ${sourceFormat.key}`);
+ }
+ const pattern = sourceFormat.value;
+ if (pattern === null) throw new Error('Null pattern');
+ return pattern.replace(/{(\d+)}/g,
+ (_, i) => {
+ const v = value.args[Number(i)].value;
+ if (v[0] !== 'Text') throw new Error('Unexpected formatArgumentValue');
+ return textToString(v[1]) ?? '';
+ });
+ } else if ('sourceValue' in value) {
+ // AsNumber (4)
+ if (value.sourceValue[0] === 'Int') {
+ return String(value.sourceValue[1]);
+ } else if (value.sourceValue[0] === 'Text') {
+ return textToString(value.sourceValue[1]);
+ } else {
+ throw new Error(`Unknown Source Value ${value}`);
}
- if (value.pattern === null) throw new Error('Null pattern');
- return value.pattern.replace(/{(\d+)}/g,
- (_, i) => value.args[Number(i)].values[0] ?? '');
- } else if ('key' in value) {
- // Base
- if (value.namespace !== '') throw new Error(`Unexpected unknown value: ${value.namespace}`);
- return value.value;
} else {
// None
if (0 === value.values.length) return null;