diff --git a/korman/nodes/__init__.py b/korman/nodes/__init__.py index f1670ef..28c65eb 100644 --- a/korman/nodes/__init__.py +++ b/korman/nodes/__init__.py @@ -22,6 +22,7 @@ import nodeitems_utils from .node_avatar import * from .node_conditions import * from .node_core import * +from .node_deprecated import * from .node_logic import * from .node_messages import * from .node_python import * @@ -53,7 +54,11 @@ _kategory_names = { _kategories = {} for cls in dict(globals()).values(): if inspect.isclass(cls): - if not issubclass(cls, PlasmaNodeBase) or not issubclass(cls, bpy.types.Node): + if not issubclass(cls, PlasmaNodeBase): + continue + if not issubclass(cls, bpy.types.Node): + continue + if issubclass(cls, PlasmaDeprecatedNode): continue else: continue diff --git a/korman/nodes/node_conditions.py b/korman/nodes/node_conditions.py index d44363d..8f87474 100644 --- a/korman/nodes/node_conditions.py +++ b/korman/nodes/node_conditions.py @@ -82,6 +82,11 @@ class PlasmaClickableNode(idprops.IDPropObjectMixin, PlasmaNodeBase, bpy.types.N interface.addIntfKey(logicmod) logicmod = logicmod.object + # If we receive an enable message, this is a one-shot type deal that needs to be disabled + # while the attached responder is running. + if self.find_input("message", "PlasmaEnableMsgNode") is not None: + logicmod.setLogicFlag(plLogicModifier.kOneShot, True) + # Try to figure out the appropriate bounds type for the clickable.... phys_mod = clickable_bo.plasma_modifiers.collision bounds = phys_mod.bounds if phys_mod.enabled else self.bounds diff --git a/korman/nodes/node_core.py b/korman/nodes/node_core.py index 1bfdcca..c3dfa2a 100644 --- a/korman/nodes/node_core.py +++ b/korman/nodes/node_core.py @@ -65,11 +65,22 @@ class PlasmaNodeBase: if idname is None or idname == node.bl_idname: yield node - def find_input_socket(self, key): + def find_input_socket(self, key, spawn_empty=False): + # In the case that this socket will be used to make new input linkage, + # we might want to allow the spawning of a new input socket... :) + # This will only be done if the node's socket definitions allow it. + options = self._socket_defs[0].get(key, {}) + spawn_empty = spawn_empty and options.get("spawn_empty", False) + for i in self.inputs: if i.alias == key: + if spawn_empty and i.is_linked: + continue return i - raise KeyError(key) + if spawn_empty: + return self._spawn_socket(key, options, self.inputs) + else: + raise KeyError(key) def find_input_sockets(self, key, idname=None): for i in self.inputs: @@ -118,7 +129,7 @@ class PlasmaNodeBase: def link_input(self, 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) + in_socket = self.find_input_socket(in_key, spawn_empty=True) else: in_socket = in_key if isinstance(out_key, str): @@ -130,7 +141,7 @@ class PlasmaNodeBase: def link_output(self, 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) + in_socket = node.find_input_socket(in_key, spawn_empty=True) else: in_socket = in_key if isinstance(out_key, str): @@ -161,6 +172,15 @@ class PlasmaNodeBase: return (getattr(self.__class__, "input_sockets", {}), getattr(self.__class__, "output_sockets", {})) + def _spawn_socket(self, key, options, sockets): + socket = sockets.new(options["type"], options["text"], key) + link_limit = options.get("link_limit", None) + if link_limit is not None: + socket.link_limit = link_limit + socket.hide = options.get("hidden", False) + socket.hide_value = options.get("hidden", False) + return socket + def _tattle(self, socket, link, reason): direction = "->" if socket.is_output else "<-" print("Removing {} {} {} {}".format(link.from_node.name, direction, link.to_node.name, reason)) @@ -187,6 +207,8 @@ class PlasmaNodeBase: link_limit = options.get("link_limit", None) if link_limit is not None: socket.link_limit = link_limit + socket.hide = options.get("hidden", False) + socket.hide_value = options.get("hidden", False) # Make sure the link is good allowed_sockets = options.get("valid_link_sockets", None) @@ -237,11 +259,12 @@ class PlasmaNodeBase: # Create any new sockets for alias in (j for j in defs if j not in done): - options = defs[alias] - socket = sockets.new(options["type"], options["text"], alias) - link_limit = options.get("link_limit", None) - if link_limit is not None: - socket.link_limit = link_limit + self._spawn_socket(alias, defs[alias], sockets) + + def _whine(self, msg, *args): + if args: + msg = msg.format(*args) + print("'{}' Node '{}': Whinging about {}".format(self.bl_idname, self.name, msg)) class PlasmaTreeOutputNodeBase(PlasmaNodeBase): diff --git a/korman/nodes/node_deprecated.py b/korman/nodes/node_deprecated.py new file mode 100644 index 0000000..4621768 --- /dev/null +++ b/korman/nodes/node_deprecated.py @@ -0,0 +1,131 @@ +# 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 collections import OrderedDict + +from .node_core import * + +class PlasmaDeprecatedNode(PlasmaNodeBase): + def upgrade(self): + raise NotImplementedError() + + +class PlasmaRespCommandSocket(PlasmaNodeSocketBase, bpy.types.NodeSocket): + bl_color = (0.451, 0.0, 0.263, 1.0) + + +class PlasmaResponderCommandNode(PlasmaDeprecatedNode, bpy.types.Node): + bl_category = "LOGIC" + bl_idname = "PlasmaResponderCommandNode" + bl_label = "Responder Command" + + input_sockets = OrderedDict([ + ("whodoneit", { + "text": "Condition", + "type": "PlasmaRespCommandSocket", + }), + ]) + + output_sockets = OrderedDict([ + ("msg", { + "link_limit": 1, + "text": "Message", + "type": "PlasmaMessageSocket", + }), + ("trigger", { + "text": "Trigger", + "type": "PlasmaRespCommandSocket", + }), + ("reenable", { + "text": "Local Reenable", + "type": "PlasmaEnableMessageSocket", + }), + ]) + + def _find_message_sender_node(self, parentCmdNode=None): + if parentCmdNode is None: + parentCmdNode = self + else: + if self == parentCmdNode: + self._whine("responder tree is circular") + return None + + if parentCmdNode.bl_idname == "PlasmaResponderStateNode": + return parentCmdNode + elif parentCmdNode.bl_idname == "PlasmaResponderCommandNode": + # potentially a responder wait command... let's see if the message can wait + if parentCmdNode != self: + cmdMsgNode = parentCmdNode.find_output("msg") + if cmdMsgNode is not None and cmdMsgNode.has_callbacks: + return cmdMsgNode + + # can't wait on this command/message, so go up the tree... + grandParentCmdNode = parentCmdNode.find_input("whodoneit") + if grandParentCmdNode is None: + self._whine("orphaned responder command") + return None + return self._find_message_sender_node(grandParentCmdNode) + else: + self._whine("unexpected command node type '{}'", parentCmdNode.bl_idname) + return None + + + def upgrade(self): + senderNode = self._find_message_sender_node() + if senderNode is None: + return + + msgNode = self.find_output("msg") + if msgNode is not None: + senderNode.link_output(msgNode, "msgs", "sender") + else: + self._whine("command node does not send a message?") + + if self.find_output("reenable") is not None: + tree = self.id_data + enableMsgNode = tree.nodes.new("PlasmaEnableMsgNode") + enableMsgNode.cmd = "kEnable" + if msgNode.has_callbacks: + msgNode.link_output(enableMsgNode, "msgs", "sender") + else: + senderNode.link_output(enableMsgNode, "msgs", "sender") + + fromSocket = enableMsgNode.find_output_socket("receivers") + for link in self.find_output_socket("reenable").links: + if not link.is_valid: + continue + tree.links.new(link.to_socket, fromSocket) + +@bpy.app.handlers.persistent +def _upgrade_node_trees(dummy): + for tree in bpy.data.node_groups: + if tree.bl_idname != "PlasmaNodeTree": + continue + + # ensure node sockets match what we expect + for node in tree.nodes: + node.update() + nuke = [] + # upgrade to new node types/linkages + for node in tree.nodes: + if isinstance(node, PlasmaDeprecatedNode): + node.upgrade() + nuke.append(node) + # toss deprecated nodes + for node in nuke: + tree.nodes.remove(node) +bpy.app.handlers.load_post.append(_upgrade_node_trees) diff --git a/korman/nodes/node_responder.py b/korman/nodes/node_responder.py index 646a6c8..70a21b5 100644 --- a/korman/nodes/node_responder.py +++ b/korman/nodes/node_responder.py @@ -126,6 +126,14 @@ class PlasmaResponderStateNode(PlasmaNodeBase, bpy.types.Node): ]) output_sockets = OrderedDict([ + # This socket has been deprecated. + ("cmds", { + "text": "Commands", + "type": "PlasmaRespCommandSocket", + "hidden": True, + }), + + # These sockets are valid. ("msgs", { "text": "Send Message", "type": "PlasmaMessageSocket", @@ -233,99 +241,3 @@ class PlasmaResponderStateNode(PlasmaNodeBase, bpy.types.Node): 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" - - input_sockets = OrderedDict([ - ("whodoneit", { - "text": "Condition", - "type": "PlasmaRespCommandSocket", - }), - ]) - - output_sockets = OrderedDict([ - ("msg", { - "link_limit": 1, - "text": "Message", - "type": "PlasmaMessageSocket", - }), - ("trigger", { - "text": "Trigger", - "type": "PlasmaRespCommandSocket", - }), - ("reenable", { - "text": "Local Reenable", - "type": "PlasmaEnableMessageSocket", - }), - ]) - - def convert_command(self, exporter, so, responder, commandMgr, waitOn=-1): - raise RuntimeError() - def prepare_message(exporter, so, responder, commandMgr, waitOn, msg): - idx, command = commandMgr.add_command(self, waitOn) - if msg.sender is None: - msg.sender = responder.key - msg.BCastFlags |= plMessage.kLocalPropagate - command.msg = msg - return (idx, command) - - # If this command has no message, there is no need to export it... - msgNode = self.find_output("msg") - if msgNode is not None: - # HACK: Some message nodes may need to sneakily send multiple messages. So, convert_message - # is therefore now a generator. We will ASSume that the first message generated is the - # primary msg that we should use for callbacks, if applicable - if inspect.isgeneratorfunction(msgNode.convert_message): - messages = tuple(msgNode.convert_message(exporter, so)) - msg = messages[0] - for i in messages[1:]: - prepare_message(exporter, so, responder, commandMgr, waitOn, i) - else: - msg = msgNode.convert_message(exporter, so) - idx, command = prepare_message(exporter, so, responder, commandMgr, waitOn, 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 or \ - self.find_output("reenable") is not None - if msgNode.has_callbacks and haveChildren: - childWaitOn = commandMgr.add_wait(idx) - msgNode.convert_callback_message(exporter, so, msg, responder.key, childWaitOn) - else: - childWaitOn = waitOn - else: - childWaitOn = waitOn - - # If they linked us back to a condition or something that exports a LogicModifier, that - # means we need to reenable it here... NOTE: it would be incredibly stupid to do this - # if we're not waiting on anything to complete - if childWaitOn != -1: - for child in self.find_outputs("reenable"): - key = child.get_key(exporter, so) - if key is None: - continue - logicmod = key.object - if not isinstance(logicmod, plLogicModifier): - continue - logicmod.setLogicFlag(plLogicModifier.kOneShot, True) - - # Yep, this is an entirely new ResponderCommand that sends a plEnableMsg - enableMsg = plEnableMsg() - enableMsg.addReceiver(key) - enableMsg.sender = responder.key - enableMsg.BCastFlags |= plMessage.kLocalPropagate - enableMsg.setCmd(plEnableMsg.kEnable, True) - logicCmdIdx, logicCmd = commandMgr.add_command(self, childWaitOn) - logicCmd.msg = enableMsg - - # Export any child commands - for i in self.find_outputs("trigger", "PlasmaResponderCommandNode"): - i.convert_command(exporter, so, responder, commandMgr, childWaitOn) - - -class PlasmaRespCommandSocket(PlasmaNodeSocketBase, bpy.types.NodeSocket): - bl_color = (0.451, 0.0, 0.263, 1.0)