Merge pull request #2046 from MoritzBrueckner/node-replacement
Fix and further improve node replacement system
This commit is contained in:
commit
fdcdf86c24
|
@ -7,6 +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
|
||||
from arm.logicnode.replacement import NodeReplacement
|
||||
import arm.node_utils
|
||||
|
||||
# When passed as a category to add_node(), this will use the capitalized
|
||||
|
@ -65,10 +67,11 @@ class ArmLogicTreeNode(bpy.types.Node):
|
|||
note that the lowest 'defined' version should be 1. if the node's version is 0, it means that it has been saved before versioning was a thing.
|
||||
NODES OF VERSION 1 AND VERSION 0 SHOULD HAVE THE SAME CONTENTS
|
||||
"""
|
||||
if self.arm_version==0 and type(self).arm_version == 1:
|
||||
return NodeReplacement.Identity(self) # in case someone doesn't implement this function, but the node has version 0.
|
||||
if self.arm_version == 0 and type(self).arm_version == 1:
|
||||
# In case someone doesn't implement this function, but the node has version 0
|
||||
return NodeReplacement.Identity(self)
|
||||
else:
|
||||
raise LookupError(f"the current node class, {repr(type(self)):s}, does not implement the getReplacementNode method, even though it has updated")
|
||||
raise LookupError(f"the current node class {repr(type(self)):s} does not implement get_replacement_node() even though it has updated")
|
||||
|
||||
def add_input(self, socket_type: str, socket_name: str, default_value: Any = None, is_var: bool = False) -> bpy.types.NodeSocket:
|
||||
"""Adds a new input socket to the node.
|
||||
|
@ -105,100 +108,6 @@ class ArmLogicTreeNode(bpy.types.Node):
|
|||
return socket
|
||||
|
||||
|
||||
class NodeReplacement:
|
||||
"""
|
||||
Represents a simple replacement rule, this can replace nodes of one type to nodes of a second type.
|
||||
However, it is fairly limited. For instance, it assumes there are no changes in the type of the inputs or outputs
|
||||
Second, it also assumes that node properties (especially EnumProperties) keep the same possible values.
|
||||
|
||||
- from_node, from_node_version: the type of node to be removed, and its version number
|
||||
- to_node, to_node_version: the type of node which takes from_node's place, and its version number
|
||||
- *_socket_mapping: a map which defines how the sockets of the old node shall be connected to the new node
|
||||
{1: 2} means that anything connected to the socket with index 1 on the original node will be connected to the socket with index 2 on the new node
|
||||
- property_mapping: the mapping used to transfer the values of the old node's properties to the new node's properties.
|
||||
{"property0": "property1"} mean that the value of the new node's property1 should be the old node's property0's value.
|
||||
- input_defaults: a mapping used to give default values to the inputs which aren't overridden otherwise.
|
||||
- property_defaults: a mapping used to define the value of the new node's properties, when they aren't overridden otherwise.
|
||||
"""
|
||||
|
||||
def __init__(self, from_node: str, from_node_version: int, to_node: str, to_node_version: int,
|
||||
in_socket_mapping: Dict[int, int], out_socket_mapping: Dict[int, int], property_mapping: Optional[Dict[str, str]] = None,
|
||||
input_defaults: Optional[Dict[int, any]] = None, property_defaults: Optional[Dict[str, any]] = None):
|
||||
self.from_node = from_node
|
||||
self.to_node = to_node
|
||||
self.from_node_version = from_node_version
|
||||
self.to_node_version = to_node_version
|
||||
|
||||
self.in_socket_mapping = in_socket_mapping
|
||||
self.out_socket_mapping = out_socket_mapping
|
||||
self.property_mapping = {} if property_mapping is None else property_mapping
|
||||
|
||||
self.input_defaults = {} if input_defaults is None else input_defaults
|
||||
self.property_defaults = {} if property_defaults is None else property_defaults
|
||||
|
||||
@classmethod
|
||||
def Identity(cls, node: ArmLogicTreeNode):
|
||||
"""returns a NodeReplacement that does nothing, while operating on a given node.
|
||||
WARNING: it assumes that all node properties have names that start with "property"
|
||||
"""
|
||||
in_socks = {i:i for i in range(len(node.inputs))}
|
||||
out_socks = {i:i for i in range(len(node.outputs))}
|
||||
props = {}
|
||||
i=0
|
||||
|
||||
# finding all the properties fo a node is not possible in a clean way for now.
|
||||
# so, I'll assume their names start with "property", and list all the node's attributes that fulfill that condition.
|
||||
# next, to check that those are indeed properties (in the blender sense), we need to check the class's type annotations.
|
||||
# those annotations are not even instances of bpy.types.Property, but tuples, with the first element being a function accessible at bpy.props.XXXProperty
|
||||
property_types = []
|
||||
for possible_prop_type in dir(bpy.props):
|
||||
if possible_prop_type.endswith('Property'):
|
||||
property_types.append( getattr(bpy.props, possible_prop_type) )
|
||||
possible_properties = []
|
||||
for attrname in dir(node):
|
||||
if attrname.startswith('property'):
|
||||
possible_properties.append(attrname)
|
||||
for attrname in possible_properties:
|
||||
if attrname not in node.__annotations__:
|
||||
continue
|
||||
if not isinstance(node.__annotations__[attrname], tuple):
|
||||
continue
|
||||
if node.__annotations__[attrname][0] in property_types:
|
||||
props[attrname] = attrname
|
||||
|
||||
return NodeReplacement(
|
||||
node.bl_idname, node.arm_version, node.bl_idname, type(node).arm_version,
|
||||
in_socket_mapping=in_socks, out_socket_mapping=out_socks,
|
||||
property_mapping=props
|
||||
)
|
||||
|
||||
def chain_with(self, other):
|
||||
"""modify the current NodeReplacement by "adding" a second replacement after it"""
|
||||
if self.to_node != other.from_node or self.to_node_version != other.from_node_version:
|
||||
raise TypeError('the given NodeReplacement-s could not be chained')
|
||||
self.to_node = other.to_node
|
||||
self.to_node_version = other.to_node_version
|
||||
|
||||
for i1, i2 in self.in_socket_mapping.items():
|
||||
i3 = other.in_socket_mapping[i2]
|
||||
self.in_socket_mapping[i1] = i3
|
||||
for i1, i2 in self.out_socket_mapping.items():
|
||||
i3 = other.out_socket_mapping[i2]
|
||||
self.out_socket_mapping[i1] = i3
|
||||
for p1, p2 in self.property_mapping.items():
|
||||
p3 = other.property_mapping[p2]
|
||||
self.property_mapping[p1] = p3
|
||||
|
||||
old_input_defaults = self.input_defaults
|
||||
self.input_defaults = other.input_defaults.copy()
|
||||
for i, x in old_input_defaults.items():
|
||||
self.input_defaults[ other.in_socket_mapping[i] ] = x
|
||||
|
||||
old_property_defaults = self.property_defaults
|
||||
self.property_defaults = other.property_defaults.copy()
|
||||
for p, x in old_property_defaults.items():
|
||||
self.property_defaults[ other.property_mapping[p] ] = x
|
||||
|
||||
class ArmNodeAddInputButton(bpy.types.Operator):
|
||||
"""Add a new input socket to the node set by node_index."""
|
||||
bl_idname = 'arm.node_add_input'
|
||||
|
|
275
blender/arm/logicnode/replacement.py
Normal file
275
blender/arm/logicnode/replacement.py
Normal file
|
@ -0,0 +1,275 @@
|
|||
"""
|
||||
This module contains the functionality to replace nodes by other nodes
|
||||
in order to keep files from older Armory versions compatible with newer versions.
|
||||
|
||||
Nodes can define custom update procedures which describe how the replacement
|
||||
should look like.
|
||||
|
||||
Original author: @niacdoial
|
||||
"""
|
||||
import os.path
|
||||
import time
|
||||
import traceback
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
|
||||
import bpy.props
|
||||
|
||||
import arm.log as log
|
||||
import arm.logicnode.arm_nodes as arm_nodes
|
||||
import arm.logicnode.arm_sockets
|
||||
|
||||
# List of errors that occurred during the replacement
|
||||
# Format: (error identifier, node.bl_idname (or None), tree name, exception traceback (optional))
|
||||
replacement_errors: List[Tuple[str, Optional[str], str, Optional[str]]] = []
|
||||
|
||||
|
||||
class NodeReplacement:
|
||||
"""
|
||||
Represents a simple replacement rule, this can replace nodes of one type to nodes of a second type.
|
||||
However, it is fairly limited. For instance, it assumes there are no changes in the type of the inputs or outputs
|
||||
Second, it also assumes that node properties (especially EnumProperties) keep the same possible values.
|
||||
|
||||
- from_node, from_node_version: the type of node to be removed, and its version number
|
||||
- to_node, to_node_version: the type of node which takes from_node's place, and its version number
|
||||
- *_socket_mapping: a map which defines how the sockets of the old node shall be connected to the new node
|
||||
{1: 2} means that anything connected to the socket with index 1 on the original node will be connected to the socket with index 2 on the new node
|
||||
- property_mapping: the mapping used to transfer the values of the old node's properties to the new node's properties.
|
||||
{"property0": "property1"} mean that the value of the new node's property1 should be the old node's property0's value.
|
||||
- input_defaults: a mapping used to give default values to the inputs which aren't overridden otherwise.
|
||||
- property_defaults: a mapping used to define the value of the new node's properties, when they aren't overridden otherwise.
|
||||
"""
|
||||
|
||||
def __init__(self, from_node: str, from_node_version: int, to_node: str, to_node_version: int,
|
||||
in_socket_mapping: Dict[int, int], out_socket_mapping: Dict[int, int], property_mapping: Optional[Dict[str, str]] = None,
|
||||
input_defaults: Optional[Dict[int, any]] = None, property_defaults: Optional[Dict[str, any]] = None):
|
||||
self.from_node = from_node
|
||||
self.to_node = to_node
|
||||
self.from_node_version = from_node_version
|
||||
self.to_node_version = to_node_version
|
||||
|
||||
self.in_socket_mapping = in_socket_mapping
|
||||
self.out_socket_mapping = out_socket_mapping
|
||||
self.property_mapping = {} if property_mapping is None else property_mapping
|
||||
|
||||
self.input_defaults = {} if input_defaults is None else input_defaults
|
||||
self.property_defaults = {} if property_defaults is None else property_defaults
|
||||
|
||||
@classmethod
|
||||
def Identity(cls, node: 'ArmLogicTreeNode'):
|
||||
"""Returns a NodeReplacement that does nothing, while operating on a given node.
|
||||
WARNING: it assumes that all node properties have names that start with "property"
|
||||
"""
|
||||
in_socks = {i: i for i in range(len(node.inputs))}
|
||||
out_socks = {i: i for i in range(len(node.outputs))}
|
||||
props = {}
|
||||
i = 0
|
||||
|
||||
# finding all the properties fo a node is not possible in a clean way for now.
|
||||
# so, I'll assume their names start with "property", and list all the node's attributes that fulfill that condition.
|
||||
# next, to check that those are indeed properties (in the blender sense), we need to check the class's type annotations.
|
||||
# those annotations are not even instances of bpy.types.Property, but tuples, with the first element being a function accessible at bpy.props.XXXProperty
|
||||
property_types = []
|
||||
for possible_prop_type in dir(bpy.props):
|
||||
if possible_prop_type.endswith('Property'):
|
||||
property_types.append(getattr(bpy.props, possible_prop_type))
|
||||
possible_properties = []
|
||||
for attrname in dir(node):
|
||||
if attrname.startswith('property'):
|
||||
possible_properties.append(attrname)
|
||||
for attrname in possible_properties:
|
||||
if attrname not in node.__annotations__:
|
||||
continue
|
||||
if not isinstance(node.__annotations__[attrname], tuple):
|
||||
continue
|
||||
if node.__annotations__[attrname][0] in property_types:
|
||||
props[attrname] = attrname
|
||||
|
||||
return NodeReplacement(
|
||||
node.bl_idname, node.arm_version, node.bl_idname, type(node).arm_version,
|
||||
in_socket_mapping=in_socks, out_socket_mapping=out_socks,
|
||||
property_mapping=props
|
||||
)
|
||||
|
||||
def chain_with(self, other):
|
||||
"""Modify the current NodeReplacement by "adding" a second replacement after it"""
|
||||
if self.to_node != other.from_node or self.to_node_version != other.from_node_version:
|
||||
raise TypeError('the given NodeReplacement-s could not be chained')
|
||||
self.to_node = other.to_node
|
||||
self.to_node_version = other.to_node_version
|
||||
|
||||
for i1, i2 in self.in_socket_mapping.items():
|
||||
i3 = other.in_socket_mapping[i2]
|
||||
self.in_socket_mapping[i1] = i3
|
||||
for i1, i2 in self.out_socket_mapping.items():
|
||||
i3 = other.out_socket_mapping[i2]
|
||||
self.out_socket_mapping[i1] = i3
|
||||
for p1, p2 in self.property_mapping.items():
|
||||
p3 = other.property_mapping[p2]
|
||||
self.property_mapping[p1] = p3
|
||||
|
||||
old_input_defaults = self.input_defaults
|
||||
self.input_defaults = other.input_defaults.copy()
|
||||
for i, x in old_input_defaults.items():
|
||||
self.input_defaults[ other.in_socket_mapping[i] ] = x
|
||||
|
||||
old_property_defaults = self.property_defaults
|
||||
self.property_defaults = other.property_defaults.copy()
|
||||
for p, x in old_property_defaults.items():
|
||||
self.property_defaults[ other.property_mapping[p] ] = x
|
||||
|
||||
|
||||
def replace(tree: bpy.types.NodeTree, node: 'ArmLogicTreeNode'):
|
||||
"""Replaces the given node with its replacement."""
|
||||
|
||||
# the node can either return a NodeReplacement object (for simple replacements)
|
||||
# or a brand new node, for more complex stuff.
|
||||
response = node.get_replacement_node(tree)
|
||||
|
||||
if isinstance(response, arm_nodes.ArmLogicTreeNode):
|
||||
newnode = response
|
||||
# some misc. properties
|
||||
newnode.parent = node.parent
|
||||
newnode.location = node.location
|
||||
newnode.select = node.select
|
||||
|
||||
elif isinstance(response, list): # a list of nodes:
|
||||
for newnode in response:
|
||||
newnode.parent = node.parent
|
||||
newnode.location = node.location
|
||||
newnode.select = node.select
|
||||
|
||||
elif isinstance(response, NodeReplacement):
|
||||
replacement = response
|
||||
# if the returned object is a NodeReplacement, check that it corresponds to the node (also, create the new node)
|
||||
if node.bl_idname != replacement.from_node or node.arm_version != replacement.from_node_version:
|
||||
raise LookupError("The provided NodeReplacement doesn't seem to correspond to the node needing replacement")
|
||||
|
||||
# Create the replacement node
|
||||
newnode = tree.nodes.new(response.to_node)
|
||||
if newnode.arm_version != replacement.to_node_version:
|
||||
tree.nodes.remove(newnode)
|
||||
raise LookupError("The provided NodeReplacement doesn't seem to correspond to the node needing replacement")
|
||||
|
||||
# some misc. properties
|
||||
newnode.parent = node.parent
|
||||
newnode.location = node.location
|
||||
newnode.select = node.select
|
||||
|
||||
# now, use the `replacement` to hook up the new node correctly
|
||||
# start by applying defaults
|
||||
for prop_name, prop_value in replacement.property_defaults.items():
|
||||
setattr(newnode, prop_name, prop_value)
|
||||
for input_id, input_value in replacement.input_defaults.items():
|
||||
input_socket = newnode.inputs[input_id]
|
||||
if isinstance(input_socket, arm.logicnode.arm_sockets.ArmCustomSocket):
|
||||
if input_socket.arm_socket_type != 'NONE':
|
||||
input_socket.default_value_raw = input_value
|
||||
elif input_socket.type != 'SHADER':
|
||||
# note: shader-type sockets don't have a default value...
|
||||
input_socket.default_value = input_value
|
||||
|
||||
# map properties
|
||||
for src_prop_name, dest_prop_name in replacement.property_mapping.items():
|
||||
setattr(newnode, dest_prop_name, getattr(node, src_prop_name))
|
||||
|
||||
# map inputs
|
||||
for src_socket_id, dest_socket_id in replacement.in_socket_mapping.items():
|
||||
src_socket = node.inputs[src_socket_id]
|
||||
dest_socket = newnode.inputs[dest_socket_id]
|
||||
if src_socket.is_linked:
|
||||
# an input socket only has one link
|
||||
datasource_socket = src_socket.links[0].from_socket
|
||||
tree.links.new(datasource_socket, dest_socket)
|
||||
else:
|
||||
if isinstance(dest_socket, arm.logicnode.arm_sockets.ArmCustomSocket):
|
||||
if dest_socket.arm_socket_type != 'NONE':
|
||||
dest_socket.default_value_raw = src_socket.default_value_raw
|
||||
elif dest_socket.type != 'SHADER':
|
||||
# note: shader-type sockets don't have a default value...
|
||||
dest_socket.default_value = src_socket.default_value
|
||||
|
||||
# map outputs
|
||||
for src_socket_id, dest_socket_id in replacement.out_socket_mapping.items():
|
||||
dest_socket = newnode.outputs[dest_socket_id]
|
||||
for link in node.outputs[src_socket_id].links:
|
||||
tree.links.new(dest_socket, link.to_socket)
|
||||
else:
|
||||
print(response)
|
||||
|
||||
tree.nodes.remove(node)
|
||||
|
||||
|
||||
def replace_all():
|
||||
"""Iterate through all logic node trees in the file and check for node updates/replacements to execute."""
|
||||
global replacement_errors
|
||||
|
||||
replacement_errors.clear()
|
||||
|
||||
for tree in bpy.data.node_groups:
|
||||
if tree.bl_idname == "ArmLogicTreeType":
|
||||
# Use list() to make a "static" copy. It's possible to iterate over it because nodes which get removed
|
||||
# from the tree leave python objects in the list
|
||||
for node in list(tree.nodes):
|
||||
# Blender nodes (layout)
|
||||
if not isinstance(node, arm_nodes.ArmLogicTreeNode):
|
||||
continue
|
||||
|
||||
# That node has been removed from the tree without replace() being called on it somehow
|
||||
elif node.type == '':
|
||||
continue
|
||||
|
||||
# Node type deleted. That's unusual. Or it has been replaced for a looong time
|
||||
elif not node.is_registered_node_type():
|
||||
replacement_errors.append(('unregistered', None, tree.name, None))
|
||||
|
||||
# Invalid version number
|
||||
elif not isinstance(type(node).arm_version, int):
|
||||
replacement_errors.append(('bad version', node.bl_idname, tree.name, None))
|
||||
|
||||
# Actual replacement
|
||||
elif node.arm_version < type(node).arm_version:
|
||||
try:
|
||||
replace(tree, node)
|
||||
except LookupError as err:
|
||||
replacement_errors.append(('update failed', node.bl_idname, tree.name, traceback.format_exc()))
|
||||
except Exception as err:
|
||||
replacement_errors.append(('misc.', node.bl_idname, tree.name, traceback.format_exc()))
|
||||
|
||||
# Node version is newer than supported by the class
|
||||
elif node.arm_version > type(node).arm_version:
|
||||
replacement_errors.append(('future version', node.bl_idname, tree.name, None))
|
||||
|
||||
# If possible, make a popup about the errors and write an error report into the .blend file's folder
|
||||
if len(replacement_errors) > 0:
|
||||
basedir = os.path.dirname(bpy.data.filepath)
|
||||
reportfile = os.path.join(
|
||||
basedir, 'node_update_failure.{:s}.txt'.format(
|
||||
time.strftime("%Y-%m-%dT%H-%M-%S%z")
|
||||
)
|
||||
)
|
||||
|
||||
with open(reportfile, 'w') as reportf:
|
||||
for error_type, node_class, tree_name, tb in replacement_errors:
|
||||
if error_type == 'unregistered':
|
||||
print(f"A node whose class doesn't exist was found in node tree \"{tree_name}\"", file=reportf)
|
||||
elif error_type == 'update failed':
|
||||
print(f"A node of type {node_class} in tree \"{tree_name}\" failed to be updated, "
|
||||
f"because there is no (longer?) an update routine for this version of the node.", file=reportf)
|
||||
elif error_type == 'future version':
|
||||
print(f"A node of type {node_class} in tree \"{tree_name}\" seemingly comes from a future version of armory. "
|
||||
f"Please check whether your version of armory is up to date", file=reportf)
|
||||
elif error_type == 'bad version':
|
||||
print(f"A node of type {node_class} in tree \"{tree_name}\" doesn't have version information attached to it. "
|
||||
f"If so, please check that the nodes in the file are compatible with the in-code node classes. "
|
||||
f"If this nodes comes from an add-on, please check that it is compatible with this version of armory.", file=reportf)
|
||||
elif error_type == 'misc.':
|
||||
print(f"A node of type {node_class} in tree \"{tree_name}\" failed to be updated, "
|
||||
f"because the node's update procedure itself failed. Original exception:"
|
||||
"\n" + tb + "\n", file=reportf)
|
||||
else:
|
||||
print(f"Whoops, we don't know what this error type (\"{error_type}\") means. You might want to report a bug here. "
|
||||
f"All we know is that it comes form a node of class {node_class} in the node tree called \"{tree_name}\".", file=reportf)
|
||||
|
||||
log.error(f'There were errors in the node update procedure, a detailed report has been written to {reportfile}')
|
||||
|
||||
bpy.ops.arm.show_node_update_errors()
|
|
@ -1,5 +1,3 @@
|
|||
import os.path
|
||||
import time
|
||||
from typing import Callable
|
||||
import webbrowser
|
||||
|
||||
|
@ -7,6 +5,7 @@ import bpy
|
|||
from bpy.props import BoolProperty, StringProperty
|
||||
|
||||
import arm.logicnode.arm_nodes as arm_nodes
|
||||
import arm.logicnode.replacement
|
||||
import arm.logicnode
|
||||
import arm.utils
|
||||
|
||||
|
@ -339,145 +338,6 @@ class ARMAddSetVarNode(bpy.types.Operator):
|
|||
return({'FINISHED'})
|
||||
|
||||
|
||||
def replace(tree: bpy.types.NodeTree, node: bpy.types.Node):
|
||||
"""Replaces the given node with its replacement."""
|
||||
|
||||
# the node can either return a NodeReplacement object (for simple replacements)
|
||||
# or a brand new node, for more complex stuff.
|
||||
response = node.get_replacement_node(tree)
|
||||
|
||||
if isinstance(response, bpy.types.Node):
|
||||
newnode = response
|
||||
# some misc. properties
|
||||
newnode.parent = node.parent
|
||||
newnode.location = node.location
|
||||
newnode.select = node.select
|
||||
elif isinstance(response, list): # a list of nodes:
|
||||
for newnode in response:
|
||||
newnode.parent = node.parent
|
||||
newnode.location = node.location
|
||||
newnode.select = node.select
|
||||
elif isinstance(response, arm_nodes.NodeReplacement):
|
||||
replacement = response
|
||||
# if the returned object is a NodeReplacement, check that it corresponds to the node (also, create the new node)
|
||||
if node.bl_idname != replacement.from_node or node.arm_version != replacement.from_node_version:
|
||||
raise LookupError("the provided NodeReplacement doesn't seem to correspond to the node needing replacement")
|
||||
newnode = tree.nodes.new(response.to_node)
|
||||
if newnode.arm_version != replacement.to_node_version:
|
||||
raise LookupError("the provided NodeReplacement doesn't seem to correspond to the node needing replacement")
|
||||
|
||||
# some misc. properties
|
||||
newnode.parent = node.parent
|
||||
newnode.location = node.location
|
||||
newnode.select = node.select
|
||||
|
||||
# now, use the `replacement` to hook up the new node correctly
|
||||
# start by applying defaults
|
||||
for prop_name, prop_value in replacement.property_defaults.items():
|
||||
setattr(newnode, prop_name, prop_value)
|
||||
for input_id, input_value in replacement.input_defaults.items():
|
||||
input_socket = newnode.inputs[input_id]
|
||||
if isinstance(input_socket, arm.logicnode.arm_sockets.ArmCustomSocket):
|
||||
if input_socket.arm_socket_type != 'NONE':
|
||||
input_socket.default_value_raw = input_value
|
||||
elif input_socket.type != 'SHADER':
|
||||
# note: shader-type sockets don't have a default value...
|
||||
input_socket.default_value = input_value
|
||||
|
||||
# map properties
|
||||
for src_prop_name, dest_prop_name in replacement.property_mapping.items():
|
||||
setattr(newnode, dest_prop_name, getattr(node, src_prop_name))
|
||||
|
||||
# map inputs
|
||||
for src_socket_id, dest_socket_id in replacement.in_socket_mapping.items():
|
||||
src_socket = node.inputs[src_socket_id]
|
||||
dest_socket = newnode.inputs[dest_socket_id]
|
||||
if src_socket.is_linked:
|
||||
# an input socket only has one link
|
||||
datasource_socket = src_socket.links[0].from_socket
|
||||
tree.links.new(datasource_socket, dest_socket)
|
||||
else:
|
||||
if isinstance(dest_socket, arm.logicnode.arm_sockets.ArmCustomSocket):
|
||||
if dest_socket.arm_socket_type != 'NONE':
|
||||
dest_socket.default_value_raw = src_socket.default_value_raw
|
||||
elif dest_socket.type != 'SHADER':
|
||||
# note: shader-type sockets don't have a default value...
|
||||
dest_socket.default_value = src_socket.default_value
|
||||
|
||||
# map outputs
|
||||
for src_socket_id, dest_socket_id in replacement.out_socket_mapping.items():
|
||||
dest_socket = newnode.outputs[dest_socket_id]
|
||||
for link in node.outputs[src_socket_id].links:
|
||||
tree.links.new(dest_socket, link.to_socket)
|
||||
else:
|
||||
print(response)
|
||||
tree.nodes.remove(node)
|
||||
|
||||
|
||||
def replaceAll():
|
||||
global replacement_errors
|
||||
list_of_errors = set()
|
||||
for tree in bpy.data.node_groups:
|
||||
if tree.bl_idname == "ArmLogicTreeType":
|
||||
for node in list(tree.nodes):
|
||||
# add the list() to make a "static" copy
|
||||
# (note: one can iterate it, because and nodes which get removed from the tree leave python objects in the list)
|
||||
if isinstance(node, (bpy.types.NodeFrame, bpy.types.NodeReroute) ):
|
||||
pass
|
||||
elif node.type=='':
|
||||
pass # that node has been removed from the tree without replace() being called on it somehow.
|
||||
elif not node.is_registered_node_type():
|
||||
# node type deleted. That's unusual. Or it has been replaced for a looong time.
|
||||
list_of_errors.add( ('unregistered', None, tree.name) )
|
||||
elif not isinstance(type(node).arm_version, int):
|
||||
list_of_errors.add( ('bad version', node.bl_idname, tree.name) )
|
||||
elif node.arm_version < type(node).arm_version:
|
||||
try:
|
||||
replace(tree, node)
|
||||
except LookupError as err:
|
||||
list_of_errors.add( ('update failed', node.bl_idname, tree.name) )
|
||||
except Exception as err:
|
||||
list_of_errors.add( ('misc.', node.bl_idname, tree.name) )
|
||||
elif node.arm_version > type(node).arm_version:
|
||||
list_of_errors.add( ('future version', node.bl_idname, tree.name) )
|
||||
|
||||
# if possible, make a popup about the errors.
|
||||
# also write an error report.
|
||||
if len(list_of_errors) > 0:
|
||||
print('there were errors in node replacement')
|
||||
basedir = os.path.dirname(bpy.data.filepath)
|
||||
reportfile = os.path.join(
|
||||
basedir, 'node_update_failure.{:s}.txt'.format(
|
||||
time.strftime("%Y-%m-%dT%H-%M-%S%z")
|
||||
)
|
||||
)
|
||||
reportf = open(reportfile, 'w')
|
||||
for error_type, node_class, tree_name in list_of_errors:
|
||||
if error_type == 'unregistered':
|
||||
print(f"A node whose class doesn't exist was found in node tree \"{tree_name}\"", file=reportf)
|
||||
elif error_type == 'update failed':
|
||||
print(f"A node of type {node_class} in tree \"{tree_name}\" failed to be updated, "
|
||||
f"because update isn't implemented (anymore?) for this version of the node", file=reportf)
|
||||
elif error_type == 'future version':
|
||||
print(f"A node of type {node_class} in tree \"{tree_name}\" seemingly comes from a future version of armory. "
|
||||
f"Please check whether your version of armory is up to date", file=reportf)
|
||||
elif error_type == 'bad version':
|
||||
print(f"A node of type {node_class} in tree \"{tree_name}\" Doesn't have version information attached to it. "
|
||||
f"If so, please check that the nodes in the file are compatible with the in-code node classes. "
|
||||
f"If this nodes comes from an add-on, please check that it is compatible with this version of armory.", file=reportf)
|
||||
elif error_type == 'misc.':
|
||||
print(f"A node of type {node_class} in tree \"{tree_name}\" failed to be updated, "
|
||||
f"because the node's update procedure itself failed.", file=reportf)
|
||||
else:
|
||||
print(f"Whoops, we don't know what this error type (\"{error_type}\") means. You might want to report a bug here. "
|
||||
f"All we know is that it comes form a node of class {node_class} in the node tree called \"{tree_name}\".", file=reportf)
|
||||
reportf.close()
|
||||
|
||||
replacement_errors = list_of_errors
|
||||
bpy.ops.arm.show_node_update_errors()
|
||||
replacement_errors = None
|
||||
|
||||
|
||||
class ReplaceNodesOperator(bpy.types.Operator):
|
||||
"""Automatically replaces deprecated nodes."""
|
||||
bl_idname = "node.replace"
|
||||
|
@ -485,7 +345,7 @@ class ReplaceNodesOperator(bpy.types.Operator):
|
|||
bl_description = "Replace deprecated nodes"
|
||||
|
||||
def execute(self, context):
|
||||
replaceAll()
|
||||
arm.logicnode.replacement.replace_all()
|
||||
return {'FINISHED'}
|
||||
|
||||
@classmethod
|
||||
|
|
|
@ -4,6 +4,7 @@ import re
|
|||
import multiprocessing
|
||||
|
||||
import arm.assets as assets
|
||||
import arm.logicnode.replacement
|
||||
import arm.make
|
||||
import arm.nodes_logic
|
||||
import arm.proxy
|
||||
|
@ -486,8 +487,9 @@ def update_armory_world():
|
|||
if rp.rp_gi != 'Off':
|
||||
rp.rp_gi = 'Off'
|
||||
rp.rp_voxelao = True
|
||||
|
||||
# Replace deprecated nodes
|
||||
arm.nodes_logic.replaceAll()
|
||||
arm.logicnode.replacement.replace_all()
|
||||
|
||||
print('Project updated to sdk v' + arm_version + ' (' + arm_commit + ')')
|
||||
wrd.arm_version = arm_version
|
||||
|
|
|
@ -7,6 +7,7 @@ from bpy.props import *
|
|||
import arm.api
|
||||
import arm.assets as assets
|
||||
import arm.log as log
|
||||
import arm.logicnode.replacement
|
||||
import arm.make as make
|
||||
import arm.make_state as state
|
||||
import arm.props as props
|
||||
|
@ -1481,7 +1482,7 @@ class ARM_PT_BakePanel(bpy.types.Panel):
|
|||
row.label(text="Last build completed in: " + str(bpy.context.scene["TLM_Buildstat"][0]))
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.label(text="Cycles Settings")
|
||||
|
||||
|
@ -1740,7 +1741,7 @@ class ARM_PT_BakePanel(bpy.types.Panel):
|
|||
row = layout.row(align=True)
|
||||
row.operator("tlm.select_lightmapped_objects")
|
||||
row = layout.row(align=True)
|
||||
|
||||
|
||||
##################
|
||||
#Additional settings
|
||||
row = layout.row(align=True)
|
||||
|
@ -1825,7 +1826,7 @@ class ARM_PT_BakePanel(bpy.types.Panel):
|
|||
if obj.TLM_ObjectProperties.tlm_postpack_object:
|
||||
if obj.TLM_ObjectProperties.tlm_postatlas_pointer == item.name:
|
||||
amount = amount + 1
|
||||
|
||||
|
||||
atlasUsedArea += int(obj.TLM_ObjectProperties.tlm_mesh_lightmap_resolution) ** 2
|
||||
|
||||
row = layout.row()
|
||||
|
@ -2313,7 +2314,7 @@ class ARM_OT_ShowNodeUpdateErrors(bpy.types.Operator):
|
|||
wrd = None # a helper internal variable
|
||||
|
||||
def draw_message_box(self, context):
|
||||
list_of_errors = arm.nodes_logic.replacement_errors.copy()
|
||||
list_of_errors = arm.logicnode.replacement.replacement_errors.copy()
|
||||
# note: list_of_errors is a set of tuples: `(error_type, node_class, tree_name)`
|
||||
# where `error_type` can be "unregistered", "update failed", "future version", "bad version", or "misc."
|
||||
|
||||
|
@ -2322,8 +2323,8 @@ class ARM_OT_ShowNodeUpdateErrors(bpy.types.Operator):
|
|||
|
||||
# this will help order versions better, somewhat.
|
||||
# note: this is NOT complete
|
||||
current_version_2 = tuple( current_version.split('.') )
|
||||
file_version_2 = tuple( file_version.split('.') )
|
||||
current_version_2 = tuple(current_version.split('.'))
|
||||
file_version_2 = tuple(file_version.split('.'))
|
||||
is_armory_upgrade = (current_version_2 > file_version_2)
|
||||
|
||||
error_types = set()
|
||||
|
@ -2338,8 +2339,8 @@ class ARM_OT_ShowNodeUpdateErrors(bpy.types.Operator):
|
|||
layout = layout.column(align=True)
|
||||
layout.alignment = 'EXPAND'
|
||||
|
||||
layout.label(text="Some nodes failed to be updated to the current armory version", icon="ERROR")
|
||||
if current_version==file_version:
|
||||
layout.label(text="Some nodes failed to be updated to the current Armory version", icon="ERROR")
|
||||
if current_version == file_version:
|
||||
layout.label(text="(This might be because you are using a development snapshot, or a homemade version ;) )", icon='BLANK1')
|
||||
elif not is_armory_upgrade:
|
||||
layout.label(text="(Please note that it is not possible do downgrade nodes to a previous version either.", icon='BLANK1')
|
||||
|
@ -2351,7 +2352,7 @@ class ARM_OT_ShowNodeUpdateErrors(bpy.types.Operator):
|
|||
|
||||
if 'update failed' in error_types:
|
||||
layout.label(text="Some nodes do not have an update procedure to deal with the version saved in this file.", icon='BLANK1')
|
||||
if current_version==file_version:
|
||||
if current_version == file_version:
|
||||
layout.label(text="(if you are a developer, this might be because you didn't implement it yet.)", icon='BLANK1')
|
||||
if 'bad version' in error_types:
|
||||
layout.label(text="Some nodes do not have version information attached to them.", icon='BLANK1')
|
||||
|
@ -2385,6 +2386,7 @@ class ARM_OT_ShowNodeUpdateErrors(bpy.types.Operator):
|
|||
row = layout.row(align=True)
|
||||
row.active_default = False
|
||||
row.operator('arm.discard_popup', text='Ok')
|
||||
row.operator('arm.open_project_folder', text='Open Project Folder', icon="FILE_FOLDER")
|
||||
|
||||
def execute(self, context):
|
||||
ARM_OT_ShowNodeUpdateErrors.wrd = bpy.data.worlds['Arm']
|
||||
|
@ -2406,7 +2408,7 @@ class ARM_OT_UpdateFileSDK(bpy.types.Operator):
|
|||
rp.rp_voxelao = True
|
||||
|
||||
# Replace deprecated nodes
|
||||
arm.nodes_logic.replaceAll()
|
||||
arm.logicnode.replacement.replace_all()
|
||||
|
||||
wrd.arm_version = props.arm_version
|
||||
wrd.arm_commit = props.arm_commit
|
||||
|
@ -2484,7 +2486,7 @@ class ArmoryUpdateListInstalledVSButton(bpy.types.Operator):
|
|||
return {"CANCELLED"}
|
||||
if not arm.utils.get_os_is_windows():
|
||||
return {"CANCELLED"}
|
||||
|
||||
|
||||
wrd = bpy.data.worlds['Arm']
|
||||
items, err = arm.utils.get_list_installed_vs_version()
|
||||
if len(err) > 0:
|
||||
|
@ -2500,7 +2502,7 @@ class ArmoryUpdateListInstalledVSButton(bpy.types.Operator):
|
|||
prev_select = wrd.arm_project_win_list_vs
|
||||
res_items_enum = []
|
||||
for vs in items_enum:
|
||||
l_vs = list(vs)
|
||||
l_vs = list(vs)
|
||||
for ver in items:
|
||||
if l_vs[0] == ver[0]:
|
||||
l_vs[1] = l_vs[1] + ' (installed)'
|
||||
|
|
Loading…
Reference in a new issue