#vngame;all;Utilities/SSS Builder v0.5.1
# this util create SSS data
# by Countd360

from UnityEngine import Quaternion, Vector4, Vector3, Vector2, Color, Rect, GameObject, Transform, Texture2D
from UnityEngine import GUI, GUILayout, GUIStyle, GUIContent, GUIUtility, TextAnchor
from UnityEngine import Time, Screen, Input, KeyCode, Event, EventType
from System import String, Array, Action, GC
from vngameengine import HSNeoOCI, HSNeoOCIItem, HSNeoOCIChar, HSNeoOCIFolder, HSNeoOCILight, HSNeoOCIRoute, HSNeoOCICamera, HSNeoOCIText, GData
from vngameengine import importOrReload, parseKeyCode, BytesPNG2Texture2D, Texture2D2BytesPNG
import copy
import traceback
import codecs
import unity_util

def sssb_toolbar_handle(param1 = "", param2 = ""):
    # function call when toolbar button clicked
    # this should be the main entry of SSSB
    #print("SSSB: toolbar button clicked: p1=%s, p2=%s"%(str(param1), str(param2)))
    from vngameengine import vnge_game as game
    if not hasattr(game.gdata, "sssb") or game.gdata.sssb == None or game.get_extra_window(game.gdata.sssb.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.sssb.wndId)
    if wnd != None:
        # already existed
        return

    # init gui data
    gd = GData()
    gd.guiFunction = None
    gd.wndSizeNormal = Rect(100, 100, 500, 600)
    gd.wndSizeHalf = Rect(100, 100, 205, 600)
    gd.wndSizeMin = Rect(100, 100, 100, 40)
    gd.mainGuiSceneScl = Vector2.zero
    gd.mainGuiNodesScl = Vector2.zero
    gd.expendedNodes = set()
    gd.halfMode = True
    gd.sceneBtnBoxMode = "normal"
    gd.showSavedNodes = True
    gd.showNodeType = True
    gd.rightPanelMode = "nodesList"
    gd.nodeBtnBoxMode = "normal"

    # skin setup func
    def skin_setup(g):
        g.windowName = "SSS Builder v0.5.1"
        g.windowRect = gd.wndSizeHalf
        g.windowStyle = g.windowStyleDefault
        g.minimized = False
    
    # setup skin
    from skin_customwindow import SkinCustomWindow
    skin = SkinCustomWindow()
    skin.funcSetup = skin_setup
    skin.funcWindowGUI = sssb_GUI_subwindow
    skin.metadata = game.gdata.sssb
    skin.guidata = gd

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

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

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

    wnd = game.get_extra_window(game.gdata.sssb.wndId)
    if wnd != None:
        # already existed
        game.close_extra_window(game.gdata.sssb.wndId)
        game.gdata.sssb.wndId = None
        # toolbar sync
        from vngameengine import tlbBtn_UpdateBtnColor, tlbBtn_VNGESSSB
        tlbBtn_UpdateBtnColor(tlbBtn_VNGESSSB, False, game.gdata.sssb.TotalSceneCount > 0)

def initialize(game, force = False):
    if force or not hasattr(game.gdata, "sssb") or game.gdata.sssb == None:
        # init main class
        print("\n\nStart a new SSS Builder application...")
        game.gdata.sssb = SceneSaveStateBuilder(game)

        # install event hook
        game.event_unreg_listener("scene_loaded_after", sssb_hook_loaded_after, "sssb_sceneload")
        game.event_reg_listener("scene_loaded_after", sssb_hook_loaded_after, "sssb_sceneload")
        game.event_unreg_listener("scene_saved", sssb_hook_saved, "sssb_scenesave")
        game.event_reg_listener("scene_saved", sssb_hook_saved, "sssb_scenesave")

    return game.gdata.sssb

def sssb_hook_saved(game, evid, u_param):
    sssb = initialize(game)
    sssb.saveData()

def sssb_hook_loaded_after(game, evid, u_param):
    sssb = initialize(game)
    # load data
    ssc = sssb.checkSceneData()
    if ssc:
        if sssb.TotalSceneCount == 0:
            sssb.loadData()
            sssb.showMsg("%d scenes auto loaded"%ssc)
        else:
            sssb.showMsg("%d scene data found, load or import them from Save/Load menu"%ssc)
    # toolbar sync
    from vngameengine import tlbBtn_UpdateBtnColor, tlbBtn_VNGESSSB
    sssb_visible = game.get_extra_window(sssb.wndId) != None
    tlbBtn_UpdateBtnColor(tlbBtn_VNGESSSB, sssb_visible, game.gdata.sssb.TotalSceneCount > 0)

def sssb_GUI_subwindow(baseController, windowid):
    oldColor = GUI.color

    try:
        guidata = baseController.skin.guidata
        sssb = baseController.skin.metadata
        game = sssb.game

        # resize
        if baseController.minimized:
            sssb_GUI_resize(baseController, guidata.wndSizeMin)
        elif guidata.guiFunction == sssb_main_GUI and guidata.halfMode:
            sssb_GUI_resize(baseController, guidata.wndSizeHalf)
        else:
            sssb_GUI_resize(baseController, guidata.wndSizeNormal)

        # gui function
        if baseController.minimized:
            pass
        elif guidata.guiFunction != None:
            guidata.guiFunction(guidata, sssb)
        else:
            sssb_main_GUI(guidata, sssb)
    except:
        traceback.print_exc()
        close(game)
    
    # minimize button
    #cbRect = Rect(baseController.windowRect.width - 30, 3, 13, 13)
    #GUI.color = Color.green if baseController.minimized else Color.yellow
    #if GUI.Button(cbRect, ""):
    #    baseController.minimized = not baseController.minimized
    #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 sssb_GUI_resize(baseController, newRect):
    if newRect.width != baseController.windowRect.width or newRect.height != baseController.windowRect.height:
        baseController.windowRect = Rect(baseController.windowRect.x, baseController.windowRect.y, newRect.width, newRect.height)
        #baseController.windowStyle = GUIStyle("window")

def sssb_main_GUI(guiData, sssb):
    oldColor = GUI.color
    leftWidth = 185
    rightWidth = 500 - leftWidth - 24
    coBtnWidth = 62
    expendMarkSpace = 17

    # switch
    if guiData.guiFunction != sssb_main_GUI:
        retnow = guiData.guiFunction != None
        guiData.guiFunction = sssb_main_GUI
        # do switch init here
        guiData.sceneAddMode = "normal"
        # return
        if retnow:
            return

    # functions
    def getCompareColor(compareRes):
        if compareRes == None:
            return oldColor
        elif compareRes == "Identical":
            return Color.green
        elif compareRes == "StatusChanged":
            return Color.yellow
        elif compareRes == "ParentChanged":
            return Color.magenta
        elif compareRes == "ObjectChanged":
            return Color(1, 0.65, 0)    # orange
        elif compareRes == "NewComer":
            return Color.cyan
        elif compareRes == "Deleted":
            return Color.red
        else:
            #print("Unexpected compareRes: " + compareRes)
            return oldColor
    def showNodes(node, intend):
        # gather node info
        canExpand = node.ChildCount > 0
        isExpanded = node in guiData.expendedNodes if canExpand else False
        # show node
        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:
                    guiData.expendedNodes.discard(node)
                else:
                    guiData.expendedNodes.add(node)
        else:
            GUILayout.Space(expendMarkSpace)
        if node.Saved:
            GUI.color = getCompareColor(node.compareResult)
            if GUILayout.Button(node.Name):
                sssb.setCurNode(node if node != sssb.CurNode else None)
                guiData.nodeBtnBoxMode = "normal"
            GUI.color = oldColor
            if guiData.showNodeType:
                GUILayout.Label(node.type)
        else:
            GUILayout.Label(node.Name)
        GUILayout.FlexibleSpace()
        GUILayout.EndHorizontal()
        # show curnode menu
        if node == sssb.CurNode:
            guiCurNodeBox(node)
        # show sub nodes
        if canExpand and isExpanded:
            cIndex = intend + 20
            for sn in node.child:
                showNodes(sn, cIndex)
    def setCurScene(ss):
        guiData.sceneBtnBoxMode = "normal"
        guiData.rightPanelMode = "nodesList"
        sssb.setCurScene(ss)
        ss.diffFromCurrent(sssb.game)
        curEnent = Event.current
        if curEnent.button == 0 and sssb.option.restoreOnClick == "left":
            _, defOpts = sssb.preRestoreScene(ss)
            if defOpts != None: sssb.doRestoreScene(ss, defOpts)
        elif curEnent.button == 1 and sssb.option.restoreOnClick == "right":            
            _, defOpts = sssb.preRestoreScene(ss)
            if defOpts != None: sssb.doRestoreScene(ss, defOpts)
    def guiCurSceneBox():
        GUILayout.BeginVertical(GUI.skin.box, GUILayout.Width(160))
        if guiData.sceneBtnBoxMode == "normal":     # normal
            GUILayout.BeginHorizontal()
            if GUILayout.Button(sssb_scene.name, GUI.skin.label):
                guiData.tempInput = sssb_scene.name
                guiData.sceneBtnBoxMode = "rename"
            GUILayout.EndHorizontal()
            GUILayout.BeginHorizontal()
            if GUILayout.Button("Update", GUILayout.Width(coBtnWidth)):
                if sssb.option.updateConfirm:
                    guiData.sceneBtnBoxMode = "update"
                else:
                    sssb.updateScene(sssb.CurScene)
            if GUILayout.Button("Restore", GUILayout.Width(coBtnWidth)):
                prs, _ = sssb.preRestoreScene(sssb_scene)
                if prs != None:
                    restoreOpts = set()
                    for i in range(len(prs)):
                        if prs[i]:
                            restoreOpts.add(SSSB_Scene.VALID_COMPARE_RESULT[i+1])
                            if not sssb.option.restoreOptionsAuto[i]:
                                restoreOpts = None
                                break
                    if restoreOpts != None:
                        sssb.doRestoreScene(sssb_scene, restoreOpts)
                    else:
                        guiData.restoreOptionReq = tuple(prs)
                        guiData.sceneBtnBoxMode = "restore"
            if GUILayout.Button(u"\u2191"): # move up
                sssb.moveCurrentScene(-1)
            GUILayout.EndHorizontal()
            GUILayout.BeginHorizontal()
            if GUILayout.Button("Merge", GUILayout.Width(coBtnWidth)):
                pass
            GUI.color = Color.red
            if GUILayout.Button("Delete", GUILayout.Width(coBtnWidth)):
                guiData.sceneBtnBoxMode = "delete"
            GUI.color = oldColor
            if GUILayout.Button(u"\u2193"): # move down
                sssb.moveCurrentScene(1)
            GUILayout.EndHorizontal()
        elif guiData.sceneBtnBoxMode == "rename":   # rename
            GUILayout.BeginHorizontal()
            guiData.tempInput = GUILayout.TextField(guiData.tempInput)
            GUILayout.EndHorizontal()
            GUILayout.BeginHorizontal()
            GUI.color = Color.green
            if GUILayout.Button("  OK  "):
                sname = guiData.tempInput.strip()
                if len(sname) == 0:
                    sname = "Scene #%d"%sssb.CurSceneNo
                sssb_scene.name = sname
                guiData.sceneBtnBoxMode = "normal"
            GUI.color = Color.red
            if GUILayout.Button("Cancel"):
                guiData.sceneBtnBoxMode = "normal"
            GUI.color = oldColor
            GUILayout.EndHorizontal()
        elif guiData.sceneBtnBoxMode == "update":   # update
            GUILayout.BeginHorizontal()
            GUILayout.FlexibleSpace()
            GUILayout.Label(redText("Update this scene?"))
            GUILayout.FlexibleSpace()
            GUILayout.EndHorizontal()
            GUILayout.BeginHorizontal()
            GUILayout.Label("Update mode:")
            GUILayout.EndHorizontal()
            GUILayout.BeginHorizontal()
            GUILayout.Space(10)
            if GUILayout.Toggle(sssb.option.updateMode == "saved", "<color=#FFEB04>StatusChanged</color> nodes"):
                sssb.option.updateMode = "saved"
            GUILayout.EndHorizontal()
            GUILayout.BeginHorizontal()
            GUILayout.Space(10)
            if GUILayout.Toggle(sssb.option.updateMode == "all", "From full scene"):
                sssb.option.updateMode = "all"
            GUILayout.EndHorizontal()
            GUILayout.BeginHorizontal()
            GUILayout.Space(10)
            if GUILayout.Toggle(sssb.option.updateMode == "selected", "From selected nodes"):
                sssb.option.updateMode = "selected"
            GUILayout.EndHorizontal()
            if sssb.option.updateMode == "saved":
                GUILayout.BeginHorizontal()
                sssb.option.updateCamera = GUILayout.Toggle(sssb.option.updateCamera, "Update camera")
                GUILayout.EndHorizontal()
            if sssb.option.enableThumbnail:
                GUILayout.BeginHorizontal()
                sssb.option.updateThumb = GUILayout.Toggle(sssb.option.updateThumb, "Update thumbnail")
                GUILayout.EndHorizontal()
            GUILayout.Space(10)
            GUILayout.BeginHorizontal()
            GUI.color = Color.green
            if GUILayout.Button("  OK  "):
                sssb.updateScene(sssb.CurScene)
                guiData.sceneBtnBoxMode = "normal"
            GUI.color = Color.red
            if GUILayout.Button("Cancel"):
                guiData.sceneBtnBoxMode = "normal"
            GUI.color = oldColor
            GUILayout.EndHorizontal()
        elif guiData.sceneBtnBoxMode == "restore":  # restore
            restoreTitle = SSSB_Scene.VALID_COMPARE_RESULT[1:8]
            restoreOptionsTitle = (("Skip", "Restore"), ("Skip", "Re-parent"), ("Skip", "Re-assign"), ("Skip", "Disable"), ("Skip", "Re-create"), ("Skip", "Restore"), ("Skip", "Restore"))
            GUILayout.BeginHorizontal()
            GUILayout.FlexibleSpace()
            GUILayout.Label("Restore options")
            GUILayout.FlexibleSpace()
            GUILayout.EndHorizontal()
            for i in range(len(guiData.restoreOptionReq)):
                if guiData.restoreOptionReq[i]:
                    GUILayout.BeginHorizontal()
                    GUILayout.Label(restoreTitle[i] + ":")
                    GUILayout.FlexibleSpace()
                    sssb.option.restoreOptionsAuto[i] = GUILayout.Toggle(sssb.option.restoreOptionsAuto[i], "Auto")
                    GUILayout.EndHorizontal()
                    GUILayout.BeginHorizontal()
                    GUILayout.FlexibleSpace()
                    for j in range(len(restoreOptionsTitle[i])):
                        GUI.color = Color.green if sssb.option.restoreOptions[i] == j else oldColor
                        if GUILayout.Button(restoreOptionsTitle[i][j]):
                            sssb.option.restoreOptions[i] = j
                    GUI.color = oldColor
                    GUILayout.EndHorizontal()
            GUILayout.Space(10)
            GUILayout.BeginHorizontal()
            GUI.color = Color.green
            if GUILayout.Button("  OK  "):
                opt = set()
                for i in range(len(guiData.restoreOptionReq)):
                    if guiData.restoreOptionReq[i] and sssb.option.restoreOptions[i] > 0:
                        opt.add(restoreTitle[i])
                sssb.doRestoreScene(sssb.CurScene, opt)
                guiData.sceneBtnBoxMode = "normal"
            GUI.color = Color.red
            if GUILayout.Button("Cancel"):
                guiData.sceneBtnBoxMode = "normal"
            GUI.color = oldColor
            GUILayout.EndHorizontal()
        elif guiData.sceneBtnBoxMode == "delete":   # delete confirm
            GUILayout.BeginHorizontal()
            GUILayout.Label(redText("Delete this scene?"))
            GUILayout.EndHorizontal()
            GUILayout.BeginHorizontal()
            GUI.color = Color.green
            if GUILayout.Button("  OK  "):
                sssb.deleteScene(sssb_scene)
                guiData.sceneBtnBoxMode = "normal"
            GUI.color = Color.red
            if GUILayout.Button("Cancel"):
                guiData.sceneBtnBoxMode = "normal"
            GUI.color = oldColor
            GUILayout.EndHorizontal()
        else:   # unexpected
            guiData.sceneBtnBoxMode = "normal"
        GUILayout.EndVertical()
    def guiNodesList(scene):
        # Nodes
        if guiData.showSavedNodes:
            GUILayout.Label("---------- SAVED WORKSPACE ----------")
            for rNode in scene.rootNodes:
                showNodes(rNode, 0)
            GUILayout.Label("---------- SAVED VNGE NODE ----------")
            showNodes(scene.cameraNode, 0)
            showNodes(scene.systemNode, 0)
        else:
            GUILayout.Label("---------- SCANED WORKSPACE ----------")
            for rNode in scene.sceneDiffRootNodes:
                showNodes(rNode, 0)
            GUILayout.Label("---------- SCANED VNGE NODE ----------")
            showNodes(scene.sceneDiffCameraNode, 0)
            showNodes(scene.sceneDiffSystemNode, 0)
        # Legend
        if True:
            GUILayout.FlexibleSpace()
            GUILayout.BeginHorizontal()
            lcount = 0
            for cr in SSSB_Scene.VALID_COMPARE_RESULT:
                if cr in scene.sceneDiffResult:
                    GUI.color = getCompareColor(cr)
                    GUILayout.Label("%s: %d"%(cr, len(scene.sceneDiffResult[cr])))
                    lcount += 1
                    if lcount > 0 and lcount % 2 == 0 and lcount < len(scene.sceneDiffResult):
                        GUILayout.EndHorizontal()
                        GUILayout.BeginHorizontal()
            GUI.color = oldColor
            GUILayout.EndHorizontal()
    def guiCurNodeBox(node):
        savedNode = sssb.CurScene.isSavedNode(node)
        diffNode = sssb.CurScene.isDiffNode(node)
        GUILayout.BeginVertical(GUI.skin.box, GUILayout.Width(rightWidth - 30))
        if guiData.nodeBtnBoxMode == "normal":      # normal
            #GUILayout.BeginHorizontal()
            #GUILayout.Label("Maybe some info for this node")
            #GUILayout.EndHorizontal()
            GUILayout.BeginHorizontal()
            if GUILayout.Button("Status detail..."):
                guiData.rightPanelMode = "statusDetail"
                guiData.highlightStatus = None
                if not hasattr(guiData, "highlightTextSepElement"):
                    guiData.highlightTextSepElement = False
            if node.IsTreeNode and GUILayout.Button("Select in scene "):
                node.selectInWorkspace(sssb.game)
            GUILayout.EndHorizontal()
            GUILayout.BeginHorizontal()
            # update status
            if savedNode and node.IsStatusChanged and GUILayout.Button("Update"):
                node.updateStatus(sssb.game)
                sssb.CurScene.diffFromCurrent(sssb.game)
                sssb.setCurNode(None)
            # restore status
            if savedNode and node.IsStatusChanged and GUILayout.Button("Restore"):
                node.restoreStatus(sssb.game)
                sssb.CurScene.diffFromCurrent(sssb.game)
                sssb.setCurNode(None)
            # restore parent
            if savedNode and node.IsParentChanged and GUILayout.Button("Restore parent"):
                node.restoreParent(sssb.game)
                sssb.CurScene.diffFromCurrent(sssb.game)
                sssb.setCurNode(None)
            # disable
            if diffNode and node.IsNewComer and GUILayout.Button("Disable"):
                node.disable(sssb.game)
                sssb.CurScene.diffFromCurrent(sssb.game)
                sssb.setCurNode(None)
            # add to scene
            if diffNode and node.IsNewComer and GUILayout.Button("Append"):
                sssb.CurScene.appendNode(sssb.game, node.treeNodeObj)
                sssb.CurScene.diffFromCurrent(sssb.game)
                sssb.setCurNode(None)
            # re-create
            if savedNode and node.IsDeleted and GUILayout.Button("Re-create"):
                msg = node.reCreate(sssb.game)
                if msg:
                    sssb.showMsg(msg)
                else:
                    sssb.CurScene.diffFromCurrent(sssb.game)
                    sssb.setCurNode(None)
            # merge
            if savedNode and node.IsObj and GUILayout.Button("Merge"):
                guiData.nodeBtnBoxMode = "merge"
                if not hasattr(guiData, "nodeBtnBoxMergeChild"): guiData.nodeBtnBoxMergeChild = True
            # delete
            GUI.color = Color.red
            if savedNode and node.IsObj and GUILayout.Button("Remove"):
                guiData.nodeBtnBoxMode = "delete"
            GUI.color = oldColor
            GUILayout.EndHorizontal()
        elif guiData.nodeBtnBoxMode == "merge":    # merge option
            GUILayout.BeginHorizontal()
            GUILayout.Label("Merge <%s> to:"%node.Name)
            GUILayout.EndHorizontal()
            selected = len(sssb.game.studio.treeNodeCtrl.selectNodes) == 1
            if selected:
                GUILayout.BeginHorizontal()
                GUILayout.FlexibleSpace()
                GUILayout.Label(greenText(sssb.game.studio.treeNodeCtrl.selectNode.textName))
                GUILayout.FlexibleSpace()
                GUILayout.EndHorizontal()
            else:
                GUILayout.BeginHorizontal()
                GUILayout.Label(redText("Select an object in workspace to merge"))
                GUILayout.EndHorizontal()
            GUILayout.BeginHorizontal()
            guiData.nodeBtnBoxMergeChild = GUILayout.Toggle(guiData.nodeBtnBoxMergeChild, "Try merge all children")
            GUILayout.EndHorizontal()
            GUILayout.BeginHorizontal()
            GUI.color = Color.green if selected else Color.grey
            if GUILayout.Button("  OK  ") and selected:
                print("\nSSSB: Re-assign node '%s'"%(node.Path))
                msg = sssb.CurScene.reAssignNode(sssb.game, node, sssb.game.studio.treeNodeCtrl.selectNode, guiData.nodeBtnBoxMergeChild)
                if msg:
                    sssb.showMsg(msg)
                else:
                    sssb.setCurNode(None)
                    sssb.CurScene.diffFromCurrent(sssb.game)
                    guiData.nodeBtnBoxMode = "normal"
            GUI.color = oldColor
            if node.IsObjectChanged and GUILayout.Button(" Auto "):
                sssb.game.studio.treeNodeCtrl.SelectSingle(None, True)
                sssb.game.studio.treeNodeCtrl.SelectSingle(sssb.game.studio.dicObjectCtrl[node.DicKey].treeNodeObject, False)
            GUI.color = Color.red
            if GUILayout.Button("Cancel"):
                guiData.nodeBtnBoxMode = "normal"
            GUI.color = oldColor
            GUILayout.EndHorizontal()
        elif guiData.nodeBtnBoxMode == "delete":   # delete confirm
            GUILayout.BeginHorizontal()
            GUILayout.Label(redText("Delete this node from saved data?"))
            GUILayout.EndHorizontal()
            GUILayout.BeginHorizontal()
            GUI.color = Color.green
            if GUILayout.Button("  OK  "):
                msg = sssb.CurScene.removeNode(node)
                if msg:
                    sssb.showMsg(msg)
                else:
                    sssb.setCurNode(None)
                    sssb.CurScene.diffFromCurrent(sssb.game)
                    guiData.nodeBtnBoxMode = "normal"
            GUI.color = Color.red
            if GUILayout.Button("Cancel"):
                guiData.nodeBtnBoxMode = "normal"
            GUI.color = oldColor
            GUILayout.EndHorizontal()
        else:   # unexpected
            guiData.nodeBtnBoxMode = "normal"
        GUILayout.EndVertical()
    def guiNodesStatus(node):
        def checkStatusChange(stKey):
            try:
                diffNode = sssb.CurScene.sceneDiffDict[node.DicKey]
                if stKey in diffNode.status:
                    return SSSB_Node.CheckValueChanged(node.type, st, node.status[st], diffNode.status[st])
                else:
                    return True
            except:
                return False
        def formatStatusText(stValue, sepElement):
            from vnframe import script2string
            if sepElement:
                if isinstance(stValue, dict):
                    seps = ["%s: %s"%(str(k), script2string(stValue[k])) for k in sorted(stValue.keys())]
                    return "\n".join(seps)
                if isinstance(stValue, list):
                    seps = [script2string(v) for v in stValue]
                    return "\n".join(seps)
                if isinstance(stValue, tuple):
                    seps = [script2string(v) for v in stValue]
                    return "\n".join(seps)
            # anything else
            return script2string(stValue)
        def parseStatusValue(stNewValueStr, stValue, sepElement):
            from vnframe import string2script
            if sepElement:
                if isinstance(stValue, dict):
                    stNewValueStr = "{" + stNewValueStr.replace("\n", ",") + "}"
                if isinstance(stValue, list):
                    stNewValueStr = "[" + stNewValueStr.replace("\n", ",") + "]"
                if isinstance(stValue, tuple):
                    stNewValueStr = "(" + stNewValueStr.replace("\n", ",") + ")"
            try:
                newStatus = string2script(stNewValueStr, stValue, True)
                return newStatus
            except Exception as e:
                sssb.showMsg("Cannot parse status input: " + str(e))
                return None
        
        # status
        sameCount = 0
        diffCount = 0
        ignCount = 0
        from vnanime import translateAndSortStatusKeys
        stList, stDict = translateAndSortStatusKeys(node.status.keys(), True)
        for st in stList:
            # status
            GUILayout.BeginHorizontal()
            oldIgnore = st in node.ignoreProperties
            newIgnore = not GUILayout.Toggle(not oldIgnore, "")
            if newIgnore != oldIgnore:
                if newIgnore:
                    node.ignoreProperties.add(st)
                else:
                    node.ignoreProperties.discard(st)
            if newIgnore:
                ignCount += 1
                GUILayout.Label(stDict[st])
            else:
                if checkStatusChange(st):
                    GUI.color = Color.red
                    diffCount += 1
                else:
                    sameCount += 1
                if GUILayout.Button(stDict[st]):
                    if st == guiData.highlightStatus:
                        guiData.highlightStatus = None
                    else:
                        guiData.highlightStatus = st
                        guiData.orgValueText = formatStatusText(node.status[st], guiData.highlightTextSepElement)
                        guiData.tmpValueText = guiData.orgValueText
                GUI.color = oldColor
            GUILayout.FlexibleSpace()
            GUILayout.EndHorizontal()
            # detail
            if not newIgnore and guiData.highlightStatus == st:
                guiData.tmpValueText = GUILayout.TextArea(guiData.tmpValueText)
                GUILayout.BeginHorizontal()
                GUI.color = Color.green
                if guiData.tmpValueText != guiData.orgValueText and GUILayout.Button("Update"):
                    newStatus = parseStatusValue(guiData.tmpValueText, node.status[st], guiData.highlightTextSepElement)
                    if newStatus != None:
                        guiData.orgValueText = guiData.tmpValueText
                        node.status[st] = newStatus
                        node.restoreStatus(sssb.game)
                        sssb.CurScene.diffFromCurrent(sssb.game)
                GUI.color = Color.red
                if guiData.tmpValueText != guiData.orgValueText and GUILayout.Button("Cancel"):
                    guiData.tmpValueText = guiData.orgValueText
                GUILayout.FlexibleSpace()
                GUI.color = oldColor
                se = GUILayout.Toggle(guiData.highlightTextSepElement, "Seperate elements")
                if se != guiData.highlightTextSepElement:
                    guiData.highlightTextSepElement = se
                    guiData.orgValueText = formatStatusText(node.status[st], guiData.highlightTextSepElement)
                    guiData.tmpValueText = guiData.orgValueText
                GUILayout.EndHorizontal()
        # Legend
        if True:
            GUILayout.FlexibleSpace()
            GUILayout.BeginHorizontal()
            GUI.color = Color.green
            GUILayout.Label("Identical: %d"%sameCount)
            GUI.color = Color.red
            GUILayout.Label("Changed: %d"%diffCount)
            GUI.color = Color.grey
            GUILayout.Label("Ignored: %d"%ignCount)
            GUI.color = oldColor
            GUILayout.EndHorizontal()

    # begin of main GUI
    GUILayout.BeginHorizontal()

    ## LEFT: Scene Parts
    GUILayout.BeginVertical(GUILayout.Width(leftWidth))
    
    # title line
    GUILayout.BeginHorizontal()
    GUILayout.Label("Scenes (%s/%d)"%(str(sssb.CurSceneNo) if sssb.CurSceneNo else "-", sssb.TotalSceneCount))
    GUILayout.FlexibleSpace()
    if GUILayout.Button(">>" if guiData.halfMode else "<<"):
        guiData.halfMode = not guiData.halfMode
    GUILayout.EndHorizontal()
    # scene list
    guiData.mainGuiSceneScl = GUILayout.BeginScrollView(guiData.mainGuiSceneScl, GUI.skin.box)
    if sssb.option.restoreHelpInfo:  # show instruction info
        if sssb.option.restoreOnClick == "left":
            GUILayout.Label(yellowLightText("Left click: quick restore\nRight click: select scene"))
        elif sssb.option.restoreOnClick == "right":
            GUILayout.Label(yellowLightText("Left click: select scene\nRight click: quick restore"))
        else:
            GUILayout.Label(yellowLightText("Quick restore disabled"))
    for sssb_scene in sssb.scene:   # scene list
        if sssb_scene.thumbTex != None and sssb.option.enableThumbnail and GUILayout.Button(sssb_scene.thumbTex, GUIStyle("label"), GUILayout.Width(160), GUILayout.Height(90)):
            setCurScene(sssb_scene)
        if sssb_scene == sssb.CurScene:
            guiCurSceneBox()
        else:
            if GUILayout.Button(sssb_scene.name, GUILayout.Width(160)):
                setCurScene(sssb_scene)
    GUILayout.EndScrollView()
    # left button
    GUILayout.BeginHorizontal()
    if guiData.sceneAddMode == "normal":
        if sssb.option.addSceneRange == "all" and GUILayout.Button("Add Scene: All"):
            sssb.addNewScene("all")
        if sssb.option.addSceneRange == "selected" and GUILayout.Button("Add Scene: Selected"):
            sssb.addNewScene("selected")
        if sssb.option.addSceneRange == "choose" and GUILayout.Button("Add Scene"):
            guiData.sceneAddMode = "choose"
    elif guiData.sceneAddMode == "choose":
        GUI.color = Color.yellow
        if GUILayout.Button("All"):
            sssb.addNewScene("all")
            guiData.sceneAddMode = "normal"
        if GUILayout.Button("Selected"):
            sssb.addNewScene("selected")
            guiData.sceneAddMode = "normal"
        GUI.color = Color.red
        if GUILayout.Button("X", GUILayout.Width(25)):
            guiData.sceneAddMode = "normal"
        GUI.color = oldColor
    else:
        guiData.sceneAddMode = "normal"
    GUILayout.EndHorizontal()
    GUILayout.BeginHorizontal()
    if GUILayout.Button("Save/Load"):
        sssb_saveload_GUI(guiData, sssb)
    if GUILayout.Button(" Setting "):
        sssb_option_GUI(guiData, sssb)
    GUILayout.EndHorizontal()
    GUILayout.EndVertical()

    # if half mode, end gui function here
    if guiData.halfMode:
        GUILayout.EndHorizontal()
        return

    ## RIGHT: Node parts
    GUILayout.BeginVertical(GUILayout.Width(rightWidth))
    # right title
    GUILayout.BeginHorizontal()
    if guiData.rightPanelMode == "nodesList":
        GUILayout.Label("Nodes: ")
        GUI.color = Color.green if guiData.showSavedNodes else oldColor
        if GUILayout.Button("Saved"):
            guiData.showSavedNodes = True
        GUI.color = Color.green if not guiData.showSavedNodes else oldColor
        if GUILayout.Button("Current"):
            guiData.showSavedNodes = False
        GUI.color = oldColor
    elif guiData.rightPanelMode == "statusDetail":
        GUILayout.Label("Status of %s:"%greenText(sssb.CurNode.Name))
    else:
        pass
    GUILayout.FlexibleSpace()
    GUILayout.EndHorizontal()
    # right contents
    guiData.mainGuiNodesScl = GUILayout.BeginScrollView(guiData.mainGuiNodesScl, GUI.skin.box)
    if guiData.rightPanelMode == "nodesList" and sssb.CurScene:
        guiNodesList(sssb.CurScene)
    elif guiData.rightPanelMode == "statusDetail" and sssb.CurNode:
        guiNodesStatus(sssb.CurNode)
    else:
        GUILayout.Label(greenText("Create or select a scene"))
    GUILayout.EndScrollView()
    # right button
    if guiData.rightPanelMode == "nodesList" and sssb.CurScene:
        if GUILayout.Button("Scan diff"):
            msg = sssb.CurScene.diffFromCurrent(sssb.game)
            if msg: sssb.showMsg(msg)
        if sssb.CurScene.sceneDiffResult.has_key(SSSB_Scene.VALID_COMPARE_RESULT[3]) and GUILayout.Button("Solve all object change"):
            msg = sssb.CurScene.reAssignObjectChange(sssb.game)
            if msg: sssb.showMsg(msg)
            msg = sssb.CurScene.diffFromCurrent(sssb.game)
            if msg: sssb.showMsg(msg)
        if sssb.CurScene.sceneDiffResult.has_key(SSSB_Scene.VALID_COMPARE_RESULT[4]) and GUILayout.Button("Append all new comer"):
            msg = sssb.CurScene.appendAllNewComer(sssb.game)
            if msg: sssb.showMsg(msg)
            msg = sssb.CurScene.diffFromCurrent(sssb.game)
            if msg: sssb.showMsg(msg)
    elif guiData.rightPanelMode == "statusDetail" and sssb.CurNode:
        GUILayout.BeginHorizontal()
        if GUILayout.Button("Scan diff"):
            msg = sssb.CurScene.diffFromCurrent(sssb.game)
            if msg: sssb.showMsg(msg)
        if sssb.CurNode.IsStatusChanged and GUILayout.Button("  Update "):
            sssb.CurNode.updateStatus(sssb.game)
            sssb.CurScene.diffFromCurrent(sssb.game)
            guiData.highlightStatus = None
        if sssb.CurNode.IsStatusChanged and GUILayout.Button(" Restore "):
            sssb.CurNode.restoreStatus(sssb.game)
            sssb.CurScene.diffFromCurrent(sssb.game)
            guiData.highlightStatus = None
        GUILayout.EndHorizontal()
        GUILayout.BeginHorizontal()
        if GUILayout.Button("Select in workspace"):
            sssb.CurNode.selectInWorkspace(sssb.game)
        if GUILayout.Button(" Back to node list "):
            sssb.CurScene.diffFromCurrent(sssb.game)
            guiData.rightPanelMode = "nodesList"
        GUILayout.EndHorizontal()
    else:
        pass
    # end of right panel
    GUILayout.EndVertical()

    # end of main gui
    GUILayout.EndHorizontal()

def sssb_saveload_GUI(guiData, sssb):
    oldColor = GUI.color
    borderWidth = 20
    buttonHeight = 50
    wrnLabel = GUIStyle("label")
    wrnLabel.fontSize = 20
    btnstyle = GUIStyle("button")
    btnstyle.fontSize = 16

    # switch
    if guiData.guiFunction != sssb_saveload_GUI:
        guiData.guiFunction = sssb_saveload_GUI
        # do switch init here
        guiData.saveLoadMode = "normal"
        return

    def bigButton(txt):
        return GUILayout.Button(txt, btnstyle, GUILayout.Height(buttonHeight))

    # begin of gui
    GUILayout.BeginHorizontal()
    GUILayout.Space(borderWidth)

    # begin of center
    GUILayout.BeginVertical()
    GUILayout.FlexibleSpace()

    if guiData.saveLoadMode == "normal":
        if bigButton("Save"):
            if sssb.saveData():
                sssb_main_GUI(guiData, sssb)
        GUILayout.BeginHorizontal()
        if bigButton(" Load "):
            if sssb.loadData(append = False):
                sssb_main_GUI(guiData, sssb)
        if bigButton("Import"):
            if sssb.loadData(append = True):
                sssb_main_GUI(guiData, sssb)
        GUILayout.EndHorizontal()
        if bigButton(redText("Reset")):
            guiData.saveLoadMode = "reset"
        if bigButton("Back"):
            sssb_main_GUI(guiData, sssb)
    if guiData.saveLoadMode == "reset":
        GUILayout.Label("Reset and delete all saved scene from SSSB, NO UNDO VALID, are you sure!?", wrnLabel)
        GUILayout.Space(10)
        GUILayout.BeginHorizontal()
        GUI.color = Color.red
        if bigButton("YES, I AM SURE"):
            sssb.reset()
            sssb_main_GUI(guiData, sssb)
        GUI.color = oldColor
        if bigButton(" NO, HELL NO! "):
            guiData.saveLoadMode = "normal"
        GUILayout.EndHorizontal()
    else:
        guiData.saveLoadMode = "normal"
    
    # end of center
    GUILayout.FlexibleSpace()

    # footer
    GUILayout.BeginHorizontal()
    GUILayout.FlexibleSpace()
    GUILayout.Label(greenText("SceneSaveStateBuilder by Countd360"))
    GUILayout.EndHorizontal()

    # end of center
    GUILayout.EndVertical()

    # end of gui
    GUILayout.Space(borderWidth)
    GUILayout.EndHorizontal()

def sssb_option_GUI(guiData, sssb):
    lWidth = 100
    oldColor = GUI.color
    
    # switch
    if guiData.guiFunction != sssb_option_GUI:
        guiData.guiFunction = sssb_option_GUI
        # do switch init here
        guiData.settingScroll = Vector2.zero
        return

    # start of options
    guiData.settingScroll = GUILayout.BeginScrollView(guiData.settingScroll)

    # Add scene option
    GUILayout.Label(greenText("Add scene options:"))
    GUILayout.BeginHorizontal()
    GUILayout.Label("Add Range:", GUILayout.Width(lWidth))
    newVal = gui_widge_select_buttons((("choose", "By choice"), ("all", "All nodes"), ("selected", "Selected nodes")), sssb.option.addSceneRange)
    if newVal != None:
        sssb.option.addSceneRange = newVal
    GUILayout.FlexibleSpace()
    GUILayout.EndHorizontal()
    GUILayout.BeginHorizontal()
    GUILayout.Label("Thumbnail:", GUILayout.Width(lWidth))
    GUI.color = Color.green if sssb.option.enableThumbnail else Color.red
    if GUILayout.Button("Enable" if sssb.option.enableThumbnail else "Disable", GUILayout.Width(60)):
        sssb.option.enableThumbnail = not sssb.option.enableThumbnail
    GUI.color = oldColor
    GUILayout.FlexibleSpace()
    GUILayout.EndHorizontal()
    GUILayout.Space(10)

    # update options
    GUILayout.Label(greenText("Update scene options:"))
    GUILayout.BeginHorizontal() # confirm
    GUILayout.Label("Confirm update:", GUILayout.Width(lWidth))
    GUI.color = Color.green if sssb.option.updateConfirm else Color.red
    if GUILayout.Button("Enable" if sssb.option.updateConfirm else "Disable", GUILayout.Width(60)):
        sssb.option.updateConfirm = not sssb.option.updateConfirm
    GUI.color = oldColor
    GUILayout.FlexibleSpace()
    GUILayout.EndHorizontal()
    GUILayout.BeginHorizontal() # default update mode
    GUILayout.Label("Update mode:", GUILayout.Width(lWidth))
    newVal = gui_widge_select_buttons((("saved", "StatusChanged nodes"), ("all", "Full scene"), ("selected", "Selected nodes")), sssb.option.updateMode)
    if newVal != None:
        sssb.option.updateMode = newVal
    GUILayout.FlexibleSpace()
    GUILayout.EndHorizontal()
    GUILayout.BeginHorizontal() # default update camera
    GUILayout.Label("Update camera:", GUILayout.Width(lWidth))
    GUI.color = Color.green if sssb.option.updateCamera else Color.red
    if GUILayout.Button("Enable" if sssb.option.updateCamera else "Disable", GUILayout.Width(60)):
        sssb.option.updateCamera = not sssb.option.updateCamera
    GUI.color = oldColor
    GUILayout.FlexibleSpace()
    GUILayout.EndHorizontal()
    GUILayout.BeginHorizontal() # default update thumb
    GUILayout.Label("Update thumb:", GUILayout.Width(lWidth))
    GUI.color = Color.green if sssb.option.updateThumb else Color.red
    if GUILayout.Button("Enable" if sssb.option.updateThumb else "Disable", GUILayout.Width(60)):
        sssb.option.updateThumb = not sssb.option.updateThumb
    GUI.color = oldColor
    GUILayout.FlexibleSpace()
    GUILayout.EndHorizontal()
    GUILayout.Space(10)

    # restore option
    restoreTitle = SSSB_Scene.VALID_COMPARE_RESULT[1:8]
    restoreOptionsTitle = (("Skip", "Restore"), ("Skip", "Re-parent"), ("Skip", "Re-assign"), ("Skip", "Disable"), ("Skip", "Re-create"), ("Skip", "Restore"), ("Skip", "Restore"))
    GUILayout.Label(greenText("Restore scene options:"))
    GUILayout.BeginHorizontal()
    GUILayout.Label("Quick restore:", GUILayout.Width(lWidth))
    newVal = gui_widge_select_buttons((("never", "Never"), ("left", "On left click"), ("right", "On right click")), sssb.option.restoreOnClick)
    if newVal != None:
        sssb.option.restoreOnClick = newVal
    GUILayout.FlexibleSpace()
    GUILayout.EndHorizontal()    
    GUILayout.BeginHorizontal()
    GUILayout.Label("Show help info:", GUILayout.Width(lWidth))
    GUI.color = Color.green if sssb.option.restoreHelpInfo else Color.red
    if GUILayout.Button("Enable" if sssb.option.restoreHelpInfo else "Disable", GUILayout.Width(60)):
        sssb.option.restoreHelpInfo = not sssb.option.restoreHelpInfo
    GUI.color = oldColor
    GUILayout.FlexibleSpace()
    GUILayout.EndHorizontal()    
    for i in range(len(restoreTitle)):
        GUILayout.BeginHorizontal()
        GUILayout.Label(restoreTitle[i] + ":", GUILayout.Width(lWidth))
        newVal = gui_widge_select_buttons(((0, restoreOptionsTitle[i][0]), (1, restoreOptionsTitle[i][1])), sssb.option.restoreOptions[i])
        if newVal != None:
            sssb.option.restoreOptions[i] = newVal
        GUILayout.FlexibleSpace()
        sssb.option.restoreOptionsAuto[i] = GUILayout.Toggle(sssb.option.restoreOptionsAuto[i], "No more ask")
        GUILayout.EndHorizontal()
    GUILayout.BeginHorizontal()
    GUILayout.Label("Re-create child:", GUILayout.Width(lWidth))
    SSSB_Scene.STOP_RECREATE_CHILD_WHEN_FAILED = GUILayout.Toggle(SSSB_Scene.STOP_RECREATE_CHILD_WHEN_FAILED, "Stop re-create child node if main node fail to re-create")
    GUILayout.FlexibleSpace()
    GUILayout.EndHorizontal()    
    GUILayout.Space(10)
    
    # merge option

    # status check
    GUILayout.Label(greenText("Status compare options:"))
    SSSB_Node.CHECK_VALUE_IGNORE_SMALL_CHANGE = GUILayout.Toggle(SSSB_Node.CHECK_VALUE_IGNORE_SMALL_CHANGE, "Check value in detail and ignore difference less than minor threshold")
    GUILayout.BeginHorizontal()
    GUILayout.Label("Minor threshold:", GUILayout.Width(lWidth))
    newTV = GUILayout.TextField(str(SSSB_Node.CHECK_VALUE_MINOR_THRESHOLD), GUILayout.Width(100))
    if newTV != str(SSSB_Node.CHECK_VALUE_MINOR_THRESHOLD):
        try:
            newTVV = float(newTV)
            if newTVV > 0:
                SSSB_Node.CHECK_VALUE_MINOR_THRESHOLD = newTVV
        except:
            pass
    GUILayout.EndHorizontal()
    GUILayout.Space(10)

    # debug option
    GUILayout.Label(greenText("Debug message options:"))
    SSSB_Scene.DEBUG_REPORT_SCENE_DIFF_DETAIL = GUILayout.Toggle(SSSB_Scene.DEBUG_REPORT_SCENE_DIFF_DETAIL, "Report scene diff details")
    SSSB_Node.DEBUG_REPORT_STATUS_DIFF_DETAIL = GUILayout.Toggle(SSSB_Node.DEBUG_REPORT_STATUS_DIFF_DETAIL, "Report status diff details")
    SSSB_Node.DEBUG_REPORT_STATUS_FILTER_DETAIL = GUILayout.Toggle(SSSB_Node.DEBUG_REPORT_STATUS_FILTER_DETAIL, "Report restored status in details")

    # end of options
    GUILayout.EndScrollView()

    # control button
    GUILayout.FlexibleSpace()
    GUILayout.BeginHorizontal()
    if GUILayout.Button("Change VNActor setting"):
        sssb_vnactor_setting_GUI(guiData, sssb)
    if GUILayout.Button(" Back to SSS Builder  "):
        sssb_main_GUI(guiData, sssb)
    GUILayout.EndHorizontal()

def sssb_vnactor_setting_GUI(guiData, sssb):
    from vnactor import set_ini_value, get_ini_options, is_ini_value_true, get_ini_exportOptionDesp
    lWidth = 100
    oldColor = GUI.color
    
    # switch
    if guiData.guiFunction != sssb_vnactor_setting_GUI:
        guiData.guiFunction = sssb_vnactor_setting_GUI
        # do switch init here
        guiData.vnaSettingScroll = Vector2.zero
        return

    # title
    GUILayout.Label("Check the options need to be saved in SSSB")
    GUILayout.Space(5)

    # start of options
    guiData.vnaSettingScroll = GUILayout.BeginScrollView(guiData.vnaSettingScroll)

    for opt in get_ini_options():
        oldEn = is_ini_value_true(opt)
        newEn = GUILayout.Toggle(oldEn, "<color=#00ff00>" + opt + "</color>: " + get_ini_exportOptionDesp(opt))
        if newEn != oldEn:
            set_ini_value(opt, newEn)

    # end of options
    GUILayout.EndScrollView()

    # control buttons
    GUILayout.FlexibleSpace()
    GUILayout.BeginHorizontal()
    if GUILayout.Button("Back to SSSB setting"):
        sssb_option_GUI(guiData, sssb)
    if GUILayout.Button(" Back to SSSB main  "):
        sssb_main_GUI(guiData, sssb)
    GUILayout.EndHorizontal()

def gui_widge_select_buttons(options, curValue):
    newVal = None
    oldColor = GUI.color
    for (val, title) in options:
        GUI.color = Color.green if val == curValue else oldColor
        if GUILayout.Button(title) and val != curValue:
            newVal = val
        GUI.color = oldColor
    return newVal

def gui_widge_toggle_buttons(options, curValue):
    newVal = None
    for i in range(len(options)):
        val, title = options[i]
        if val != curValue and GUILayout.Button(title):
            newIdx = (i+1) % len(options)
            newVal = options[newIdx][0]
    return newVal

# ====================================  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, "555555")
def yellowLightText(text):
    return colorText(text, "f8e473")

def loadData(settingDict, key, defaultValue = None):
    if key in settingDict:
        return settingDict[key]
    else:
        return defaultValue

# ====================================  SSSB classes ====================================
class SceneSaveStateBuilder():
    SAVE_ID = "vnge_sssb"
    CUR_SAVE_VERSION = "1"
    VALID_SAVE_VERSION = ["1"]

    @property
    def TotalSceneCount(self):
        return len(self.scene)

    @property
    def CurSceneNo(self):
        return self.curSceneIndex + 1

    @property
    def CurScene(self):
        if 0 <= self.curSceneIndex < self.TotalSceneCount:
            return self.scene[self.curSceneIndex]
        else:
            return None

    @property
    def CurNode(self):
        return self.curNode

    def __init__(self, game):
        self.game = game
        self.wndId = None
        
        self.reset()
        self.loadOption()
    
    def reset(self):
        self.curSceneIndex = -1
        self.curNode = None
        self.scene = []

    def loadOption(self):
        self.option = GData()
        # init
        self.option.addSceneRange = "all"  # choose, all, selected
        self.option.enableThumbnail = True # True/False

        self.option.updateConfirm = True
        self.option.updateMode = "saved"    # saved, all, selected
        self.option.updateCamera = True
        self.option.updateThumb = True

        self.option.restoreOnClick = "right"    # never, left, right
        self.option.restoreHelpInfo = True
        self.option.restoreOptions = [1, 1, 1, 1, 0, 1, 1]  # for ("StatusChanged", "ParentChanged", "ObjectChanged", "NewComer", "Deleted", "Camera", "System")
        self.option.restoreOptionsAuto = [1, 0, 0, 0, 0, 0, 0]


    def exportSetting(self):
        setting = {}
        setting["scene"] = []
        for sc in self.scene:
            setting["scene"].append(sc.exportSetting())
        return setting

    def importSetting(self, setting, append):
        if not append:
            self.reset()
        for scs in setting["scene"]:
            sssb_scene = SSSB_Scene()
            msg = sssb_scene.createFromSetting(self.game, scs)
            if msg:
                self.showMsg(msg)
                continue
            self.scene.append(sssb_scene)
        if not append and len(self.scene) > 0:
            self.curSceneIndex = 0

    def checkSceneData(self):
        from vnframe import loadFromJsonString
        try:            
            saveData, version = self.game.dataPNG_load_checkversion(SceneSaveStateBuilder.SAVE_ID, SceneSaveStateBuilder.VALID_SAVE_VERSION)
            if saveData != None and len(saveData) != 0:
                sssb_setting = loadFromJsonString(saveData)
                return len(sssb_setting["scene"])
        except:
            print("Fail to check SSSB scene count in PNG data")
        return 0

    def saveData(self, filename = None, forUser = False):
        from vnframe import dumpToJsonString
        print("SSSB: scene saving...")
        strData = dumpToJsonString(self.exportSetting(), forUser)
        if filename == None:
            self.game.dataPNG_save(SceneSaveStateBuilder.SAVE_ID, strData, SceneSaveStateBuilder.CUR_SAVE_VERSION)
            print("SSSB: %d scene saved"%(len(self.scene)))
        else:
            try:
                fp = codecs.open(filename, "w", 'utf-8')
                fp.write(strData)
                fp.close()
                print("SSSB: %d scene export to file %s"%(len(self.scene), filename))
            except:
                print("SSSB: Fail to save to file %s"%str(filename))
                return False
        return True

    def loadData(self, filename = None, append = False):
        from vnframe import loadFromJsonString
        # get data
        print("SSSB: scene loading...")
        if filename == None:
            saveData, version = self.game.dataPNG_load_checkversion(SceneSaveStateBuilder.SAVE_ID, SceneSaveStateBuilder.VALID_SAVE_VERSION)
        else:
            try:
                fp = codecs.open(filename, "r", 'utf-8')
                saveData = fp.read()
                fp.close()
            except:
                print("SSSB: Fail to load from file %s"%str(filename))
                return False
        # import data
        if saveData != None and len(saveData) != 0:
            try:
                oldSceneCount = len(self.scene) if append else 0
                sssb_setting = loadFromJsonString(saveData)
                self.importSetting(sssb_setting, append)
                if append:
                    print("SSSB: append %d scenes"%(len(self.scene) - oldSceneCount))
                else:
                    print("SSSB: loaded %d scenes"%len(self.scene))
            except:
                print("SSSB: Fail to import from setting")
                return False
        else:
            print("SSSB: No data found...")
        return True

    def addNewScene(self, mode):
        sssb_scene = SSSB_Scene()
        name = "Scene #%d"%(self.TotalSceneCount + 1)
        selected = mode == "selected"
        thumb = None if self.option.enableThumbnail else Texture2D.blackTexture
        msg = sssb_scene.createFromCurrent(self.game, name, selected, thumb)
        if msg:
            self.showMsg(msg)
            return
        self.scene.append(sssb_scene)
        self.curSceneIndex = self.TotalSceneCount - 1

    def updateScene(self, tgtScene):
        if tgtScene in self.scene:
            if self.option.updateMode == "saved":
                msg = tgtScene.diffFromCurrent(self.game)
                if msg:
                    self.showMsg(msg)
                    return
                if "StatusChanged" in tgtScene.sceneDiffResult:
                    for node in tgtScene.sceneDiffResult["StatusChanged"]:
                        node.updateStatus(self.game)
                if tgtScene.systemNode.IsStatusChanged:
                    tgtScene.systemNode.updateStatus(self.game)
                if self.option.updateCamera:
                    tgtScene.cameraNode.updateStatus(self.game)
                if self.option.updateThumb:
                    tgtScene.thumbTex = SSSB_Scene.CaptureThumb(self.game)
                tgtScene.diffFromCurrent(self.game)
            else:
                sssb_scene = SSSB_Scene()
                name = tgtScene.name
                selected = self.option.updateMode == "selected"
                thumb = None if self.option.updateThumb else tgtScene.thumbTex
                msg = sssb_scene.createFromCurrent(self.game, name, selected, thumb)
                if msg:
                    self.showMsg(msg)
                    return
                self.scene[self.scene.index(tgtScene)] = sssb_scene
                sssb_scene.diffFromCurrent(self.game)

    def deleteScene(self, tgtScene):
        if tgtScene in self.scene:
            self.scene.remove(tgtScene)
            self.curSceneIndex = - 1
            self.renameAllScene()
    
    def preRestoreScene(self, tgtScene):
        msg = tgtScene.diffFromCurrent(self.game)
        if msg:
            self.showMsg(msg)
            return None # cannot restore because of diff error
        # what change need to be processed
        # status, parent, object, newComer, delete, camera, system
        chgs = (
            "StatusChanged" in tgtScene.sceneDiffResult,
            "ParentChanged" in tgtScene.sceneDiffResult,
            "ObjectChanged" in tgtScene.sceneDiffResult,
            "NewComer" in tgtScene.sceneDiffResult,
            "Deleted" in tgtScene.sceneDiffResult,
            not tgtScene.cameraNode.IsIdentical,
            not tgtScene.systemNode.IsIdentical,
        )
        # default option
        defUpdOpt = set()
        for i in range(7):
            if chgs[i] and self.option.restoreOptions[i]:
                defUpdOpt.add(SSSB_Scene.VALID_COMPARE_RESULT[i+1])
        # return
        if not any(chgs):
            self.showMsg("Nothing need restore")
            return None, None # nothing need restore
        else:
            return chgs, defUpdOpt
    
    def doRestoreScene(self, tgtScene, options):
        defRestoreSeq = ("Deleted", "ObjectChanged", "ParentChanged", "System", "Camera", "NewComer", "StatusChanged")
        restoreSeq = list([wp for wp in defRestoreSeq if wp in options])
        # check option
        if len(restoreSeq):
            print("\nSSSB: Restore scene %s by "%tgtScene.name + " > ".join(restoreSeq))
        else:
            print("\nSSSB: Restore scene %s with no option, auto restore status change"%tgtScene.name)
            restoreSeq = ["StatusChanged"]
        # regist work
        restoreSeq.append("-rescan-")
        self.game.append_update_job(SceneSaveStateBuilder.RestoreJob, (self, tgtScene, restoreSeq))

    @staticmethod
    def RestoreJob(game, restoreJobs):
        sssb, tgtScene, restoreSeq = restoreJobs
        # do restore
        curJob = restoreSeq.pop(0)
        if True:
            if "Deleted" == curJob:
                msg = tgtScene.reCreateDeletedNodes(game)
                if msg:
                    sssb.showMsg(msg)

            elif "ObjectChanged" == curJob:
                msg = tgtScene.restoreNodeObject(game)
                if msg:
                    sssb.showMsg(msg)

            elif "ParentChanged" == curJob:
                msg = tgtScene.restoreNodeParent(game)
                if msg:
                    sssb.showMsg(msg)

            elif "System" == curJob:
                msg = tgtScene.restoreSystemStatus(game)
                if msg:
                    sssb.showMsg(msg)

            elif "StatusChanged" == curJob:
                msg = tgtScene.restoreNodeStatus(game)
                if msg:
                    sssb.showMsg(msg)

            elif "Camera" == curJob:
                msg = tgtScene.restoreCameraStatus(game)
                if msg:
                    sssb.showMsg(msg)

            elif "NewComer" == curJob:
                msg = tgtScene.disableNewComer(game)
                if msg:
                    sssb.showMsg(msg)

            elif "-rescan-" == curJob:
                msg = tgtScene.diffFromCurrent(game)
                if msg:
                    sssb.showMsg(msg)

            else:
                pass

        # check done
        return len(restoreSeq) == 0

    def renameAllScene(self):
        for i in range(self.TotalSceneCount):
            ss = self.scene[i]
            if ss.name.startswith("Scene #"):
                ss.name = "Scene #" + str(i + 1)

    def moveCurrentScene(self, direction):
        if direction == 1 and 0 <= self.curSceneIndex < self.TotalSceneCount - 1:
            swapIndex = self.curSceneIndex + 1
        elif direction == -1 and 0 < self.curSceneIndex < self.TotalSceneCount:
            swapIndex = self.curSceneIndex - 1
        else:
            return
        self.scene[self.curSceneIndex], self.scene[swapIndex] = self.scene[swapIndex], self.scene[self.curSceneIndex]
        self.curSceneIndex = swapIndex
        self.renameAllScene()

    def setCurScene(self, tgtScene):
        if tgtScene in self.scene:
            self.curSceneIndex = self.scene.index(tgtScene)
            self.curNode = None
        else:
            self.curSceneIndex = -1
            self.curNode = None

    def setCurNode(self, tgtNode):
        if tgtNode == None:
            self.curNode = None
            return
        if self.CurScene == None:
            self.showMsg("Cannot set current node when no scene is selected as current")
            return
        if self.CurScene.isSavedNode(tgtNode) or self.CurScene.isDiffNode(tgtNode):
            if tgtNode.Saved:
                self.curNode = tgtNode
            else:
                self.showMsg("Cannot set a non-object and non-saved node as current node")
        else:
            self.showMsg("Cannot set current node, it is not belong to current scene")

    def showMsg(self, msg):
        self.game.logger_log_message(msg, "SSSB: ")

class SSSB_Scene():
    VALID_COMPARE_RESULT = ("Identical", "StatusChanged", "ParentChanged", "ObjectChanged", "NewComer", "Deleted", "Camera", "System", "NoneObjMatch", "NoneObjMiss", "NoneObj")
    DEBUG_REPORT_SCENE_DIFF_DETAIL = False
    STOP_RECREATE_CHILD_WHEN_FAILED = True

    def __init__(self):
        self.name = None
        self.systemNode = None
        self.cameraNode = None
        self.additionalCameraNodes = []
        self.rootNodes = []
        self.nodesList = []
        self.nodesDict = {}
        self.thumbTex = None
        # diff
        self.sceneDiffRootNodes = []
        self.sceneDiffNodesList = []
        self.sceneDiffKeys = []
        self.sceneDiffDict = {}
        self.sceneDiffResult = {}
        self.sceneDiffCameraNode = None
        self.sceneDiffSystemNode = None

    def exportSetting(self):
        from vnactor import bytearray_to_str64

        setting = {}
        # basic
        setting["name"] = self.name
        # nodes
        setting["systemNode"] = self.systemNode.exportSetting()
        setting["cameraNode"] = self.cameraNode.exportSetting()
        setting["additionalCameraNodes"] = []
        for cNode in self.additionalCameraNodes:
            setting["additionalCameraNodes"].append(cNode.exportSetting())
        setting["rootNodes"] = []
        for rNode in self.rootNodes:
            setting["rootNodes"].append(rNode.exportSetting())
        # thumb
        if self.thumbTex == None:
            setting["thumbTex"] = None
        else:
            ba = Texture2D2BytesPNG(self.thumbTex)
            setting["thumbTex"] = bytearray_to_str64(ba)
        return setting

    def createFromSetting(self, game, setting):
        from vnactor import str64_to_bytearray
        try:
            # name
            self.name = loadData(setting, "name")
            print("\nSSSB: Load scene: %s"%self.name)
            # system
            self.systemNode = SSSB_Node.CreateFromSetting(game, setting["systemNode"])
            # camera
            self.cameraNode = SSSB_Node.CreateFromSetting(game, setting["cameraNode"])
            # root nodes
            self.additionalCameraNodes = []
            for cs in loadData(setting, "additionalCameraNodes", []):
                cNode = SSSB_Node.CreateFromSetting(game, cs)
                if cNode: self.additionalCameraNodes.append(cNode)
            # root nodes
            self.rootNodes = []
            for rs in setting["rootNodes"]:
                rNode = SSSB_Node.CreateFromSetting(game, rs)
                if rNode: self.rootNodes.append(rNode)
            # all nodes list
            self.listAllNodes()
            # create nodes dict
            self.createNodesDic(game)
            # diff current
            self.diffFromCurrent(game)
            # registered actor/prop info
            # thumb
            thumbData = loadData(setting, "thumbTex")
            if thumbData == None:
                self.thumbTex = None
            else:
                ba = str64_to_bytearray(thumbData)
                self.thumbTex = BytesPNG2Texture2D(ba)
            # Done
            return None
        except Exception as e:
            traceback.print_exc()
            return "Fail to create from setting: " + str(e)

    def createFromCurrent(self, game, name, selected, thumb):
        try:
            print("\nSSSB: Create new scene: %s"%name)
            import vnactor
            # name
            self.name = name
            # system
            self.systemNode = SSSB_Node.CreateFromStudioSystem(game)
            # camera
            self.cameraNode = SSSB_Node.CreateFromStudioCamera(game)
            # root nodes
            self.rootNodes = []
            treeNodeCtrl = game.studio.treeNodeCtrl
            for tno in treeNodeCtrl.m_TreeNodeObject:
                if selected and tno not in treeNodeCtrl.selectNodes:
                    continue
                rNode = SSSB_Node.CreateFromTreeNodeObject(game, tno)
                self.rootNodes.append(rNode)
            # all nodes list
            self.listAllNodes()
            # create nodes dict
            self.createNodesDic(game)
            # diff current... should be identical
            self.diffFromCurrent(game)
            # registered actor/prop info
            # thumb
            if thumb == None:
                self.thumbTex = SSSB_Scene.CaptureThumb(game)
            else:
                self.thumbTex = thumb
            # Done
            return None
        except Exception as e:
            traceback.print_exc()
            return "Fail to create current scene: " + str(e)

    def listAllNodes(self):
        self.nodesList = []
        
        def listSubNodes(pNode):
            subNodes = []
            for sNode in pNode.child:
                subNodes.append(sNode)
                subNodes.extend(listSubNodes(sNode))
            return subNodes
        # workspace nodes
        for rNode in self.rootNodes:
            self.nodesList.append(rNode)
            self.nodesList.extend(listSubNodes(rNode))
        # studio nodes
        self.nodesList.append(self.cameraNode)
        self.nodesList.append(self.systemNode)

        print("  Collected %d nodes"%len(self.nodesList))

    def createNodesDic(self, game):
        self.nodesDict = {}
        """
        for key in game.studio.dicObjectCtrl.Keys:
            for node in self.nodesList:
                if node.DicKey == key:
                    self.nodesDict[key] = node
                    break
        """
        for node in self.nodesList:
            if node.DicKey != None and node.DicKey in game.studio.dicObjectCtrl.Keys:
                self.nodesDict[node.DicKey] = node
        print("  Collected %d objects"%len(self.nodesDict))

    def isSavedNode(self, node):
        return node in self.nodesList

    def isDiffNode(self, node):
        return node in self.sceneDiffNodesList

    def diffFromCurrent(self, game):

        def compareNode(node, searchNodes, foundKeys):
            if node.objctrl == None:
                # not an object
                for tnode in searchNodes:
                    if tnode.Name == node.Name:
                        # found
                        node.compareResult = "NoneObjMatch"
                        # compare children
                        for cnode in node.child:
                            compareNode(cnode, tnode.child, foundKeys)
                        return
                # not found
                node.compareResult = "NoneObjMiss"
                return
            else:
                # node is object
                for tnode in searchNodes:
                    if tnode.objctrl == node.objctrl:
                        # found
                        if tnode.DicKey != node.DicKey:
                            #node.compareResult = "DicKeyChanged"
                            raise Exception("DicKey changed? this is not expected")
                        elif tnode.checkStatusChange(node.status):
                            node.compareResult = "StatusChanged"
                        else:
                            node.compareResult = "Identical"
                        # add to found
                        foundKeys.add(tnode.DicKey)
                        # compare children
                        for cnode in node.child:
                            compareNode(cnode, tnode.child, foundKeys)
                        return
                # not found
                node.compareResult = "-NotFound-"
                return
        def registerNodeDict(node):
            self.sceneDiffNodesList.append(node)
            if node.DicKey != None:
                self.sceneDiffKeys.append(node.DicKey)
                self.sceneDiffDict[node.DicKey] = node
            for snode in node.child:
                registerNodeDict(snode)
        def registerCompareResult(node):
            if node.compareResult in self.sceneDiffResult:
                self.sceneDiffResult[node.compareResult].append(node)
            else:
                self.sceneDiffResult[node.compareResult] = [node]
        def debugPrint(msg):
            if SSSB_Scene.DEBUG_REPORT_SCENE_DIFF_DETAIL:
                print(msg)

        try:
            debugPrint("\nSSSB: Compare [%s (%d saved objects)] to current scene:"%(self.name, len(self.nodesDict)))
            self.sceneDiffRootNodes = []
            self.sceneDiffNodesList = []
            self.sceneDiffKeys = []
            self.sceneDiffDict = {}
            self.sceneDiffResult = {}
            foundKeysInSavedScene = set()

            # phase 1
            treeNodeCtrl = game.studio.treeNodeCtrl
            for tno in treeNodeCtrl.m_TreeNodeObject:
                rNode = SSSB_Node.CreateFromTreeNodeObject(game, tno)
                registerNodeDict(rNode)
                compareNode(rNode, self.rootNodes, foundKeysInSavedScene)
                self.sceneDiffRootNodes.append(rNode)
            if len(self.sceneDiffKeys) != len(self.sceneDiffDict):
                raise Exception("Phase 1 error! DicKeys(%d) collected in current scene not match with DicKey-Node entries(%d)!"%(len(self.sceneDiffKeys), len(self.sceneDiffDict)))
            if len(self.sceneDiffKeys) != len(self.sceneDiffDict):
                raise Exception("Phase 1 error! DicKeys(%d) collected in current scene not match with studio dicObjectCtrl entries(%d)!"%(len(self.sceneDiffKeys), len(game.studio.dicObjectCtrl)))
            debugPrint("  Compare phase 1: Total %d objects in current scene, %d matched with saved scene"%(len(self.sceneDiffDict), len(foundKeysInSavedScene)))
            #print("foundKeysInSavedScene:" + str(foundKeysInSavedScene))

            # phase 2
            if len(foundKeysInSavedScene) == len(self.sceneDiffKeys):
                debugPrint("  Compare phase 2: SKIP, all current object found in saved scene")
            else:
                notfoundKeysInCurScene = []
                for k in self.sceneDiffKeys:
                    if k not in foundKeysInSavedScene:
                        notfoundKeysInCurScene.append(k)
                totalNotFound = len(notfoundKeysInCurScene)
                totalNewComer = 0
                totalParentChange = 0
                totalObjectChange = 0
                totalNewFound = 0

                while len(notfoundKeysInCurScene) > 0:
                    dicKey = notfoundKeysInCurScene.pop(0)
                    mNode = self.sceneDiffDict[dicKey]
                    if dicKey in self.nodesDict:
                        # found in saved scene
                        if mNode.objctrl == self.nodesDict[dicKey].objctrl:
                            mNode.compareResult = "ParentChanged"
                            totalParentChange += 1
                        else:
                            mNode.compareResult = "ObjectChanged"
                            totalObjectChange += 1
                        foundKeysInSavedScene.add(dicKey)
                        # match child node
                        newFound = set()
                        for cnode in mNode.child:
                            compareNode(cnode, self.nodesDict[dicKey].child, newFound)
                        foundKeysInSavedScene.update(newFound)
                        for nk in newFound:
                            notfoundKeysInCurScene.remove(nk)
                        totalNewFound += len(newFound)
                    else:
                        # not found in saved
                        mNode.compareResult = "NewComer"
                        registerCompareResult(mNode)
                        totalNewComer += 1
                if totalNotFound != totalNewComer + totalParentChange + totalObjectChange + totalNewFound:
                    raise Exception("Phase 2 error! NewComer(%d) + ParentChange(%d) + ObjectChange(%d) + NewFound(%d) != totalNotFound(%d)"%(totalNewComer, totalParentChange, totalObjectChange, totalNewFound, totalNotFound))
                debugPrint("  Compare phase 2: Detect %d new comer, %d nodes changed parent, %d nodes changed object, and %d nodes matched again"%(totalNewComer, totalParentChange, totalObjectChange, totalNewFound))
                #debugPrint("foundKeysInSavedScene:" + str(foundKeysInSavedScene))

            # phase 3
            self.sceneDiffDeletedKey = []
            for dNode in self.nodesList:
                dKey = dNode.DicKey
                if dKey != None:
                    #if dNode.treeNodeObj == None:
                    #    dNode.compareResult = "Deleted"
                    #    self.sceneDiffDeletedKey.append(dKey)
                    #else:
                    #    dNode.compareResult = self.sceneDiffDict[dKey].compareResult
                    if dKey in foundKeysInSavedScene:
                        dNode.compareResult = self.sceneDiffDict[dKey].compareResult
                    else:
                        dNode.compareResult = "Deleted"
                        self.sceneDiffDeletedKey.append(dKey)
                    registerCompareResult(dNode)
                else:
                    dNode.compareResult = "NoneObj"
            if len(self.nodesDict) - len(foundKeysInSavedScene) != len(self.sceneDiffDeletedKey):
                raise Exception("Phase 3 error! Saved nodes(%d) != found nodes(%d) + deleted nodes(%d)"%(len(self.nodesDict), len(foundKeysInSavedScene), len(self.sceneDiffDeletedKey)))
            if len(foundKeysInSavedScene) == len(self.nodesDict):
                debugPrint("  Compare phase 3: DONE, all saved object found in current scene")
            else:
                debugPrint("  Compare phase 3: DONE, %d objects deleted from current scene"%(len(self.sceneDiffDeletedKey)))
                for dKey in self.sceneDiffDeletedKey:
                    debugPrint("    dicKey: %d, path: %s"%(dKey, self.nodesDict[dKey].Path))

            # phase 4
            self.sceneDiffCameraNode = SSSB_Node.CreateFromStudioCamera(game)
            camDiff = self.cameraNode.checkStatusChange(self.sceneDiffCameraNode.status)
            self.cameraNode.compareResult = self.sceneDiffCameraNode.compareResult = SSSB_Scene.VALID_COMPARE_RESULT[1 if camDiff else 0]
            #registerCompareResult(self.cameraNode)
            self.sceneDiffSystemNode = SSSB_Node.CreateFromStudioSystem(game)
            sysDiff = self.systemNode.checkStatusChange(self.sceneDiffSystemNode.status)
            self.systemNode.compareResult = self.sceneDiffSystemNode.compareResult = SSSB_Scene.VALID_COMPARE_RESULT[1 if sysDiff else 0]
            #registerCompareResult(self.systemNode)

            return None
        except Exception as e:
            traceback.print_exc()
            return "Fail to diff from current scene: " + str(e)

    def reCreateDeletedNodes(self, game):
        def recreateRecursion(node):
            failed = 0
            if node.IsObj and node.IsDeleted:
                print("  Try to re-create %s node %s"%(node.type, node.Name))
                msg = node.reCreate(game)
                if msg:
                    print("    > Failed: " + msg)
                    # stop re-create subnodes
                    if SSSB_Scene.STOP_RECREATE_CHILD_WHEN_FAILED:
                        return 1
            for snode in node.child:
                failed += recreateRecursion(snode)
            return failed

        # re-create deleted nodes
        try:
            allFail = 0
            if "Deleted" in self.sceneDiffResult:
                for node in self.rootNodes:
                    allFail += recreateRecursion(node)
            return None if allFail == 0 else "Fail to re-create %d objects"%allFail
        except Exception as e:
            return "Re-create deleted objects failed: " + str(e)

    def restoreNodeParent(self, game):
        # restore parent change
        try:
            if "ParentChanged" in self.sceneDiffResult:
                for node in self.sceneDiffResult["ParentChanged"]:
                    if node.IsObj:
                        print("  Try to restore parent of %s to %s"%(node.Name, "root" if node.parent == None else node.parent.Name))
                        node.restoreParent(game)
            return None
        except Exception as e:
            return "Restore parent change failed: " + str(e)

    def restoreNodeObject(self, game):
        # restore object change
        try:
            if "ObjectChanged" in self.sceneDiffResult:
                for node in self.sceneDiffResult["ObjectChanged"]:
                    if node.IsObj:
                        print("  Try to restore status of %s to another object"%(node.Name))
                        tgtNode = self.sceneDiffDict[node.DicKey]
                        curStatus = tgtNode.vnObj.export_full_status()
                        updStatus = node.filterRestoreStatus(curStatus)
                        tgtNode.vnObj.import_status(updStatus)
            return None
        except Exception as e:
            return "Restore object change failed: " + str(e)

    def restoreNodeStatus(self, game):
        # restore status changed
        try:
            if "StatusChanged" in self.sceneDiffResult:
                for node in self.sceneDiffResult["StatusChanged"]:
                    if node.IsObj:
                        node.restoreStatus(game)

            return None
        except Exception as e:
            return "Restore scene failed: " + str(e)

    def restoreCameraStatus(self, game):
        # restore camera changed
        try:
            if not self.cameraNode.IsIdentical:
                self.cameraNode.restoreStatus(game)
            return None
        except Exception as e:
            return "Restore camera failed: " + str(e)

    def restoreSystemStatus(self, game):
        # restore system changed
        try:
            if not self.systemNode.IsIdentical:
                self.systemNode.restoreStatus(game)
            return None
        except Exception as e:
            return "Restore system failed: " + str(e)

    def disableNewComer(self, game):
        # disable new comer
        try:
            if "NewComer" in self.sceneDiffResult:
                for node in self.sceneDiffResult["NewComer"]:
                    node.disable(game)
            return None
        except Exception as e:
            return "Disable new comer failed: " + str(e)

    def reAssignNode(self, game, node, treeNodeObject, tryMergeChildren):
        def trySearchAndMergeChildren(nodeChild, tnoChild):
            for nc in nodeChild:
                for tc in tnoChild:
                    if nc.Name == tc.textName:
                        if nc.IsObj:
                            msg = self.reAssignNode(game, nc, tc, False)
                            if msg:
                                print("  Assign child node '%s' failed: %s"%(nc.Path, msg))
                                continue    # failed to merge object child, try next one
                            else:
                                print("  Assign child node '%s' successed!"%(nc.Path))
                        trySearchAndMergeChildren(nc.child, tc.child)

        # check
        if node not in self.nodesList:
            return "Fail to re-assign node: not saved node in this scene"
        if node.DicKey == None:
            return "Fail to re-assign node: not an object node"
        if node.treeNodeObj == treeNodeObject:
            return "Fail to re-assign node: no need to re-assign to itself"
        for aNode in self.nodesList:
            if aNode.IsTreeNode and aNode.treeNodeObj == treeNodeObject:
                return "Fail to re-assign node: target object is already saved in this scene."
        # do merge
        try:
            # merge node
            oldDicKey = node.DicKey
            msg = node.reAssignTreeNodeObject(game, treeNodeObject)
            if msg:
                return msg
            # check and register in dict
            if node.DicKey == None:
                raise Exception("Unexpected! re-assign node should have a VALID DicKey!")
            if node != self.nodesDict.pop(oldDicKey):
                raise Exception("Unexpected! re-assign node object should match!")
            self.nodesDict[node.DicKey] = node
            # try merge children
            if tryMergeChildren:
                trySearchAndMergeChildren(node.child, treeNodeObject.child)
            return None
        except Exception as e:
            return "Fail to re-assign node: " + str(e)

    def reAssignObjectChange(self, game):
        try:
            for node in self.nodesList:
                if node.IsObjectChanged:
                    tgtNode = self.sceneDiffDict[node.DicKey] 
                    tgtTno = tgtNode.treeNodeObj
                    if tgtTno != game.studio.dicObjectCtrl[node.DicKey].treeNodeObject:
                        raise Exception("Current scene and sceneDiff check not match?")
                    self.reAssignNode(game, node, tgtTno, False)
            return None
        except Exception as e:
            return "Re-assign object changed nodes failed: " + str(e)

    def appendNode(self, game, treeNodeObject):
        def searchAppendPosition(tno):
            path = []
            while tno.parent:
                tno = tno.parent
                path.append(tno)
            path.reverse()

            # sequence may be not match
            appendNodeList = self.rootNodes
            while len(path):
                ptno = path.pop(0)
                matchedNode = None
                for sNode in appendNodeList:
                    if sNode.treeNodeObj == ptno:
                        matchedNode = sNode
                        break
                if matchedNode != None:
                    appendNodeList = matchedNode.child
                else:
                    return None
            return appendNodeList

        try:
            if treeNodeObject == None or treeNodeObject not in game.studio.dicInfo:
                raise Exception("TreeNodeObject not valid in current scene")

            node = SSSB_Node.CreateFromTreeNodeObject(game, treeNodeObject)
            
            appendNodeList = searchAppendPosition(node.treeNodeObj)
            if appendNodeList == None:
                raise Exception("The parent of selected node not found in scene.")
            
            appendNodeList.append(node)

            # update info
            self.listAllNodes()
            self.createNodesDic(game)

        except Exception as e:
            return "Append node failed: " + str(e)

    def appendAllNewComer(self, game):
        def addNewComerInList(nodeList):
            for node in nodeList:
                if node.IsNewComer:
                    self.appendNode(game, node.treeNodeObj)
                else:
                    addNewComerInList(node.child)
        try:
            addNewComerInList(self.sceneDiffRootNodes)
            return None
        except Exception as e:
            return "Re-assign object changed nodes failed: " + str(e)

    def removeNode(self, tgtNode):
        def removeAllChildNodes(node):
            for sNode in node.child:
                # deal child first
                removeAllChildNodes(sNode)
                # from nodesList
                self.nodesList.remove(sNode)
                # from nodesDict with check
                if sNode.DicKey in self.nodesDict:
                    self.nodesDict.pop(sNode.DicKey)

        try:
            if not tgtNode in self.nodesList:
                raise Exception("Target node <%s> not belong to current scene"%tgtNode.Path)
            if not tgtNode.IsObj:
                raise Exception("Target node <%s> is not map to an object"%tgtNode.Path)
            # delete all children
            removeAllChildNodes(tgtNode)
            # from rootNodes
            if tgtNode.parent:
                tgtNode.parent.child.remove(tgtNode)
            else:
                self.rootNodes.remove(tgtNode)
            # from nodesList
            self.nodesList.remove(tgtNode)
            # from nodesDict
            self.nodesDict.pop(tgtNode.DicKey)
            # all compare result should be redo
            return None
        except Exception as e:
            traceback.print_exc()
            return "Remove node failed: " + str(e)
        
    @staticmethod
    def CaptureThumb(game):
        return BytesPNG2Texture2D(game.makePngScreenshot(160, 90))

class SSSB_Node():
    PATH_SEP = " > "
    KNOWN_NOT_SUPPORT_OBJTYPE = {"OCIRoutePoint"}
    CHECK_VALUE_IGNORE_SMALL_CHANGE = True
    CHECK_VALUE_MINOR_THRESHOLD = 0.001
    DEBUG_REPORT_STATUS_DIFF_DETAIL = False
    DEBUG_REPORT_STATUS_FILTER_DETAIL = False

    @property
    def Name(self):
        if self.IsTreeNode:
            return self.treeNodeObj.textName
        else:
            return self.alias

    @property
    def IsTreeNode(self):
        return self.treeNodeObj != None

    @property
    def IsObj(self):
        return self.vnObj != None

    @property
    def Saved(self):
        return self.status != None

    @property
    def Path(self):
        if self.parent != None:
            return self.parent.Path + SSSB_Node.PATH_SEP + self.Name
        else:
            return self.Name

    @property
    def DicKey(self):
        if self.objctrl != None:
            return self.objctrl.objectInfo.dicKey
        return None

    @property
    def ParentKey(self):
        pNode = self.parent
        while pNode != None and pNode.DicKey == None:
            pNode = pNode.parent
        if pNode != None:
            return pNode.DicKey
        else:
            return None

    @property
    def IsIdentical(self):
        return self.compareResult == SSSB_Scene.VALID_COMPARE_RESULT[0]

    @property
    def IsStatusChanged(self):
        return self.compareResult == SSSB_Scene.VALID_COMPARE_RESULT[1]

    @property
    def IsParentChanged(self):
        return self.compareResult == SSSB_Scene.VALID_COMPARE_RESULT[2]

    @property
    def IsObjectChanged(self):
        return self.compareResult == SSSB_Scene.VALID_COMPARE_RESULT[3]

    @property
    def IsNewComer(self):
        return self.compareResult == SSSB_Scene.VALID_COMPARE_RESULT[4]

    @property
    def IsDeleted(self):
        return self.compareResult == SSSB_Scene.VALID_COMPARE_RESULT[5]

    @property
    def ChildCount(self):
        return len(self.child)

    def __init__(self):
        # basic data
        self.alias = ""
        self.treeNodeObj = None
        self.objctrl = None
        # type data
        self.type = "unknown"
        self.vnObj = None
        self.vnTag = None
        self.recreateInfo = None
        # family info
        self.parent = None
        self.child = []
        # status
        self.status = None
        self.ignoreProperties = set()
        # compare
        self.compareResult = None

    def __str__(self):
        return self.Name

    def exportSetting(self):
        setting = {}
        # basic data
        setting["ver"] = "1"
        setting["alias"] = self.alias
        setting["recreateInfo"] = self.recreateInfo
        setting["status"] = self.status
        setting["ignoreProperties"] = tuple(self.ignoreProperties)
        # rebuild data
        setting["dicKey"] = self.DicKey
        setting["type"] = self.type
        setting["vnTag"] = self.vnTag
        # child
        setting["child"] = []
        for cNode in self.child:
            setting["child"].append(cNode.exportSetting())
        # done
        return setting

    def initFromSetting(self, game, setting, parentNode):
        # basic data
        self.alias = loadData(setting, "alias", "")
        self.recreateInfo = loadData(setting, "recreateInfo")
        self.status = loadData(setting, "status")
        self.ignoreProperties = set(loadData(setting, "ignoreProperties", ()))
        # rebuild data
        dicKey = loadData(setting, "dicKey")
        rType = loadData(setting, "type", "unknown")
        vnTag = loadData(setting, "vnTag")
        if dicKey == None:
            # check non-obj node type
            if rType in {"unknown", "-camera-", "-system-"}:
                self.type = rType
            else:
                raise Exception("Fail to restore node: dicKey =  None, type = " + rType)
            # build treeNodeObj if possible
            if parentNode != None and parentNode.treeNodeObj != None:
                for subTNO in parentNode.treeNodeObj.child:
                    if subTNO.textName == self.alias:
                        self.treeNodeObj = subTNO
                        break
        else:
            # rebuild by dicKey
            if dicKey in game.studio.dicObjectCtrl:
                objctrl = game.studio.dicObjectCtrl[dicKey]
                oci = HSNeoOCI.create_from(objctrl)
            else:
                raise Exception("Fail to restore node: dicKey = %d not found in scene dic"%dicKey)
            if objctrl == None:
                raise Exception("Fail to restore node: got an objctrl == None")
            # check type
            recreateInfo = None
            if isinstance(oci, HSNeoOCIChar):
                type = "char"
                vnObj = oci.as_actor
            elif isinstance(oci, HSNeoOCIItem):
                type = "item"
                vnObj = oci.as_prop
                recreateInfo = SSSB_Node.GetItemInfo(objctrl)
            elif isinstance(oci, HSNeoOCIFolder):
                type = "folder"
                vnObj = oci.as_prop
            elif isinstance(oci, HSNeoOCILight):
                type = "light"
                vnObj = oci.as_prop
            elif isinstance(oci, HSNeoOCIRoute):
                type = "route"
                vnObj = oci.as_prop
            elif isinstance(oci, HSNeoOCICamera):
                type = "camera"
                vnObj = oci.as_prop
            elif isinstance(oci, HSNeoOCIText):
                type = "text"
                vnObj = oci.as_prop
            else:
                raise Exception("Fail to restore node: unknown node type from objctrl <%s>"%str(objctrl))
            if type != rType:
                raise Exception("Fail to restore node: node type mismatch! Expect for <%s> but found <%s>"%(rType, type))
            if recreateInfo != self.recreateInfo:
                raise Exception("Fail to restore node: node recreate info mismatch! Expect for <%s> but found <%s>"%(str(self.recreateInfo), str(recreateInfo)))
            # check vnTag conflict
            # TODO
            # setup
            self.treeNodeObj = vnObj.treeNodeObject
            self.objctrl = objctrl
            self.type = rType
            self.vnObj = vnObj
            self.vnTag = vnTag
        # set parent
        self.parent = parentNode
        # load child
        for cs in loadData(setting, "child", []):
            cNode = SSSB_Node.CreateFromSetting(game, cs, self)
            if cNode: self.child.append(cNode)

    def initFromTreeNodeObject(self, game, treeNodeObject):
        # basic data
        self.treeNodeObj = treeNodeObject
        self.alias = treeNodeObject.textName
        if treeNodeObject in game.studio.dicInfo:
            self.objctrl = game.studio.dicInfo[treeNodeObject]
            oci = HSNeoOCI.create_from(self.objctrl)
        else:
            self.objctrl = None
        # type dependent init
        self.type = "unknown"
        self.vnObj = None
        if self.objctrl != None:
            if isinstance(oci, HSNeoOCIChar):
                self.type = "char"
                self.vnObj = oci.as_actor
            elif isinstance(oci, HSNeoOCIItem):
                self.type = "item"
                self.vnObj = oci.as_prop
                self.recreateInfo = SSSB_Node.GetItemInfo(self.objctrl)
            elif isinstance(oci, HSNeoOCIFolder):
                self.type = "folder"
                self.vnObj = oci.as_prop
            elif isinstance(oci, HSNeoOCILight):
                self.type = "light"
                self.vnObj = oci.as_prop
            elif isinstance(oci, HSNeoOCIRoute):
                self.type = "route"
                self.vnObj = oci.as_prop
            elif isinstance(oci, HSNeoOCICamera):
                self.type = "camera"
                self.vnObj = oci.as_prop
            elif isinstance(oci, HSNeoOCIText):
                self.type = "text"
                self.vnObj = oci.as_prop
            else:
                typestr = str(type(self.objctrl))[7:-2]
                if typestr not in SSSB_Node.KNOWN_NOT_SUPPORT_OBJTYPE:
                    print("Object '%s' not supported: this object's type '%s' is not supported"%(self.Name, typestr))
        # family info
        self.parent = None
        self.child = []
        for tno in treeNodeObject.child:
            cNode = SSSB_Node.CreateFromTreeNodeObject(game, tno)
            cNode.parent = self
            self.child.append(cNode)
        # status 
        self.status = None
        if self.IsObj:
            #game.append_update_job(SSSB_Node.GetNodeStatus, self)
            SSSB_Node.GetNodeStatus(game, self)

    def initFromCamera(self, game):
        import vnactor
        self.alias = "Studio Camera"
        self.type = "-camera-"
        self.status = vnactor.export_camera_status(game)

    def initFromSystem(self, game):
        import vnactor
        self.alias = "Studio Environment"
        self.type = "-system-"
        self.status = vnactor.export_sys_status(game)

    def updateStatus(self, game):
        if self.IsTreeNode:
            self.alias = self.treeNodeObj.textName
        if self.IsObj:
            SSSB_Node.GetNodeStatus(game, self)
        elif self.type == "-camera-":
            import vnactor
            self.status = vnactor.export_camera_status(game)
        elif self.type == "-system-":
            import vnactor
            self.status = vnactor.export_sys_status(game)
        else:
            raise Exception("Unknown node to update, not a vnnode or supported node")

    def restoreStatus(self, game):
        if self.IsObj:
            curStatus = self.vnObj.export_full_status()
            updStatus = self.filterRestoreStatus(curStatus)
            self.vnObj.import_status(updStatus)
        elif self.type == "-camera-":
            import vnactor
            curStatus = vnactor.export_camera_status(game)
            updStatus = self.filterRestoreStatus(curStatus)
            vnactor.import_camera_status(game, updStatus)
        elif self.type == "-system-":
            import vnactor
            curStatus = vnactor.export_sys_status(game)
            updStatus = self.filterRestoreStatus(curStatus)
            vnactor.import_sys_status(game, updStatus)
        else:
            raise Exception("Unknown node to restore, not a vnnode or supported node")

    def restoreParent(self, game):
        if self.IsObj:
            game.studio.treeNodeCtrl.SetParent(self.treeNodeObj, None if self.parent == None else self.parent.treeNodeObj)
            self.restoreStatus(game)
        else:
            raise Exception("Only object node can change parent")

    def disable(self, game):
        if self.IsObj:
            self.treeNodeObj.visible = False
        if self.type == "light":
            from vnactor import prop_enable
            prop_enable(self.vnObj, 0)

    def reCreate(self, game):
        # create item
        if self.type == "item" and isinstance(self.recreateInfo, tuple):
            # item
            (group, category, no) = self.recreateInfo

            from extplugins import Sideloader
            sl = Sideloader()
            no = sl.UniGetUARLocalSlotId(no)

            from Studio import AddObjectItem
            if group != None:
                ociObj = AddObjectItem.Add(group, category, no)
            else:
                ociObj = AddObjectItem.Add(no)
        elif self.type == "folder":
            # folder
            from Studio import AddObjectFolder
            ociObj = AddObjectFolder.Add()
            ociObj.name = self.alias
        else:
            return "Re-create for this '%s' object not supported yet"%self.type
        # re-assign
        self.reAssignTreeNodeObject(game, ociObj.treeNodeObject)
        self.restoreParent(game)
        return None

    def filterRestoreStatus(self, tgtStatus):
        def debugMsg(msg):
            if SSSB_Node.DEBUG_REPORT_STATUS_FILTER_DETAIL:
                print(msg)

        fStatus = {}
        debugMsg("\nFilter restore status for: %s (%s)"%(self.Name, self.type))
        for key in self.status.keys():
            if key in self.ignoreProperties:
                continue
            if key not in tgtStatus:
                continue
            if SSSB_Node.CheckValueChanged(self.type, key, self.status[key], tgtStatus[key]):
                debugMsg("  <%s> saved:"%key + str(self.status[key]))
                debugMsg("  >> current:" + str(tgtStatus[key]))
                fStatus[key] = self.status[key]
        return fStatus

    def checkStatusChange(self, tgtStatus):
        def debugMsg(msg):
            if SSSB_Node.DEBUG_REPORT_STATUS_DIFF_DETAIL:
                print(msg)
        # total
        if not self.Saved:
            debugMsg("<%s> can not check status change because it has no status saved"%self.Name)
            return True
        if tgtStatus == None:
            debugMsg("<%s> fail to check status: Missed target status"%self.Name)
            return True
        # every
        for key in self.status.keys():
            if key in self.ignoreProperties:
                continue
            if key not in tgtStatus:
                debugMsg("<%s> changed: <%s> not in target status"%(self.Name, key))
                return True
            elif SSSB_Node.CheckValueChanged(self.type, key, self.status[key], tgtStatus[key]):
                debugMsg("<%s['%s']> value change from:\n  %s\n>>to:\n  %s"%(self.Name, key, str(self.status[key]), str(tgtStatus[key])))
                return True
        # Done
        return False

    def reAssignTreeNodeObject(self, game, treeNodeObject):
        def reAssginNonObjSubNodes(node, tno):
            for cNode in node.child:
                if not cNode.IsObj:
                    for cTno in tno.child:
                        if cTno.textName == cNode.Name:
                            # update none-obj node
                            cNode.treeNodeObj = cTno
                            # for child
                            reAssginNonObjSubNodes(cNode, cTno)
                            break
        
        # check treeNodeObject
        if treeNodeObject in game.studio.dicInfo:
            objctrl = game.studio.dicInfo[treeNodeObject]
            oci = HSNeoOCI.create_from(objctrl)
        else:
            return "Can not found objctrl by TreeNodeObject"
        # check change
        if objctrl == self.objctrl:
            return "Can not re-assign to itself"
        # type dependent init
        type = "unknown"
        vnObj = None
        recreateInfo = None
        if objctrl != None:
            if isinstance(oci, HSNeoOCIChar):
                type = "char"
                vnObj = oci.as_actor
            elif isinstance(oci, HSNeoOCIItem):
                type = "item"
                vnObj = oci.as_prop
                recreateInfo = SSSB_Node.GetItemInfo(objctrl)
            elif isinstance(oci, HSNeoOCIFolder):
                type = "folder"
                vnObj = oci.as_prop
            elif isinstance(oci, HSNeoOCILight):
                type = "light"
                vnObj = oci.as_prop
            elif isinstance(oci, HSNeoOCIRoute):
                type = "route"
                vnObj = oci.as_prop
            elif isinstance(oci, HSNeoOCICamera):
                type = "camera"
                vnObj = oci.as_prop
            elif isinstance(oci, HSNeoOCIText):
                type = "text"
                vnObj = oci.as_prop
            else:
                typestr = str(type(self.objctrl))[7:-2]
                if typestr not in SSSB_Node.KNOWN_NOT_SUPPORT_OBJTYPE:
                    return "Object not supported: this object's type '%s' is not supported"%(typestr)
        else:
            return "Can not found objctrl by TreeNodeObject"
        # compare type
        if self.type != type:
            return "Can not re-assign a different type of object, require a <%s> type, get a <%s>."%(self.type, type)
        # re-assign
        self.treeNodeObj = treeNodeObject
        self.alias = treeNodeObject.textName
        self.objctrl = objctrl
        self.vnObj = vnObj
        self.recreateInfo = recreateInfo
        # re-assign non-obj subnode
        reAssginNonObjSubNodes(self, treeNodeObject)
        return None

    def selectInWorkspace(self, game):
        if self.IsTreeNode:
            game.studio.treeNodeCtrl.SelectSingle(None, True)
            game.studio.treeNodeCtrl.SelectSingle(self.treeNodeObj, False)

    @staticmethod
    def CreateFromSetting(game, setting, parentNode = None):
        node = SSSB_Node()
        try:
            node.initFromSetting(game, setting, parentNode)
            return node
        except Exception as e:
            print("SSSB_Node CreateFromSetting failed: " + str(e))
            return None

    @staticmethod
    def CreateFromTreeNodeObject(game, treeNodeObject):
        node = SSSB_Node()
        node.initFromTreeNodeObject(game, treeNodeObject)
        return node

    @staticmethod
    def CreateFromStudioCamera(game):
        node = SSSB_Node()
        node.initFromCamera(game)
        return node

    @staticmethod
    def CreateFromStudioSystem(game):
        node = SSSB_Node()
        node.initFromSystem(game)
        return node

    @staticmethod
    def GetItemInfo(itemObjctrl):
        if hasattr(itemObjctrl.objectInfo, "group"):
            group = itemObjctrl.objectInfo.group
        else:
            group = None
        if hasattr(itemObjctrl.objectInfo, "category"):
            category = itemObjctrl.objectInfo.category
        else:
            category = None
        no = itemObjctrl.objectInfo.no

        from extplugins import Sideloader
        sl = Sideloader()
        no = sl.UniGetUARResolveData(no)

        return (group, category, no)

    @staticmethod
    def GetNodeStatus(game, node):
        try:
            if node.IsObj:
                node.status = node.vnObj.export_full_status()
            else:
                node.status = None
        except Exception as e:
            print("Failed to get status for node <%s>: %s"%(node.Name, str(e)))
        return True

    @staticmethod
    def CheckValueChanged(type, key, val1, val2):
        # normal check
        if SSSB_Node.CHECK_VALUE_IGNORE_SMALL_CHANGE:
            # special keys
            if type == "-camera-" and key == "cam_rot":
                return SSSB_Node.CheckValueIgnoreSmallChange(val1, val2) and SSSB_Node.CheckValueIgnoreSmallChange(val1.eulerAngles, val2.eulerAngles)
            # others
            return SSSB_Node.CheckValueIgnoreSmallChange(val1, val2)
        else:
            # special keys
            if type == "-camera-" and key == "cam_rot":
                return val1 != val2 and val1.eulerAngles != val2.eulerAngles
            # others
            return val1 != val2

    @staticmethod
    def CheckValueIgnoreSmallChange(val1, val2):
        from System import Single, Byte
        try:
            if val1 == None:
                return val2 != None
            elif isinstance(val1, list) or isinstance(val1, tuple):
                if len(val1) != len(val2):
                    return True
                for i in range(len(val1)):
                    if SSSB_Node.CheckValueIgnoreSmallChange(val1[i], val2[i]):
                        return True
                return False
            elif isinstance(val1, dict):
                if len(val1) != len(val2):
                    return True
                for k in val1.keys():
                    if k not in val2:
                        return True
                    if SSSB_Node.CheckValueIgnoreSmallChange(val1[k], val2[k]):
                        return True
                return False
            elif isinstance(val1, Vector4):
                return SSSB_Node.CheckValueIgnoreSmallChange(val1.x, val2.x) or \
                        SSSB_Node.CheckValueIgnoreSmallChange(val1.y, val2.y) or \
                        SSSB_Node.CheckValueIgnoreSmallChange(val1.z, val2.z) or \
                        SSSB_Node.CheckValueIgnoreSmallChange(val1.w, val2.w)
            elif isinstance(val1, Vector3):
                return SSSB_Node.CheckValueIgnoreSmallChange(val1.x, val2.x) or \
                        SSSB_Node.CheckValueIgnoreSmallChange(val1.y, val2.y) or \
                        SSSB_Node.CheckValueIgnoreSmallChange(val1.z, val2.z)
            elif isinstance(val1, Vector2):
                return SSSB_Node.CheckValueIgnoreSmallChange(val1.x, val2.x) or \
                        SSSB_Node.CheckValueIgnoreSmallChange(val1.y, val2.y)
            elif isinstance(val1, Color):
                return SSSB_Node.CheckValueIgnoreSmallChange(val1.r, val2.r) or \
                        SSSB_Node.CheckValueIgnoreSmallChange(val1.g, val2.g) or \
                        SSSB_Node.CheckValueIgnoreSmallChange(val1.b, val2.b) or \
                        SSSB_Node.CheckValueIgnoreSmallChange(val1.a, val2.a)
            elif isinstance(val1, Quaternion):
                return SSSB_Node.CheckValueIgnoreSmallChange(val1.x, val2.x) or \
                        SSSB_Node.CheckValueIgnoreSmallChange(val1.y, val2.y) or \
                        SSSB_Node.CheckValueIgnoreSmallChange(val1.z, val2.z) or \
                        SSSB_Node.CheckValueIgnoreSmallChange(val1.w, val2.w)
            elif isinstance(val1, str):
                return val1 != val2
            elif isinstance(val1, bool):
                return val1 != val2
            elif isinstance(val1, int) or isinstance(val1, Byte):
                return val1 != val2
            elif isinstance(val1, float) or isinstance(val1, Single):        
                diff = abs(val1 - val2)
                return diff > SSSB_Node.CHECK_VALUE_MINOR_THRESHOLD
            else:
                raise Exception("CheckValueIgnoreSmallChange: Unknown type " + str(type(val1)) +  " of " + str(val1))
        except Exception as e:
            if SSSB_Node.DEBUG_REPORT_STATUS_DIFF_DETAIL:
                print("CheckValueIgnoreSmallChange failed: " + str(e))
                print("  val1 = " + str(val1))
                print("  val2 = " + str(val1))
            return True
