Task 799: .VTF File Format

Task 799: .VTF File Format

1. Properties of the .VTF File Format

The .VTF (Valve Texture Format) is a binary file format used for textures in Valve's Source engine. Its intrinsic properties are derived from its header structure and associated data sections, which vary slightly by version (major version is always 7, minor versions range from 0 to 6). The format supports 2D textures, cubemaps, volumetric textures, mipmaps, animations, and various compression schemes. Below is a comprehensive list of properties based on the file structure, including header fields, enums, and data sections. These are intrinsic to the file's binary layout and are consistent across implementations.

Header Properties (Common to All Versions Unless Noted)

  • Signature: A 4-byte string identifier, always "VTF\0" (offset 0).
  • Major Version: A 32-bit unsigned integer, always 7 (offset 4).
  • Minor Version: A 32-bit unsigned integer (0–6), indicating format revisions (offset 8). Examples: 0 (initial), 2 (adds depth), 3 (adds resources), 5 (revises flags).
  • Header Size: A 32-bit unsigned integer specifying the size of the header in bytes (offset 12). Typically 80 bytes for v7.0–v7.2, 96 bytes for v7.3+.
  • Width: A 16-bit unsigned integer for the base texture width in pixels (offset 16). Must be a power of 2 or multiple of 4 for certain formats.
  • Height: A 16-bit unsigned integer for the base texture height in pixels (offset 18). Same constraints as width.
  • Flags: A 32-bit bitfield of texture flags (offset 20). Bit values include:
  • 0x00000001: Point sampling (no filtering).
  • 0x00000002: Trilinear filtering.
  • 0x00000004: Clamp S (X-axis wrapping).
  • 0x00000008: Clamp T (Y-axis wrapping).
  • 0x00000010: Anisotropic filtering.
  • 0x00000040: sRGB gamma correction (position varies by version; 0x00000040 in v7.4, 0x00080000 in v7.5+).
  • 0x00000080: Normal map.
  • 0x00000100: No mipmaps.
  • 0x00000200: No level-of-detail.
  • 0x00000400: All mips (v7.3+) or minimum mip (v7.2–v7.3).
  • 0x00000800: Procedural texture.
  • 0x00001000: One-bit alpha.
  • 0x00002000: Eight-bit alpha.
  • 0x00004000: Environment map (cubemap).
  • 0x00020000: No debug override.
  • 0x00040000: Single copy (unused).
  • 0x00080000: sRGB (v7.5+).
  • 0x00800000: No depth buffer (v7.2+).
  • 0x02000000: Clamp U (Z-axis wrapping, v7.2+).
  • 0x04000000: Vertex texture (v7.3+).
  • 0x08000000: Self-shadowing bump map (v7.3+).
  • 0x10000000: Most mips (v7.5+).
  • 0x20000000: Border clamping (v7.3+).
  • Additional game-specific flags in certain branches (e.g., 0x00200000 for combined textures in CS:GO).
  • Frame Count: A 16-bit unsigned integer for the number of animation frames (offset 24). Default: 1.
  • Start Frame: A 16-bit unsigned integer for the first frame in the animation sequence (offset 26). Default: 0.
  • Padding 0: 4 bytes reserved (offset 28).
  • Reflectivity: Three 32-bit floats (RGB vector) for surface reflectivity (offset 32).
  • Padding 1: 4 bytes reserved (offset 44).
  • Bump Map Scale: A 32-bit float for bump map intensity (offset 48).
  • Image Format: A 32-bit unsigned integer ID for the pixel format (offset 52). Supported values include:
  • 0: RGBA8888
  • 1: ABGR8888
  • 2: RGB888
  • 3: BGR888
  • 4: RGB565
  • 5: I8 (greyscale)
  • 6: IA88 (greyscale + alpha)
  • 7: P8 (palette, unused)
  • 8: A8 (alpha only)
  • 9: RGB888_BLUESCREEN (blue = transparent)
  • 10: BGR888_BLUESCREEN (red = transparent)
  • 11: ARGB8888
  • 12: BGRA8888
  • 13: DXT1 (compressed)
  • 14: DXT3 (compressed with alpha)
  • 15: DXT5 (compressed with alpha)
  • 16: BGRX8888
  • 17: BGR565
  • 18: BGRX5551
  • 19: BGRA4444
  • 20: DXT1_ONE_BIT_ALPHA
  • 21: BGRA5551
  • 22: UV88
  • 23: UVWQ8888
  • 24: RGBA16161616F (HDR)
  • 25: RGBA16161616
  • 26: UVLX8888
  • 27: R32F
  • 28: RGB323232F
  • 29: RGBA32323232F
  • Additional formats in later branches (e.g., 30: RG1616F, 34: ATI2N, 70: BC7 in Strata Source).
  • Mip Count: An 8-bit unsigned integer for the number of mipmap levels (offset 56). Calculated as log2(max(width, height)) + 1.
  • Thumbnail Format: A 32-bit unsigned integer for low-resolution thumbnail format (offset 57). Always DXT1 (13), regardless of value.
  • Thumbnail Width: An 8-bit unsigned integer for thumbnail width (offset 61). Typically 16 or proportional (max dimension 16).
  • Thumbnail Height: An 8-bit unsigned integer for thumbnail height (offset 62).

Additional Properties (v7.2+)

  • Depth: A 16-bit unsigned integer for texture depth (volumetric textures) (offset 64). Default: 1 (2D).

Additional Properties (v7.3+)

  • Padding 2: 3 bytes reserved (offset 66).
  • Resource Count: A 32-bit unsigned integer for the number of resource entries (offset 69). Max: 32.
  • Padding 3: 8 bytes reserved (offset 73).
  • Resources: An array of resource entries immediately after the header. Each is 8 bytes:
  • Type: 3-byte identifier (e.g., "\x30\0\0" for image data).
  • Flags: 1-byte bitfield (bit 2: inline data if set).
  • Data: 32-bit value (offset to data or inline value if flagged).
  • Supported types: Thumbnail (\x01\0\0), Particle Sheet (\x10\0\0), Image Data (\x30\0\0), CRC ("CRC"), LOD Control ("LOD"), Extra Flags ("TSO"), KeyValues ("KVD"), Compression Info ("AXC" in Strata).

Data Sections

  • Thumbnail Data: Low-res DXT1-compressed image data (after header in pre-v7.3; as resource in v7.3+).
  • Image Data: High-res image data, interleaved by mip, frame, face, slice. Order: smallest mip first. Compression optional (e.g., Deflate/Zstd via AXC resource).
  • Face Count: Implied (1 for 2D, 6 for cubemap, 7 for cubemap + spheremap in v7.1–v7.4).
  • Compression Info (AXC Resource, Strata): Versioned struct with compression method (Deflate/Zstd) and per-block sizes.

The file ends with resource data blocks, aligned to the offsets specified.

3. HTML/JavaScript for Drag-and-Drop .VTF Property Dumper

This is an embeddable HTML snippet with JavaScript that can be placed in a blog post (e.g., Ghost platform). It creates a drag-and-drop area. When a .VTF file is dropped, it parses the binary data and displays all properties from the list above in a readable format.

VTF Property Dumper
Drag and drop a .VTF file here

4. Python Class for .VTF Handling

This class can open a .VTF file, decode its properties, print them to console, and write a new .VTF file with modified properties (basic implementation; assumes v7.5 for simplicity, without full image data writing).

import struct
import sys

class VTFHandler:
    def __init__(self, filepath=None):
        self.properties = {}
        if filepath:
            self.read(filepath)

    def read(self, filepath):
        with open(filepath, 'rb') as f:
            data = f.read()
            offset = 0

            # Signature
            self.properties['signature'] = struct.unpack_from('<4s', data, offset)[0].decode('utf-8')
            offset += 4

            # Versions
            self.properties['major_version'] = struct.unpack_from('<I', data, offset)[0]
            offset += 4
            self.properties['minor_version'] = struct.unpack_from('<I', data, offset)[0]
            offset += 4

            # Header size
            self.properties['header_size'] = struct.unpack_from('<I', data, offset)[0]
            offset += 4

            # Width, height
            self.properties['width'] = struct.unpack_from('<H', data, offset)[0]
            offset += 2
            self.properties['height'] = struct.unpack_from('<H', data, offset)[0]
            offset += 2

            # Flags
            self.properties['flags'] = struct.unpack_from('<I', data, offset)[0]
            offset += 4

            # Frames
            self.properties['frame_count'] = struct.unpack_from('<H', data, offset)[0]
            offset += 2
            self.properties['start_frame'] = struct.unpack_from('<H', data, offset)[0]
            offset += 2

            # Padding 0
            offset += 4

            # Reflectivity
            self.properties['reflectivity'] = list(struct.unpack_from('<3f', data, offset))
            offset += 12

            # Padding 1
            offset += 4

            # Bump scale
            self.properties['bump_scale'] = struct.unpack_from('<f', data, offset)[0]
            offset += 4

            # Image format
            self.properties['image_format'] = struct.unpack_from('<I', data, offset)[0]
            offset += 4

            # Mip count
            self.properties['mip_count'] = struct.unpack_from('<B', data, offset)[0]
            offset += 1

            # Thumbnail
            self.properties['thumbnail_format'] = struct.unpack_from('<I', data, offset)[0]
            offset += 4
            self.properties['thumbnail_width'] = struct.unpack_from('<B', data, offset)[0]
            offset += 1
            self.properties['thumbnail_height'] = struct.unpack_from('<B', data, offset)[0]
            offset += 1

            # Depth (v7.2+)
            if self.properties['minor_version'] >= 2:
                self.properties['depth'] = struct.unpack_from('<H', data, offset)[0]
                offset += 2

            # Resources (v7.3+)
            if self.properties['minor_version'] >= 3:
                offset += 3  # Padding 2
                self.properties['resource_count'] = struct.unpack_from('<I', data, offset)[0]
                offset += 4
                offset += 8  # Padding 3
                self.properties['resources'] = []
                for _ in range(self.properties['resource_count']):
                    res_type = data[offset:offset+3].decode('utf-8', errors='ignore')
                    res_flags = struct.unpack_from('<B', data, offset+3)[0]
                    res_data = struct.unpack_from('<I', data, offset+4)[0]
                    self.properties['resources'].append({'type': res_type, 'flags': res_flags, 'data': res_data})
                    offset += 8

            # Note: Image data parsing omitted for brevity; can be added based on offsets.

    def print_properties(self):
        for key, value in self.properties.items():
            print(f"{key}: {value}")

    def write(self, filepath):
        # Basic write: Reconstruct header from properties (assumes no image data changes)
        with open(filepath, 'wb') as f:
            offset = 0
            data = bytearray(1024)  # Buffer

            # Signature
            struct.pack_into('<4s', data, offset, self.properties['signature'].encode('utf-8'))
            offset += 4

            # Versions
            struct.pack_into('<I', data, offset, self.properties['major_version'])
            offset += 4
            struct.pack_into('<I', data, offset, self.properties['minor_version'])
            offset += 4

            # Header size (placeholder; update later)
            header_size_pos = offset
            offset += 4

            # Width, height
            struct.pack_into('<H', data, offset, self.properties['width'])
            offset += 2
            struct.pack_into('<H', data, offset, self.properties['height'])
            offset += 2

            # Flags
            struct.pack_into('<I', data, offset, self.properties['flags'])
            offset += 4

            # Frames
            struct.pack_into('<H', data, offset, self.properties['frame_count'])
            offset += 2
            struct.pack_into('<H', data, offset, self.properties['start_frame'])
            offset += 2

            # Padding 0
            offset += 4

            # Reflectivity
            struct.pack_into('<3f', data, offset, *self.properties['reflectivity'])
            offset += 12

            # Padding 1
            offset += 4

            # Bump scale
            struct.pack_into('<f', data, offset, self.properties['bump_scale'])
            offset += 4

            # Image format
            struct.pack_into('<I', data, offset, self.properties['image_format'])
            offset += 4

            # Mip count
            struct.pack_into('<B', data, offset, self.properties['mip_count'])
            offset += 1

            # Thumbnail
            struct.pack_into('<I', data, offset, self.properties['thumbnail_format'])
            offset += 4
            struct.pack_into('<B', data, offset, self.properties['thumbnail_width'])
            offset += 1
            struct.pack_into('<B', data, offset, self.properties['thumbnail_height'])
            offset += 1

            # Depth (v7.2+)
            if self.properties['minor_version'] >= 2:
                struct.pack_into('<H', data, offset, self.properties['depth'])
                offset += 2

            # Resources (v7.3+)
            if self.properties['minor_version'] >= 3:
                offset += 3  # Padding 2
                struct.pack_into('<I', data, offset, self.properties['resource_count'])
                offset += 4
                offset += 8  # Padding 3
                for res in self.properties['resources']:
                    struct.pack_into('<3s', data, offset, res['type'].encode('utf-8'))
                    struct.pack_into('<B', data, offset + 3, res['flags'])
                    struct.pack_into('<I', data, offset + 4, res['data'])
                    offset += 8

            # Update header size
            struct.pack_into('<I', data, header_size_pos, offset)

            # Write header (image data omitted for brevity)
            f.write(data[:offset])

# Example usage
if __name__ == '__main__':
    if len(sys.argv) > 1:
        vtf = VTFHandler(sys.argv[1])
        vtf.print_properties()
        vtf.write('output.vtf')

5. Java Class for .VTF Handling

This class handles opening, decoding, printing, and writing .VTF files (basic; v7.5 assumed, little-endian).

import java.io.*;
import java.nio.*;
import java.util.*;

public class VTFHandler {
    private Map<String, Object> properties = new HashMap<>();

    public VTFHandler(String filepath) throws IOException {
        if (filepath != null) {
            read(filepath);
        }
    }

    public void read(String filepath) throws IOException {
        try (RandomAccessFile raf = new RandomAccessFile(filepath, "r")) {
            ByteBuffer buffer = ByteBuffer.allocate((int) raf.length()).order(ByteOrder.LITTLE_ENDIAN);
            raf.getChannel().read(buffer);
            buffer.flip();

            // Signature
            byte[] sigBytes = new byte[4];
            buffer.get(sigBytes);
            properties.put("signature", new String(sigBytes));

            // Versions
            properties.put("major_version", buffer.getInt());
            properties.put("minor_version", buffer.getInt());

            // Header size
            properties.put("header_size", buffer.getInt());

            // Width, height
            properties.put("width", (int) buffer.getShort());
            properties.put("height", (int) buffer.getShort());

            // Flags
            properties.put("flags", buffer.getInt());

            // Frames
            properties.put("frame_count", (int) buffer.getShort());
            properties.put("start_frame", (int) buffer.getShort());

            // Padding 0
            buffer.getInt();

            // Reflectivity
            float[] reflectivity = new float[3];
            for (int i = 0; i < 3; i++) reflectivity[i] = buffer.getFloat();
            properties.put("reflectivity", reflectivity);

            // Padding 1
            buffer.getInt();

            // Bump scale
            properties.put("bump_scale", buffer.getFloat());

            // Image format
            properties.put("image_format", buffer.getInt());

            // Mip count
            properties.put("mip_count", Byte.toUnsignedInt(buffer.get()));

            // Thumbnail
            properties.put("thumbnail_format", buffer.getInt());
            properties.put("thumbnail_width", Byte.toUnsignedInt(buffer.get()));
            properties.put("thumbnail_height", Byte.toUnsignedInt(buffer.get()));

            // Depth (v7.2+)
            int minor = (int) properties.get("minor_version");
            if (minor >= 2) {
                properties.put("depth", (int) buffer.getShort());
            }

            // Resources (v7.3+)
            if (minor >= 3) {
                buffer.position(buffer.position() + 3); // Padding 2
                int resourceCount = buffer.getInt();
                properties.put("resource_count", resourceCount);
                buffer.position(buffer.position() + 8); // Padding 3
                List<Map<String, Object>> resources = new ArrayList<>();
                for (int i = 0; i < resourceCount; i++) {
                    byte[] typeBytes = new byte[3];
                    buffer.get(typeBytes);
                    String type = new String(typeBytes);
                    int flags = Byte.toUnsignedInt(buffer.get());
                    int data = buffer.getInt();
                    Map<String, Object> res = new HashMap<>();
                    res.put("type", type);
                    res.put("flags", flags);
                    res.put("data", data);
                    resources.add(res);
                }
                properties.put("resources", resources);
            }
        }
    }

    public void printProperties() {
        for (Map.Entry<String, Object> entry : properties.entrySet()) {
            System.out.println(entry.getKey() + ": " + entry.getValue());
        }
    }

    public void write(String filepath) throws IOException {
        try (RandomAccessFile raf = new RandomAccessFile(filepath, "rw")) {
            ByteBuffer buffer = ByteBuffer.allocate(1024).order(ByteOrder.LITTLE_ENDIAN);

            // Signature
            buffer.put(((String) properties.get("signature")).getBytes());

            // Versions
            buffer.putInt((int) properties.get("major_version"));
            buffer.putInt((int) properties.get("minor_version"));

            // Header size (placeholder)
            int headerSizePos = buffer.position();
            buffer.putInt(0);

            // Width, height
            buffer.putShort((short) (int) properties.get("width"));
            buffer.putShort((short) (int) properties.get("height"));

            // Flags
            buffer.putInt((int) properties.get("flags"));

            // Frames
            buffer.putShort((short) (int) properties.get("frame_count"));
            buffer.putShort((short) (int) properties.get("start_frame"));

            // Padding 0
            buffer.putInt(0);

            // Reflectivity
            float[] reflectivity = (float[]) properties.get("reflectivity");
            for (float val : reflectivity) buffer.putFloat(val);

            // Padding 1
            buffer.putInt(0);

            // Bump scale
            buffer.putFloat((float) properties.get("bump_scale"));

            // Image format
            buffer.putInt((int) properties.get("image_format"));

            // Mip count
            buffer.put((byte) (int) properties.get("mip_count"));

            // Thumbnail
            buffer.putInt((int) properties.get("thumbnail_format"));
            buffer.put((byte) (int) properties.get("thumbnail_width"));
            buffer.put((byte) (int) properties.get("thumbnail_height"));

            // Depth (v7.2+)
            int minor = (int) properties.get("minor_version");
            if (minor >= 2) {
                buffer.putShort((short) (int) properties.get("depth"));
            }

            // Resources (v7.3+)
            if (minor >= 3) {
                buffer.position(buffer.position() + 3); // Padding 2
                int resourceCount = (int) properties.get("resource_count");
                buffer.putInt(resourceCount);
                buffer.position(buffer.position() + 8); // Padding 3
                @SuppressWarnings("unchecked")
                List<Map<String, Object>> resources = (List<Map<String, Object>>) properties.get("resources");
                for (Map<String, Object> res : resources) {
                    buffer.put(((String) res.get("type")).getBytes());
                    buffer.put((byte) (int) res.get("flags"));
                    buffer.putInt((int) res.get("data"));
                }
            }

            // Update header size
            int headerSize = buffer.position();
            buffer.putInt(headerSizePos, headerSize);

            // Write
            buffer.flip();
            raf.getChannel().write(buffer);
        }
    }

    public static void main(String[] args) throws IOException {
        if (args.length > 0) {
            VTFHandler vtf = new VTFHandler(args[0]);
            vtf.printProperties();
            vtf.write("output.vtf");
        }
    }
}

6. JavaScript Class for .VTF Handling

This class works in Node.js (requires 'fs' module). It opens, decodes, prints (to console), and writes .VTF files.

const fs = require('fs');

class VTFHandler {
    constructor(filepath) {
        this.properties = {};
        if (filepath) {
            this.read(filepath);
        }
    }

    read(filepath) {
        const data = fs.readFileSync(filepath);
        const dv = new DataView(data.buffer);
        let offset = 0;

        // Signature
        this.properties.signature = String.fromCharCode(dv.getUint8(offset++), dv.getUint8(offset++), dv.getUint8(offset++), dv.getUint8(offset++));

        // Versions
        this.properties.major_version = dv.getUint32(offset, true); offset += 4;
        this.properties.minor_version = dv.getUint32(offset, true); offset += 4;

        // Header size
        this.properties.header_size = dv.getUint32(offset, true); offset += 4;

        // Width, height
        this.properties.width = dv.getUint16(offset, true); offset += 2;
        this.properties.height = dv.getUint16(offset, true); offset += 2;

        // Flags
        this.properties.flags = dv.getUint32(offset, true); offset += 4;

        // Frames
        this.properties.frame_count = dv.getUint16(offset, true); offset += 2;
        this.properties.start_frame = dv.getUint16(offset, true); offset += 2;

        // Padding 0
        offset += 4;

        // Reflectivity
        this.properties.reflectivity = [dv.getFloat32(offset, true), dv.getFloat32(offset + 4, true), dv.getFloat32(offset + 8, true)];
        offset += 12;

        // Padding 1
        offset += 4;

        // Bump scale
        this.properties.bump_scale = dv.getFloat32(offset, true); offset += 4;

        // Image format
        this.properties.image_format = dv.getUint32(offset, true); offset += 4;

        // Mip count
        this.properties.mip_count = dv.getUint8(offset++);

        // Thumbnail
        this.properties.thumbnail_format = dv.getUint32(offset, true); offset += 4;
        this.properties.thumbnail_width = dv.getUint8(offset++);
        this.properties.thumbnail_height = dv.getUint8(offset++);

        // Depth (v7.2+)
        if (this.properties.minor_version >= 2) {
            this.properties.depth = dv.getUint16(offset, true); offset += 2;
        }

        // Resources (v7.3+)
        if (this.properties.minor_version >= 3) {
            offset += 3; // Padding 2
            this.properties.resource_count = dv.getUint32(offset, true); offset += 4;
            offset += 8; // Padding 3
            this.properties.resources = [];
            for (let i = 0; i < this.properties.resource_count; i++) {
                const type = String.fromCharCode(dv.getUint8(offset), dv.getUint8(offset + 1), dv.getUint8(offset + 2));
                const flags = dv.getUint8(offset + 3);
                const resData = dv.getUint32(offset + 4, true);
                this.properties.resources.push({ type, flags, data: resData });
                offset += 8;
            }
        }
    }

    printProperties() {
        console.log(JSON.stringify(this.properties, null, 2));
    }

    write(filepath) {
        const buffer = new ArrayBuffer(1024);
        const dv = new DataView(buffer);
        let offset = 0;

        // Signature
        for (let char of this.properties.signature) {
            dv.setUint8(offset++, char.charCodeAt(0));
        }

        // Versions
        dv.setUint32(offset, this.properties.major_version, true); offset += 4;
        dv.setUint32(offset, this.properties.minor_version, true); offset += 4;

        // Header size (placeholder)
        const headerSizePos = offset;
        offset += 4;

        // Width, height
        dv.setUint16(offset, this.properties.width, true); offset += 2;
        dv.setUint16(offset, this.properties.height, true); offset += 2;

        // Flags
        dv.setUint32(offset, this.properties.flags, true); offset += 4;

        // Frames
        dv.setUint16(offset, this.properties.frame_count, true); offset += 2;
        dv.setUint16(offset, this.properties.start_frame, true); offset += 2;

        // Padding 0
        offset += 4;

        // Reflectivity
        for (let val of this.properties.reflectivity) {
            dv.setFloat32(offset, val, true); offset += 4;
        }

        // Padding 1
        offset += 4;

        // Bump scale
        dv.setFloat32(offset, this.properties.bump_scale, true); offset += 4;

        // Image format
        dv.setUint32(offset, this.properties.image_format, true); offset += 4;

        // Mip count
        dv.setUint8(offset++, this.properties.mip_count);

        // Thumbnail
        dv.setUint32(offset, this.properties.thumbnail_format, true); offset += 4;
        dv.setUint8(offset++, this.properties.thumbnail_width);
        dv.setUint8(offset++, this.properties.thumbnail_height);

        // Depth (v7.2+)
        if (this.properties.minor_version >= 2) {
            dv.setUint16(offset, this.properties.depth, true); offset += 2;
        }

        // Resources (v7.3+)
        if (this.properties.minor_version >= 3) {
            offset += 3; // Padding 2
            dv.setUint32(offset, this.properties.resource_count, true); offset += 4;
            offset += 8; // Padding 3
            for (let res of this.properties.resources) {
                for (let char of res.type) {
                    dv.setUint8(offset++, char.charCodeAt(0));
                }
                dv.setUint8(offset++, res.flags);
                dv.setUint32(offset, res.data, true); offset += 4;
            }
        }

        // Update header size
        dv.setUint32(headerSizePos, offset, true);

        // Write
        fs.writeFileSync(filepath, new Uint8Array(buffer, 0, offset));
    }
}

// Example usage: node script.js input.vtf
if (process.argv.length > 2) {
    const vtf = new VTFHandler(process.argv[2]);
    vtf.printProperties();
    vtf.write('output.vtf');
}

7. C "Class" for .VTF Handling

In C, we use a struct with functions. This handles opening, decoding, printing, and writing (basic; assumes little-endian host).

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>

typedef struct {
    char signature[5];
    uint32_t major_version;
    uint32_t minor_version;
    uint32_t header_size;
    uint16_t width;
    uint16_t height;
    uint32_t flags;
    uint16_t frame_count;
    uint16_t start_frame;
    float reflectivity[3];
    float bump_scale;
    uint32_t image_format;
    uint8_t mip_count;
    uint32_t thumbnail_format;
    uint8_t thumbnail_width;
    uint8_t thumbnail_height;
    uint16_t depth; // v7.2+
    uint32_t resource_count; // v7.3+
    struct Resource {
        char type[4];
        uint8_t flags;
        uint32_t data;
    } *resources; // Array of resources
} VTFProperties;

void read_vtf(const char* filepath, VTFProperties* props) {
    FILE* f = fopen(filepath, "rb");
    if (!f) return;

    fseek(f, 0, SEEK_END);
    long size = ftell(f);
    fseek(f, 0, SEEK_SET);
    uint8_t* data = malloc(size);
    fread(data, 1, size, f);
    fclose(f);

    uint32_t offset = 0;

    // Signature
    memcpy(props->signature, data + offset, 4);
    props->signature[4] = '\0';
    offset += 4;

    // Versions
    memcpy(&props->major_version, data + offset, 4); offset += 4;
    memcpy(&props->minor_version, data + offset, 4); offset += 4;

    // Header size
    memcpy(&props->header_size, data + offset, 4); offset += 4;

    // Width, height
    memcpy(&props->width, data + offset, 2); offset += 2;
    memcpy(&props->height, data + offset, 2); offset += 2;

    // Flags
    memcpy(&props->flags, data + offset, 4); offset += 4;

    // Frames
    memcpy(&props->frame_count, data + offset, 2); offset += 2;
    memcpy(&props->start_frame, data + offset, 2); offset += 2;

    // Padding 0
    offset += 4;

    // Reflectivity
    memcpy(props->reflectivity, data + offset, 12); offset += 12;

    // Padding 1
    offset += 4;

    // Bump scale
    memcpy(&props->bump_scale, data + offset, 4); offset += 4;

    // Image format
    memcpy(&props->image_format, data + offset, 4); offset += 4;

    // Mip count
    props->mip_count = data[offset++];

    // Thumbnail
    memcpy(&props->thumbnail_format, data + offset, 4); offset += 4;
    props->thumbnail_width = data[offset++];
    props->thumbnail_height = data[offset++];

    // Depth (v7.2+)
    if (props->minor_version >= 2) {
        memcpy(&props->depth, data + offset, 2); offset += 2;
    } else {
        props->depth = 1;
    }

    // Resources (v7.3+)
    if (props->minor_version >= 3) {
        offset += 3; // Padding 2
        memcpy(&props->resource_count, data + offset, 4); offset += 4;
        offset += 8; // Padding 3
        props->resources = malloc(sizeof(struct Resource) * props->resource_count);
        for (uint32_t i = 0; i < props->resource_count; i++) {
            memcpy(props->resources[i].type, data + offset, 3);
            props->resources[i].type[3] = '\0';
            props->resources[i].flags = data[offset + 3];
            memcpy(&props->resources[i].data, data + offset + 4, 4);
            offset += 8;
        }
    } else {
        props->resource_count = 0;
        props->resources = NULL;
    }

    free(data);
}

void print_properties(const VTFProperties* props) {
    printf("signature: %s\n", props->signature);
    printf("major_version: %u\n", props->major_version);
    printf("minor_version: %u\n", props->minor_version);
    printf("header_size: %u\n", props->header_size);
    printf("width: %u\n", props->width);
    printf("height: %u\n", props->height);
    printf("flags: %u\n", props->flags);
    printf("frame_count: %u\n", props->frame_count);
    printf("start_frame: %u\n", props->start_frame);
    printf("reflectivity: [%f, %f, %f]\n", props->reflectivity[0], props->reflectivity[1], props->reflectivity[2]);
    printf("bump_scale: %f\n", props->bump_scale);
    printf("image_format: %u\n", props->image_format);
    printf("mip_count: %u\n", props->mip_count);
    printf("thumbnail_format: %u\n", props->thumbnail_format);
    printf("thumbnail_width: %u\n", props->thumbnail_width);
    printf("thumbnail_height: %u\n", props->thumbnail_height);
    printf("depth: %u\n", props->depth);
    printf("resource_count: %u\n", props->resource_count);
    for (uint32_t i = 0; i < props->resource_count; i++) {
        printf("resource %u: type=%s, flags=%u, data=%u\n", i, props->resources[i].type, props->resources[i].flags, props->resources[i].data);
    }
}

void write_vtf(const char* filepath, const VTFProperties* props) {
    FILE* f = fopen(filepath, "wb");
    if (!f) return;

    uint32_t offset = 0;
    uint8_t buffer[1024] = {0};

    // Signature
    memcpy(buffer + offset, props->signature, 4); offset += 4;

    // Versions
    memcpy(buffer + offset, &props->major_version, 4); offset += 4;
    memcpy(buffer + offset, &props->minor_version, 4); offset += 4;

    // Header size (placeholder)
    uint32_t header_size_pos = offset;
    offset += 4;

    // Width, height
    memcpy(buffer + offset, &props->width, 2); offset += 2;
    memcpy(buffer + offset, &props->height, 2); offset += 2;

    // Flags
    memcpy(buffer + offset, &props->flags, 4); offset += 4;

    // Frames
    memcpy(buffer + offset, &props->frame_count, 2); offset += 2;
    memcpy(buffer + offset, &props->start_frame, 2); offset += 2;

    // Padding 0
    offset += 4;

    // Reflectivity
    memcpy(buffer + offset, props->reflectivity, 12); offset += 12;

    // Padding 1
    offset += 4;

    // Bump scale
    memcpy(buffer + offset, &props->bump_scale, 4); offset += 4;

    // Image format
    memcpy(buffer + offset, &props->image_format, 4); offset += 4;

    // Mip count
    buffer[offset++] = props->mip_count;

    // Thumbnail
    memcpy(buffer + offset, &props->thumbnail_format, 4); offset += 4;
    buffer[offset++] = props->thumbnail_width;
    buffer[offset++] = props->thumbnail_height;

    // Depth (v7.2+)
    if (props->minor_version >= 2) {
        memcpy(buffer + offset, &props->depth, 2); offset += 2;
    }

    // Resources (v7.3+)
    if (props->minor_version >= 3) {
        offset += 3; // Padding 2
        memcpy(buffer + offset, &props->resource_count, 4); offset += 4;
        offset += 8; // Padding 3
        for (uint32_t i = 0; i < props->resource_count; i++) {
            memcpy(buffer + offset, props->resources[i].type, 3); offset += 3;
            buffer[offset++] = props->resources[i].flags;
            memcpy(buffer + offset, &props->resources[i].data, 4); offset += 4;
        }
    }

    // Update header size
    memcpy(buffer + header_size_pos, &offset, 4);

    // Write
    fwrite(buffer, 1, offset, f);
    fclose(f);
}

void free_vtf(VTFProperties* props) {
    if (props->resources) free(props->resources);
}

int main(int argc, char** argv) {
    if (argc > 1) {
        VTFProperties props = {0};
        read_vtf(argv[1], &props);
        print_properties(&props);
        write_vtf("output.vtf", &props);
        free_vtf(&props);
    }
    return 0;
}