From 4ebd70d8245e837fde317f5a3538c23dd5a8749a Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Tue, 14 Jul 2015 13:07:16 -0400 Subject: [PATCH 1/6] Begin work on PythonFileMod nodes This has some simple attribute nodes for example purposes. Unfortunately, this is so far UI only. More node types need to be added, then we can begin working on exporting. --- korman/nodes/__init__.py | 2 + korman/nodes/node_conditions.py | 1 + korman/nodes/node_core.py | 5 + korman/nodes/node_python.py | 314 ++++++++++++++++++++++++++++++++ korman/nodes/node_responder.py | 1 + korman/operators/__init__.py | 1 + korman/operators/op_nodes.py | 87 +++++++++ 7 files changed, 411 insertions(+) create mode 100644 korman/nodes/node_python.py create mode 100644 korman/operators/op_nodes.py diff --git a/korman/nodes/__init__.py b/korman/nodes/__init__.py index 41e9155..8d9ec3d 100644 --- a/korman/nodes/__init__.py +++ b/korman/nodes/__init__.py @@ -23,6 +23,7 @@ from .node_avatar import * from .node_conditions import * from .node_core import * from .node_messages import * +from .node_python import * from .node_responder import * class PlasmaNodeCategory(NodeCategory): @@ -42,6 +43,7 @@ _kategory_names = { "CONDITIONS": "Conditions", "LOGIC": "Logic", "MSG": "Message", + "PYTHON": "Python", } # Now, generate the categories as best we can... diff --git a/korman/nodes/node_conditions.py b/korman/nodes/node_conditions.py index 0ac2fe6..ea9af0b 100644 --- a/korman/nodes/node_conditions.py +++ b/korman/nodes/node_conditions.py @@ -135,6 +135,7 @@ class PlasmaClickableRegionNode(PlasmaNodeBase, bpy.types.Node): default="hull") def init(self, context): + self.outputs.new("PlasmaPythonReferenceNodeSocket", "References", "keyref") self.outputs.new("PlasmaClickableRegionSocket", "Satisfies", "satisfies") def draw_buttons(self, context, layout): diff --git a/korman/nodes/node_core.py b/korman/nodes/node_core.py index 358cd75..127f5d6 100644 --- a/korman/nodes/node_core.py +++ b/korman/nodes/node_core.py @@ -123,6 +123,11 @@ class PlasmaNodeBase: out_socket = out_key link = self.id_data.links.new(in_socket, out_socket) + @property + def node_path(self): + """Returns an absolute path to this Node. Needed because repr() uses an elipsis...""" + return "{}.{}".format(repr(self.id_data), self.path_from_id()) + @classmethod def poll(cls, context): return (context.bl_idname == "PlasmaNodeTree") diff --git a/korman/nodes/node_python.py b/korman/nodes/node_python.py new file mode 100644 index 0000000..a5471ad --- /dev/null +++ b/korman/nodes/node_python.py @@ -0,0 +1,314 @@ +# This file is part of Korman. +# +# Korman is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Korman is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Korman. If not, see . + +import bpy +from bpy.props import * +import os.path +from PyHSPlasma import * + +from .node_core import * + +_attrib_colors = { + "ptAttribActivator": (0.451, 0.0, 0.263, 1.0), + "ptAttribActivatorList": (0.451, 0.0, 0.263, 1.0), + "ptAttribBoolean": (0.71, 0.706, 0.655, 1.0), + "ptAttribFloat": (0.443, 0.439, 0.392, 1.0), + ("ptAttribFloat", "ptAttribInt"): (0.443, 0.439, 0.392, 1.0), + "ptAttribInt": (0.443, 0.439, 0.392, 1.0), + "ptAttribResponder": (0.031, 0.110, 0.290, 1.0), + "ptAttribResponderList": (0.031, 0.110, 0.290, 1.0), + "ptAttribString": (0.675, 0.659, 0.494, 1.0), +} + +_single_user_attribs = { + "ptAttribBoolean", "ptAttribInt", "ptAttribFloat", "ptAttribString", "ptAttribDropDownList", + "ptAttribSceneobject", "ptAttribDynamicMap", "ptAttribGUIDialog", "ptAttribExcludeRegion", + "ptAttribWaveSet", "ptAttribSwimCurrent", "ptAttribAnimation", "ptAttribBehavior", + "ptAttribMaterial", "ptAttribMaterialAnimation", "ptAttribGUIPopUpMenu", "ptAttribGUISkin", + "ptAttribGrassShader", +} + +class PlasmaAttribute(bpy.types.PropertyGroup): + attribute_id = IntProperty() + attribute_type = StringProperty() + attribute_name = StringProperty() + attribute_description = StringProperty() + + # These shall be default values + value_string = StringProperty() + value_int = IntProperty() + value_float = FloatProperty() + value_bool = BoolProperty() + + _simple_attrs = { + "ptAttribString": "value_string", + "ptAttribInt": "value_int", + "ptAttribFloat": "value_float", + "ptAttribBoolean": "value_bool", + } + + @property + def is_simple_value(self): + return self.attribute_type in self._simple_attrs + + def _get_simple_value(self): + return getattr(self, self._simple_attrs[self.attribute_type]) + def _set_simple_value(self, value): + setattr(self, self._simple_attrs[self.attribute_type], value) + simple_value = property(_get_simple_value, _set_simple_value) + + +class PlasmaPythonFileNode(PlasmaNodeBase, bpy.types.Node): + bl_category = "PYTHON" + bl_idname = "PlasmaPythonFileNode" + bl_label = "Python File" + bl_width_default = 210 + + def _update_pyfile(self, context): + # Changing the file path? let's start anew. + self.attributes.clear() + self.inputs.clear() + + # Now populate that BAMF + bpy.ops.node.plasma_attributes_to_node(node_path=self.node_path, python_path=self.filepath) + + filename = StringProperty(name="File", + description="Python Filename") + filepath = StringProperty(update=_update_pyfile, + options={"HIDDEN"}) + + attributes = CollectionProperty(type=PlasmaAttribute, options={"HIDDEN"}) + dirty_attributes = BoolProperty(options={"HIDDEN"}) + + @property + def attribute_map(self): + return { i.attribute_id: i for i in self.attributes } + + def draw_buttons(self, context, layout): + row = layout.row(align=True) + if self.filename: + row.prop(self, "filename") + operator = row.operator("node.plasma_attributes_to_node", icon="FILE_REFRESH", text="") + operator.python_path = self.filepath + operator.node_path = self.node_path + + op_text = "" if self.filename else "Select" + operator = row.operator("file.plasma_file_picker", icon="SCRIPT", text=op_text) + operator.filter_glob = "*.py" + operator.data_path = self.node_path + operator.filepath_property = "filepath" + operator.filename_property = "filename" + + def _get_attrib_sockets(self, idx): + for i in self.inputs: + if i.attribute_id == idx: + yield i + + def _make_attrib_socket(self, attrib, is_init=False): + new_pos = len(self.inputs) + if not is_init: + for i, socket in enumerate(self.inputs): + if attrib.attribute_id < socket.attribute_id: + new_pos = i + break + old_pos = len(self.inputs) + socket = self.inputs.new("PlasmaPythonFileNodeSocket", "", "") + socket.attribute_id = attrib.attribute_id + if not is_init and new_pos != old_pos: + self.inputs.move(old_pos, new_pos) + + def update(self): + attribs = self.attribute_map + empty = not self.inputs + for idx in sorted(attribs): + attrib = attribs[idx] + + # Delete any attribute sockets whose type changed + for i in self._get_attrib_sockets(attrib.attribute_id): + if i.attribute_type != attrib.attribute_type: + self.inputs.remove(i) + + # Fetch the list of sockets again because we may have nuked some + inputs = list(self._get_attrib_sockets(attrib.attribute_id)) + if not inputs: + self._make_attrib_socket(attrib, empty) + elif attrib.attribute_type not in _single_user_attribs: + unconnected = [socket for socket in inputs if not socket.is_linked] + if not unconnected: + self._make_attrib_socket(attrib, empty) + while len(unconnected) > 1: + self.inputs.remove(unconnected.pop()) + + +class PlasmaPythonFileNodeSocket(bpy.types.NodeSocket): + attribute_id = IntProperty(options={"HIDDEN"}) + + @property + def attribute_description(self): + return self.node.attribute_map[self.attribute_id].attribute_description + + @property + def attribute_name(self): + return self.node.attribute_map[self.attribute_id].attribute_name + + @property + def attribute_type(self): + return self.node.attribute_map[self.attribute_id].attribute_type + + def draw(self, context, layout, node, text): + layout.alignment = "LEFT" + layout.label("ID: {}".format(self.attribute_id)) + layout.label(self.attribute_description) + + def draw_color(self, context, node): + return _attrib_colors.get(self.attribute_type, (0.0, 0.0, 0.0, 1.0)) + + @property + def is_simple_value(self): + return self.node.attribute_map[self.attribute_id].is_simple_value + + @property + def simple_value(self): + return self.node.attribute_map[self.attribute_id].simple_value + + +class PlasmaPythonAttribNodeSocket(bpy.types.NodeSocket): + def draw(self, context, layout, node, text): + attrib = node.to_socket + if attrib is None: + layout.label(text) + else: + layout.label("ID: {}".format(attrib.attribute_id)) + + def draw_color(self, context, node): + return _attrib_colors.get(node.pl_attrib, (0.0, 0.0, 0.0, 1.0)) + + +class PlasmaPythonReferenceNodeSocket(PlasmaNodeSocketBase, bpy.types.NodeSocket): + bl_color = (0.031, 0.110, 0.290, 1.0) + + +class PlasmaAttribNodeBase(PlasmaNodeBase): + def init(self, context): + self.outputs.new("PlasmaPythonAttribNodeSocket", "Python File", "pfm") + + @property + def attribute_name(self): + attr = self.to_socket + return "Value" if attr is None else attr.attribute_name + + @property + def to_socket(self): + """Returns the socket linked to IF only one link has been made""" + socket = self.outputs[0] + if len(socket.links) == 1: + return socket.links[0].to_socket + return None + + def update(self): + pl_id = self.pl_attrib + socket = self.outputs[0] + for link in socket.links: + if link.to_node.bl_idname != "PlasmaPythonFileNode": + self.id_data.links.remove(link) + if isinstance(pl_id, tuple): + if link.to_socket.attribute_type not in pl_id: + self.id_data.links.remove(link) + else: + if pl_id != link.to_socket.attribute_type: + self.id_data.links.remove(link) + + +class PlasmaAttribBoolNode(PlasmaAttribNodeBase, bpy.types.Node): + bl_category = "PYTHON" + bl_idname = "PlasmaAttribBoolNode" + bl_label = "Boolean Attribute" + + def _on_update(self, context): + self.inited = True + + pl_attrib = "ptAttribBoolean" + value = BoolProperty() + inited = BoolProperty(options={"HIDDEN"}) + + def draw_buttons(self, context, layout): + layout.prop(self, "value", text=self.attribute_name) + + def update(self): + super().update() + attrib = self.to_socket + if attrib is not None and not self.inited: + self.value = attrib.simple_value + self.inited = True + + +class PlasmaAttribNumericNode(PlasmaAttribNodeBase, bpy.types.Node): + bl_category = "PYTHON" + bl_idname = "PlasmaAttribIntNode" + bl_label = "Numeric Attribute" + + def _on_update_int(self, context): + self.value_float = float(self.value_int) + self.inited = True + + def _on_update_float(self, context): + self.value_int = int(self.value_float) + self.inited = True + + pl_attrib = ("ptAttribFloat", "ptAttribInt") + value_int = IntProperty(update=_on_update_int, options={"HIDDEN"}) + value_float = FloatProperty(update=_on_update_float, options={"HIDDEN"}) + inited = BoolProperty(options={"HIDDEN"}) + + def init(self, context): + super().init(context) + # because we're trying to be for both int and float... + self.outputs[0].link_limit = 1 + + def draw_buttons(self, context, layout): + attrib = self.to_socket + if attrib is None: + layout.prop(self, "value_int", text="Value") + elif attrib.attribute_type == "ptAttribFloat": + layout.prop(self, "value_float", text=attrib.name) + elif attrib.attribute_type == "ptAttribInt": + layout.prop(self, "value_int", text=attrib.name) + else: + raise RuntimeError() + + def update(self): + super().update() + attrib = self.to_socket + if attrib is not None and not self.inited: + self.value = attrib.simple_value + self.inited = True + + +class PlasmaAttribStringNode(PlasmaAttribNodeBase, bpy.types.Node): + bl_category = "PYTHON" + bl_idname = "PlasmaAttribStringNode" + bl_label = "String Attribute" + + pl_attrib = "ptAttribString" + value = StringProperty() + + def draw_buttons(self, context, layout): + layout.prop(self, "value", text=self.attribute_name) + + def update(self): + super().update() + attrib = self.to_socket + if attrib is not None: + self.value = attrib.simple_value diff --git a/korman/nodes/node_responder.py b/korman/nodes/node_responder.py index 343b8da..9ad6ab0 100644 --- a/korman/nodes/node_responder.py +++ b/korman/nodes/node_responder.py @@ -38,6 +38,7 @@ class PlasmaResponderNode(PlasmaNodeVariableInput, bpy.types.Node): def init(self, context): self.inputs.new("PlasmaConditionSocket", "Condition", "condition") + self.outputs.new("PlasmaPythonReferenceNodeSocket", "References", "keyref") self.outputs.new("PlasmaRespStateSocket", "States", "states") def draw_buttons(self, context, layout): diff --git a/korman/operators/__init__.py b/korman/operators/__init__.py index 65305c0..0c03ab1 100644 --- a/korman/operators/__init__.py +++ b/korman/operators/__init__.py @@ -16,6 +16,7 @@ from . import op_export as exporter from . import op_lightmap as lightmap from . import op_modifier as modifier +from . import op_nodes as nodes from . import op_toolbox as toolbox from . import op_world as world diff --git a/korman/operators/op_nodes.py b/korman/operators/op_nodes.py new file mode 100644 index 0000000..17a8579 --- /dev/null +++ b/korman/operators/op_nodes.py @@ -0,0 +1,87 @@ +# This file is part of Korman. +# +# Korman is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Korman is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Korman. If not, see . + +import bpy +from bpy.props import * +import itertools + +class NodeOperator: + @classmethod + def poll(cls, context): + return context.scene.render.engine == "PLASMA_GAME" + + +class SelectFileOperator(NodeOperator, bpy.types.Operator): + bl_idname = "file.plasma_file_picker" + bl_label = "Select" + + filter_glob = StringProperty(options={"HIDDEN"}) + filepath = StringProperty(subtype="FILE_PATH") + filename = StringProperty(options={"HIDDEN"}) + + data_path = StringProperty(options={"HIDDEN"}) + filepath_property = StringProperty(description="Name of property to store filepath in", options={"HIDDEN"}) + filename_property = StringProperty(description="Name of property to store filename in", options={"HIDDEN"}) + + def execute(self, context): + dest = eval(self.data_path) + if self.filepath_property: + setattr(dest, self.filepath_property, self.filepath) + if self.filename_property: + setattr(dest, self.filename_property, self.filename) + return {"FINISHED"} + + def invoke(self, context, event): + context.window_manager.fileselect_add(self) + return {"RUNNING_MODAL"} + + +class PlPyAttributeNodeOperator(NodeOperator, bpy.types.Operator): + bl_idname = "node.plasma_attributes_to_node" + bl_label = "R" + bl_options = {"INTERNAL"} + + python_path = StringProperty(subtype="FILE_PATH") + node_path = StringProperty() + + def execute(self, context): + from ..plasma_attributes import get_attributes + attribs = get_attributes(self.python_path) + + node = eval(self.node_path) + node_attrib_map = node.attribute_map + node_attribs = node.attributes + + # Remove any that p00fed + for cached in node.attributes: + if cached.attribute_id not in attribs: + node_attribs.remove(cached) + + # Update or create + for idx, attrib in attribs.items(): + cached = node_attrib_map.get(idx, None) + if cached is None: + cached = node_attribs.add() + cached.attribute_id = idx + cached.attribute_type = attrib["type"] + cached.attribute_name = attrib["name"] + cached.attribute_description = attrib["desc"] + default = attrib.get("default", None) + if default is not None and cached.is_simple_value: + cached.simple_value = default + + # Manually cause the node to update its inputs + node.update() + return {"FINISHED"} From 0b4ddd2bc044b8953ccce22c0ea478ffceae32e2 Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Tue, 14 Jul 2015 20:59:06 -0400 Subject: [PATCH 2/6] PFM exporting Hopefully this isn't too holey --- korman/nodes/node_python.py | 285 ++++++++++++++++++++++++++++++----- korman/operators/op_nodes.py | 2 - 2 files changed, 247 insertions(+), 40 deletions(-) diff --git a/korman/nodes/node_python.py b/korman/nodes/node_python.py index a5471ad..9918604 100644 --- a/korman/nodes/node_python.py +++ b/korman/nodes/node_python.py @@ -20,18 +20,6 @@ from PyHSPlasma import * from .node_core import * -_attrib_colors = { - "ptAttribActivator": (0.451, 0.0, 0.263, 1.0), - "ptAttribActivatorList": (0.451, 0.0, 0.263, 1.0), - "ptAttribBoolean": (0.71, 0.706, 0.655, 1.0), - "ptAttribFloat": (0.443, 0.439, 0.392, 1.0), - ("ptAttribFloat", "ptAttribInt"): (0.443, 0.439, 0.392, 1.0), - "ptAttribInt": (0.443, 0.439, 0.392, 1.0), - "ptAttribResponder": (0.031, 0.110, 0.290, 1.0), - "ptAttribResponderList": (0.031, 0.110, 0.290, 1.0), - "ptAttribString": (0.675, 0.659, 0.494, 1.0), -} - _single_user_attribs = { "ptAttribBoolean", "ptAttribInt", "ptAttribFloat", "ptAttribString", "ptAttribDropDownList", "ptAttribSceneobject", "ptAttribDynamicMap", "ptAttribGUIDialog", "ptAttribExcludeRegion", @@ -40,6 +28,61 @@ _single_user_attribs = { "ptAttribGrassShader", } +_attrib2param = { + "ptAttribInt": plPythonParameter.kInt, + "ptAttribFloat": plPythonParameter.kFloat, + "ptAttribBoolean": plPythonParameter.kBoolean, + "ptAttribString": plPythonParameter.kString, + "ptAttribSceneobject": plPythonParameter.kSceneObject, + "ptAttribSceneobjectList": plPythonParameter.kSceneObjectList, + "ptAttribActivator": plPythonParameter.kActivator, + "ptAttribActivatorList": plPythonParameter.kActivator, + "ptAttribNamedActivator": plPythonParameter.kActivator, + "ptAttribResponder": plPythonParameter.kResponder, + "ptAttribResponderList": plPythonParameter.kResponder, + "ptAttribNamedResponder": plPythonParameter.kResponder, + "ptAttribDynamicMap": plPythonParameter.kDynamicText, + "ptAttribGUIDialog": plPythonParameter.kGUIDialog, + "ptAttribExcludeRegion": plPythonParameter.kExcludeRegion, + "ptAttribAnimation": plPythonParameter.kAnimation, + "ptAttribBehavior": plPythonParameter.kBehavior, + "ptAttribMaterial": plPythonParameter.kMaterial, + "ptAttribMaterialList": plPythonParameter.kMaterial, + "ptAttribGUIPopUpMenu": plPythonParameter.kGUIPopUpMenu, + "ptAttribGUISkin": plPythonParameter.kGUISkin, + "ptAttribWaveSet": plPythonParameter.kWaterComponent, + "ptAttribSwimCurrent": plPythonParameter.kSwimCurrentInterface, + "ptAttribClusterList": plPythonParameter.kClusterComponent, + "ptAttribMaterialAnimation": plPythonParameter.kMaterialAnimation, + "ptAttribGrassShader": plPythonParameter.kGrassShaderComponent, +} + +_attrib_key_types = { + "ptAttribSceneobject": plFactory.ClassIndex("plSceneObject"), + "ptAttribSceneobjectList": plFactory.ClassIndex("plSceneObject"), + "ptAttribActivator": plFactory.ClassIndex("plLogicModifier"), + "ptAttribActivatorList": plFactory.ClassIndex("plLogicModifier"), + "ptAttribNamedActivator": plFactory.ClassIndex("plLogicModifier"), + "ptAttribResponder": plFactory.ClassIndex("plResponderModifier"), + "ptAttribResponderList": plFactory.ClassIndex("plResponderModifier"), + "ptAttribNamedResponder": plFactory.ClassIndex("plResponderModifier"), + "ptAttribDynamicMap": plFactory.ClassIndex("plDynamicTextMap"), + "ptAttribGUIDialog": plFactory.ClassIndex("pfGUIDialogMod"), + "ptAttribExcludeRegion": plFactory.ClassIndex("plExcludeRegionMod"), + "ptAttribAnimation": plFactory.ClassIndex("plAGMasterMod"), + "ptAttribBehavior": plFactory.ClassIndex("plMultistageBehMod"), + "ptAttribMaterial": plFactory.ClassIndex("plLayer"), + "ptAttribMaterialList": plFactory.ClassIndex("plLayer"), + "ptAttribGUIPopUpMenu": plFactory.ClassIndex("pfGUIPopUpMenu"), + "ptAttribGUISkin": plFactory.ClassIndex("pfGUISkin"), + "ptAttribWaveSet": plFactory.ClassIndex("plWaveSet7"), + "ptAttribSwimCurrent": (plFactory.ClassIndex("plSwimCircularCurrentRegion"), + plFactory.ClassIndex("plSwimStraightCurrentRegion")), + "ptAttribClusterList": plFactory.ClassIndex("plClusterGroup"), + "ptAttribMaterialAnimation": plFactory.ClassIndex("plLayerAnimation"), + "ptAttribGrassShader": plFactory.ClassIndex("plGrassShaderMod"), +} + class PlasmaAttribute(bpy.types.PropertyGroup): attribute_id = IntProperty() attribute_type = StringProperty() @@ -76,12 +119,18 @@ class PlasmaPythonFileNode(PlasmaNodeBase, bpy.types.Node): bl_label = "Python File" bl_width_default = 210 - def _update_pyfile(self, context): - # Changing the file path? let's start anew. - self.attributes.clear() - self.inputs.clear() + class _NoUpdate: + def __init__(self, node): + self._node = node + def __enter__(self): + self._node.no_update = True + def __exit__(self, type, value, traceback): + self._node.no_update = False - # Now populate that BAMF + def _update_pyfile(self, context): + with self._NoUpdate(self) as _hack: + self.attributes.clear() + self.inputs.clear() bpy.ops.node.plasma_attributes_to_node(node_path=self.node_path, python_path=self.filepath) filename = StringProperty(name="File", @@ -90,7 +139,7 @@ class PlasmaPythonFileNode(PlasmaNodeBase, bpy.types.Node): options={"HIDDEN"}) attributes = CollectionProperty(type=PlasmaAttribute, options={"HIDDEN"}) - dirty_attributes = BoolProperty(options={"HIDDEN"}) + no_update = BoolProperty(default=False, options={"HIDDEN", "SKIP_SAVE"}) @property def attribute_map(self): @@ -111,6 +160,41 @@ class PlasmaPythonFileNode(PlasmaNodeBase, bpy.types.Node): operator.filepath_property = "filepath" operator.filename_property = "filename" + def get_key(self, exporter, so): + return exporter.mgr.find_create_key(plPythonFileMod, name=self.key_name, so=so) + + def export(self, exporter, bo, so): + pfm = self.get_key(exporter, so).object + pfm.filename = os.path.splitext(self.filename)[0] + attrib_sockets = (i for i in self.inputs if i.is_linked) + for socket in attrib_sockets: + attrib = socket.attribute_type + from_node = socket.links[0].from_node + + param = plPythonParameter() + param.id = socket.attribute_id + param.valueType = _attrib2param[attrib] + if socket.is_simple_value: + param.value = from_node.value + else: + key = from_node.get_key(exporter, so) + if key is None: + msg = "'{}' Node '{}' didn't return a key and therefore will be unavailable to Python".format( + self.id_data.name, from_node.name) + exporter.report.warn(msg, indent=3) + else: + key_type = _attrib_key_types[attrib] + if isinstance(key_type, tuple): + good_key = key.type in key_type + else: + good_key = key.type == key.type + if not good_key: + msg = "'{}' Node '{}' returned an unexpected key type '{}'".format( + self.id_data.name, from_node.name, plFactory.ClassName(key.type)) + exporter.report.warn(msg, indent=3) + param.value = key + pfm.addParameter(param) + def _get_attrib_sockets(self, idx): for i in self.inputs: if i.attribute_id == idx: @@ -124,32 +208,35 @@ class PlasmaPythonFileNode(PlasmaNodeBase, bpy.types.Node): new_pos = i break old_pos = len(self.inputs) - socket = self.inputs.new("PlasmaPythonFileNodeSocket", "", "") + socket = self.inputs.new("PlasmaPythonFileNodeSocket", attrib.attribute_name) socket.attribute_id = attrib.attribute_id if not is_init and new_pos != old_pos: self.inputs.move(old_pos, new_pos) def update(self): - attribs = self.attribute_map - empty = not self.inputs - for idx in sorted(attribs): - attrib = attribs[idx] - - # Delete any attribute sockets whose type changed - for i in self._get_attrib_sockets(attrib.attribute_id): - if i.attribute_type != attrib.attribute_type: - self.inputs.remove(i) - - # Fetch the list of sockets again because we may have nuked some - inputs = list(self._get_attrib_sockets(attrib.attribute_id)) - if not inputs: - self._make_attrib_socket(attrib, empty) - elif attrib.attribute_type not in _single_user_attribs: - unconnected = [socket for socket in inputs if not socket.is_linked] - if not unconnected: + if self.no_update: + return + with self._NoUpdate(self) as _no_recurse: + attribs = self.attribute_map + empty = not self.inputs + for idx in sorted(attribs): + attrib = attribs[idx] + + # Delete any attribute sockets whose type changed + for i in self._get_attrib_sockets(idx): + if i.attribute_type != attrib.attribute_type: + self.inputs.remove(i) + + # Fetch the list of sockets again because we may have nuked some + inputs = list(self._get_attrib_sockets(idx)) + if not inputs: self._make_attrib_socket(attrib, empty) - while len(unconnected) > 1: - self.inputs.remove(unconnected.pop()) + elif attrib.attribute_type not in _single_user_attribs: + unconnected = [socket for socket in inputs if not socket.is_linked] + if not unconnected: + self._make_attrib_socket(attrib, empty) + while len(unconnected) > 1: + self.inputs.remove(unconnected.pop()) class PlasmaPythonFileNodeSocket(bpy.types.NodeSocket): @@ -217,6 +304,15 @@ class PlasmaAttribNodeBase(PlasmaNodeBase): return socket.links[0].to_socket return None + @classmethod + def register(cls): + pl_attrib = cls.pl_attrib + if isinstance(pl_attrib, tuple): + color = _attrib_colors.get(pl_attrib, None) + if color is not None: + for i in pl_attrib: + _attrib_colors[i] = color + def update(self): pl_id = self.pl_attrib socket = self.outputs[0] @@ -295,6 +391,54 @@ class PlasmaAttribNumericNode(PlasmaAttribNodeBase, bpy.types.Node): self.value = attrib.simple_value self.inited = True + @property + def value(self): + attrib = self.to_socket + if attrib is None or attrib.attribute_type == "ptAttribInt": + return self.value_int + else: + return self.value_float + + +class PlasmaAttribObjectNode(PlasmaAttribNodeBase, bpy.types.Node): + bl_category = "PYTHON" + bl_idname = "PlasmaAttribObjectNode" + bl_label = "Object Attribute" + + pl_attrib = ("ptAttribSceneobject", "ptAttribSceneobjectList", "ptAttribAnimation") + + object_name = StringProperty(name="Object", + description="Object containing the required data") + + def init(self, context): + super().init(context) + # keep the code simple + self.outputs[0].link_limit = 1 + + def draw_buttons(self, context, layout): + layout.prop_search(self, "object_name", bpy.data, "objects", text=self.attribute_name) + + def get_key(self, exporter, so): + attrib = self.to_socket + if attrib is None: + self.raise_error("must be connected to a Python File node!") + attrib = attrib.attribute_type + + bo = bpy.objects.data.get(self.object_name, None) + if bo is None: + self.raise_error("invalid object specified: '{}'".format(self.object_name)) + ref_so_key = exporter.mgr.find_create_key(plSceneObject, bl=bo) + ref_so = ref_so_key.object + + # Add your attribute type handling here... + if attrib in {"ptAttribSceneobject", "ptAttribSceneobjectList"}: + return ref_so_key + elif attrib == "ptAttribAnimation": + anim = bo.plasma_modifiers.animation + agmod = exporter.mgr.find_create_key(plAGModifier, so=ref_so, name=anim.display_name) + agmaster = exporter.mgr.find_create_key(plAGMasterModifier, so=ref_so, name=anim.display_name) + return agmaster + class PlasmaAttribStringNode(PlasmaAttribNodeBase, bpy.types.Node): bl_category = "PYTHON" @@ -312,3 +456,68 @@ class PlasmaAttribStringNode(PlasmaAttribNodeBase, bpy.types.Node): attrib = self.to_socket if attrib is not None: self.value = attrib.simple_value + + +class PlasmaAttribTextureNode(PlasmaAttribNodeBase, bpy.types.Node): + bl_category = "PYTHON" + bl_idname = "PlasmaAttribTextureNode" + bl_label = "Texture Attribute" + bl_width_default = 175 + + pl_attrib = ("ptAttribMaterial", "ptAttribMaterialList", + "ptAttribDynamicMap", "ptAttribMaterialAnimation") + material_name = StringProperty(name="Material") + texture_name = StringProperty(name="Texture") + + def init(self, context): + super().init(context) + # keep the code simple + self.outputs[0].link_limit = 1 + + def draw_buttons(self, context, layout): + layout.prop_search(self, "material_name", bpy.data, "materials") + material = bpy.data.materials.get(self.material_name, None) + if material is not None: + layout.prop_search(self, "texture_name", material, "texture_slots") + + def get_key(self, exporter, so): + material = bpy.data.materials.get(self.material_name, None) + if material is None: + self.raise_error("invalid Material '{}'".format(self.material_name)) + tex_slot = material.texture_slots.get(self.texture_name, None) + if tex_slot is None: + self.raise_error("invalid Texture '{}'".format(self.texture_name)) + attrib = self.attribute_type + + # Helpers + texture = tex_slot.texture + is_animated = ((material.animation_data is not None and material.animation_data.action is not None) + or (texture.animation_data is not None and texture.animation_data.action is not None)) + is_dyntext = texture.type == "IMAGE" and texture.image is None + + # Your attribute stuff here... + if attrib == "ptAttribDynamicMap": + if not is_dyntext: + self.raise_error("Texture '{}' is not a Dynamic Text Map".format(self.texture_name)) + name = "{}_{}_DynText".format(self.material_name, self.texture_name) + return exporter.mgr.find_create_key(plDynamicTextMap, name=name, so=so) + elif is_animated: + name = "{}_{}_LayerAnim".format(self.material_name, self.texture_name) + return exporter.mgr.find_create_key(plLayerAnimation, name=name, so=so) + else: + name = "{}_{}".format(self.material_name, self.texture_name) + return exporter.mgr.find_create_key(plLayer, name=name, so=so) + + +_attrib_colors = { + "ptAttribActivator": (0.031, 0.110, 0.290, 1.0), + "ptAttribActivatorList": (0.451, 0.0, 0.263, 1.0), + "ptAttribBoolean": (0.71, 0.706, 0.655, 1.0), + "ptAttribResponder": (0.031, 0.110, 0.290, 1.0), + "ptAttribResponderList": (0.031, 0.110, 0.290, 1.0), + "ptAttribString": (0.675, 0.659, 0.494, 1.0), + + PlasmaAttribNumericNode.pl_attrib: (0.443, 0.439, 0.392, 1.0), + PlasmaAttribObjectNode.pl_attrib: (0.565, 0.267, 0.0, 1.0), + PlasmaAttribTextureNode.pl_attrib: (0.035, 0.353, 0.0, 1.0), +} diff --git a/korman/operators/op_nodes.py b/korman/operators/op_nodes.py index 17a8579..689b308 100644 --- a/korman/operators/op_nodes.py +++ b/korman/operators/op_nodes.py @@ -81,7 +81,5 @@ class PlPyAttributeNodeOperator(NodeOperator, bpy.types.Operator): default = attrib.get("default", None) if default is not None and cached.is_simple_value: cached.simple_value = default - - # Manually cause the node to update its inputs node.update() return {"FINISHED"} From 8e71703e3c85517adbd1451329f999b28803b4b2 Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Tue, 14 Jul 2015 21:32:46 -0400 Subject: [PATCH 3/6] Fix linking activators to PFMs ... And finally relent on exposing the keys of region sensors. Oh boy, the fallout from this change will be fun to contain :/ --- korman/nodes/node_conditions.py | 59 ++++++++++++++++++++++++++++----- korman/nodes/node_core.py | 3 ++ korman/nodes/node_python.py | 49 ++++++++++++++------------- 3 files changed, 79 insertions(+), 32 deletions(-) diff --git a/korman/nodes/node_conditions.py b/korman/nodes/node_conditions.py index ea9af0b..ce2333c 100644 --- a/korman/nodes/node_conditions.py +++ b/korman/nodes/node_conditions.py @@ -38,6 +38,7 @@ class PlasmaClickableNode(PlasmaNodeVariableInput, bpy.types.Node): self.inputs.new("PlasmaClickableRegionSocket", "Avatar Inside Region", "region") self.inputs.new("PlasmaFacingTargetSocket", "Avatar Facing Target", "facing") self.inputs.new("PlasmaRespCommandSocket", "Local Reenable", "enable_callback") + self.outputs.new("PlasmaPythonReferenceNodeSocket", "References", "keyref") self.outputs.new("PlasmaConditionSocket", "Satisfies", "satisfies") def draw_buttons(self, context, layout): @@ -135,7 +136,6 @@ class PlasmaClickableRegionNode(PlasmaNodeBase, bpy.types.Node): default="hull") def init(self, context): - self.outputs.new("PlasmaPythonReferenceNodeSocket", "References", "keyref") self.outputs.new("PlasmaClickableRegionSocket", "Satisfies", "satisfies") def draw_buttons(self, context, layout): @@ -303,6 +303,7 @@ class PlasmaVolumeSensorNode(PlasmaNodeBase, bpy.types.Node): def init(self, context): self.inputs.new("PlasmaVolumeSettingsSocketIn", "Trigger on Enter", "enter") self.inputs.new("PlasmaVolumeSettingsSocketIn", "Trigger on Exit", "exit") + self.outputs.new("PlasmaPythonReferenceNodeSocket", "References", "keyref") self.outputs.new("PlasmaConditionSocket", "Satisfies", "satisfies") def draw_buttons(self, context, layout): @@ -312,29 +313,52 @@ class PlasmaVolumeSensorNode(PlasmaNodeBase, bpy.types.Node): layout.prop_search(self, "region", bpy.data, "objects", icon="MESH_DATA") layout.prop(self, "bounds") - def export(self, exporter, bo, so): - interface = exporter.mgr.add_object(plInterfaceInfoModifier, name=self.key_name, so=so) + def get_key(self, exporter, parent_so): + bo = self.region_object + so = exporter.find_create_object(plSceneObject, bl=bo) + rgn_enter, rgn_exit = None, None + + if self.report_enters: + theName = "{}_{}_Enter".format(self.id_data.name, self.name) + rgn_enter = exporter.mgr.find_create_key(plLogicModifier, name=theName, so=so) + if self.report_exits: + theName = "{}_{}_Exit".format(self.id_data.name, self.name) + rgn_exit = exporter.mgr.find_create_key(plLogicModifier, name=theName, so=so) + + if rgn_enter is None: + return rgn_exit + elif rgn_exit is None: + return rgn_enter + else: + # !!! ... !!! + # Sorry + # -- Hoikas + # !!! ... !!! + return (rgn_enter, rgn_exit) + + def export(self, exporter, bo, parent_so): + # We need to ensure we export to the correct SO + region_bo = self.region_object + region_so = exporter.mgr.find_create_object(plSceneObject, bl=region_bo) + interface = exporter.mgr.find_create_object(plInterfaceInfoModifier, name=self.key_name, so=region_so) # Region Enters enter_simple = self.find_input_socket("enter").allow enter_settings = self.find_input("enter", "PlasmaVolumeReportNode") if enter_simple or enter_settings is not None: - key = self._export_volume_event(exporter, bo, so, plVolumeSensorConditionalObject.kTypeEnter, enter_settings) + key = self._export_volume_event(exporter, region_bo, region_so, plVolumeSensorConditionalObject.kTypeEnter, enter_settings) interface.addIntfKey(key) # Region Exits exit_simple = self.find_input_socket("exit").allow exit_settings = self.find_input("exit", "PlasmaVolumeReportNode") if exit_simple or exit_settings is not None: - key = self._export_volume_event(exporter, bo, so, plVolumeSensorConditionalObject.kTypeExit, exit_settings) + key = self._export_volume_event(exporter, region_bo, region_so, plVolumeSensorConditionalObject.kTypeExit, exit_settings) interface.addIntfKey(key) # Don't forget to export the physical object itself! # [trollface.jpg] - phys_bo = bpy.data.objects.get(self.region, None) - if phys_bo is None: - self.raise_error("invalid Region object: '{}'".format(self.region)) - simIface, physical = exporter.physics.generate_physical(phys_bo, so, self.bounds, "{}_VolumeSensor".format(bo.name)) + simIface, physical = exporter.physics.generate_physical(region_bo, region_so, self.bounds, "{}_VolumeSensor".format(region_bo.name)) physical.memberGroup = plSimDefs.kGroupDetector if "avatar" in self.report_on: @@ -379,6 +403,23 @@ class PlasmaVolumeSensorNode(PlasmaNodeBase, bpy.types.Node): logicmod.addCondition(volKey) return logicKey + @property + def region_object(self): + phys_bo = bpy.data.objects.get(self.region, None) + if phys_bo is None: + self.raise_error("invalid Region object: '{}'".format(self.region)) + return phys_bo + + @property + def report_enters(self): + return (self.find_input_socket("enter").allow or + self.find_input("enter", "PlasmaVolumeReportNode") is not None) + + @property + def report_exits(self): + return (self.find_input_socket("exit").allow or + self.find_input("exit", "PlasmaVolumeReportNode") is not None) + class PlasmaVolumeSettingsSocket(PlasmaNodeSocketBase): bl_color = (43.1, 24.7, 0.0, 1.0) diff --git a/korman/nodes/node_core.py b/korman/nodes/node_core.py index 127f5d6..cdc30d5 100644 --- a/korman/nodes/node_core.py +++ b/korman/nodes/node_core.py @@ -27,6 +27,9 @@ class PlasmaNodeBase: key = i.get_key(exporter, so) if key is None: exporter.report.warn(" '{}' Node '{}' doesn't expose a key. It won't be triggered by '{}'!".format(i.bl_idname, i.name, self.name), indent=3) + elif isinstance(key, tuple): + for i in key: + notify.addReceiver(key) else: notify.addReceiver(key) return notify diff --git a/korman/nodes/node_python.py b/korman/nodes/node_python.py index 9918604..9845b68 100644 --- a/korman/nodes/node_python.py +++ b/korman/nodes/node_python.py @@ -171,29 +171,32 @@ class PlasmaPythonFileNode(PlasmaNodeBase, bpy.types.Node): attrib = socket.attribute_type from_node = socket.links[0].from_node - param = plPythonParameter() - param.id = socket.attribute_id - param.valueType = _attrib2param[attrib] - if socket.is_simple_value: - param.value = from_node.value - else: - key = from_node.get_key(exporter, so) - if key is None: - msg = "'{}' Node '{}' didn't return a key and therefore will be unavailable to Python".format( - self.id_data.name, from_node.name) - exporter.report.warn(msg, indent=3) - else: - key_type = _attrib_key_types[attrib] - if isinstance(key_type, tuple): - good_key = key.type in key_type - else: - good_key = key.type == key.type - if not good_key: - msg = "'{}' Node '{}' returned an unexpected key type '{}'".format( - self.id_data.name, from_node.name, plFactory.ClassName(key.type)) + value = from_node.value if socket.is_simple_value else from_node.get_key(exporter, so) + if not isinstance(value, tuple): + value = (value,) + for i in value: + param = plPythonParameter() + param.id = socket.attribute_id + param.valueType = _attrib2param[attrib] + param.value = i + + # Key type sanity checking... Because I trust no user. + if not socket.is_simple_value: + if i is None: + msg = "'{}' Node '{}' didn't return a key and therefore will be unavailable to Python".format( + self.id_data.name, from_node.name) exporter.report.warn(msg, indent=3) - param.value = key - pfm.addParameter(param) + else: + key_type = _attrib_key_types[attrib] + if isinstance(key_type, tuple): + good_key = i.type in key_type + else: + good_key = i.type == key_type + if not good_key: + msg = "'{}' Node '{}' returned an unexpected key type '{}'".format( + self.id_data.name, from_node.name, plFactory.ClassName(i.type)) + exporter.report.warn(msg, indent=3) + pfm.addParameter(param) def _get_attrib_sockets(self, idx): for i in self.inputs: @@ -424,7 +427,7 @@ class PlasmaAttribObjectNode(PlasmaAttribNodeBase, bpy.types.Node): self.raise_error("must be connected to a Python File node!") attrib = attrib.attribute_type - bo = bpy.objects.data.get(self.object_name, None) + bo = bpy.data.objects.get(self.object_name, None) if bo is None: self.raise_error("invalid object specified: '{}'".format(self.object_name)) ref_so_key = exporter.mgr.find_create_key(plSceneObject, bl=bo) From da254af886be85817ea902cdc8da369fa556dddd Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Tue, 14 Jul 2015 21:53:38 -0400 Subject: [PATCH 4/6] More improper linkage protection --- korman/nodes/node_conditions.py | 6 ++++++ korman/nodes/node_python.py | 16 +++++++++++++++- korman/nodes/node_responder.py | 3 +++ 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/korman/nodes/node_conditions.py b/korman/nodes/node_conditions.py index ce2333c..a632e51 100644 --- a/korman/nodes/node_conditions.py +++ b/korman/nodes/node_conditions.py @@ -27,6 +27,9 @@ class PlasmaClickableNode(PlasmaNodeVariableInput, bpy.types.Node): bl_label = "Clickable" bl_width_default = 160 + # These are the Python attributes we can fill in + pl_attrib = {"ptAttribActivator", "ptAttribActivatorList", "ptAttribNamedActivator"} + clickable = StringProperty(name="Clickable", description="Mesh that is clickable") bounds = EnumProperty(name="Bounds", @@ -285,6 +288,9 @@ class PlasmaVolumeSensorNode(PlasmaNodeBase, bpy.types.Node): bl_label = "Region Sensor" bl_width_default = 190 + # These are the Python attributes we can fill in + pl_attrib = {"ptAttribActivator", "ptAttribActivatorList", "ptAttribNamedActivator"} + # Region Mesh region = StringProperty(name="Region", description="Object that defines the region mesh") diff --git a/korman/nodes/node_python.py b/korman/nodes/node_python.py index 9845b68..9ffd3d9 100644 --- a/korman/nodes/node_python.py +++ b/korman/nodes/node_python.py @@ -220,6 +220,20 @@ class PlasmaPythonFileNode(PlasmaNodeBase, bpy.types.Node): if self.no_update: return with self._NoUpdate(self) as _no_recurse: + # First, we really want to make sure our junk matches up. Yes, this does dupe what + # happens in PlasmaAttribNodeBase, but we can link much more than those node types... + toasty_sockets = [] + input_nodes = (i for i in self.inputs if i.is_linked and i.links) + for i in input_nodes: + link = i.links[0] + allowed_attribs = getattr(link.from_node, "pl_attrib", set()) + if i.attribute_type not in allowed_attribs: + self.id_data.links.remove(link) + # Bad news, old chap... Even though we're doing this before we figure out + # how many socket we need, the changes won't be committed to the socket's links + # until later. damn. We'll have to track it manually + toasty_sockets.append(i) + attribs = self.attribute_map empty = not self.inputs for idx in sorted(attribs): @@ -235,7 +249,7 @@ class PlasmaPythonFileNode(PlasmaNodeBase, bpy.types.Node): if not inputs: self._make_attrib_socket(attrib, empty) elif attrib.attribute_type not in _single_user_attribs: - unconnected = [socket for socket in inputs if not socket.is_linked] + unconnected = [socket for socket in inputs if not socket.is_linked or socket in toasty_sockets] if not unconnected: self._make_attrib_socket(attrib, empty) while len(unconnected) > 1: diff --git a/korman/nodes/node_responder.py b/korman/nodes/node_responder.py index 9ad6ab0..8fdcb33 100644 --- a/korman/nodes/node_responder.py +++ b/korman/nodes/node_responder.py @@ -26,6 +26,9 @@ class PlasmaResponderNode(PlasmaNodeVariableInput, bpy.types.Node): bl_label = "Responder" bl_width_default = 145 + # These are the Python attributes we can fill in + pl_attrib = {"ptAttribResponder", "ptAttribResponderList", "ptAttribNamedResponder"} + detect_trigger = BoolProperty(name="Detect Trigger", description="When notified, trigger the Responder", default=True) From 6dcab85c1dc261d308a9e2c5685e4b700003cc9e Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Tue, 14 Jul 2015 23:02:12 -0400 Subject: [PATCH 5/6] Allow Advanced Logic to reference specific Nodes This allows us to ensure that a given NodeTree is only ever exported once. A node reference acts as a way to attach a plMultiModifier to a plSceneObject without using those terms. Things will work just fine if the node reference isn't used, so as long as the entire tree is only used once. Future work: ensuring the whole logic tree is only ever exported once. --- korman/exporter/convert.py | 15 ++++++++++++++- korman/properties/modifiers/logic.py | 21 +++++++++++++++++++-- korman/ui/modifiers/logic.py | 12 +++++++++--- 3 files changed, 42 insertions(+), 6 deletions(-) diff --git a/korman/exporter/convert.py b/korman/exporter/convert.py index 6f43726..6795be9 100644 --- a/korman/exporter/convert.py +++ b/korman/exporter/convert.py @@ -33,6 +33,8 @@ class Exporter: self._op = op # Blender export operator self._objects = [] self.actors = set() + self.node_trees_exported = set() + self.want_node_trees = {} @property def age_name(self): @@ -66,7 +68,10 @@ class Exporter: # Step 3: Export all the things! self._export_scene_objects() - # Step 3.1: Now that all Plasma Objects (save Mipmaps) are exported, we do any post + # Step 3.1: Ensure referenced logic node trees are exported + self._export_referenced_node_trees() + + # Step 3.2: Now that all Plasma Objects (save Mipmaps) are exported, we do any post # processing that needs to inspect those objects self._post_process_scene_objects() @@ -212,6 +217,14 @@ class Exporter: else: print(" No material(s) on the ObData, so no drawables") + def _export_referenced_node_trees(self): + print("\nChecking Logic Trees...") + need_to_export = ((name, bo, so) for name, (bo, so) in self.want_node_trees.items() + if name not in self.node_trees_exported) + for tree, bo, so in need_to_export: + print(" NodeTree '{}'".format(tree)) + bpy.data.node_groups[tree].export(self, bo, so) + def _harvest_actors(self): for bl_obj in self._objects: for mod in bl_obj.plasma_modifiers.modifiers: diff --git a/korman/properties/modifiers/logic.py b/korman/properties/modifiers/logic.py index 4d1e1a0..fcba3c7 100644 --- a/korman/properties/modifiers/logic.py +++ b/korman/properties/modifiers/logic.py @@ -33,13 +33,16 @@ class PlasmaVersionedNodeTree(bpy.types.PropertyGroup): default=set(list(zip(*game_versions))[0])) node_tree_name = StringProperty(name="Node Tree", description="Node Tree to export") + node_name = StringProperty(name="Node Ref", + description="Attach a reference to this node") @property def node_tree(self): try: return bpy.data.node_groups[self.node_tree_name] except KeyError: - raise ExportError("Node Tree {} does not exist!".format(self.node_tree_name)) + raise ExportError("Node Tree '{}' does not exist!".format(self.node_tree_name)) + class PlasmaAdvancedLogic(PlasmaModifierProperties): pl_id = "advanced_logic" @@ -60,7 +63,20 @@ class PlasmaAdvancedLogic(PlasmaModifierProperties): for i in self.logic_groups: our_versions = [globals()[j] for j in i.version] if version in our_versions: - i.node_tree.export(exporter, bo, so) + # If node_name is defined, then we're only adding a reference. We will make sure that + # the entire node tree is exported once before the post_export step, however. + if i.node_name: + exporter.want_node_trees[i.node_tree_name] = (bo, so) + node = i.node_tree.nodes.get(i.node_name, None) + if node is None: + raise ExportError("Node '{}' does not exist in '{}'".format(i.node_name, i.node_tree_name)) + # We are going to assume get_key will do the adding correctly. Single modifiers + # should fetch the appropriate SceneObject before doing anything, so this will + # be a no-op in that case. Multi modifiers should accept any SceneObject, however + node.get_key(exporter, so) + else: + exporter.node_trees_exported.add(i.node_tree_name) + i.node_tree.export(exporter, bo, so) def harvest_actors(self): actors = set() @@ -88,6 +104,7 @@ class PlasmaSpawnPoint(PlasmaModifierProperties): def requires_actor(self): return True + class PlasmaMaintainersMarker(PlasmaModifierProperties): pl_id = "maintainersmarker" diff --git a/korman/ui/modifiers/logic.py b/korman/ui/modifiers/logic.py index 70f53f9..9dc27f1 100644 --- a/korman/ui/modifiers/logic.py +++ b/korman/ui/modifiers/logic.py @@ -19,6 +19,7 @@ class LogicListUI(bpy.types.UIList): def draw_item(self, context, layout, data, item, icon, active_data, active_property, index=0, flt_flag=0): layout.prop(item, "name", emboss=False, text="", icon="NODETREE") + def advanced_logic(modifier, layout, context): row = layout.row() row.template_list("LogicListUI", "logic_groups", modifier, "logic_groups", modifier, "active_group_index", @@ -37,9 +38,14 @@ def advanced_logic(modifier, layout, context): # Modify the loop points if modifier.logic_groups: logic = modifier.logic_groups[modifier.active_group_index] - row = layout.row() - row.prop_menu_enum(logic, "version") - row.prop_search(logic, "node_tree_name", bpy.data, "node_groups", icon="NODETREE", text="") + layout.row().prop_menu_enum(logic, "version") + layout.prop_search(logic, "node_tree_name", bpy.data, "node_groups", icon="NODETREE") + try: + layout.prop_search(logic, "node_name", logic.node_tree, "nodes", icon="NODE") + except: + row = layout.row() + row.enabled = False + row.prop(logic, "node_name", icon="NODE") def spawnpoint(modifier, layout, context): layout.label(text="Avatar faces negative Y.") From 345ac83c18475cef378e212bcaf8f8de9e27c874 Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Tue, 14 Jul 2015 23:37:27 -0400 Subject: [PATCH 6/6] Only allow python refreshing if the file exists --- korman/nodes/node_python.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/korman/nodes/node_python.py b/korman/nodes/node_python.py index 9ffd3d9..a46ade6 100644 --- a/korman/nodes/node_python.py +++ b/korman/nodes/node_python.py @@ -149,9 +149,10 @@ class PlasmaPythonFileNode(PlasmaNodeBase, bpy.types.Node): row = layout.row(align=True) if self.filename: row.prop(self, "filename") - operator = row.operator("node.plasma_attributes_to_node", icon="FILE_REFRESH", text="") - operator.python_path = self.filepath - operator.node_path = self.node_path + if os.path.isfile(self.filepath): + operator = row.operator("node.plasma_attributes_to_node", icon="FILE_REFRESH", text="") + operator.python_path = self.filepath + operator.node_path = self.node_path op_text = "" if self.filename else "Select" operator = row.operator("file.plasma_file_picker", icon="SCRIPT", text=op_text)