largely loadable model assets
[convexer.git] / cxr / cxr_valve_bin.h
index da6c84d6e11f81c12e00fe7e7081f897babfba90..b11f71f5d7f1cd00b067fcd9a4af5851d3b0828e 100644 (file)
@@ -24,19 +24,66 @@ typedef struct VPKDirectoryEntry VPKDirectoryEntry;
 typedef struct vdf_kv vdf_kv;
 typedef struct vdf_node vdf_node;
 typedef struct vdf_ctx vdf_ctx;
+typedef struct fs_locator fs_locator;
+
+/*
+ * These are 'unofficial' representations of the original formats, more C
+ * friendly 
+ */
 typedef struct valve_file_system valve_file_system;
+typedef struct valve_model valve_model;
+typedef struct valve_model_batch valve_model_batch;
+typedef struct valve_material valve_material;
+
+/* Api */
+
+CXR_API i32 cxr_fs_set_gameinfo( const char *path ); /* Setup system */
+CXR_API void cxr_fs_exit(void);                       /* Clean up */
+CXR_API void *cxr_fs_get( const char *path, i32 stringbuffer ); /* Get a file */
+CXR_API i32 cxr_fs_find( const char *path, fs_locator *locator );
+
+CXR_API valve_model *valve_load_model( const char *relpath );
+CXR_API void valve_free_model( valve_model *model );
+CXR_API valve_material *valve_load_material( const char *path );
+CXR_API void valve_free_material( valve_material *material );
 
 /*
- * File system
+ * File system implementation
  */
 
+#pragma pack(push, 1)
+
+struct VPKHeader
+{
+       u32 Signature;
+       u32 Version;
+       u32 TreeSize;
+       u32 FileDataSectionSize;
+       u32 ArchiveMD5SectionSize;
+       u32 OtherMD5SectionSize;
+       u32 SignatureSectionSize;
+};
+
+struct VPKDirectoryEntry
+{
+       u32 CRC;
+       u16 PreloadBytes; 
+       u16 ArchiveIndex;
+       u32 EntryOffset;
+       u32 EntryLength;
+       u16 Terminator;
+};
+
+#pragma pack(pop)
+
 static struct valve_file_system
 {
    char *gamedir,
         *exedir;
 
    /* Runtime */
-   VPKHeader *vpk;
+   VPKHeader vpk;
+   char *directory_tree;
 
    cxr_abuffer searchpaths;
 
@@ -47,16 +94,17 @@ static struct valve_file_system
 }
 fs_global = { .initialized = 0 };
 
-CXR_API int cxr_fs_set_gameinfo( const char *path ); /* Setup system */
-CXR_API void cxr_fs_exit(void);                       /* Clean up */
-CXR_API char *cxr_fs_get( const char *path );         /* Get a file */
+struct fs_locator
+{
+   VPKDirectoryEntry *vpk_entry;
+   char path[ 1024 ];
+};
 
 /* 
  * VPK reading
  */
 
-static VPKDirectoryEntry *vpk_find( VPKHeader *self, const char *asset );
-static void vpk_free( VPKHeader *self );
+static VPKDirectoryEntry *vpk_find( const char *asset );
 
 /*
  * VDF reading
@@ -97,39 +145,13 @@ while( (AS = vdf_next( NODE, STR, &__vdf_it_##AS )) )
 int __kv_it_##AS = 0; \
 const char * AS;\
 while( (AS = kv_next( NODE, STR, &__kv_it_##AS )) )
-#pragma pack(push, 1)
 
-struct VPKHeader
+static VPKDirectoryEntry *vpk_find( const char *asset )
 {
-       u32 Signature;
-       u32 Version;
-       u32 TreeSize;
-       u32 FileDataSectionSize;
-       u32 ArchiveMD5SectionSize;
-       u32 OtherMD5SectionSize;
-       u32 SignatureSectionSize;
-};
-
-struct VPKDirectoryEntry
-{
-       u32 CRC;
-       u16 PreloadBytes; 
-       u16 ArchiveIndex;
-       u32 EntryOffset;
-       u32 EntryLength;
-       u16 Terminator;
-};
-#pragma pack(pop)
-
-static void vpk_free( VPKHeader *self )
-{
-       free( self );
-}
+   valve_file_system *fs = &fs_global;
 
-static VPKDirectoryEntry *vpk_find( VPKHeader *self, const char *asset )
-{
-       if( !self ) 
-               return NULL;
+   if( !fs->directory_tree )
+      return NULL;
        
        char wbuf[ 512 ];
        strcpy( wbuf, asset );
@@ -151,7 +173,7 @@ static VPKDirectoryEntry *vpk_find( VPKHeader *self, const char *asset )
        *(fn-1) = 0x00;
 
        char *dir = wbuf;
-       char *pCur = ((char *)self) + sizeof( VPKHeader );
+       char *pCur = fs->directory_tree;
        
        while( 1 )
        {
@@ -540,6 +562,7 @@ static void vdf_parse_string( vdf_ctx *ctx )
                if( vdf_line_control( ctx ) )
                {
          /* Unexpected end of line */
+         cxr_log( "vdf: unexpected EOL\n" );
                        return;
                }
        
@@ -554,6 +577,7 @@ static int vdf_parse_structure( vdf_ctx *ctx )
                if( ctx->st.tokens[0] || !ctx->st.expect_decl )
                {
          /* Unexpected token '{' */
+         cxr_log( "vdf: Unexpected token '{'\n" );
                        ctx->errors ++;
                }
                
@@ -569,6 +593,7 @@ static int vdf_parse_structure( vdf_ctx *ctx )
                if( !ctx->st.pnode->parent )
                {
          /* Unexpected token '}' */
+         cxr_log( "vdf: Unexpected token '}'\n" );
                        ctx->errors ++;
                }
                else
@@ -590,6 +615,7 @@ static void vdf_parse_begin_token( vdf_ctx *ctx, char *ptr )
        if( ctx->st.expect_decl )
        {
       /* Unexpected token 'name' */
+      cxr_log( "vdf: unexpected token 'name'\n" );
                ctx->errors ++;
        }
 }
@@ -657,37 +683,68 @@ static void vdf_parse_feedbuffer( vdf_ctx *ctx, char *buf )
        }
 }
 
-static int vdf_load_into( const char *fn, vdf_node *node )
+static void vdf_debug_indent( int level )
 {
-       char *text_src = cxr_textasset_read( fn );
-       
-       if( !text_src )
-       {
-               return 0;
-       }
-       
+   for(int i=0; i<level; i++)
+      cxr_log(" ");
+}
+
+static void vdf_debug_r( vdf_node *node, int level )
+{
+   vdf_debug_indent(level);
+   cxr_log( "vdf_node(%p, name: '%s')\n", node, node->name );
+
+   vdf_debug_indent(level);
+   cxr_log( "{\n" );
+
+   for( int i=0; i<node->abpairs.count; i++ )
+   {
+      vdf_kv *kv = cxr_ab_ptr( &node->abpairs, i );
+
+      vdf_debug_indent(level+1);
+      cxr_log( "vdf_kv(%p, k: '%s', v: '%s')\n",
+         kv, kv->key, kv->value );
+   }
+
+   for( int i=0; i<node->abnodes.count; i++ )
+   {
+      vdf_node **child = cxr_ab_ptr( &node->abnodes, i );
+      vdf_debug_r( *child, level+1 );
+   }
+
+   vdf_debug_indent(level);
+   cxr_log( "}\n" );
+}
+
+/* This will wreck the original buffer, but must stay alive! */
+static vdf_node *vdf_from_buffer( char *buffer )
+{
+   vdf_node *root = vdf_create_node( NULL, NULL );
+
        vdf_ctx ctx = {0};
-       ctx.root = ctx.st.pnode = node;
+       ctx.root = ctx.st.pnode = root;
        
        vdf_newln( &ctx );
-       vdf_parse_feedbuffer( &ctx, text_src );
-       free( text_src );
-       
-       return 1;
+       vdf_parse_feedbuffer( &ctx, buffer );
+   
+#if 0
+   vdf_debug_r( root, 0 );
+#endif
+
+   return root;
 }
 
 static vdf_node *vdf_open_file( const char *fn )
 {      
-       vdf_node *root = vdf_create_node( NULL, NULL );
-       if( vdf_load_into( fn, root ) )
-       {
-               return root;
-       }
-       else
-       {
-               vdf_free_r( root );
+       char *text_src = cxr_textasset_read( fn );
+       
+       if( !text_src )
                return NULL;
-       }
+
+   vdf_node *root = vdf_from_buffer( text_src );
+   free( text_src );
+
+   return root;
 }
 
 /*
@@ -761,18 +818,31 @@ CXR_API i32 cxr_fs_set_gameinfo( const char *path )
        
    /* Find pack diretory */
        char pack_path[512];
+   fs->current_archive = NULL;
+   fs->current_idx = 0x7fff;
+
        for( int i = 0; i < fs->searchpaths.count; i ++ )
        {
       char **sp = cxr_ab_ptr( &fs->searchpaths, i );
 
                strcpy( pack_path, *sp );
                strcat( pack_path, "pak01_dir.vpk" );
-       
-               if( (fs->vpk = (VPKHeader *)cxr_asset_read( pack_path )) )
-                       break;
+
+      fs->current_archive = fopen( pack_path, "rb" );
+   
+      /* Read vpk directory */
+      if( fs->current_archive )
+      {
+         fread( &fs->vpk, sizeof(VPKHeader), 1, fs->current_archive );
+
+         fs->directory_tree = malloc( fs->vpk.TreeSize );
+         fread( fs->directory_tree, fs->vpk.TreeSize, 1, fs->current_archive );
+
+         break;
+      }
        }
        
-       if( !fs->vpk )
+       if( !fs->current_archive )
        {
                cxr_log( "Could not locate pak01_dir.vpk in %i searchpaths. "
                "Stock models will not load!\n", fs->searchpaths.count );
@@ -794,10 +864,10 @@ CXR_API void cxr_fs_exit(void)
    
    cxr_ab_free( &fs->searchpaths );
 
-       if( fs->vpk )
+       if( fs->directory_tree )
        {
-               vpk_free( fs->vpk );
-               fs->vpk = NULL;
+      free( fs->directory_tree );
+      fs->directory_tree = NULL;
        }
        
        if( fs->current_archive )
@@ -812,77 +882,148 @@ CXR_API void cxr_fs_exit(void)
    memset( fs, 0, sizeof( valve_file_system ) );
 }
 
-CXR_API char *cxr_fs_get( const char *path )
+static char *cxr_vpk_read( VPKDirectoryEntry *entry, int stringbuffer )
 {
        valve_file_system *fs = &fs_global;
 
    if( !fs->initialized )
       return NULL;
+
+   char pak[1024];
+   
+   /* Check if we need to change file handle */
+   if( entry->ArchiveIndex != fs->current_idx )
+   {
+      if( fs->current_archive )
+         fclose( fs->current_archive );
+
+      fs->current_archive = NULL;
+      fs->current_idx = entry->ArchiveIndex;
+
+      if( entry->ArchiveIndex == 0x7fff )
+      {
+         snprintf( pak, 1023, "%scsgo/pak01_dir.vpk", fs->exedir );
+      }
+      else
+      {
+         snprintf( pak, 1023, "%scsgo/pak01_%03hu.vpk", 
+               fs->exedir, entry->ArchiveIndex );
+      }
+
+      fs->current_archive = fopen( pak, "rb" );
+
+      if( !fs->current_archive )
+         cxr_log( "Warning: could not locate %s\n", pak );
+   }
+
+   if( !fs->current_archive )
+      return NULL;
+
+   size_t offset = entry->EntryOffset,
+          length = entry->EntryLength;
+
+   /*
+    * File is stored in the index, after the tree
+    */
+   if( entry->ArchiveIndex == 0x7fff )
+      offset += fs->vpk.TreeSize + sizeof(VPKHeader);
+   
+   /* 
+    * Entire file is stored in the preload bytes;
+    * Backtrack offset from directory to get absolute offset 
+    */
+   if( length == 0 )
+   {
+      offset = (char *)entry - (char *)fs->directory_tree;
+      offset += sizeof( VPKHeader );
+      offset += sizeof( VPKDirectoryEntry );
+
+      length = entry->PreloadBytes;
+   }
+   else
+      length += entry->PreloadBytes;
+
+   fseek( fs->current_archive, offset, SEEK_SET );
+   
+   size_t alloc_size = stringbuffer? length+1: length;
+   char *filebuf = malloc( alloc_size );
+
+   if( stringbuffer )
+      filebuf[length] = 0x00;
+
+   if( fread( filebuf, 1, length, fs->current_archive ) == length )
+      return filebuf;
+   else
+   {
+      /* Invalid read */
+      free( filebuf );
+      return NULL;
+   }
+}
+
+CXR_API i32 cxr_fs_find( const char *path, fs_locator *locator )
+{
+       valve_file_system *fs = &fs_global;
+
+   if( !fs->initialized )
+      return 0;
        
        VPKDirectoryEntry *entry;
-       char pak[ 533 ];
 
-       if( fs->vpk )
+       if( fs->directory_tree )
        {
-               if( (entry = vpk_find( fs->vpk, path )) )
+               if( (entry = vpk_find( path )) )
                {
-                       if( entry->ArchiveIndex != fs->current_idx )
-                       {
-                               if( fs->current_archive )
-                               {
-                                       fclose( fs->current_archive );
-                                       fs->current_archive = NULL;
-                               }
-                               
-                               fs->current_idx = entry->ArchiveIndex;
-                       }
-                       
-                       if( !fs->current_archive )
-                       {
-                               sprintf( pak, "%scsgo/pak01_%03hu.vpk", fs->exedir, 
-                  fs->current_idx );
-                               fs->current_archive = fopen( pak, "rb" );
-                               
-                               if( !fs->current_archive )
-                               {
-                                       cxr_log( "Could not locate %s\n", pak );
-                                       return NULL;
-                               }
-                       }
-                       
-                       char *filebuf = malloc( entry->EntryLength );
-                       
-                       fseek( fs->current_archive, entry->EntryOffset, SEEK_SET );
-                       if( fread( filebuf, 1, entry->EntryLength, fs->current_archive ) 
-               == entry->EntryLength )
-                       {
-                               return filebuf;
-                       }
-                       else
-                       {
-                               free( filebuf );
-                               return NULL;
-                       }
+         locator->vpk_entry = entry;
+         locator->path[0] = 0x00;
+         return 1;
                }
        }
-       
+
+   locator->vpk_entry = NULL;
+
    /* Use physical search paths */
-       char path_buf[ 512 ];
-       
        for( int i = 0; i < fs->searchpaths.count; i ++ )
        {
       char **sp = cxr_ab_ptr( &fs->searchpaths, i );
 
-               strcpy( path_buf, *sp );
-               strcat( path_buf, path );
-               
-               char *filebuf;
-               if( (filebuf = cxr_asset_read( path_buf )) )
-                       return filebuf;
+      snprintf( locator->path, 1023, "%s%s", *sp, path );
+      
+      if( cxr_file_exists( locator->path ) )
+         return 1;
        }
        
    /* File not found */
-       return NULL;
+       return 0;
+}
+
+CXR_API void *cxr_fs_get( const char *path, i32 stringbuffer )
+{
+       valve_file_system *fs = &fs_global;
+
+   if( !fs->initialized )
+      return NULL;
+
+   fs_locator locator;
+
+   if( cxr_fs_find( path, &locator ) )
+   {
+      if( locator.vpk_entry )
+      {
+         return cxr_vpk_read( locator.vpk_entry, stringbuffer );
+      }
+      else
+      {
+         char *filebuf;
+
+         if( stringbuffer )
+            return cxr_textasset_read( locator.path );
+         else
+            return cxr_asset_read( locator.path );
+      }
+   }
+
+   return NULL;
 }
 
 /*
@@ -1264,16 +1405,44 @@ typedef struct
 } 
 studiohdr_t;
 
+static char *studiohdr_pCdtexture( studiohdr_t *t, int i )
+{ 
+   return (((char *)t) + *((int *)(((u8 *)t) + t->cdtextureindex) + i)); 
+}
+
 static mstudiobodyparts_t *studiohdr_pBodypart( studiohdr_t *t, int i ) 
 { 
        return (mstudiobodyparts_t *)(((char *)t) + t->bodypartindex) + i; 
 }
 
+typedef struct
+{
+   int                                         sznameindex;
+   int                                         flags;
+   int                                         used;
+   
+   /* There is some extra unused stuff that was here... 
+    * Luckily since byte offsets are used, structure size doesn't matter */
+} 
+mstudiotexture_t;
+
+static char *mstudiotexture_pszName( mstudiotexture_t *t )
+{ 
+   return ((char *)t) + t->sznameindex;
+}
+
+static mstudiotexture_t *studiohdr_pTexture( studiohdr_t *t, int i )
+{ 
+   return (mstudiotexture_t *)(((u8 *)t) + t->textureindex) + i; 
+} 
+
 #pragma pack(pop)
 
-static u32 vtx_count_indices( VTXFileHeader_t *pVtxHdr, studiohdr_t *pMdl )
+static void vtx_resource_counts( VTXFileHeader_t *pVtxHdr, studiohdr_t *pMdl,
+      u32 *indices, u32 *meshes )
 {
-   int indice_count = 0;
+   *indices = 0;
+   *meshes = 0;
 
        for( int bodyID = 0; bodyID < pVtxHdr->numBodyParts; ++bodyID )
        {
@@ -1297,6 +1466,8 @@ static u32 vtx_count_indices( VTXFileHeader_t *pVtxHdr, studiohdr_t *pMdl )
                                VTXMeshHeader_t* pVtxMesh = pMeshVTX( pVtxLOD, nMesh );
                                mstudiomesh_t* pMesh = studiomodel_pMesh( pStudioModel, nMesh );
 
+            (*meshes)++;
+
                                for ( int nGroup = 0; nGroup < pVtxMesh->numStripGroups; ++nGroup )
                                {
                /* groups */
@@ -1310,15 +1481,293 @@ static u32 vtx_count_indices( VTXFileHeader_t *pVtxHdr, studiohdr_t *pMdl )
 
                                                if ( pStrip->flags & STRIP_IS_TRILIST )
                                                {
-                     indice_count += pStrip->numIndices;
+                     *indices += pStrip->numIndices;
+                                               }
+                                       }
+                               }
+                       }
+               }
+       }
+}
+
+/* 
+ * The following section is the wrappers for the underlying types
+ */
+
+struct valve_material
+{
+   char *basetexture,
+        *bumpmap;
+};
+
+struct valve_model
+{
+   float *vertex_data;  /* pos xyz, norm xyz, uv xy */
+
+   u32 *indices,
+        indices_count,
+        vertex_count,
+        part_count,
+        material_count;
+
+   char **materials;
+
+   struct valve_model_batch
+   {
+      u32 material,
+          ibstart,
+          ibcount;
+   }
+   *parts;
+
+   /* Internal valve data */
+   studiohdr_t *studiohdr;
+   VTXFileHeader_t *vtxhdr;
+   vertexFileHeader_t *vvdhdr;
+};
+
+CXR_API valve_model *valve_load_model( const char *relpath )
+{
+   char path[1024];
+   strcpy( path, relpath );
+
+   char *ext = cxr_stripext( path );
+
+   if( !ext )
+      return NULL;
+
+   /* Load data files */
+   valve_model *model = malloc( sizeof( valve_model ) );
+   model->studiohdr = NULL;
+   model->vtxhdr = NULL;
+   model->vvdhdr = NULL;
+
+   strcpy( ext, ".dx90.vtx" );
+   model->vtxhdr = cxr_fs_get( path, 0 );
+
+   strcpy( ext, ".vvd" );
+   model->vvdhdr = cxr_fs_get( path, 0 );
+
+   strcpy( ext, ".mdl" );
+   model->studiohdr = cxr_fs_get( path, 0 );
+
+   if( !model->vvdhdr || !model->studiohdr || !model->vtxhdr )
+   {
+      cxr_log( "Error, failed to load: (%s)\n", relpath );
+
+      free( model->studiohdr );
+      free( model->vvdhdr );
+      free( model->studiohdr );
+      free( model );
+
+      return NULL;
+   }
+
+   /* allocate resources */
+   vtx_resource_counts( model->vtxhdr, model->studiohdr, 
+         &model->indices_count, &model->part_count );
+   model->vertex_count = model->vvdhdr->numLodVertexes[0];
+   model->material_count = model->studiohdr->numtextures;
+
+   model->materials = malloc( model->material_count * sizeof(char *) );
+   model->parts = malloc( sizeof( valve_model_batch ) * model->part_count );
+   model->indices = malloc( sizeof( u32 ) * model->indices_count );
+   model->vertex_data = malloc( sizeof( float ) * 8 * model->vertex_count );
+   
+   /* Find materials */
+   for( int i=0; i<model->studiohdr->numtextures; i++ )
+   {
+      char material_path[ 1024 ];
+      fs_locator locator;
+
+      mstudiotexture_t *tex = studiohdr_pTexture( model->studiohdr, i );
+      const char *name = mstudiotexture_pszName( tex );
+
+      model->materials[i] = NULL;
+
+      for( int j=0; j<model->studiohdr->numcdtextures; j++ )
+      {
+         char *cdpath = studiohdr_pCdtexture( model->studiohdr, j );
+         snprintf( material_path, 1023, "materials/%s%s.vmt", cdpath, name );
+         cxr_unixpath( material_path );
+
+         if( cxr_fs_find( material_path, &locator ))
+         {
+            model->materials[i] = cxr_str_clone( material_path, 0 );
+            break;
+         }
+      }
+   }
+
+   u32 i_index = 0,
+       i_mesh = 0;
+
+   /* Extract meshes */
+       for( int bodyID = 0; bodyID < model->studiohdr->numbodyparts; ++bodyID )
+       {
+               /* Body parts */
+               VTXBodyPartHeader_t* pVtxBodyPart = pBodyPartVTX( model->vtxhdr, bodyID );
+               mstudiobodyparts_t *pBodyPart = 
+         studiohdr_pBodypart( model->studiohdr, bodyID );
+               
+               for( int modelID = 0; modelID < pBodyPart->nummodels; ++modelID )
+               {       
+         /* models */
+                       VTXModelHeader_t* pVtxModel = pModelVTX( pVtxBodyPart, modelID );
+                       mstudiomodel_t *pStudioModel = 
+            mstudiobodyparts_pModel( pBodyPart, modelID );
+
+                       int nLod = 0;
+                       VTXModelLODHeader_t *pVtxLOD = pLODVTX( pVtxModel, nLod );
+
+                       for( int nMesh = 0; nMesh < pStudioModel->nummeshes; ++nMesh )
+                       {
+            /* meshes, each of these creates a new draw CMD */
+                               VTXMeshHeader_t* pVtxMesh = pMeshVTX( pVtxLOD, nMesh );
+                               mstudiomesh_t* pMesh = studiomodel_pMesh( pStudioModel, nMesh );
+
+            valve_model_batch *curBatch = &model->parts[ i_mesh ++ ];
+            curBatch->material = pMesh->material;
+            curBatch->ibstart = i_index;
+            curBatch->ibcount = 0;
+
+                               for( int nGroup = 0; nGroup < pVtxMesh->numStripGroups; ++nGroup )
+                               {
+               /* groups */
+                                       VTXStripGroupHeader_t* pStripGroup = 
+                  pStripGroupVTX( pVtxMesh, nGroup );
+
+                                       for( int nStrip = 0; nStrip < pStripGroup->numStrips; nStrip++ )
+                                       {
+                  /* strips */
+                                               VTXStripHeader_t *pStrip = pStripVTX( pStripGroup, nStrip );
+
+                                               if( pStrip->flags & STRIP_IS_TRILIST )
+                                               {
+                     /* indices */
+                                                       for( int i = 0; i < pStrip->numIndices; i ++ )
+                                                       {
+                                                               u16 i1 = *pIndexVTX( pStripGroup, 
+                              pStrip->indexOffset + i );                                                               
+
+                                                               model->indices[ i_index ++ ] = 
+                           pVertexVTX( pStripGroup, i1 )->origMeshVertID + 
+                           pMesh->vertexoffset;
+
+                        curBatch->ibcount ++;
+                                                       }
                                                }
+                  else
+                  {
+                     /* This is unused? */
+                  }
                                        }
                                }
                        }
                }
        }
+       
+       mstudiovertex_t *vertexData = GetVertexData( model->vvdhdr );
+
+       for( int i = 0; i < model->vertex_count; i ++ )
+       {
+               mstudiovertex_t *vert = &vertexData[i];
+      
+      float *dest = &model->vertex_data[ i*8 ];
+
+      dest[0] = vert->pos[0];
+      dest[1] = vert->pos[1];
+      dest[2] = vert->pos[2];
 
-   return indice_count;
+      dest[3] = vert->norm[0];
+      dest[4] = vert->norm[1];
+      dest[5] = vert->norm[2];
+
+      dest[6] = vert->uv[0];
+      dest[7] = vert->uv[1];
+       }
+       
+   return model;
+}
+
+CXR_API void valve_free_model( valve_model *model )
+{
+   for( int i=0; i<model->material_count; i++ )
+      free( model->materials[i] );
+
+   free( model->materials );
+   free( model->parts );
+   free( model->indices );
+   free( model->vertex_data );
+
+   free( model->studiohdr );
+   free( model->vtxhdr );
+   free( model->vvdhdr );
+   free( model );
+}
+
+static char *valve_texture_path( const char *path )
+{
+   if( !path )
+      return NULL;
+
+   char *buf = cxr_str_clone( path, 4 );
+
+   strcat( buf, ".vtf" );
+   cxr_unixpath( buf );
+   cxr_lowercase( buf );
+
+   return buf;
+}
+
+CXR_API valve_material *valve_load_material( const char *path )
+{
+   char *vmt = cxr_fs_get( path, 1 );
+
+   if( vmt )
+   {
+      valve_material *material = malloc( sizeof(valve_material) );
+      vdf_node *vmt_root = vdf_from_buffer( vmt );
+
+      if( vmt_root->abnodes.count == 0 )
+      {
+         cxr_log( "Error: vmt has no nodes\n" );
+         free( vmt );
+         vdf_free_r( vmt_root );
+         return 0;
+      }
+      
+      vdf_node **body = cxr_ab_ptr( &vmt_root->abnodes, 0 );
+
+      /* Path semantics here are inconsistent
+       * I believe they should all just be converted to lowercase, though */
+
+      for( int i=0; i<(*body)->abpairs.count; i++ )
+      {
+         vdf_kv *kv = cxr_ab_ptr( &(*body)->abpairs, i );
+         cxr_lowercase( kv->key );
+      }
+      
+      const char *basetexture = kv_get( *body, "$basetexture", NULL ),
+                 *bumpmap = kv_get( *body, "$bumpmap", NULL );
+      
+      /* TODO: other shader parameters */
+      material->basetexture = valve_texture_path( basetexture );
+      material->bumpmap = valve_texture_path( bumpmap );
+      
+      vdf_free_r( vmt_root );
+      free(vmt);
+
+      return material;
+   }
+
+   return NULL;
+}
+
+CXR_API void valve_free_material( valve_material *material )
+{
+   free( material->basetexture );
+   free( material->bumpmap );
 }
 
 #endif /* CXR_VALVE_BIN_H */