From 5192be428e52a45ab4f257688ed0647d36055cd8 Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Mon, 8 Jun 2015 14:12:57 -0400 Subject: [PATCH 1/7] Begin work on Logic Nodes UI --- korman/__init__.py | 3 + korman/nodes/__init__.py | 68 ++++++++++++++++ korman/nodes/node_conditions.py | 98 +++++++++++++++++++++++ korman/nodes/node_core.py | 87 +++++++++++++++++++++ korman/nodes/node_logic.py | 31 ++++++++ korman/nodes/node_messages.py | 40 ++++++++++ korman/nodes/node_responder.py | 108 ++++++++++++++++++++++++++ korman/operators/__init__.py | 1 + korman/operators/op_nodes.py | 55 +++++++++++++ korman/properties/modifiers/region.py | 35 +++++++++ 10 files changed, 526 insertions(+) create mode 100644 korman/nodes/__init__.py create mode 100644 korman/nodes/node_conditions.py create mode 100644 korman/nodes/node_core.py create mode 100644 korman/nodes/node_logic.py create mode 100644 korman/nodes/node_messages.py create mode 100644 korman/nodes/node_responder.py create mode 100644 korman/operators/op_nodes.py 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/nodes/__init__.py b/korman/nodes/__init__.py new file mode 100644 index 0000000..869f1b0 --- /dev/null +++ b/korman/nodes/__init__.py @@ -0,0 +1,68 @@ +# 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_logic 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..1ac9574 --- /dev/null +++ b/korman/nodes/node_conditions.py @@ -0,0 +1,98 @@ +# 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 .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"), + ("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, "meshes", icon="MESH_DATA") + layout.prop(self, "bounds") + + +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..f00e389 --- /dev/null +++ b/korman/nodes/node_core.py @@ -0,0 +1,87 @@ +# This file is part of Korman. +# +# Korman is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Korman is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Korman. If not, see . + +import bpy + +class PlasmaNodeBase: + 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_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""" + in_socket = self.find_input_socket(in_key) + out_socket = node.find_output_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""" + in_socket = node.find_input_socket(in_key) + out_socket = self.find_output_socket(out_key) + link = tree.links.new(in_socket, out_socket) + + @classmethod + def poll(cls, context): + return (context.bl_idname == "PlasmaNodeTree") + + +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" + + @classmethod + def poll(cls, context): + return (context.scene.render.engine == "PLASMA_GAME") diff --git a/korman/nodes/node_logic.py b/korman/nodes/node_logic.py new file mode 100644 index 0000000..48971cc --- /dev/null +++ b/korman/nodes/node_logic.py @@ -0,0 +1,31 @@ +# 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 .node_core import * + +class PlasmaLogicTriggerNode(PlasmaNodeBase, bpy.types.Node): + bl_category = "LOGIC" + bl_idname = "PlasmaLogicTriggerNode" + bl_label = "Logic Trigger" + + def init(self, context): + self.inputs.new("PlasmaConditionSocket", "Condition", "condition") + self.outputs.new("PlasmaRespTriggerSocket", "Trigger", "trigger") + + +class PlasmaRespTriggerSocket(PlasmaNodeSocketBase, bpy.types.NodeSocket): + bl_color = (0.384, 0.239, 0.239, 1.0) diff --git a/korman/nodes/node_messages.py b/korman/nodes/node_messages.py new file mode 100644 index 0000000..bec5981 --- /dev/null +++ b/korman/nodes/node_messages.py @@ -0,0 +1,40 @@ +# 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 .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") diff --git a/korman/nodes/node_responder.py b/korman/nodes/node_responder.py new file mode 100644 index 0000000..47e6811 --- /dev/null +++ b/korman/nodes/node_responder.py @@ -0,0 +1,108 @@ +# 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 uuid + +from .node_core import * + +class PlasmaResponderNode(PlasmaNodeBase, bpy.types.Node): + bl_category = "LOGIC" + bl_idname = "PlasmaResponderNode" + bl_label = "Responder" + + def init(self, context): + self.inputs.new("PlasmaRespTriggerSocket", "Trigger", "whodoneit") + self.outputs.new("PlasmaRespStateSocket", "States", "states") + + +class PlasmaResponderStateNode(PlasmaNodeBase, bpy.types.Node): + bl_category = "LOGIC" + bl_idname = "PlasmaResponderStateNode" + bl_label = "Responder State" + + def init(self, context): + self.inputs.new("PlasmaRespStateSocket", "Condition", "whodoneit") + self.outputs.new("PlasmaRespCommandSocket", "Commands", "cmds") + self.outputs.new("PlasmaRespStateSocket", "Trigger", "gotostate").link_limit = 1 + + +class PlasmaRespStateSocketBase(PlasmaNodeSocketBase): + bl_color = (0.388, 0.78, 0.388, 1.0) + + +class PlasmaRespStateSocket(PlasmaRespStateSocketBase, bpy.types.NodeSocket): + default_state = BoolProperty(name="Default State", + description="This state is the Responder's default", + default=False) + + def draw(self, context, layout, node, text): + # If this is a RespoderState node and the parent is a Responder, offer the user the + # ability to make this the default state. + if self.is_linked and not self.is_output: + # Before we do anything, see if we need to do a delayed update... + if node.bl_idname == "PlasmaResponderStateNode": + parent = node.find_input("whodoneit", "PlasmaResponderNode") + if parent is not None: + layout.prop(self, "default_state") + return + + # Still here? Draw the text. + layout.label(text) + + +class PlasmaResponderStateListNode(PlasmaNodeBase, bpy.types.Node): + bl_category = "LOGIC" + bl_idname = "PlasmaResponderStateListNode" + bl_label = "Responder State List" + + def add_state_input(self): + self.inputs.new("PlasmaRespStateListSocket", str(uuid.uuid4())) + + def init(self, context): + # Inputs will be added by the user + self.outputs.new("PlasmaRespStateSocket", "Go To State", "gotostate") + + def draw_buttons(self, context, layout): + # This will allow us to add input states on the fly. + # Caveat: We're only showing this operator in the properties because we need the node + # to be active in the operator... + op = layout.operator("node.plasma_add_responder_state", text="Add State", icon="ZOOMIN") + op.node_name = self.name + + +class PlasmaRespStateListSocket(PlasmaRespStateSocketBase, bpy.types.NodeSocket): + def draw(self, context, layout, node, text): + # We'll allow them to delete all their inputs if they want to be stupid... + props = layout.operator("node.plasma_remove_responder_state", text="", icon="X") + props.node_name = node.name + props.socket_name = self.name + + +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") + + +class PlasmaRespCommandSocket(PlasmaNodeSocketBase, bpy.types.NodeSocket): + bl_color = (0.451, 0.0, 0.263, 1.0) + diff --git a/korman/operators/__init__.py b/korman/operators/__init__.py index f0c38fe..bc307e9 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 node from . import op_world as world def register(): diff --git a/korman/operators/op_nodes.py b/korman/operators/op_nodes.py new file mode 100644 index 0000000..6840467 --- /dev/null +++ b/korman/operators/op_nodes.py @@ -0,0 +1,55 @@ +# 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 * + +class NodeOperator: + def get_node_tree(self, context): + space = context.space_data + if space.type != "NODE_EDITOR": + raise RuntimeError("Operator '{}' should only be used in the node editor".format(self.bl_idname)) + return space.node_tree + + @classmethod + def poll(cls, context): + return context.scene.render.engine == "PLASMA_GAME" + + +class ResponderStateAddOperator(NodeOperator, bpy.types.Operator): + bl_idname = "node.plasma_add_responder_state" + bl_label = "Add Responder State Socket" + + node_name = StringProperty(name="Node's name", options={"HIDDEN"}) + + def execute(self, context): + tree = self.get_node_tree(context) + tree.nodes[self.node_name].add_state_input() + return {"FINISHED"} + + +class ResponderStateRemoveOperator(NodeOperator, bpy.types.Operator): + bl_idname = "node.plasma_remove_responder_state" + bl_label = "Remove Responder State Socket" + + node_name = StringProperty(name="Node's name", options={"HIDDEN"}) + socket_name = StringProperty(name="Socket name to remove", options={"HIDDEN"}) + + def execute(self, context): + tree = self.get_node_tree(context) + node = tree.nodes[self.node_name] + socket = node.inputs[self.socket_name] + node.inputs.remove(socket) + return {"FINISHED"} diff --git a/korman/properties/modifiers/region.py b/korman/properties/modifiers/region.py index 899a6c5..708b55e 100644 --- a/korman/properties/modifiers/region.py +++ b/korman/properties/modifiers/region.py @@ -19,6 +19,41 @@ from PyHSPlasma import * from .base import PlasmaModifierProperties +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 PlasmaPanicLinkRegion(PlasmaModifierProperties): pl_id = "paniclink" From 4ecf262c613695927eea404c3267ecd92402fe26 Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Mon, 8 Jun 2015 19:47:40 -0400 Subject: [PATCH 2/7] Footstep Region Modifier: Logic Node test case This commit adds a footstep region modifier that exports nothing at the moment. This is just a test case for logic node generation. If you want to bake footstep regions into logic nodes, select an object and execute `bpy.ops.object.plasma_logicwiz()`. :) --- korman/operators/op_modifier.py | 25 +++++++++++++ korman/properties/modifiers/base.py | 15 ++++++++ korman/properties/modifiers/region.py | 53 ++++++++++++++++++++++++++- korman/ui/modifiers/region.py | 4 ++ 4 files changed, 96 insertions(+), 1 deletion(-) 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..3432682 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 = "LOGICWIZ_{}".format(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/region.py b/korman/properties/modifiers/region.py index 708b55e..c3c1152 100644 --- a/korman/properties/modifiers/region.py +++ b/korman/properties/modifiers/region.py @@ -17,7 +17,8 @@ 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, @@ -53,6 +54,56 @@ footstep_surfaces = [("dirt", "Dirt", "Dirt"), ("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 logicwiz(self, bo): + tree = self.node_tree + nodes = tree.nodes + nodes.clear() + + # Region Sensor + volsens = nodes.new("PlasmaVolumeSensorNode") + volsens.region = bo.data.name + volsens.bounds = self.bounds + volsens.find_input_socket("enter").allow = True + volsens.find_input_socket("exit").allow = True + + # LogicMod + logicmod = nodes.new("PlasmaLogicTriggerNode") + logicmod.link_input(tree, volsens, "satisfies", "condition") + + # Responder + respmod = nodes.new("PlasmaResponderNode") + respmod.link_input(tree, logicmod, "trigger", "whodoneit") + respstate = nodes.new("PlasmaResponderStateNode") + respstate.link_input(tree, respmod, "states", "whodoneit") + respstate.find_input_socket("whodoneit").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/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() From b9381e078e2830cbea676867c430a243c9b450d3 Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Tue, 9 Jun 2015 00:47:26 -0400 Subject: [PATCH 3/7] Replace RespStateList crazy with a nicer solution Many thanks to Deledrius for pointing out an example of this hackery in Blender's node groups! --- korman/nodes/node_core.py | 33 ++++++++++++-- korman/nodes/node_responder.py | 63 ++++++--------------------- korman/operators/__init__.py | 1 - korman/operators/op_nodes.py | 55 ----------------------- korman/properties/modifiers/region.py | 4 +- 5 files changed, 44 insertions(+), 112 deletions(-) delete mode 100644 korman/operators/op_nodes.py diff --git a/korman/nodes/node_core.py b/korman/nodes/node_core.py index f00e389..574fecb 100644 --- a/korman/nodes/node_core.py +++ b/korman/nodes/node_core.py @@ -51,14 +51,26 @@ class PlasmaNodeBase: 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""" - in_socket = self.find_input_socket(in_key) - out_socket = node.find_output_socket(out_key) + 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""" - in_socket = node.find_input_socket(in_key) - out_socket = self.find_output_socket(out_key) + 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 @@ -66,6 +78,19 @@ class PlasmaNodeBase: 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) diff --git a/korman/nodes/node_responder.py b/korman/nodes/node_responder.py index 47e6811..36ed13a 100644 --- a/korman/nodes/node_responder.py +++ b/korman/nodes/node_responder.py @@ -29,67 +29,30 @@ class PlasmaResponderNode(PlasmaNodeBase, bpy.types.Node): self.outputs.new("PlasmaRespStateSocket", "States", "states") -class PlasmaResponderStateNode(PlasmaNodeBase, bpy.types.Node): +class PlasmaResponderStateNode(PlasmaNodeVariableInput, bpy.types.Node): bl_category = "LOGIC" bl_idname = "PlasmaResponderStateNode" bl_label = "Responder State" - def init(self, context): - self.inputs.new("PlasmaRespStateSocket", "Condition", "whodoneit") - self.outputs.new("PlasmaRespCommandSocket", "Commands", "cmds") - self.outputs.new("PlasmaRespStateSocket", "Trigger", "gotostate").link_limit = 1 - - -class PlasmaRespStateSocketBase(PlasmaNodeSocketBase): - bl_color = (0.388, 0.78, 0.388, 1.0) - - -class PlasmaRespStateSocket(PlasmaRespStateSocketBase, bpy.types.NodeSocket): default_state = BoolProperty(name="Default State", - description="This state is the Responder's default", + description="This state is the responder's default", default=False) - def draw(self, context, layout, node, text): - # If this is a RespoderState node and the parent is a Responder, offer the user the - # ability to make this the default state. - if self.is_linked and not self.is_output: - # Before we do anything, see if we need to do a delayed update... - if node.bl_idname == "PlasmaResponderStateNode": - parent = node.find_input("whodoneit", "PlasmaResponderNode") - if parent is not None: - layout.prop(self, "default_state") - return - - # Still here? Draw the text. - layout.label(text) - + def init(self, context): + self.outputs.new("PlasmaRespCommandSocket", "Commands", "cmds") + self.outputs.new("PlasmaRespStateSocket", "Trigger", "gotostate").link_limit = 1 -class PlasmaResponderStateListNode(PlasmaNodeBase, bpy.types.Node): - bl_category = "LOGIC" - bl_idname = "PlasmaResponderStateListNode" - bl_label = "Responder State List" + 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") - def add_state_input(self): - self.inputs.new("PlasmaRespStateListSocket", str(uuid.uuid4())) + # Now draw a prop + layout.prop(self, "default_state") - def init(self, context): - # Inputs will be added by the user - self.outputs.new("PlasmaRespStateSocket", "Go To State", "gotostate") - def draw_buttons(self, context, layout): - # This will allow us to add input states on the fly. - # Caveat: We're only showing this operator in the properties because we need the node - # to be active in the operator... - op = layout.operator("node.plasma_add_responder_state", text="Add State", icon="ZOOMIN") - op.node_name = self.name - - -class PlasmaRespStateListSocket(PlasmaRespStateSocketBase, bpy.types.NodeSocket): - def draw(self, context, layout, node, text): - # We'll allow them to delete all their inputs if they want to be stupid... - props = layout.operator("node.plasma_remove_responder_state", text="", icon="X") - props.node_name = node.name - props.socket_name = self.name +class PlasmaRespStateSocket(PlasmaNodeSocketBase, bpy.types.NodeSocket): + bl_color = (0.388, 0.78, 0.388, 1.0) class PlasmaResponderCommandNode(PlasmaNodeBase, bpy.types.Node): diff --git a/korman/operators/__init__.py b/korman/operators/__init__.py index bc307e9..f0c38fe 100644 --- a/korman/operators/__init__.py +++ b/korman/operators/__init__.py @@ -16,7 +16,6 @@ from . import op_export as exporter from . import op_lightmap as lightmap from . import op_modifier as modifier -from . import op_nodes as node from . import op_world as world def register(): diff --git a/korman/operators/op_nodes.py b/korman/operators/op_nodes.py deleted file mode 100644 index 6840467..0000000 --- a/korman/operators/op_nodes.py +++ /dev/null @@ -1,55 +0,0 @@ -# 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 * - -class NodeOperator: - def get_node_tree(self, context): - space = context.space_data - if space.type != "NODE_EDITOR": - raise RuntimeError("Operator '{}' should only be used in the node editor".format(self.bl_idname)) - return space.node_tree - - @classmethod - def poll(cls, context): - return context.scene.render.engine == "PLASMA_GAME" - - -class ResponderStateAddOperator(NodeOperator, bpy.types.Operator): - bl_idname = "node.plasma_add_responder_state" - bl_label = "Add Responder State Socket" - - node_name = StringProperty(name="Node's name", options={"HIDDEN"}) - - def execute(self, context): - tree = self.get_node_tree(context) - tree.nodes[self.node_name].add_state_input() - return {"FINISHED"} - - -class ResponderStateRemoveOperator(NodeOperator, bpy.types.Operator): - bl_idname = "node.plasma_remove_responder_state" - bl_label = "Remove Responder State Socket" - - node_name = StringProperty(name="Node's name", options={"HIDDEN"}) - socket_name = StringProperty(name="Socket name to remove", options={"HIDDEN"}) - - def execute(self, context): - tree = self.get_node_tree(context) - node = tree.nodes[self.node_name] - socket = node.inputs[self.socket_name] - node.inputs.remove(socket) - return {"FINISHED"} diff --git a/korman/properties/modifiers/region.py b/korman/properties/modifiers/region.py index c3c1152..c239eec 100644 --- a/korman/properties/modifiers/region.py +++ b/korman/properties/modifiers/region.py @@ -94,8 +94,8 @@ class PlasmaFootstepRegion(PlasmaModifierProperties, PlasmaModifierLogicWiz): respmod = nodes.new("PlasmaResponderNode") respmod.link_input(tree, logicmod, "trigger", "whodoneit") respstate = nodes.new("PlasmaResponderStateNode") - respstate.link_input(tree, respmod, "states", "whodoneit") - respstate.find_input_socket("whodoneit").default_state = True + respstate.link_input(tree, respmod, "states", respstate.inputs.new("PlasmaRespStateSocket", "Responder")) + respstate.default_state = True respcmd = nodes.new("PlasmaResponderCommandNode") respcmd.link_input(tree, respstate, "cmds", "whodoneit") From 53b1b66258ecaf056ad9ca74ab76fb4d9f549660 Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Wed, 10 Jun 2015 15:49:54 -0400 Subject: [PATCH 4/7] Responder nodes now export themselves! Please note this does not address logic triggers or region sensors. Those will be added in a future commit. --- korman/exporter/convert.py | 6 +- korman/exporter/manager.py | 18 ++-- korman/exporter/rtlight.py | 2 +- korman/nodes/node_core.py | 27 +++++ korman/nodes/node_logic.py | 31 ------ korman/nodes/node_messages.py | 6 ++ korman/nodes/node_responder.py | 137 ++++++++++++++++++++++++++ korman/properties/modifiers/region.py | 7 ++ 8 files changed, 192 insertions(+), 42 deletions(-) delete mode 100644 korman/nodes/node_logic.py 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/node_core.py b/korman/nodes/node_core.py index 574fecb..f214310 100644 --- a/korman/nodes/node_core.py +++ b/korman/nodes/node_core.py @@ -13,9 +13,19 @@ # 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: @@ -34,6 +44,18 @@ class PlasmaNodeBase: 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: @@ -107,6 +129,11 @@ class PlasmaNodeTree(bpy.types.NodeTree): 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_logic.py b/korman/nodes/node_logic.py deleted file mode 100644 index 48971cc..0000000 --- a/korman/nodes/node_logic.py +++ /dev/null @@ -1,31 +0,0 @@ -# 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 .node_core import * - -class PlasmaLogicTriggerNode(PlasmaNodeBase, bpy.types.Node): - bl_category = "LOGIC" - bl_idname = "PlasmaLogicTriggerNode" - bl_label = "Logic Trigger" - - def init(self, context): - self.inputs.new("PlasmaConditionSocket", "Condition", "condition") - self.outputs.new("PlasmaRespTriggerSocket", "Trigger", "trigger") - - -class PlasmaRespTriggerSocket(PlasmaNodeSocketBase, bpy.types.NodeSocket): - bl_color = (0.384, 0.239, 0.239, 1.0) diff --git a/korman/nodes/node_messages.py b/korman/nodes/node_messages.py index bec5981..ccb8080 100644 --- a/korman/nodes/node_messages.py +++ b/korman/nodes/node_messages.py @@ -15,6 +15,7 @@ import bpy from bpy.props import * +from PyHSPlasma import * from .node_core import * from ..properties.modifiers.region import footstep_surfaces, footstep_surface_ids @@ -38,3 +39,8 @@ class PlasmaFootstepSoundMsgNode(PlasmaNodeBase, bpy.types.Node): 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 index 36ed13a..ca71fe2 100644 --- a/korman/nodes/node_responder.py +++ b/korman/nodes/node_responder.py @@ -15,6 +15,7 @@ import bpy from bpy.props import * +from PyHSPlasma import * import uuid from .node_core import * @@ -28,6 +29,40 @@ class PlasmaResponderNode(PlasmaNodeBase, bpy.types.Node): self.inputs.new("PlasmaRespTriggerSocket", "Trigger", "whodoneit") self.outputs.new("PlasmaRespStateSocket", "States", "states") + 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") + + 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" @@ -50,6 +85,59 @@ class PlasmaResponderStateNode(PlasmaNodeVariableInput, bpy.types.Node): # 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) @@ -65,6 +153,55 @@ class PlasmaResponderCommandNode(PlasmaNodeBase, bpy.types.Node): 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/properties/modifiers/region.py b/korman/properties/modifiers/region.py index c239eec..407d454 100644 --- a/korman/properties/modifiers/region.py +++ b/korman/properties/modifiers/region.py @@ -74,6 +74,13 @@ class PlasmaFootstepRegion(PlasmaModifierProperties, PlasmaModifierLogicWiz): 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 From 1b5b229a334ca06adc054c7cfd1dc1e43af603b8 Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Wed, 10 Jun 2015 18:22:37 -0400 Subject: [PATCH 5/7] Export Region Sensors --- korman/nodes/__init__.py | 1 - korman/nodes/node_conditions.py | 82 ++++++++++++++++++++++++++- korman/nodes/node_responder.py | 10 +++- korman/properties/modifiers/base.py | 2 +- korman/properties/modifiers/region.py | 12 ++-- 5 files changed, 94 insertions(+), 13 deletions(-) diff --git a/korman/nodes/__init__.py b/korman/nodes/__init__.py index 869f1b0..d34b022 100644 --- a/korman/nodes/__init__.py +++ b/korman/nodes/__init__.py @@ -21,7 +21,6 @@ import nodeitems_utils # Put all Korman node modules here... from .node_conditions import * from .node_core import * -from .node_logic import * from .node_messages import * from .node_responder import * diff --git a/korman/nodes/node_conditions.py b/korman/nodes/node_conditions.py index 1ac9574..6aad214 100644 --- a/korman/nodes/node_conditions.py +++ b/korman/nodes/node_conditions.py @@ -15,6 +15,7 @@ import bpy from bpy.props import * +from PyHSPlasma import * from .node_core import PlasmaNodeBase, PlasmaNodeSocketBase from ..properties.modifiers.physics import bounds_types @@ -31,6 +32,7 @@ class PlasmaVolumeReportNode(PlasmaNodeBase, bpy.types.Node): 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", @@ -77,9 +79,87 @@ class PlasmaVolumeSensorNode(PlasmaNodeBase, bpy.types.Node): 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, "meshes", icon="MESH_DATA") + 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) diff --git a/korman/nodes/node_responder.py b/korman/nodes/node_responder.py index ca71fe2..70162a6 100644 --- a/korman/nodes/node_responder.py +++ b/korman/nodes/node_responder.py @@ -20,15 +20,18 @@ import uuid from .node_core import * -class PlasmaResponderNode(PlasmaNodeBase, bpy.types.Node): +class PlasmaResponderNode(PlasmaNodeVariableInput, bpy.types.Node): bl_category = "LOGIC" bl_idname = "PlasmaResponderNode" bl_label = "Responder" def init(self, context): - self.inputs.new("PlasmaRespTriggerSocket", "Trigger", "whodoneit") + self.inputs.new("PlasmaConditionSocket", "Condition", "condition") self.outputs.new("PlasmaRespStateSocket", "States", "states") + def draw_buttons(self, context, layout): + self.ensure_sockets("PlasmaConditionSocket", "Condition", "condition") + def get_key(self, exporter, tree, so): return exporter.mgr.find_create_key(plResponderModifier, name=self.create_key_name(tree), so=so) @@ -74,13 +77,14 @@ class PlasmaResponderStateNode(PlasmaNodeVariableInput, bpy.types.Node): 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") + self.ensure_sockets("PlasmaRespStateSocket", "Condition", "condition") # Now draw a prop layout.prop(self, "default_state") diff --git a/korman/properties/modifiers/base.py b/korman/properties/modifiers/base.py index 3432682..7f15211 100644 --- a/korman/properties/modifiers/base.py +++ b/korman/properties/modifiers/base.py @@ -54,7 +54,7 @@ class PlasmaModifierProperties(bpy.types.PropertyGroup): class PlasmaModifierLogicWiz: @property def node_tree(self): - name = "LOGICWIZ_{}".format(self.display_name) + name = self.display_name try: return bpy.data.node_groups[name] except LookupError: diff --git a/korman/properties/modifiers/region.py b/korman/properties/modifiers/region.py index 407d454..89c96f3 100644 --- a/korman/properties/modifiers/region.py +++ b/korman/properties/modifiers/region.py @@ -88,20 +88,18 @@ class PlasmaFootstepRegion(PlasmaModifierProperties, PlasmaModifierLogicWiz): # Region Sensor volsens = nodes.new("PlasmaVolumeSensorNode") - volsens.region = bo.data.name + 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 - # LogicMod - logicmod = nodes.new("PlasmaLogicTriggerNode") - logicmod.link_input(tree, volsens, "satisfies", "condition") - # Responder respmod = nodes.new("PlasmaResponderNode") - respmod.link_input(tree, logicmod, "trigger", "whodoneit") + respmod.name = "Resp" + respmod.link_input(tree, volsens, "satisfies", "condition") respstate = nodes.new("PlasmaResponderStateNode") - respstate.link_input(tree, respmod, "states", respstate.inputs.new("PlasmaRespStateSocket", "Responder")) + respstate.link_input(tree, respmod, "states", "condition") respstate.default_state = True respcmd = nodes.new("PlasmaResponderCommandNode") respcmd.link_input(tree, respstate, "cmds", "whodoneit") From af2229ec64cabb224bb7002c4d979b9d254ed8ba Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Wed, 10 Jun 2015 18:54:54 -0400 Subject: [PATCH 6/7] Expose some useful ResponderModifier flags --- korman/nodes/node_responder.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/korman/nodes/node_responder.py b/korman/nodes/node_responder.py index 70162a6..9f6e24b 100644 --- a/korman/nodes/node_responder.py +++ b/korman/nodes/node_responder.py @@ -24,6 +24,17 @@ 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") @@ -32,6 +43,10 @@ class PlasmaResponderNode(PlasmaNodeVariableInput, bpy.types.Node): 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) @@ -40,6 +55,13 @@ class PlasmaResponderNode(PlasmaNodeVariableInput, bpy.types.Node): 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 = [] From 89dd050110a806ba721feb49acf3d83b35f1a23b Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Wed, 10 Jun 2015 19:52:27 -0400 Subject: [PATCH 7/7] Add "Advanced Logic" Modifier This modifier allows us to manually construct a Plasma logic node tree and have it export. --- korman/properties/modifiers/logic.py | 19 +++++++++++++++++++ korman/ui/modifiers/logic.py | 5 +++++ 2 files changed, 24 insertions(+) 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/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.")