music player, credits
[carveJwlIkooP6JGAAIwe30JlM.git] / blender_export.py
index eed99af0d9293536fa9e43335a686fe090564069..32e548b4afa0d1583f4171949d0fec4e534b4886 100644 (file)
@@ -17,17 +17,37 @@ bl_info = {
    "category":"Import/Export",
 }
 
-sr_entity_alias = {
-   'ent_gate': 1,
-   'ent_spawn': 2,
-   'ent_route_node': 3,
-   'ent_route': 4,
-   'ent_water': 5,
-   'ent_volume': 6,
-   'ent_audio': 7,
-   'ent_marker': 8,
-   'ent_glyph': 9
-}
+sr_entity_list = [
+   ('none',             'None',           '', 0  ),
+   ('ent_gate',         'Gate',           '', 1  ),
+   ('ent_spawn',        'Spawn Point',    '', 2  ),
+   ('ent_route_node',   'Routing Path',   '', 3  ),
+   ('ent_route',        'Skate Course',   '', 4  ),
+   ('ent_water',        'Water Surface',  '', 5  ),
+   ('ent_volume',       'Volume/Trigger', '', 6  ),
+   ('ent_audio',        'Audio',          '', 7  ),
+   ('ent_marker',       'Marker',         '', 8  ),
+   ('ent_font',         'Font',           '', 9  ),
+   ('ent_font_variant', 'Font:Variant',   '', 10 ),
+   ('ent_traffic',      'Traffic Model',  '', 11 ),
+   ('ent_skateshop',    'Skate Shop',     '', 12 ),
+   ('ent_camera',       'Camera',         '', 13 ),
+   ('ent_swspreview', 'Workshop Preview', '', 14 ),
+   ('ent_menuitem',     'Menu Item',      '', 15 ),
+   ('ent_worldinfo',    'World Info',     '', 16 ),
+   ('ent_ccmd',         'CCmd',           '', 17 )
+]
+
+def get_entity_enum_id( alias ):
+#{
+   for et in sr_entity_list:#{
+      if et[0] == alias:#{
+         return et[3]
+      #}
+   #}
+
+   return 0
+#}
 
 class mdl_vert(Structure):              # 48 bytes. Quite large. Could compress
 #{                                      # the normals and uvs to i16s. Not an
@@ -54,7 +74,8 @@ class mdl_submesh(Structure):
                ("vertex_start",c_uint32),
                ("vertex_count",c_uint32),
                ("bbx",(c_float*3)*2),
-               ("material_id",c_uint32)]        # index into the material array
+               ("material_id",c_uint16), # index into the material array
+               ("flags",c_uint16)]
 #}
 
 class mdl_material(Structure):
@@ -107,7 +128,7 @@ class mdl_mesh(Structure):
                ("submesh_start",c_uint32),
                ("submesh_count",c_uint32),
                ("pstr_name",c_uint32),
-               ("flags",c_uint32),
+               ("entity_id",c_uint32),
                ("armature_id",c_uint32)]
 #}
 
@@ -121,7 +142,7 @@ class mdl_file(Structure):
 class mdl_texture(Structure):
 #{
    _fields_ = [("file",mdl_file),
-               ("type",c_uint32)]
+               ("glname",c_uint32)]
 #}
 
 class mdl_array(Structure):
@@ -166,6 +187,7 @@ class ent_gate(Structure):
 #{
    _fields_ = [("type",c_uint32),
                ("target", c_uint32),
+               ("key",c_uint32),
                ("dimensions", c_float*3),
                ("co", (c_float*3)*2),
                ("q", (c_float*4)*2),
@@ -256,19 +278,13 @@ class volume_union(Union):
                ("particles",volume_particles)]
 #}
 
-class ent_index(Structure):
-#{
-   _fields_ = [("type",c_uint32),
-               ("index",c_uint32)]
-#}
-
 class ent_volume(Structure):
 #{
    _fields_ = [("transform",mdl_transform),
                ("to_world",(c_float*3)*4),
                ("to_local",(c_float*3)*4),
                ("type",c_uint32),
-               ("target",ent_index),
+               ("target",c_uint32),
                ("_anon",volume_union)]
 #}
 
@@ -315,10 +331,125 @@ class ent_font(Structure):
                ("glyph_utf32_base",c_uint32)]
 #}
 
+class ent_traffic(Structure):
+#{
+   _fields_ = [("transform",mdl_transform),
+               ("submesh_start",c_uint32),
+               ("submesh_count",c_uint32),
+               ("start_node",c_uint32),
+               ("node_count",c_uint32),
+               ("speed",c_float),
+               ("t",c_float),
+               ("index",c_uint32)]
+#}
+
+# Skateshop
+# ---------------------------------------------------------------
+class ent_skateshop_characters(Structure):
+#{
+   _fields_ = [("id_display",c_uint32),
+               ("id_info",c_uint32)]
+#}
+class ent_skateshop_boards(Structure):
+#{
+   _fields_ = [("id_display",c_uint32),
+               ("id_info",c_uint32),
+               ("id_rack",c_uint32)]
+#}  
+class ent_skateshop_worlds(Structure):
+#{
+   _fields_ = [("id_display",c_uint32),
+               ("id_info",c_uint32)]
+#}
+class ent_skateshop_anon_union(Union):
+#{
+   _fields_ = [("boards",ent_skateshop_boards),
+               ("character",ent_skateshop_characters),
+               ("worlds",ent_skateshop_worlds)]
+#}
+class ent_skateshop(Structure):
+#{
+   _fields_ = [("transform",mdl_transform), ("type",c_uint32),
+               ("id_camera",c_uint32), 
+               ("_anonymous_union",ent_skateshop_anon_union)]
+#}
+
+class ent_swspreview(Structure):
+#{
+   _fields_ = [("id_camera",c_uint32),
+               ("id_display",c_uint32),
+               ("id_display1",c_uint32)]
+#}
+
+# Menu
+# -----------------------------------------------------------------
+class ent_menuitem_slider(Structure):
+#{
+   _fields_ = [("id_min",c_uint32),
+               ("id_max",c_uint32),
+               ("id_handle",c_uint32),
+               ("pstr_data",c_uint32)]
+#}
+class ent_menuitem_button(Structure):
+#{
+   _fields_ = [("pstr",c_uint32)]
+#}
+class ent_menuitem_checkmark(Structure):
+#{
+   _fields_ = [("id_check",c_uint32),
+               ("pstr_data",c_uint32),
+               ("offset",c_float*3)]
+#}
+class ent_menuitem_page(Structure):
+#{
+   _fields_ = [("pstr_name",c_uint32),
+               ("id_entrypoint",c_uint32),
+               ("id_viewpoint",c_uint32)]
+#}
+class ent_menuitem_anon_union(Union):
+#{
+   _fields_ = [("slider",ent_menuitem_slider),
+               ("button",ent_menuitem_button),
+               ("checkmark",ent_menuitem_checkmark),
+               ("page",ent_menuitem_page)]
+#}
+class ent_menuitem(Structure):
+#{
+   _fields_ = [("type",c_uint32), ("groups",c_uint32),
+               ("id_links",c_uint32*4), 
+               ("factive",c_float), ("fvisible",c_float),
+               #-- TODO: Refactor this into a simple mesh structure
+               ("transform",mdl_transform),
+               ("submesh_start",c_uint32),("submesh_count",c_uint32),
+               ("_u64",c_uint64),
+               #-- end
+               ("_anonymous_union", ent_menuitem_anon_union)]
+#}
+
+class ent_camera(Structure):
+#{
+   _fields_ = [("transform",mdl_transform),
+               ("fov",c_float)]
+#}
+
+class ent_worldinfo(Structure):
+#{
+   _fields_ = [("pstr_name",c_uint32),
+               ("pstr_author",c_uint32),    # unused
+               ("pstr_desc",c_uint32),      # unused
+               ("timezone",c_float)]
+#}
+
+class ent_ccmd(Structure):
+#{
+   _fields_ = [("pstr_command",c_uint32)]
+#}
+
 def obj_ent_type( obj ):
 #{
    if obj.type == 'ARMATURE': return 'mdl_armature'
    elif obj.type == 'LIGHT': return 'ent_light'
+   elif obj.type == 'CAMERA': return 'ent_camera'
    else: return obj.SR_data.ent_type
 #}
 
@@ -340,7 +471,9 @@ def sr_filter_ent_type( obj, ent_types ):
 def compile_obj_transform( obj, transform ):
 #{
    co = obj.matrix_world @ Vector((0,0,0))
-   q = obj.matrix_local.to_quaternion()
+
+   # This was changed from matrix_local on 09.05.23
+   q = obj.matrix_world.to_quaternion() 
    s = obj.scale
    
    # Setup transform
@@ -387,6 +520,7 @@ def sr_compile_string( s ):
    
    index = len( sr_compile.string_data )
    sr_compile.string_cache[s] = index
+   sr_compile.string_data.extend( c_uint32(hash_djb2(s)) )
    sr_compile.string_data.extend( s.encode('utf-8') )
    sr_compile.string_data.extend( b'\0' )
 
@@ -428,6 +562,10 @@ cxr_graph_mapping = \
             "Color": material_tex_image("tex_normal")
          }
       }
+   },
+   "Emission":
+   {
+      "Color": material_tex_image("tex_diffuse")
    }
 }
 
@@ -538,7 +676,7 @@ def sr_compile_texture( img ):
    texture_index = (len(sr_compile.texture_data)//sizeof(mdl_texture)) +1
 
    tex = mdl_texture()
-   tex.type = 0
+   tex.glname = 0
 
    if sr_compile.pack_textures:#{
       filedata = qoi_encode( img )
@@ -565,12 +703,18 @@ def sr_compile_material( mat ):
    
    flags = 0x00
    if mat.SR_data.collision:#{
-      flags |= 0x2
-      if mat.SR_data.skate_surface: flags |= 0x1
-      if mat.SR_data.grind_surface: flags |= (0x8|0x1)
+      flags |= 0x2 # collision flag
+      if (mat.SR_data.shader != 'invisible') and \
+         (mat.SR_data.shader != 'boundary'):#{
+         if mat.SR_data.skate_surface: flags |= 0x1
+         if mat.SR_data.grow_grass: flags |= 0x4
+         if mat.SR_data.grind_surface: flags |= 0x8
+         if mat.SR_data.preview_visibile: flags |= 0x40
+      #}
+      if mat.SR_data.shader == 'invisible': flags |= 0x10
+      if mat.SR_data.shader == 'boundary': flags |= (0x10|0x20)
    #}
 
-   if mat.SR_data.grow_grass: flags |= 0x4
    m.flags = flags
 
    m.surface_prop = int(mat.SR_data.surface_prop)
@@ -608,6 +752,14 @@ def sr_compile_material( mat ):
       m.colour1[2] = pow( mat.SR_data.ocean_colour[2], 1.0/2.2 )
       m.colour1[3] = 1.0
    #}
+
+   if mat.SR_data.shader == 'invisible':#{
+      m.shader = 5
+   #}
+
+   if mat.SR_data.shader == 'boundary':#{
+      m.shader = 6
+   #}
    
    inf = material_info( mat )
 
@@ -637,16 +789,24 @@ def sr_armature_bones( armature ):
          yield from _recurse_bone( b )
 #}
 
-def sr_compile_mesh( obj ):
+def sr_entity_id( obj ):
 #{
-   node=mdl_mesh()
-   compile_obj_transform(obj, node.transform)
-   node.pstr_name = sr_compile_string(obj.name)
-   node.flags = 0
+   tipo = get_entity_enum_id( obj_ent_type(obj) )
+   index = sr_compile.entity_ids[ obj.name ]
 
+   return (tipo&0xffff)<<16 | (index&0xffff)
+#}
+
+# Returns submesh_start,count and armature_id
+def sr_compile_mesh_internal( obj ):
+#{
    can_use_cache = True
    armature = None
 
+   submesh_start = 0
+   submesh_count = 0
+   armature_id = 0
+
    for mod in obj.modifiers:#{
       if mod.type == 'DATA_TRANSFER' or mod.type == 'SHRINKWRAP' or \
          mod.type == 'BOOLEAN' or mod.type == 'CURVE' or \
@@ -656,11 +816,10 @@ def sr_compile_mesh( obj ):
       #}
 
       if mod.type == 'ARMATURE': #{
-         node.flags = 1
          armature = mod.object
          rig_weight_groups = \
                ['0 [ROOT]']+[_.name for _ in sr_armature_bones(mod.object)]
-         node.armature_id = sr_compile.entity_ids[armature.name]
+         armature_id = sr_compile.entity_ids[armature.name]
 
          POSE_OR_REST_CACHE = armature.data.pose_position
          armature.data.pose_position = 'REST'
@@ -671,16 +830,15 @@ def sr_compile_mesh( obj ):
    #
    if can_use_cache and (obj.data.name in sr_compile.mesh_cache):#{
       ref = sr_compile.mesh_cache[obj.data.name]
-      node.submesh_start = ref[0]
-      node.submesh_count = ref[1]
-      sr_compile.mesh_data.extend(bytearray(node))
-      return
+      submesh_start = ref[0]
+      submesh_count = ref[1]
+      return (submesh_start,submesh_count,armature_id)
    #}
 
    # Compile a whole new mesh
    #
-   node.submesh_start = len(sr_compile.submesh_data)//sizeof(mdl_submesh)
-   node.submesh_count = 0
+   submesh_start = len(sr_compile.submesh_data)//sizeof(mdl_submesh)
+   submesh_count = 0
 
    dgraph = bpy.context.evaluated_depsgraph_get()
    data = obj.evaluated_get(dgraph).data
@@ -864,16 +1022,37 @@ def sr_compile_mesh( obj ):
       # Add submesh to encoder
       #
       sr_compile.submesh_data.extend( bytearray(sm) )
-      node.submesh_count += 1
+      submesh_count += 1
    #}
 
    if armature:#{
       armature.data.pose_position = POSE_OR_REST_CACHE
    #}
 
-   # Save a reference to this node since we want to reuse the submesh indices
+   # Save a reference to this mesh since we want to reuse the submesh indices
    # later.
-   sr_compile.mesh_cache[obj.data.name]=(node.submesh_start,node.submesh_count)
+   sr_compile.mesh_cache[obj.data.name]=(submesh_start,submesh_count)
+   return (submesh_start,submesh_count,armature_id)
+#}
+
+def sr_compile_mesh( obj ):
+#{
+   node=mdl_mesh()
+   compile_obj_transform(obj, node.transform)
+   node.pstr_name = sr_compile_string(obj.name)
+   ent_type = obj_ent_type( obj )
+
+   node.entity_id = 0
+
+   if ent_type != 'none':#{
+      ent_id_lwr = sr_compile.entity_ids[obj.name]
+      ent_id_upr = get_entity_enum_id( obj_ent_type(obj) )
+      node.entity_id = (ent_id_upr << 16) | ent_id_lwr
+   #}
+   
+   node.submesh_start, node.submesh_count, node.armature_id = \
+         sr_compile_mesh_internal( obj )
+
    sr_compile.mesh_data.extend(bytearray(node))
 #}
 
@@ -896,7 +1075,7 @@ def sr_compile_fonts( collection ):
       font.glyph_start = glyph_count
 
       glyph_base = data.glyphs[0].utf32
-      glyph_range = data.glyphs[-1].utf32 - glyph_base
+      glyph_range = data.glyphs[-1].utf32+1 - glyph_base
 
       font.glyph_utf32_base = glyph_base
       font.glyph_count = glyph_range
@@ -1007,6 +1186,75 @@ def sr_compile_fonts( collection ):
    #}
 #}
 
+def sr_compile_menus( collection ):
+#{
+   print( "[SR1] Compiling menus" )
+   groups = []
+
+   for obj in collection.all_objects:#{
+      if obj_ent_type(obj) != 'ent_menuitem': continue
+      obj_data = obj.SR_data.ent_menuitem[0]
+
+      bitmask = 0x00000000
+
+      for col in obj.users_collection:#{
+         name = col.name
+         if name not in groups: groups.append( name )
+         bitmask |= (0x1 << groups.index(name))
+      #}
+
+      item = ent_menuitem()
+      item.type = int( obj_data.tipo )
+      item.groups = bitmask
+
+      compile_obj_transform( obj, item.transform )
+      if obj.type == 'MESH':#{
+         item.submesh_start, item.submesh_count, _ = \
+               sr_compile_mesh_internal( obj )
+      #}
+
+      if item.type == 1 or item.type == 2:#{
+         item_button = item._anonymous_union.button
+         item_button.pstr = sr_compile_string( obj_data.string )
+      #}
+      elif item.type == 3:#{
+         item_checkmark = item._anonymous_union.checkmark
+         item_checkmark.pstr_data = sr_compile_string( obj_data.string )
+         item_checkmark.id_check = sr_entity_id( obj_data.checkmark )
+         delta = obj_data.checkmark.location - obj.location
+         item_checkmark.offset[0] =  delta[0]
+         item_checkmark.offset[1] =  delta[2]
+         item_checkmark.offset[2] = -delta[1]
+      #}
+      elif item.type == 4:#{
+         item_slider = item._anonymous_union.slider
+         item_slider.id_min = sr_entity_id( obj_data.slider_minloc )
+         item_slider.id_max = sr_entity_id( obj_data.slider_maxloc )
+         item_slider.id_handle = sr_entity_id( obj_data.slider_handle )
+         item_slider.pstr_data = sr_compile_string( obj_data.string )
+      #}
+      elif item.type == 5:#{
+         item_page = item._anonymous_union.page
+         item_page.pstr_name = sr_compile_string( obj_data.string )
+         item_page.id_entrypoint = sr_entity_id( obj_data.newloc )
+         item_page.id_viewpoint = sr_entity_id( obj_data.camera )
+      #}
+
+      if obj_data.link0:
+         item.id_links[0] = sr_entity_id( obj_data.link0 )
+      if obj_data.link1:
+         item.id_links[1] = sr_entity_id( obj_data.link1 )
+      if item.type != 4:#{
+         if obj_data.link2:
+            item.id_links[2] = sr_entity_id( obj_data.link2 )
+         if obj_data.link3:
+            item.id_links[3] = sr_entity_id( obj_data.link3 )
+      #}
+
+      sr_ent_push( item )
+   #}
+#}
+
 def sr_compile_armature( obj ):
 #{
    node = mdl_armature()
@@ -1199,6 +1447,15 @@ def sr_array_title( arr, name, count, size, offset ):
    arr.item_size = size
 #}
 
+def hash_djb2(s):
+#{
+   picadillo = 5381
+   for x in s:#{
+      picadillo = (((picadillo << 5) + picadillo) + ord(x)) & 0xFFFFFFFF
+   #}
+   return picadillo
+#}
+
 def sr_compile( collection ):
 #{
    print( F"[SR] compiler begin ({collection.name}.mdl)" )
@@ -1237,9 +1494,16 @@ def sr_compile( collection ):
    sr_compile.entities = {}
    sr_compile.entity_ids = {}
 
+   # begin
+   # -------------------------------------------------------
+
+   sr_compile_string( "null" )
+
    mesh_count = 0
    for obj in collection.all_objects: #{
-      if obj.type == 'MESH': mesh_count += 1
+      if obj.type == 'MESH':#{
+         mesh_count += 1
+      #}
 
       ent_type = obj_ent_type( obj )
       if ent_type == 'none': continue
@@ -1254,6 +1518,17 @@ def sr_compile( collection ):
    for obj in collection.all_objects:#{
       if obj.type == 'MESH':#{
          i+=1
+
+         ent_type = obj_ent_type( obj )
+
+         # entity ignore mesh list
+         #
+         if ent_type == 'ent_traffic': continue
+         if ent_type == 'ent_font': continue
+         if ent_type == 'ent_font_variant': continue
+         if ent_type == 'ent_menuitem': continue
+         #--------------------------
+
          print( F'[SR] {i: 3}/{mesh_count} {obj.name:<40}', end='\r' )
          sr_compile_mesh( obj )
       #}
@@ -1288,15 +1563,30 @@ def sr_compile( collection ):
             light.colour[3] = obj.data.energy
             sr_ent_push( light )
          #}
+         elif ent_type == 'ent_camera': #{
+            cam = ent_camera()
+            compile_obj_transform( obj, cam.transform )
+            cam.fov = obj.data.angle * 45.0
+            sr_ent_push(cam)
+         #}
          elif ent_type == 'ent_gate': #{
             gate = ent_gate()
-            gate.type = 0
             obj_data = obj.SR_data.ent_gate[0]
             mesh_data = obj.data.SR_data.ent_gate[0]
-            if obj_data.target:#{
-               gate.target = sr_compile.entity_ids[obj_data.target.name]
-               gate.type = 1
+
+            if obj_data.tipo == 'default':#{
+               if obj_data.target:#{
+                  gate.target = sr_compile.entity_ids[obj_data.target.name]
+                  gate.type = 1
+               #}
+            #}
+            elif obj_data.tipo == 'nonlocal':#{
+               gate.target = 0
+               gate.key = sr_compile_string(obj_data.key)
+               gate.type = 2
             #}
+            else: gate.type = 0
+            
             gate.dimensions[0] = mesh_data.dimensions[0]
             gate.dimensions[1] = mesh_data.dimensions[1]
             gate.dimensions[2] = mesh_data.dimensions[2]
@@ -1391,9 +1681,7 @@ def sr_compile( collection ):
             compile_obj_transform( obj, volume.transform )
 
             if obj_data.target:#{
-               target = obj_data.target
-               volume.target.type = sr_entity_alias[obj_ent_type(target)]
-               volume.target.index = sr_compile.entity_ids[ target.name ]
+               volume.target = sr_entity_id( obj_data.target )
             #}
 
             sr_ent_push(volume)
@@ -1404,10 +1692,58 @@ def sr_compile( collection ):
             compile_obj_transform( obj, marker.transform )
             sr_ent_push(marker)
          #}
+         elif ent_type == 'ent_skateshop':#{
+            skateshop = ent_skateshop()
+            obj_data = obj.SR_data.ent_skateshop[0]
+            skateshop.type = int(obj_data.tipo)
+            if skateshop.type == 0:#{
+               boardshop = skateshop._anonymous_union.boards
+               boardshop.id_display = sr_entity_id( obj_data.mark_display )
+               boardshop.id_info = sr_entity_id( obj_data.mark_info )
+               boardshop.id_rack = sr_entity_id( obj_data.mark_rack )
+            #}
+            elif skateshop.type == 1:#{
+               charshop = skateshop._anonymous_union.character
+               charshop.id_display = sr_entity_id( obj_data.mark_display )
+               charshop.id_info = sr_entity_id( obj_data.mark_info )
+            #}
+            elif skateshop.type == 2:#{
+               worldshop = skateshop._anonymous_union.worlds
+               worldshop.id_display = sr_entity_id( obj_data.mark_display )
+               worldshop.id_info = sr_entity_id( obj_data.mark_info )
+            #}
+            skateshop.id_camera = sr_entity_id( obj_data.cam )
+            compile_obj_transform( obj, skateshop.transform )
+            sr_ent_push(skateshop)
+         #}
+         elif ent_type == 'ent_swspreview':#{
+            workshop_preview = ent_swspreview()
+            obj_data = obj.SR_data.ent_swspreview[0]
+            workshop_preview.id_display = sr_entity_id( obj_data.mark_display )
+            workshop_preview.id_display1 = sr_entity_id( obj_data.mark_display1)
+            workshop_preview.id_camera = sr_entity_id( obj_data.cam )
+            sr_ent_push( workshop_preview )
+         #}
+         elif ent_type == 'ent_worldinfo':#{
+            worldinfo = ent_worldinfo()
+            obj_data = obj.SR_data.ent_worldinfo[0]
+            worldinfo.pstr_name = sr_compile_string( obj_data.name )
+            worldinfo.pstr_author = sr_compile_string( obj_data.author )
+            worldinfo.pstr_desc = sr_compile_string( obj_data.desc )
+            worldinfo.timezone = obj_data.timezone
+            sr_ent_push( worldinfo )
+         #}
+         elif ent_type == 'ent_ccmd':#{
+            ccmd = ent_ccmd()
+            obj_data = obj.SR_data.ent_ccmd[0]
+            ccmd.pstr_command = sr_compile_string( obj_data.command )
+            sr_ent_push( ccmd )
+         #}
       #}
    #}
 
-   sr_compile_fonts(collection)
+   sr_compile_menus( collection )
+   sr_compile_fonts( collection )
 
    def _children( col ):#{
       yield col
@@ -1425,6 +1761,7 @@ def sr_compile( collection ):
       route_gates = []
       route_curves = []
       routes = []
+      traffics = []
 
       for obj in col.objects:#{
          if obj.type == 'ARMATURE': pass
@@ -1440,6 +1777,8 @@ def sr_compile( collection ):
             #}
             elif ent_type == 'ent_route':
                routes += [obj]
+            elif ent_type == 'ent_traffic':
+               traffics += [obj]
          #}
       #}
 
@@ -1498,6 +1837,52 @@ def sr_compile( collection ):
          sr_ent_push( route )
       #}
 
+      for obj in traffics:#{
+         traffic = ent_traffic()
+         compile_obj_transform( obj, traffic.transform )
+         traffic.submesh_start, traffic.submesh_count, _ = \
+               sr_compile_mesh_internal( obj )
+         
+         # find best subsection
+         
+         graph_keys = list(dij.graph)
+         min_dist = 100.0
+         best_point = 0
+
+         for j in range(len(dij.points)):#{
+            point = dij.points[j]
+            dist = (point-obj.location).magnitude
+
+            if dist < min_dist:#{
+               min_dist = dist
+               best_point = j
+            #}
+         #}
+
+         # scan to each edge
+         best_begin = best_point
+         best_end = best_point
+
+         while True:#{
+            map0 = dij.subsections[best_begin]
+            if map0[1] == -1: break
+            best_begin = map0[1]
+         #}
+         while True:#{
+            map1 = dij.subsections[best_end]
+            if map1[2] == -1: break
+            best_end = map1[2]
+         #}
+
+         traffic.start_node = routenode_count + best_begin
+         traffic.node_count = best_end - best_begin
+         traffic.index = best_point - best_begin
+         traffic.speed = obj.SR_data.ent_traffic[0].speed
+         traffic.t = 0.0
+
+         sr_ent_push(traffic)
+      #}
+
       for point in dij.points:#{
          rn = ent_route_node()
          rn.co[0] =  point[0]
@@ -1509,7 +1894,6 @@ def sr_compile( collection ):
       routenode_count += len(dij.points)
    #}
 
-
    print( F"[SR] Writing file" )
 
    file_array_instructions = {}
@@ -1550,9 +1934,10 @@ def sr_compile( collection ):
    path = F"{folder}{collection.name}.mdl"
    print( path )
 
+   os.makedirs(os.path.dirname(path),exist_ok=True)
    fp = open( path, "wb" )
    header = mdl_header()
-   header.version = 40
+   header.version = 101
    sr_array_title( header.arrays, \
                    'index', len(file_array_instructions), \
                    sizeof(mdl_array), header_size )
@@ -1858,9 +2243,16 @@ class SR_MATERIAL_PANEL(bpy.types.Panel):
       _.layout.prop( active_mat.SR_data, "collision" )
 
       if active_mat.SR_data.collision:#{
-         _.layout.prop( active_mat.SR_data, "skate_surface" )
-         _.layout.prop( active_mat.SR_data, "grind_surface" )
-         _.layout.prop( active_mat.SR_data, "grow_grass" )
+         box = _.layout.box()
+         row = box.row()
+
+         if (active_mat.SR_data.shader != 'invisible') and \
+            (active_mat.SR_data.shader != 'boundary'):#{
+            row.prop( active_mat.SR_data, "skate_surface" )
+            row.prop( active_mat.SR_data, "grind_surface" )
+            row.prop( active_mat.SR_data, "grow_grass" )
+            row.prop( active_mat.SR_data, "preview_visibile" )
+         #}
       #}
 
       if active_mat.SR_data.shader == "terrain_blend":#{
@@ -1926,6 +2318,20 @@ class SR_OBJECT_ENT_GATE(bpy.types.PropertyGroup):
    target: bpy.props.PointerProperty( \
                type=bpy.types.Object, name="destination", \
                poll=lambda self,obj: sr_filter_ent_type(obj,['ent_gate']))
+
+   key: bpy.props.StringProperty()
+   tipo: bpy.props.EnumProperty(items=(('default', 'Default', ""),
+                                       ('nonlocal', 'Non-Local', ""),))
+
+   @staticmethod
+   def sr_inspector( layout, data ):
+   #{
+      box = layout.box()
+      box.prop( data[0], 'tipo', text="subtype" )
+
+      if   data[0].tipo == 'default':  box.prop( data[0], 'target' )
+      elif data[0].tipo == 'nonlocal': box.prop( data[0], 'key' )
+   #}
 #}
 
 class SR_MESH_ENT_GATE(bpy.types.PropertyGroup):
@@ -2228,7 +2634,8 @@ class SR_UL_FONT_GLYPH_LIST(bpy.types.UIList):
       s1 = c.split(factor=0.3)
       c = s1.column()
       row = c.row()
-      lbl = chr(item.utf32) if item.utf32 >= 32 and item.utf32 <= 126 else 'ERR'
+      lbl = chr(item.utf32) if item.utf32 >= 32 and item.utf32 <= 126 else \
+                                                              f'x{item.utf32:x}'
       row.label(text=lbl)
       c = s1.column()
       c.prop( item, 'utf32', text='', emboss=True )
@@ -2281,7 +2688,8 @@ class SR_OBJECT_ENT_VOLUME(bpy.types.PropertyGroup):
 
    target: bpy.props.PointerProperty( \
            type=bpy.types.Object, name="Target", \
-           poll=lambda self,obj: sr_filter_ent_type(obj,['ent_audio']))
+           poll=lambda self,obj: sr_filter_ent_type(obj,\
+                                    ['ent_audio','ent_skateshop','ent_ccmd']))
 
    @staticmethod
    def sr_inspector( layout, data ):
@@ -2352,16 +2760,15 @@ class SR_OBJECT_ENT_AUDIO(bpy.types.PropertyGroup):
       box.prop( data[0], 'flag_loop' )
       box.prop( data[0], 'flag_auto' )
 
+      layout.prop( data[0], 'probability_curve' )
+
       split = layout.split(factor=0.7)
       c = split.column()
       c.label( text='Filepath' )
       c = split.column()
-      c.label( text='Chance (0.1s)' )
-
-      layout.prop( data[0], 'probability_curve' )
-
+      c.label( text='Chance' )
       layout.template_list('SR_UL_AUDIO_LIST', 'Files', \
-                            data[0], 'files', data[0], 'file_index', rows=5)
+                            data[0], 'files', data[0], 'files_index', rows=5)
 
       row = layout.row()
       row.operator( 'skaterift.al_new_entry', text='Add' )
@@ -2427,6 +2834,150 @@ class SR_OBJECT_ENT_FONT(bpy.types.PropertyGroup):
    #}
 #}
 
+class SR_OBJECT_ENT_TRAFFIC(bpy.types.PropertyGroup):
+#{
+   speed: bpy.props.FloatProperty(default=1.0)
+#}
+
+class SR_OBJECT_ENT_SKATESHOP(bpy.types.PropertyGroup):
+#{
+   tipo: bpy.props.EnumProperty( name='Type',
+                                 items=[('0','boards',''),
+                                        ('1','character',''),
+                                        ('2','world','')] )
+   mark_rack: bpy.props.PointerProperty( \
+           type=bpy.types.Object, name="Board Rack", \
+           poll=lambda self,obj: sr_filter_ent_type(obj,['ent_marker']))
+   mark_display: bpy.props.PointerProperty( \
+           type=bpy.types.Object, name="Selected Board Display", \
+           poll=lambda self,obj: sr_filter_ent_type(obj,['ent_marker']))
+   mark_info: bpy.props.PointerProperty( \
+           type=bpy.types.Object, name="Selected Board Info", \
+           poll=lambda self,obj: sr_filter_ent_type(obj,['ent_marker']))
+   cam: bpy.props.PointerProperty( \
+           type=bpy.types.Object, name="Viewpoint", \
+           poll=lambda self,obj: sr_filter_ent_type(obj,['ent_camera']))
+#}
+
+class SR_OBJECT_ENT_WORKSHOP_PREVIEW(bpy.types.PropertyGroup):
+#{
+   mark_display: bpy.props.PointerProperty( \
+           type=bpy.types.Object, name="Board Display", \
+           poll=lambda self,obj: sr_filter_ent_type(obj,['ent_marker']))
+   mark_display1: bpy.props.PointerProperty( \
+           type=bpy.types.Object, name="Board Display (other side)", \
+           poll=lambda self,obj: sr_filter_ent_type(obj,['ent_marker']))
+   cam: bpy.props.PointerProperty( \
+           type=bpy.types.Object, name="Viewpoint", \
+           poll=lambda self,obj: sr_filter_ent_type(obj,['ent_camera']))
+#}
+
+class SR_OBJECT_ENT_MENU_ITEM(bpy.types.PropertyGroup):
+#{
+   link0: bpy.props.PointerProperty( \
+           type=bpy.types.Object, name="Link 0", \
+           poll=lambda self,obj: sr_filter_ent_type(obj,['ent_menuitem']))
+   link1: bpy.props.PointerProperty( \
+           type=bpy.types.Object, name="Link 1", \
+           poll=lambda self,obj: sr_filter_ent_type(obj,['ent_menuitem']))
+   link2: bpy.props.PointerProperty( \
+           type=bpy.types.Object, name="Link 2", \
+           poll=lambda self,obj: sr_filter_ent_type(obj,['ent_menuitem']))
+   link3: bpy.props.PointerProperty( \
+           type=bpy.types.Object, name="Link 3", \
+           poll=lambda self,obj: sr_filter_ent_type(obj,['ent_menuitem']))
+
+   newloc: bpy.props.PointerProperty( \
+           type=bpy.types.Object, name="New location", \
+           poll=lambda self,obj: sr_filter_ent_type(obj,['ent_menuitem']))
+   camera: bpy.props.PointerProperty( \
+           type=bpy.types.Object, name="Camera", \
+           poll=lambda self,obj: sr_filter_ent_type(obj,['ent_camera']))
+
+   slider_minloc: bpy.props.PointerProperty( \
+           type=bpy.types.Object, name="Slider min", \
+           poll=lambda self,obj: sr_filter_ent_type(obj,['ent_marker']))
+   slider_maxloc: bpy.props.PointerProperty( \
+           type=bpy.types.Object, name="Slider max", \
+           poll=lambda self,obj: sr_filter_ent_type(obj,['ent_marker']))
+   slider_handle: bpy.props.PointerProperty( \
+           type=bpy.types.Object, name="Slider handle", \
+           poll=lambda self,obj: sr_filter_ent_type(obj,['ent_menuitem']))
+
+   checkmark: bpy.props.PointerProperty( \
+           type=bpy.types.Object, name="Checked", \
+           poll=lambda self,obj: sr_filter_ent_type(obj,['ent_menuitem']))
+
+   string: bpy.props.StringProperty( name="String" )
+   tipo: bpy.props.EnumProperty( name='Type',
+                                 items=[('0','visual',''),
+                                        ('1','event button',''),
+                                        ('2','page button',''),
+                                        ('3','toggle', ''),
+                                        ('4','slider',''),
+                                        ('5','page','')])
+
+   @staticmethod
+   def sr_inspector( layout, data ):
+   #{
+      data = data[0]
+      box = layout.box()
+      box.prop( data, 'tipo' )
+
+      if data.tipo == '0':#{
+         return
+      #}
+      elif data.tipo == '1':#{
+         box.prop( data, 'string', text='Event' )
+      #}
+      elif data.tipo == '2':#{
+         box.prop( data, 'string', text='Page' )
+         box.prop( data, 'newloc' )
+      #}
+      elif data.tipo == '3':#{
+         box.prop( data, 'string', text='Data (i32)' )
+         box.prop( data, 'checkmark' )
+      #}
+      elif data.tipo == '4':#{
+         box.prop( data, 'string', text='Data (f32)' )
+         box.prop( data, 'slider_minloc' )
+         box.prop( data, 'slider_maxloc' )
+         box.prop( data, 'slider_handle' )
+         box = box.box()
+         box.label( text="Links" )
+         box.prop( data, 'link0', text='v0' )
+         box.prop( data, 'link1', text='v1' )
+         return
+      #}
+      elif data.tipo == '5':#{
+         box.prop( data, 'string', text='Page Name' )
+         box.prop( data, 'newloc', text='Entry Point' )
+         box.prop( data, 'camera', text='Viewpoint' )
+         return
+      #}
+      
+      box = box.box()
+      box.label( text="Links" )
+      box.prop( data, 'link0' )
+      box.prop( data, 'link1' )
+      box.prop( data, 'link2' )
+      box.prop( data, 'link3' )
+   #}
+#}
+
+class SR_OBJECT_ENT_WORLD_INFO(bpy.types.PropertyGroup):
+#{
+   name: bpy.props.StringProperty(name="Name")
+   desc: bpy.props.StringProperty(name="Description")
+   author: bpy.props.StringProperty(name="Author")
+   timezone: bpy.props.FloatProperty(name="Timezone(hrs) (UTC0 +hrs)")
+#}
+
+class SR_OBJECT_ENT_CCMD(bpy.types.PropertyGroup):
+#{
+   command: bpy.props.StringProperty(name="Command Line")
+#}
+
 class SR_OBJECT_PROPERTIES(bpy.types.PropertyGroup):
 #{
    ent_gate: bpy.props.CollectionProperty(type=SR_OBJECT_ENT_GATE)
@@ -2437,19 +2988,17 @@ class SR_OBJECT_PROPERTIES(bpy.types.PropertyGroup):
    ent_marker: bpy.props.CollectionProperty(type=SR_OBJECT_ENT_MARKER)
    ent_glyph: bpy.props.CollectionProperty(type=SR_OBJECT_ENT_GLYPH)
    ent_font: bpy.props.CollectionProperty(type=SR_OBJECT_ENT_FONT)
+   ent_traffic: bpy.props.CollectionProperty(type=SR_OBJECT_ENT_TRAFFIC)
+   ent_skateshop: bpy.props.CollectionProperty(type=SR_OBJECT_ENT_SKATESHOP)
+   ent_swspreview: \
+         bpy.props.CollectionProperty(type=SR_OBJECT_ENT_WORKSHOP_PREVIEW)
+   ent_menuitem: bpy.props.CollectionProperty(type=SR_OBJECT_ENT_MENU_ITEM)
+   ent_worldinfo: bpy.props.CollectionProperty(type=SR_OBJECT_ENT_WORLD_INFO)
+   ent_ccmd: bpy.props.CollectionProperty(type=SR_OBJECT_ENT_CCMD)
+
    ent_type: bpy.props.EnumProperty(
       name="Type",
-      items=[('none', 'None', '', 0),
-             ('ent_gate','Gate','', 1),
-             ('ent_spawn','Spawn','', 2),
-             ('ent_route_node', 'Route Node', '', 3 ),
-             ('ent_route', 'Route', '', 4),
-             ('ent_water', 'Water Surface', '', 5),
-             ('ent_volume', 'Volume', '', 6 ),
-             ('ent_audio', 'Audio Files', '', 7),
-             ('ent_marker', 'Marker', '', 8),
-             ('ent_font', 'Font', '', 9),
-             ('ent_font_variant','Font variant','',10)],
+      items=sr_entity_list,
       update=sr_on_type_change
    )
 #}
@@ -2518,7 +3067,9 @@ class SR_MATERIAL_PROPERTIES(bpy.types.PropertyGroup):
       ('standard_cutout', "standard_cutout", ''),
       ('terrain_blend', "terrain_blend", ''),
       ('vertex_blend', "vertex_blend", ''),
-      ('water',"water",'')
+      ('water',"water",''),
+      ('invisible','Invisible',''),
+      ('boundary','Boundary','')
       ])
 
    surface_prop: bpy.props.EnumProperty(
@@ -2534,23 +3085,28 @@ class SR_MATERIAL_PROPERTIES(bpy.types.PropertyGroup):
    collision: bpy.props.BoolProperty( \
          name="Collisions Enabled",\
          default=True,\
-         description = "Can the player collide with this material"\
+         description = "Can the player collide with this material?"\
    )
    skate_surface: bpy.props.BoolProperty( \
-         name="Skate Surface", \
+         name="Skate Target", \
          default=True,\
          description = "Should the game try to target this surface?" \
    )
    grind_surface: bpy.props.BoolProperty( \
-         name="Grind Surface", \
-         default=False,\
-         description = "Grind face?" \
+         name="Grindable", \
+         default=True,\
+         description = "Can you grind on this surface?" \
    )
    grow_grass: bpy.props.BoolProperty( \
          name="Grow Grass", \
          default=False,\
          description = "Spawn grass sprites on this surface?" \
    )
+   preview_visibile: bpy.props.BoolProperty( \
+         name="Preview visibile", \
+         default=True,\
+         description = "Show this material in preview models?" \
+   )
    blend_offset: bpy.props.FloatVectorProperty( \
          name="Blend Offset", \
          size=2, \
@@ -3395,6 +3951,134 @@ def cv_draw():
                #}
             #}
          #}
+         elif ent_type == 'ent_skateshop':#{
+            data = obj.SR_data.ent_skateshop[0]
+            display = data.mark_display
+            info = data.mark_info
+
+            if data.tipo == '0':#{
+               cc = (0.0,0.9,0.6)
+               cc1 = (0.4,0.9,0.2)
+               cc2 = (0.9,0.6,0.1)
+
+               rack = data.mark_rack
+
+               rack_cu = Vector((3.15,2.0,0.1))*0.5
+               rack_co = Vector((0.0,0.0,0.0))
+               display_cu = Vector((0.3,1.2,0.1))*0.5
+               display_co = Vector((0.0,0.0,0.1))*0.5
+               info_cu = Vector((1.2,0.01,0.3))*0.5
+               info_co = Vector((0.0,0.0,0.0))*0.5
+            #}
+            elif data.tipo == '1':#{
+               rack = None
+               cc1 = (1.0,0.0,0.0)
+               cc2 = (1.0,0.5,0.0)
+               display_cu = Vector((0.4,0.4,2.0))*0.5
+               display_co = Vector((0.0,0.0,1.0))*0.5
+               info_cu = Vector((1.2,0.01,0.3))*0.5
+               info_co = Vector((0.0,0.0,0.0))*0.5
+            #}
+            elif data.tipo == '2':#{
+               rack = None
+               cc1 = (1.0,0.0,0.0)
+               cc2 = (1.0,0.5,0.0)
+               display_cu = Vector((1.0,1.0,0.5))*0.5
+               display_co = Vector((0.0,0.0,0.5))*0.5
+               info_cu = Vector((1.2,0.01,0.3))*0.5
+               info_co = Vector((0.0,0.0,0.0))*0.5
+            #}
+
+            if rack:
+               cv_draw_ucube( rack.matrix_world, cc, rack_cu, rack_co )
+            if display:
+               cv_draw_ucube( display.matrix_world, cc1, display_cu, display_co)
+            if info:
+               cv_draw_ucube( info.matrix_world, cc2, info_cu, info_co )
+         #}
+         elif ent_type == 'ent_swspreview':#{
+            cc1 = (0.4,0.9,0.2)
+            data = obj.SR_data.ent_swspreview[0]
+            display = data.mark_display
+            display1 = data.mark_display1
+            display_cu = Vector((0.3,1.2,0.1))*0.5
+            display_co = Vector((0.0,0.0,0.1))*0.5
+            if display:
+               cv_draw_ucube( display.matrix_world, cc1, display_cu, display_co)
+            if display1:
+               cv_draw_ucube(display1.matrix_world, cc1, display_cu, display_co)
+         #}
+         elif ent_type == 'ent_menuitem':#{
+            for i,col in enumerate(obj.users_collection):#{
+               colour32 = hash_djb2( col.name )
+               r = pow(((colour32    ) & 0xff) / 255.0, 2.2 )
+               g = pow(((colour32>>8 ) & 0xff) / 255.0, 2.2 )
+               b = pow(((colour32>>16) & 0xff) / 255.0, 2.2 )
+               cc = (r,g,b)
+               vs = [None for _ in range(8)]
+               scale = i*0.02
+               for j in range(8):#{
+                  v0 = Vector([(obj.bound_box[j][z]+\
+                        ((-1.0 if obj.bound_box[j][z]<0.0 else 1.0)*scale)) \
+                           for z in range(3)])
+                  vs[j] = obj.matrix_world @ v0
+               #}
+               indices = [(0,1),(1,2),(2,3),(3,0),(4,5),(5,6),(6,7),(7,4),\
+                          (0,4),(1,5),(2,6),(3,7)]
+               for l in indices:#{
+                  v0 = vs[l[0]]
+                  v1 = vs[l[1]]
+                  cv_view_verts += [(v0[0],v0[1],v0[2])]
+                  cv_view_verts += [(v1[0],v1[1],v1[2])]
+                  cv_view_colours += [cc,cc]
+               #}
+            #}
+            cv_draw_lines()
+            cc = (1.0,1.0,1.0)
+            data = obj.SR_data.ent_menuitem[0]
+            if data.tipo == '4':#{
+               if data.slider_minloc and data.slider_maxloc:#{
+                  v0 = data.slider_minloc.location
+                  v1 = data.slider_maxloc.location
+                  cv_draw_line( v0, v1, cc )
+               #}
+            #}
+
+            colour32 = hash_djb2(obj.name)
+            r = ((colour32    ) & 0xff) / 255.0
+            g = ((colour32>>8 ) & 0xff) / 255.0
+            b = ((colour32>>16) & 0xff) / 255.0
+            cc = (r,g,b)
+            origin = obj.location + (Vector((r,g,b))*2.0-Vector((1.0,1.0,1.0)))\
+                        * 0.04
+
+            size = 0.01
+
+            if data.tipo != '0':#{
+               if data.tipo == '4':#{
+                  if data.link0:#{
+                     cv_draw_arrow( origin, data.link0.location, cc, size )
+                  #}
+                  if data.link1:#{
+                     cv_draw_arrow( origin, data.link1.location, cc, size )
+                  #}
+               #}
+               else:#{
+                  if data.link0:#{
+                     cv_draw_arrow( origin, data.link0.location, cc, size )
+                  #}
+                  if data.link1:#{
+                     cv_draw_arrow( origin, data.link1.location, cc, size )
+                  #}
+                  if data.link2:#{
+                     cv_draw_arrow( origin, data.link2.location, cc, size )
+                  #}
+                  if data.link3:#{
+                     cv_draw_arrow( origin, data.link3.location, cc, size )
+                  #}
+               #}
+            #}
+         #}
       #}
    #}
 
@@ -3428,7 +4112,9 @@ classes = [ SR_INTERFACE, SR_MATERIAL_PANEL,\
             SR_OBJECT_ENT_FONT_VARIANT,
             SR_OBJECT_ENT_GLYPH_ENTRY,\
             SR_UL_FONT_VARIANT_LIST,SR_UL_FONT_GLYPH_LIST,\
-            SR_OBJECT_ENT_FONT,\
+            SR_OBJECT_ENT_FONT,SR_OBJECT_ENT_TRAFFIC,SR_OBJECT_ENT_SKATESHOP,\
+            SR_OBJECT_ENT_WORKSHOP_PREVIEW,SR_OBJECT_ENT_MENU_ITEM,\
+            SR_OBJECT_ENT_WORLD_INFO,SR_OBJECT_ENT_CCMD,\
             \
             SR_OBJECT_PROPERTIES, SR_LIGHT_PROPERTIES, SR_BONE_PROPERTIES, 
             SR_MESH_PROPERTIES, SR_MATERIAL_PROPERTIES \