From 4ebd70d8245e837fde317f5a3538c23dd5a8749a Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Tue, 14 Jul 2015 13:07:16 -0400 Subject: [PATCH] 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"}