UNPKG

vtf-js

Version:

A javascript IO library for the Valve Texture Format.

272 lines (271 loc) 11.8 kB
import { VEncodedImageData, getCodec } from './image.js'; import { DataBuffer } from './buffer.js'; import { VFormats, NO_DATA } from './enums.js'; import { VDataCollection } from './providers.js'; import { getFaceCount, getMipSize, compress, decompress } from './utils.js'; /** A map of header tags and their corresponding decoders. Using {@link registerResourceType} to register new tags is recommended! */ export const VResourceTypes = {}; /** Registers a resource to be used when the specified tag is encountered. */ export function registerResourceType(resource, tag) { VResourceTypes[tag] = resource; } /** A collection of common resource header tags as BE 24-bit integers. */ export var VHeaderTags; (function (VHeaderTags) { VHeaderTags[VHeaderTags["TAG_LEGACY_BODY"] = 3145728] = "TAG_LEGACY_BODY"; VHeaderTags[VHeaderTags["TAG_LEGACY_THUMB"] = 65536] = "TAG_LEGACY_THUMB"; VHeaderTags[VHeaderTags["TAG_SHEET"] = 1048576] = "TAG_SHEET"; VHeaderTags[VHeaderTags["TAG_AXC"] = 4282435] = "TAG_AXC"; VHeaderTags[VHeaderTags["TAG_HOTSPOT"] = 2818048] = "TAG_HOTSPOT"; })(VHeaderTags || (VHeaderTags = {})); /** Implements a resource header. This serves as a container to provide to {@link VResourceStatic} when decoding. */ export class VHeader { tag; flags; start; length; constructor(tag, flags, start, length) { this.tag = tag; this.flags = flags; this.start = start; this.length = length; } /** Returns true if the `0x2` flag is unset. */ hasData() { return !(this.flags & NO_DATA); } } /** Implements a generic resource entry. This can be subclassed to quickly implement {@link VResource}. */ export class VBaseResource { tag; flags; raw; constructor(tag, flags, raw) { this.tag = tag; this.flags = flags; this.raw = raw; } isLegacy() { return this.tag === VHeaderTags.TAG_LEGACY_BODY || this.tag === VHeaderTags.TAG_LEGACY_THUMB; } static decode(header, view, info) { return new VBaseResource(header.tag, header.flags, view); } encode(info) { return this.raw?.buffer; } } /** @internal The hi-res image data resource. This is managed internally! */ export class VBodyResource extends VBaseResource { images; constructor(flags, images) { super(VHeaderTags.TAG_LEGACY_BODY, flags); this.images = images; } static async decode(header, view, info, lazy = false) { const face_count = getFaceCount(info); const codec = getCodec(info.format); const mips = new Array(info.mipmaps); for (let x = info.mipmaps - 1; x >= 0; x--) { // Vtfs store mipmaps smallest-to-largest const frames = mips[x] = new Array(info.frames); for (let y = 0; y < info.frames; y++) { const faces = frames[y] = new Array(face_count); for (let z = 0; z < face_count; z++) { const [width, height] = getMipSize(x, info.width, info.height); const uncompressed_length = codec.length(width, height); // AXC compression works on every mip/frame/face, but joins all slices together let subview; if (info.compression_level !== 0) { const compressed_length = info.compressed_lengths[x][y][z]; const slice_data = view.read_u8(compressed_length); subview = new DataBuffer(await decompress(slice_data, info.compression_method, info.compression_level)); } else { subview = view.ref(view.pointer, uncompressed_length * info.slices); view.pointer += subview.length; } const slices = faces[z] = new Array(info.slices); for (let w = 0; w < info.slices; w++) { const data = subview.read_u8(uncompressed_length); const encoded = new VEncodedImageData(data, width, height, info.format); if (lazy) slices[w] = encoded; else slices[w] = encoded.decode(); } } } } const images = new VDataCollection(mips); return new VBodyResource(header.flags, images); } async encode(info) { const face_count = getFaceCount(info); const codec = getCodec(info.format); const packed_slices = []; let packed_length = 0; const cl_mipmaps = info.compressed_lengths = new Array(info.mipmaps); for (let x = info.mipmaps - 1; x >= 0; x--) { // mipmaps const cl_frames = cl_mipmaps[x] = new Array(info.frames); for (let y = 0; y < info.frames; y++) { // frames const cl_faces = cl_frames[y] = new Array(face_count); for (let z = 0; z < face_count; z++) { // faces const [width, height] = getMipSize(x, info.width, info.height); const uncompressed_length = codec.length(width, height); const subview = new DataBuffer(uncompressed_length * info.slices); for (let w = 0; w < info.slices; w++) { // slices const slice = this.images.getImage(x, y, z, w, true); // If slice is encoded, .encode() will no-op if the format matches. Otherwise, it is re-encoded. // If slice isn't encoded, it will be encoded into the desired format. const sliceData = slice.encode(info.format).data; subview.write_u8(sliceData); } // Compress let data = subview; if (info.compression_level !== 0) { data = await compress(data, info.compression_method, info.compression_level); } cl_faces[z] = data.length; packed_slices.push(data); packed_length += data.length; } } } const view = new DataBuffer(packed_length); for (let i = 0; i < packed_slices.length; i++) { view.write_u8(packed_slices[i]); } return view.buffer; } } /** @internal The low-res image data resource. This is managed internally! */ export class VThumbResource extends VBaseResource { image; constructor(flags, image) { super(VHeaderTags.TAG_LEGACY_THUMB, flags); this.image = image; } static decode(header, view, info) { const codec = getCodec(info.thumb_format); const data = view.read_u8(codec.length(info.thumb_width, info.thumb_height)); const image = new VEncodedImageData(data, info.thumb_width, info.thumb_height, info.thumb_format); return new VThumbResource(header.flags, image); } encode(info) { if (this.image.width === 0 || this.image.height === 0) return new ArrayBuffer(0); return this.image.encode(VFormats.DXT1).data.buffer; } } export class VSheetResource extends VBaseResource { sequences; static { registerResourceType(VSheetResource, VHeaderTags.TAG_SHEET); } constructor(flags, sequences) { super(VHeaderTags.TAG_SHEET, flags); this.sequences = sequences; } static decode(header, view, info) { const coord_count = info.version === 0 ? 1 : 4; const sequence_count = view.read_u32(); const sequences = new Array(sequence_count); for (let i = 0; i < sequence_count; i++) { view.pad(4); // const id = view.read_u32(); const clamp = !!view.read_u32(); const frame_count = view.read_u32(); const duration = view.read_f32(); const frames = new Array(frame_count); for (let j = 0; j < frame_count; j++) { const duration = view.read_f32(); const coords = new Array(coord_count); for (let k = 0; k < coord_count; k++) { coords[k] = view.read_f32(4); } frames[j] = { duration, coords }; } sequences[i] = { clamp, duration, frames }; } return new VSheetResource(header.flags, sequences); } encode(info) { const coord_count = info.version === 0 ? 1 : 4; let buffer_length = 4; for (let i = 0; i < this.sequences.length; i++) { buffer_length += 16 + this.sequences[i].frames.length * (4 + 4 * 4 * coord_count); } const view = new DataBuffer(buffer_length); for (let i = 0; i < this.sequences.length; i++) { const sequence = this.sequences[i]; view.write_u32(i); view.write_u32(sequence.clamp ? 0xff : 0x00); view.write_u32(sequence.frames.length); view.write_f32(sequence.duration); for (let j = 0; j < sequence.frames.length; j++) { const frame = sequence.frames[j]; view.write_f32(frame.duration); if (coord_count !== frame.coords.length) throw Error(`Expected ${coord_count} coordinate sets, but got ${frame.coords.length}!`); for (let k = 0; k < coord_count; k++) { if (frame.coords[k].length !== 4) throw Error('SheetFrame coords must be of length 4!'); view.write_f32(frame.coords[k]); } } } return view.buffer; } } /** The Hotspot data resource. See {@link https://wiki.stratasource.org/modding/overview/vtf-hotspot-resource this page} for more information. */ export class VHotspotResource extends VBaseResource { version; editorFlags; rects; static { registerResourceType(VHotspotResource, VHeaderTags.TAG_HOTSPOT); } constructor(flags, version, editorFlags, rects) { super(VHeaderTags.TAG_HOTSPOT, flags); this.version = version; this.editorFlags = editorFlags; this.rects = rects; } static decode(header, view, info) { if (!header.hasData()) return new VHotspotResource(header.flags, 0, 0, []); const version = view.read_u8(); const flags = view.read_u8(); const rectCount = view.read_u16(); if (version !== 0x1) throw Error(`Failed to parse VHotspotResource: Invalid version! (Expected 1, got ${version})`); const rects = Array(rectCount); for (let i = 0; i < rectCount; i++) { rects[i] = { flags: view.read_u8(), min_x: view.read_u16(), min_y: view.read_u16(), max_x: view.read_u16(), max_y: view.read_u16(), }; } return new VHotspotResource(header.flags, version, flags, rects); } encode(info) { const length = 4 + this.rects.length * 9; const view = new DataBuffer(length); if (this.version !== 0x1) throw Error(`Failed to write VHotspotResource: Invalid version! (Expected 1, got ${this.version})`); view.write_u8(this.version); view.write_u8(this.editorFlags); view.write_u16(this.rects.length); for (let i = 0; i < this.rects.length; i++) { const rect = this.rects[i]; view.write_u8(rect.flags); view.write_u16(rect.min_x); view.write_u16(rect.min_y); view.write_u16(rect.max_x); view.write_u16(rect.max_y); } return view.buffer; } }