diff --git a/src/bfres/bfres.ts b/src/bfres/bfres.ts new file mode 100644 index 0000000..84890ce --- /dev/null +++ b/src/bfres/bfres.ts @@ -0,0 +1,325 @@ +import FileStream from '@/file-stream'; +import { parseIndexGroup } from '@/bfres'; +import FMDL from '@/bfres/fmdl'; + +const BFRES_MAGIC = Buffer.from('FRES'); +const BIG_ENDIAN = Buffer.from('FEFF', 'hex'); +const LITTLE_ENDIAN = Buffer.from('FFFE', 'hex'); + +export interface IndexGroup { + length: number; + entryCount: number; + entries: IndexGroupEntry[]; +} + +export interface IndexGroupEntry { + isRoot: boolean; + searchValue: number; + leftIndex: number; + rightIndex: number; + namePointer: number; + dataPointer: number; + name: string; +} + +export default class BFRES { + private stream: FileStream; + + public version: string; + public stringTableLength: number; + public stringTableOffset: number; + public indexGroupOffets: number[] = []; + public fileCounts: number[] = []; + public stringTable: { offset: number; value: string; }[] = []; + public indexGroups: (IndexGroup | null)[] = []; + public models: FMDL[] = []; + + /** + * Parses the BFRES from the provided `fdOrPath` + * + * @param fdOrPath - Either an open `fd` or a path to a file on disk + */ + public parseFromFile(fdOrPath: number | string): void { + this.stream = new FileStream(fdOrPath); + this.parse(); + } + + /** + * Parses the BFRES from the provided `buffer` + * + * @param buffer - BFRES data buffer + */ + public parseFromBuffer(buffer: Buffer): void { + this.stream = new FileStream(buffer); + this.parse(); + } + + /** + * Parses the BFRES from the provided string + * + * Calls `parseFromBuffer` internally + * + * @param base64 - Base64 encoded BFRES data + */ + public parseFromString(base64: string): void { + this.parseFromBuffer(Buffer.from(base64, 'base64')); + } + + /** + * Parses the BFRES from an existing file stream + * + * @param stream - An existing file stream + */ + public parseFromFileStream(stream: FileStream): void { + this.stream = stream; + this.parse(); + } + + /** + * Creates a new instance of `BFRES` and + * parses the BFRES from the provided `fdOrPath` + * + * @param fdOrPath - Either an open `fd` or a path to a file on disk + */ + public static fromFile(fdOrPath: number | string): BFRES { + const bfres = new BFRES(); + bfres.parseFromFile(fdOrPath); + + return bfres; + } + + /** + * Creates a new instance of `BFRES` and + * parses the BFRES from the provided `buffer` + * + * @param buffer - BFRES data buffer + */ + public static fromBuffer(buffer: Buffer): BFRES { + const bfres = new BFRES(); + bfres.parseFromBuffer(buffer); + + return bfres; + } + + /** + * Creates a new instance of `BFRES` and + * parses the BFRES from the provided string + * + * Calls `parseFromBuffer` internally + * + * @param base64 - Base64 encoded BFRES data + */ + public static fromString(base64: string): BFRES { + const bfres = new BFRES(); + bfres.parseFromString(base64); + + return bfres; + } + + /** + * Creates a new instance of `BFRES` and + * parses the BFRES from an existing file stream + * + * @param stream - An existing file stream + */ + public static fromFileStream(stream: FileStream): BFRES { + const bfres = new BFRES(); + bfres.parseFromFileStream(stream); + + return bfres; + } + + /** + * Parses the BFRES from the input source provided at instantiation + */ + public parse(): void { + this.stream.consumeAll(); // * Read the whole file into memory since we need to jump around a lot + + const magic = this.stream.readBytes(0x4); + + if (!BFRES_MAGIC.equals(magic)) { + throw new Error('Invalid BFRES magic'); + } + + const versionMajor = this.stream.readUInt8(); + const versionMinor = this.stream.readUInt8(); + const versionPatch = this.stream.readUInt8(); + const versionPre = this.stream.readUInt8(); + + this.version = `${versionMajor}.${versionMinor}.${versionPatch}.${versionPre}`; + this.stream.metadata['bfres-version'] = this.version; // * Subfiles have differing behavior based on the BFRES version + + const bom = this.stream.readBytes(0x2); + + if (!LITTLE_ENDIAN.equals(bom) && !BIG_ENDIAN.equals(bom)) { + throw new Error('Invalid BOM indicator'); + } + + if (BIG_ENDIAN.equals(bom)) { + this.stream.bom = 'be'; + } + + const headerLength = this.stream.readUInt16(); + + if (headerLength !== 0x10) { + throw new Error(`Invalid header length. Expected ${0x10}, got ${headerLength}`); + } + + this.stream.skip(0x4); // * Total BFRES file size + this.stream.skip(0x4); // * Alignment + this.stream.skip(0x4); // * File name offset + + this.stringTableLength = this.stream.readInt32(); + this.stringTableOffset = this.stream.tell() + this.stream.readInt32(); // TODO - Offsets are relative to themselves. This pattern shows up A LOT. Move this to the Stream class? + + for (let i = 0; i < 12; i++) { + const offsetBase = 0x20 + (i * 4); + const relativeOffset = this.stream.readInt32(); + + if (relativeOffset === 0) { + // * 0 indicates this subfile type is not present + this.indexGroupOffets.push(0); + } else { + this.indexGroupOffets.push(relativeOffset + offsetBase); + } + } + + for (let i = 0; i < 12; i++) { + this.fileCounts.push(this.stream.readUInt16()); + } + + this.parseStringTable(); + this.parseIndexGroups(); + + for (const indexGroup of this.indexGroups) { + if (!indexGroup) { + continue; + } + + for (const entry of indexGroup.entries) { + if (entry.isRoot) { + continue; + } + + this.stream.seek(entry.dataPointer); + + // TODO - This is a hack and will break on emebdded files (group 11) since they can be ANY data, and may not have a 4 byte magic + const magic = this.stream.readBytes(0x4).toString(); + this.stream.skip(-0x4); // * Put it back so the subfile reads correctly + + switch (magic) { + case 'FMDL': + this.models.push(FMDL.fromFileStream(this.stream)); + break; + } + } + } + } + + public exportModelAsOBJ(modelName: string): string { + const model = this.models.find(({ fileName }) => fileName === modelName); + + if (!model) { + return ''; + } + + let obj = '# Exported from BFRES\n'; + obj += `# Model: ${modelName}\n\n`; + + for (const fvtx of model.vertexArray) { + for (const vertex of fvtx.vertices) { + if (vertex.position && Array.isArray(vertex.position)) { + const [x, y, z] = vertex.position; + obj += `v ${x} ${y} ${z}\n`; + } + } + } + + for (const fvtx of model.vertexArray) { + for (const vertex of fvtx.vertices) { + if (vertex.normal && Array.isArray(vertex.normal)) { + const [x, y, z] = vertex.normal; + obj += `vn ${x} ${y} ${z}\n`; + } + } + } + + for (const shape of model.shapes) { + obj += `o ${shape.polygonName}\n`; + + if (shape.levelOfDetailModels.length > 0) { + const lodModel = shape.levelOfDetailModels[0]; + const indices = lodModel.indices; + + for (let i = 0; i < indices.length; i += 3) { + if (i + 2 < indices.length) { + const idx1 = indices[i] + 1; + const idx2 = indices[i + 1] + 1; + const idx3 = indices[i + 2] + 1; + + obj += `f ${idx1}//${idx1} ${idx2}//${idx2} ${idx3}//${idx3}\n`; + } + } + + obj += '\n'; + } + } + + return obj; + } + + public exportModelAsGLB(modelName: string): Buffer { + const model = this.models.find(({ fileName }) => fileName === modelName); + + // * https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#glb-file-format-specification + const glb = Buffer.from([ + 0x67, 0x6C, 0x54, 0x46, // * ASCII string "glTF" + 0x02, 0x00, 0x00, 0x00, // * Version 2 + 0x0C, 0x00, 0x00, 0x00 // * Length, including the header. 12 for now because that's how long the header is + ]); + + if (!model) { + return glb; + } + + // TODO - Actually fill this + // TODO - Make this function accept some settings to set custom skeletons/animations? Arian mentioned that Miis have a separate BFRES file for animations + + return glb; + } + + private parseStringTable(): void { + this.stream.seek(this.stringTableOffset); + + // * References to strings are offsets to this table, so pre-parse it + // * to reduce seeks + while (this.stream.tell() !== this.stringTableOffset+this.stringTableLength) { + const offset = this.stream.tell() + 4; // * References are offset to the string directly, not including the length header + const stringLength = this.stream.readUInt32(); + const string = this.stream.readBytes(stringLength).toString(); + + this.stream.skip(1); // * Null byte + this.stream.alignBlock(4); + + this.stringTable.push({ + offset: offset, + value: string + }); + } + + // * Subfiles need access to this + this.stream.metadata['bfres-string-table'] = this.stringTable; + } + + private parseIndexGroups(): void { + for (const indexGroupOffset of this.indexGroupOffets) { + if (indexGroupOffset === 0) { + this.indexGroups.push(null); + continue; + } + + this.stream.seek(indexGroupOffset); + this.indexGroups.push(parseIndexGroup(this.stream)); + } + } +} \ No newline at end of file diff --git a/src/bfres/fmdl/fmdl.ts b/src/bfres/fmdl/fmdl.ts new file mode 100644 index 0000000..3fade1e --- /dev/null +++ b/src/bfres/fmdl/fmdl.ts @@ -0,0 +1,137 @@ +import FileStream from '@/file-stream'; +import FSKL from '@/bfres/fmdl/fskl'; +import FVTX from '@/bfres/fmdl/fvtx'; +import FSHP from '@/bfres/fmdl/fshp'; +import { parseIndexGroup, parseStringTableString } from '@/bfres/index'; +import type { IndexGroup } from '@/bfres/index'; + +const FMDL_MAGIC = Buffer.from('FMDL'); + +export default class FMDL { + private stream: FileStream; + + public fileNameOffset: number; + public filePathOffset: number; + public skeletonOffset: number; + public vertexArrayOffset: number; + public shapeIndexGroupOffset: number; + public materialIndexGroupOffset: number; + public userDataIndexGroupOffsetStart: number; + public userDataIndexGroupOffset: number; + public vertexArrayCount: number; + public shapeCount: number; + public materialCount: number; + public userDataEntryCount: number; + public verticesCount: number; + public fileName: string; + public filePath: string; + public skeleton: FSKL; + public vertexArray: FVTX[] = []; + public shapeIndexGroup: IndexGroup; + public materialIndexGroup: IndexGroup; + public userDataIndexGroup: IndexGroup | null = null; + public shapes: FSHP[] = []; + + /** + * Parses the FMDL from an existing file stream + * + * @param stream - An existing file stream + */ + public parseFromFileStream(stream: FileStream): void { + this.stream = stream; + this.parse(); + } + + /** + * Creates a new instance of `FMDL` and + * parses the FMDL from an existing file stream + * + * @param stream - An existing file stream + */ + public static fromFileStream(stream: FileStream): FMDL { + const fmdl = new FMDL(); + fmdl.parseFromFileStream(stream); + + return fmdl; + } + + /** + * Parses the FMDL from the input source provided at instantiation + */ + public parse(): void { + this.stream.consumeAll(); + + const magic = this.stream.readBytes(0x4); + + if (!FMDL_MAGIC.equals(magic)) { + throw new Error('Invalid FMDL magic'); + } + + this.fileNameOffset = this.stream.tell() + this.stream.readInt32(); + this.filePathOffset = this.stream.tell() + this.stream.readInt32(); + this.skeletonOffset = this.stream.tell() + this.stream.readInt32(); + this.vertexArrayOffset = this.stream.tell() + this.stream.readInt32(); + this.shapeIndexGroupOffset = this.stream.tell() + this.stream.readInt32(); + this.materialIndexGroupOffset = this.stream.tell() + this.stream.readInt32(); + this.userDataIndexGroupOffsetStart = this.stream.tell(); // * The offset can be 0, so that needs to be preserved. Start offset can be added later + this.userDataIndexGroupOffset = this.stream.readInt32(); + this.vertexArrayCount = this.stream.readUInt16(); + this.shapeCount = this.stream.readUInt16(); + this.materialCount = this.stream.readUInt16(); + this.userDataEntryCount = this.stream.readUInt16(); + this.verticesCount = this.stream.readUInt32(); + + this.stream.skip(0x4); // * User pointer + + this.fileName = parseStringTableString(this.stream, this.fileNameOffset); + this.filePath = parseStringTableString(this.stream, this.filePathOffset); + + this.parseSkeleton(); + this.parseVertexArray(); + this.parseShapeIndexGroup(); + this.parseMaterialIndexGroup(); + this.parseUserDataIndexGroup(); + } + + private parseSkeleton(): void { + this.stream.seek(this.skeletonOffset); + this.skeleton = FSKL.fromFileStream(this.stream); + } + + private parseVertexArray(): void { + this.stream.seek(this.vertexArrayOffset); + for (let i = 0; i < this.vertexArrayCount; i++) { + this.vertexArray.push(FVTX.fromFileStream(this.stream)); + + // * This array only stores the headers. Parsing the file data + // * moves the stream position, so we need to reset it here + this.stream.seek(this.vertexArrayOffset + (0x20 * i)); + } + } + + private parseShapeIndexGroup(): void { + this.stream.seek(this.shapeIndexGroupOffset); + this.shapeIndexGroup = parseIndexGroup(this.stream); + + for (const entry of this.shapeIndexGroup.entries) { + if (entry.isRoot) { + continue; + } + + this.stream.seek(entry.dataPointer); + this.shapes.push(FSHP.fromFileStream(this.stream)); + } + } + + private parseMaterialIndexGroup(): void { + this.stream.seek(this.materialIndexGroupOffset); + this.materialIndexGroup = parseIndexGroup(this.stream); + } + + private parseUserDataIndexGroup(): void { + if (this.userDataIndexGroupOffset) { + this.stream.seek(this.userDataIndexGroupOffsetStart + this.userDataIndexGroupOffset); + this.userDataIndexGroup = parseIndexGroup(this.stream); + } + } +} \ No newline at end of file diff --git a/src/bfres/fmdl/fshp.ts b/src/bfres/fmdl/fshp.ts new file mode 100644 index 0000000..2771b73 --- /dev/null +++ b/src/bfres/fmdl/fshp.ts @@ -0,0 +1,243 @@ +import FileStream from '@/file-stream'; +import { parseStringTableString } from '@/bfres'; +import type { FVTX_BUFFER } from '@/bfres/fmdl/fvtx'; + +const FSHP_MAGIC = Buffer.from('FSHP'); + +// * Stolen from https://pastebin.com/DCrP1w9x because lazy +export enum GX2PrimitiveType { + GX2_PRIMITIVE_POINTS = 0x01, + GX2_PRIMITIVE_LINES = 0x02, + GX2_PRIMITIVE_LINE_STRIP = 0x03, + GX2_PRIMITIVE_TRIANGLES = 0x04, + GX2_PRIMITIVE_TRIANGLE_FAN = 0x05, + GX2_PRIMITIVE_TRIANGLE_STRIP = 0x06, + GX2_PRIMITIVE_LINES_ADJACENCY = 0x0a, + GX2_PRIMITIVE_LINE_STRIP_ADJACENCY = 0x0b, + GX2_PRIMITIVE_TRIANGLES_ADJACENCY = 0x0c, + GX2_PRIMITIVE_TRIANGLE_STRIP_ADJACENCY = 0x0d, + GX2_PRIMITIVE_RECTS = 0x11, + GX2_PRIMITIVE_LINE_LOOP = 0x12, + GX2_PRIMITIVE_QUADS = 0x13, + GX2_PRIMITIVE_QUAD_STRIP = 0x14, + GX2_PRIMITIVE_TESSELLATE_LINES = 0x82, + GX2_PRIMITIVE_TESSELLATE_LINE_STRIP = 0x83, + GX2_PRIMITIVE_TESSELLATE_TRIANGLES = 0x84, + GX2_PRIMITIVE_TESSELLATE_TRIANGLE_STRIP = 0x86, + GX2_PRIMITIVE_TESSELLATE_QUADS = 0x93, + GX2_PRIMITIVE_TESSELLATE_QUAD_STRIP = 0x94 +} + +// * Stolen from https://pastebin.com/DCrP1w9x because lazy +export enum GX2IndexFormat { + GX2_INDEX_FORMAT_U16_LE = 0, + GX2_INDEX_FORMAT_U32_LE = 1, + GX2_INDEX_FORMAT_U16 = 4, + GX2_INDEX_FORMAT_U32 = 9 +} + +export type FSHP_LOD_MODEL = { + primitiveType: GX2PrimitiveType; + indexFormat: GX2IndexFormat; + maxDrawnPointsCount: number; + visibilityGroupCount: number; + visibilityGroupOffset: number; + indexBufferOffset: number; + skippedVerticesCount: number; + indexBuffer: FVTX_BUFFER; + visibilityGroups: FSHP_VISIBILITY_GROUP[]; + indices: number[]; +}; + +export type FSHP_VISIBILITY_GROUP = { + indexBufferOffset: number; + drawnPointsCount: number; +}; + +export default class FSHP { + private stream: FileStream; + + public polygonNameOffset: number; + public polygonName: string; + public flags: number; + public sectionIndex: number; + public materialIndex: number; + public skeletonIndex: number; + public vertexIndex: number; + public skeletonBoneIndex: number; + public vertexSkinCount: number; + public levelOfDetailModelCount: number; + public keyShapeCount: number; + public targetAttributeCount: number; + public visibilityGroupTreeNodeCount: number; + public boundingBoxRadius: number; + public vertexBufferOffset: number; + public levelOfDetailModelOffset: number; + public skeletonIndexArrayOffset: number; + public keyShapeIndexOffset: number; + public visibilityGroupTreeNodesOffset: number; + public visibilityGroupTreeRangesOffset: number; + public visibilityGroupTreeIndicesOffset: number; + public levelOfDetailModels: FSHP_LOD_MODEL[] = []; + + /** + * Parses the FSHP from an existing file stream + * + * @param stream - An existing file stream + */ + public parseFromFileStream(stream: FileStream): void { + this.stream = stream; + this.parse(); + } + + /** + * Creates a new instance of `FSHP` and + * parses the FSHP from an existing file stream + * + * @param stream - An existing file stream + */ + public static fromFileStream(stream: FileStream): FSHP { + const fshp = new FSHP(); + fshp.parseFromFileStream(stream); + + return fshp; + } + + /** + * Parses the FSHP from the input source provided at instantiation + */ + public parse(): void { + this.stream.consumeAll(); + + const magic = this.stream.readBytes(0x4); + + if (!FSHP_MAGIC.equals(magic)) { + throw new Error('Invalid FSHP magic'); + } + + this.polygonNameOffset = this.stream.tell() + this.stream.readInt32(); + this.flags = this.stream.readUInt32(); + this.sectionIndex = this.stream.readUInt16(); + this.materialIndex = this.stream.readUInt16(); + this.skeletonIndex = this.stream.readUInt16(); + this.vertexIndex = this.stream.readUInt16(); + this.skeletonBoneIndex = this.stream.readUInt16(); + this.vertexSkinCount = this.stream.readUInt8(); + this.levelOfDetailModelCount = this.stream.readUInt8(); + this.keyShapeCount = this.stream.readUInt8(); + this.targetAttributeCount = this.stream.readUInt8(); + this.visibilityGroupTreeNodeCount = this.stream.readUInt16(); + this.boundingBoxRadius = this.stream.readFloat(); + this.vertexBufferOffset = this.stream.tell() + this.stream.readInt32(); + this.levelOfDetailModelOffset = this.stream.tell() + this.stream.readInt32(); + this.skeletonIndexArrayOffset = this.stream.tell() + this.stream.readInt32(); + this.keyShapeIndexOffset = this.stream.tell() + this.stream.readInt32(); + this.visibilityGroupTreeNodesOffset = this.stream.tell() + this.stream.readInt32(); + this.visibilityGroupTreeRangesOffset = this.stream.tell() + this.stream.readInt32(); + this.visibilityGroupTreeIndicesOffset = this.stream.tell() + this.stream.readInt32(); + + this.stream.skip(0x4); // * User pointer + + this.polygonName = parseStringTableString(this.stream, this.polygonNameOffset); + this.parseLoDModels(); + this.parseKeyShapes(); + this.parseTargetAttributes(); + } + + private parseLoDModels(): void { + for (let i = 0; i < this.levelOfDetailModelCount; i++) { + this.stream.seek(this.levelOfDetailModelOffset + (0x1C * i)); + + const primitiveType = this.stream.readUInt32(); + const indexFormat = this.stream.readUInt32(); + const maxDrawnPointsCount = this.stream.readUInt32(); + const visibilityGroupCount = this.stream.readUInt16(); + + this.stream.skip(0x2); // * Padding + + const visibilityGroupOffset = this.stream.tell() + this.stream.readInt32(); + const indexBufferOffset = this.stream.tell() + this.stream.readInt32(); + const skippedVerticesCount = this.stream.readUInt32(); + + this.stream.seek(indexBufferOffset); + + const indexBufferDataPointer = this.stream.readUInt32(); + const indexBufferSize = this.stream.readUInt32(); + const indexBufferBufferHandle = this.stream.readUInt32(); + const indexBufferStride = this.stream.readUInt16(); + const indexBufferBufferingCount = this.stream.readUInt16(); + const indexBufferContextPointer = this.stream.readUInt32(); + const indexBufferDataOffset = this.stream.tell() + this.stream.readInt32(); + + this.stream.seek(indexBufferDataOffset); + + const indexBufferData = this.stream.read(indexBufferSize); + + const indexBuffer: FVTX_BUFFER = { + dataPointer: indexBufferDataPointer, + size: indexBufferSize, + bufferHandle: indexBufferBufferHandle, + stride: indexBufferStride, + bufferingCount: indexBufferBufferingCount, + contextPointer: indexBufferContextPointer, + dataOffset: indexBufferDataOffset, + data: indexBufferData + }; + + const visibilityGroups: FSHP_VISIBILITY_GROUP[] = []; + + for (let j = 0; j < visibilityGroupCount; j++) { + this.stream.seek(visibilityGroupOffset + (0x08 * j)); + + visibilityGroups.push({ + indexBufferOffset: this.stream.readUInt32(), + drawnPointsCount: this.stream.readUInt32() + }); + } + + const indices: number[] = []; + + // TODO - This is mostly a hack, ideally this would be done with "this.stream" + if (indexFormat === GX2IndexFormat.GX2_INDEX_FORMAT_U16_LE) { + for (let i = 2 * skippedVerticesCount; i < indexBufferData.length; i += 2) { + indices.push(indexBufferData.readUInt16LE(i)); + } + } else if (indexFormat === GX2IndexFormat.GX2_INDEX_FORMAT_U32_LE) { + for (let i = 4 * skippedVerticesCount; i < indexBufferData.length; i += 4) { + indices.push(indexBufferData.readUInt32LE(i)); + } + } else if (indexFormat === GX2IndexFormat.GX2_INDEX_FORMAT_U16) { + // TODO - This should use the streams endianness I think? + for (let i = 2 * skippedVerticesCount; i < indexBufferData.length; i += 2) { + indices.push(indexBufferData.readUInt16BE(i)); + } + } else if (indexFormat === GX2IndexFormat.GX2_INDEX_FORMAT_U32) { + // TODO - This should use the streams endianness I think? + for (let i = 4 * skippedVerticesCount; i < indexBufferData.length; i += 4) { + indices.push(indexBufferData.readUInt32BE(i)); + } + } + + this.levelOfDetailModels.push({ + primitiveType: primitiveType, + indexFormat: indexFormat, + maxDrawnPointsCount: maxDrawnPointsCount, + visibilityGroupCount: visibilityGroupCount, + visibilityGroupOffset: visibilityGroupOffset, + indexBufferOffset: indexBufferOffset, + skippedVerticesCount: skippedVerticesCount, + indexBuffer: indexBuffer, + visibilityGroups: visibilityGroups, + indices + }); + } + } + + private parseKeyShapes(): void { + // * https://mk8.tockdom.com/wiki/FMDL_(File_Format) doesn't document this + } + + private parseTargetAttributes(): void { + // * https://mk8.tockdom.com/wiki/FMDL_(File_Format) doesn't document this + } +} \ No newline at end of file diff --git a/src/bfres/fmdl/fskl.ts b/src/bfres/fmdl/fskl.ts new file mode 100644 index 0000000..01e0ae6 --- /dev/null +++ b/src/bfres/fmdl/fskl.ts @@ -0,0 +1,202 @@ +import FileStream from '@/file-stream'; +import { parseIndexGroup, parseStringTableString } from '@/bfres'; +import type { IndexGroup } from '@/bfres/bfres'; + +const FSKL_MAGIC = Buffer.from('FSKL'); + +export enum FSKL_SCALING_MODE { + NONE, + STANDARD, + MAYA, + SOFTIMAGE +} + +export enum FSKL_ROTATION_MODE { + QUATERNION, + EULER_XYZ +} + +export enum FSKL_BONE_BILLBOARD_PROJECTION { + NONE, + CHILD, + WORLD_VIEW_VECTOR, + WORLD_VIEW_POINT, + SCREEN_VIEW_VECTOR, + SCREEN_VIEW_POINT, + Y_AXIS_VECTOR, + Y_AXIS_POINT +} + +export const FSKL_BONE_TRANSFORM_FLAGS = { + SEGMENT_SCALE_COMPENSATION: 1, + SCALE_UNIFORMLY: 2, + SCALE_VOLUME_BY_1: 4, + NO_ROTATION: 8, + NO_TRANSLATION: 16 +} as const; + +export const FSKL_BONE_HIERARCHY_TRANSFORM_FLAGS = { + SCALE_UNIFORMLY: 1, + SCALE_VOLUME_BY_1: 2, + NO_ROTATION: 4, + NO_TRANSLATION: 8 +} as const; + +export type FSKLBone = { + nameOffset: number; + name: string; + boneIndex: number; + parentIndex: number; + smoothMatrixIndex: number; + rigidMatrixIndex: number; + billboardIndex: number; + userDataEntryCount: number; + flagsRaw: number; + flags: { + visible: boolean; + rotationMode: FSKL_ROTATION_MODE; + billboardProjection: FSKL_BONE_BILLBOARD_PROJECTION; + transformFlags: number; + transformHierarchyFlags: number; + }; + scaleVectors: number[]; + rotationVectors: number[]; + translationVectors: number[]; + userIndexGroupOffsetStart: number; + userIndexGroupOffset: number; + userIndexGroup: IndexGroup | null; +}; + +export default class FSKL { + private stream: FileStream; + + public boneArrayCount: number; + public smoothIndexCount: number; + public rigidIndexCount: number; + public boneIndexGroupOffset: number; + public boneArrayOffset: number; + public smoothIndexArrayOffset: number; + public smoothMatrixArrayOffset: number; + public boneIndexGroup: IndexGroup; + public bones: FSKLBone[] = []; + public flags: number; + public scalingMode: FSKL_SCALING_MODE; + public rotationMode: FSKL_ROTATION_MODE; + + /** + * Parses the FSKL from an existing file stream + * + * @param stream - An existing file stream + */ + public parseFromFileStream(stream: FileStream): void { + this.stream = stream; + this.parse(); + } + + /** + * Creates a new instance of `FSKL` and + * parses the FSKL from an existing file stream + * + * @param stream - An existing file stream + */ + public static fromFileStream(stream: FileStream): FSKL { + const fskl = new FSKL(); + fskl.parseFromFileStream(stream); + + return fskl; + } + + /** + * Parses the FSKL from the input source provided at instantiation + */ + public parse(): void { + this.stream.consumeAll(); + + const magic = this.stream.readBytes(0x4); + + if (!FSKL_MAGIC.equals(magic)) { + throw new Error('Invalid FSKL magic'); + } + + this.flags = this.stream.readUInt32(); + this.boneArrayCount = this.stream.readUInt16(); + this.smoothIndexCount = this.stream.readUInt16(); + this.rigidIndexCount = this.stream.readUInt16(); + + this.stream.skip(0x2); // * Padding + + this.boneIndexGroupOffset = this.stream.tell() + this.stream.readUInt32(); + this.boneArrayOffset = this.stream.tell() + this.stream.readUInt32(); + this.smoothIndexArrayOffset = this.stream.tell() + this.stream.readUInt32(); + this.smoothMatrixArrayOffset = this.stream.tell() + this.stream.readUInt32(); + + this.stream.skip(0x4); // * User pointer + + this.scalingMode = (this.flags >> 8) & 0x3; + this.rotationMode = (this.flags >> 12) & 0x1; + + this.stream.seek(this.boneIndexGroupOffset); + this.boneIndexGroup = parseIndexGroup(this.stream); + + this.parseBoneArray(); + + // TODO - Parse smooth index/matrix + // TODO - Parse rigid index/matrix + } + + private parseBoneArray(): void { + this.stream.seek(this.boneArrayOffset); + + for (let i = 0; i < this.boneArrayCount; i++) { + const nameOffset = this.stream.tell() + this.stream.readInt32(); + const boneIndex = this.stream.readInt16(); + const parentIndex = this.stream.readInt16(); + const smoothMatrixIndex = this.stream.readInt16(); + const rigidMatrixIndex = this.stream.readInt16(); + const billboardIndex = this.stream.readInt16(); + const userDataEntryCount = this.stream.readInt16(); + const flags = this.stream.readUInt32(); + const scaleVectors = Array.from({ length: 3 }, () => this.stream.readFloat()); + const rotationVectors = Array.from({ length: 4 }, () => this.stream.readFloat()); + const translationVectors = Array.from({ length: 3 }, () => this.stream.readFloat()); + const userIndexGroupOffsetStart = this.stream.tell(); // * The offset can be 0, so that needs to be preserved. Start offset can be added later + const userIndexGroupOffset = this.stream.readInt32(); + const name = parseStringTableString(this.stream, nameOffset); + + this.stream.seek(userIndexGroupOffset); + + this.bones.push({ + nameOffset: nameOffset, + name: name, + boneIndex: boneIndex, + parentIndex: parentIndex, + smoothMatrixIndex: smoothMatrixIndex, + rigidMatrixIndex: rigidMatrixIndex, + billboardIndex: billboardIndex, + userDataEntryCount: userDataEntryCount, + flagsRaw: flags, + flags: { + visible: !!(flags & 0x1), + rotationMode: (flags >> 4) & 0x1, + billboardProjection: (flags >> 8) & 0x7, + transformFlags: (flags >> 16) & 0xF, + transformHierarchyFlags: (flags >> 28) & 0xF + }, + scaleVectors: scaleVectors, + rotationVectors: rotationVectors, + translationVectors: translationVectors, + userIndexGroupOffsetStart: userIndexGroupOffsetStart, + userIndexGroupOffset: userIndexGroupOffset, + userIndexGroup: null + }); + } + + for (const bone of this.bones) { + if (bone.userIndexGroupOffset !== 0) { + this.stream.seek(bone.userIndexGroupOffsetStart + bone.userIndexGroupOffset); + + bone.userIndexGroup = parseIndexGroup(this.stream); + } + } + } +} \ No newline at end of file diff --git a/src/bfres/fmdl/fvtx.ts b/src/bfres/fmdl/fvtx.ts new file mode 100644 index 0000000..45558ba --- /dev/null +++ b/src/bfres/fmdl/fvtx.ts @@ -0,0 +1,376 @@ +import FileStream from '@/file-stream'; +import { parseIndexGroup, parseStringTableString } from '@/bfres'; +import type { IndexGroup } from '@/bfres/bfres'; +import type Vec2 from '@/types/vec2'; +import type Vec3 from '@/types/vec3'; +import type Vec4 from '@/types/vec4'; + +const FVTX_MAGIC = Buffer.from('FVTX'); + +export enum GX2AttribFormat { + UNORM_8 = 0X0000, + UNORM_8_8 = 0X0004, + UNORM_16_16 = 0X0007, + UNORM_8_8_8_8 = 0X000A, + UINT_8 = 0X0100, + UINT_8_8 = 0X0104, + UINT_8_8_8_8 = 0X010A, + SNORM_8 = 0X0200, + SNORM_8_8 = 0X0204, + SNORM_16_16 = 0X0207, + SNORM_8_8_8_8 = 0X020A, + SNORM_10_10_10_2 = 0X020B, + SINT_8 = 0X0300, + SINT_8_8 = 0X0304, + SINT_8_8_8_8 = 0X030A, + FLOAT_32 = 0X0806, + FLOAT_16_16 = 0X0808, + FLOAT_32_32 = 0X080D, + FLOAT_16_16_16_16 = 0X080F, + FLOAT_32_32_32 = 0X0811, + FLOAT_32_32_32_32 = 0X0813 +} + +export type FVTX_ATTRIBUTE_NAME = + | '_p0' // * position0 - The position of the vertex + | '_n0' // * normal0 - The normal of the vertex used in lighting calculations + | '_t0' // * tangent0 - The tangent of the vertex used in advanced lighting calculations + | '_b0' // * binormal0 - The binormal of the vertex used in advanced lighting calculations + | '_w0' // * blendweight0 - Influence amount of the smooth skinning matrix at the index given in blendindex0 + | '_i0' // * blendindex0 - Index of influencing smooth skinning matrix + | '_u0' // * uv0 - Texture coordinates used for texture mapping + | '_u1' // * uv1 - Texture coordinates used for texture mapping + | '_u2' // * uv2 - Texture coordinates used for texture mapping + | '_u3' // * uv3 - Texture coordinates used for texture mapping + | '_c0' // * color0 - Vertex colors used for simple shadow mapping + | '_c1' // * color1 - Vertex colors used for simple shadow mapping + +export type FVTX_ATTRIBUTE_HEADER = { + nameOffset: number; + name: FVTX_ATTRIBUTE_NAME; + bufferIndex: number; + bufferOffset: number; + format: GX2AttribFormat; +}; + +export type FVTX_BUFFER = { + dataPointer: number; + size: number; + bufferHandle: number; + stride: number; + bufferingCount: number; + contextPointer: number; + dataOffset: number; + data: Buffer; +}; + +export type FVTX_VERTEX_ATTRIBUTE = { + position?: Vec3 | Vec4; // * If Vec4, the 4th element is always 1.0 + normal?: Vec3 | Vec4; // * If Vec4, the 4th element is always 1.0 + tangent?: Vec3 | Vec4; + binormal?: Vec3 | Vec4; + blendweight?: number; + blendindex?: number; + uv0?: Vec2; + uv1?: Vec2; + uv2?: Vec2; + uv3?: Vec2; + color0?: Vec4; + color1?: Vec4; +} + +export default class FVTX { + private stream: FileStream; + + public attributeCount: number; + public bufferCount: number; + public sectionIndex: number; + public verticesCount: number; + public vertexSkinCount: number; + public attributeArrayOffset: number; + public attributeIndexGroupOffset: number; + public attributeIndexGroup: IndexGroup; + public bufferArrayOffset: number; + public attributeHeaders: FVTX_ATTRIBUTE_HEADER[] = []; + public bufferHeaders: FVTX_BUFFER[] = []; + public vertices: FVTX_VERTEX_ATTRIBUTE[] = []; + + /** + * Parses the FVTX from an existing file stream + * + * @param stream - An existing file stream + */ + public parseFromFileStream(stream: FileStream): void { + this.stream = stream; + this.parse(); + } + + /** + * Creates a new instance of `FVTX` and + * parses the FVTX from an existing file stream + * + * @param stream - An existing file stream + */ + public static fromFileStream(stream: FileStream): FVTX { + const fvtx = new FVTX(); + fvtx.parseFromFileStream(stream); + + return fvtx; + } + + /** + * Parses the FVTX from the input source provided at instantiation + */ + public parse(): void { + this.stream.consumeAll(); + + const magic = this.stream.readBytes(0x4); + + if (!FVTX_MAGIC.equals(magic)) { + throw new Error('Invalid FVTX magic'); + } + + this.attributeCount = this.stream.readUInt8(); + this.bufferCount = this.stream.readUInt8(); + this.sectionIndex = this.stream.readUInt16(); + this.verticesCount = this.stream.readUInt32(); + this.vertexSkinCount = this.stream.readUInt8(); + + this.stream.skip(0x3); // * Padding + + this.attributeArrayOffset = this.stream.tell() + this.stream.readInt32(); + this.attributeIndexGroupOffset = this.stream.tell() + this.stream.readInt32(); + this.bufferArrayOffset = this.stream.tell() + this.stream.readInt32(); + + this.stream.skip(0x4); // * User pointer + + this.parseAttributeHeaders(); + this.parseBufferHeaders(); + + this.stream.seek(this.attributeIndexGroupOffset); + this.attributeIndexGroup = parseIndexGroup(this.stream); + + this.parseAttributes(); + } + + private parseAttributeHeaders(): void { + this.stream.seek(this.attributeArrayOffset); + + for (let i = 0; i < this.attributeCount; i++) { + const nameOffset = this.stream.tell() + this.stream.readInt32(); + const bufferIndex = this.stream.readInt8(); + + this.stream.skip(0x1); // * Padding + + const bufferOffset = this.stream.readInt16(); + const format = this.stream.readInt32(); + const name = parseStringTableString(this.stream, nameOffset); + + this.attributeHeaders.push({ + nameOffset: nameOffset, + name: name as FVTX_ATTRIBUTE_NAME, // TODO - Verify this + bufferIndex: bufferIndex, + bufferOffset: bufferOffset, + format: format + }); + } + } + + private parseBufferHeaders(): void { + for (let i = 0; i < this.bufferCount; i++) { + this.stream.seek(this.bufferArrayOffset + (0x18 * i)); + + const dataPointer = this.stream.readUInt32(); + const size = this.stream.readUInt32(); + const bufferHandle = this.stream.readUInt32(); + const stride = this.stream.readUInt16(); + const bufferingCount = this.stream.readUInt16(); + const contextPointer = this.stream.readUInt32(); + const dataOffset = this.stream.tell() + this.stream.readInt32(); + + this.stream.seek(dataOffset); + + const data = this.stream.read(size); + + this.bufferHeaders.push({ + dataPointer: dataPointer, + size: size, + bufferHandle: bufferHandle, + stride: stride, + bufferingCount: bufferingCount, + contextPointer: contextPointer, + dataOffset: dataOffset, + data: data + }); + } + } + + private parseAttributes(): void { + for (let vertexIndex = 0; vertexIndex < this.verticesCount; vertexIndex++) { + const attributes: FVTX_VERTEX_ATTRIBUTE = {}; + + for (const header of this.attributeHeaders) { + const value = this.parseAttributeValue(header, vertexIndex); + + // * A lot of these return types are guesses, I've only seen + // * _p0, _n0 and _i0 used in my samples + switch (header.name) { + case '_p0': + attributes.position = value as Vec3 | Vec4; + break; + case '_n0': + attributes.normal = value as Vec3 | Vec4; + break; + case '_t0': + attributes.tangent = value as Vec3 | Vec4; + break; + case '_b0': + attributes.binormal = value as Vec3 | Vec4; + break; + case '_w0': + attributes.blendweight = value as number; + break; + case '_i0': + attributes.blendindex = value as number; + break; + case '_u0': + attributes.uv0 = value as Vec2; + break; + case '_u1': + attributes.uv1 = value as Vec2; + break; + case '_u2': + attributes.uv2 = value as Vec2; + break; + case '_u3': + attributes.uv3 = value as Vec2; + break; + case '_c0': + attributes.color0 = value as Vec4; + break; + case '_c1': + attributes.color1 = value as Vec4; + break; + } + } + + this.vertices.push(attributes); + } + } + + private parseAttributeValue(header: FVTX_ATTRIBUTE_HEADER, vertexIndex: number): number | Vec2 | Vec3 | Vec4 { + const bufferHeader = this.bufferHeaders[header.bufferIndex]; + const bufferOffset = header.bufferOffset + (vertexIndex * bufferHeader.stride); + + this.stream.seek(bufferHeader.dataOffset + bufferOffset); + + switch (header.format) { + case GX2AttribFormat.UNORM_8: + return this.stream.readUInt8() / 255.0; + case GX2AttribFormat.UNORM_8_8: + return [ + this.stream.readUInt8() / 255.0, + this.stream.readUInt8() / 255.0 + ]; + case GX2AttribFormat.UNORM_16_16: + return [ + this.stream.readUInt16() / 65535.0, + this.stream.readUInt16() / 65535.0 + ]; + case GX2AttribFormat.UNORM_8_8_8_8: + return [ + this.stream.readUInt8() / 255.0, + this.stream.readUInt8() / 255.0, + this.stream.readUInt8() / 255.0, + this.stream.readUInt8() / 255.0 + ]; + case GX2AttribFormat.UINT_8: + return this.stream.readUInt8(); + case GX2AttribFormat.UINT_8_8: + return [ + this.stream.readUInt8(), + this.stream.readUInt8() + ]; + case GX2AttribFormat.UINT_8_8_8_8: + return [ + this.stream.readUInt8(), + this.stream.readUInt8(), + this.stream.readUInt8(), + this.stream.readUInt8() + ]; + case GX2AttribFormat.SNORM_8: + return Math.max(this.stream.readInt8() / 127.0, -1.0); + case GX2AttribFormat.SNORM_8_8: + return [ + Math.max(this.stream.readInt8() / 127.0, -1.0), + Math.max(this.stream.readInt8() / 127.0, -1.0) + ]; + case GX2AttribFormat.SNORM_16_16: + return [ + Math.max(this.stream.readInt16() / 32767.0, -1.0), + Math.max(this.stream.readInt16() / 32767.0, -1.0) + ]; + case GX2AttribFormat.SNORM_8_8_8_8: + return [ + Math.max(this.stream.readInt8() / 127.0, -1.0), + Math.max(this.stream.readInt8() / 127.0, -1.0), + Math.max(this.stream.readInt8() / 127.0, -1.0), + Math.max(this.stream.readInt8() / 127.0, -1.0) + ]; + case GX2AttribFormat.SNORM_10_10_10_2: + const packed = this.stream.readUInt32(); + return [ + Math.max(this.stream.extractSignedBits(packed, 0, 10) / 511.0, -1.0), + Math.max(this.stream.extractSignedBits(packed, 10, 10) / 511.0, -1.0), + Math.max(this.stream.extractSignedBits(packed, 20, 10) / 511.0, -1.0), + Math.max(this.stream.extractSignedBits(packed, 30, 2) / 1.0, -1.0) + ]; + case GX2AttribFormat.SINT_8: + return this.stream.readInt8(); + case GX2AttribFormat.SINT_8_8: + return [ + this.stream.readInt8(), + this.stream.readInt8() + ]; + case GX2AttribFormat.SINT_8_8_8_8: + return [ + this.stream.readInt8(), + this.stream.readInt8(), + this.stream.readInt8(), + this.stream.readInt8() + ]; + case GX2AttribFormat.FLOAT_32: + return this.stream.readFloat(); + case GX2AttribFormat.FLOAT_16_16: + return [ + this.stream.readHalf(), + this.stream.readHalf() + ]; + case GX2AttribFormat.FLOAT_32_32: + return [ + this.stream.readFloat(), + this.stream.readFloat() + ]; + case GX2AttribFormat.FLOAT_16_16_16_16: + return [ + this.stream.readHalf(), + this.stream.readHalf(), + this.stream.readHalf(), + this.stream.readHalf() + ]; + case GX2AttribFormat.FLOAT_32_32_32: + return [ + this.stream.readFloat(), + this.stream.readFloat(), + this.stream.readFloat() + ]; + case GX2AttribFormat.FLOAT_32_32_32_32: + return [ + this.stream.readFloat(), + this.stream.readFloat(), + this.stream.readFloat(), + this.stream.readFloat() + ]; + } + } +} \ No newline at end of file diff --git a/src/bfres/fmdl/index.ts b/src/bfres/fmdl/index.ts new file mode 100644 index 0000000..e0e3239 --- /dev/null +++ b/src/bfres/fmdl/index.ts @@ -0,0 +1,2 @@ +export { default as FMDL, default } from '@/bfres/fmdl/fmdl'; +export * from '@/bfres/fmdl/fmdl'; \ No newline at end of file diff --git a/src/bfres/index.ts b/src/bfres/index.ts new file mode 100644 index 0000000..8c12359 --- /dev/null +++ b/src/bfres/index.ts @@ -0,0 +1,60 @@ +import type FileStream from '@/file-stream'; +import type { IndexGroup, IndexGroupEntry } from '@/bfres/bfres'; + +// * I *REALLY* hate that this is here, it's SO HACKY. But multiple BFRES subfile types +// * need to parse index groups, and this was the easiest way I could think to do it without +// * having to drill the BFRES reference all the way through the chain and without duplicating +// * all this logic in multiple classes +export function parseIndexGroup(stream: FileStream): IndexGroup { + const groupLength = stream.readUInt32(); + const entryCount = stream.readInt32(); + const entries: IndexGroupEntry[] = []; + + for (let i = 0; i < entryCount + 1; i++) { + const searchValue = stream.readUInt32(); + const leftIndex = stream.readUInt16(); + const rightIndex = stream.readUInt16(); + const namePointer = stream.tell() + stream.readInt32(); + const dataPointer = stream.readInt32(); // * Unlike other offsets, this one already seems to be relative to the start of the file? Even though it's not documented as such? + + const name = parseStringTableString(stream, namePointer); + const actualDataPointer = dataPointer !== 0 ? dataPointer + stream.tell() - 4 : 0; + + entries.push({ + isRoot: i === 0, + searchValue, + leftIndex, + rightIndex, + namePointer, + dataPointer: actualDataPointer, + name + }); + } + + return { + length: groupLength, + entryCount, + entries + }; +} + +// * I *REALLY* hate that this is here, it's SO HACKY. But multiple BFRES subfile types +// * need to parse strings, and this was the easiest way I could think to do it without +// * having to drill the BFRES reference all the way through the chain and without duplicating +// * all this logic in multiple classes +export function parseStringTableString(stream: FileStream, offset: number): string { + const stringTable =stream.metadata['bfres-string-table'] as { offset: number; value: string; }[]; + + let name = ''; + if (offset !== 0) { + const stringEntry = stringTable.find(entry => entry.offset === offset); + if (stringEntry) { + name = stringEntry.value; + } + } + + return name; +} + +export { default as BFRES, default } from '@/bfres/bfres'; +export * from '@/bfres/bfres'; \ No newline at end of file diff --git a/src/stream.ts b/src/stream.ts index 0ddf425..7bd96a1 100644 --- a/src/stream.ts +++ b/src/stream.ts @@ -11,6 +11,13 @@ export default class Stream { */ public bom: 'le' | 'be' = 'le'; + /** + * Allows for the storing of context-specific metdata. Useful in + * cases such as passing context to sub-streams for files which + * contain sub-files + */ + public metadata: Record = {}; + constructor(bufferOrStream: Buffer | Stream) { // TODO - This is a hack to support FileStream in the MSBT parser if (bufferOrStream instanceof Buffer) { @@ -113,6 +120,15 @@ export default class Stream { return this.readBytes(1).readUInt8(); } + /** + * Reads a int8 from the current offset + * + * @returns the read number + */ + public readInt8(): number { + return this.readBytes(1).readInt8(); + } + /** * Reads a big-endian uint16 from the current offset * @@ -122,6 +138,15 @@ export default class Stream { return this.readBytes(2).readUInt16BE(); } + /** + * Reads a big-endian int16 from the current offset + * + * @returns the read number + */ + public readInt16BE(): number { + return this.readBytes(2).readInt16BE(); + } + /** * Reads a big-endian 3 byte unsigned integer from the current offset * @@ -131,6 +156,15 @@ export default class Stream { return this.readBytes(3).readUIntBE(0, 3); } + /** + * Reads a big-endian 3 byte signed integer from the current offset + * + * @returns the read number + */ + public readInt24BE(): number { + return this.readBytes(3).readIntBE(0, 3); + } + /** * Reads a big-endian uint32 from the current offset * @@ -140,15 +174,62 @@ export default class Stream { return this.readBytes(4).readUInt32BE(); } + /** + * Reads a big-endian int32 from the current offset + * + * @returns the read number + */ + public readInt32BE(): number { + return this.readBytes(4).readInt32BE(); + } + /** * Reads a big-endian uint64 from the current offset * * @returns the read number */ public readUInt64BE(): bigint { + return this.readBytes(8).readBigUInt64BE(); + } + + /** + * Reads a big-endian int64 from the current offset + * + * @returns the read number + */ + public readInt64BE(): bigint { return this.readBytes(8).readBigInt64BE(); } + /** + * Reads a big-endian float16 (half) from the current offset + * + * @returns the read number + */ + public readHalfBE(): number { + const bytes = this.readBytes(2); + const uint16 = bytes.readUInt16BE(); + return float16ToFloat32(uint16); + } + + /** + * Reads a big-endian float32 from the current offset + * + * @returns the read number + */ + public readFloatBE(): number { + return this.readBytes(4).readFloatBE(); + } + + /** + * Reads a big-endian float64 (double) from the current offset + * + * @returns the read number + */ + public readDoubleBE(): number { + return this.readBytes(8).readDoubleBE(); + } + /** * Reads a little-endian uint16 from the current offset * @@ -158,6 +239,15 @@ export default class Stream { return this.readBytes(2).readUInt16LE(); } + /** + * Reads a little-endian int16 from the current offset + * + * @returns the read number + */ + public readInt16LE(): number { + return this.readBytes(2).readInt16LE(); + } + /** * Reads a little-endian 3 byte unsigned integer from the current offset * @@ -167,6 +257,15 @@ export default class Stream { return this.readBytes(3).readUIntLE(0, 3); } + /** + * Reads a little-endian 3 byte signed integer from the current offset + * + * @returns the read number + */ + public readInt24LE(): number { + return this.readBytes(3).readIntLE(0, 3); + } + /** * Reads a little-endian uint32 from the current offset * @@ -176,15 +275,62 @@ export default class Stream { return this.readBytes(4).readUInt32LE(); } + /** + * Reads a little-endian int32 from the current offset + * + * @returns the read number + */ + public readInt32LE(): number { + return this.readBytes(4).readInt32LE(); + } + /** * Reads a little-endian uint64 from the current offset * * @returns the read number */ public readUInt64LE(): bigint { + return this.readBytes(8).readBigUInt64LE(); + } + + /** + * Reads a little-endian int64 from the current offset + * + * @returns the read number + */ + public readInt64LE(): bigint { return this.readBytes(8).readBigInt64LE(); } + /** + * Reads a little-endian float16 (half) from the current offset + * + * @returns the read number + */ + public readHalfLE(): number { + const bytes = this.readBytes(2); + const uint16 = bytes.readUInt16LE(); + return float16ToFloat32(uint16); + } + + /** + * Reads a little-endian float32 from the current offset + * + * @returns the read number + */ + public readFloatLE(): number { + return this.readBytes(4).readFloatLE(); + } + + /** + * Reads a little-endian float64 (double) from the current offset + * + * @returns the read number + */ + public readDoubleLE(): number { + return this.readBytes(8).readDoubleLE(); + } + /** * Reads a uint16 from the current offset * @@ -196,6 +342,17 @@ export default class Stream { return this.bom === 'le' ? this.readUInt16LE() : this.readUInt16BE(); } + /** + * Reads a int16 from the current offset + * + * Uses the `bom` field to determine endianness + * + * @returns the read number + */ + public readInt16(): number { + return this.bom === 'le' ? this.readInt16LE() : this.readInt16BE(); + } + /** * Reads a 3 byte unsigned integer from the current offset * @@ -207,6 +364,17 @@ export default class Stream { return this.bom === 'le' ? this.readUInt24LE() : this.readUInt24BE(); } + /** + * Reads a 3 byte signed integer from the current offset + * + * Uses the `bom` field to determine endianness + * + * @returns the read number + */ + public readInt24(): number { + return this.bom === 'le' ? this.readInt24LE() : this.readInt24BE(); + } + /** * Reads a uint32 from the current offset * @@ -218,6 +386,17 @@ export default class Stream { return this.bom === 'le' ? this.readUInt32LE() : this.readUInt32BE(); } + /** + * Reads a int32 from the current offset + * + * Uses the `bom` field to determine endianness + * + * @returns the read number + */ + public readInt32(): number { + return this.bom === 'le' ? this.readInt32LE() : this.readInt32BE(); + } + /** * Reads a uint64 from the current offset * @@ -228,4 +407,88 @@ export default class Stream { public readUInt64(): bigint { return this.bom === 'le' ? this.readUInt64LE() : this.readUInt64BE(); } + + /** + * Reads a int64 from the current offset + * + * Uses the `bom` field to determine endianness + * + * @returns the read number + */ + public readInt64(): bigint { + return this.bom === 'le' ? this.readInt64LE() : this.readInt64BE(); + } + + /** + * Reads a float16 (half) from the current offset + * + * Uses the `bom` field to determine endianness + * + * @returns the read number + */ + public readHalf(): number { + return this.bom === 'le' ? this.readHalfLE() : this.readHalfBE(); + } + + /** + * Reads a float32 from the current offset + * + * Uses the `bom` field to determine endianness + * + * @returns the read number + */ + public readFloat(): number { + return this.bom === 'le' ? this.readFloatLE() : this.readFloatBE(); + } + + /** + * Reads a float64 (double) from the current offset + * + * Uses the `bom` field to determine endianness + * + * @returns the read number + */ + public readDouble(): number { + return this.bom === 'le' ? this.readDoubleLE() : this.readDoubleBE(); + } + + /** + * Extracts a signed integer from a packed value + * + * @param value - The packed value to extract from + * @param bitOffset - Starting bit position (0-based, from LSB) + * @param bitLength - Number of bits to extract + * @returns the extracted signed integer + */ + public extractSignedBits(value: number, bitOffset: number, bitLength: number): number { + const mask = (1 << bitLength) - 1; + let extracted = (value >> bitOffset) & mask; + + const signBit = 1 << (bitLength - 1); + if (extracted & signBit) { + extracted |= ~mask; + } + + return extracted; + } +} + +function float16ToFloat32(uint16: number): number { + const sign = (uint16 & 0x8000) >> 15; + const exponent = (uint16 & 0x7C00) >> 10; + const fraction = uint16 & 0x03FF; + + if (exponent === 0) { + if (fraction === 0) { + return sign ? -0 : 0; + } + + return (sign ? -1 : 1) * Math.pow(2, -14) * (fraction / 1024); + } + + if (exponent === 0x1F) { + return fraction ? NaN : (sign ? -Infinity : Infinity); + } + + return (sign ? -1 : 1) * Math.pow(2, exponent - 15) * (1 + fraction / 1024); } \ No newline at end of file diff --git a/src/types/vec2.ts b/src/types/vec2.ts new file mode 100644 index 0000000..f607c55 --- /dev/null +++ b/src/types/vec2.ts @@ -0,0 +1,3 @@ +type Vec2 = [number, number]; + +export default Vec2; \ No newline at end of file diff --git a/src/types/vec3.ts b/src/types/vec3.ts new file mode 100644 index 0000000..291b031 --- /dev/null +++ b/src/types/vec3.ts @@ -0,0 +1,3 @@ +type Vec3 = [number, number, number]; + +export default Vec3; \ No newline at end of file diff --git a/src/types/vec4.ts b/src/types/vec4.ts new file mode 100644 index 0000000..4c8fe92 --- /dev/null +++ b/src/types/vec4.ts @@ -0,0 +1,3 @@ +type Vec4 = [number, number, number, number]; + +export default Vec4; \ No newline at end of file