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/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..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", @@ -38,6 +41,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): @@ -284,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") @@ -302,6 +309,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): @@ -311,29 +319,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: @@ -378,6 +409,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 358cd75..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 @@ -123,6 +126,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..a46ade6 --- /dev/null +++ b/korman/nodes/node_python.py @@ -0,0 +1,541 @@ +# 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 * + +_single_user_attribs = { + "ptAttribBoolean", "ptAttribInt", "ptAttribFloat", "ptAttribString", "ptAttribDropDownList", + "ptAttribSceneobject", "ptAttribDynamicMap", "ptAttribGUIDialog", "ptAttribExcludeRegion", + "ptAttribWaveSet", "ptAttribSwimCurrent", "ptAttribAnimation", "ptAttribBehavior", + "ptAttribMaterial", "ptAttribMaterialAnimation", "ptAttribGUIPopUpMenu", "ptAttribGUISkin", + "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() + 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 + + 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 + + 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", + description="Python Filename") + filepath = StringProperty(update=_update_pyfile, + options={"HIDDEN"}) + + attributes = CollectionProperty(type=PlasmaAttribute, options={"HIDDEN"}) + no_update = BoolProperty(default=False, options={"HIDDEN", "SKIP_SAVE"}) + + @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") + 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) + operator.filter_glob = "*.py" + operator.data_path = self.node_path + 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 + + 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) + 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: + 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", 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): + 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): + 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) + elif attrib.attribute_type not in _single_user_attribs: + 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: + 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 + + @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] + 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 + + @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.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) + 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" + 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 + + +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/nodes/node_responder.py b/korman/nodes/node_responder.py index 343b8da..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) @@ -38,6 +41,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..689b308 --- /dev/null +++ b/korman/operators/op_nodes.py @@ -0,0 +1,85 @@ +# 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 + node.update() + return {"FINISHED"} 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.")