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"}, default={"kApproachFront", "kApproachLeft", "kApproachRight"},
options={"ENUM_FLAG"}) options={"ENUM_FLAG"})
def init(self, context): input_sockets = {
self.inputs.new("PlasmaConditionSocket", "Condition", "condition") "condition": {
# This makes me determined to create and release a whoopee cushion age... "text": "Condition",
self.outputs.new("PlasmaConditionSocket", "Satisfies", "satisfies") "type": "PlasmaConditionSocket",
},
}
output_sockets = {
"satisfies": {
"text": "Satisfies",
"type": "PlasmaConditionSocket",
},
}
def draw_buttons(self, context, layout): def draw_buttons(self, context, layout):
col = layout.column() col = layout.column()

93
korman/nodes/node_conditions.py

@ -18,10 +18,10 @@ from bpy.props import *
import math import math
from PyHSPlasma import * from PyHSPlasma import *
from .node_core import PlasmaNodeBase, PlasmaNodeSocketBase, PlasmaNodeVariableInput from .node_core import *
from ..properties.modifiers.physics import bounds_types from ..properties.modifiers.physics import bounds_types
class PlasmaClickableNode(PlasmaNodeVariableInput, bpy.types.Node): class PlasmaClickableNode(PlasmaNodeBase, bpy.types.Node):
bl_category = "CONDITIONS" bl_category = "CONDITIONS"
bl_idname = "PlasmaClickableNode" bl_idname = "PlasmaClickableNode"
bl_label = "Clickable" bl_label = "Clickable"
@ -37,12 +37,32 @@ class PlasmaClickableNode(PlasmaNodeVariableInput, bpy.types.Node):
items=bounds_types, items=bounds_types,
default="hull") default="hull")
def init(self, context): input_sockets = {
self.inputs.new("PlasmaClickableRegionSocket", "Avatar Inside Region", "region") "region": {
self.inputs.new("PlasmaFacingTargetSocket", "Avatar Facing Target", "facing") "text": "Avatar Inside Region",
self.inputs.new("PlasmaRespCommandSocket", "Local Reenable", "enable_callback") "type": "PlasmaClickableRegionSocket",
self.outputs.new("PlasmaPythonReferenceNodeSocket", "References", "keyref") },
self.outputs.new("PlasmaConditionSocket", "Satisfies", "satisfies") "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): def draw_buttons(self, context, layout):
layout.prop_search(self, "clickable", bpy.data, "objects", icon="MESH_DATA") 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): def harvest_actors(self):
return (self.clickable,) return (self.clickable,)
def update(self):
self.ensure_sockets("PlasmaRespCommandSocket", "Local Reenable", "enable_callback")
class PlasmaClickableRegionNode(PlasmaNodeBase, bpy.types.Node): class PlasmaClickableRegionNode(PlasmaNodeBase, bpy.types.Node):
bl_category = "CONDITIONS" bl_category = "CONDITIONS"
@ -138,8 +155,12 @@ class PlasmaClickableRegionNode(PlasmaNodeBase, bpy.types.Node):
items=bounds_types, items=bounds_types,
default="hull") default="hull")
def init(self, context): output_sockets = {
self.outputs.new("PlasmaClickableRegionSocket", "Satisfies", "satisfies") "satisfies": {
"text": "Satisfies",
"type": "PlasmaClickableRegionSocket",
}
}
def draw_buttons(self, context, layout): def draw_buttons(self, context, layout):
layout.prop_search(self, "region", bpy.data, "objects", icon="MESH_DATA") 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)", description="How far away from the target the avatar can turn (in degrees)",
min=-180, max=180, default=45) min=-180, max=180, default=45)
def init(self, context): output_sockets = {
self.outputs.new("PlasmaFacingTargetSocket", "Satisfies", "satisfies") "satisfies": {
"text": "Satisfies",
"type": "PlasmaFacingTargetSocket",
},
}
def draw_buttons(self, context, layout): def draw_buttons(self, context, layout):
layout.prop(self, "directional") 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", description="How many objects should be in the region for it to trigger",
min=1) min=1)
def init(self, context): output_sockets = {
self.outputs.new("PlasmaVolumeSettingsSocketOut", "Trigger Settings") "settings": {
"text": "Trigger Settings",
"type": "PlasmaVolumeSettingsSocketOut",
"valid_link_sockets": {"PlasmaVolumeSettingsSocketIn"},
},
}
def draw_buttons(self, context, layout): def draw_buttons(self, context, layout):
layout.prop(self, "report_when") 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)")], ("dynamics", "Dynamics", "Any non-avatar dynamic physical object (eg kickables)")],
default={"avatar"}) default={"avatar"})
def init(self, context): input_sockets = {
self.inputs.new("PlasmaVolumeSettingsSocketIn", "Trigger on Enter", "enter") "enter": {
self.inputs.new("PlasmaVolumeSettingsSocketIn", "Trigger on Exit", "exit") "text": "Trigger on Enter",
self.outputs.new("PlasmaPythonReferenceNodeSocket", "References", "keyref") "type": "PlasmaVolumeSettingsSocketIn",
self.outputs.new("PlasmaConditionSocket", "Satisfies", "satisfies") "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): def draw_buttons(self, context, layout):
layout.prop(self, "report_on") layout.prop(self, "report_on")

106
korman/nodes/node_core.py

@ -42,7 +42,7 @@ class PlasmaNodeBase:
def find_input(self, key, idname=None): def find_input(self, key, idname=None):
for i in self.inputs: for i in self.inputs:
if i.identifier == key: if i.alias == key:
if i.links: if i.links:
node = i.links[0].from_node node = i.links[0].from_node
if idname is not None and idname != node.bl_idname: if idname is not None and idname != node.bl_idname:
@ -54,13 +54,13 @@ class PlasmaNodeBase:
def find_input_socket(self, key): def find_input_socket(self, key):
for i in self.inputs: for i in self.inputs:
if i.identifier == key: if i.alias == key:
return i return i
raise KeyError(key) raise KeyError(key)
def find_input_sockets(self, key, idname=None): def find_input_sockets(self, key, idname=None):
for i in self.inputs: for i in self.inputs:
if i.identifier == key: if i.alias == key:
if idname is None: if idname is None:
yield i yield i
elif i.links: elif i.links:
@ -70,7 +70,7 @@ class PlasmaNodeBase:
def find_output(self, key, idname=None): def find_output(self, key, idname=None):
for i in self.outputs: for i in self.outputs:
if i.identifier == key: if i.alias == key:
if i.links: if i.links:
node = i.links[0].to_node node = i.links[0].to_node
if idname is not None and idname != node.bl_idname: if idname is not None and idname != node.bl_idname:
@ -82,7 +82,7 @@ class PlasmaNodeBase:
def find_outputs(self, key, idname=None): def find_outputs(self, key, idname=None):
for i in self.outputs: for i in self.outputs:
if i.identifier == key: if i.alias == key:
for j in i.links: for j in i.links:
node = j.to_node node = j.to_node
if idname is not None and idname != node.bl_idname: if idname is not None and idname != node.bl_idname:
@ -91,13 +91,24 @@ class PlasmaNodeBase:
def find_output_socket(self, key): def find_output_socket(self, key):
for i in self.outputs: for i in self.outputs:
if i.identifier == key: if i.alias == key:
return i return i
raise KeyError(key) raise KeyError(key)
def harvest_actors(self): def harvest_actors(self):
return set() 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 @property
def key_name(self): def key_name(self):
return "{}_{}".format(self.id_data.name, self.name) return "{}_{}".format(self.id_data.name, self.name)
@ -143,21 +154,84 @@ class PlasmaNodeBase:
def requires_actor(self): def requires_actor(self):
return False 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): # If this is a multiple input node, make sure we have exactly one empty socket
def ensure_sockets(self, idname, name, identifier=None): if (not socket.is_output and options.get("spawn_empty", False) and not socket.alias in done):
"""Ensures there is one (and only one) empty input socket""" empty_sockets = [i for i in sockets if i.bl_idname == socket.bl_idname and not i.links]
empty = [i for i in self.inputs if i.bl_idname == idname and not i.links] if not empty_sockets:
if not empty: dbg = sockets.new(socket.bl_idname, socket.name, socket.alias)
if identifier is None:
self.inputs.new(idname, name)
else: else:
self.inputs.new(idname, name, identifier) while len(empty_sockets) > 1:
while len(empty) > 1: sockets.remove(empty_sockets.pop())
self.inputs.remove(empty.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: 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): def draw(self, context, layout, node, text):
layout.label(text) layout.label(text)

15
korman/nodes/node_messages.py

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

79
korman/nodes/node_responder.py

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

Loading…
Cancel
Save