Merge pull request #2046 from MoritzBrueckner/node-replacement

Fix and further improve node replacement system
This commit is contained in:
Lubos Lenco 2020-12-06 09:17:38 +01:00 committed by GitHub
commit fdcdf86c24
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 300 additions and 252 deletions

View file

@ -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'

View 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()

View file

@ -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

View file

@ -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

View file

@ -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)'