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"}