# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#

import aud
import os
import bpy
import bmesh
import struct
import ctypes
import random
import math
import subprocess
import re

from mathutils import Vector
from mathutils import Quaternion
from bpy_extras.image_utils import load_image
from . import turok_material_panel
from . import turok_object_panel

mat_color_red = "_color_red_t1_" #for map actors with no model
mat_color_green = "_color_green_t1_" #for map staticmeshes with no model

mat_sector_secret = "_sector_secret_t1_"
mat_sector_restricted = "_sector_restricted_t1_"
mat_sector_checkpoint = "_sector_checkpoint_t1_"
mat_sector_savegame = "_sector_savegame_t1_"
mat_sector_teleport = "_sector_teleport_t1_"
mat_sector_teleportair = "_sector_teleportair_t1_"
mat_sector_slopetest = "_sector_slopetest_t1_"
mat_sector_ladder = "_sector_ladder_t1_"
mat_sector_climb = "_sector_climb_t1_"
mat_sector_entercrawl = "_sector_entercrawl_t1_"
mat_sector_crawl = "_sector_crawl_t1_"
mat_sector_cliff = "_sector_cliff_t1_"
mat_sector_block = "_sector_block_t1_"
mat_sector_event = "_sector_event_t1_"
mat_sector_antigravity = "_sector_antigravity_t1_"
mat_sector_water = "_sector_water_t1_"
mat_sector_bridge = "_sector_bridge_t1_"
mat_sector_deathpit = "_sector_deathpit_t1_"
mat_sector_lava = "_sector_lava_t1_"
mat_sector_damage = "_sector_damage_t1_"
mat_sector_default = "_sector_default_t1_"
mat_sector_error = "_sector_error_t1_"
mat_sector_none = "_sector_none_t1_"
mat_sector_ceiling = "_sector_ceiling_t1_"

T1_MATS_COLORS = [(mat_color_red, (1.0, 0.0, 0.0, 1.0)),
                (mat_color_green, (0.0, 1.0, 0.0, 1.0))]
                
T1_MATS_SECTORS = [(mat_sector_none,     (1.000000, 1.000000, 1.000000, 1.0)),
                (mat_sector_error,       (1.000000, 0.000000, 0.000000, 1.0)),
                (mat_sector_secret,      (0.000000, 0.439215, 0.878431, 0.2)),
                (mat_sector_restricted,  (0.000000, 0.000000, 0.000000, 0.8)),
                (mat_sector_checkpoint,  (0.250980, 0.752941, 0.250980, 0.2)),
                (mat_sector_savegame,    (0.250980, 0.752941, 0.501960, 0.2)),
                (mat_sector_teleport,    (0.000000, 0.700000, 1.000000, 0.3)),
                (mat_sector_teleportair, (1.000000, 0.878431, 0.188235, 0.2)),
                (mat_sector_slopetest,   (0.100000, 0.100000, 0.100000, 0.65)),
                (mat_sector_ladder,      (0.878431, 0.439215, 0.878431, 0.25)),
                (mat_sector_climb,       (0.752941, 0.000000, 0.752941, 0.2)),
                (mat_sector_entercrawl,  (0.752941, 0.376470, 0.000000, 0.2)),
                (mat_sector_crawl,       (0.615686, 0.521568, 0.376470, 0.2)),
                (mat_sector_cliff,       (1.000000, 0.376470, 0.000000, 0.2)),
                (mat_sector_block,       (0.188235, 1.000000, 0.500000, 0.2)),
                (mat_sector_event,       (0.000000, 0.752941, 0.000000, 0.2)),
                (mat_sector_antigravity, (0.662745, 0.662745, 0.000000, 0.2)),
                (mat_sector_water,       (0.000000, 0.000000, 0.752941, 0.2)),
                (mat_sector_bridge,      (0.000000, 0.752941, 0.752941, 0.2)),
                (mat_sector_deathpit,    (1.000000, 1.000000, 0.000000, 0.2)),
                (mat_sector_lava,        (0.878431, 0.000000, 0.000000, 0.2)),
                (mat_sector_damage,      (0.300000, 0.000000, 0.000000, 0.3)),
                (mat_sector_default,     (1.000000, 0.131343, 0.131343, 0.2)),
                (mat_sector_ceiling,     (0.1, 1.0, 0.1, 0.1))]
                
curTime = 0.0
deltaTime = 0.0
turok1Defs = {}
turok1CopiedKeyFrame = None
turok1ActionPointName = '__TEMP_ActionPoint_Turok1_'
turok1BoundingBoxName = '__TEMP_BoundingBox_Turok1_'
turok1PlayerScaleName = '__Player_Scale_Turok1_'
turok1WeaponScaleName = '__Weapon_Scale_Turok1_'
turok1WeaponCamName = '__Weapon_Camera_Turok1_'
turokObjectCollectionName = "Turok"
IMPORT_EXPORT_SCALE = 1.0
turok1FPS = 15.0
T1WEAPON_FOV = 47.5
turok1BuiltinTextures = ["_default", "_black", "_white", "_depthbuffer", "_framebuffer", "_simpleShadow", "_wireframeMesh", "_wireframeMeshSelected"]
MELEE_RADIUS_EVENTS = [31, 32, 33, 34, 35, 36, 37, 38, 376, 377, 388]
playingSounds = {} #key is ksnd local path + sound index, value is array of AudioSound
lastSelectedSectorIndex = -1
sectorDirty = False
sectorErrorUpdateTime = 0.0
cachedRegion3D = None
MAT_TEXSET_SUFFIX = "__texset__"

#bmesh Face Layers for sector data
FL_DRAWORDER = None
FL_FLAGS = None
FL_FOGCOLORR = None
FL_FOGCOLORG = None
FL_FOGCOLORB = None
FL_FOGCOLORA = None
FL_WATERCOLORR = None
FL_WATERCOLORG = None
FL_WATERCOLORB = None
FL_WATERCOLORA = None
FL_FOGZFAR = None
FL_WATERZFAR = None
FL_FOGSTART = None
FL_WATERSTART = None
FL_WATERHEIGHT = None
FL_SKYHEIGHT = None
FL_SKYSPEED = None
FL_BLENDLENGTH = None
FL_ARG1 = None
FL_ARG2 = None
FL_ARG3 = None
FL_ARG4 = None
FL_ARG5 = None
FL_ARG6 = None
FL_FLOOR = None
FL_WALL = None
FL_AMBIENCE = None
FL_AUTOMAPCOLOR = None
FL_MUSIC = None
FL_CULLBITS = None
FL_UNUSED1 = None
FL_UNUSED2 = None

#Vertice Layers
VL_CEILINGLINK = None #vertex index in sector mesh
VL_CEILINGZ = None
# -----------------------------------------------------------------------------
# 
def create_BMLayers(bm):
    global FL_DRAWORDER
    global FL_FLAGS
    global FL_FOGCOLORR
    global FL_FOGCOLORG
    global FL_FOGCOLORB
    global FL_FOGCOLORA
    global FL_WATERCOLORR
    global FL_WATERCOLORG
    global FL_WATERCOLORB
    global FL_WATERCOLORA
    global FL_FOGZFAR
    global FL_WATERZFAR
    global FL_FOGSTART
    global FL_WATERSTART
    global FL_WATERHEIGHT
    global FL_SKYHEIGHT
    global FL_SKYSPEED
    global FL_BLENDLENGTH
    global FL_ARG1
    global FL_ARG2
    global FL_ARG3
    global FL_ARG4
    global FL_ARG5
    global FL_ARG6
    global FL_FLOOR
    global FL_WALL
    global FL_AMBIENCE
    global FL_AUTOMAPCOLOR
    global FL_MUSIC
    global FL_CULLBITS
    global FL_UNUSED1
    global FL_UNUSED2
    global VL_CEILINGLINK
    global VL_CEILINGZ
    FL_DRAWORDER = create_iBMLayer(bm, "t1_drawOrder")
    FL_FLAGS = create_iBMLayer(bm, "t1_flags")
    FL_FOGCOLORR = create_fBMLayer(bm, "t1_fogcolorr")
    FL_FOGCOLORG = create_fBMLayer(bm, "t1_fogcolorg")
    FL_FOGCOLORB = create_fBMLayer(bm, "t1_fogcolorb")
    FL_FOGCOLORA = create_fBMLayer(bm, "t1_fogcolora")
    FL_WATERCOLORR = create_fBMLayer(bm, "t1_watercolorr")
    FL_WATERCOLORG = create_fBMLayer(bm, "t1_watercolorg")
    FL_WATERCOLORB = create_fBMLayer(bm, "t1_watercolorb")
    FL_WATERCOLORA = create_fBMLayer(bm, "t1_watercolora")
    FL_FOGZFAR = create_fBMLayer(bm, "t1_fogzfar")
    FL_WATERZFAR = create_fBMLayer(bm, "t1_waterzfar")
    FL_FOGSTART = create_fBMLayer(bm, "t1_fogstart")
    FL_WATERSTART = create_fBMLayer(bm, "t1_waterstart")
    FL_WATERHEIGHT = create_fBMLayer(bm, "t1_waterheight")
    FL_SKYHEIGHT = create_fBMLayer(bm, "t1_skyheight")
    FL_SKYSPEED = create_fBMLayer(bm, "t1_skyspeed")
    FL_BLENDLENGTH = create_fBMLayer(bm, "t1_blendlength")
    FL_ARG1 = create_iBMLayer(bm, "t1_arg1")
    FL_ARG2 = create_iBMLayer(bm, "t1_arg2")
    FL_ARG3 = create_iBMLayer(bm, "t1_arg3")
    FL_ARG4 = create_iBMLayer(bm, "t1_arg4")
    FL_ARG5 = create_iBMLayer(bm, "t1_arg5")
    FL_ARG6 = create_iBMLayer(bm, "t1_arg6")
    FL_FLOOR = create_iBMLayer(bm, "t1_floor")
    FL_WALL = create_iBMLayer(bm, "t1_wall")
    FL_AMBIENCE = create_iBMLayer(bm, "t1_ambience")
    FL_AUTOMAPCOLOR = create_iBMLayer(bm, "t1_automapcolor")
    FL_MUSIC = create_iBMLayer(bm, "t1_music")
    FL_CULLBITS = create_iBMLayer(bm, "t1_cullbits")
    FL_UNUSED1 = create_iBMLayer(bm, "t1_unused1")
    FL_UNUSED2 = create_iBMLayer(bm, "t1_unused2")
    
    VL_CEILINGLINK = create_vert_iBMLayer(bm, "t1_ceilinglink")
    VL_CEILINGZ = create_vert_fBMLayer(bm, "t1_ceilingz")
#end func

# -----------------------------------------------------------------------------
# map vert to bmVert
def bmvert_to_map_sector(map, bmVert):
    if not bmVert.is_valid:
        return
        
    mapVert = map.collisionVertices[bmVert.index]
    bmVert[VL_CEILINGZ] = mapVert[3]
#end func

# -----------------------------------------------------------------------------
# face to map.sector
def bmface_to_map_sector(map, bmFace):
    if not bmFace.is_valid:
        return
        
    sector = map.collisionSectors[bmFace.index]
    sectorset = map.collisionSectorSets[sector.sectorSetIndex]
    bmFace[FL_DRAWORDER] = sector.drawOrder
    bmFace[FL_FLAGS] = (sector.flags | sectorset.flags)
    bmFace[FL_FOGCOLORR] = sectorset.fogColor[0] / 255
    bmFace[FL_FOGCOLORG] = sectorset.fogColor[1] / 255
    bmFace[FL_FOGCOLORB] = sectorset.fogColor[2] / 255
    bmFace[FL_FOGCOLORA] = sectorset.fogColor[3] / 255
    bmFace[FL_WATERCOLORR] = sectorset.waterColor[0] / 255
    bmFace[FL_WATERCOLORG] = sectorset.waterColor[1] / 255
    bmFace[FL_WATERCOLORB] = sectorset.waterColor[2] / 255
    bmFace[FL_WATERCOLORA] = sectorset.waterColor[3] / 255
    bmFace[FL_FOGZFAR] = sectorset.fogZFar
    bmFace[FL_WATERZFAR] = sectorset.waterZFar
    bmFace[FL_FOGSTART] = sectorset.fogStart
    bmFace[FL_WATERSTART] = sectorset.waterStart
    bmFace[FL_WATERHEIGHT] = sectorset.waterHeight
    bmFace[FL_SKYHEIGHT] = sectorset.skyHeight
    bmFace[FL_SKYSPEED] = sectorset.skySpeed
    bmFace[FL_BLENDLENGTH] = sectorset.blendLength
    bmFace[FL_ARG1] = sectorset.args[0]
    bmFace[FL_ARG2] = sectorset.args[1]
    bmFace[FL_ARG3] = sectorset.args[2]
    bmFace[FL_ARG4] = sectorset.args[3]
    bmFace[FL_ARG5] = sectorset.args[4]
    bmFace[FL_ARG6] = sectorset.args[5]
    bmFace[FL_FLOOR] = sectorset.floorImpactID
    bmFace[FL_WALL] = sectorset.wallImpactID
    bmFace[FL_AMBIENCE] = sectorset.ambience
    bmFace[FL_AUTOMAPCOLOR] = sectorset.automapColor
    bmFace[FL_MUSIC] = sectorset.music
    bmFace[FL_CULLBITS] = sectorset.cullBits
    bmFace[FL_UNUSED1] = sectorset.unused1
    bmFace[FL_UNUSED2] = sectorset.unused2    
#end if

# -----------------------------------------------------------------------------
# face to UI
def bmface_to_obj_sector(obj, bmFace):
    if not bmFace.is_valid:
        return
        
    bmFace[FL_DRAWORDER] = obj.t1.sec_drawOrder
    bmFace[FL_FLAGS] = flags_to_int(turok_object_panel.T1SectorSetFlags, obj.t1.sec_flags)
    bmFace[FL_FOGCOLORR] = obj.t1.sec_fogColor[0]
    bmFace[FL_FOGCOLORG] = obj.t1.sec_fogColor[1]
    bmFace[FL_FOGCOLORB] = obj.t1.sec_fogColor[2]
    bmFace[FL_FOGCOLORA] = obj.t1.sec_fogColor[3]
    bmFace[FL_WATERCOLORR] = obj.t1.sec_waterColor[0]
    bmFace[FL_WATERCOLORG] = obj.t1.sec_waterColor[1]
    bmFace[FL_WATERCOLORB] = obj.t1.sec_waterColor[2]
    bmFace[FL_WATERCOLORA] = obj.t1.sec_waterColor[3]
    bmFace[FL_FOGZFAR] = obj.t1.sec_fogZFar
    bmFace[FL_WATERZFAR] = obj.t1.sec_waterZFar
    bmFace[FL_FOGSTART] = obj.t1.sec_fogStart
    bmFace[FL_WATERSTART] = obj.t1.sec_waterStart
    bmFace[FL_WATERHEIGHT] = obj.t1.sec_waterHeight
    bmFace[FL_SKYHEIGHT] = obj.t1.sec_skyHeight
    bmFace[FL_SKYSPEED] = obj.t1.sec_skySpeed
    bmFace[FL_BLENDLENGTH] = obj.t1.sec_blendLength
    bmFace[FL_ARG1] = obj.t1.sec_arg1
    bmFace[FL_ARG2] = obj.t1.sec_arg2
    bmFace[FL_ARG3] = obj.t1.sec_arg3
    bmFace[FL_ARG4] = obj.t1.sec_arg4
    bmFace[FL_ARG5] = obj.t1.sec_arg5
    bmFace[FL_ARG6] = obj.t1.sec_arg6
    bmFace[FL_FLOOR] = get_enum_type(turok_object_panel.T1ImpactTypes, obj.t1.sec_floorImpactID)
    bmFace[FL_WALL] = get_enum_type(turok_object_panel.T1ImpactTypes, obj.t1.sec_wallImpactID)
    bmFace[FL_AMBIENCE] = obj.t1.sec_ambience
    bmFace[FL_AUTOMAPCOLOR] = get_enum_type(turok_object_panel.T1SectorColorTypes, obj.t1.sec_automapColor)
    bmFace[FL_MUSIC] = obj.t1.sec_music
    bmFace[FL_CULLBITS] = obj.t1.sec_cullBits
    bmFace[FL_UNUSED1] = obj.t1.sec_unused1
    bmFace[FL_UNUSED2] = obj.t1.sec_unused2
#end if

# -----------------------------------------------------------------------------
# returns a dictionary of obj.t1.sector data difference compared to the bmFace
def get_obj_sector_dict(obj, bmFace):
    dict = {}
    if obj.t1.sec_drawOrder != bmFace[FL_DRAWORDER]: dict["drawOrder"] = obj.t1.sec_drawOrder
    dict["flags"] = flags_to_int(turok_object_panel.T1SectorSetFlags, obj.t1.sec_flags)
    dict["flagmask"] = bmFace[FL_FLAGS] ^ dict["flags"]
    if obj.t1.sec_fogColor[0] != bmFace[FL_FOGCOLORR]: dict["fogColorR"] = obj.t1.sec_fogColor[0]
    if obj.t1.sec_fogColor[1] != bmFace[FL_FOGCOLORG]: dict["fogColorG"] = obj.t1.sec_fogColor[1]
    if obj.t1.sec_fogColor[2] != bmFace[FL_FOGCOLORB]: dict["fogColorB"] = obj.t1.sec_fogColor[2]
    if obj.t1.sec_fogColor[3] != bmFace[FL_FOGCOLORA]: dict["fogColorA"] = obj.t1.sec_fogColor[3]
    if obj.t1.sec_waterColor[0] != bmFace[FL_WATERCOLORR]: dict["waterColorR"] = obj.t1.sec_waterColor[0]
    if obj.t1.sec_waterColor[1] != bmFace[FL_WATERCOLORG]: dict["waterColorG"] = obj.t1.sec_waterColor[1]
    if obj.t1.sec_waterColor[2] != bmFace[FL_WATERCOLORB]: dict["waterColorB"] = obj.t1.sec_waterColor[2]
    if obj.t1.sec_waterColor[3] != bmFace[FL_WATERCOLORA]: dict["waterColorA"] = obj.t1.sec_waterColor[3]
    if obj.t1.sec_fogZFar != bmFace[FL_FOGZFAR]: dict["fogZFar"] = obj.t1.sec_fogZFar
    if obj.t1.sec_waterZFar != bmFace[FL_WATERZFAR]: dict["waterZFar"] = obj.t1.sec_waterZFar
    if obj.t1.sec_fogStart != bmFace[FL_FOGSTART]: dict["fogStart"] = obj.t1.sec_fogStart
    if obj.t1.sec_waterStart != bmFace[FL_WATERSTART]: dict["waterStart"] = obj.t1.sec_waterStart
    if obj.t1.sec_waterHeight != bmFace[FL_WATERHEIGHT]: dict["waterHeight"] = obj.t1.sec_waterHeight
    if obj.t1.sec_skyHeight != bmFace[FL_SKYHEIGHT]: dict["skyHeight"] = obj.t1.sec_skyHeight
    if obj.t1.sec_skySpeed != bmFace[FL_SKYSPEED]: dict["skySpeed"] = obj.t1.sec_skySpeed
    if obj.t1.sec_blendLength != bmFace[FL_BLENDLENGTH]: dict["blendLength"] = obj.t1.sec_blendLength
    if obj.t1.sec_arg1 != bmFace[FL_ARG1]: dict["arg1"] = obj.t1.sec_arg1
    if obj.t1.sec_arg2 != bmFace[FL_ARG2]: dict["arg2"] = obj.t1.sec_arg2
    if obj.t1.sec_arg3 != bmFace[FL_ARG3]: dict["arg3"] = obj.t1.sec_arg3
    if obj.t1.sec_arg4 != bmFace[FL_ARG4]: dict["arg4"] = obj.t1.sec_arg4
    if obj.t1.sec_arg5 != bmFace[FL_ARG5]: dict["arg5"] = obj.t1.sec_arg5
    if obj.t1.sec_arg6 != bmFace[FL_ARG6]: dict["arg6"] = obj.t1.sec_arg6
    floorImpactID = get_enum_type(turok_object_panel.T1ImpactTypes, obj.t1.sec_floorImpactID)
    wallImpactID = get_enum_type(turok_object_panel.T1ImpactTypes, obj.t1.sec_wallImpactID)
    automapColorID = get_enum_type(turok_object_panel.T1SectorColorTypes, obj.t1.sec_automapColor)
    if floorImpactID != bmFace[FL_FLOOR]: dict["floor"] = floorImpactID
    if wallImpactID != bmFace[FL_WALL]: dict["wall"] = wallImpactID
    if obj.t1.sec_ambience != bmFace[FL_AMBIENCE]: dict["ambience"] = obj.t1.sec_ambience
    if automapColorID != bmFace[FL_AUTOMAPCOLOR]: dict["automapColor"] = automapColorID
    if obj.t1.sec_music != bmFace[FL_MUSIC]: dict["music"] = obj.t1.sec_music
    if obj.t1.sec_cullBits != bmFace[FL_CULLBITS]: dict["cullbits"] = obj.t1.sec_cullBits
    if obj.t1.sec_unused1 != bmFace[FL_UNUSED1]: dict["unused1"] = obj.t1.sec_unused1
    if obj.t1.sec_unused2 != bmFace[FL_UNUSED2]: dict["unused2"] = obj.t1.sec_unused2
    
    return dict
#end func

# -----------------------------------------------------------------------------
# face to dictionary of obj.t1.sector data
def bmface_to_dict(dict, bmFace):
    if not bmFace.is_valid:
        return
    
    if "drawOrder" in dict: bmFace[FL_DRAWORDER] = dict["drawOrder"]
    # set bits that have changed to 0 and then add bits that have changed
    bmFace[FL_FLAGS] = (bmFace[FL_FLAGS] & ~dict["flagmask"]) | (dict["flags"] & dict["flagmask"])
    if "fogColorR" in dict: bmFace[FL_FOGCOLORR] = dict["fogColorR"]
    if "fogColorG" in dict: bmFace[FL_FOGCOLORG] = dict["fogColorG"]
    if "fogColorB" in dict: bmFace[FL_FOGCOLORB] = dict["fogColorB"]
    if "fogColorA" in dict: bmFace[FL_FOGCOLORA] = dict["fogColorA"]
    if "waterColorR" in dict: bmFace[FL_WATERCOLORR] = dict["waterColorR"]
    if "waterColorG" in dict: bmFace[FL_WATERCOLORG] = dict["waterColorG"]
    if "waterColorB" in dict: bmFace[FL_WATERCOLORB] = dict["waterColorB"]
    if "waterColorA" in dict: bmFace[FL_WATERCOLORA] = dict["waterColorA"]
    if "fogZFar" in dict: bmFace[FL_FOGZFAR] = dict["fogZFar"]
    if "waterZFar" in dict: bmFace[FL_WATERZFAR] = dict["waterZFar"]
    if "fogStart" in dict: bmFace[FL_FOGSTART] = dict["fogStart"]
    if "waterStart" in dict: bmFace[FL_WATERSTART] = dict["waterStart"]
    if "waterHeight" in dict: bmFace[FL_WATERHEIGHT] = dict["waterHeight"]
    if "skyHeight" in dict: bmFace[FL_SKYHEIGHT] = dict["skyHeight"]
    if "skySpeed" in dict: bmFace[FL_SKYSPEED] = dict["skySpeed"]
    if "blendLength" in dict: bmFace[FL_BLENDLENGTH] = dict["blendLength"]
    if "arg1" in dict: bmFace[FL_ARG1] = dict["arg1"]
    if "arg2" in dict: bmFace[FL_ARG2] = dict["arg2"]
    if "arg3" in dict: bmFace[FL_ARG3] = dict["arg3"]
    if "arg4" in dict: bmFace[FL_ARG4] = dict["arg4"]
    if "arg5" in dict: bmFace[FL_ARG5] = dict["arg5"]
    if "arg6" in dict: bmFace[FL_ARG6] = dict["arg6"]
    if "floor" in dict: bmFace[FL_FLOOR] = dict["floor"]
    if "wall" in dict: bmFace[FL_WALL] = dict["wall"]
    if "ambience" in dict: bmFace[FL_AMBIENCE] = dict["ambience"]
    if "automapColor" in dict: bmFace[FL_AUTOMAPCOLOR] = dict["automapColor"]
    if "music" in dict: bmFace[FL_MUSIC] = dict["music"]
    if "cullbits" in dict: bmFace[FL_CULLBITS] = dict["cullbits"]
    if "unused1" in dict: bmFace[FL_UNUSED1] = dict["unused1"]
    if "unused2" in dict: bmFace[FL_UNUSED2] = dict["unused2"]
#end if

# -----------------------------------------------------------------------------
# Returns the event TID of the face or None if does not have the event flag on
def bmface_has_ceiling(bmFace):
    if not bmFace.is_valid:
        return None
        
    return (bmFace[FL_FLAGS] & get_enum_type(turok_object_panel.T1SectorSetFlags, "CEILING")) != 0
#end def

# -----------------------------------------------------------------------------
# Returns the event TID of the face or None if does not have the event flag on
def bmface_eventTID(bmFace):
    if not bmFace.is_valid:
        return None
        
    if (bmFace[FL_FLAGS] & get_enum_type(turok_object_panel.T1SectorSetFlags, "EVENT")) == 0:
        return None
        
    return bmFace[FL_ARG4]
#end def

# -----------------------------------------------------------------------------
# UI to face
def obj_sector_to_bmface(obj, bmFace):
    if not bmFace.is_valid:
        return
        
    obj.t1.sec_drawOrder = bmFace[FL_DRAWORDER]
    obj.t1.sec_flags = int_to_flags(turok_object_panel.T1SectorSetFlags, bmFace[FL_FLAGS])
    obj.t1.sec_fogColor[0] = bmFace[FL_FOGCOLORR]
    obj.t1.sec_fogColor[1] = bmFace[FL_FOGCOLORG]
    obj.t1.sec_fogColor[2] = bmFace[FL_FOGCOLORB]
    obj.t1.sec_fogColor[3] = bmFace[FL_FOGCOLORA]
    obj.t1.sec_waterColor[0] = bmFace[FL_WATERCOLORR]
    obj.t1.sec_waterColor[1] = bmFace[FL_WATERCOLORG]
    obj.t1.sec_waterColor[2] = bmFace[FL_WATERCOLORB]
    obj.t1.sec_waterColor[3] = bmFace[FL_WATERCOLORA]
    obj.t1.sec_fogZFar = bmFace[FL_FOGZFAR]
    obj.t1.sec_waterZFar = bmFace[FL_WATERZFAR]
    obj.t1.sec_fogStart = bmFace[FL_FOGSTART]
    obj.t1.sec_waterStart = bmFace[FL_WATERSTART]
    obj.t1.sec_waterHeight = bmFace[FL_WATERHEIGHT]
    obj.t1.sec_skyHeight = bmFace[FL_SKYHEIGHT]
    obj.t1.sec_skySpeed = bmFace[FL_SKYSPEED]
    obj.t1.sec_blendLength = bmFace[FL_BLENDLENGTH]
    obj.t1.sec_arg1 = bmFace[FL_ARG1]
    obj.t1.sec_arg2 = bmFace[FL_ARG2]
    obj.t1.sec_arg3 = bmFace[FL_ARG3]
    obj.t1.sec_arg4 = bmFace[FL_ARG4]
    obj.t1.sec_arg5 = bmFace[FL_ARG5]
    obj.t1.sec_arg6 = bmFace[FL_ARG6]
    obj.t1.sec_floorImpactID = int_to_enum(turok_object_panel.T1ImpactTypes, bmFace[FL_FLOOR])
    obj.t1.sec_wallImpactID = int_to_enum(turok_object_panel.T1ImpactTypes, bmFace[FL_WALL])
    obj.t1.sec_ambience = bmFace[FL_AMBIENCE]
    obj.t1.sec_automapColor = int_to_enum(turok_object_panel.T1SectorColorTypes, bmFace[FL_AUTOMAPCOLOR])
    obj.t1.sec_music = bmFace[FL_MUSIC]
    obj.t1.sec_cullBits = bmFace[FL_CULLBITS]
    obj.t1.sec_unused1 = bmFace[FL_UNUSED1]
    obj.t1.sec_unused2 = bmFace[FL_UNUSED2]
#end if

# -----------------------------------------------------------------------------
# face to UI
def bmface_update_material_index(mesh, bmFace):
    if not bmFace.is_valid:
        return
        
    enumFlags = turok_object_panel.T1SectorSetFlags
    flags = bmFace[FL_FLAGS]
    if bmFace.normal.z <= 0.0:
        set_bmface_material_index(mesh, bmFace, mat_sector_error)
    elif (flags & get_enum_type(enumFlags, "SECRET")) != 0:
        set_bmface_material_index(mesh, bmFace, mat_sector_secret)
    elif (flags & get_enum_type(enumFlags, "RESTRICTED")) != 0:
        set_bmface_material_index(mesh, bmFace, mat_sector_restricted)
    elif (flags & get_enum_type(enumFlags, "CHECKPOINT")) != 0:
        set_bmface_material_index(mesh, bmFace, mat_sector_checkpoint)
    elif (flags & get_enum_type(enumFlags, "SAVEGAME")) != 0:
        set_bmface_material_index(mesh, bmFace, mat_sector_savegame)
    elif (flags & get_enum_type(enumFlags, "TELEPORT")) != 0:
        set_bmface_material_index(mesh, bmFace, mat_sector_teleport)
    elif (flags & get_enum_type(enumFlags, "TELEPORTAIR")) != 0:
        set_bmface_material_index(mesh, bmFace, mat_sector_teleportair)
    elif (flags & get_enum_type(enumFlags, "SLOPETEST")) != 0:
        set_bmface_material_index(mesh, bmFace, mat_sector_slopetest)
    elif (flags & get_enum_type(enumFlags, "LADDER")) != 0:
        set_bmface_material_index(mesh, bmFace, mat_sector_ladder)
    elif (flags & get_enum_type(enumFlags, "CLIMB")) != 0:
        set_bmface_material_index(mesh, bmFace, mat_sector_climb)
    elif (flags & get_enum_type(enumFlags, "ENTERCRAWL")) != 0:
        set_bmface_material_index(mesh, bmFace, mat_sector_entercrawl)
    elif (flags & get_enum_type(enumFlags, "CRAWL")) != 0:
        set_bmface_material_index(mesh, bmFace, mat_sector_crawl)
    elif (flags & get_enum_type(enumFlags, "CLIFF")) != 0:
        set_bmface_material_index(mesh, bmFace, mat_sector_cliff)
    elif (flags & get_enum_type(enumFlags, "BLOCK")) != 0:
        set_bmface_material_index(mesh, bmFace, mat_sector_block)
    elif (flags & get_enum_type(enumFlags, "EVENT")) != 0:
        set_bmface_material_index(mesh, bmFace, mat_sector_event)
    elif (flags & get_enum_type(enumFlags, "ANTIGRAVITY")) != 0:
        set_bmface_material_index(mesh, bmFace, mat_sector_antigravity)
    elif (flags & get_enum_type(enumFlags, "ANTIGRAVITY")) != 0:
        set_bmface_material_index(mesh, bmFace, mat_sector_antigravity)
    elif (flags & get_enum_type(enumFlags, "WATER")) != 0:
        set_bmface_material_index(mesh, bmFace, mat_sector_water)
    elif (flags & get_enum_type(enumFlags, "BRIDGE")) != 0:
        set_bmface_material_index(mesh, bmFace, mat_sector_bridge)
    elif (flags & get_enum_type(enumFlags, "DEATHPIT")) != 0:
        set_bmface_material_index(mesh, bmFace, mat_sector_deathpit)
    elif (flags & get_enum_type(enumFlags, "LAVA")) != 0:
        set_bmface_material_index(mesh, bmFace, mat_sector_lava)
    elif (flags & get_enum_type(enumFlags, "DAMAGE")) != 0:
        set_bmface_material_index(mesh, bmFace, mat_sector_damage)
    else:
        set_bmface_material_index(mesh, bmFace, mat_sector_default)
    #end if
#end if

# -----------------------------------------------------------------------------
# if layer already exists then returns it else creates it and returns it
def create_iBMLayer(bm, name):
    layer = bm.faces.layers.int.get(name)
    if layer is None:
        return bm.faces.layers.int.new(name)
    return layer
#end func

# -----------------------------------------------------------------------------
# if layer already exists then returns it else creates it and returns it
def create_fBMLayer(bm, name):
    layer = bm.faces.layers.float.get(name)
    if layer is None:
        return bm.faces.layers.float.new(name)
    return layer
#end func

# -----------------------------------------------------------------------------
# if layer already exists then returns it else creates it and returns it
def create_vert_iBMLayer(bm, name):
    layer = bm.verts.layers.int.get(name)
    if layer is None:
        return bm.verts.layers.int.new(name)
    return layer
#end func

# -----------------------------------------------------------------------------
# if layer already exists then returns it else creates it and returns it
def create_vert_fBMLayer(bm, name):
    layer = bm.verts.layers.float.get(name)
    if layer is None:
        return bm.verts.layers.float.new(name)
    return layer
#end func

# -----------------------------------------------------------------------------
# 
def get_last_of_type(type_, iterable):
    for el in reversed(iterable):
        if isinstance(el, type_):
            return el
        #end if
    #ened for
    return None
#end func

# -----------------------------------------------------------------------------
# Sets the bmesh face material index to the material that the mesh has that matches the matName.
# Does nothing if material is not found.
def set_bmface_material_index(mesh, face, matName):
    matIndex = find_material_index(mesh, bpy.data.materials[matName])
    if matIndex != -1:
        face.material_index = matIndex
    #end if
#end func

# -----------------------------------------------------------------------------
# Returns a string of the bad chars that str contains
def string_contains_any(str, set):
    result = ""
    for c in set:
        if c in str:
            result += c
        #end if
    #end for
    return result
#end func

# -----------------------------------------------------------------------------
# Returns material index for this mesh of the mat name or -1 if not found
def find_material_index(mesh, mat):
    for i in range(len(mesh.materials)):
        if mat == mesh.materials[i]:
            return i
    #end for
    return -1
#end func

# -----------------------------------------------------------------------------
# Returns array of all file paths in the directory and its sub directories
def get_all_files_in_dir(dirName):
    filePaths = []
    for root, dirs, files in os.walk(dirName):
        for name in files:
            filePaths.append(os.path.realpath(os.path.join(root, name)))
        #end for
    #end for
    
    return filePaths
#end func
# -----------------------------------------------------------------------------
# 
def open_file(path):
    try:
        if os.path.isfile(path):
            if os.name == "nt":
                os.startfile(path)
            else:
                subprocess.Popen([path], shell=True)
            #end if
        #end if
    except:
        pass
    #end try except
#end func

# -----------------------------------------------------------------------------
# Open windows explorer (if on windows)
def open_folder(path):
    try:
        if os.path.isdir(path) and os.name == "nt":
            os.startfile(path)
    except:
        pass
    #end try except
#end func

# -----------------------------------------------------------------------------
# Returns current time in float
def time_from(timeOffset=0.0):
    global curTime
    return curTime + timeOffset
#end func

# -----------------------------------------------------------------------------
# Returns true if current time has reached the target time
def time_reached(time):
    global curTime
    return curTime >= time
#end func

# -----------------------------------------------------------------------------
# Returns the amount of time from curTime to time
def time_diff(time):
    global curTime
    return math.fabs(curTime - time)
#end func

# -----------------------------------------------------------------------------
# Returns the normalized time from 0..1 using the params and curTime
def time_normalize(timeFrom, timeTo):
    length = timeTo - timeFrom;
    if length <= 0.0:
        return 1.0
    
    return clamp(time_diff(timeFrom) / length, 0.0, 1.0)
#end func

# -----------------------------------------------------------------------------
# Returns the normalized time from 0..1 using the params
def time_normalize(timeCurrent, timeFrom, timeTo):
    length = timeTo - timeFrom;
    if length <= 0.0:
        return 1.0;
    
    return clamp((timeCurrent - timeFrom) / length, 0.0, 1.0);
#end func

# -----------------------------------------------------------------------------
# Returns the dir path in the form of kmat string where each folder is separated by a /
def GetLocalKexPathFromDir(dir):
    result = ""
    path = dir
    while True:
        path, name = os.path.split(path)
        if name == "":
            break
        #end if
        if result == "":
            result = name
        else:
            result = "%s/%s" % (name, result)
        #end if
    #end while
    
    return result
#end func

# -----------------------------------------------------------------------------
# Returns a path by finding a folder named folderName in fullDir and returning it's parent path.
# If folderName is not found returns the fullDir.
# Second Return is the path after the folderName or fullDir if not found.
def GetPathFromDirName(fullDir, folderName):
    path = fullDir
    path2 = ""
    while True:
        path, name = os.path.split(path)
        if name == folderName:
            return path, path2
        elif path == "" or name == "":
            break
        #end if
        if path2 == "":
            path2 = name
        else:
            path2 = os.path.join(name, path2)
        #end if
    #end while
    
    return fullDir, fullDir
#end func

# -----------------------------------------------------------------------------
# Returns the local game texture path from the absolute texture path
def texture_filepath_to_local_path(filepath, startWithTextures=True):
    global turok1BuiltinTextures
    
    newFilePath = os.path.normpath(filepath).strip('\\')
    texFileDir, texfileName = os.path.split(newFilePath)
    texfileNameNoExt = os.path.splitext(texfileName)[0]

    if texfileNameNoExt in turok1BuiltinTextures:
        return texfileNameNoExt
    else:
        parentTexturesDir, texturesSubDir = GetPathFromDirName(texFileDir, "textures")
        #if found a textures sub directory in file path
        if parentTexturesDir != texFileDir and texturesSubDir != "":
            if startWithTextures:
                return "textures/%s/%s" % (texturesSubDir, texfileName)
            else:
                return "%s/%s" % (texturesSubDir, texfileName)
        else:
            if startWithTextures:
                return "textures/%s" % (texfileName)
            else:
                return texfileName
        #end if
    #end if
#end func

# -----------------------------------------------------------------------------
# Returns all actions for the object. Objects actions must be in it's NLA-actions.
def get_nla_actions(obj):
    actions = []
    for track in obj.animation_data.nla_tracks:
        for strip in track.strips:
            if strip.action not in actions:
                actions.append(strip.action)
    return actions
#end func
# -----------------------------------------------------------------------------
# 
def add_handler(handler, func):
    remove_handler(handler, func)
    handler.append(func)
#end func    
# -----------------------------------------------------------------------------
# 
def remove_handler(handler, func):
    for i in range(len(handler)-1, -1, -1):
        if handler[i].__name__.startswith(func.__name__):
            del handler[i]
#end func    
# -----------------------------------------------------------------------------
# Loads .ksnd at local game path and plays the sounds
def play_ksnd(ksndPath, useLocalGamePath=True):
    global playingSounds
    
    if useLocalGamePath:
        path = find_t1_file_path(ksndPath)
    else:
        path = ksndPath
    #end if
    if path is not None:
        ksnds = T1ReadKSound(path)
        if ksnds is not None:
            key = ""
            for ksndIndex, ksnd in enumerate(ksnds):
                key = "%s%i" % (ksndPath, ksndIndex)
                
                #Random chance failed
                if random.random() > ksnd.random:
                    continue
                    
                #Volume Envelope start value too low under certain conditions
                if ksnd.veEnable:
                    if ksnd.veStart <= 0.002 and ksnd.veEnvStartTime != 0:
                        continue
                else:
                    if ksnd.veStart <= 0.002:
                        continue
                #end if
                
                #Pitch Envelope max and start value too low
                if ksnd.peEnable:
                    if ksnd.peStart <= 0.0:
                        continue
                else:
                    if ksnd.peMax <= 0.0:
                        continue
                #end if

                if key in playingSounds:
                    #Can't play until sound is finished playing
                    if ksnd.oneInstance:
                        continue
                    #end if
                else:
                    playingSounds[key] = []
                #end if
                playingSounds[key].append(AudioSound(ksnd, ksndIndex))
            #end for
        #end if
    #end if
#end func

# -----------------------------------------------------------------------------
# 
def play_music_path(relPath):
    addon_prefs = bpy.context.preferences.addons[__package__].preferences
    absPath = os.path.normpath(os.path.join(addon_prefs.devDirectory, relPath))
    if os.path.isfile(absPath):
        aud.Device().play(aud.Sound.file(absPath))
    #end if
#end func
# -----------------------------------------------------------------------------
# 
def play_sound_path(path):
    if os.path.isfile(path):
        aud.Device().play(aud.Sound.file(path))
        
# -----------------------------------------------------------------------------
# Play random sounds from a list of filenames that are in the addon sounds folder
def play_random_addon_sound(list):
    play_sound_path(os.path.join(addon_sounds_dir(), list[random.randint(0, len(list) - 1)] + ".wav"))
#end func

# -----------------------------------------------------------------------------
# Stops all sounds
def stop_sound():
    global playingSounds
    
    playingSounds.clear()
    aud.Device().stopAll()
#end func

# -----------------------------------------------------------------------------
# Returns a list of the used frame Indexes. Always includes the first and last frame.
# aryValues: An array of frame indexes(int) for all frames used
def get_used_frames(aryValues):
    usedFrames = []
    frameLength = len(aryValues)
    if frameLength > 0:
        usedFrames.append(0)
        if frameLength > 1:
            for frameIndex in range(1, frameLength - 1):
                if aryValues[frameIndex] != aryValues[frameIndex-1] or aryValues[frameIndex] != aryValues[frameIndex+1]:
                    usedFrames.append(frameIndex)
                #end if
            #end for
            usedFrames.append(frameLength - 1)
        #end if
    #end if
    return usedFrames
#end func

# -----------------------------------------------------------------------------
# 
def set_sector_mats_alpha():
    addon_prefs = bpy.context.preferences.addons[__package__].preferences
    for sectorMatInfo in T1_MATS_SECTORS:
        if sectorMatInfo[0] == mat_sector_error:
            continue
        mat = bpy.data.materials.get(sectorMatInfo[0])
        if mat is not None:
            bsdf = None
            for node in mat.node_tree.nodes:
                if node.bl_idname == 'ShaderNodeBsdfPrincipled':
                    bsdf = node
                #end if
            #end for
            if node is not None:
                bsdf.inputs['Alpha'].default_value = clamp(sectorMatInfo[1][3] + addon_prefs.sectorAlpha, 0.0, 1.0)
            #end if
        #end if
    #end for
#end func

# -----------------------------------------------------------------------------
# Create Blender Materials for colors (if need to)
def try_create_color_mats():
    addon_prefs = bpy.context.preferences.addons[__package__].preferences
    for sectorMatInfo in T1_MATS_COLORS:
        if sectorMatInfo[0] not in bpy.data.materials:
            mat = bpy.data.materials.new(sectorMatInfo[0])
            mat.use_fake_user = True
            mat.use_nodes = False
            mat.diffuse_color = sectorMatInfo[1]
            mat.use_backface_culling = True
            mat.shadow_method = "NONE"
        #end if
    #end for
    for sectorMatInfo in T1_MATS_SECTORS:
        if sectorMatInfo[0] not in bpy.data.materials:
            mat = bpy.data.materials.new(sectorMatInfo[0])
            mat.use_fake_user = True
            mat.use_nodes = True
            mat.diffuse_color = sectorMatInfo[1]
            if sectorMatInfo[0] == mat_sector_ceiling:
                mat.use_backface_culling = False
            else:
                mat.use_backface_culling = True
            mat.blend_method = "BLEND"
            mat.shadow_method = "NONE"
            
            mat.node_tree.nodes.clear()
            moNode = mat.node_tree.nodes.new("ShaderNodeOutputMaterial")
            moNode.location = [300, 300]

            bsdf = mat.node_tree.nodes.new("ShaderNodeBsdfPrincipled")
            bsdf.location = [10, 300]
            bsdf.inputs['Specular'].default_value = 0.0
            bsdf.inputs['Base Color'].default_value = (0.0, 0.0, 0.0, 1.0)
            if sectorMatInfo[0] == mat_sector_error:
                bsdf.inputs['Alpha'].default_value = clamp(sectorMatInfo[1][3], 0.0, 1.0)
                bsdf.inputs['Emission'].default_value = sectorMatInfo[1]
                node_mixrgb = mat.node_tree.nodes.new("ShaderNodeMixRGB")
                node_mixrgb.location = [-190, 300]
                node_mixrgb.blend_type = "MIX"
                node_mixrgb.use_clamp = True
                node_mixrgb.inputs[1].default_value = (0.0, 0.0, 0.0, 1.0)
                node_mixrgb.inputs[2].default_value = (1.0, 0.0, 0.0, 1.0)
                mat.node_tree.links.new(bsdf.inputs['Emission'], node_mixrgb.outputs['Color'])
                node_wire = mat.node_tree.nodes.new("ShaderNodeWireframe")
                node_wire.location = [-390, 300]
                node_wire.use_pixel_size = False
                node_wire.inputs[0].default_value = 20.0
                mat.node_tree.links.new(node_mixrgb.inputs['Fac'], node_wire.outputs['Fac'])
            else:
                bsdf.inputs['Alpha'].default_value = clamp(sectorMatInfo[1][3] + addon_prefs.sectorAlpha, 0.0, 1.0)
                bsdf.inputs['Emission'].default_value = sectorMatInfo[1]
            #end if
            
            mat.node_tree.links.new(moNode.inputs['Surface'], bsdf.outputs['BSDF'])
        #end if
    #end if
#end func
# -----------------------------------------------------------------------------
# Sets the blender mode. If context object is None then does nothing. (instead of throwing an error)
def mode_set(newMode):
    if bpy.context.object is None:
        return
        
    bpy.ops.object.mode_set(mode=newMode)
#end func

# -----------------------------------------------------------------------------
# Returns a list of all the children of obj and their children
def get_all_children(obj, includeSelf=False):
    return [o for o in bpy.data.objects if obj_child_of(obj, o, includeSelf)]
#end func

# -----------------------------------------------------------------------------
# Returns a list of all the children of obj and their children
def get_all_mesh_children(obj, includeSelf=False):
    return [o for o in bpy.data.objects if o.type == 'MESH' and obj_child_of(obj, o, includeSelf)]
#end func

# -----------------------------------------------------------------------------
# Returns true if childObj is a child or sub child and so on of obj
def obj_child_of(obj, childObj, includeSelf=False):
    if obj == childObj:
        if includeSelf:
            return True
        else:
            return False
        #end if
    #end if
    parentObj = childObj.parent
    while parentObj is not None:
        if parentObj == obj:
            return True
        parentObj = parentObj.parent
    #end while
    
    return False
#end func

# -----------------------------------------------------------------------------
# Mutiplies each vector component in a by b and returns the new Vector
def VectorMultiply(a, b):
    return Vector((a.x * b.x, a.y * b.y, a.z * b.z))
#end func
# -----------------------------------------------------------------------------
# Creates a cube mesh object in the scene and returns the Object
def create_cube(objName='Cube', width=1.0, height=1.0, depth=1.0, triangulate=False):
    return create_cube(objName, Vector((-width, -depth, -height)), Vector((width, depth, height)), triangulate)
#end func
# -----------------------------------------------------------------------------
# Creates a cube mesh object in the scene and returns the Object
def create_cube(objName, min, max, triangulate=False):
    verts = []
    faces = []
    if triangulate:
        verts = [(min.x, min.y, min.z), (min.x, min.y, max.z),
                (min.x, max.y, min.z), (min.x, max.y, max.z),
                (max.x, min.y, min.z), (max.x, min.y, max.z),
                (max.x, max.y, min.z), (max.x, max.y, max.z)]
        faces = [(1, 2, 0), (3, 6, 2), (7, 4, 6), (5, 0, 4), (6, 0, 2), (3, 5, 7), (1, 3, 2),
                (3, 7, 6), (7, 5, 4), (5, 1, 0), (6, 4, 0), (3, 1, 5)]
    else:
        verts = [
            (max.x, max.y, min.z),
            (max.x, min.y, min.z),
            (min.x, min.y, min.z),
            (min.x, max.y, min.z),
            (max.x, max.y, max.z),
            (max.x, min.y, max.z),
            (min.x, min.y, max.z),
            (min.x, max.y, max.z)
        ]
        faces = [
            (0, 1, 2, 3),
            (4, 7, 6, 5),
            (0, 4, 5, 1),
            (1, 5, 6, 2),
            (2, 6, 7, 3),
            (4, 0, 3, 7)
        ]
    #end if
        
    mesh = bpy.data.meshes.new("Box")
    mesh.from_pydata(verts, [], faces)
    mesh.validate(clean_customdata=False)
    mesh.update(calc_edges=True)

    obj = bpy.data.objects.new(objName, mesh)
    bpy.context.scene.collection.objects.link(obj)
    return obj
    
# -----------------------------------------------------------------------------
# Returns Object
def create_turok_object(objName, objData, turokObjType, rotMode="QUATERNION"):
    global turokObjectCollectionName
    obj = bpy.data.objects.new(name=objName, object_data=objData)
    obj.rotation_mode = rotMode
    obj.t1.objType = turokObjType
    bpy.context.scene.collection.objects.link(obj)
    get_collection(turokObjectCollectionName).objects.link(obj)
    return obj
    
# -----------------------------------------------------------------------------
# Returns Object
def create_turok_object_cube(objName, turokObjType, min, max, rotMode="QUATERNION"):
    global turokObjectCollectionName
    obj = create_cube(objName, min, max)
    obj.rotation_mode = rotMode
    obj.t1.objType = turokObjType
    get_collection(turokObjectCollectionName).objects.link(obj)
    return obj

# -----------------------------------------------------------------------------
# Returns the root turok object from current obj
def get_root_turok_object(obj, turokObjType, useNone=False):
    rootObj = None if useNone else obj
    parentObj = obj.parent
    while True:
        if parentObj is None:
            return rootObj
        #end if
        if parentObj.t1.objType == turokObjType:
            rootObj = parentObj
        #end if
        parentObj = parentObj.parent
    #end while
#end func
# -----------------------------------------------------------------------------
# Returns True if object is in collection name
def object_in_collection(obj, cName):
    for col in obj.users_collection:
        if col.name == "Turok":
            return True
    return False
    
# # -----------------------------------------------------------------------------
# # Expands/Collapses outliner. if state is 1 will expand all collections.
# # if state is 2 will collapse them.
# def outliner_expand(state):
    # area = next(a for a in bpy.context.screen.areas if a.type == 'OUTLINER')
    # bpy.ops.outliner.show_hierarchy({'area': area}, 'INVOKE_DEFAULT')
    # bpy.ops.wm.redraw_timer(type='DRAW_WIN', iterations=1)
    # for i in range(state):
        # bpy.ops.outliner.expanded_toggle({'area': area})
    # #end for
    # area.tag_redraw()
# #end func

# -----------------------------------------------------------------------------
# Returns Object else returns None
def get_collection(colName, addIfNone = True):
    if colName not in bpy.data.collections:
        collection = bpy.data.collections.new(colName)
        bpy.context.scene.collection.children.link(collection)
        # outliner_expand(2)
        return collection
    else:
        return bpy.data.collections[colName]
    #end if
#end for
    
# -----------------------------------------------------------------------------
# Returns Object else returns None
def get_object(objName):
    if objName in bpy.data.objects:
        return bpy.data.objects[objName]
    return None
# -----------------------------------------------------------------------------
# Returns Object Bone else returns None
def get_object_bone(obj, boneName):
    if boneName in obj.data.bones:
        return obj.data.bones[boneName]
    return None
    
# -----------------------------------------------------------------------------
# Returns index of item in array or -1 if not found
def index_of(ary, item):
    return next((i for i, x in enumerate(ary) if x == item), -1)
# -----------------------------------------------------------------------------
#
def addon_defs_dir():
    return os.path.join(os.path.dirname(os.path.realpath(__file__)), "defs")
# -----------------------------------------------------------------------------
#
def addon_texture_dir():
    return os.path.join(os.path.dirname(os.path.realpath(__file__)), "textures")
# -----------------------------------------------------------------------------
#
def addon_sounds_dir():
    return os.path.join(os.path.dirname(os.path.realpath(__file__)), "sounds")
# -----------------------------------------------------------------------------
# Returns the enum value if matches the enums stringID. Else returns 0.
def get_enum_type(enumItems, enumStr):
    for e in enumItems:
        if e[0] == enumStr:
            return e[3]
            
    return 0
#end func
# -----------------------------------------------------------------------------
# Returns the enum index if matches the enums stringID. Else returns -1
def get_enum_index(enumItems, enumStr):
    for eIdx, e in enumerate(enumItems):
        if e[0] == enumStr:
            return eIdx
            
    return -1
#end func
# -----------------------------------------------------------------------------
# Returns true if string is in the enumItems
def has_enum_type(enumItems, enumStr):
    for e in enumItems:
        if e[0] == enumStr:
            return True
            
    return False

# -----------------------------------------------------------------------------
# Returns the enum name string that has the same id value else returns the first enum name string
def int_to_enum(enumItems, iValue):
    for e in enumItems:
        if e[3] == iValue:
            return e[0]
            
    return enumItems[0][0]
# -----------------------------------------------------------------------------
#
def flags_to_int(itemTypes, enumItems):
    v = 0
    for itemType in itemTypes:
        if itemType[0] in enumItems:
            v += itemType[3]
            
    return v
# -----------------------------------------------------------------------------
#
def int_to_flags(itemTypes, iFlags):
    flags = set()
    for itemType in itemTypes:
        if (iFlags & itemType[3]) != 0:
            flags.add(itemType[0])
            
    return flags

# -----------------------------------------------------------------------------
#
def lerp(a, b, t):
    return (1 - t) * a + t * b
#end func

# -----------------------------------------------------------------------------
#
def max(a, b):
    if a > b:
        return a
    return b
#end func

# -----------------------------------------------------------------------------
#
def min(a, b):
    if a < b:
        return a
    return b
#end func
    
# -----------------------------------------------------------------------------
#
def clamp(value, a, b):
    if value < a:
        return a
    if value > b:
        return b
    return value
#end func

# -----------------------------------------------------------------------------
#
def vec3_distance(v1, v2):
    return math.sqrt(math.pow(v2[0] - v1[0], 2) + math.pow(v2[1] - v1[1], 2) + math.pow(v2[2] - v1[2], 2))
#end func

# -----------------------------------------------------------------------------
#
def is_vectors3_equal(v1, v2):
    return v1[0] == v2[0] and v1[1] == v2[1] and v1[2] == v2[2]
#end func

# -----------------------------------------------------------------------------
#
def is_vectors2_equal(v1, v2):
    return v1[0] == v2[0] and v1[1] == v2[1]
#end func

# -----------------------------------------------------------------------------
# 
def ceil_divide(numerator, denominator):
    return 1 + int((numerator - 1) / denominator)
#end func

# -----------------------------------------------------------------------------
#
def read32(data, is_signed):
    return int.from_bytes(data.read(4), 'little', signed=is_signed)

# -----------------------------------------------------------------------------
#
def read16(data, is_signed):
    return int.from_bytes(data.read(2), 'little', signed=is_signed)
    
# -----------------------------------------------------------------------------
#
def read8(data, is_signed):
    return int.from_bytes(data.read(1), 'little', signed=is_signed)

# -----------------------------------------------------------------------------
# Returns bytes object
def readBytes(data, length):
    return bytes(struct.unpack("<%iB" % (length), data.read(length)))

# -----------------------------------------------------------------------------
#
def readFloat(data):
    return struct.unpack('<f', data.read(4))[0]
    
# -----------------------------------------------------------------------------
#
def readVector(data):
    return Vector(struct.unpack('<3f', data.read(12)))
    
# -----------------------------------------------------------------------------
#
def readVector2(data):
    return struct.unpack('<2f', data.read(8))
    
# -----------------------------------------------------------------------------
#
def readVector4(data):
    return struct.unpack('<4f', data.read(16))
    
# -----------------------------------------------------------------------------
#
def readQuat(data):
    v = struct.unpack('<4f', data.read(16))
    qRot = Quaternion((v[3], v[0], v[1], v[2]))
    #qRot.normalize() #normalize alters the data
    return qRot
#end func

# -----------------------------------------------------------------------------
#
def readColorByteRGBA(data):
    return struct.unpack('<4B', data.read(4))
#end func

# -----------------------------------------------------------------------------
#
def readString(data):
    string = ""
    while 1:
        c = read8(data, False)
        if c == 0:
            break
            
        string += str(chr(c))
    #end while
        
    return string
#end func

# -----------------------------------------------------------------------------
#
def readStringCount(data, length):
    return struct.unpack("<%is" % (length), data.read(length))[0].decode("utf-8")
#end func

# -----------------------------------------------------------------------------
#
def write8(data, val, is_signed):
    data.write(val.to_bytes(1, 'little', signed=is_signed))

# -----------------------------------------------------------------------------
#
def write16(data, val, is_signed):
    data.write(val.to_bytes(2, 'little', signed=is_signed))
    
# -----------------------------------------------------------------------------
#
def write32(data, val, is_signed):
    data.write(val.to_bytes(4, 'little', signed=is_signed))

# -----------------------------------------------------------------------------
#  
def writeFloat(data, val):
    data.write(struct.pack('<f', val))
#end func

# -----------------------------------------------------------------------------
#  
def writeColorByteRGBA(data, color):
    data.write(struct.pack('<4B', color[0], color[1], color[2], color[3]))
#end func

# -----------------------------------------------------------------------------
#  
def writeString(data, str):
    data.write(bytes(str, 'utf-8')+b'\0')
    
# -----------------------------------------------------------------------------
#  
def writeBytes(data, bytesObj):
    data.write(bytesObj)
   
# -----------------------------------------------------------------------------
#   
def writeVector(data, vec):
    data.write(struct.pack('<3f', vec.x, vec.y, vec.z))

# -----------------------------------------------------------------------------
#   
def writeVector2(data, vec):
    data.write(struct.pack('<2f', vec[0], vec[1]))
    
# -----------------------------------------------------------------------------
#   
def writeVector4(data, vec):
    data.write(struct.pack('<4f', vec[0], vec[1], vec[2], vec[3]))
    
# -----------------------------------------------------------------------------
#   
def writeQuat(data, quat):
    data.write(struct.pack('<4f', quat.x, quat.y, quat.z, quat.w))
    
# -----------------------------------------------------------------------------
#
def mesh_triangulate(me):
    bm = bmesh.new()
    bm.from_mesh(me)
    bmesh.ops.triangulate(bm, faces=bm.faces)
    bm.to_mesh(me)
    bm.free()
    
# -----------------------------------------------------------------------------
# 
def show_hidden_data():
    addon_prefs = bpy.context.preferences.addons[__package__].preferences
    return addon_prefs.showHiddenData
#end func
# -----------------------------------------------------------------------------
# 
def find_t1_file_path(relPath):
    addon_prefs = bpy.context.preferences.addons[__package__].preferences
    absPaths = [os.path.join(addon_prefs.devDirectory, relPath), os.path.join(addon_prefs.gameDirectory, relPath)]
    for absPath in absPaths:
        absPath = os.path.normpath(absPath)
        if os.path.isfile(absPath):
            return absPath
            
    return None
    
# -----------------------------------------------------------------------------
# 
def prefs():
    return bpy.context.preferences.addons[__package__].preferences

# -----------------------------------------------------------------------------
# 
def get_bone_name(boneIndex):
    return "bone_%03d" % boneIndex
    
# -----------------------------------------------------------------------------
# 
def message_box_draw(self, context):
    msg = context.preferences.addons[__package__].preferences.lastMessage
    self.layout.label(text=msg)
    
# -----------------------------------------------------------------------------
# 
def show_message(message, title = "Info", icon = 'INFO', toConsole = True):
    bpy.context.preferences.addons[__package__].preferences.lastMessage = message
    bpy.context.window_manager.popup_menu(message_box_draw, title = title, icon = icon)
    if toConsole:
        print("[%s] %s" % (title, message))

# -----------------------------------------------------------------------------
# 
def show_error(message, title = "Error", icon = 'ERROR', toConsole = True):
    bpy.context.preferences.addons[__package__].preferences.lastMessage = message
    bpy.context.window_manager.popup_menu(message_box_draw, title = title, icon = icon)
    if toConsole:
        print("[%s] %s" % (title, message))

# -----------------------------------------------------------------------------
# 
def get_fx_desc(fxID):
    global turok1Defs
    
    if "fx" in turok1Defs and fxID in turok1Defs["fx"]:
        return turok1Defs["fx"][fxID]["description"]
        
    return "Fx " + str(fxID)
# -----------------------------------------------------------------------------
# 
def get_sound_desc(soundID):
    global turok1Defs
    
    if "sounds" in turok1Defs and soundID in turok1Defs["sounds"]:
        return turok1Defs["sounds"][soundID]["description"]
        
    return "Sound " + str(soundID)
# -----------------------------------------------------------------------------
# 
def get_ambience_desc(ambienceID):
    global turok1Defs
    
    if "ambience" in turok1Defs and ambienceID in turok1Defs["ambience"]:
        return turok1Defs["ambience"][ambienceID]["name"]
        
    return "Ambience " + str(ambienceID)
# -----------------------------------------------------------------------------
# 
def get_ambience(ambienceID):
    global turok1Defs
    
    if "ambience" in turok1Defs and ambienceID in turok1Defs["ambience"]:
        return turok1Defs["ambience"][ambienceID]
        
    return None
#end func
    
# -----------------------------------------------------------------------------
# Returns created material with texture set index. Returns None if texSetIndex doesn't exist in defs
def create_material_texset(firstMat, texSetIndex):
    # Mat has no nodes
    if firstMat.node_tree is None:
        return None
    #end if
    
    #check if any texture nodes in firstMat have a def for the texSetIndex
    isValidTexSetIndex = False
    for node in firstMat.node_tree.nodes:
        if node.bl_idname == 'ShaderNodeTexImage' and node.image is not None:
            texSetKey = texture_filepath_to_local_path(node.image.filepath)
            texSet = get_textureset(texSetKey)
            if texSet is not None and texSetIndex >= 0 and texSetIndex < len(texSet.textures):
                isValidTexSetIndex = True
                break
            #end if
        #end if
    #end for
    
    if not isValidTexSetIndex:
        return None
    #end if

    realMatName, matTexIndex = get_real_material_name(firstMat.name)
    newName = get_texset_material_name(realMatName, texSetIndex)
    newMat = firstMat.copy()
    newMat.name = newName

    #for every texNode
    for node in newMat.node_tree.nodes:
        if node.bl_idname == 'ShaderNodeTexImage' and node.image is not None:
            texSetKey = texture_filepath_to_local_path(node.image.filepath)
            texLocalPath = get_textureset_texturepath(texSetKey, texSetIndex)
            if texLocalPath is not None:
                texPath = find_t1_file_path(texLocalPath)
                if texPath is not None:
                    node.image = load_image(texPath, check_existing=True)
                #end if
            #end if
        #end if
    #end for
    
    return newMat
#end func

# -----------------------------------------------------------------------------
# Sets the object and it's children materials to use the material with the targetTextureSetIndex
def set_obj_material_tex_index(obj, targetTextureSetIndex):
    #for all child objects in object
    meshChildren = get_all_mesh_children(obj, True)
    for meshChild in meshChildren:
        for matIndex, mat in enumerate(meshChild.data.materials):
            #skip empty materials
            if mat is None:
                continue
            
            firstMat = get_material_texset_index(mat, 0, False)

            newMat = get_material_texset_index(mat, targetTextureSetIndex, True)
            #if is already the right material don't do anything
            if newMat == mat:
                continue
            #end if
            
            #create a new material for the texturesetIndex if it doesn't exist
            if newMat is None:
                newMat = create_material_texset(firstMat, targetTextureSetIndex)
                #texture sets doesn't have a index of targetTextureSetIndex, then use the firstMat
                if newMat is None:
                    newMat = firstMat
                    #if is already the right material don't do anything
                    if newMat == mat:
                        continue
                    #end if
                #end if
            #end if
            
            meshChild.data.materials[matIndex] = newMat
        #end for
    #end for
#end func

# -----------------------------------------------------------------------------
# 
def get_textureset_texturepath(key, index):
    global turok1Defs
    
    if "textureSets" in turok1Defs and key in turok1Defs["textureSets"]:
        texSet = turok1Defs["textureSets"][key]
        if index >= 0 and index < len(texSet.textures):
            return texSet.textures[index]
        else:
            return texSet.textures[0]
        #end if
    #end if
        
    return None
#end func

# -----------------------------------------------------------------------------
# 
def get_textureset(key):
    global turok1Defs
    
    if "textureSets" in turok1Defs and key in turok1Defs["textureSets"]:
        return turok1Defs["textureSets"][key]
    #end if
        
    return None
#end func

# -----------------------------------------------------------------------------
# 
def get_music_desc(musicID):
    global turok1Defs
    
    if "music" in turok1Defs and musicID in turok1Defs["music"]:
        return turok1Defs["music"][musicID]["description"]
        
    return "Music " + str(musicID)
# -----------------------------------------------------------------------------
# 
def get_music_path(musicID):
    global turok1Defs
    
    if "music" in turok1Defs and musicID in turok1Defs["music"]:
        return turok1Defs["music"][musicID]["path"]
        
    return ""
    
# -----------------------------------------------------------------------------
# 
def get_fx_path(fxID):
    global turok1Defs
    
    if "fx" in turok1Defs and fxID in turok1Defs["fx"]:
        return turok1Defs["fx"][fxID]["path"]
        
    return ""
# -----------------------------------------------------------------------------
# 
def get_sound_path(soundID):
    global turok1Defs
    
    if "sounds" in turok1Defs and soundID in turok1Defs["sounds"]:
        return turok1Defs["sounds"][soundID]["path"]
        
    return ""
# -----------------------------------------------------------------------------
# 
def get_animation_name(animID):
    global turok1Defs
    
    if "animations" in turok1Defs and animID in turok1Defs["animations"]:
        return turok1Defs["animations"][animID]["description"]
        
    return "Anim " + str(animID)
# -----------------------------------------------------------------------------
# 
def get_animation_action_name(actionID):
    global turok1Defs
    
    if "animactions" in turok1Defs and actionID in turok1Defs["animactions"]:
        return turok1Defs["animactions"][actionID]["description"]
        
    return "Action " + str(actionID)
# -----------------------------------------------------------------------------
# 
def get_animation_action_full_desc(actionID, arg1):
    global turok1Defs
    
    if "animactions" in turok1Defs and actionID in turok1Defs["animactions"]:
        desc = turok1Defs["animactions"][actionID]["description"]
        argDesc = get_animation_action_arg_lookup(actionID, 0, arg1)
        if argDesc != "":
            return ("%s: %s" % (desc, argDesc))
            
        return desc
        
    return "Action " + str(actionID)
# -----------------------------------------------------------------------------
# 
def get_animation_action_arg_lookup(actionID, argIndex, argValue):
    argValue = int(argValue)
    argDesc = get_animation_action_arg_name(actionID, argIndex)
    if argDesc == "Sound ID":
        return get_sound_desc(argValue)
    elif argDesc == "Fx ID":
        return get_fx_desc(argValue)
        
    return ""
# -----------------------------------------------------------------------------
# 
def is_arg_sound(actionID, argIndex):
    return get_animation_action_arg_name(actionID, argIndex) == "Sound ID"
# -----------------------------------------------------------------------------
# 
def is_arg_fx(actionID, argIndex):
    return get_animation_action_arg_name(actionID, argIndex) == "Fx ID"
# -----------------------------------------------------------------------------
# 
def get_animation_action_arg_name(actionID, argIndex):
    global turok1Defs
    
    if "animactions" in turok1Defs and actionID in turok1Defs["animactions"] and turok1Defs["animactions"][actionID]["argDesc"][argIndex] != "":
        return turok1Defs["animactions"][actionID]["argDesc"][argIndex]
        
    return "Arg " + str(argIndex)
# -----------------------------------------------------------------------------
# Loads animation, animation actions, fx, and sounds names/description
# into the turok1Defs dictionary
# -----------------------------------------------------------------------------
def load_turok_defs():
    with open(os.path.join(addon_defs_dir(), "animactions.txt"), 'r', encoding='utf-8') as defFile:
        parse_turok_animation_actions_text(defFile.read())
    with open(os.path.join(addon_defs_dir(), "animations.txt"), 'r', encoding='utf-8') as defFile:
        parse_turok_animations_text(defFile.read())
    with open(os.path.join(addon_defs_dir(), "fx.txt"), 'r', encoding='utf-8') as defFile:
        parse_turok_fx_text(defFile.read())
    with open(os.path.join(addon_defs_dir(), "sounds.txt"), 'r', encoding='utf-8') as defFile:
        parse_turok_sounds_text(defFile.read())
    with open(os.path.join(addon_defs_dir(), "music.txt"), 'r', encoding='utf-8') as defFile:
        parse_turok_music_text(defFile.read())
    with open(os.path.join(addon_defs_dir(), "ambience.txt"), 'r', encoding='utf-8') as defFile:
        parse_turok_ambience_text(defFile.read())
    with open(os.path.join(addon_defs_dir(), "shaders.txt"), 'r', encoding='utf-8') as defFile:
        parse_turok_shaders_text(defFile.read())
    textureSetInfoPath = find_t1_file_path("defs/textureSetInfo.txt")
    if textureSetInfoPath is not None:
        with open(textureSetInfoPath, 'r', encoding='utf-8') as defFile:
            parse_turok_textureset_text(defFile.read())
    #end if
#end func

# -----------------------------------------------------------------------------
# 
def parse_turok_animation_actions_text(animActionsText):
    from . import kex_lexer
    global turok1Defs
    
    if animActionsText == "":
        print("Turok1 animation actions def is empty")
        return None

    turok1Defs["animactions"] = {}
    actionsDef = turok1Defs["animactions"]
    kex_lexer.lexer.input(animActionsText)
    expectToken = 0 #animationActionID
    actionID = 0
    argIndex = 0
    for tok in kex_lexer.lexer:
        tokValue = tok.value
        if expectToken == 0: #animationActionID
            if tok.type != kex_lexer.TOKEN_INT:
                print("[Turok1 animation actions def] id type %s was not a %s" % (tok.type, kex_lexer.TOKEN_INT))
                return None
            actionID = tokValue
            actionsDef[actionID] = { "id" : actionID, "description" : "", "argDesc" : [""] * 4}
            for i in range(4):
                actionsDef[actionID]["argDesc"][i] = ""
            expectToken = 1 #animation action description
            
        elif expectToken == 1: #animation action description
            if tok.type != kex_lexer.TOKEN_STRING:
                print("[Turok1 animation actions def] description type %s was not a %s" % (tok.type, kex_lexer.TOKEN_STRING))
                return None
            actionsDef[actionID]["description"] = tokValue
            argIndex = 0
            expectToken = 2 #arg description
            
        elif expectToken == 2: #arg description or new animActionID
            if tok.type == kex_lexer.TOKEN_INT: #is the next anim action ID
                actionID = tokValue
                actionsDef[actionID] = { "id" : actionID, "description" : "", "argDesc" : [""] * 4}
                for i in range(4):
                    actionsDef[actionID]["argDesc"][i] = ""
                expectToken = 1 #animation action description
            elif tok.type == kex_lexer.TOKEN_STRING:
                actionsDef[actionID]["argDesc"][argIndex] = tokValue
                argIndex += 1
                if argIndex > 3:
                    expectToken = 0 #animationActionID
            else:
                print("[Turok1 animation actions def] arg %d description type %s was not a %s" % (argIndex, tok.type, kex_lexer.TOKEN_STRING))
                return None
    
# -----------------------------------------------------------------------------
# 
def parse_turok_animations_text(animationsText):
    from . import kex_lexer
    global turok1Defs
    
    if animationsText == "":
        print("Turok1 animations def is empty")
        return None

    turok1Defs["animations"] = {}
    animsDef = turok1Defs["animations"]
    kex_lexer.lexer.input(animationsText)
    expectToken = 0 #animationID
    animID = 0
    for tok in kex_lexer.lexer:
        tokValue = tok.value
        if expectToken == 0: #animationID
            if tok.type != kex_lexer.TOKEN_INT:
                print("[Turok1 animations def] id type %s was not a %s" % (tok.type, kex_lexer.TOKEN_INT))
                return None
            animID = tokValue
            animsDef[animID] = { "id" : animID, "description" : ""}
            expectToken = 1 #animation description
            
        elif expectToken == 1: #animation description
            if tok.type != kex_lexer.TOKEN_STRING:
                print("[Turok1 animations def] description type %s was not a %s" % (tok.type, kex_lexer.TOKEN_STRING))
                return None
            animsDef[animID]["description"] = tokValue
            expectToken = 0 #animationID

# -----------------------------------------------------------------------------
# 
def parse_turok_fx_text(fxText):
    from . import kex_lexer
    global turok1Defs
    
    if fxText == "":
        print("Turok1 fx def is empty")
        return None

    turok1Defs["fx"] = {}
    fxDef = turok1Defs["fx"]
    kex_lexer.lexer.input(fxText)
    expectToken = 0 #fxID
    fxID = 0
    for tok in kex_lexer.lexer:
        tokValue = tok.value
        if expectToken == 0: #fxID
            if tok.type != kex_lexer.TOKEN_INT:
                print("[Turok1 fx def] id type %s was not a %s" % (tok.type, kex_lexer.TOKEN_INT))
                return None
            fxID = tokValue
            fxDef[fxID] = { "id" : fxID, "path" : "", "description" : "" }
            expectToken = 1 #fx path
            
        elif expectToken == 1: #fx path
            if tok.type != kex_lexer.TOKEN_STRING:
                print("[Turok1 fx def] path type %s was not a %s" % (tok.type, kex_lexer.TOKEN_STRING))
                return None
            fxDef[fxID]["path"] = tokValue
            expectToken = 2 #fx description
            
        elif expectToken == 2: #fx description or fxID
            if tok.type == kex_lexer.TOKEN_INT: #is a new fxID
                fxID = tokValue
                fxDef[fxID] = { "id" : fxID, "path" : "", "description" : "" }
                expectToken = 1 #fx path
            elif tok.type == kex_lexer.TOKEN_STRING:
                fxDef[fxID]["description"] = tokValue
                expectToken = 0 #fxID
            else:
                print("[Turok1 fx def] description type %s was not a %s" % (tok.type, kex_lexer.TOKEN_STRING))
                return None

# -----------------------------------------------------------------------------
# 
def parse_turok_sounds_text(soundsText):
    from . import kex_lexer
    global turok1Defs
    
    if soundsText == "":
        print("Turok1 sounds def is empty")
        return None

    turok1Defs["sounds"] = {}
    soundsDef = turok1Defs["sounds"]
    kex_lexer.lexer.input(soundsText)
    expectToken = 0 #soundID
    soundID = 0
    for tok in kex_lexer.lexer:
        tokValue = tok.value
        if expectToken == 0: #soundID
            if tok.type != kex_lexer.TOKEN_INT:
                print("[Turok1 sound def] id type %s was not a %s" % (tok.type, kex_lexer.TOKEN_INT))
                return None
            soundID = tokValue
            soundsDef[soundID] = { "id" : soundID, "path" : "", "description" : "" }
            expectToken = 1 #sound path
            
        elif expectToken == 1: #sound path
            if tok.type != kex_lexer.TOKEN_STRING:
                print("[Turok1 sound def] path type %s was not a %s" % (tok.type, kex_lexer.TOKEN_STRING))
                return None
            soundsDef[soundID]["path"] = tokValue
            expectToken = 2 #sound description
            
        elif expectToken == 2: #sound description or soundID
            if tok.type == kex_lexer.TOKEN_INT: #is a new soundID
                soundID = tokValue
                soundsDef[soundID] = { "id" : soundID, "path" : "", "description" : "" }
                expectToken = 1 #sound path
            elif tok.type == kex_lexer.TOKEN_STRING:
                soundsDef[soundID]["description"] = tokValue
                expectToken = 0 #soundID
            else:
                print("[Turok1 sound def] description type %s was not a %s" % (tok.type, kex_lexer.TOKEN_STRING))
                return None
                
# -----------------------------------------------------------------------------
# 
def parse_turok_music_text(musicText):
    from . import kex_lexer
    global turok1Defs
    
    if musicText == "":
        print("Turok1 music def is empty")
        return None
    #end if
    turok1Defs["music"] = {}
    musicDef = turok1Defs["music"]
    kex_lexer.lexer.input(musicText)
    expectToken = 0 #musicID
    musicID = 0
    for tok in kex_lexer.lexer:
        tokValue = tok.value
        if expectToken == 0: #musicID
            if tok.type != kex_lexer.TOKEN_INT:
                print("[Turok1 music def] id type %s was not a %s" % (tok.type, kex_lexer.TOKEN_INT))
                return None
            #end if
            musicID = tokValue
            musicDef[musicID] = { "id" : musicID, "path" : "", "description" : "" }
            expectToken = 1 #music path
            
        elif expectToken == 1: #music path
            if tok.type != kex_lexer.TOKEN_STRING:
                print("[Turok1 music def] path type %s was not a %s" % (tok.type, kex_lexer.TOKEN_STRING))
                return None
            #end if
            musicDef[musicID]["path"] = tokValue
            expectToken = 2 #music description
        
        elif expectToken == 2: #music description or musicID
            if tok.type == kex_lexer.TOKEN_INT: #is a new musicID
                musicID = tokValue
                musicDef[musicID] = { "id" : musicID, "path" : "", "description" : "" }
                expectToken = 1 #music path
            elif tok.type == kex_lexer.TOKEN_STRING:
                musicDef[musicID]["description"] = tokValue
                expectToken = 0 #musicID
            else:
                print("[Turok1 music def] description type %s was not a %s" % (tok.type, kex_lexer.TOKEN_STRING))
                return None
            #end if
        #end if
    #end for
#end func                
# -----------------------------------------------------------------------------
# 
def parse_turok_shaders_text(shadersText):
    from . import kex_lexer
    global turok1Defs
    
    if shadersText == "":
        print("Turok1 shaders def is empty")
        return None
    #end if
    
    turok1Defs["shaders"] = {}
    shadersDef = turok1Defs["shaders"]
    kex_lexer.lexer.input(shadersText)
    expectToken = 0 #shaderPath
    shaderKey = ""
    for tok in kex_lexer.lexer:
        tokValue = tok.value
        if expectToken == 0: #shaderPath
            if tok.type != kex_lexer.TOKEN_STRING:
                print("[Turok1 shader def] path type %s was not a %s" % (tok.type, kex_lexer.TOKEN_STRING))
                return None
            #end if
            shaderKey = tokValue
            shadersDef[shaderKey] = { "description" : "" }
            expectToken = 1 #shaderDescription
        elif expectToken == 1: #shaderDescription
            if tok.type != kex_lexer.TOKEN_STRING:
                print("[Turok1 shader def] description type %s was not a %s" % (tok.type, kex_lexer.TOKEN_STRING))
                return None
            #end if
            shadersDef[shaderKey]["description"] = tokValue
            expectToken = 0 #shaderPath
        #end if
    #end for
#end func
# -----------------------------------------------------------------------------
# Return all surfaces in kMatFile as a dictionary of "Surface Name", KmatMaterial.
# If there was an error returns None.
def parse_turok_ambience_text(ambienceText):
    from . import kex_lexer
    global turok1Defs
    
    if ambienceText == "":
        print("Turok1 ambience def is empty")
        return None
    #end if

    turok1Defs["ambience"] = {}
    ambienceDef = turok1Defs["ambience"]
    kex_lexer.lexer.input(ambienceText)
    expectToken = 0 # ambience def name
    ambienceName = ""
    ambienceID = 0
    soundIndex = 0
    for tok in kex_lexer.lexer:
        tokValue = tok.value
        #print('LexToken(%s,%r,%d,%d)' % (tok.type, tok.value, tok.lineno, tok.lexpos))
        
        if expectToken == 0: #ambience def name
            if tok.type != kex_lexer.TOKEN_NAME:
                print("ambience def name type %s was not a %s" % (tok.type, kex_lexer.TOKEN_NAME))
                return None
            expectToken = 1 #ambience id
            ambienceName = tokValue
            ambienceID = 0
            
        elif expectToken == 1: #ambience id
            if tok.type != kex_lexer.TOKEN_INT:
                print("ambience id name type %s was not a %s" % (tok.type, kex_lexer.TOKEN_INT))
                return None
            expectToken = 2 #open ambience brace
            ambienceID = tokValue
            ambienceDef[ambienceID] = {"name": ambienceName, "randomFactor": 0, "reverbType": 0, "sounds": []}
                
        elif expectToken == 2: #open ambience brace
            if tok.type != kex_lexer.TOKEN_OPEN_BRACE:
                print("open ambience brace type %s was not a %s" % (tok.type, kex_lexer.TOKEN_OPEN_BRACE))
                return None
            expectToken = 3 #ambience property or close ambience brace
            
        elif expectToken == 3: #ambience property or close ambience brace
            if tok.type == kex_lexer.TOKEN_CLOSE_BRACE:
                expectToken = 0 #ambience def name
            elif tok.type == kex_lexer.TOKEN_NAME:
                if tokValue == "randomFactor":
                    expectToken = 4 #randomFactor
                elif tokValue == "reverbType":
                    expectToken = 5 #reverbType
                elif tokValue.startswith("sound_"):
                    soundIndex = max(int(tokValue[6:]) - 1, 0)
                    expectToken = 6 #sound path
                    
        elif expectToken == 4: #randomFactor
            if tok.type != kex_lexer.TOKEN_INT:
                print("randomFactor type %s was not a %s" % (tok.type, kex_lexer.TOKEN_INT))
                return None
            expectToken = 3 #ambience property
            ambienceDef[ambienceID]["randomFactor"] = tokValue
            
        elif expectToken == 5: #reverbType
            if tok.type != kex_lexer.TOKEN_INT:
                print("reverbType type %s was not a %s" % (tok.type, kex_lexer.TOKEN_INT))
                return None
            expectToken = 3 #ambience property
            ambienceDef[ambienceID]["reverbType"] = tokValue
            
        elif expectToken == 6: #sound path
            if tok.type != kex_lexer.TOKEN_STRING:
                print("sound path type %s was not a %s" % (tok, kex_lexer.TOKEN_STRING))
                return None
            expectToken = 3 #material property
            sounds = ambienceDef[ambienceID]["sounds"]
            while len(sounds) <= soundIndex:
                sounds.append("")
            #end while
            sounds[soundIndex] = tokValue
            
        #end if
    #end for
#end func

# -----------------------------------------------------------------------------
# Returns the name of the material without the texture set suffix and then
# returns the texture set index if any else returns -1
def get_real_material_name(matName):
    match1 = re.search("%s[0-9]+$" % (MAT_TEXSET_SUFFIX), matName)
    if match1:
        match2 = re.search("[0-9]+", match1.group())
        textureSetIndex = int(match2.group())
        realMatName = matName[:match1.span()[0]]
        return realMatName, textureSetIndex
    #end if
    
    return matName, -1
#end func

# -----------------------------------------------------------------------------
# Returns the name of the material with the texture set suffix and number
def get_texset_material_name(matName, textureSetIndex=0):
    return "%s%s%i" % (matName, MAT_TEXSET_SUFFIX, textureSetIndex)
#end func

# -----------------------------------------------------------------------------
# Returns the material with the texture set index in it's name based on the mat name
def get_material_texset_index(mat, targetTexSetIndex, useNoneForMat=False):
    matName, textureSetIndex = get_real_material_name(mat.name)
    if targetTexSetIndex == 0:
        baseMat = bpy.data.materials.get(matName)
        if baseMat is not None:
            return baseMat
        #end if
    #end if
    baseMat = bpy.data.materials.get(get_texset_material_name(matName, targetTexSetIndex))
    if baseMat is not None:
        return baseMat
    #end if
    if useNoneForMat:
        return None
    else:
        return mat
#end func

# -----------------------------------------------------------------------------
# 
class KmatMaterial:
    def __init__(self, name):
        self.name = name
        self.shaderPath = ""
        self.fullBright = False
        self.blend = False
        self.alphaTest = False
        self.alphaFunc = "gequal" #0:gequal, 1:equal, 2:lequal
        self.alphaMask = 0.6525
        self.sort = "default" #0:default, 1:masked, 2:translucent, 3:water, 4:custom1, 5:custom2, 6:custom3
        self.cull = "none" #0:none, 1:back, 2:front
        self.noDraw = False
        self.depthTest = False
        self.noDepthMask = False
        self.diffuseColor = [1.0, 1.0, 1.0, 1.0] #rgba
        self.textureInfo = [] #list of KMatTextureInfo
        self.params = [] #list of KMatParamInfo
        self.bMat = None
        
    def __str__(self):
        return "%s, %s, fullbright:%s, blend:%s, alphaTest:%s, %s, %s, sort:%s, cull:%s, noDraw:%s, depthTest:%s, noDepthMask:%s, diffuseColor:%s, textureInfos: %s, params: %s" % (self.name,
        self.shaderPath, self.fullBright, self.blend, self.alphaTest, self.alphaFunc,
        self.alphaMask, self.sort, self.cull, self.noDraw, self.depthTest, self.noDepthMask,
        self.diffuseColor, self.textureInfo, self.params)

    def __repr__(self):
        return str(self)
        
    def get_sort_types(self):
        return ["default", "masked", "translucent", "water", "custom1", "custom2", "custom3"]
        
# -----------------------------------------------------------------------------
# 
class KMatTextureInfo:
    def __init__(self):
        self.type = "diffuse" #always "diffuse" except if path is _depthbuffer then it's "depthBuffer"
        self.index = 0 #material texture slot index
        self.path = "_default" #texture path relative to devDirectory/gameDirectory
        self.filter = "linear" #0:linear, 1:nearest
        self.wrap = "repeat" #0:repeat, 1:clamp, 2:mirrored
        
    def __str__(self):
        return "type:%s, index:%s, path:%s, filter:%s, wrap:%s" % (self.type,
        self.index, self.path, self.filter, self.wrap)

    def __repr__(self):
        return str(self)
        
    def __lt__(self, other):
        return self.index < other.index

# -----------------------------------------------------------------------------
# 
class KMatParamInfo:
    def __init__(self):
        self.name = "" #uniform name
        self.valueType = 1 #var type - 0=Integer, 1=Float
        self.value = "" #string value of uniform param
        
    def __str__(self):
        return "name:%s, valueType:%d, value:%s" % (self.name, self.valueType, self.value)

    def __repr__(self):
        return str(self)

# -----------------------------------------------------------------------------
# Return all surfaces in kMatFile as a dictionary of "Surface Name", KmatMaterial.
# If there was an error returns None.
def T1ReadKMat(kMatPath):
    from . import kex_lexer
    
    materials = {}
    kmatText = ""
    with open(kMatPath, 'r', encoding='utf-8') as kmatFile:
        kmatText = kmatFile.read()
        
    kex_lexer.lexer.input(kmatText)
    expectToken = 0 # mat def name
    matName = ""
    samplerIndex = -1
    paramIndex = -1
    for tok in kex_lexer.lexer:
        tokValue = tok.value
        #print('LexToken(%s,%r,%d,%d)' % (tok.type, tok.value, tok.lineno, tok.lexpos))
        
        if expectToken == 0: #material def name
            if tok.type != kex_lexer.TOKEN_NAME:
                print("material def name type %s was not a %s" % (tok.type, kex_lexer.TOKEN_NAME))
                return None
            expectToken = 1 #material id name
            samplerIndex = -1
            paramIndex = -1
            
        elif expectToken == 1: #material id name
            if tok.type != kex_lexer.TOKEN_NAME:
                print("material id name type %s was not a %s" % (tok.type, kex_lexer.TOKEN_NAME))
                return None
            expectToken = 2 #open mat brace
            matName = tokValue
            materials[matName] = KmatMaterial(matName)
                
        elif expectToken == 2: #open mat brace
            if tok.type != kex_lexer.TOKEN_OPEN_BRACE:
                print("open mat brace type %s was not a %s" % (tok.type, kex_lexer.TOKEN_OPEN_BRACE))
                return None
            expectToken = 3 #material property or close mat brace
            
        elif expectToken == 3: #material property or close mat brace
            if tok.type == kex_lexer.TOKEN_CLOSE_BRACE:
                expectToken = 0 #mat def name
                materials[matName].textureInfo.sort()
            elif tok.type == kex_lexer.TOKEN_NAME:
                if tokValue == "shader":
                    expectToken = 5 #shader
                elif tokValue == "fullbright":
                    materials[matName].fullBright = True
                elif tokValue == "blend":
                    materials[matName].blend = True
                elif tokValue == "alphatest":
                    materials[matName].alphaTest = True
                elif tokValue == "alphafunc":
                    expectToken = 4 #alphafunc
                elif tokValue == "alphamask":
                    expectToken = 6 #alphamask
                elif tokValue == "sort":
                    expectToken = 7 #sort
                elif tokValue == "cull":
                    expectToken = 8 #cull
                elif tokValue == "nodraw":
                    materials[matName].noDraw = True
                elif tokValue == "depthtest":
                    materials[matName].depthTest = True
                elif tokValue == "nodepthmask":
                    materials[matName].noDepthMask = True
                elif tokValue == "color_diffuse":
                    expectToken = 9 #color_diffuse
                elif tokValue == "sampler":
                    expectToken = 10 #sampler type
                    materials[matName].textureInfo.append(KMatTextureInfo())
                    samplerIndex += 1
                elif tokValue == "param":
                    expectToken = 17 #param name
                    materials[matName].params.append(KMatParamInfo())
                    paramIndex += 1
                    
        elif expectToken == 4: #alphafunc
            if tok.type != kex_lexer.TOKEN_NAME:
                print("alphafunc type %s was not a %s" % (tok.type, kex_lexer.TOKEN_NAME))
                return None
            expectToken = 3 #material property
            materials[matName].alphaFunc = tokValue
            
        elif expectToken == 5: #shader
            if tok.type != kex_lexer.TOKEN_STRING:
                print("shader type %s was not a %s" % (tok.type, kex_lexer.TOKEN_STRING))
                return None
            expectToken = 3 #material property
            materials[matName].shaderPath = tokValue
            
        elif expectToken == 6: #alphamask
            if tok.type != kex_lexer.TOKEN_FLOAT:
                print("alphamask type %s was not a %s" % (tok, kex_lexer.TOKEN_FLOAT))
                return None
            expectToken = 3 #material property
            materials[matName].alphaMask = tokValue
            
        elif expectToken == 7: #sort
            if tok.type != kex_lexer.TOKEN_NAME:
                print("sort type %s was not a %s" % (tok.type, kex_lexer.TOKEN_NAME));
                return None
            expectToken = 3 #material property
            materials[matName].sort = tokValue

        elif expectToken == 8: #cull
            if tok.type != kex_lexer.TOKEN_NAME:
                print("cull type %s was not a %s" % (tok.type, kex_lexer.TOKEN_NAME))
                return None
            expectToken = 3 #material property
            materials[matName].cull = tokValue
            
        elif expectToken == 9: #color_diffuse
            if tok.type != kex_lexer.TOKEN_VECTOR4:
                print("color_diffuse type %s was not a %s" % (tok.type, kex_lexer.TOKEN_VECTOR4))
                return None
            expectToken = 3 #material property
            materials[matName].diffuseColor = tokValue.split()
            for i in range(len(materials[matName].diffuseColor)):
                materials[matName].diffuseColor[i] = float(materials[matName].diffuseColor[i])
            
        elif expectToken == 10: #sampler type
            if tok.type != kex_lexer.TOKEN_STRING:
                print("sampler type type %s was not a %s" % (tok.type, kex_lexer.TOKEN_STRING))
                return None
            expectToken = 11 #sampler index
            materials[matName].textureInfo[samplerIndex].type = tokValue
            
        elif expectToken == 11: #sampler index
            if tok.type != kex_lexer.TOKEN_INT:
                print("sampler index type %s was not a %s" % (tok.type + (str(tokValue)), kex_lexer.TOKEN_INT))
                return None
            expectToken = 12 #sampler tex path
            materials[matName].textureInfo[samplerIndex].index = tokValue
            materials[matName].textureInfo.sort(key=lambda x: x.index)
            
        elif expectToken == 12: #sampler tex path
            if tok.type != kex_lexer.TOKEN_STRING:
                print("sampler tex path type %s was not a %s" % (tok.type, kex_lexer.TOKEN_STRING))
                return None
            expectToken = 13 #sampler open brace
            materials[matName].textureInfo[samplerIndex].path = tokValue
            
        elif expectToken == 13: #sampler open brace
            if tok.type != kex_lexer.TOKEN_OPEN_BRACE:
                print("sampler open brace type %s was not a %s" % (tok.type, kex_lexer.TOKEN_OPEN_BRACE))
                return None
            expectToken = 14 #sampler property
            
        elif expectToken == 14: #sampler property
            if tok.type == kex_lexer.TOKEN_CLOSE_BRACE:
                expectToken = 3 #material property
            elif tok.type == kex_lexer.TOKEN_NAME:
                if tokValue == "filter":
                    expectToken = 15 #sampler filter
                elif tokValue == "wrap":
                    expectToken = 16 #sampler wrap
                    
        elif expectToken == 15: #sampler filter
            if tok.type != kex_lexer.TOKEN_NAME:
                print("sampler filter type %s was not a %s" % (tok.type, kex_lexer.TOKEN_NAME))
                return None
            expectToken = 14 #sampler property
            materials[matName].textureInfo[samplerIndex].filter = tokValue
            
        elif expectToken == 16: #sampler wrap
            if tok.type != kex_lexer.TOKEN_NAME:
                print("sampler wrap type %s was not a %s" % (tok.type, kex_lexer.TOKEN_NAME))
                return None
            expectToken = 14 #sampler property
            materials[matName].textureInfo[samplerIndex].wrap = tokValue
            
        elif expectToken == 17: #param name
            if tok.type != kex_lexer.TOKEN_STRING:
                print("param name type %s was not a %s" % (tok.type, kex_lexer.TOKEN_STRING))
                return None
            expectToken = 18 #param num
            materials[matName].params[paramIndex].name = tokValue

        elif expectToken == 18: #param num
            if tok.type != kex_lexer.TOKEN_INT:
                print("param num type %s was not a %s" % (tok.type, kex_lexer.TOKEN_INT))
                return None
            expectToken = 19 #param value
            materials[matName].params[paramIndex].num = tokValue

        elif expectToken == 19: #param value
            if tok.type != kex_lexer.TOKEN_STRING:
                print("param value type %s was not a %s" % (tok.type, kex_lexer.TOKEN_STRING))
                return None
            expectToken = 3 #material property
            materials[matName].params[paramIndex].value = tokValue

    return materials
    
# -----------------------------------------------------------------------------
# 
def kmat_to_material(kMatInfo, material):
    global turok1BuiltinTextures
    kMatInfo.bMat = material
    kMatInfo.bMat.name = kMatInfo.name #get_texset_material_name(kMatInfo.name)
    kMatInfo.bMat.preview_render_type = 'FLAT'
    kMatInfo.bMat.use_nodes = True
    kMatInfo.bMat.specular_intensity = 0.0
    kMatInfo.bMat.diffuse_color = kMatInfo.diffuseColor
    kMatInfo.bMat.t1.showConButtons = False
    if kMatInfo.blend:
        if kMatInfo.alphaTest:
            kMatInfo.bMat.blend_method = "CLIP"
        else:
            kMatInfo.bMat.blend_method = "BLEND"
        kMatInfo.bMat.alpha_threshold = 0.3475
        kMatInfo.bMat.t1.blend = True
    else:
        kMatInfo.bMat.blend_method = "OPAQUE"
        kMatInfo.bMat.t1.blend = False
    if kMatInfo.cull == "back":
        kMatInfo.bMat.use_backface_culling = True
    else:
        kMatInfo.bMat.use_backface_culling = False
        
    cullType = kMatInfo.cull.upper()
    if has_enum_type(turok_material_panel.T1MaterialCulls, cullType):
        kMatInfo.bMat.t1.cull = cullType
    kMatInfo.bMat.t1.shaderPath = kMatInfo.shaderPath
    kMatInfo.bMat.t1.fullBright = kMatInfo.fullBright
    kMatInfo.bMat.t1.alphaTest = kMatInfo.alphaTest
    alphaFuncType = kMatInfo.alphaFunc.upper()
    if has_enum_type(turok_material_panel.T1MaterialAlphaFuncs, alphaFuncType):
        kMatInfo.bMat.t1.alphaFunc = alphaFuncType
    kMatInfo.bMat.t1.alphaMask = kMatInfo.alphaMask
    sortType = kMatInfo.sort.upper()
    if has_enum_type(turok_material_panel.T1MaterialSorts, sortType):
        kMatInfo.bMat.t1.sort = sortType
    kMatInfo.bMat.t1.noDraw = kMatInfo.noDraw
    kMatInfo.bMat.t1.depthTest = kMatInfo.depthTest
    kMatInfo.bMat.t1.noDepthMask = kMatInfo.noDepthMask
    kMatInfo.bMat.t1.diffuseColor = kMatInfo.diffuseColor
    kMatInfo.bMat.t1.params.clear()
    for i in range(len(kMatInfo.params)):
        item = kMatInfo.bMat.t1.params.add()
        item.name = kMatInfo.params[i].name
        item.valueType = kMatInfo.params[i].valueType
        item.value = kMatInfo.params[i].value
    #end for
    
    kMatInfo.bMat.node_tree.nodes.clear()
    moNode = kMatInfo.bMat.node_tree.nodes.new("ShaderNodeOutputMaterial")
    moNode.location = [300, 300]

    bsdf = kMatInfo.bMat.node_tree.nodes.new("ShaderNodeBsdfPrincipled")
    bsdf.location = [10, 300]
    bsdf.inputs['Specular'].default_value = 0.0
    
    kMatInfo.bMat.node_tree.links.new(moNode.inputs['Surface'], bsdf.outputs['BSDF'])

    texIndex = 0
    #add to unique textures used
    for texInfo in kMatInfo.textureInfo:
        #create blender texture shader node
        texName = os.path.split(texInfo.path)[1]
        if texName is None:
            texName = texInfo.path
        
        texShaderNode = kMatInfo.bMat.node_tree.nodes.new('ShaderNodeTexImage')
        texShaderNode.t1.samplerIndex = texIndex
        texShaderNode.label = texName
        texShaderNode.location = [-280.0, 300.0 + (texIndex * 280.0)]
        texShaderNode.projection = 'FLAT'
        texFilter = texInfo.filter.lower()
        if texFilter == "linear":
            texShaderNode.interpolation = 'Linear'
        else:
            texShaderNode.interpolation = 'Closest'
        
        texWrap = texInfo.wrap.lower()
        if texWrap == "clamp":
            texShaderNode.extension = 'EXTEND'
        elif texWrap == "repeat":
            texShaderNode.extension = 'REPEAT'
        else: #mirrored
            texShaderNode.extension = 'REPEAT'
            texShaderNode.t1.mirrored = True
            
        #load unique image
        if texInfo.path in turok1BuiltinTextures:
            texImage = load_image(os.path.join(addon_texture_dir(), texInfo.path + ".png"), check_existing=True)
        else:
            texPath = find_t1_file_path(texInfo.path)
            if texPath is not None:
                texImage = load_image(texPath, check_existing=True)
            else:
                texImage = load_image(os.path.join(addon_texture_dir(), "_default.png"), check_existing=True)
            #end if
        #end if
        texShaderNode.image = texImage;
        
        if texIndex == 0:
            kMatInfo.bMat.node_tree.links.new(bsdf.inputs['Base Color'], texShaderNode.outputs['Color'])
            kMatInfo.bMat.node_tree.links.new(bsdf.inputs['Alpha'], texShaderNode.outputs['Alpha'])
        #end if
        texIndex += 1
        
    #end for
    for shaderNode in kMatInfo.bMat.node_tree.nodes:
        shaderNode.select = False
# -----------------------------------------------------------------------------
# 
def material_to_kmat(material):
    global turok1BuiltinTextures
    
    firstMat = get_material_texset_index(material, 0, False)
    matName = get_real_material_name(firstMat.name)[0]
    kMatInfo = KmatMaterial(matName)
    kMatInfo.bMat = firstMat
    kMatInfo.name = matName
    kMatInfo.shaderPath = firstMat.t1.shaderPath
    kMatInfo.fullBright = firstMat.t1.fullBright
    kMatInfo.blend = firstMat.t1.blend    
    kMatInfo.alphaTest = firstMat.t1.alphaTest
    kMatInfo.alphaFunc = firstMat.t1.alphaFunc.lower()
    kMatInfo.alphaMask = firstMat.t1.alphaMask
    kMatInfo.sort = firstMat.t1.sort.lower()
    if firstMat.t1.cull == "AUTO":
        kMatInfo.cull = "back" if firstMat.use_backface_culling else "none"
    else:
        kMatInfo.cull = firstMat.t1.cull.lower()
    kMatInfo.noDraw = firstMat.t1.noDraw
    kMatInfo.depthTest = firstMat.t1.depthTest
    kMatInfo.noDepthMask = firstMat.t1.noDepthMask
    kMatInfo.diffuseColor = firstMat.t1.diffuseColor
    
    #texture nodes to KMatTextureInfo
    textureInfos = []
    if firstMat.node_tree is not None:
        for node in firstMat.node_tree.nodes:
            if node.bl_idname == 'ShaderNodeTexImage':
                texInfo = KMatTextureInfo()
                if node.image is not None:
                    texInfo.path = texture_filepath_to_local_path(node.image.filepath)
                #end if
                if texInfo.path == "_depthbuffer":
                    texInfo.type = "depthBuffer"
                texInfo.index = node.t1.samplerIndex
                texInfo.filter = "nearest" if node.interpolation == 'Closest' else "linear"
                if node.t1.mirrored:
                    texInfo.wrap = "mirrored"
                elif node.extension == 'REPEAT':
                    texInfo.wrap = "repeat"
                else:
                    texInfo.wrap = "clamp"
                #end if
                textureInfos.append(texInfo)
            #end if
        #end for
    #end if
    textureInfos.sort(key=lambda x: x.index) #sort by index
    #make sure has at least 1 texture
    if len(textureInfos) == 0:
        textureInfos.append(KMatTextureInfo())
    #end if
    kMatInfo.textureInfo = textureInfos
    
    params = []
    for param in firstMat.t1.params:
        if param.name and param.value:
            paramInfo = KMatParamInfo()
            paramInfo.name = param.name
            paramInfo.value = param.value
            paramInfo.valueType = param.valueType
            params.append(paramInfo)
        #end if
    #end for
    kMatInfo.params = params
    
    return kMatInfo
#end func

# -----------------------------------------------------------------------------
# 
class KSound:
    def __init__(self):
        self.wavefile = ""
        self.oneInstance = False
        self.loop = False
        self.fadeIfObstructed = False
        self.updateNoEnvelope = False
        self.linkToEnvelope = False
        self.fullVolume = False
        self.delay = 0
        self.priority = 100
        self.random = 1.0
        self.radioVolume = 1
        self.veEnable = False
        self.veMax = 1.0
        self.veStart = 1.0
        self.veEnd = 1.0
        self.veDuration = 60
        self.veEnvStartTime = 0
        self.peEnable = False
        self.peMax = 1.0
        self.peStart = 1.0
        self.peEnd = 1.0
        self.peDuration = 60
        self.peEnvStartTime = 0
    #end func
        
    def __str__(self):
        return "wavefile:%s, oneInstance:%s, loop:%s, fadeIfObstructed:%s, updateNoEnvelope:%s, linkToEnvelope:%s, fullVolume:%s, delay:%d, priority:%d, random:%f, radioVolume:%d, veEnable:%s, veMax:%f, veStart:%f, veEnd:%f, veDuration:%d, veEnvStartTime:%d, peEnable:%s, peMax:%f, peStart:%f, peEnd:%f, peDuration:%d, peEnvStartTime:%d" % (self.wavefile,
        self.oneInstance, self.loop, self.fadeIfObstructed, self.updateNoEnvelope, self.linkToEnvelope, self.fullVolume, self.delay, self.priority,
        self.random, self.radioVolume, self.veEnable, self.veMax, self.veStart, self.veEnd, self.veDuration, self.veEnvStartTime,
        self.peEnable, self.peMax, self.peStart, self.peEnd, self.peDuration, self.peEnvStartTime)
    #end func

    def __repr__(self):
        return str(self)
    #end func
#end class

# -----------------------------------------------------------------------------
# Return all sounds in KSound as an array. If there was an error returns None.
def T1ReadKSound(kSndPath):
    from . import kex_lexer
    
    sounds = []
    ksndText = ""
    with open(kSndPath, 'r', encoding='utf-8') as ksndFile:
        ksndText = ksndFile.read()
        
    kex_lexer.lexer.input(ksndText)
    expectToken = 0 #open all sounds brace
    equalsExpectToken = 0
    soundIndex = -1
    for tok in kex_lexer.lexer:
        tokValue = tok.value
        #print('LexToken(%s,%r,%d,%d)' % (tok.type, tok.value, tok.lineno, tok.lexpos))
        
        if expectToken == 0: #open all sounds brace
            if tok.type == kex_lexer.TOKEN_OPEN_BRACE:
                expectToken = 1 #open sound brace or close all sounds brace
            #end if
        elif expectToken == 1: #open sound brace or close all sounds brace
            if tok.type == kex_lexer.TOKEN_CLOSE_BRACE:
                #end of all sounds list
                break
            elif tok.type == kex_lexer.TOKEN_OPEN_BRACE:
                soundIndex = len(sounds)
                sounds.append(KSound())
                expectToken = 2 #sound props or close sound brace
            #end if
        elif expectToken == 2: #sound props or close sound brace
            if tok.type == kex_lexer.TOKEN_CLOSE_BRACE: #end of current sound block
                expectToken = 1 #open sound brace or close sound brace
            elif tok.type == kex_lexer.TOKEN_NAME:
                if tokValue == "wavefile":
                    expectToken = 3 #equals
                    equalsExpectToken = 4 #wavefile path
                elif tokValue == "delay":
                    expectToken = 3 #equals
                    equalsExpectToken = 5 #prop delay
                elif tokValue == "volume_envelope":
                    expectToken = 3 #equals
                    equalsExpectToken = 6 #volume envelope open brace
                elif tokValue == "pitch_envelope":
                    expectToken = 3 #equals
                    equalsExpectToken = 8 #pitch envelope open brace
                elif tokValue == "bLoop":
                    expectToken = 3 #equals
                    equalsExpectToken = 10 #prop bLoop
                elif tokValue == "bOneInstancePerSource":
                    expectToken = 3 #equals
                    equalsExpectToken = 11 #prop oneInstance
                elif tokValue == "bEnableVolumeEnvelope":
                    expectToken = 3 #equals
                    equalsExpectToken = 12 #prop veEnable
                elif tokValue == "bEnablePitchEnvelope":
                    expectToken = 3 #equals
                    equalsExpectToken = 13 #prop peEnable
                elif tokValue == "bFadeIfObstructed":
                    expectToken = 3 #equals
                    equalsExpectToken = 14 #prop fadeIfObstructed
                elif tokValue == "bUpdateNoEnvelope":
                    expectToken = 3 #equals
                    equalsExpectToken = 15 #prop updateNoEnvelope
                elif tokValue == "bLinkToEnvelope":
                    expectToken = 3 #equals
                    equalsExpectToken = 16 #prop linkToEnvelope
                elif tokValue == "bFullVolume":
                    expectToken = 3 #equals
                    equalsExpectToken = 17 #prop fullVolume
                elif tokValue == "priority":
                    expectToken = 3 #equals
                    equalsExpectToken = 18 #prop priority
                elif tokValue == "random":
                    expectToken = 3 #equals
                    equalsExpectToken = 19 #prop random
                elif tokValue == "radioVolume":
                    expectToken = 3 #equals
                    equalsExpectToken = 20 #prop radioVolume
            #end if
        elif expectToken == 3: #equals
            if tok.type != kex_lexer.TOKEN_EQUALS:
                print("expected = but type %s was not a %s" % (tok.type, kex_lexer.TOKEN_EQUALS))
                return None
            expectToken = equalsExpectToken
            
        elif expectToken == 4: #wavefile path
            if tok.type != kex_lexer.TOKEN_STRING:
                print("wavefile type %s was not a %s" % (tok.type, kex_lexer.TOKEN_STRING))
                return None
            sounds[soundIndex].wavefile = tokValue
            expectToken = 2 #sound props or close sound brace
            
        elif expectToken == 5: #prop delay
            if tok.type != kex_lexer.TOKEN_INT:
                print("delay type %s was not a %s" % (tok.type, kex_lexer.TOKEN_INT))
                return None
            sounds[soundIndex].delay = tokValue
            expectToken = 2 #sound props or close sound brace
            
        elif expectToken == 6: #volume envelope open brace
            if tok.type != kex_lexer.TOKEN_OPEN_BRACE:
                print("open volume envelope brace type %s was not a %s" % (tok.type, kex_lexer.TOKEN_OPEN_BRACE))
                return None
            expectToken = 7 #volume envelope close brace or props
            
        elif expectToken == 7: #volume envelope close brace or props
            if tok.type == kex_lexer.TOKEN_CLOSE_BRACE: #end of volume envelope
                expectToken = 2 #sound props or close sound brace
            elif tok.type == kex_lexer.TOKEN_NAME:
                if tokValue == "max":
                    expectToken = 3 #equals
                    equalsExpectToken = 21 #veMax
                elif tokValue == "start":
                    expectToken = 3 #equals
                    equalsExpectToken = 22 #veStart
                elif tokValue == "end":
                    expectToken = 3 #equals
                    equalsExpectToken = 23 #veEnd
                elif tokValue == "duration":
                    expectToken = 3 #equals
                    equalsExpectToken = 24 #veDuration
                elif tokValue == "envStartTime":
                    expectToken = 3 #equals
                    equalsExpectToken = 25 #veEnvStartTime
            #end if

        elif expectToken == 8: #pitch envelope open brace
            if tok.type != kex_lexer.TOKEN_OPEN_BRACE:
                print("open pitch envelope brace type %s was not a %s" % (tok.type, kex_lexer.TOKEN_OPEN_BRACE))
                return None
            expectToken = 9 #pitch envelope close brace or props
            
        elif expectToken == 9: #pitch envelope close brace or props
            if tok.type == kex_lexer.TOKEN_CLOSE_BRACE: #end of pitch envelope
                expectToken = 2 #sound props or close sound brace
            elif tok.type == kex_lexer.TOKEN_NAME:
                if tokValue == "max":
                    expectToken = 3 #equals
                    equalsExpectToken = 26 #peMax
                elif tokValue == "start":
                    expectToken = 3 #equals
                    equalsExpectToken = 27 #peStart
                elif tokValue == "end":
                    expectToken = 3 #equals
                    equalsExpectToken = 28 #peEnd
                elif tokValue == "duration":
                    expectToken = 3 #equals
                    equalsExpectToken = 29 #peDuration
                elif tokValue == "envStartTime":
                    expectToken = 3 #equals
                    equalsExpectToken = 30 #peEnvStartTime
            #end if

        elif expectToken == 10: #prop bLoop
            if tok.type != kex_lexer.TOKEN_INT:
                print("bLoop type %s was not a %s" % (tok.type, kex_lexer.TOKEN_INT))
                return None
            sounds[soundIndex].loop = tokValue != 0
            expectToken = 2 #sound props or close sound brace
            
        elif expectToken == 11: #prop oneInstance
            if tok.type != kex_lexer.TOKEN_INT:
                print("bOneInstancePerSource type %s was not a %s" % (tok.type, kex_lexer.TOKEN_INT))
                return None
            sounds[soundIndex].oneInstance = tokValue != 0
            expectToken = 2 #sound props or close sound brace
            
        elif expectToken == 12: #prop veEnable
            if tok.type != kex_lexer.TOKEN_INT:
                print("bEnableVolumeEnvelope type %s was not a %s" % (tok.type, kex_lexer.TOKEN_INT))
                return None
            sounds[soundIndex].veEnable = tokValue != 0
            expectToken = 2 #sound props or close sound brace
            
        elif expectToken == 13: #prop peEnable
            if tok.type != kex_lexer.TOKEN_INT:
                print("bEnablePitchEnvelope type %s was not a %s" % (tok.type, kex_lexer.TOKEN_INT))
                return None
            sounds[soundIndex].peEnable = tokValue != 0
            expectToken = 2 #sound props or close sound brace

        elif expectToken == 14: #prop fadeIfObstructed
            if tok.type != kex_lexer.TOKEN_INT:
                print("bFadeIfObstructed type %s was not a %s" % (tok.type, kex_lexer.TOKEN_INT))
                return None
            sounds[soundIndex].fadeIfObstructed = tokValue != 0
            expectToken = 2 #sound props or close sound brace

        elif expectToken == 15: #prop updateNoEnvelope
            if tok.type != kex_lexer.TOKEN_INT:
                print("bUpdateNoEnvelope type %s was not a %s" % (tok.type, kex_lexer.TOKEN_INT))
                return None
            sounds[soundIndex].updateNoEnvelope = tokValue != 0
            expectToken = 2 #sound props or close sound brace

        elif expectToken == 16: #prop linkToEnvelope
            if tok.type != kex_lexer.TOKEN_INT:
                print("bLinkToEnvelope type %s was not a %s" % (tok.type, kex_lexer.TOKEN_INT))
                return None
            sounds[soundIndex].linkToEnvelope = tokValue != 0
            expectToken = 2 #sound props or close sound brace
            
        elif expectToken == 17: #prop fullVolume
            if tok.type != kex_lexer.TOKEN_INT:
                print("bFullVolume type %s was not a %s" % (tok.type, kex_lexer.TOKEN_INT))
                return None
            sounds[soundIndex].fullVolume = tokValue != 0
            expectToken = 2 #sound props or close sound brace

        elif expectToken == 18: #prop priority
            if tok.type != kex_lexer.TOKEN_INT:
                print("priority type %s was not a %s" % (tok.type, kex_lexer.TOKEN_INT))
                return None
            sounds[soundIndex].priority = tokValue
            expectToken = 2 #sound props or close sound brace
            
        elif expectToken == 19: #prop random
            if tok.type != kex_lexer.TOKEN_FLOAT:
                print("random type %s was not a %s" % (tok.type, kex_lexer.TOKEN_FLOAT))
                return None
            sounds[soundIndex].random = tokValue
            expectToken = 2 #sound props or close sound brace
            
        elif expectToken == 20: #prop radioVolume
            if tok.type != kex_lexer.TOKEN_INT:
                print("radioVolume type %s was not a %s" % (tok.type, kex_lexer.TOKEN_INT))
                return None
            sounds[soundIndex].radioVolume = tokValue
            expectToken = 2 #sound props or close sound brace
            
        elif expectToken == 21: #veMax
            if tok.type != kex_lexer.TOKEN_FLOAT:
                print("veMax type %s was not a %s" % (tok.type, kex_lexer.TOKEN_FLOAT))
                return None
            sounds[soundIndex].veMax = tokValue
            expectToken = 7 #volume envelope close brace or props
            
        elif expectToken == 22: #veStart
            if tok.type != kex_lexer.TOKEN_FLOAT:
                print("veStart type %s was not a %s" % (tok.type, kex_lexer.TOKEN_FLOAT))
                return None
            sounds[soundIndex].veStart = tokValue
            expectToken = 7 #volume envelope close brace or props
            
        elif expectToken == 23: #veEnd
            if tok.type != kex_lexer.TOKEN_FLOAT:
                print("veEnd type %s was not a %s" % (tok.type, kex_lexer.TOKEN_FLOAT))
                return None
            sounds[soundIndex].veEnd = tokValue
            expectToken = 7 #volume envelope close brace or props
            
        elif expectToken == 24: #veDuration
            if tok.type != kex_lexer.TOKEN_INT:
                print("veDuration type %s was not a %s" % (tok.type, kex_lexer.TOKEN_INT))
                return None
            sounds[soundIndex].veDuration = tokValue
            expectToken = 7 #volume envelope close brace or props
            
        elif expectToken == 25: #veEnvStartTime
            if tok.type != kex_lexer.TOKEN_INT:
                print("veEnvStartTime type %s was not a %s" % (tok.type, kex_lexer.TOKEN_INT))
                return None
            sounds[soundIndex].veEnvStartTime = tokValue
            expectToken = 7 #volume envelope close brace or props
            
        elif expectToken == 26: #peMax
            if tok.type != kex_lexer.TOKEN_FLOAT:
                print("peMax type %s was not a %s" % (tok.type, kex_lexer.TOKEN_FLOAT))
                return None
            sounds[soundIndex].peMax = tokValue
            expectToken = 9 #pitch envelope close brace or props
            
        elif expectToken == 27: #peStart
            if tok.type != kex_lexer.TOKEN_FLOAT:
                print("peStart type %s was not a %s" % (tok.type, kex_lexer.TOKEN_FLOAT))
                return None
            sounds[soundIndex].peStart = tokValue
            expectToken = 9 #pitch envelope close brace or props
            
        elif expectToken == 28: #peEnd
            if tok.type != kex_lexer.TOKEN_FLOAT:
                print("peEnd type %s was not a %s" % (tok.type, kex_lexer.TOKEN_FLOAT))
                return None
            sounds[soundIndex].peEnd = tokValue
            expectToken = 9 #pitch envelope close brace or props
            
        elif expectToken == 29: #peDuration
            if tok.type != kex_lexer.TOKEN_INT:
                print("peDuration type %s was not a %s" % (tok.type, kex_lexer.TOKEN_INT))
                return None
            sounds[soundIndex].peDuration = tokValue
            expectToken = 9 #pitch envelope close brace or props
            
        elif expectToken == 30: #peEnvStartTime
            if tok.type != kex_lexer.TOKEN_INT:
                print("peEnvStartTime type %s was not a %s" % (tok.type, kex_lexer.TOKEN_INT))
                return None
            sounds[soundIndex].peEnvStartTime = tokValue
            expectToken = 9 #pitch envelope close brace or props

    #end for

    return sounds
#end func

# -----------------------------------------------------------------------------
# 
class TextureSet:
    def __init__(self, key):
        self.animated = 0
        self.speed = 0
        self.textures = [key]
    #end func
        
    def __str__(self):
        return "animated:%s, speed:%s, firstTexture:%s, textureCount:%s" % (self.animated, self.speed, self.textures[0], len(self.textures))
    #end func

    def __repr__(self):
        return str(self)
    #end func
#end class

# -----------------------------------------------------------------------------
# 
def parse_turok_textureset_text(textureSetFileText):
    from . import kex_lexer
    global turok1Defs
    
    if textureSetFileText == "":
        print("Turok1 textureSetInfo def is empty")
        return None
    #end if
    
    turok1Defs["textureSets"] = {}
    textureSetInfo = turok1Defs["textureSets"]
    kex_lexer.lexer.input(textureSetFileText)
    expectToken = 0 #TextureSet key
    textureSetKey = ""
    textureIndex = 0
    for tok in kex_lexer.lexer:
        tokValue = tok.value
        #print('LexToken(%s,%r,%d,%d)' % (tok.type, tok.value, tok.lineno, tok.lexpos))
        
        if expectToken == 0: #TextureSet key
            if tok.type != kex_lexer.TOKEN_STRING:
                print("TextureSet Key type %s was not a %s" % (tok.type, kex_lexer.TOKEN_STRING))
                return None
            textureSetKey = tokValue
            textureSetInfo[tokValue] = TextureSet(tokValue)
            expectToken = 1 #Open TextureSet Brace
        elif expectToken == 1: #Open TextureSet Brace
            if tok.type != kex_lexer.TOKEN_OPEN_BRACE:
                print("TextureSet opening brace bracket type %s was not a %s" % (tok.type, kex_lexer.TOKEN_OPEN_BRACE))
                return None
            expectToken = 2 #TextureSet props or close brace
        elif expectToken == 2: #TextureSet props or close brace
            if tok.type == kex_lexer.TOKEN_CLOSE_BRACE: #end of current TextureSet block
                expectToken = 0 #TextureSet key
            elif tok.type == kex_lexer.TOKEN_NAME:
                if tokValue == "animated":
                    expectToken = 3 #prop animated
                elif tokValue == "speed":
                    expectToken = 4 #prop speed
                elif tokValue.startswith("texture_"):
                    textureIndex = max(int(tokValue[8:]), 0)
                    expectToken = 5 #texture path
                #end if
            #end if
        elif expectToken == 3: #prop animated
            if tok.type != kex_lexer.TOKEN_INT:
                print("animated type %s was not a %s" % (tok.type, kex_lexer.TOKEN_INT))
                return None
            textureSetInfo[textureSetKey].animated = tokValue
            expectToken = 2 #TextureSet props or close brace
            
        elif expectToken == 4: #prop speed
            if tok.type != kex_lexer.TOKEN_INT:
                print("speed type %s was not a %s" % (tok.type, kex_lexer.TOKEN_INT))
                return None
            textureSetInfo[textureSetKey].speed = tokValue
            expectToken = 2 #TextureSet props or close brace
            
        elif expectToken == 5: #texture path
            if tok.type != kex_lexer.TOKEN_STRING:
                print("texture path type %s was not a %s" % (tok, kex_lexer.TOKEN_STRING))
                return None
            expectToken = 2 #TextureSet props or close brace
            textures = textureSetInfo[textureSetKey].textures
            while len(textures) <= textureIndex:
                textures.append("")
            #end while
            textures[textureIndex] = tokValue
        #end if
    #end for

#end func

# -----------------------------------------------------------------------------
# Return dictionary, errors: array of string, warnings, info
def T1ValidateModel(context, checkObj=None, animNLATracks=False):
    global turok1FPS
    
    result = { "errors": [], "warnings": [], "info": [], "actions": []}
    obj = None
    if checkObj is None:
        obj = context.object
    else:
        obj = checkObj
    
    if obj is None:
        result["errors"].append("Object is None")
        return result
    #end if
    
    if obj.type != "ARMATURE" and obj.type != "MESH":
        result["errors"].append("Object is not an Armature or Mesh")
        return result
    #end if
        
    isRootTurokObject = get_root_turok_object(obj, "MODEL") == obj
    if not isRootTurokObject:
        result["errors"].append("Object is not the Root Turok Object")
        return result
    #end if

    #Error: Armature must have at least 1 bone
    if obj.type == "ARMATURE":
        if len(obj.data.bones) == 0:
            result["errors"].append("Object \"%s\" has no bones" % (obj.name))
        #end if
    #end if

    #Warning: Check boneIndex is being used in another bone
    if obj.type == "ARMATURE":
        boneIndexes = []
        boneIndexObjs = []
        for bone in obj.data.bones:
            index = index_of(boneIndexes, bone.t1.boneIndex)
            if index < 0:
                boneIndexes.append(bone.t1.boneIndex)
                boneIndexObjs.append(bone)
            else:
                result["warnings"].append("Bone \"%s\" is using the same bone index of %i as Bone \"%s\"" % (bone.name, bone.t1.boneIndex, boneIndexObjs[index].name))
            #end if
        #end for
    #end if

    #Warning: Make sure Model Type is set to all child objects (if this obj is an armature)
    if obj.type == "ARMATURE":
        for child in obj.children:
            if child.type == 'MESH' and child.t1.objType != "MODEL":
                result["warnings"].append("Object \"%s\" is not set to the Turok Model type." % (child.name))
            #end if
        #end for
    #end if
    
    #Error: Check if bounding box min is less than bound box max (except if all zeros)
    bbMin = obj.t1.boundingBoxMin
    bbMax = obj.t1.boundingBoxMax
    if bbMin != Vector((0.0, 0.0, 0.0)) or bbMax != Vector((0.0, 0.0, 0.0)):
        if bbMin[0] >= bbMax[0] or bbMin[1] >= bbMax[1] or bbMin[2] >= bbMax[2]:
            result["errors"].append("Object \"%s\" BoundingBoxMin%s is greater than or equal to boundingBoxMax%s" % (obj.name, bbMin, bbMax))
        #end if
    #end if

    #Error: Make sure each mesh has only 3 vertices per poly.
    meshObjs = []
    if obj.type == "ARMATURE":
        for child in obj.children:
            if child.type == 'MESH':
                meshObjs.append(child)
            #end if
        #end for
    else: #MESH
        meshObjs.append(obj)
    #end if
    for meshObj in meshObjs:
        #check if material names are valid
        for mat in meshObj.data.materials:
            if mat is None:
                continue
            matName = get_real_material_name(mat.name)[0]
            invalid_chars = string_contains_any(matName, "\\/\"{}#$ ")
            if invalid_chars:
                result["errors"].append("Object \"%s\", Material \"%s\", contains invalid characters. Rename the material. (Remove all spaces) The following invalid characters were found: %s" % (meshObj.name, mat.name, invalid_chars))
            #end if
        #end for
        for poly in meshObj.data.polygons:
            if poly.loop_total != 3:
                result["errors"].append("Object \"%s\" has %d verts per poly. Each poly must only have 3 verts. Triangulate the mesh." % (meshObj.name, poly.loop_total))
                break
            #end if
        #end for
    #end for

    #Warning: Check mesh variantIndex (mesh index) if it's the same as other ones
    if obj.type == "ARMATURE":
        for bone in obj.data.bones:
            variantIndexes = []
            variantIndexObjs = []
            for child in obj.children:
                if child.type == 'MESH' and child.parent_bone == bone.name:
                    index = index_of(variantIndexes, child.t1.variantIndex)
                    if index < 0:
                        variantIndexes.append(child.t1.variantIndex)
                        variantIndexObjs.append(child)
                    else:
                        result["warnings"].append("Object \"%s\", is using the same mesh index of %i as Object \"%s\" for Bone \"%s\"" % (child.name, child.t1.variantIndex, variantIndexObjs[index].name, bone.name))
                    #end if
                #end if
            #end for
        #end for
    #end if

    #Check Actions
    if obj.type == "ARMATURE":
        scene = context.scene
        if scene.render.fps < turok1FPS:
            result["errors"].append("Cannot export animations with scene render FPS of less than %d" % (turok1FPS))
            return result
        #end if
            
        if scene.render.fps % turok1FPS != 0:
            result["errors"].append("Cannot export animations with scene render FPS that is not a multiple of %d" % (turok1FPS))
            return result
        #end if
            
        framesPerGameFrame = int(scene.render.fps / turok1FPS)

        # Find all the animation actions this object could be using
        # Must be using fake user, must be using frames 0 and 1, and must have all properties that all the fcurves use
        for action in bpy.data.actions:
            if (not action.use_fake_user):
                continue
            #end if
            if len(action.fcurves) == 0:
                result["warnings"].append("Animation Action \"%s\", has no fcurves" % (action.name))
                continue
            #end if
                
            frameStart, frameEnd = action.frame_range
            if frameEnd < frameStart + framesPerGameFrame:
                continue
            isUsed = True
            for fcurve in action.fcurves:
                prop_path = fcurve.data_path.rpartition('.')[0]
                try:
                    obj.path_resolve(prop_path)
                except ValueError:
                    result["warnings"].append("Animation Action \"%s\", fcurve \"%s\" is not valid. This action will not be exported." % (action.name, fcurve.data_path))
                    isUsed = False
                    break
                #end try except
            #end for
            if isUsed:
                result["actions"].append(action)
            #end if
        #end for
        
        #check if action must be in nla actions
        if animNLATracks:
            nlaActions = get_nla_actions(obj)
            result["actions"] = [action for action in result["actions"] if action in nlaActions]
        #end if
    
        #Error: Must have at least 1 action to export
        if len(result["actions"]) == 0:
            result["errors"].append("No Animation Actions to export for this Armature.")
        #end if
        
        for action in result["actions"]:
            frameStart, frameEnd = action.frame_range
            frameStart = int(frameStart)
            frameEnd = int(frameEnd)
            frameCount = int((frameEnd - frameStart) / framesPerGameFrame) + 1
            #Error: Check if Loop Frame is greater than end of frame (taking into account the current render fps)
            if action.t1.loopFrame >= frameCount:
                result["errors"].append("Animation Action \"%s\" loopFrame %i is greater then last Turok frame of %i" % (action.name, action.t1.loopFrame, frameCount))
            #end if
            
            #Error: check each key frame action's frame if it went beyond the last frame (taking into account the current render fps)
            for keyFrame in action.t1.keyFrames:
                if keyFrame.frame >= frameCount:
                    result["errors"].append("Animation Action \"%s\", a keyframe action has a frame of %i which is greater then last Turok frame of %i" % (action.name, keyFrame.frame, frameCount))
                #end if
            #end for
        #end for
    #end if
    
    return result
#end func
# -----------------------------------------------------------------------------
# 
class AudioSound:
    def __init__(self, newKsnd, index):
        self.ksnd = newKsnd
        self.soundIndex = index
        self.handle = None
        self.lastPos = 0.0
        self.soundObj = None
        self.ticks = 0
        self.isDelaying = True
        self.doRemove = False
        self.LoopEndReached = False
        self.delayTime = 0.0
        self.length = 1.0 #length of audio clip in seconds
        self.volShiftStartTime = self.ksnd.veEnvStartTime / 60.0
        self.volShiftEndTime = self.volShiftStartTime + max(self.ksnd.veDuration / 60.0, 0.0)
        if self.ksnd.veEnable:
            self.volumeTarget = clamp(self.ksnd.veStart * self.ksnd.veMax, 0.0, 1.0)
        else:
            self.volumeTarget = clamp(self.ksnd.veMax, 0.0, 1.0)
        #end if
        self.pitchShiftStartTime = self.ksnd.peEnvStartTime / 60.0
        self.pitchShiftEndTime = self.pitchShiftStartTime + max(self.ksnd.peDuration / 60.0, 0.0)
        if self.ksnd.peEnable:
            if self.ksnd.veEnvStartTime == 0 and self.ksnd.peDuration <= 0:
                self.pitchTarget = max(self.ksnd.peEnd, 0.01)
            else:
                self.pitchTarget = max(self.ksnd.peStart, 0.01)
        #end if
        else:
            self.pitchTarget = max(self.ksnd.peMax, 0.01)
        #end if
        
        wavPath = find_t1_file_path(self.ksnd.wavefile)
        if wavPath is not None:
            self.soundObj = aud.Sound.file(wavPath).volume(0.8) #setting volume to 0.8 to match turok game global volume
            self.length = self.soundObj.length / self.soundObj.specs[0] #soundLength / The sampling rate in Hz
        else:
            self.doRemove = True
        #end if
    #end func
    
    # -----------------------------------------------------------------------------
    # 
    def update(self):
        if self.ticks == 0:
            self.delayTime = time_from() + max(self.ksnd.delay / 60.0, 0.0)
        #end if
        if self.handle is not None and not self.isDelaying:
            if self.handle.status:
                soundPos = self.handle.position
                newVolume = self.volumeTarget
                newPitch = self.pitchTarget
                if not self.LoopEndReached:
                    if soundPos > self.lastPos:
                        self.lastPos = soundPos
                        if self.ksnd.veEnable:
                            lerpTime = time_normalize(soundPos, self.volShiftStartTime, self.volShiftEndTime)
                            newVolume = clamp(lerp(self.ksnd.veStart, self.ksnd.veEnd, lerpTime) * self.ksnd.veMax, 0.0, 1.0)
                        else:
                            newVolume = clamp(self.ksnd.veMax, 0.0, 1.0)
                        #end if
                        
                        if self.ksnd.peEnable:
                            lerpTime = time_normalize(soundPos, self.pitchShiftStartTime, self.pitchShiftEndTime)
                            newPitch = max(lerp(self.ksnd.peStart, self.ksnd.peEnd, lerpTime), 0.01)
                        else:
                            newPitch = max(self.ksnd.peMax, 0.01)
                        #end if
                    elif self.ksnd.loop and soundPos < 0.0: #loop sound reached end
                        self.LoopEndReached = True
                        if self.ksnd.veEnable:
                            newVolume = clamp(self.ksnd.veEnd * self.ksnd.veMax, 0.0, 1.0)
                        #end if
                        if self.ksnd.peEnable:
                            newPitch = max(self.ksnd.peEnd, 0.01)
                        #end if
                        #if ends with no volume while looping then stop playing
                        if newVolume == 0.0:
                            self.handle.stop()
                            self.doRemove = True
                            self.handle = None
                        #end if
                    #end if
                #end if
                
                if self.handle is not None:
                    if newVolume != self.volumeTarget:
                        self.volumeTarget = newVolume
                        try:
                            self.handle.volume = self.volumeTarget
                        except:
                            pass
                        #end try except
                    #end if

                    if newPitch != self.pitchTarget:
                        self.pitchTarget = newPitch
                        try:
                            self.handle.pitch = self.pitchTarget
                        except:
                            pass
                        #end try except
                    #end if
                #end if
            #end if

            #Check if sound is finished playing
            if not self.doRemove and not self.handle.status:
                self.doRemove = True
                self.handle = None
            #end if
        #end if
        
        self.ticks += 1
    #end func
    
    # -----------------------------------------------------------------------------
    # 
    def play_sound(self):
        self.handle = aud.Device().play(self.soundObj)
        try:
            self.handle.volume = self.volumeTarget
            self.handle.pitch = self.pitchTarget
        except:
            pass
        #end try except
        if self.ksnd.loop:
            self.handle.loop_count = -1
        #end if
    #end func
    
    # -----------------------------------------------------------------------------
    # 
    def delay_finished(self):
        self.isDelaying = False
        self.play_sound()
    #end func
        
    # -----------------------------------------------------------------------------
    # 
    def __str__(self):
        return "delayTime:%f, pitchTarget:%f, volumeTarget:%f, length:%f" % (self.delayTime,
        self.pitchTarget, self.volumeTarget, self.length)
    #end func

    # -----------------------------------------------------------------------------
    # 
    def __repr__(self):
        return str(self)
    #end func
#end class
