#vngame;all;Utilities/VN Physics v1.0.1
# # this module create physics for studio
# by Countd360

from UnityEngine import Quaternion, Vector3, Vector2, Color, Rect, GameObject, Transform, Texture2D
from UnityEngine import GUI, GUILayout, GUIStyle, GUIContent, GUIUtility, TextAnchor, Mathf
from UnityEngine import Time, Screen, Input, KeyCode, Event, EventType, Camera, Physics, RaycastHit, PhysicMaterialCombine
from vngameengine import HSNeoOCI, HSNeoOCIItem, HSNeoOCIChar, HSNeoOCIFolder, HSNeoOCILight, HSNeoOCIRoute, HSNeoOCICamera, HSNeoOCIText, GData
from vngameengine import importOrReload, parseKeyCode, BytesPNG2Texture2D, Texture2D2BytesPNG
from libkfguictrl import DragInputUnit
import copy
import clr
import traceback
import codecs
from os import path
import hashlib

def vnphy_toolbar_handle(param1 = "", param2 = ""):
    # function call when toolbar button clicked
    # this should be the main entry of VNPhysics
    #print("VNPhy: toolbar button clicked: p1=%s, p2=%s"%(str(param1), str(param2)))
    from vngameengine import vnge_game as game
    if not hasattr(game.gdata, "vnphy") or game.gdata.vnphy == None or game.get_extra_window(game.gdata.vnphy.wndId) == None:
        start(game)
    else:
        close(game)

def start(game):
    # normal start entry from "ALL GAME > Utilities"

    # app init
    initialize(game)

    # UI
    wnd = game.get_extra_window(game.gdata.vnphy.wndId)
    if wnd != None:
        # already existed
        return

    # init gui data
    gd = GData()
    gd.guiFunction = None
    gd.wndNameNormal = "VN Physics v1.0.1"
    gd.wndSizeNormal = Rect(100, 100, 300, 500)
    gd.mainTab = "Collider"
    gd.currentCollider = None
    gd.currentRigidBody = None
    gd.currentTrigger = None
    gd.currentAction = None
    gd.posRateX, gd.posRateY = 0.01, 0.001
    gd.sclRateX, gd.sclRateY = 0.01, 0.001
    gd.setting = game.gdata.vnphySetting

    # skin setup func
    def skin_setup(g):
        g.windowName = gd.wndNameNormal
        g.windowRect = gd.wndSizeNormal
        g.windowStyle = g.windowStyleDefault
    
    # setup skin
    from skin_customwindow import SkinCustomWindow
    skin = SkinCustomWindow()
    skin.funcSetup = skin_setup
    skin.funcWindowGUI = vnphy_GUI_subwindow
    skin.metadata = game.gdata.vnphy
    skin.guidata = gd

    # open sub-window
    game.gdata.vnphy.wndId = game.new_extra_window_skin(skin)

    # toolbar sync
    from vngameengine import tlbBtn_UpdateBtnColor, tlbBtn_VNGEPhysics
    tlbBtn_UpdateBtnColor(tlbBtn_VNGEPhysics, True)

def close(game):
    if not hasattr(game.gdata, "vnphy"):
        return

    wnd = game.get_extra_window(game.gdata.vnphy.wndId)
    if wnd != None:
        # already existed
        game.close_extra_window(game.gdata.vnphy.wndId)
        game.gdata.vnphy.wndId = None
        # toolbar sync
        from vngameengine import tlbBtn_UpdateBtnColor, tlbBtn_VNGEPhysics
        tlbBtn_UpdateBtnColor(tlbBtn_VNGEPhysics, False, game.gdata.vnphy.hasContents)

def initialize(game, force = False):
    if force or not hasattr(game.gdata, "vnphy") or game.gdata.vnphy == None:
        # init main class
        print("\n\nStart a new VN Physics application...")
        importOrReload("vnphysics_core")
        import vnphysics_core
        game.gdata.vnphy = vnphysics_core.PhysicsManager(game)
        game.gdata.vnphySetting = loadSetting(game, force)

        # install event hook
        game.event_unreg_listener("scene_saved", vnphy_hook_saved, "vnphy_scenesave")
        game.event_reg_listener("scene_saved", vnphy_hook_saved, "vnphy_scenesave")
        game.event_unreg_listener("scene_loaded_after", vnphy_hook_loaded_after, "vnphy_sceneload_after")
        game.event_reg_listener("scene_loaded_after", vnphy_hook_loaded_after, "vnphy_sceneload_after")
        game.event_unreg_listener("scene_imported_after2", vnphy_hook_import_after2, "vnphy_sceneimport_after2")
        game.event_reg_listener("scene_imported_after2", vnphy_hook_import_after2, "vnphy_sceneimport_after2")
        game.event_unreg_listener("update", vnphy_update, "vnphy_update")
        game.event_reg_listener("update", vnphy_update, "vnphy_update")

    return game.gdata.vnphy

def loadSetting(game, force = False):
    from vnphysics_core import VNAction

    if not hasattr(game.gdata, "vnphySetting") or force:
        # initialize default value
        setting = GData()
        setting.DEBUG_SHOW_LAST_HITINFO = False
        setting.DEBUG_SHOW_COLLIDER_BOUNDS = False
        setting.DEBUG_SHOW_RIGIDBODY_INFO = False
        setting.DEBUG_SHOW_RESET_BUTTON = False
        setting.DEBUG_PRINT_RUN_LOG = False
        setting.DEBUG_PRINT_CALLER_INFO = False
        setting.DEBUG_PRINT_INPUT_PARAM = False
        setting.SECURITY_TRUST_MY_SAVE = True

        # load
        setting.pythonWhitelist = loadWhitelist()
    else:
        setting = game.gdata.vnphySetting

    # set
    VNAction.DEBUG_PRINT_RUN_LOG = setting.DEBUG_PRINT_RUN_LOG
    VNAction.DEBUG_PRINT_CALLER_INFO = setting.DEBUG_PRINT_CALLER_INFO
    VNAction.DEBUG_PRINT_INPUT_PARAM = setting.DEBUG_PRINT_INPUT_PARAM

    return setting

def loadWhitelist():
    whilelist = {}
    try:
        basePath = path.split(path.abspath(__file__))[0]
        basePath = path.join(basePath, "vnphydata")
        whilelistFile = path.join(basePath, "vnphy_python_whitelist.json")        
        if path.exists(whilelistFile):
            file = codecs.open(whilelistFile, "r", "utf-8")
            wlstr = file.read()
            file.close()

            from vnframe import loadFromJsonString
            whilelist = loadFromJsonString(wlstr)
    except:
        traceback.print_exc()
    return whilelist

def saveWhitelist(whilelist):
    try:
        basePath = path.split(path.abspath(__file__))[0]
        basePath = path.join(basePath, "vnphydata")
        if not path.isdir(basePath):
            import os
            os.mkdir(basePath)
        whilelistFile = path.join(basePath, "vnphy_python_whitelist.json")

        from vnframe import dumpToJsonString
        wlstr = dumpToJsonString(whilelist, True)
        file = codecs.open(whilelistFile, "w", "utf-8")
        file.write(wlstr)
        file.close()
        return True
    except:
        traceback.print_exc()
        return False

def vnphy_self_reload(game, param):
    
    _, step = param
    try:
        if step == 1:
            # close old window
            from vnphysics_core import VNAction
            close(game)
            game.gdata.vnphy.saveData()
            game.gdata.temp_allowpython = VNAction.SCENE_PYTHON_ALLOWED
            game.event_unreg_listener("update", vnphy_update, "vnphy_update")
        if step == 2:
            # clear gdata
            game.gdata.vnphy.cleanup()
            del game.gdata.vnphy
        if step == 3:
            # reload module
            importOrReload("vnphysics")
            importOrReload("vnphysics_core")
        if step == 4:
            # restart
            start(game)
            game.gdata.vnphy.loadData()
        if step == 5:
            #print("restore security setting: " + str(game.gdata.temp_allowpython))
            from vnphysics_core import VNAction
            VNAction.SCENE_PYTHON_ALLOWED = game.gdata.temp_allowpython
            del game.gdata.temp_allowpython
            return True
    except:
        print("vnphy self reload failed at step %d"%step)
        traceback.print_exc()
        return True
    param[1] += 1
    return False

# event handler
def vnphy_hook_saved(game, evid, u_param):
    # save data
    vnphy = initialize(game)
    vnphy.saveData()
    # scene event
    vnphy.checkSceneEventTrigger("Save", u_param)
    # security trust
    vnphy_security_save(vnphy, u_param)

def vnphy_hook_loaded_after(game, evid, u_param):
    # load data
    vnphy = initialize(game)

    # let autorun kennel handle data loading
    #vnphy.cleanup()
    #vnphy.loadData()

    # toolbar sync
    from vngameengine import tlbBtn_UpdateBtnColor, tlbBtn_VNGEPhysics
    wndVisible = game.get_extra_window(vnphy.wndId) != None
    tlbBtn_UpdateBtnColor(tlbBtn_VNGEPhysics, wndVisible, vnphy.hasContents)

    # security check
    vnphy_security_check(vnphy, u_param)
        
    # scene event
    vnphy.checkSceneEventTrigger("Load", u_param)

def vnphy_hook_import_after2(game, evid, u_param):
    # import data
    vnphy = initialize(game)

    # let autorun kennel handle data loading
    #ret = vnphy.loadData(None, True, game.gdata.import_info_old_to_new_key)
    #if ret:
    #    showMsg(game, "vnPHY data imported as #%d, please check id changing!"%vnphy.appendCount)

    # toolbar sync
    from vngameengine import tlbBtn_UpdateBtnColor, tlbBtn_VNGEPhysics
    wndVisible = game.get_extra_window(vnphy.wndId) != None
    tlbBtn_UpdateBtnColor(tlbBtn_VNGEPhysics, wndVisible, vnphy.hasContents)

    # security check
    vnphy_security_check(vnphy, u_param)
        
    # scene event
    vnphy.checkSceneEventTrigger("Import", u_param)

def vnphy_update(game, evid, u_param):
    try:
        vnphy = game.gdata.vnphy

        # house keeping
        vnphy.houseKeeping(Time.deltaTime)

        # scene event
        vnphy.checkSceneEventTrigger("Update", None)

        # sync mode
        if vnphy.linkedPyFilepath:
            vnphy_update_from_linkfile(vnphy, False)

        # key detect mode
        if vnphy.keycodeDetectMode:
            baseCtrl = game.get_extra_window(vnphy.wndId)
            baseCtrl.skin.eatMouseInput = False
            # search input keycode
            for kc in KeyCode.GetValues(KeyCode):
                if Input.GetKeyDown(kc):
                    vnphy.lastDetectedKeycode = str(kc)
                    vnphy.keycodeDetectMode = False
                    baseCtrl.skin.eatMouseInput = True
            # anyway, no more input process in this mode
            return

        # key and mouse input check
        if Input.anyKeyDown:
            # click trigger
            mLeft = Input.GetMouseButtonDown(0)
            mRight = Input.GetMouseButtonDown(1)
            mMid = Input.GetMouseButtonDown(2)
            if mLeft or mRight or mMid:
                mBtn = 0 if mLeft else (1 if mRight else 2)
                mPos = Input.mousePosition
                ray = Camera.main.ScreenPointToRay(mPos)
                hitInfo = clr.Reference[RaycastHit](RaycastHit())
                registeredCldHit = False
                if Physics.Raycast(ray, hitInfo, Mathf.Infinity, 65535):
                    vnphy.lastMouseHitInfo = hitInfo.Value
                    vnphy.lastMouseHitted = True
                    # flash object
                    hitCollider = hitInfo.Value.collider
                    if hitCollider in vnphy.colliderObjDict:
                        registeredCldHit = True
                        vnphy.flashObject(vnphy.colliderObjDict[hitCollider])
                else:
                    vnphy.lastMouseHitted = False
                # check click triggers
                vnphy.checkClickTrigger(mBtn, mPos, vnphy.lastMouseHitted and registeredCldHit, vnphy.lastMouseHitInfo)
            
        # input trigger
        vnphy.checkInputTrigger()

        # drag on guide object detect
        if Input.GetMouseButtonDown(0):
            hitInfo = clr.Reference[RaycastHit](RaycastHit())
            if Physics.Raycast(ray, hitInfo, Mathf.Infinity, 268435456):
                #guideTargetTrans = hitInfo.Value.transform
                #while guideTargetTrans.name != "M Root(Clone)" and guideTargetTrans.parent != None:
                #    guideTargetTrans = guideTargetTrans.parent
                #goc = guideTargetTrans.gameObject.GetComponent[GuideObject]()
                #vnphy.goc = goc
                #print("Drag started: %s")
                vnphy.draggingGuideObj = True
            #else:
            #    if Physics.Raycast(ray, hitInfo, Mathf.Infinity, -1):
            #        print("hit " + getPathOfGameObject(hitInfo.Value.transform.gameObject))
        if Input.GetMouseButtonUp(0):
            #if vnphy.draggingGuideObj:
            #    print("Drag released")
            vnphy.draggingGuideObj = False
    except:
        traceback.print_exc()

# vnphy window
def vnphy_GUI_subwindow(baseController, windowid):
    oldColor = GUI.color

    try:
        guidata = baseController.skin.guidata
        vnphy = baseController.skin.metadata
        game = vnphy.game
        if guidata.guiFunction != None:
            guidata.guiFunction(guidata, vnphy)
        else:
            vnphy_main_GUI(guidata, vnphy)
    except:
        traceback.print_exc()
        close(game)
    
    # reset button
    if guidata.setting.DEBUG_SHOW_RESET_BUTTON:
        cbRect = Rect(baseController.windowRect.width - 30, 3, 13, 13)
        GUI.color = Color.yellow
        if GUI.Button(cbRect, ""):
            game.append_update_job(vnphy_self_reload, [vnphy, 0])
        GUI.color = oldColor

    # close button
    cbRect = Rect(baseController.windowRect.width - 16, 3, 13, 13)
    GUI.color = Color.red
    if GUI.Button(cbRect, ""):
        close(game)
    GUI.color = oldColor

def vnphy_main_GUI(guiData, vnphy):
    oldColor = GUI.color

    # switch
    if guiData.guiFunction != vnphy_main_GUI:
        retnow = guiData.guiFunction != None
        guiData.guiFunction = vnphy_main_GUI
        # do switch init here
        guiData.colliderMode = "list"
        guiData.colliderListScroll = Vector2.zero
        guiData.colliderBoneScroll = Vector2.zero

        guiData.rigidBodyMode = "list"
        guiData.rigidBodyListScroll = Vector2.zero
        guiData.rigidBodyBoneScroll = Vector2.zero

        guiData.triggerMode = "list"
        guiData.triggerListScroll = Vector2.zero
        guiData.triggerEditScroll = Vector2.zero

        guiData.actionMode = "list"
        guiData.actionListScroll = Vector2.zero
        guiData.actionEditScroll = Vector2.zero
        # return
        if retnow:
            return

    # tab header
    GUILayout.BeginHorizontal()
    for p in ("Collider", "Rigidbody", "Trigger", "Action"):
        pw = p.strip()
        GUI.color = Color.green if pw == guiData.mainTab else oldColor
        if GUILayout.Button(p):
            guiData.mainTab = pw
    GUI.color = oldColor
    if False and GUILayout.Button("?", GUILayout.ExpandWidth(False)):
        print("show some help")
    GUILayout.EndHorizontal()

    # tab content
    if guiData.mainTab == "Collider":
        vnphy_main_collider_tab(guiData, vnphy)
    elif guiData.mainTab == "Rigidbody":
        vnphy_main_rigidbody_tab(guiData, vnphy)
    elif guiData.mainTab == "Trigger":
        vnphy_main_trigger_tab(guiData, vnphy)
    elif guiData.mainTab == "Action":
        vnphy_main_action_tab(guiData, vnphy)
    else:
        guiData.mainTab = "Collider"

# collider tab
def vnphy_main_collider_tab(gd, vnphy):
    if gd.colliderMode == "list":
        vnphy_collider_tab_list(gd, vnphy)
    elif gd.colliderMode == "create":
        vnphy_collider_tab_add(gd, vnphy)
    elif gd.colliderMode == "delete":
        vnphy_collider_tab_list(gd, vnphy)
    elif gd.colliderMode == "material":
        vnphy_collider_tab_list(gd, vnphy)
    else:
        gd.colliderMode = "list"

def vnphy_collider_tab_list(gd, vnphy):
    oldColor = GUI.color
    game = vnphy.game
    tWidth = 40
    adjFullWidth = 180
    adjTitleWidth = 90
    adjResetWidth = 40
    mtlTitleWidth = 120
    mtlContentWidth = 90

    def getNewId():
        i = 1
        while "vcd%d"%i in vnphy.colliderIdDict:
            i += 1
        return "vcd%d"%i
    def initAdjustUI(phyCld):
        # center, for all
        gd.adjCenterX = DragInputUnit(Rect(4, 25*1+4, adjFullWidth, 21), "Center X", False, adjTitleWidth)
        gd.adjCenterY = DragInputUnit(Rect(4, 25*2+4, adjFullWidth, 21), "Center Y", False, adjTitleWidth)
        gd.adjCenterZ = DragInputUnit(Rect(4, 25*3+4, adjFullWidth, 21), "Center Z", False, adjTitleWidth)
        gd.adjCenterX.setValueFormat(vFormat="%.3f")
        gd.adjCenterY.setValueFormat(vFormat="%.3f")
        gd.adjCenterZ.setValueFormat(vFormat="%.3f")
        gd.adjCenterX.setDragRate(gd.posRateX, gd.posRateY)
        gd.adjCenterY.setDragRate(gd.posRateX, gd.posRateY)
        gd.adjCenterZ.setDragRate(gd.posRateX, gd.posRateY)
        # size, for box
        gd.adjSizeX = DragInputUnit(Rect(4, 25*4+4, adjFullWidth, 21), "Size X", False, adjTitleWidth)
        gd.adjSizeY = DragInputUnit(Rect(4, 25*5+4, adjFullWidth, 21), "Size Y", False, adjTitleWidth)
        gd.adjSizeZ = DragInputUnit(Rect(4, 25*6+4, adjFullWidth, 21), "Size Z", False, adjTitleWidth)
        gd.adjSizeX.setValueFormat(vFormat="%.3f", minValue = 0.001)
        gd.adjSizeY.setValueFormat(vFormat="%.3f", minValue = 0.001)
        gd.adjSizeZ.setValueFormat(vFormat="%.3f", minValue = 0.001)
        gd.adjSizeX.setDragRate(gd.sclRateX, gd.sclRateY)
        gd.adjSizeY.setDragRate(gd.sclRateX, gd.sclRateY)
        gd.adjSizeZ.setDragRate(gd.sclRateX, gd.sclRateY)
        # radius, for sphere and capsule
        gd.adjRadius = DragInputUnit(Rect(4, 25*4+4, adjFullWidth, 21), "Radius", False, adjTitleWidth)
        gd.adjRadius.setValueFormat(vFormat="%.3f", minValue = 0.001)
        gd.adjRadius.setDragRate(gd.posRateX, gd.posRateY)
        # height, for capsule
        gd.adjHeight = DragInputUnit(Rect(4, 25*5+4, adjFullWidth, 21), "Height", False, adjTitleWidth)
        gd.adjHeight.setValueFormat(vFormat="%.3f", minValue = 0.001)
        gd.adjHeight.setDragRate(gd.posRateX, gd.posRateY)
        # update
        updateAdjustUI(phyCld)
    def updateAdjustUI(phyCld):
        gd.adjCenterX.setValue(phyCld.collider.center.x)
        gd.adjCenterY.setValue(phyCld.collider.center.y)
        gd.adjCenterZ.setValue(phyCld.collider.center.z)
        if phyCld.type == "Box":
            gd.adjSizeX.setValue(phyCld.collider.size.x)
            gd.adjSizeY.setValue(phyCld.collider.size.y)
            gd.adjSizeZ.setValue(phyCld.collider.size.z)
        if phyCld.type in {"Sphere", "Capsule"}:
            gd.adjRadius.setValue(phyCld.collider.radius)
        if phyCld.type == "Capsule":
            gd.adjHeight.setValue(phyCld.collider.height)
    def adjustColliderCenter(phyCld):
        cld = phyCld.collider
        # center x
        GUILayout.BeginHorizontal()
        GUILayout.Space(adjFullWidth + 8)
        if gd.adjCenterX.paint():
            cld.center = Vector3(gd.adjCenterX.value, gd.adjCenterY.value, gd.adjCenterZ.value)
        if GUILayout.Button("0", GUILayout.Width(adjResetWidth)):
            cld.center = Vector3(0, cld.center.y, cld.center.z)
            updateAdjustUI(phyCld)
        GUILayout.EndHorizontal()
        # center y
        GUILayout.BeginHorizontal()
        GUILayout.Space(adjFullWidth + 8)
        if gd.adjCenterY.paint():
            cld.center = Vector3(gd.adjCenterX.value, gd.adjCenterY.value, gd.adjCenterZ.value)
        if GUILayout.Button("0", GUILayout.Width(adjResetWidth)):
            cld.center = Vector3(cld.center.x, 0, cld.center.z)
            updateAdjustUI(phyCld)
        GUILayout.EndHorizontal()
        # center z
        GUILayout.BeginHorizontal()
        GUILayout.Space(adjFullWidth + 8)
        if gd.adjCenterZ.paint():
            cld.center = Vector3(gd.adjCenterX.value, gd.adjCenterY.value, gd.adjCenterZ.value)
        if GUILayout.Button("0", GUILayout.Width(adjResetWidth)):
            cld.center = Vector3(cld.center.x, cld.center.y, 0)
            updateAdjustUI(phyCld)
        GUILayout.EndHorizontal()
    def adjustColliderSize(phyCld):
        cld = phyCld.collider
        # size x
        GUILayout.BeginHorizontal()
        GUILayout.Space(adjFullWidth + 8)
        if gd.adjSizeX.paint():
            cld.size = Vector3(gd.adjSizeX.value, gd.adjSizeY.value, gd.adjSizeZ.value)
        if GUILayout.Button(str(phyCld.colliderScale), GUILayout.Width(adjResetWidth)):
            cld.size = Vector3(phyCld.colliderScale, cld.size.y, cld.size.z)
            updateAdjustUI(phyCld)
        GUILayout.EndHorizontal()
        # size y
        GUILayout.BeginHorizontal()
        GUILayout.Space(adjFullWidth + 8)
        if gd.adjSizeY.paint():
            cld.size = Vector3(gd.adjSizeX.value, gd.adjSizeY.value, gd.adjSizeZ.value)
        if GUILayout.Button(str(phyCld.colliderScale), GUILayout.Width(adjResetWidth)):
            cld.size = Vector3(cld.size.x, phyCld.colliderScale, cld.size.z)
            updateAdjustUI(phyCld)
        GUILayout.EndHorizontal()
        # size z
        GUILayout.BeginHorizontal()
        GUILayout.Space(adjFullWidth + 8)
        if gd.adjSizeZ.paint():
            cld.size = Vector3(gd.adjSizeX.value, gd.adjSizeY.value, gd.adjSizeZ.value)
        if GUILayout.Button(str(phyCld.colliderScale), GUILayout.Width(adjResetWidth)):
            cld.size = Vector3(cld.size.x, cld.size.y, phyCld.colliderScale)
            updateAdjustUI(phyCld)
        GUILayout.EndHorizontal()
    def adjustColliderRadius(phyCld):
        cld = phyCld.collider
        # radius
        GUILayout.BeginHorizontal()
        GUILayout.Space(adjFullWidth + 8)
        if gd.adjRadius.paint():
            cld.radius = gd.adjRadius.value
        if GUILayout.Button(str(0.5 * phyCld.colliderScale)[:4], GUILayout.Width(adjResetWidth)):
            cld.radius = 0.5 * phyCld.colliderScale
            updateAdjustUI(phyCld)
        GUILayout.EndHorizontal()
    def adjustColliderHeight(phyCld):
        cld = phyCld.collider
        # radius
        GUILayout.BeginHorizontal()
        GUILayout.Space(adjFullWidth + 8)
        if gd.adjHeight.paint():
            cld.height = gd.adjHeight.value
        if GUILayout.Button(str(2 * phyCld.colliderScale)[:3], GUILayout.Width(adjResetWidth)):
            cld.height = 2 * phyCld.colliderScale
            updateAdjustUI(phyCld)
        GUILayout.EndHorizontal()
    def adjustColliderTrigger(phyCld):
        GUILayout.BeginHorizontal()
        GUILayout.Label("Is Trigger:", GUILayout.Width(adjTitleWidth))
        phyCld.collider.isTrigger = GUILayout.Toggle(phyCld.collider.isTrigger, "")
        GUILayout.FlexibleSpace()
        GUILayout.EndHorizontal()
    def adjustColliderAutoEnable(phyCld):
        GUILayout.BeginHorizontal()
        GUILayout.Label("Auto enable:", GUILayout.Width(adjTitleWidth))
        phyCld.autoEnable = GUILayout.Toggle(phyCld.autoEnable, " When object visible" if phyCld.autoEnable else "")
        GUILayout.FlexibleSpace()
        GUILayout.EndHorizontal()
    def adjustBoxCollider(phyCld):
        GUILayout.BeginScrollView(Vector2.zero, GUILayout.Height(230))
        GUILayout.Label("BoxCollider Setting:")
        #GUILayout.Label("Center: %.3f, %.3f, %.3f"%(boxCld.center.x, boxCld.center.y, boxCld.center.z))
        #GUILayout.Label("Size: %.3f, %.3f, %.3f"%(boxCld.size.x, boxCld.size.y, boxCld.size.z))
        adjustColliderCenter(phyCld)
        adjustColliderSize(phyCld)
        adjustColliderTrigger(phyCld)
        adjustColliderAutoEnable(phyCld)
        GUILayout.EndScrollView()
    def adjustSphereCollider(phyCld):
        #GUILayout.Label("SphereCollider Param:")
        #GUILayout.Label("Center: %.3f, %.3f, %.3f"%(phyCld.collider.center.x, phyCld.collider.center.y, phyCld.collider.center.z))
        #GUILayout.Label("Radius: %.3f"%(phyCld.collider.radius))
        GUILayout.BeginScrollView(Vector2.zero, GUILayout.Height(180))
        GUILayout.Label("SphereCollider Dimension:")
        adjustColliderCenter(phyCld)
        adjustColliderRadius(phyCld)
        adjustColliderTrigger(phyCld)
        adjustColliderAutoEnable(phyCld)
        GUILayout.EndScrollView()
    def adjustCapsuleCollider(phyCld):
        #GUILayout.Label("CapsuleCollider Param:")
        #GUILayout.Label("Center: %.3f, %.3f, %.3f"%(phyCld.collider.center.x, phyCld.collider.center.y, phyCld.collider.center.z))
        #GUILayout.Label("Direction: %s"%(("X", "Y", "Z")[phyCld.collider.direction]))
        #GUILayout.Label("Height: %.3f"%(phyCld.collider.height))
        #GUILayout.Label("Radius: %.3f"%(phyCld.collider.radius))
        GUILayout.BeginScrollView(Vector2.zero, GUILayout.Height(230))
        GUILayout.Label("SphereCollider Dimension:")
        adjustColliderCenter(phyCld)
        adjustColliderRadius(phyCld)
        adjustColliderHeight(phyCld)
        # direction
        GUILayout.BeginHorizontal()
        GUILayout.Label("Direction", GUILayout.Width(adjTitleWidth))
        for i in range(3):
            GUI.color = Color.green if i == phyCld.collider.direction else oldColor
            if GUILayout.Button(("X", "Y", "Z")[i]):
                phyCld.collider.direction = i
            GUI.color = oldColor
        GUILayout.FlexibleSpace()
        GUILayout.EndHorizontal()
        adjustColliderTrigger(phyCld)
        adjustColliderAutoEnable(phyCld)
        GUILayout.EndScrollView()
    def createPhyMaterialTemp(phyCld):
        phyMtlTemp = GData()
        cld = phyCld.collider
        if not cld.material:
            phyMtlTemp.staticFriction = "0.1"
            phyMtlTemp.dynamicFriction = "0.1"
            phyMtlTemp.frictionCombine = "Average"
            phyMtlTemp.bounciness = "0.1"
            phyMtlTemp.bounceCombine = "Average"
        else:
            phyMtlTemp.staticFriction = "%.3f"%cld.material.staticFriction
            phyMtlTemp.dynamicFriction = "%.3f"%cld.material.dynamicFriction
            phyMtlTemp.frictionCombine = int(cld.material.frictionCombine)
            phyMtlTemp.bounciness = "%.3f"%cld.material.bounciness
            phyMtlTemp.bounceCombine = int(cld.material.bounceCombine)
        return phyMtlTemp
    def selectPhyMatCombine(index):
        BTNS = ("Average", "Minimum", "Multiply", "Maximum")
        if GUILayout.Button(BTNS[index], GUILayout.Width(mtlContentWidth)):
            index += 1
            index %= 4
        return index
    def adjustPhyMaterial(phyCld):
        # staticFriction
        GUILayout.BeginHorizontal()
        GUILayout.Label("Static Friction", GUILayout.Width(mtlTitleWidth))
        gd.phyMaterialTemp.staticFriction = GUILayout.TextField(gd.phyMaterialTemp.staticFriction, GUILayout.Width(mtlContentWidth))
        GUILayout.FlexibleSpace()
        GUILayout.EndHorizontal()
        # dynamicFriction
        GUILayout.BeginHorizontal()
        GUILayout.Label("Dynamic Friction", GUILayout.Width(mtlTitleWidth))
        gd.phyMaterialTemp.dynamicFriction = GUILayout.TextField(gd.phyMaterialTemp.dynamicFriction, GUILayout.Width(mtlContentWidth))
        GUILayout.FlexibleSpace()
        GUILayout.EndHorizontal()
        # frictionCombine
        GUILayout.BeginHorizontal()
        GUILayout.Label("Friction Combine", GUILayout.Width(mtlTitleWidth))
        gd.phyMaterialTemp.frictionCombine = selectPhyMatCombine(gd.phyMaterialTemp.frictionCombine)
        GUILayout.FlexibleSpace()
        GUILayout.EndHorizontal()
        # bounciness
        GUILayout.BeginHorizontal()
        GUILayout.Label("Bounciness", GUILayout.Width(mtlTitleWidth))
        gd.phyMaterialTemp.bounciness = GUILayout.TextField(gd.phyMaterialTemp.bounciness, GUILayout.Width(mtlContentWidth))
        GUILayout.FlexibleSpace()
        GUILayout.EndHorizontal()
        # bounceCombine
        GUILayout.BeginHorizontal()
        GUILayout.Label("Bounce Combine", GUILayout.Width(mtlTitleWidth))
        gd.phyMaterialTemp.bounceCombine = selectPhyMatCombine(gd.phyMaterialTemp.bounceCombine)
        GUILayout.FlexibleSpace()
        GUILayout.EndHorizontal()
    def updatePhyMaterial(phyCld):
        # staticFriction
        try:
            sf = float(gd.phyMaterialTemp.staticFriction)
            if sf < 0:
                raise Exception()
        except:
            showMsg(game, "Invalid static friction value, must be greater than or equal to 0")
            return False
        # dynamicFriction
        try:
            df = float(gd.phyMaterialTemp.dynamicFriction)
            if df < 0:
                raise Exception()
        except:
            showMsg(game, "Invalid dynamic friction value, must be greater than or equal to 0")
            return False
        # bounciness
        try:
            bn = float(gd.phyMaterialTemp.bounciness)
            if bn < 0 or bn > 1:
                raise Exception()
        except:
            showMsg(game, "Invalid bounciness value, must between 0 and 1")
            return False
        # frictionCombine and bounceCombine
        try:
            from vnactor import value_2_enum
            fc = value_2_enum(PhysicMaterialCombine, gd.phyMaterialTemp.frictionCombine)
            bc = value_2_enum(PhysicMaterialCombine, gd.phyMaterialTemp.bounceCombine)
        except:
            showMsg(game, "Invalid combine setting!?")
            return False
        # setup
        phyCld.collider.material.staticFriction = sf
        phyCld.collider.material.dynamicFriction = df
        phyCld.collider.material.frictionCombine = fc
        phyCld.collider.material.bounciness = bn
        phyCld.collider.material.bounceCombine = bc
        return True
    def toggleGizmos(phyCld):
        if phyCld.gizmosObj:
            if game.studio.treeNodeCtrl.selectNode == phyCld.gizmosObj.treeNodeObject:
                game.studio.treeNodeCtrl.selectNode = None
            phyCld.removeGizmos()
        else:
            if phyCld.createGizmos():
                game.studio.treeNodeCtrl.selectNode = phyCld.gizmosObj.treeNodeObject
                
    # list
    gd.colliderListScroll = GUILayout.BeginScrollView(gd.colliderListScroll)
    for phyCld in vnphy.colliders:
        if not phyCld.valid:
            continue    # should be deleted by housekeeping
        if phyCld == gd.currentCollider:    # current collider setting
            if not hasattr(gd, "adjCenterX"):
                initAdjustUI(phyCld)

            GUILayout.BeginVertical(GUI.skin.box)
            if gd.colliderMode == "delete":
                GUILayout.Label(yellowText("Delect this collider?"))
                GUILayout.BeginHorizontal()
                GUILayout.FlexibleSpace()
                GUILayout.Label(phyCld.title)
                GUILayout.FlexibleSpace()
                GUILayout.EndHorizontal()
                GUILayout.BeginHorizontal()
                GUI.color = Color.red
                if GUILayout.Button("Delete"):
                    vnphy.deleteCollider(phyCld)
                    gd.colliderMode = "list"
                GUI.color = oldColor
                if GUILayout.Button("Cancel"):
                    gd.colliderMode = "list"
                GUILayout.EndHorizontal()
            elif gd.colliderMode == "material":
                # title
                GUILayout.BeginHorizontal()
                GUI.color = Color.magenta if phyCld in vnphy.flashingObjects else Color.green
                if GUILayout.Button(phyCld.id, GUI.skin.label):
                    gd.currentCollider = None
                GUI.color = oldColor
                GUILayout.Label(yellowText("physics material"), GUILayout.ExpandWidth(False))
                GUILayout.EndHorizontal()

                # material
                adjustPhyMaterial(phyCld)
                
                # buttons
                GUILayout.BeginHorizontal()
                if GUILayout.Button("Update"):
                    if updatePhyMaterial(phyCld):
                        del gd.phyMaterialTemp
                        gd.colliderMode = "list"
                if GUILayout.Button("Back"):
                    del gd.phyMaterialTemp
                    gd.colliderMode = "list"
                GUILayout.EndHorizontal()
            else:
                # id
                GUILayout.BeginHorizontal()
                if GUILayout.Button("ID:", GUI.skin.label, GUILayout.Width(tWidth)):
                    gd.currentCollider = None
                if GUILayout.Button(magentaText(phyCld.id) if phyCld in vnphy.flashingObjects else greenText(phyCld.id), GUI.skin.label):
                    gd.currentCollider = None
                GUILayout.Label(yellowText(phyCld.type), GUILayout.ExpandWidth(False))
                GUILayout.EndHorizontal()

                # desp
                GUILayout.BeginHorizontal()
                GUILayout.Label("Desp:", GUILayout.Width(tWidth))
                phyCld.desp = GUILayout.TextField(phyCld.desp)
                GUILayout.EndHorizontal()
                
                # bounds
                if gd.setting.DEBUG_SHOW_COLLIDER_BOUNDS:
                    bounds = phyCld.collider.bounds
                    GUILayout.Label("Bounds Center: %.3f, %.3f, %.3f"%(bounds.center.x, bounds.center.y, bounds.center.z))
                    GUILayout.Label("Bounds Extents: %.3f, %.3f, %.3f"%(bounds.extents.x, bounds.extents.y, bounds.extents.z))

                # dimention edit by type
                if phyCld.type == "Box":
                    adjustBoxCollider(phyCld)
                elif phyCld.type == "Sphere":
                    adjustSphereCollider(phyCld)
                elif phyCld.type == "Capsule":
                    adjustCapsuleCollider(phyCld)
                if phyCld.gizmosEditMode:
                    updateAdjustUI(phyCld)

                # attached rigidbody
                phyRgb = vnphy.getAttachedRigidBodyByColliderId(phyCld.id)
                if phyRgb:
                    GUILayout.BeginHorizontal()
                    GUILayout.Label("Attached Rigidbody:")
                    GUILayout.Space(5)
                    if GUILayout.Button(greenText(phyRgb.id)):
                        gd.currentRigidBody = phyRgb
                        gd.mainTab = "Rigidbody"
                        gd.rigidBodyMode = "list"
                    if GUILayout.Button("Reset"):
                        phyRgb.resetRigidBody()
                    GUILayout.FlexibleSpace()
                    GUILayout.EndHorizontal()

                # button
                GUILayout.BeginHorizontal()
                GUI.color = Color.green if phyCld.gizmosObj else oldColor
                if GUILayout.Button("Gizmos"):
                    toggleGizmos(phyCld)
                GUI.color = oldColor
                if GUILayout.Button("PhyMaterial"):
                    gd.phyMaterialTemp = createPhyMaterialTemp(phyCld)
                    gd.colliderMode = "material"
                GUI.color = Color.red
                if GUILayout.Button("Delete"):
                    gd.colliderMode = "delete"
                GUI.color = oldColor
                GUILayout.EndHorizontal()
            GUILayout.EndVertical()
        else:   # list collider
            GUILayout.BeginHorizontal(GUI.skin.box)
            phyCld.enable = GUILayout.Toggle(phyCld.enable, "", GUILayout.ExpandWidth(False))
            if phyCld in vnphy.flashingObjects:
                title = magentaText(phyCld.title)
            elif phyCld.enable:
                title = greenText(phyCld.id) + ": " + phyCld.desp
            else:
                title = greyText(phyCld.title)
            if GUILayout.Button(title, GUI.skin.label):
                gd.currentCollider = phyCld
                initAdjustUI(phyCld)
            GUILayout.Label(yellowText(phyCld.type), GUILayout.ExpandWidth(False))
            GUI.color = Color.green if phyCld.gizmosObj else oldColor
            if GUILayout.Button("G", GUILayout.ExpandWidth(False)):
                toggleGizmos(phyCld)
            GUI.color = oldColor
            GUILayout.EndHorizontal()
    GUILayout.EndScrollView()

    # collider detect
    if gd.setting.DEBUG_SHOW_LAST_HITINFO:
        GUILayout.Label("Collider detect:" + (greenText(" <On last click>") if vnphy.lastMouseHitted else ""))
        if vnphy.lastMouseHitInfo and vnphy.lastMouseHitInfo.transform:
            txt = getPathOfGameObject(vnphy.lastMouseHitInfo.transform.gameObject)
            txt = greenText(txt)
            #txt += "\nlayer: %d"%vnphy.lastMouseHitInfo.transform.gameObject.layer
        else:
            txt = "- not detected yet -"
            txt = yellowText(txt)
        GUILayout.Label(txt)

    # buttons
    GUILayout.FlexibleSpace()
    GUILayout.BeginHorizontal()
    if GUILayout.Button("Add new collider"):
        gd.tempColliderId = getNewId()
        gd.tempColliderType = "Box"
        gd.tempSearchKeyword = ""
        gd.selectedOciObj = None
        gd.expendedBones = set()
        gd.selectedBoneGO = None
        gd.colliderMode = "create"
    if GUILayout.Button("...", GUILayout.ExpandWidth(False)):
        vnphy_setting_GUI(gd, vnphy)
    GUILayout.EndHorizontal()

def vnphy_collider_tab_add(gd, vnphy):
    oldColor = GUI.color
    game = vnphy.game
    expendMarkSpace = 17
    from vnphysics_core import PhyCollider

    def clearAndQuit():
        del gd.tempColliderId
        del gd.tempColliderType
        del gd.tempSearchKeyword
        del gd.selectedOciObj
        del gd.expendedBones
        del gd.selectedBoneGO
        gd.colliderMode = "list"
    def checkAndBuild():
        # id
        gd.tempColliderId = gd.tempColliderId.strip()
        if len(gd.tempColliderId) == 0 or gd.tempColliderId in vnphy.colliderIdDict:
            showMsg(game, "Collider id is empty or used")
            return False
        # type
        if gd.tempColliderType not in PhyCollider.COLLIDER_TYPES:
            showMsg(game, "Collider type not selected")
            return False
        # object
        if gd.selectedOciObj == None or gd.selectedBoneGO == None:
            showMsg(game, "Target object not selected, choose an object and select a base bone to create collider")
            return False
        # create
        try:
            collider = PhyCollider.CreateFromOciObject(gd.tempColliderId, gd.tempColliderType, gd.selectedOciObj, gd.selectedBoneGO)
            vnphy.registerCollider(collider)
            return True
        except Exception as e:
            showMsg(game, "Fail to create collider: " + str(e))
            return False
    def ListBones(boneGO, intend):
        canExpand = boneGO.transform.childCount > 0
        isExpanded = boneGO in gd.expendedBones
        GUILayout.BeginHorizontal()
        if intend > 0:
            GUILayout.Space(intend)
            if canExpand:
                if GUILayout.Button(u"\u25bc" if isExpanded else u"\u25ba", GUI.skin.label):
                    if isExpanded:
                        gd.expendedBones.discard(boneGO)
                    else:
                        gd.expendedBones.add(boneGO)
            else:
                GUILayout.Space(expendMarkSpace)
        else:
            GUILayout.Label(u"\u2605")
        GUI.color = Color.green if boneGO == gd.selectedBoneGO else oldColor
        if GUILayout.Button(boneGO.name):
            gd.selectedBoneGO = boneGO
        GUI.color = oldColor
        GUILayout.FlexibleSpace()
        GUILayout.EndHorizontal()
        # show sub nodes
        if canExpand and (isExpanded or intend == 0):
            cIndex = intend + 20
            for i in range(boneGO.transform.childCount):
                sn = boneGO.transform.GetChild(i).gameObject
                ListBones(sn, cIndex)
    def SearchBones(boneGO, keyword):
        found = 0
        if keyword in boneGO.name.lower():
            GUI.color = Color.green if boneGO == gd.selectedBoneGO else oldColor
            if GUILayout.Button(boneGO.name, GUILayout.ExpandWidth(False)):
                gd.selectedBoneGO = boneGO
            GUI.color = oldColor
            found = 1
        for i in range(boneGO.transform.childCount):
            sn = boneGO.transform.GetChild(i).gameObject
            found += SearchBones(sn, keyword)
        return found

    # id
    GUILayout.BeginHorizontal()
    GUILayout.Label("Id:", GUILayout.Width(40))
    gd.tempColliderId = GUILayout.TextField(gd.tempColliderId, GUILayout.Width(150))
    GUILayout.FlexibleSpace()
    GUILayout.EndHorizontal()

    # type
    GUILayout.BeginHorizontal()
    GUILayout.Label("Type:", GUILayout.Width(40))
    for cType in PhyCollider.COLLIDER_TYPES:
        GUI.color = Color.green if gd.tempColliderType == cType else oldColor
        if GUILayout.Button(cType):
            gd.tempColliderType = cType
    GUI.color = oldColor
    GUILayout.EndHorizontal()

    # select object
    GUILayout.Label("Selected target object:")
    selOcis = getCurrentSelectedOciObjects(game)
    selOciObj = None
    hsOCI = None
    if len(selOcis) == 1:
        selOciObj = selOcis[0]
        txt = greenText("%s")%selOciObj.treeNodeObject.textName
        hsOCI = HSNeoOCI.create_from(selOciObj)
    elif len(selOcis) > 1:
        txt = yellowText("- %d object selected -"%(len(selOcis)))
    else:
        txt = yellowText("- none -")
    if hsOCI:
        if isinstance(hsOCI, HSNeoOCIChar):
            rootBone = selOciObj.charInfo.objRoot
        elif isinstance(hsOCI, HSNeoOCIItem):
            rootBone = selOciObj.objectItem
        elif isinstance(hsOCI, HSNeoOCIFolder):
            rootBone = selOciObj.objectItem
        else:
            rootBone = None
            txt = yellowText("- Only character/item/folder supported -")
    else:
        rootBone = None
    if gd.selectedOciObj != selOciObj:
        gd.selectedOciObj = selOciObj
        gd.expendedBones = set()
        gd.selectedBoneGO = rootBone
    GUILayout.Label(txt)

    # select bone
    if rootBone:
        # bone title
        GUILayout.BeginHorizontal()
        GUILayout.Label("Selected base bone:")
        GUILayout.FlexibleSpace()
        gd.tempSearchKeyword = GUILayout.TextField(gd.tempSearchKeyword, GUILayout.Width(100))
        if GUILayout.Button("X"):
            gd.tempSearchKeyword = ""
        GUILayout.EndHorizontal()
        # bone list
        gd.colliderBoneScroll = GUILayout.BeginScrollView(gd.colliderBoneScroll, GUI.skin.box)
        if len(gd.tempSearchKeyword.strip()):
            found = SearchBones(rootBone, gd.tempSearchKeyword.strip().lower())
            if not found:
                GUILayout.Label(yellowText("No matched bone"))
        else:
            ListBones(rootBone, 0)
        GUILayout.EndScrollView()

    # buttons
    GUILayout.FlexibleSpace()
    GUILayout.BeginHorizontal()
    if GUILayout.Button("Create"):
        if checkAndBuild():
            clearAndQuit()
    if GUILayout.Button("Cancel"):
        clearAndQuit()
    GUILayout.EndHorizontal()

# rigidbody tab
def vnphy_main_rigidbody_tab(gd, vnphy):
    if gd.rigidBodyMode == "list":
        vnphy_rigidbody_tab_list(gd, vnphy)
    elif gd.rigidBodyMode == "create":
        vnphy_rigidbody_tab_add(gd, vnphy)
    elif gd.rigidBodyMode == "delete":
        vnphy_rigidbody_tab_list(gd, vnphy)
    else:
        gd.rigidBodyMode = "list"

def vnphy_rigidbody_tab_list(gd, vnphy):
    oldColor = GUI.color
    game = vnphy.game
    tWidth = 40
    adjFullWidth = 180
    adjTitleWidth = 90
    adjResetWidth = 40
    
    def getNewId():
        i = 1
        while "vrb%d"%i in vnphy.rigidBodyIdDict:
            i += 1
        return "vrb%d"%i
    def initAdjustUI(phyRgb):
        # mass
        gd.adjMass = DragInputUnit(Rect(4, 25*0+4, adjFullWidth, 21), "Mass", False, adjTitleWidth)
        gd.adjMass.setValueFormat(vFormat="%.3f", minValue = 0.001)
        gd.adjMass.setDragRate(gd.posRateX, gd.posRateY)
        # drag
        gd.adjDrag = DragInputUnit(Rect(4, 25*1+4, adjFullWidth, 21), "Drag", False, adjTitleWidth)
        gd.adjDrag.setValueFormat(vFormat="%.3f", minValue = 0.000)
        gd.adjDrag.setDragRate(gd.posRateX, gd.posRateY)
        # angular drag
        gd.adjADrag = DragInputUnit(Rect(4, 25*2+4, adjFullWidth, 21), "Angular Drag", False, adjTitleWidth)
        gd.adjADrag.setValueFormat(vFormat="%.3f", minValue = 0.000)
        gd.adjADrag.setDragRate(gd.posRateX, gd.posRateY)
        # update
        updateAdjustUI(phyRgb)
    def updateAdjustUI(phyRgb):
        gd.adjMass.setValue(phyRgb.rigidBody.mass)
        gd.adjDrag.setValue(phyRgb.rigidBody.drag)
        gd.adjADrag.setValue(phyRgb.rigidBody.angularDrag)
    def adjustRigidBodySetting(phyRgb):
        GUILayout.BeginScrollView(Vector2.zero, GUILayout.Height(25*5 + 5))
        rgb = phyRgb.rigidBody
        # mass
        GUILayout.BeginHorizontal()
        GUILayout.Space(adjFullWidth + 8)
        if gd.adjMass.paint():
            rgb.mass = gd.adjMass.value
        if GUILayout.Button("1", GUILayout.Width(adjResetWidth)):
            rgb.mass = 1
            updateAdjustUI(phyRgb)
        GUILayout.EndHorizontal()
        # drag
        GUILayout.BeginHorizontal()
        GUILayout.Space(adjFullWidth + 8)
        if gd.adjDrag.paint():
            rgb.drag = gd.adjDrag.value
        if GUILayout.Button("0", GUILayout.Width(adjResetWidth)):
            rgb.drag = 0
            updateAdjustUI(phyRgb)
        GUILayout.EndHorizontal()
        # angular drag
        GUILayout.BeginHorizontal()
        GUILayout.Space(adjFullWidth + 8)
        if gd.adjADrag.paint():
            rgb.angularDrag = gd.adjADrag.value
        if GUILayout.Button("0.05", GUILayout.Width(adjResetWidth)):
            rgb.angularDrag = 0.05
            updateAdjustUI(phyRgb)
        GUILayout.EndHorizontal()
        # use gravity
        GUILayout.BeginHorizontal()
        GUILayout.Label("Use Gravity", GUILayout.Width(adjTitleWidth))
        rgb.useGravity = GUILayout.Toggle(rgb.useGravity, "")
        GUILayout.FlexibleSpace()
        GUILayout.EndHorizontal()
        # isKinematic
        GUILayout.BeginHorizontal()
        GUILayout.Label("Is Kinematic", GUILayout.Width(adjTitleWidth))
        rgb.isKinematic = GUILayout.Toggle(rgb.isKinematic, "")
        GUILayout.FlexibleSpace()
        GUILayout.EndHorizontal()
        # constraints
        if 0:
            GUILayout.BeginHorizontal()
            GUILayout.Label("Freeze Pos", GUILayout.Width(adjTitleWidth))
            fpx = GUILayout.Toggle(False, "X")
            fpy = GUILayout.Toggle(False, "Y")
            fpz = GUILayout.Toggle(False, "Z")
            GUILayout.FlexibleSpace()
            GUILayout.EndHorizontal()
            GUILayout.BeginHorizontal()
            GUILayout.Label("Freeze Rot", GUILayout.Width(adjTitleWidth))
            frx = GUILayout.Toggle(False, "X")
            fry = GUILayout.Toggle(False, "Y")
            frz = GUILayout.Toggle(False, "Z")
            GUILayout.FlexibleSpace()
            GUILayout.EndHorizontal()

        GUILayout.EndScrollView()

    # list
    gd.rigidBodyListScroll = GUILayout.BeginScrollView(gd.rigidBodyListScroll)
    for phyRgb in vnphy.rigidBodys:
        if not phyRgb.valid:
            continue    # should be deleted by housekeeping
        if phyRgb == gd.currentRigidBody:    # current rigidbody setting
            if not hasattr(gd, "adjMass"):
                initAdjustUI(phyRgb)

            GUILayout.BeginVertical(GUI.skin.box)
            if gd.rigidBodyMode == "delete":
                GUILayout.Label(yellowText("Delect this rigidbody?"))
                GUILayout.BeginHorizontal()
                GUILayout.FlexibleSpace()
                GUILayout.Label(phyRgb.title)
                GUILayout.FlexibleSpace()
                GUILayout.EndHorizontal()
                GUILayout.BeginHorizontal()
                GUI.color = Color.red
                if GUILayout.Button("Delete"):
                    vnphy.deleteRigidBody(phyRgb)
                    gd.rigidBodyMode = "list"
                GUI.color = oldColor
                if GUILayout.Button("Cancel"):
                    gd.rigidBodyMode = "list"
                GUILayout.EndHorizontal()
            else:
                # id
                GUILayout.BeginHorizontal()
                if GUILayout.Button("ID:", GUI.skin.label, GUILayout.Width(tWidth)):
                    gd.currentRigidBody = None
                if GUILayout.Button(magentaText(phyRgb.id) if phyRgb in vnphy.flashingObjects else greenText(phyRgb.id), GUI.skin.label):
                    gd.currentRigidBody = None
                GUILayout.EndHorizontal()

                # desp
                GUILayout.BeginHorizontal()
                GUILayout.Label("Desp:", GUILayout.Width(tWidth))
                phyRgb.desp = GUILayout.TextField(phyRgb.desp)
                GUILayout.EndHorizontal()
                
                # setting of rigidbody
                adjustRigidBodySetting(phyRgb)

                # infos
                if gd.setting.DEBUG_SHOW_RIGIDBODY_INFO:
                    rgb = phyRgb.rigidBody
                    GUILayout.Label("Position: %.3f, %.3f, %.3f"%(rgb.position.x, rgb.position.y, rgb.position.z))
                    GUILayout.Label("Rotation: %.3f, %.3f, %.3f"%(rgb.rotation.x, rgb.rotation.y, rgb.rotation.z))
                    GUILayout.Label("velocity: %.3f, %.3f, %.3f"%(rgb.velocity.x, rgb.velocity.y, rgb.velocity.z))
                    GUILayout.Label("A-velocity: %.3f, %.3f, %.3f"%(rgb.angularVelocity.x, rgb.angularVelocity.y, rgb.angularVelocity.z))

                # button
                GUILayout.BeginHorizontal()
                if GUILayout.Button("Reset"):
                    phyRgb.resetRigidBody()
                if GUILayout.Button("Update Pos/Rot"):
                    phyRgb.orgPosition = phyRgb.rigidBody.position
                    phyRgb.orgRotation = phyRgb.rigidBody.rotation
                GUI.color = Color.red
                if GUILayout.Button("Delete"):
                    gd.rigidBodyMode = "delete"
                GUI.color = oldColor
                if GUILayout.Button(u"\u25B2", GUILayout.ExpandWidth(False)):
                    gd.currentRigidBody = None
                GUILayout.EndHorizontal()

            GUILayout.EndVertical()
        else:   # list rigidbody
            GUILayout.BeginHorizontal(GUI.skin.box)
            if GUILayout.Button(greenText(phyRgb.id), GUI.skin.label, GUILayout.ExpandWidth(False)):
                gd.currentRigidBody = phyRgb
                initAdjustUI(phyRgb)
            GUI.color = Color.magenta if phyRgb in vnphy.flashingObjects else oldColor
            if GUILayout.Button(phyRgb.desp, GUI.skin.label):
                gd.currentRigidBody = phyRgb
                initAdjustUI(phyRgb)
            GUI.color = oldColor
            if phyRgb.rigidBody.isKinematic:
                GUILayout.Label(greyText("Kinematic"), GUILayout.ExpandWidth(False))
            elif phyRgb.rigidBody.IsSleeping():
                GUILayout.Label(yellowText("Sleep"), GUILayout.ExpandWidth(False))
            else:
                GUILayout.Label(greenText("Awake"), GUILayout.ExpandWidth(False))
            if GUILayout.Button("R", GUILayout.ExpandWidth(False)):
                phyRgb.resetRigidBody()
            GUILayout.EndHorizontal()
    GUILayout.EndScrollView()

    # buttons
    GUILayout.FlexibleSpace()
    GUILayout.BeginHorizontal()
    if GUILayout.Button("Add new rigidBody"):
        gd.tempRigidBodyId = getNewId()
        gd.tempSearchKeyword = ""
        gd.selectedOciObj = None
        gd.expendedBones = set()
        gd.selectedBoneGO = None
        gd.rigidBodyMode = "create"
    if GUILayout.Button("Reset All", GUILayout.ExpandWidth(False)):
        for phyRgb in vnphy.rigidBodys:
            phyRgb.resetRigidBody()
    if GUILayout.Button("...", GUILayout.ExpandWidth(False)):
        vnphy_setting_GUI(gd, vnphy)
    GUILayout.EndHorizontal()

def vnphy_rigidbody_tab_add(gd, vnphy):
    oldColor = GUI.color
    game = vnphy.game
    expendMarkSpace = 17

    def clearAndQuit():
        del gd.tempRigidBodyId
        del gd.tempSearchKeyword
        del gd.selectedOciObj
        del gd.expendedBones
        del gd.selectedBoneGO
        gd.rigidBodyMode = "list"
    def checkAndBuild():
        # id
        gd.tempRigidBodyId = gd.tempRigidBodyId.strip()
        if len(gd.tempRigidBodyId) == 0 or gd.tempRigidBodyId in vnphy.rigidBodyIdDict:
            showMsg(game, "rigidbody id is empty or used")
            return False
        # object
        if gd.selectedOciObj == None or gd.selectedBoneGO == None:
            showMsg(game, "Target object not selected, choose an object and select a base bone to create rigidbody")
            return False
        # create
        try:
            from vnphysics_core import PhyRigidBody
            rigidbody = PhyRigidBody.CreateFromOciObject(gd.tempRigidBodyId, gd.selectedOciObj, gd.selectedBoneGO)
            vnphy.registerRigidBody(rigidbody)
            return True
        except Exception as e:
            showMsg(game, "Fail to create rigidbody: " + str(e))
            return False
    def ListBones(boneGO, intend):
        canExpand = boneGO.transform.childCount > 0
        isExpanded = boneGO in gd.expendedBones
        GUILayout.BeginHorizontal()
        if intend > 0:
            GUILayout.Space(intend)
            if canExpand:
                if GUILayout.Button(u"\u25bc" if isExpanded else u"\u25ba", GUI.skin.label):
                    if isExpanded:
                        gd.expendedBones.discard(boneGO)
                    else:
                        gd.expendedBones.add(boneGO)
            else:
                GUILayout.Space(expendMarkSpace)
        else:
            GUILayout.Label(u"\u2605")
        GUI.color = Color.green if boneGO == gd.selectedBoneGO else oldColor
        if GUILayout.Button(boneGO.name):
            gd.selectedBoneGO = boneGO
        GUI.color = oldColor
        GUILayout.FlexibleSpace()
        GUILayout.EndHorizontal()
        # show sub nodes
        if canExpand and (isExpanded or intend == 0):
            cIndex = intend + 20
            for i in range(boneGO.transform.childCount):
                sn = boneGO.transform.GetChild(i).gameObject
                ListBones(sn, cIndex)
    def SearchBones(boneGO, keyword):
        found = 0
        if keyword in boneGO.name.lower():
            GUI.color = Color.green if boneGO == gd.selectedBoneGO else oldColor
            if GUILayout.Button(boneGO.name, GUILayout.ExpandWidth(False)):
                gd.selectedBoneGO = boneGO
            GUI.color = oldColor
            found = 1
        for i in range(boneGO.transform.childCount):
            sn = boneGO.transform.GetChild(i).gameObject
            found += SearchBones(sn, keyword)
        return found

    # id
    GUILayout.BeginHorizontal()
    GUILayout.Label("Id:", GUILayout.Width(40))
    gd.tempRigidBodyId = GUILayout.TextField(gd.tempRigidBodyId, GUILayout.Width(150))
    GUILayout.FlexibleSpace()
    GUILayout.EndHorizontal()

    # select object
    GUILayout.Label("Selected target object:")
    selOcis = getCurrentSelectedOciObjects(game)
    selOciObj = None
    hsOCI = None
    if len(selOcis) == 1:
        selOciObj = selOcis[0]
        txt = greenText("%s")%selOciObj.treeNodeObject.textName
        hsOCI = HSNeoOCI.create_from(selOciObj)
    elif len(selOcis) > 1:
        txt = yellowText("- %d object selected -"%(len(selOcis)))
    else:
        txt = yellowText("- none -")
    if hsOCI:
        if isinstance(hsOCI, HSNeoOCIChar):
            rootBone = selOciObj.charInfo.objRoot
        elif isinstance(hsOCI, HSNeoOCIItem):
            rootBone = selOciObj.objectItem
        elif isinstance(hsOCI, HSNeoOCIFolder):
            rootBone = selOciObj.objectItem
        else:
            rootBone = None
            txt = yellowText("- Only character/item/folder supported -")
    else:
        rootBone = None
    if gd.selectedOciObj != selOciObj:
        gd.selectedOciObj = selOciObj
        gd.expendedBones = set()
        gd.selectedBoneGO = rootBone
    GUILayout.Label(txt)

    # select bone
    if rootBone:
        # bone title
        GUILayout.BeginHorizontal()
        GUILayout.Label("Selected base bone:")
        GUILayout.FlexibleSpace()
        gd.tempSearchKeyword = GUILayout.TextField(gd.tempSearchKeyword, GUILayout.Width(100))
        if GUILayout.Button("X"):
            gd.tempSearchKeyword = ""
        GUILayout.EndHorizontal()
        # bone list
        gd.rigidBodyBoneScroll = GUILayout.BeginScrollView(gd.rigidBodyBoneScroll, GUI.skin.box)
        if len(gd.tempSearchKeyword.strip()):
            found = SearchBones(rootBone, gd.tempSearchKeyword.strip().lower())
            if not found:
                GUILayout.Label(yellowText("No matched bone"))
        else:
            ListBones(rootBone, 0)
        GUILayout.EndScrollView()

    # buttons
    GUILayout.FlexibleSpace()
    GUILayout.BeginHorizontal()
    if GUILayout.Button("Create"):
        if checkAndBuild():
            clearAndQuit()
    if GUILayout.Button("Cancel"):
        clearAndQuit()
    GUILayout.EndHorizontal()

# trigger tab
def vnphy_main_trigger_tab(gd, vnphy):
    if gd.triggerMode == "list":
        vnphy_trigger_tab_list(gd, vnphy)
    elif gd.triggerMode == "add":
        vnphy_trigger_tab_edit(gd, vnphy, "Create")
    elif gd.triggerMode == "edit":
        vnphy_trigger_tab_edit(gd, vnphy, "Update")
    elif gd.triggerMode == "delete":
        vnphy_trigger_tab_list(gd, vnphy)
    else:
        gd.triggerMode = "list"

def vnphy_trigger_tab_list(gd, vnphy):
    oldColor = GUI.color
    tWidth = 40

    def getNewId():
        from vnframe import verifyTagName
        i = 1
        while True:
            newId = "vtg%d"%i
            if newId in vnphy.triggerIdDict:
                i += 1
            else:
                return newId
    def getColoredId(trg):
        if not trg.enable:
            return trg.id
        elif trg in vnphy.flashingObjects:
            return magentaText(trg.id)
        elif trg.cdTimer == -1:
            return greyText(trg.id)
        elif trg.cdTimer > 0:
            return yellowText(trg.id)
        else:
            return greenText(trg.id)
    def getColoredTitle(trg):
        if not trg.enable:
            return greyText(trg.id + ": " + trg.desp)
        elif trg in vnphy.flashingObjects:
            return magentaText(trg.id + ": " + trg.desp)
        elif trg.cdTimer == -1:
            return greyText(trg.id) + ": " + trg.desp
        elif trg.cdTimer > 0:
            return yellowText(trg.id) + ": " + trg.desp
        else:
            return greenText(trg.id) + ": " + trg.desp

    # list
    gd.triggerListScroll = GUILayout.BeginScrollView(gd.triggerListScroll)
    for trg in vnphy.triggers:
        if gd.currentTrigger == trg:    # current trigger
            GUILayout.BeginVertical(GUI.skin.box)
            if gd.triggerMode == "list":
                # id
                GUILayout.BeginHorizontal()
                if GUILayout.Button("ID:", GUI.skin.label, GUILayout.Width(tWidth)):
                    gd.currentTrigger = None
                if GUILayout.Button(getColoredId(trg), GUI.skin.label):
                    gd.currentTrigger = None
                GUILayout.Label(yellowText(trg.type), GUILayout.ExpandWidth(False))
                GUILayout.EndHorizontal()
                # desp
                GUILayout.BeginHorizontal()
                GUILayout.Label("Desp:", GUILayout.Width(tWidth))
                trg.desp = GUILayout.TextField(trg.desp)
                GUILayout.EndHorizontal()
                # buttons
                GUILayout.BeginHorizontal()
                if GUILayout.Button(" Edit "):
                    gd.tempTrigger = trg.copy()
                    gd.triggerMode = "edit"
                if GUILayout.Button(" Copy "):
                    gd.tempTrigger = trg.copy()
                    gd.tempTrigger.id = getNewId()
                    gd.triggerMode = "add"
                if GUILayout.Button("Cooldown"):
                    trg.cdTimer = 0
                GUI.color = Color.red
                if GUILayout.Button("Delete"):
                    gd.triggerMode = "delete"
                GUI.color = oldColor
                GUILayout.EndHorizontal()
            else:
                GUILayout.Label(yellowText("Delete this trigger?"))
                GUILayout.BeginHorizontal()
                GUILayout.FlexibleSpace()
                GUILayout.Label("%s: %s"%(greenText(trg.id), trg.desp))
                GUILayout.FlexibleSpace()
                GUILayout.EndHorizontal()
                GUILayout.Space(5)
                GUILayout.BeginHorizontal()
                GUI.color = Color.red
                if GUILayout.Button("Delete"):
                    vnphy.deleteTrigger(trg)
                    gd.triggerMode = "list"
                GUI.color = oldColor
                if GUILayout.Button("Cancel"):
                    gd.triggerMode = "list"
                GUILayout.EndHorizontal()
            # end
            GUILayout.EndVertical()
        else:   # non-current trigger
            GUILayout.BeginHorizontal(GUI.skin.box)
            trg.enable = GUILayout.Toggle(trg.enable, "", GUILayout.ExpandWidth(False))
            if GUILayout.Button(getColoredTitle(trg), GUI.skin.label):
                gd.currentTrigger = trg
            if GUILayout.Button(greenText(u'\u25b6'), GUILayout.ExpandWidth(False)):
                ci = {"caller": "vnphy_trigger_list"}
                vnphy.fireTrigger(trg.id, ci)
            GUILayout.EndHorizontal()
    GUILayout.EndScrollView()

    # buttons
    GUILayout.FlexibleSpace()
    GUILayout.BeginHorizontal()
    if GUILayout.Button("Add new trigger"):
        from vnphysics_core import PhyTrigger
        gd.tempTrigger = PhyTrigger()
        gd.tempTrigger.id = getNewId()
        gd.triggerMode = "add"
    if GUILayout.Button("...", GUILayout.ExpandWidth(False)):
        vnphy_setting_GUI(gd, vnphy)
    GUILayout.EndHorizontal()

def vnphy_trigger_tab_edit(gd, vnphy, mode):
    oldColor = GUI.color
    tWidth = 60

    if not hasattr(gd, "tempSelectCollider"):
        gd.tempSelectCollider = False
        gd.tempSelectAction = False
        gd.tempWaitForKeycode = False

    def clearAndQuit():
        del gd.tempTrigger
        del gd.tempSelectCollider
        del gd.tempSelectAction
        gd.triggerMode = "list"
    
    def selectCollider():
        GUILayout.BeginVertical(GUI.skin.box)
        canidates = [cld.id for cld in vnphy.colliders if cld.id not in gd.tempTrigger.options["colliders"]]
        if len(canidates):
            for cldId in canidates:
                if GUILayout.Button(vnphy.colliderIdDict[cldId].title, GUILayout.ExpandWidth(False)):
                    gd.tempSelectCollider = False
                    return cldId
        else:
            GUILayout.Label(yellowText("- No valid collider -"))
        GUILayout.EndVertical()
        return None

    def selectAction():
        GUILayout.BeginVertical(GUI.skin.box)
        canidates = [act.id for act in vnphy.actions if act.id not in gd.tempTrigger.actions]
        if len(canidates):
            for actId in canidates:
                if GUILayout.Button(vnphy.actionIdDict[actId].title, GUILayout.ExpandWidth(False)):
                    gd.tempSelectAction = False
                    return actId
        else:
            GUILayout.Label(yellowText("- No valid action -"))
        GUILayout.EndVertical()
        return None

    # gui total start
    gd.triggerEditScroll = GUILayout.BeginScrollView(gd.triggerEditScroll)

    # id
    GUILayout.BeginHorizontal()
    GUILayout.Label("ID:", GUILayout.Width(tWidth))
    if mode == "Create":
        gd.tempTrigger.id = GUILayout.TextField(gd.tempTrigger.id, GUILayout.Width(100))
    else:
        GUILayout.Label(gd.tempTrigger.id)
    GUILayout.EndHorizontal()
    # desp
    GUILayout.BeginHorizontal()
    GUILayout.Label("Desp:", GUILayout.Width(tWidth))
    gd.tempTrigger.desp = GUILayout.TextField(gd.tempTrigger.desp)
    GUILayout.EndHorizontal()
    # type
    GUILayout.BeginHorizontal()
    GUILayout.Label("Type:", GUILayout.Width(tWidth))
    if mode == "Create":
        for ttype in gd.tempTrigger.TRIGGER_TYPES:
            GUI.color = Color.green if gd.tempTrigger.type == ttype else oldColor
            if GUILayout.Button(ttype):
                gd.tempTrigger.type = ttype
                gd.tempTrigger.options = {}
                if ttype == "Click":
                    gd.tempTrigger.options["colliders"] = []
                    gd.tempTrigger.options["clickTarget"] = "any"
                    gd.tempTrigger.options["mouseButton"] = [0]
                elif ttype == "Input":
                    gd.tempTrigger.options["keycodes"] = []
                    gd.tempTrigger.options["timing"] = "Down"
                elif ttype == "Collision":
                    gd.tempTrigger.options["colliders"] = []
                    gd.tempTrigger.options["event"] = "Collision"
                    gd.tempTrigger.options["timing"] = "Enter"
                elif ttype == "SceneEvent":
                    gd.tempTrigger.options["event"] = "Load"
                else:
                    pass
            if ttype == "Collision":
                GUILayout.EndHorizontal()
                GUILayout.BeginHorizontal()
                GUILayout.Label(" ", GUILayout.Width(tWidth))
    else:
        GUI.color = Color.yellow
        GUILayout.Label(gd.tempTrigger.type)
    GUI.color = oldColor
    GUILayout.EndHorizontal()
    
    if gd.tempTrigger.type:
        # interval
        GUILayout.BeginHorizontal()
        GUILayout.Label("Interval:", GUILayout.Width(tWidth))
        tempInterval = GUILayout.TextField("%.3f"%gd.tempTrigger.interval, GUILayout.Width(60))
        if tempInterval != "%.3f"%gd.tempTrigger.interval:
            try:
                gd.tempTrigger.interval = float(tempInterval)
                if gd.tempTrigger.interval < 0:
                    gd.tempTrigger.interval = -1
            except:
                pass
        GUILayout.Label("sec ")
        if GUILayout.Button("0"):
            gd.tempTrigger.interval = 0
        if GUILayout.Button("Once"):
            gd.tempTrigger.interval = -1
        GUILayout.FlexibleSpace()
        GUILayout.EndHorizontal()
        # options
        if gd.tempTrigger.type == "Click":  # click type
            # mouse button
            GUILayout.BeginHorizontal()
            GUILayout.Label("Mouse:", GUILayout.Width(tWidth))
            mouseBtn = gd.tempTrigger.options["mouseButton"]
            mouseBtnName = ("Left", "Right", "Middle")
            for i in range(3):
                if GUILayout.Button(greenText(mouseBtnName[i]) if i in mouseBtn else mouseBtnName[i]):
                    if i in mouseBtn:
                        mouseBtn.remove(i)
                    else:
                        mouseBtn.append(i)
                    mouseBtn.sort()
            GUILayout.FlexibleSpace()
            GUILayout.EndHorizontal()
            # target option
            GUILayout.BeginHorizontal()
            GUILayout.Label("Click on:", GUILayout.Width(tWidth))
            if GUILayout.Toggle(gd.tempTrigger.options["clickTarget"] == "any", "Anywhere"):
                gd.tempTrigger.options["clickTarget"] = "any"
            GUILayout.EndHorizontal()
            GUILayout.BeginHorizontal()
            GUILayout.Label(" ", GUILayout.Width(tWidth))
            if GUILayout.Toggle(gd.tempTrigger.options["clickTarget"] == "sel", "Selected colliders"):
                gd.tempTrigger.options["clickTarget"] = "sel"
            GUILayout.EndHorizontal()
            GUILayout.BeginHorizontal()
            GUILayout.Label(" ", GUILayout.Width(tWidth))
            if GUILayout.Toggle(gd.tempTrigger.options["clickTarget"] == "exd", "Except selected colliders" if len(gd.tempTrigger.options["colliders"]) else "Except any colliders"):
                gd.tempTrigger.options["clickTarget"] = "exd"
            GUILayout.EndHorizontal()
            # target collider
            GUILayout.BeginHorizontal()
            GUILayout.Label("Colliders:", GUILayout.Width(tWidth))
            if GUILayout.Button("Cancel" if gd.tempSelectCollider else "Add collider"):
                gd.tempSelectCollider = not gd.tempSelectCollider
            if gd.tempSelectCollider and GUILayout.Button("All"):
                gd.tempTrigger.options["colliders"] = list([cld.id for cld in vnphy.colliders])
                gd.tempSelectCollider = False
            GUILayout.FlexibleSpace()
            GUILayout.EndHorizontal()
            if gd.tempSelectCollider:
                # select area
                selCldId = selectCollider()
                if selCldId:
                    gd.tempTrigger.options["colliders"].append(selCldId)
            elif len(gd.tempTrigger.options["colliders"]):
                # added one
                GUILayout.BeginVertical(GUI.skin.box)
                for cldId in gd.tempTrigger.options["colliders"]:
                    GUILayout.BeginHorizontal()
                    if cldId in vnphy.colliderIdDict:
                        cldIdTxt = vnphy.colliderIdDict[cldId].title
                        if gd.tempTrigger.options["clickTarget"] == "any":
                            cldIdTxt = greyText(cldIdTxt)
                        elif gd.tempTrigger.options["clickTarget"] == "sel":
                            cldIdTxt = greenText(cldIdTxt)
                        elif gd.tempTrigger.options["clickTarget"] == "exd":
                            cldIdTxt = yellowText(cldIdTxt)
                    else:
                        cldIdTxt = redText("[Missing] " + cldId)
                    GUILayout.Label(cldIdTxt)
                    GUILayout.FlexibleSpace()
                    GUI.color = Color.red
                    if GUILayout.Button("X"):
                        gd.tempTrigger.options["colliders"].remove(cldId)
                    GUI.color = oldColor
                    GUILayout.EndHorizontal()
                GUILayout.EndVertical()
        elif gd.tempTrigger.type == "Collision":    # collision type
            # Event type
            GUILayout.BeginHorizontal()
            GUILayout.Label("Event:", GUILayout.Width(tWidth))
            for tet in gd.tempTrigger.TRIGGER_EVENT_TYPES:
                if GUILayout.Button(greenText(tet) if tet == gd.tempTrigger.options["event"] else tet):
                    gd.tempTrigger.options["event"] = tet
            GUILayout.FlexibleSpace()
            GUILayout.EndHorizontal()
            # Event Timing
            GUILayout.BeginHorizontal()
            GUILayout.Label("Timing:", GUILayout.Width(tWidth))
            for ttt in gd.tempTrigger.TRIGGER_TIMING_TYPES:
                if GUILayout.Button(greenText(ttt) if ttt == gd.tempTrigger.options["timing"] else ttt):
                    gd.tempTrigger.options["timing"] = ttt
            GUILayout.FlexibleSpace()
            GUILayout.EndHorizontal()
            # collider
            GUILayout.BeginHorizontal()
            GUILayout.Label("Collider:", GUILayout.Width(tWidth))
            if gd.tempSelectCollider:
                btnTxt = "Cancel"
            elif len(gd.tempTrigger.options["colliders"]):
                cldId = gd.tempTrigger.options["colliders"][0]
                if cldId in vnphy.colliderIdDict:
                    btnTxt = greenText(vnphy.colliderIdDict[cldId].title)
                else:
                    btnTxt = redText("[Missing] " + cldId)
            else:
                btnTxt = yellowText("Select collider")
            if GUILayout.Button(btnTxt):
                gd.tempSelectCollider = not gd.tempSelectCollider
            GUILayout.FlexibleSpace()
            GUILayout.EndHorizontal()
            if gd.tempSelectCollider:
                # select area
                selCldId = selectCollider()
                if selCldId:
                    gd.tempTrigger.options["colliders"] = [selCldId]
        elif gd.tempTrigger.type == "Input":
            # Key Timing
            GUILayout.BeginHorizontal()
            GUILayout.Label("Timing:", GUILayout.Width(tWidth))
            for tit in gd.tempTrigger.TRIGGER_INPUT_TYPES:
                if GUILayout.Button(greenText(tit) if tit == gd.tempTrigger.options["timing"] else tit):
                    gd.tempTrigger.options["timing"] = tit
            GUILayout.FlexibleSpace()
            GUILayout.EndHorizontal()
            # keycodes
            GUILayout.BeginHorizontal()
            GUILayout.Label("Keycode:", GUILayout.Width(tWidth))
            if not vnphy.keycodeDetectMode and GUILayout.Button("Add key/button"):
                vnphy.keycodeDetectMode = True
                gd.tempWaitForKeycode = True
            GUILayout.FlexibleSpace()
            GUILayout.EndHorizontal()
            # add keycode
            if vnphy.keycodeDetectMode:
                GUILayout.BeginHorizontal()
                GUILayout.Label("\npress a key or button\n", GUI.skin.box)
                GUILayout.EndHorizontal()
            else:
                if gd.tempWaitForKeycode:
                    if vnphy.lastDetectedKeycode and vnphy.lastDetectedKeycode not in gd.tempTrigger.options["keycodes"]:
                        gd.tempTrigger.options["keycodes"].append(vnphy.lastDetectedKeycode)
                    else:
                        showMsg(vnphy.game, "Key or button <%s> is already registered in this trigger"%vnphy.lastDetectedKeycode)
                    gd.tempWaitForKeycode = False
                if len(gd.tempTrigger.options["keycodes"]):
                    GUILayout.BeginVertical(GUI.skin.box)
                    lineLength = 0
                    GUILayout.BeginHorizontal()
                    for kc in gd.tempTrigger.options["keycodes"]:
                        btnTitle = str(kc)
                        # line length countup and line change
                        lineLength += len(btnTitle) + 7
                        if lineLength > 40:
                            GUILayout.FlexibleSpace()
                            GUILayout.EndHorizontal()
                            GUILayout.BeginHorizontal()
                            lineLength = 0
                        # show keycode in list
                        GUILayout.Label(btnTitle)
                        GUI.color = Color.red
                        if GUILayout.Button("X"):
                            gd.tempTrigger.options["keycodes"].remove(kc)
                        GUI.color = oldColor
                        GUILayout.Space(10)
                    GUILayout.FlexibleSpace()
                    GUILayout.EndHorizontal()
                    GUILayout.EndVertical()
        elif gd.tempTrigger.type == "SceneEvent":
            # Scene event
            GUILayout.BeginHorizontal()
            GUILayout.Label("Event:", GUILayout.Width(tWidth))
            for tst in gd.tempTrigger.TRIGGER_SCENEEVENT_TYPES:
                if GUILayout.Button(greenText(tst) if tst == gd.tempTrigger.options["event"] else tst):
                    gd.tempTrigger.options["event"] = tst
            GUILayout.FlexibleSpace()
            GUILayout.EndHorizontal()
        elif gd.tempTrigger.type == "Manual":
            pass
        else:
            raise Exception("Unknown trigger type")
        
        # actions
        GUILayout.BeginHorizontal()
        GUILayout.Label("Actions:", GUILayout.Width(tWidth))
        if GUILayout.Button("Cancel" if gd.tempSelectAction else "Add action"):
            gd.tempSelectAction = not gd.tempSelectAction
        GUILayout.FlexibleSpace()
        GUILayout.EndHorizontal()
        if gd.tempSelectAction:
            # select area
            selActId = selectAction()
            if selActId:
                gd.tempTrigger.actions.append(selActId)
        elif len(gd.tempTrigger.actions):
            # added one
            GUILayout.BeginVertical(GUI.skin.box)
            for actId in gd.tempTrigger.actions:
                idx = gd.tempTrigger.actions.index(actId)
                canUp = idx > 0
                canDown = idx < len(gd.tempTrigger.actions) - 1
                GUILayout.BeginHorizontal()
                if actId in vnphy.actionIdDict:
                    GUILayout.Label(greenText(vnphy.actionIdDict[actId].title))
                else:
                    GUILayout.Label(redText("[Missing] " + actId))
                GUILayout.FlexibleSpace()
                if GUILayout.Button(u"\u2191" if canUp else greyText(u"\u2191")) and canUp:
                    gd.tempTrigger.actions[idx - 1], gd.tempTrigger.actions[idx] = gd.tempTrigger.actions[idx], gd.tempTrigger.actions[idx - 1]
                if GUILayout.Button(u"\u2193" if canDown else greyText(u"\u2193")) and canDown:
                    gd.tempTrigger.actions[idx + 1], gd.tempTrigger.actions[idx] = gd.tempTrigger.actions[idx], gd.tempTrigger.actions[idx + 1]
                GUI.color = Color.red
                if GUILayout.Button("X"):
                    gd.tempTrigger.actions.remove(actId)
                GUI.color = oldColor
                GUILayout.EndHorizontal()
            GUILayout.EndVertical()
    else:
        GUILayout.BeginHorizontal()
        GUILayout.Label(" ", GUILayout.Width(tWidth))
        GUILayout.Label(yellowText("Set trigger type please"))
        GUILayout.EndHorizontal()

    # gui total end
    GUILayout.EndScrollView()
    
    # buttons
    GUILayout.FlexibleSpace()
    GUILayout.BeginHorizontal()
    if GUILayout.Button(mode):
        if mode == "Create" and gd.tempTrigger.id in vnphy.triggerIdDict:
            showMsg(vnphy.game, "Invalid trigger setting: ID <%s> already existed"%gd.tempTrigger.id)
        else:
            chk = gd.tempTrigger.checkSetting()
            if chk:
                showMsg(vnphy.game, "Invalid trigger setting: " + chk)
            else:
                vnphy.registerTrigger(gd.tempTrigger)
                clearAndQuit()
    if GUILayout.Button("Cancel"):
        clearAndQuit()
    GUILayout.EndHorizontal()

# action tab
def vnphy_main_action_tab(gd, vnphy):

    if gd.actionMode == "list":
        vnphy_action_tab_list(gd, vnphy)
    elif gd.actionMode == "add":
        vnphy_action_tab_edit(gd, vnphy, "Create")
    elif gd.actionMode == "edit":
        vnphy_action_tab_edit(gd, vnphy, "Update")
    elif gd.actionMode == "delete":
        vnphy_action_tab_list(gd, vnphy)
    else:
        gd.actionMode = "list"

def vnphy_action_tab_list(gd, vnphy):
    oldColor = GUI.color
    tWidth = 40

    def getNewId():
        from vnframe import verifyTagName
        i = 1
        while True:
            newId = "vpa%d"%i
            if verifyTagName(newId):
                i += 1
            else:
                return newId
    def scriptPreview(orgScript):
        lineLimit = 5
        charLimit = 30
        previewLine = []
        moreLine = 0
        for line in orgScript.splitlines():
            if len(line.strip()) == 0:
                continue
            if len(previewLine) < lineLimit:
                if len(line) > charLimit:
                    previewLine.append(line[:charLimit] + yellowText(" ..."))
                else:
                    previewLine.append(line)
            else:
                moreLine += 1
        preview = "\n".join(previewLine)
        if moreLine:
            preview += yellowText("\n  -- and %d more lines --"%moreLine)
        return preview
    
    # list
    gd.actionListScroll = GUILayout.BeginScrollView(gd.actionListScroll)
    for pa in vnphy.actions:
        paAllowed = pa.type == "VNSS" or pa.SCENE_PYTHON_ALLOWED
        if gd.currentAction == pa:
            GUILayout.BeginVertical(GUI.skin.box)
            if gd.actionMode == "list":
                # id
                GUILayout.BeginHorizontal()
                if GUILayout.Button("ID:", GUI.skin.label, GUILayout.Width(tWidth)):
                    gd.currentAction = None
                if GUILayout.Button(magentaText(pa.id) if pa in vnphy.flashingObjects else greenText(pa.id), GUI.skin.label):
                    gd.currentAction = None
                GUILayout.Label(yellowText(pa.type), GUILayout.ExpandWidth(False))
                GUILayout.EndHorizontal()
                # desp
                GUILayout.BeginHorizontal()
                GUILayout.Label("Desp:", GUILayout.Width(tWidth))
                pa.desp = GUILayout.TextField(pa.desp)
                GUILayout.EndHorizontal()
                # script, readonly
                #GUILayout.Label("Script:", GUILayout.Width(tWidth))
                GUILayout.Label(scriptPreview(pa.script))
                # buttons
                GUILayout.BeginHorizontal()
                if GUILayout.Button(" Edit "):
                    gd.tempAction = pa.copy()
                    gd.actionMode = "edit"
                if GUILayout.Button(" Copy "):
                    gd.tempAction = pa.copy()
                    gd.tempAction.id = getNewId()
                    gd.actionMode = "add"
                if GUILayout.Button(" Run "):
                    if paAllowed:
                        ci = {"caller": "vnphy_action_list"}
                        vnphy.runAction(pa.id, ci)
                    else:
                        showMsg(vnphy.game, "Python script not allowed! Check setting(...)->Security")
                GUI.color = Color.red
                if GUILayout.Button("Delete"):
                    gd.actionMode = "delete"
                GUI.color = oldColor
                GUILayout.EndHorizontal()
            else:
                GUILayout.Label(yellowText("Delete this action?"))
                GUILayout.BeginHorizontal()
                GUILayout.FlexibleSpace()
                GUILayout.Label("%s: %s"%(greenText(pa.id), pa.desp))
                GUILayout.FlexibleSpace()
                GUILayout.EndHorizontal()
                GUILayout.Space(5)
                GUILayout.BeginHorizontal()
                GUI.color = Color.red
                if GUILayout.Button("Delete"):
                    vnphy.deleteAction(pa)
                    if vnphy.linkedPyFilepath:
                        vnphy_initialize_linkfile(vnphy, vnphy.linkedPyFilepath, {pa.id})
                    gd.actionMode = "list"
                GUI.color = oldColor
                if GUILayout.Button("Cancel"):
                    gd.actionMode = "list"
                GUILayout.EndHorizontal()
            # end
            GUILayout.EndVertical()
        else:
            GUILayout.BeginHorizontal(GUI.skin.box)
            pa.enable = GUILayout.Toggle(pa.enable, "", GUILayout.ExpandWidth(False))
            if pa in vnphy.flashingObjects:
                title = magentaText(pa.title)
            elif pa.enable:
                title = greenText(pa.id) + ": " + pa.desp
            else:
                title = greyText(pa.title)
            if GUILayout.Button(title, GUI.skin.label):
                gd.currentAction = pa
            if GUILayout.Button(greenText(u'\u25b6'), GUILayout.ExpandWidth(False)):
                if paAllowed:
                    ci = {"caller": "vnphy_action_list"}
                    vnphy.runAction(pa.id, ci)
                else:
                    showMsg(vnphy.game, "Python script not allowed! Check setting(...)->Security")
            GUILayout.EndHorizontal()
    GUILayout.EndScrollView()

    # buttons
    GUILayout.FlexibleSpace()
    GUILayout.BeginHorizontal()
    if GUILayout.Button("Add new action"):
        from vnphysics_core import VNAction
        gd.tempAction = VNAction()
        gd.tempAction.id = getNewId()
        gd.actionMode = "add"
    if GUILayout.Button("...", GUILayout.ExpandWidth(False)):
        vnphy_setting_GUI(gd, vnphy)
    GUILayout.EndHorizontal()

def vnphy_action_tab_edit(gd, vnphy, mode):
    oldColor = GUI.color
    tWidth = 40
    editorStyle = GUIStyle("textArea")
    editorStyle.wordWrap = False
    editorStyle.stretchHeight = True

    if not hasattr(gd.tempAction, "vnssDispMode"):
        gd.tempAction.vnssDispMode = "txt"
        gd.tempAction.vnssCommands = None
        gd.tempAction.backupScript = gd.tempAction.script

    # id
    GUILayout.BeginHorizontal()
    GUILayout.Label("ID:", GUILayout.Width(tWidth))
    if mode == "Create":
        gd.tempAction.id = GUILayout.TextField(gd.tempAction.id, GUILayout.Width(100))
    else:
        GUILayout.Label(gd.tempAction.id)
    GUILayout.EndHorizontal()
    # desp
    GUILayout.BeginHorizontal()
    GUILayout.Label("Desp:", GUILayout.Width(tWidth))
    gd.tempAction.desp = GUILayout.TextField(gd.tempAction.desp)
    GUILayout.EndHorizontal()
    # type
    GUILayout.BeginHorizontal()
    GUILayout.Label("Type:", GUILayout.Width(tWidth))
    if mode == "Create":
        for vtype in gd.tempAction.ACTION_TYPES:
            GUI.color = Color.green if gd.tempAction.type == vtype else oldColor
            if GUILayout.Button(vtype):
                gd.tempAction.type = vtype
                gd.tempAction.vnssDispMode = "txt"
                gd.tempAction.vnssCommands = None
    else:
        GUI.color = Color.yellow
        GUILayout.Label(gd.tempAction.type)
    GUI.color = oldColor
    GUILayout.EndHorizontal()

    if gd.tempAction.type:
        isWizardOn = hasattr(gd, "wizardData")
        # script title
        GUILayout.BeginHorizontal()
        GUILayout.Label("Script:", GUILayout.Width(tWidth))
        if gd.tempAction.type == "VNSS":
            # VNSS, show wizard button
            if isWizardOn:
                GUI.color = Color.green
                if GUILayout.Button("VNSS Wizard"):
                    vnphy_vnss_wizard_close(vnphy.game, gd)
                GUI.color = oldColor
            else:
                if GUILayout.Button("VNSS Wizard"):
                    vnphy_vnss_wizard_open(vnphy.game, gd)
            #GUILayout.FlexibleSpace()
            if gd.tempAction.vnssDispMode == "txt":
                if GUILayout.Button("TXT MODE", GUILayout.ExpandWidth(False)):
                    from scenesavestate_vnssext import analysis_vnss_cmd
                    gd.tempAction.vnssDispMode = "cmd"
                    gd.tempAction.vnssCommands = analysis_vnss_cmd(gd.tempAction.script)
            else:
                if GUILayout.Button(yellowText("CMD MODE"), GUILayout.ExpandWidth(False)):
                    gd.tempAction.vnssDispMode = "txt"
                    gd.tempAction.vnssCommands = None
        else:
            # PYTHON, show link info if linked
            if isWizardOn:
                vnphy_vnss_wizard_close(vnphy.game, gd)
            if vnphy.linkedPyFilepath != None:
                # link info
                linkFilename = path.split(vnphy.linkedPyFilepath)[1]
                GUILayout.Label("from " + greenText(linkFilename), GUILayout.ExpandWidth(False))
        GUILayout.EndHorizontal()
        # script contents
        gd.actionEditScroll = GUILayout.BeginScrollView(gd.actionEditScroll)
        if gd.tempAction.type == "VNSS" and gd.tempAction.vnssDispMode == "cmd":
            GUILayout.BeginVertical(GUI.skin.box)
            for cmd in gd.tempAction.vnssCommands:
                GUILayout.BeginHorizontal()
                if cmd["error"]:
                    GUILayout.Label(redText(cmd["desp"]))
                else:
                    GUILayout.Label(yellowText(cmd["desp"]))
                GUILayout.FlexibleSpace()
                if GUILayout.Button("<color=#ff0000>X</color>", GUILayout.ExpandWidth(False)):
                    gd.tempAction.vnssCommands.remove(cmd)
                    gd.tempAction.script = "\n".join((c["org"] for c in gd.tempAction.vnssCommands))
                    if isWizardOn:
                        gd.wizardData.cam_addvncmds = gd.tempAction.script
                GUILayout.EndHorizontal()
            GUILayout.EndVertical()
        else:
            gd.tempAction.script = GUILayout.TextArea(gd.tempAction.script, editorStyle)#, GUILayout.MaxHeight(200))
        # test result
        if hasattr(gd, "testResult"):
            GUILayout.BeginHorizontal()
            GUILayout.Label("Test result: ")
            if GUILayout.Button("Clear"):
                del gd.testResult
            GUILayout.FlexibleSpace()
            GUILayout.EndHorizontal()
        if hasattr(gd, "testResult"):
            GUILayout.Label(gd.testResult)
        GUILayout.EndScrollView()
    else:
        GUILayout.BeginHorizontal()
        GUILayout.Label(" ", GUILayout.Width(tWidth))
        GUILayout.Label(yellowText("Set type of action"))
        GUILayout.EndHorizontal()
        GUILayout.FlexibleSpace()

    # buttons
    GUILayout.BeginHorizontal()
    if GUILayout.Button(mode):
        chk = gd.tempAction.checkAction(mode == "Create")
        if chk:
            showMsg(vnphy.game, "Invalid action setting: " + chk)
        else:
            # security warning
            if gd.tempAction.type == "PYTHON" and gd.tempAction.SCENE_PYTHON_ALLOWED == False:
                if mode == "Create":
                    from vnphysics_core import VNAction
                    VNAction.SCENE_PYTHON_ALLOWED = True
                    showMsg(vnphy.game, "Python script auto enabled due to you created your own script")
                else:
                    showMsg(vnphy.game, "Python script will NOT run until you allow it in setting(...)->Security")
            # register
            vnphy.registerAction(gd.tempAction)
            # sync to linked file
            if (gd.tempAction.backupScript != gd.tempAction.script or mode == "Create") and vnphy.linkedPyFilepath:
                vnphy_initialize_linkfile(vnphy, vnphy.linkedPyFilepath, {gd.tempAction.id})
            # quit
            del gd.tempAction
            vnphy_vnss_wizard_close(vnphy.game, gd)
            gd.actionMode = "list"
    if GUILayout.Button(" Test "):
        # Test run, action maybe not registered yet
        chk = gd.tempAction.checkAction(mode == "Create")
        if chk:
            showMsg(vnphy.game, "Invalid action setting: " + chk)
        else:
            if gd.tempAction.type == "PYTHON" and gd.tempAction.SCENE_PYTHON_ALLOWED == False:
                showMsg(vnphy.game, "Python script not allowed! Check setting(...)->Security")
            else:
                ci = {"caller": "vnphy_action_edit"}
                res = gd.tempAction.run(vnphy.game, ci, {})
                if res:
                    gd.testResult = redText("ERROR: ") + res
                else:
                    gd.testResult = greenText("PASSED")
    if GUILayout.Button("Cancel"):
        del gd.tempAction
        vnphy_vnss_wizard_close(vnphy.game, gd)
        gd.actionMode = "list"
    GUILayout.EndHorizontal()

# setting window
def vnphy_setting_GUI(gd, vnphy):
    from vnphysics_core import VNAction
    oldColor = GUI.color

    def quitSetting():
        del gd.settingScroll
        del gd.settingLinkFilename

    # switch
    if gd.guiFunction != vnphy_setting_GUI:
        gd.guiFunction = vnphy_setting_GUI
        # do switch init here
        gd.settingScroll = Vector2.zero
        gd.settingLinkFilename = path.split(vnphy.linkedPyFilepath)[1] if vnphy.linkedPyFilepath != None else ""
        gd.settingPrompt = None
        return

    # Settings
    gd.settingScroll = GUILayout.BeginScrollView(gd.settingScroll)
    # save/load
    GUILayout.BeginHorizontal()
    GUILayout.FlexibleSpace()
    GUILayout.Label("---- SAVE / LOAD ----")
    GUILayout.FlexibleSpace()
    GUILayout.EndHorizontal()
    if gd.settingPrompt == "Save":
        GUILayout.Label("Save data to scene, and you have to save the scene (PNG) to keep data permanently. When you save scene (PNG), data is auto saved. So no need to save data manually except you want to keep current data as a recover point.")
        GUILayout.BeginHorizontal()
        if GUILayout.Button("  OK  "):
            vnphy.saveData()
            gd.settingPrompt = None
        if GUILayout.Button("Cancel"):
            gd.settingPrompt = None
        GUILayout.EndHorizontal()
    elif gd.settingPrompt == "Load":
        GUILayout.Label("Load data from scene, not saved data will lost!")
        GUILayout.BeginHorizontal()
        if GUILayout.Button("  OK  "):
            vnphy.loadData()
            gd.settingPrompt = None
        if GUILayout.Button("Cancel"):
            gd.settingPrompt = None
        GUILayout.EndHorizontal()
    elif gd.settingPrompt == "Reset":
        GUI.color = Color.yellow
        GUILayout.Label("Sure to delete all colliders, rigedbodies, triggers and actions?")
        GUILayout.BeginHorizontal()
        GUI.color = Color.red
        if GUILayout.Button("  OK  "):
            vnphy.cleanup()
            gd.settingPrompt = None
        GUI.color = oldColor
        if GUILayout.Button("Cancel"):
            gd.settingPrompt = None
        GUILayout.EndHorizontal()
    else:
        GUILayout.BeginHorizontal()
        if GUILayout.Button("Save"):
            gd.settingPrompt = "Save"
        if GUILayout.Button("Load"):
            gd.settingPrompt = "Load"
        GUI.color = Color.red
        if GUILayout.Button("Reset"):
            gd.settingPrompt = "Reset"            
        GUI.color = oldColor
        GUILayout.EndHorizontal()

    GUILayout.Space(10)
    # debug infomation
    GUILayout.BeginHorizontal()
    GUILayout.FlexibleSpace()
    GUILayout.Label("---- DEBUG INFO ----")
    GUILayout.FlexibleSpace()
    GUILayout.EndHorizontal()
    gd.setting.DEBUG_SHOW_LAST_HITINFO = GUILayout.Toggle(gd.setting.DEBUG_SHOW_LAST_HITINFO, "Show clicked collider infomation")
    gd.setting.DEBUG_SHOW_COLLIDER_BOUNDS = GUILayout.Toggle(gd.setting.DEBUG_SHOW_COLLIDER_BOUNDS, "Show collider bounds info")
    gd.setting.DEBUG_SHOW_RIGIDBODY_INFO = GUILayout.Toggle(gd.setting.DEBUG_SHOW_RIGIDBODY_INFO, "Show rigidbody status info")
    gd.setting.DEBUG_SHOW_RESET_BUTTON = GUILayout.Toggle(gd.setting.DEBUG_SHOW_RESET_BUTTON, "Show module reload button")
    VNAction.DEBUG_PRINT_RUN_LOG = gd.setting.DEBUG_PRINT_RUN_LOG = GUILayout.Toggle(gd.setting.DEBUG_PRINT_RUN_LOG, "[Console] Print action log")
    VNAction.DEBUG_PRINT_CALLER_INFO = gd.setting.DEBUG_PRINT_CALLER_INFO = GUILayout.Toggle(gd.setting.DEBUG_PRINT_CALLER_INFO, "[Console] Print caller info")
    VNAction.DEBUG_PRINT_INPUT_PARAM = gd.setting.DEBUG_PRINT_INPUT_PARAM = GUILayout.Toggle(gd.setting.DEBUG_PRINT_INPUT_PARAM, "[Console] Print input param")

    GUILayout.Space(10)
    # security infomation
    GUILayout.BeginHorizontal()
    GUILayout.FlexibleSpace()
    GUILayout.Label("---- SECURITY ----")
    GUILayout.FlexibleSpace()
    GUILayout.EndHorizontal()
    VNAction.SCENE_PYTHON_ALLOWED = GUILayout.Toggle(VNAction.SCENE_PYTHON_ALLOWED, yellowText("Allow python type action"))
    gd.setting.SECURITY_TRUST_MY_SAVE = GUILayout.Toggle(gd.setting.SECURITY_TRUST_MY_SAVE, "Trust scene saved by myself")


    GUILayout.Space(10)
    # editor helper infomation
    GUILayout.BeginHorizontal()
    GUILayout.FlexibleSpace()
    GUILayout.Label("---- EDITOR HELPER ----")
    GUILayout.FlexibleSpace()
    GUILayout.EndHorizontal()
    GUILayout.BeginHorizontal()
    GUILayout.Label("Link py file: ", GUILayout.ExpandWidth(False))
    if vnphy.linkedPyFilepath == None:
        # link-up
        gd.settingLinkFilename = GUILayout.TextField(gd.settingLinkFilename)
        if GUILayout.Button("Link", GUILayout.ExpandWidth(False)) and len(gd.settingLinkFilename):
            vnphy_attach_linkfile(vnphy, gd.settingLinkFilename)
    else:
        # unlink
        GUILayout.Label(greenText(path.split(vnphy.linkedPyFilepath)[1]), GUILayout.ExpandWidth(False))
        if GUILayout.Button("Unlink", GUILayout.ExpandWidth(False)):
            vnphy.linkedPyFilepath = None
    GUILayout.EndHorizontal()

    #GUILayout.BeginHorizontal()
    #GUILayout.EndHorizontal()

    GUILayout.EndScrollView()

    # Control button
    GUILayout.FlexibleSpace()
    GUILayout.BeginHorizontal()
    if GUILayout.Button("Back"):
        quitSetting()
        vnphy_main_GUI(gd, vnphy)
    GUILayout.EndHorizontal()

# py file link
def vnphy_attach_linkfile(vnphy, linkFilename):
    try:
        if not linkFilename.lower().endswith(".py"):
            linkFilename += ".py"
        basePath = path.split(path.abspath(__file__))[0]
        basePath = path.join(basePath, "vnphydata")
        linkFilePath = path.join(basePath, linkFilename)
        
        # check is linkfile match current vnphy, but
        # if link file not exist -> match
        # if vnaction not exist in link file -> match
        NonMatchIds = []
        if path.exists(linkFilePath):
            loadActions = vnphy_load_linkfile(linkFilePath)
            for actId in vnphy.actionIdDict.keys():
                if actId in loadActions:
                    if vnphy.actionIdDict[actId].script != loadActions[actId].script:
                        NonMatchIds.append(actId)

        # compare
        sw_width = 700
        sw_height = 300

        # init gui data
        gd = GData()
        gd.vnphy = vnphy
        gd.game = vnphy.game
        gd.filename = linkFilename
        gd.filepath = linkFilePath
        gd.nonMatchIds = NonMatchIds

        # skin setup func
        def skin_setup(g):
            g.windowName = "VNPHY External File Link"
            g.windowRect = Rect((Screen.width - sw_width)/2, (Screen.height - sw_height)/2, sw_width, sw_height)
            g.windowStyle = g.windowStyleDefault

        from skin_customwindow import SkinCustomWindow
        skin = SkinCustomWindow()
        skin.funcSetup = skin_setup
        skin.funcWindowGUI = vnphy_attach_linkfile_GUI
        skin.guidata = gd

        # open sub-window
        gd.wndId = gd.game.new_extra_window_skin(skin)

    except Exception as e:
        traceback.print_exc()
        from vngameengine import vnge_game as game
        showMsg(game, "Fail to link py file: " + str(e))
        return None

def vnphy_attach_linkfile_GUI(baseController, windowid):
    gd = baseController.skin.guidata

    def closeWarning():
        # close self
        if gd.wndId != windowid:
            print("Mmmmm.... windowid parameter not equal to winId saved in guiData...")
        gd.game.close_extra_window(gd.wndId)

    try:
        lls = GUIStyle("label")
        lls.fontSize = 18
        lbs = GUIStyle("button")
        lbs.fontSize = 18
        GUILayout.Label("Link a external py file help you edit python code easily. You can do coding in external editor, then save the file and ALL actions in this scene will be updated.", lls)
        GUILayout.Label("Link info will NOT be saved in scene, after code updated into scene, you can unlink and delete external py file.", lls)
        if len(gd.nonMatchIds) > 0:
            GUILayout.Label(yellowText("Following action existed in py file and not match with current data:"), lls)
            GUILayout.Label(yellowText("  " + ", ".join(gd.nonMatchIds)), lls)
            GUILayout.Label(yellowText("Continue linking will overwrite current data!"), lls)
        GUILayout.FlexibleSpace()
        GUILayout.BeginHorizontal()
        if GUILayout.Button("Link with " + greenText(gd.filename), lbs):
            gd.vnphy.linkedPyFilepath = vnphy_initialize_linkfile(gd.vnphy, gd.filepath, set())
            closeWarning()
        if GUILayout.Button("Cancel", lbs):
            gd.vnphy.linkedPyFilepath = None
            closeWarning()
        GUILayout.EndHorizontal()
    except:
        traceback.print_exc()

def vnphy_initialize_linkfile(vnphy, linkFilePath, updateToFile):
    BEGINOFFILE = "#!!! DO NOT EDIT THIS LINE !!! VNPHY EXTERNAL LINK FILE"
    BEGINOFVNSS = "#!!! DO NOT EDIT THIS LINE !!! VNPHY VNSS ACTION: "
    BEGINOFPYTHON = "#!!! DO NOT EDIT THIS LINE !!! VNPHY PYTHON ACTION: "
    try:
        # load all script in file
        if path.exists(linkFilePath):
            loadActions = vnphy_load_linkfile(linkFilePath)
        else:
            loadActions = {}

        # append new action from vnphy
        aids = []
        for act in vnphy.actions:
            aids.append(act.id)
            if act.id not in loadActions or act.id in updateToFile:
                loadActions[act.id] = act
        for aid in loadActions.keys():
            if aid not in vnphy.actionIdDict and aid not in updateToFile:
                aids.append(aid)

        # create new contents
        lfmk = [
            BEGINOFFILE,
            "",
        ]
        for aid in aids:
            act = loadActions[aid]
            if act.type == "VNSS":
                lfmk.append(BEGINOFVNSS + aid)
            else:
                lfmk.append(BEGINOFPYTHON + aid)
            lfmk.append("# " + act.desp)
            lfmk.extend(act.script.split("\n"))
            lfmk.append("")

        # update link file
        lf = open(linkFilePath, "w")
        for ln in lfmk:
            lf.write(ln + "\n")
        lf.close()

        vnphy_update_from_linkfile(vnphy, True)
        return linkFilePath
    
    except:
        traceback.print_exc()
        return None

def vnphy_load_linkfile(linkFilePath):
    BEGINOFFILE = "#!!! DO NOT EDIT THIS LINE !!! VNPHY EXTERNAL LINK FILE"
    BEGINOFVNSS = "#!!! DO NOT EDIT THIS LINE !!! VNPHY VNSS ACTION: "
    BEGINOFPYTHON = "#!!! DO NOT EDIT THIS LINE !!! VNPHY PYTHON ACTION: "
    try:
        lf = open(linkFilePath, "r")
        lines = lf.readlines()
        lf.close()

        fileMarkFound = False
        activeId = None
        loadIds = []
        loadTypes = {}
        loadScripts = {}
        for line in lines:
            line = line.strip("\n")
            if not fileMarkFound:
                if line == BEGINOFFILE:
                    fileMarkFound = True
            else:
                if line.startswith(BEGINOFVNSS):
                    activeId = line[len(BEGINOFVNSS):].strip()
                    loadIds.append(activeId)
                    loadTypes[activeId] = "VNSS"
                    loadScripts[activeId] = []
                elif line.startswith(BEGINOFPYTHON):
                    activeId = line[len(BEGINOFPYTHON):].strip()
                    loadIds.append(activeId)
                    loadTypes[activeId] = "PYTHON"
                    loadScripts[activeId] = []
                elif activeId != None and activeId in loadScripts:
                    loadScripts[activeId].append(line)
                else:
                    pass

        loadActions = {}
        from vnphysics_core import VNAction
        for aid in loadIds:
            act = VNAction()
            act.id = aid
            if len(loadScripts[aid]) > 0 and len(loadScripts[aid][0]) > 0 and loadScripts[aid][0][0] == "#":
                act.desp = loadScripts[aid][0][1:].strip()
                sfrom = 1
            else:
                act.desp = ""
                sfrom = 0
            act.type = loadTypes[aid]
            act.script = "\n".join(loadScripts[aid][sfrom:]).strip()
            loadActions[aid] = act

        return loadActions
    except:
        traceback.print_exc()
        return None

def vnphy_update_from_linkfile(vnphy, force):
    if vnphy.linkedPyFilepath == None or not path.exists(vnphy.linkedPyFilepath):
        vnphy.linkedPyFilepath = None
        return
    if not hasattr(vnphy, "lastLinkFileUpdate"):
        force = True
    if not force:
        needUpd = vnphy.lastLinkFileUpdate < path.getmtime(vnphy.linkedPyFilepath)
    else:
        needUpd = True
    if not needUpd:
        return
    
    # do sync
    loadActions = vnphy_load_linkfile(vnphy.linkedPyFilepath)
    for act in vnphy.actions:
        if act.id in loadActions and act.type == loadActions[act.id].type:
            act.desp = loadActions[act.id].desp
            act.script = loadActions[act.id].script
    for aid in loadActions.keys():
        if aid not in vnphy.actionIdDict:
            vnphy.registerAction(loadActions[aid])
            showMsg(vnphy.game, "Load %s from file"%loadActions[aid])
    vnphy.lastLinkFileUpdate = path.getmtime(vnphy.linkedPyFilepath)

    # done
    import time
    showMsg(vnphy.game, "Update script from %s @ %s"%(path.split(vnphy.linkedPyFilepath)[1], time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(vnphy.lastLinkFileUpdate))))

# vnss wizard window
def vnphy_vnss_wizard_open(game, gd):
    # open vnss wizard dialog
    ww_width = 500
    ww_height = 500

    # skin setup func
    def skin_setup(g):
        g.windowName = "VNSS WIZARD"
        g.windowRect = Rect((Screen.width - ww_width)/2, (Screen.height - ww_height)/2, ww_width, ww_height)
        g.windowStyle = g.windowStyleDefault

    # create dummy scene block
    def dummy_scene_block(game):
        dummyScene = GData()
        dummyScene.actors = {}
        for aid in game.scenef_get_all_actors().keys():
            dummyScene.actors[aid] = {}
        dummyScene.props = {}
        for pid in game.scenef_get_all_props().keys():
            dummyScene.props[pid] = {}
        return dummyScene

    from skin_customwindow import SkinCustomWindow
    skin = SkinCustomWindow()
    skin.funcSetup = skin_setup
    skin.funcWindowGUI = vnphy_vnss_wizard_GUI
    skin.guidata = gd

    # open sub-window
    gd.wizardData = GData()
    gd.wizardData.game = game
    gd.wizardData.wizScroll = Vector2.zero
    gd.wizardData.vnss_wizard_ui_mode = "main"
    gd.wizardData.wiz_step = 1
    gd.wizardData.cam_addvncmds = gd.tempAction.script
    gd.wizardData.block = [dummy_scene_block(game)]
    gd.wizardData.cur_index = 0
    gd.wizardData.wndId = game.new_extra_window_skin(skin)

def vnphy_vnss_wizard_close(game, gd):
    # close vnss wizard dialog
    if hasattr(gd, "wizardData"):
        game.close_extra_window(gd.wizardData.wndId)
        del gd.wizardData

def vnphy_vnss_wizard_GUI(baseController, windowid):
    gd = baseController.skin.guidata
    game = gd.wizardData.game

    try:
        from scenesavestate_vnssext import render_vnss_wizard, analysis_vnss_cmd, render_vnframe_manual, render_vnframe_manual_options
        if gd.wizardData.vnss_wizard_ui_mode == "main":
            # render wizard
            gd.wizardData.wizScroll = GUILayout.BeginScrollView(gd.wizardData.wizScroll)
            ret = render_vnss_wizard(gd.wizardData)
            if gd.wizardData.wiz_step == 0:
                # start from expanded menu
                gd.wizardData.wiz_step = 1
            if gd.wizardData.wiz_step == 20:
                # show a note for vnframe ext
                GUILayout.Space(10)
                GUILayout.Label(yellowText("*** NOTE for VNFrame Ext in vnPHY Action ***"))
                GUILayout.Label("VNFrame ext in PHY Action CANNOT check the diff status because there is no 'prev status' to diff with. Instead, the 'snapshot' will capture all status of all tracked actors and props. If you want to set just some properties of specified actor/prop, please use 'Manual mode' and select what you want.")
            GUILayout.EndScrollView()
            if hasattr(gd, "tempAction"):
                if gd.tempAction.script != gd.wizardData.cam_addvncmds:
                    gd.tempAction.script = gd.wizardData.cam_addvncmds
                    gd.tempAction.vnssCommands = analysis_vnss_cmd(gd.tempAction.script)
            else:
                ret = False
            if ret == False:
                vnphy_vnss_wizard_close(game, gd)

        elif gd.wizardData.vnss_wizard_ui_mode == "vnframe_manual":
            render_vnframe_manual(gd.wizardData)
        elif gd.wizardData.vnss_wizard_ui_mode == "vnframe_manual_options":
            render_vnframe_manual_options(gd.wizardData)
        else:
            print("Unknown vnss_wizard_ui_mode: " + str(gd.wizardData.vnss_wizard_ui_mode))
            gd.wizardData.vnss_wizard_ui_mode = "main"
    except:
        vnphy_vnss_wizard_close(game, gd)
        traceback.print_exc()

# security window
def vnphy_security_warning(game, filename):
    sw_width = 700
    sw_height = 300

    # init gui data
    gd = GData()
    gd.game = game
    gd.vnphy = game.gdata.vnphy
    gd.filename = filename

    # skin setup func
    def skin_setup(g):
        g.windowName = "VNPHY Security Warning"
        g.windowRect = Rect((Screen.width - sw_width)/2, (Screen.height - sw_height)/2, sw_width, sw_height)
        g.windowStyle = g.windowStyleDefault

    from skin_customwindow import SkinCustomWindow
    skin = SkinCustomWindow()
    skin.funcSetup = skin_setup
    skin.funcWindowGUI = vnphy_security_warning_GUI
    skin.guidata = gd

    # open sub-window
    gd.wndId = game.new_extra_window_skin(skin)

def vnphy_security_warning_GUI(baseController, windowid):
    gd = baseController.skin.guidata

    def closeWarning(allow):
        # allow flag
        from vnphysics_core import VNAction
        VNAction.SCENE_PYTHON_ALLOWED = allow
        # close self
        if gd.wndId != windowid:
            print("Mmmmm.... windowid parameter not equal to winId saved in guiData...")
        gd.game.close_extra_window(gd.wndId)

    try:
        lls = GUIStyle("label")
        lls.fontSize = 18
        lbs = GUIStyle("button")
        lbs.fontSize = 18
        GUILayout.Label(yellowText("Python script found in this scene! Do you want to allow python script execution?"), lls)
        GUILayout.Label(yellowText("ALL ON YOUR OWN RISK!"), lls)
        GUILayout.Label("If you want to disable security warning and run Python from any scene data, set AllowScenePython=1 in vngameengine.ini. NOT RECOMMENDED!", lls)
        GUILayout.FlexibleSpace()
        GUILayout.BeginHorizontal()
        if GUILayout.Button("NOT Allow", lbs):
            closeWarning(False)
        if GUILayout.Button("Allow Once", lbs):
            closeWarning(True)
        if GUILayout.Button("Allow and ALWAYS trust this scene", lbs):
            closeWarning(True)
            vnphy_trust_file(gd.game, gd.filename)
        GUILayout.EndHorizontal()
    except:
        traceback.print_exc()

def vnphy_security_check(vnphy, filename):
    from vnphysics_core import VNAction
    globalAllow = vnphy.game.engineOptions.get("allowscenepython") == "1"
    VNAction.SCENE_PYTHON_ALLOWED = globalAllow
    if not globalAllow and vnphy.hasScenePython:
        # check if is trusted
        isTrusted = vnphy_check_trustlist(vnphy.game, filename)
        if isTrusted:
            # free to go
            VNAction.SCENE_PYTHON_ALLOWED = True
        else:
            # prompt user
            vnphy_security_warning(vnphy.game, filename)

def vnphy_security_save(vnphy, filename):
    def wait_and_trust(game, param):
        if param[0] > 0:
            param[0] -= 1
            return False
        else:
            filename = param[1]
            vnphy_trust_file(game, filename)
            return True

    from vnphysics_core import VNAction
    if vnphy.hasScenePython and VNAction.SCENE_PYTHON_ALLOWED and vnphy.game.gdata.vnphySetting.SECURITY_TRUST_MY_SAVE:
        vnphy.game.append_update_job(wait_and_trust, [30, filename])

def vnphy_check_trustlist(game, filename):
    filepath = path.abspath(filename)
    filename = path.split(filepath)[1]
    whitelist = game.gdata.vnphySetting.pythonWhitelist
    if filename in whitelist:
        file = open(filepath, "rb")
        fileData = file.read()
        file.close()
        hash_sha256 = hashlib.sha256(fileData).hexdigest()
        return hash_sha256 == whitelist[filename]
    return False

def vnphy_trust_file(game, filename):
    try:
        filepath = path.abspath(filename)
        filename = path.split(filepath)[1]
        whitelist = game.gdata.vnphySetting.pythonWhitelist
        # get file sha256
        file = open(filepath, "rb")
        fileData = file.read()
        file.close()
        hash_sha256 = hashlib.sha256(fileData).hexdigest()
        whitelist[filename] = hash_sha256
        if not saveWhitelist(whitelist):
            showMsg(game, "Fail to save trusted file: error while writing whitelist")
        else:
            showMsg(game, "Trusted file: " + filename)
    except:
        showMsg(game, "Fail to save trusted file: error while get file info")

# ====================================  vnphy help function ====================================
def showMsg(game, msg):
    game.logger_log_message(msg, "VNPHY: ")

def getCurrentSelectedOciObjects(game):
    selObjs = []
    for tno in game.studio.treeNodeCtrl.selectNodes:
        if tno in game.studio.dicInfo:
            selObjs.append(game.studio.dicInfo[tno])
    return selObjs

def getPathOfGameObject(gameObj):
    pathSeg = [gameObj.name]
    while gameObj.transform.parent != None:
        gameObj = gameObj.transform.parent.gameObject
        pathSeg.append(gameObj.name)
    pathSeg.reverse()
    return "\\".join(pathSeg)

# ====================================  general help function ====================================
def colorText(text, color = "ffffff"):
    return "<color=#" + color + ">" + text + "</color>"
def redText(text):
    return colorText(text, "ff0000")
def greenText(text):
    return colorText(text, "00ff00")
def blueText(text):
    return colorText(text, "0000ff")
def yellowText(text):
    return colorText(text, "ffff00")
def orangeText(text):
    return colorText(text, "ffa500")
def cyanText(text):
    return colorText(text, "00ffff")
def magentaText(text):
    return colorText(text, "ff00ff")
def greyText(text):
    return colorText(text, "888888")

