Live patch: add support for creating connections between nodes

This commit is contained in:
Moritz Brückner 2021-06-28 12:07:57 +02:00
parent 441f42383e
commit 202138304a
7 changed files with 140 additions and 27 deletions

View file

@ -6,12 +6,15 @@ class LogicNode {
var inputs: Array<LogicNodeInput> = [];
var outputs: Array<Array<LogicNode>> = [];
#if arm_debug
#if (arm_debug || arm_patch)
public var name = "";
public function watch(b: Bool) { // Watch in debug console
var nodes = armory.trait.internal.DebugConsole.watchNodes;
b ? nodes.push(this) : nodes.remove(this);
}
#if (arm_debug)
public function watch(b: Bool) { // Watch in debug console
var nodes = armory.trait.internal.DebugConsole.watchNodes;
b ? nodes.push(this) : nodes.remove(this);
}
#end
#end
public function new(tree: LogicTree) {

View file

@ -2,10 +2,23 @@ package armory.logicnode;
class LogicTree extends iron.Trait {
#if arm_patch
public static var nodeTrees = new Map<String, LogicTree>();
/**
[node name => logic node] for later node replacement for live patching.
**/
public var nodes: Map<String, LogicNode>;
#end
public var loopBreak = false; // Trigger break from loop nodes
public function new() {
super();
#if arm_patch
nodes = new Map<String, LogicNode>();
#end
}
public function add() {}

View file

@ -1,8 +1,16 @@
package armory.trait.internal;
import armory.logicnode.LogicNode.LogicNodeInput;
import armory.logicnode.LogicTree;
#if arm_patch @:expose("LivePatch") #end
@:access(armory.logicnode.LogicNode)
class LivePatch extends iron.Trait {
#if arm_patch
#if !arm_patch
public function new() { super(); }
#else
static var patchId = 0;
@ -23,9 +31,19 @@ class LivePatch extends iron.Trait {
});
}
#else
public static function patchCreateNodeLink(treeName: String, fromNodeName: String, toNodeName: String, fromIndex: Int, toIndex: Int) {
var tree = LogicTree.nodeTrees[treeName];
if (tree == null) return;
public function new() { super(); }
var fromNode = tree.nodes[fromNodeName];
var toNode = tree.nodes[toNodeName];
if (fromNode == null || toNode == null) return;
// Don't add a connection twice
if (!fromNode.outputs[fromIndex].contains(toNode)) {
fromNode.outputs[fromIndex].push(toNode);
}
toNode.inputs[toIndex] = new LogicNodeInput(fromNode, fromIndex);
}
#end
}

View file

@ -1,12 +1,14 @@
import os
import shutil
from typing import Type
from typing import Any, Type
import bpy
import arm.assets as assets
import arm.assets
import arm.node_utils
from arm.exporter import ArmoryExporter
import arm.log as log
from arm.logicnode.arm_nodes import ArmLogicTreeNode
import arm.make as make
import arm.make_state as state
import arm.utils
@ -44,7 +46,7 @@ def patch_export():
if state.proc_build is not None:
return
assets.invalidate_enabled = False
arm.assets.invalidate_enabled = False
with arm.utils.WorkingDir(arm.utils.get_fp()):
asset_path = arm.utils.get_fp_build() + '/compiled/Assets/' + arm.utils.safestr(bpy.context.scene.name) + '.arm'
@ -66,7 +68,7 @@ def patch_export():
'--noproject'
]
assets.invalidate_enabled = True
arm.assets.invalidate_enabled = True
state.proc_build = make.run_proc(cmd, patch_done)
@ -99,7 +101,7 @@ def listen(rna_type: Type[bpy.types.bpy_struct], prop: str, event_id: str):
)
def send_event(event_id: str):
def send_event(event_id: str, opt_data: Any = None):
"""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
@ -143,6 +145,29 @@ def send_event(event_id: str):
else:
patch_export()
if event_id == 'ln_insert_link':
node: ArmLogicTreeNode
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:
node_tree = node.get_tree()
tree_name = arm.node_utils.get_export_tree_name(node_tree)
# [1:] is used here because make_logic already uses that for
# node names if arm_debug is used
from_node_name = arm.node_utils.get_export_node_name(node)[1:]
to_node_name = arm.node_utils.get_export_node_name(link.to_node)[1:]
from_index = arm.node_utils.get_socket_index(node.outputs, link.from_socket)
to_index = arm.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)
def on_operator(operator_id: str):
"""As long as bpy.msgbus doesn't listen to changes made by
@ -151,7 +176,7 @@ def on_operator(operator_id: str):
(*) 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"):
if operator_id in ("VIEW3D_OT_select", "OUTLINER_OT_item_activate", "OBJECT_OT_editmode_toggle", "NODE_OT_select", "NODE_OT_translate_attach_remove_on_cancel"):
return
if operator_id == "TRANSFORM_OT_translate":

View file

@ -7,7 +7,8 @@ import bpy.types
from bpy.props import *
from nodeitems_utils import NodeItem
# Pass NodeReplacment forward to individual node modules that import arm_nodes
import arm # we cannot import arm.livepatch here or we have a circular import
# Pass NodeReplacement forward to individual node modules that import arm_nodes
from arm.logicnode.replacement import NodeReplacement
import arm.node_utils
@ -48,6 +49,13 @@ class ArmLogicTreeNode(bpy.types.Node):
def on_unregister(cls):
pass
def get_tree(self):
return self.id_data
def insert_link(self, link: bpy.types.NodeLink):
"""Called on *both* nodes when a link between two nodes is created."""
arm.live_patch.send_event('ln_insert_link', (self, link))
def get_replacement_node(self, node_tree: bpy.types.NodeTree):
# needs to be overridden by individual node classes with arm_version>1
"""(only called if the node's version is inferior to the node class's version)

View file

@ -5,6 +5,7 @@ import bpy
from arm.exporter import ArmoryExporter
import arm.log
import arm.node_utils
import arm.utils
parsed_nodes = []
@ -13,14 +14,16 @@ function_nodes = dict()
function_node_outputs = dict()
group_name = ''
def get_logic_trees():
def get_logic_trees() -> list['arm.nodes_logic.ArmLogicTree']:
ar = []
for node_group in bpy.data.node_groups:
if node_group.bl_idname == 'ArmLogicTreeType':
node_group.use_fake_user = True # Keep fake references for now
node_group.use_fake_user = True # Keep fake references for now
ar.append(node_group)
return ar
# Generating node sources
def build():
os.chdir(arm.utils.get_fp())
@ -34,7 +37,7 @@ def build():
for tree in trees:
build_node_tree(tree)
def build_node_tree(node_group):
def build_node_tree(node_group: 'arm.nodes_logic.ArmLogicTree'):
global parsed_nodes
global parsed_ids
global function_nodes
@ -48,12 +51,8 @@ def build_node_tree(node_group):
pack_path = arm.utils.safestr(bpy.data.worlds['Arm'].arm_project_package)
path = 'Sources/' + pack_path.replace('.', '/') + '/node/'
group_name = arm.utils.safesrc(node_group.name[0].upper() + node_group.name[1:])
if group_name != node_group.name:
arm.log.warn('Logic node tree and generated trait names differ! Node'
f' tree: "{node_group.name}", trait: "{group_name}"')
group_name = arm.node_utils.get_export_tree_name(node_group, do_warn=True)
file = path + group_name + '.hx'
# Import referenced node group
@ -65,6 +64,8 @@ def build_node_tree(node_group):
if node_group.arm_cached and os.path.isfile(file):
return
wrd = bpy.data.worlds['Arm']
with open(file, 'w', encoding="utf-8") as f:
f.write('package ' + pack_path + '.node;\n\n')
f.write('@:keep class ' + group_name + ' extends armory.logicnode.LogicTree {\n\n')
@ -72,10 +73,12 @@ def build_node_tree(node_group):
f.write('\tvar functionOutputNodes:Map<String, armory.logicnode.FunctionOutputNode>;\n\n')
f.write('\tpublic function new() {\n')
f.write('\t\tsuper();\n')
if bpy.data.worlds['Arm'].arm_debug_console:
if wrd.arm_debug_console:
f.write('\t\tname = "' + group_name + '";\n')
f.write('\t\tthis.functionNodes = new Map();\n')
f.write('\t\tthis.functionOutputNodes = new Map();\n')
if wrd.arm_live_patch:
f.write(f'\t\tarmory.logicnode.LogicTree.nodeTrees["{group_name}"] = this;\n')
f.write('\t\tnotifyOnAdd(add);\n')
f.write('\t}\n\n')
f.write('\toverride public function add() {\n')
@ -116,7 +119,7 @@ def build_node(node: bpy.types.Node, f: TextIO) -> Optional[str]:
return None
# Get node name
name = '_' + arm.utils.safesrc(node.name)
name = arm.node_utils.get_export_node_name(node)
# Link nodes using IDs
if node.arm_logic_id != '':
@ -143,11 +146,17 @@ def build_node(node: bpy.types.Node, f: TextIO) -> Optional[str]:
# Index function output name by corresponding function name
function_node_outputs[node.function_name] = name
wrd = bpy.data.worlds['Arm']
# Watch in debug console
if node.arm_watch and bpy.data.worlds['Arm'].arm_debug_console:
if node.arm_watch and wrd.arm_debug_console:
f.write('\t\t' + name + '.name = "' + name[1:] + '";\n')
f.write('\t\t' + name + '.watch(true);\n')
elif wrd.arm_live_patch:
f.write('\t\t' + name + '.name = "' + name[1:] + '";\n')
f.write(f'\t\tthis.nodes["{name[1:]}"] = {name};\n')
# Properties
for i in range(0, 10):
prop_name = 'property' + str(i) + '_get'

View file

@ -1,8 +1,12 @@
from typing import Type
from typing import Type, Union
import bpy
from bpy.types import NodeSocket, NodeInputs, NodeOutputs
from nodeitems_utils import NodeItem
import arm.log
import arm.utils
def find_node_by_link(node_group, to_node, inp):
for link in node_group.links:
@ -46,6 +50,39 @@ def get_output_node(node_group, from_node, output_index):
return link.to_node
def get_socket_index(sockets: Union[NodeInputs, NodeOutputs], socket: NodeSocket) -> int:
"""Find the socket index in the given node input or output
collection, return -1 if not found.
"""
for i in range(0, len(sockets)):
if sockets[i] == socket:
return i
return -1
def get_export_tree_name(tree: bpy.types.NodeTree, do_warn=False) -> str:
"""Return the name of the given node tree that's used in the
exported Haxe code.
If `do_warn` is true, a warning is displayed if the export name
differs from the actual tree name.
"""
export_name = arm.utils.safesrc(tree.name[0].upper() + tree.name[1:])
if export_name != tree.name:
arm.log.warn('Logic node tree and generated trait names differ! Node'
f' tree: "{tree.name}", trait: "{export_name}"')
return export_name
def get_export_node_name(node: bpy.types.Node) -> str:
"""Return the name of the given node that's used in the exported
Haxe code.
"""
return '_' + arm.utils.safesrc(node.name)
def nodetype_to_nodeitem(node_type: Type[bpy.types.Node]) -> NodeItem:
"""Create a NodeItem from a given node class."""
# Internal node types seem to have no bl_idname attribute