diff --git a/korman/exporter/convert.py b/korman/exporter/convert.py index bd5353d..e1ed25a 100644 --- a/korman/exporter/convert.py +++ b/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): diff --git a/korman/nodes/node_avatar.py b/korman/nodes/node_avatar.py index 7d818e6..22deeb4 100644 --- a/korman/nodes/node_avatar.py +++ b/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"): diff --git a/korman/nodes/node_conditions.py b/korman/nodes/node_conditions.py index 04a5673..90f741f 100644 --- a/korman/nodes/node_conditions.py +++ b/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"} diff --git a/korman/nodes/node_core.py b/korman/nodes/node_core.py index e84a1fb..073ad65 100644 --- a/korman/nodes/node_core.py +++ b/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: - node.export(exporter, bo, so) + 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: diff --git a/korman/nodes/node_logic.py b/korman/nodes/node_logic.py index 812571d..27804a2 100644 --- a/korman/nodes/node_logic.py +++ b/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"} diff --git a/korman/nodes/node_messages.py b/korman/nodes/node_messages.py index 7ac4f09..3e6ad30 100644 --- a/korman/nodes/node_messages.py +++ b/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() diff --git a/korman/nodes/node_python.py b/korman/nodes/node_python.py index 0780c6b..cc58c5c 100644 --- a/korman/nodes/node_python.py +++ b/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 diff --git a/korman/nodes/node_responder.py b/korman/nodes/node_responder.py index a79c9b8..a400182 100644 --- a/korman/nodes/node_responder.py +++ b/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 diff --git a/korman/nodes/node_softvolume.py b/korman/nodes/node_softvolume.py index bf68cc9..06b8144 100644 --- a/korman/nodes/node_softvolume.py +++ b/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) diff --git a/korman/properties/modifiers/logic.py b/korman/properties/modifiers/logic.py index f291462..a37faf1 100644 --- a/korman/properties/modifiers/logic.py +++ b/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() diff --git a/korman/properties/modifiers/region.py b/korman/properties/modifiers/region.py index 61ae21e..e45a59e 100644 --- a/korman/properties/modifiers/region.py +++ b/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: diff --git a/korman/ui/modifiers/logic.py b/korman/ui/modifiers/logic.py index 2a789a1..d657b5f 100644 --- a/korman/ui/modifiers/logic.py +++ b/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.")