From 81774cd121f516a74bb2abac52b5e8d5ae1c2477 Mon Sep 17 00:00:00 2001 From: Joseph Davies Date: Fri, 8 Nov 2019 10:26:53 -0800 Subject: [PATCH] Add simple modifier for creating Linking Books. --- korman/properties/modifiers/base.py | 4 + korman/properties/modifiers/gui.py | 310 ++++++++++++++++++++++++++++ korman/ui/modifiers/gui.py | 45 ++++ 3 files changed, 359 insertions(+) diff --git a/korman/properties/modifiers/base.py b/korman/properties/modifiers/base.py index 0d2049d..5f4ea34 100644 --- a/korman/properties/modifiers/base.py +++ b/korman/properties/modifiers/base.py @@ -92,6 +92,10 @@ class PlasmaModifierLogicWiz: def logicwiz(self, bo, tree): pass + def export_logic(self, exporter, bo, so, **kwargs): + with self.generate_logic(bo, **kwargs) as tree: + tree.export(exporter, bo, so) + class PlasmaModifierUpgradable: @property diff --git a/korman/properties/modifiers/gui.py b/korman/properties/modifiers/gui.py index 773ec5a..47164bc 100644 --- a/korman/properties/modifiers/gui.py +++ b/korman/properties/modifiers/gui.py @@ -17,12 +17,15 @@ import bpy import math import bmesh import mathutils +from pathlib import Path from bpy.props import * from PyHSPlasma import * from ...addon_prefs import game_versions +from ...exporter import ExportError, utils from .base import PlasmaModifierProperties, PlasmaModifierLogicWiz, PlasmaModifierUpgradable from ... import idprops +from ...helpers import TemporaryObject journal_pfms = { @@ -330,3 +333,310 @@ class PlasmaJournalBookModifier(PlasmaModifierProperties, PlasmaModifierLogicWiz def requires_actor(self): # We are too late in the export to be harvested automatically, so let's be explicit return True + + +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" }, + ) + }, + 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" + + bl_category = "GUI" + bl_label = "Linking Book" + bl_description = "Linking Book" + bl_icon = "FILE_IMAGE" + + versions = EnumProperty(name="Export Targets", + description="Plasma versions for which this journal exports", + items=game_versions, + options={"ENUM_FLAG"}, + default={"pvMoul"}) + + # Link Info + link_type = EnumProperty(name="Linking Type", + description="The type of Link this Linking Book will use", + items=[ + ("kBasicLink", "Public Link", "Links to a public instance of the specified Age"), + ("kOriginalBook", "Private Link", "Links to a new or existing private instance of the specified Age"), + ("kSubAgeBook", "Closed Loop Link", "Links between instances of the specifed Age and the current one"),], + options=set(), + default="kOriginalBook") + age_name = StringProperty(name="Age Name", + description="Filename of the Age to link to (e.g. Garrison)",) + age_instance = StringProperty(name="Age Instance", + description="Friendly name of the Age to link to (e.g. Gahreesen)",) + age_uuid = StringProperty(name="Age GUID", + description="GUID for a specific instance (used with public Ages)",) + age_parent = StringProperty(name="Parent Age", + description="Name of the Child Age's parent Age",) + + spawn_title = StringProperty(name="Spawn Title", + description="Title of the Spawn Point", + default="Default") + spawn_point = StringProperty(name="Spawn Point", + description="Name of the Spawn Point to arrive at after the link", + default="LinkInPointDefault") + anim_type = EnumProperty(name="Link Animation", + description="Type of Linking Animation to use", + items=[("LinkOut", "Standing", "The avatar steps up to the book and places their hand on the panel"), + ("FishBookLinkOut", "Kneeling", "The avatar kneels in front of the book and places their hand on the panel"),], + default="LinkOut", + options=set()) + link_destination = StringProperty(name="Linking Panel Name", + description="Optional: Name of Linking Panel to use for this link-in point if it differs from the Age Name",) + + # Interactables + seek_point = PointerProperty(name="Seek Point", + description="The point the avatar will seek to before opening the Linking Book GUI", + type=bpy.types.Object, + poll=idprops.poll_empty_objects) + clickable_region = PointerProperty(name="Clickable Region", + description="The region in which the avatar must be standing before they can click on the Linking Book", + type=bpy.types.Object, + poll=idprops.poll_mesh_objects) + clickable = PointerProperty(name="Clickable", + description="The object the avatar will click on to activate the Linking Book GUI", + type=bpy.types.Object, + poll=idprops.poll_mesh_objects) + + # -- Path of the Shell options -- + # Popup Appearance + book_cover_image = PointerProperty(name="Book Cover", + description="Image to use for the Linking Book's cover (Optional: book starts open if left blank)", + type=bpy.types.Image, + options=set()) + link_panel_image = PointerProperty(name="Linking Panel", + description="Image to use for the Linking Panel", + type=bpy.types.Image, + options=set()) + stamp_image = PointerProperty(name="Stamp Image", + description="Image to use for the stamp on the page opposite the book's linking panel, if any", + type=bpy.types.Image, + options=set()) + stamp_x = IntProperty(name="Stamp Position X", + description="X position of Stamp", + default=140, + subtype="UNSIGNED") + stamp_y = IntProperty(name="Stamp Position Y", + description="Y position of Stamp", + default=255, + subtype="UNSIGNED") + + def export(self, exporter, bo, so): + our_versions = (globals()[j] for j in self.versions) + version = exporter.mgr.getVer() + if version not in our_versions: + # We aren't needed here + exporter.report.port("Object '{}' has a LinkingBookMod not enabled for export to the selected engine. Skipping.", + bo.name, indent=2) + return + + if self.clickable is None: + raise ExportError("{}: Linking Book modifier requires a clickable!", bo.name) + + if self.seek_point is None: + raise ExportError("{}: Linking Book modifier requires a seek point!", bo.name) + + if version <= pvPots: + # Create ImageLibraryMod in which to store the Cover, Linking Panel, and Stamp images + ilmod = exporter.mgr.find_create_object(plImageLibMod, so=so, name=self.key_name) + + user_images = (i for i in (self.book_cover_image, self.link_panel_image, self.stamp_image) + if i is not None) + for image in user_images: + exporter.mesh.material.export_prepared_image(owner=ilmod, image=image, + allowed_formats={"JPG", "PNG"}, extension="hsm") + + # Auto-generate a six-foot cube region around the clickable if none was provided. + if self.clickable_region is None: + # Create a region for the clickable's condition + def _make_rgn(bm): + bmesh.ops.create_cube(bm, size=(6.0)) + rgn_offset = mathutils.Matrix.Translation(self.clickable.location - bo.location) + bmesh.ops.transform(bm, matrix=rgn_offset, space=bo.matrix_world, verts=bm.verts) + + with utils.bmesh_temporary_object("{}_LinkingBook_ClkRgn".format(self.key_name), + _make_rgn, self.clickable.plasma_object.page) as temp_rgn: + # Generate the logic nodes + self.export_logic(exporter, bo, so, age_name=exporter.age_name, version=version, + region=temp_rgn) + else: + # Generate the logic nodes + self.export_logic(exporter, bo, so, age_name=exporter.age_name, version=version, + region=self.clickable_region) + + def logicwiz(self, bo, tree, age_name, version, region): + nodes = tree.nodes + + # Assign linking book script based on target version + linking_pfm = linking_pfms[version] + linkingnode = nodes.new("PlasmaPythonFileNode") + with linkingnode.NoUpdate(): + linkingnode.filename = linking_pfm["filename"] + + # Manually add required attributes to the PFM + linking_attribs = linking_pfm["attribs"] + for attr in linking_attribs: + new_attr = linkingnode.attributes.add() + new_attr.attribute_id = attr["id"] + new_attr.attribute_type = attr["type"] + new_attr.attribute_name = attr["name"] + linkingnode.update() + + if version <= pvPots: + self._create_pots_nodes(bo, nodes, linkingnode, age_name, region) + else: + self._create_moul_nodes(bo, nodes, linkingnode, age_name, region) + + def _create_pots_nodes(self, clickable_object, nodes, linkingnode, age_name, clk_region): + # Clickable + clickable_region = nodes.new("PlasmaClickableRegionNode") + clickable_region.region_object = clk_region + + clickable = nodes.new("PlasmaClickableNode") + clickable.clickable_object = self.clickable + clickable.find_input_socket("facing").allow_simple = False + clickable.link_input(clickable_region, "satisfies", "region") + clickable.link_output(linkingnode, "satisfies", "bookClickable") + + # Destination Age Name + age_name = nodes.new("PlasmaAttribStringNode") + age_name.value = self.age_name + age_name.link_output(linkingnode, "pfm", "destinationAge") + + # Spawn Point Name + spawn_point = nodes.new("PlasmaAttribStringNode") + spawn_point.value = self.spawn_point + spawn_point.link_output(linkingnode, "pfm", "spawnPoint") + + # Book Cover Image + if self.book_cover_image: + book_cover_name = nodes.new("PlasmaAttribStringNode") + book_cover_name.value = str(Path(self.book_cover_image.name).with_suffix(".hsm")) + book_cover_name.link_output(linkingnode, "pfm", "bookCover") + + # Linking Panel Image + if self.link_panel_image: + linking_panel_name = nodes.new("PlasmaAttribStringNode") + linking_panel_name.value = str(Path(self.link_panel_image.name).with_suffix(".hsm")) + linking_panel_name.link_output(linkingnode, "pfm", "linkPanel") + + # Stamp Image + if self.stamp_image: + stamp_texture_name = nodes.new("PlasmaAttribStringNode") + stamp_texture_name.value = str(Path(self.stamp_image.name).with_suffix(".hsm")) + stamp_texture_name.link_output(linkingnode, "pfm", "stampTexture") + + # Stamp X Position + stamp_x = nodes.new("PlasmaAttribIntNode") + stamp_x.value = self.stamp_x + stamp_x.link_output(linkingnode, "pfm", "stampX") + + # Stamp Y Position + stamp_y = nodes.new("PlasmaAttribIntNode") + stamp_y.value = self.stamp_y + stamp_y.link_output(linkingnode, "pfm", "stampY") + + # MSB + seek = nodes.new("PlasmaSeekTargetNode") + seek.target = self.seek_point + + anim_stage = nodes.new("PlasmaAnimStageNode") + anim_stage.anim_name = "LinkOut" + anim_settings = nodes.new("PlasmaAnimStageSettingsNode") + anim_stage.link_input(anim_settings, "stage", "stage_settings") + + msb = nodes.new("PlasmaMultiStageBehaviorNode") + msb.link_input(seek, "seekers", "seek_target") + msb.link_input(anim_stage, "stage", "stage_refs") + msb.link_output(linkingnode, "hosts", "msbSeekBeforeUI") + + def _create_moul_nodes(self, clickable_object, nodes, linkingnode, age_name, clk_region): + # Clickable + clickable_region = nodes.new("PlasmaClickableRegionNode") + clickable_region.region_object = clk_region + + clickable = nodes.new("PlasmaClickableNode") + clickable.clickable_object = self.clickable + clickable.find_input_socket("facing").allow_simple = False + clickable.link_input(clickable_region, "satisfies", "region") + clickable.link_output(linkingnode, "satisfies", "actClickableBook") + + # MSB + seek = nodes.new("PlasmaSeekTargetNode") + seek.target = self.seek_point + + anim_stage = nodes.new("PlasmaAnimStageNode") + anim_stage.anim_name = "LinkOut" + anim_settings = nodes.new("PlasmaAnimStageSettingsNode") + anim_stage.link_input(anim_settings, "stage", "stage_settings") + + msb = nodes.new("PlasmaMultiStageBehaviorNode") + msb.link_input(seek, "seekers", "seek_target") + msb.link_input(anim_stage, "stage", "stage_refs") + msb.link_output(linkingnode, "hosts", "SeekBehavior") + + # Responder + link_message = nodes.new("PlasmaLinkToAgeMsg") + link_message.rules = self.link_type + link_message.parent_filename = self.age_parent + link_message.age_filename = self.age_name + link_message.age_instance = self.age_instance + link_message.age_uuid = self.age_uuid + link_message.spawn_title = self.spawn_title + link_message.spawn_point = self.spawn_point + + one_shot = nodes.new("PlasmaOneShotMsgNode") + one_shot.animation = self.anim_type + one_shot.marker = "touch" + one_shot.pos_object = self.seek_point + one_shot.link_output(link_message, "msgs", "sender") + + responder_state = nodes.new("PlasmaResponderStateNode") + responder_state.link_output(one_shot, "msgs", "sender") + + responder = nodes.new("PlasmaResponderNode") + responder.link_output(responder_state, "state_refs", "resp") + responder.link_output(linkingnode, "keyref", "respLinkResponder") + + # Linking Panel Name + linking_panel_name = nodes.new("PlasmaAttribStringNode") + linking_panel_name.value = self.link_destination if self.link_destination else self.age_name + linking_panel_name.link_output(linkingnode, "pfm", "TargetAge") + + def harvest_actors(self): + if self.seek_point is not None: + yield self.seek_point.name diff --git a/korman/ui/modifiers/gui.py b/korman/ui/modifiers/gui.py index 68fe543..3577cd5 100644 --- a/korman/ui/modifiers/gui.py +++ b/korman/ui/modifiers/gui.py @@ -67,3 +67,48 @@ def journalbookmod(modifier, layout, context): main_col.label("Clickable Region:") main_col.prop(modifier, "clickable_region", text="") + +def linkingbookmod(modifier, layout, context): + def row_alert(prop_name, **kwargs): + row = layout.row() + row.alert = not getattr(modifier, prop_name) + row.prop(modifier, prop_name, **kwargs) + + layout.prop_menu_enum(modifier, "versions") + layout.separator() + + row = layout.row() + row.alert = modifier.clickable is None + row.prop(modifier, "clickable") + layout.prop(modifier, "clickable_region") + + if "pvMoul" in modifier.versions: + row_alert("seek_point") + layout.prop(modifier, "anim_type") + + layout.separator() + layout.prop(modifier, "link_type") + row_alert("age_instance") + if modifier.link_type == "kChildAgeBook": + row_alert("age_parent") + if modifier.link_type == "kBasicLink": + row_alert("age_uuid") + + row_alert("age_name") + + if "pvMoul" in modifier.versions: + layout.separator() + layout.prop(modifier, "link_destination") + layout.prop(modifier, "spawn_title") + layout.prop(modifier, "spawn_point") + + if "pvPots" in modifier.versions: + layout.separator() + layout.prop(modifier, "link_panel_image") + layout.prop(modifier, "book_cover_image") + layout.prop(modifier, "stamp_image") + if modifier.stamp_image: + row = layout.row(align=True) + row.label("Stamp Position:") + row.prop(modifier, "stamp_x", text="X") + row.prop(modifier, "stamp_y", text="Y")