forked from LeenkxTeam/LNXSDK
		
	
		
			
	
	
		
			393 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
		
		
			
		
	
	
			393 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
|  | import os | ||
|  | import shutil | ||
|  | from typing import Any, Type | ||
|  | 
 | ||
|  | import bpy | ||
|  | 
 | ||
|  | import lnx.assets | ||
|  | from lnx import log, make | ||
|  | from lnx.exporter import LeenkxExporter | ||
|  | from lnx.logicnode.lnx_nodes import LnxLogicTreeNode | ||
|  | import lnx.make_state as state | ||
|  | import lnx.node_utils | ||
|  | import lnx.utils | ||
|  | 
 | ||
|  | if lnx.is_reload(__name__): | ||
|  |     lnx.assets = lnx.reload_module(lnx.assets) | ||
|  |     lnx.exporter = lnx.reload_module(lnx.exporter) | ||
|  |     from lnx.exporter import LeenkxExporter | ||
|  |     log = lnx.reload_module(log) | ||
|  |     lnx.logicnode.lnx_nodes = lnx.reload_module(lnx.logicnode.lnx_nodes) | ||
|  |     from lnx.logicnode.lnx_nodes import LnxLogicTreeNode | ||
|  |     make = lnx.reload_module(make) | ||
|  |     state = lnx.reload_module(state) | ||
|  |     lnx.node_utils = lnx.reload_module(lnx.node_utils) | ||
|  |     lnx.utils = lnx.reload_module(lnx.utils) | ||
|  | else: | ||
|  |     lnx.enable_reload(__name__) | ||
|  | 
 | ||
|  |     patch_id = 0 | ||
|  |     """Current patch id""" | ||
|  | 
 | ||
|  |     __running = False | ||
|  |     """Whether live patch is currently active""" | ||
|  | 
 | ||
|  | # Any object can act as a message bus owner | ||
|  | msgbus_owner = object() | ||
|  | 
 | ||
|  | 
 | ||
|  | def start(): | ||
|  |     """Start the live patch session.""" | ||
|  |     log.debug("Live patch session started") | ||
|  | 
 | ||
|  |     listen(bpy.types.Object, "location", "obj_location") | ||
|  |     listen(bpy.types.Object, "rotation_euler", "obj_rotation") | ||
|  |     listen(bpy.types.Object, "scale", "obj_scale") | ||
|  | 
 | ||
|  |     # 'energy' is defined in sub classes only, also workaround for | ||
|  |     # https://developer.blender.org/T88408 | ||
|  |     for light_type in (bpy.types.AreaLight, bpy.types.PointLight, bpy.types.SpotLight, bpy.types.SunLight): | ||
|  |         listen(light_type, "color", "light_color") | ||
|  |         listen(light_type, "energy", "light_energy") | ||
|  | 
 | ||
|  |     global __running | ||
|  |     __running = True | ||
|  | 
 | ||
|  | 
 | ||
|  | def stop(): | ||
|  |     """Stop the live patch session.""" | ||
|  |     global __running, patch_id | ||
|  |     if __running: | ||
|  |         __running = False | ||
|  |         patch_id = 0 | ||
|  | 
 | ||
|  |         log.debug("Live patch session stopped") | ||
|  |         bpy.msgbus.clear_by_owner(msgbus_owner) | ||
|  | 
 | ||
|  | 
 | ||
|  | def patch_export(): | ||
|  |     """Re-export the current scene and update the game accordingly.""" | ||
|  |     if not __running or state.proc_build is not None: | ||
|  |         return | ||
|  | 
 | ||
|  |     lnx.assets.invalidate_enabled = False | ||
|  | 
 | ||
|  |     with lnx.utils.WorkingDir(lnx.utils.get_fp()): | ||
|  |         asset_path = lnx.utils.get_fp_build() + '/compiled/Assets/' + lnx.utils.safestr(bpy.context.scene.name) + '.lnx' | ||
|  |         LeenkxExporter.export_scene(bpy.context, asset_path, scene=bpy.context.scene) | ||
|  | 
 | ||
|  |         dir_std_shaders_dst = os.path.join(lnx.utils.build_dir(), 'compiled', 'Shaders', 'std') | ||
|  |         if not os.path.isdir(dir_std_shaders_dst): | ||
|  |             dir_std_shaders_src = os.path.join(lnx.utils.get_sdk_path(), 'leenkx', 'Shaders', 'std') | ||
|  |             shutil.copytree(dir_std_shaders_src, dir_std_shaders_dst) | ||
|  | 
 | ||
|  |         node_path = lnx.utils.get_node_path() | ||
|  |         khamake_path = lnx.utils.get_khamake_path() | ||
|  |         cmd = [ | ||
|  |             node_path, khamake_path, 'krom', | ||
|  |             '--shaderversion', '330', | ||
|  |             '--parallelAssetConversion', '4', | ||
|  |             '--to', lnx.utils.build_dir() + '/debug', | ||
|  |             '--nohaxe', | ||
|  |             '--noproject' | ||
|  |         ] | ||
|  | 
 | ||
|  |         lnx.assets.invalidate_enabled = True | ||
|  |         state.proc_build = make.run_proc(cmd, patch_done) | ||
|  | 
 | ||
|  | 
 | ||
|  | def patch_done(): | ||
|  |     """Signal Iron to reload the running scene after a re-export.""" | ||
|  |     js = 'iron.Scene.patch();' | ||
|  |     write_patch(js) | ||
|  |     state.proc_build = None | ||
|  | 
 | ||
|  | 
 | ||
|  | def write_patch(js: str): | ||
|  |     """Write the given javascript code to 'krom.patch'.""" | ||
|  |     global patch_id | ||
|  |     with open(lnx.utils.get_fp_build() + '/debug/krom/krom.patch', 'w', encoding='utf-8') as f: | ||
|  |         patch_id += 1 | ||
|  |         f.write(str(patch_id) + '\n') | ||
|  |         f.write(js) | ||
|  | 
 | ||
|  | 
 | ||
|  | def listen(rna_type: Type[bpy.types.bpy_struct], prop: str, event_id: str): | ||
|  |     """Subscribe to '<rna_type>.<prop>'. The event_id can be choosen
 | ||
|  |     freely but must match with the id used in send_event(). | ||
|  |     """
 | ||
|  |     bpy.msgbus.subscribe_rna( | ||
|  |         key=(rna_type, prop), | ||
|  |         owner=msgbus_owner, | ||
|  |         args=(event_id, ), | ||
|  |         notify=send_event | ||
|  |         # options={"PERSISTENT"} | ||
|  |     ) | ||
|  | 
 | ||
|  | 
 | ||
|  | def send_event(event_id: str, opt_data: Any = None): | ||
|  |     """Send the result of the given event to Krom.""" | ||
|  |     if not __running: | ||
|  |         return | ||
|  | 
 | ||
|  |     if hasattr(bpy.context, 'object') and bpy.context.object is not None: | ||
|  |         obj = bpy.context.object.name | ||
|  | 
 | ||
|  |         if bpy.context.object.mode == "OBJECT": | ||
|  |             if event_id == "obj_location": | ||
|  |                 vec = bpy.context.object.location | ||
|  |                 js = f'var o = iron.Scene.active.getChild("{obj}"); o.transform.loc.set({vec[0]}, {vec[1]}, {vec[2]}); o.transform.dirty = true;' | ||
|  |                 write_patch(js) | ||
|  | 
 | ||
|  |             elif event_id == 'obj_scale': | ||
|  |                 vec = bpy.context.object.scale | ||
|  |                 js = f'var o = iron.Scene.active.getChild("{obj}"); o.transform.scale.set({vec[0]}, {vec[1]}, {vec[2]}); o.transform.dirty = true;' | ||
|  |                 write_patch(js) | ||
|  | 
 | ||
|  |             elif event_id == 'obj_rotation': | ||
|  |                 vec = bpy.context.object.rotation_euler.to_quaternion() | ||
|  |                 js = f'var o = iron.Scene.active.getChild("{obj}"); o.transform.rot.set({vec[1]}, {vec[2]}, {vec[3]}, {vec[0]}); o.transform.dirty = true;' | ||
|  |                 write_patch(js) | ||
|  | 
 | ||
|  |             elif event_id == 'light_color': | ||
|  |                 light: bpy.types.Light = bpy.context.object.data | ||
|  |                 vec = light.color | ||
|  |                 js = f'var lRaw = iron.Scene.active.getLight("{light.name}").data.raw; lRaw.color[0]={vec[0]}; lRaw.color[1]={vec[1]}; lRaw.color[2]={vec[2]};' | ||
|  |                 write_patch(js) | ||
|  | 
 | ||
|  |             elif event_id == 'light_energy': | ||
|  |                 light: bpy.types.Light = bpy.context.object.data | ||
|  | 
 | ||
|  |                 # Align strength to Leenkx, see exporter.export_light() | ||
|  |                 # TODO: Use exporter.export_light() and simply reload all raw light data in Iron? | ||
|  |                 strength_fac = 1.0 | ||
|  |                 if light.type == 'SUN': | ||
|  |                     strength_fac = 0.325 | ||
|  |                 elif light.type in ('POINT', 'SPOT', 'AREA'): | ||
|  |                     strength_fac = 0.01 | ||
|  | 
 | ||
|  |                 js = f'var lRaw = iron.Scene.active.getLight("{light.name}").data.raw; lRaw.strength={light.energy * strength_fac};' | ||
|  |                 write_patch(js) | ||
|  | 
 | ||
|  |         else: | ||
|  |             patch_export() | ||
|  | 
 | ||
|  |     if event_id == 'ln_insert_link': | ||
|  |         node: LnxLogicTreeNode | ||
|  |         link: bpy.types.NodeLink | ||
|  |         node, link = opt_data | ||
|  | 
 | ||
|  |         # This event is called twice for a connection but we only need | ||
|  |         # send it once | ||
|  |         if node == link.from_node: | ||
|  |             tree_name = lnx.node_utils.get_export_tree_name(node.get_tree()) | ||
|  | 
 | ||
|  |             # [1:] is used here because make_logic already uses that for | ||
|  |             # node names if lnx_debug is used | ||
|  |             from_node_name = lnx.node_utils.get_export_node_name(node)[1:] | ||
|  |             to_node_name = lnx.node_utils.get_export_node_name(link.to_node)[1:] | ||
|  | 
 | ||
|  |             from_index = lnx.node_utils.get_socket_index(node.outputs, link.from_socket) | ||
|  |             to_index = lnx.node_utils.get_socket_index(link.to_node.inputs, link.to_socket) | ||
|  | 
 | ||
|  |             js = f'LivePatch.patchCreateNodeLink("{tree_name}", "{from_node_name}", "{to_node_name}", "{from_index}", "{to_index}");' | ||
|  |             write_patch(js) | ||
|  | 
 | ||
|  |     elif event_id == 'ln_update_prop': | ||
|  |         node: LnxLogicTreeNode | ||
|  |         prop_name: str | ||
|  |         node, prop_name = opt_data | ||
|  | 
 | ||
|  |         tree_name = lnx.node_utils.get_export_tree_name(node.get_tree()) | ||
|  |         node_name = lnx.node_utils.get_export_node_name(node)[1:] | ||
|  | 
 | ||
|  |         value = lnx.node_utils.haxe_format_prop_value(node, prop_name) | ||
|  | 
 | ||
|  |         if prop_name.endswith('_get'): | ||
|  |             # Hack because some nodes use a different Python property | ||
|  |             # name than they use in Haxe | ||
|  |             prop_name = prop_name[:-4] | ||
|  | 
 | ||
|  |         js = f'LivePatch.patchUpdateNodeProp("{tree_name}", "{node_name}", "{prop_name}", {value});' | ||
|  |         write_patch(js) | ||
|  | 
 | ||
|  |     elif event_id == 'ln_socket_val': | ||
|  |         node: LnxLogicTreeNode | ||
|  |         socket: bpy.types.NodeSocket | ||
|  |         node, socket = opt_data | ||
|  | 
 | ||
|  |         socket_index = lnx.node_utils.get_socket_index(node.inputs, socket) | ||
|  | 
 | ||
|  |         if socket_index != -1: | ||
|  |             tree_name = lnx.node_utils.get_export_tree_name(node.get_tree()) | ||
|  |             node_name = lnx.node_utils.get_export_node_name(node)[1:] | ||
|  | 
 | ||
|  |             value = socket.get_default_value() | ||
|  |             inp_type = socket.lnx_socket_type | ||
|  | 
 | ||
|  |             if inp_type in ('VECTOR', 'RGB'): | ||
|  |                 value = f'new iron.Vec4({lnx.node_utils.haxe_format_socket_val(value, array_outer_brackets=False)}, 1.0)' | ||
|  |             elif inp_type == 'RGBA': | ||
|  |                 value = f'new iron.Vec4({lnx.node_utils.haxe_format_socket_val(value, array_outer_brackets=False)})' | ||
|  |             elif inp_type == 'ROTATION': | ||
|  |                 value = f'new iron.Quat({lnx.node_utils.haxe_format_socket_val(value, array_outer_brackets=False)})' | ||
|  |             elif inp_type == 'OBJECT': | ||
|  |                 value = f'iron.Scene.active.getChild("{value}")' if value != '' else 'null' | ||
|  |             else: | ||
|  |                 value = lnx.node_utils.haxe_format_socket_val(value) | ||
|  | 
 | ||
|  |             js = f'LivePatch.patchUpdateNodeInputVal("{tree_name}", "{node_name}", {socket_index}, {value});' | ||
|  |             write_patch(js) | ||
|  | 
 | ||
|  |     elif event_id == 'ln_create': | ||
|  |         node: LnxLogicTreeNode = opt_data | ||
|  | 
 | ||
|  |         tree_name = lnx.node_utils.get_export_tree_name(node.get_tree()) | ||
|  |         node_name = lnx.node_utils.get_export_node_name(node)[1:] | ||
|  |         node_type = 'leenkx.logicnode.' + node.bl_idname[2:] | ||
|  | 
 | ||
|  |         prop_names = list(lnx.node_utils.get_haxe_property_names(node)) | ||
|  |         prop_py_names, prop_hx_names = zip(*prop_names) if len(prop_names) > 0 else ([], []) | ||
|  |         prop_values = (getattr(node, prop_name) for prop_name in prop_py_names) | ||
|  |         prop_datas = lnx.node_utils.haxe_format_socket_val(list(zip(prop_hx_names, prop_values))) | ||
|  | 
 | ||
|  |         inp_data = [(inp.lnx_socket_type, inp.get_default_value()) for inp in node.inputs] | ||
|  |         inp_data = lnx.node_utils.haxe_format_socket_val(inp_data) | ||
|  |         out_data = [(out.lnx_socket_type, out.get_default_value()) for out in node.outputs] | ||
|  |         out_data = lnx.node_utils.haxe_format_socket_val(out_data) | ||
|  | 
 | ||
|  |         js = f'LivePatch.patchNodeCreate("{tree_name}", "{node_name}", "{node_type}", {prop_datas}, {inp_data}, {out_data});' | ||
|  |         write_patch(js) | ||
|  | 
 | ||
|  |     elif event_id == 'ln_delete': | ||
|  |         node: LnxLogicTreeNode = opt_data | ||
|  | 
 | ||
|  |         tree_name = lnx.node_utils.get_export_tree_name(node.get_tree()) | ||
|  |         node_name = lnx.node_utils.get_export_node_name(node)[1:] | ||
|  | 
 | ||
|  |         js = f'LivePatch.patchNodeDelete("{tree_name}", "{node_name}");' | ||
|  |         write_patch(js) | ||
|  | 
 | ||
|  |     elif event_id == 'ln_copy': | ||
|  |         newnode: LnxLogicTreeNode | ||
|  |         node: LnxLogicTreeNode | ||
|  |         newnode, node = opt_data | ||
|  | 
 | ||
|  |         # Use newnode to get the tree, node has no id_data at this moment | ||
|  |         tree_name = lnx.node_utils.get_export_tree_name(newnode.get_tree()) | ||
|  | 
 | ||
|  |         newnode_name = lnx.node_utils.get_export_node_name(newnode)[1:] | ||
|  |         node_name = lnx.node_utils.get_export_node_name(node)[1:] | ||
|  | 
 | ||
|  |         props_list = '[' + ','.join(f'"{p}"' for _, p in lnx.node_utils.get_haxe_property_names(node)) + ']' | ||
|  | 
 | ||
|  |         inp_data = [(inp.lnx_socket_type, inp.get_default_value()) for inp in newnode.inputs] | ||
|  |         inp_data = lnx.node_utils.haxe_format_socket_val(inp_data) | ||
|  |         out_data = [(out.lnx_socket_type, out.get_default_value()) for out in newnode.outputs] | ||
|  |         out_data = lnx.node_utils.haxe_format_socket_val(out_data) | ||
|  | 
 | ||
|  |         js = f'LivePatch.patchNodeCopy("{tree_name}", "{node_name}", "{newnode_name}", {props_list}, {inp_data}, {out_data});' | ||
|  |         write_patch(js) | ||
|  | 
 | ||
|  |     elif event_id == 'ln_update_sockets': | ||
|  |         node: LnxLogicTreeNode = opt_data | ||
|  | 
 | ||
|  |         tree_name = lnx.node_utils.get_export_tree_name(node.get_tree()) | ||
|  |         node_name = lnx.node_utils.get_export_node_name(node)[1:] | ||
|  | 
 | ||
|  |         inp_data = '[' | ||
|  |         for idx, inp in enumerate(node.inputs): | ||
|  |             inp_data += '{' | ||
|  |             # is_linked can be true even if there are no links if the | ||
|  |             # user starts dragging a connection away before releasing | ||
|  |             # the mouse | ||
|  |             if inp.is_linked and len(inp.links) > 0: | ||
|  |                 inp_data += 'isLinked: true,' | ||
|  |                 inp_data += f'fromNode: "{lnx.node_utils.get_export_node_name(inp.links[0].from_node)[1:]}",' | ||
|  |                 inp_data += f'fromIndex: {lnx.node_utils.get_socket_index(inp.links[0].from_node.outputs, inp.links[0].from_socket)},' | ||
|  |             else: | ||
|  |                 inp_data += 'isLinked: false,' | ||
|  |                 inp_data += f'socketType: "{inp.lnx_socket_type}",' | ||
|  |                 inp_data += f'socketValue: {lnx.node_utils.haxe_format_socket_val(inp.get_default_value())},' | ||
|  | 
 | ||
|  |             inp_data += f'toIndex: {idx}' | ||
|  |             inp_data += '},' | ||
|  |         inp_data += ']' | ||
|  | 
 | ||
|  |         out_data = '[' | ||
|  |         for idx, out in enumerate(node.outputs): | ||
|  |             out_data += '[' | ||
|  |             for link in out.links: | ||
|  |                 out_data += '{' | ||
|  |                 if out.is_linked: | ||
|  |                     out_data += 'isLinked: true,' | ||
|  |                     out_data += f'toNode: "{lnx.node_utils.get_export_node_name(link.to_node)[1:]}",' | ||
|  |                     out_data += f'toIndex: {lnx.node_utils.get_socket_index(link.to_node.inputs, link.to_socket)},' | ||
|  |                 else: | ||
|  |                     out_data += 'isLinked: false,' | ||
|  |                     out_data += f'socketType: "{out.lnx_socket_type}",' | ||
|  |                     out_data += f'socketValue: {lnx.node_utils.haxe_format_socket_val(out.get_default_value())},' | ||
|  | 
 | ||
|  |                 out_data += f'fromIndex: {idx}' | ||
|  |                 out_data += '},' | ||
|  |             out_data += '],' | ||
|  |         out_data += ']' | ||
|  | 
 | ||
|  |         js = f'LivePatch.patchSetNodeLinks("{tree_name}", "{node_name}", {inp_data}, {out_data});' | ||
|  |         write_patch(js) | ||
|  | 
 | ||
|  | 
 | ||
|  | def on_operator(operator_id: str): | ||
|  |     """As long as bpy.msgbus doesn't listen to changes made by
 | ||
|  |     operators (*), additionally notify the callback manually. | ||
|  | 
 | ||
|  |     (*) https://developer.blender.org/T72109 | ||
|  |     """
 | ||
|  |     if not __running: | ||
|  |         return | ||
|  | 
 | ||
|  |     if operator_id in IGNORE_OPERATORS: | ||
|  |         return | ||
|  | 
 | ||
|  |     if operator_id == 'TRANSFORM_OT_translate': | ||
|  |         send_event('obj_location') | ||
|  |     elif operator_id in ('TRANSFORM_OT_rotate', 'TRANSFORM_OT_trackball'): | ||
|  |         send_event('obj_rotation') | ||
|  |     elif operator_id == 'TRANSFORM_OT_resize': | ||
|  |         send_event('obj_scale') | ||
|  | 
 | ||
|  |     # Rebuild | ||
|  |     else: | ||
|  |         patch_export() | ||
|  | 
 | ||
|  | 
 | ||
|  | # Don't re-export the scene for the following operators | ||
|  | IGNORE_OPERATORS = ( | ||
|  |     'LNX_OT_node_add_input', | ||
|  |     'LNX_OT_node_add_input_output', | ||
|  |     'LNX_OT_node_add_input_value', | ||
|  |     'LNX_OT_node_add_output', | ||
|  |     'LNX_OT_node_call_func', | ||
|  |     'LNX_OT_node_remove_input', | ||
|  |     'LNX_OT_node_remove_input_output', | ||
|  |     'LNX_OT_node_remove_input_value', | ||
|  |     'LNX_OT_node_remove_output', | ||
|  |     'LNX_OT_node_search', | ||
|  | 
 | ||
|  |     'NODE_OT_delete', | ||
|  |     'NODE_OT_duplicate_move', | ||
|  |     'NODE_OT_hide_toggle', | ||
|  |     'NODE_OT_link', | ||
|  |     'NODE_OT_move_detach_links', | ||
|  |     'NODE_OT_select', | ||
|  |     'NODE_OT_translate_attach', | ||
|  |     'NODE_OT_translate_attach_remove_on_cancel', | ||
|  | 
 | ||
|  |     'OBJECT_OT_editmode_toggle', | ||
|  |     'OUTLINER_OT_item_activate', | ||
|  |     'UI_OT_button_string_clear', | ||
|  |     'UI_OT_eyedropper_id', | ||
|  |     'VIEW3D_OT_select', | ||
|  |     'VIEW3D_OT_select_box', | ||
|  | ) |