review save method
[carveJwlIkooP6JGAAIwe30JlM.git] / blender_export.py
index 32e548b4afa0d1583f4171949d0fec4e534b4886..f9b8f6925120eb57da1fd0760681f93d44153e7a 100644 (file)
@@ -1,9 +1,10 @@
-import bpy, math, gpu, os
+import bpy, blf, math, gpu, os
 import cProfile
 from ctypes import *
 from mathutils import *
 from gpu_extras.batch import batch_for_shader
 from bpy_extras import mesh_utils
+from bpy_extras import view3d_utils
 
 bl_info = {
    "name":"Skaterift .mdl exporter",
@@ -35,9 +36,16 @@ sr_entity_list = [
    ('ent_swspreview', 'Workshop Preview', '', 14 ),
    ('ent_menuitem',     'Menu Item',      '', 15 ),
    ('ent_worldinfo',    'World Info',     '', 16 ),
-   ('ent_ccmd',         'CCmd',           '', 17 )
+   ('ent_ccmd',         'CCmd',           '', 17 ),
+   ('ent_challenge',    'Challenge',      '', 18 ),
+   ('ent_unlock',       'Unlockable',     '', 19 ),
+   ('ent_relay',        'Relay',          '', 20 )
 ]
 
+MDL_VERSION_NR = 102
+SR_TRIGGERABLE = [ 'ent_audio', 'ent_ccmd', 'ent_gate', 'ent_unlock', \
+                   'ent_relay', 'ent_skateshop', 'ent_challenge' ]
+
 def get_entity_enum_id( alias ):
 #{
    for et in sr_entity_list:#{
@@ -185,7 +193,7 @@ class version_refcount_union(Union):
 
 class ent_gate(Structure):
 #{
-   _fields_ = [("type",c_uint32),
+   _fields_ = [("flags",c_uint32),
                ("target", c_uint32),
                ("key",c_uint32),
                ("dimensions", c_float*3),
@@ -196,7 +204,11 @@ class ent_gate(Structure):
                ("_anonymous_union",version_refcount_union),
                ("timing_time",c_double),
                ("routes",c_uint16*4),
-               ("route_count",c_uint8)]
+               ("route_count",c_uint8),
+               ("submesh_start",c_uint32), # v102+ 
+               ("submesh_count",c_uint32), # v102+ (can be 0)
+               ]
+   sr_functions = { 0: 'unlock' }
 #}
 
 class ent_route_node(Structure):
@@ -445,6 +457,33 @@ class ent_ccmd(Structure):
    _fields_ = [("pstr_command",c_uint32)]
 #}
 
+class ent_challenge(Structure):#{
+   _fields_ = [("transform",mdl_transform),
+               ("submesh_start",c_uint32), ("submesh_count",c_uint32),
+               ("id_next",c_uint32),
+               ("filter",c_uint32),
+               ("id_win",c_uint32),
+               ("win_event",c_uint32),
+               ("time_limit",c_float)]
+
+   sr_functions = { 0: 'trigger',
+                    1: 'start_challenge' }
+#}
+
+class ent_unlock(Structure):#{
+   _fields_ = [("pstr_alias",c_uint32),
+               ("target",c_uint32),
+               ("target_event",c_uint32),
+               ("status",c_uint32)]
+   sr_functions = { 0: 'unlock' }
+#}
+
+class ent_relay(Structure):#{
+   _fields_ = [("targets",(c_uint32*2)*4),
+               ("targets_events",c_uint32*4)]
+   sr_functions = { 0: 'trigger' }
+#}
+
 def obj_ent_type( obj ):
 #{
    if obj.type == 'ARMATURE': return 'mdl_armature'
@@ -468,6 +507,31 @@ def sr_filter_ent_type( obj, ent_types ):
    return False
 #}
 
+def v4_dot( a, b ):#{
+   return a[0]*b[0] + a[1]*b[1] + a[2]*b[2] + a[3]*b[3]
+#}
+
+def q_identity( q ):#{
+   q[0] = 0.0
+   q[1] = 0.0
+   q[2] = 0.0
+   q[3] = 1.0
+#}
+
+def q_normalize( q ):#{
+   l2 = v4_dot(q,q)
+   if( l2 < 0.00001 ):#{
+      q_identity( q )
+   #}
+   else:#{
+      s = 1.0/math.sqrt(l2)
+      q[0] *= s
+      q[1] *= s
+      q[2] *= s
+      q[3] *= s
+   #}
+#}
+
 def compile_obj_transform( obj, transform ):
 #{
    co = obj.matrix_world @ Vector((0,0,0))
@@ -475,6 +539,7 @@ def compile_obj_transform( obj, transform ):
    # This was changed from matrix_local on 09.05.23
    q = obj.matrix_world.to_quaternion() 
    s = obj.scale
+   q_normalize( q )
    
    # Setup transform
    #
@@ -760,14 +825,18 @@ def sr_compile_material( mat ):
    if mat.SR_data.shader == 'boundary':#{
       m.shader = 6
    #}
+
+   if mat.SR_data.shader == 'fxglow':#{
+      m.shader = 7
+   #}
    
    inf = material_info( mat )
 
    if mat.SR_data.shader == 'standard' or \
       mat.SR_data.shader == 'standard_cutout' or \
       mat.SR_data.shader == 'terrain_blend' or \
-      mat.SR_data.shader == 'vertex_blend':
-   #{
+      mat.SR_data.shader == 'vertex_blend' or \
+      mat.SR_data.shader == 'fxglow': #{
       if 'tex_diffuse' in inf: 
          m.tex_diffuse = sr_compile_texture(inf['tex_diffuse'])
    #}
@@ -789,8 +858,9 @@ def sr_armature_bones( armature ):
          yield from _recurse_bone( b )
 #}
 
-def sr_entity_id( obj ):
-#{
+def sr_entity_id( obj ):#{
+   if not obj: return 0
+   
    tipo = get_entity_enum_id( obj_ent_type(obj) )
    index = sr_compile.entity_ids[ obj.name ]
 
@@ -1384,6 +1454,7 @@ def sr_compile_armature( obj ):
                      lc_m = smtx.inverted() @ lc_m
                   #}
                   rq = lc_m.to_quaternion()
+                  q_normalize( rq )
 
                   kf = mdl_transform()
                   kf.co[0] =  loc[0]
@@ -1527,6 +1598,13 @@ def sr_compile( collection ):
          if ent_type == 'ent_font': continue
          if ent_type == 'ent_font_variant': continue
          if ent_type == 'ent_menuitem': continue
+         if ent_type == 'ent_challenge': continue
+
+         #TODO: This is messy.
+         if ent_type == 'ent_gate':#{
+            obj_data = obj.SR_data.ent_gate[0]
+            if obj_data.custom: continue
+         #}
          #--------------------------
 
          print( F'[SR] {i: 3}/{mesh_count} {obj.name:<40}', end='\r' )
@@ -1574,18 +1652,28 @@ def sr_compile( collection ):
             obj_data = obj.SR_data.ent_gate[0]
             mesh_data = obj.data.SR_data.ent_gate[0]
 
+            flags = 0x0000
+
             if obj_data.tipo == 'default':#{
                if obj_data.target:#{
                   gate.target = sr_compile.entity_ids[obj_data.target.name]
-                  gate.type = 1
+                  flags |= 0x0001
                #}
             #}
             elif obj_data.tipo == 'nonlocal':#{
                gate.target = 0
                gate.key = sr_compile_string(obj_data.key)
-               gate.type = 2
+               flags |= 0x0002
+            #}
+
+            if obj_data.flip:   flags |= 0x0004
+            if obj_data.custom:#{
+               flags |= 0x0008
+               gate.submesh_start, gate.submesh_count, _ = \
+                     sr_compile_mesh_internal( obj )
             #}
-            else: gate.type = 0
+            if obj_data.locked: flags |= 0x0010
+            gate.flags = flags
             
             gate.dimensions[0] = mesh_data.dimensions[0]
             gate.dimensions[1] = mesh_data.dimensions[1]
@@ -1682,6 +1770,7 @@ def sr_compile( collection ):
 
             if obj_data.target:#{
                volume.target = sr_entity_id( obj_data.target )
+               volume._anon.trigger.event = obj_data.target_event
             #}
 
             sr_ent_push(volume)
@@ -1739,6 +1828,43 @@ def sr_compile( collection ):
             ccmd.pstr_command = sr_compile_string( obj_data.command )
             sr_ent_push( ccmd )
          #}
+         elif ent_type == 'ent_challenge':#{
+            challenge = ent_challenge()
+            obj_data = obj.SR_data.ent_challenge[0]
+            challenge.id_next = sr_entity_id( obj_data.proxima )
+            challenge.id_win = sr_entity_id( obj_data.target )
+            challenge.win_event = obj_data.target_event
+            challenge.filter = 0
+            challenge.time_limit = obj_data.time_limit
+
+            compile_obj_transform( obj, challenge.transform )
+            challenge.submesh_start, challenge.submesh_count, _ = \
+                  sr_compile_mesh_internal( obj )
+
+            sr_ent_push( challenge )
+         #}
+         elif ent_type == 'ent_unlock':#{
+            unlock = ent_unlock()
+            obj_data = obj.SR_data.ent_unlock[0]
+            unlock.pstr_alias = sr_compile_string( obj_data.alias )
+            unlock.target = sr_entity_id( obj_data.target )
+            unlock.target_event = obj_data.target_event
+            unlock.status = 0
+            sr_ent_push( unlock )
+         #}
+         elif ent_type == 'ent_relay':#{
+            relay = ent_relay()
+            obj_data = obj.SR_data.ent_relay[0]
+            relay.targets[0][0] = sr_entity_id( obj_data.target0 )
+            relay.targets[1][0] = sr_entity_id( obj_data.target1 )
+            relay.targets[2][0] = sr_entity_id( obj_data.target2 )
+            relay.targets[3][0] = sr_entity_id( obj_data.target3 )
+            relay.targets[0][1] = obj_data.target0_event
+            relay.targets[1][1] = obj_data.target1_event
+            relay.targets[2][1] = obj_data.target2_event
+            relay.targets[3][1] = obj_data.target3_event
+            sr_ent_push( relay )
+         #}
       #}
    #}
 
@@ -1937,7 +2063,7 @@ def sr_compile( collection ):
    os.makedirs(os.path.dirname(path),exist_ok=True)
    fp = open( path, "wb" )
    header = mdl_header()
-   header.version = 101
+   header.version = MDL_VERSION_NR
    sr_array_title( header.arrays, \
                    'index', len(file_array_instructions), \
                    sizeof(mdl_array), header_size )
@@ -2151,9 +2277,12 @@ 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' )
+         amount = max( 0, len(context.selected_objects)-1 )
+         
+         row = _.layout.row()
+         row.operator( 'skaterift.copy_entity_data', \
+               text=F'Copy entity data to {amount} other objects' )
+         if amount == 0: row.enabled=False
 
          box = _.layout.box()
          row = box.row()
@@ -2161,13 +2290,13 @@ class SR_INTERFACE(bpy.types.Panel):
          row.label( text=active_object.name )
          row.scale_y = 1.5
 
-         def _draw_prop_collection( data ): #{
+         def _draw_prop_collection( source, data ): #{
             nonlocal box
             row = box.row()
             row.alignment = 'CENTER'
             row.enabled = False
             row.scale_y = 1.5
-            row.label( text=F'{data[0]}' )
+            row.label( text=F'{source}' )
             
             if hasattr(type(data[0]),'sr_inspector'):#{
                type(data[0]).sr_inspector( box, data )
@@ -2187,7 +2316,9 @@ class SR_INTERFACE(bpy.types.Panel):
                                  text=F'Mirror attributes to {mb.name}' )
                #}
 
-               _draw_prop_collection( [bones.active.SR_data ] )
+               _draw_prop_collection( \
+                     F'bpy.types.Bone["{bones.active.name}"].SR_data',\
+                     [bones.active.SR_data ] )
             #}
             else: #{
                row = box.row()
@@ -2198,18 +2329,26 @@ class SR_INTERFACE(bpy.types.Panel):
             #}
          #}
          elif active_object.type == 'LIGHT': #{
-            _draw_prop_collection( [active_object.data.SR_data] )
+            _draw_prop_collection( \
+                  F'bpy.types.Light["{active_object.data.name}"].SR_data', \
+                  [active_object.data.SR_data] )
          #}
          elif active_object.type in ['EMPTY','CURVE','MESH']:#{
             box.prop( active_object.SR_data, "ent_type" )
             ent_type = active_object.SR_data.ent_type
             
             col = getattr( active_object.SR_data, ent_type, None )
-            if col != None and len(col)!=0: _draw_prop_collection( col )
+            if col != None and len(col)!=0: 
+               _draw_prop_collection( \
+         F'bpy.types.Object["{active_object.name}"].SR_data.{ent_type}[0]', \
+         col )
 
             if active_object.type == 'MESH':#{
                col = getattr( active_object.data.SR_data, ent_type, None )
-               if col != None and len(col)!=0: _draw_prop_collection( col )
+               if col != None and len(col)!=0: 
+                  _draw_prop_collection( \
+         F'bpy.types.Mesh["{active_object.data.name}"].SR_data.{ent_type}[0]', \
+         col )
             #}
          #}
       #}
@@ -2321,7 +2460,11 @@ class SR_OBJECT_ENT_GATE(bpy.types.PropertyGroup):
 
    key: bpy.props.StringProperty()
    tipo: bpy.props.EnumProperty(items=(('default', 'Default', ""),
-                                       ('nonlocal', 'Non-Local', ""),))
+                                       ('nonlocal', 'Non-Local', "")))
+
+   flip: bpy.props.BoolProperty( name="Flip exit", default=False )
+   custom: bpy.props.BoolProperty( name="Mesh is surface", default=False )
+   locked: bpy.props.BoolProperty( name="Start Locked", default=False )
 
    @staticmethod
    def sr_inspector( layout, data ):
@@ -2331,6 +2474,11 @@ class SR_OBJECT_ENT_GATE(bpy.types.PropertyGroup):
 
       if   data[0].tipo == 'default':  box.prop( data[0], 'target' )
       elif data[0].tipo == 'nonlocal': box.prop( data[0], 'key' )
+
+      flags = box.box()
+      flags.prop( data[0], 'flip' )
+      flags.prop( data[0], 'custom' )
+      flags.prop( data[0], 'locked' )
    #}
 #}
 
@@ -2678,8 +2826,7 @@ class SR_OBJECT_ENT_ROUTE(bpy.types.PropertyGroup):
    #}
 #}
 
-class SR_OBJECT_ENT_VOLUME(bpy.types.PropertyGroup):
-#{
+class SR_OBJECT_ENT_VOLUME(bpy.types.PropertyGroup):#{
    subtype: bpy.props.EnumProperty(
       name="Subtype",
       items=[('0','Trigger',''),
@@ -2689,14 +2836,42 @@ 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','ent_skateshop','ent_ccmd']))
+                                    ['ent_audio','ent_skateshop','ent_ccmd',\
+                                     'ent_challenge']))
+   target_event: bpy.props.IntProperty( name="Event/Method" )
 
    @staticmethod
-   def sr_inspector( layout, data ):
-   #{
-      data = data[0]
-      layout.prop( data, 'subtype' )
-      layout.prop( data, 'target' )
+   def inspect_target( layout, data, propname ):#{
+      box = layout.box()
+      box.prop( data[0], propname )
+
+      row = box.row()
+      row.prop( data[0], propname + '_event')
+
+      target = getattr( data[0], propname )
+      if target:#{
+         tipo = target.SR_data.ent_type
+         cls = globals()[ tipo ]
+
+         table = getattr( cls, 'sr_functions', None )
+         if table:#{
+            index = getattr( data[0], propname+'_event')
+            if index in table:
+               row.label( text=table[index] )
+            else:
+               row.label( text="undefined function" )
+         #}
+      #}
+      else:#{
+         row.label( text="..." )
+         row.enabled=False
+      #}
+   #}
+
+   @staticmethod
+   def sr_inspector( layout, data ):#{
+      layout.prop( data[0], 'subtype' )
+      SR_OBJECT_ENT_VOLUME.inspect_target( layout, data, 'target' )
    #}
 #}
 
@@ -2978,6 +3153,66 @@ class SR_OBJECT_ENT_CCMD(bpy.types.PropertyGroup):
    command: bpy.props.StringProperty(name="Command Line")
 #}
 
+class SR_OBJECT_ENT_CHALLENGE(bpy.types.PropertyGroup):#{
+   proxima: bpy.props.PointerProperty( \
+            type=bpy.types.Object, name="Next", \
+            poll=lambda self,obj: sr_filter_ent_type(obj,['ent_challenge']))
+   target: bpy.props.PointerProperty( \
+           type=bpy.types.Object, name="Win", \
+           poll=lambda self,obj: sr_filter_ent_type(obj,SR_TRIGGERABLE))
+   target_event: bpy.props.IntProperty( name="Event/Method" )
+   time_limit: bpy.props.FloatProperty( name="Time Limit", default=1.0 )
+
+   @staticmethod
+   def sr_inspector( layout, data ):#{
+      layout.prop( data[0], 'proxima' )
+      layout.prop( data[0], 'time_limit' )
+      SR_OBJECT_ENT_VOLUME.inspect_target( layout, data, 'target' )
+   #}
+#}
+
+class SR_OBJECT_ENT_UNLOCK(bpy.types.PropertyGroup):#{
+   alias: bpy.props.StringProperty( name="Alias" )
+   target: bpy.props.PointerProperty( \
+            type=bpy.types.Object, name="Target", \
+            poll=lambda self,obj: sr_filter_ent_type(obj,SR_TRIGGERABLE))
+   target_event: bpy.props.IntProperty( name="Event/Method" )
+
+   @staticmethod
+   def sr_inspector( layout, data ):#{
+      layout.prop( data[0], 'alias' )
+      SR_OBJECT_ENT_VOLUME.inspect_target( layout, data, 'target' )
+   #}
+#}
+
+class SR_OBJECT_ENT_RELAY(bpy.types.PropertyGroup):#{
+   target0: bpy.props.PointerProperty( \
+            type=bpy.types.Object, name="Target 0", \
+            poll=lambda self,obj: sr_filter_ent_type(obj,SR_TRIGGERABLE))
+   target1: bpy.props.PointerProperty( \
+            type=bpy.types.Object, name="Target 1", \
+            poll=lambda self,obj: sr_filter_ent_type(obj,SR_TRIGGERABLE))
+   target2: bpy.props.PointerProperty( \
+            type=bpy.types.Object, name="Target 2", \
+            poll=lambda self,obj: sr_filter_ent_type(obj,SR_TRIGGERABLE))
+   target3: bpy.props.PointerProperty( \
+            type=bpy.types.Object, name="Target 3", \
+            poll=lambda self,obj: sr_filter_ent_type(obj,SR_TRIGGERABLE))
+
+   target0_event: bpy.props.IntProperty( name="Event" )
+   target1_event: bpy.props.IntProperty( name="Event" )
+   target2_event: bpy.props.IntProperty( name="Event" )
+   target3_event: bpy.props.IntProperty( name="Event" )
+
+   @staticmethod
+   def sr_inspector( layout, data ):#{
+      SR_OBJECT_ENT_VOLUME.inspect_target( layout, data, 'target0' )
+      SR_OBJECT_ENT_VOLUME.inspect_target( layout, data, 'target1' )
+      SR_OBJECT_ENT_VOLUME.inspect_target( layout, data, 'target2' )
+      SR_OBJECT_ENT_VOLUME.inspect_target( layout, data, 'target3' )
+   #}
+#}
+
 class SR_OBJECT_PROPERTIES(bpy.types.PropertyGroup):
 #{
    ent_gate: bpy.props.CollectionProperty(type=SR_OBJECT_ENT_GATE)
@@ -2995,6 +3230,9 @@ class SR_OBJECT_PROPERTIES(bpy.types.PropertyGroup):
    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_challenge: bpy.props.CollectionProperty(type=SR_OBJECT_ENT_CHALLENGE)
+   ent_unlock: bpy.props.CollectionProperty(type=SR_OBJECT_ENT_UNLOCK)
+   ent_relay: bpy.props.CollectionProperty(type=SR_OBJECT_ENT_RELAY)
 
    ent_type: bpy.props.EnumProperty(
       name="Type",
@@ -3069,7 +3307,8 @@ class SR_MATERIAL_PROPERTIES(bpy.types.PropertyGroup):
       ('vertex_blend', "vertex_blend", ''),
       ('water',"water",''),
       ('invisible','Invisible',''),
-      ('boundary','Boundary','')
+      ('boundary','Boundary',''),
+      ('fxglow','FX Glow',''),
       ])
 
    surface_prop: bpy.props.EnumProperty(
@@ -3144,6 +3383,7 @@ class SR_MATERIAL_PROPERTIES(bpy.types.PropertyGroup):
 # ---------------------------------------------------------------------------- #
 
 cv_view_draw_handler = None
+cv_view_pixel_handler = None
 cv_view_shader = gpu.shader.from_builtin('3D_SMOOTH_COLOR')
 cv_view_verts = []
 cv_view_colours = []
@@ -3299,7 +3539,7 @@ def cv_tangent_basis( n, tx, ty ):
 
 # Draw coloured arrow
 #
-def cv_draw_arrow( p0, p1, c0, size=0.15 ):
+def cv_draw_arrow( p0, p1, c0, size=0.25 ):
 #{
    global cv_view_verts, cv_view_colours
 
@@ -3403,7 +3643,8 @@ def cv_draw_lines():
          cv_view_shader, 'LINES', \
          { "pos":cv_view_verts, "color":cv_view_colours })
 
-   lines.draw( cv_view_shader )
+   if bpy.context.scene.SR_data.gizmos:
+      lines.draw( cv_view_shader )
 
    cv_view_verts = []
    cv_view_colours = []
@@ -3480,8 +3721,8 @@ def draw_cone_twist( center, vx, vy, va ):
       p0 = center + (axis + vx*c0 + vy*s0).normalized() * size
       p1 = center + (axis + vx*c1 + vy*s1).normalized() * size
 
-      col0 = ( abs(c0), abs(s0), 0.0, 1.0 )
-      col1 = ( abs(c1), abs(s1), 0.0, 1.0 )
+      col0 = ( abs(c0), abs(s0), 0.0 )
+      col1 = ( abs(c1), abs(s1), 0.0 )
 
       cv_view_verts += [center, p0, p0, p1]
       cv_view_colours += [ (0,0,0), col0, col0, col1 ]
@@ -3638,14 +3879,14 @@ def cv_ent_volume( obj ):
       cv_draw_ucube( obj.matrix_world, (0,1,0) )
 
       if data.target:#{
-         cv_draw_line( obj.location, data.target.location, (0,1,0) )
+         cv_draw_arrow( 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) )
+         cv_draw_arrow( obj.location, data.target.location, (1,1,0) )
       #}
    #}
 #}
@@ -3880,8 +4121,7 @@ def cv_draw_route( route, dij ):
    #}
 #}
 
-def cv_draw():
-#{
+def cv_draw():#{
    global cv_view_shader
    global cv_view_verts
    global cv_view_colours
@@ -3924,6 +4164,30 @@ def cv_draw():
          elif ent_type == 'ent_volume':#{
             cv_ent_volume( obj )
          #}
+         elif ent_type == 'ent_challenge':#{
+            data = obj.SR_data.ent_challenge[0]
+            if data.proxima:#{
+               cv_draw_arrow( obj.location, data.proxima.location, (0,0.2,1.0) )
+            #}
+            if data.target:
+               cv_draw_arrow( obj.location, data.target.location, (0,1.0,0.0) )
+         #}
+         elif ent_type == 'ent_relay':#{
+            data = obj.SR_data.ent_relay[0]
+            if data.target0:
+               cv_draw_arrow( obj.location, data.target0.location, (0,1,0) )
+            if data.target1:
+               cv_draw_arrow( obj.location, data.target1.location, (0,1,0) )
+            if data.target2:
+               cv_draw_arrow( obj.location, data.target2.location, (0,1,0) )
+            if data.target3:
+               cv_draw_arrow( obj.location, data.target3.location, (0,1,0) )
+         #}
+         elif ent_type == 'ent_unlock':#{
+            data = obj.SR_data.ent_unlock[0]
+            if data.target:
+               cv_draw_arrow( obj.location, data.target.location, (0,1.0,0.0) )
+         #}
          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) )
@@ -4090,7 +4354,31 @@ def cv_draw():
    #}
 
    cv_draw_lines()
-   return
+#}
+
+def pos3d_to_2d( pos ):#{
+   return view3d_utils.location_3d_to_region_2d( \
+            bpy.context.region, \
+            bpy.context.space_data.region_3d, pos )
+#}
+
+def cv_draw_pixel():#{
+   if not bpy.context.scene.SR_data.gizmos: return
+   blf.size(0,10)
+   blf.color(0, 1.0,1.0,1.0,0.9)
+   blf.enable(0,blf.SHADOW)
+   blf.shadow(0,3,0.0,0.0,0.0,1.0)
+   for obj in bpy.context.collection.objects:#{
+      ent_type = obj_ent_type( obj )
+      
+      if ent_type != 'none':#{
+         co = pos3d_to_2d( obj.location )
+
+         if not co: continue
+         blf.position(0,co[0],co[1],0)
+         blf.draw(0,ent_type)
+      #}
+   #}
 #}
 
 classes = [ SR_INTERFACE, SR_MATERIAL_PANEL,\
@@ -4115,6 +4403,7 @@ classes = [ SR_INTERFACE, SR_MATERIAL_PANEL,\
             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_ENT_CHALLENGE,SR_OBJECT_ENT_UNLOCK,SR_OBJECT_ENT_RELAY,\
             \
             SR_OBJECT_PROPERTIES, SR_LIGHT_PROPERTIES, SR_BONE_PROPERTIES, 
             SR_MESH_PROPERTIES, SR_MATERIAL_PROPERTIES \
@@ -4141,9 +4430,11 @@ def register():
    bpy.types.Material.SR_data = \
          bpy.props.PointerProperty(type=SR_MATERIAL_PROPERTIES)
 
-   global cv_view_draw_handler
+   global cv_view_draw_handler, cv_view_pixel_handler
    cv_view_draw_handler = bpy.types.SpaceView3D.draw_handler_add(\
       cv_draw,(),'WINDOW','POST_VIEW')
+   cv_view_pixel_handler = bpy.types.SpaceView3D.draw_handler_add(\
+      cv_draw_pixel,(),'WINDOW','POST_PIXEL')
 #}
 
 def unregister():
@@ -4151,8 +4442,9 @@ def unregister():
    for c in classes:
       bpy.utils.unregister_class(c)
 
-   global cv_view_draw_handler
+   global cv_view_draw_handler, cv_view_pixel_handler
    bpy.types.SpaceView3D.draw_handler_remove(cv_view_draw_handler,'WINDOW')
+   bpy.types.SpaceView3D.draw_handler_remove(cv_view_pixel_handler,'WINDOW')
 #}
 
 # ---------------------------------------------------------------------------- #