Live patch: move to dedicated module and use msgbus

This commit is contained in:
Moritz Brückner 2021-05-18 21:24:45 +02:00
parent 3b72fff76d
commit 07ffe06c1d
3 changed files with 143 additions and 58 deletions

View file

@ -6,13 +6,14 @@ import bpy
from bpy.app.handlers import persistent
import arm.api
import arm.live_patch as live_patch
import arm.logicnode.arm_nodes as arm_nodes
import arm.nodes_logic
import arm.make as make
import arm.make_state as state
import arm.props as props
import arm.utils
@persistent
def on_depsgraph_update_post(self):
if state.proc_build != None:
@ -40,12 +41,10 @@ def on_depsgraph_update_post(self):
# Send last operator to Krom
wrd = bpy.data.worlds['Arm']
if state.proc_play != None and \
state.target == 'krom' and \
wrd.arm_live_patch:
if state.proc_play is not None and state.target == 'krom' and wrd.arm_live_patch:
ops = bpy.context.window_manager.operators
if len(ops) > 0 and ops[-1] != None:
send_operator(ops[-1])
if len(ops) > 0 and ops[-1] is not None:
live_patch.on_operator(ops[-1].bl_idname)
# Hacky solution to update armory props after operator executions
last_operator = bpy.context.active_operator
@ -71,24 +70,6 @@ def on_operator_post(operator_id: str) -> None:
target_obj.arm_rb_collision_filter_mask = source_obj.arm_rb_collision_filter_mask
def send_operator(op):
if hasattr(bpy.context, 'object') and bpy.context.object != None:
obj = bpy.context.object.name
if op.name == 'Move':
vec = bpy.context.object.location
js = 'var o = iron.Scene.active.getChild("' + obj + '"); o.transform.loc.set(' + str(vec[0]) + ', ' + str(vec[1]) + ', ' + str(vec[2]) + '); o.transform.dirty = true;'
make.write_patch(js)
elif op.name == 'Resize':
vec = bpy.context.object.scale
js = 'var o = iron.Scene.active.getChild("' + obj + '"); o.transform.scale.set(' + str(vec[0]) + ', ' + str(vec[1]) + ', ' + str(vec[2]) + '); o.transform.dirty = true;'
make.write_patch(js)
elif op.name == 'Rotate':
vec = bpy.context.object.rotation_euler.to_quaternion()
js = 'var o = iron.Scene.active.getChild("' + obj + '"); o.transform.rot.set(' + str(vec[1]) + ', ' + str(vec[2]) + ', ' + str(vec[3]) + ' ,' + str(vec[0]) + '); o.transform.dirty = true;'
make.write_patch(js)
else: # Rebuild
make.patch()
def always():
# Force ui redraw
if state.redraw_ui and context_screen != None:
@ -102,9 +83,11 @@ def always():
space.node_tree.arm_cached = False
return 0.5
appended_py_paths = []
context_screen = None
@persistent
def on_load_post(context):
global appended_py_paths

134
blender/arm/live_patch.py Normal file
View file

@ -0,0 +1,134 @@
import os
import shutil
from typing import Type
import bpy
import arm.assets as assets
from arm.exporter import ArmoryExporter
import arm.log as log
import arm.make as make
import arm.make_state as state
import arm.utils
# Current patch id
patch_id = 0
# Any object can act as a message bus owner
msgbus_owner = object()
def start():
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")
def patch_export():
"""Re-export the current scene and update the game accordingly."""
if state.proc_build is not None:
return
assets.invalidate_enabled = False
fp = arm.utils.get_fp()
with arm.utils.WorkingDir(fp):
asset_path = arm.utils.get_fp_build() + '/compiled/Assets/' + arm.utils.safestr(bpy.context.scene.name) + '.arm'
ArmoryExporter.export_scene(bpy.context, asset_path, scene=bpy.context.scene)
if not os.path.isdir(arm.utils.build_dir() + '/compiled/Shaders/std'):
raw_shaders_path = arm.utils.get_sdk_path() + '/armory/Shaders/'
shutil.copytree(raw_shaders_path + 'std', arm.utils.build_dir() + '/compiled/Shaders/std')
node_path = arm.utils.get_node_path()
khamake_path = arm.utils.get_khamake_path()
cmd = [
node_path, khamake_path, 'krom',
'--shaderversion', '330',
'--parallelAssetConversion', '4',
'--to', arm.utils.build_dir() + '/debug',
'--nohaxe',
'--noproject'
]
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
bpy.msgbus.clear_by_owner(msgbus_owner)
def write_patch(js: str):
"""Write the given javascript code to 'krom.patch'."""
global patch_id
with open(arm.utils.get_fp_build() + '/debug/krom/krom.patch', 'w') as f:
patch_id += 1
f.write(str(patch_id) + '\n')
f.write(js)
def listen(rna_type: Type[bpy.types.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):
"""Send the result of the given event to Krom."""
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)
else:
patch_export()
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
"""
# Don't re-export the scene for the following operators
if operator_id in ("VIEW3D_OT_select", "OUTLINER_OT_item_activate", "OBJECT_OT_editmode_toggle"):
return
if operator_id == "TRANSFORM_OT_translate":
send_event("obj_location")
elif operator_id == "TRANSFORM_OT_rotate":
send_event("obj_rotation")
elif operator_id == "TRANSFORM_OT_resize":
send_event("obj_scale")
# Rebuild
else:
patch_export()

View file

@ -17,6 +17,7 @@ import arm.assets as assets
from arm.exporter import ArmoryExporter
import arm.lib.make_datas
import arm.lib.server
import arm.live_patch as live_patch
import arm.log as log
import arm.make_logic as make_logic
import arm.make_renderpath as make_renderpath
@ -445,40 +446,6 @@ def build_done():
else:
log.error('Build failed, check console')
def patch():
if state.proc_build != None:
return
assets.invalidate_enabled = False
fp = arm.utils.get_fp()
os.chdir(fp)
asset_path = arm.utils.get_fp_build() + '/compiled/Assets/' + arm.utils.safestr(bpy.context.scene.name) + '.arm'
ArmoryExporter.export_scene(bpy.context, asset_path, scene=bpy.context.scene)
if not os.path.isdir(arm.utils.build_dir() + '/compiled/Shaders/std'):
raw_shaders_path = arm.utils.get_sdk_path() + '/armory/Shaders/'
shutil.copytree(raw_shaders_path + 'std', arm.utils.build_dir() + '/compiled/Shaders/std')
node_path = arm.utils.get_node_path()
khamake_path = arm.utils.get_khamake_path()
cmd = [node_path, khamake_path, 'krom']
cmd.extend(('--shaderversion', '330', '--parallelAssetConversion', '4',
'--to', arm.utils.build_dir() + '/debug', '--nohaxe', '--noproject'))
assets.invalidate_enabled = True
state.proc_build = run_proc(cmd, patch_done)
def patch_done():
js = 'iron.Scene.patch();'
write_patch(js)
state.proc_build = None
patch_id = 0
def write_patch(js):
global patch_id
with open(arm.utils.get_fp_build() + '/debug/krom/krom.patch', 'w') as f:
patch_id += 1
f.write(str(patch_id) + '\n')
f.write(js)
def runtime_to_target():
wrd = bpy.data.worlds['Arm']
@ -545,6 +512,7 @@ def build_success():
webbrowser.open(html5_app_path)
elif wrd.arm_runtime == 'Krom':
if wrd.arm_live_patch:
live_patch.start()
open(arm.utils.get_fp_build() + '/debug/krom/krom.patch', 'w').close()
krom_location, krom_path = arm.utils.krom_paths()
os.chdir(krom_location)