diff --git a/korman/__init__.py b/korman/__init__.py
index bc52614..cd51a9c 100644
--- a/korman/__init__.py
+++ b/korman/__init__.py
@@ -16,6 +16,7 @@
import bpy
from . import exporter, render
from . import properties, ui
+from . import nodes
from . import operators
bl_info = {
@@ -37,6 +38,7 @@ def register():
bpy.utils.register_module(__name__)
# Sigh... Blender isn't totally automated.
+ nodes.register()
operators.register()
properties.register()
@@ -44,6 +46,7 @@ def register():
def unregister():
"""Unregisters all Blender operators and GUI items"""
bpy.utils.unregister_module(__name__)
+ nodes.unregister()
operators.unregister()
diff --git a/korman/exporter/convert.py b/korman/exporter/convert.py
index fc24434..4d3dfa6 100644
--- a/korman/exporter/convert.py
+++ b/korman/exporter/convert.py
@@ -111,7 +111,7 @@ class Exporter:
# Instead of exporting a skeleton now, we'll just make an orphaned CI.
# The bl_obj export will make this work.
- parent_ci = self.mgr.find_create_key(parent, plCoordinateInterface).object
+ parent_ci = self.mgr.find_create_key(plCoordinateInterface, bl=bo, so=so).object
parent_ci.addChild(so.key)
else:
self.report.warn("You have parented Plasma Object '{}' to '{}', which has not been marked for export. \
@@ -122,7 +122,7 @@ class Exporter:
"""Ensures that the SceneObject has a CoordinateInterface"""
if not so.coord:
print(" Exporting CoordinateInterface")
- ci = self.mgr.find_create_key(bo, plCoordinateInterface).object
+ ci = self.mgr.find_create_key(plCoordinateInterface, bl=bo, so=so).object
# Now we have the "fun" work of filling in the CI
ci.localToWorld = utils.matrix44(bo.matrix_basis)
@@ -148,7 +148,7 @@ class Exporter:
# Create a sceneobject if one does not exist.
# Before we call the export_fn, we need to determine if this object is an actor of any
# sort, and barf out a CI.
- sceneobject = self.mgr.find_create_key(bl_obj, plSceneObject).object
+ sceneobject = self.mgr.find_create_key(plSceneObject, bl=bl_obj).object
self._export_actor(sceneobject, bl_obj)
export_fn(sceneobject, bl_obj)
diff --git a/korman/exporter/manager.py b/korman/exporter/manager.py
index 7706ea7..fa7e10d 100644
--- a/korman/exporter/manager.py
+++ b/korman/exporter/manager.py
@@ -91,7 +91,7 @@ class ExportManager:
if isinstance(pl, plObjInterface):
if so is None:
- key = self.find_key(bl, plSceneObject)
+ key = self.find_key(plSceneObject, bl)
# prevent race conditions
if key is None:
so = self.add_object(plSceneObject, name=name, loc=location)
@@ -158,22 +158,26 @@ class ExportManager:
self._nodes[location] = None
return location
- def find_create_key(self, bl_obj, pClass, so=None):
- key = self.find_key(bl_obj, pClass, so=so)
+ def find_create_key(self, pClass, bl=None, name=None, so=None):
+ key = self.find_key(pClass, bl, name, so)
if key is None:
- key = self.add_object(pl=pClass, bl=bl_obj, so=so).key
+ key = self.add_object(pl=pClass, name=name, bl=bl, so=so).key
return key
- def find_key(self, bl_obj, pClass, so=None):
+ def find_key(self, pClass, bl=None, name=None, so=None):
"""Given a blender Object and a Plasma class, find (or create) an exported plKey"""
+ assert (bl or name) and (bl or so)
+
if so is None:
- location = self._pages[bl_obj.plasma_object.page]
+ location = self._pages[bl.plasma_object.page]
else:
location = so.key.location
+ if name is None:
+ name = bl.name
index = plFactory.ClassIndex(pClass.__name__)
for key in self.mgr.getKeys(location, index):
- if bl_obj.name == key.name:
+ if name == key.name:
return key
return None
diff --git a/korman/exporter/rtlight.py b/korman/exporter/rtlight.py
index 0fc8bbf..7cf29df 100644
--- a/korman/exporter/rtlight.py
+++ b/korman/exporter/rtlight.py
@@ -92,7 +92,7 @@ class LightConverter:
def _create_light_key(self, bo, bl_light, so):
try:
xlate = _BL2PL[bl_light.type]
- return self.mgr.find_create_key(bo, xlate, so=so)
+ return self.mgr.find_create_key(xlate, bl=bo, so=so)
except LookupError:
raise BlenderOptionNotSupported("Object ('{}') lamp type '{}'".format(bo.name, bl_light.type))
diff --git a/korman/nodes/__init__.py b/korman/nodes/__init__.py
new file mode 100644
index 0000000..d34b022
--- /dev/null
+++ b/korman/nodes/__init__.py
@@ -0,0 +1,67 @@
+# 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
+import inspect
+from nodeitems_utils import NodeCategory, NodeItem
+import nodeitems_utils
+
+# Put all Korman node modules here...
+from .node_conditions import *
+from .node_core import *
+from .node_messages import *
+from .node_responder import *
+
+class PlasmaNodeCategory(NodeCategory):
+ """Plasma Node Category"""
+
+ @classmethod
+ def poll(cls, context):
+ return (context.space_data.tree_type == "PlasmaNodeTree")
+
+# Here's what you need to know about this...
+# If you add a new category, put the pretty name here!
+# If you're making a new Node, ensure that your bl_idname attribute is present AND matches
+# the class name. Otherwise, absolutely fascinating things will happen. Don't expect for me
+# to come and rescue you from it, either.
+_kategory_names = {
+ "CONDITIONS": "Conditions",
+ "LOGIC": "Logic",
+ "MSG": "Message",
+}
+
+# Now, generate the categories as best we can...
+_kategories = {}
+for cls in dict(globals()).values():
+ if inspect.isclass(cls):
+ if not issubclass(cls, PlasmaNodeBase) or not issubclass(cls, bpy.types.Node):
+ continue
+ else:
+ continue
+ try:
+ _kategories[cls.bl_category].append(cls)
+ except LookupError:
+ _kategories[cls.bl_category] = [cls,]
+_actual_kategories = []
+for i in sorted(_kategories.keys(), key=lambda x: _kategory_names[x]):
+ # Note that even though we're sorting the category names, Blender appears to not care...
+ _kat_items = [NodeItem(j.bl_idname) for j in sorted(_kategories[i], key=lambda x: x.bl_label)]
+ _actual_kategories.append(PlasmaNodeCategory(i, _kategory_names[i], items=_kat_items))
+
+def register():
+ nodeitems_utils.register_node_categories("PLASMA_NODES", _actual_kategories)
+
+def unregister():
+ nodeitems_utils.unregister_node_categories("PLASMA_NODES")
diff --git a/korman/nodes/node_conditions.py b/korman/nodes/node_conditions.py
new file mode 100644
index 0000000..6aad214
--- /dev/null
+++ b/korman/nodes/node_conditions.py
@@ -0,0 +1,178 @@
+# 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 *
+from PyHSPlasma import *
+
+from .node_core import PlasmaNodeBase, PlasmaNodeSocketBase
+from ..properties.modifiers.physics import bounds_types
+
+class PlasmaConditionSocket(PlasmaNodeSocketBase, bpy.types.NodeSocket):
+ bl_color = (0.188, 0.086, 0.349, 1.0)
+
+
+class PlasmaVolumeReportNode(PlasmaNodeBase, bpy.types.Node):
+ bl_category = "CONDITIONS"
+ bl_idname = "PlasmaVoumeReportNode"
+ bl_label = "Region Trigger Settings"
+
+ report_when = EnumProperty(name="When",
+ description="When the region should trigger",
+ items=[("each", "Each Event", "The region will trigger on every enter/exit"),
+ ("first", "First Event", "The region will trigger on the first event only"),
+ ("count", "Population", "When the region has a certain number of objects inside it")])
+ threshold = IntProperty(name="Threshold",
+ description="How many objects should be in the region for it to trigger",
+ min=1)
+
+ def init(self, context):
+ self.outputs.new("PlasmaVolumeSettingsSocketOut", "Trigger Settings")
+
+ def draw_buttons(self, context, layout):
+ layout.prop(self, "report_when")
+ if self.report_when == "count":
+ row = layout.row()
+ row.label("Threshold: ")
+ row.prop(self, "threshold", text="")
+
+
+class PlasmaVolumeSensorNode(PlasmaNodeBase, bpy.types.Node):
+ bl_category = "CONDITIONS"
+ bl_idname = "PlasmaVolumeSensorNode"
+ bl_label = "Region Sensor"
+ bl_width_default = 190
+
+ # Region Mesh
+ region = StringProperty(name="Region",
+ description="Object that defines the region mesh")
+ bounds = EnumProperty(name="Bounds",
+ description="Physical object's bounds",
+ items=bounds_types)
+
+ # Detector Properties
+ report_on = EnumProperty(name="Triggerers",
+ description="What triggers this region?",
+ options={"ANIMATABLE", "ENUM_FLAG"},
+ items=[("avatar", "Avatars", "Avatars trigger this region"),
+ ("dynamics", "Dynamics", "Any non-avatar dynamic physical object (eg kickables)")],
+ default={"avatar"})
+
+ def init(self, context):
+ self.inputs.new("PlasmaVolumeSettingsSocketIn", "Trigger on Enter", "enter")
+ self.inputs.new("PlasmaVolumeSettingsSocketIn", "Trigger on Exit", "exit")
+ self.outputs.new("PlasmaConditionSocket", "Satisfies", "satisfies")
+
+ def draw_buttons(self, context, layout):
+ layout.prop(self, "report_on")
+
+ # Okay, if they changed the name of the ObData, that's THEIR problem...
+ layout.prop_search(self, "region", bpy.data, "objects", icon="MESH_DATA")
+ layout.prop(self, "bounds")
+
+ def export(self, exporter, tree, bo, so):
+ interface = exporter.mgr.add_object(plInterfaceInfoModifier, name=self.create_key_name(tree), so=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, tree, bo, 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, tree, bo, so, plVolumeSensorConditionalObject.kTypeExit, exit_settings)
+ interface.addIntfKey(key)
+
+ # Don't forget to export the physical object itself!
+ # [trollface.jpg]
+ simIface, physical = exporter.physics.generate_physical(bo, so, "{}_VolumeSensor".format(bo.name))
+ phys_bo = bpy.data.objects[self.region]
+ exporter.physics.export(phys_bo, physical, self.bounds)
+
+ physical.memberGroup = plSimDefs.kGroupDetector
+ if "avatar" in self.report_on:
+ physical.reportGroup |= 1 << plSimDefs.kGroupAvatar
+ if "dynamics" in self.report_on:
+ physical.reportGroup |= 1 << plSimDefs.kGroupDynamic
+
+ def _export_volume_event(self, exporter, tree, bo, so, event, settings):
+ if event == plVolumeSensorConditionalObject.kTypeEnter:
+ suffix = "Enter"
+ else:
+ suffix = "Exit"
+
+ theName = "{}_{}_{}".format(tree.name, self.name, suffix)
+ print(" [LogicModifier '{}']".format(theName))
+ logicKey = exporter.mgr.find_create_key(plLogicModifier, name=theName, so=so)
+ logicmod = logicKey.object
+ logicmod.setLogicFlag(plLogicModifier.kMultiTrigger, True)
+
+ # LogicMod notification... This is one of the cases where the linked node needs to match
+ # exactly one key...
+ notify = plNotifyMsg()
+ notify.BCastFlags = (plMessage.kNetPropagate | plMessage.kLocalPropagate)
+ for i in self.find_outputs("satisfies"):
+ key = i.get_key(exporter, tree, so)
+ if key is None:
+ print(" WARNING: '{}' Node '{}' doesn't expose a key. It won't be triggered!".format(i.bl_idname, i.name))
+ else:
+ notify.addReceiver(key)
+ logicmod.notify = notify
+
+ # Now, the detector objects
+ print(" [ObjectInVolumeDetector '{}']".format(theName))
+ detKey = exporter.mgr.find_create_key(plObjectInVolumeDetector, name=theName, so=so)
+ det = detKey.object
+
+ print(" [VolumeSensorConditionalObject '{}']".format(theName))
+ volKey = exporter.mgr.find_create_key(plVolumeSensorConditionalObject, name=theName, so=so)
+ volsens = volKey.object
+
+ volsens.type = event
+ if settings is not None:
+ if settings.report_when == "first":
+ volsens.first = True
+ elif settings.report_when == "threshold":
+ volsens.trigNum = settings.threshold
+
+ # There appears to be a mandatory order for these keys...
+ det.addReceiver(volKey)
+ det.addReceiver(logicKey)
+
+ # End mandatory order
+ logicmod.addCondition(volKey)
+ return logicKey
+
+
+
+class PlasmaVolumeSettingsSocket(PlasmaNodeSocketBase):
+ bl_color = (43.1, 24.7, 0.0, 1.0)
+
+
+class PlasmaVolumeSettingsSocketIn(PlasmaVolumeSettingsSocket, bpy.types.NodeSocket):
+ allow = BoolProperty()
+
+ def draw(self, context, layout, node, text):
+ if not self.is_linked:
+ layout.prop(self, "allow", text="")
+ layout.label(text)
+
+
+class PlasmaVolumeSettingsSocketOut(PlasmaVolumeSettingsSocket, bpy.types.NodeSocket):
+ pass
diff --git a/korman/nodes/node_core.py b/korman/nodes/node_core.py
new file mode 100644
index 0000000..f214310
--- /dev/null
+++ b/korman/nodes/node_core.py
@@ -0,0 +1,139 @@
+# 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 abc
+import bpy
+
+class PlasmaNodeBase:
+ def create_key_name(self, tree):
+ return "{}_{}".format(tree.name, self.name)
+
+ def get_key(self, exporter, tree, so):
+ return None
+
+ def export(self, exporter, tree, bo, so):
+ pass
+
+ def find_input(self, key, idname=None):
+ for i in self.inputs:
+ if i.identifier == key:
+ if i.links:
+ node = i.links[0].from_node
+ if idname is not None and idname != node.bl_idname:
+ return None
+ return node
+ else:
+ return None
+ raise KeyError(key)
+
+ def find_input_socket(self, key):
+ for i in self.inputs:
+ if i.identifier == key:
+ return i
+ raise KeyError(key)
+
+ def find_output(self, key, idname=None):
+ for i in self.outputs:
+ if i.identifier == key:
+ if i.links:
+ node = i.links[0].to_node
+ if idname is not None and idname != node.bl_idname:
+ return None
+ return node
+ else:
+ return None
+ raise KeyError(key)
+
+ def find_outputs(self, key, idname=None):
+ for i in self.outputs:
+ if i.identifier == key:
+ for j in i.links:
+ node = j.to_node
+ if idname is not None and idname != node.bl_idname:
+ continue
+ yield node
+
+ def find_output_socket(self, key):
+ for i in self.outputs:
+ if i.identifier == key:
+ return i
+ raise KeyError(key)
+
+ def link_input(self, tree, node, out_key, in_key):
+ """Links a given Node's output socket to a given input socket on this Node"""
+ if isinstance(in_key, str):
+ in_socket = self.find_input_socket(in_key)
+ else:
+ in_socket = in_key
+ if isinstance(out_key, str):
+ out_socket = node.find_output_socket(out_key)
+ else:
+ out_socket = out_key
+ link = tree.links.new(in_socket, out_socket)
+
+ def link_output(self, tree, node, out_key, in_key):
+ """Links a given Node's input socket to a given output socket on this Node"""
+ if isinstance(in_key, str):
+ in_socket = node.find_input_socket(in_key)
+ else:
+ in_socket = in_key
+ if isinstance(out_key, str):
+ out_socket = self.find_output_socket(out_key)
+ else:
+ out_socket = out_key
+ link = tree.links.new(in_socket, out_socket)
+
+ @classmethod
+ def poll(cls, context):
+ return (context.bl_idname == "PlasmaNodeTree")
+
+
+class PlasmaNodeVariableInput(PlasmaNodeBase):
+ def ensure_sockets(self, idname, name, identifier=None):
+ """Ensures there is one (and only one) empty input socket"""
+ empty = [i for i in self.inputs if i.bl_idname == idname and not i.links]
+ if not empty:
+ if identifier is None:
+ self.inputs.new(idname, name)
+ else:
+ self.inputs.new(idname, name, identifier)
+ while len(empty) > 1:
+ self.inputs.remove(empty.pop())
+
+
+class PlasmaNodeSocketBase:
+ def draw(self, context, layout, node, text):
+ layout.label(text)
+
+ def draw_color(self, context, node):
+ # It's so tempting to just do RGB sometimes... Let's be nice.
+ if len(self.bl_color) == 3:
+ return tuple(self.bl_color[0], self.bl_color[1], self.bl_color[2], 1.0)
+ return self.bl_color
+
+
+class PlasmaNodeTree(bpy.types.NodeTree):
+ bl_idname = "PlasmaNodeTree"
+ bl_label = "Plasma"
+ bl_icon = "NODETREE"
+
+ def export(self, exporter, bo, so):
+ # just pass it off to each node
+ for node in self.nodes:
+ node.export(exporter, self, bo, so)
+
+ @classmethod
+ def poll(cls, context):
+ return (context.scene.render.engine == "PLASMA_GAME")
diff --git a/korman/nodes/node_messages.py b/korman/nodes/node_messages.py
new file mode 100644
index 0000000..ccb8080
--- /dev/null
+++ b/korman/nodes/node_messages.py
@@ -0,0 +1,46 @@
+# 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 *
+from PyHSPlasma import *
+
+from .node_core import *
+from ..properties.modifiers.region import footstep_surfaces, footstep_surface_ids
+
+class PlasmaMessageSocket(PlasmaNodeSocketBase, bpy.types.NodeSocket):
+ bl_color = (0.004, 0.282, 0.349, 1.0)
+
+
+class PlasmaFootstepSoundMsgNode(PlasmaNodeBase, bpy.types.Node):
+ bl_category = "MSG"
+ bl_idname = "PlasmaFootstepSoundMsgNode"
+ bl_label = "Footstep Sound"
+
+ surface = EnumProperty(name="Surface",
+ description="What kind of surface are we walking on?",
+ items=footstep_surfaces,
+ default="stone")
+
+ def init(self, context):
+ self.inputs.new("PlasmaMessageSocket", "Sender", "sender")
+
+ def draw_buttons(self, context, layout):
+ layout.prop(self, "surface")
+
+ def convert_message(self, exporter):
+ msg = plArmatureEffectStateMsg()
+ msg.surface = footstep_surface_ids[self.surface]
+ return msg
diff --git a/korman/nodes/node_responder.py b/korman/nodes/node_responder.py
new file mode 100644
index 0000000..9f6e24b
--- /dev/null
+++ b/korman/nodes/node_responder.py
@@ -0,0 +1,234 @@
+# 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 *
+from PyHSPlasma import *
+import uuid
+
+from .node_core import *
+
+class PlasmaResponderNode(PlasmaNodeVariableInput, bpy.types.Node):
+ bl_category = "LOGIC"
+ bl_idname = "PlasmaResponderNode"
+ bl_label = "Responder"
+ bl_width_default = 145
+
+ detect_trigger = BoolProperty(name="Detect Trigger",
+ description="When notified, trigger the Responder",
+ default=True)
+ detect_untrigger = BoolProperty(name="Detect UnTrigger",
+ description="When notified, untrigger the Responder",
+ default=False)
+ no_ff_sounds = BoolProperty(name="Don't F-Fwd Sounds",
+ description="When fast-forwarding, play sound effects",
+ default=False)
+
+ def init(self, context):
+ self.inputs.new("PlasmaConditionSocket", "Condition", "condition")
+ self.outputs.new("PlasmaRespStateSocket", "States", "states")
+
+ def draw_buttons(self, context, layout):
+ self.ensure_sockets("PlasmaConditionSocket", "Condition", "condition")
+
+ layout.prop(self, "detect_trigger")
+ layout.prop(self, "detect_untrigger")
+ layout.prop(self, "no_ff_sounds")
+
+ def get_key(self, exporter, tree, so):
+ return exporter.mgr.find_create_key(plResponderModifier, name=self.create_key_name(tree), so=so)
+
+ def export(self, exporter, tree, bo, so):
+ responder = self.get_key(exporter, tree, so).object
+ if not bo.plasma_net.manual_sdl:
+ responder.setExclude("Responder")
+
+ if self.detect_trigger:
+ responder.flags |= plResponderModifier.kDetectTrigger
+ if self.detect_untrigger:
+ responder.flags |= plResponderModifier.kDetectUnTrigger
+ if self.no_ff_sounds:
+ responder.flags |= plResponderModifier.kSkipFFSound
+
+ class ResponderStateMgr:
+ def __init__(self, respNode, respMod):
+ self.states = []
+ self.parent = respNode
+ self.responder = respMod
+
+ def get_state(self, node):
+ for idx, (theNode, theState) in enumerate(self.states):
+ if theNode == node:
+ return (idx, theState)
+ state = plResponderModifier_State()
+ self.states.append((node, state))
+ return (len(self.states) - 1, state)
+
+ def save(self):
+ resp = self.responder
+ resp.clearStates()
+ for node, state in self.states:
+ resp.addState(state)
+
+ # Convert the Responder states
+ stateMgr = ResponderStateMgr(self, responder)
+ for stateNode in self.find_outputs("states", "PlasmaResponderStateNode"):
+ stateNode.convert_state(exporter, stateMgr)
+ stateMgr.save()
+
+
+class PlasmaResponderStateNode(PlasmaNodeVariableInput, bpy.types.Node):
+ bl_category = "LOGIC"
+ bl_idname = "PlasmaResponderStateNode"
+ bl_label = "Responder State"
+
+ default_state = BoolProperty(name="Default State",
+ description="This state is the responder's default",
+ default=False)
+
+ def init(self, context):
+ self.inputs.new("PlasmaRespStateSocket", "Condition", "condition")
+ self.outputs.new("PlasmaRespCommandSocket", "Commands", "cmds")
+ self.outputs.new("PlasmaRespStateSocket", "Trigger", "gotostate").link_limit = 1
+
+ def draw_buttons(self, context, layout):
+ # This actually draws nothing, but it makes sure we have at least one empty input slot
+ # We need this because it's possible that multiple OTHER states can call us
+ self.ensure_sockets("PlasmaRespStateSocket", "Condition", "condition")
+
+ # Now draw a prop
+ layout.prop(self, "default_state")
+
+ def convert_state(self, exporter, stateMgr):
+ idx, state = stateMgr.get_state(self)
+
+ # No sanity checking here. Hopefully nothing crazy has happened in the UI.
+ if self.default_state:
+ stateMgr.responder.curState = idx
+
+ # Where do we go from heah?
+ toStateNode = self.find_output("gotostate", "PlasmaResponderStateNode")
+ if toStateNode is None:
+ state.switchToState = idx
+ else:
+ toIdx, toState = stateMgr.get_state(toStateNode)
+ state.switchToState = toIdx
+
+ class CommandMgr:
+ def __init__(self):
+ self.commands = []
+ self.waits = {}
+
+ def add_command(self, node):
+ cmd = type("ResponderCommand", (), {"msg": None, "waitOn": -1})
+ self.commands.append((node, cmd))
+ return (len(self.commands) - 1, cmd)
+
+ def add_wait(self, parentCmd):
+ try:
+ idx = self.commands.index(parentCmd)
+ except ValueError:
+ # The parent command didn't export for some reason... Probably no message.
+ # So, wait on nothing!
+ return -1
+ else:
+ wait = len(self.waits)
+ self.waits[wait] = idx
+ return idx
+
+ def save(self, state):
+ for node, cmd in self.commands:
+ # Amusing, PyHSPlasma doesn't actually want a plResponderModifier_Cmd
+ # Meh, I'll let this one slide.
+ state.addCommand(cmd.msg, cmd.waitOn)
+ state.numCallbacks = len(self.waits)
+ state.waitToCmd = self.waits
+
+ # Convert the commands
+ commands = CommandMgr()
+ for i in self.find_outputs("cmds", "PlasmaResponderCommandNode"):
+ # slight optimization--commands attached to states can't wait on other commands
+ # namely because it's impossible to wait on a command that doesn't exist...
+ i.convert_command(exporter, stateMgr.responder, commands, True)
+ commands.save(state)
+
+
+class PlasmaRespStateSocket(PlasmaNodeSocketBase, bpy.types.NodeSocket):
+ bl_color = (0.388, 0.78, 0.388, 1.0)
+
+
+class PlasmaResponderCommandNode(PlasmaNodeBase, bpy.types.Node):
+ bl_category = "LOGIC"
+ bl_idname = "PlasmaResponderCommandNode"
+ bl_label = "Responder Command"
+
+ def init(self, context):
+ self.inputs.new("PlasmaRespCommandSocket", "Condition", "whodoneit")
+ self.outputs.new("PlasmaMessageSocket", "Message", "msg")
+ self.outputs.new("PlasmaRespCommandSocket", "Trigger", "trigger")
+
+ def convert_command(self, exporter, responder, commandMgr, forceNoWait=False):
+ # If this command has no message, there is no need to export it...
+ msgNode = self.find_output("msg")
+ if msgNode is not None:
+ idx, command = commandMgr.add_command(self)
+
+ # If the thingthatdoneit is another command, we need to register a wait.
+ # We could hack and assume the parent is idx-1, but that won't work if the parent has many
+ # child commands. Le whoops!
+ if not forceNoWait:
+ parentCmd = self.find_input("whodoneit", "PlasmaResponderCommandNode")
+ if parentCmd is not None:
+ command.waitOn = commandMgr.add_wait(parentCmd)
+
+ # Finally, convert our message...
+ msg = msgNode.convert_message(exporter)
+ self._finalize_message(exporter, responder, msg)
+
+ # If we have child commands, we need to make sure that we support chaining this message as a callback
+ # If not, we'll export our children and tell them to not actually wait on us.
+ haveChildren = self.find_output("trigger", "PlasmaResponderCommandNode") is not None
+ if haveChildren:
+ nowait = not self._add_msg_callback(exporter, responder, msg)
+ command.msg = msg
+ else:
+ nowait = True
+
+ # Export any child commands
+ for i in self.find_outputs("trigger", "PlasmaResponderCommandNode"):
+ i.convert_command(exporter, responder, commandMgr, nowait)
+
+ _bcast_flags = {
+ plArmatureEffectStateMsg: (plMessage.kPropagateToModifiers | plMessage.kNetPropagate),
+ }
+
+ def _finalize_message(self, exporter, responder, msg):
+ msg.sender = responder.key
+
+ # BCast Flags are pretty common...
+ _cls = msg.__class__
+ if _cls in self._bcast_flags:
+ msg.BCastFlags = self._bcast_flags[_cls]
+ msg.BCastFlags |= plMessage.kLocalPropagate
+
+ def _add_msg_callback(self, exporter, responder, msg):
+ """Prepares a given message to be a callback to the responder"""
+ # We do not support callback messages ATM
+ return False
+
+
+class PlasmaRespCommandSocket(PlasmaNodeSocketBase, bpy.types.NodeSocket):
+ bl_color = (0.451, 0.0, 0.263, 1.0)
+
diff --git a/korman/operators/op_modifier.py b/korman/operators/op_modifier.py
index 6b5311f..6c4e882 100644
--- a/korman/operators/op_modifier.py
+++ b/korman/operators/op_modifier.py
@@ -15,6 +15,7 @@
import bpy
from bpy.props import *
+import time
from ..properties import modifiers
@@ -123,3 +124,27 @@ class ModifierMoveDownOperator(ModifierMoveOperator, bpy.types.Operator):
if self.active_modifier < last:
self.swap_modifier_ids(plmods, self.active_modifier, self.active_modifier+1)
return {"FINISHED"}
+
+
+class ModifierLogicWizOperator(ModifierOperator, bpy.types.Operator):
+ bl_idname = "object.plasma_logicwiz"
+ bl_label = "Plasma LogicWiz"
+ bl_description = "Generates logic nodes from a given modifier on the active object"
+
+ modifier = StringProperty(name="Modifier", default="footstep")
+
+ def execute(self, context):
+ obj = context.active_object
+ mod = getattr(obj.plasma_modifiers, self.modifier)
+
+ print("--- Plasma LogicWiz ---")
+ print("Object: '{}'".format(obj.name))
+ print("Modifier: '{}'".format(self.modifier))
+ if not mod.enabled:
+ print("WRN: This modifier is not actually enabled!")
+
+ start = time.process_time()
+ mod.logicwiz(obj)
+ end = time.process_time()
+ print("\nLogicWiz finished in {:.2f} seconds".format(end-start))
+ return {"FINISHED"}
diff --git a/korman/properties/modifiers/base.py b/korman/properties/modifiers/base.py
index a8a5211..7f15211 100644
--- a/korman/properties/modifiers/base.py
+++ b/korman/properties/modifiers/base.py
@@ -13,6 +13,7 @@
# You should have received a copy of the GNU General Public License
# along with Korman. If not, see .
+import abc
import bpy
from bpy.props import *
@@ -48,3 +49,17 @@ class PlasmaModifierProperties(bpy.types.PropertyGroup):
"default": True,
"options": {"HIDDEN"}})
}
+
+
+class PlasmaModifierLogicWiz:
+ @property
+ def node_tree(self):
+ name = self.display_name
+ try:
+ return bpy.data.node_groups[name]
+ except LookupError:
+ return bpy.data.node_groups.new(name, "PlasmaNodeTree")
+
+ @abc.abstractmethod
+ def logicwiz(self, bo):
+ pass
diff --git a/korman/properties/modifiers/logic.py b/korman/properties/modifiers/logic.py
index e1c3965..92ac41b 100644
--- a/korman/properties/modifiers/logic.py
+++ b/korman/properties/modifiers/logic.py
@@ -14,10 +14,29 @@
# along with Korman. If not, see .
import bpy
+from bpy.props import *
from PyHSPlasma import *
from .base import PlasmaModifierProperties
+class PlasmaAdvancedLogic(PlasmaModifierProperties):
+ pl_id = "advanced_logic"
+
+ bl_category = "Logic"
+ bl_label = "Advanced"
+ bl_description = "Plasma Logic Nodes"
+ bl_icon = "NODETREE"
+
+ tree_name = StringProperty(name="Node Tree", description="Plasma Logic Nodes")
+
+ def created(self, obj):
+ self.display_name = "Advanced Logic"
+
+ def export(self, exporter, bo, so):
+ tree = bpy.data.node_groups[self.tree_name]
+ tree.export(exporter, bo, so)
+
+
class PlasmaSpawnPoint(PlasmaModifierProperties):
pl_id = "spawnpoint"
diff --git a/korman/properties/modifiers/region.py b/korman/properties/modifiers/region.py
index 899a6c5..89c96f3 100644
--- a/korman/properties/modifiers/region.py
+++ b/korman/properties/modifiers/region.py
@@ -17,7 +17,98 @@ import bpy
from bpy.props import *
from PyHSPlasma import *
-from .base import PlasmaModifierProperties
+from .base import PlasmaModifierProperties, PlasmaModifierLogicWiz
+from .physics import bounds_types
+
+footstep_surface_ids = {
+ "dirt": 0,
+ # 1 = NULL
+ "puddle": 2,
+ # 3 = tile (NULL in MOUL)
+ "metal": 4,
+ "woodbridge": 5,
+ "rope": 6,
+ "grass": 7,
+ # 8 = NULL
+ "woodfloor": 9,
+ "rug": 10,
+ "stone": 11,
+ # 12 = NULL
+ # 13 = metal ladder (dupe of metal)
+ "woodladder": 14,
+ "water": 15,
+ # 16 = maintainer's glass (NULL in PotS)
+ # 17 = maintainer's metal grating (NULL in PotS)
+ # 18 = swimming (why would you want this?)
+}
+
+footstep_surfaces = [("dirt", "Dirt", "Dirt"),
+ ("grass", "Grass", "Grass"),
+ ("metal", "Metal", "Metal Catwalk"),
+ ("puddle", "Puddle", "Shallow Water"),
+ ("rope", "Rope", "Rope Ladder"),
+ ("rug", "Rug", "Carpet Rug"),
+ ("stone", "Stone", "Stone Tile"),
+ ("water", "Water", "Deep Water"),
+ ("woodbridge", "Wood Bridge", "Wood Bridge"),
+ ("woodfloor", "Wood Floor", "Wood Floor"),
+ ("woodladder", "Wood Ladder", "Wood Ladder")]
+
+class PlasmaFootstepRegion(PlasmaModifierProperties, PlasmaModifierLogicWiz):
+ pl_id = "footstep"
+
+ bl_category = "Region"
+ bl_label = "Footstep"
+ bl_description = "Footstep Region"
+
+ surface = EnumProperty(name="Surface",
+ description="What kind of surface are we walking on?",
+ items=footstep_surfaces,
+ default="stone")
+ bounds = EnumProperty(name="Region Bounds",
+ description="Physical object's bounds",
+ items=bounds_types,
+ default="hull")
+
+
+ def created(self, obj):
+ self.display_name = "{}_FootRgn".format(obj.name)
+
+ def export(self, exporter, bo, so):
+ # Generate the logic nodes now
+ self.logicwiz(bo)
+
+ # Now, export the node tree
+ self.node_tree.export(exporter, bo, so)
+
+ def logicwiz(self, bo):
+ tree = self.node_tree
+ nodes = tree.nodes
+ nodes.clear()
+
+ # Region Sensor
+ volsens = nodes.new("PlasmaVolumeSensorNode")
+ volsens.name = "RegionSensor"
+ volsens.region = bo.name
+ volsens.bounds = self.bounds
+ volsens.find_input_socket("enter").allow = True
+ volsens.find_input_socket("exit").allow = True
+
+ # Responder
+ respmod = nodes.new("PlasmaResponderNode")
+ respmod.name = "Resp"
+ respmod.link_input(tree, volsens, "satisfies", "condition")
+ respstate = nodes.new("PlasmaResponderStateNode")
+ respstate.link_input(tree, respmod, "states", "condition")
+ respstate.default_state = True
+ respcmd = nodes.new("PlasmaResponderCommandNode")
+ respcmd.link_input(tree, respstate, "cmds", "whodoneit")
+
+ # ArmatureEffectStateMsg
+ msg = nodes.new("PlasmaFootstepSoundMsgNode")
+ msg.link_input(tree, respcmd, "msg", "sender")
+ msg.surface = self.surface
+
class PlasmaPanicLinkRegion(PlasmaModifierProperties):
pl_id = "paniclink"
diff --git a/korman/ui/modifiers/logic.py b/korman/ui/modifiers/logic.py
index 08cc998..c250d02 100644
--- a/korman/ui/modifiers/logic.py
+++ b/korman/ui/modifiers/logic.py
@@ -13,6 +13,11 @@
# You should have received a copy of the GNU General Public License
# along with Korman. If not, see .
+import bpy
+
+def advanced_logic(modifier, layout, context):
+ layout.prop_search(modifier, "tree_name", bpy.data, "node_groups", icon="NODETREE")
+
def spawnpoint(modifier, layout, context):
layout.label(text="The Y axis indicates the direction the avatar is facing.")
diff --git a/korman/ui/modifiers/region.py b/korman/ui/modifiers/region.py
index 111967d..823e408 100644
--- a/korman/ui/modifiers/region.py
+++ b/korman/ui/modifiers/region.py
@@ -13,6 +13,10 @@
# You should have received a copy of the GNU General Public License
# along with Korman. If not, see .
+def footstep(modifier, layout, context):
+ layout.prop(modifier, "bounds")
+ layout.prop(modifier, "surface")
+
def paniclink(modifier, layout, context):
split = layout.split()
col = split.column()