From b30f03eafcfdcf25747c7a920285d79ce12b305b Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Sun, 17 Jun 2018 20:33:07 -0400 Subject: [PATCH] Responder Command node upgrader/deprecate Adds a framework for dealing with deprecated logic nodes. First implementation is for Responder Commands, which hooks the messages directly onto the Responder States. Note that the old socket definitions are left alone because the upgrader will need that data. --- korman/nodes/__init__.py | 7 +- korman/nodes/node_conditions.py | 5 ++ korman/nodes/node_core.py | 41 +++++++--- korman/nodes/node_deprecated.py | 131 ++++++++++++++++++++++++++++++++ korman/nodes/node_responder.py | 104 ++----------------------- 5 files changed, 182 insertions(+), 106 deletions(-) create mode 100644 korman/nodes/node_deprecated.py 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)