Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 127 additions & 0 deletions korman/nodes/node_conditions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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",
},
}

Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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)

Expand Down
52 changes: 41 additions & 11 deletions korman/nodes/node_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand All @@ -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

Expand Down
Loading