the asumptions were of course, incorrect
[convexer.git] / __init__.py
index 544d770b19433a22b846762c0bf2ac2afe7ca5ba..05aefbcb1066a2c5592a6c5d9f1ac5c3e55bd0c0 100644 (file)
@@ -1,4 +1,12 @@
-# Copyright (C) 2022 Harry Godden (hgn)
+#                              CONVEXER v0.95
+#
+#               A GNU/Linux-first Source1 Hammer replacement
+#                    built with Blender, for mapmakers
+#
+#                  Copyright (C) 2022 Harry Godden (hgn)
+#
+# LICENSE: GPLv3.0, please see COPYING and LICENSE for more information
+#
 
 bl_info = {
    "name":"Convexer",
@@ -15,17 +23,341 @@ bl_info = {
 print( "Convexer reload" )
 
 #from mathutils import *
-import bpy, gpu, math, os, time, mathutils
+import bpy, gpu, math, os, time, mathutils, blf, subprocess, shutil, hashlib
 from ctypes import *
 from gpu_extras.batch import batch_for_shader
 from bpy.app.handlers import persistent
 
-# Globals and tweaks
-vmt_param_dynamic_class = None
+# Setup platform dependent variables
+
+exec(open(F'{os.path.dirname(__file__)}/platform.py').read())
+if CXR_GNU_LINUX==1:
+   CXR_SHARED_EXT=".so"
+   CXR_EXE_EXT=""
+else:
+   CXR_SHARED_EXT=".dll"
+   CXR_EXE_EXT=".exe"
+
+# GPU and viewport drawing
+# ------------------------------------------------------------------------------
+
+# Handlers
+cxr_view_draw_handler = None
+cxr_ui_draw_handler = None
+
+# Batches
+cxr_view_lines = None
+cxr_view_mesh = None
+cxr_jobs_batch = None
+cxr_jobs_inf = []
+cxr_error_inf = None
+cxr_test_mdl = None
+
+cxr_asset_lib = \
+{
+   "models": {},
+   "materials": {},
+   "textures": {}
+}
+
+# Shaders
+cxr_view_shader = gpu.shader.from_builtin('3D_SMOOTH_COLOR')
+
+cxr_ui_shader = gpu.types.GPUShader("""
+uniform mat4 ModelViewProjectionMatrix;
+uniform float scale;
+
+in vec2 aPos;
+in vec4 aColour;
+
+out vec4 colour;
+
+void main()
+{
+   gl_Position = ModelViewProjectionMatrix * vec4(aPos.x*scale,aPos.y, 0.0, 1.0);
+   colour = aColour;
+}
+""","""
+in vec4 colour;
+out vec4 FragColor;
+
+void main()
+{
+   FragColor = colour;
+}
+""")
+
+cxr_mdl_shader = gpu.types.GPUShader("""
+uniform mat4 modelMatrix;
+uniform mat4 viewProjectionMatrix;
+
+in vec3 aPos;
+in vec3 aNormal;
+in vec2 aUv;
+
+out vec3 lPos;
+out vec3 lNormal;
+out vec2 lUv;
+
+void main()
+{
+   vec4 pWorldPos = modelMatrix * vec4(aPos, 1.0); 
+   vec3 worldPos = pWorldPos.xyz;
+
+   gl_Position = viewProjectionMatrix * pWorldPos;
+   lNormal = normalize(mat3(transpose(inverse(modelMatrix))) * aNormal);
+   lPos = worldPos;
+   lUv = aUv;
+}
+""","""
+
+uniform vec4 colour;
+uniform vec3 testLightDir;
+uniform sampler2D uBasetexture;
+
+in vec3 lNormal;
+in vec3 lPos;
+in vec2 lUv;
+
+out vec4 FragColor;
+
+float SoftenCosineTerm( float flDot )
+{
+       return ( flDot + ( flDot * flDot ) ) * 0.5;
+}
+
+vec3 DiffuseTerm( vec3 worldNormal, vec3 lightDir )
+{
+   float fResult = 0.0;
+   float NDotL = dot( worldNormal, lightDir );
+
+   fResult = clamp( NDotL, 0.0, 1.0 );
+   fResult = SoftenCosineTerm( fResult );
+
+   vec3 fOut = vec3( fResult, fResult, fResult );
+   return fOut;
+}
+
+vec3 PixelShaderDoLightingLinear( vec3 worldPos, vec3 worldNormal )
+{
+   vec3 linearColor = vec3(0.0,0.0,0.0);
+   linearColor += DiffuseTerm( worldNormal, testLightDir );
+
+   return linearColor;
+}
+
+vec3 LinearToGamma( vec3 f3linear )
+{
+       return pow( f3linear, vec3(1.0 / 2.2) );
+}
+
+vec3 GammaToLinear( vec3 f3gamma )
+{
+   return pow( f3gamma, vec3(2.2) );
+}
+
+void main()
+{
+   vec3 tangentSpaceNormal = vec3( 0.0, 0.0, 1.0 );
+   vec4 normalTexel = vec4(1.0,1.0,1.0,1.0);
+   vec3 colorInput = GammaToLinear( texture( uBasetexture, lUv ).rgb );
+
+   vec4 baseColor = vec4( colorInput * colour.rgb, 1.0 );
+
+       //normalTexel = tex2D( BumpmapSampler, i.detailOrBumpTexCoord );
+       //tangentSpaceNormal = 2.0 * normalTexel - 1.0;
+
+       vec3 diffuseLighting = vec3( 1.0, 1.0, 1.0 );
+
+   vec3 staticLightingColor = vec3( 0.0, 0.0, 0.0 );
+   diffuseLighting = PixelShaderDoLightingLinear( lPos, lNormal );
+
+   // multiply by .5 since we want a 50% (in gamma space) reflective surface)
+   diffuseLighting *= pow( 0.5, 2.2 );
+
+   vec3 result = diffuseLighting * baseColor.xyz;
+
+   FragColor = vec4( LinearToGamma(result), 1.0 );
+}
+""")
+
+# Render functions
+#
+def cxr_ui(_,context):
+   global cxr_jobs_batch, cxr_ui_shader, cxr_jobs_inf, cxr_error_inf
+
+   w = gpu.state.viewport_get()[2]
+   cxr_ui_shader.bind()
+   cxr_ui_shader.uniform_float( "scale", w )
+
+   if cxr_error_inf != None:
+      err_begin = 50
+
+      if isinstance(cxr_error_inf[1],list):
+         err_begin += 20*(len(cxr_error_inf[1])-1)
+
+      blf.position(0,2,err_begin,0)
+      blf.size(0,50,48)
+      blf.color(0, 1.0,0.2,0.2,0.9)
+      blf.draw(0,cxr_error_inf[0])
+      
+      blf.size(0,50,24)
+      blf.color(0, 1.0,1.0,1.0,1.0)
+
+      if isinstance(cxr_error_inf[1],list):
+         for i,inf in enumerate(cxr_error_inf[1]):
+            blf.position(0,2,err_begin-30-i*20,0)
+            blf.draw(0,inf)
+      else:
+         blf.position(0,2,err_begin-30,0)
+         blf.draw(0,cxr_error_inf[1])
+
+   elif cxr_jobs_batch != None:
+      gpu.state.blend_set('ALPHA')
+      cxr_jobs_batch.draw(cxr_ui_shader)
+
+      blf.position(0,2,50,0)
+      blf.size(0,50,48)
+      blf.color(0,1.0,1.0,1.0,1.0)
+      blf.draw(0,"Compiling")
+
+      for ji in cxr_jobs_inf:
+         blf.position(0,ji[0]*w,35,0)
+         blf.size(0,50,20)
+         blf.draw(0,ji[1])
+      
+      py = 80
+      blf.size(0,50,16)
+      for ln in reversed(CXR_COMPILER_CHAIN.LOG[-25:]):
+         blf.position(0,2,py,0)
+         blf.draw(0,ln[:-1])
+         py += 16
+
+   # Something is off with TIMER,
+   # this forces the viewport to redraw before we can continue with our
+   # compilation stuff.
+
+   CXR_COMPILER_CHAIN.WAIT_REDRAW = False
+
+def cxr_draw():
+   global cxr_view_shader, cxr_view_mesh, cxr_view_lines, cxr_mdl_shader,\
+          cxr_mdl_mesh, cxr_test_mdl
+
+   cxr_view_shader.bind()
+
+   gpu.state.depth_mask_set(False)
+   gpu.state.line_width_set(1.5)
+   gpu.state.face_culling_set('BACK')
+   gpu.state.depth_test_set('NONE')
+   gpu.state.blend_set('ALPHA')
+
+   if cxr_view_lines != None:
+      cxr_view_lines.draw( cxr_view_shader )
+
+   if cxr_view_mesh != None:
+      gpu.state.depth_test_set('LESS_EQUAL')
+      gpu.state.blend_set('ADDITIVE')
+      
+      cxr_view_mesh.draw( cxr_view_shader )
+
+   # Models
+   gpu.state.depth_mask_set(True)
+   gpu.state.depth_test_set('LESS_EQUAL')
+   gpu.state.face_culling_set('FRONT')
+   gpu.state.blend_set('NONE')
+
+   cxr_mdl_shader.bind()
+   cxr_mdl_shader.uniform_float("viewProjectionMatrix", \
+         bpy.context.region_data.perspective_matrix)
+
+   if cxr_test_mdl != None:
+      cxr_mdl_shader.uniform_float('colour',(1.0,1.0,1.0,1.0))
+
+      #temp light dir
+      testmdl = bpy.context.scene.objects['target']
+      light = bpy.context.scene.objects['point']
+      relative = light.location - testmdl.location 
+      relative.normalize()
+      cxr_mdl_shader.uniform_float("modelMatrix", testmdl.matrix_world)
+      cxr_mdl_shader.uniform_float("testLightDir", relative)
+
+      for part in cxr_test_mdl:
+         cxr_mdl_shader.uniform_sampler("uBasetexture", part[0]['basetexture'])
+         part[1].draw( cxr_mdl_shader )
+
+def cxr_jobs_update_graph(jobs):
+   global cxr_jobs_batch, cxr_ui_shader, cxr_jobs_inf
+
+   cxr_jobs_inf = []
+
+   total_width = 0
+   verts = []
+   colours = []
+   indices = []
+
+   for sys in jobs:
+      total_width += sys['w']
+
+   sf = 1.0/total_width
+   cur = 0.0
+   ci = 0
+
+   for sys in jobs:
+      w = sys['w']
+      h = 30.0
+      colour = sys['colour']
+      colourwait = (colour[0],colour[1],colour[2],0.4)
+      colourrun =  (colour[0]*1.5,colour[1]*1.5,colour[2]*1.5,0.5)
+      colourdone = (colour[0],colour[1],colour[2],1.0)
+      
+      jobs = sys['jobs']
+      sfsub = (1.0/(len(jobs)))*w
+      i = 0
+      
+      for j in jobs:
+         if j == None: colour = colourdone
+         else: colour = colourwait
+
+         px = (cur + (i)*sfsub) * sf
+         px1 = (cur + (i+1.0)*sfsub) * sf
+         i += 1
+
+         verts += [(px,0), (px, h), (px1, 0.0), (px1,h)]
+         colours += [colour,colour,colour,colour]
+         indices += [(ci+0,ci+2,ci+3),(ci+0,ci+3,ci+1)]
+         ci += 4
+
+      cxr_jobs_inf += [((sf*cur), sys['title'])]
+      cur += w
+
+   cxr_jobs_batch = batch_for_shader(
+      cxr_ui_shader, 'TRIS',
+      { "aPos": verts, "aColour": colours },
+      indices = indices
+   )
+
+# view_layer.update() doesnt seem to work,
+# tag_redraw() seems to have broken
+#  therefore, change a property
+def scene_redraw():
+   ob = bpy.context.scene.objects[0]
+   ob.hide_render = ob.hide_render
 
-# libcxr interface (TODO: We can probably automate this)
-# ======================================================
+   # the 'real' way to refresh the scene
+   for area in bpy.context.window.screen.areas:
+      if area.type == 'view_3d':
+         area.tag_redraw()
+
+# Shared libraries
+# ------------------------------------------------------------------------------
 
+if CXR_GNU_LINUX==1:
+   # dlclose for reloading modules manually
+   libc_dlclose = None
+   libc_dlclose = cdll.LoadLibrary(None).dlclose
+   libc_dlclose.argtypes = [c_void_p]
+
+# wrapper for ctypes binding
 class extern():
    def __init__(_,name,argtypes,restype):
       _.name = name
@@ -40,25 +372,24 @@ class extern():
       if _.restype != None:
          _.call.restype = _.restype
 
-libc_dlclose = None
-libc_dlclose = cdll.LoadLibrary(None).dlclose
-libc_dlclose.argtypes = [c_void_p]
+# libcxr (convexer)
+# ------------------------------------------------------------------------------
 
-# Callback ctypes wrapper...
 libcxr = None
-c_libcxr_log_callback = None
-c_libcxr_line_callback = None
 
 # Structure definitions
+#
 class cxr_edge(Structure):
    _fields_ = [("i0",c_int32),
                ("i1",c_int32),
-               ("freestyle",c_int32)]
+               ("freestyle",c_int32),
+               ("sharp",c_int32)]
 
 class cxr_static_loop(Structure):
    _fields_ = [("index",c_int32),
                ("edge_index",c_int32),
-               ("uv",c_double * 2)]
+               ("uv",c_double * 2),
+               ("alpha",c_double)]
 
 class cxr_polygon(Structure):
    _fields_ =  [("loop_start",c_int32),
@@ -86,89 +417,205 @@ class cxr_static_mesh(Structure):
 
 class cxr_tri_mesh(Structure):
    _fields_ = [("vertices",POINTER(c_double *3)),
+               ("normals",POINTER(c_double *3)),
+               ("uvs",POINTER(c_double *2)),
                ("colours",POINTER(c_double *4)),
                ("indices",POINTER(c_int32)),
                ("indices_count",c_int32),
                ("vertex_count",c_int32)]
 
+class cxr_visgroup(Structure):
+   _fields_ = [("name",c_char_p)]
+
 class cxr_vmf_context(Structure):
    _fields_ = [("mapversion",c_int32),
                ("skyname",c_char_p),
                ("detailvbsp",c_char_p),
                ("detailmaterial",c_char_p),
+               ("visgroups",POINTER(cxr_visgroup)),
+               ("visgroup_count",c_int32),
                ("scale",c_double),
                ("offset",c_double *3),
                ("lightmap_scale",c_int32),
+               ("visgroupid",c_int32),
                ("brush_count",c_int32),
                ("entity_count",c_int32),
                ("face_count",c_int32)]
 
-# Public API
-libcxr_decompose = extern( "cxr_decompose", \
-      [POINTER(cxr_static_mesh)], c_void_p )
+# Valve wrapper types
+class fs_locator(Structure):
+   _fields_ = [("vpk_entry",c_void_p),
+               ("path",c_char_p*1024)]
+
+class valve_material(Structure):
+   _fields_ = [("basetexture",c_char_p),
+               ("bumpmap",c_char_p)]
+
+class valve_model_batch(Structure):
+   _fields_ = [("material",c_uint32),
+               ("ibstart",c_uint32),
+               ("ibcount",c_uint32)]
+
+class valve_model(Structure):
+   _fields_ = [("vertex_data",POINTER(c_float)),
+               ("indices",POINTER(c_uint32)),
+               ("indices_count",c_uint32),
+               ("vertex_count",c_uint32),
+               ("part_count",c_uint32),
+               ("material_count",c_uint32),
+               ("materials",POINTER(c_char_p)),
+               ("parts",POINTER(valve_model_batch)),
+               ("studiohdr",c_void_p),
+               ("vtxhdr",c_void_p),
+               ("vvdhdr",c_void_p)]
+
+# Convert blenders mesh format into CXR's static format (they are very similar)
+#
+def mesh_cxr_format(obj):
+   orig_state = None
 
-libcxr_free_world = extern( "cxr_free_world", [c_void_p], None )
-libcxr_write_test_data = extern( "cxr_write_test_data", \
-      [POINTER(cxr_static_mesh)], None )
-libcxr_world_preview = extern( "cxr_world_preview", [c_void_p], \
-        POINTER(cxr_tri_mesh))
-libcxr_free_tri_mesh = extern( "cxr_free_tri_mesh", [c_void_p], None )
+   if bpy.context.active_object != None:
+      orig_state = obj.mode
+      if orig_state != 'OBJECT':
+         bpy.ops.object.mode_set(mode='OBJECT')
 
-# VMF
-libcxr_begin_vmf = extern( "cxr_begin_vmf", \
-      [POINTER(cxr_vmf_context), c_void_p], None )
+   dgraph = bpy.context.evaluated_depsgraph_get()
+   data = obj.evaluated_get(dgraph).data
+   
+   _,mtx_rot,_ = obj.matrix_world.decompose()
 
-libcxr_vmf_begin_entities = extern( "cxr_vmf_begin_entities", \
-      [POINTER(cxr_vmf_context), c_void_p], None )
+   mesh = cxr_static_mesh()
 
-libcxr_push_world_vmf = extern("cxr_push_world_vmf", \
-      [c_void_p,POINTER(cxr_vmf_context),c_void_p], None )
+   vertex_data = ((c_double*3)*len(data.vertices))()
+   for i, vert in enumerate(data.vertices):
+      v = obj.matrix_world @ vert.co
+      vertex_data[i][0] = c_double(v[0])
+      vertex_data[i][1] = c_double(v[1])
+      vertex_data[i][2] = c_double(v[2])
 
-libcxr_end_vmf = extern( "cxr_end_vmf", \
-      [POINTER(cxr_vmf_context),c_void_p], None )
+   loop_data = (cxr_static_loop*len(data.loops))()
+   polygon_data = (cxr_polygon*len(data.polygons))()
 
-# VDF
-libcxr_vdf_open = extern( "cxr_vdf_open", [c_char_p], c_void_p )
-libcxr_vdf_close = extern( "cxr_vdf_close", [c_void_p], None )
-libcxr_vdf_put = extern( "cxr_vdf_put", [c_void_p,c_char_p], None )
-libcxr_vdf_node = extern( "cxr_vdf_node", [c_void_p,c_char_p], None )
-libcxr_vdf_edon = extern( "cxr_vdf_edon", [c_void_p], None )
-libcxr_vdf_kv = extern( "cxr_vdf_kv", [c_void_p,c_char_p,c_char_p], None )
+   for i, poly in enumerate(data.polygons):
+      loop_start = poly.loop_start
+      loop_end = poly.loop_start + poly.loop_total
+      for loop_index in range(loop_start, loop_end):
+         loop = data.loops[loop_index]
+         loop_data[loop_index].index = loop.vertex_index
+         loop_data[loop_index].edge_index = loop.edge_index
 
-# Other
-libcxr_lightpatch_bsp = extern( "cxr_lightpatch_bsp", [c_char_p], None )
+         if data.uv_layers:
+             uv = data.uv_layers.active.data[loop_index].uv
+             loop_data[loop_index].uv[0] = c_double(uv[0])
+             loop_data[loop_index].uv[1] = c_double(uv[1])
+         else:
+             loop_data[loop_index].uv[0] = c_double(0.0)
+             loop_data[loop_index].uv[1] = c_double(0.0)
 
-libcxr_funcs = [ libcxr_decompose, libcxr_free_world, libcxr_begin_vmf, \
-                 libcxr_vmf_begin_entities, libcxr_push_world_vmf, \
-                 libcxr_end_vmf, libcxr_vdf_open, libcxr_vdf_close, \
-                 libcxr_vdf_put, libcxr_vdf_node, libcxr_vdf_edon, 
-                 libcxr_vdf_kv, libcxr_lightpatch_bsp, libcxr_write_test_data,\
-                 libcxr_world_preview, libcxr_free_tri_mesh ]
+         if data.vertex_colors:
+            alpha = data.vertex_colors.active.data[loop_index].color[0]
+         else:
+            alpha = 0.0
 
-# libnbvtf interface
-# ==================
-libnbvtf = None
+         loop_data[loop_index].alpha = alpha
 
-libnbvtf_convert = extern( "nbvtf_convert", \
-      [c_char_p,c_int32,c_int32,c_int32,c_int32,c_int32,c_uint32,c_char_p],
-      c_int32 )
+      center = obj.matrix_world @ poly.center
+      normal = mtx_rot @ poly.normal
+
+      polygon_data[i].loop_start = poly.loop_start
+      polygon_data[i].loop_total = poly.loop_total
+      polygon_data[i].normal[0] = normal[0]
+      polygon_data[i].normal[1] = normal[1]
+      polygon_data[i].normal[2] = normal[2]
+      polygon_data[i].center[0] = center[0]
+      polygon_data[i].center[1] = center[1]
+      polygon_data[i].center[2] = center[2]
+      polygon_data[i].material_id = poly.material_index
 
-libnbvtf_funcs = [ libnbvtf_convert ]
+   edge_data = (cxr_edge*len(data.edges))()
 
-# NBVTF constants
-# ===============
+   for i, edge in enumerate(data.edges):
+      edge_data[i].i0 = edge.vertices[0]
+      edge_data[i].i1 = edge.vertices[1]
+      edge_data[i].freestyle = edge.use_freestyle_mark
+      edge_data[i].sharp = edge.use_edge_sharp
 
-NBVTF_IMAGE_FORMAT_RGBA8888 = 0
-NBVTF_IMAGE_FORMAT_RGB888 = 2
-NBVTF_IMAGE_FORMAT_DXT1 = 13
-NBVTF_IMAGE_FORMAT_DXT5 = 15
-NBVTF_TEXTUREFLAGS_CLAMPS = 0x00000004
-NBVTF_TEXTUREFLAGS_CLAMPT = 0x00000008
-NBVTF_TEXTUREFLAGS_NORMAL = 0x00000080
-NBVTF_TEXTUREFLAGS_NOMIP = 0x00000100
-NBVTF_TEXTUREFLAGS_NOLOD = 0x00000200
+   material_data = (cxr_material*len(obj.material_slots))()
+
+   for i, ms in enumerate(obj.material_slots):
+      inf = material_info(ms.material)
+      material_data[i].res[0] = inf['res'][0]
+      material_data[i].res[1] = inf['res'][1]
+      material_data[i].name = inf['name'].encode('utf-8')
+   
+   mesh.edges = cast(edge_data, POINTER(cxr_edge))
+   mesh.vertices = cast(vertex_data, POINTER(c_double*3))
+   mesh.loops = cast(loop_data,POINTER(cxr_static_loop))
+   mesh.polys = cast(polygon_data, POINTER(cxr_polygon))
+   mesh.materials = cast(material_data, POINTER(cxr_material))
+   
+   mesh.poly_count =  len(data.polygons)
+   mesh.vertex_count = len(data.vertices)
+   mesh.edge_count =  len(data.edges)
+   mesh.loop_count =  len(data.loops)
+   mesh.material_count = len(obj.material_slots)
+
+   if orig_state != None:
+      bpy.ops.object.mode_set(mode=orig_state)
+
+   return mesh
+
+# Callback ctypes indirection things.. not really sure.
+c_libcxr_log_callback = None
+c_libcxr_line_callback = None
+
+# Public API
+# -------------------------------------------------------------
+libcxr_decompose = extern( "cxr_decompose",
+      [POINTER(cxr_static_mesh), POINTER(c_int32)],
+      c_void_p
+)
+libcxr_free_world = extern( "cxr_free_world",
+      [c_void_p],
+      None
+)
+libcxr_write_test_data = extern( "cxr_write_test_data",
+      [POINTER(cxr_static_mesh)],
+      None
+)
+libcxr_world_preview = extern( "cxr_world_preview",
+      [c_void_p],
+      POINTER(cxr_tri_mesh)
+)
+libcxr_free_tri_mesh = extern( "cxr_free_tri_mesh",
+      [c_void_p],
+      None
+)
+libcxr_begin_vmf = extern( "cxr_begin_vmf",
+      [POINTER(cxr_vmf_context), c_void_p],
+      None
+)
+libcxr_vmf_begin_entities = extern( "cxr_vmf_begin_entities",
+      [POINTER(cxr_vmf_context), c_void_p],
+      None
+)
+libcxr_push_world_vmf = extern("cxr_push_world_vmf",
+      [c_void_p,POINTER(cxr_vmf_context),c_void_p],
+      None
+)
+libcxr_end_vmf = extern( "cxr_end_vmf",
+      [POINTER(cxr_vmf_context),c_void_p],
+      None
+)
+
+# VDF + with open wrapper
+libcxr_vdf_open = extern( "cxr_vdf_open", [c_char_p], c_void_p )
+libcxr_vdf_close = extern( "cxr_vdf_close", [c_void_p], None )
+libcxr_vdf_put = extern( "cxr_vdf_put", [c_void_p,c_char_p], None )
+libcxr_vdf_node = extern( "cxr_vdf_node", [c_void_p,c_char_p], None )
+libcxr_vdf_edon = extern( "cxr_vdf_edon", [c_void_p], None )
+libcxr_vdf_kv = extern( "cxr_vdf_kv", [c_void_p,c_char_p,c_char_p], None )
 
-# Wrapper for vdf functions to allow: with o = vdf_structure ...
 class vdf_structure():
    def __init__(_,path):
       _.path = path
@@ -181,7 +628,6 @@ class vdf_structure():
    def __exit__(_,type,value,traceback):
       if _.fp != None:
          libcxr_vdf_close.call(_.fp)
-
    def put(_,s):
       libcxr_vdf_put.call(_.fp, s.encode('utf-8') )
    def node(_,name):
@@ -191,381 +637,170 @@ class vdf_structure():
    def kv(_,k,v):
       libcxr_vdf_kv.call(_.fp, k.encode('utf-8'), v.encode('utf-8'))
 
-class cxr_object_context():
-   def __init__(_,scale,offset_z):
-      _.scale=scale
-      _.offset_z=offset_z
+# Other
+libcxr_lightpatch_bsp = extern( "cxr_lightpatch_bsp", [c_char_p], None )
+
+# Binary file formats and FS
+libcxr_fs_set_gameinfo = extern( "cxr_fs_set_gameinfo", [c_char_p], c_int32 )
+libcxr_fs_exit = extern( "cxr_fs_exit", [], None )
+libcxr_fs_get = extern( "cxr_fs_get", [c_char_p, c_int32], c_void_p )
+libcxr_fs_free = extern( "cxr_fs_free", [c_void_p], None )
+libcxr_fs_find = extern( "cxr_fs_find", [c_char_p, POINTER(fs_locator)],\
+      c_int32 )
+
+libcxr_valve_load_model = extern( "valve_load_model", [c_char_p], \
+      POINTER(valve_model) )
+libcxr_valve_free_model = extern( "valve_free_model", [POINTER(valve_model)],\
+      None )
 
-debug_gpu_lines = None
-debug_gpu_mesh = None
-debug_gpu_shader = gpu.shader.from_builtin('3D_SMOOTH_COLOR')
-debug_draw_handler = None
+libcxr_valve_load_material = extern( "valve_load_material", [c_char_p], \
+      POINTER(valve_material) )
+libcxr_valve_free_material = extern( "valve_free_material", \
+      [POINTER(valve_material)], None )
 
+libcxr_funcs = [ libcxr_decompose, libcxr_free_world, libcxr_begin_vmf, \
+                 libcxr_vmf_begin_entities, libcxr_push_world_vmf, \
+                 libcxr_end_vmf, libcxr_vdf_open, libcxr_vdf_close, \
+                 libcxr_vdf_put, libcxr_vdf_node, libcxr_vdf_edon, \
+                 libcxr_vdf_kv, libcxr_lightpatch_bsp, libcxr_write_test_data,\
+                 libcxr_world_preview, libcxr_free_tri_mesh, \
+                 libcxr_fs_set_gameinfo, libcxr_fs_exit, libcxr_fs_get, \
+                 libcxr_fs_find, libcxr_fs_free, \
+                 libcxr_valve_load_model, libcxr_valve_free_model,\
+                 libcxr_valve_load_material, libcxr_valve_free_material ]
 
+# Callbacks
 def libcxr_log_callback(logStr):
    print( F"{logStr.decode('utf-8')}",end='' )
 
-debug_lines_positions = None
-debug_lines_colours = None
-
-def libcxr_reset_debug_lines():
-   global debug_lines_positions
-   global debug_lines_colours
+cxr_line_positions = None
+cxr_line_colours = None
 
-   debug_lines_positions = []
-   debug_lines_colours = []
+def cxr_reset_lines():
+   global cxr_line_positions, cxr_line_colours
 
-def libcxr_batch_debug_lines():
-   global debug_lines_positions
-   global debug_lines_colours
-   global debug_gpu_lines
-   global debug_gpu_shader
+   cxr_line_positions = []
+   cxr_line_colours = []
 
-   debug_gpu_lines = batch_for_shader(\
-      debug_gpu_shader, 'LINES',\
-      { "pos": debug_lines_positions, "color": debug_lines_colours })
+def cxr_batch_lines():
+   global cxr_line_positions, cxr_line_colours, cxr_view_shader, cxr_view_lines
 
-@persistent
-def cxr_on_load(dummy):
-   libcxr_reset_debug_lines()
-   libcxr_batch_debug_lines()
+   cxr_view_lines = batch_for_shader(\
+      cxr_view_shader, 'LINES',\
+      { "pos": cxr_line_positions, "color": cxr_line_colours })
 
-@persistent
-def cxr_dgraph_update(scene,dgraph):
-   return
-   print( F"Hallo {time.time()}" )
+def libcxr_line_callback( p0,p1,colour ):
+   global cxr_line_colours, cxr_line_positions
 
-def libcxr_line_callback(p0,p1,colour):
-   global debug_lines_positions
-   global debug_lines_colours
-   debug_lines_positions += [(p0[0],p0[1],p0[2])]
-   debug_lines_positions += [(p1[0],p1[1],p1[2])]
-   debug_lines_colours += [(colour[0],colour[1],colour[2],colour[3])]
-   debug_lines_colours += [(colour[0],colour[1],colour[2],colour[3])]
+   cxr_line_positions += [(p0[0],p0[1],p0[2])]
+   cxr_line_positions += [(p1[0],p1[1],p1[2])]
+   cxr_line_colours += [(colour[0],colour[1],colour[2],colour[3])]
+   cxr_line_colours += [(colour[0],colour[1],colour[2],colour[3])]
 
-def cxr_draw():
-   global debug_gpu_lines
-   global debug_gpu_shader
-   global debug_gpu_mesh
+def cxr_reset_all():
+   global cxr_jobs_inf, cxr_jobs_batch, cxr_error_inf, cxr_view_mesh, \
+          cxr_asset_lib
+   cxr_jobs_inf = None
+   cxr_jobs_batch = None
+   cxr_error_inf = None
 
-   debug_gpu_shader.bind()
+   cxr_reset_lines()
+   cxr_batch_lines()
+   cxr_view_mesh = None
 
-   gpu.state.depth_test_set('LESS_EQUAL')
-   gpu.state.depth_mask_set(False)
-   gpu.state.line_width_set(1.5)
-   gpu.state.face_culling_set('BACK')
+   cxr_asset_lib['models'] = {}
+   cxr_asset_lib['materials'] = {}
+   cxr_asset_lib['textures'] = {}
+   
+   scene_redraw()
 
-   gpu.state.blend_set('ALPHA')
-   if debug_gpu_lines != None:
-      debug_gpu_lines.draw(debug_gpu_shader)
+# libnbvtf
+# ------------------------------------------------------------------------------
 
-   gpu.state.blend_set('ADDITIVE')
-   if debug_gpu_mesh != None:
-      debug_gpu_mesh.draw(debug_gpu_shader)
+libnbvtf = None
 
-class CXR_RELOAD(bpy.types.Operator):
-   bl_idname="convexer.reload"
-   bl_label="Reload convexer"
+# Constants
+NBVTF_IMAGE_FORMAT_ABGR8888 = 1
+NBVTF_IMAGE_FORMAT_BGR888 = 3
+NBVTF_IMAGE_FORMAT_DXT1 = 13
+NBVTF_IMAGE_FORMAT_DXT5 = 15
+NBVTF_TEXTUREFLAGS_CLAMPS = 0x00000004
+NBVTF_TEXTUREFLAGS_CLAMPT = 0x00000008
+NBVTF_TEXTUREFLAGS_NORMAL = 0x00000080
+NBVTF_TEXTUREFLAGS_NOMIP = 0x00000100
+NBVTF_TEXTUREFLAGS_NOLOD = 0x00000200
 
-   def execute(_,context):
-      global libcxr, libnbvtf, libcxr_funcs, libnbvtf_funcs
+libnbvtf_convert = extern( "nbvtf_convert", \
+      [c_char_p,c_int32,c_int32,c_int32,c_int32,c_int32,c_uint32,c_char_p], \
+      c_int32 )
 
-      # Load vtf library
-      libnbvtf = cdll.LoadLibrary( os.path.dirname(__file__)+'/libnbvtf.so')
+libnbvtf_read = extern( "nbvtf_read", \
+      [c_void_p,POINTER(c_int32),POINTER(c_int32), c_int32], \
+      POINTER(c_uint8) )
 
-      if libcxr != None:
-         _handle = libcxr._handle
-         
-         for i in range(10): libc_dlclose( _handle )
-         del libcxr
+libnbvtf_free = extern( "nbvtf_free", [POINTER(c_uint8)], None )
 
-         libcxr = None
-      libcxr = cdll.LoadLibrary( os.path.dirname(__file__)+'/libcxr.so')
-      
-      build_time = c_char_p.in_dll(libcxr,'cxr_build_time')
-      print( F"libcxr build time: {build_time.value}" )
-   
-      for fd in libnbvtf_funcs:
-         fd.loadfrom( libnbvtf )
+libnbvtf_init = extern( "nbvtf_init", [], None )
+libnbvtf_funcs = [ libnbvtf_convert, libnbvtf_init, libnbvtf_read, \
+                   libnbvtf_free ]
 
-      for fd in libcxr_funcs:
-         fd.loadfrom( libcxr )
+# Loading
+# --------------------------
 
-      # Callbacks
-      global c_libcxr_log_callback, c_libcxr_line_callback
+def shared_reload():
+   global libcxr, libnbvtf, libcxr_funcs, libnbvtf_funcs
 
-      LOG_FUNCTION_TYPE = CFUNCTYPE(None,c_char_p)
-      c_libcxr_log_callback = LOG_FUNCTION_TYPE(libcxr_log_callback)
-      libcxr.cxr_set_log_function(cast(c_libcxr_log_callback,c_void_p))
+   # Unload libraries if existing
+   def _reload( lib, path ):
+      if CXR_GNU_LINUX==1:
+         if lib != None:
+            _handle = lib._handle
+            for i in range(10): libc_dlclose( _handle )
+            lib = None
+            del lib
 
-      LINE_FUNCTION_TYPE = CFUNCTYPE(None,\
-         POINTER(c_double),POINTER(c_double),POINTER(c_double))
-      c_libcxr_line_callback = LINE_FUNCTION_TYPE(libcxr_line_callback)
-      libcxr.cxr_set_line_function(cast(c_libcxr_line_callback,c_void_p))
+      libpath = F'{os.path.dirname(__file__)}/{path}{CXR_SHARED_EXT}'
+      return cdll.LoadLibrary( libpath )
 
-      return {'FINISHED'}
+   libnbvtf = _reload( libnbvtf, "libnbvtf" )
+   libcxr = _reload( libcxr, "libcxr" )
+   
+   for fd in libnbvtf_funcs:
+      fd.loadfrom( libnbvtf )
+   libnbvtf_init.call()
 
-def libcxr_use():
-   global libcxr
+   for fd in libcxr_funcs:
+      fd.loadfrom( libcxr )
 
-   if libcxr == None:
-      bpy.ops.convexer.reload()
+   # Callbacks
+   global c_libcxr_log_callback, c_libcxr_line_callback
 
-def to_aeiou( v ):
-   ret = ""
-   if v == 0:
-      return "z"
-   dig = []
-   while v:
-      dig.append( int( v % 5 ) )
-      v //= 5
-   for d in dig[::-1]:
-      ret += [ 'a','e','i','o','u' ][d]
-   return ret
+   LOG_FUNCTION_TYPE = CFUNCTYPE(None,c_char_p)
+   c_libcxr_log_callback = LOG_FUNCTION_TYPE(libcxr_log_callback)
 
-def asset_uid(asset):
-   if isinstance(asset,str):
-      return asset
-   name = to_aeiou(asset.cxr_data.asset_id)
-   if bpy.context.scene.cxr_data.include_names:
-      name += asset.name.replace('.','_')
-   return name
+   LINE_FUNCTION_TYPE = CFUNCTYPE(None,\
+      POINTER(c_double), POINTER(c_double), POINTER(c_double))
+   c_libcxr_line_callback = LINE_FUNCTION_TYPE(libcxr_line_callback)
 
-# -> <project_name>/<asset_name>
-def asset_name(asset):
-   return F"{bpy.context.scene.cxr_data.project_name}/{asset_uid(asset)}"
+   libcxr.cxr_set_log_function(cast(c_libcxr_log_callback,c_void_p))
+   libcxr.cxr_set_line_function(cast(c_libcxr_line_callback,c_void_p))
 
-# -> <subdir>/<project_name>/<asset_name>
-def asset_path(subdir, asset):
-   return F"{subdir}/{asset_name(asset_uid(asset))}"
+   build_time = c_char_p.in_dll(libcxr,'cxr_build_time')
+   print( F"libcxr build time: {build_time.value}" )
 
-# -> <csgo>/<subdir>/<project_name>/<asset_name>
-def asset_full_path(sdir,asset):
-   return F"{bpy.context.scene.cxr_data.subdir}/"+\
-      F"{asset_path(sdir,asset_uid(asset))}"
+shared_reload()
 
-# view_layer.update() doesnt seem to work,
-# tag_redraw() seems to have broken
-#  therefore, change a property
-def scene_redraw():
-   ob = bpy.context.scene.objects[0]
-   ob.hide_render = ob.hide_render
-  
-   # the 'real' way to refresh the scene
-   #for area in bpy.context.window.screen.areas:
-   #   if area.type == 'view_3d':
-   #      area.tag_redraw()
+# Configuration
+# ------------------------------------------------------------------------------
 
-# The default shader is the first entry
+# Standard entity functions, think of like base.fgd
 #
-cxr_shaders = {
-   "LightMappedGeneric":
-   { 
-      "name": "Light Mapped",
-      "id": 0
-   },
-   "VertexLitGeneric":
-   { 
-      "name": "Vertex Lit",
-      "id": 1
-   },
-   "UnlitGeneric":
-   { 
-      "name": "Unlit",
-      "id": 2
-   },
-   "Builtin":
-   {
-      "name": "Builtin",
-      "id": 3
-   }
-}
-
-def material_tex_image(v):
-   return {\
-      "ShaderNodeTexImage":
-      {
-         "image": F"${v}"
-      }
-   }
-
-cxr_graph_mapping = {
-   "ShaderNodeBsdfPrincipled":
-   {
-      "Base Color":
-      {
-         "ShaderNodeMixRGB":
-         {
-            "Color1": material_tex_image("basetexture"),
-            "Color2": material_tex_image("decaltexture")
-         },
-         "ShaderNodeTexImage":
-         {
-            "image":"$basetexture"
-         },
-         "default":
-            [("VertexLitGeneric","$color2"),\
-             ("UnlitGeneric","$color2"),\
-             ("LightMappedGeneric","$color")]
-      },
-      "Normal":
-      {
-         "ShaderNodeNormalMap":
-         {
-            "Color": material_tex_image("bumpmap")
-         }
-      }
-   }
-}
-
-cxr_shader_params = {
-   "Textures":
-   {
-      "type": "ui",
-      "shaders": ("UnlitGeneric","VertexLitGeneric","LightMappedGeneric"),
-
-      "$basetexture":
-      {
-         "name": "Base Texture",
-         "type": "intrinsic",
-         "default": None
-      },
-      "$decaltexture":
-      {
-         "name": "Decal Texture",
-         "type": "intrinsic",
-         "default": None,
-
-         "$decalblendmode":
-         {
-            "name": "Blend Mode",
-            "type": "enum",
-            "items": [
-               ('0',"AlphaOver","Default",'',0),
-               ('1',"Multiply","",'',1),
-               ('2',"Modulate","",'',2),
-               ('3',"Additive","",'',3)
-            ],
-            "default": 0,
-            "always": True
-         }
-      },
-      "$bumpmap":
-      {
-         "name": "Normal Map",
-         "type": "intrinsic",
-         "flags": NBVTF_TEXTUREFLAGS_NORMAL,
-         "default": None
-      }
-   },
-   "$color":
-   {
-      "name": "Color",
-      "type": "intrinsic",
-      "default": None,
-      "exponent": 2.2
-   },
-   "$color2":
-   {
-      "name": "Color2",
-      "type": "intrinsic",
-      "default": None,
-      "exponent": 2.2
-   },
-   "Lighting":
-   {
-      "type": "ui",
-      "shaders": ("VertexLitGeneric", "LightMappedGeneric"),
-
-      "$phong":
-      {
-         "name": "Phong",
-         "type": "bool",
-         "default": False,
-
-         "$phongexponent":
-         {
-            "name": "Exponent",
-            "type": "float",
-            "default": 5.0
-         },
-         "$phongboost":
-         {
-            "name": "Boost",
-            "type": "float",
-            "default": 1.0
-         },
-         "$phongfresnelranges":
-         {
-            "name": "Fresnel Ranges",
-            "type": "vector",
-            "default":(1.0,1.0,1.0)
-         }
-      },
-      "$envmap":
-      {
-         "name": "Cubemap",
-         "type": "string",
-         "default": "",
-
-         "$envmaptint":
-         {
-            "name": "Tint",
-            "type": "vector",
-            "subtype": 'COLOR',
-            "default": (1.0,1.0,1.0)
-         },
-         "$envmaplightscale":
-         {
-            "name": "Light Scale",
-            "type": "float",
-            "default": 0.0
-         },
-         "$envmaplightscaleminmax":
-         {
-            "name": "Min/Max",
-            "type": "vector",
-            "default": (0.0,1.0)
-         }
-      }
-   },
-   "Transparency":
-   {
-      "type": "ui",
-      "shaders": ("UnlitGeneric","VertexLitGeneric","LightMappedGeneric"),
-
-      "$translucent":
-      {
-         "name": "Translucent",
-         "type": "bool",
-         "default": False
-      },
-      "$alphatest":
-      {
-         "name": "Alpha Test",
-         "type": "bool",
-         "default": False,
-
-         "$alphatestreference":
-         {
-            "name": "Step",
-            "type": "float",
-            "default": 0.5
-         }
-      },
-      "$nocull":
-      {
-         "name": "No Cull",
-         "type": "bool",
-         "default": False
-      }
-   }
-}
-
-def ent_get_origin(obj,context):
-   return obj.location * context.scale
+def cxr_get_origin(context):
+   return context['object'].location * context['transform']['scale'] + \
+         mathutils.Vector(context['transform']['offset'])
 
-def ent_get_angles(obj,context):
+def cxr_get_angles(context):
+   obj = context['object']
    euler = [ a*57.295779513 for a in obj.rotation_euler ]
    angle = [0,0,0]
    angle[0] = euler[1]
@@ -573,40 +808,54 @@ def ent_get_angles(obj,context):
    angle[2] = euler[0]
    return angle
 
-def ent_baseclass(classes, other):
+def cxr_baseclass(classes, other):
    base = other.copy()
    for x in classes:
       base.update(x.copy())
    return base
 
-ent_origin = { "origin": ent_get_origin }
-ent_angles = { "angles": ent_get_angles }
-ent_transform = ent_baseclass( [ent_origin], ent_angles )
+def ent_soundscape(context):
+   obj = context['object']
+   kvs = cxr_baseclass([ent_origin],\
+   {
+      "radius": obj.scale.x * bpy.context.scene.cxr_data.scale_factor,
+      "soundscape": {"type":"string","default":""}
+   })
+
+   return kvs
 
-def ent_lights(obj,context):
-   kvs = ent_baseclass([ent_origin],\
+# EEVEE Light component converter -> Source 1
+#
+def ent_lights(context):
+   obj = context['object']
+   kvs = cxr_baseclass([ent_origin],\
    {
       "_distance": (0.0 if obj.data.cxr_data.realtime else -1.0),
-      "_light": [int(pow(obj.data.color[i],1.0/2.2)*255.0) for i in range(3)] +\
-                [int(obj.data.energy * bpy.context.scene.cxr_data.light_scale)],
       "_lightHDR": '-1 -1 -1 1',
       "_lightscaleHDR": 1
    })
-
-   if obj.data.type == 'SPOT':
-      kvs['_cone'] = obj.data.spot_size*(57.295779513/2.0)
-      kvs['_inner_cone'] = (1.0-obj.data.spot_blend)*kvs['_cone']
       
-      # Blenders spotlights are -z forward
+   light_base = [(pow(obj.data.color[i],1.0/2.2)*255.0) for i in range(3)] +\
+                [obj.data.energy * bpy.context.scene.cxr_data.light_scale]
+
+   if obj.data.type == 'SPOT' or obj.data.type == 'SUN':
+      # Blenders directional lights are -z forward
       # Source is +x, however, it seems to use a completely different system.
       # Since we dont care about roll for spotlights, we just take the
       # pitch and yaw via trig
 
       _,mtx_rot,_ = obj.matrix_world.decompose()
       fwd = mtx_rot @ mathutils.Vector((0,0,-1))
+      dir_pitch = math.asin(fwd[2]) * 57.295779513
+      dir_yaw = math.atan2(fwd[1],fwd[0]) * 57.295779513
+
+   if obj.data.type == 'SPOT':
+      kvs['_light'] = [ int(x) for x in light_base ]
+      kvs['_cone'] = obj.data.spot_size*(57.295779513/2.0)
+      kvs['_inner_cone'] = (1.0-obj.data.spot_blend)*kvs['_cone']
       
-      kvs['pitch'] = math.asin(fwd[2]) * 57.295779513
-      kvs['angles'] = [ 0.0, math.atan2(fwd[1],fwd[0]) * 57.295779513, 0.0 ]
+      kvs['pitch'] = dir_pitch
+      kvs['angles'] = [ 0, dir_yaw, 0 ]
       kvs['_quadratic_attn'] = 0.0  # Source spotlights + quadratic falloff look
                                     # Really bad...
                                     #
@@ -615,61 +864,239 @@ def ent_lights(obj,context):
       kvs['_linear_attn'] = 1.0
    
    elif obj.data.type == 'POINT':
+      kvs['_light'] = [ int(x) for x in light_base]
       kvs['_quadratic_attn'] = 1.0
-      kvs['_linear_attn'] = 0.0
+      kvs['_linear_attn'] = 1.0
    
    elif obj.data.type == 'SUN':
-      pass # TODO
+      light_base[3] *= 300.0 * 5
+      kvs['_light'] = [ int(x) for x in light_base ]
+
+      ambient = bpy.context.scene.world.color
+      kvs['_ambient'] = [int(pow(ambient[i],1.0/2.2)*255.0) for i in range(3)] +\
+                        [80 * 5]
+      kvs['_ambientHDR'] = [-1,-1,-1,1]
+      kvs['_AmbientScaleHDR'] = 1
+      kvs['pitch'] = dir_pitch
+      kvs['angles'] = [ dir_pitch, dir_yaw, 0.0 ]
+      kvs['SunSpreadAngle'] = 0
 
    return kvs
 
-def ent_cubemap(obj,context):
-   return ent_baseclass([ent_origin],\
-      {"cubemapsize": obj.data.cxr_data.size})
+def ent_prop(context):
+   if isinstance( context['object'], bpy.types.Collection ):
+      kvs = {}
+      target = context['object']
+      pos = mathutils.Vector(context['origin'])
+      pos += mathutils.Vector(context['transform']['offset'])
+
+      kvs['origin'] = [pos[1],-pos[0],pos[2]]
+      kvs['angles'] = [0,180,0]
+      kvs['uniformscale'] = 1.0
+   else:
+      kvs = cxr_baseclass([ent_origin],{})
+      target = context['object'].instance_collection
+
+      obj = context['object']
+      euler = [ a*57.295779513 for a in obj.rotation_euler ]
+      angle = [0,0,0]
+      angle[0] = euler[1]
+      angle[1] = euler[2] + 180.0 # Dunno...
+      angle[2] = euler[0]
+      
+      kvs['angles'] = angle
+      kvs['uniformscale'] = obj.scale[0]
+   
+   if target.cxr_data.shadow_caster:
+      kvs['enablelightbounce'] = 1
+      kvs['disableshadows'] = 0
+   else:
+      kvs['enablelightbounce'] = 0
+      kvs['disableshadows'] = 1
+
+   kvs['fademindist'] = -1
+   kvs['fadescale'] = 1
+   kvs['model'] = F"{asset_path('models',target)}.mdl".lower()
+   kvs['renderamt'] = 255
+   kvs['rendercolor'] = [255, 255, 255]
+   kvs['skin'] = 0
+   kvs['solid'] = 6
 
-cxr_entities = {
-   "info_player_counterterrorist":
-   {
-      "gizmo": [],
-      "allow": ('EMPTY',),
-      "keyvalues": ent_baseclass([ent_transform],\
-      {
-         "priority": {"type": "int", "default": 0 },
-         "enabled": {"type": "int", "default": 1 },
-      })
-   },
-   "info_player_terrorist":
-   {
-      "gizmo": [],
-      "allow": ('EMPTY',),
-      "keyvalues": ent_baseclass([ent_transform],\
-      {
-         "priority": {"type": "int", "default": 0 },
-         "enabled": {"type": "int", "default": 1 },
-      })
-   },
-   "light": { "keyvalues": ent_lights },
-   "light_spot": { "keyvalues": ent_lights },
-   # SUN
-   "env_cubemap": { "keyvalues": ent_cubemap },
-
-   # Brush entites
-   "func_buyzone": 
-   {
-      "allow": ('MESH',),
-      "keyvalues": 
-      {
-         "TeamNum": {"type": "int", "default": 0 }
-      }
+   return kvs
+
+def ent_sky_camera(context):
+   settings = bpy.context.scene.cxr_data
+   scale = settings.scale_factor / settings.skybox_scale_factor
+
+   kvs = {
+      "origin": [_ for _ in context['transform']['offset']],
+      "angles": [ 0, 0, 0 ],
+      "fogcolor": [255, 255, 255],
+      "fogcolor2": [255, 255, 255],
+      "fogdir": [1,0,0],
+      "fogend": 2000.0,
+      "fogmaxdensity": 1,
+      "fogstart": 500.0,
+      "HDRColorScale": 1.0,
+      "scale": scale
    }
-}
+   return kvs
+
+def ent_cubemap(context):
+   obj = context['object']
+   return cxr_baseclass([ent_origin], {"cubemapsize": obj.data.cxr_data.size})
+
+ent_origin = { "origin": cxr_get_origin }
+ent_angles = { "angles": cxr_get_angles }
+ent_transform = cxr_baseclass( [ent_origin], ent_angles )
+
+#include the user config
+exec(open(F'{os.path.dirname(__file__)}/config.py').read())
+
+# Blender state callbacks
+# ------------------------------------------------------------------------------
+
+@persistent
+def cxr_on_load(dummy):
+   global cxr_view_lines, cxr_view_mesh
+
+   cxr_view_lines = None
+   cxr_view_mesh = None
+
+@persistent
+def cxr_dgraph_update(scene,dgraph):
+   return
+   print( F"Hallo {time.time()}" )
+
+# Convexer compilation functions
+# ------------------------------------------------------------------------------
+
+# Asset path management
+
+def asset_uid(asset):
+   if isinstance(asset,str):
+      return asset
+
+   # Create a unique ID string
+   base = "bopshei"
+   v = asset.cxr_data.asset_id
+   name = ""
+
+   if v == 0:
+      name = "a"
+   else:
+      dig = []
+      
+      while v:
+         dig.append( int( v % len(base) ) )
+         v //= len(base)
+
+      for d in dig[::-1]:
+         name += base[d]
+
+   if bpy.context.scene.cxr_data.include_names:
+      name += asset.name.replace('.','_')
+
+   return name
+
+# -> <project_name>/<asset_name>
+def asset_name(asset):
+   return F"{bpy.context.scene.cxr_data.project_name}/{asset_uid(asset)}"
+
+# -> <subdir>/<project_name>/<asset_name>
+def asset_path(subdir, asset):
+   return F"{subdir}/{asset_name(asset_uid(asset))}"
+
+# -> <csgo>/<subdir>/<project_name>/<asset_name>
+def asset_full_path(sdir,asset):
+   return F"{bpy.context.scene.cxr_data.subdir}/"+\
+      F"{asset_path(sdir,asset_uid(asset))}"
+
+# Decomposes mesh, and sets global error information if failed. 
+# - returns None on fail
+# - returns world on success
+def cxr_decompose_globalerr( mesh_src ):
+   global cxr_error_inf
+
+   err = c_int32(0)
+   world = libcxr_decompose.call( mesh_src, pointer(err) )
+
+   if not world:
+      cxr_view_mesh = None
+      cxr_batch_lines()
+      
+      cxr_error_inf = [\
+          ("No Error", "There is no error?"),\
+          ("Bad input", "Non manifold geometry is present in the input mesh"),\
+          ("Bad result","An invalid manifold was generated, try to simplify"),\
+          ("Bad result","Make sure there is a clear starting point"),\
+          ("Bad result","Implicit vertex was invalid, try to simplify"),\
+          ("Bad input","Non coplanar vertices are in the source mesh"),\
+          ("Bad input","Non convex polygon is in the source mesh"),\
+          ("Bad result","Undefined failure"),\
+          ("Invalid Input", "Undefined failure"),\
+         ][err.value]
+
+      scene_redraw()
+
+   return world
+
+# Entity functions / infos
+# ------------------------
+
+def cxr_collection_purpose(collection):
+   if collection.name.startswith('.'): return None
+   if collection.hide_render: return None
+   if collection.name.startswith('mdl_'): return 'model'
+   return 'group'
+
+def cxr_object_purpose(obj):
+   objpurpose = None
+   group = None
+
+   def _search(collection):
+      nonlocal objpurpose, group, obj
+
+      purpose = cxr_collection_purpose( collection )
+      if purpose == None: return
+      if purpose == 'model':
+         for o in collection.objects:
+            if o == obj:
+               if o.type != 'EMPTY':
+                  objpurpose = 'model'
+                  group = collection
+               return
+         return
+      for o in collection.objects:
+         if o == obj:
+            classname = cxr_classname(o)
+            if classname != None: 
+               objpurpose = 'entity'
+               if o.type == 'MESH':
+                  objpurpose = 'brush_entity'
+               group = collection
+            else: 
+               if o.type == 'MESH':
+                  objpurpose = 'brush'
+                  group = collection
+            return
+      for c in collection.children:
+         _search(c)
+
+   if 'main' in bpy.data.collections:
+      _search( bpy.data.collections['main'] )
+
+   if objpurpose == None and 'skybox' in bpy.data.collections:
+      _search( bpy.data.collections['skybox'] )
+
+   return (group,objpurpose)
 
 def cxr_intrinsic_classname(obj):
    if obj.type == 'LIGHT':
       return {
          'SPOT': "light_spot",
          'POINT': "light",
-         'SUN': "light_directional" }[ obj.data.type ]
+         'SUN': "light_environment" }[ obj.data.type ]
    
    elif obj.type == 'LIGHT_PROBE':
       return "env_cubemap"
@@ -701,7 +1128,9 @@ def cxr_classname(obj):
 #
 # Error: None
 #
-def cxr_entity_keyvalues(obj,context,classname):
+def cxr_entity_keyvalues(context):
+   classname = context['classname']
+   obj = context['object']
    if classname not in cxr_entities: return None
 
    result = []
@@ -709,7 +1138,7 @@ def cxr_entity_keyvalues(obj,context,classname):
    entdef = cxr_entities[classname]
    kvs = entdef['keyvalues']
 
-   if callable(kvs): kvs = kvs(obj, context)
+   if callable(kvs): kvs = kvs(context)
 
    for k in kvs:
       kv = kvs[k]
@@ -721,7 +1150,7 @@ def cxr_entity_keyvalues(obj,context,classname):
          value = obj[ F"cxrkv_{k}" ]
       else:
          if callable(kv):
-            value = kv(obj,context)
+            value = kv(context)
 
       if isinstance(value,mathutils.Vector):
          value = [_ for _ in value]
@@ -730,6 +1159,8 @@ def cxr_entity_keyvalues(obj,context,classname):
 
    return result
 
+# Extract material information from shader graph data
+#
 def material_info(mat):
    info = {}
    info['res'] = (512,512)
@@ -743,11 +1174,7 @@ def material_info(mat):
       info['name'] = mat.name
       return info
 
-   if not hasattr(material_info,'references'):
-      material_info.references = set()
-
-   # Custom material
-   material_info.references.add(mat)
+   # Custom materials
    info['name'] = asset_name(mat)
    
    # Using the cxr_graph_mapping as a reference, go through the shader
@@ -760,12 +1187,13 @@ def material_info(mat):
       def _variant_apply( val ):
          nonlocal mat
          
-         if isinstance( val, str ):
-            return val
-         else:
+         if isinstance( val, list ):
             for shader_variant in val:
                if shader_variant[0] == mat.cxr_data.shader:
                   return shader_variant[1]
+            return val[0][1]
+         else:
+            return val
       
       # Find rootnodes
       if node == None:
@@ -773,36 +1201,38 @@ def material_info(mat):
 
          for node_idname in node_def:
             for n in mat.node_tree.nodes:
-               if n.bl_idname == node_idname:
+               if n.name == node_idname:
                   node_def = node_def[node_idname]
                   node = n
                   break
 
       for link in node_def:
-         if isinstance( node_def[link], dict ):
-            inputt = node.inputs[link]
-            inputt_def = node_def[link]
+         link_def = _variant_apply( node_def[link] )
 
-            if inputt.is_linked:
+         if isinstance( link_def, dict ):
+            node_link = node.inputs[link]
+
+            if node_link.is_linked:
 
                # look for definitions for the connected node type
-               con = inputt.links[0].from_node
+               from_node = node_link.links[0].from_node
                
-               for node_idname in inputt_def:
-                  if con.bl_idname == node_idname:
-                     con_def = inputt_def[ node_idname ]
-                     _graph_read( con_def, con, depth+1 )
+               node_name = from_node.name.split('.')[0]
+               if node_name in link_def:
+                  from_node_def = link_def[ node_name ]
+
+                  _graph_read( from_node_def, from_node, depth+1 )
                
-               # No definition found! :(
+               # No definition! :(
                #  TODO: Make a warning for this?
 
             else:
-               if "default" in inputt_def:
-                  prop = _variant_apply( inputt_def['default'] )
-                  info[prop] = inputt.default_value
+               if "default" in link_def:
+                  prop = _variant_apply( link_def['default'] )
+                  info[prop] = node_link.default_value
          else:
-            prop = _variant_apply( node_def[link] )
-            info[prop] = getattr(node,link)
+            prop = _variant_apply( link_def )
+            info[prop] = getattr( node, link )
 
    _graph_read(cxr_graph_mapping)
    
@@ -812,306 +1242,1266 @@ def material_info(mat):
 
    return info
 
-def mesh_cxr_format(obj):
-   dgraph = bpy.context.evaluated_depsgraph_get()
-   data = obj.evaluated_get(dgraph).data
+def vec3_min( a, b ):
+   return mathutils.Vector((min(a[0],b[0]),min(a[1],b[1]),min(a[2],b[2])))
+def vec3_max( a, b ):
+   return mathutils.Vector((max(a[0],b[0]),max(a[1],b[1]),max(a[2],b[2])))
+
+def cxr_collection_center(collection, transform):
+   BIG=999999999
+   bounds_min = mathutils.Vector((BIG,BIG,BIG))
+   bounds_max = mathutils.Vector((-BIG,-BIG,-BIG))
    
-   _,mtx_rot,_ = obj.matrix_world.decompose()
+   for obj in collection.objects:
+      if obj.type == 'MESH':
+         corners = [ mathutils.Vector(c) for c in obj.bound_box ]
 
-   mesh = cxr_static_mesh()
+         for corner in [ obj.matrix_world@c for c in corners ]:
+            bounds_min = vec3_min( bounds_min, corner )
+            bounds_max = vec3_max( bounds_max, corner )
 
-   vertex_data = ((c_double*3)*len(data.vertices))()
-   for i, vert in enumerate(data.vertices):
-      v = obj.matrix_world @ vert.co
-      vertex_data[i][0] = c_double(v[0])
-      vertex_data[i][1] = c_double(v[1])
-      vertex_data[i][2] = c_double(v[2])
+   center = (bounds_min + bounds_max) / 2.0
+         
+   origin = mathutils.Vector((-center[1],center[0],center[2]))
+   origin *= transform['scale']
 
-   loop_data = (cxr_static_loop*len(data.loops))()
-   polygon_data = (cxr_polygon*len(data.polygons))()
+   return origin
 
-   for i, poly in enumerate(data.polygons):
-      loop_start = poly.loop_start
-      loop_end = poly.loop_start + poly.loop_total
-      for loop_index in range(loop_start, loop_end):
-         loop = data.loops[loop_index]
-         loop_data[loop_index].index = loop.vertex_index
-         loop_data[loop_index].edge_index = loop.edge_index
+# Prepares Scene into dictionary format
+#
+def cxr_scene_collect():
+   context = bpy.context
+
+   # Make sure all of our asset types have a unique ID
+   def _uid_prepare(objtype):
+      used_ids = [0]
+      to_generate = []
+      id_max = 0
+      for o in objtype:
+         vs = o.cxr_data
+         if vs.asset_id in used_ids:
+            to_generate+=[vs]
+         else:
+            id_max = max(id_max,vs.asset_id)
+            used_ids+=[vs.asset_id]
+      for vs in to_generate:
+         id_max += 1
+         vs.asset_id = id_max
+   _uid_prepare(bpy.data.materials)
+   _uid_prepare(bpy.data.images)
+   _uid_prepare(bpy.data.collections)
+
+   sceneinfo = {
+      "entities": [], # Everything with a classname
+      "geo": [],      # All meshes without a classname
+      "heros": []     # Collections prefixed with mdl_
+   }
 
-         if data.uv_layers:
-             uv = data.uv_layers.active.data[loop_index].uv
-             loop_data[loop_index].uv[0] = c_double(uv[0])
-             loop_data[loop_index].uv[1] = c_double(uv[1])
+   def _collect(collection,transform):
+      nonlocal sceneinfo
+      
+      purpose = cxr_collection_purpose( collection )
+      if purpose == None: return
+      if purpose == 'model':
+         sceneinfo['entities'] += [{
+            "object": collection,
+            "classname": "prop_static",
+            "transform": transform,
+            "origin": cxr_collection_center( collection, transform )
+         }]
+
+         sceneinfo['heros'] += [{
+            "collection": collection,
+            "transform": transform,
+            "origin": cxr_collection_center( collection, transform )
+         }]
+         return
+
+      for obj in collection.objects:
+         if obj.hide_get(): continue
+
+         classname = cxr_classname( obj )
+
+         if classname != None:
+            sceneinfo['entities'] += [{
+               "object": obj,
+               "classname": classname,
+               "transform": transform
+            }]
+         elif obj.type == 'MESH':
+            sceneinfo['geo'] += [{
+               "object": obj,
+               "transform": transform
+            }]
+
+      for c in collection.children:
+         _collect( c, transform )
+
+   transform_main = {
+      "scale": context.scene.cxr_data.scale_factor,
+      "offset": (0,0,0)
+   }
+
+   transform_sky = {
+      "scale": context.scene.cxr_data.skybox_scale_factor,
+      "offset": (0,0,context.scene.cxr_data.skybox_offset )
+   }
+
+   if 'main' in bpy.data.collections:
+      _collect( bpy.data.collections['main'], transform_main )
+
+   if 'skybox' in bpy.data.collections:
+      _collect( bpy.data.collections['skybox'], transform_sky )
+
+      sceneinfo['entities'] += [{
+         "object": None,
+         "transform": transform_sky,
+         "classname": "sky_camera"
+      }]
+
+   return sceneinfo
+
+# Write VMF out to file (JOB HANDLER)
+# 
+def cxr_export_vmf(sceneinfo, output_vmf):
+   cxr_reset_lines()
+
+   with vdf_structure(output_vmf) as m:
+      print( F"Write: {output_vmf}" )
+
+      vmfinfo = cxr_vmf_context()
+      vmfinfo.mapversion = 4
+      
+      #TODO: These need to be in options...
+      vmfinfo.skyname = bpy.context.scene.cxr_data.skyname.encode('utf-8')
+      vmfinfo.detailvbsp = b"detail.vbsp"
+      vmfinfo.detailmaterial = b"detail/detailsprites"
+      vmfinfo.lightmap_scale = 12
+
+      vmfinfo.brush_count = 0
+      vmfinfo.entity_count = 0
+      vmfinfo.face_count = 0
+      
+      visgroups = (cxr_visgroup*len(cxr_visgroups))()
+      for i, vg in enumerate(cxr_visgroups):
+         visgroups[i].name = vg.encode('utf-8')
+      vmfinfo.visgroups = cast(visgroups, POINTER(cxr_visgroup))
+      vmfinfo.visgroup_count = len(cxr_visgroups)
+      
+      libcxr_begin_vmf.call( pointer(vmfinfo), m.fp )
+
+      def _buildsolid( cmd ):
+         nonlocal m
+         
+         print( F"{vmfinfo.brush_count} :: {cmd['object'].name}" )
+
+         baked = mesh_cxr_format( cmd['object'] )
+         world = cxr_decompose_globalerr( baked )
+         
+         if world == None:
+            return False
+
+         vmfinfo.scale = cmd['transform']['scale']
+
+         offset = cmd['transform']['offset']
+         vmfinfo.offset[0] = offset[0]
+         vmfinfo.offset[1] = offset[1]
+         vmfinfo.offset[2] = offset[2]
+
+         if cmd['object'].cxr_data.lightmap_override > 0:
+            vmfinfo.lightmap_scale = cmd['object'].cxr_data.lightmap_override
          else:
-             loop_data[loop_index].uv[0] = c_double(0.0)
-             loop_data[loop_index].uv[1] = c_double(0.0)
-      center = obj.matrix_world @ poly.center
-      normal = mtx_rot @ poly.normal
+            vmfinfo.lightmap_scale = bpy.context.scene.cxr_data.lightmap_scale
 
-      polygon_data[i].loop_start = poly.loop_start
-      polygon_data[i].loop_total = poly.loop_total
-      polygon_data[i].normal[0] = normal[0]
-      polygon_data[i].normal[1] = normal[1]
-      polygon_data[i].normal[2] = normal[2]
-      polygon_data[i].center[0] = center[0]
-      polygon_data[i].center[1] = center[1]
-      polygon_data[i].center[2] = center[2]
-      polygon_data[i].material_id = poly.material_index
+         libcxr_push_world_vmf.call( world, pointer(vmfinfo), m.fp )
+         libcxr_free_world.call( world )
 
-   edge_data = (cxr_edge*len(data.edges))()
+         return True
 
-   for i, edge in enumerate(data.edges):
-      edge_data[i].i0 = edge.vertices[0]
-      edge_data[i].i1 = edge.vertices[1]
-      edge_data[i].freestyle = edge.use_freestyle_mark
+      # World geometry
+      for brush in sceneinfo['geo']:
+         vmfinfo.visgroupid = int(brush['object'].cxr_data.visgroup)
+         if not _buildsolid( brush ):
+            cxr_batch_lines()
+            scene_redraw()
+            return False
+      vmfinfo.visgroupid = 0
 
-   material_data = (cxr_material*len(obj.material_slots))()
+      libcxr_vmf_begin_entities.call(pointer(vmfinfo), m.fp)
+      
+      # Entities
+      for ent in sceneinfo['entities']:
+         obj = ent['object']
+         ctx = ent['transform']
+         cls = ent['classname']
 
-   for i, ms in enumerate(obj.material_slots):
-      inf = material_info(ms.material)
-      material_data[i].res[0] = inf['res'][0]
-      material_data[i].res[1] = inf['res'][1]
-      material_data[i].vmt_path = inf['name'].encode('utf-8')
-   
-   mesh.edges = cast(edge_data, POINTER(cxr_edge))
-   mesh.vertices = cast(vertex_data, POINTER(c_double*3))
-   mesh.loops = cast(loop_data,POINTER(cxr_static_loop))
-   mesh.polys = cast(polygon_data, POINTER(cxr_polygon))
-   mesh.materials = cast(material_data, POINTER(cxr_material))
-   
-   mesh.poly_count =  len(data.polygons)
-   mesh.vertex_count = len(data.vertices)
-   mesh.edge_count =  len(data.edges)
-   mesh.loop_count =  len(data.loops)
-   mesh.material_count = len(obj.material_slots)
+         m.node( 'entity' )
+         m.kv( 'classname', cls )
 
-   return mesh
+         kvs = cxr_entity_keyvalues( ent )
+
+         for kv in kvs:
+            if isinstance(kv[2], list): 
+               m.kv( kv[0], ' '.join([str(_) for _ in kv[2]]) )
+            else: m.kv( kv[0], str(kv[2]) )
+         
+         if obj == None:
+            pass
+         elif not isinstance( obj, bpy.types.Collection ):
+            if obj.type == 'MESH':
+               vmfinfo.visgroupid = int(obj.cxr_data.visgroup)
+               if not _buildsolid( ent ):
+                  cxr_batch_lines()
+                  scene_redraw()
+                  return False
+
+         if obj != None:
+            m.node( 'editor' )
+            m.kv( 'visgroupid', str(obj.cxr_data.visgroup) )
+            m.kv( 'visgroupshown', '1' )
+            m.kv( 'visgroupautoshown', '1' )
+            m.edon()
+
+         m.edon()
+      vmfinfo.visgroupid = 0
+
+   print( "Done" )
+   return True
+
+# COmpile image using NBVTF and hash it (JOB HANDLER)
+# 
+def compile_image(img):
+   if img==None:
+      return None
+
+   name = asset_name(img)
+   src_path = bpy.path.abspath(img.filepath)
+
+   dims = img.cxr_data.export_res
+   fmt = { 
+      'RGBA': NBVTF_IMAGE_FORMAT_ABGR8888,
+      'DXT1': NBVTF_IMAGE_FORMAT_DXT1,
+      'DXT5': NBVTF_IMAGE_FORMAT_DXT5,
+      'RGB': NBVTF_IMAGE_FORMAT_BGR888
+   }[ img.cxr_data.fmt ]
+
+   mipmap = img.cxr_data.mipmap
+   lod = img.cxr_data.lod
+   clamp = img.cxr_data.clamp
+   flags = img.cxr_data.flags
+
+   q=bpy.context.scene.cxr_data.image_quality
+
+   userflag_hash = F"{mipmap}.{lod}.{clamp}.{flags}"
+   file_hash = F"{name}.{os.path.getmtime(src_path)}"
+   comphash = F"{file_hash}.{dims[0]}.{dims[1]}.{fmt}.{userflag_hash}.{q}"
+
+   if img.cxr_data.last_hash != comphash:
+      print( F"Texture update: {img.filepath}" )
+
+      src = src_path.encode('utf-8')
+      dst = (asset_full_path('materials',img)+'.vtf').encode('utf-8')
+
+      flags_full = flags
+
+      # texture setting flags
+      if not lod: flags_full |= NBVTF_TEXTUREFLAGS_NOLOD
+      if clamp:
+         flags_full |= NBVTF_TEXTUREFLAGS_CLAMPS
+         flags_full |= NBVTF_TEXTUREFLAGS_CLAMPT
+
+      if libnbvtf_convert.call(src,dims[0],dims[1],mipmap,fmt,q,flags_full,dst):
+         img.cxr_data.last_hash = comphash
+
+   return name
+
+# 
+# Compile a material to VMT format. This is quick to run, doesnt need to be a 
+# job handler.
+#
+def compile_material(mat):
+   info = material_info(mat)
+   properties = mat.cxr_data
+
+   print( F"Compile {asset_full_path('materials',mat)}.vmt" )
+   if properties.shader == 'Builtin':
+      return []
+
+   props = []
+   
+   # Walk the property tree
+   def _mlayer( layer ):
+      nonlocal properties, props
+
+      for decl in layer:
+         if isinstance(layer[decl],dict):    # $property definition
+            pdef = layer[decl]
+            ptype = pdef['type']
+
+            subdefines = False
+            default = None
+            prop = None
+
+            if 'shaders' in pdef and properties.shader not in pdef['shaders']:
+               continue
+
+            # Group expansion (does it have subdefinitions?)
+            for ch in pdef:
+               if isinstance(pdef[ch],dict):
+                  subdefines = True
+                  break
+            
+            expandview = False
+
+            if ptype == 'ui':
+               expandview = True
+            else:
+               if ptype == 'intrinsic':
+                  if decl in info:
+                     prop = info[decl]
+               else:
+                  prop = getattr(properties,decl)
+                  default = pdef['default']
+                  
+               if not isinstance(prop,str) and \
+                  not isinstance(prop,bpy.types.Image) and \
+                  hasattr(prop,'__getitem__'):
+                  prop = tuple([p for p in prop])
+
+               if prop != default:
+                  # write prop
+                  props += [(decl,pdef,prop)]
+                  
+                  if subdefines:
+                     expandview = True
+
+            if expandview: _mlayer(pdef)
+
+   _mlayer( cxr_shader_params )
+
+   # Write the vmt
+   with vdf_structure( F"{asset_full_path('materials',mat)}.vmt" ) as vmt:
+      vmt.node( properties.shader )
+      vmt.put( "// Convexer export\n" )
+
+      for pair in props:
+         decl = pair[0]
+         pdef = pair[1]
+         prop = pair[2]
+
+         def _numeric(v):
+            nonlocal pdef
+            if 'exponent' in pdef: return str(pow( v, pdef['exponent'] ))
+            else: return str(v)
+
+         if isinstance(prop,bpy.types.Image):
+            vmt.kv( decl, asset_name(prop))
+         elif isinstance(prop,bool):
+            vmt.kv( decl, '1' if prop else '0' )
+         elif isinstance(prop,str):
+            vmt.kv( decl, prop )
+         elif isinstance(prop,float) or isinstance(prop,int):
+            vmt.kv( decl, _numeric(prop) )
+         elif isinstance(prop,tuple):
+            vmt.kv( decl, F"[{' '.join([_numeric(_) for _ in prop])}]" )
+         else:
+            vmt.put( F"// (cxr) unkown shader value type'{type(prop)}'" )
+
+      vmt.edon()
+   return props
+
+def cxr_modelsrc_vphys( mdl ):
+   for obj in mdl.objects:
+      if obj.name == F"{mdl.name}_phy":
+         return obj
+   return None
+
+def cxr_export_modelsrc( mdl, origin, asset_dir, project_name, transform ):
+   dgraph = bpy.context.evaluated_depsgraph_get()
+
+   # Compute hash value
+   chash = asset_uid(mdl)+str(origin)+str(transform)
+   
+   #for obj in mdl.objects:
+   #   if obj.type != 'MESH':
+   #      continue
+
+   #   ev = obj.evaluated_get(dgraph).data
+   #   srcverts=[(v.co[0],v.co[1],v.co[2]) for v in ev.vertices]
+   #   srcloops=[(l.normal[0],l.normal[1],l.normal[2]) for l in ev.loops]
+
+   #   chash=hashlib.sha224((str(srcverts)+chash).encode()).hexdigest()
+   #   chash=hashlib.sha224((str(srcloops)+chash).encode()).hexdigest()
+
+   #   if ev.uv_layers.active != None:
+   #      uv_layer = ev.uv_layers.active.data
+   #      srcuv=[(uv.uv[0],uv.uv[1]) for uv in uv_layer]
+   #   else:
+   #      srcuv=['none']
+
+   #   chash=hashlib.sha224((str(srcuv)+chash).encode()).hexdigest()
+   #   srcmats=[ ms.material.name for ms in obj.material_slots ]
+   #   chash=hashlib.sha224((str(srcmats)+chash).encode()).hexdigest()
+   #   transforms=[ obj.location, obj.rotation_euler, obj.scale ]
+   #   srctr=[(v[0],v[1],v[2]) for v in transforms]
+   #   chash=hashlib.sha224((str(srctr)+chash).encode()).hexdigest()
+
+   #if chash != mdl.cxr_data.last_hash:
+   #   mdl.cxr_data.last_hash = chash
+   #   print( F"Compile: {mdl.name}" )
+   #else:
+   #   return True
+
+   bpy.ops.object.select_all(action='DESELECT')
+   
+   # Get viewlayer
+   def _get_layer(col,name):   
+      for c in col.children:
+         if c.name == name:
+            return c
+         sub = _get_layer(c,name)
+         if sub != None:
+            return sub
+      return None
+   layer = _get_layer(bpy.context.view_layer.layer_collection,mdl.name)
+
+   prev_state = layer.hide_viewport
+   layer.hide_viewport=False
+
+   # Collect materials to be compiled, and temp rename for export
+   mat_dict = {}
+   
+   vphys = None
+   for obj in mdl.objects:
+      if obj.name == F"{mdl.name}_phy":
+         vphys = obj
+         continue
+
+      obj.select_set(state=True)
+      for ms in obj.material_slots:
+         if ms.material != None:
+            if ms.material not in mat_dict:
+               mat_dict[ms.material] = ms.material.name
+               ms.material.name = asset_uid(ms.material)
+               ms.material.use_nodes = False
+
+   uid=asset_uid(mdl)
+   bpy.ops.export_scene.fbx( filepath=F'{asset_dir}/{uid}_ref.fbx',\
+      check_existing=False,
+      use_selection=True,
+      apply_unit_scale=False,
+      bake_space_transform=False
+   )
+   
+   bpy.ops.object.select_all(action='DESELECT')
+   
+   if vphys != None:
+      vphys.select_set(state=True)
+      bpy.ops.export_scene.fbx( filepath=F'{asset_dir}/{uid}_phy.fbx',\
+         check_existing=False,
+         use_selection=True,
+         apply_unit_scale=False,
+         bake_space_transform=False
+      )
+      bpy.ops.object.select_all(action='DESELECT')
+
+   # Fix material names back to original
+   for mat in mat_dict:
+      mat.name = mat_dict[mat]
+      mat.use_nodes = True
+
+   layer.hide_viewport=prev_state
+
+   # Write out QC file
+   with open(F'{asset_dir}/{uid}.qc','w') as o:
+      o.write(F'$modelname "{project_name}/{uid}"\n')
+      #o.write(F'$scale .32\n')
+      o.write(F'$scale {transform["scale"]/100.0}\n')
+      o.write(F'$body _ "{uid}_ref.fbx"\n')
+      o.write(F'$staticprop\n')
+      o.write(F'$origin {origin[0]:.6f} {origin[1]:.6f} {origin[2]:.6f}\n')
+      
+      if mdl.cxr_data.preserve_order:
+         o.write(F"$preservetriangleorder\n")
+
+      if mdl.cxr_data.texture_shadows:
+         o.write(F"$casttextureshadows\n")
+
+      o.write(F"$surfaceprop {mdl.cxr_data.surfaceprop}\n")
+
+      if vphys != None:
+         o.write(F'$collisionmodel "{uid}_phy.fbx"\n')
+         o.write("{\n")
+         o.write(" $concave\n")
+         o.write("}\n")
+
+      o.write(F'$cdmaterials {project_name}\n')
+      o.write(F'$sequence idle {uid}_ref.fbx\n')
+
+   return True
+# 
+# Copy bsp file (and also lightpatch it)
+# 
+def cxr_patchmap( src, dst ):
+   libcxr_lightpatch_bsp.call( src.encode('utf-8') )
+   shutil.copyfile( src, dst )
+   return True
+
+# Convexer operators 
+# ------------------------------------------------------------------------------
+
+# Force reload of shared libraries
+#
+class CXR_RELOAD(bpy.types.Operator):
+   bl_idname="convexer.reload"
+   bl_label="Reload"
+   def execute(_,context):
+      shared_reload()
+      return {'FINISHED'}
+
+# Reset all debugging/ui information
+#
+class CXR_RESET(bpy.types.Operator):
+   bl_idname="convexer.reset"
+   bl_label="Reset Convexer"
+   def execute(_,context):
+      cxr_reset_all()
+      return {'FINISHED'}
+
+# Used for exporting data to use with ASAN builds
+#
+class CXR_DEV_OPERATOR(bpy.types.Operator):
+   bl_idname="convexer.dev_test"
+   bl_label="Export development data"
+
+   def execute(_,context):
+      # Prepare input data
+      mesh_src = mesh_cxr_format(context.active_object)
+      libcxr_write_test_data.call( pointer(mesh_src) )
+      return {'FINISHED'}
+
+class CXR_INIT_FS_OPERATOR(bpy.types.Operator):
+   bl_idname="convexer.fs_init"
+   bl_label="Initialize filesystem"
+
+   def execute(_,context):
+      gameinfo = F'{bpy.context.scene.cxr_data.subdir}/gameinfo.txt'
+      
+      if libcxr_fs_set_gameinfo.call( gameinfo.encode('utf-8') ) == 1:
+         print( "File system ready" )
+      else:
+         print( "File system failed to initialize" )
+
+      return {'FINISHED'}
+
+def cxr_load_texture( path, is_normal ):
+   global cxr_asset_lib
+
+   if path in cxr_asset_lib['textures']:
+      return cxr_asset_lib['textures'][path]
+
+   print( F"cxr_load_texture( '{path}' )" )
+
+   pvtf = libcxr_fs_get.call( path.encode('utf-8'), 0 )
+
+   if not pvtf:
+      print( "vtf failed to load" )
+      cxr_asset_lib['textures'][path] = None
+      return None
+
+   x = c_int32(0)
+   y = c_int32(0)
+
+   img_data = libnbvtf_read.call( pvtf, pointer(x), pointer(y), \
+         c_int32(is_normal) )
+
+   x = x.value
+   y = y.value
+
+   if not img_data:
+      print( "vtf failed to decode" )
+      libcxr_fs_free.call( pvtf )
+      cxr_asset_lib['textures'][path] = None
+      return None
+
+   img_buf = gpu.types.Buffer('FLOAT', [x*y*4], [_/255.0 for _ in img_data[:x*y*4]])
+
+   tex = cxr_asset_lib['textures'][path] = \
+         gpu.types.GPUTexture( size=(x,y), layers=0, is_cubemap=False,\
+                               format='RGBA8', data=img_buf )
+   
+   libnbvtf_free.call( img_data )
+   libcxr_fs_free.call( pvtf )
+   return tex
+
+def cxr_load_material( path ):
+   global cxr_asset_lib
+
+   if path in cxr_asset_lib['materials']:
+      return cxr_asset_lib['materials'][path]
+
+   print( F"cxr_load_material( '{path}' )" )
+   
+   pvmt = libcxr_valve_load_material.call( path.encode( 'utf-8') )
+   
+   if not pvmt:
+      cxr_asset_lib['materials'][path] = None
+      return None
+
+   vmt = pvmt[0]
+   mat = cxr_asset_lib['materials'][path] = {}
+
+   if vmt.basetexture:
+      mat['basetexture'] = cxr_load_texture( vmt.basetexture.decode('utf-8'), 0)
+   
+   if vmt.bumpmap:
+      mat['bumpmap'] = cxr_load_texture( vmt.bumpmap.decode('utf-8'), 1)
+
+   libcxr_valve_free_material.call( pvmt )
+
+   return mat
+
+def cxr_load_model_full( path ):
+   global cxr_asset_lib, cxr_mdl_shader
+
+   if path in cxr_asset_lib['models']:
+      return cxr_asset_lib['models'][path]
+   
+   pmdl = libcxr_valve_load_model.call( path.encode( 'utf-8' ) )
+
+   print( F"cxr_load_model_full( '{path}' )" )
+
+   if not pmdl:
+      print( "Failed to load model" )
+      cxr_asset_lib['models'][path] = None
+      return None
+
+   mdl = pmdl[0]
+
+   # Convert our lovely interleaved vertex stream into, whatever this is.
+   positions = [ (mdl.vertex_data[i*8+0], \
+                  mdl.vertex_data[i*8+1], \
+                  mdl.vertex_data[i*8+2]) for i in range(mdl.vertex_count) ]
+
+   normals = [ (mdl.vertex_data[i*8+3], \
+                mdl.vertex_data[i*8+4], \
+                mdl.vertex_data[i*8+5]) for i in range(mdl.vertex_count) ]
+
+   uvs = [ (mdl.vertex_data[i*8+6], \
+            mdl.vertex_data[i*8+7]) for i in range(mdl.vertex_count) ]
+
+   fmt = gpu.types.GPUVertFormat()
+   fmt.attr_add(id="aPos", comp_type='F32', len=3, fetch_mode='FLOAT')
+   fmt.attr_add(id="aNormal", comp_type='F32', len=3, fetch_mode='FLOAT')
+   fmt.attr_add(id="aUv", comp_type='F32', len=2, fetch_mode='FLOAT')
+
+   vbo = gpu.types.GPUVertBuf(len=mdl.vertex_count, format=fmt)
+   vbo.attr_fill(id="aPos", data=positions )
+   vbo.attr_fill(id="aNormal", data=normals )
+   vbo.attr_fill(id="aUv", data=uvs )
+
+   batches = cxr_asset_lib['models'][path] = []
+
+   for p in range(mdl.part_count):
+      part = mdl.parts[p]
+      indices = mdl.indices[part.ibstart:part.ibstart+part.ibcount]
+      indices = [ (indices[i*3+0],indices[i*3+1],indices[i*3+2]) \
+                  for i in range(part.ibcount//3) ]
+
+      ibo = gpu.types.GPUIndexBuf( type='TRIS', seq=indices )
+
+      batch = gpu.types.GPUBatch( type='TRIS', buf=vbo, elem=ibo )
+      batch.program_set( cxr_mdl_shader )
+
+      mat_str = cast( mdl.materials[ part.material ], c_char_p )
+      batches += [( cxr_load_material( mat_str.value.decode('utf-8') ), batch )]
+
+   libcxr_valve_free_model.call( pmdl )
+
+   return batches
+
+class CXR_LOAD_MODEL_OPERATOR(bpy.types.Operator):
+   bl_idname="convexer.model_load"
+   bl_label="Load model"
+
+   def execute(_,context):
+      global cxr_test_mdl, cxr_mdl_shader, cxr_asset_lib
+
+      cxr_test_mdl = cxr_load_model_full( bpy.context.scene.cxr_data.dev_mdl )
+
+      scene_redraw()
+      return {'FINISHED'}
+
+# UI: Preview how the brushes will looks in 3D view
+#
+class CXR_PREVIEW_OPERATOR(bpy.types.Operator):
+   bl_idname="convexer.preview"
+   bl_label="Preview Brushes"
+
+   RUNNING = False
+
+   def execute(_,context):
+      global cxr_view_mesh
+      global cxr_view_shader, cxr_view_mesh, cxr_error_inf
+      
+      cxr_reset_all()
+
+      static = _.__class__
+
+      mesh_src = mesh_cxr_format(context.active_object)
+      world = cxr_decompose_globalerr( mesh_src )
+
+      if world == None:
+         return {'FINISHED'}
+      
+      # Generate preview using cxr
+      #
+      ptrpreview = libcxr_world_preview.call( world )
+      preview = ptrpreview[0]
+
+      vertices = preview.vertices[:preview.vertex_count]
+      vertices = [(_[0],_[1],_[2]) for _ in vertices]
+
+      colours = preview.colours[:preview.vertex_count]
+      colours = [(_[0],_[1],_[2],_[3]) for _ in colours]
+
+      indices = preview.indices[:preview.indices_count]
+      indices = [ (indices[i*3+0],indices[i*3+1],indices[i*3+2]) \
+                  for i in range(int(preview.indices_count/3)) ]
+
+      cxr_view_mesh = batch_for_shader(
+         cxr_view_shader, 'TRIS',
+         { "pos": vertices, "color": colours },
+         indices = indices,
+      )
+
+      libcxr_free_tri_mesh.call( ptrpreview )
+      libcxr_free_world.call( world )
+      cxr_batch_lines()
+      scene_redraw()
+      
+      return {'FINISHED'}
+
+# Search for VMF compiler executables in subdirectory
+#
+class CXR_DETECT_COMPILERS(bpy.types.Operator):
+   bl_idname="convexer.detect_compilers"
+   bl_label="Find compilers"
+   
+   def execute(self,context):
+      scene = context.scene
+      settings = scene.cxr_data
+      subdir = settings.subdir
+
+      for exename in ['studiomdl','vbsp','vvis','vrad']:
+         searchpath = os.path.normpath(F'{subdir}/../bin/{exename}.exe')
+         if os.path.exists(searchpath):
+            settings[F'exe_{exename}'] = searchpath
+
+      return {'FINISHED'}
+
+def cxr_compiler_path( compiler ):
+   settings = bpy.context.scene.cxr_data
+   subdir = settings.subdir
+   path = os.path.normpath(F'{subdir}/../bin/{compiler}.exe')
+   
+   if os.path.exists( path ): return path
+   else: return None
+
+# Compatibility layer
+#
+def cxr_temp_file( fn ):
+   if CXR_GNU_LINUX == 1:
+      return F"/tmp/fn"
+   else:
+      filepath = bpy.data.filepath
+      directory = os.path.dirname(filepath)
+      return F"{directory}/{fn}"
+
+def cxr_winepath( path ):
+   if CXR_GNU_LINUX == 1:
+      return 'z:'+path.replace('/','\\')
+   else:
+      return path
+
+# Main compile function
+#
+class CXR_COMPILER_CHAIN(bpy.types.Operator):
+   bl_idname="convexer.chain"
+   bl_label="Compile Chain"
+   
+   # 'static'
+   USER_EXIT = False
+   SUBPROC = None
+   TIMER = None
+   TIMER_LAST = 0.0
+   WAIT_REDRAW = False
+   FILE = None
+   LOG = []
+
+   JOBINFO = None
+   JOBID = 0
+   JOBSYS = None
+
+   def cancel(_,context):
+      #global cxr_jobs_batch
+      static = _.__class__
+      wm = context.window_manager
+      
+      if static.SUBPROC != None:
+         static.SUBPROC.terminate()
+         static.SUBPROC = None
+
+      if static.TIMER != None:
+         wm.event_timer_remove( static.TIMER )
+         static.TIMER = None
+      
+      static.FILE.close()
+
+      #cxr_jobs_batch = None
+      scene_redraw()
+      return {'FINISHED'}
+
+   def modal(_,context,ev):
+      static = _.__class__
+
+      if ev.type == 'TIMER':
+         global cxr_jobs_batch, cxr_error_inf
+
+         if static.WAIT_REDRAW:
+            scene_redraw()
+            return {'PASS_THROUGH'}
+         static.WAIT_REDRAW = True
+
+         if static.USER_EXIT:
+            print( "Chain USER_EXIT" )
+            return _.cancel(context)
+
+         if static.SUBPROC != None:
+            # Deal with async modes
+            status = static.SUBPROC.poll()
+
+            # Cannot redirect STDOUT through here without causing 
+            # undefined behaviour due to the Blender Python specification.
+            #
+            # Have to write it out to a file and read it back in.
+            #
+
+            with open(cxr_temp_file("convexer_compile_log.txt"),"r") as log:
+               static.LOG = log.readlines()
+            if status == None:
+               return {'PASS_THROUGH'}
+            else:
+               #for l in static.SUBPROC.stdout:
+               #   print( F'-> {l.decode("utf-8")}',end='' )
+               static.SUBPROC = None
+
+               if status != 0:
+                  print(F'Compiler () error: {status}')
+
+                  jobn = static.JOBSYS['jobs'][static.JOBID]
+                  cxr_error_inf = ( F"{static.JOBSYS['title']} error {status}", jobn )
+
+                  return _.cancel(context)
+
+               static.JOBSYS['jobs'][static.JOBID] = None
+               cxr_jobs_update_graph( static.JOBINFO )
+               scene_redraw()
+               return {'PASS_THROUGH'}
+         
+         # Compile syncronous thing
+         for sys in static.JOBINFO:
+            for i,target in enumerate(sys['jobs']):
+               if target != None:
+
+                  if callable(sys['exec']):
+                     print( F"Run (sync): {static.JOBID} @{time.time()}" )
+                     
+                     if not sys['exec'](*target):
+                        print( "Job failed" )
+                        return _.cancel(context)
+
+                     sys['jobs'][i] = None
+                     static.JOBID += 1
+                  else:
+                     # Run external executable (wine)
+                     static.SUBPROC = subprocess.Popen( target,
+                        stdout=static.FILE,\
+                        stderr=subprocess.PIPE,\
+                        cwd=sys['cwd'])
+                     static.JOBSYS = sys
+                     static.JOBID = i
+
+                  cxr_jobs_update_graph( static.JOBINFO )
+                  scene_redraw()
+                  return {'PASS_THROUGH'}
+
+         # All completed
+         print( "All jobs completed!" )
+         #cxr_jobs_batch = None
+         #scene_redraw()
+         return _.cancel(context)
+      
+      return {'PASS_THROUGH'}
 
-class CXR_WRITE_VMF(bpy.types.Operator):
-   bl_idname="convexer.write_vmf"
-   bl_label="Write VMF"
+   def invoke(_,context,event):
+      global cxr_error_inf
 
-   def execute(_,context):
-      libcxr_use()
-      libcxr_reset_debug_lines()
+      static = _.__class__
+      wm = context.window_manager
       
-      # Setup output and state
+      if static.TIMER != None:
+         print("Chain exiting...")
+         static.USER_EXIT=True
+         return {'RUNNING_MODAL'}
+
+      print("Launching compiler toolchain")
+      cxr_reset_all()
+
+      # Run static compilation units now (collect, vmt..)
       filepath = bpy.data.filepath
       directory = os.path.dirname(filepath)
-      settings = context.scene.cxr_data
-      
-      asset_dir = F"{directory}/bin"
+      settings = bpy.context.scene.cxr_data
+
+      asset_dir = F"{directory}/modelsrc"
       material_dir = F"{settings.subdir}/materials/{settings.project_name}"
       model_dir = F"{settings.subdir}/models/{settings.project_name}"
+      output_vmf = F"{directory}/{settings.project_name}.vmf"
+
+      bsp_local = F"{directory}/{settings.project_name}.bsp"
+      bsp_remote = F"{settings.subdir}/maps/{settings.project_name}.bsp"
+      bsp_packed = F"{settings.subdir}/maps/{settings.project_name}_pack.bsp"
+      packlist = F"{directory}/{settings.project_name}_assets.txt"
 
       os.makedirs( asset_dir, exist_ok=True )
       os.makedirs( material_dir, exist_ok=True )
       os.makedirs( model_dir, exist_ok=True )
       
-      # States
-      material_info.references = set()
+      static.FILE = open(cxr_temp_file("convexer_compile_log.txt"),"w")
+      static.LOG = []
+
+      sceneinfo = cxr_scene_collect()
+      image_jobs = []
+      qc_jobs = []
+
+      # Collect materials
+      a_materials = set()
+      for brush in sceneinfo['geo']:
+         for ms in brush['object'].material_slots:
+            a_materials.add( ms.material )
+            if ms.material.cxr_data.shader == 'VertexLitGeneric':
+               errmat = ms.material.name
+               errnam = brush['object'].name
+
+               cxr_error_inf = ( "Shader error", \
+                  F"Vertex shader ({errmat}) used on model ({errnam})" )
+
+               print( F"Vertex shader {errmat} used on {errnam}")
+               scene_redraw()
+               return {'CANCELLED'}
       
-      output_vmf = F"{directory}/{settings.project_name}.vmf"
-
-      with vdf_structure(output_vmf) as m:
-         print( F"Write: {output_vmf}" )
-
-         vmfinfo = cxr_vmf_context()
-         vmfinfo.mapversion = 4
-         vmfinfo.skyname = b"sky_csgo_night02b"
-         vmfinfo.detailvbsp = b"detail.vbsp"
-         vmfinfo.detailmaterial = b"detail/detailsprites"
-         vmfinfo.lightmap_scale = 12
-         vmfinfo.brush_count = 0
-         vmfinfo.entity_count = 0
-         vmfinfo.face_count = 0
-         
-         libcxr_begin_vmf.call( pointer(vmfinfo), m.fp )
-
-         # Make sure all of our asset types have a unique ID
-         def _uid_prepare(objtype):
-            used_ids = [0]
-            to_generate = []
-            id_max = 0
-            for o in objtype:
-               vs = o.cxr_data
-               if vs.asset_id in used_ids:
-                  to_generate+=[vs]
-               else:
-                  id_max = max(id_max,vs.asset_id)
-                  used_ids+=[vs.asset_id]
-            for vs in to_generate:
-               id_max += 1
-               vs.asset_id = id_max
+      a_models = set()
+      model_jobs = []
+      for ent in sceneinfo['entities']:
+         if ent['object'] == None: continue
+
+         if ent['classname'] == 'prop_static':
+            obj = ent['object']
+            if isinstance(obj,bpy.types.Collection):
+               target = obj
+               a_models.add( target )
+               model_jobs += [(target, ent['origin'], asset_dir, \
+                              settings.project_name, ent['transform'])]
+            else:
+               target = obj.instance_collection
+               if target in a_models:
+                  continue
+               a_models.add( target )
+               
+               # TODO: Should take into account collection instancing offset
+               model_jobs += [(target, [0,0,0], asset_dir, \
+                              settings.project_name, ent['transform'])]
+      
+         elif ent['object'].type == 'MESH':
+            for ms in ent['object'].material_slots:
+               a_materials.add( ms.material )
          
-         _uid_prepare(bpy.data.materials)
-         _uid_prepare(bpy.data.images)
-         _uid_prepare(bpy.data.collections)
-
-         # Export Brushes and displacement
-         def _collect(collection,transform):
-            if collection.name.startswith('.'):
-               return
-            
-            if collection.hide_render:
-               return
-
-            if collection.name.startswith('mdl_'):
-               _collect.heros += [(collection,transform)]
-               return
+      for mdl in a_models:
+         uid = asset_uid(mdl)
+         qc_jobs += [F'{uid}.qc']
 
-            for obj in collection.objects:
-               if obj.hide_get(): continue
+         for obj in mdl.objects:
+            for ms in obj.material_slots:
+               a_materials.add( ms.material )
+               if ms.material.cxr_data.shader == 'LightMappedGeneric' or \
+                  ms.material.cxr_data.shader == 'WorldVertexTransition':
 
-               classname = cxr_classname( obj )
+                  errmat = ms.material.name
+                  errnam = obj.name
 
-               if classname != None:
-                  _collect.entities += [( obj,transform,classname )]
-               elif obj.type == 'MESH':
-                  _collect.geo += [(obj,transform)]
+                  cxr_error_inf = ( "Shader error", \
+                     F"Lightmapped shader ({errmat}) used on model ({errnam})" )
 
-            for c in collection.children:
-               _collect( c, transform )
-         
-         _collect.a_models = set()
-         _collect.entities = []
-         _collect.geo = []
-         _collect.heros = []
-
-         transform_main = cxr_object_context( \
-               context.scene.cxr_data.scale_factor, 0.0 )
-
-         transform_sky = cxr_object_context( \
-               context.scene.cxr_data.skybox_scale_factor, \
-               context.scene.cxr_data.skybox_offset )
+                  print( F"Lightmapped shader {errmat} used on {errnam}")
+                  scene_redraw()
+                  return {'CANCELLED'}
+      
+      # Collect images
+      for mat in a_materials:
+         for pair in compile_material(mat):
+            decl = pair[0]
+            pdef = pair[1]
+            prop = pair[2]
+
+            if isinstance(prop,bpy.types.Image):
+               flags = 0
+               if 'flags' in pdef: flags = pdef['flags']
+               if prop not in image_jobs:
+                  image_jobs += [(prop,)]
+                  prop.cxr_data.flags = flags
+
+      # Create packlist
+      with open( packlist, "w" ) as fp:
+
+         for mat in a_materials:
+            if mat.cxr_data.shader == 'Builtin': continue
+            fp.write(F"{asset_path('materials',mat)}.vmt\n")
+            fp.write(F"{cxr_winepath(asset_full_path('materials',mat))}.vmt\n")
+
+         for img_job in image_jobs:
+            img = img_job[0]
+            fp.write(F"{asset_path('materials',img)}.vtf\n")
+            fp.write(F"{cxr_winepath(asset_full_path('materials',img))}.vtf\n")
+
+         for mdl in a_models:
+            local = asset_path('models',mdl)
+            winep = cxr_winepath(asset_full_path('models',mdl))
+
+            fp.write(F"{local}.vvd\n")
+            fp.write(F"{winep}.vvd\n")
+            fp.write(F"{local}.dx90.vtx\n")
+            fp.write(F"{winep}.dx90.vtx\n")
+            fp.write(F"{local}.mdl\n")
+            fp.write(F"{winep}.mdl\n")
+            fp.write(F"{local}.vvd\n")
+            fp.write(F"{winep}.vvd\n")
+
+            if cxr_modelsrc_vphys(mdl):
+               fp.write(F"{local}.phy\n")
+               fp.write(F"{winep}.phy\n")
+      
+      # Convexer jobs
+      static.JOBID = 0
+      static.JOBINFO = []
+      
+      if settings.comp_vmf:
+         static.JOBINFO += [{ 
+            "title": "Convexer",
+            "w": 20,
+            "colour": (0.863, 0.078, 0.235,1.0),
+            "exec": cxr_export_vmf,
+            "jobs": [(sceneinfo,output_vmf)]
+         }]
+      
+      if settings.comp_textures:
+         if len(image_jobs) > 0:
+            static.JOBINFO += [{
+               "title": "Textures",
+               "w": 40,
+               "colour": (1.000, 0.271, 0.000,1.0),
+               "exec": compile_image,
+               "jobs": image_jobs
+            }]
+
+      game = cxr_winepath( settings.subdir )
+      args = [ \
+          '-game', game, settings.project_name
+      ]
+
+      # FBX stage
+      if settings.comp_models:
+         if len(model_jobs) > 0:
+            static.JOBINFO += [{
+               "title": "Batches",
+               "w": 25,
+               "colour": (1.000, 0.647, 0.000,1.0),
+               "exec": cxr_export_modelsrc,
+               "jobs": model_jobs
+            }]
+      
+         if len(qc_jobs) > 0:
+            static.JOBINFO += [{
+               "title": "StudioMDL",
+               "w": 20,
+               "colour": (1.000, 0.843, 0.000, 1.0),
+               "exec": "studiomdl",
+               "jobs": [[settings[F'exe_studiomdl']] + [\
+                     '-nop4', '-game', game, qc] for qc in qc_jobs],
+               "cwd": asset_dir
+            }]
+
+      # VBSP stage
+      if settings.comp_compile:
+         if not settings.opt_vbsp.startswith( 'disable' ):
+            vbsp_opt = settings.opt_vbsp.split()
+            static.JOBINFO += [{
+               "title": "VBSP",
+               "w": 25,
+               "colour": (0.678, 1.000, 0.184,1.0),
+               "exec": "vbsp",
+               "jobs": [[settings[F'exe_vbsp']] + vbsp_opt + args],
+               "cwd": directory
+            }]
          
-         if 'main' in bpy.data.collections:
-            _collect( bpy.data.collections['main'], transform_main )
-
-         if 'skybox' in bpy.data.collections:
-            _collect( bpy.data.collections['skybox'], transform_sky )
+         if not settings.opt_vvis.startswith( 'disable' ):
+            vvis_opt = settings.opt_vvis.split()
+            static.JOBINFO += [{
+               "title": "VVIS",
+               "w": 25,
+               "colour": (0.000, 1.000, 0.498,1.0),
+               "exec": "vvis",
+               "jobs": [[settings[F'exe_vvis']] + vvis_opt + args ],
+               "cwd": directory
+            }]
          
-         def _buildsolid( obj, ctx ):
-            nonlocal m
-
-            baked = mesh_cxr_format( brush[0] )
-            world = libcxr_decompose.call( baked )
-            
-            if world == None:
-               return False
+         if not settings.opt_vrad.startswith( 'disable' ):
+            vrad_opt = settings.opt_vrad.split()
+            static.JOBINFO += [{
+               "title": "VRAD",
+               "w": 25,
+               "colour": (0.125, 0.698, 0.667,1.0),
+               "exec": "vrad",
+               "jobs": [[settings[F'exe_vrad']] + vrad_opt + args ],
+               "cwd": directory
+            }]
+
+         static.JOBINFO += [{
+            "title": "CXR",
+            "w": 5,
+            "colour": (0.118, 0.565, 1.000,1.0),
+            "exec": cxr_patchmap,
+            "jobs": [(bsp_local,bsp_remote)]
+         }]
+
+      if settings.comp_pack:
+         static.JOBINFO += [{
+            "title": "Pack",
+            "w": 5,
+            "colour": (0.541, 0.169, 0.886,1.0),
+            "exec": "bspzip",
+            "jobs": [[cxr_compiler_path("bspzip"), '-addlist', \
+                  cxr_winepath(bsp_remote), 
+                  cxr_winepath(packlist),
+                  cxr_winepath(bsp_packed) ]],
+            "cwd": directory
+         }]
+
+      if len(static.JOBINFO) == 0:
+         return {'CANCELLED'}
 
-            vmfinfo.scale = brush[1].scale
-            vmfinfo.offset[0] = 0.0
-            vmfinfo.offset[1] = 0.0
-            vmfinfo.offset[2] = brush[1].offset_z
+      static.USER_EXIT=False
+      static.TIMER=wm.event_timer_add(0.1,window=context.window)
+      wm.modal_handler_add(_)
 
-            libcxr_push_world_vmf.call( world, pointer(vmfinfo), m.fp )
-            libcxr_free_world.call( world )
+      cxr_jobs_update_graph( static.JOBINFO )
+      scene_redraw()
+      return {'RUNNING_MODAL'}
 
-            return True
+class CXR_RESET_HASHES(bpy.types.Operator):
+   bl_idname="convexer.hash_reset"
+   bl_label="Reset asset hashes"
 
-         # World geometry
-         for brush in _collect.geo:
-            if not _buildsolid( brush[0], brush[1] ):
-               print( "error" )
-               return {'CANCELLED'}
+   def execute(_,context):
+      for c in bpy.data.collections:
+         c.cxr_data.last_hash = F"<RESET>{time.time()}"
+         c.cxr_data.asset_id=0
 
-         m.edon()
-         
-         # Entities
-         for entity in _collect.entities:
-            obj = entity[0]
-            ctx = entity[1]
-            cls = entity[2]
-            m.node( 'entity' )
-            m.kv( 'classname', cls )
-
-            kvs = cxr_entity_keyvalues( obj, ctx, cls )
-
-            for kv in kvs:
-               if isinstance(kv[2], list): 
-                  m.kv( kv[0], ' '.join([str(_) for _ in kv[2]]) )
-               else: m.kv( kv[0], str(kv[2]) )
-
-            if not _buildsolid( obj, ctx ):
-               return {'CANCELLED'}
+      for t in bpy.data.images:
+         t.cxr_data.last_hash = F"<RESET>{time.time()}"
+         t.cxr_data.asset_id=0
 
-            m.edon()
-         
-      print( "[CONVEXER] Compile materials / textures" )
+      return {'FINISHED'}
 
-      for mat in material_info.references:
-         compile_material(mat)
+class CXR_COMPILE_MATERIAL(bpy.types.Operator):
+   bl_idname="convexer.matcomp"
+   bl_label="Recompile Material"
 
-      print( "[CONVEXER] Compiling models" )
+   def execute(_,context):
+      active_obj = bpy.context.active_object
+      active_mat = active_obj.active_material
+      
+      #TODO: reduce code dupe (L1663)
+      for pair in compile_material(active_mat):
+         decl = pair[0]
+         pdef = pair[1]
+         prop = pair[2]
 
-      libcxr_batch_debug_lines()
-      scene_redraw()
+         if isinstance(prop,bpy.types.Image):
+            flags = 0
+            if 'flags' in pdef: flags = pdef['flags']
+            prop.cxr_data.flags = flags
 
-      return {'FINISHED'}
+            compile_image( prop )
 
-class CXR_DEV_OPERATOR(bpy.types.Operator):
-   bl_idname="convexer.dev_test"
-   bl_label="Export development data"
+      settings = bpy.context.scene.cxr_data
+      with open(F'{settings.subdir}/cfg/convexer_mat_update.cfg','w') as o:
+         o.write(F'mat_reloadmaterial {asset_name(active_mat)}')
 
-   def execute(_,context):
-      libcxr_use()
+      # TODO: Move this
+      with open(F'{settings.subdir}/cfg/convexer.cfg','w') as o:
+         o.write('sv_cheats 1\n')
+         o.write('mp_warmup_pausetimer 1\n')
+         o.write('bot_kick\n')
+         o.write('alias cxr_reload "exec convexer_mat_update"\n')
 
-      # Prepare input data
-      mesh_src = mesh_cxr_format(context.active_object)
-      
-      libcxr_reset_debug_lines()
-      libcxr_write_test_data.call( pointer(mesh_src) )
-      libcxr_batch_debug_lines()
-         
-      scene_redraw()
       return {'FINISHED'}
 
-class CXR_PREVIEW_OPERATOR(bpy.types.Operator):
-   bl_idname="convexer.preview"
-   bl_label="Preview"
-
-   def execute(_,context):
-      libcxr_use()
+# Convexer panels 
+# ------------------------------------------------------------------------------
 
-      libcxr_reset_debug_lines()
+# Helper buttons for 3d toolbox view
+#
+class CXR_VIEW3D( bpy.types.Panel ):
+   bl_idname = "VIEW3D_PT_convexer"
+   bl_label = "Convexer"
+   bl_space_type = 'VIEW_3D'
+   bl_region_type = 'UI'
+   bl_category = "Convexer"
 
-      mesh_src = mesh_cxr_format(context.active_object)
-      world = libcxr_decompose.call( mesh_src )
-      
-      global debug_gpu_shader, debug_gpu_mesh
+   def draw(_, context):
+      layout = _.layout
 
-      if world == None:
-         debug_gpu_mesh = None
-         libcxr_batch_debug_lines()
-         return {'CANCELLED'}
-      
-      ptrpreview = libcxr_world_preview.call( world )
-      preview = ptrpreview[0]
+      active_object = context.object
+      if active_object == None: return 
 
-      vertices = preview.vertices[:preview.vertex_count]
-      vertices = [(_[0],_[1],_[2]) for _ in vertices]
+      purpose = cxr_object_purpose( active_object )
 
-      colours = preview.colours[:preview.vertex_count]
-      colours = [(_[0],_[1],_[2],_[3]) for _ in colours]
+      if purpose[0] == None or purpose[1] == None:
+         usage_str = "No purpose"
+      else:
+         if purpose[1] == 'model':
+            usage_str = F'mesh in {asset_name( purpose[0] )}.mdl'
+         else:
+            usage_str = F'{purpose[1]} in {purpose[0].name}'
 
-      indices = preview.indices[:preview.indices_count]
-      indices = [ (indices[i*3+0],indices[i*3+1],indices[i*3+2]) \
-                  for i in range(int(preview.indices_count/3)) ]
+      layout.label(text=F"Currently editing:")
+      box = layout.box()
+      box.label(text=usage_str)
+      
+      if purpose[1] == 'brush' or purpose[1] == 'brush_entity':
+         row = layout.row()
+         row.scale_y = 2
+         row.operator("convexer.preview")
 
-      debug_gpu_mesh = batch_for_shader(
-         debug_gpu_shader, 'TRIS',
-         { "pos": vertices, "color": colours },
-         indices = indices,
-      )
+      row = layout.row()
+      row.scale_y = 2
+      row.operator("convexer.reset")
 
-      libcxr_free_tri_mesh.call( ptrpreview )
-      libcxr_free_world.call( world )
-      
-      libcxr_batch_debug_lines()
-      scene_redraw()
-      return {'FINISHED'}
+      layout.prop( bpy.context.scene.cxr_data, "dev_mdl" )
+      layout.operator( "convexer.model_load" )
 
+# Main scene properties interface, where all the settings go
+#
 class CXR_INTERFACE(bpy.types.Panel):
    bl_label="Convexer"
    bl_idname="SCENE_PT_convexer"
@@ -1120,17 +2510,20 @@ class CXR_INTERFACE(bpy.types.Panel):
    bl_context="scene"
 
    def draw(_,context):
-      _.layout.operator("convexer.reload")
-      _.layout.operator("convexer.dev_test")
-      _.layout.operator("convexer.preview")
-      _.layout.operator("convexer.write_vmf")
+      if CXR_GNU_LINUX==1:
+         _.layout.operator("convexer.reload")
+         _.layout.operator("convexer.dev_test")
+         _.layout.operator("convexer.fs_init")
 
+      _.layout.operator("convexer.hash_reset")
       settings = context.scene.cxr_data
 
-      _.layout.prop(settings, "debug")
       _.layout.prop(settings, "scale_factor")
+      _.layout.prop(settings, "skybox_scale_factor")
+      _.layout.prop(settings, "skyname" )
       _.layout.prop(settings, "lightmap_scale")
       _.layout.prop(settings, "light_scale" )
+      _.layout.prop(settings, "image_quality" )
       
       box = _.layout.box()
 
@@ -1141,142 +2534,32 @@ class CXR_INTERFACE(bpy.types.Panel):
       box.operator("convexer.detect_compilers")
       box.prop(settings, "exe_studiomdl")
       box.prop(settings, "exe_vbsp")
-      box.prop(settings, "exe_vvis")
-      box.prop(settings, "exe_vrad")
-
-# COmpile image using NBVTF and hash it
-def compile_image(img,flags):
-   if img==None:
-      return None
-
-   name = asset_name(img)
-   src_path = bpy.path.abspath(img.filepath)
-
-   dims = img.cxr_data.export_res
-   fmt = { 
-      'RGBA': NBVTF_IMAGE_FORMAT_RGBA8888,
-      'DXT1': NBVTF_IMAGE_FORMAT_DXT1,
-      'DXT5': NBVTF_IMAGE_FORMAT_DXT5,
-      'RGB': NBVTF_IMAGE_FORMAT_RGB888
-   }[ img.cxr_data.fmt ]
-
-   mipmap = img.cxr_data.mipmap
-   lod = img.cxr_data.lod
-   clamp = img.cxr_data.clamp
-
-   userflag_hash = F"{mipmap}.{lod}.{clamp}"
-   file_hash = F"{name}.{os.path.getmtime(src_path)}"
-   comphash = F"{file_hash}.{dims[0]}.{dims[1]}.{fmt}.{userflag_hash}"
-
-   if img.cxr_data.last_hash != comphash:
-      print( F"Texture update: {img.filepath}" )
-
-      src = src_path.encode('utf-8')
-      dst = (asset_full_path('materials',img)+'.vtf').encode('utf-8')
-
-      flags_full = flags
-
-      # texture setting flags
-      if not lod: flags_full |= NBVTF_TEXTUREFLAGS_NOLOD
-      if clamp:
-         flags_full |= NBVTF_TEXTUREFLAGS_CLAMPS
-         flags_full |= NBVTF_TEXTUREFLAGS_CLAMPT
-
-      if libnbvtf_convert.call(src,dims[0],dims[1],mipmap,fmt,0,flags_full,dst):
-         img.cxr_data.last_hash = comphash
-
-   return name
-
-def compile_material(mat):
-   print( F"Compile {asset_full_path('materials',mat)}.vmt" )
-
-   info = material_info(mat)
-   properties = mat.cxr_data
-
-   props = []
-   
-   def _mlayer( layer ):
-      nonlocal properties, props
-
-      for decl in layer:
-         if isinstance(layer[decl],dict):    # $property definition
-            pdef = layer[decl]
-            ptype = pdef['type']
-
-            subdefines = False
-            default = None
-            prop = None
-
-            if 'shaders' in pdef and properties.shader not in pdef['shaders']:
-               continue
-
-            # Group expansion (does it have subdefinitions?)
-            for ch in pdef:
-               if isinstance(pdef[ch],dict):
-                  subdefines = True
-                  break
-            
-            expandview = False
-
-            if ptype == 'ui':
-               expandview = True
-            else:
-               if ptype == 'intrinsic':
-                  if decl in info:
-                     prop = info[decl]
-               else:
-                  prop = getattr(properties,decl)
-                  default = pdef['default']
-                  
-               if not isinstance(prop,str) and \
-                  not isinstance(prop,bpy.types.Image) and \
-                  hasattr(prop,'__getitem__'):
-                  prop = tuple([p for p in prop])
-
-               if prop != default:
-                  # write prop
-                  props += [(decl,pdef,prop)]
-                  
-                  if subdefines:
-                     expandview = True
-
-            if expandview: _mlayer(pdef)
-
-   _mlayer( cxr_shader_params )
-
-   with vdf_structure( F"{asset_full_path('materials',mat)}.vmt" ) as vmt:
-      vmt.node( properties.shader )
-      vmt.put( "// Convexer export\n" )
-
-      for pair in props:
-         decl = pair[0]
-         pdef = pair[1]
-         prop = pair[2]
-
-         def _numeric(v):
-            nonlocal pdef
+      box.prop(settings, "opt_vbsp")
 
-            if 'exponent' in pdef: return str(pow( v, pdef['exponent'] ))
-            else: return str(v)
+      box.prop(settings, "exe_vvis")
+      box.prop(settings, "opt_vvis")
 
-         if isinstance(prop,bpy.types.Image):
-            flags = 0
-            if 'flags' in pdef:
-               flags = pdef['flags']
-            vmt.kv( decl,compile_image(prop,flags))
-            
-         elif isinstance(prop,bool):
-            vmt.kv( decl, '1' if prop else '0' )
-         elif isinstance(prop,str):
-            vmt.kv( decl, prop )
-         elif isinstance(prop,float) or isinstance(prop,int):
-            vmt.kv( decl, _numeric(prop) )
-         elif isinstance(prop,tuple):
-            vmt.kv( decl, F"[{' '.join([_numeric(_) for _ in prop])}]" )
-         else:
-            vmt.put( F"// (cxr) unkown shader value type'{type(prop)}'" )
+      box.prop(settings, "exe_vrad")
+      box.prop(settings, "opt_vrad")
+
+      box = box.box()
+      row = box.row()
+      row.prop(settings,"comp_vmf")
+      row.prop(settings,"comp_textures")
+      row.prop(settings,"comp_models")
+      row.prop(settings,"comp_compile")
+      row.prop(settings,"comp_pack")
+      
+      text = "Compile" if CXR_COMPILER_CHAIN.TIMER == None else "Cancel"
+      row = box.row()
+      row.scale_y = 3
+      row.operator("convexer.chain", text=text)
 
-      vmt.edon()
+      row = box.row()
+      row.scale_y = 2
+      row.operator("convexer.reset")
+      if CXR_COMPILER_CHAIN.TIMER != None:
+         row.enabled = False
 
 class CXR_MATERIAL_PANEL(bpy.types.Panel):
    bl_label="VMT Properties"
@@ -1296,10 +2579,11 @@ class CXR_MATERIAL_PANEL(bpy.types.Panel):
       info = material_info( active_material )
 
       _.layout.label(text=F"{info['name']} @{info['res'][0]}x{info['res'][1]}")
-      _.layout.prop( properties, "shader" )
+      row = _.layout.row()
+      row.prop( properties, "shader" )
+      row.operator( "convexer.matcomp" )
 
-      for xk in info:
-         _.layout.label(text=F"{xk}:={info[xk]}")
+      #for xk in info: _.layout.label(text=F"{xk}:={info[xk]}")
       
       def _mtex( name, img, uiParent ):
          nonlocal properties
@@ -1387,13 +2671,13 @@ def cxr_entity_changeclass(_,context):
 
    # Create ID properties
    entdef = None
-   classname = active_object.cxr_data.classname 
+   classname = cxr_custom_class(active_object)
 
    if classname in cxr_entities:
       entdef = cxr_entities[classname]
 
       kvs = entdef['keyvalues']
-      if callable(kvs): kvs = kvs(active_object)
+      if callable(kvs): kvs = kvs( {'object': active_object} )
 
       for k in kvs:
          kv = kvs[k]
@@ -1418,8 +2702,10 @@ class CXR_ENTITY_PANEL(bpy.types.Panel):
 
       if active_object == None: return
       
-      default_context = cxr_object_context( \
-            bpy.context.scene.cxr_data.scale_factor, 0.0 )
+      default_context = {
+         "scale": bpy.context.scene.cxr_data.scale_factor,
+         "offset": (0,0,0)
+      }
 
       ecn = cxr_intrinsic_classname( active_object )
       classname = cxr_custom_class( active_object )
@@ -1429,6 +2715,9 @@ class CXR_ENTITY_PANEL(bpy.types.Panel):
             _.layout.prop( active_object.cxr_data, 'brushclass' )
          else: _.layout.prop( active_object.cxr_data, 'classname' )
 
+         _.layout.prop( active_object.cxr_data, 'visgroup' )
+         _.layout.prop( active_object.cxr_data, 'lightmap_override' )
+
          if classname == 'NONE':
             return
       else: 
@@ -1436,7 +2725,12 @@ class CXR_ENTITY_PANEL(bpy.types.Panel):
          _.layout.enabled=False
          classname = ecn
       
-      kvs = cxr_entity_keyvalues( active_object, default_context, classname )
+      kvs = cxr_entity_keyvalues( {
+         "object": active_object, 
+         "transform": default_context,
+         "classname": classname
+      })
+
       if kvs != None:
          for kv in kvs:
             if kv[1]:
@@ -1472,6 +2766,29 @@ class CXR_LIGHT_PANEL(bpy.types.Panel):
          elif active_object.type == 'LIGHT_PROBE':
             layout.prop( properties, "size" )
 
+class CXR_COLLECTION_PANEL(bpy.types.Panel):
+   bl_label = "Source Settings"
+   bl_idname = "COL_PT_cxr"
+   bl_space_type = 'PROPERTIES'
+   bl_region_type = 'WINDOW'
+   bl_context = "collection"
+   
+   def draw(self, context):
+      layout = self.layout
+      scene = context.scene
+      
+      active_collection = bpy.context.collection
+      
+      if active_collection != None:
+         layout.prop( active_collection.cxr_data, "shadow_caster" )
+         layout.prop( active_collection.cxr_data, "texture_shadows" )
+         layout.prop( active_collection.cxr_data, "preserve_order" )
+         layout.prop( active_collection.cxr_data, "surfaceprop" )
+         layout.prop( active_collection.cxr_data, "visgroup" )
+
+# Settings groups
+# ------------------------------------------------------------------------------
+
 class CXR_IMAGE_SETTINGS(bpy.types.PropertyGroup):
    export_res: bpy.props.IntVectorProperty(
       name="",
@@ -1498,6 +2815,7 @@ class CXR_IMAGE_SETTINGS(bpy.types.PropertyGroup):
    mipmap: bpy.props.BoolProperty(name="MIP",default=True)
    lod: bpy.props.BoolProperty(name="LOD",default=True)
    clamp: bpy.props.BoolProperty(name="CLAMP",default=False)
+   flags: bpy.props.IntProperty(name="flags",default=0)
 
 class CXR_LIGHT_SETTINGS(bpy.types.PropertyGroup):
    realtime: bpy.props.BoolProperty(name="Realtime Light", default=True)
@@ -1537,14 +2855,29 @@ class CXR_ENTITY_SETTINGS(bpy.types.PropertyGroup):
 
    brushclass: bpy.props.EnumProperty(items=enum_brushents, name="Class", \
          update=cxr_entity_changeclass, default='NONE' )
+   
+   enum_classes = [('0',"None","")]
+   for i, vg in enumerate(cxr_visgroups):
+      enum_classes += [(str(i+1),vg,"")]
+   visgroup: bpy.props.EnumProperty(name="visgroup",items=enum_classes,default=0)
+   lightmap_override: bpy.props.IntProperty(name="Lightmap Override",default=0)
 
 class CXR_MODEL_SETTINGS(bpy.types.PropertyGroup):
    last_hash: bpy.props.StringProperty( name="" )
    asset_id: bpy.props.IntProperty(name="vmf_settings",default=0)
+   shadow_caster: bpy.props.BoolProperty( name="Shadow caster", default=True )
+   texture_shadows: bpy.props.BoolProperty( name="Texture Shadows", default=False )
+   preserve_order: bpy.props.BoolProperty( name="Preserve Order", default=False )
+   surfaceprop: bpy.props.StringProperty( name="Suface prop",default="default" )
+
+   enum_classes = [('0',"None","")]
+   for i, vg in enumerate(cxr_visgroups):
+      enum_classes += [(str(i+1),vg,"")]
+   visgroup: bpy.props.EnumProperty(name="visgroup",items=enum_classes,default=0)
 
 class CXR_SCENE_SETTINGS(bpy.types.PropertyGroup):
    project_name: bpy.props.StringProperty( name="Project Name" )
-   subdir: bpy.props.StringProperty( name="Subdirectory" )
+   subdir: bpy.props.StringProperty( name="../csgo/ folder" )
    
    exe_studiomdl: bpy.props.StringProperty( name="studiomdl" )
    exe_vbsp: bpy.props.StringProperty( name="vbsp" )
@@ -1552,45 +2885,44 @@ class CXR_SCENE_SETTINGS(bpy.types.PropertyGroup):
    exe_vvis: bpy.props.StringProperty( name="vvis" )
    opt_vvis: bpy.props.StringProperty( name="args" )
    exe_vrad: bpy.props.StringProperty( name="vrad" )
-   opt_vrad: bpy.props.StringProperty( name="args" )
+   opt_vrad: bpy.props.StringProperty( name="args", \
+         default="-reflectivityScale 0.35 -aoscale 1.4 -final -textureshadows -hdr -StaticPropLighting -StaticPropPolys" )
 
-   debug: bpy.props.BoolProperty(name="Debug",default=False)
    scale_factor: bpy.props.FloatProperty( name="VMF Scale factor", \
          default=32.0,min=1.0)
    skybox_scale_factor: bpy.props.FloatProperty( name="Sky Scale factor", \
          default=1.0,min=0.01)
-
+   skyname: bpy.props.StringProperty(name="Skyname",default="sky_csgo_night02b")
    skybox_offset: bpy.props.FloatProperty(name="Sky offset",default=-4096.0)
    light_scale: bpy.props.FloatProperty(name="Light Scale",default=1.0/5.0)
    include_names: bpy.props.BoolProperty(name="Append original file names",\
          default=True)
    lightmap_scale: bpy.props.IntProperty(name="Global Lightmap Scale",\
          default=12)
+   image_quality: bpy.props.IntProperty(name="Texture Quality (0-18)",\
+         default=8, min=0, max=18 )
 
-class CXR_DETECT_COMPILERS(bpy.types.Operator):
-   bl_idname="convexer.detect_compilers"
-   bl_label="Find compilers"
-   
-   def execute(self,context):
-      scene = context.scene
-      settings = scene.cxr_data
-      subdir = settings.subdir
-
-      for exename in ['studiomdl','vbsp','vvis','vrad']:
-         searchpath = os.path.normpath(F'{subdir}/../bin/{exename}.exe')
-         if os.path.exists(searchpath):
-            settings[F'exe_{exename}'] = searchpath
+   comp_vmf: bpy.props.BoolProperty(name="VMF",default=True)
+   comp_models: bpy.props.BoolProperty(name="Models",default=True)
+   comp_textures: bpy.props.BoolProperty(name="Textures",default=True)
+   comp_compile: bpy.props.BoolProperty(name="Compile",default=True)
+   comp_pack: bpy.props.BoolProperty(name="Pack",default=False)
 
-      return {'FINISHED'}
+   dev_mdl: bpy.props.StringProperty(name="Model",default="")
 
 classes = [ CXR_RELOAD, CXR_DEV_OPERATOR, CXR_INTERFACE, \
-            CXR_WRITE_VMF, CXR_MATERIAL_PANEL, CXR_IMAGE_SETTINGS,\
+            CXR_MATERIAL_PANEL, CXR_IMAGE_SETTINGS,\
             CXR_MODEL_SETTINGS, CXR_ENTITY_SETTINGS, CXR_CUBEMAP_SETTINGS,\
             CXR_LIGHT_SETTINGS, CXR_SCENE_SETTINGS, CXR_DETECT_COMPILERS,\
-            CXR_ENTITY_PANEL, CXR_LIGHT_PANEL, CXR_PREVIEW_OPERATOR ]
+            CXR_ENTITY_PANEL, CXR_LIGHT_PANEL, CXR_PREVIEW_OPERATOR,\
+            CXR_VIEW3D, CXR_COMPILER_CHAIN, CXR_RESET_HASHES,\
+            CXR_COMPILE_MATERIAL, CXR_COLLECTION_PANEL, CXR_RESET, \
+            CXR_INIT_FS_OPERATOR, CXR_LOAD_MODEL_OPERATOR ]
+
+vmt_param_dynamic_class = None
 
 def register():
-   global debug_draw_handler, vmt_param_dynamic_class
+   global cxr_view_draw_handler, vmt_param_dynamic_class, cxr_ui_handler
 
    for c in classes:
       bpy.utils.register_class(c)
@@ -1685,14 +3017,17 @@ def register():
    # CXR Scene settings
 
    # GPU / callbacks
-   debug_draw_handler = bpy.types.SpaceView3D.draw_handler_add(\
+   cxr_view_draw_handler = bpy.types.SpaceView3D.draw_handler_add(\
       cxr_draw,(),'WINDOW','POST_VIEW')
 
+   cxr_ui_handler = bpy.types.SpaceView3D.draw_handler_add(\
+      cxr_ui,(None,None),'WINDOW','POST_PIXEL')
+
    bpy.app.handlers.load_post.append(cxr_on_load)
    bpy.app.handlers.depsgraph_update_post.append(cxr_dgraph_update)
 
 def unregister():
-   global debug_draw_handler, vmt_param_dynamic_class
+   global cxr_view_draw_handler, vmt_param_dynamic_class, cxr_ui_handler
 
    bpy.utils.unregister_class( vmt_param_dynamic_class )
    for c in classes:
@@ -1701,4 +3036,5 @@ def unregister():
    bpy.app.handlers.depsgraph_update_post.remove(cxr_dgraph_update)
    bpy.app.handlers.load_post.remove(cxr_on_load)
 
-   bpy.types.SpaceView3D.draw_handler_remove(debug_draw_handler,'WINDOW')
+   bpy.types.SpaceView3D.draw_handler_remove(cxr_view_draw_handler,'WINDOW')
+   bpy.types.SpaceView3D.draw_handler_remove(cxr_ui_handler,'WINDOW')