diff --git a/korman/nodes/node_avatar.py b/korman/nodes/node_avatar.py index 7103fa7..bf2a40e 100644 --- a/korman/nodes/node_avatar.py +++ b/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() diff --git a/korman/nodes/node_conditions.py b/korman/nodes/node_conditions.py index a632e51..ed53def 100644 --- a/korman/nodes/node_conditions.py +++ b/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") diff --git a/korman/nodes/node_core.py b/korman/nodes/node_core.py index cdc30d5..0ba3f71 100644 --- a/korman/nodes/node_core.py +++ b/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", {})) -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) - else: - self.inputs.new(idname, name, identifier) - while len(empty) > 1: - self.inputs.remove(empty.pop()) + 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 + + # 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: + 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) diff --git a/korman/nodes/node_messages.py b/korman/nodes/node_messages.py index 182c45a..83fe49c 100644 --- a/korman/nodes/node_messages.py +++ b/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" diff --git a/korman/nodes/node_responder.py b/korman/nodes/node_responder.py index 8fdcb33..8dd4e36 100644 --- a/korman/nodes/node_responder.py +++ b/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