Browse Source

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.
pull/107/head
Adam Johnson 6 years ago
parent
commit
32dac5472f
Signed by: Hoikas
GPG Key ID: 0B6515D6FF6F271E
  1. 39
      korman/nodes/node_core.py
  2. 11
      korman/nodes/node_deprecated.py
  3. 144
      korman/nodes/node_responder.py

39
korman/nodes/node_core.py

@ -113,12 +113,32 @@ class PlasmaNodeBase:
continue continue
yield node 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: for i in self.outputs:
if i.alias == key: if i.alias == key:
if spawn_empty and i.is_linked:
continue
return i return i
if spawn_empty:
return self._spawn_socket(key, options, self.outputs)
raise KeyError(key) 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): def harvest_actors(self):
return set() return set()
@ -133,7 +153,7 @@ class PlasmaNodeBase:
else: else:
in_socket = in_key in_socket = in_key
if isinstance(out_key, str): 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: else:
out_socket = out_key out_socket = out_key
link = self.id_data.links.new(in_socket, out_socket) link = self.id_data.links.new(in_socket, out_socket)
@ -145,7 +165,7 @@ class PlasmaNodeBase:
else: else:
in_socket = in_key in_socket = in_key
if isinstance(out_key, str): 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: else:
out_socket = out_key out_socket = out_key
link = self.id_data.links.new(in_socket, out_socket) link = self.id_data.links.new(in_socket, out_socket)
@ -185,6 +205,15 @@ class PlasmaNodeBase:
direction = "->" if socket.is_output else "<-" direction = "->" if socket.is_output else "<-"
print("Removing {} {} {} {}".format(link.from_node.name, direction, link.to_node.name, reason)) 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): def update(self):
"""Ensures that sockets are linked appropriately and there are enough inputs""" """Ensures that sockets are linked appropriately and there are enough inputs"""
input_defs, output_defs = self._socket_defs input_defs, output_defs = self._socket_defs
@ -255,8 +284,8 @@ class PlasmaNodeBase:
pass pass
continue continue
# If this is a multiple input node, make sure we have exactly one empty socket # If this is a spawn empty socket, 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 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] empty_sockets = [j for j in sockets if j.bl_idname == socket.bl_idname and not j.is_used]
if not empty_sockets: if not empty_sockets:
idx = len(sockets) idx = len(sockets)

11
korman/nodes/node_deprecated.py

@ -24,6 +24,15 @@ class PlasmaDeprecatedNode(PlasmaNodeBase):
raise NotImplementedError() 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): class PlasmaRespCommandSocket(PlasmaNodeSocketBase, bpy.types.NodeSocket):
bl_color = (0.451, 0.0, 0.263, 1.0) bl_color = (0.451, 0.0, 0.263, 1.0)
@ -125,6 +134,8 @@ def _upgrade_node_trees(dummy):
if isinstance(node, PlasmaDeprecatedNode): if isinstance(node, PlasmaDeprecatedNode):
node.upgrade() node.upgrade()
nuke.append(node) nuke.append(node)
elif isinstance(node, PlasmaVersionedNode):
node.upgrade()
# toss deprecated nodes # toss deprecated nodes
for node in nuke: for node in nuke:
tree.nodes.remove(node) tree.nodes.remove(node)

144
korman/nodes/node_responder.py

@ -21,8 +21,9 @@ from PyHSPlasma import *
import uuid import uuid
from .node_core import * 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_category = "LOGIC"
bl_idname = "PlasmaResponderNode" bl_idname = "PlasmaResponderNode"
bl_label = "Responder" bl_label = "Responder"
@ -40,6 +41,8 @@ class PlasmaResponderNode(PlasmaNodeBase, bpy.types.Node):
no_ff_sounds = BoolProperty(name="Don't F-Fwd Sounds", no_ff_sounds = BoolProperty(name="Don't F-Fwd Sounds",
description="When fast-forwarding, play sound effects", description="When fast-forwarding, play sound effects",
default=False) default=False)
default_state = IntProperty(name="Default State Index",
options=set())
input_sockets = OrderedDict([ input_sockets = OrderedDict([
("condition", { ("condition", {
@ -55,9 +58,22 @@ class PlasmaResponderNode(PlasmaNodeBase, bpy.types.Node):
"type": "PlasmaPythonReferenceNodeSocket", "type": "PlasmaPythonReferenceNodeSocket",
"valid_link_nodes": {"PlasmaPythonFileNode"}, "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", { ("states", {
"text": "States", "text": "States",
"type": "PlasmaRespStateSocket", "type": "PlasmaRespStateSocket",
"hidden": True,
}), }),
]) ])
@ -80,6 +96,7 @@ class PlasmaResponderNode(PlasmaNodeBase, bpy.types.Node):
responder.flags |= plResponderModifier.kDetectUnTrigger responder.flags |= plResponderModifier.kDetectUnTrigger
if self.no_ff_sounds: if self.no_ff_sounds:
responder.flags |= plResponderModifier.kSkipFFSound responder.flags |= plResponderModifier.kSkipFFSound
responder.curState = self.default_state
class ResponderStateMgr: class ResponderStateMgr:
def __init__(self, respNode, respMod): def __init__(self, respNode, respMod):
@ -87,25 +104,58 @@ class PlasmaResponderNode(PlasmaNodeBase, bpy.types.Node):
self.parent = respNode self.parent = respNode
self.responder = respMod 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): def get_state(self, node):
for idx, (theNode, theState) in enumerate(self.states): for idx, (theNode, theState) in enumerate(self.states):
if theNode == node: if theNode == node:
return (idx, theState, True) return (idx, theState)
state = plResponderModifier_State() state = plResponderModifier_State()
self.states.append((node, state)) self.states.append((node, state))
return (len(self.states) - 1, state, False) return (len(self.states) - 1, state)
def save(self): def register_state(self, node):
resp = self.responder self.states.append((node, plResponderModifier_State()))
resp.clearStates()
for node, state in self.states:
resp.addState(state)
# Convert the Responder states # Convert the Responder states
stateMgr = ResponderStateMgr(self, responder) stateMgr = ResponderStateMgr(self, responder)
for stateNode in self.find_outputs("states", "PlasmaResponderStateNode"): for stateNode in self.find_outputs("state_refs", "PlasmaResponderStateNode"):
stateNode.convert_state(exporter, so, stateMgr) stateMgr.register_state(stateNode)
stateMgr.save() 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): class PlasmaResponderStateNode(PlasmaNodeBase, bpy.types.Node):
@ -113,16 +163,45 @@ class PlasmaResponderStateNode(PlasmaNodeBase, bpy.types.Node):
bl_idname = "PlasmaResponderStateNode" bl_idname = "PlasmaResponderStateNode"
bl_label = "Responder State" 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", default_state = BoolProperty(name="Default State",
description="This state is the responder's default", description="This state is the responder's default",
default=False) get=_get_default_state,
set=_set_default_state,
options=set())
input_sockets = OrderedDict([ input_sockets = OrderedDict([
("condition", { ("condition", {
"text": "Condition", "text": "Triggers State",
"type": "PlasmaRespStateSocket", "type": "PlasmaRespStateSocket",
"spawn_empty": True, "spawn_empty": True,
}), }),
("resp", {
"text": "Responder",
"type": "PlasmaRespStateRefSocket",
"valid_link_nodes": "PlasmaResponderNode",
"valid_link_sockets": "PlasmaRespStateRefSocket",
}),
]) ])
output_sockets = OrderedDict([ output_sockets = OrderedDict([
@ -141,30 +220,23 @@ class PlasmaResponderStateNode(PlasmaNodeBase, bpy.types.Node):
}), }),
("gotostate", { ("gotostate", {
"link_limit": 1, "link_limit": 1,
"text": "Trigger", "text": "Triggers State",
"type": "PlasmaRespStateSocket", "type": "PlasmaRespStateSocket",
}), }),
]) ])
def draw_buttons(self, context, layout): def draw_buttons(self, context, layout):
layout.active = self.find_input("resp") is not None
layout.prop(self, "default_state") layout.prop(self, "default_state")
def convert_state(self, exporter, so, stateMgr): def convert_state(self, exporter, so, state, idx, 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
# Where do we go from heah? # Where do we go from heah?
toStateNode = self.find_output("gotostate", "PlasmaResponderStateNode") toStateNode = self.find_output("gotostate", "PlasmaResponderStateNode")
if toStateNode is None: if toStateNode is None:
state.switchToState = idx state.switchToState = idx
else: else:
toIdx, toState, converted = stateMgr.get_state(toStateNode) toIdx, toState = stateMgr.get_state(toStateNode)
state.switchToState = toIdx state.switchToState = toIdx
if not converted:
toStateNode.convert_state(exporter, so, stateMgr)
class CommandMgr: class CommandMgr:
def __init__(self): def __init__(self):
@ -229,15 +301,21 @@ class PlasmaResponderStateNode(PlasmaNodeBase, bpy.types.Node):
for i in msgNode.find_outputs("msgs"): for i in msgNode.find_outputs("msgs"):
self._generate_command(exporter, so, responder, commandMgr, i, childWaitOn) 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): class PlasmaRespStateSocket(PlasmaNodeSocketBase, bpy.types.NodeSocket):
bl_color = (0.388, 0.78, 0.388, 1.0) 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)

Loading…
Cancel
Save