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.")