From 4f7f8a05fe7e3deffa3afa8961d2cb4d99f4c617 Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Mon, 14 Jan 2019 17:05:15 -0500 Subject: [PATCH 1/3] Fix #129 --- korman/exporter/convert.py | 3 + korman/exporter/locman.py | 142 ++++++++++++++++++++++++++ korman/properties/modifiers/gui.py | 153 +++++++++++++++++++---------- korman/ui/modifiers/gui.py | 46 +++++---- 4 files changed, 273 insertions(+), 71 deletions(-) create mode 100644 korman/exporter/locman.py diff --git a/korman/exporter/convert.py b/korman/exporter/convert.py index a55e18d..bd5353d 100644 --- a/korman/exporter/convert.py +++ b/korman/exporter/convert.py @@ -24,6 +24,7 @@ from . import camera from . import explosions from . import etlight from . import image +from . import locman from . import logger from . import manager from . import mesh @@ -52,6 +53,7 @@ class Exporter: self.output = outfile.OutputFiles(self, self._op.filepath) self.camera = camera.CameraConverter(self) self.image = image.ImageCache(self) + self.locman = locman.LocalizationConverter(self) # Step 0.8: Init the progress mgr self.mesh.add_progress_presteps(self.report) @@ -368,6 +370,7 @@ class Exporter: # If something bad happens in the final flush, it would be a shame to # simply toss away the potentially freshly regenerated texture cache. try: + self.locman.save() self.mgr.save_age() self.output.save() finally: diff --git a/korman/exporter/locman.py b/korman/exporter/locman.py new file mode 100644 index 0000000..72dd9ed --- /dev/null +++ b/korman/exporter/locman.py @@ -0,0 +1,142 @@ +# 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 . + +from PyHSPlasma import * +import weakref +from xml.sax.saxutils import escape as xml_escape + +_SP_LANGUAGES = {"English", "French", "German", "Italian", "Spanish"} + +class LocalizationConverter: + def __init__(self, exporter): + self._exporter = weakref.ref(exporter) + self._journals = {} + self._strings = {} + + def add_journal(self, name, language, text_id, indent=0): + if text_id.is_modified: + self._report.warn("Journal '{}' translation for '{}' is modified on the disk but not reloaded in Blender.", + name, language, indent=indent) + journal = self._journals.setdefault(name, {}) + journal[language] = text_id.as_string() + + def add_string(self, set_name, element_name, language, value): + trans_set = self._strings.setdefault(set_name, {}) + trans_element = trans_set.setdefault(element_name, {}) + trans_element[language] = value + + def _generate_journal_texts(self): + age_name = self._exporter().age_name + output = self._exporter().output + + def write_journal_file(language, file_name, contents): + try: + with output.generate_dat_file(dirname="ageresources", filename=file_name) as stream: + stream.write(contents.encode("windows-1252")) + except UnicodeEncodeError: + self._report.error("Translation '{}': Contents contains characters that cannot be used in this version of Plasma", + language, indent=2) + return False + else: + return True + + for journal_name, translations in self._journals.items(): + self._report.msg("Copying Journal '{}'", journal_name, indent=1) + for language_name, value in translations.items(): + if language_name not in _SP_LANGUAGES: + self._report.warn("Translation '{}' will not be used because it is not supported in this version of Plasma.", + language_name, indent=2) + continue + suffix = "_{}".format(language_name.lower()) if language_name != "English" else "" + file_name = "{}--{}{}.txt".format(age_name, journal_name, suffix) + write_journal_file(language_name, file_name, value) + + # Ensure that default (read: "English") journal is available + if "English" not in translations: + language_name, value = next(((language_name, value) for language_name, value in translations.items() + if language_name in _SP_LANGUAGES), (None, None)) + if language_name is not None: + file_name = "{}--{}.txt".format(age_name, journal_name) + # If you manage to screw up this badly... Well, I am very sorry. + if write_journal_file(language_name, file_name, value): + self._report.warn("No 'English' translation available, so '{}' will be used as the default", + language_name, indent=2) + else: + self._report.port("No 'English' nor any other suitable default translation available", indent=2) + + def _generate_loc_file(self): + # Only generate this junk if needed + if not self._strings and not self._journals: + return + + def write_line(value, *args, **kwargs): + # tabs suck, then you die... + whitespace = " " * kwargs.pop("indent", 0) + if args or kwargs: + value = value.format(*args, **kwargs) + line = "".join((whitespace, value, "\n")) + stream.write(line.encode("utf-16_le")) + + age_name = self._exporter().age_name + enc = plEncryptedStream.kEncAes if self._version == pvEoa else None + file_name = "{}.loc".format(age_name) + with self._exporter().output.generate_dat_file(file_name, enc=enc) as stream: + # UTF-16 little endian byte order mark + stream.write(b"\xFF\xFE") + + write_line("") + write_line("") + write_line("", age_name, indent=1) + + # Arbitrary strings defined by something like a GUI or a node tree + for set_name, elements in self._strings.items(): + write_line("", set_name, indent=2) + for element_name, translations in elements.items(): + write_line("", element_name, indent=3) + for language_name, value in translations.items(): + write_line("{translation}", + language=language_name, translation=xml_escape(value), indent=4) + write_line("", indent=3) + write_line("", indent=2) + + # Journals + if self._journals: + write_line("", indent=2) + for journal_name, translations in self._journals.items(): + write_line("", journal_name, indent=3) + for language_name, value in translations.items(): + write_line("{translation}", + language=language_name, translation=xml_escape(value), indent=4) + write_line("", indent=3) + write_line("", indent=2) + + # Verbose XML junk... + # You call it verbose. I call it unambiguously complete. + write_line("", indent=1) + write_line("") + + def save(self): + if self._version > pvPots: + self._generate_loc_file() + else: + self._generate_journal_texts() + + @property + def _report(self): + return self._exporter().report + + @property + def _version(self): + return self._exporter().mgr.getVer() diff --git a/korman/properties/modifiers/gui.py b/korman/properties/modifiers/gui.py index cc3adf7..773ec5a 100644 --- a/korman/properties/modifiers/gui.py +++ b/korman/properties/modifiers/gui.py @@ -21,19 +21,11 @@ from bpy.props import * from PyHSPlasma import * from ...addon_prefs import game_versions -from .base import PlasmaModifierProperties, PlasmaModifierLogicWiz +from .base import PlasmaModifierProperties, PlasmaModifierLogicWiz, PlasmaModifierUpgradable from ... import idprops journal_pfms = { - pvPrime : { - "filename": "xJournalBookGUIPopup.py", - "attribs": ( - { 'id': 1, 'type': "ptAttribActivator", "name": "actClickableBook" }, - { 'id': 3, 'type': "ptAttribString", "name": "JournalName" }, - { 'id': 10, 'type': "ptAttribBoolean", 'name': "StartOpen" }, - ) - }, pvPots : { # Supplied by the OfflineKI script: # https://gitlab.com/diafero/offline-ki/blob/master/offlineki/xSimpleJournal.py @@ -59,6 +51,26 @@ journal_pfms = { }, } +# Do not change the numeric IDs. They allow the list to be rearranged. +_languages = [("Dutch", "Nederlands", "Dutch", 0), + ("English", "English", "", 1), + ("Finnish", "Suomi", "Finnish", 2), + ("French", "Français", "French", 3), + ("German", "Deutsch", "German", 4), + ("Hungarian", "Magyar", "Hungarian", 5), + ("Italian", "Italiano ", "Italian", 6), + # Blender 2.79b can't render 日本語 by default + ("Japanese", "Nihongo", "Japanese", 7), + ("Norwegian", "Norsk", "Norwegian", 8), + ("Polish", "Polski", "Polish", 9), + ("Romanian", "Română", "Romanian", 10), + ("Russian", "Pyccĸий", "Russian", 11), + ("Spanish", "Español", "Spanish", 12), + ("Swedish", "Svenska", "Swedish", 13)] +languages = sorted(_languages, key=lambda x: x[1]) +_DEFAULT_LANGUAGE_NAME = "English" +_DEFAULT_LANGUAGE_ID = 1 + class ImageLibraryItem(bpy.types.PropertyGroup): image = bpy.props.PointerProperty(name="Image Item", @@ -91,6 +103,22 @@ class PlasmaImageLibraryModifier(PlasmaModifierProperties): exporter.mesh.material.export_prepared_image(owner=ilmod, image=item.image, allowed_formats={"JPG", "PNG"}, extension="hsm") +class PlasmaJournalTranslation(bpy.types.PropertyGroup): + def _poll_nonpytext(self, value): + return not value.name.endswith(".py") + + language = EnumProperty(name="Language", + description="Language of this translation", + items=languages, + default=_DEFAULT_LANGUAGE_NAME, + options=set()) + text_id = PointerProperty(name="Journal Contents", + description="Text data block containing the journal's contents for this language", + type=bpy.types.Text, + poll=_poll_nonpytext, + options=set()) + + class PlasmaJournalBookModifier(PlasmaModifierProperties, PlasmaModifierLogicWiz): pl_id = "journalbookmod" @@ -122,28 +150,69 @@ class PlasmaJournalBookModifier(PlasmaModifierProperties, PlasmaModifierLogicWiz description="Height scale", default=100, min=0, max=100, subtype="PERCENTAGE") - book_source_locpath = StringProperty(name="Book Source LocPath", - description="LocPath for book's text (MO:UL)", - default="Global.Journals.Empty") - book_source_filename = StringProperty(name="Book Source Filename", - description="Filename for book's text (Uru:CC)", - default="") - book_source_name = StringProperty(name="Book Source Name", - description="Name of xJournalBookDefs.py entry for book's text (Uru:ABM)", - default="Dummy") clickable_region = PointerProperty(name="Region", description="Region inside which the avatar must stand to be able to open the journal (optional)", type=bpy.types.Object, poll=idprops.poll_mesh_objects) + def _get_translation(self): + # Ensure there is always a default (read: English) translation available. + default_idx, default = next(((idx, translation) for idx, translation in enumerate(self.journal_translations) + if translation.language == _DEFAULT_LANGUAGE_NAME), (None, None)) + if default is None: + default_idx = len(self.journal_translations) + default = self.journal_translations.add() + default.language = _DEFAULT_LANGUAGE_NAME + if self.active_translation_index < len(self.journal_translations): + language = self.journal_translations[self.active_translation_index].language + else: + self.active_translation_index = default_idx + language = default.language + + # Due to the fact that we are using IDs to keep the data from becoming insane on new + # additions, we must return the integer id... + return next((idx for key, _, _, idx in languages if key == language)) + + def _set_translation(self, value): + # We were given an int here, must change to a string + language_name = next((key for key, _, _, i in languages if i == value)) + idx = next((idx for idx, translation in enumerate(self.journal_translations) + if translation.language == language_name), None) + if idx is None: + self.active_translation_index = len(self.journal_translations) + translation = self.journal_translations.add() + translation.language = language_name + else: + self.active_translation_index = idx + + journal_translations = CollectionProperty(name="Journal Translations", + type=PlasmaJournalTranslation, + options=set()) + active_translation_index = IntProperty(options={"HIDDEN"}) + active_translation = EnumProperty(name="Language", + description="Language of this translation", + items=languages, + get=_get_translation, set=_set_translation, + options=set()) + def export(self, exporter, bo, so): - our_versions = [globals()[j] for j in self.versions] + 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 JournalMod not enabled for export to the selected engine. Skipping.".format(bo.name, version), indent=2) + exporter.report.port("Object '{}' has a JournalMod not enabled for export to the selected engine. Skipping.", + bo.name, version, indent=2) return + # Export the Journal translation contents + translations = [i for i in self.journal_translations if i.text_id is not None] + if not translations: + exporter.report.error("Journal '{}': No content translations available. The journal will not be exported.", + bo.name, indent=2) + return + for i in translations: + exporter.locman.add_journal(self.key_name, i.language, i.text_id, indent=2) + if self.clickable_region is None: # Create a region for the clickable's condition rgn_mesh = bpy.data.meshes.new("{}_Journal_ClkRgn".format(self.key_name)) @@ -163,14 +232,14 @@ class PlasmaJournalBookModifier(PlasmaModifierProperties, PlasmaModifierLogicWiz self.temp_rgn = self.clickable_region # Generate the logic nodes - with self.generate_logic(bo, version=version) as tree: + with self.generate_logic(bo, age_name=exporter.age_name, version=version) as tree: tree.export(exporter, bo, so) # Get rid of our temporary clickable region if self.clickable_region is None: bpy.context.scene.objects.unlink(self.temp_rgn) - def logicwiz(self, bo, tree, version): + def logicwiz(self, bo, tree, age_name, version): nodes = tree.nodes # Assign journal script based on target version @@ -188,36 +257,12 @@ class PlasmaJournalBookModifier(PlasmaModifierProperties, PlasmaModifierLogicWiz new_attr.attribute_name = attr["name"] journalnode.update() - if version == pvPrime: - self.create_prime_nodes(bo, nodes, journalnode) - elif version == pvPots: - self.create_pots_nodes(bo, nodes, journalnode) - elif version == pvMoul: - self.create_moul_nodes(bo, nodes, journalnode) - - def create_prime_nodes(self, clickable_object, nodes, journalnode): - clickable_region = nodes.new("PlasmaClickableRegionNode") - clickable_region.region_object = self.temp_rgn - - facing_object = nodes.new("PlasmaFacingTargetNode") - facing_object.directional = False - facing_object.tolerance = math.degrees(-1) - - clickable = nodes.new("PlasmaClickableNode") - clickable.link_input(clickable_region, "satisfies", "region") - clickable.link_input(facing_object, "satisfies", "facing") - clickable.link_output(journalnode, "satisfies", "actClickableBook") - clickable.clickable_object = clickable_object - - start_open = nodes.new("PlasmaAttribBoolNode") - start_open.link_output(journalnode, "pfm", "StartOpen") - start_open.value = self.start_state == "OPEN" - - journal_name = nodes.new("PlasmaAttribStringNode") - journal_name.link_output(journalnode, "pfm", "JournalName") - journal_name.value = self.book_source_name + if version <= pvPots: + self._create_pots_nodes(bo, nodes, journalnode, age_name) + else: + self._create_moul_nodes(bo, nodes, journalnode, age_name) - def create_pots_nodes(self, clickable_object, nodes, journalnode): + def _create_pots_nodes(self, clickable_object, nodes, journalnode, age_name): clickable_region = nodes.new("PlasmaClickableRegionNode") clickable_region.region_object = self.temp_rgn @@ -233,7 +278,7 @@ class PlasmaJournalBookModifier(PlasmaModifierProperties, PlasmaModifierLogicWiz srcfile = nodes.new("PlasmaAttribStringNode") srcfile.link_output(journalnode, "pfm", "journalFileName") - srcfile.value = self.book_source_filename + srcfile.value = self.key_name guitype = nodes.new("PlasmaAttribBoolNode") guitype.link_output(journalnode, "pfm", "isNotebook") @@ -247,7 +292,7 @@ class PlasmaJournalBookModifier(PlasmaModifierProperties, PlasmaModifierLogicWiz height.link_output(journalnode, "pfm", "BookHeight") height.value_float = self.book_scale_h / 100.0 - def create_moul_nodes(self, clickable_object, nodes, journalnode): + def _create_moul_nodes(self, clickable_object, nodes, journalnode, age_name): clickable_region = nodes.new("PlasmaClickableRegionNode") clickable_region.region_object = self.temp_rgn @@ -275,7 +320,7 @@ class PlasmaJournalBookModifier(PlasmaModifierProperties, PlasmaModifierLogicWiz locpath = nodes.new("PlasmaAttribStringNode") locpath.link_output(journalnode, "pfm", "LocPath") - locpath.value = self.book_source_locpath + locpath.value = "{}.Journals.{}".format(age_name, self.key_name) guitype = nodes.new("PlasmaAttribStringNode") guitype.link_output(journalnode, "pfm", "GUIType") diff --git a/korman/ui/modifiers/gui.py b/korman/ui/modifiers/gui.py index 143469d..68fe543 100644 --- a/korman/ui/modifiers/gui.py +++ b/korman/ui/modifiers/gui.py @@ -36,22 +36,34 @@ def imagelibmod(modifier, layout, context): def journalbookmod(modifier, layout, context): layout.prop_menu_enum(modifier, "versions") + layout.separator() - if not {"pvPrime", "pvMoul"}.isdisjoint(modifier.versions): - layout.prop(modifier, "start_state") + split = layout.split() + main_col = split.column() - if not {"pvPots", "pvMoul"}.isdisjoint(modifier.versions): - layout.prop(modifier, "book_type") - row = layout.row(align=True) - row.label("Book Scaling:") - row.prop(modifier, "book_scale_w", text="Width", slider=True) - row.prop(modifier, "book_scale_h", text="Height", slider=True) - - if "pvPrime" in modifier.versions: - layout.prop(modifier, "book_source_name", text="Name") - if "pvPots" in modifier.versions: - layout.prop(modifier, "book_source_filename", text="Filename") - if "pvMoul" in modifier.versions: - layout.prop(modifier, "book_source_locpath", text="LocPath") - - layout.prop(modifier, "clickable_region") + main_col.label("Display Settings:") + col = main_col.column() + col.active = "pvMoul" in modifier.versions + col.prop(modifier, "start_state", text="") + main_col.prop(modifier, "book_type", text="") + main_col.separator() + main_col.label("Book Scaling:") + col = main_col.column(align=True) + col.prop(modifier, "book_scale_w", text="Width", slider=True) + col.prop(modifier, "book_scale_h", text="Height", slider=True) + + main_col = split.column() + main_col.label("Content Translations:") + main_col.prop(modifier, "active_translation", text="") + # This should never fail... + try: + translation = modifier.journal_translations[modifier.active_translation_index] + except Exception as e: + main_col.label(text="Error (see console)", icon="ERROR") + print(e) + else: + main_col.prop(translation, "text_id", text="") + main_col.separator() + + main_col.label("Clickable Region:") + main_col.prop(modifier, "clickable_region", text="") From ad2b463d715922e0c25055fed179028cd46c2b22 Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Mon, 28 Jan 2019 19:45:32 -0500 Subject: [PATCH 2/3] Implement localization export operator This is like the Package Python operator in that the artist can now export the loc data independent of the rest of the age. The difference is that due to the extremely different loc formats from PotS+UAM to MOUL, we can't easily allow the user to specify where the data will be exported to. So, we only allow this operation in the context of a game. --- korman/exporter/__init__.py | 1 + korman/exporter/locman.py | 89 +++++++++++++++++++++++++++++------ korman/operators/op_export.py | 48 +++++++++++++++++++ korman/ui/ui_world.py | 11 +++++ 4 files changed, 134 insertions(+), 15 deletions(-) diff --git a/korman/exporter/__init__.py b/korman/exporter/__init__.py index e8652a8..60123e0 100644 --- a/korman/exporter/__init__.py +++ b/korman/exporter/__init__.py @@ -18,5 +18,6 @@ from PyHSPlasma import * from .convert import * from .explosions import * +from .locman import * from .python import * from . import utils diff --git a/korman/exporter/locman.py b/korman/exporter/locman.py index 72dd9ed..205b7d1 100644 --- a/korman/exporter/locman.py +++ b/korman/exporter/locman.py @@ -13,6 +13,12 @@ # You should have received a copy of the GNU General Public License # along with Korman. If not, see . +import bpy +from contextlib import contextmanager +from .explosions import NonfatalExportError +from .. import korlib +from . import logger +from pathlib import Path from PyHSPlasma import * import weakref from xml.sax.saxutils import escape as xml_escape @@ -20,8 +26,17 @@ from xml.sax.saxutils import escape as xml_escape _SP_LANGUAGES = {"English", "French", "German", "Italian", "Spanish"} class LocalizationConverter: - def __init__(self, exporter): - self._exporter = weakref.ref(exporter) + def __init__(self, exporter=None, **kwargs): + if exporter is not None: + self._exporter = weakref.ref(exporter) + self._age_name = exporter.age_name + self._report = exporter.report + self._version = exporter.mgr.getVer() + else: + self._exporter = None + self._age_name = kwargs.get("age_name") + self._path = kwargs.get("path") + self._version = kwargs.get("version") self._journals = {} self._strings = {} @@ -37,13 +52,28 @@ class LocalizationConverter: trans_element = trans_set.setdefault(element_name, {}) trans_element[language] = value + @contextmanager + def _generate_file(self, filename, **kwargs): + if self._exporter is not None: + with self._exporter().output.generate_dat_file(filename, **kwargs) as handle: + yield handle + else: + dirname = kwargs.get("dirname", "dat") + filepath = str(Path(self._path) / dirname / filename) + handle = open(filepath, "wb") + try: + yield handle + except: + raise + finally: + handle.close() + def _generate_journal_texts(self): - age_name = self._exporter().age_name - output = self._exporter().output + age_name = self._age_name def write_journal_file(language, file_name, contents): try: - with output.generate_dat_file(dirname="ageresources", filename=file_name) as stream: + with self._generate_file(dirname="ageresources", filename=file_name) as stream: stream.write(contents.encode("windows-1252")) except UnicodeEncodeError: self._report.error("Translation '{}': Contents contains characters that cannot be used in this version of Plasma", @@ -89,10 +119,10 @@ class LocalizationConverter: line = "".join((whitespace, value, "\n")) stream.write(line.encode("utf-16_le")) - age_name = self._exporter().age_name + age_name = self._age_name enc = plEncryptedStream.kEncAes if self._version == pvEoa else None file_name = "{}.loc".format(age_name) - with self._exporter().output.generate_dat_file(file_name, enc=enc) as stream: + with self._generate_file(file_name, enc=enc) as stream: # UTF-16 little endian byte order mark stream.write(b"\xFF\xFE") @@ -127,16 +157,45 @@ class LocalizationConverter: write_line("", indent=1) write_line("") + def run(self): + age_props = bpy.context.scene.world.plasma_age + loc_path = str(Path(self._path) / "dat" / "{}.loc".format(self._age_name)) + log = logger.ExportVerboseLogger if age_props.verbose else logger.ExportProgressLogger + with korlib.ConsoleToggler(age_props.show_console), log(loc_path) as self._report: + self._report.progress_add_step("Harvesting Journals") + self._report.progress_add_step("Generating Localization") + self._report.progress_start("Exporting Localization Data") + + self._run_harvest_journals() + self._run_generate() + + # DONE + self._report.progress_end() + self._report.raise_errors() + + def _run_harvest_journals(self): + objects = bpy.context.scene.objects + self._report.progress_advance() + self._report.progress_range = len(objects) + inc_progress = self._report.progress_increment + + for i in objects: + journal = i.plasma_modifiers.journalbookmod + if journal.enabled: + translations = [j for j in journal.journal_translations if j.text_id is not None] + if not translations: + self._report.error("Journal '{}': No content translations available. The journal will not be exported.", + i.name, indent=2) + for j in translations: + self.add_journal(journal.key_name, j.language, j.text_id, indent=1) + inc_progress() + + def _run_generate(self): + self._report.progress_advance() + self.save() + def save(self): if self._version > pvPots: self._generate_loc_file() else: self._generate_journal_texts() - - @property - def _report(self): - return self._exporter().report - - @property - def _version(self): - return self._exporter().mgr.getVer() diff --git a/korman/operators/op_export.py b/korman/operators/op_export.py index e4df68e..f5fa58e 100644 --- a/korman/operators/op_export.py +++ b/korman/operators/op_export.py @@ -218,6 +218,54 @@ class PlasmaAgeExportOperator(ExportOperator, bpy.types.Operator): setattr(PlasmaAge, name, prop(**age_options)) +class PlasmaLocalizationExportOperator(ExportOperator, bpy.types.Operator): + bl_idname = "export.plasma_loc" + bl_label = "Export Localization" + bl_description = "Export Age Localization Data" + + filepath = StringProperty(subtype="DIR_PATH") + filter_glob = StringProperty(default="*.pak", options={'HIDDEN'}) + + version = EnumProperty(name="Version", + description="Plasma version to export this age for", + items=game_versions, + default="pvPots", + options=set()) + + def execute(self, context): + path = Path(self.filepath) + if not self.filepath: + self.report({"ERROR"}, "No file specified") + return {"CANCELLED"} + else: + if not path.exists: + try: + path.mkdir(parents=True) + except OSError: + self.report({"ERROR"}, "Failed to create export directory") + return {"CANCELLED"} + + # Age names cannot be python keywords + age_name = context.scene.world.plasma_age.age_name + if korlib.is_python_keyword(age_name): + self.report({"ERROR"}, "The Age name conflicts with the Python keyword '{}'".format(age_name)) + return {"CANCELLED"} + + # Bonus Fun: Implement Profile-mode here (later...) + e = exporter.LocalizationConverter(age_name=age_name, path=self.filepath, + version=globals()[self.version]) + try: + e.run() + except exporter.ExportError as error: + self.report({"ERROR"}, str(error)) + return {"CANCELLED"} + except exporter.NonfatalExportError as error: + self.report({"WARNING"}, str(error)) + return {"FINISHED"} + else: + return {"FINISHED"} + + class PlasmaPythonExportOperator(ExportOperator, bpy.types.Operator): bl_idname = "export.plasma_pak" bl_label = "Package Scripts" diff --git a/korman/ui/ui_world.py b/korman/ui/ui_world.py index 6618bfb..d97a073 100644 --- a/korman/ui/ui_world.py +++ b/korman/ui/ui_world.py @@ -55,6 +55,7 @@ class PlasmaGamePanel(AgeButtonsPanel, bpy.types.Panel): row = layout.row(align=True) legal_game = bool(age.age_name.strip()) and active_game is not None + # Export Age row.operator_context = "EXEC_DEFAULT" row.enabled = legal_game op = row.operator("export.plasma_age", icon="EXPORT") @@ -62,6 +63,7 @@ class PlasmaGamePanel(AgeButtonsPanel, bpy.types.Panel): op.dat_only = False op.filepath = str((Path(active_game.path) / "dat" / age.age_name).with_suffix(".age")) op.version = active_game.version + # Package Age row = row.row(align=True) row.enabled = legal_game row.operator_context = "INVOKE_DEFAULT" @@ -70,6 +72,15 @@ class PlasmaGamePanel(AgeButtonsPanel, bpy.types.Panel): op.dat_only = False op.filepath = "{}.zip".format(age.age_name) op.version = active_game.version + # Export Localization + row = row.row(align=True) + row.operator_context = "EXEC_DEFAULT" + row.enabled = legal_game + op = row.operator("export.plasma_loc", icon="FILE_SCRIPT") + if active_game is not None: + op.filepath = active_game.path + op.version = active_game.version + # Package Scripts row = row.row(align=True) row.operator_context = "EXEC_DEFAULT" row.enabled = legal_game and active_game.version != "pvMoul" From 5d1bf5d1b157432c3ec28e7d5b7cd3731cf23f5e Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Mon, 28 Jan 2019 20:07:28 -0500 Subject: [PATCH 3/3] Cleanup export button layout --- korman/ui/ui_world.py | 89 ++++++++++++++++++++++++++++++------------- 1 file changed, 62 insertions(+), 27 deletions(-) diff --git a/korman/ui/ui_world.py b/korman/ui/ui_world.py index d97a073..220466c 100644 --- a/korman/ui/ui_world.py +++ b/korman/ui/ui_world.py @@ -29,7 +29,59 @@ class AgeButtonsPanel: return context.world and context.scene.render.engine == "PLASMA_GAME" -class PlasmaGamePanel(AgeButtonsPanel, bpy.types.Panel): +class PlasmaGameHelper: + @property + def active_game(self): + games = bpy.context.world.plasma_games + prefs = bpy.context.user_preferences.addons["korman"].preferences + active_game_index = games.active_game_index + if active_game_index < len(prefs.games): + return prefs.games[active_game_index] + else: + return None + + def format_path(self, dirname="dat", ext=".age"): + active_game = self.active_game + if active_game is None: + return "" + age_name = bpy.context.world.plasma_age.age_name + return str((Path(active_game.path) / dirname / age_name).with_suffix(ext)) + + @property + def legal_game(self): + if self.active_game is not None: + return bool(bpy.context.world.plasma_age.age_name.strip()) + + +class PlasmaGameExportMenu(PlasmaGameHelper, bpy.types.Menu): + bl_label = "Plasma Export Menu" + + def draw(self, context): + layout = self.layout + age_name = context.world.plasma_age.age_name + active_game = self.active_game + legal_game = self.legal_game + + # Localization + row = layout.row() + row.operator_context = "EXEC_DEFAULT" + row.enabled = legal_game + op = row.operator("export.plasma_loc", icon="FILE_SCRIPT") + if active_game is not None: + op.filepath = active_game.path + op.version = active_game.version + + # Python + row = layout.row() + row.operator_context = "EXEC_DEFAULT" + row.enabled = legal_game and active_game.version != "pvMoul" + op = row.operator("export.plasma_pak", icon="FILE_SCRIPT") + op.filepath = self.format_path("Python", ".pak") + if active_game is not None: + op.version = active_game.version + + +class PlasmaGamePanel(AgeButtonsPanel, PlasmaGameHelper, bpy.types.Panel): bl_label = "Plasma Games" def draw(self, context): @@ -37,6 +89,8 @@ class PlasmaGamePanel(AgeButtonsPanel, bpy.types.Panel): prefs = context.user_preferences.addons["korman"].preferences games = context.world.plasma_games age = context.world.plasma_age + active_game = self.active_game + legal_game = self.legal_game row = layout.row() # Remember: game storage moved to addon preferences! @@ -44,16 +98,8 @@ class PlasmaGamePanel(AgeButtonsPanel, bpy.types.Panel): "active_game_index", rows=2) row.operator("ui.korman_open_prefs", icon="PREFERENCES", text="") - # Game Tools - active_game_index = games.active_game_index - if active_game_index < len(prefs.games): - active_game = prefs.games[active_game_index] - else: - active_game = None - layout.separator() row = layout.row(align=True) - legal_game = bool(age.age_name.strip()) and active_game is not None # Export Age row.operator_context = "EXEC_DEFAULT" @@ -61,33 +107,22 @@ class PlasmaGamePanel(AgeButtonsPanel, bpy.types.Panel): op = row.operator("export.plasma_age", icon="EXPORT") if active_game is not None: op.dat_only = False - op.filepath = str((Path(active_game.path) / "dat" / age.age_name).with_suffix(".age")) + op.filepath = self.format_path() op.version = active_game.version + # Package Age row = row.row(align=True) row.enabled = legal_game row.operator_context = "INVOKE_DEFAULT" op = row.operator("export.plasma_age", icon="PACKAGE", text="Package Age") + op.dat_only = False + op.filepath = "{}.zip".format(age.age_name) if active_game is not None: - op.dat_only = False - op.filepath = "{}.zip".format(age.age_name) op.version = active_game.version - # Export Localization - row = row.row(align=True) - row.operator_context = "EXEC_DEFAULT" - row.enabled = legal_game - op = row.operator("export.plasma_loc", icon="FILE_SCRIPT") - if active_game is not None: - op.filepath = active_game.path - op.version = active_game.version - # Package Scripts + + # Special Menu row = row.row(align=True) - row.operator_context = "EXEC_DEFAULT" - row.enabled = legal_game and active_game.version != "pvMoul" - op = row.operator("export.plasma_pak", icon="FILE_SCRIPT") - if active_game is not None: - op.filepath = str((Path(active_game.path) / "Python" / age.age_name).with_suffix(".pak")) - op.version = active_game.version + row.menu("PlasmaGameExportMenu", icon='DOWNARROW_HLT', text="") class PlasmaGameListRO(bpy.types.UIList):