Browse Source

Make node trees reusable.

Previously, reusing node trees required careful thought and
specification of specific nodes in the advanced logic modifier. This
tedious requirement has been removed in favor of ensuring that the nodes
themselves take into account whether or not they should generate and
attach plasma objects.
pull/156/head
Adam Johnson 5 years ago
parent
commit
9f450d0bdb
Signed by: Hoikas
GPG Key ID: 0B6515D6FF6F271E
  1. 14
      korman/exporter/convert.py
  2. 4
      korman/nodes/node_avatar.py
  3. 54
      korman/nodes/node_conditions.py
  4. 48
      korman/nodes/node_core.py
  5. 6
      korman/nodes/node_logic.py
  6. 7
      korman/nodes/node_messages.py
  7. 7
      korman/nodes/node_python.py
  8. 7
      korman/nodes/node_responder.py
  9. 14
      korman/nodes/node_softvolume.py
  10. 19
      korman/properties/modifiers/logic.py
  11. 5
      korman/properties/modifiers/region.py
  12. 15
      korman/ui/modifiers/logic.py

14
korman/exporter/convert.py

@ -38,8 +38,8 @@ class Exporter:
self._op = op # Blender export operator
self._objects = []
self.actors = set()
self.node_trees_exported = set()
self.want_node_trees = {}
self.exported_nodes = {}
def run(self):
log = logger.ExportVerboseLogger if self._op.verbose else logger.ExportProgressLogger
@ -276,13 +276,11 @@ class Exporter:
inc_progress = self.report.progress_increment
self.report.msg("\nChecking Logic Trees...")
need_to_export = [(name, bo, so) for name, (bo, so) in self.want_node_trees.items()
if name not in self.node_trees_exported]
self.report.progress_value = len(self.want_node_trees) - len(need_to_export)
for tree, bo, so in need_to_export:
self.report.msg("NodeTree '{}'", tree, indent=1)
bpy.data.node_groups[tree].export(self, bo, so)
for tree_name, references in self.want_node_trees.items():
self.report.msg("NodeTree '{}'", tree_name, indent=1)
tree = bpy.data.node_groups[tree_name]
for bo, so in references:
tree.export(self, bo, so)
inc_progress()
def _harvest_actors(self):

4
korman/nodes/node_avatar.py

@ -56,10 +56,10 @@ class PlasmaSittingBehaviorNode(PlasmaNodeBase, bpy.types.Node):
layout.prop_menu_enum(self, "approach")
def get_key(self, exporter, so):
return exporter.mgr.find_create_key(plSittingModifier, name=self.key_name, so=so)
return self._find_create_key(plSittingModifier, exporter, so=so)
def export(self, exporter, bo, so):
sitmod = self.get_key(exporter, so).object
sitmod = self._find_create_object(plSittingModifier, exporter, so=so)
for flag in self.approach:
sitmod.miscFlags |= getattr(plSittingModifier, flag)
for i in self.find_outputs("satisfies"):

54
korman/nodes/node_conditions.py

@ -74,9 +74,8 @@ class PlasmaClickableNode(idprops.IDPropObjectMixin, PlasmaNodeBase, bpy.types.N
if clickable_bo is None:
clickable_bo = parent_bo
name = self.key_name
interface = exporter.mgr.find_create_key(plInterfaceInfoModifier, name=name, so=clickable_so).object
logicmod = exporter.mgr.find_create_key(plLogicModifier, name=name, so=clickable_so)
interface = self._find_create_object(plInterfaceInfoModifier, exporter, bl=clickable_bo, so=clickable_so)
logicmod = self._find_create_key(plLogicModifier, exporter, bl=clickable_bo, so=clickable_so)
interface.addIntfKey(logicmod)
# Matches data seen in Cyan's PRPs...
interface.addIntfKey(logicmod)
@ -105,11 +104,11 @@ class PlasmaClickableNode(idprops.IDPropObjectMixin, PlasmaNodeBase, bpy.types.N
physical.LOSDBs |= plSimDefs.kLOSDBUIItems
# Picking Detector -- detect when the physical is clicked
detector = exporter.mgr.find_create_key(plPickingDetector, name=name, so=clickable_so).object
detector = self._find_create_object(plPickingDetector, exporter, bl=clickable_bo, so=clickable_so)
detector.addReceiver(logicmod.key)
# Clickable
activator = exporter.mgr.find_create_key(plActivatorConditionalObject, name=name, so=clickable_so).object
activator = self._find_create_object(plActivatorConditionalObject, exporter, bl=clickable_bo, so=clickable_so)
activator.addActivator(detector.key)
logicmod.addCondition(activator.key)
logicmod.setLogicFlag(plLogicModifier.kLocalElement, True)
@ -125,10 +124,14 @@ class PlasmaClickableNode(idprops.IDPropObjectMixin, PlasmaNodeBase, bpy.types.N
face_target = self.find_input_socket("facing")
face_target.convert_subcondition(exporter, clickable_bo, clickable_so, logicmod)
@property
def export_once(self):
return self.clickable_object is not None
def get_key(self, exporter, parent_so):
# careful... we really make lots of keys...
clickable_bo, clickable_so = self._get_objects(exporter, parent_so)
key = exporter.mgr.find_create_key(plLogicModifier, name=self.key_name, so=clickable_so)
key = self._find_create_key(plLogicModifier, exporter, bl=clickable_bo, so=clickable_so)
return key
def _get_objects(self, exporter, parent_so):
@ -185,7 +188,7 @@ class PlasmaClickableRegionNode(idprops.IDPropObjectMixin, PlasmaNodeBase, bpy.t
region_bo = self.region_object
if region_bo is None:
self.raise_error("invalid Region")
region_so = exporter.mgr.find_create_key(plSceneObject, bl=region_bo).object
region_so = exporter.mgr.find_create_object(plSceneObject, bl=region_bo)
# Try to figure out the appropriate bounds type for the region....
phys_mod = region_bo.plasma_modifiers.collision
@ -202,15 +205,13 @@ class PlasmaClickableRegionNode(idprops.IDPropObjectMixin, PlasmaNodeBase, bpy.t
# one detector for many unrelated logic mods. However, LogicMods and Conditions appear to
# assume they pwn each other... so we need a unique detector. This detector must be attached
# as a modifier to the region's SO however.
name = self.key_name
detector_key = exporter.mgr.find_create_key(plObjectInVolumeDetector, name=name, so=region_so)
detector = detector_key.object
detector = self._find_create_object(plObjectInVolumeDetector, exporter, bl=region_bo, so=region_so)
detector.addReceiver(logicmod.key)
detector.type = plObjectInVolumeDetector.kTypeAny
# Now, the conditional object. At this point, these seem very silly. At least it's not a plModifier.
# All they really do is hold a satisfied boolean...
objinbox_key = exporter.mgr.find_create_key(plObjectInBoxConditionalObject, name=name, so=parent_so)
objinbox_key = self._find_create_key(plObjectInBoxConditionalObject, exporter, bl=region_bo, so=parent_so)
objinbox_key.object.satisfied = True
logicmod.addCondition(objinbox_key)
@ -270,19 +271,18 @@ class PlasmaFacingTargetSocket(PlasmaNodeSocketBase, bpy.types.NodeSocket):
# First, gather the schtuff from the appropriate blah blah blah
if self.simple_mode:
node = self.node
directional = True
tolerance = 45
name = "{}_SimpleFacing".format(self.node.key_name)
elif self.is_linked:
node = self.links[0].from_node
directional = node.directional
tolerance = node.tolerance
name = node.key_name
else:
# This is a programmer failure, so we need a traceback.
raise RuntimeError("Tried to export an unused PlasmaFacingTargetSocket")
facing_key = exporter.mgr.find_create_key(plFacingConditionalObject, name=name, so=so)
facing_key = node._find_create_key(plFacingConditionalObject, exporter, bl=bo, so=so)
facing = facing_key.object
facing.directional = directional
facing.satisfied = True
@ -395,13 +395,12 @@ class PlasmaVolumeSensorNode(idprops.IDPropObjectMixin, PlasmaNodeBase, bpy.type
self.raise_error("Region cannot be empty")
so = exporter.mgr.find_create_object(plSceneObject, bl=bo)
rgn_enter, rgn_exit = None, None
parent_key = parent_so.key
if self.report_enters:
theName = "{}_{}_Enter".format(self.id_data.name, self.name)
rgn_enter = exporter.mgr.find_create_key(plLogicModifier, name=theName, so=so)
rgn_enter = self._find_create_key(plLogicModifier, exporter, suffix="Enter", bl=bo, so=so)
if self.report_exits:
theName = "{}_{}_Exit".format(self.id_data.name, self.name)
rgn_exit = exporter.mgr.find_create_key(plLogicModifier, name=theName, so=so)
rgn_exit = self._find_create_key(plLogicModifier, exporter, suffix="Exit", bl=bo, so=so)
if rgn_enter is None:
return rgn_exit
@ -415,12 +414,12 @@ class PlasmaVolumeSensorNode(idprops.IDPropObjectMixin, PlasmaNodeBase, bpy.type
return (rgn_enter, rgn_exit)
def export(self, exporter, bo, parent_so):
# We need to ensure we export to the correct SO
region_bo = self.region_object
if region_bo is None:
self.raise_error("Region cannot be empty")
region_so = exporter.mgr.find_create_object(plSceneObject, bl=region_bo)
interface = exporter.mgr.find_create_object(plInterfaceInfoModifier, name=self.key_name, so=region_so)
interface = self._find_create_object(plInterfaceInfoModifier, exporter, bl=region_bo, so=region_so)
# Region Enters
enter_simple = self.find_input_socket("enter").allow
@ -452,20 +451,15 @@ class PlasmaVolumeSensorNode(idprops.IDPropObjectMixin, PlasmaNodeBase, bpy.type
else:
suffix = "Exit"
theName = "{}_{}_{}".format(self.id_data.name, self.name, suffix)
exporter.report.msg("[LogicModifier '{}']", theName, indent=2)
logicKey = exporter.mgr.find_create_key(plLogicModifier, name=theName, so=so)
logicKey = self._find_create_key(plLogicModifier, exporter, suffix=suffix, bl=bo, so=so)
logicmod = logicKey.object
logicmod.setLogicFlag(plLogicModifier.kMultiTrigger, True)
logicmod.notify = self.generate_notify_msg(exporter, so, "satisfies")
# Now, the detector objects
exporter.report.msg("[ObjectInVolumeDetector '{}']", theName, indent=2)
detKey = exporter.mgr.find_create_key(plObjectInVolumeDetector, name=theName, so=so)
det = detKey.object
det = self._find_create_object(plObjectInVolumeDetector, exporter, suffix=suffix, bl=bo, so=so)
exporter.report.msg("[VolumeSensorConditionalObject '{}']", theName, indent=2)
volKey = exporter.mgr.find_create_key(plVolumeSensorConditionalObject, name=theName, so=so)
volKey = self._find_create_key(plVolumeSensorConditionalObject, exporter, suffix=suffix, bl=bo, so=so)
volsens = volKey.object
volsens.type = event
@ -483,6 +477,10 @@ class PlasmaVolumeSensorNode(idprops.IDPropObjectMixin, PlasmaNodeBase, bpy.type
logicmod.addCondition(volKey)
return logicKey
@property
def export_once(self):
return True
@classmethod
def _idprop_mapping(cls):
return {"region_object": "region"}

48
korman/nodes/node_core.py

@ -15,7 +15,7 @@
import abc
import bpy
from PyHSPlasma import plMessage, plNotifyMsg
from PyHSPlasma import *
from ..exporter import ExportError
@ -37,6 +37,20 @@ class PlasmaNodeBase:
def get_key(self, exporter, so):
return None
def get_key_name(self, single, suffix=None, bl=None, so=None):
assert bl or so
if single:
name = bl.name if bl is not None else so.key.name
if suffix:
return "{}_{}_{}_{}".format(name, self.id_data.name, self.name, suffix)
else:
return "{}_{}_{}".format(name, self.id_data.name, self.name)
else:
if suffix:
return "{}_{}_{}".format(self.id_data.name, self.name, suffix)
else:
return "{}_{}".format(self.id_data.name, self.name)
def draw_label(self):
if hasattr(self, "pl_label_attr") and self.hide:
return str(getattr(self, self.pl_label_attrib, self.bl_label))
@ -45,6 +59,27 @@ class PlasmaNodeBase:
def export(self, exporter, bo, so):
pass
@property
def export_once(self):
"""This node can only be exported once because it is a targeted plSingleModifier"""
return False
def _find_create_object(self, pClass, exporter, **kwargs):
"""Finds or creates an hsKeyedObject specific to this node."""
assert "name" not in kwargs
kwargs["name"] = self.get_key_name(issubclass(pClass, (plObjInterface, plSingleModifier)),
kwargs.pop("suffix", ""), kwargs.get("bl"),
kwargs.get("so"))
return exporter.mgr.find_create_object(pClass, **kwargs)
def _find_create_key(self, pClass, exporter, **kwargs):
"""Finds or creates a plKey specific to this node."""
assert "name" not in kwargs
kwargs["name"] = self.get_key_name(issubclass(pClass, (plObjInterface, plSingleModifier)),
kwargs.pop("suffix", ""), kwargs.get("bl"),
kwargs.get("so"))
return exporter.mgr.find_create_key(pClass, **kwargs)
def find_input(self, key, idname=None):
for i in self.inputs:
if i.alias == key:
@ -188,10 +223,6 @@ class PlasmaNodeBase:
def harvest_actors(self, bo):
return set()
@property
def key_name(self):
return "{}_{}".format(self.id_data.name, self.name)
def link_input(self, node, out_key, in_key):
"""Links a given Node's output socket to a given input socket on this Node"""
if isinstance(in_key, str):
@ -221,6 +252,9 @@ class PlasmaNodeBase:
"""Returns an absolute path to this Node. Needed because repr() uses an elipsis..."""
return "{}.{}".format(repr(self.id_data), self.path_from_id())
def previously_exported(self, exporter):
return self.name in exporter.exported_nodes[self.id_data.name]
@classmethod
def poll(cls, context):
return (context.bl_idname == "PlasmaNodeTree")
@ -420,9 +454,11 @@ class PlasmaNodeTree(bpy.types.NodeTree):
bl_icon = "NODETREE"
def export(self, exporter, bo, so):
# just pass it off to each node
exported_nodes = exporter.exported_nodes.setdefault(self.name, set())
for node in self.nodes:
if not (node.export_once and node.previously_exported(exporter)):
node.export(exporter, bo, so)
exported_nodes.add(node.name)
def find_output(self, idname):
for node in self.nodes:

6
korman/nodes/node_logic.py

@ -82,7 +82,7 @@ class PlasmaExcludeRegionNode(idprops.IDPropObjectMixin, PlasmaNodeBase, bpy.typ
def get_key(self, exporter, parent_so):
if self.region_object is None:
self.raise_error("Region must be set")
return exporter.mgr.find_create_key(plExcludeRegionModifier, bl=self.region_object, name=self.key_name)
return self._find_create_key(plExcludeRegionModifier, exporter, bl=self.region_object)
def harvest_actors(self, bo):
return [i.safepoint.name for i in self.find_input_sockets("safe_points") if i.safepoint is not None]
@ -108,6 +108,10 @@ class PlasmaExcludeRegionNode(idprops.IDPropObjectMixin, PlasmaNodeBase, bpy.typ
physical.memberGroup = plSimDefs.kGroupDetector
physical.collideGroup |= 1 << plSimDefs.kGroupDynamic
@property
def export_once(self):
return True
@classmethod
def _idprop_mapping(cls):
return {"region_object": "region"}

7
korman/nodes/node_messages.py

@ -544,6 +544,8 @@ class PlasmaOneShotMsgNode(idprops.IDPropObjectMixin, PlasmaMessageWithCallbacks
layout.prop(self, "seek")
def export(self, exporter, bo, so):
# Note: we purposefully allow this to proceed because plOneShotMod is a MultiMod, so we
# want all referencing SOs to get a copy of the modifier.
oneshotmod = self.get_key(exporter, so).object
oneshotmod.animName = self.animation
oneshotmod.drivable = self.drivable
@ -553,12 +555,11 @@ class PlasmaOneShotMsgNode(idprops.IDPropObjectMixin, PlasmaMessageWithCallbacks
oneshotmod.seekDuration = 1.0
def get_key(self, exporter, so):
name = self.key_name
if self.pos_object is not None:
pos_so = exporter.mgr.find_create_object(plSceneObject, bl=self.pos_object)
return exporter.mgr.find_create_key(plOneShotMod, name=name, so=pos_so)
return self._find_create_key(plOneShotMod, exporter, so=pos_so)
else:
return exporter.mgr.find_create_key(plOneShotMod, name=name, so=so)
return self._find_create_key(plOneShotMod, exporter, so=so)
def harvest_actors(self, bo):
actors = set()

7
korman/nodes/node_python.py

@ -252,10 +252,15 @@ class PlasmaPythonFileNode(PlasmaVersionedNode, bpy.types.Node):
layout.label(text="Script '{}' is not loaded in Blender".format(self.filename), icon="ERROR")
def get_key(self, exporter, so):
return exporter.mgr.find_create_key(plPythonFileMod, name=self.key_name, so=so)
return self._find_create_key(plPythonFileMod, exporter, so=so)
def export(self, exporter, bo, so):
pfm = self.get_key(exporter, so).object
# No need to continue if the PFM was already generated.
if self.previously_exported(exporter):
return
py_name = Path(self.filename).stem
pfm.filename = py_name

7
korman/nodes/node_responder.py

@ -83,7 +83,7 @@ class PlasmaResponderNode(PlasmaVersionedNode, bpy.types.Node):
layout.prop(self, "no_ff_sounds")
def get_key(self, exporter, so):
return exporter.mgr.find_create_key(plResponderModifier, name=self.key_name, so=so)
return self._find_create_key(plResponderModifier, exporter, so=so)
def export(self, exporter, bo, so):
responder = self.get_key(exporter, so).object
@ -135,6 +135,11 @@ class PlasmaResponderNode(PlasmaVersionedNode, bpy.types.Node):
stateMgr.register_state(stateNode)
stateMgr.convert_states(exporter, so)
@property
def export_once(self):
# What exactly is a reused responder? All the messages are directed, after all...
return True
@property
def latest_version(self):
return 2

14
korman/nodes/node_softvolume.py

@ -130,7 +130,7 @@ class PlasmaSoftVolumeInvertNode(PlasmaNodeBase, bpy.types.Node):
])
def get_key(self, exporter, so):
return exporter.mgr.find_create_key(plSoftVolumeInvert, name=self.key_name, so=so)
return self._find_create_key(plSoftVolumeInvert, exporter, so=so)
def export(self, exporter, bo, so):
parent = self.find_input("input")
@ -147,6 +147,10 @@ class PlasmaSoftVolumeInvertNode(PlasmaNodeBase, bpy.types.Node):
sv.insideStrength = 1.0
sv.outsideStrength = 0.0
@property
def export_once(self):
return True
class PlasmaSoftVolumeLinkNode(PlasmaNodeBase):
input_sockets = OrderedDict([
@ -180,6 +184,10 @@ class PlasmaSoftVolumeLinkNode(PlasmaNodeBase):
sv.insideStrength = 1.0
sv.outsideStrength = 0.0
@property
def export_once(self):
return True
class PlasmaSoftVolumeIntersectNode(PlasmaSoftVolumeLinkNode, bpy.types.Node):
bl_category = "SV"
@ -188,7 +196,7 @@ class PlasmaSoftVolumeIntersectNode(PlasmaSoftVolumeLinkNode, bpy.types.Node):
def get_key(self, exporter, so):
## FIXME: SoftVolumeIntersect should not be listed as an interface
return exporter.mgr.find_create_key(plSoftVolumeIntersect, name=self.key_name, so=so)
return self._find_create_key(plSoftVolumeIntersect, exporter, so=so)
class PlasmaSoftVolumeUnionNode(PlasmaSoftVolumeLinkNode, bpy.types.Node):
@ -198,4 +206,4 @@ class PlasmaSoftVolumeUnionNode(PlasmaSoftVolumeLinkNode, bpy.types.Node):
def get_key(self, exporter, so):
## FIXME: SoftVolumeUnion should not be listed as an interface
return exporter.mgr.find_create_key(plSoftVolumeUnion, name=self.key_name, so=so)
return self._find_create_key(plSoftVolumeUnion, exporter, so=so)

19
korman/properties/modifiers/logic.py

@ -23,7 +23,6 @@ from ...exporter import ExportError
from ... import idprops
class PlasmaVersionedNodeTree(idprops.IDPropMixin, bpy.types.PropertyGroup):
name = StringProperty(name="Name")
version = EnumProperty(name="Version",
description="Plasma versions this node tree exports under",
items=game_versions,
@ -32,8 +31,6 @@ class PlasmaVersionedNodeTree(idprops.IDPropMixin, bpy.types.PropertyGroup):
node_tree = PointerProperty(name="Node Tree",
description="Node Tree to export",
type=bpy.types.NodeTree)
node_name = StringProperty(name="Node Ref",
description="Attach a reference to this node")
@classmethod
def _idprop_mapping(cls):
@ -62,20 +59,8 @@ class PlasmaAdvancedLogic(PlasmaModifierProperties):
if i.node_tree is None:
raise ExportError("'{}': Advanced Logic is missing a node tree for '{}'".format(bo.name, i.name))
# If node_name is defined, then we're only adding a reference. We will make sure that
# the entire node tree is exported once before the post_export step, however.
if i.node_name:
exporter.want_node_trees[i.node_tree.name] = (bo, so)
node = i.node_tree.nodes.get(i.node_name, None)
if node is None:
raise ExportError("Node '{}' does not exist in '{}'".format(i.node_name, i.node_tree.name))
# We are going to assume get_key will do the adding correctly. Single modifiers
# should fetch the appropriate SceneObject before doing anything, so this will
# be a no-op in that case. Multi modifiers should accept any SceneObject, however
node.get_key(exporter, so)
else:
exporter.node_trees_exported.add(i.node_tree.name)
i.node_tree.export(exporter, bo, so)
# Defer node tree export until all trees are harvested.
exporter.want_node_trees.setdefault(i.node_tree.name, set()).add((bo, so))
def harvest_actors(self):
actors = set()

5
korman/properties/modifiers/region.py

@ -284,9 +284,8 @@ class PlasmaSoftVolume(idprops.IDPropMixin, PlasmaModifierProperties):
def _export_sv_nodes(self, exporter, bo, so):
tree = self.get_node_tree()
if tree.name not in exporter.node_trees_exported:
exporter.node_trees_exported.add(tree.name)
tree.export(exporter, bo, so)
# Stash for later
exporter.want_node_trees.setdefault(tree.name, set()).add((bo, so))
def get_node_tree(self):
if self.node_tree is None:

15
korman/ui/modifiers/logic.py

@ -19,25 +19,22 @@ from .. import ui_list
class LogicListUI(bpy.types.UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_property, index=0, flt_flag=0):
layout.prop(item, "name", emboss=False, text="", icon="NODETREE")
if item.node_tree:
# Using layout.prop on the pointer prevents clicking on the item O.o
layout.label(item.node_tree.name, icon="NODETREE")
else:
layout.label("[Empty]")
def advanced_logic(modifier, layout, context):
ui_list.draw_modifier_list(layout, "LogicListUI", modifier, "logic_groups",
"active_group_index", name_prefix="Logic",
name_prop="name", rows=2, maxrows=3)
"active_group_index", rows=2, maxrows=3)
# Modify the logic groups
if modifier.logic_groups:
logic = modifier.logic_groups[modifier.active_group_index]
layout.row().prop_menu_enum(logic, "version")
layout.prop(logic, "node_tree", icon="NODETREE")
try:
layout.prop_search(logic, "node_name", logic.node_tree, "nodes", icon="NODE")
except:
row = layout.row()
row.enabled = False
row.prop(logic, "node_name", icon="NODE")
def spawnpoint(modifier, layout, context):
layout.label(text="Avatar faces negative Y.")

Loading…
Cancel
Save