diff --git a/korman/nodes/node_core.py b/korman/nodes/node_core.py index 040fe21..31c31ac 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) 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")