Browse Source

Merge pull request #95 from Hoikas/responder_cmd

Deprecated Responder Command Nodes
pull/97/head
Adam Johnson 6 years ago committed by GitHub
parent
commit
6b296dd904
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 7
      korman/nodes/__init__.py
  2. 5
      korman/nodes/node_conditions.py
  3. 54
      korman/nodes/node_core.py
  4. 131
      korman/nodes/node_deprecated.py
  5. 67
      korman/nodes/node_messages.py
  6. 114
      korman/nodes/node_responder.py
  7. 4
      korman/properties/modifiers/region.py

7
korman/nodes/__init__.py

@ -22,6 +22,7 @@ import nodeitems_utils
from .node_avatar import * from .node_avatar import *
from .node_conditions import * from .node_conditions import *
from .node_core import * from .node_core import *
from .node_deprecated import *
from .node_logic import * from .node_logic import *
from .node_messages import * from .node_messages import *
from .node_python import * from .node_python import *
@ -53,7 +54,11 @@ _kategory_names = {
_kategories = {} _kategories = {}
for cls in dict(globals()).values(): for cls in dict(globals()).values():
if inspect.isclass(cls): 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 continue
else: else:
continue continue

5
korman/nodes/node_conditions.py

@ -82,6 +82,11 @@ class PlasmaClickableNode(idprops.IDPropObjectMixin, PlasmaNodeBase, bpy.types.N
interface.addIntfKey(logicmod) interface.addIntfKey(logicmod)
logicmod = logicmod.object 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.... # Try to figure out the appropriate bounds type for the clickable....
phys_mod = clickable_bo.plasma_modifiers.collision phys_mod = clickable_bo.plasma_modifiers.collision
bounds = phys_mod.bounds if phys_mod.enabled else self.bounds bounds = phys_mod.bounds if phys_mod.enabled else self.bounds

54
korman/nodes/node_core.py

@ -65,10 +65,21 @@ class PlasmaNodeBase:
if idname is None or idname == node.bl_idname: if idname is None or idname == node.bl_idname:
yield node 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: for i in self.inputs:
if i.alias == key: if i.alias == key:
if spawn_empty and i.is_linked:
continue
return i return i
if spawn_empty:
return self._spawn_socket(key, options, self.inputs)
else:
raise KeyError(key) raise KeyError(key)
def find_input_sockets(self, key, idname=None): def find_input_sockets(self, key, idname=None):
@ -118,7 +129,7 @@ class PlasmaNodeBase:
def link_input(self, node, out_key, in_key): def link_input(self, node, out_key, in_key):
"""Links a given Node's output socket to a given input socket on this Node""" """Links a given Node's output socket to a given input socket on this Node"""
if isinstance(in_key, str): 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: else:
in_socket = in_key in_socket = in_key
if isinstance(out_key, str): if isinstance(out_key, str):
@ -130,7 +141,7 @@ class PlasmaNodeBase:
def link_output(self, node, out_key, in_key): def link_output(self, node, out_key, in_key):
"""Links a given Node's input socket to a given output socket on this Node""" """Links a given Node's input socket to a given output socket on this Node"""
if isinstance(in_key, str): 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: else:
in_socket = in_key in_socket = in_key
if isinstance(out_key, str): if isinstance(out_key, str):
@ -161,6 +172,15 @@ class PlasmaNodeBase:
return (getattr(self.__class__, "input_sockets", {}), return (getattr(self.__class__, "input_sockets", {}),
getattr(self.__class__, "output_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): def _tattle(self, socket, link, reason):
direction = "->" if socket.is_output else "<-" direction = "->" if socket.is_output else "<-"
print("Removing {} {} {} {}".format(link.from_node.name, direction, link.to_node.name, reason)) print("Removing {} {} {} {}".format(link.from_node.name, direction, link.to_node.name, reason))
@ -176,6 +196,7 @@ class PlasmaNodeBase:
i = 0 i = 0
while i < len(sockets): while i < len(sockets):
socket = sockets[i] socket = sockets[i]
node = socket.node
options = defs.get(socket.alias, None) options = defs.get(socket.alias, None)
if options is None or socket.bl_idname != options["type"]: if options is None or socket.bl_idname != options["type"]:
@ -187,11 +208,27 @@ class PlasmaNodeBase:
link_limit = options.get("link_limit", None) link_limit = options.get("link_limit", None)
if link_limit is not None: if link_limit is not None:
socket.link_limit = link_limit socket.link_limit = link_limit
socket.hide = options.get("hidden", False)
socket.hide_value = options.get("hidden", False)
# Make sure the link is good # Make sure the link is good
allowed_sockets = options.get("valid_link_sockets", None) allowed_sockets = options.get("valid_link_sockets", None)
allowed_nodes = options.get("valid_link_nodes", None) allowed_nodes = options.get("valid_link_nodes", None)
# The socket may decide it doesn't want anyone linked to it.
can_link_attr = options.get("can_link", None)
if can_link_attr is not None:
can_link = getattr(node, can_link_attr)
socket.enabled = can_link
if not can_link:
for link in socket.links:
try:
self._tattle(socket, link, "(socket refused link)")
self.id_data.links.remove(link)
except RuntimeError:
# was already removed by someone else
pass
# Helpful default... If neither are set, require the link to be to the same socket type # Helpful default... If neither are set, require the link to be to the same socket type
if allowed_nodes is None and allowed_sockets is None: if allowed_nodes is None and allowed_sockets is None:
allowed_sockets = frozenset((options["type"],)) allowed_sockets = frozenset((options["type"],))
@ -237,11 +274,12 @@ class PlasmaNodeBase:
# Create any new sockets # Create any new sockets
for alias in (j for j in defs if j not in done): for alias in (j for j in defs if j not in done):
options = defs[alias] self._spawn_socket(alias, defs[alias], sockets)
socket = sockets.new(options["type"], options["text"], alias)
link_limit = options.get("link_limit", None) def _whine(self, msg, *args):
if link_limit is not None: if args:
socket.link_limit = link_limit msg = msg.format(*args)
print("'{}' Node '{}': Whinging about {}".format(self.bl_idname, self.name, msg))
class PlasmaTreeOutputNodeBase(PlasmaNodeBase): class PlasmaTreeOutputNodeBase(PlasmaNodeBase):

131
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 <http://www.gnu.org/licenses/>.
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)

67
korman/nodes/node_messages.py

@ -34,17 +34,66 @@ class PlasmaMessageNode(PlasmaNodeBase):
("sender", { ("sender", {
"text": "Sender", "text": "Sender",
"type": "PlasmaMessageSocket", "type": "PlasmaMessageSocket",
"valid_link_sockets": "PlasmaMessageSocket",
"spawn_empty": True, "spawn_empty": True,
}), }),
]) ])
@property @property
def has_callbacks(self): def has_callbacks(self):
"""This message has callbacks that can be waited on by a Responder""" """This message does not have callbacks that can be waited on by a Responder"""
return False return False
class PlasmaAnimCmdMsgNode(idprops.IDPropMixin, PlasmaMessageNode, bpy.types.Node): class PlasmaMessageWithCallbacksNode(PlasmaMessageNode):
output_sockets = OrderedDict([
("msgs", {
"can_link": "can_link_callback",
"text": "Send On Completion",
"type": "PlasmaMessageSocket",
"valid_link_sockets": "PlasmaMessageSocket",
}),
])
@property
def can_link_callback(self):
"""Determines if a callback message can be linked to this socket"""
# Node Graphs enable us to draw lots of fancy logic, unfortunately, not
# everything that can potentially be represented in a node tree can be
# exported to URU in a way that will actually work. Responder commands can
# wait on other responder commands, but the way they are executed in Plasma is
# serialized. It's really a list of commands that are executed until a wait
# is encountered. At that time, Plasma waits and resumes running the list when
# the wait callback is received.
# So what does this mean???
# It means that only one "branch" of message nodes can have waits.
def check_for_callbacks(parent_node, child_node):
for sibling_node in parent_node.find_outputs("msgs"):
if sibling_node == child_node:
continue
if getattr(sibling_node, "has_linked_callbacks", False):
return True
for grandparent_node in parent_node.find_inputs("sender"):
return check_for_callbacks(grandparent_node, parent_node)
return False
for sender_node in self.find_inputs("sender"):
if check_for_callbacks(sender_node, self):
return False
return True
@property
def has_callbacks(self):
"""This message has callbacks that can be waited on by a Responder"""
return True
@property
def has_linked_callbacks(self):
return self.find_output("msgs") is not None
class PlasmaAnimCmdMsgNode(idprops.IDPropMixin, PlasmaMessageWithCallbacksNode, bpy.types.Node):
bl_category = "MSG" bl_category = "MSG"
bl_idname = "PlasmaAnimCmdMsgNode" bl_idname = "PlasmaAnimCmdMsgNode"
bl_label = "Animation Command" bl_label = "Animation Command"
@ -407,7 +456,7 @@ class PlasmaLinkToAgeMsg(PlasmaMessageNode, bpy.types.Node):
layout.prop(self, "spawn_point") layout.prop(self, "spawn_point")
class PlasmaOneShotMsgNode(idprops.IDPropObjectMixin, PlasmaMessageNode, bpy.types.Node): class PlasmaOneShotMsgNode(idprops.IDPropObjectMixin, PlasmaMessageWithCallbacksNode, bpy.types.Node):
bl_category = "MSG" bl_category = "MSG"
bl_idname = "PlasmaOneShotMsgNode" bl_idname = "PlasmaOneShotMsgNode"
bl_label = "One Shot" bl_label = "One Shot"
@ -523,7 +572,7 @@ class PlasmaSceneObjectMsgRcvrNode(idprops.IDPropObjectMixin, PlasmaNodeBase, bp
return {"target_object": "object_name"} return {"target_object": "object_name"}
class PlasmaSoundMsgNode(idprops.IDPropObjectMixin, PlasmaMessageNode, bpy.types.Node): class PlasmaSoundMsgNode(idprops.IDPropObjectMixin, PlasmaMessageWithCallbacksNode, bpy.types.Node):
bl_category = "MSG" bl_category = "MSG"
bl_idname = "PlasmaSoundMsgNode" bl_idname = "PlasmaSoundMsgNode"
bl_label = "Sound" bl_label = "Sound"
@ -646,16 +695,12 @@ class PlasmaSoundMsgNode(idprops.IDPropObjectMixin, PlasmaMessageNode, bpy.types
layout.prop(self, "looping") layout.prop(self, "looping")
layout.prop(self, "volume") layout.prop(self, "volume")
@property
def has_callbacks(self):
return True
@classmethod @classmethod
def _idprop_mapping(cls): def _idprop_mapping(cls):
return {"emitter_object": "object_name"} return {"emitter_object": "object_name"}
class PlasmaTimerCallbackMsgNode(PlasmaMessageNode, bpy.types.Node): class PlasmaTimerCallbackMsgNode(PlasmaMessageWithCallbacksNode, bpy.types.Node):
bl_category = "MSG" bl_category = "MSG"
bl_idname = "PlasmaTimerCallbackMsgNode" bl_idname = "PlasmaTimerCallbackMsgNode"
bl_label = "Timed Callback" bl_label = "Timed Callback"
@ -677,10 +722,6 @@ class PlasmaTimerCallbackMsgNode(PlasmaMessageNode, bpy.types.Node):
msg.time = self.delay msg.time = self.delay
return msg return msg
@property
def has_callbacks(self):
return True
class PlasmaFootstepSoundMsgNode(PlasmaMessageNode, bpy.types.Node): class PlasmaFootstepSoundMsgNode(PlasmaMessageNode, bpy.types.Node):
bl_category = "MSG" bl_category = "MSG"

114
korman/nodes/node_responder.py

@ -126,9 +126,18 @@ class PlasmaResponderStateNode(PlasmaNodeBase, bpy.types.Node):
]) ])
output_sockets = OrderedDict([ output_sockets = OrderedDict([
# This socket has been deprecated.
("cmds", { ("cmds", {
"text": "Commands", "text": "Commands",
"type": "PlasmaRespCommandSocket", "type": "PlasmaRespCommandSocket",
"hidden": True,
}),
# These sockets are valid.
("msgs", {
"text": "Send Message",
"type": "PlasmaMessageSocket",
"valid_link_sockets": "PlasmaMessageSocket",
}), }),
("gotostate", { ("gotostate", {
"link_limit": 1, "link_limit": 1,
@ -182,55 +191,13 @@ class PlasmaResponderStateNode(PlasmaNodeBase, bpy.types.Node):
# Convert the commands # Convert the commands
commands = CommandMgr() commands = CommandMgr()
for i in self.find_outputs("cmds", "PlasmaResponderCommandNode"): for i in self.find_outputs("msgs"):
# slight optimization--commands attached to states can't wait on other commands # 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... # namely because it's impossible to wait on a command that doesn't exist...
i.convert_command(exporter, so, stateMgr.responder, commands) self._generate_command(exporter, so, stateMgr.responder, commands, i)
commands.save(state) commands.save(state)
def update(self): def _generate_command(self, exporter, so, responder, commandMgr, msgNode, waitOn=-1):
super().update()
# Check to see if we're the default state
if not self.default_state:
inputs = list(self.find_input_sockets("condition", "PlasmaResponderNode"))
if len(inputs) == 1:
self.default_state = True
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):
def prepare_message(exporter, so, responder, commandMgr, waitOn, msg): def prepare_message(exporter, so, responder, commandMgr, waitOn, msg):
idx, command = commandMgr.add_command(self, waitOn) idx, command = commandMgr.add_command(self, waitOn)
if msg.sender is None: if msg.sender is None:
@ -239,9 +206,6 @@ class PlasmaResponderCommandNode(PlasmaNodeBase, bpy.types.Node):
command.msg = msg command.msg = msg
return (idx, command) 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 # 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 # 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 # primary msg that we should use for callbacks, if applicable
@ -254,44 +218,26 @@ class PlasmaResponderCommandNode(PlasmaNodeBase, bpy.types.Node):
msg = msgNode.convert_message(exporter, so) msg = msgNode.convert_message(exporter, so)
idx, command = prepare_message(exporter, so, responder, commandMgr, waitOn, msg) 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 the callback message node is not properly set up for event callbacks, we don't want to
# If not, we'll export our children and tell them to not actually wait on us. if msgNode.has_callbacks and msgNode.find_output("msgs"):
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) childWaitOn = commandMgr.add_wait(idx)
msgNode.convert_callback_message(exporter, so, msg, responder.key, childWaitOn) msgNode.convert_callback_message(exporter, so, msg, responder.key, childWaitOn)
else: else:
childWaitOn = waitOn childWaitOn = waitOn
else:
childWaitOn = waitOn
# If they linked us back to a condition or something that exports a LogicModifier, that # Export any linked callback messages
# means we need to reenable it here... NOTE: it would be incredibly stupid to do this for i in msgNode.find_outputs("msgs"):
# if we're not waiting on anything to complete self._generate_command(exporter, so, responder, commandMgr, i, childWaitOn)
if childWaitOn != -1:
for child in self.find_outputs("reenable"): def update(self):
key = child.get_key(exporter, so) super().update()
if key is None:
continue # Check to see if we're the default state
logicmod = key.object if not self.default_state:
if not isinstance(logicmod, plLogicModifier): inputs = list(self.find_input_sockets("condition", "PlasmaResponderNode"))
continue if len(inputs) == 1:
logicmod.setLogicFlag(plLogicModifier.kOneShot, True) self.default_state = True
# Yep, this is an entirely new ResponderCommand that sends a plEnableMsg
enableMsg = plEnableMsg() class PlasmaRespStateSocket(PlasmaNodeSocketBase, bpy.types.NodeSocket):
enableMsg.addReceiver(key) bl_color = (0.388, 0.78, 0.388, 1.0)
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)

4
korman/properties/modifiers/region.py

@ -101,12 +101,10 @@ class PlasmaFootstepRegion(PlasmaModifierProperties, PlasmaModifierLogicWiz):
respstate = nodes.new("PlasmaResponderStateNode") respstate = nodes.new("PlasmaResponderStateNode")
respstate.link_input(respmod, "states", "condition") respstate.link_input(respmod, "states", "condition")
respstate.default_state = True respstate.default_state = True
respcmd = nodes.new("PlasmaResponderCommandNode")
respcmd.link_input(respstate, "cmds", "whodoneit")
# ArmatureEffectStateMsg # ArmatureEffectStateMsg
msg = nodes.new("PlasmaFootstepSoundMsgNode") msg = nodes.new("PlasmaFootstepSoundMsgNode")
msg.link_input(respcmd, "msg", "sender") msg.link_input(respstate, "msgs", "sender")
msg.surface = self.surface msg.surface = self.surface
@property @property

Loading…
Cancel
Save