From 32dac5472f8b9291b608e0fd9e6f11573fb0a4df Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Sat, 18 Aug 2018 19:41:39 -0400 Subject: [PATCH 1/2] Responder Nodes v2 This introduces the ability to version nodes. Responders have been bumped to version 2. The difference is that v2 responder nodes maintain a list (hopefully) all responder states on the responder itself. Each state has its own socket and displays its index for usage in python scripts. For better predictability, any states that are only linked to another state's gotostate socket will be exported at the end of the state list. --- korman/nodes/node_core.py | 39 +++++++-- korman/nodes/node_deprecated.py | 11 +++ korman/nodes/node_responder.py | 144 ++++++++++++++++++++++++-------- 3 files changed, 156 insertions(+), 38 deletions(-) diff --git a/korman/nodes/node_core.py b/korman/nodes/node_core.py index ed439fe..b0d8230 100644 --- a/korman/nodes/node_core.py +++ b/korman/nodes/node_core.py @@ -113,12 +113,32 @@ class PlasmaNodeBase: continue yield node - def find_output_socket(self, key): + def find_output_socket(self, key, spawn_empty=False): + # In the case that this socket will be used to make new output linkage, + # we might want to allow the spawning of a new output socket... :) + # This will only be done if the node's socket definitions allow it. + options = self._socket_defs[1].get(key, {}) + spawn_empty = spawn_empty and options.get("spawn_empty", False) + for i in self.outputs: if i.alias == key: + if spawn_empty and i.is_linked: + continue return i + if spawn_empty: + return self._spawn_socket(key, options, self.outputs) raise KeyError(key) + def find_output_sockets(self, key, idname=None): + for i in self.outputs: + if i.alias == key: + if idname is None: + yield i + elif i.links: + node = i.links[0].from_node + if idname == node.bl_idname: + yield i + def harvest_actors(self): return set() @@ -133,7 +153,7 @@ class PlasmaNodeBase: else: in_socket = in_key if isinstance(out_key, str): - out_socket = node.find_output_socket(out_key) + out_socket = node.find_output_socket(out_key, spawn_empty=True) else: out_socket = out_key link = self.id_data.links.new(in_socket, out_socket) @@ -145,7 +165,7 @@ class PlasmaNodeBase: else: in_socket = in_key if isinstance(out_key, str): - out_socket = self.find_output_socket(out_key) + out_socket = self.find_output_socket(out_key, spawn_empty=True) else: out_socket = out_key link = self.id_data.links.new(in_socket, out_socket) @@ -185,6 +205,15 @@ class PlasmaNodeBase: direction = "->" if socket.is_output else "<-" print("Removing {} {} {} {}".format(link.from_node.name, direction, link.to_node.name, reason)) + def unlink_outputs(self, alias, reason=None): + links = self.id_data.links + from_socket = next((i for i in self.outputs if i.alias == alias)) + i = 0 + while i < len(from_socket.links): + link = from_socket.links[i] + self._tattle(from_socket, link, reason if reason else "socket unlinked") + links.remove(link) + def update(self): """Ensures that sockets are linked appropriately and there are enough inputs""" input_defs, output_defs = self._socket_defs @@ -255,8 +284,8 @@ class PlasmaNodeBase: 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): + # If this is a spawn empty socket, make sure we have exactly one empty socket + if options.get("spawn_empty", False) and not socket.alias in done: empty_sockets = [j for j in sockets if j.bl_idname == socket.bl_idname and not j.is_used] if not empty_sockets: idx = len(sockets) diff --git a/korman/nodes/node_deprecated.py b/korman/nodes/node_deprecated.py index 4621768..7743cfe 100644 --- a/korman/nodes/node_deprecated.py +++ b/korman/nodes/node_deprecated.py @@ -24,6 +24,15 @@ class PlasmaDeprecatedNode(PlasmaNodeBase): raise NotImplementedError() +class PlasmaVersionedNode(PlasmaNodeBase): + @classmethod + def register(cls): + cls.version = IntProperty(name="Node Version", default=1, options=set()) + + def upgrade(self, from_version): + raise NotImplementedError() + + class PlasmaRespCommandSocket(PlasmaNodeSocketBase, bpy.types.NodeSocket): bl_color = (0.451, 0.0, 0.263, 1.0) @@ -125,6 +134,8 @@ def _upgrade_node_trees(dummy): if isinstance(node, PlasmaDeprecatedNode): node.upgrade() nuke.append(node) + elif isinstance(node, PlasmaVersionedNode): + node.upgrade() # toss deprecated nodes for node in nuke: tree.nodes.remove(node) diff --git a/korman/nodes/node_responder.py b/korman/nodes/node_responder.py index 70a21b5..09a7c9a 100644 --- a/korman/nodes/node_responder.py +++ b/korman/nodes/node_responder.py @@ -21,8 +21,9 @@ from PyHSPlasma import * import uuid from .node_core import * +from .node_deprecated import PlasmaVersionedNode -class PlasmaResponderNode(PlasmaNodeBase, bpy.types.Node): +class PlasmaResponderNode(PlasmaVersionedNode, bpy.types.Node): bl_category = "LOGIC" bl_idname = "PlasmaResponderNode" bl_label = "Responder" @@ -40,6 +41,8 @@ class PlasmaResponderNode(PlasmaNodeBase, bpy.types.Node): no_ff_sounds = BoolProperty(name="Don't F-Fwd Sounds", description="When fast-forwarding, play sound effects", default=False) + default_state = IntProperty(name="Default State Index", + options=set()) input_sockets = OrderedDict([ ("condition", { @@ -55,9 +58,22 @@ class PlasmaResponderNode(PlasmaNodeBase, bpy.types.Node): "type": "PlasmaPythonReferenceNodeSocket", "valid_link_nodes": {"PlasmaPythonFileNode"}, }), + ("state_refs", { + "text": "State", + "type": "PlasmaRespStateRefSocket", + "valid_link_nodes": "PlasmaResponderStateNode", + "valid_link_sockets": "PlasmaRespStateRefSocket", + "link_limit": 1, + "spawn_empty": True, + }), + + # This version of the states socket has been deprecated. + # We need to be able to track 1 socket -> 1 state to manage + # responder state IDs ("states", { "text": "States", "type": "PlasmaRespStateSocket", + "hidden": True, }), ]) @@ -80,6 +96,7 @@ class PlasmaResponderNode(PlasmaNodeBase, bpy.types.Node): responder.flags |= plResponderModifier.kDetectUnTrigger if self.no_ff_sounds: responder.flags |= plResponderModifier.kSkipFFSound + responder.curState = self.default_state class ResponderStateMgr: def __init__(self, respNode, respMod): @@ -87,25 +104,58 @@ class PlasmaResponderNode(PlasmaNodeBase, bpy.types.Node): self.parent = respNode self.responder = respMod + def convert_states(self, exporter, so): + # This could implicitly export more states... + i = 0 + while i < len(self.states): + node, state = self.states[i] + node.convert_state(exporter, so, state, i, self) + i += 1 + + resp = self.responder + resp.clearStates() + for node, state in self.states: + resp.addState(state) + def get_state(self, node): for idx, (theNode, theState) in enumerate(self.states): if theNode == node: - return (idx, theState, True) + return (idx, theState) state = plResponderModifier_State() self.states.append((node, state)) - return (len(self.states) - 1, state, False) + return (len(self.states) - 1, state) - def save(self): - resp = self.responder - resp.clearStates() - for node, state in self.states: - resp.addState(state) + def register_state(self, node): + self.states.append((node, plResponderModifier_State())) # Convert the Responder states stateMgr = ResponderStateMgr(self, responder) - for stateNode in self.find_outputs("states", "PlasmaResponderStateNode"): - stateNode.convert_state(exporter, so, stateMgr) - stateMgr.save() + for stateNode in self.find_outputs("state_refs", "PlasmaResponderStateNode"): + stateMgr.register_state(stateNode) + stateMgr.convert_states(exporter, so) + + def upgrade(self): + # In version 1 responder nodes, responder states could be linked to the responder + # or to subsequent responder state nodes and be exported. The problem with this + # is that to use responder states in Python attributes, we need to be able to + # inform the user as to what the ID of the responder state will be. + # Version 2 make it slightly more mandatory that states be linked to a responder + # and will display the ID of each state linked to the responder. Any states only + # linked to other states will be converted at the end of the list. + if self.version == 1: + states = set() + def _link_states(state): + if state in states: + return + states.add(state) + self.link_output(state, "state_refs", "resp") + goto = state.find_output("gotostate") + if goto is not None: + _link_states(goto) + for i in self.find_outputs("states"): + _link_states(i) + self.unlink_outputs("states", "socket deprecated (upgrade complete)") + self.version = 2 class PlasmaResponderStateNode(PlasmaNodeBase, bpy.types.Node): @@ -113,16 +163,45 @@ class PlasmaResponderStateNode(PlasmaNodeBase, bpy.types.Node): bl_idname = "PlasmaResponderStateNode" bl_label = "Responder State" + def _get_default_state(self): + resp_node = self.find_input("resp") + if resp_node is not None: + try: + state_idx = next((idx for idx, node in enumerate(resp_node.find_outputs("state_refs")) if node == self)) + except StopIteration: + return False + else: + return resp_node.default_state == state_idx + return False + def _set_default_state(self, value): + if value: + resp_node = self.find_input("resp") + if resp_node is not None: + try: + state_idx = next((idx for idx, node in enumerate(resp_node.find_outputs("state_refs")) if node == self)) + except StopIteration: + self._whine("unable to set default state on responder") + else: + resp_node.default_state = state_idx + default_state = BoolProperty(name="Default State", description="This state is the responder's default", - default=False) + get=_get_default_state, + set=_set_default_state, + options=set()) input_sockets = OrderedDict([ ("condition", { - "text": "Condition", + "text": "Triggers State", "type": "PlasmaRespStateSocket", "spawn_empty": True, }), + ("resp", { + "text": "Responder", + "type": "PlasmaRespStateRefSocket", + "valid_link_nodes": "PlasmaResponderNode", + "valid_link_sockets": "PlasmaRespStateRefSocket", + }), ]) output_sockets = OrderedDict([ @@ -141,30 +220,23 @@ class PlasmaResponderStateNode(PlasmaNodeBase, bpy.types.Node): }), ("gotostate", { "link_limit": 1, - "text": "Trigger", + "text": "Triggers State", "type": "PlasmaRespStateSocket", }), ]) def draw_buttons(self, context, layout): + layout.active = self.find_input("resp") is not None layout.prop(self, "default_state") - def convert_state(self, exporter, so, stateMgr): - idx, state, converted = stateMgr.get_state(self) - - # No sanity checking here. Hopefully nothing crazy has happened in the UI. - if self.default_state: - stateMgr.responder.curState = idx - + def convert_state(self, exporter, so, state, idx, stateMgr): # Where do we go from heah? toStateNode = self.find_output("gotostate", "PlasmaResponderStateNode") if toStateNode is None: state.switchToState = idx else: - toIdx, toState, converted = stateMgr.get_state(toStateNode) + toIdx, toState = stateMgr.get_state(toStateNode) state.switchToState = toIdx - if not converted: - toStateNode.convert_state(exporter, so, stateMgr) class CommandMgr: def __init__(self): @@ -229,15 +301,21 @@ class PlasmaResponderStateNode(PlasmaNodeBase, bpy.types.Node): for i in msgNode.find_outputs("msgs"): self._generate_command(exporter, so, responder, commandMgr, i, childWaitOn) - def update(self): - 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 PlasmaRespStateRefSocket(PlasmaNodeSocketBase, bpy.types.NodeSocket): + bl_color = (1.00, 0.980, 0.322, 1.0) + + def draw(self, context, layout, node, text): + if isinstance(node, PlasmaResponderNode): + try: + idx = next((idx for idx, socket in enumerate(node.find_output_sockets("state_refs")) if socket == self)) + except StopIteration: + layout.label(text) + else: + layout.label("State (ID: {})".format(idx)) + else: + layout.label(text) From 9f99e1974dc422bf911eb06e163eca7185882ffc Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Thu, 23 Aug 2018 21:23:57 -0400 Subject: [PATCH 2/2] Update footstep regions for respv2 --- korman/properties/modifiers/region.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/korman/properties/modifiers/region.py b/korman/properties/modifiers/region.py index 99cde54..3377f32 100644 --- a/korman/properties/modifiers/region.py +++ b/korman/properties/modifiers/region.py @@ -99,8 +99,7 @@ class PlasmaFootstepRegion(PlasmaModifierProperties, PlasmaModifierLogicWiz): respmod.name = "Resp" respmod.link_input(volsens, "satisfies", "condition") respstate = nodes.new("PlasmaResponderStateNode") - respstate.link_input(respmod, "states", "condition") - respstate.default_state = True + respstate.link_input(respmod, "state_refs", "resp") # ArmatureEffectStateMsg msg = nodes.new("PlasmaFootstepSoundMsgNode")