From 778caadf1132fa9b7d8c8481b4292006492dbec5 Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Sun, 23 Jun 2024 15:35:15 -0400 Subject: [PATCH 1/3] Centralize the definitions for standard Plasma APIs. This helps make Korman more maintainable by reducing potential duplication of Python attribute definitions. Further, it simplifies the code to create a Python File Node for a standard API file. --- korman/plasma_api.py | 118 ++++++++++++++++++++++++++ korman/properties/modifiers/avatar.py | 12 +-- korman/properties/modifiers/base.py | 5 ++ korman/properties/modifiers/gui.py | 87 ++----------------- korman/properties/modifiers/logic.py | 28 +----- korman/properties/modifiers/render.py | 26 +----- 6 files changed, 132 insertions(+), 144 deletions(-) create mode 100644 korman/plasma_api.py diff --git a/korman/plasma_api.py b/korman/plasma_api.py new file mode 100644 index 0000000..98a6182 --- /dev/null +++ b/korman/plasma_api.py @@ -0,0 +1,118 @@ +# This file is part of Korman. +# +# Korman is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Korman is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Korman. If not, see . + +python_files = { + # Provided by all variants of Uru and Myst V + "xDialogToggle.py": ( + { "id": 1, "type": "ptAttribActivator", "name": "Activate" }, + { "id": 4, "type": "ptAttribString", "name": "Vignette" }, + ), + + # Provided by CWE or OfflineKI + "xDynTextLoc.py": ( + { "id": 1, "type": "ptAttribDynamicMap", "name": "dynTextMap", }, + { "id": 2, "type": "ptAttribString", "name": "locPath" }, + { "id": 3, "type": "ptAttribString", "name": "fontFace" }, + { "id": 4, "type": "ptAttribInt", "name": "fontSize" }, + { "id": 5, "type": "ptAttribFloat", "name": "fontColorR" }, + { "id": 6, "type": "ptAttribFloat", "name": "fontColorG" }, + { "id": 7, "type": "ptAttribFloat", "name": "fontColorB" }, + { "id": 8, "type": "ptAttribFloat", "name": "fontColorA" }, + { "id": 9, "type": "ptAttribInt", "name": "marginTop" }, + { "id": 10, "type": "ptAttribInt", "name": "marginLeft" }, + { "id": 11, "type": "ptAttribInt", "name": "marginBottom" }, + { "id": 12, "type": "ptAttribInt", "name": "marginRight" }, + { "id": 13, "type": "ptAttribInt", "name": "lineSpacing" }, + # Yes, it"s really a ptAttribDropDownList, but those are only for use in + # artist generated node trees. + { "id": 14, "type": "ptAttribString", "name": "justify" }, + { "id": 15, "type": "ptAttribFloat", "name": "clearColorR" }, + { "id": 16, "type": "ptAttribFloat", "name": "clearColorG" }, + { "id": 17, "type": "ptAttribFloat", "name": "clearColorB" }, + { "id": 18, "type": "ptAttribFloat", "name": "clearColorA" }, + { "id": 19, "type": "ptAttribBoolean", "name": "blockRGB" }, + ), + + # Provided by CWE and OfflineKI + "xEntryCam.py": ( + { "id": 1, "type": "ptAttribActivator", "name": "actRegionSensor" }, + { "id": 2, "type": "ptAttribSceneobject", "name": "camera" }, + { "id": 3, "type": "ptAttribBoolean", "name": "undoFirstPerson" }, + ), + + # Provided by CWE + "xJournalBookGUIPopup.py": ( + { "id": 1, "type": "ptAttribActivator", "name": "actClickableBook" }, + { "id": 10, "type": "ptAttribBoolean", "name": "StartOpen" }, + { "id": 11, "type": "ptAttribFloat", "name": "BookWidth" }, + { "id": 12, "type": "ptAttribFloat", "name": "BookHeight" }, + { "id": 13, "type": "ptAttribString", "name": "LocPath" }, + { "id": 14, "type": "ptAttribString", "name": "GUIType" }, + ), + + # Provided by all variants of Uru and Myst V + "xLinkingBookGUIPopup.py": ( + { "id": 1, "type": "ptAttribActivator", "name": "actClickableBook" }, + { "id": 2, "type": "ptAttribBehavior", "name": "SeekBehavior" }, + { "id": 3, "type": "ptAttribResponder", "name": "respLinkResponder" }, + { "id": 4, "type": "ptAttribString", "name": "TargetAge" }, + { "id": 5, "type": "ptAttribActivator", "name": "actBookshelf" }, + { "id": 6, "type": "ptAttribActivator", "name": "shareRegion" }, + { "id": 7, "type": "ptAttribBehavior", "name": "shareBookSeek" }, + { "id": 10, "type": "ptAttribBoolean", "name": "IsDRCStamped" }, + { "id": 11, "type": "ptAttribBoolean", "name": "ForceThirdPerson" }, + ), + + # Supplied by the OfflineKI script: + # https://gitlab.com/diafero/offline-ki/blob/master/offlineki/xSimpleJournal.py + "xSimpleJournal.py": ( + { "id": 1, "type": "ptAttribActivator", "name": "bookClickable" }, + { "id": 2, "type": "ptAttribString", "name": "journalFileName" }, + { "id": 3, "type": "ptAttribBoolean", "name": "isNotebook" }, + { "id": 4, "type": "ptAttribFloat", "name": "BookWidth" }, + { "id": 5, "type": "ptAttribFloat", "name": "BookHeight" }, + ), + + # Supplied by the OfflineKI script: + # https://gitlab.com/diafero/offline-ki/blob/master/offlineki/xSimpleLinkingBook.py + "xSimpleLinkingBook.py": ( + { "id": 1, "type": "ptAttribActivator", "name": "bookClickable" }, + { "id": 2, "type": "ptAttribString", "name": "destinationAge" }, + { "id": 3, "type": "ptAttribString", "name": "spawnPoint" }, + { "id": 4, "type": "ptAttribString", "name": "linkPanel" }, + { "id": 5, "type": "ptAttribString", "name": "bookCover" }, + { "id": 6, "type": "ptAttribString", "name": "stampTexture" }, + { "id": 7, "type": "ptAttribFloat", "name": "stampX" }, + { "id": 8, "type": "ptAttribFloat", "name": "stampY" }, + { "id": 9, "type": "ptAttribFloat", "name": "bookWidth" }, + { "id": 10, "type": "ptAttribFloat", "name": "BookHeight" }, + { "id": 11, "type": "ptAttribBehavior", "name": "msbSeekBeforeUI" }, + { "id": 12, "type": "ptAttribResponder", "name": "respOneShot" }, + ), + + # Provided by CWE or OfflineKI + "xSitCam.py": ( + { "id": 1, "type": "ptAttribActivator", "name": "sitAct" }, + { "id": 2, "type": "ptAttribSceneobject", "name": "sitCam" }, + ), + + # Provided by all variants of Uru and Myst V + "xTelescope.py": ( + { "id": 1, "type": "ptAttribActivator", "name": "Activate" }, + { "id": 2, "type": "ptAttribSceneobject", "name": "Camera" }, + { "id": 3, "type": "ptAttribBehavior", "name": "Behavior" }, + { "id": 4, "type": "ptAttribString", "name": "Vignette" }, + ) +} diff --git a/korman/properties/modifiers/avatar.py b/korman/properties/modifiers/avatar.py index c6397f5..bbd0bc9 100644 --- a/korman/properties/modifiers/avatar.py +++ b/korman/properties/modifiers/avatar.py @@ -78,16 +78,6 @@ class PlasmaLadderModifier(PlasmaModifierProperties): return True -# Use xSitCam.py for when we want a camera to pop up -sitting_pfm = { - "filename": "xSitCam.py", - "attribs": ( - { 'id': 1, 'type': "ptAttribActivator", 'name': "sitAct" }, - { 'id': 2, 'type': "ptAttribSceneobject", 'name': "sitCam" }, - ) -} - - sitting_approach_flags = [("kApproachFront", "Front", "Approach from the font"), ("kApproachLeft", "Left", "Approach from the left"), ("kApproachRight", "Right", "Approach from the right"), @@ -140,7 +130,7 @@ class PlasmaSittingBehavior(idprops.IDPropObjectMixin, PlasmaModifierProperties, sittingmod.name = "SittingBeh" # xSitCam.py PythonFileMod if self.sitting_camera is not None: - sittingpynode = self._create_python_file_node(tree, sitting_pfm["filename"], sitting_pfm["attribs"]) + sittingpynode = self._create_python_standard_file_node(tree, "xSitCam.py") sittingmod.link_output(sittingpynode, "satisfies", "sitAct") # Camera Object diff --git a/korman/properties/modifiers/base.py b/korman/properties/modifiers/base.py index 2a984a2..1495c86 100644 --- a/korman/properties/modifiers/base.py +++ b/korman/properties/modifiers/base.py @@ -26,6 +26,7 @@ if TYPE_CHECKING: from ...nodes.node_python import * from ... import helpers +from ... import plasma_api class PlasmaModifierProperties(bpy.types.PropertyGroup): @property @@ -207,6 +208,10 @@ class PlasmaModifierLogicWiz: pfm_node.update() return pfm_node + def _create_standard_python_file_node(self, tree, filename: str) -> bpy.types.Node: + """Create a Python File Node for a standard Plasma Python API file (e.g. xAgeSDLBoolShowHide.py)""" + return self._create_python_file_node(tree, filename, plasma_api.python_files[filename]) + def _create_python_attribute(self, pfm_node: PlasmaPythonFileNode, attribute_name: str, **kwargs): """Creates and links a Python Attribute Node to the Python File Node given by `pfm_node`. For attribute nodes that require multiple values, the `value` may be set to None and diff --git a/korman/properties/modifiers/gui.py b/korman/properties/modifiers/gui.py index 5b06df2..0016e4d 100644 --- a/korman/properties/modifiers/gui.py +++ b/korman/properties/modifiers/gui.py @@ -35,33 +35,6 @@ if TYPE_CHECKING: from ...exporter import Exporter from .game_gui import PlasmaGameGuiDialogModifier - -journal_pfms = { - pvPots : { - # Supplied by the OfflineKI script: - # https://gitlab.com/diafero/offline-ki/blob/master/offlineki/xSimpleJournal.py - "filename": "xSimpleJournal.py", - "attribs": ( - { 'id': 1, 'type': "ptAttribActivator", "name": "bookClickable" }, - { 'id': 2, 'type': "ptAttribString", "name": "journalFileName" }, - { 'id': 3, 'type': "ptAttribBoolean", "name": "isNotebook" }, - { 'id': 4, 'type': "ptAttribFloat", "name": "BookWidth" }, - { 'id': 5, 'type': "ptAttribFloat", "name": "BookHeight" }, - ) - }, - pvMoul : { - "filename": "xJournalBookGUIPopup.py", - "attribs": ( - { 'id': 1, 'type': "ptAttribActivator", 'name': "actClickableBook" }, - { 'id': 10, 'type': "ptAttribBoolean", 'name': "StartOpen" }, - { 'id': 11, 'type': "ptAttribFloat", 'name': "BookWidth" }, - { 'id': 12, 'type': "ptAttribFloat", 'name': "BookHeight" }, - { 'id': 13, 'type': "ptAttribString", 'name': "LocPath" }, - { 'id': 14, 'type': "ptAttribString", 'name': "GUIType" }, - ) - }, -} - # Do not change the numeric IDs. They allow the list to be rearranged. _languages = [("Dutch", "Nederlands", "Dutch", 0), ("English", "English", "", 1), @@ -246,11 +219,11 @@ class PlasmaJournalBookModifier(PlasmaModifierProperties, PlasmaModifierLogicWiz def logicwiz(self, bo, tree, age_name, version): # Assign journal script based on target version - journal_pfm = journal_pfms[version] - journalnode = self._create_python_file_node(tree, journal_pfm["filename"], journal_pfm["attribs"]) if version <= pvPots: + journalnode = self._create_standard_python_file_node(tree, "xSimpleJournal.py") self._create_pots_nodes(bo, tree.nodes, journalnode, age_name) else: + journalnode = self._create_standard_python_file_node(tree, "xJournalBookGUIPopup.py") self._create_moul_nodes(bo, tree.nodes, journalnode, age_name) def _create_pots_nodes(self, clickable_object, nodes, journalnode, age_name): @@ -324,43 +297,6 @@ class PlasmaJournalBookModifier(PlasmaModifierProperties, PlasmaModifierLogicWiz return self.journal_translations -linking_pfms = { - pvPots : { - # Supplied by the OfflineKI script: - # https://gitlab.com/diafero/offline-ki/blob/master/offlineki/xSimpleLinkingBook.py - "filename": "xSimpleLinkingBook.py", - "attribs": ( - { 'id': 1, 'type': "ptAttribActivator", "name": "bookClickable" }, - { 'id': 2, 'type': "ptAttribString", "name": "destinationAge" }, - { 'id': 3, 'type': "ptAttribString", "name": "spawnPoint" }, - { 'id': 4, 'type': "ptAttribString", "name": "linkPanel" }, - { 'id': 5, 'type': "ptAttribString", "name": "bookCover" }, - { 'id': 6, 'type': "ptAttribString", "name": "stampTexture" }, - { 'id': 7, 'type': "ptAttribFloat", "name": "stampX" }, - { 'id': 8, 'type': "ptAttribFloat", "name": "stampY" }, - { 'id': 9, 'type': "ptAttribFloat", "name": "bookWidth" }, - { 'id': 10, 'type': "ptAttribFloat", "name": "BookHeight" }, - { 'id': 11, 'type': "ptAttribBehavior", "name": "msbSeekBeforeUI" }, - { 'id': 12, 'type': "ptAttribResponder", "name": "respOneShot" }, - ) - }, - pvMoul : { - "filename": "xLinkingBookGUIPopup.py", - "attribs": ( - { 'id': 1, 'type': "ptAttribActivator", 'name': "actClickableBook" }, - { 'id': 2, 'type': "ptAttribBehavior", 'name': "SeekBehavior" }, - { 'id': 3, 'type': "ptAttribResponder", 'name': "respLinkResponder" }, - { 'id': 4, 'type': "ptAttribString", 'name': "TargetAge" }, - { 'id': 5, 'type': "ptAttribActivator", 'name': "actBookshelf" }, - { 'id': 6, 'type': "ptAttribActivator", 'name': "shareRegion" }, - { 'id': 7, 'type': "ptAttribBehavior", 'name': "shareBookSeek" }, - { 'id': 10, 'type': "ptAttribBoolean", 'name': "IsDRCStamped" }, - { 'id': 11, 'type': "ptAttribBoolean", 'name': "ForceThirdPerson" }, - ) - }, -} - - class PlasmaLinkingBookModifier(PlasmaModifierProperties, PlasmaModifierLogicWiz): pl_id = "linkingbookmod" @@ -495,12 +431,11 @@ class PlasmaLinkingBookModifier(PlasmaModifierProperties, PlasmaModifierLogicWiz yield self.seek_point.name def logicwiz(self, bo, tree, age_name, version): - # Assign linking book script based on target version - linking_pfm = linking_pfms[version] - linkingnode = self._create_python_file_node(tree, linking_pfm["filename"], linking_pfm["attribs"]) if version <= pvPots: + linkingnode = self._create_standard_python_file_node(tree, "xSimpleLinkingBook.py") self._create_pots_nodes(bo, tree.nodes, linkingnode, age_name) else: + linkingnode = self._create_standard_python_file_node(tree, "xLinkingBookGUIPopup.py") self._create_moul_nodes(bo, tree.nodes, linkingnode, age_name) def _create_pots_nodes(self, clickable_object, nodes, linkingnode, age_name): @@ -667,14 +602,6 @@ class PlasmaLinkingBookModifier(PlasmaModifierProperties, PlasmaModifierLogicWiz raise ExportError("{}: Linking Book modifier requires a seek point!", self.id_data.name) -dialog_toggle = { - "filename": "xDialogToggle.py", - "attribs": ( - { 'id': 1, 'type': "ptAttribActivator", 'name': "Activate" }, - { 'id': 4, 'type': "ptAttribString", 'name': "Vignette" }, - ) -} - class PlasmaNotePopupModifier(PlasmaModifierProperties, PlasmaModifierLogicWiz): pl_id = "note_popup" @@ -755,11 +682,7 @@ class PlasmaNotePopupModifier(PlasmaModifierProperties, PlasmaModifierLogicWiz): nodes = tree.nodes # xDialogToggle.py PythonFile Node - dialog_node = self._create_python_file_node( - tree, - dialog_toggle["filename"], - dialog_toggle["attribs"] - ) + dialog_node = self._create_standard_python_file_node(tree, "xDialogToggle.py") self._create_python_attribute(dialog_node, "Vignette", value=self.gui_page) # Clickable diff --git a/korman/properties/modifiers/logic.py b/korman/properties/modifiers/logic.py index 4cb4fd9..80a266b 100644 --- a/korman/properties/modifiers/logic.py +++ b/korman/properties/modifiers/logic.py @@ -34,15 +34,6 @@ from ...exporter import ExportError, utils from ... import idprops from .physics import bounds_type_index, bounds_type_str, bounds_types -entry_cam_pfm = { - "filename": "xEntryCam.py", - "attribs": ( - { 'id': 1, 'type': "ptAttribActivator", 'name': "actRegionSensor" }, - { 'id': 2, 'type': "ptAttribSceneobject", 'name': "camera" }, - { 'id': 3, 'type': "ptAttribBoolean", 'name': "undoFirstPerson" }, - ) -} - class PlasmaVersionedNodeTree(idprops.IDPropMixin, bpy.types.PropertyGroup): version = EnumProperty(name="Version", description="Plasma versions this node tree exports under", @@ -148,11 +139,7 @@ class PlasmaSpawnPoint(PlasmaModifierProperties, PlasmaModifierLogicWiz): yield self.convert_logic(bo) def logicwiz(self, bo, tree): - pfm_node = self._create_python_file_node( - tree, - entry_cam_pfm["filename"], - entry_cam_pfm["attribs"] - ) + pfm_node = self._create_standard_python_file_node(tree, "xEntryCam.py") volume_sensor: PlasmaVolumeSensorNode = tree.nodes.new("PlasmaVolumeSensorNode") volume_sensor.find_input_socket("enter").allow = True @@ -204,17 +191,6 @@ class PlasmaMaintainersMarker(PlasmaModifierProperties): return True -telescope_pfm = { - "filename": "xTelescope.py", - "attribs": ( - { 'id': 1, 'type': "ptAttribActivator", 'name': "Activate" }, - { 'id': 2, 'type': "ptAttribSceneobject", 'name': "Camera" }, - { 'id': 3, 'type': "ptAttribBehavior", 'name': "Behavior" }, - { 'id': 4, 'type': "ptAttribString", 'name': "Vignette" }, - ) -} - - class PlasmaTelescope(PlasmaModifierProperties, PlasmaModifierLogicWiz): pl_id="telescope" @@ -255,7 +231,7 @@ class PlasmaTelescope(PlasmaModifierProperties, PlasmaModifierLogicWiz): nodes = tree.nodes # Create Python Node - telescopepynode = self._create_python_file_node(tree, telescope_pfm["filename"], telescope_pfm["attribs"]) + telescopepynode = self._create_standard_python_file_node(tree, "xTelescope.py") # Clickable telescopeclick = nodes.new("PlasmaClickableNode") diff --git a/korman/properties/modifiers/render.py b/korman/properties/modifiers/render.py index ec0645e..3488bfa 100644 --- a/korman/properties/modifiers/render.py +++ b/korman/properties/modifiers/render.py @@ -624,30 +624,6 @@ class PlasmaLightingMod(PlasmaModifierProperties): return False -_LOCALIZED_TEXT_PFM = ( - { 'id': 1, 'type': "ptAttribDynamicMap", 'name': "dynTextMap", }, - { 'id': 2, 'type': "ptAttribString", 'name': "locPath" }, - { 'id': 3, 'type': "ptAttribString", 'name': "fontFace" }, - { 'id': 4, 'type': "ptAttribInt", 'name': "fontSize" }, - { 'id': 5, 'type': "ptAttribFloat", 'name': "fontColorR" }, - { 'id': 6, 'type': "ptAttribFloat", 'name': "fontColorG" }, - { 'id': 7, 'type': "ptAttribFloat", 'name': "fontColorB" }, - { 'id': 8, 'type': "ptAttribFloat", 'name': "fontColorA" }, - { 'id': 9, 'type': "ptAttribInt", 'name': "marginTop" }, - { 'id': 10, 'type': "ptAttribInt", 'name': "marginLeft" }, - { 'id': 11, 'type': "ptAttribInt", 'name': "marginBottom" }, - { 'id': 12, 'type': "ptAttribInt", 'name': "marginRight" }, - { 'id': 13, 'type': "ptAttribInt", 'name': "lineSpacing" }, - # Yes, it's really a ptAttribDropDownList, but those are only for use in - # artist generated node trees. - { 'id': 14, 'type': "ptAttribString", 'name': "justify" }, - { 'id': 15, 'type': "ptAttribFloat", 'name': "clearColorR" }, - { 'id': 16, 'type': "ptAttribFloat", 'name': "clearColorG" }, - { 'id': 17, 'type': "ptAttribFloat", 'name': "clearColorB" }, - { 'id': 18, 'type': "ptAttribFloat", 'name': "clearColorA" }, - { 'id': 19, 'type': "ptAttribBoolean", 'name': "blockRGB" }, -) - class PlasmaLocalizedTextModifier(PlasmaModifierProperties, PlasmaModifierLogicWiz, TranslationMixin): pl_id = "dynatext" pl_page_types = {"gui", "room"} @@ -738,7 +714,7 @@ class PlasmaLocalizedTextModifier(PlasmaModifierProperties, PlasmaModifierLogicW self._create_nodes(bo, tree, age_name=age_name, version=version) def _create_nodes(self, bo, tree, *, age_name, version, material=None, clear_color=None): - pfm_node = self._create_python_file_node(tree, "xDynTextLoc.py", _LOCALIZED_TEXT_PFM) + pfm_node = self._create_standard_python_file_node(tree, "xDynTextLoc.py") loc_path = self.key_name if version <= pvPots else "{}.{}.{}".format(age_name, self.localization_set, self.key_name) self._create_python_attribute(pfm_node, "dynTextMap", "ptAttribDynamicMap", From 1cf33ba28615847c69b47e3d08fcdab75345dbb6 Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Sun, 23 Jun 2024 18:35:26 -0400 Subject: [PATCH 2/3] Allow `NodeTree`s to be reused in the `pre_export` phase. Logic trees can be shared, so it is conceivable that a logic tree might be pre-exported more than once. They can already be created by artists and referenced from multiple Advanced Logic modifiers, so this is good for consistency. --- korman/exporter/convert.py | 8 ++++++-- korman/properties/modifiers/base.py | 28 ++++++++++++++++------------ 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/korman/exporter/convert.py b/korman/exporter/convert.py index 644e100..df8d366 100644 --- a/korman/exporter/convert.py +++ b/korman/exporter/convert.py @@ -510,8 +510,12 @@ class Exporter: @handle_temporary.register(bpy.types.NodeTree) def _(temporary, parent): - self.exit_stack.enter_context(TemporaryObject(temporary, bpy.data.node_groups.remove)) - log_msg(f"'{parent.name}' generated NodeTree '{temporary.name}'") + # NodeTrees are reuseable, so make sure we haven't already encountered it. + if not temporary.name in self.want_node_trees: + self.exit_stack.enter_context(TemporaryObject(temporary, bpy.data.node_groups.remove)) + log_msg(f"'{parent.name}' generated NodeTree '{temporary.name}'") + else: + log_msg(f"'{parent.name}' reused NodeTree '{temporary.name}'") if temporary.bl_idname == "PlasmaNodeTree": parent_so = self.mgr.find_create_object(plSceneObject, bl=parent) self.want_node_trees[temporary.name].add((parent, parent_so)) diff --git a/korman/properties/modifiers/base.py b/korman/properties/modifiers/base.py index 1495c86..dc90484 100644 --- a/korman/properties/modifiers/base.py +++ b/korman/properties/modifiers/base.py @@ -181,20 +181,24 @@ class PlasmaModifierProperties(bpy.types.PropertyGroup): class PlasmaModifierLogicWiz: def convert_logic(self, bo, **kwargs): - """Creates, converts, and returns an unmanaged NodeTree for this logic wizard. If the wizard - fails during conversion, the temporary tree is deleted for you. However, on success, you - are responsible for removing the tree from Blender, if applicable.""" + """Attempts to look up an already existing logic tree matching the name provided and returns + it, if found. If not, creates, converts, and returns an unmanaged NodeTree for this wizard. + If the wizard fails during conversion, the temporary tree is deleted for you. However, on + success, you are responsible for removing the tree from Blender, if applicable.""" name = kwargs.pop("name", self.key_name) assert not "tree" in kwargs - tree = bpy.data.node_groups.new(name, "PlasmaNodeTree") - kwargs["tree"] = tree - try: - self.logicwiz(bo, **kwargs) - except: - bpy.data.node_groups.remove(tree) - raise - else: - return tree + + node_groups = bpy.data.node_groups + tree = node_groups.get(name) + if tree is None: + tree = node_groups.new(name, "PlasmaNodeTree") + kwargs["tree"] = tree + try: + self.logicwiz(bo, **kwargs) + except: + bpy.data.node_groups.remove(tree) + raise + return tree def _create_python_file_node(self, tree, filename: str, attributes: Dict[str, Any]) -> bpy.types.Node: pfm_node = tree.nodes.new("PlasmaPythonFileNode") From 0f0d6e1086e53e8ecfd54c7a0f1d4c8e6b190058 Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Sun, 23 Jun 2024 18:38:07 -0400 Subject: [PATCH 3/3] Add the SDL Show/Hide Modifier. This adds a modifier for the very common PythonFileMods xAgeSDLBoolShowHude and xAgeSDLIntShowHide. Right now, Korman doesn't try to parse SDL files, so the variable name and type have to be specified manually. The benefit to this, however, is the ability to easily wire up global SDL visibility type features. --- korman/exporter/convert.py | 4 ++ korman/operators/op_ui.py | 8 ++- korman/plasma_api.py | 16 ++++++ korman/properties/modifiers/logic.py | 86 +++++++++++++++++++++++++++- korman/ui/modifiers/logic.py | 37 ++++++++++++ 5 files changed, 148 insertions(+), 3 deletions(-) diff --git a/korman/exporter/convert.py b/korman/exporter/convert.py index df8d366..c12627b 100644 --- a/korman/exporter/convert.py +++ b/korman/exporter/convert.py @@ -598,6 +598,10 @@ class Exporter: else: return bpy.context.scene.world.plasma_age.age_name + @property + def age_sdl(self) -> bool: + return bpy.context.scene.world.plasma_age.age_sdl + @property def dat_only(self): return self._op.dat_only diff --git a/korman/operators/op_ui.py b/korman/operators/op_ui.py index ffa3b17..fa90436 100644 --- a/korman/operators/op_ui.py +++ b/korman/operators/op_ui.py @@ -77,11 +77,17 @@ class CollectionRemoveOperator(UIOperator, bpy.types.Operator): index_prop = StringProperty(name="Index Property", description="Name of the active element index property", options=set()) + manual_index = IntProperty(name="Manual Index", + description="Manual integer index to remove", + options=set()) def execute(self, context): props = getattr(context, self.context).path_resolve(self.group_path) collection = getattr(props, self.collection_prop) - index = getattr(props, self.index_prop) + if self.index_prop: + index = getattr(props, self.index_prop) + else: + index = self.manual_index if len(collection) > index: collection.remove(index) setattr(props, self.index_prop, index - 1) diff --git a/korman/plasma_api.py b/korman/plasma_api.py index 98a6182..0d63a8e 100644 --- a/korman/plasma_api.py +++ b/korman/plasma_api.py @@ -14,6 +14,22 @@ # along with Korman. If not, see . python_files = { + "xAgeSDLBoolShowHide.py": ( + { "id": 1, "type": "ptAttribString", "name": "sdlName" }, + { "id": 2, "type": "ptAttribBoolean", "name": "showOnTrue" }, + # --- CWE Only Below --- + { "id": 3, "type": "ptAttribBoolean", "name": "defaultValue" }, + { "id": 4, "type": "ptAttribBoolean", "name": "evalOnFirstUpdate "}, + ), + + "xAgeSDLIntShowHide.py": ( + { "id": 1, "type": "ptAttribString", "name": "stringVarName" }, + { "id": 2, "type": "ptAttribString", "name": "stringShowStates" }, + # --- CWE Only Below --- + { "id": 3, "type": "ptAttribInt", "name": "intDefault" }, + { "id": 4, "type": "ptAttribBoolean", "name": "boolFirstUpdate "}, + ), + # Provided by all variants of Uru and Myst V "xDialogToggle.py": ( { "id": 1, "type": "ptAttribActivator", "name": "Activate" }, diff --git a/korman/properties/modifiers/logic.py b/korman/properties/modifiers/logic.py index 80a266b..7ce8595 100644 --- a/korman/properties/modifiers/logic.py +++ b/korman/properties/modifiers/logic.py @@ -15,10 +15,8 @@ from __future__ import annotations -import bmesh import bpy from bpy.props import * -import mathutils from PyHSPlasma import * from typing import * @@ -28,6 +26,11 @@ if TYPE_CHECKING: from ...nodes.node_messages import * from ...nodes.node_responder import * +from typing import * + +if TYPE_CHECKING: + from ...exporter import Exporter + from ...addon_prefs import game_versions from .base import PlasmaModifierProperties, PlasmaModifierLogicWiz from ...exporter import ExportError, utils @@ -191,6 +194,85 @@ class PlasmaMaintainersMarker(PlasmaModifierProperties): return True +class PlasmaSDLIntState(bpy.types.PropertyGroup): + value: int = IntProperty( + name="State Value", + description="The object is shown when the SDL variable is set to this value", + min=0, + soft_max=255, + options=set() + ) + + +class PlasmaSDLShowHide(PlasmaModifierProperties, PlasmaModifierLogicWiz): + pl_id = "sdl_showhide" + + bl_category = "Logic" + bl_label = "SDL Show/Hide" + bl_description = "Show/Hide an object based on an SDL Variable" + bl_object_types = {"MESH", "FONT"} + bl_icon = "VISIBLE_IPO_OFF" + + sdl_variable: str = StringProperty( + name="SDL Variable", + description="Name of the SDL variable that controls visibility", + options=set() + ) + variable_type: str = EnumProperty( + name="Type", + description="Data type of the SDL variable", + items=[ + ("bool", "Boolean", "A boolean, used to represent simple on/off for a single state"), + ("int", "Integer", "An integer, used to represent multiple state combinations"), + ], + options=set() + ) + + int_states = CollectionProperty(type=PlasmaSDLIntState) + bool_state: bool = BoolProperty( + name="Show When True", + description="If checked, show this object when the SDL Variable is TRUE. If not, hide it when TRUE.", + default=True, + options=set() + ) + + def created(self): + # Ensure at least one SDL int state is precreated for ease of use. + # REMEMBER: Blender's "sequences" don't do truthiness correctly... + if len(self.int_states) == 0: + self.int_states.add() + + def sanity_check(self, exporter: Exporter): + if not exporter.age_sdl: + raise ExportError(f"'{self.id_data.name}': Age Global SDL is required for the SDL Show/Hide modifier!") + if not self.sdl_variable.strip(): + raise ExportError(f"'{self.id_data.name}': A valid SDL variable is required for the SDL Show/Hide modifier!") + + def logicwiz(self, bo, tree): + if self.variable_type == "bool": + pfm_node = self._create_standard_python_file_node(tree, "xAgeSDLBoolShowHide.py") + self._create_python_attribute(pfm_node, "sdlName", value=self.sdl_variable) + self._create_python_attribute(pfm_node, "showOnTrue", value=self.bool_state) + elif self.variable_type == "int": + pfm_node = self._create_standard_python_file_node(tree, "xAgeSDLIntShowHide.py") + self._create_python_attribute(pfm_node, "stringVarName", value=self.sdl_variable) + self._create_python_attribute(pfm_node, "stringShowStates", value=",".join(self._states)) + else: + raise RuntimeError() + + @property + def key_name(self): + if self.variable_type == "bool": + return f"cPythBoolShowHide_{self.sdl_variable}_{self.bool_state:d}" + elif self.variable_type == "int": + return f"cPythIntShowHide_{self.sdl_variable}_{'-'.join(self._states)}" + + @property + def _states(self) -> Iterable[str]: + """Returns a sorted, deduplicated iterable of the integer (converted to strings) states we should be visible in.""" + return (str(i) for i in sorted(frozenset((i.value for i in self.int_states)))) + + class PlasmaTelescope(PlasmaModifierProperties, PlasmaModifierLogicWiz): pl_id="telescope" diff --git a/korman/ui/modifiers/logic.py b/korman/ui/modifiers/logic.py index a99b169..f401ed5 100644 --- a/korman/ui/modifiers/logic.py +++ b/korman/ui/modifiers/logic.py @@ -60,6 +60,43 @@ def maintainersmarker(modifier, layout, context): layout.label(text="Positive Y is North, positive Z is up.") layout.prop(modifier, "calibration") +def sdl_showhide(modifier: PlasmaSDLShowHide, layout, context): + if not context.scene.world.plasma_age.age_sdl: + layout.label("This modifier requires Age Global SDL!", icon="ERROR") + return + + valid_variable = modifier.sdl_variable.strip() + layout.alert = not valid_variable + layout.prop(modifier, "sdl_variable") + if not valid_variable: + layout.label("A valid SDL variable is required!", icon="ERROR") + layout.alert = False + layout.prop(modifier, "variable_type") + layout.separator() + + def setup_collection_operator(op): + op.context = "object" + op.group_path = modifier.path_from_id() + op.collection_prop = "int_states" + op.index_prop = "" + + if modifier.variable_type == "bool": + layout.prop(modifier, "bool_state") + elif modifier.variable_type == "int": + layout.label("Show when SDL variable is:") + sub = layout.column_flow() + for i, state in enumerate(modifier.int_states): + row = sub.row(align=True) + row.prop(state, "value", text="Value") + op = row.operator("ui.plasma_collection_remove", icon="ZOOMOUT", text="") + setup_collection_operator(op) + op.manual_index = i + + op = layout.operator("ui.plasma_collection_add", icon="ZOOMIN", text="Add State Value") + setup_collection_operator(op) + else: + raise RuntimeError() + def telescope(modifier, layout, context): layout.prop(modifier, "clickable_region") layout.prop(modifier, "seek_target_object", icon="EMPTY_DATA")