nonlocal stuff again
[carveJwlIkooP6JGAAIwe30JlM.git] / blender_export.py
index 721bba6962b4fd5bc3ecc27d0a984761acde58c1..c8639711a06a352c4a852945ea929c22e693aecd 100644 (file)
@@ -3,6 +3,7 @@ import cProfile
 from ctypes import *
 from mathutils import *
 from gpu_extras.batch import batch_for_shader
+from bpy_extras import mesh_utils
 
 bl_info = {
    "name":"Skaterift .mdl exporter",
@@ -16,6 +17,18 @@ 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
+}
+
 class mdl_vert(Structure):              # 48 bytes. Quite large. Could compress
 #{                                      # the normals and uvs to i16s. Not an
    _pack_ = 1                           # real issue, yet.
@@ -160,7 +173,8 @@ class ent_gate(Structure):
                ("transport",(c_float*3)*4),
                ("_anonymous_union",version_refcount_union),
                ("timing_time",c_double),
-               ("routes",c_uint16*4)]
+               ("routes",c_uint16*4),
+               ("route_count",c_uint8)]
 #}
 
 class ent_route_node(Structure):
@@ -175,6 +189,26 @@ class ent_path_index(Structure):
    _fields_ = [("index",c_uint16)]
 #}
 
+class vg_audio_clip(Structure):
+#{
+   _fields_ = [("path",c_uint64),
+               ("flags",c_uint32),
+               ("size",c_uint32),
+               ("data",c_uint64)]
+#}
+
+class union_file_audio_clip(Union):
+#{
+   _fields_ = [("file",mdl_file),
+               ("reserved",vg_audio_clip)]
+#}
+
+class ent_audio_clip(Structure):
+#{
+   _fields_ = [("_anon",union_file_audio_clip),
+               ("probability",c_float)]
+#}
+
 class ent_checkpoint(Structure):
 #{
    _fields_ = [("gate_index",c_uint16),
@@ -182,7 +216,7 @@ class ent_checkpoint(Structure):
                ("path_count",c_uint16)]
 #}
 
-class ent_route(Structure):
+class ent_route(Structure): 
 #{
    _fields_ = [("transform",mdl_transform),
                ("pstr_name",c_uint32),
@@ -196,6 +230,91 @@ class ent_route(Structure):
                ("latest_pass",c_double)]
 #}
 
+class ent_water(Structure):
+#{
+   _fields_ = [("transform",mdl_transform),
+               ("max_dist",c_float),
+               ("reserved0",c_uint32),
+               ("reserved1",c_uint32)]
+#}
+
+class volume_trigger(Structure):
+#{
+   _fields_ = [("event",c_uint32),
+               ("blank",c_uint32)]
+#}
+
+class volume_particles(Structure):
+#{
+   _fields_ = [("blank",c_uint32),
+               ("blank2",c_uint32)]
+#}
+
+class volume_union(Union):
+#{
+   _fields_ = [("trigger",volume_trigger),
+               ("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),
+               ("_anon",volume_union)]
+#}
+
+class ent_audio(Structure):
+#{
+   _fields_ = [("transform",mdl_transform),
+               ("flags",c_uint32),
+               ("clip_start",c_uint32),
+               ("clip_count",c_uint32),
+               ("volume",c_float),
+               ("crossfade",c_float),
+               ("channel_behaviour",c_uint32),
+               ("group",c_uint32),
+               ("probability_curve",c_uint32),
+               ("max_channels",c_uint32)]
+#}
+
+class ent_marker(Structure):
+#{
+   _fields_ = [("transform",mdl_transform),
+               ("name",c_uint32)]
+#}
+
+class ent_glyph(Structure):
+#{
+   _fields_ = [("size",c_float*2),
+               ("indice_start",c_uint32),
+               ("indice_count",c_uint32)]
+#}
+
+class ent_font_variant(Structure):
+#{
+   _fields_ = [("name",c_uint32),
+               ("material_id",c_uint32)]
+#}
+
+class ent_font(Structure):
+#{
+   _fields_ = [("alias",c_uint32),
+               ("variant_start",c_uint32),
+               ("variant_count",c_uint32),
+               ("glyph_start",c_uint32),
+               ("glyph_count",c_uint32),
+               ("glyph_utf32_base",c_uint32)]
+#}
+
 def obj_ent_type( obj ):
 #{
    if obj.type == 'ARMATURE': return 'mdl_armature'
@@ -203,14 +322,14 @@ def obj_ent_type( obj ):
    else: return obj.SR_data.ent_type
 #}
 
-def sr_filter_ent_type( obj, ent_type ):
+def sr_filter_ent_type( obj, ent_types ):
 #{
    if obj == bpy.context.active_object: return False
 
    for c0 in obj.users_collection:#{
       for c1 in bpy.context.active_object.users_collection:#{
          if c0 == c1:#{
-            return ent_type == obj_ent_type( obj )
+            return  obj_ent_type( obj ) in ent_types
          #}
       #}
    #}
@@ -366,9 +485,6 @@ def material_info(mat):
 
                   _graph_read( from_node_def, from_node, depth+1 )
                #}
-               
-               # No definition! :(
-               #  TODO: Make a warning for this?
             #}
             else:#{
                if "default" in link_def:#{
@@ -388,6 +504,17 @@ def material_info(mat):
    return info
 #}
 
+def vg_str_bin( s ):
+#{
+   decoded = bytearray()
+   for i in range(len(s)//2):#{
+      c  = (ord(s[i*2+0])-0x41)
+      c |= (ord(s[i*2+1])-0x41)<<4
+      decoded.extend(bytearray(c_uint8(c))) #??
+   #}
+   return decoded
+#}
+
 def sr_pack_file( file, path, data ):
 #{
    file.path = sr_compile_string( path )
@@ -587,8 +714,7 @@ def sr_compile_mesh( obj ):
       # Write the vertex / indice data
       #
       for tri_index, tri in enumerate(data.loop_triangles):#{
-         if tri.material_index != material_id:
-            continue
+         if tri.material_index != material_id: continue
 
          for j in range(3):#{
             vert = data.vertices[tri.vertices[j]]
@@ -751,6 +877,136 @@ def sr_compile_mesh( obj ):
    sr_compile.mesh_data.extend(bytearray(node))
 #}
 
+def sr_compile_fonts( collection ):
+#{
+   print( F"[SR] Compiling fonts" )
+
+   glyph_count = 0
+   variant_count = 0
+
+   for obj in collection.all_objects:#{
+      if obj_ent_type(obj) != 'ent_font': continue
+
+      data = obj.SR_data.ent_font[0]
+
+      font=ent_font()
+      font.alias = sr_compile_string( data.alias )
+      font.variant_start = variant_count
+      font.variant_count = 0
+      font.glyph_start = glyph_count
+
+      glyph_base = data.glyphs[0].utf32
+      glyph_range = data.glyphs[-1].utf32 - glyph_base
+
+      font.glyph_utf32_base = glyph_base
+      font.glyph_count = glyph_range
+
+      for i in range(len(data.variants)):#{
+         data_var = data.variants[i]
+         if not data_var.mesh: continue 
+
+         mesh = data_var.mesh.data
+
+         variant = ent_font_variant()
+         variant.name = sr_compile_string( data_var.tipo )
+
+         # fonts (variants) only support one material each
+         mat = None
+         if len(mesh.materials) != 0:
+            mat = mesh.materials[0]
+         variant.material_id = sr_compile_material( mat )
+
+         font.variant_count += 1
+
+         islands = mesh_utils.mesh_linked_triangles(mesh)
+         centroids = [Vector((0,0)) for _ in range(len(islands))]
+
+         for j in range(len(islands)):#{
+            for tri in islands[j]:#{
+               centroids[j].x += tri.center[0]
+               centroids[j].y += tri.center[2]
+            #}
+
+            centroids[j] /= len(islands[j])
+         #}
+
+         for j in range(glyph_range):#{
+            data_glyph = data.glyphs[j]
+            glyph = ent_glyph()
+            glyph.indice_start = len(sr_compile.indice_data)//sizeof(c_uint32)
+            glyph.indice_count = 0
+            glyph.size[0] = data_glyph.bounds[2]
+            glyph.size[1] = data_glyph.bounds[3]
+
+            vertex_reference = {}
+
+            for k in range(len(islands)):#{
+               if centroids[k].x < data_glyph.bounds[0] or \
+                  centroids[k].x > data_glyph.bounds[0]+data_glyph.bounds[2] or\
+                  centroids[k].y < data_glyph.bounds[1] or \
+                  centroids[k].y > data_glyph.bounds[1]+data_glyph.bounds[3]:
+               #{
+                  continue
+               #}
+
+               for l in range(len(islands[k])):#{
+                  tri = islands[k][l]
+                  for m in range(3):#{
+                     vert = mesh.vertices[tri.vertices[m]]
+                     li = tri.loops[m]
+                     vi = mesh.loops[li].vertex_index
+                     
+                     # Gather vertex information
+                     #
+                     co      = [vert.co[_] for _ in range(3)]
+                     co[0]  -= data_glyph.bounds[0]
+                     co[2]  -= data_glyph.bounds[1]
+                     norm    = mesh.loops[li].normal
+                     uv      = (0,0)
+                     if mesh.uv_layers: uv = mesh.uv_layers.active.data[li].uv
+
+                     TOLERENCE = float(10**4)
+                     key = (int(co[0]*TOLERENCE+0.5),
+                            int(co[1]*TOLERENCE+0.5),
+                            int(co[2]*TOLERENCE+0.5),
+                            int(norm[0]*TOLERENCE+0.5),
+                            int(norm[1]*TOLERENCE+0.5),
+                            int(norm[2]*TOLERENCE+0.5),
+                            int(uv[0]*TOLERENCE+0.5),
+                            int(uv[1]*TOLERENCE+0.5))
+
+                     if key in vertex_reference:
+                        index = vertex_reference[key]
+                     else:#{
+                        vindex = len(sr_compile.vertex_data)//sizeof(mdl_vert)
+                        index = bytearray(c_uint32(vindex))
+                        vertex_reference[key] = index
+                        v = mdl_vert()
+                        v.co[0]   =  co[0]
+                        v.co[1]   =  co[2]
+                        v.co[2]   = -co[1]
+                        v.norm[0] =  norm[0]
+                        v.norm[1] =  norm[2]
+                        v.norm[2] = -norm[1]
+                        v.uv[0]   =  uv[0]
+                        v.uv[1]   =  uv[1]
+                        
+                        sr_compile.vertex_data.extend(bytearray(v))
+                     #}
+                     
+                     glyph.indice_count += 1
+                     sr_compile.indice_data.extend( index )
+                  #}
+               #}
+            #}
+            sr_ent_push( glyph )
+         #}
+         sr_ent_push( variant )
+      #}
+      sr_ent_push( font )
+   #}
+#}
+
 def sr_compile_armature( obj ):
 #{
    node = mdl_armature()
@@ -1003,8 +1259,7 @@ def sr_compile( collection ):
       #}
    #}
 
-   checkpoint_count = 0
-   pathindice_count = 0
+   audio_clip_count = 0
 
    for ent_type, arr in sr_compile.entities.items():#{
       print(F"[SR] Compiling {len(arr)} {ent_type}{'s' if len(arr)>1 else ''}")
@@ -1035,13 +1290,21 @@ def sr_compile( collection ):
          #}
          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 = 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]
@@ -1072,79 +1335,189 @@ def sr_compile( collection ):
             spawn = ent_spawn()
             compile_obj_transform( obj, spawn.transform )
             obj_data = obj.SR_data.ent_spawn[0]
-            spawn.pstr_name = sr_compile_string( obj_data.name )
+            spawn.pstr_name = sr_compile_string( obj_data.alias )
             sr_ent_push( spawn )
          #}
-         elif ent_type == 'ent_route': #{
-            obj_data = obj.SR_data.ent_route[0]
-            route = ent_route()
-            route.pstr_name = sr_compile_string( obj_data.alias ) #TODO
-            route.checkpoints_start = checkpoint_count
-            route.checkpoints_count = 0
-
-            for ci in range(3):
-               route.colour[ci] = obj_data.colour[ci]
-            route.colour[3] = 1.0
-
-            compile_obj_transform( obj, route.transform )
-
-            checkpoints = obj_data.gates
-            route_nodes = []
-
-            for uc in obj.users_collection[0].objects:#{
-               uc_type = obj_ent_type( uc )
-               if uc_type == 'ent_gate' or uc_type == 'ent_route_node':
-                  route_nodes += [uc]
+         elif ent_type == 'ent_water':#{
+            water = ent_water()
+            compile_obj_transform( obj, water.transform )
+            water.max_dist = 0.0
+            sr_ent_push( water )
+         #}
+         elif ent_type == 'ent_audio':#{
+            obj_data = obj.SR_data.ent_audio[0]
+            audio = ent_audio()
+            compile_obj_transform( obj, audio.transform )
+            audio.clip_start = audio_clip_count
+            audio.clip_count = len(obj_data.files)
+            audio_clip_count += audio.clip_count
+            audio.max_channels = obj_data.max_channels
+            audio.volume = obj_data.volume
+
+            # TODO flags: 
+            #   - allow/disable doppler
+            #   - channel group tags with random colours
+            #   - transition properties
+
+            if obj_data.flag_loop: audio.flags |= 0x1
+            if obj_data.flag_nodoppler: audio.flags |= 0x2
+            if obj_data.flag_3d: audio.flags |= 0x4
+            if obj_data.flag_auto: audio.flags |= 0x8
+            if   obj_data.formato == '0': audio.flags |= 0x000
+            elif obj_data.formato == '1': audio.flags |= 0x400
+            elif obj_data.formato == '2': audio.flags |= 0x1000
+
+            audio.channel_behaviour = int(obj_data.channel_behaviour)
+            if audio.channel_behaviour >= 1:#{
+               audio.group = obj_data.group
+            #}
+            if audio.channel_behaviour == 2:#{
+               audio.crossfade = obj_data.transition_duration
             #}
-            graph = node_graph( route_nodes )
+            audio.probability_curve = int(obj_data.probability_curve)
+
+            for ci in range(audio.clip_count):#{
+               entry = obj_data.files[ci]
+               clip = ent_audio_clip()
+               clip.probability = entry.probability
+               if obj_data.formato == '2':#{
+                  sr_pack_file( clip._anon.file, '', vg_str_bin(entry.path) ) 
+               #}
+               else:#{
+                  clip._anon.file.path = sr_compile_string( entry.path )
+                  clip._anon.file.pack_offset = 0
+                  clip._anon.file.pack_size = 0
+               #}
+               sr_ent_push( clip )
+            #}
+            sr_ent_push( audio )
+         #}
+         elif ent_type == 'ent_volume':#{
+            obj_data = obj.SR_data.ent_volume[0]
+            volume = ent_volume()
+            volume.type = int(obj_data.subtype)
+            compile_obj_transform( obj, volume.transform )
 
-            for i in range(len(checkpoints)):#{
-               gi = checkpoints[i].target
-               gj = checkpoints[(i+1)%len(checkpoints)].target
-               gate = gi
+            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 ]
+            #}
 
-               if gi:#{
-                  dest = gi.SR_data.ent_gate[0].target
-                  gi = dest
-               #}
+            sr_ent_push(volume)
+         #}
+         elif ent_type == 'ent_marker':#{
+            marker = ent_marker()
+            marker.name = sr_compile_string( obj.SR_data.ent_marker[0].alias )
+            compile_obj_transform( obj, marker.transform )
+            sr_ent_push(marker)
+         #}
+      #}
+   #}
 
-               if gi==gj: continue # error?
-               if not gi or not gj: continue
+   sr_compile_fonts(collection)
 
-               checkpoint = ent_checkpoint()
-               checkpoint.gate_index = sr_compile.entity_ids[gate.name]
-               checkpoint.path_start = pathindice_count
-               checkpoint.path_count = 0
-               
-               path = dijkstra( graph, gj.name, gi.name )
-               if path:#{
-                  for pi in range(1,len(path)-1):#{
-                     pathindice = ent_path_index()
-                     pathindice.index = sr_compile.entity_ids[path[pi]]
-                     sr_ent_push( pathindice )
-
-                     checkpoint.path_count += 1
-                     pathindice_count += 1
-                  #}
+   def _children( col ):#{
+      yield col
+      for c in col.children:#{
+         yield from _children(c)
+      #}
+   #}
+
+   checkpoint_count = 0
+   pathindice_count = 0
+   routenode_count = 0
+
+   for col in _children(collection):#{
+      print( F"Adding routes for subcollection: {col.name}" )
+      route_gates = []
+      route_curves = []
+      routes = []
+
+      for obj in col.objects:#{
+         if obj.type == 'ARMATURE': pass
+         else:#{
+            ent_type = obj_ent_type( obj )
+
+            if ent_type == 'ent_gate':
+               route_gates += [obj]
+            elif ent_type == 'ent_route_node':#{
+               if obj.type == 'CURVE':#{
+                  route_curves += [obj]
                #}
-               
-               sr_ent_push( checkpoint )
-               route.checkpoints_count += 1
-               checkpoint_count += 1
             #}
-
-            sr_ent_push( route )
+            elif ent_type == 'ent_route':
+               routes += [obj]
          #}
-         elif ent_type == 'ent_route_node':#{
-            rn = ent_route_node()
-            rn.co[0] =  obj.location[0]
-            rn.co[1] =  obj.location[2]
-            rn.co[2] = -obj.location[1]
-            sr_ent_push( rn )
+      #}
+
+      dij = create_node_graph( route_curves, route_gates )
+      
+      for obj in routes:#{
+         obj_data = obj.SR_data.ent_route[0]
+         route = ent_route()
+         route.pstr_name = sr_compile_string( obj_data.alias )
+         route.checkpoints_start = checkpoint_count
+         route.checkpoints_count = 0
+
+         for ci in range(3):
+            route.colour[ci] = obj_data.colour[ci]
+         route.colour[3] = 1.0
+
+         compile_obj_transform( obj, route.transform )
+         checkpoints = obj_data.gates
+
+         for i in range(len(checkpoints)):#{
+            gi = checkpoints[i].target
+            gj = checkpoints[(i+1)%len(checkpoints)].target
+            gate = gi
+
+            if gi:#{
+               dest = gi.SR_data.ent_gate[0].target
+               gi = dest
+            #}
+
+            if gi==gj: continue # error?
+            if not gi or not gj: continue
+
+            checkpoint = ent_checkpoint()
+            checkpoint.gate_index = sr_compile.entity_ids[gate.name]
+            checkpoint.path_start = pathindice_count
+            checkpoint.path_count = 0
+            
+            path = solve_graph( dij, gi.name, gj.name )
+
+            if path:#{
+               for pi in range(len(path)):#{
+                  pathindice = ent_path_index()
+                  pathindice.index = routenode_count + path[pi]
+                  sr_ent_push( pathindice )
+
+                  checkpoint.path_count += 1
+                  pathindice_count += 1
+               #}
+            #}
+            
+            sr_ent_push( checkpoint )
+            route.checkpoints_count += 1
+            checkpoint_count += 1
          #}
+
+         sr_ent_push( route )
       #}
+
+      for point in dij.points:#{
+         rn = ent_route_node()
+         rn.co[0] =  point[0]
+         rn.co[1] =  point[2]
+         rn.co[2] = -point[1]
+         sr_ent_push( rn )
+      #}
+
+      routenode_count += len(dij.points)
    #}
-   
+
+
    print( F"[SR] Writing file" )
 
    file_array_instructions = {}
@@ -1401,6 +1774,10 @@ class SR_INTERFACE(bpy.types.Panel):
          active_object = context.active_object
          if not active_object: return
 
+         _.layout.operator( 'skaterift.copy_entity_data', \
+               text=F'Copy entity data to {len(context.selected_objects)-1} '+\
+                    F'other objects' )
+
          box = _.layout.box()
          row = box.row()
          row.alignment = 'CENTER'
@@ -1446,7 +1823,7 @@ class SR_INTERFACE(bpy.types.Panel):
          elif active_object.type == 'LIGHT': #{
             _draw_prop_collection( [active_object.data.SR_data] )
          #}
-         elif active_object.type == 'EMPTY' or active_object.type == 'MESH': #{
+         elif active_object.type in ['EMPTY','CURVE','MESH']:#{
             box.prop( active_object.SR_data, "ent_type" )
             ent_type = active_object.SR_data.ent_type
             
@@ -1516,7 +1893,7 @@ class SR_MATERIAL_PANEL(bpy.types.Panel):
 def sr_get_type_enum( scene, context ):
 #{
    items = [('none','None',"")]
-   mesh_entities=['ent_gate']
+   mesh_entities=['ent_gate','ent_water']
    point_entities=['ent_spawn','ent_route_node','ent_route']
 
    for e in point_entities: items += [(e,e,'')]
@@ -1556,7 +1933,21 @@ 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'))
+               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):
@@ -1568,7 +1959,7 @@ class SR_OBJECT_ENT_ROUTE_ENTRY(bpy.types.PropertyGroup):
 #{
    target: bpy.props.PointerProperty( \
                type=bpy.types.Object, name='target', \
-               poll=lambda self,obj: sr_filter_ent_type(obj,'ent_gate'))
+               poll=lambda self,obj: sr_filter_ent_type(obj,['ent_gate']))
 #}
 
 class SR_UL_ROUTE_NODE_LIST(bpy.types.UIList):
@@ -1581,15 +1972,86 @@ class SR_UL_ROUTE_NODE_LIST(bpy.types.UIList):
    #}
 #}
 
+def internal_listdel_execute(self,context,ent_name,collection_name):
+#{
+   active_object = context.active_object
+   data = getattr(active_object.SR_data,ent_name)[0]
+   lista = getattr(data,collection_name)
+   index = getattr(data,F'{collection_name}_index')
+
+   lista.remove(index)
+
+   setattr(data,F'{collection_name}_index', min(max(0,index-1), len(lista)-1))
+   return{'FINISHED'}
+#}
+
+def internal_listadd_execute(self,context,ent_name,collection_name):
+#{
+   active_object = context.active_object
+   getattr(getattr(active_object.SR_data,ent_name)[0],collection_name).add()
+   return{'FINISHED'}
+#}
+
+def copy_propgroup( de, to ):
+#{
+   for a in de.__annotations__:#{
+      if isinstance(getattr(de,a), bpy.types.bpy_prop_collection):#{
+         ca = getattr(de,a)
+         cb = getattr(to,a)
+
+         while len(cb) != len(ca):#{
+            if len(cb) < len(ca): cb.add()
+            else: cb.remove(0)
+         #}
+         for i in range(len(ca)):#{
+            copy_propgroup(ca[i],cb[i])
+         #}
+      #}
+      else:#{
+         setattr(to,a,getattr(de,a))
+      #}
+   #}
+#}
+
+class SR_OT_COPY_ENTITY_DATA(bpy.types.Operator):
+#{
+   bl_idname = "skaterift.copy_entity_data"
+   bl_label = "Copy entity data"
+   
+   def execute(self, context):#{
+      data = context.active_object.SR_data
+      new_type = data.ent_type
+      print( F"Copy entity data from: {context.active_object.name}" )
+
+      for obj in context.selected_objects:#{
+         if obj != context.active_object:#{
+            print( F"   To: {obj.name}" )
+
+            obj.SR_data.ent_type = new_type
+
+            if active_object.type == 'MESH':#{
+               col = getattr( obj.data.SR_data, new_type, None )
+               if col != None and len(col)==0: col.add()
+               mdata = context.active_object.data.SR_data
+               copy_propgroup( getattr(mdata,new_type)[0], col[0] )
+            #}
+            
+            col = getattr( obj.SR_data, new_type, None )
+            if col != None and len(col)==0: col.add()
+            copy_propgroup( getattr(data,new_type)[0], col[0] )
+         #}
+      #}
+      return{'FINISHED'}
+   #}
+#}
+
 class SR_OT_ROUTE_LIST_NEW_ITEM(bpy.types.Operator):
 #{
    bl_idname = "skaterift.new_entry"
    bl_label = "Add gate"
    
    def execute(self, context):#{
-      active_object = context.active_object
-      active_object.SR_data.ent_route[0].gates.add()
-      return{'FINISHED'}
+      return internal_listadd_execute(self,context,'ent_route','gates')
    #}
 #}
 
@@ -1601,23 +2063,203 @@ class SR_OT_ROUTE_LIST_DEL_ITEM(bpy.types.Operator):
    @classmethod 
    def poll(cls, context):#{
       active_object = context.active_object
-      if obj_ent_type == 'ent_gate':#{
+      if obj_ent_type(active_object) == 'ent_route':#{
          return active_object.SR_data.ent_route[0].gates
       #}
       else: return False
    #}
    
    def execute(self, context):#{
+      return internal_listdel_execute(self,context,'ent_route','gates')
+   #}
+#}
+
+class SR_OT_AUDIO_LIST_NEW_ITEM(bpy.types.Operator):
+#{
+   bl_idname = "skaterift.al_new_entry"
+   bl_label = "Add file"
+   
+   def execute(self, context):#{
+      return internal_listadd_execute(self,context,'ent_audio','files')
+   #}
+#}
+
+class SR_OT_AUDIO_LIST_DEL_ITEM(bpy.types.Operator):
+#{
+   bl_idname = "skaterift.al_del_entry"
+   bl_label = "Remove file"
+
+   @classmethod 
+   def poll(cls, context):#{
+      active_object = context.active_object
+      if obj_ent_type(active_object) == 'ent_audio':#{
+         return active_object.SR_data.ent_audio[0].files
+      #}
+      else: return False
+   #}
+   
+   def execute(self, context):#{
+      return internal_listdel_execute(self,context,'ent_audio','files')
+      return{'FINISHED'}
+   #}
+#}
+
+class SR_OT_GLYPH_LIST_NEW_ITEM(bpy.types.Operator):
+#{
+   bl_idname = "skaterift.gl_new_entry"
+   bl_label = "Add glyph"
+   
+   def execute(self, context):#{
+      active_object = context.active_object
+
+      font = active_object.SR_data.ent_font[0]
+      font.glyphs.add()
+
+      if len(font.glyphs) > 1:#{
+         prev = font.glyphs[-2]
+         cur = font.glyphs[-1]
+
+         cur.bounds = prev.bounds
+         cur.utf32 = prev.utf32+1
+      #}
+
+      return{'FINISHED'}
+   #}
+#}
+
+class SR_OT_GLYPH_LIST_DEL_ITEM(bpy.types.Operator):
+#{
+   bl_idname = "skaterift.gl_del_entry"
+   bl_label = "Remove Glyph"
+
+   @classmethod 
+   def poll(cls, context):#{
+      active_object = context.active_object
+      if obj_ent_type(active_object) == 'ent_font':#{
+         return active_object.SR_data.ent_font[0].glyphs
+      #}
+      else: return False
+   #}
+   
+   def execute(self, context):#{
+      return internal_listdel_execute(self,context,'ent_font','glyphs')
+   #}
+#}
+
+class SR_OT_GLYPH_LIST_MOVE_ITEM(bpy.types.Operator):
+#{
+   bl_idname = "skaterift.gl_move_item"
+   bl_label = "aa"
+   direction: bpy.props.EnumProperty(items=(('UP', 'Up', ""),
+                                             ('DOWN', 'Down', ""),))
+   
+   @classmethod 
+   def poll(cls, context):#{
+      active_object = context.active_object
+      if obj_ent_type(active_object) == 'ent_font':#{
+         return active_object.SR_data.ent_font[0].glyphs
+      #}
+      else: return False
+   #}
+
+   def execute(_, context):#{
       active_object = context.active_object
-      lista = active_object.SR_data.ent_route[0].gates
-      index = active_object.SR_data.ent_route[0].gates_index
-      lista.remove(index) 
-      active_object.SR_data.ent_route[0].gates_index = \
-            min(max(0, index-1), len(lista) - 1)
+      data = active_object.SR_data.ent_font[0]
+
+      index = data.glyphs_index
+      neighbor = index + (-1 if _.direction == 'UP' else 1)
+      data.glyphs.move( neighbor, index )
+
+      list_length = len(data.glyphs) - 1
+      new_index = index + (-1 if _.direction == 'UP' else 1)
+
+      data.glyphs_index = max(0, min(new_index, list_length))
+
       return{'FINISHED'}
    #}
 #}
 
+class SR_OT_FONT_VARIANT_LIST_NEW_ITEM(bpy.types.Operator):
+#{
+   bl_idname = "skaterift.fv_new_entry"
+   bl_label = "Add variant"
+   
+   def execute(self, context):#{
+      return internal_listadd_execute(self,context,'ent_font','variants')
+   #}
+#}
+
+class SR_OT_FONT_VARIANT_LIST_DEL_ITEM(bpy.types.Operator):
+#{
+   bl_idname = "skaterift.fv_del_entry"
+   bl_label = "Remove variant"
+
+   @classmethod 
+   def poll(cls, context):#{
+      active_object = context.active_object
+      if obj_ent_type(active_object) == 'ent_font':#{
+         return active_object.SR_data.ent_font[0].variants
+      #}
+      else: return False
+   #}
+   
+   def execute(self, context):#{
+      return internal_listdel_execute(self,context,'ent_font','variants')
+   #}
+#}
+
+class SR_OBJECT_ENT_AUDIO_FILE_ENTRY(bpy.types.PropertyGroup):
+#{
+   path: bpy.props.StringProperty( name="Path" )
+   probability: bpy.props.FloatProperty( name="Probability",default=100.0 )
+#}
+
+class SR_UL_AUDIO_LIST(bpy.types.UIList):
+#{
+   bl_idname = 'SR_UL_AUDIO_LIST'
+
+   def draw_item(_,context,layout,data,item,icon,active_data,active_propname):
+   #{
+      split = layout.split(factor=0.7)
+      c = split.column()
+      c.prop( item, 'path', text='', emboss=False )
+      c = split.column()
+      c.prop( item, 'probability', text='%', emboss=True )
+   #}
+#}
+
+class SR_UL_FONT_VARIANT_LIST(bpy.types.UIList):
+#{
+   bl_idname = 'SR_UL_FONT_VARIANT_LIST'
+
+   def draw_item(_,context,layout,data,item,icon,active_data,active_propname):
+   #{
+      layout.prop( item, 'mesh', emboss=False )
+      layout.prop( item, 'tipo' )
+   #}
+#}
+
+class SR_UL_FONT_GLYPH_LIST(bpy.types.UIList):
+#{
+   bl_idname = 'SR_UL_FONT_GLYPH_LIST'
+
+   def draw_item(_,context,layout,data,item,icon,active_data,active_propname):
+   #{
+      s0 = layout.split(factor=0.3)
+      c = s0.column()
+      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'
+      row.label(text=lbl)
+      c = s1.column()
+      c.prop( item, 'utf32', text='', emboss=True )
+      c = s0.column()
+      row = c.row()
+      row.prop( item, 'bounds', text='', emboss=False )
+   #}
+#}
+
 class SR_OBJECT_ENT_ROUTE(bpy.types.PropertyGroup):
 #{
    gates: bpy.props.CollectionProperty(type=SR_OBJECT_ENT_ROUTE_ENTRY)
@@ -1651,19 +2293,185 @@ class SR_OBJECT_ENT_ROUTE(bpy.types.PropertyGroup):
    #}
 #}
 
+class SR_OBJECT_ENT_VOLUME(bpy.types.PropertyGroup):
+#{
+   subtype: bpy.props.EnumProperty(
+      name="Subtype",
+      items=[('0','Trigger',''),
+             ('1','Particles (0.1s)','')]
+   )
+
+   target: bpy.props.PointerProperty( \
+           type=bpy.types.Object, name="Target", \
+           poll=lambda self,obj: sr_filter_ent_type(obj,['ent_audio']))
+
+   @staticmethod
+   def sr_inspector( layout, data ):
+   #{
+      data = data[0]
+      layout.prop( data, 'subtype' )
+      layout.prop( data, 'target' )
+   #}
+#}
+
+class SR_OBJECT_ENT_AUDIO(bpy.types.PropertyGroup):
+#{
+   files: bpy.props.CollectionProperty(type=SR_OBJECT_ENT_AUDIO_FILE_ENTRY)
+   files_index: bpy.props.IntProperty()
+
+   flag_3d: bpy.props.BoolProperty( name="3D audio",default=True )
+   flag_loop: bpy.props.BoolProperty( name="Loop",default=False )
+   flag_auto: bpy.props.BoolProperty( name="Play at start",default=False )
+   flag_nodoppler: bpy.props.BoolProperty( name="No Doppler",default=False )
+
+   group: bpy.props.IntProperty( name="Group ID", default=0 )
+   formato: bpy.props.EnumProperty(
+      name="Format",
+      items=[('0','Uncompressed Mono',''),
+             ('1','Compressed Vorbis',''),
+             ('2','[vg] Bird Synthesis','')]
+   )
+   probability_curve: bpy.props.EnumProperty(
+      name="Probability Curve",
+      items=[('0','Constant',''),
+             ('1','Wildlife Daytime',''),
+             ('2','Wildlife Nighttime','')])
+   channel_behaviour: bpy.props.EnumProperty(
+      name="Channel Behaviour",
+      items=[('0','Unlimited',''),
+             ('1','Discard if group full', ''),
+             ('2','Crossfade if group full','')])
+
+   transition_duration: bpy.props.FloatProperty(name="Transition Time",\
+                                                default=0.2)
+   
+   max_channels: bpy.props.IntProperty( name="Max Channels", default=1 )
+   volume: bpy.props.FloatProperty( name="Volume",default=1.0 )
+
+   @staticmethod
+   def sr_inspector( layout, data ):
+   #{
+      layout.prop( data[0], 'formato' )
+      layout.prop( data[0], 'volume' )
+
+      box = layout.box()
+      box.label( text='Channels' )
+      split = box.split(factor=0.3)
+      c = split.column()
+      c.prop( data[0], 'max_channels' )
+      c = split.column()
+      c.prop( data[0], 'channel_behaviour', text='Behaviour' )
+      if data[0].channel_behaviour >= '1':
+         box.prop( data[0], 'group' )
+      if data[0].channel_behaviour == '2': 
+         box.prop( data[0], 'transition_duration' )
+
+      box = layout.box()
+      box.label( text='Flags' )
+      box.prop( data[0], 'flag_3d' )
+      if data[0].flag_3d: box.prop( data[0], 'flag_nodoppler' )
+
+      box.prop( data[0], 'flag_loop' )
+      box.prop( data[0], 'flag_auto' )
+
+      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' )
+
+      layout.template_list('SR_UL_AUDIO_LIST', 'Files', \
+                            data[0], 'files', data[0], 'file_index', rows=5)
+
+      row = layout.row()
+      row.operator( 'skaterift.al_new_entry', text='Add' )
+      row.operator( 'skaterift.al_del_entry', text='Remove' )
+   #}
+#}
+
+class SR_OBJECT_ENT_MARKER(bpy.types.PropertyGroup):
+#{
+   alias: bpy.props.StringProperty()
+#}
+
+class SR_OBJECT_ENT_GLYPH(bpy.types.PropertyGroup):
+#{
+   mini: bpy.props.FloatVectorProperty(size=2)
+   maxi: bpy.props.FloatVectorProperty(size=2)
+   utf32: bpy.props.IntProperty()
+#}
+
+class SR_OBJECT_ENT_GLYPH_ENTRY(bpy.types.PropertyGroup):
+#{
+   bounds: bpy.props.FloatVectorProperty(size=4,subtype='NONE')
+   utf32: bpy.props.IntProperty()
+#}
+
+class SR_OBJECT_ENT_FONT_VARIANT(bpy.types.PropertyGroup):
+#{
+   mesh: bpy.props.PointerProperty(type=bpy.types.Object)
+   tipo: bpy.props.StringProperty()
+#}
+
+class SR_OBJECT_ENT_FONT(bpy.types.PropertyGroup):
+#{
+   variants: bpy.props.CollectionProperty(type=SR_OBJECT_ENT_FONT_VARIANT)
+   glyphs: bpy.props.CollectionProperty(type=SR_OBJECT_ENT_GLYPH_ENTRY)
+   alias: bpy.props.StringProperty()
+
+   glyphs_index: bpy.props.IntProperty()
+   variants_index: bpy.props.IntProperty()
+
+   @staticmethod
+   def sr_inspector( layout, data ):
+   #{
+      layout.prop( data[0], 'alias' )
+
+      layout.label( text='Variants' )
+      layout.template_list('SR_UL_FONT_VARIANT_LIST', 'Variants', \
+                            data[0], 'variants', data[0], 'variants_index',\
+                            rows=5 )
+      row = layout.row()
+      row.operator( 'skaterift.fv_new_entry', text='Add' )
+      row.operator( 'skaterift.fv_del_entry', text='Remove' )
+
+      layout.label( text='ASCII Glyphs' )
+      layout.template_list('SR_UL_FONT_GLYPH_LIST', 'Glyphs', \
+                            data[0], 'glyphs', data[0], 'glyphs_index', rows=5)
+
+      row = layout.row()
+      row.operator( 'skaterift.gl_new_entry', text='Add' )
+      row.operator( 'skaterift.gl_del_entry', text='Remove' )
+      row.operator( 'skaterift.gl_move_item', text='^' ).direction='UP'
+      row.operator( 'skaterift.gl_move_item', text='v' ).direction='DOWN'
+   #}
+#}
+
 class SR_OBJECT_PROPERTIES(bpy.types.PropertyGroup):
 #{
    ent_gate: bpy.props.CollectionProperty(type=SR_OBJECT_ENT_GATE)
    ent_spawn: bpy.props.CollectionProperty(type=SR_OBJECT_ENT_SPAWN)
    ent_route: bpy.props.CollectionProperty(type=SR_OBJECT_ENT_ROUTE)
-
+   ent_volume: bpy.props.CollectionProperty(type=SR_OBJECT_ENT_VOLUME)
+   ent_audio: bpy.props.CollectionProperty(type=SR_OBJECT_ENT_AUDIO)
+   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_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_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)],
       update=sr_on_type_change
    )
 #}
@@ -1741,7 +2549,8 @@ class SR_MATERIAL_PROPERTIES(bpy.types.PropertyGroup):
       ('0','concrete',''),
       ('1','wood',''),
       ('2','grass',''),
-      ('3','tiles','')
+      ('3','tiles',''),
+      ('4','metal','')
       ])
    
    collision: bpy.props.BoolProperty( \
@@ -1818,8 +2627,7 @@ def cv_draw_sphere( pos, radius, colour ):
    
    pi = 3.14159265358979323846264
 
-   for i in range(16):
-   #{
+   for i in range(16):#{
       t = ((i+1.0) * 1.0/16.0) * pi * 2.0
       s = math.sin(t)
       c = math.cos(t)
@@ -1853,8 +2661,7 @@ def cv_draw_halfsphere( pos, tx, ty, tz, radius, colour ):
    
    pi = 3.14159265358979323846264
 
-   for i in range(16):
-   #{
+   for i in range(16):#{
       t = ((i+1.0) * 1.0/16.0) * pi
       s = math.sin(t)
       c = math.cos(t)
@@ -1901,8 +2708,7 @@ def cv_draw_ucube( transform, colour, s=Vector((1,1,1)), o=Vector((0,0,0)) ):
    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:
-   #{
+   for l in indices:#{
       v0 = vs[l[0]]
       v1 = vs[l[1]]
       cv_view_verts += [(v0[0],v0[1],v0[2])]
@@ -1938,14 +2744,12 @@ def cv_draw_line2( p0, p1, c0, c1 ):
 #
 def cv_tangent_basis( n, tx, ty ):
 #{
-   if abs( n[0] ) >= 0.57735027:
-   #{
+   if abs( n[0] ) >= 0.57735027:#{
       tx[0] =  n[1]
       tx[1] = -n[0]
       tx[2] =  0.0
    #}
-   else:
-   #{
+   else:#{
       tx[0] =  0.0
       tx[1] =  n[2]
       tx[2] = -n[1]
@@ -1975,7 +2779,7 @@ def cv_draw_arrow( p0, p1, c0, size=0.15 ):
    
    cv_view_verts += [p0,p1, midpt+(tx-n)*size,midpt, midpt+(-tx-n)*size,midpt ]
    cv_view_colours += [c0,c0,c0,c0,c0,c0]
-   cv_draw_lines()
+   #cv_draw_lines()
 #}
 
 def cv_draw_line_dotted( p0, p1, c0, dots=10 ):
@@ -1992,7 +2796,7 @@ def cv_draw_line_dotted( p0, p1, c0, dots=10 ):
       cv_view_verts += [p2,p3]
       cv_view_colours += [c0,c0]
    #}
-   cv_draw_lines()
+   #cv_draw_lines()
 #}
 
 # Drawhandles of a bezier control point
@@ -2017,8 +2821,7 @@ def cv_draw_bezier( p0,h0,p1,h1,c0,c1 ):
    global cv_view_verts, cv_view_colours
 
    last = p0
-   for i in range(10):
-   #{
+   for i in range(10):#{
       t = (i+1)/10
       a0 = 1-t
 
@@ -2129,7 +2932,7 @@ def draw_cone_twist( center, vx, vy, va ):
    size = 0.12
 
    cv_view_verts += [center, center+va*size]
-   cv_view_colours += [ (1,1,1,1), (1,1,1,1) ]
+   cv_view_colours += [ (1,1,1), (1,1,1) ]
 
    for x in range(32):#{
       t0 = (x/32) * math.tau
@@ -2147,7 +2950,7 @@ def draw_cone_twist( center, vx, vy, va ):
       col1 = ( abs(c1), abs(s1), 0.0, 1.0 )
 
       cv_view_verts += [center, p0, p0, p1]
-      cv_view_colours += [ (0,0,0,0), col0, col0, col1 ]
+      cv_view_colours += [ (0,0,0), col0, col0, col1 ]
    #}
 
    cv_draw_lines()
@@ -2192,7 +2995,7 @@ def draw_skeleton_helpers( obj ):
 
             cv_view_verts += [(v0[0],v0[1],v0[2])]
             cv_view_verts += [(v1[0],v1[1],v1[2])]
-            cv_view_colours += [(0.5,0.5,0.5,0.5),(0.5,0.5,0.5,0.5)]
+            cv_view_colours += [(0.5,0.5,0.5),(0.5,0.5,0.5)]
          #}
       #}
       elif bone.SR_data.collider == '2':#{
@@ -2220,7 +3023,7 @@ def draw_skeleton_helpers( obj ):
          p0 = obj.matrix_world@Vector( c + (a+b)*0.5 + v1*l*-0.5 )
          p1 = obj.matrix_world@Vector( c + (a+b)*0.5 + v1*l* 0.5 )
 
-         colour = [0.2,0.2,0.2,1.0]
+         colour = [0.2,0.2,0.2]
          colour[major_axis] = 0.5
 
          cv_draw_halfsphere( p0, -v1, ty, tx, r, colour )
@@ -2269,19 +3072,50 @@ def cv_ent_gate( obj ):
 
    indices = [(0,1),(1,2),(2,3),(3,0),(4,5),(5,6),(7,8)]
 
+   r3d = bpy.context.area.spaces.active.region_3d
+
+   p0 = r3d.view_matrix.inverted().translation
+   v0 = (obj.matrix_world@Vector((0,0,0))) - p0
+   v1 = obj.matrix_world.to_3x3() @ Vector((0,1,0))
+
+   if v0.dot(v1) > 0.0: cc = (0,1,0)
+   else: cc = (1,0,0)
+
    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 += [(1,1,0,1),(1,1,0,1)]
+      cv_view_colours += [cc,cc]
    #}
 
-   sw = (0.4,0.4,0.4,0.2)
+   sw = (0.4,0.4,0.4)
    if data.target != None:
       cv_draw_arrow( obj.location, data.target.location, sw )
 #}
 
+def cv_ent_volume( obj ):
+#{
+   global cv_view_verts, cv_view_colours
+
+   data = obj.SR_data.ent_volume[0]
+
+   if data.subtype == '0':#{
+      cv_draw_ucube( obj.matrix_world, (0,1,0) )
+
+      if data.target:#{
+         cv_draw_line( obj.location, data.target.location, (0,1,0) )
+      #}
+   #}
+   elif data.subtype == '1':#{
+      cv_draw_ucube( obj.matrix_world, (1,1,0) )
+
+      if data.target:#{
+         cv_draw_line( obj.location, data.target.location, (1,1,0) )
+      #}
+   #}
+#}
+
 def dijkstra( graph, start_node, target_node ):
 #{
    unvisited = [_ for _ in graph]
@@ -2327,53 +3161,148 @@ def dijkstra( graph, start_node, target_node ):
    return path
 #}
 
-def node_graph( route_nodes ):
+class dij_graph():
+#{
+   def __init__(_,points,graph,subsections):#{
+      _.points = points
+      _.graph = graph
+      _.subsections = subsections
+   #}
+#}
+
+def create_node_graph( curves, gates ):
 #{
+   # add endpoints of curves
    graph = {}
-   for n in route_nodes:
-      graph[n.name] = {}
+   route_points = []
+   subsections = []
+   point_count = 0
+   spline_count = 0
 
-   for i in range(len(route_nodes)-1):#{
-      for j in range(i+1, len(route_nodes)):#{
-         ni = route_nodes[i]
-         nj = route_nodes[j]
+   for c in range(len(curves)):#{
+      for s in range(len(curves[c].data.splines)):#{
+         spline = curves[c].data.splines[s]
+         l = len(spline.points)
+         if l < 2: continue
 
-         v0 = ni.location - nj.location
+         dist = round(spline.calc_length(),2)
 
-         gate = None
+         ia = point_count
+         ib = point_count+l-1
 
-         if ni.SR_data.ent_type == 'ent_gate':
-            gate = ni
+         graph[ia] = { ib: dist }
+         graph[ib] = { ia: dist }
+         
+         for i in range(len(spline.points)):#{
+            wco = curves[c].matrix_world @ spline.points[i].co
+            route_points.append(Vector((wco[0],wco[1],wco[2]+0.5)))
+
+            previous = ia+i-1
+            proxima = ia+i+1
 
-         if nj.SR_data.ent_type == 'ent_gate':#{
-            if gate: continue
-            gate = nj
+            if i == 0: previous = -1
+            if i == len(spline.points)-1: proxima = -1
+
+            subsections.append((spline_count,previous,proxima))
+            point_count += 1
          #}
 
-         if gate:#{
-            v1 = gate.matrix_world.to_3x3() @ Vector((0,-1,0))
-            if gate.SR_data.ent_gate[0].target:
-               if v1.dot(v0) > 0.0: continue
-            else:
-               if v1.dot(v0) < 0.0: continue
+         spline_count += 1
+      #}
+   #}
+
+   # link endpoints
+   graph_keys = list(graph)
+   for i in range(len(graph_keys)-1):#{
+      for j in range(i+1, len(graph_keys)):#{
+         if i%2==0 and i+1==j: continue
+
+         ni = graph_keys[i]
+         nj = graph_keys[j]
+         pi = route_points[ni]
+         pj = route_points[nj]
+
+         dist = round((pj-pi).magnitude,2)
+
+         if dist < 10.0:#{
+            graph[ni][nj] = dist
+            graph[nj][ni] = dist
          #}
+      #}
+   #}
+
+   # add and link gates( by name )
+   for gate in gates:#{
+      v1 = gate.matrix_world.to_3x3() @ Vector((0,1,0))
+      if gate.SR_data.ent_gate[0].target:
+         v1 = v1 * -1.0
+
+      graph[ gate.name ] = {}
 
-         dist = v0.magnitude
+      for i in range(len(graph_keys)):#{
+         ni = graph_keys[i]
+         pi = route_points[ni]
 
-         if dist > 25.0: continue
-         graph[route_nodes[i].name][route_nodes[j].name] = dist
-         graph[route_nodes[j].name][route_nodes[i].name] = dist
+         v0 = pi-gate.location
+         if v0.dot(v1) < 0.0: continue
+
+         dist = round(v0.magnitude,2)
+
+         if dist < 10.0:#{
+            graph[ gate.name ][ ni ] = dist
+            graph[ ni ][ gate.name ] = dist
+         #}
       #}
    #}
 
-   return graph
+   return dij_graph(route_points,graph,subsections)
+#}
+
+def solve_graph( dij, start, end ):
+#{
+   path = dijkstra( dij.graph, end, start )
+   full = []
+
+   if path:#{
+      for sj in range(1,len(path)-2):#{
+         i0 = path[sj]
+         i1 = path[sj+1]
+         map0 = dij.subsections[i0]
+         map1 = dij.subsections[i1]
+
+         if map0[0] == map1[0]:#{
+            if map0[1] == -1: direction = 2
+            else: direction = 1
+            sent = 0
+
+            while True:#{
+               map0 = dij.subsections[i0]
+               i1 = map0[direction]
+               if i1 == -1: break
+               
+               full.append( i0 )
+               sent += 1
+               i0 = i1
+               if sent > 50: break
+            #}
+         #}
+         else:#{
+            full.append( i0 )
+         #}
+      #}
+
+      full.append( path[-2] )
+   #}
+   return full
 #}
 
-def cv_draw_route( route, route_nodes ):
+def cv_draw_route( route, dij ):
 #{
    pole = Vector((0.2,0.2,10))
    hat = Vector((1,8,0.2))
-   cc = route.SR_data.ent_route[0].colour
+   cc = (route.SR_data.ent_route[0].colour[0],
+         route.SR_data.ent_route[0].colour[1],
+         route.SR_data.ent_route[0].colour[2])
 
    cv_draw_ucube(route.matrix_world,cc,Vector((0.5,-7.5,6)),\
                                        Vector((0,-6.5,5.5)))
@@ -2383,7 +3312,6 @@ def cv_draw_route( route, route_nodes ):
    cv_draw_ucube(route.matrix_world,cc,hat, Vector((-0.5,-6.5,-1)) )
 
    checkpoints = route.SR_data.ent_route[0].gates
-   graph = node_graph( route_nodes )
 
    for i in range(len(checkpoints)):#{
       gi = checkpoints[i].target
@@ -2399,13 +3327,17 @@ def cv_draw_route( route, route_nodes ):
       if gi==gj: continue # error?
       if not gi or not gj: continue
 
-      path = dijkstra( graph, gj.name, gi.name )
+      path = solve_graph( dij, gi.name, gj.name )
 
       if path:#{
-         for sj in range(len(path)-1):#{
-            o0 = bpy.data.objects[ path[sj] ]
-            o1 = bpy.data.objects[ path[sj+1] ]
-            cv_draw_arrow(o0.location,o1.location,cc,1.5)
+         cv_draw_arrow(gi.location,dij.points[path[0]],cc,1.5)
+         cv_draw_arrow(dij.points[path[len(path)-1]],gj.location,cc,1.5)
+         for j in range(len(path)-1):#{
+            i0 = path[j]
+            i1 = path[j+1]
+            o0 = dij.points[ i0 ]
+            o1 = dij.points[ i1 ]
+            cv_draw_arrow(o0,o1,cc,1.5)
          #}
       #}
       else:#{
@@ -2432,7 +3364,8 @@ def cv_draw():
    gpu.state.depth_test_set('LESS')
    gpu.state.blend_set('NONE')
 
-   route_nodes = []
+   route_gates = []
+   route_curves = []
    routes = []
 
    for obj in bpy.context.collection.objects:#{
@@ -2445,18 +3378,53 @@ def cv_draw():
 
          if ent_type == 'ent_gate':#{
             cv_ent_gate( obj )
-            route_nodes += [obj]
+            route_gates += [obj]
+         #}
+         elif ent_type == 'ent_route_node':#{
+            if obj.type == 'CURVE':#{
+               route_curves += [obj]
+            #}
          #}
-         elif ent_type == 'ent_route_node':
-            route_nodes += [obj]
          elif ent_type == 'ent_route':
             routes += [obj]
+         elif ent_type == 'ent_volume':#{
+            cv_ent_volume( obj )
+         #}
+         elif ent_type == 'ent_audio':#{
+            if obj.SR_data.ent_audio[0].flag_3d:
+               cv_draw_sphere( obj.location, obj.scale[0], (1,1,0) )
+         #}
+         elif ent_type == 'ent_font':#{
+            data = obj.SR_data.ent_font[0]
+
+            for i in range(len(data.variants)):#{
+               sub = data.variants[i].mesh
+               if not sub: continue
+
+               for ch in data.glyphs:#{
+                  mini = (ch.bounds[0],ch.bounds[1])
+                  maxi = (ch.bounds[2]+mini[0],ch.bounds[3]+mini[1])
+                  p0 = sub.matrix_world @ Vector((mini[0],0.0,mini[1]))
+                  p1 = sub.matrix_world @ Vector((maxi[0],0.0,mini[1]))
+                  p2 = sub.matrix_world @ Vector((maxi[0],0.0,maxi[1]))
+                  p3 = sub.matrix_world @ Vector((mini[0],0.0,maxi[1]))
+
+                  if i == data.variants_index: cc = (0.5,0.5,0.5)
+                  else: cc = (0,0,0)
+
+                  cv_view_verts += [p0,p1,p1,p2,p2,p3,p3,p0]
+                  cv_view_colours += [cc,cc,cc,cc,cc,cc,cc,cc]
+               #}
+            #}
+         #}
       #}
    #}
+
+   dij = create_node_graph( route_curves, route_gates )
    
    #cv_draw_route_map( route_nodes )
    for route in routes:#{
-      cv_draw_route( route, route_nodes )
+      cv_draw_route( route, dij )
    #}
 
    cv_draw_lines()
@@ -2469,8 +3437,20 @@ classes = [ SR_INTERFACE, SR_MATERIAL_PANEL,\
             \
             SR_OBJECT_ENT_GATE, SR_MESH_ENT_GATE, SR_OBJECT_ENT_SPAWN, \
             SR_OBJECT_ENT_ROUTE_ENTRY, SR_UL_ROUTE_NODE_LIST, \
-            SR_OBJECT_ENT_ROUTE, SR_OT_ROUTE_LIST_NEW_ITEM,
+            SR_OBJECT_ENT_ROUTE, SR_OT_ROUTE_LIST_NEW_ITEM,\
+            SR_OT_GLYPH_LIST_NEW_ITEM, SR_OT_GLYPH_LIST_DEL_ITEM,\
+            SR_OT_GLYPH_LIST_MOVE_ITEM,\
+            SR_OT_AUDIO_LIST_NEW_ITEM,SR_OT_AUDIO_LIST_DEL_ITEM,\
+            SR_OT_FONT_VARIANT_LIST_NEW_ITEM,SR_OT_FONT_VARIANT_LIST_DEL_ITEM,\
+            SR_OT_COPY_ENTITY_DATA, \
+            SR_OBJECT_ENT_VOLUME, \
+            SR_UL_AUDIO_LIST, SR_OBJECT_ENT_AUDIO_FILE_ENTRY,\
             SR_OT_ROUTE_LIST_DEL_ITEM,\
+            SR_OBJECT_ENT_AUDIO,SR_OBJECT_ENT_MARKER,SR_OBJECT_ENT_GLYPH,\
+            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_PROPERTIES, SR_LIGHT_PROPERTIES, SR_BONE_PROPERTIES, 
             SR_MESH_PROPERTIES, SR_MATERIAL_PROPERTIES \