Browse Source

Abstract away some NodeSocket bookkeeping

Also, this fixes a bug with auto-expanding input sockets. It turns out
Blender was renaming them behind the scenes. We now ensure that we ignore
this renaming -- fixes many potential gotchas.
pull/10/head
Adam Johnson 9 years ago
parent
commit
11a653f9b0
  1. 17
      korman/nodes/node_avatar.py
  2. 93
      korman/nodes/node_conditions.py
  3. 106
      korman/nodes/node_core.py
  4. 15
      korman/nodes/node_messages.py
  5. 79
      korman/nodes/node_responder.py

17
korman/nodes/node_avatar.py

@ -32,10 +32,19 @@ class PlasmaSittingBehaviorNode(PlasmaNodeBase, bpy.types.Node):
default={"kApproachFront", "kApproachLeft", "kApproachRight"},
options={"ENUM_FLAG"})
def init(self, context):
self.inputs.new("PlasmaConditionSocket", "Condition", "condition")
# This makes me determined to create and release a whoopee cushion age...
self.outputs.new("PlasmaConditionSocket", "Satisfies", "satisfies")
input_sockets = {
"condition": {
"text": "Condition",
"type": "PlasmaConditionSocket",
},
}
output_sockets = {
"satisfies": {
"text": "Satisfies",
"type": "PlasmaConditionSocket",
},
}
def draw_buttons(self, context, layout):
col = layout.column()

93
korman/nodes/node_conditions.py

@ -18,10 +18,10 @@ from bpy.props import *
import math
from PyHSPlasma import *
from .node_core import PlasmaNodeBase, PlasmaNodeSocketBase, PlasmaNodeVariableInput
from .node_core import *
from ..properties.modifiers.physics import bounds_types
class PlasmaClickableNode(PlasmaNodeVariableInput, bpy.types.Node):
class PlasmaClickableNode(PlasmaNodeBase, bpy.types.Node):
bl_category = "CONDITIONS"
bl_idname = "PlasmaClickableNode"
bl_label = "Clickable"
@ -37,12 +37,32 @@ class PlasmaClickableNode(PlasmaNodeVariableInput, bpy.types.Node):
items=bounds_types,
default="hull")
def init(self, context):
self.inputs.new("PlasmaClickableRegionSocket", "Avatar Inside Region", "region")
self.inputs.new("PlasmaFacingTargetSocket", "Avatar Facing Target", "facing")
self.inputs.new("PlasmaRespCommandSocket", "Local Reenable", "enable_callback")
self.outputs.new("PlasmaPythonReferenceNodeSocket", "References", "keyref")
self.outputs.new("PlasmaConditionSocket", "Satisfies", "satisfies")
input_sockets = {
"region": {
"text": "Avatar Inside Region",
"type": "PlasmaClickableRegionSocket",
},
"facing": {
"text": "Avatar Facing Target",
"type": "PlasmaFacingTargetSocket",
},
"enable_callback": {
"text": "Local Reenable",
"type": "PlasmaRespCommandSocket",
"spawn_empty": True,
}
}
output_sockets = {
"keyref": {
"text": "References",
"type": "PlasmaPythonReferenceNodeSocket",
},
"satisfies": {
"text": "Satisfies",
"type": "PlasmaConditionSocket",
},
}
def draw_buttons(self, context, layout):
layout.prop_search(self, "clickable", bpy.data, "objects", icon="MESH_DATA")
@ -121,9 +141,6 @@ class PlasmaClickableNode(PlasmaNodeVariableInput, bpy.types.Node):
def harvest_actors(self):
return (self.clickable,)
def update(self):
self.ensure_sockets("PlasmaRespCommandSocket", "Local Reenable", "enable_callback")
class PlasmaClickableRegionNode(PlasmaNodeBase, bpy.types.Node):
bl_category = "CONDITIONS"
@ -138,8 +155,12 @@ class PlasmaClickableRegionNode(PlasmaNodeBase, bpy.types.Node):
items=bounds_types,
default="hull")
def init(self, context):
self.outputs.new("PlasmaClickableRegionSocket", "Satisfies", "satisfies")
output_sockets = {
"satisfies": {
"text": "Satisfies",
"type": "PlasmaClickableRegionSocket",
}
}
def draw_buttons(self, context, layout):
layout.prop_search(self, "region", bpy.data, "objects", icon="MESH_DATA")
@ -200,8 +221,12 @@ class PlasmaFacingTargetNode(PlasmaNodeBase, bpy.types.Node):
description="How far away from the target the avatar can turn (in degrees)",
min=-180, max=180, default=45)
def init(self, context):
self.outputs.new("PlasmaFacingTargetSocket", "Satisfies", "satisfies")
output_sockets = {
"satisfies": {
"text": "Satisfies",
"type": "PlasmaFacingTargetSocket",
},
}
def draw_buttons(self, context, layout):
layout.prop(self, "directional")
@ -271,8 +296,13 @@ class PlasmaVolumeReportNode(PlasmaNodeBase, bpy.types.Node):
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")
output_sockets = {
"settings": {
"text": "Trigger Settings",
"type": "PlasmaVolumeSettingsSocketOut",
"valid_link_sockets": {"PlasmaVolumeSettingsSocketIn"},
},
}
def draw_buttons(self, context, layout):
layout.prop(self, "report_when")
@ -306,11 +336,30 @@ class PlasmaVolumeSensorNode(PlasmaNodeBase, bpy.types.Node):
("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("PlasmaPythonReferenceNodeSocket", "References", "keyref")
self.outputs.new("PlasmaConditionSocket", "Satisfies", "satisfies")
input_sockets = {
"enter": {
"text": "Trigger on Enter",
"type": "PlasmaVolumeSettingsSocketIn",
"valid_link_sockets": {"PlasmaVolumeSettingsSocketOut"},
},
"exit": {
"text": "Trigger on Exit",
"type": "PlasmaVolumeSettingsSocketIn",
"valid_link_sockets": {"PlasmaVolumeSettingsSocketOut"},
},
}
output_sockets = {
"keyref": {
"text": "References",
"type": "PlasmaPythonReferenceNodeSocket",
"valid_link_nodes": {"PlasmaPythonFileNode"},
},
"satisfies": {
"text": "Satisfies",
"type": "PlasmaConditionSocket",
},
}
def draw_buttons(self, context, layout):
layout.prop(self, "report_on")

106
korman/nodes/node_core.py

@ -42,7 +42,7 @@ class PlasmaNodeBase:
def find_input(self, key, idname=None):
for i in self.inputs:
if i.identifier == key:
if i.alias == key:
if i.links:
node = i.links[0].from_node
if idname is not None and idname != node.bl_idname:
@ -54,13 +54,13 @@ class PlasmaNodeBase:
def find_input_socket(self, key):
for i in self.inputs:
if i.identifier == key:
if i.alias == key:
return i
raise KeyError(key)
def find_input_sockets(self, key, idname=None):
for i in self.inputs:
if i.identifier == key:
if i.alias == key:
if idname is None:
yield i
elif i.links:
@ -70,7 +70,7 @@ class PlasmaNodeBase:
def find_output(self, key, idname=None):
for i in self.outputs:
if i.identifier == key:
if i.alias == key:
if i.links:
node = i.links[0].to_node
if idname is not None and idname != node.bl_idname:
@ -82,7 +82,7 @@ class PlasmaNodeBase:
def find_outputs(self, key, idname=None):
for i in self.outputs:
if i.identifier == key:
if i.alias == key:
for j in i.links:
node = j.to_node
if idname is not None and idname != node.bl_idname:
@ -91,13 +91,24 @@ class PlasmaNodeBase:
def find_output_socket(self, key):
for i in self.outputs:
if i.identifier == key:
if i.alias == key:
return i
raise KeyError(key)
def harvest_actors(self):
return set()
def init(self, context):
"""Initializes the sockets as defined on the subclass"""
input_defs, output_defs = self._socket_defs
for defs, sockets in ((input_defs, self.inputs), (output_defs, self.outputs)):
for name, options in defs.items():
assert name.find('.') == -1
socket = sockets.new(options["type"], options["text"], name)
link_limit = options.get("link_limit", None)
if link_limit is not None:
socket.link_limit = link_limit
@property
def key_name(self):
return "{}_{}".format(self.id_data.name, self.name)
@ -143,21 +154,84 @@ class PlasmaNodeBase:
def requires_actor(self):
return False
@property
def _socket_defs(self):
return (getattr(self.__class__, "input_sockets", {}),
getattr(self.__class__, "output_sockets", {}))
def update(self):
"""Ensures that sockets are linked appropriately and there are enough inputs"""
input_defs, output_defs = self._socket_defs
for defs, sockets in ((input_defs, self.inputs), (output_defs, self.outputs)):
done = set()
for socket in sockets:
options = defs.get(socket.alias, None)
if options is None or socket.bl_idname != options["type"]:
sockets.remove(socket)
continue
# Make sure the socket info is up to date
socket.name = options["text"]
link_limit = options.get("link_limit", None)
if link_limit is not None:
socket.link_limit = link_limit
# Make sure the link is good
allowed_sockets = options.get("valid_link_sockets", None)
allowed_nodes = options.get("valid_link_nodes", None)
# 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:
allowed_sockets = frozenset((options["type"],))
if allowed_sockets or allowed_nodes:
for link in socket.links:
if allowed_nodes:
to_from_node = link.to_node if socket.is_output else link.from_node
if to_from_node.bl_idname not in allowed_nodes:
try:
self.id_data.links.remove(link)
except RuntimeError:
# was already removed by someone else
pass
continue
if allowed_sockets:
to_from_socket = link.to_socket if socket.is_output else link.from_socket
if to_from_socket.bl_idname not in allowed_sockets:
try:
self.id_data.links.remove(link)
except RuntimeError:
# was already removed by someone else
pass
continue
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)
# If this is a multiple input node, make sure we have exactly one empty socket
if (not socket.is_output and options.get("spawn_empty", False) and not socket.alias in done):
empty_sockets = [i for i in sockets if i.bl_idname == socket.bl_idname and not i.links]
if not empty_sockets:
dbg = sockets.new(socket.bl_idname, socket.name, socket.alias)
else:
self.inputs.new(idname, name, identifier)
while len(empty) > 1:
self.inputs.remove(empty.pop())
while len(empty_sockets) > 1:
sockets.remove(empty_sockets.pop())
done.add(socket.alias)
# Create any new sockets
for alias in (i for i in defs if i 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
class PlasmaNodeSocketBase:
@property
def alias(self):
"""Blender appends .000 stuff if it's a dupe. We don't care about dupe identifiers..."""
ident = self.identifier
if ident.find('.') == -1:
return ident
return ident.rsplit('.', 1)[0]
def draw(self, context, layout, node, text):
layout.label(text)

15
korman/nodes/node_messages.py

@ -27,18 +27,19 @@ class PlasmaMessageSocket(PlasmaMessageSocketBase, bpy.types.NodeSocket):
pass
class PlasmaMessageNode(PlasmaNodeVariableInput):
class PlasmaMessageNode(PlasmaNodeBase):
input_sockets = {
"sender": {
"text": "Sender",
"type": "PlasmaMessageSocket",
},
}
@property
def has_callbacks(self):
"""This message has callbacks that can be waited on by a Responder"""
return False
def init(self, context):
self.inputs.new("PlasmaMessageSocket", "Sender", "sender")
def update(self):
self.ensure_sockets("PlasmaMessageSocket", "Sender", "sender")
class PlasmaAnimCmdMsgNode(PlasmaMessageNode, bpy.types.Node):
bl_category = "MSG"

79
korman/nodes/node_responder.py

@ -20,7 +20,7 @@ import uuid
from .node_core import *
class PlasmaResponderNode(PlasmaNodeVariableInput, bpy.types.Node):
class PlasmaResponderNode(PlasmaNodeBase, bpy.types.Node):
bl_category = "LOGIC"
bl_idname = "PlasmaResponderNode"
bl_label = "Responder"
@ -39,10 +39,25 @@ class PlasmaResponderNode(PlasmaNodeVariableInput, bpy.types.Node):
description="When fast-forwarding, play sound effects",
default=False)
def init(self, context):
self.inputs.new("PlasmaConditionSocket", "Condition", "condition")
self.outputs.new("PlasmaPythonReferenceNodeSocket", "References", "keyref")
self.outputs.new("PlasmaRespStateSocket", "States", "states")
input_sockets = {
"condition": {
"text": "Condition",
"type": "PlasmaConditionSocket",
"spawn_empty": True,
},
}
output_sockets = {
"keyref": {
"text": "References",
"type": "PlasmaPythonReferenceNodeSocket",
"valid_link_nodes": {"PlasmaPythonFileNode"},
},
"states": {
"text": "States",
"type": "PlasmaRespStateSocket",
},
}
def draw_buttons(self, context, layout):
layout.prop(self, "detect_trigger")
@ -90,11 +105,8 @@ class PlasmaResponderNode(PlasmaNodeVariableInput, bpy.types.Node):
stateNode.convert_state(exporter, so, stateMgr)
stateMgr.save()
def update(self):
self.ensure_sockets("PlasmaConditionSocket", "Condition", "condition")
class PlasmaResponderStateNode(PlasmaNodeVariableInput, bpy.types.Node):
class PlasmaResponderStateNode(PlasmaNodeBase, bpy.types.Node):
bl_category = "LOGIC"
bl_idname = "PlasmaResponderStateNode"
bl_label = "Responder State"
@ -103,10 +115,25 @@ class PlasmaResponderStateNode(PlasmaNodeVariableInput, bpy.types.Node):
description="This state is the responder's default",
default=False)
def init(self, context):
self.inputs.new("PlasmaRespStateSocket", "Condition", "condition")
self.outputs.new("PlasmaRespCommandSocket", "Commands", "cmds")
self.outputs.new("PlasmaRespStateSocket", "Trigger", "gotostate").link_limit = 1
input_sockets = {
"condition": {
"text": "Condition",
"type": "PlasmaRespStateSocket",
"spawn_empty": True,
},
}
output_sockets = {
"cmds": {
"text": "Commands",
"type": "PlasmaRespCommandSocket",
},
"gotostate": {
"link_limit": 1,
"text": "Trigger",
"type": "PlasmaRespStateSocket",
},
}
def draw_buttons(self, context, layout):
layout.prop(self, "default_state")
@ -160,9 +187,7 @@ class PlasmaResponderStateNode(PlasmaNodeVariableInput, bpy.types.Node):
commands.save(state)
def update(self):
# This actually draws nothing, but it makes sure we have at least one empty input slot
# We need this because it's possible that multiple OTHER states can call us
self.ensure_sockets("PlasmaRespStateSocket", "Condition", "condition")
super().update()
# Check to see if we're the default state
if not self.default_state:
@ -180,6 +205,28 @@ class PlasmaResponderCommandNode(PlasmaNodeBase, bpy.types.Node):
bl_idname = "PlasmaResponderCommandNode"
bl_label = "Responder Command"
input_sockets = {
"whodoneit": {
"text": "Condition",
"type": "PlasmaRespCommandSocket",
# command sockets are on some unrelated outputs...
"valid_link_nodes": {"PlasmaResponderCommandNode", "PlasmaResponderStateNode"},
"valid_link_sockets": {"PlasmaRespCommandSocket"},
},
}
output_sockets = {
"msg": {
"link_limit": 1,
"text": "Message",
"type": "PlasmaMessageSocket",
},
"trigger": {
"text": "Trigger",
"type": "PlasmaRespCommandSocket",
},
}
def init(self, context):
self.inputs.new("PlasmaRespCommandSocket", "Condition", "whodoneit")
self.outputs.new("PlasmaMessageSocket", "Message", "msg").link_limit = 1

Loading…
Cancel
Save