diff --git a/korman/nodes/node_conditions.py b/korman/nodes/node_conditions.py index de5adcf5..1673b499 100644 --- a/korman/nodes/node_conditions.py +++ b/korman/nodes/node_conditions.py @@ -25,6 +25,7 @@ from .node_core import * from .node_deprecated import PlasmaVersionedNode from .. import idprops +from ..ui.ui_list import draw_node_list class PlasmaClickableNode(idprops.IDPropObjectMixin, PlasmaVersionedNode, bpy.types.Node): bl_category = "CONDITIONS" @@ -68,6 +69,7 @@ class PlasmaClickableNode(idprops.IDPropObjectMixin, PlasmaVersionedNode, bpy.ty "text": "Satisfies", "type": "PlasmaConditionSocket", "valid_link_sockets": {"PlasmaConditionSocket", "PlasmaPythonFileNodeSocket"}, + "validate_func": "validate_satisfy_link", }, } @@ -176,6 +178,18 @@ def upgrade(self): # Version 2 was created in Korman 0.17, no changes required. self.version = 3 + def validate_satisfy_link(self, socket: bpy.types.NodeSocket, link: bpy.types.NodeLink) -> bool: + # If any of the links are a clickable SDL node, then we can only allow that SINGLE node + # to be linked. + sdl_link_iter = ( + i for i in socket.links + if i.to_node.bl_idname == "PlasmaClickableSDLNode" + ) + first_sdl_link = next(sdl_link_iter, None) + if first_sdl_link is not None and first_sdl_link != link: + return False + return True + class PlasmaClickableRegionNode(idprops.IDPropObjectMixin, PlasmaVersionedNode, bpy.types.Node): bl_category = "CONDITIONS" @@ -255,6 +269,119 @@ def upgrade(self): self.version = 2 +class ClickableSDLValue(bpy.types.PropertyGroup): + value = IntProperty( + name="SDL Value", + description="Enable the clickable when the SDL Variable is set to this", + default=1, + options=set() + ) + + +class PlasmaClickableSDLNode(PlasmaNodeBase, bpy.types.Node): + bl_category = "CONDITIONS" + bl_idname = "PlasmaClickableSDLNode" + bl_label = "SDL Clickable" + bl_width_default = 170 + + # These can satisfy Python activator attributes just like the clickable node. + pl_attrib = {"ptAttribActivator", "ptAttribActivatorList", "ptAttribNamedActivator"} + + input_sockets: dict[str, dict[str, Any]] = { + "condition": { + "text": "Clickable Condition", + "type": "PlasmaConditionSocket", + # This could theoretically work on any activator that respects plEnableMsg being sent + # to its SceneObject. At this time, though, it might be best to limit it to clickables + # to prevent surprises. + "valid_link_nodes": {"PlasmaClickableNode"}, + }, + "variable": { + "text": "Dependends on SDL", + "type": "PlasmaSDLTriggererSocket", + }, + } + + output_sockets: dict[str, dict[str, Any]] = { + "satisfies": { + "text": "Satisfies", + "type": "PlasmaConditionSocket", + "valid_link_sockets": {"PlasmaConditionSocket", "PlasmaPythonFileNodeSocket"}, + } + } + + states = CollectionProperty(type=ClickableSDLValue) + + def init(self, context): + self.states.add() + + def draw_buttons(self, context, layout: bpy.types.UILayout): + draw_node_list( + self, + layout, + "states", + self._draw_state, + header="Enable Clickable On", + footer="Add New State" + ) + + def _draw_state(self, state, layout: bpy.types.UILayout): + layout.alert = any((state.value == i.value for i in self.states if i != state)) + layout.prop(state, "value") + + def get_key(self, exporter, so): + clickable_node = self.find_input("condition") + if clickable_node is None: + self.raise_error("Must be linked to a clickable!") + return clickable_node.get_key(exporter, so) + + def get_notify_key(self, exporter, so): + return [i.get_key(exporter, so) for i in self.find_outputs("satisfies")] + + def export(self, exporter, bo, so): + clickable_node = self.find_input("condition") + if clickable_node is None: + self.raise_error("Condition must be linked!") + + # If the artist has removed all of the state values from the node, don't export anything. + # This will (hopefully) be the least surprising option in this case. Remember: Blender + # property collections don't handle implicit truthiness correctly. + if not bool(self.states): + exporter.report.warn("No 'enabled' states specified for clickable, assuming always enabled") + return + + _, clickable_so = clickable_node._get_objects(exporter, so) + pfm = self._find_create_object(plPythonFileMod, exporter, so=clickable_so) + exported = bool(pfm.filename) + logic_key = clickable_node.get_key(exporter, clickable_so) + + # If we've already been exported, we just need to add the logic key, if it's not already + # present in the PFM's list of activators. + if exported: + extant_keys = frozenset((i.value for i in pfm.parameters if i.id == 2)) + if logic_key not in extant_keys: + self._add_py_parameter(pfm, 2, plPythonParameter.kActivator, logic_key) + else: + variable_node = self.find_input("variable") + if variable_node is None: + self.raise_error("SDL Variable must be linked!") + + pfm.filename = "xAgeSDLIntActEnabler" + self._add_py_parameter(pfm, 1, plPythonParameter.kString, variable_node.variable_name) + self._add_py_parameter( + pfm, 3, plPythonParameter.kString, ",".join(frozenset((str(i.value) for i in self.states))) + ) + # Yep, out of order, but I want to keep all of the activators at the end of the list. + self._add_py_parameter(pfm, 2, plPythonParameter.kActivator, logic_key) + + @property + def export_once(self): + clickable_node = self.find_input("condition") + if clickable_node is not None: + return clickable_node.export_once + return True + + class PlasmaClickableRegionSocket(PlasmaNodeSocketBase, bpy.types.NodeSocket): bl_color = (0.412, 0.0, 0.055, 1.0) diff --git a/korman/nodes/node_core.py b/korman/nodes/node_core.py index ed41c135..0584e7c4 100644 --- a/korman/nodes/node_core.py +++ b/korman/nodes/node_core.py @@ -35,23 +35,41 @@ def _add_py_parameter(self, pfm: plPythonFileMod, id: int, param_type: int, valu param.value = value pfm.addParameter(param) - def generate_notify_msg(self, exporter: Exporter, so: plSceneObject, socket_id: str, idname: Optional[str] = None) -> plNotifyMsg: - notify = plNotifyMsg() - notify.BCastFlags = (plMessage.kNetPropagate | plMessage.kLocalPropagate) + def iter_notify_keys( + self, + exporter: Exporter, + so: plSceneObject, + socket_id: str, + idname: Optional[str] = None + ) -> Iterator[plKey]: for i in self.find_outputs(socket_id, idname): - key = i.get_key(exporter, so) + key = i.get_notify_key(exporter, so) if key is None: - exporter.report.warn(f"'{i.bl_idname}' Node '{i.name}' doesn't expose a key. It won't be triggered by '{self.name}'!") - elif isinstance(key, tuple): - for i in key: - notify.addReceiver(key) + exporter.report.warn( + f"'{i.bl_idname}' Node '{i.name}' doesn't expose a key." + f"It won't be triggered by '{self.name}'!" + ) + elif isinstance(key, plKey): + yield key else: - notify.addReceiver(key) + for i in key: + yield i + + def generate_notify_msg(self, exporter: Exporter, so: plSceneObject, socket_id: str, idname: Optional[str] = None) -> plNotifyMsg: + notify = plNotifyMsg() + notify.BCastFlags = (plMessage.kNetPropagate | plMessage.kLocalPropagate) + for i in self.iter_notify_keys(exporter, so, socket_id, idname): + notify.addReceiver(i) return notify - def get_key(self, exporter: Exporter, so: plSceneObject) -> Optional[plKey]: + def get_key(self, exporter: Exporter, so: plSceneObject) -> Union[None, plKey, Iterable[plKey]]: return None + def get_notify_key(self, exporter: Exporter, so: plSceneObject) -> Union[None, plKey, Iterable[plKey]]: + # This is an optional safety valve to allow a node to send notifies to something other than + # itself, allowing clever proxying to other objects. + return self.get_key(exporter, so) + def get_key_name(self, single, suffix=None, bl=None, so=None): assert bl or so if single: @@ -404,6 +422,9 @@ def _update_extant_sockets(self, defs, sockets): # Make sure the link is good allowed_sockets = options.get("valid_link_sockets", None) allowed_nodes = options.get("valid_link_nodes", None) + validate_func = options.get("validate_func", None) + if validate_func is not None: + validate_func = getattr(node, validate_func) # The socket may decide it doesn't want anyone linked to it. can_link_attr = options.get("can_link", None) @@ -422,7 +443,7 @@ def _update_extant_sockets(self, defs, sockets): # 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: + if any((allowed_sockets, allowed_nodes, validate_func)): for link in socket.links: if allowed_nodes: to_from_node = link.to_node if socket.is_output else link.from_node @@ -444,6 +465,15 @@ def _update_extant_sockets(self, defs, sockets): # was already removed by someone else pass continue + if validate_func is not None: + if not validate_func(socket, link): + try: + self._tattle(socket, link, "(validation func failed)") + self.id_data.links.remove(link) + except RuntimeError: + # was already removed by someone else + pass + continue i += 1